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);
Related Topics¶
- Flag Values - Working with typed flag values
- Caching - Understanding cache behavior
- Dashboard - Managing flags through the UI
- Providers - Configuring storage providers