Skip to content

Programmatic API

Flaggy provides a comprehensive programmatic API for managing feature flags without using the web dashboard. This includes extension methods, fluent builders, and helper utilities for seamless flag management in code.

Overview

The programmatic API offers:

  • Full CRUD Operations: Create, read, update, delete flags
  • Extension Methods: Convenient helpers for common operations
  • Fluent Builder API: Chainable flag configuration
  • Seeding Support: Initialize flags at startup
  • Batch Operations: Manage multiple flags efficiently
  • Type-Safe: Strongly-typed flag operations
  • Async/Await: All operations are asynchronous

Core Interface

All flag operations go through IFeatureFlagService:

public interface IFeatureFlagService
{
    // Query operations
    Task<bool> IsEnabledAsync(string key, CancellationToken cancellationToken = default);
    Task<string?> GetValueAsync(string key, string? defaultValue = null, CancellationToken cancellationToken = default);
    Task<T?> GetValueAsync<T>(string key, T? defaultValue = null, CancellationToken cancellationToken = default) where T : struct;
    Task<FeatureFlag?> GetFlagAsync(string key, CancellationToken cancellationToken = default);
    Task<IEnumerable<FeatureFlag>> GetAllFlagsAsync(CancellationToken cancellationToken = default);

    // CRUD operations
    Task<bool> CreateFlagAsync(FeatureFlag flag, CancellationToken cancellationToken = default);
    Task<bool> UpdateFlagAsync(FeatureFlag flag, CancellationToken cancellationToken = default);
    Task<bool> DeleteFlagAsync(string key, CancellationToken cancellationToken = default);

    // Cache management
    Task RefreshCacheAsync(CancellationToken cancellationToken = default);
}

Basic CRUD Operations

Creating Flags

using Flaggy.Abstractions;
using Flaggy.Models;

public class FlagManagementService
{
    private readonly IFeatureFlagService _flagService;

    public FlagManagementService(IFeatureFlagService flagService)
    {
        _flagService = flagService;
    }

    public async Task CreateNewFlag()
    {
        var flag = new FeatureFlag
        {
            Key = "new-feature",
            IsEnabled = false,
            Value = "beta",
            Description = "New feature in beta testing"
        };

        var created = await _flagService.CreateFlagAsync(flag);
        if (created)
        {
            Console.WriteLine("Flag created successfully");
        }
    }
}

Reading Flags

// Get a single flag
public async Task<FeatureFlag?> GetFlag(string key)
{
    return await _flagService.GetFlagAsync(key);
}

// Get all flags
public async Task<IEnumerable<FeatureFlag>> GetAllFlags()
{
    return await _flagService.GetAllFlagsAsync();
}

// Check if enabled
public async Task<bool> CheckIfEnabled(string key)
{
    return await _flagService.IsEnabledAsync(key);
}

// Get flag value
public async Task<string?> GetFlagValue(string key)
{
    return await _flagService.GetValueAsync(key);
}

Updating Flags

public async Task UpdateExistingFlag(string key)
{
    // Get the flag first
    var flag = await _flagService.GetFlagAsync(key);
    if (flag == null)
    {
        Console.WriteLine("Flag not found");
        return;
    }

    // Modify properties
    flag.IsEnabled = true;
    flag.Value = "production";
    flag.Description = "Updated description";

    // Update
    var updated = await _flagService.UpdateFlagAsync(flag);
    if (updated)
    {
        Console.WriteLine("Flag updated successfully");
    }
}

Deleting Flags

public async Task DeleteFlag(string key)
{
    var deleted = await _flagService.DeleteFlagAsync(key);
    if (deleted)
    {
        Console.WriteLine($"Flag {key} deleted successfully");
    }
    else
    {
        Console.WriteLine($"Flag {key} not found");
    }
}

Extension Methods

Flaggy provides convenient extension methods for common operations:

CreateFlagIfNotExistsAsync

Creates a flag only if it doesn't already exist:

using Flaggy.Extensions;

// Create flag only if it doesn't exist
await flagService.CreateFlagIfNotExistsAsync(
    key: "dark-mode",
    isEnabled: true,
    value: "auto",
    description: "Dark mode preference"
);

// Safe to call multiple times - won't fail if flag exists
await flagService.CreateFlagIfNotExistsAsync("dark-mode", isEnabled: false);

UpsertFlagAsync

Creates or updates a flag (insert or update):

using Flaggy.Extensions;

// Creates if doesn't exist, updates if exists
await flagService.UpsertFlagAsync(
    key: "maintenance-mode",
    isEnabled: false,
    value: null,
    description: "Enable maintenance mode"
);

// Update the same flag
await flagService.UpsertFlagAsync(
    key: "maintenance-mode",
    isEnabled: true
);

EnableFlagAsync / DisableFlagAsync

Toggle flag enabled state:

using Flaggy.Extensions;

// Enable a flag
await flagService.EnableFlagAsync("new-feature");

// Disable a flag
await flagService.DisableFlagAsync("old-feature");

ToggleFlagAsync

Toggle flag between enabled and disabled:

using Flaggy.Extensions;

// Toggle flag state
await flagService.ToggleFlagAsync("beta-features");

// If was enabled, now disabled
// If was disabled, now enabled

UpdateFlagValueAsync

Update only the value of a flag:

using Flaggy.Extensions;

// Update only the value
await flagService.UpdateFlagValueAsync("theme", "dark");

// Update to null
await flagService.UpdateFlagValueAsync("config", null);

UpdateFlagDescriptionAsync

Update only the description:

using Flaggy.Extensions;

await flagService.UpdateFlagDescriptionAsync(
    "premium-features",
    "Premium features for tier-1 and tier-2 users"
);

FlagExistsAsync

Check if a flag exists:

using Flaggy.Extensions;

if (await flagService.FlagExistsAsync("new-dashboard"))
{
    Console.WriteLine("Flag exists");
}
else
{
    Console.WriteLine("Flag doesn't exist");
}

GetEnabledFlagsAsync / GetDisabledFlagsAsync

Get filtered lists of flags:

using Flaggy.Extensions;

// Get all enabled flags
var enabledFlags = await flagService.GetEnabledFlagsAsync();
Console.WriteLine($"Enabled: {enabledFlags.Count()}");

// Get all disabled flags
var disabledFlags = await flagService.GetDisabledFlagsAsync();
Console.WriteLine($"Disabled: {disabledFlags.Count()}");

DeleteFlagsAsync

Delete multiple flags at once:

using Flaggy.Extensions;

// Delete multiple flags
var deletedCount = await flagService.DeleteFlagsAsync(new[]
{
    "old-feature-1",
    "old-feature-2",
    "deprecated-flag"
});

Console.WriteLine($"Deleted {deletedCount} flags");

DeleteAllDisabledFlagsAsync

Clean up disabled flags:

using Flaggy.Extensions;

// Delete all disabled flags
var deletedCount = await flagService.DeleteAllDisabledFlagsAsync();
Console.WriteLine($"Cleaned up {deletedCount} disabled flags");

GetFlagsSummaryAsync

Get summary statistics:

using Flaggy.Extensions;

var summary = await flagService.GetFlagsSummaryAsync();
Console.WriteLine($"Total: {summary.TotalCount}");
Console.WriteLine($"Enabled: {summary.EnabledCount}");
Console.WriteLine($"Disabled: {summary.DisabledCount}");

Fluent Builder API

The fluent API provides a clean, chainable interface for flag creation:

Basic Usage

using Flaggy.Helpers;

var initializer = new FeatureFlagInitializer(flagService);

// Create an enabled flag
await initializer.CreateFlag("premium-features")
    .Enabled()
    .WithDescription("Premium features for paid users")
    .WithValue("tier-1,tier-2,tier-3")
    .CreateAsync();

// Create a disabled flag
await initializer.CreateFlag("maintenance-mode")
    .Disabled()
    .WithDescription("Enable maintenance mode")
    .CreateAsync();

CreateIfNotExistsAsync

Create only if doesn't exist:

await initializer.CreateFlag("dark-theme")
    .Enabled()
    .WithValue("auto")
    .WithDescription("Dark theme preference")
    .CreateIfNotExistsAsync();  // Safe to call multiple times

UpsertAsync

Create or update:

await initializer.CreateFlag("api-version")
    .Enabled()
    .WithValue("v2")
    .WithDescription("Current API version")
    .UpsertAsync();  // Updates if exists, creates if doesn't

Fluent API Methods

Method Description Returns
Enabled() Set flag as enabled FlagBuilder
Disabled() Set flag as disabled FlagBuilder
WithValue(string) Set flag value FlagBuilder
WithDescription(string) Set flag description FlagBuilder
CreateAsync() Create flag (fails if exists) Task<bool>
CreateIfNotExistsAsync() Create only if doesn't exist Task<bool>
UpsertAsync() Create or update Task<bool>

Complex Example

var initializer = new FeatureFlagInitializer(flagService);

// Feature rollout flag
await initializer.CreateFlag("new-checkout-flow")
    .Disabled()  // Start disabled
    .WithValue("0.1")  // 10% rollout
    .WithDescription("New checkout flow - gradual rollout")
    .CreateIfNotExistsAsync();

// Configuration flag
await initializer.CreateFlag("max-concurrent-users")
    .Enabled()
    .WithValue("1000")
    .WithDescription("Maximum concurrent users allowed")
    .UpsertAsync();

// A/B test flag
await initializer.CreateFlag("homepage-variant")
    .Enabled()
    .WithValue("variant-b")
    .WithDescription("Homepage A/B test variant")
    .CreateIfNotExistsAsync();

Seeding Flags at Startup

Method 1: Using Extension Method

using Flaggy.Extensions;
using Flaggy.Helpers;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFlaggy(new InMemoryFeatureFlagProvider());

var app = builder.Build();

// Seed flags at startup
await app.SeedFeatureFlagsAsync(
    new FeatureFlag { Key = "welcome-banner", IsEnabled = true, Description = "Show welcome banner" },
    new FeatureFlag { Key = "new-dashboard", IsEnabled = false, Description = "New dashboard UI" },
    new FeatureFlag { Key = "beta-features", IsEnabled = false, Value = "v1", Description = "Beta features" }
);

app.Run();

Method 2: Using Initializer

using Flaggy.Helpers;

await app.InitializeFeatureFlagsAsync(async flagService =>
{
    var initializer = new FeatureFlagInitializer(flagService);

    await initializer.CreateFlag("notifications")
        .Enabled()
        .WithDescription("Email notifications")
        .CreateIfNotExistsAsync();

    await initializer.CreateFlag("analytics")
        .Enabled()
        .WithValue("google-analytics-4")
        .WithDescription("Analytics provider")
        .CreateIfNotExistsAsync();

    await initializer.CreateFlag("feature-preview")
        .Disabled()
        .WithDescription("Show feature previews")
        .CreateIfNotExistsAsync();
});

Method 3: Manual Seeding

using (var scope = app.Services.CreateScope())
{
    var flagService = scope.ServiceProvider.GetRequiredService<IFeatureFlagService>();

    // Define default flags
    var defaultFlags = new[]
    {
        new FeatureFlag
        {
            Key = "maintenance-mode",
            IsEnabled = false,
            Description = "Enable maintenance mode"
        },
        new FeatureFlag
        {
            Key = "max-upload-size",
            IsEnabled = true,
            Value = "10485760",  // 10MB in bytes
            Description = "Maximum file upload size"
        },
        new FeatureFlag
        {
            Key = "rate-limit",
            IsEnabled = true,
            Value = "100",  // Requests per minute
            Description = "API rate limit per user"
        }
    };

    // Seed flags
    foreach (var flag in defaultFlags)
    {
        await flagService.CreateFlagIfNotExistsAsync(
            flag.Key,
            flag.IsEnabled,
            flag.Value,
            flag.Description
        );
    }
}

Using Flags in Application Code

In Controllers

using Microsoft.AspNetCore.Mvc;
using Flaggy.Abstractions;

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IFeatureFlagService _flagService;
    private readonly IProductService _productService;

    public ProductsController(IFeatureFlagService flagService, IProductService productService)
    {
        _flagService = flagService;
        _productService = productService;
    }

    [HttpGet]
    public async Task<IActionResult> GetProducts()
    {
        // Check feature flag
        var useNewApi = await _flagService.IsEnabledAsync("new-product-api");

        if (useNewApi)
        {
            var products = await _productService.GetProductsV2Async();
            return Ok(products);
        }
        else
        {
            var products = await _productService.GetProductsV1Async();
            return Ok(products);
        }
    }

    [HttpGet("pricing")]
    public async Task<IActionResult> GetPricing()
    {
        // Get discount rate from flag
        var discountRate = await _flagService.GetValueAsync<double>("discount-rate", defaultValue: 0.0);
        var basePrice = 99.99;
        var finalPrice = basePrice * (1 - discountRate.Value);

        return Ok(new { basePrice, discountRate, finalPrice });
    }
}

In Services

public class EmailService
{
    private readonly IFeatureFlagService _flagService;
    private readonly IEmailProvider _emailProvider;

    public EmailService(IFeatureFlagService flagService, IEmailProvider emailProvider)
    {
        _flagService = flagService;
        _emailProvider = emailProvider;
    }

    public async Task SendWelcomeEmailAsync(string email)
    {
        // Check if email notifications are enabled
        var emailEnabled = await _flagService.IsEnabledAsync("email-notifications");
        if (!emailEnabled)
        {
            return;  // Skip sending
        }

        // Get email template version
        var templateVersion = await _flagService.GetValueAsync("email-template", defaultValue: "v1");

        await _emailProvider.SendAsync(email, $"welcome-{templateVersion}.html");
    }
}

In Minimal APIs

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

app.MapPost("/api/features/{key}/toggle", async (string key, IFeatureFlagService flagService) =>
{
    await flagService.ToggleFlagAsync(key);
    return Results.Ok(new { message = $"Flag {key} toggled" });
});

app.MapGet("/api/config", async (IFeatureFlagService flagService) =>
{
    var maxUsers = await flagService.GetValueAsync<int>("max-users", defaultValue: 100);
    var apiVersion = await flagService.GetValueAsync("api-version", defaultValue: "v1");
    var maintenanceMode = await flagService.IsEnabledAsync("maintenance-mode");

    return Results.Ok(new
    {
        maxUsers = maxUsers.Value,
        apiVersion,
        maintenanceMode
    });
});

In Middleware

public class MaintenanceModeMiddleware
{
    private readonly RequestDelegate _next;

    public MaintenanceModeMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context, IFeatureFlagService flagService)
    {
        // Check maintenance mode flag
        var maintenanceMode = await flagService.IsEnabledAsync("maintenance-mode");

        if (maintenanceMode)
        {
            context.Response.StatusCode = 503;
            await context.Response.WriteAsJsonAsync(new
            {
                error = "Service temporarily unavailable",
                message = "System is currently under maintenance"
            });
            return;
        }

        await _next(context);
    }
}

// Register middleware
app.UseMiddleware<MaintenanceModeMiddleware>();

Console Application

Use Flaggy in console applications:

using Flaggy.Extensions;
using Flaggy.Providers;
using Microsoft.Extensions.DependencyInjection;

class Program
{
    static async Task Main(string[] args)
    {
        // Setup dependency injection
        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 = "debug-mode",
            IsEnabled = true,
            Description = "Enable debug logging"
        });

        await flagService.CreateFlagAsync(new FeatureFlag
        {
            Key = "max-retries",
            IsEnabled = true,
            Value = "3",
            Description = "Maximum retry attempts"
        });

        // Use flags
        var debugMode = await flagService.IsEnabledAsync("debug-mode");
        if (debugMode)
        {
            Console.WriteLine("Debug mode enabled");
        }

        var maxRetries = await flagService.GetValueAsync<int>("max-retries", defaultValue: 1);
        Console.WriteLine($"Max retries: {maxRetries}");

        // List all flags
        Console.WriteLine("\nAll flags:");
        var flags = await flagService.GetAllFlagsAsync();
        foreach (var flag in flags)
        {
            Console.WriteLine($"  {flag.Key}: {(flag.IsEnabled ? "" : "")} (value: {flag.Value ?? "null"})");
        }

        // Get summary
        var summary = await flagService.GetFlagsSummaryAsync();
        Console.WriteLine($"\nTotal: {summary.TotalCount}, Enabled: {summary.EnabledCount}, Disabled: {summary.DisabledCount}");
    }
}

Best Practices

1. Use Dependency Injection

Always inject IFeatureFlagService:

public class MyService
{
    private readonly IFeatureFlagService _flagService;

    public MyService(IFeatureFlagService flagService)
    {
        _flagService = flagService;
    }
}

2. Use Extension Methods

Prefer extension methods for common operations:

// Good
await flagService.EnableFlagAsync("new-feature");

// Less convenient
var flag = await flagService.GetFlagAsync("new-feature");
flag.IsEnabled = true;
await flagService.UpdateFlagAsync(flag);

3. Provide Default Values

Always provide sensible defaults:

// Good - has default
var maxUsers = await flagService.GetValueAsync<int>("max-users", defaultValue: 100);

// Risky - null if flag doesn't exist
var maxUsers = await flagService.GetValueAsync<int>("max-users");

4. Use Fluent API for Seeding

Use fluent API for readable flag initialization:

// Good - readable and clear
await initializer.CreateFlag("feature")
    .Enabled()
    .WithValue("v2")
    .WithDescription("Feature description")
    .CreateIfNotExistsAsync();

// Less readable
await flagService.CreateFlagIfNotExistsAsync("feature", true, "v2", "Feature description");

5. Handle Missing Flags Gracefully

var flag = await flagService.GetFlagAsync("unknown-flag");
if (flag == null)
{
    // Handle missing flag
    _logger.LogWarning("Flag {Key} not found", "unknown-flag");
    // Use default behavior
}

6. Batch Operations

Use batch operations when possible:

// Good - single query
var allFlags = await flagService.GetAllFlagsAsync();
var enabledCount = allFlags.Count(f => f.IsEnabled);

// Less efficient - multiple queries
var enabled = await flagService.GetEnabledFlagsAsync();
var enabledCount = enabled.Count();

7. Cache-Aware Operations

Remember that flags are cached:

// Update flag
await flagService.UpdateFlagAsync(flag);

// Cache is automatically invalidated
// Next read will get the updated value
var updatedFlag = await flagService.GetFlagAsync(flag.Key);