Skip to content

Commit fd86423

Browse files
authored
Merge pull request #149 from microsoft/bugfix/odata-cast
type cast segments for derived types
2 parents b339855 + 306e946 commit fd86423

33 files changed

+34651
-5992
lines changed

src/Microsoft.OpenApi.OData.Reader/Edm/ODataPath.cs

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -289,13 +289,15 @@ private ODataPathKind CalcPathType()
289289
{
290290
return ODataPathKind.Metadata;
291291
}
292-
293-
if (Segments.Last().Kind == ODataSegmentKind.DollarCount)
292+
else if (Segments.Last().Kind == ODataSegmentKind.DollarCount)
294293
{
295294
return ODataPathKind.DollarCount;
296295
}
297-
298-
if (Segments.Any(c => c.Kind == ODataSegmentKind.StreamProperty || c.Kind == ODataSegmentKind.StreamContent))
296+
else if (Segments.Last().Kind == ODataSegmentKind.TypeCast)
297+
{
298+
return ODataPathKind.TypeCast;
299+
}
300+
else if (Segments.Any(c => c.Kind == ODataSegmentKind.StreamProperty || c.Kind == ODataSegmentKind.StreamContent))
299301
{
300302
return ODataPathKind.MediaEntity;
301303
}
@@ -315,20 +317,15 @@ private ODataPathKind CalcPathType()
315317
{
316318
return ODataPathKind.NavigationProperty;
317319
}
318-
319-
if (Segments.Count == 1)
320+
else if (Segments.Count == 1 && Segments[0] is ODataNavigationSourceSegment segment)
320321
{
321-
ODataNavigationSourceSegment segment = Segments[0] as ODataNavigationSourceSegment;
322-
if (segment != null)
322+
if (segment.NavigationSource is IEdmSingleton)
323323
{
324-
if (segment.NavigationSource is IEdmSingleton)
325-
{
326-
return ODataPathKind.Singleton;
327-
}
328-
else
329-
{
330-
return ODataPathKind.EntitySet;
331-
}
324+
return ODataPathKind.Singleton;
325+
}
326+
else
327+
{
328+
return ODataPathKind.EntitySet;
332329
}
333330
}
334331
else if (Segments.Count == 2 && Segments.Last().Kind == ODataSegmentKind.Key)
@@ -338,5 +335,13 @@ private ODataPathKind CalcPathType()
338335

339336
return ODataPathKind.Unknown;
340337
}
338+
339+
/// <summary>
340+
/// Provides a suffix for the operation id based on the operation path.
341+
/// </summary>
342+
/// <param name="settings">The settings.</param>
343+
///<returns>The suffix.</returns>
344+
public string GetPathHash(OpenApiConvertSettings settings) =>
345+
LastSegment.GetPathHash(settings, this);
341346
}
342347
}

src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathKind.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,14 @@ public enum ODataPathKind
6060
/// </summary>
6161
DollarCount,
6262

63+
/// <summary>
64+
/// Represents a type cast path, for example: ~/groups/{id}/members/microsoft.graph.user
65+
/// </summary>
66+
TypeCast,
67+
6368
/// <summary>
6469
/// Represents an un-supported/unknown path.
6570
/// </summary>
66-
Unknown
67-
}
71+
Unknown,
72+
}
6873
}

src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathProvider.cs

Lines changed: 109 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Linq;
1010
using Microsoft.OData.Edm;
1111
using Microsoft.OData.Edm.Vocabularies;
12+
using Microsoft.OpenApi.OData.Common;
1213
using Microsoft.OpenApi.OData.Vocabulary.Capabilities;
1314

1415
namespace Microsoft.OpenApi.OData.Edm
@@ -127,6 +128,7 @@ private void AppendPath(ODataPath path)
127128
ODataPathKind kind = path.Kind;
128129
switch(kind)
129130
{
131+
case ODataPathKind.TypeCast:
130132
case ODataPathKind.DollarCount:
131133
case ODataPathKind.Entity:
132134
case ODataPathKind.EntitySet:
@@ -186,11 +188,19 @@ private void RetrieveNavigationSourcePaths(IEdmNavigationSource navigationSource
186188
if (entitySet != null)
187189
{
188190
count = _model.GetRecord<CountRestrictionsType>(entitySet, CapabilitiesConstants.CountRestrictions);
189-
if(count?.Countable ?? true)
191+
if(count?.Countable ?? true) // ~/entitySet/$count
190192
CreateCountPath(path, convertSettings);
191193

194+
CreateTypeCastPaths(path, convertSettings, entityType, entitySet, true); // ~/entitySet/subType
195+
192196
path.Push(new ODataKeySegment(entityType));
193197
AppendPath(path.Clone());
198+
199+
CreateTypeCastPaths(path, convertSettings, entityType, entitySet, false); // ~/entitySet/{id}/subType
200+
}
201+
else if (navigationSource is IEdmSingleton singleton)
202+
{ // ~/singleton/subType
203+
CreateTypeCastPaths(path, convertSettings, entityType, singleton, false);
194204
}
195205

196206
// media entity
@@ -285,13 +295,20 @@ private void RetrieveNavigationPropertyPaths(IEdmNavigationProperty navigationPr
285295
IEdmEntityType navEntityType = navigationProperty.ToEntityType();
286296
var targetsMany = navigationProperty.TargetMultiplicity() == EdmMultiplicity.Many;
287297
var propertyPath = navigationProperty.GetPartnerPath()?.Path;
298+
var propertyPathIsEmpty = string.IsNullOrEmpty(propertyPath);
288299

289-
if (targetsMany && (string.IsNullOrEmpty(propertyPath) ||
290-
(count?.IsNonCountableNavigationProperty(propertyPath) ?? true)))
300+
if (targetsMany)
291301
{
292-
// ~/entityset/{key}/collection-valued-Nav/$count
293-
CreateCountPath(currentPath, convertSettings);
302+
if(propertyPathIsEmpty ||
303+
(count?.IsNonCountableNavigationProperty(propertyPath) ?? true))
304+
{
305+
// ~/entityset/{key}/collection-valued-Nav/$count
306+
CreateCountPath(currentPath, convertSettings);
307+
}
294308
}
309+
// ~/entityset/{key}/collection-valued-Nav/subtype
310+
// ~/entityset/{key}/single-valued-Nav/subtype
311+
CreateTypeCastPaths(currentPath, convertSettings, navigationProperty.DeclaringType, navigationProperty, targetsMany);
295312

296313
if (!navigationProperty.ContainsTarget)
297314
{
@@ -305,6 +322,8 @@ private void RetrieveNavigationPropertyPaths(IEdmNavigationProperty navigationPr
305322
// Collection-valued: DELETE ~/entityset/{key}/collection-valued-Nav/{key}/$ref
306323
currentPath.Push(new ODataKeySegment(navEntityType));
307324
CreateRefPath(currentPath);
325+
326+
CreateTypeCastPaths(currentPath, convertSettings, navigationProperty.DeclaringType, navigationProperty, false); // ~/entityset/{key}/collection-valued-Nav/{id}/subtype
308327
}
309328

310329
// Get possible stream paths for the navigation entity type
@@ -317,6 +336,8 @@ private void RetrieveNavigationPropertyPaths(IEdmNavigationProperty navigationPr
317336
{
318337
currentPath.Push(new ODataKeySegment(navEntityType));
319338
AppendPath(currentPath.Clone());
339+
340+
CreateTypeCastPaths(currentPath, convertSettings, navigationProperty.DeclaringType, navigationProperty, false); // ~/entityset/{key}/collection-valued-Nav/{id}/subtype
320341
}
321342

322343
// Get possible stream paths for the navigation entity type
@@ -393,6 +414,60 @@ private void CreateCountPath(ODataPath currentPath, OpenApiConvertSettings conve
393414
AppendPath(countPath);
394415
}
395416

417+
/// <summary>
418+
/// Create OData type cast paths.
419+
/// </summary>
420+
/// <param name="currentPath">The current OData path.</param>
421+
/// <param name="convertSettings">The settings for the current conversion.</param>
422+
/// <param name="structuredType">The type that is being inherited from to which this method will add downcast path segments.</param>
423+
/// <param name="annotable">The annotable navigation source to read cast annotations from.</param>
424+
/// <param name="targetsMany">Whether the annotable navigation source targets many entities.</param>
425+
private void CreateTypeCastPaths(ODataPath currentPath, OpenApiConvertSettings convertSettings, IEdmStructuredType structuredType, IEdmVocabularyAnnotatable annotable, bool targetsMany)
426+
{
427+
if(currentPath == null) throw Error.ArgumentNull(nameof(currentPath));
428+
if(convertSettings == null) throw new ArgumentNullException(nameof(convertSettings));
429+
if(structuredType == null) throw new ArgumentNullException(nameof(structuredType));
430+
if(annotable == null) throw new ArgumentNullException(nameof(annotable));
431+
if(!convertSettings.EnableODataTypeCast) return;
432+
433+
var annotedTypeNames = GetDerivedTypeConstaintTypeNames(annotable);
434+
435+
if(!annotedTypeNames.Any() && convertSettings.RequireDerivedTypesConstraintForODataTypeCastSegments) return; // we don't want to generate any downcast path item if there is no type cast annotation.
436+
437+
var annotedTypeNamesSet = new HashSet<string>(annotedTypeNames, StringComparer.OrdinalIgnoreCase);
438+
439+
bool filter(IEdmStructuredType x) =>
440+
convertSettings.RequireDerivedTypesConstraintForODataTypeCastSegments && annotedTypeNames.Contains(x.FullTypeName()) ||
441+
!convertSettings.RequireDerivedTypesConstraintForODataTypeCastSegments && (
442+
!annotedTypeNames.Any() ||
443+
annotedTypeNames.Contains(x.FullTypeName())
444+
);
445+
446+
var targetTypes = _model
447+
.FindAllDerivedTypes(structuredType)
448+
.Where(x => x.TypeKind == EdmTypeKind.Entity && filter(x))
449+
.OfType<IEdmEntityType>()
450+
.ToArray();
451+
452+
foreach(var targetType in targetTypes)
453+
{
454+
var castPath = currentPath.Clone();
455+
castPath.Push(new ODataTypeCastSegment(targetType));
456+
AppendPath(castPath);
457+
if(targetsMany)
458+
{
459+
CreateCountPath(castPath, convertSettings);
460+
}
461+
else
462+
{
463+
foreach(var declaredNavigationProperty in targetType.DeclaredNavigationProperties())
464+
{
465+
RetrieveNavigationPropertyPaths(declaredNavigationProperty, null, castPath, convertSettings);
466+
}
467+
}
468+
}
469+
}
470+
396471
/// <summary>
397472
/// Retrieve all bounding <see cref="IEdmOperation"/>.
398473
/// </summary>
@@ -419,26 +494,19 @@ private void RetrieveBoundOperationPaths(OpenApiConvertSettings convertSettings)
419494
}
420495

421496
var firstEntityType = bindingType.AsEntity().EntityDefinition();
422-
var allEntitiesForOperation= new List<IEdmEntityType>(){ firstEntityType };
423497

424-
System.Func<IEdmNavigationSource, bool> filter = (z) =>
498+
bool filter(IEdmNavigationSource z) =>
425499
z.EntityType() != firstEntityType &&
426500
z.EntityType().FindAllBaseTypes().Contains(firstEntityType);
427501

428-
//Search all EntitySets
429-
allEntitiesForOperation.AddRange(
430-
_model.EntityContainer.EntitySets()
431-
.Where(filter).Select(x => x.EntityType())
432-
);
433-
434-
//Search all singletons
435-
allEntitiesForOperation.AddRange(
436-
_model.EntityContainer.Singletons()
437-
.Where(filter).Select(x => x.EntityType())
438-
);
439-
440-
allEntitiesForOperation = allEntitiesForOperation.Distinct().ToList();
441-
502+
var allEntitiesForOperation = new IEdmEntityType[] { firstEntityType }
503+
.Union(_model.EntityContainer.EntitySets()
504+
.Where(filter).Select(x => x.EntityType())) //Search all EntitySets
505+
.Union(_model.EntityContainer.Singletons()
506+
.Where(filter).Select(x => x.EntityType())) //Search all singletons
507+
.Distinct()
508+
.ToList();
509+
442510
foreach (var bindingEntityType in allEntitiesForOperation)
443511
{
444512
// 1. Search for corresponding navigation source path
@@ -468,7 +536,7 @@ private void RetrieveBoundOperationPaths(OpenApiConvertSettings convertSettings)
468536
}
469537
}
470538
}
471-
private static readonly HashSet<ODataPathKind> _oDataPathKindsToSkipForOperations = new HashSet<ODataPathKind>() {
539+
private static readonly HashSet<ODataPathKind> _oDataPathKindsToSkipForOperationsWhenSingle = new() {
472540
ODataPathKind.EntitySet,
473541
ODataPathKind.MediaEntity,
474542
ODataPathKind.DollarCount
@@ -483,8 +551,22 @@ private bool AppendBoundOperationOnNavigationSourcePath(IEdmOperation edmOperati
483551

484552
foreach (var subPath in value)
485553
{
486-
if ((isCollection && subPath.Kind == ODataPathKind.EntitySet) ||
487-
(!isCollection && !_oDataPathKindsToSkipForOperations.Contains(subPath.Kind)))
554+
var lastPathSegment = subPath.LastOrDefault();
555+
var secondLastPathSegment = subPath.Count > 1 ? subPath.ElementAt(subPath.Count - 2) : null;
556+
if (subPath.Kind == ODataPathKind.TypeCast &&
557+
!isCollection &&
558+
secondLastPathSegment != null &&
559+
secondLastPathSegment is not ODataKeySegment &&
560+
(secondLastPathSegment is not ODataNavigationSourceSegment navSource || navSource.NavigationSource is not IEdmSingleton) &&
561+
(secondLastPathSegment is not ODataNavigationPropertySegment navProp || navProp.NavigationProperty.Type.IsCollection()))
562+
{// we don't want to add operations bound to single elements on type cast segments under collections, only under the key segment, singletons and nav props bound to singles.
563+
continue;
564+
}
565+
else if ((lastPathSegment is not ODataTypeCastSegment castSegment ||
566+
castSegment.EntityType == bindingEntityType ||
567+
bindingEntityType.InheritsFrom(castSegment.EntityType)) && // we don't want to add operations from the parent types under type cast segments because they already are present without the cast
568+
((isCollection && subPath.Kind == ODataPathKind.EntitySet) ||
569+
(!isCollection && !_oDataPathKindsToSkipForOperationsWhenSingle.Contains(subPath.Kind))))
488570
{
489571
ODataPath newPath = subPath.Clone();
490572
newPath.Push(new ODataOperationSegment(edmOperation, isEscapedFunction));
@@ -611,9 +693,11 @@ private bool HasUnsatisfiedDerivedTypeConstraint(
611693
OpenApiConvertSettings convertSettings)
612694
{
613695
return convertSettings.RequireDerivedTypesConstraintForBoundOperations &&
614-
!(_model.GetCollection(annotatable, "Org.OData.Validation.V1.DerivedTypeConstraint") ?? Enumerable.Empty<string>())
696+
!GetDerivedTypeConstaintTypeNames(annotatable)
615697
.Any(c => c.Equals(baseType.FullName(), StringComparison.OrdinalIgnoreCase));
616698
}
699+
private IEnumerable<string> GetDerivedTypeConstaintTypeNames(IEdmVocabularyAnnotatable annotatable) =>
700+
_model.GetCollection(annotatable, "Org.OData.Validation.V1.DerivedTypeConstraint") ?? Enumerable.Empty<string>();
617701

618702
private bool AppendBoundOperationOnDerivedNavigationPropertyPath(
619703
IEdmOperation edmOperation,

src/Microsoft.OpenApi.OData.Reader/Edm/ODataSegment.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55

66
using System;
77
using System.Collections.Generic;
8+
using System.Linq;
89
using Microsoft.OData.Edm;
910
using Microsoft.OpenApi.Models;
11+
using Microsoft.OpenApi.OData.Common;
1012

1113
namespace Microsoft.OpenApi.OData.Edm
1214
{
@@ -100,6 +102,17 @@ public string GetPathItemName(OpenApiConvertSettings settings)
100102
{
101103
return GetPathItemName(settings, new HashSet<string>());
102104
}
105+
/// <summary>
106+
/// Profides a suffix for the operation id based on the operation path.
107+
/// </summary>
108+
/// <param name="path">Path to use to deduplicate.</param>
109+
/// <param name="settings">The settings.</param>
110+
///<returns>The suffix.</returns>
111+
public string GetPathHash(OpenApiConvertSettings settings, ODataPath path = default)
112+
{
113+
var suffix = string.Join("/", path?.Segments.Select(x => x.Identifier).Distinct() ?? Enumerable.Empty<string>());
114+
return (GetPathItemName(settings) + suffix).GetHashSHA256().Substring(0, 4);
115+
}
103116

104117
/// <summary>
105118
/// Gets the path item name for this segment.

src/Microsoft.OpenApi.OData.Reader/Edm/ODataStreamPropertySegment.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public class ODataStreamPropertySegment : ODataSegment
1414
{
1515
private readonly string _streamPropertyName;
1616
/// <summary>
17-
/// Initializes a new instance of <see cref="ODataTypeCastSegment"/> class.
17+
/// Initializes a new instance of <see cref="ODataStreamPropertySegment"/> class.
1818
/// </summary>
1919
/// <param name="streamPropertyName">The name of the stream property.</param>
2020
public ODataStreamPropertySegment(string streamPropertyName)

src/Microsoft.OpenApi.OData.Reader/OpenApiConvertSettings.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,16 @@ public string PathPrefix
192192
/// Gets/sets a value indicating whether or not single quotes surrounding string parameters in url templates should be added.
193193
/// </summary>
194194
public bool AddSingleQuotesForStringParameters { get; set; } = false;
195+
196+
/// <summary>
197+
/// Gets/sets a value indicating whether or not to include the OData type cast segments.
198+
/// </summary>
199+
public bool EnableODataTypeCast { get; set; } = true;
200+
201+
/// <summary>
202+
/// Gets/sets a value indicating whether or not to require a derived types constraint to include the OData type cast segments.
203+
/// </summary>
204+
public bool RequireDerivedTypesConstraintForODataTypeCastSegments { get; set; } = true;
195205

196206
internal OpenApiConvertSettings Clone()
197207
{
@@ -225,6 +235,8 @@ internal OpenApiConvertSettings Clone()
225235
PathProvider = this.PathProvider,
226236
EnableDollarCountPath = this.EnableDollarCountPath,
227237
AddSingleQuotesForStringParameters = this.AddSingleQuotesForStringParameters,
238+
EnableODataTypeCast = this.EnableODataTypeCast,
239+
RequireDerivedTypesConstraintForODataTypeCastSegments = this.RequireDerivedTypesConstraintForODataTypeCastSegments,
228240
};
229241

230242
return newSettings;

src/Microsoft.OpenApi.OData.Reader/Operation/DollarCountGetOperationHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ protected override void SetBasicInfo(OpenApiOperation operation)
4646
// OperationId
4747
if (Context.Settings.EnableOperationId)
4848
{
49-
operation.OperationId = $"Get.Count.{LastSecondSegment.Identifier}";
49+
operation.OperationId = $"Get.Count.{LastSecondSegment.Identifier}-{Path.GetPathHash(Context.Settings)}";
5050
}
5151

5252
base.SetBasicInfo(operation);

src/Microsoft.OpenApi.OData.Reader/Operation/EdmOperationImportOperationHandler.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,9 @@ protected override void SetBasicInfo(OpenApiOperation operation)
5555
}
5656
else
5757
{
58-
ODataOperationImportSegment operationImportSegment = Path.LastSegment as ODataOperationImportSegment;
59-
string pathItemName = operationImportSegment.GetPathItemName(Context.Settings, new HashSet<string>());
6058
if (Context.Model.IsOperationImportOverload(EdmOperationImport))
6159
{
62-
string hash = pathItemName.GetHashSHA256();
63-
operation.OperationId = "FunctionImport." + EdmOperationImport.Name + "-" + hash.Substring(0, 4);
60+
operation.OperationId = "FunctionImport." + EdmOperationImport.Name + "-" + Path.LastSegment.GetPathHash(Context.Settings);
6461
}
6562
else
6663
{

0 commit comments

Comments
 (0)