Skip to content

Commit ae2b270

Browse files
Chris Martinezcommonsensesoftware
Chris Martinez
authored andcommitted
Refactor API versioning properties to a feature. Closes #280.
1 parent 93b6049 commit ae2b270

14 files changed

+271
-82
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
namespace Microsoft.AspNetCore.Builder
2+
{
3+
using Microsoft.AspNetCore.Mvc.Versioning;
4+
using System;
5+
6+
/// <summary>
7+
/// Provides extension methods for the <see cref="IApplicationBuilder"/> interface.
8+
/// </summary>
9+
[CLSCompliant( false )]
10+
public static class IApplicationBuilderExtensions
11+
{
12+
/// <summary>
13+
/// Uses the API versioning middleware.
14+
/// </summary>
15+
/// <param name="app">The <see cref="IApplicationBuilder">application</see> to configure.</param>
16+
/// <returns>The original <see cref="IApplicationBuilder"/>.</returns>
17+
/// <remarks>
18+
/// <para>
19+
/// Configuration of the API versioning middleware is not necessary unless you explicitly
20+
/// want to control when the middleware is added in the pipline. API versioning automatically configures
21+
/// the required middleware, which is usually early enough for dependent middleware.
22+
/// </para>
23+
/// <para>
24+
/// In addition to invoking <see cref="UseApiVersioning(IApplicationBuilder)"/>, you must also set
25+
/// <see cref="ApiVersioningOptions.RegisterMiddleware"/> to <c>false</c>.
26+
/// </para>
27+
/// </remarks>
28+
public static IApplicationBuilder UseApiVersioning( this IApplicationBuilder app )
29+
{
30+
Arg.NotNull( app, nameof( app ) );
31+
return app.UseMiddleware<ApiVersioningMiddleware>();
32+
}
33+
}
34+
}

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ public static ApiVersionRequestProperties ApiVersionProperties( this HttpContext
4343
public static ApiVersion GetRequestedApiVersion( this HttpContext context )
4444
{
4545
Arg.NotNull( context, nameof( context ) );
46-
return context.ApiVersionProperties().ApiVersion;
46+
47+
var feature = context.Features.Get<IApiVersioningFeature>();
48+
return feature?.RequestedApiVersion;
4749
}
4850
}
4951
}

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

+12-7
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,16 @@ public static IServiceCollection AddApiVersioning( this IServiceCollection servi
4646
services.Add( new ServiceDescriptor( typeof( IErrorResponseProvider ), options.ErrorResponses ) );
4747
services.Add( Singleton<IOptions<ApiVersioningOptions>>( new OptionsWrapper<ApiVersioningOptions>( options ) ) );
4848
services.Replace( Singleton<IActionSelector, ApiVersionActionSelector>() );
49-
services.TryAddSingleton<IApiVersionRoutePolicy, DefaultApiVersionRoutePolicy>();
5049
services.TryAddSingleton<ReportApiVersionsAttribute>();
51-
services.AddTransient<IStartupFilter, InjectApiVersionRoutePolicy>();
50+
services.AddTransient<IStartupFilter, AutoRegisterMiddleware>();
5251
services.AddMvcCore( mvcOptions => AddMvcOptions( mvcOptions, options ) );
5352
services.AddRouting( routeOptions => routeOptions.ConstraintMap.Add( options.RouteConstraintName, typeof( ApiVersionRouteConstraint ) ) );
5453

54+
if ( options.RegisterMiddleware )
55+
{
56+
services.TryAddSingleton<IApiVersionRoutePolicy, DefaultApiVersionRoutePolicy>();
57+
}
58+
5559
if ( options.ReportApiVersions )
5660
{
5761
services.TryAddSingleton<IReportApiVersions, DefaultApiVersionReporter>();
@@ -78,20 +82,21 @@ static void AddMvcOptions( MvcOptions mvcOptions, ApiVersioningOptions options )
7882
mvcOptions.Conventions.Add( new ApiVersionConvention( options.DefaultApiVersion, options.Conventions ) );
7983
}
8084

81-
sealed class InjectApiVersionRoutePolicy : IStartupFilter
85+
sealed class AutoRegisterMiddleware : IStartupFilter
8286
{
8387
readonly IApiVersionRoutePolicy routePolicy;
8488

85-
public InjectApiVersionRoutePolicy( IApiVersionRoutePolicy routePolicy ) => this.routePolicy = routePolicy;
89+
public AutoRegisterMiddleware( IApiVersionRoutePolicy routePolicy ) => this.routePolicy = routePolicy;
8690

87-
public Action<IApplicationBuilder> Configure( Action<IApplicationBuilder> next )
91+
public Action<IApplicationBuilder> Configure( Action<IApplicationBuilder> configure )
8892
{
89-
Contract.Requires( next != null );
93+
Contract.Requires( configure != null );
9094
Contract.Ensures( Contract.Result<Action<IApplicationBuilder>>() != null );
9195

9296
return app =>
9397
{
94-
next( app );
98+
app.UseApiVersioning();
99+
configure( app );
95100
app.UseRouter( builder => builder.Routes.Add( routePolicy ) );
96101
};
97102
}

src/Microsoft.AspNetCore.Mvc.Versioning/Routing/ApiVersionRouteConstraint.cs

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
namespace Microsoft.AspNetCore.Mvc.Routing
22
{
3-
using AspNetCore.Routing;
4-
using Http;
3+
using Microsoft.AspNetCore.Http;
4+
using Microsoft.AspNetCore.Mvc.Versioning;
5+
using Microsoft.AspNetCore.Routing;
56
using System;
67
using static ApiVersion;
78
using static AspNetCore.Routing.RouteDirection;
@@ -29,11 +30,11 @@ public bool Match( HttpContext httpContext, IRouter route, string routeKey, Rout
2930
return false;
3031
}
3132

32-
var properties = httpContext.ApiVersionProperties();
33+
var feature = httpContext.Features.Get<IApiVersioningFeature>();
3334

3435
if ( values.TryGetValue( routeKey, out string value ) )
3536
{
36-
properties.RawApiVersion = value;
37+
feature.RawRequestedApiVersion = value;
3738
}
3839
else
3940
{
@@ -47,7 +48,7 @@ public bool Match( HttpContext httpContext, IRouter route, string routeKey, Rout
4748

4849
if ( TryParse( value, out var requestedVersion ) )
4950
{
50-
properties.ApiVersion = requestedVersion;
51+
feature.RequestedApiVersion = requestedVersion;
5152
return true;
5253
}
5354

src/Microsoft.AspNetCore.Mvc.Versioning/Routing/DefaultApiVersionRoutePolicy.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -258,11 +258,11 @@ RequestHandler ClientError( HttpContext httpContext, ActionSelectionResult selec
258258
return NotFound;
259259
}
260260

261-
var properties = httpContext.ApiVersionProperties();
261+
var feature = httpContext.Features.Get<IApiVersioningFeature>();
262262
var method = httpContext.Request.Method;
263263
var requestUrl = new Lazy<string>( httpContext.Request.GetDisplayUrl );
264-
var requestedVersion = properties.RawApiVersion;
265-
var parsedVersion = properties.ApiVersion;
264+
var requestedVersion = feature.RawRequestedApiVersion;
265+
var parsedVersion = feature.RequestedApiVersion;
266266
var actionNames = new Lazy<string>( () => Join( NewLine, candidates.Select( a => a.DisplayName ) ) );
267267
var allowedMethods = new Lazy<HashSet<string>>( () => AllowedMethodsFromCandidates( candidates ) );
268268
var apiVersions = new Lazy<ApiVersionModel>( selectionResult.CandidateActions.SelectMany( l => l.Value.Select( a => a.GetProperty<ApiVersionModel>() ).Where( m => m != null ) ).Aggregate );

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -152,10 +152,11 @@ public virtual ActionDescriptor SelectBestCandidate( RouteContext context, IRead
152152

153153
var selectionContext = new ActionSelectionContext( httpContext, matches, apiVersion );
154154
var finalMatches = SelectBestActions( selectionContext );
155+
var feature = httpContext.Features.Get<IApiVersioningFeature>();
155156
var properties = httpContext.ApiVersionProperties();
156157
var selectionResult = properties.SelectionResult;
157158

158-
properties.ApiVersion = selectionContext.RequestedVersion;
159+
feature.RequestedApiVersion = selectionContext.RequestedVersion;
159160
selectionResult.AddCandidates( candidates );
160161

161162
if ( finalMatches != null )
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
namespace Microsoft.AspNetCore.Mvc.Versioning
2+
{
3+
using Microsoft.AspNetCore.Http;
4+
using Microsoft.Extensions.DependencyInjection;
5+
using System;
6+
7+
/// <summary>
8+
/// Represents the API versioning feature.
9+
/// </summary>
10+
public sealed class ApiVersioningFeature : IApiVersioningFeature
11+
{
12+
readonly HttpContext context;
13+
string rawApiVersion;
14+
ApiVersion apiVersion;
15+
16+
/// <summary>
17+
/// Initializes a new instance of the <see cref="ApiVersioningFeature"/> class.
18+
/// </summary>
19+
/// <param name="context">The current <see cref="HttpContext">HTTP context</see>.</param>
20+
[CLSCompliant( false )]
21+
public ApiVersioningFeature( HttpContext context )
22+
{
23+
Arg.NotNull( context, nameof( context ) );
24+
this.context = context;
25+
}
26+
27+
/// <summary>
28+
/// Gets or sets the raw, unparsed API version for the current request.
29+
/// </summary>
30+
/// <value>The unparsed API version value for the current request.</value>
31+
public string RawRequestedApiVersion
32+
{
33+
get
34+
{
35+
if ( rawApiVersion == null )
36+
{
37+
var reader = context.RequestServices.GetService<IApiVersionReader>() ?? new QueryStringApiVersionReader();
38+
rawApiVersion = reader.Read( context.Request );
39+
}
40+
41+
return rawApiVersion;
42+
}
43+
set => rawApiVersion = value;
44+
}
45+
46+
/// <summary>
47+
/// Gets or sets the API version for the current request.
48+
/// </summary>
49+
/// <value>The current <see cref="ApiVersion">API version</see> for the current request.</value>
50+
/// <remarks>If an API version was not provided for the current request or the value
51+
/// provided is invalid, this property will return <c>null</c>.</remarks>
52+
public ApiVersion RequestedApiVersion
53+
{
54+
get
55+
{
56+
if ( apiVersion == null )
57+
{
58+
#pragma warning disable CA1806 // Do not ignore method results
59+
ApiVersion.TryParse( RawRequestedApiVersion, out apiVersion );
60+
#pragma warning restore CA1806
61+
}
62+
63+
return apiVersion;
64+
}
65+
set => apiVersion = value;
66+
}
67+
}
68+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
namespace Microsoft.AspNetCore.Mvc.Versioning
2+
{
3+
using Microsoft.AspNetCore.Http;
4+
using System.Threading.Tasks;
5+
6+
sealed class ApiVersioningMiddleware
7+
{
8+
readonly RequestDelegate next;
9+
10+
public ApiVersioningMiddleware( RequestDelegate next ) => this.next = next;
11+
12+
public Task InvokeAsync( HttpContext context )
13+
{
14+
var feature = context.Features.Get<IApiVersioningFeature>();
15+
16+
if ( feature == null )
17+
{
18+
feature = new ApiVersioningFeature( context );
19+
context.Features.Set( feature );
20+
}
21+
22+
return next( context );
23+
}
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
namespace Microsoft.AspNetCore.Mvc.Versioning
2+
{
3+
using Microsoft.Extensions.DependencyInjection;
4+
5+
/// <content>
6+
/// Provides additional implementation specific to ASP.NET Core.
7+
/// </content>
8+
public partial class ApiVersioningOptions
9+
{
10+
/// <summary>
11+
/// Gets or sets a value indicating whether the API versioning middleware
12+
/// should automatically be registered.
13+
/// </summary>
14+
/// <value>True if the API versioning middleware should be automatically
15+
/// registered; otherwise, false. The default value is <c>true</c>.</value>
16+
/// <remarks>If this property is set to false, then
17+
/// <see cref="IApplicationBuilderExtensions.UseApiVersioning(Builder.IApplicationBuilder)"/>
18+
/// must be called.</remarks>
19+
public bool RegisterMiddleware { get; set; } = true;
20+
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
namespace Microsoft.AspNetCore.Mvc.Versioning
2+
{
3+
using System;
4+
5+
/// <summary>
6+
/// Defines the behavior of the API versioning feature.
7+
/// </summary>
8+
public interface IApiVersioningFeature
9+
{
10+
/// <summary>
11+
/// Gets or sets the raw, unparsed API version for the current request.
12+
/// </summary>
13+
/// <value>The unparsed API version value for the current request.</value>
14+
string RawRequestedApiVersion { get; set; }
15+
16+
/// <summary>
17+
/// Gets or sets the API version for the current request.
18+
/// </summary>
19+
/// <value>The current <see cref="ApiVersion">API version</see> for the current request.</value>
20+
/// <remarks>If an API version was not provided for the current request or the value
21+
/// provided is invalid, this property will return <c>null</c>.</remarks>
22+
ApiVersion RequestedApiVersion { get; set; }
23+
}
24+
}

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ public virtual string Read( HttpRequest request )
2525
}
2626

2727
reentrant = true;
28-
var value = request.HttpContext.ApiVersionProperties().RawApiVersion;
28+
var feature = request.HttpContext.Features.Get<IApiVersioningFeature>();
29+
var value = feature.RawRequestedApiVersion;
2930
reentrant = false;
3031

3132
return value;

0 commit comments

Comments
 (0)