@@ -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