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?
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:
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);
}
}
}
}
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));
});
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With