Skip to content
  • English Deutsch

benjamin-abt.com

Performance is a feature | Cloud - AI - Azure - .NET - Sustainability

  • Home
  • Blog
  • Portfolio
    • Overview
    • Workshops & Training
    • Publications
  • Contact
  • Home
  • Blog

Custom IFeatureDefinitionProvider: Feature Flags from Tests and Databases

Custom IFeatureDefinitionProvider: Feature Flags from Tests and Databases
November 28, 2025 5 min read BEN ABT
.NET ASP.NET Core Feature Flags Testing Architecture

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:

Bash
dotnet 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.

C#
 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.

C#
 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

C#
 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:

C#
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.

C#
 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:

C#
 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

C#
 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.

Related articles

Telemetry, Logging and Auditing in .NET: Designing the Right Data Path

Aug 24, 2026 - 18 min read

Telemetry, Logging and Auditing in .NET: Designing the Right Data Path

Telemetry, logging and auditing are often implemented through the same API because all three produce timestamped records. That similarity is …

Handle time correctly in .NET: TimeProvider

Jul 27, 2026 - 10 min read

Handle time correctly in .NET: TimeProvider

Time is an external dependency. That may sound unusual at first because DateTime.UtcNow is available anywhere without configuration. This …

Production Rate Limiting in ASP.NET Core

Nov 3, 2026 - 16 min read

Production Rate Limiting in ASP.NET Core

Most ASP.NET Core rate-limiting examples stop after registering a fixed window. That is enough to demonstrate the middleware, but it is not …


BEN ABT

BEN ABT

Ben is Chief Engineering Officer and Principal Software Engineer at Medialesson.de, with a strong focus on .NET and Microsoft Azure. In his professional role, he designs and builds highly scalable platforms for hybrid and cloud-based scenarios. He also advises companies and C-levels on migrating to the sovereign cloud.

He runs myCSharp.de, the largest and most active German-speaking C# community forum, actively contributes to both his own and external open-source projects and regularly shares his knowledge through his blog, at community events, conferences and as a writer for technical magazines.

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

Contact Me
X Facebook LinkedIn WhatsApp
Back Next Blog Post

Open for work!

Contact Me
BEN ABT BEN ABT Software Engineer · Microsoft MVP Passionate about .NET, Azure, performance optimization and sustainable software engineering.

Medialesson GmbH Logo

Microsoft MVP Logo

Recent posts

View all
Production Rate Limiting in ASP.NET Core

Production Rate Limiting in ASP.NET Core

Nov 3, 2026
The Big Decimal Bugfix with .NET 11

The Big Decimal Bugfix with .NET 11

Nov 2, 2026
Distroless, Chiseled or Alpine: Choosing a .NET Base Image

Distroless, Chiseled or Alpine: Choosing a .NET Base Image

Oct 27, 2026
Stop Using nginx as Your Default Static File Server in Docker

Stop Using nginx as Your Default Static File Server in …

Oct 24, 2026
Containerizing .NET in 2026 Without a Dockerfile

Containerizing .NET in 2026 Without a Dockerfile

Oct 20, 2026

Tags

.NET (195) Csharp (99) ASP.NET Core (49) Azure (41) DevOps (17) Docker (15) Microsoft (14) EF Core (11) Microsoft MVP (11) Security (11) AI (10) GitHub (10) GitHub Copilot (10) ASP.NET (9) Synology (9) .NET 11 (8) Architecture (8) Raspberry (8) Azure Functions (7) Cloudflare (7)
benjamin-abt.com

About BEN

Benjamin Abt is a Software Engineer, Platform Architect and Microsoft MVP specializing in .NET, Azure, cloud architecture, performance optimization and sustainable software development.

Connect

  • Follow me on GitHub
  • Follow me on LinkedIn
  • Follow me on Mastodon
  • Microsoft MVP profile
  • Follow SchwabenCode on GitHub
  • Follow me on X

Quick Links

  • Portfolio
  • Blog
  • Workshops & Training
  • Publications
  • Get in Touch
  • Sitemap
Open for Work

© Copyright Benjamin Abt & SchwabenCode | 2011 - 2026

Content licensed under CC BY 4.0 unless otherwise noted.

  • Impressum
  • Privacy
  • Contact