Custom IFeatureDefinitionProvider: Feature Flags from Tests and Databases

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 → FeatureDefinition and 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

Twitter Facebook LinkedIn WhatsApp