Skip to content

Commit a6ca972

Browse files
author
Chris Martinez
committed
Fix OData API exploration. Fixes #529, #599, #573, #610.
1 parent c93fb91 commit a6ca972

File tree

18 files changed

+546
-338
lines changed

18 files changed

+546
-338
lines changed

src/Common.OData.ApiExplorer/AspNet.OData/Routing/ODataRouteActionType.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ enum ODataRouteActionType
66
EntitySet,
77
BoundOperation,
88
UnboundOperation,
9+
Singleton,
910
}
1011
}

src/Common.OData.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilder.cs

Lines changed: 133 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -49,24 +49,81 @@ sealed partial class ODataRouteBuilder
4949

5050
internal ODataRouteBuilder( ODataRouteBuilderContext context ) => Context = context;
5151

52+
internal bool IsNavigationPropertyLink { get; private set; }
53+
54+
ODataRouteBuilderContext Context { get; }
55+
5256
internal string Build()
5357
{
5458
var builder = new StringBuilder();
5559

60+
IsNavigationPropertyLink = false;
5661
BuildPath( builder );
5762
BuildQuery( builder );
5863

5964
return builder.ToString();
6065
}
6166

62-
ODataRouteBuilderContext Context { get; }
67+
internal string GetRoutePrefix() =>
68+
IsNullOrEmpty( Context.RoutePrefix ) ? string.Empty : RemoveRouteConstraints( Context.RoutePrefix! );
69+
70+
internal IReadOnlyList<string> ExpandNavigationPropertyLinkTemplate( string template )
71+
{
72+
if ( IsNullOrEmpty( template ) )
73+
{
74+
#if WEBAPI
75+
return new string[0];
76+
#else
77+
return Array.Empty<string>();
78+
#endif
79+
}
80+
81+
var token = Concat( "{", NavigationProperty, "}" );
82+
83+
if ( template.IndexOf( token, OrdinalIgnoreCase ) < 0 )
84+
{
85+
return new[] { template };
86+
}
87+
88+
IEdmEntityType entity;
89+
90+
switch ( Context.ActionType )
91+
{
92+
case EntitySet:
93+
entity = Context.EntitySet.EntityType();
94+
break;
95+
case Singleton:
96+
entity = Context.Singleton.EntityType();
97+
break;
98+
default:
99+
#if WEBAPI
100+
return new string[0];
101+
#else
102+
return Array.Empty<string>();
103+
#endif
104+
}
105+
106+
var properties = entity.NavigationProperties().ToArray();
107+
var refLinks = new string[properties.Length];
108+
109+
for ( var i = 0; i < properties.Length; i++ )
110+
{
111+
#if WEBAPI
112+
refLinks[i] = template.Replace( token, properties[i].Name );
113+
#else
114+
refLinks[i] = template.Replace( token, properties[i].Name, OrdinalIgnoreCase );
115+
#endif
116+
}
117+
118+
return refLinks;
119+
}
63120

64121
void BuildPath( StringBuilder builder )
65122
{
66123
var segments = new List<string>();
67124

68125
AppendRoutePrefix( segments );
69-
AppendEntitySetOrOperation( segments );
126+
AppendPath( segments );
70127

71128
builder.Append( Join( "/", segments ) );
72129
}
@@ -84,7 +141,7 @@ void AppendRoutePrefix( IList<string> segments )
84141
segments.Add( prefix );
85142
}
86143

87-
void AppendEntitySetOrOperation( IList<string> segments )
144+
void AppendPath( IList<string> segments )
88145
{
89146
#if WEBAPI
90147
var controllerDescriptor = Context.ActionDescriptor.ControllerDescriptor;
@@ -95,19 +152,21 @@ void AppendEntitySetOrOperation( IList<string> segments )
95152
if ( Context.IsAttributeRouted )
96153
{
97154
#if WEBAPI
98-
var prefix = controllerDescriptor.GetCustomAttributes<ODataRoutePrefixAttribute>().FirstOrDefault()?.Prefix?.Trim( '/' );
155+
var attributes = controllerDescriptor.GetCustomAttributes<ODataRoutePrefixAttribute>();
99156
#else
100-
var prefix = controllerDescriptor.ControllerTypeInfo.GetCustomAttributes<ODataRoutePrefixAttribute>().FirstOrDefault()?.Prefix?.Trim( '/' );
157+
var attributes = controllerDescriptor.ControllerTypeInfo.GetCustomAttributes<ODataRoutePrefixAttribute>();
101158
#endif
102-
AppendEntitySetOrOperationFromAttributes( segments, prefix );
159+
var prefix = attributes.FirstOrDefault()?.Prefix?.Trim( '/' );
160+
161+
AppendPathFromAttributes( segments, prefix );
103162
}
104163
else
105164
{
106-
AppendEntitySetOrOperationFromConvention( segments, controllerDescriptor.ControllerName );
165+
AppendPathFromConventions( segments, controllerDescriptor.ControllerName );
107166
}
108167
}
109168

110-
void AppendEntitySetOrOperationFromAttributes( IList<string> segments, string? prefix )
169+
void AppendPathFromAttributes( IList<string> segments, string? prefix )
111170
{
112171
var template = Context.RouteTemplate;
113172

@@ -141,7 +200,7 @@ void AppendEntitySetOrOperationFromAttributes( IList<string> segments, string? p
141200
}
142201
}
143202

144-
void AppendEntitySetOrOperationFromConvention( IList<string> segments, string controllerName )
203+
void AppendPathFromConventions( IList<string> segments, string controllerName )
145204
{
146205
var builder = new StringBuilder();
147206

@@ -150,7 +209,11 @@ void AppendEntitySetOrOperationFromConvention( IList<string> segments, string co
150209
case EntitySet:
151210
builder.Append( controllerName );
152211
AppendEntityKeysFromConvention( builder );
153-
AppendNavigationPropertyFromConvention( builder );
212+
AppendNavigationPropertyFromConvention( builder, Context.EntitySet.EntityType() );
213+
break;
214+
case Singleton:
215+
builder.Append( controllerName );
216+
AppendNavigationPropertyFromConvention( builder, Context.Singleton.EntityType() );
154217
break;
155218
case BoundOperation:
156219
builder.Append( controllerName );
@@ -175,10 +238,21 @@ void AppendEntitySetOrOperationFromConvention( IList<string> segments, string co
175238
void AppendEntityKeysFromConvention( StringBuilder builder )
176239
{
177240
// REF: http://odata.github.io/WebApi/#13-06-KeyValueBinding
178-
var entityKeys = ( Context.EntitySet?.EntityType().Key() ?? Empty<IEdmStructuralProperty>() ).ToArray();
241+
if ( Context.EntitySet == null )
242+
{
243+
return;
244+
}
245+
246+
var entityKeys = Context.EntitySet.EntityType().Key().ToArray();
247+
248+
if ( entityKeys.Length == 0 )
249+
{
250+
return;
251+
}
252+
179253
var parameterKeys = Context.ParameterDescriptions.Where( p => p.Name.StartsWith( Key, OrdinalIgnoreCase ) ).ToArray();
180254

181-
if ( entityKeys.Length == 0 || entityKeys.Length != parameterKeys.Length )
255+
if ( entityKeys.Length != parameterKeys.Length )
182256
{
183257
return;
184258
}
@@ -219,18 +293,22 @@ void AppendEntityKeysFromConvention( StringBuilder builder )
219293
}
220294
}
221295

222-
void AppendNavigationPropertyFromConvention( StringBuilder builder )
296+
void AppendNavigationPropertyFromConvention( StringBuilder builder, IEdmEntityType entityType )
223297
{
224298
var actionName = Context.ActionDescriptor.ActionName;
225-
var navigationProperties = new Lazy<IEdmNavigationProperty[]>( Context.EntitySet.EntityType().NavigationProperties().ToArray );
226299
#if API_EXPLORER
227-
var refLink = TryAppendNavigationPropertyLink( builder, actionName, navigationProperties );
300+
var navigationProperties = entityType.NavigationProperties().ToArray();
301+
302+
IsNavigationPropertyLink = TryAppendNavigationPropertyLink( builder, actionName, navigationProperties );
228303
#else
229-
var refLink = TryAppendNavigationPropertyLink( builder, actionName );
304+
IsNavigationPropertyLink = TryAppendNavigationPropertyLink( builder, actionName );
230305
#endif
231306

232-
if ( !refLink )
307+
if ( !IsNavigationPropertyLink )
233308
{
309+
#if !API_EXPLORER
310+
var navigationProperties = entityType.NavigationProperties().ToArray();
311+
#endif
234312
TryAppendNavigationProperty( builder, actionName, navigationProperties );
235313
}
236314
}
@@ -494,12 +572,11 @@ IList<ApiParameterDescription> GetQueryParameters( IList<ApiParameterDescription
494572
return queryParameters;
495573
}
496574

497-
bool TryAppendNavigationProperty( StringBuilder builder, string name, Lazy<IEdmNavigationProperty[]> navigationProperties )
575+
bool TryAppendNavigationProperty( StringBuilder builder, string name, IReadOnlyList<IEdmNavigationProperty> navigationProperties )
498576
{
499577
// REF: https://github.com/OData/WebApi/blob/master/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/PropertyRoutingConvention.cs
500-
const string NavigationPropertyPrefix = @"(?:Get|(?:Post|Put|Delete|Patch)To)(\w+)";
501-
const string NavigationProperty = "^" + NavigationPropertyPrefix + "$";
502-
const string NavigationPropertyFromDeclaringType = "^" + NavigationPropertyPrefix + @"From(\w+)$";
578+
const string NavigationProperty = @"(?:Get|(?:Post|Put|Delete|Patch)To)(\w+)";
579+
const string NavigationPropertyFromDeclaringType = NavigationProperty + @"From(\w+)";
503580
var match = Regex.Match( name, NavigationPropertyFromDeclaringType, RegexOptions.Singleline );
504581

505582
if ( !match.Success )
@@ -519,7 +596,7 @@ bool TryAppendNavigationProperty( StringBuilder builder, string name, Lazy<IEdmN
519596

520597
if ( Context.Options.UseQualifiedNames )
521598
{
522-
var navigationProperty = navigationProperties.Value.First( p => p.Name.Equals( navigationPropertyName, OrdinalIgnoreCase ) );
599+
var navigationProperty = navigationProperties.First( p => p.Name.Equals( navigationPropertyName, OrdinalIgnoreCase ) );
523600
builder.Append( navigationProperty.Type.ShortQualifiedName() );
524601
}
525602
else
@@ -535,19 +612,22 @@ bool TryAppendNavigationProperty( StringBuilder builder, string name, Lazy<IEdmN
535612

536613
return true;
537614
}
615+
538616
#if API_EXPLORER
539-
bool TryAppendNavigationPropertyLink( StringBuilder builder, string name, Lazy<IEdmNavigationProperty[]> navigationProperties )
617+
bool TryAppendNavigationPropertyLink( StringBuilder builder, string name, IReadOnlyList<IEdmNavigationProperty> navigationProperties )
540618
#else
541-
static bool TryAppendNavigationPropertyLink( StringBuilder builder, string name )
619+
bool TryAppendNavigationPropertyLink( StringBuilder builder, string name )
542620
#endif
543621
{
544622
// REF: https://github.com/OData/WebApi/blob/master/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/RefRoutingConvention.cs
545-
const string NavigationPropertyLinkPrefix = "(?:Create|Delete|Get)Ref";
546-
const string NavigationPropertyLink = "^" + NavigationPropertyLinkPrefix + "$";
547-
const string NavigationPropertyLinkTo = "^" + NavigationPropertyLinkPrefix + @"To(\w+)$";
548-
const string NavigationPropertyLinkFrom = "^" + NavigationPropertyLinkPrefix + @"To(\w+)From(\w+)$";
549-
var patterns = new[] { NavigationPropertyLinkFrom, NavigationPropertyLinkTo, NavigationPropertyLink };
623+
const int Link = 1;
624+
const int LinkTo = 2;
625+
const int LinkFrom = 3;
626+
const string NavigationPropertyLink = "(?:Create|Delete|Get)Ref";
627+
const string NavigationPropertyLinkTo = NavigationPropertyLink + @"To(\w+)";
628+
const string NavigationPropertyLinkFrom = NavigationPropertyLinkTo + @"From(\w+)";
550629
var i = 0;
630+
var patterns = new[] { NavigationPropertyLinkFrom, NavigationPropertyLinkTo, NavigationPropertyLink };
551631
var match = Regex.Match( name, patterns[i], RegexOptions.Singleline );
552632

553633
while ( !match.Success && ++i < patterns.Length )
@@ -560,56 +640,60 @@ static bool TryAppendNavigationPropertyLink( StringBuilder builder, string name
560640
return false;
561641
}
562642

643+
var convention = match.Groups.Count;
563644
var propertyName = match.Groups[1].Value;
564645

565646
builder.Append( '/' );
566647

567-
switch ( match.Groups.Count )
648+
switch ( convention )
568649
{
569-
case 1:
650+
case Link:
570651
builder.Append( '{' ).Append( NavigationProperty ).Append( '}' );
571652
#if API_EXPLORER
572-
AddOrReplaceNavigationPropertyParameter();
653+
RemoveNavigationPropertyParameter();
573654
#endif
574655
break;
575-
case 2:
576-
case 3:
656+
case LinkTo:
657+
case LinkFrom:
577658
builder.Append( propertyName );
578-
#if API_EXPLORER
579-
var parameters = Context.ParameterDescriptions;
580-
581-
for ( i = 0; i < parameters.Count; i++ )
582-
{
583-
if ( parameters[i].Name.Equals( NavigationProperty, OrdinalIgnoreCase ) )
584-
{
585-
parameters.RemoveAt( i );
586-
break;
587-
}
588-
}
589-
#endif
659+
RemoveNavigationPropertyParameter();
590660
break;
591661
}
592662

593663
builder.Append( "/$ref" );
594664

595665
#if API_EXPLORER
596-
if ( name.StartsWith( "DeleteRef", OrdinalIgnoreCase ) )
666+
if ( name.StartsWith( "DeleteRef", Ordinal ) && !IsNullOrEmpty( propertyName ) )
597667
{
598-
var property = navigationProperties.Value.First( p => p.Name.Equals( propertyName, OrdinalIgnoreCase ) );
668+
var property = navigationProperties.First( p => p.Name.Equals( propertyName, OrdinalIgnoreCase ) );
599669

600670
if ( property.TargetMultiplicity() == EdmMultiplicity.Many )
601671
{
602672
AddOrReplaceRefIdQueryParameter();
603673
}
604674
}
605-
else if ( name.StartsWith( "CreateRef", OrdinalIgnoreCase ) )
675+
else if ( name.StartsWith( "CreateRef", Ordinal ) )
606676
{
607677
AddOrReplaceIdBodyParameter();
608678
}
609679
#endif
610680
return true;
611681
}
612682

683+
void RemoveNavigationPropertyParameter()
684+
{
685+
var parameters = Context.ParameterDescriptions;
686+
687+
for ( var i = 0; i < parameters.Count; i++ )
688+
{
689+
if ( parameters[i].Name.Equals( NavigationProperty, OrdinalIgnoreCase ) )
690+
{
691+
parameters.RemoveAt( i );
692+
break;
693+
}
694+
}
695+
}
696+
613697
static string GetRouteParameterName( IReadOnlyDictionary<string, ApiParameterDescription> actionParameters, string name )
614698
{
615699
if ( !actionParameters.TryGetValue( name, out var parameter ) )

0 commit comments

Comments
 (0)