Skip to content

Commit 0494a71

Browse files
Fix route template building with optimizations. Fixes #689, Fixes #710
1 parent 42ba3eb commit 0494a71

File tree

57 files changed

+842
-213
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+842
-213
lines changed

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

+56-29
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
using System.Reflection;
1414
using System.Text;
1515
using System.Text.RegularExpressions;
16-
using System.Threading.Tasks;
1716
#if WEBAPI
1817
using System.Web.Http.Description;
1918
#endif
@@ -108,11 +107,7 @@ internal IReadOnlyList<string> ExpandNavigationPropertyLinkTemplate( string temp
108107

109108
for ( var i = 0; i < properties.Length; i++ )
110109
{
111-
#if WEBAPI
112-
refLinks[i] = template.Replace( token, properties[i].Name );
113-
#else
114110
refLinks[i] = template.Replace( token, properties[i].Name, OrdinalIgnoreCase );
115-
#endif
116111
}
117112

118113
return refLinks;
@@ -143,21 +138,37 @@ void AppendRoutePrefix( IList<string> segments )
143138

144139
void AppendPath( IList<string> segments )
145140
{
141+
var controllerDescriptor = Context.ActionDescriptor
146142
#if WEBAPI
147-
var controllerDescriptor = Context.ActionDescriptor.ControllerDescriptor;
148-
#else
149-
var controllerDescriptor = Context.ActionDescriptor;
143+
.ControllerDescriptor
150144
#endif
145+
;
151146

152147
if ( Context.IsAttributeRouted )
153148
{
154-
#if WEBAPI
155-
var attributes = controllerDescriptor.GetCustomAttributes<ODataRoutePrefixAttribute>();
156-
#else
157-
var attributes = controllerDescriptor.ControllerTypeInfo.GetCustomAttributes<ODataRoutePrefixAttribute>();
149+
var prefix = default( string );
150+
var attribute = controllerDescriptor
151+
#if !WEBAPI
152+
.ControllerTypeInfo
158153
#endif
159-
var prefix = attributes.FirstOrDefault()?.Prefix?.Trim( '/' );
154+
.GetCustomAttributes<ODataRoutePrefixAttribute>()
155+
.FirstOrDefault();
160156

157+
if ( attribute is not null )
158+
{
159+
prefix = attribute.Prefix?.Trim( '/' );
160+
}
161+
#if !WEBAPI
162+
else
163+
{
164+
var nativeEndpointRouting = Context.RouteTemplateProvider is not null;
165+
166+
if ( nativeEndpointRouting )
167+
{
168+
prefix = controllerDescriptor.ControllerName;
169+
}
170+
}
171+
#endif
161172
AppendPathFromAttributes( segments, prefix );
162173
}
163174
else
@@ -168,7 +179,7 @@ void AppendPath( IList<string> segments )
168179

169180
void AppendPathFromAttributes( IList<string> segments, string? prefix )
170181
{
171-
var template = Context.RouteTemplate;
182+
var template = Context.RouteTemplate?.Replace( "[action]", Context.ActionDescriptor.ActionName, OrdinalIgnoreCase );
172183

173184
if ( Context.IsOperation && Context.RouteTemplateGeneration == Client && !IsNullOrEmpty( template ) )
174185
{
@@ -550,11 +561,11 @@ IList<ApiParameterDescription> GetQueryParameters( IList<ApiParameterDescription
550561
continue;
551562
}
552563

564+
var parameterType = parameter
553565
#if API_EXPLORER
554-
var parameterType = parameter.ParameterDescriptor?.ParameterType;
555-
#else
556-
var parameterType = parameter.ParameterType;
566+
.ParameterDescriptor?
557567
#endif
568+
.ParameterType;
558569

559570
if ( parameterType == null || IsBuiltInParameter( parameterType ) )
560571
{
@@ -574,43 +585,59 @@ IList<ApiParameterDescription> GetQueryParameters( IList<ApiParameterDescription
574585

575586
bool TryAppendNavigationProperty( StringBuilder builder, string name, IReadOnlyList<IEdmNavigationProperty> navigationProperties )
576587
{
588+
if ( navigationProperties.Count == 0 )
589+
{
590+
return false;
591+
}
592+
577593
// REF: https://github.com/OData/WebApi/blob/master/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/PropertyRoutingConvention.cs
578594
const string NavigationProperty = @"(?:Get|(?:Post|Put|Delete|Patch)To)(\w+)";
579595
const string NavigationPropertyFromDeclaringType = NavigationProperty + @"From(\w+)";
580596
var match = Regex.Match( name, NavigationPropertyFromDeclaringType, RegexOptions.Singleline );
597+
string propertyName;
581598

582-
if ( !match.Success )
599+
if ( match.Success )
600+
{
601+
propertyName = match.Groups[2].Value;
602+
}
603+
else
583604
{
584605
match = Regex.Match( name, NavigationProperty, RegexOptions.Singleline );
585606

586-
if ( !match.Success )
607+
if ( match.Success )
608+
{
609+
propertyName = match.Groups[1].Value;
610+
}
611+
else
587612
{
588613
return false;
589614
}
590615
}
591-
else
616+
617+
for ( var i = 0; i < navigationProperties.Count; i++ )
592618
{
593-
var navigationPropertyName = match.Groups[2].Value;
619+
var navigationProperty = navigationProperties[i];
620+
621+
if ( !navigationProperty.Name.Equals( propertyName, OrdinalIgnoreCase ) )
622+
{
623+
continue;
624+
}
594625

595626
builder.Append( '/' );
596627

597628
if ( Context.Options.UseQualifiedNames )
598629
{
599-
var navigationProperty = navigationProperties.First( p => p.Name.Equals( navigationPropertyName, OrdinalIgnoreCase ) );
600630
builder.Append( navigationProperty.Type.ShortQualifiedName() );
601631
}
602632
else
603633
{
604-
builder.Append( navigationPropertyName );
634+
builder.Append( propertyName );
605635
}
606-
}
607636

608-
var propertyName = match.Groups[1].Value;
609-
610-
builder.Append( '/' );
611-
builder.Append( propertyName );
637+
return true;
638+
}
612639

613-
return true;
640+
return false;
614641
}
615642

616643
#if API_EXPLORER

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Microsoft.AspNetCore.Mvc.Abstractions;
66
using Microsoft.AspNetCore.Mvc.ApiExplorer;
77
using Microsoft.AspNetCore.Mvc.Controllers;
8+
using Microsoft.AspNetCore.Mvc.Routing;
89
using Microsoft.AspNetCore.Mvc.Versioning;
910
#endif
1011
using Microsoft.Extensions.DependencyInjection;
@@ -54,6 +55,10 @@ sealed partial class ODataRouteBuilderContext
5455

5556
internal string? RouteTemplate { get; }
5657

58+
#if !WEBAPI
59+
internal IRouteTemplateProvider? RouteTemplateProvider { get; }
60+
#endif
61+
5762
internal string? RoutePrefix { get; }
5863

5964
internal ControllerActionDescriptor ActionDescriptor { get; }
@@ -162,7 +167,7 @@ static bool IsActionOrFunction( IEdmEntitySet? entitySet, IEdmSingleton? singlet
162167
else if ( FunctionMethod.Equals( method, OrdinalIgnoreCase ) && actionName != FunctionMethod )
163168
{
164169
if ( actionName.StartsWith( "GetRef", Ordinal ) ||
165-
( entitySet != null && actionName == ( ActionMethod + entitySet.Name ) ) )
170+
( entitySet != null && actionName == ( FunctionMethod + entitySet.Name ) ) )
166171
{
167172
return false;
168173
}

src/Microsoft.AspNet.OData.Versioning.ApiExplorer/Microsoft.AspNet.OData.Versioning.ApiExplorer.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
<ItemGroup>
1515
<Compile Include="..\Common.OData\TypeExtensions.cs" Link="AspNet.OData\TypeExtensions.cs" />
1616
<Compile Include="..\Microsoft.AspNet.WebApi.Versioning.ApiExplorer\System.Web.Http\Controllers\*.cs" Link="System.Web.Http\Controllers\%(Filename)%(Extension)" />
17+
<Compile Include="..\Microsoft.AspNet.WebApi.Versioning.ApiExplorer\StringExtensions.cs" Link="%(Filename)%(Extension)" />
1718
</ItemGroup>
1819

1920
<ItemGroup>

src/Microsoft.AspNet.OData.Versioning/Microsoft.AspNet.OData.Versioning.csproj

+2-13
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,15 @@
1616
</ItemGroup>
1717

1818
<ItemGroup>
19-
<PackageReference Include="Microsoft.AspNet.OData" Version="[7.5.0,8.0.0)" />
19+
<PackageReference Include="Microsoft.AspNet.OData" Version="[7.5.8,8.0.0)" />
2020
</ItemGroup>
2121

22-
<ItemGroup Condition=" '$(TargetFramework)' == 'net45' ">
22+
<ItemGroup>
2323
<Reference Include="System" />
2424
<Reference Include="Microsoft.CSharp" />
2525
</ItemGroup>
2626

27-
<ItemGroup>
28-
<Compile Update="LocalSR.Designer.cs">
29-
<DesignTime>True</DesignTime>
30-
</Compile>
31-
</ItemGroup>
32-
3327
<Import Project="..\Shared\Shared.projitems" Label="Shared" />
3428
<Import Project="..\Common.OData\Common.OData.projitems" Label="Shared" />
35-
<ItemGroup>
36-
<Compile Update="C:\Dev\Projects\aspnet-api-versioning\src\Common.OData\SR.Designer.cs">
37-
<DesignTime>True</DesignTime>
38-
</Compile>
39-
</ItemGroup>
4029

4130
</Project>

src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/Microsoft.AspNet.WebApi.Versioning.ApiExplorer.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<ProjectReference Include="..\Microsoft.AspNet.WebApi.Versioning\Microsoft.AspNet.WebApi.Versioning.csproj" />
2020
</ItemGroup>
2121

22-
<ItemGroup Condition=" '$(TargetFramework)' == 'net45' ">
22+
<ItemGroup>
2323
<Reference Include="System" />
2424
<Reference Include="Microsoft.CSharp" />
2525
</ItemGroup>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
namespace System
2+
{
3+
using System.Text;
4+
5+
static class StringExtensions
6+
{
7+
// REF: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs#L890
8+
internal static string Replace( this string @string, string oldValue, string? newValue, StringComparison comparisonType )
9+
{
10+
if ( string.IsNullOrEmpty( oldValue ) )
11+
{
12+
throw new ArgumentNullException( nameof( oldValue ) );
13+
}
14+
15+
var length = @string.Length;
16+
17+
if ( length == 0 )
18+
{
19+
return @string;
20+
}
21+
22+
var result = new StringBuilder( length );
23+
var start = 0;
24+
var matchLength = oldValue.Length;
25+
var index = @string.IndexOf( oldValue, start, comparisonType );
26+
27+
while ( index >= 0 )
28+
{
29+
result.Append( @string.Substring( start, index - start ) );
30+
result.Append( newValue );
31+
start = index + matchLength;
32+
index = @string.IndexOf( oldValue, start, comparisonType );
33+
}
34+
35+
if ( result.Length == 0 )
36+
{
37+
return @string;
38+
}
39+
40+
if ( start < length )
41+
{
42+
result.Append( @string.Substring( start ) );
43+
}
44+
45+
return result.ToString();
46+
}
47+
}
48+
}

src/Microsoft.AspNet.WebApi.Versioning/Microsoft.AspNet.WebApi.Versioning.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<PackageReference Include="Microsoft.AspNet.WebApi.Core" Version="[5.2.3,6.0.0)" />
1616
</ItemGroup>
1717

18-
<ItemGroup Condition=" '$(TargetFramework)' == 'net45' ">
18+
<ItemGroup>
1919
<Reference Include="System" />
2020
<Reference Include="Microsoft.CSharp" />
2121
</ItemGroup>

src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersionActionSelector.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -724,8 +724,8 @@ public CacheEntry( List<ActionConstraintItem> items )
724724

725725
sealed partial class StringArrayComparer : IEqualityComparer<string[]>
726726
{
727-
public static readonly StringArrayComparer Ordinal = new StringArrayComparer( StringComparer.Ordinal );
728-
public static readonly StringArrayComparer OrdinalIgnoreCase = new StringArrayComparer( StringComparer.OrdinalIgnoreCase );
727+
public static readonly StringArrayComparer Ordinal = new( StringComparer.Ordinal );
728+
public static readonly StringArrayComparer OrdinalIgnoreCase = new( StringComparer.OrdinalIgnoreCase );
729729
readonly StringComparer valueComparer;
730730

731731
StringArrayComparer( StringComparer valueComparer ) => this.valueComparer = valueComparer;

src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs

+27-5
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,11 @@
66
using Microsoft.AspNetCore.Mvc.ApiExplorer;
77
using Microsoft.AspNetCore.Mvc.Controllers;
88
using Microsoft.AspNetCore.Mvc.ModelBinding;
9+
using Microsoft.AspNetCore.Mvc.Routing;
910
using Microsoft.Extensions.DependencyInjection;
1011
using Microsoft.OData;
1112
using Microsoft.OData.Edm;
1213
using System.Collections.Generic;
13-
using System.Reflection;
14-
using static System.Linq.Enumerable;
1514

1615
partial class ODataRouteBuilderContext
1716
{
@@ -23,8 +22,8 @@ internal ODataRouteBuilderContext(
2322
{
2423
ApiVersion = apiVersion;
2524
Services = routeMapping.Services;
26-
routeAttribute = actionDescriptor.MethodInfo.GetCustomAttributes<ODataRouteAttribute>().FirstOrDefault();
27-
RouteTemplate = routeAttribute?.PathTemplate;
25+
(RouteTemplateProvider, routeAttribute) = TryGetRouteAttribute( actionDescriptor );
26+
RouteTemplate = routeAttribute?.PathTemplate ?? RouteTemplateProvider?.Template;
2827
RoutePrefix = routeMapping.RoutePrefix;
2928
ActionDescriptor = actionDescriptor;
3029
ParameterDescriptions = new List<ApiParameterDescription>();
@@ -54,8 +53,31 @@ internal ODataRouteBuilderContext(
5453
internal IODataPathTemplateHandler PathTemplateHandler =>
5554
templateHandler ??= Services.GetRequiredService<IODataPathTemplateHandler>();
5655

56+
internal IModelMetadataProvider? ModelMetadataProvider { get; set; }
57+
5758
static IEnumerable<string> GetHttpMethods( ControllerActionDescriptor action ) => action.GetHttpMethods();
5859

59-
internal IModelMetadataProvider? ModelMetadataProvider { get; set; }
60+
static (IRouteTemplateProvider? RouteTemplateProvider, ODataRouteAttribute? RouteAttribute) TryGetRouteAttribute( ControllerActionDescriptor actionDescriptor )
61+
{
62+
var attributes = actionDescriptor.MethodInfo.GetCustomAttributes( inherit: false );
63+
var templateProvider = default( IRouteTemplateProvider );
64+
65+
for ( var i = 0; i < attributes.Length; i++ )
66+
{
67+
var attribute = attributes[i];
68+
69+
if ( attribute is ODataRouteAttribute routeAttribute )
70+
{
71+
return (default, routeAttribute);
72+
}
73+
74+
if ( templateProvider is null )
75+
{
76+
templateProvider = attribute as IRouteTemplateProvider;
77+
}
78+
}
79+
80+
return templateProvider is null ? default : (templateProvider, new ODataRouteAttribute( templateProvider.Template ));
81+
}
6082
}
6183
}

src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ImplicitHttpMethodConvention.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,13 @@ public void Apply( ActionDescriptorProviderContext context, ControllerActionDesc
4141
static string GetImplicitHttpMethod( ControllerActionDescriptor action )
4242
{
4343
const int Post = 2;
44+
var name = action.MethodInfo.Name;
4445

4546
for ( var i = 0; i < SupportedHttpMethodConventions.Count; i++ )
4647
{
4748
var supportedHttpMethod = SupportedHttpMethodConventions[i];
4849

49-
if ( action.MethodInfo.Name.StartsWith( supportedHttpMethod, OrdinalIgnoreCase ) )
50+
if ( name.StartsWith( supportedHttpMethod, OrdinalIgnoreCase ) )
5051
{
5152
return supportedHttpMethod;
5253
}

0 commit comments

Comments
 (0)