19

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?

5 Answers 5

23

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://learn.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.
Sign up to request clarification or add additional context in comments.

1 Comment

For swagger you can use this filter stackoverflow.com/a/66867378/6097503
14

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.Remove(manager.FeatureProviders.OfType<ControllerFeatureProvider>().FirstOrDefault());
            manager.FeatureProviders.Add(new CustomControllerFeatureProvider(_configuration));
        });
}

2 Comments

You need to remove the existing provider before adding the custom provider: manager.FeatureProviders.Remove(manager.FeatureProviders.OfType<ControllerFeatureProvider>().FirstOrDefault());
You save my life, bruv. I needed a new swagger doc api, but only include new controllers from a monolithic project where swagger was broken
10

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);
            }
        }
    }
}

Comments

5

Alternatively, you could use one of the filters in ASP.NET Core to do this with more flexibility and readability.

And importantly, controllers behind feature toggles are still a valid controllers in your application. That, if you want to test those controllers with your special test purpose requests, you can still do it.

You could apply filter to either controller level or action level to either feature toggle all actions in controller, or a single action in controller.

Here is an example done using ActionFilterAttribute:

public class ToggledAttribute : ActionFilterAttribute
{
    // Can reuse the attribute for different controllers / actions
    // based on different configuration
    public ToggledAttribute(string configurationName = null)
    {
        ConfigurationName = configurationName;
    }

    public string ConfigurationName { get; }

    public override void OnActionExecuting(ActionExecutingContext context)
    {
        var isTestRequest =
            context.HttpContext.Request.Headers["x-my-test-header"].Count > 0;

        if (isTestRequest)
        {
            return;
        }

        var configuration = (IConfiguration)context.HttpContext.RequestServices
            .GetService(typeof(IConfiguration));

        // Somehow read toggle from configuration
        var featureEnabled = ...

        if (!featureEnabled)
        {
            context.Result = new NotFoundResult();
        }
    }
}

Apply to your controllers or actions where toggling needed:

[Route("[controller]")]
[Toggled]
public class MyToggledController : ControllerBase
{
    // OR
    [Toggled]
    [HttpGet]
    public int Get()
    {
        return 1;
    }
}

Comments

2

The example from @live2 is nice; just needs some modifications so it copes with multiple feature flags on the same controller:

    /// <summary>
    /// Swagger filter which allows [FeatureGate] attribute to be used to filter out 
    /// controllers and methods so feature flags take effect.
    /// </summary>
    public class FeatureGateDocumentFilter : IDocumentFilter
    {
        private readonly IFeatureManager _featureManager;

        /// <summary>
        /// Create new instance of filter.
        /// </summary>
        /// <param name="featureManager">Feature manager.</param>
        public FeatureGateDocumentFilter(IFeatureManager featureManager)
        {
            _featureManager = featureManager;
        }

        /// <inheritdoc />
        public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
        {
            foreach (var apiDescription in context.ApiDescriptions)
            {
                var filterPipeline = apiDescription.ActionDescriptor.FilterDescriptors;
                var featureAttributes = filterPipeline.Select(filterInfo => filterInfo.Filter).OfType<FeatureGateAttribute>().ToList();

                // Check the feature flags on all the [FeatureGate] attributes.
                bool allOk = true;
                foreach (var attribute in featureAttributes)
                {
                    var values = attribute.Features.Select(feature => _featureManager.IsEnabledAsync(feature).Result);
                    allOk &= (attribute.RequirementType == RequirementType.Any
                            ? values.Any(isEnabled => isEnabled)
                            : values.All(isEnabled => isEnabled)
                        );
                }

                if (!allOk)
                {
                    var apiPath = swaggerDoc.Paths.FirstOrDefault(o => o.Key.Contains(apiDescription.RelativePath!));
                    swaggerDoc.Paths.Remove(apiPath.Key);
                }
            }
        }
    }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.