Skip to content

Commit 42b0d53

Browse files
Chris Martinezcommonsensesoftware
authored andcommitted
Add support for Endpoint Routing
1 parent 2f07397 commit 42b0d53

File tree

63 files changed

+1177
-282
lines changed

Some content is hidden

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

63 files changed

+1177
-282
lines changed

src/Microsoft.AspNetCore.Mvc.Versioning/Abstractions/ActionDescriptorExtensions.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
namespace Microsoft.AspNetCore.Mvc.Abstractions
22
{
33
using Microsoft.AspNetCore.Mvc.ApplicationModels;
4+
using Microsoft.AspNetCore.Mvc.Controllers;
45
using Microsoft.AspNetCore.Mvc.Versioning;
56
using System;
67
using System.Diagnostics.Contracts;
78
using System.Linq;
9+
using System.Text;
810
using static Microsoft.AspNetCore.Mvc.Versioning.ApiVersionMapping;
911
using static System.Linq.Enumerable;
1012

@@ -101,5 +103,47 @@ public static bool IsMappedTo( this ActionDescriptor action, ApiVersion apiVersi
101103
Arg.NotNull( action, nameof( action ) );
102104
return action.MappingTo( apiVersion ) > None;
103105
}
106+
107+
internal static string ExpandSignature( this ActionDescriptor action )
108+
{
109+
Contract.Requires( action != null );
110+
Contract.Ensures( !string.IsNullOrEmpty( Contract.Result<string>() ) );
111+
112+
if ( !( action is ControllerActionDescriptor controllerAction ) )
113+
{
114+
return action.DisplayName;
115+
}
116+
117+
var signature = new StringBuilder();
118+
var controllerType = controllerAction.ControllerTypeInfo;
119+
120+
signature.Append( controllerType.GetTypeDisplayName() );
121+
signature.Append( '.' );
122+
signature.Append( controllerAction.MethodInfo.Name );
123+
signature.Append( '(' );
124+
125+
using ( var parameter = action.Parameters.GetEnumerator() )
126+
{
127+
if ( parameter.MoveNext() )
128+
{
129+
var parameterType = parameter.Current.ParameterType;
130+
131+
signature.Append( parameterType.GetTypeDisplayName( false ) );
132+
133+
while ( parameter.MoveNext() )
134+
{
135+
parameterType = parameter.Current.ParameterType;
136+
signature.Append( ", " );
137+
signature.Append( parameterType.GetTypeDisplayName( false ) );
138+
}
139+
}
140+
}
141+
142+
signature.Append( ") (" );
143+
signature.Append( controllerType.Assembly.GetName().Name );
144+
signature.Append( ')' );
145+
146+
return signature.ToString();
147+
}
104148
}
105149
}

src/Microsoft.AspNetCore.Mvc.Versioning/Microsoft.Extensions.DependencyInjection/AutoRegisterMiddleware.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
{
33
using Microsoft.AspNetCore.Builder;
44
using Microsoft.AspNetCore.Hosting;
5+
using Microsoft.AspNetCore.Mvc;
56
using Microsoft.AspNetCore.Mvc.Routing;
67
using Microsoft.AspNetCore.Mvc.Versioning;
78
using Microsoft.Extensions.Options;
@@ -12,14 +13,20 @@ sealed class AutoRegisterMiddleware : IStartupFilter
1213
{
1314
readonly IApiVersionRoutePolicy routePolicy;
1415
readonly IOptions<ApiVersioningOptions> options;
16+
readonly IOptions<MvcOptions> mvcOptions;
1517

16-
public AutoRegisterMiddleware( IApiVersionRoutePolicy routePolicy, IOptions<ApiVersioningOptions> options )
18+
public AutoRegisterMiddleware(
19+
IApiVersionRoutePolicy routePolicy,
20+
IOptions<ApiVersioningOptions> options,
21+
IOptions<MvcOptions> mvcOptions )
1722
{
1823
Contract.Requires( routePolicy != null );
1924
Contract.Requires( options != null );
25+
Contract.Requires( mvcOptions != null );
2026

2127
this.routePolicy = routePolicy;
2228
this.options = options;
29+
this.mvcOptions = mvcOptions;
2330
}
2431

2532
public Action<IApplicationBuilder> Configure( Action<IApplicationBuilder> next )
@@ -35,7 +42,11 @@ public Action<IApplicationBuilder> Configure( Action<IApplicationBuilder> next )
3542
}
3643

3744
next( app );
38-
app.UseRouter( builder => builder.Routes.Add( new CatchAllRouteHandler( routePolicy ) ) );
45+
46+
if ( !mvcOptions.Value.EnableEndpointRouting )
47+
{
48+
app.UseRouter( builder => builder.Routes.Add( new CatchAllRouteHandler( routePolicy ) ) );
49+
}
3950
};
4051
}
4152
}

src/Microsoft.AspNetCore.Mvc.Versioning/Microsoft.Extensions.DependencyInjection/IServiceCollectionExtensions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,15 @@ static void AddApiVersioningServices( IServiceCollection services )
6060
services.Add( Singleton( sp => sp.GetRequiredService<IOptions<ApiVersioningOptions>>().Value.ErrorResponses ) );
6161
services.Replace( Singleton<IActionSelector, ApiVersionActionSelector>() );
6262
services.TryAddSingleton<IApiVersionRoutePolicy, DefaultApiVersionRoutePolicy>();
63+
services.TryAddSingleton<IApiControllerFilter, DefaultApiControllerFilter>();
6364
services.TryAddSingleton<ReportApiVersionsAttribute>();
6465
services.TryAddSingleton( OnRequestIReportApiVersions );
6566
services.TryAddEnumerable( Transient<IPostConfigureOptions<MvcOptions>, ApiVersioningMvcOptionsSetup>() );
6667
services.TryAddEnumerable( Transient<IPostConfigureOptions<RouteOptions>, ApiVersioningRouteOptionsSetup>() );
6768
services.TryAddEnumerable( Transient<IApplicationModelProvider, ApiVersioningApplicationModelProvider>() );
6869
services.TryAddEnumerable( Transient<IActionDescriptorProvider, ApiVersionCollator>() );
70+
services.TryAddEnumerable( Transient<IApiControllerSpecification, ApiBehaviorSpecification>() );
71+
services.TryAddEnumerable( Singleton<MatcherPolicy, ApiVersionMatcherPolicy>() );
6972
services.AddTransient<IStartupFilter, AutoRegisterMiddleware>();
7073
}
7174

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
namespace Microsoft.AspNetCore.Mvc.Routing
2+
{
3+
using Microsoft.AspNetCore.Http;
4+
using Microsoft.AspNetCore.Mvc.Abstractions;
5+
using Microsoft.AspNetCore.Mvc.Versioning;
6+
using Microsoft.AspNetCore.Routing;
7+
using Microsoft.AspNetCore.Routing.Matching;
8+
using Microsoft.Extensions.Logging;
9+
using Microsoft.Extensions.Options;
10+
using System;
11+
using System.Collections.Generic;
12+
using System.Diagnostics.Contracts;
13+
using System.Linq;
14+
using System.Threading.Tasks;
15+
using static Microsoft.AspNetCore.Mvc.Versioning.ApiVersionMapping;
16+
using static Microsoft.AspNetCore.Mvc.Versioning.ErrorCodes;
17+
using static System.Threading.Tasks.Task;
18+
19+
/// <summary>
20+
/// Represents the <see cref="IEndpointSelectorPolicy">endpoint selector policy</see> for API versions.
21+
/// </summary>
22+
[CLSCompliant( false )]
23+
public sealed class ApiVersionMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy
24+
{
25+
readonly IOptions<ApiVersioningOptions> options;
26+
27+
/// <summary>
28+
/// Initializes a new instance of the <see cref="ApiVersionMatcherPolicy"/> class.
29+
/// </summary>
30+
/// <param name="options">The <see cref="ApiVersioningOptions">options</see> associated with the action selector.</param>
31+
/// <param name="reportApiVersions">The <see cref="IReportApiVersions">object</see> used to report API versions.</param>
32+
/// <param name="loggerFactory">The <see cref="ILoggerFactory">factory</see> used to create <see cref="ILogger">loggers</see>.</param>
33+
public ApiVersionMatcherPolicy(
34+
IOptions<ApiVersioningOptions> options,
35+
IReportApiVersions reportApiVersions,
36+
ILoggerFactory loggerFactory )
37+
{
38+
Arg.NotNull( options, nameof( options ) );
39+
Arg.NotNull( reportApiVersions, nameof( reportApiVersions ) );
40+
Arg.NotNull( loggerFactory, nameof( loggerFactory ) );
41+
42+
this.options = options;
43+
ApiVersionReporter = reportApiVersions;
44+
Logger = loggerFactory.CreateLogger( GetType() );
45+
}
46+
47+
/// <inheritdoc />
48+
public override int Order => 0;
49+
50+
ApiVersioningOptions Options => options.Value;
51+
52+
IApiVersionSelector ApiVersionSelector => Options.ApiVersionSelector;
53+
54+
IReportApiVersions ApiVersionReporter { get; }
55+
56+
ILogger Logger { get; }
57+
58+
/// <inheritdoc />
59+
public bool AppliesToEndpoints( IReadOnlyList<Endpoint> endpoints )
60+
{
61+
Arg.NotNull( endpoints, nameof( endpoints ) );
62+
63+
for ( var i = 0; i < endpoints.Count; i++ )
64+
{
65+
var action = endpoints[i].Metadata?.GetMetadata<ActionDescriptor>();
66+
67+
if ( action?.GetProperty<ApiVersionModel>() != null )
68+
{
69+
return true;
70+
}
71+
}
72+
73+
return false;
74+
}
75+
76+
/// <inheritdoc />
77+
public Task ApplyAsync( HttpContext httpContext, EndpointSelectorContext context, CandidateSet candidates )
78+
{
79+
Arg.NotNull( httpContext, nameof( httpContext ) );
80+
Arg.NotNull( context, nameof( context ) );
81+
Arg.NotNull( candidates, nameof( candidates ) );
82+
83+
if ( IsRequestedApiVersionAmbiguous( httpContext, context, out var apiVersion ) )
84+
{
85+
return CompletedTask;
86+
}
87+
88+
if ( apiVersion == null && Options.AssumeDefaultVersionWhenUnspecified )
89+
{
90+
apiVersion = TrySelectApiVersion( httpContext, candidates );
91+
httpContext.Features.Get<IApiVersioningFeature>().RequestedApiVersion = apiVersion;
92+
}
93+
94+
var finalMatches = EvaluateApiVersion( httpContext, candidates, apiVersion );
95+
96+
if ( finalMatches.Count == 0 )
97+
{
98+
context.Endpoint = ClientError( httpContext, candidates );
99+
}
100+
else
101+
{
102+
for ( var i = 0; i < finalMatches.Count; i++ )
103+
{
104+
candidates.SetValidity( finalMatches[i].index, true );
105+
}
106+
}
107+
108+
return CompletedTask;
109+
}
110+
111+
static IReadOnlyList<(int index, ActionDescriptor action)> EvaluateApiVersion(
112+
HttpContext httpContext,
113+
CandidateSet candidates,
114+
ApiVersion apiVersion )
115+
{
116+
Contract.Requires( httpContext != null );
117+
Contract.Requires( candidates != null );
118+
Contract.Ensures( Contract.Result<IReadOnlyList<(int index, ActionDescriptor action)>>() != null );
119+
120+
var bestMatches = new List<(int index, ActionDescriptor action)>();
121+
var implicitMatches = new List<(int, ActionDescriptor)>();
122+
123+
for ( var i = 0; i < candidates.Count; i++ )
124+
{
125+
if ( !candidates.IsValidCandidate( i ) )
126+
{
127+
continue;
128+
}
129+
130+
ref var candidate = ref candidates[i];
131+
var action = candidate.Endpoint.Metadata?.GetMetadata<ActionDescriptor>();
132+
133+
if ( action == null )
134+
{
135+
candidates.SetValidity( i, false );
136+
continue;
137+
}
138+
139+
switch ( action.MappingTo( apiVersion ) )
140+
{
141+
case Explicit:
142+
bestMatches.Add( (i, action) );
143+
break;
144+
case Implicit:
145+
implicitMatches.Add( (i, action) );
146+
break;
147+
}
148+
149+
// perf: always make the candidate invalid so we only need to loop through the
150+
// final, best matches for any remaining, valid candidates
151+
candidates.SetValidity( i, false );
152+
}
153+
154+
switch ( bestMatches.Count )
155+
{
156+
case 0:
157+
bestMatches.AddRange( implicitMatches );
158+
break;
159+
case 1:
160+
var model = bestMatches[0].action.GetApiVersionModel();
161+
162+
if ( model.IsApiVersionNeutral )
163+
{
164+
bestMatches.AddRange( implicitMatches );
165+
}
166+
167+
break;
168+
}
169+
170+
return bestMatches.ToArray();
171+
}
172+
173+
bool IsRequestedApiVersionAmbiguous( HttpContext httpContext, EndpointSelectorContext context, out ApiVersion apiVersion )
174+
{
175+
Contract.Requires( httpContext != null );
176+
Contract.Requires( context != null );
177+
178+
try
179+
{
180+
apiVersion = httpContext.GetRequestedApiVersion();
181+
}
182+
catch ( AmbiguousApiVersionException ex )
183+
{
184+
Logger.LogInformation( ex.Message );
185+
apiVersion = default;
186+
187+
var handlerContext = new RequestHandlerContext( Options.ErrorResponses )
188+
{
189+
Code = AmbiguousApiVersion,
190+
Message = ex.Message,
191+
};
192+
193+
context.Endpoint = new BadRequestHandler( handlerContext );
194+
return true;
195+
}
196+
197+
return false;
198+
}
199+
200+
ApiVersion TrySelectApiVersion( HttpContext httpContext, CandidateSet candidates )
201+
{
202+
Contract.Requires( httpContext != null );
203+
Contract.Requires( candidates != null );
204+
205+
var models = new List<ApiVersionModel>();
206+
207+
for ( var i = 0; i < candidates.Count; i++ )
208+
{
209+
ref var candidate = ref candidates[i];
210+
var model = candidate.Endpoint.Metadata?.GetMetadata<ActionDescriptor>()?.GetApiVersionModel();
211+
212+
if ( model != null )
213+
{
214+
models.Add( model );
215+
}
216+
}
217+
218+
return ApiVersionSelector.SelectVersion( httpContext.Request, models.Aggregate() );
219+
}
220+
221+
RequestHandler ClientError( HttpContext httpContext, CandidateSet candidateSet )
222+
{
223+
var candidates = new List<ActionDescriptor>( candidateSet.Count );
224+
225+
for ( var i = 0; i < candidateSet.Count; i++ )
226+
{
227+
ref var candidate = ref candidateSet[i];
228+
var action = candidate.Endpoint.Metadata?.GetMetadata<ActionDescriptor>();
229+
230+
if ( action != null )
231+
{
232+
candidates.Add( action );
233+
}
234+
}
235+
236+
var builder = new ClientErrorBuilder()
237+
{
238+
Options = Options,
239+
ApiVersionReporter = ApiVersionReporter,
240+
HttpContext = httpContext,
241+
Candidates = candidates,
242+
Logger = Logger,
243+
};
244+
245+
return builder.Build();
246+
}
247+
}
248+
}

0 commit comments

Comments
 (0)