Skip to content

Commit 9914482

Browse files
Chris Martinezcommonsensesoftware
Chris Martinez
authored andcommitted
Support model binding for API version. Closes #320.
1 parent b841a10 commit 9914482

File tree

7 files changed

+234
-3
lines changed

7 files changed

+234
-3
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
namespace Microsoft.Web.Http.Controllers
2+
{
3+
using System.Diagnostics.Contracts;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using System.Web.Http;
7+
using System.Web.Http.Controllers;
8+
using System.Web.Http.Metadata;
9+
10+
/// <summary>
11+
/// Represents the binding for an <see cref="ApiVersion">API version</see> parameter.
12+
/// </summary>
13+
public class ApiVersionParameterBinding : HttpParameterBinding
14+
{
15+
static readonly Task CompletedTask = Task.FromResult( default( object ) );
16+
17+
/// <summary>
18+
/// Initializes a new instance of the <see cref="ApiVersionParameterBinding"/> class.
19+
/// </summary>
20+
/// <param name="descriptor">The <see cref="HttpParameterDescriptor">parameter descriptor</see> associated with the binding.</param>
21+
public ApiVersionParameterBinding( HttpParameterDescriptor descriptor ) : base( descriptor ) { }
22+
23+
/// <inheritdoc />
24+
public override Task ExecuteBindingAsync( ModelMetadataProvider metadataProvider, HttpActionContext actionContext, CancellationToken cancellationToken )
25+
{
26+
Contract.Assert( metadataProvider != null );
27+
Contract.Assert( actionContext != null );
28+
29+
var value = actionContext.Request.ApiVersionProperties().RequestedApiVersion;
30+
SetValue( actionContext, value );
31+
return CompletedTask;
32+
}
33+
34+
/// <summary>
35+
/// Creates and returns a new parameter binding for the specified descriptor.
36+
/// </summary>
37+
/// <param name="descriptor">The <see cref="HttpParameterDescriptor">parameter descriptor</see> to create a binding for.</param>
38+
/// <returns>A new <see cref="HttpParameterBinding">parameter binding</see>.</returns>
39+
public static HttpParameterBinding Create( HttpParameterDescriptor descriptor ) => new ApiVersionParameterBinding( descriptor );
40+
}
41+
}

src/Microsoft.AspNet.WebApi.Versioning/System.Web.Http/HttpConfigurationExtensions.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
namespace System.Web.Http
22
{
3-
using System.Diagnostics.Contracts;
4-
using System.Web.Http.Controllers;
5-
using System.Web.Http.Dispatcher;
63
using Microsoft;
74
using Microsoft.Web.Http;
85
using Microsoft.Web.Http.Controllers;
96
using Microsoft.Web.Http.Dispatcher;
107
using Microsoft.Web.Http.Versioning;
8+
using System.Diagnostics.Contracts;
9+
using System.Web.Http.Controllers;
10+
using System.Web.Http.Dispatcher;
1111

1212
/// <summary>
1313
/// Provides extension methods for the <see cref="HttpConfiguration"/> class.
@@ -58,6 +58,7 @@ public static void AddApiVersioning( this HttpConfiguration configuration, Actio
5858
}
5959

6060
configuration.Properties.AddOrUpdate( ApiVersioningOptionsKey, options, ( key, oldValue ) => options );
61+
configuration.ParameterBindingRules.Add( typeof( ApiVersion ), ApiVersionParameterBinding.Create );
6162
}
6263
}
6364
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
namespace Microsoft.AspNetCore.Mvc.Versioning
2+
{
3+
using Microsoft.AspNetCore.Mvc.ModelBinding;
4+
using System;
5+
using System.Diagnostics.Contracts;
6+
using System.Threading.Tasks;
7+
using static System.Threading.Tasks.Task;
8+
9+
/// <summary>
10+
/// Represents a model binder for an <see cref="ApiVersion">API version</see>.
11+
/// </summary>
12+
[CLSCompliant( false )]
13+
public class ApiVersionModelBinder : IModelBinder
14+
{
15+
/// <inheritdoc />
16+
public virtual Task BindModelAsync( ModelBindingContext bindingContext )
17+
{
18+
Contract.Assert( bindingContext != null );
19+
20+
var feature = bindingContext.HttpContext.Features.Get<IApiVersioningFeature>();
21+
var model = feature.RequestedApiVersion;
22+
23+
bindingContext.Result = ModelBindingResult.Success( model );
24+
25+
return CompletedTask;
26+
}
27+
}
28+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
namespace Microsoft.AspNetCore.Mvc.Versioning
2+
{
3+
using Microsoft.AspNetCore.Mvc.ModelBinding;
4+
using System;
5+
using System.Diagnostics.Contracts;
6+
7+
sealed class ApiVersionModelBinderProvider : IModelBinderProvider
8+
{
9+
static readonly Type ApiVersionType = typeof( ApiVersion );
10+
static readonly ApiVersionModelBinder binder = new ApiVersionModelBinder();
11+
12+
public IModelBinder GetBinder( ModelBinderProviderContext context )
13+
{
14+
Contract.Assert( context != null );
15+
16+
if ( context.Metadata.ModelType.IsAssignableFrom( ApiVersionType ) )
17+
{
18+
return binder;
19+
}
20+
21+
return default;
22+
}
23+
}
24+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ public virtual void PostConfigure( string name, MvcOptions options )
2626
{
2727
options.Filters.AddService<ReportApiVersionsAttribute>();
2828
}
29+
30+
options.ModelBinderProviders.Insert( 0, new ApiVersionModelBinderProvider() );
2931
}
3032
}
3133
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
namespace Microsoft.Web.Http.Controllers
2+
{
3+
using FluentAssertions;
4+
using Moq;
5+
using System.Net.Http;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using System.Web.Http;
9+
using System.Web.Http.Controllers;
10+
using System.Web.Http.Metadata;
11+
using Xunit;
12+
13+
public class ApiVersionParameterBindingTest
14+
{
15+
[Fact]
16+
public async Task execute_async_should_bind_parameter_value()
17+
{
18+
// arrange
19+
var apiVersion = new ApiVersion( 42, 0 );
20+
var metadataProvider = Mock.Of<ModelMetadataProvider>();
21+
var actionContext = NewActionContext( apiVersion );
22+
var parameter = NewParameter( nameof( apiVersion ) );
23+
var binding = new ApiVersionParameterBinding( parameter );
24+
25+
// act
26+
await binding.ExecuteBindingAsync( metadataProvider, actionContext, CancellationToken.None );
27+
28+
// assert
29+
actionContext.ActionArguments[nameof( apiVersion )].Should().Be( apiVersion );
30+
}
31+
32+
[Fact]
33+
public async Task execute_async_should_bind_null_parameter_value()
34+
{
35+
// arrange
36+
var metadataProvider = Mock.Of<ModelMetadataProvider>();
37+
var actionContext = NewActionContext( default );
38+
var parameter = NewParameter( "requestedApiVersion" );
39+
var binding = new ApiVersionParameterBinding( parameter );
40+
41+
// act
42+
await binding.ExecuteBindingAsync( metadataProvider, actionContext, CancellationToken.None );
43+
44+
// assert
45+
actionContext.ActionArguments["requestedApiVersion"].Should().BeNull();
46+
}
47+
48+
static HttpActionContext NewActionContext( ApiVersion apiVersion )
49+
{
50+
var configuration = new HttpConfiguration();
51+
var request = new HttpRequestMessage();
52+
var controllerContext = new HttpControllerContext() { Configuration = configuration, Request = request };
53+
var actionContext = new HttpActionContext() { ControllerContext = controllerContext };
54+
55+
request.SetConfiguration( configuration );
56+
request.ApiVersionProperties().RequestedApiVersion = apiVersion;
57+
58+
return actionContext;
59+
}
60+
61+
static HttpParameterDescriptor NewParameter( string name )
62+
{
63+
var parameter = new Mock<HttpParameterDescriptor>();
64+
parameter.Setup( p => p.ParameterName ).Returns( name );
65+
return parameter.Object;
66+
}
67+
}
68+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
namespace Microsoft.AspNetCore.Mvc.Versioning
2+
{
3+
using FluentAssertions;
4+
using Microsoft.AspNetCore.Http;
5+
using Microsoft.AspNetCore.Http.Features;
6+
using Microsoft.AspNetCore.Mvc.ModelBinding;
7+
using Moq;
8+
using System.Threading.Tasks;
9+
using Xunit;
10+
11+
public class ApiVersionModelBinderTest
12+
{
13+
[Fact]
14+
public async Task bind_model_should_set_api_version_as_result()
15+
{
16+
// arrange
17+
var apiVersion = new ApiVersion( 42, 0 );
18+
var bindingContext = NewModelBindingContext( apiVersion );
19+
var binder = new ApiVersionModelBinder();
20+
21+
// act
22+
await binder.BindModelAsync( bindingContext );
23+
24+
// assert
25+
bindingContext.Result.Model.Should().Be( apiVersion );
26+
}
27+
28+
[Fact]
29+
public async Task bind_model_should_set_null_api_version_as_result()
30+
{
31+
// arrange
32+
var bindingContext = NewModelBindingContext( default );
33+
var binder = new ApiVersionModelBinder();
34+
35+
// act
36+
await binder.BindModelAsync( bindingContext );
37+
38+
// assert
39+
bindingContext.Result.IsModelSet.Should().BeTrue();
40+
bindingContext.Result.Model.Should().BeNull();
41+
}
42+
43+
static HttpContext NewHttpContext( ApiVersion apiVersion )
44+
{
45+
var feature = new Mock<IApiVersioningFeature>();
46+
var featureCollection = new Mock<IFeatureCollection>();
47+
var httpContext = new Mock<HttpContext>();
48+
49+
feature.SetupProperty( f => f.RequestedApiVersion, apiVersion );
50+
featureCollection.Setup( fc => fc.Get<IApiVersioningFeature>() ).Returns( feature.Object );
51+
httpContext.SetupGet( hc => hc.Features ).Returns( featureCollection.Object );
52+
53+
return httpContext.Object;
54+
}
55+
56+
static ModelBindingContext NewModelBindingContext( ApiVersion apiVersion )
57+
{
58+
var httpContext = NewHttpContext( apiVersion );
59+
var bindingContext = new Mock<ModelBindingContext>();
60+
61+
bindingContext.SetupGet( bc => bc.HttpContext ).Returns( httpContext );
62+
bindingContext.SetupProperty( bc => bc.Result );
63+
64+
return bindingContext.Object;
65+
}
66+
}
67+
}

0 commit comments

Comments
 (0)