Deploying new features can be stressful. What if a bug slips through? What if you want to release a feature only to internal testers first? The traditional “deploy to release” model couples your deployment schedule directly with your feature launch, creating a high-stakes event. Feature flags, also known as feature toggles, offer a better way.

Feature flags allow you to decouple deployment from release. You can deploy new, unfinished, or experimental features to production but keep them “turned off.” This lets you enable them on-the-fly for specific users or a percentage of your user base, or quickly disable a feature that’s causing problems—all without a single redeployment.

In this post, we’ll explore how to implement feature flags in an ASP.NET Core application using the Microsoft.FeatureManagement library and custom middleware to control access to entire endpoints dynamically.

What Are Feature Flags?

At its core, a feature flag is a decision point in your code that can be toggled via configuration. Think of it as a dynamic if statement that you can control from outside your application’s code.

The benefits are significant:

  • Test in Production Safely: Deploy features to production and enable them only for your QA team.
  • Canary Releases & A/B Testing: Gradually roll out a new feature to a small percentage of users to monitor its performance and impact.
  • Kill Switch: Instantly disable a faulty feature in production if it’s causing issues, minimizing downtime and user impact.
  • Reduced Merge Conflicts: Mainline development becomes easier as feature branches can be merged more frequently, even if the feature isn’t complete.

Setting Up Microsoft.FeatureManagement

Microsoft provides a powerful and extensible library to make implementing feature flags straightforward.

First, add the necessary NuGet package to your project:

dotnet add package Microsoft.FeatureManagement.AspNetCore

Next, define your feature flags in your appsettings.json configuration file. For this example, we’ll create a flag for a new beta feature.

appsettings.json:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "FeatureManagement": {
    "BetaFeature": false,
    "NewApiEndpoint": true
  }
}

Here, we’ve defined two features: BetaFeature (currently disabled) and NewApiEndpoint (currently enabled).

Finally, register the feature management services in Program.cs.

Program.cs:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddFeatureManagement();
builder.Services.AddControllers();

var app = builder.Build();

// ... app pipeline

app.Run();

That’s it for the basic setup. Now you can inject IFeatureManager anywhere in your application to check if a feature is enabled.

Creating a Middleware for Feature Gating

While you can check flags directly within your controllers or services, a more elegant solution for gating HTTP endpoints is to use middleware. Let’s create a middleware that blocks access to a specific route if its corresponding feature flag is disabled.

Here is our custom middleware, which will protect the /beta-dashboard route.

Middleware/FeatureGateMiddleware.cs:

using Microsoft.FeatureManagement;

public class FeatureGateMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IFeatureManager _featureManager;

    public FeatureGateMiddleware(RequestDelegate next, IFeatureManager featureManager)
    {
        _next = next;
        _featureManager = featureManager;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // Target a specific path for this example
        if (context.Request.Path.Equals("/beta-dashboard", StringComparison.OrdinalIgnoreCase))
        {
            // Check if the "BetaFeature" is enabled
            if (!await _featureManager.IsEnabledAsync("BetaFeature"))
            {
                // If disabled, short-circuit the request and return 404 Not Found
                context.Response.StatusCode = StatusCodes.Status404NotFound;
                return;
            }
        }

        // If the path doesn't match or the feature is enabled, continue to the next middleware
        await _next(context);
    }
}

Now, register this middleware in the request processing pipeline in Program.cs. It’s important to place it before the middleware that will handle the request, like UseRouting or MapControllers.

Program.cs:

// ... other configurations

var app = builder.Build();

app.UseHttpsRedirection();

// Use our custom middleware
app.UseMiddleware<FeatureGateMiddleware>();

app.UseRouting();
app.UseAuthorization();
app.MapControllers();

// A simple endpoint for our beta dashboard
app.MapGet("/beta-dashboard", () => "Welcome to the secret beta dashboard!");

app.Run();

Now, run your application.

  • If you navigate to /beta-dashboard while "BetaFeature": false is set in appsettings.json, you’ll receive a 404 Not Found response.
  • Change the value to true, restart the application, and the endpoint will now be accessible.

A Note from Experience While appsettings.json is great for development, its real power is unlocked when you pair feature flags with a dynamic configuration source like Azure App Configuration. This allows you and your team to toggle features in a live environment via a web portal without restarting or redeploying the application. This level of agility is a huge win for modern DevOps workflows.

A More Declarative Approach with Endpoint Metadata

The previous middleware was hardcoded to check a specific path and feature. A more scalable and reusable approach is to use endpoint metadata. We can create a custom attribute to decorate our Minimal API endpoints and a middleware that reads this metadata.

First, let’s define an attribute.

Attributes/RequiresFeatureAttribute.cs:

[AttributeUsage(AttributeTargets.Method)]
public class RequiresFeatureAttribute : Attribute
{
    public string FeatureName { get; }

    public RequiresFeatureAttribute(string featureName)
    {
        FeatureName = featureName;
    }
}

Next, create a new middleware that inspects endpoint metadata for this attribute.

Middleware/EndpointFeatureGateMiddleware.cs:

using Microsoft.FeatureManagement;

public class EndpointFeatureGateMiddleware
{
    private readonly RequestDelegate _next;

    public EndpointFeatureGateMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context, IFeatureManager featureManager)
    {
        var endpoint = context.GetEndpoint();
        if (endpoint != null)
        {
            var featureAttribute = endpoint.Metadata.GetMetadata<RequiresFeatureAttribute>();
            if (featureAttribute != null)
            {
                if (!await featureManager.IsEnabledAsync(featureAttribute.FeatureName))
                {
                    context.Response.StatusCode = StatusCodes.Status404NotFound;
                    return;
                }
            }
        }

        await _next(context);
    }
}

This middleware retrieves the current endpoint, looks for our RequiresFeatureAttribute metadata, and if found, checks the specified feature flag.

Now, let’s update Program.cs to use this new middleware and apply our attribute to a Minimal API endpoint. Note that this middleware should be placed after UseRouting so it has access to endpoint information.

Program.cs:

// ... service registration

var app = builder.Build();

app.UseHttpsRedirection();
app.UseRouting(); // Middleware needs to be after routing

app.UseMiddleware<EndpointFeatureGateMiddleware>();

app.UseAuthorization();
app.MapControllers();

// This endpoint is always available
app.MapGet("/", () => "Welcome home!");

// This endpoint is protected by our feature flag attribute
app.MapGet("/new-api", () => new { Message = "This is the new, gated API!" })
   .WithMetadata(new RequiresFeatureAttribute("NewApiEndpoint"));

app.Run();

With "NewApiEndpoint": true in appsettings.json, navigating to /new-api works. If you set it to false and restart, the endpoint will return a 404. This declarative approach is much cleaner and easier to manage as your application grows.

Conclusion

Feature flags are an indispensable tool for modern application development and delivery. By integrating the Microsoft.FeatureManagement library with custom ASP.NET Core middleware, you can build a robust system for gating access to features at the request level. This approach not only makes your deployments safer but also provides incredible flexibility for rolling out new functionality, testing in production, and responding quickly to issues.

References