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.


Let's Work Together

Looking for an experienced Platform Architect or Engineer for your next project? Whether it's cloud migration, platform modernization or building new solutions from scratch - I'm here to help you succeed.

New Platforms
Modernization
Training & Consulting

Comments

Twitter Facebook LinkedIn WhatsApp