Propel.FeatureFlags.SqlServer
1.0.0-beta.1
See the version list below for details.
dotnet add package Propel.FeatureFlags.SqlServer --version 1.0.0-beta.1
NuGet\Install-Package Propel.FeatureFlags.SqlServer -Version 1.0.0-beta.1
<PackageReference Include="Propel.FeatureFlags.SqlServer" Version="1.0.0-beta.1" />
<PackageVersion Include="Propel.FeatureFlags.SqlServer" Version="1.0.0-beta.1" />
<PackageReference Include="Propel.FeatureFlags.SqlServer" />
paket add Propel.FeatureFlags.SqlServer --version 1.0.0-beta.1
#r "nuget: Propel.FeatureFlags.SqlServer, 1.0.0-beta.1"
#:package Propel.FeatureFlags.SqlServer@1.0.0-beta.1
#addin nuget:?package=Propel.FeatureFlags.SqlServer&version=1.0.0-beta.1&prerelease
#tool nuget:?package=Propel.FeatureFlags.SqlServer&version=1.0.0-beta.1&prerelease
Propel.FeatureFlags
A type-safe feature flag library for .NET that separates continuous delivery from release management. Developers define flags in code, product owners control releases through configuration.
Table of Contents
- Overview
- Installation
- Quick Start
- Configuration
- Middleware Configuration
- Evaluation Modes
- Flag Factory Pattern
- Application vs Global Flags
- Working in legacy application
- Best Practices
- Examples
- Package Reference
- Management Dashboard
- Contributing
- License
- Support
Overview
The Problem
Traditional feature flag implementations couple developers to release decisions:
// Magic strings, no type safety, hard to find during cleanup
if (config["new-feature"] == "true")
{
// New implementation
}
// PO wants scheduled release? Developer must change code to add scheduling logic
// PO wants percentage rollout? Developer must implement rollout logic
// PO wants to target specific users? More developer work...
This makes developers responsible for release timing, rollout strategies, and configuration management instead of focusing on building features.
The Solution
Propel separates concerns: developers define flags as strongly-typed classes, product owners configure release strategies through a management dashboard.
Developer defines the flag once:
public class NewCheckoutFeatureFlag : FeatureFlagBase
{
public NewCheckoutFeatureFlag()
: base(key: "new-checkout",
name: "New Checkout Flow",
description: "Enhanced checkout with improved UX",
onOfMode: EvaluationMode.Off) // Deploy disabled by default
{
}
}
Developer uses the flag in code:
var flag = new NewCheckoutFeatureFlag();
if (await context.IsFeatureFlagEnabledAsync(flag))
{
return NewCheckoutImplementation();
}
return LegacyCheckoutImplementation();
Product owner configures release strategy (no developer involvement):
- Schedule: Enable feature on specific date/time
- Time windows: Enable only during business hours
- User targeting: Enable for specific users or user groups
- Percentage rollout: Gradually roll out to 10%, 25%, 50%, 100% of users
- Targeting rules: Enable based on custom attributes (region, tier, department, etc.)
Key Benefits
- Type Safety: Compile-time validation prevents runtime errors from typos or missing flags
- Easy Maintenance: Find-all-references works; no magic strings scattered across config files
- Clean Code Hygiene: Delete the flag class when done, compiler tells you everywhere it was used
- Attribute-Based Flags: Decorate methods with
[FeatureFlagged]to keep your business logic clean - Auto-Deployment: Flags automatically register in database on application startup
- Zero Configuration: Works out of the box with sensible defaults
Installation
# Core library (required)
dotnet add package Propel.FeatureFlags
# ASP.NET Core integration
dotnet add package Propel.FeatureFlags.AspNetCore
# Database persistence (choose one)
dotnet add package Propel.FeatureFlags.Infrastructure.PostgresSql
dotnet add package Propel.FeatureFlags.Infrastructure.SqlServer
# Caching (optional but recommended)
dotnet add package Propel.FeatureFlags.Infrastructure.Redis
# AOP-style attributes (optional)
dotnet add package Propel.FeatureFlags.Attributes
Quick Start
1. Configure Services
var builder = WebApplication.CreateBuilder(args);
builder.ConfigureFeatureFlags(config =>
{
config.RegisterFlagsWithContainer = true; // Auto-register all flags in DI
config.EnableFlagFactory = true; // Enable type-safe flag retrieval
config.SqlConnection = builder.Configuration
.GetConnectionString("DefaultConnection")!;
config.Cache = new CacheOptions
{
EnableInMemoryCache = true,
CacheDurationInMinutes = TimeSpan.FromMinutes(30),
SlidingDurationInMinutes = TimeSpan.FromMinutes(10)
};
// For ASP.NET Core apps with HTTP context-based targeting
config.Interception.EnableHttpIntercepter = true;
// For console apps or non-HTTP scenarios
// config.Interception.EnableIntercepter = true;
})
.AddRedisCache(builder.Configuration.GetConnectionString("RedisConnection")!);
var app = builder.Build();
// Initialize database (development only - use migrations in production)
if (app.Environment.IsDevelopment())
{
await app.InitializeFeatureFlagsDatabase();
}
// Auto-deploy all flags defined in code
await app.AutoDeployFlags();
// Add middleware for global flag evaluation and context extraction
app.UseFeatureFlags();
app.Run();
2. Define Feature Flags
public class NewProductApiFeatureFlag : FeatureFlagBase
{
public NewProductApiFeatureFlag()
: base(key: "new-product-api",
name: "New Product API",
description: "Enhanced product API with improved performance",
onOfMode: EvaluationMode.Off) // Start disabled
{
}
}
3. Evaluate Flags
Direct evaluation:
app.MapGet("/products", async (HttpContext context) =>
{
var flag = new NewProductApiFeatureFlag();
if (await context.IsFeatureFlagEnabledAsync(flag))
{
return Results.Ok(GetProductsV2());
}
return Results.Ok(GetProductsV1());
});
Using factory pattern:
app.MapGet("/products", async (HttpContext context, IFeatureFlagFactory factory) =>
{
var flag = factory.GetFlagByType<NewProductApiFeatureFlag>();
if (await context.IsFeatureFlagEnabledAsync(flag))
{
return Results.Ok(GetProductsV2());
}
return Results.Ok(GetProductsV1());
});
Getting variations:
app.MapGet("/recommendations/{userId}", async (HttpContext context) =>
{
var flag = new RecommendationAlgorithmFeatureFlag();
var algorithm = await context.GetFeatureFlagVariationAsync(flag, "collaborative-filtering");
return algorithm switch
{
"machine-learning" => GetMLRecommendations(userId),
"content-based" => GetContentBasedRecommendations(userId),
_ => GetCollaborativeRecommendations(userId)
};
});
4. Attribute-Based Flags (Optional)
For the cleanest code, use attributes to automatically call fallback methods when flags are disabled:
// Register service with interceptor support
builder.Services.RegisterWithFeatureFlagInterception<INotificationService, NotificationService>();
public interface INotificationService
{
Task<string> SendEmailAsync(string userId, string subject, string body);
Task<string> SendEmailLegacyAsync(string userId, string subject, string body);
}
public class NotificationService : INotificationService
{
[FeatureFlagged(type: typeof(NewEmailServiceFeatureFlag),
fallbackMethod: nameof(SendEmailLegacyAsync))]
public virtual async Task<string> SendEmailAsync(string userId, string subject, string body)
{
// New implementation - called when flag is enabled
return "Email sent using new service";
}
public virtual async Task<string> SendEmailLegacyAsync(string userId, string subject, string body)
{
// Fallback - called when flag is disabled
return "Email sent using legacy service";
}
}
Configuration
PropelConfiguration Options
builder.ConfigureFeatureFlags(config =>
{
// Auto-register all IFeatureFlag implementations in DI container
config.RegisterFlagsWithContainer = true;
// Enable IFeatureFlagFactory for type-safe flag access
config.EnableFlagFactory = true;
// Database connection string
config.SqlConnection = "Host=localhost;Database=propel;...";
// Default timezone for scheduled flags and time windows
config.DefaultTimeZone = "UTC";
// Caching configuration
config.Cache = new CacheOptions
{
EnableInMemoryCache = true, // Per-instance cache
EnableDistributedCache = false, // Requires Redis
CacheDurationInMinutes = TimeSpan.FromMinutes(30),
SlidingDurationInMinutes = TimeSpan.FromMinutes(10)
};
// AOP interceptor configuration
config.Interception = new AOPOptions
{
EnableHttpIntercepter = true, // For ASP.NET Core apps
EnableIntercepter = false // For console apps
};
});
Database Setup
PostgreSQL:
builder.ConfigureFeatureFlags(config =>
{
config.SqlConnection = builder.Configuration
.GetConnectionString("DefaultConnection")!;
});
// In development, auto-initialize database
if (app.Environment.IsDevelopment())
{
await app.InitializeFeatureFlagsDatabase();
}
// Optional: seed with SQL script
// await app.Services.SeedFeatureFlags("seed-db.sql");
SQL Server:
using Propel.FeatureFlags.SqlServer.Extensions;
builder.ConfigureFeatureFlags(config =>
{
config.SqlConnection = builder.Configuration
.GetConnectionString("DefaultConnection")!;
});
Both databases support identical functionality. The only difference is in the generated SQL queries.
Caching
In-Memory Cache (single instance, fastest):
config.Cache = new CacheOptions
{
EnableInMemoryCache = true
};
Distributed Cache (Redis, shared across instances):
builder.ConfigureFeatureFlags(config => { ... })
.AddRedisCache("localhost:6379");
config.Cache = new CacheOptions
{
EnableDistributedCache = true,
CacheDurationInMinutes = TimeSpan.FromMinutes(30),
SlidingDurationInMinutes = TimeSpan.FromMinutes(10)
};
No Cache (not recommended for production):
config.Cache = new CacheOptions
{
EnableInMemoryCache = false,
EnableDistributedCache = false
};
Middleware Configuration
The middleware handles global flags, maintenance mode, and extracts context (user ID, attributes) from HTTP requests.
Basic Configuration
app.UseFeatureFlags(); // Default configuration
Maintenance Mode
Enable global kill switch for the entire API:
app.UseFeatureFlags(options =>
{
options.EnableMaintenanceMode = true;
options.MaintenanceFlagKey = "api-maintenance";
options.MaintenanceResponse = new
{
message = "API is temporarily down for maintenance",
estimatedDuration = "30 minutes",
contact = "support@company.com"
};
});
When the api-maintenance global flag is enabled, all requests return 503 with the configured response.
Custom User ID Extraction
Extract user identity from JWT tokens, API keys, or headers:
app.UseFeatureFlags(options =>
{
options.UserIdExtractor = context =>
{
// Try JWT sub claim
var jwtUserId = context.User.FindFirst("sub")?.Value;
if (!string.IsNullOrEmpty(jwtUserId)) return jwtUserId;
// Try API key
var apiKey = context.Request.Headers["X-API-Key"].FirstOrDefault();
if (!string.IsNullOrEmpty(apiKey)) return $"api:{apiKey}";
// Try session ID
return context.Request.Headers["X-Session-ID"].FirstOrDefault();
};
});
Attribute Extractors for Targeting Rules
Extract attributes from requests to enable complex targeting:
app.UseFeatureFlags(options =>
{
options.AttributeExtractors.Add(context =>
{
var attributes = new Dictionary<string, object>();
// Extract tenant information
if (context.Request.Headers.TryGetValue("X-Tenant-ID", out var tenantId))
attributes["tenantId"] = tenantId.ToString();
// Extract user tier from JWT
var userTier = context.User.FindFirst("tier")?.Value;
if (!string.IsNullOrEmpty(userTier))
attributes["userTier"] = userTier;
// Extract geographic info
if (context.Request.Headers.TryGetValue("Country", out var country))
attributes["country"] = country.ToString();
return attributes;
});
});
Complete Example
Combine maintenance mode with attribute extraction:
app.UseFeatureFlags(options =>
{
// Enable maintenance mode
options.EnableMaintenanceMode = true;
options.MaintenanceFlagKey = "api-maintenance";
// Custom user extraction
options.UserIdExtractor = context =>
{
return context.User.Identity?.Name ??
context.Request.Headers["User-Id"].FirstOrDefault();
};
// Extract targeting attributes
options.AttributeExtractors.Add(context =>
{
var attributes = new Dictionary<string, object>();
if (context.Request.Headers.TryGetValue("Role", out var role))
attributes["role"] = role.ToString();
if (context.Request.Headers.TryGetValue("Department", out var dept))
attributes["department"] = dept.ToString();
return attributes;
});
});
Evaluation Modes
Propel supports 9 evaluation modes that can be configured from the management dashboard:
| Mode | Description | Use Case |
|---|---|---|
Off |
Flag is disabled | Default safe state, kill switches |
On |
Flag is enabled | Enable completed features |
Scheduled |
Time-based activation | Coordinated releases, marketing campaigns |
TimeWindow |
Daily/weekly time ranges | Business hours features, maintenance windows |
UserTargeted |
Specific user allowlists/blocklists | Beta testing, VIP access |
UserRolloutPercentage |
Percentage-based user rollout | Gradual rollouts, A/B testing |
TenantRolloutPercentage |
Percentage-based tenant rollout | Multi-tenant gradual rollouts |
TenantTargeted |
Specific tenant allowlists/blocklists | Tenant-specific features |
TargetingRules |
Custom attribute-based rules | Complex targeting (region, tier, etc.) |
Note: Developers define flags with OnOffMode set to either On or Off. All other evaluation modes are configured by product owners through the management dashboard.
Flag Factory Pattern
For centralized flag management in larger codebases:
public interface IFeatureFlagFactory
{
IFeatureFlag? GetFlagByKey(string key);
IFeatureFlag? GetFlagByType<T>() where T : IFeatureFlag;
IEnumerable<IFeatureFlag> GetAllFlags();
}
// Automatically registered when EnableFlagFactory = true
app.MapGet("/products", async (IFeatureFlagFactory factory, HttpContext context) =>
{
var flag = factory.GetFlagByType<NewProductApiFeatureFlag>();
if (await context.IsFeatureFlagEnabledAsync(flag))
{
return Results.Ok("New API");
}
return Results.Ok("Legacy API");
});
Application vs Global Flags
Application Flags
Application flags are defined in code and scoped to specific applications. They auto-deploy on startup.
public class MyFeatureFlag : FeatureFlagBase
{
public MyFeatureFlag()
: base(key: "my-feature",
name: "My Feature",
description: "Description of my feature",
onOfMode: EvaluationMode.Off)
{
}
}
// Usage
var flag = new MyFeatureFlag();
var isEnabled = await context.IsFeatureFlagEnabledAsync(flag);
Global Flags
Global flags are created through the management dashboard and apply system-wide across all applications. Use them for:
- Maintenance mode
- API deprecation
- System-wide kill switches
// Global flags are evaluated by key only
var isEnabled = await globalFlagClient.IsEnabledAsync("api-maintenance");
Important: Global flags cannot be defined in code. They must be created through the dashboard.
Best Practices
1. Default to Disabled
Always set onOfMode: EvaluationMode.Off when defining new flags. This allows you to deploy code without immediately releasing the feature.
public class NewFeatureFlag : FeatureFlagBase
{
public NewFeatureFlag()
: base(key: "new-feature",
name: "New Feature",
description: "Description",
onOfMode: EvaluationMode.Off) // ✅ Deploy first, release later
{
}
}
This separates deployment from release. Developers deploy the code, product owners control when it's released by enabling the flag in the dashboard.
2. Never Use Feature Flags for Business Logic
Feature flags control how your application behaves technically, not what it does from a business perspective.
❌ Wrong - Business Logic:
// DON'T use flags to control which users have access to premium features
if (await IsFeatureFlagEnabled("premium-features"))
{
return GetPremiumContent();
}
// DON'T use flags for pricing tiers
if (await IsFeatureFlagEnabled("enterprise-pricing"))
{
return CalculateEnterprisePricing();
}
// DON'T use flags for user permissions
if (await IsFeatureFlagEnabled("admin-access"))
{
return AdminDashboard();
}
✅ Correct - Technical Implementation:
// DO use flags to test new technical implementations
if (await IsFeatureFlagEnabled("new-pricing-algorithm"))
{
return CalculatePricingV2(); // New algorithm
}
return CalculatePricingV1(); // Old algorithm
// DO use flags for infrastructure changes
if (await IsFeatureFlagEnabled("new-email-service"))
{
return SendEmailViaSendGrid();
}
return SendEmailViaLegacySmtp();
// DO use flags for UI component changes
if (await IsFeatureFlagEnabled("new-checkout-ui"))
{
return RenderNewCheckoutComponent();
}
return RenderLegacyCheckoutComponent();
Why this matters: Business logic belongs in your domain layer and should be driven by data (user roles, subscription tiers, entitlements). Feature flags are for deployment safety, gradual rollouts, and A/B testing technical implementations.
3. Clean Up Expired Flags
Feature flags are temporary. Delete them when:
- The feature has been fully rolled out to all users
- You've decided to keep one implementation and remove the other
- The flag has expired (check the management dashboard for expiration dates)
Cleanup process:
- Delete the flag class from your codebase
- Remove all references (compiler will help you find them)
- Delete the flag from the database through the management dashboard
Important: If you only delete from the database, the flag will be auto-created again on the next deployment. Always delete from code first.
// After rollout is complete and you've decided to keep v2
// 1. Delete this class
public class NewCheckoutFeatureFlag : FeatureFlagBase { ... }
// 2. Replace conditional code with the new implementation
// Before:
if (await context.IsFeatureFlagEnabledAsync(new NewCheckoutFeatureFlag()))
{
return NewCheckout();
}
return LegacyCheckout();
// After:
return NewCheckout(); // New implementation is now the standard
// 3. Delete flag from database via dashboard
4. Use Descriptive Names and Descriptions
Good flag definitions help everyone understand the flag's purpose:
public class EnhancedRecommendationEngineFeatureFlag : FeatureFlagBase
{
public EnhancedRecommendationEngineFeatureFlag()
: base(
key: "enhanced-recommendation-engine",
name: "Enhanced Recommendation Engine",
description: "Enables ML-based recommendation engine with improved accuracy. " +
"Falls back to collaborative filtering if disabled. " +
"Safe to roll out gradually to measure performance impact.",
onOfMode: EvaluationMode.Off)
{
}
}
5. Safety Through Auto-Recreation
Propel automatically recreates flags that are missing from the database. This prevents production errors if a flag is accidentally deleted from the database.
If a flag is evaluated but doesn't exist in the database:
- Propel creates it with the default state from code (
onOfMode) - The evaluation continues using the default state
- Product owners can then configure the flag in the dashboard
This safety mechanism ensures your application never crashes due to missing flags.
Examples
Complete working examples are available in the /usage-demo directory:
WebClientDemo - ASP.NET Core Web API demonstrating:
- Simple on/off flags
- Scheduled releases
- Time window flags
- Percentage rollouts with variations
- User targeting with custom attributes
- Attribute-based flags with interceptors
ConsoleAppDemo - Console worker application demonstrating:
- Background service integration
- Attribute-based flags without HTTP context
- Direct flag evaluation via
IApplicationFlagClient - Using
IFeatureFlagFactoryfor type-safe access
Legacy .NET Framework - Working with full .NET Framework applications (documentation)
Package Reference
| Package | Purpose | Target Framework |
|---|---|---|
Propel.FeatureFlags |
Core library and interfaces | .NET Standard 2.0 |
Propel.FeatureFlags.AspNetCore |
ASP.NET Core middleware and extensions | .NET 9.0 |
Propel.FeatureFlags.Infrastructure.PostgresSql |
PostgreSQL persistence | .NET 9.0 |
Propel.FeatureFlags.Infrastructure.SqlServer |
SQL Server persistence | .NET 9.0 |
Propel.FeatureFlags.Infrastructure.Redis |
Redis distributed caching | .NET Standard 2.0 |
Propel.FeatureFlags.Attributes |
AOP-style method attributes | .NET 9.0 |
Management Dashboard
The Propel.FeatureFlags.Dashboard provides a web interface for product owners to:
- View all application flags deployed from code
- Configure evaluation modes (scheduled, time windows, targeting, rollouts)
- Set up targeting rules based on custom attributes
- Monitor flag usage and expiration dates
- Manage global flags for system-wide concerns
The dashboard is under development and will be released separately.
Contributing
Contributions are welcome! See CONTRIBUTING.md for guidelines.
License
Apache-2.0 License - see LICENSE file for details.
Support
- Issues: GitHub Issues
- Documentation: Wiki
- Examples: usage-demo/
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net9.0
- Microsoft.Data.SqlClient (>= 6.1.1)
- Propel.FeatureFlags.AspNetCore (>= 1.0.0-beta.1)
- Propel.FeatureFlags.Attributes (>= 1.0.0-beta.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.2.1 | 171 | 10/20/2025 |
| 2.2.1-beta.1.2 | 118 | 10/19/2025 |
| 2.1.1-beta.1.2 | 43 | 10/18/2025 |
| 2.1.0-beta.1.2 | 120 | 10/16/2025 |
| 2.0.0-beta.1.2 | 124 | 10/14/2025 |
| 1.0.1-beta.1 | 121 | 10/7/2025 |
| 1.0.0-beta.1 | 123 | 10/7/2025 |