@@ -49,24 +49,81 @@ sealed partial class ODataRouteBuilder
49
49
50
50
internal ODataRouteBuilder ( ODataRouteBuilderContext context ) => Context = context ;
51
51
52
+ internal bool IsNavigationPropertyLink { get ; private set ; }
53
+
54
+ ODataRouteBuilderContext Context { get ; }
55
+
52
56
internal string Build ( )
53
57
{
54
58
var builder = new StringBuilder ( ) ;
55
59
60
+ IsNavigationPropertyLink = false ;
56
61
BuildPath ( builder ) ;
57
62
BuildQuery ( builder ) ;
58
63
59
64
return builder . ToString ( ) ;
60
65
}
61
66
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
+ }
63
120
64
121
void BuildPath ( StringBuilder builder )
65
122
{
66
123
var segments = new List < string > ( ) ;
67
124
68
125
AppendRoutePrefix ( segments ) ;
69
- AppendEntitySetOrOperation ( segments ) ;
126
+ AppendPath ( segments ) ;
70
127
71
128
builder . Append ( Join ( "/" , segments ) ) ;
72
129
}
@@ -84,7 +141,7 @@ void AppendRoutePrefix( IList<string> segments )
84
141
segments . Add ( prefix ) ;
85
142
}
86
143
87
- void AppendEntitySetOrOperation ( IList < string > segments )
144
+ void AppendPath ( IList < string > segments )
88
145
{
89
146
#if WEBAPI
90
147
var controllerDescriptor = Context . ActionDescriptor . ControllerDescriptor ;
@@ -95,19 +152,21 @@ void AppendEntitySetOrOperation( IList<string> segments )
95
152
if ( Context . IsAttributeRouted )
96
153
{
97
154
#if WEBAPI
98
- var prefix = controllerDescriptor . GetCustomAttributes < ODataRoutePrefixAttribute > ( ) . FirstOrDefault ( ) ? . Prefix ? . Trim ( '/' ) ;
155
+ var attributes = controllerDescriptor . GetCustomAttributes < ODataRoutePrefixAttribute > ( ) ;
99
156
#else
100
- var prefix = controllerDescriptor . ControllerTypeInfo . GetCustomAttributes < ODataRoutePrefixAttribute > ( ) . FirstOrDefault ( ) ? . Prefix ? . Trim ( '/' ) ;
157
+ var attributes = controllerDescriptor . ControllerTypeInfo . GetCustomAttributes < ODataRoutePrefixAttribute > ( ) ;
101
158
#endif
102
- AppendEntitySetOrOperationFromAttributes ( segments , prefix ) ;
159
+ var prefix = attributes . FirstOrDefault ( ) ? . Prefix ? . Trim ( '/' ) ;
160
+
161
+ AppendPathFromAttributes ( segments , prefix ) ;
103
162
}
104
163
else
105
164
{
106
- AppendEntitySetOrOperationFromConvention ( segments , controllerDescriptor . ControllerName ) ;
165
+ AppendPathFromConventions ( segments , controllerDescriptor . ControllerName ) ;
107
166
}
108
167
}
109
168
110
- void AppendEntitySetOrOperationFromAttributes ( IList < string > segments , string ? prefix )
169
+ void AppendPathFromAttributes ( IList < string > segments , string ? prefix )
111
170
{
112
171
var template = Context . RouteTemplate ;
113
172
@@ -141,7 +200,7 @@ void AppendEntitySetOrOperationFromAttributes( IList<string> segments, string? p
141
200
}
142
201
}
143
202
144
- void AppendEntitySetOrOperationFromConvention ( IList < string > segments , string controllerName )
203
+ void AppendPathFromConventions ( IList < string > segments , string controllerName )
145
204
{
146
205
var builder = new StringBuilder ( ) ;
147
206
@@ -150,7 +209,11 @@ void AppendEntitySetOrOperationFromConvention( IList<string> segments, string co
150
209
case EntitySet :
151
210
builder . Append ( controllerName ) ;
152
211
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 ( ) ) ;
154
217
break ;
155
218
case BoundOperation :
156
219
builder . Append ( controllerName ) ;
@@ -175,10 +238,21 @@ void AppendEntitySetOrOperationFromConvention( IList<string> segments, string co
175
238
void AppendEntityKeysFromConvention ( StringBuilder builder )
176
239
{
177
240
// 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
+
179
253
var parameterKeys = Context . ParameterDescriptions . Where ( p => p . Name . StartsWith ( Key , OrdinalIgnoreCase ) ) . ToArray ( ) ;
180
254
181
- if ( entityKeys . Length == 0 || entityKeys . Length != parameterKeys . Length )
255
+ if ( entityKeys . Length != parameterKeys . Length )
182
256
{
183
257
return ;
184
258
}
@@ -219,18 +293,22 @@ void AppendEntityKeysFromConvention( StringBuilder builder )
219
293
}
220
294
}
221
295
222
- void AppendNavigationPropertyFromConvention ( StringBuilder builder )
296
+ void AppendNavigationPropertyFromConvention ( StringBuilder builder , IEdmEntityType entityType )
223
297
{
224
298
var actionName = Context . ActionDescriptor . ActionName ;
225
- var navigationProperties = new Lazy < IEdmNavigationProperty [ ] > ( Context . EntitySet . EntityType ( ) . NavigationProperties ( ) . ToArray ) ;
226
299
#if API_EXPLORER
227
- var refLink = TryAppendNavigationPropertyLink ( builder , actionName , navigationProperties ) ;
300
+ var navigationProperties = entityType . NavigationProperties ( ) . ToArray ( ) ;
301
+
302
+ IsNavigationPropertyLink = TryAppendNavigationPropertyLink ( builder , actionName , navigationProperties ) ;
228
303
#else
229
- var refLink = TryAppendNavigationPropertyLink ( builder , actionName ) ;
304
+ IsNavigationPropertyLink = TryAppendNavigationPropertyLink ( builder , actionName ) ;
230
305
#endif
231
306
232
- if ( ! refLink )
307
+ if ( ! IsNavigationPropertyLink )
233
308
{
309
+ #if ! API_EXPLORER
310
+ var navigationProperties = entityType . NavigationProperties ( ) . ToArray ( ) ;
311
+ #endif
234
312
TryAppendNavigationProperty ( builder , actionName , navigationProperties ) ;
235
313
}
236
314
}
@@ -494,12 +572,11 @@ IList<ApiParameterDescription> GetQueryParameters( IList<ApiParameterDescription
494
572
return queryParameters ;
495
573
}
496
574
497
- bool TryAppendNavigationProperty ( StringBuilder builder , string name , Lazy < IEdmNavigationProperty [ ] > navigationProperties )
575
+ bool TryAppendNavigationProperty ( StringBuilder builder , string name , IReadOnlyList < IEdmNavigationProperty > navigationProperties )
498
576
{
499
577
// 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+)" ;
503
580
var match = Regex . Match ( name , NavigationPropertyFromDeclaringType , RegexOptions . Singleline ) ;
504
581
505
582
if ( ! match . Success )
@@ -519,7 +596,7 @@ bool TryAppendNavigationProperty( StringBuilder builder, string name, Lazy<IEdmN
519
596
520
597
if ( Context . Options . UseQualifiedNames )
521
598
{
522
- var navigationProperty = navigationProperties . Value . First ( p => p . Name . Equals ( navigationPropertyName , OrdinalIgnoreCase ) ) ;
599
+ var navigationProperty = navigationProperties . First ( p => p . Name . Equals ( navigationPropertyName , OrdinalIgnoreCase ) ) ;
523
600
builder . Append ( navigationProperty . Type . ShortQualifiedName ( ) ) ;
524
601
}
525
602
else
@@ -535,19 +612,22 @@ bool TryAppendNavigationProperty( StringBuilder builder, string name, Lazy<IEdmN
535
612
536
613
return true ;
537
614
}
615
+
538
616
#if API_EXPLORER
539
- bool TryAppendNavigationPropertyLink ( StringBuilder builder , string name , Lazy < IEdmNavigationProperty [ ] > navigationProperties )
617
+ bool TryAppendNavigationPropertyLink ( StringBuilder builder , string name , IReadOnlyList < IEdmNavigationProperty > navigationProperties )
540
618
#else
541
- static bool TryAppendNavigationPropertyLink ( StringBuilder builder , string name )
619
+ bool TryAppendNavigationPropertyLink ( StringBuilder builder , string name )
542
620
#endif
543
621
{
544
622
// 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+)" ;
550
629
var i = 0 ;
630
+ var patterns = new [ ] { NavigationPropertyLinkFrom , NavigationPropertyLinkTo , NavigationPropertyLink } ;
551
631
var match = Regex . Match ( name , patterns [ i ] , RegexOptions . Singleline ) ;
552
632
553
633
while ( ! match . Success && ++ i < patterns . Length )
@@ -560,56 +640,60 @@ static bool TryAppendNavigationPropertyLink( StringBuilder builder, string name
560
640
return false ;
561
641
}
562
642
643
+ var convention = match . Groups . Count ;
563
644
var propertyName = match . Groups [ 1 ] . Value ;
564
645
565
646
builder . Append ( '/' ) ;
566
647
567
- switch ( match . Groups . Count )
648
+ switch ( convention )
568
649
{
569
- case 1 :
650
+ case Link :
570
651
builder . Append ( '{' ) . Append ( NavigationProperty ) . Append ( '}' ) ;
571
652
#if API_EXPLORER
572
- AddOrReplaceNavigationPropertyParameter ( ) ;
653
+ RemoveNavigationPropertyParameter ( ) ;
573
654
#endif
574
655
break ;
575
- case 2 :
576
- case 3 :
656
+ case LinkTo :
657
+ case LinkFrom :
577
658
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 ( ) ;
590
660
break ;
591
661
}
592
662
593
663
builder . Append ( "/$ref" ) ;
594
664
595
665
#if API_EXPLORER
596
- if ( name . StartsWith ( "DeleteRef" , OrdinalIgnoreCase ) )
666
+ if ( name . StartsWith ( "DeleteRef" , Ordinal ) && ! IsNullOrEmpty ( propertyName ) )
597
667
{
598
- var property = navigationProperties . Value . First ( p => p . Name . Equals ( propertyName , OrdinalIgnoreCase ) ) ;
668
+ var property = navigationProperties . First ( p => p . Name . Equals ( propertyName , OrdinalIgnoreCase ) ) ;
599
669
600
670
if ( property . TargetMultiplicity ( ) == EdmMultiplicity . Many )
601
671
{
602
672
AddOrReplaceRefIdQueryParameter ( ) ;
603
673
}
604
674
}
605
- else if ( name . StartsWith ( "CreateRef" , OrdinalIgnoreCase ) )
675
+ else if ( name . StartsWith ( "CreateRef" , Ordinal ) )
606
676
{
607
677
AddOrReplaceIdBodyParameter ( ) ;
608
678
}
609
679
#endif
610
680
return true ;
611
681
}
612
682
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
+
613
697
static string GetRouteParameterName ( IReadOnlyDictionary < string , ApiParameterDescription > actionParameters , string name )
614
698
{
615
699
if ( ! actionParameters . TryGetValue ( name , out var parameter ) )
0 commit comments