Skip to content

Commit

Permalink
Added IEndpointInspector so that controller action endpoints are not …
Browse files Browse the repository at this point in the history
…processed by min api endpoint collators. Related #1066
  • Loading branch information
commonsensesoftware committed Mar 20, 2024
1 parent 1c13628 commit 9d18108
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.

namespace Asp.Versioning.ApiExplorer;

using Microsoft.AspNetCore.Http;

/// <summary>
/// Represents the default <see cref="IEndpointInspector">endpoint inspector</see>.
/// </summary>
[CLSCompliant(false)]
public sealed class DefaultEndpointInspector : IEndpointInspector
{
/// <inheritdoc />
public bool IsControllerAction( Endpoint endpoint ) => false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,29 @@ namespace Asp.Versioning.ApiExplorer;
public sealed class EndpointApiVersionMetadataCollationProvider : IApiVersionMetadataCollationProvider
{
private readonly EndpointDataSource endpointDataSource;
private readonly IEndpointInspector endpointInspector;
private int version;

/// <summary>
/// Initializes a new instance of the <see cref="EndpointApiVersionMetadataCollationProvider"/> class.
/// </summary>
/// <param name="endpointDataSource">The underlying <see cref="endpointDataSource">endpoint data source</see>.</param>
[Obsolete( "Use the constructor that accepts IEndpointInspector. This constructor will be removed in a future version." )]
public EndpointApiVersionMetadataCollationProvider( EndpointDataSource endpointDataSource )
: this( endpointDataSource, new DefaultEndpointInspector() ) { }

/// <summary>
/// Initializes a new instance of the <see cref="EndpointApiVersionMetadataCollationProvider"/> class.
/// </summary>
/// <param name="endpointDataSource">The underlying <see cref="endpointDataSource">endpoint data source</see>.</param>
/// <param name="endpointInspector">The <see cref="IEndpointInspector">endpoint inspector</see> used to inspect endpoints.</param>
public EndpointApiVersionMetadataCollationProvider( EndpointDataSource endpointDataSource, IEndpointInspector endpointInspector )
{
this.endpointDataSource = endpointDataSource ?? throw new ArgumentNullException( nameof( endpointDataSource ) );
ArgumentNullException.ThrowIfNull( endpointDataSource );
ArgumentNullException.ThrowIfNull( endpointInspector );

this.endpointDataSource = endpointDataSource;
this.endpointInspector = endpointInspector;
ChangeToken.OnChange( endpointDataSource.GetChangeToken, () => ++version );
}

Expand All @@ -38,7 +52,8 @@ public void Execute( ApiVersionMetadataCollationContext context )
{
var endpoint = endpoints[i];

if ( endpoint.Metadata.GetMetadata<ApiVersionMetadata>() is not ApiVersionMetadata item )
if ( endpoint.Metadata.GetMetadata<ApiVersionMetadata>() is not ApiVersionMetadata item ||
endpointInspector.IsControllerAction( endpoint ) )
{
continue;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.

namespace Asp.Versioning.ApiExplorer;

using Microsoft.AspNetCore.Http;

/// <summary>
/// Defines the behavior of an endpoint inspector.
/// </summary>
[CLSCompliant( false )]
public interface IEndpointInspector
{
/// <summary>
/// Determines whether the specified endpoint is a controller action.
/// </summary>
/// <param name="endpoint">The <see cref="Endpoint">endpoint</see> to inspect.</param>
/// <returns>True if the <paramref name="endpoint"/> is for a controller action; otherwise, false.</returns>
bool IsControllerAction( Endpoint endpoint );
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ private static void AddApiVersioningServices( IServiceCollection services )
services.TryAddEnumerable( Transient<IPostConfigureOptions<RouteOptions>, ApiVersioningRouteOptionsSetup>() );
services.TryAddEnumerable( Singleton<MatcherPolicy, ApiVersionMatcherPolicy>() );
services.TryAddEnumerable( Singleton<IApiVersionMetadataCollationProvider, EndpointApiVersionMetadataCollationProvider>() );
services.TryAddTransient<IEndpointInspector, DefaultEndpointInspector>();
services.Replace( WithLinkGeneratorDecorator( services ) );
TryAddProblemDetailsRfc7231Compliance( services );
TryAddErrorObjectJsonOptions( services );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,29 @@ internal sealed class ApiVersionDescriptionProviderFactory : IApiVersionDescript
{
private readonly ISunsetPolicyManager sunsetPolicyManager;
private readonly IApiVersionMetadataCollationProvider[] providers;
private readonly IEndpointInspector endpointInspector;
private readonly IOptions<ApiExplorerOptions> options;
private readonly Activator activator;

public ApiVersionDescriptionProviderFactory(
Activator activator,
ISunsetPolicyManager sunsetPolicyManager,
IEnumerable<IApiVersionMetadataCollationProvider> providers,
IEndpointInspector endpointInspector,
IOptions<ApiExplorerOptions> options )
{
this.activator = activator;
this.sunsetPolicyManager = sunsetPolicyManager;
this.providers = providers.ToArray();
this.endpointInspector = endpointInspector;
this.options = options;
}

public IApiVersionDescriptionProvider Create( EndpointDataSource endpointDataSource )
{
var collators = new List<IApiVersionMetadataCollationProvider>( capacity: providers.Length + 1 )
{
new EndpointApiVersionMetadataCollationProvider( endpointDataSource ),
new EndpointApiVersionMetadataCollationProvider( endpointDataSource, endpointInspector ),
};

collators.AddRange( providers );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ namespace Microsoft.Extensions.DependencyInjection;
using Asp.Versioning;
using Asp.Versioning.ApiExplorer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using static ServiceDescriptor;

/// <summary>
Expand Down Expand Up @@ -70,40 +72,36 @@ private static IApiVersionDescriptionProviderFactory ResolveApiVersionDescriptio
{
var sunsetPolicyManager = serviceProvider.GetRequiredService<ISunsetPolicyManager>();
var providers = serviceProvider.GetServices<IApiVersionMetadataCollationProvider>();
var inspector = serviceProvider.GetRequiredService<IEndpointInspector>();
var options = serviceProvider.GetRequiredService<IOptions<ApiExplorerOptions>>();
var mightUseCustomGroups = options.Value.FormatGroupName is not null;

return new ApiVersionDescriptionProviderFactory(
mightUseCustomGroups ? NewGroupedProvider : NewDefaultProvider,
NewDefaultProvider,
sunsetPolicyManager,
providers,
inspector,
options );

static IApiVersionDescriptionProvider NewDefaultProvider(
static DefaultApiVersionDescriptionProvider NewDefaultProvider(
IEnumerable<IApiVersionMetadataCollationProvider> providers,
ISunsetPolicyManager sunsetPolicyManager,
IOptions<ApiExplorerOptions> apiExplorerOptions ) =>
new DefaultApiVersionDescriptionProvider( providers, sunsetPolicyManager, apiExplorerOptions );

static IApiVersionDescriptionProvider NewGroupedProvider(
IEnumerable<IApiVersionMetadataCollationProvider> providers,
ISunsetPolicyManager sunsetPolicyManager,
IOptions<ApiExplorerOptions> apiExplorerOptions ) =>
new GroupedApiVersionDescriptionProvider( providers, sunsetPolicyManager, apiExplorerOptions );
new( providers, sunsetPolicyManager, apiExplorerOptions );
}

private static IApiVersionDescriptionProvider ResolveApiVersionDescriptionProvider( IServiceProvider serviceProvider )
{
var providers = serviceProvider.GetServices<IApiVersionMetadataCollationProvider>();
var sunsetPolicyManager = serviceProvider.GetRequiredService<ISunsetPolicyManager>();
var options = serviceProvider.GetRequiredService<IOptions<ApiExplorerOptions>>();
var mightUseCustomGroups = options.Value.FormatGroupName is not null;
var factory = serviceProvider.GetRequiredService<IApiVersionDescriptionProviderFactory>();
var endpointDataSource = new EmptyEndpointDataSource();
return factory.Create( endpointDataSource );
}

private sealed class EmptyEndpointDataSource : EndpointDataSource
{
public override IReadOnlyList<Endpoint> Endpoints => Array.Empty<Endpoint>();

if ( mightUseCustomGroups )
{
return new GroupedApiVersionDescriptionProvider( providers, sunsetPolicyManager, options );
}
public override IChangeToken GetChangeToken() => new CancellationChangeToken( CancellationToken.None );

return new DefaultApiVersionDescriptionProvider( providers, sunsetPolicyManager, options );
public override IReadOnlyList<Endpoint> GetGroupedEndpoints( RouteGroupContext context ) => Array.Empty<Endpoint>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.

namespace Asp.Versioning.ApiExplorer;

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

/// <summary>
/// Represents the <see cref="IEndpointInspector">inspector</see> that understands
/// <see cref="Endpoint">endpoints</see> defined by MVC controllers.
/// </summary>
[CLSCompliant(false)]
public sealed class MvcEndpointInspector : IEndpointInspector
{
/// <inheritdoc />
public bool IsControllerAction( Endpoint endpoint )
{
ArgumentNullException.ThrowIfNull( endpoint );
return endpoint.Metadata.Any( static attribute => attribute is ControllerAttribute );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ private static void AddServices( IServiceCollection services )
services.TryAddEnumerable( Transient<IApiControllerSpecification, ApiBehaviorSpecification>() );
services.TryAddEnumerable( Singleton<IApiVersionMetadataCollationProvider, ActionApiVersionMetadataCollationProvider>() );
services.Replace( WithUrlHelperFactoryDecorator( services ) );
services.TryReplace<IEndpointInspector, DefaultEndpointInspector, MvcEndpointInspector>();
}

private static object CreateInstance( this IServiceProvider services, ServiceDescriptor descriptor )
Expand All @@ -84,6 +85,23 @@ private static object CreateInstance( this IServiceProvider services, ServiceDes
return ActivatorUtilities.GetServiceOrCreateInstance( services, descriptor.ImplementationType! );
}

private static void TryReplace<TService, TImplementation, TReplacement>( this IServiceCollection services )
{
var serviceType = typeof( TService );
var implementationType = typeof( TImplementation );

for ( var i = services.Count - 1; i >= 0; i-- )
{
var service = services[i];

if ( service.ServiceType == serviceType && service.ImplementationType == implementationType )
{
services[i] = Describe( serviceType, typeof( TReplacement ), service.Lifetime );
break;
}
}
}

[SkipLocalsInit]
private static DecoratedServiceDescriptor WithUrlHelperFactoryDecorator( IServiceCollection services )
{
Expand Down

0 comments on commit 9d18108

Please sign in to comment.