Skip to content

MuratDincc/Flaggy

Repository files navigation

Flaggy Logo

Flaggy

A simple, fast, and modular feature flag library for .NET

FeaturesQuick StartPackagesProvidersSchemaContributing


Features

  • 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

Packages

Package Description NuGet
Flaggy Core library with abstractions, services, MemoryCache, Redis, and UI dashboard NuGet
Flaggy.Provider.MySQL MySQL storage provider with auto-migration NuGet
Flaggy.Provider.PostgreSQL PostgreSQL storage provider with auto-migration NuGet
Flaggy.Provider.MsSql Microsoft SQL Server storage provider with auto-migration NuGet

Quick Start

1. Install Packages

# Install core library with a provider
dotnet add package Flaggy
dotnet add package Flaggy.Provider.MySQL

# Optional: Add UI dashboard
dotnet add package Flaggy.UI

2. Configure Services

using 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();

3. Use Feature Flags

Simple Enabled/Disabled Check

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" });
});

Using Flag Values

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. GetValueAsync returns the defaultValue (or null if no default provided).
  • Enabled = true: Flag is active. GetValueAsync returns the flag's value.
  • Flag doesn't exist: GetValueAsync returns the defaultValue (or null if no default provided).
  • Value: Can be any string. Use GetValueAsync<T>() for type conversion (int, double, bool, etc.).

4. Access Dashboard

Navigate to https://localhost:5001/flaggy to manage your feature flags through the web UI.

Provider Configuration

InMemory Provider (Development)

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

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 version

PostgreSQL Provider

PostgreSQL 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 version

MS SQL Server Provider

MS 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 version

Programmatic Flag Management (Without UI)

You can manage feature flags programmatically without using the web UI. Flaggy provides extension methods and helpers for easy flag management.

Basic Operations

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");
    }
}

Extension Methods

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();

Fluent API Builder

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();

Seeding Flags at Startup

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();

Console Application Example

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 ? "✓" : "✗")}");
}

Complete Examples

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

Advanced Usage

Custom Authorization for Dashboard

app.UseFlaggyUI(options =>
{
    options.RoutePrefix = "/admin/flags";
    options.RequireAuthorization = true;
    options.AuthorizationFilter = context =>
    {
        // Custom authorization logic
        return context.User.IsInRole("Admin");
    };
});

Manual Cache Refresh

app.MapPost("/api/flags/refresh", async (IFeatureFlagService flagService) =>
{
    await flagService.RefreshCacheAsync();
    return Results.Ok(new { message = "Cache refreshed" });
});

Get Flag Details

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);
});

Get All Flags

app.MapGet("/api/flags", async (IFeatureFlagService flagService) =>
{
    var flags = await flagService.GetAllFlagsAsync();
    return Results.Ok(flags);
});

Custom Provider

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());

Database Schema

Note: These tables are created automatically by the migration system. You don't need to run these scripts manually!

MySQL

-- 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
);

PostgreSQL

-- 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
);

Migration System

Flaggy includes a built-in migration system with version tracking. When you provide a connection string:

  1. Version Table Creation: On first run, flaggy_migrations table is created automatically
  2. Version Check: Checks current database version against available migrations
  3. Auto-Upgrade: Runs pending migrations in order (Version 1, 2, 3, etc.)
  4. Version Tracking: Each migration is recorded with version, description, and timestamp

Disable Auto-Migration

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));
});

Migration History

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.

Caching

Flaggy provides flexible caching strategies to optimize performance:

Memory Cache (Default)

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)
);

Redis Cache

For distributed scenarios, use Redis caching:

dotnet add package Flaggy.Caching.Redis
using 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 authentication
  • localhost:6379,password=pass - Password only
  • localhost: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

Cache Features

  • 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

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the MIT License.

Sample Project

Check out the sample project in samples/Flaggy.Sample.WebApi for a complete working example with PostgreSQL.

Quick Start

# Start PostgreSQL with Docker
cd samples/Flaggy.Sample.WebApi
docker-compose up -d postgres

# Run the application
dotnet run

The 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

About

A simple, fast, and modular feature flag library for .NET

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

No packages published