A simple, fast, and modular feature flag library for .NET
Features • Quick Start • Packages • Providers • Schema • Contributing
- Simple & Fast - Minimal overhead with efficient caching
- Flexible Caching - Support for MemoryCache (default) and Redis with automatic cache invalidation
- Modular Design - Use only what you need with separate provider packages
- Multiple Providers - MySQL, PostgreSQL, MS SQL Server, and InMemory (with JSON persistence) providers included
- Auto-Migration - Automatic database schema creation and version management
- Flag Values - Store typed values (string, int, double, etc.) in flags, not just on/off states
- Beautiful Dashboard - Web UI to manage feature flags with database-backed user authentication
- User Management - Built-in user management with BCrypt password hashing and secure authentication
- Programmatic API - Full CRUD operations with extension methods and fluent API for managing flags without UI
- Easy Integration - Simple configuration with dependency injection
- Production Ready - Built with best practices and performance in mind
# Install core library with a provider
dotnet add package Flaggy
dotnet add package Flaggy.Provider.MySQL
# Optional: Add UI dashboard
dotnet add package Flaggy.UIusing Flaggy.Extensions;
using Flaggy.UI.Extensions;
var builder = WebApplication.CreateBuilder(args);
// Add Flaggy with MySQL provider (auto-migration enabled by default)
builder.Services.AddFlaggy(options =>
{
options.UseMySQL(
connectionString: "Server=localhost;Database=myapp;User=root;Password=pass;",
tableName: "feature_flags", // optional, default is "feature_flags"
autoMigrate: true); // optional, default is true - automatically creates tables
options.UseMemoryCache(cacheExpiration: TimeSpan.FromMinutes(5));
});
var app = builder.Build();
// Add Flaggy UI Dashboard (optional)
app.UseFlaggyUI(options =>
{
options.RoutePrefix = "/flaggy"; // default is "/flaggy"
options.RequireAuthorization = false; // optional
});
app.Run();app.MapGet("/api/products", async (IFeatureFlagService flagService) =>
{
var isNewUIEnabled = await flagService.IsEnabledAsync("new-product-ui");
if (isNewUIEnabled)
{
return Results.Ok(new { message = "New UI is enabled!" });
}
return Results.Ok(new { message = "Using legacy UI" });
});Flags can have values (string, int, double, etc.). The IsEnabled property controls whether the flag is published/active.
You can optionally provide a default value that will be returned if the flag doesn't exist or is disabled.
// String value with default
app.MapGet("/welcome", async (IFeatureFlagService flagService) =>
{
// Returns "Welcome!" if flag doesn't exist or is disabled
var message = await flagService.GetValueAsync("welcome-message", defaultValue: "Welcome!");
return Results.Ok(new { message });
});
// Integer value with default
app.MapGet("/users/max-limit", async (IFeatureFlagService flagService) =>
{
// Returns 100 if flag doesn't exist or is disabled
var maxUsers = await flagService.GetValueAsync<int>("max-users", defaultValue: 100);
return Results.Ok(new { limit = maxUsers });
});
// Double value (discount rate) with default
app.MapGet("/products/price", async (IFeatureFlagService flagService) =>
{
// Returns 0.0 (no discount) if flag doesn't exist or is disabled
var discountRate = await flagService.GetValueAsync<double>("discount-rate", defaultValue: 0.0);
var originalPrice = 100.0;
var finalPrice = originalPrice * (1 - discountRate.Value);
return Results.Ok(new { originalPrice, finalPrice });
});How it works:
- Enabled = false: Flag is defined but not active.
GetValueAsyncreturns thedefaultValue(ornullif no default provided). - Enabled = true: Flag is active.
GetValueAsyncreturns the flag's value. - Flag doesn't exist:
GetValueAsyncreturns thedefaultValue(ornullif no default provided). - Value: Can be any string. Use
GetValueAsync<T>()for type conversion (int, double, bool, etc.).
Navigate to https://localhost:5001/flaggy to manage your feature flags through the web UI.
The InMemory provider persists flags to a JSON file for durability across restarts.
using Flaggy.Extensions;
using Flaggy.Providers;
// Default: saves to "flaggy-flags.json" in current directory
builder.Services.AddFlaggy(options =>
{
options.UseInMemory();
options.UseMemoryCache(TimeSpan.FromMinutes(5));
});MySQL provider includes automatic migration with version tracking. No manual schema setup required!
using Flaggy.Extensions;
// Auto-migration enabled by default
builder.Services.AddFlaggy(options =>
{
options.UseMySQL(
connectionString: "Server=localhost;Database=myapp;User=root;Password=pass;",
tableName: "feature_flags", // optional, default is "feature_flags"
userTableName: "users", // optional, default is "users"
autoMigrate: true); // optional, default is true
options.UseMemoryCache(cacheExpiration: TimeSpan.FromMinutes(5));
});
// Tables will be created automatically:
// - feature_flags: stores your feature flags
// - users: stores dashboard users with BCrypt hashed passwords
// - flaggy_migrations: tracks schema versionPostgreSQL provider also includes automatic migration with version tracking.
using Flaggy.Extensions;
// Auto-migration enabled by default
builder.Services.AddFlaggy(options =>
{
options.UsePostgreSQL(
connectionString: "Host=localhost;Database=myapp;Username=postgres;Password=pass",
tableName: "feature_flags", // optional, default is "feature_flags"
userTableName: "users", // optional, default is "users"
autoMigrate: true); // optional, default is true
options.UseMemoryCache(cacheExpiration: TimeSpan.FromMinutes(5));
});
// Tables will be created automatically:
// - feature_flags: stores your feature flags
// - users: stores dashboard users with BCrypt hashed passwords
// - flaggy_migrations: tracks schema versionMS SQL Server provider includes automatic migration with version tracking.
using Flaggy.Extensions;
// Auto-migration enabled by default
builder.Services.AddFlaggy(options =>
{
options.UseMsSql(
connectionString: "Server=localhost;Database=myapp;User Id=sa;Password=pass;TrustServerCertificate=True",
tableName: "feature_flags", // optional, default is "feature_flags"
userTableName: "users", // optional, default is "users"
autoMigrate: true); // optional, default is true
options.UseMemoryCache(cacheExpiration: TimeSpan.FromMinutes(5));
});
// Tables will be created automatically:
// - feature_flags: stores your feature flags
// - users: stores dashboard users with BCrypt hashed passwords
// - flaggy_migrations: tracks schema versionYou can manage feature flags programmatically without using the web UI. Flaggy provides extension methods and helpers for easy flag management.
public class MyService
{
private readonly IFeatureFlagService _flagService;
public MyService(IFeatureFlagService flagService)
{
_flagService = flagService;
}
public async Task ManageFlags()
{
// Create a new flag
await _flagService.CreateFlagAsync(new FeatureFlag
{
Key = "new-feature",
IsEnabled = false,
Value = "beta",
Description = "New feature in beta"
});
// List all flags
var allFlags = await _flagService.GetAllFlagsAsync();
// Get specific flag
var flag = await _flagService.GetFlagAsync("new-feature");
// Update flag
flag.IsEnabled = true;
await _flagService.UpdateFlagAsync(flag);
// Delete flag
await _flagService.DeleteFlagAsync("new-feature");
}
}Flaggy provides convenient extension methods for common operations:
using Flaggy.Extensions;
// Create only if doesn't exist
await flagService.CreateFlagIfNotExistsAsync("dark-mode", isEnabled: true);
// Upsert (create or update)
await flagService.UpsertFlagAsync("beta-features", isEnabled: false);
// Enable/Disable flags
await flagService.EnableFlagAsync("dark-mode");
await flagService.DisableFlagAsync("beta-features");
// Toggle flag
await flagService.ToggleFlagAsync("dark-mode");
// Update only value or description
await flagService.UpdateFlagValueAsync("theme", "auto");
await flagService.UpdateFlagDescriptionAsync("theme", "Auto theme detection");
// Check if flag exists
bool exists = await flagService.FlagExistsAsync("dark-mode");
// Get enabled/disabled flags
var enabledFlags = await flagService.GetEnabledFlagsAsync();
var disabledFlags = await flagService.GetDisabledFlagsAsync();
// Get summary
var summary = await flagService.GetFlagsSummaryAsync();
Console.WriteLine($"Total: {summary.TotalCount}, Enabled: {summary.EnabledCount}");
// Delete multiple flags
await flagService.DeleteFlagsAsync(new[] { "flag1", "flag2" });
// Delete all disabled flags
await flagService.DeleteAllDisabledFlagsAsync();Use the fluent API for cleaner flag creation:
using Flaggy.Helpers;
var initializer = new FeatureFlagInitializer(flagService);
await initializer.CreateFlag("premium-features")
.Enabled()
.WithDescription("Premium features for paid users")
.WithValue("tier-1,tier-2")
.CreateIfNotExistsAsync();
await initializer.CreateFlag("maintenance-mode")
.Disabled()
.WithDescription("Enable maintenance mode")
.UpsertAsync();Seed default flags when your application starts:
using Flaggy.Extensions;
using Flaggy.Helpers;
var builder = WebApplication.CreateBuilder();
builder.Services.AddFlaggy(options =>
{
options.UsePostgreSQL("Host=localhost;...");
options.UseMemoryCache(TimeSpan.FromMinutes(5));
});
var app = builder.Build();
// Method 1: Using extension method
await app.SeedFeatureFlagsAsync(
new FeatureFlag { Key = "welcome-banner", IsEnabled = true },
new FeatureFlag { Key = "new-dashboard", IsEnabled = false }
);
// Method 2: Using initializer
await app.InitializeFeatureFlagsAsync(async flagService =>
{
var initializer = new FeatureFlagInitializer(flagService);
await initializer.CreateFlag("notifications")
.Enabled()
.WithDescription("Email notifications")
.CreateIfNotExistsAsync();
});
app.Run();using Flaggy.Extensions;
using Flaggy.Providers;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddFlaggy(new InMemoryFeatureFlagProvider());
var serviceProvider = services.BuildServiceProvider();
var flagService = serviceProvider.GetRequiredService<IFeatureFlagService>();
// Create flags
await flagService.CreateFlagAsync(new FeatureFlag
{
Key = "feature-1",
IsEnabled = true,
Description = "First feature"
});
// List all flags
var flags = await flagService.GetAllFlagsAsync();
foreach (var flag in flags)
{
Console.WriteLine($"{flag.Key}: {(flag.IsEnabled ? "✓" : "✗")}");
}For more examples, see ProgrammaticUsageExamples.cs which includes:
- Basic CRUD operations
- Extension method usage
- Fluent builder API
- Seeding and initialization
- Console application examples
- Controller/Service integration patterns
app.UseFlaggyUI(options =>
{
options.RoutePrefix = "/admin/flags";
options.RequireAuthorization = true;
options.AuthorizationFilter = context =>
{
// Custom authorization logic
return context.User.IsInRole("Admin");
};
});app.MapPost("/api/flags/refresh", async (IFeatureFlagService flagService) =>
{
await flagService.RefreshCacheAsync();
return Results.Ok(new { message = "Cache refreshed" });
});app.MapGet("/api/flags/{key}", async (string key, IFeatureFlagService flagService) =>
{
var flag = await flagService.GetFlagAsync(key);
if (flag == null)
{
return Results.NotFound();
}
return Results.Ok(flag);
});app.MapGet("/api/flags", async (IFeatureFlagService flagService) =>
{
var flags = await flagService.GetAllFlagsAsync();
return Results.Ok(flags);
});Create your own provider by implementing IFeatureFlagProvider:
using Flaggy.Abstractions;
using Flaggy.Models;
public class CustomFeatureFlagProvider : IFeatureFlagProvider
{
public async Task<FeatureFlag?> GetFlagAsync(string key, CancellationToken cancellationToken = default)
{
// Your implementation
}
public async Task<IEnumerable<FeatureFlag>> GetAllFlagsAsync(CancellationToken cancellationToken = default)
{
// Your implementation
}
public async Task<bool> CreateFlagAsync(FeatureFlag flag, CancellationToken cancellationToken = default)
{
// Your implementation
}
public async Task<bool> UpdateFlagAsync(FeatureFlag flag, CancellationToken cancellationToken = default)
{
// Your implementation
}
public async Task<bool> DeleteFlagAsync(string key, CancellationToken cancellationToken = default)
{
// Your implementation
}
}Then use it:
builder.Services.AddFlaggy(new CustomFeatureFlagProvider());Note: These tables are created automatically by the migration system. You don't need to run these scripts manually!
-- Feature flags table (auto-created by migrations)
CREATE TABLE feature_flags (
`key` VARCHAR(255) PRIMARY KEY,
is_enabled BOOLEAN NOT NULL DEFAULT FALSE,
`value` TEXT NULL,
description TEXT NULL,
created_at DATETIME NULL,
updated_at DATETIME NULL
);
-- Migration version tracking table (auto-created)
CREATE TABLE flaggy_migrations (
id INT AUTO_INCREMENT PRIMARY KEY,
version INT NOT NULL UNIQUE,
description VARCHAR(500) NOT NULL,
applied_at DATETIME NOT NULL
);-- Feature flags table (auto-created by migrations)
CREATE TABLE feature_flags (
key VARCHAR(255) PRIMARY KEY,
is_enabled BOOLEAN NOT NULL DEFAULT FALSE,
value TEXT NULL, -- Added in Version 2
description TEXT NULL,
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL
);
-- Migration version tracking table (auto-created)
CREATE TABLE flaggy_migrations (
id SERIAL PRIMARY KEY,
version INT NOT NULL UNIQUE,
description VARCHAR(500) NOT NULL,
applied_at TIMESTAMP NOT NULL
);Flaggy includes a built-in migration system with version tracking. When you provide a connection string:
- Version Table Creation: On first run,
flaggy_migrationstable is created automatically - Version Check: Checks current database version against available migrations
- Auto-Upgrade: Runs pending migrations in order (Version 1, 2, 3, etc.)
- Version Tracking: Each migration is recorded with version, description, and timestamp
If you prefer manual control:
using Flaggy.Extensions;
builder.Services.AddFlaggy(options =>
{
options.UseMySQL(
connectionString: "...",
autoMigrate: false); // Disable auto-migration
options.UseMemoryCache(TimeSpan.FromMinutes(5));
});The migration system automatically applies schema changes as new versions are released:
- Version 1: Initial table creation with Key, IsEnabled, Description, CreatedAt, UpdatedAt
- Version 2: Added Value column for storing flag values (current)
Future migrations will be applied automatically when you upgrade to newer versions. The system ensures migrations run only once and in the correct order.
Flaggy provides flexible caching strategies to optimize performance:
By default, Flaggy uses IMemoryCache for caching:
using Flaggy.Enums;
using Flaggy.Extensions;
// Default: MemoryCache with 5 minutes expiration
builder.Services.AddFlaggy(new InMemoryFeatureFlagProvider());
// Custom cache expiration
builder.Services.AddFlaggy(
provider: new InMemoryFeatureFlagProvider(),
cachingProvider: CachingProvider.Memory,
cacheExpiration: TimeSpan.FromMinutes(10)
);For distributed scenarios, use Redis caching:
dotnet add package Flaggy.Caching.Redisusing Flaggy.Enums;
using Flaggy.Extensions;
// Simple Redis configuration (no authentication)
builder.Services.AddFlaggy(
provider: new InMemoryFeatureFlagProvider(),
cachingProvider: CachingProvider.Redis,
cacheExpiration: TimeSpan.FromMinutes(10),
redisConnectionString: "localhost:6379",
redisDatabase: 0 // optional, default is 0
);
// With password authentication
builder.Services.AddFlaggy(
provider: new InMemoryFeatureFlagProvider(),
cachingProvider: CachingProvider.Redis,
redisConnectionString: "localhost:6379,password=mypassword",
cacheExpiration: TimeSpan.FromMinutes(10)
);
// With username and password (Redis 6+)
builder.Services.AddFlaggy(
provider: new InMemoryFeatureFlagProvider(),
cachingProvider: CachingProvider.Redis,
redisConnectionString: "localhost:6379,user=myuser,password=mypassword",
cacheExpiration: TimeSpan.FromMinutes(10)
);
// With SSL/TLS
builder.Services.AddFlaggy(
provider: new InMemoryFeatureFlagProvider(),
cachingProvider: CachingProvider.Redis,
redisConnectionString: "localhost:6380,ssl=true,password=mypassword"
);
// With MySQL provider
builder.Services.AddFlaggy(options =>
{
options.UseMySQL("Server=localhost;Database=myapp;User=root;Password=pass;");
options.UseRedisCache("localhost:6379,password=mypassword", TimeSpan.FromMinutes(10));
});Supported Redis Connection String Formats:
localhost:6379- No authenticationlocalhost:6379,password=pass- Password onlylocalhost:6379,user=user,password=pass- Username and password (Redis 6+)localhost:6380,ssl=true,password=pass- SSL/TLS connection- Multiple hosts:
server1:6379,server2:6379,password=pass
- Automatic Invalidation: Cache is automatically cleared when flags are created, updated, or deleted
- Configurable Expiration: Set custom cache duration (default: 5 minutes)
- Manual Refresh: Call
RefreshCacheAsync()to manually reload cache - Thread-Safe: All cache operations are thread-safe
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License.
Check out the sample project in samples/Flaggy.Sample.WebApi for a complete working example with PostgreSQL.
# Start PostgreSQL with Docker
cd samples/Flaggy.Sample.WebApi
docker-compose up -d postgres
# Run the application
dotnet runThe application will:
- Automatically create database tables
- Seed 6 demo feature flags
- Create default admin user (admin/admin)
Then navigate to https://localhost:5001/flaggy to see the dashboard.
See the Sample Project README for more details.
Made with ❤️ for the .NET community