By BEN ABT
.NET
,
ASP.NET Core
,
Feature Flags
,
Testing
,
Architecture
5 min read
Updated 2025-12-27

Feature flags are a great way to ship safely, run experiments, and keep production changes reversible. In .NET, the de-facto standard is Microsoft.FeatureManagement .
In many projects, feature definitions live in appsettings.json (or Azure App Configuration). That’s a solid default. But sometimes you need something different:
- Deterministic tests (no config files, no environment variables)
- Custom database schemas (you already have a table for flags)
- Multi-tenant / per-customer flags (flags depend on tenant, subscription, or region)
This post shows how to implement a custom IFeatureDefinitionProvider and wire it into your app.
What IFeatureDefinitionProvider does
IFeatureDefinitionProvider is the abstraction that the feature management system uses to retrieve feature definitions (names + how they are enabled).
In other words: IFeatureDefinitionProvider answers questions like:
- “What is the definition of
NewCheckout?” - “What feature flags exist?”
Once a definition is available, IFeatureManager evaluates it using configured filters.
Note: API details can differ slightly across package versions. The approach and patterns in this post remain the same.
Package
Install the feature management package:
1dotnet add package Microsoft.FeatureManagement.AspNetCore
A simple in-memory provider (perfect for tests)
This implementation holds feature definitions in memory. It’s ideal for integration tests where you want explicit, deterministic feature state.
1using Microsoft.FeatureManagement;
2
3public sealed class InMemoryFeatureDefinitionProvider : IFeatureDefinitionProvider
4{
5 private readonly IReadOnlyDictionary<string, FeatureDefinition> _definitions;
6
7 public InMemoryFeatureDefinitionProvider(IEnumerable<FeatureDefinition> definitions)
8 {
9 _definitions = definitions.ToDictionary(
10 d => d.Name,
11 StringComparer.OrdinalIgnoreCase);
12 }
13
14 public Task<FeatureDefinition?> GetFeatureDefinitionAsync(string featureName)
15 {
16 return Task.FromResult(
17 _definitions.TryGetValue(featureName, out var definition)
18 ? definition
19 : null);
20 }
21
22 public IAsyncEnumerable<FeatureDefinition> GetAllFeatureDefinitionsAsync()
23 {
24 return _definitions.Values.ToAsyncEnumerable();
25 }
26}
Creating feature definitions
A FeatureDefinition can enable a feature using filter configurations. The simplest “always on” case uses the built-in AlwaysOn filter.
1using Microsoft.FeatureManagement;
2
3var definitions = new[]
4{
5 new FeatureDefinition
6 {
7 Name = "NewCheckout",
8 EnabledFor = new List<FeatureFilterConfiguration>
9 {
10 new() { Name = "AlwaysOn" }
11 }
12 },
13 new FeatureDefinition
14 {
15 Name = "BetaBanner",
16 EnabledFor = new List<FeatureFilterConfiguration>()
17 }
18};
Registering the provider in ASP.NET Core
1using Microsoft.FeatureManagement;
2using Microsoft.Extensions.DependencyInjection.Extensions;
3
4var builder = WebApplication.CreateBuilder(args);
5
6builder.Services.AddFeatureManagement();
7
8// Ensure there is exactly one IFeatureDefinitionProvider.
9builder.Services.RemoveAll<IFeatureDefinitionProvider>();
10builder.Services.AddSingleton<IFeatureDefinitionProvider>(
11 new InMemoryFeatureDefinitionProvider(definitions));
12
13var app = builder.Build();
Now you can inject IFeatureManager and evaluate flags as usual:
1app.MapGet("/", async (IFeatureManager features) =>
2{
3 if (await features.IsEnabledAsync("NewCheckout"))
4 {
5 return Results.Ok("New checkout is enabled");
6 }
7
8 return Results.Ok("New checkout is disabled");
9});
Overriding feature flags in integration tests
If you’re using WebApplicationFactory, you can override the provider per test suite.
1using Microsoft.AspNetCore.Mvc.Testing;
2using Microsoft.Extensions.DependencyInjection;
3using Microsoft.Extensions.DependencyInjection.Extensions;
4using Microsoft.FeatureManagement;
5
6public sealed class AppFactory : WebApplicationFactory<Program>
7{
8 protected override void ConfigureWebHost(IWebHostBuilder builder)
9 {
10 builder.ConfigureServices(services =>
11 {
12 services.RemoveAll<IFeatureDefinitionProvider>();
13
14 services.AddSingleton<IFeatureDefinitionProvider>(
15 new InMemoryFeatureDefinitionProvider(new[]
16 {
17 new FeatureDefinition
18 {
19 Name = "NewCheckout",
20 EnabledFor = new List<FeatureFilterConfiguration>
21 {
22 new() { Name = "AlwaysOn" }
23 }
24 }
25 }));
26 });
27 }
28}
This keeps tests stable even if your production config changes.
A database-backed provider (custom schema)
If you store flags in your own database schema, the provider is the right place to map your data to FeatureDefinition.
A minimal example using EF Core + in-memory caching:
1using Microsoft.EntityFrameworkCore;
2using Microsoft.Extensions.Caching.Memory;
3using Microsoft.FeatureManagement;
4
5public sealed class DbFeatureDefinitionProvider : IFeatureDefinitionProvider
6{
7 private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
8 private readonly IMemoryCache _cache;
9
10 public DbFeatureDefinitionProvider(
11 IDbContextFactory<AppDbContext> dbContextFactory,
12 IMemoryCache cache)
13 {
14 _dbContextFactory = dbContextFactory;
15 _cache = cache;
16 }
17
18 public Task<FeatureDefinition?> GetFeatureDefinitionAsync(string featureName)
19 {
20 return _cache.GetOrCreateAsync($"featuredef:{featureName}", async entry =>
21 {
22 entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(30);
23
24 await using var db = await _dbContextFactory.CreateDbContextAsync();
25
26 var entity = await db.FeatureFlags
27 .AsNoTracking()
28 .SingleOrDefaultAsync(x => x.Name == featureName);
29
30 return entity is null ? null : Map(entity);
31 });
32 }
33
34 public async IAsyncEnumerable<FeatureDefinition> GetAllFeatureDefinitionsAsync()
35 {
36 await using var db = await _dbContextFactory.CreateDbContextAsync();
37
38 var all = await db.FeatureFlags
39 .AsNoTracking()
40 .ToListAsync();
41
42 foreach (var entity in all)
43 {
44 yield return Map(entity);
45 }
46 }
47
48 private static FeatureDefinition Map(FeatureFlagEntity entity)
49 {
50 // Minimal mapping: true => AlwaysOn; false => no filters.
51 return new FeatureDefinition
52 {
53 Name = entity.Name,
54 EnabledFor = entity.Enabled
55 ? new List<FeatureFilterConfiguration> { new() { Name = "AlwaysOn" } }
56 : new List<FeatureFilterConfiguration>()
57 };
58 }
59}
60
61public sealed class FeatureFlagEntity
62{
63 public required string Name { get; init; }
64 public required bool Enabled { get; init; }
65}
66
67public sealed class AppDbContext : DbContext
68{
69 public DbSet<FeatureFlagEntity> FeatureFlags => Set<FeatureFlagEntity>();
70
71 public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
72
73 protected override void OnModelCreating(ModelBuilder modelBuilder)
74 {
75 modelBuilder.Entity<FeatureFlagEntity>().HasKey(x => x.Name);
76 modelBuilder.Entity<FeatureFlagEntity>().ToTable("FeatureFlags");
77 }
78}
Registering the DB provider
1using Microsoft.Extensions.DependencyInjection.Extensions;
2using Microsoft.FeatureManagement;
3
4builder.Services.AddMemoryCache();
5builder.Services.AddDbContextFactory<AppDbContext>(options =>
6{
7 // Configure your provider (SQL Server/Postgres/etc.)
8});
9
10builder.Services.AddFeatureManagement();
11
12builder.Services.RemoveAll<IFeatureDefinitionProvider>();
13builder.Services.AddSingleton<IFeatureDefinitionProvider, DbFeatureDefinitionProvider>();
Design notes (what makes a provider “good”)
- Cache definitions. Feature flags are read frequently, and most apps don’t need per-request DB reads.
- Avoid complex logic inside the provider. Map your storage →
FeatureDefinitionand keep it readable. - Plan invalidation. If flags change often, consider:
- short TTLs, or
- push-based invalidation (e.g., a message bus), or
- a version column / “last updated” strategy.
- Multi-tenancy: if feature definitions are tenant-specific, your provider needs a way to resolve tenant context (e.g.,
IHttpContextAccessor, a scoped tenant service, or a header). In that case, you may need a scoped provider with careful caching keys.
When not to write your own provider
Before you build a custom provider, check whether you can use a built-in integration:
- Configuration (
appsettings.json) - Azure App Configuration feature flags
A custom provider makes sense when you truly need your own storage model or deterministic testing behavior.

Comments