Skip to content

Commit 2f07397

Browse files
Chris Martinezcommonsensesoftware
authored andcommitted
Add enhanced support filtering API controllers
1 parent 75305e8 commit 2f07397

File tree

12 files changed

+336
-17
lines changed

12 files changed

+336
-17
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
2+
{
3+
using System;
4+
using System.Linq;
5+
using System.Reflection;
6+
7+
/// <summary>
8+
/// Represents a specification that matches API controllers by the presence of API behaviors.
9+
/// </summary>
10+
[CLSCompliant( false )]
11+
public sealed class ApiBehaviorSpecification : IApiControllerSpecification
12+
{
13+
/// <summary>
14+
/// Initializes a new instance of the <see cref="ApiBehaviorSpecification"/> class.
15+
/// </summary>
16+
public ApiBehaviorSpecification()
17+
{
18+
const string ApiBehaviorApplicationModelProviderTypeName = "Microsoft.AspNetCore.Mvc.ApplicationModels.ApiBehaviorApplicationModelProvider, Microsoft.AspNetCore.Mvc.Core";
19+
var type = Type.GetType( ApiBehaviorApplicationModelProviderTypeName, throwOnError: true );
20+
var method = type.GetRuntimeMethods().Single( m => m.Name == "IsApiController" );
21+
22+
IsApiController = (Func<ControllerModel, bool>) method.CreateDelegate( typeof( Func<ControllerModel, bool> ) );
23+
}
24+
25+
Func<ControllerModel, bool> IsApiController { get; }
26+
27+
/// <inheritdoc />
28+
public bool IsSatisfiedBy( ControllerModel controller )
29+
{
30+
Arg.NotNull( controller, nameof( controller ) );
31+
return IsApiController( controller );
32+
}
33+
}
34+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
2+
{
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Diagnostics.Contracts;
6+
using System.Linq;
7+
8+
/// <summary>
9+
/// Represents the default API controller filter.
10+
/// </summary>
11+
[CLSCompliant( false )]
12+
public sealed class DefaultApiControllerFilter : IApiControllerFilter
13+
{
14+
readonly IReadOnlyList<IApiControllerSpecification> specifications;
15+
16+
/// <summary>
17+
/// Initializes a new instance of the <see cref="DefaultApiControllerFilter"/> class.
18+
/// </summary>
19+
/// <param name="specifications">The <see cref="IEnumerable{T}">sequence</see> of
20+
/// <see cref="IApiControllerSpecification">specifications</see> used by the filter
21+
/// to identify API controllers.</param>
22+
public DefaultApiControllerFilter( IEnumerable<IApiControllerSpecification> specifications )
23+
{
24+
Arg.NotNull( specifications, nameof( specifications ) );
25+
this.specifications = specifications.ToArray();
26+
}
27+
28+
/// <inheritdoc />
29+
public IList<ControllerModel> Apply( IList<ControllerModel> controllers )
30+
{
31+
Arg.NotNull( controllers, nameof( controllers ) );
32+
Contract.Ensures( Contract.Result<IList<ControllerModel>>() != null );
33+
34+
if ( specifications.Count == 0 )
35+
{
36+
return controllers;
37+
}
38+
39+
var filtered = controllers.ToList();
40+
41+
for ( var i = filtered.Count - 1; i >= 0; i-- )
42+
{
43+
if ( !IsApiController( filtered[i] ) )
44+
{
45+
filtered.RemoveAt( i );
46+
}
47+
}
48+
49+
return filtered;
50+
}
51+
52+
bool IsApiController( ControllerModel controller )
53+
{
54+
Contract.Requires( controller != null );
55+
56+
for ( var i = 0; i < specifications.Count; i++ )
57+
{
58+
if ( specifications[i].IsSatisfiedBy( controller ) )
59+
{
60+
return true;
61+
}
62+
}
63+
64+
return false;
65+
}
66+
}
67+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
2+
{
3+
using System;
4+
using System.Collections.Generic;
5+
6+
/// <summary>
7+
/// Defines the behavior of an API controller filter.
8+
/// </summary>
9+
[CLSCompliant( false )]
10+
public interface IApiControllerFilter
11+
{
12+
/// <summary>
13+
/// Applies the filter to the provided list of controllers.
14+
/// </summary>
15+
/// <param name="controllers">The <see cref="IList{T}">list</see> of
16+
/// <see cref="ControllerModel">controllers</see> to filter.</param>
17+
/// <returns>A new, filtered <see cref="IList{T}">list</see> of API
18+
/// <see cref="ControllerModel">controllers</see>.</returns>
19+
IList<ControllerModel> Apply( IList<ControllerModel> controllers );
20+
}
21+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
2+
{
3+
using System;
4+
5+
/// <summary>
6+
/// Defines the behavior of an API controller specification.
7+
/// </summary>
8+
[CLSCompliant( false )]
9+
public interface IApiControllerSpecification
10+
{
11+
/// <summary>
12+
/// Determines whether the specification is satisified by the provided controller model.
13+
/// </summary>
14+
/// <param name="controller">The <see cref="ControllerModel">controller model</see> to evaluate.</param>
15+
/// <returns>True if the <paramref name="controller"/> satisifies the specification; otherwise, false.</returns>
16+
bool IsSatisfiedBy( ControllerModel controller );
17+
}
18+
}

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

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,7 @@
44
using Microsoft.AspNetCore.Mvc.Versioning.Conventions;
55
using Microsoft.Extensions.Options;
66
using System;
7-
using System.Collections.Generic;
87
using System.Diagnostics.Contracts;
9-
using System.Linq;
10-
using System.Reflection;
118

129
/// <summary>
1310
/// Represents an <see cref="IApplicationModelProvider">application model provider</see>, which
@@ -17,16 +14,19 @@
1714
public class ApiVersioningApplicationModelProvider : IApplicationModelProvider
1815
{
1916
readonly IOptions<ApiVersioningOptions> options;
20-
readonly Lazy<Func<ControllerModel, bool>> isApiController = new Lazy<Func<ControllerModel, bool>>( NewIsApiControllerFunc );
2117

2218
/// <summary>
2319
/// Initializes a new instance of the <see cref="ApiVersioningApplicationModelProvider"/> class.
2420
/// </summary>
2521
/// <param name="options">The current <see cref="ApiVersioningOptions">API versioning options</see>.</param>
26-
public ApiVersioningApplicationModelProvider( IOptions<ApiVersioningOptions> options )
22+
/// <param name="controllerFilter">The <see cref="IApiControllerFilter">filter</see> used for API controllers.</param>
23+
public ApiVersioningApplicationModelProvider( IOptions<ApiVersioningOptions> options, IApiControllerFilter controllerFilter )
2724
{
2825
Arg.NotNull( options, nameof( options ) );
26+
Arg.NotNull( controllerFilter, nameof( controllerFilter ) );
27+
2928
this.options = options;
29+
ControllerFilter = controllerFilter;
3030
}
3131

3232
/// <summary>
@@ -35,6 +35,12 @@ public ApiVersioningApplicationModelProvider( IOptions<ApiVersioningOptions> opt
3535
/// <value>The current <see cref="ApiVersioningOptions">API versioning options</see>.</value>
3636
protected ApiVersioningOptions Options => options.Value;
3737

38+
/// <summary>
39+
/// Gets the filter used for API controllers.
40+
/// </summary>
41+
/// <value>The <see cref="IApiControllerFilter"/> used to filter API controllers.</value>
42+
protected IApiControllerFilter ControllerFilter { get; }
43+
3844
/// <inheritdoc />
3945
public int Order { get; protected set; }
4046

@@ -46,11 +52,11 @@ public virtual void OnProvidersExecuted( ApplicationModelProviderContext context
4652
var implicitVersionModel = new ApiVersionModel( Options.DefaultApiVersion );
4753
var conventionBuilder = Options.Conventions;
4854
var application = context.Result;
49-
IEnumerable<ControllerModel> controllers = application.Controllers;
55+
var controllers = application.Controllers;
5056

5157
if ( Options.UseApiBehavior )
5258
{
53-
controllers = controllers.Where( isApiController.Value );
59+
controllers = ControllerFilter.Apply( controllers );
5460
}
5561

5662
foreach ( var controller in controllers )
@@ -106,12 +112,5 @@ static void ApplyAttributeOrImplicitConventions( ControllerModel controller, Api
106112
ApplyImplicitConventions( controller, implicitVersionModel );
107113
}
108114
}
109-
110-
static Func<ControllerModel, bool> NewIsApiControllerFunc()
111-
{
112-
var type = Type.GetType( "Microsoft.AspNetCore.Mvc.ApplicationModels.ApiBehaviorApplicationModelProvider, Microsoft.AspNetCore.Mvc.Core", throwOnError: true );
113-
var method = type.GetRuntimeMethods().Single( m => m.Name == "IsApiController" );
114-
return (Func<ControllerModel, bool>) method.CreateDelegate( typeof( Func<ControllerModel, bool> ) );
115-
}
116115
}
117116
}

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
namespace Microsoft.AspNetCore.Mvc.Versioning
22
{
33
using Microsoft.AspNetCore.Builder;
4+
using Microsoft.AspNetCore.Mvc.ApplicationModels;
45

56
/// <content>
67
/// Provides additional implementation specific to ASP.NET Core.
@@ -22,9 +23,10 @@ public partial class ApiVersioningOptions
2223
/// Gets or sets a value indicating whether to use web API behaviors.
2324
/// </summary>
2425
/// <value>True to use web API behaviors; otherwise, false. The default value is <c>true</c>.</value>
25-
/// <remarks>When this property is set to <c>true</c>, API versioning policies only apply to controllers that
26-
/// have the ApiControllerAttribute applied. When this property is set to <c>false</c>, API versioning
27-
/// policies are considers for all controllers. This was default behavior prior to ASP.NET Core 2.2.</remarks>
26+
/// <remarks>When this property is set to <c>true</c>, API versioning policies only apply to controllers
27+
/// that remain after the <see cref="IApiControllerFilter"/> has been applied. When this property is set
28+
/// to <c>false</c>, API versioning policies are considers for all controllers. This was default behavior
29+
/// behavior in previous versions.</remarks>
2830
public bool UseApiBehavior { get; set; } = true;
2931
}
3032
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
2+
{
3+
using Microsoft.AspNet.OData;
4+
using System;
5+
6+
/// <summary>
7+
/// Represents a specification that matches API controllers if they use the OData protocol.
8+
/// </summary>
9+
[CLSCompliant( false )]
10+
public sealed class ODataControllerSpecification : IApiControllerSpecification
11+
{
12+
/// <inheritdoc />
13+
public bool IsSatisfiedBy( ControllerModel controller )
14+
{
15+
Arg.NotNull( controller, nameof( controller ) );
16+
return controller.ControllerType.IsODataController();
17+
}
18+
}
19+
}

src/Microsoft.AspNetCore.OData.Versioning/Extensions.DependencyInjection/IODataBuilderExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ static void AddODataServices( IServiceCollection services )
7272
services.AddTransient<IApplicationModelProvider, ODataApplicationModelProvider>();
7373
services.AddTransient<IActionDescriptorProvider, ODataActionDescriptorProvider>();
7474
services.AddSingleton<IActionDescriptorChangeProvider>( ODataActionDescriptorChangeProvider.Instance );
75+
services.TryAddEnumerable( Transient<IApiControllerSpecification, ODataControllerSpecification>() );
7576
services.AddModelConfigurationsAsServices( partManager );
7677
}
7778

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
2+
{
3+
using FluentAssertions;
4+
using System;
5+
using System.Reflection;
6+
using Xunit;
7+
8+
public class ApiBehaviorSpecificationTest
9+
{
10+
[Theory]
11+
[InlineData( typeof( ApiBehaviorController ), true )]
12+
[InlineData( typeof( NonApiBehaviorController ), false )]
13+
public void is_satisfied_by_should_return_expected_result( Type controllerType, bool expected )
14+
{
15+
// arrange
16+
var specification = new ApiBehaviorSpecification();
17+
var attributes = controllerType.GetCustomAttributes( inherit: false );
18+
var controller = new ControllerModel( controllerType.GetTypeInfo(), attributes );
19+
20+
// act
21+
var result = specification.IsSatisfiedBy( controller );
22+
23+
// assert
24+
result.Should().Be( expected );
25+
}
26+
27+
[ApiController]
28+
[Route( "api/test" )]
29+
sealed class ApiBehaviorController : ControllerBase
30+
{
31+
[HttpGet]
32+
public IActionResult Get() => Ok();
33+
}
34+
35+
[Route( "/" )]
36+
sealed class NonApiBehaviorController : Controller
37+
{
38+
[HttpGet]
39+
public IActionResult Index() => View();
40+
}
41+
}
42+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
2+
{
3+
using FluentAssertions;
4+
using Moq;
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Linq;
8+
using System.Reflection;
9+
using Xunit;
10+
11+
public class DefaultApiControllerFilterTest
12+
{
13+
[Fact]
14+
public void apply_should_not_filter_list_without_specifications()
15+
{
16+
// arrange
17+
var filter = new DefaultApiControllerFilter( Enumerable.Empty<IApiControllerSpecification>() );
18+
var controllerType = typeof( ControllerBase ).GetTypeInfo();
19+
var attributes = Array.Empty<object>();
20+
var controllers = new List<ControllerModel>()
21+
{
22+
new ControllerModel( controllerType, attributes ),
23+
new ControllerModel( controllerType, attributes ),
24+
new ControllerModel( controllerType, attributes ),
25+
};
26+
27+
// act
28+
var result = filter.Apply( controllers );
29+
30+
// assert
31+
result.Should().BeSameAs( controllers );
32+
}
33+
34+
[Fact]
35+
public void apply_should_filter_controllers()
36+
{
37+
// arrange
38+
var specification = new Mock<IApiControllerSpecification>();
39+
var controllerBaseType = typeof( ControllerBase ).GetTypeInfo();
40+
var controllerType = typeof( Controller ).GetTypeInfo();
41+
42+
specification.Setup( s => s.IsSatisfiedBy( It.Is<ControllerModel>( m => m.ControllerType.Equals( controllerBaseType ) ) ) ).Returns( true );
43+
specification.Setup( s => s.IsSatisfiedBy( It.Is<ControllerModel>( m => m.ControllerType.Equals( controllerType ) ) ) ).Returns( false );
44+
45+
var filter = new DefaultApiControllerFilter( new[] { specification.Object } );
46+
var attributes = Array.Empty<object>();
47+
var controllers = new List<ControllerModel>()
48+
{
49+
new ControllerModel( controllerType, attributes ),
50+
new ControllerModel( controllerBaseType, attributes ),
51+
new ControllerModel( controllerType, attributes ),
52+
};
53+
54+
// act
55+
var result = filter.Apply( controllers );
56+
57+
// assert
58+
result.Single().Should().BeSameAs( controllers[1] );
59+
}
60+
}
61+
}

0 commit comments

Comments
 (0)