Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I exclude a controller from ASP.NET Core 3 Attribute Routing

I am using ASP.NET Core 3.1 for my web API. I have multiple controllers all using attribute-based routing and all is well.

We want to be able to toggle one or more controllers with feature flags in the app configuration. Ideally if a flag is not set then the corresponding controller should cease to exist in the eyes of the API. I am trying to come up with the best (or any) way to do this.

There appears to be no built-in way to configure which controllers are scanned when using attribute routing, and no way to modify the collection of controllers or endpoints that the routing finds. Here is the Startup.cs snippet in question:

    public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
    {            
        app.UseRouting();
        app.UseEndpoints(e =>
        {
            if (!this.FeatureEnabled)
            {
                // DO SOMETHING?
            }

            e.MapControllers();
        });
    }

I realize I can probably switch to the more manual hand-written routing and specify every controller, action, and parameter in the Startup class, but I would sooner abandon this feature flag requirement than go down that messy path.

Is there any way to select which controllers are used in the API when using attribute-based routing?

like image 521
Nathan Daniels Avatar asked Apr 20 '20 14:04

Nathan Daniels


3 Answers

The other answers are possible solutions, however we discovered a much easier solution that uses the feature flag functionality provided by Microsoft for ASP.NET Core that only required a couple lines of code.

https://docs.microsoft.com/en-us/azure/azure-app-configuration/use-feature-flags-dotnet-core

PM> install-package Microsoft.FeatureManagement.AspNetCore

So our startup has this line:

public void ConfigureServices(IServiceCollection services)
{
    // ...
    
    // By default this looks at the "FeatureManagement" config section
    services.AddFeatureManagement();
}

And our feature-gated controller has a new attribute at the top:

[ApiController]
[Route("api/v{version:apiVersion}/customers/{token}")]
// Feature.FooService is an enumeration we provide whose name is used as the feature flag
[FeatureGate(Feature.FooService)] 
public class FooController : ControllerBase
{
    ...
}

And our appsettings.json has the following section:

{
  "FeatureManagement": {
    "FooService" :  false
  }
}

When the flag is disabled, the entire controller returns a 404 for any action, and it works just fine when the flag is enabled.

There are two outstanding minor problems with this approach:

  • The controller still shows up in our Swagger documentation and Swagger UI. I don't know if it's possible to work around that.
  • The controller is still instantiated/constructed when a request is made to it, even if the feature flag is disabled and the actions would return 404. This meant for us that our IoC system (Autofac) was creating the entire object graph needed for the controller even though it wasn't really needed. There is also no easy way around this either.
like image 195
Nathan Daniels Avatar answered Oct 24 '22 23:10

Nathan Daniels


If you are use the FeatureManagement of the answer of Nathan Daniels. You can use this DocumentFilter to hide the controller in Swashbucke.

services.AddSwaggerGen(c =>
{
    c.DocumentFilter<FeatureGateDocumentFilter>();
});

FeatureGateDocumentFilter.cs

using Microsoft.FeatureManagement;
using Microsoft.FeatureManagement.Mvc;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Linq;

namespace Portalum.Sales.WebShopApi.OperationFilters
{
    public class FeatureGateDocumentFilter : IDocumentFilter
    {
        private readonly IFeatureManager _featureManager;

        public FeatureGateDocumentFilter(IFeatureManager featureManager)
        {
            this._featureManager = featureManager;
        }

        public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
        {      
            foreach (var apiDescription in context.ApiDescriptions)
            {
                var filterPipeline = apiDescription.ActionDescriptor.FilterDescriptors;
                var filterMetaData = filterPipeline.Select(filterInfo => filterInfo.Filter).SingleOrDefault(filter => filter is FeatureGateAttribute);
                if (filterMetaData == default)
                {
                    continue;
                }

                var featureGateAttribute = filterMetaData as FeatureGateAttribute;
                var isActive = this._featureManager.IsEnabledAsync(featureGateAttribute.Features.Single()).GetAwaiter().GetResult();
                if (isActive)
                {
                    continue;
                }

                var apiPath = swaggerDoc.Paths.FirstOrDefault(o => o.Key.Contains(apiDescription.RelativePath));
                swaggerDoc.Paths.Remove(apiPath.Key);
            }
        }
    }
}
like image 42
live2 Avatar answered Oct 24 '22 22:10

live2


You can implement your own ControllerFeatureProvider and decide which controllers you want to have in your application.

public class CustomControllerFeatureProvider : ControllerFeatureProvider
{
    private readonly IConfiguration _configuration;

    public CustomControllerFeatureProvider(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    protected override bool IsController(TypeInfo typeInfo)
    {
        var isController = base.IsController(typeInfo);

        if (isController)
        {
            var enabledController = _configuration.GetValue<string[]>("EnabledController");

            isController = enabledController.Any(x => typeInfo.Name.Equals(x, StringComparison.InvariantCultureIgnoreCase));
        }

        return isController;
    }
}

And add it in startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers()
        .ConfigureApplicationPartManager(manager =>
        {
            manager.FeatureProviders.Add(new CustomControllerFeatureProvider(_configuration));
        });
}
like image 21
Kahbazi Avatar answered Oct 24 '22 22:10

Kahbazi