Feature Flags in ConnectSoft Microservice Template¶
Purpose & Overview¶
Feature flags (also known as feature toggles) enable runtime control of application capabilities without code changes or deployments. In the ConnectSoft Microservice Template, feature flags are a first-class capability that allows teams to:
- Release Incrementally: Deploy code without exposing new features until ready
- Separate Deployment from Release: Deploy code changes independently of feature activation
- Enable Kill Switches: Quickly disable problematic features without redeployment
- Target Users: Enable features for specific users, groups, environments, or percentages of traffic
- Experiment Safely: A/B test features and measure impact before full rollout
- Gradual Rollout: Enable features for a percentage of users and gradually increase
The template uses Microsoft.FeatureManagement, a Microsoft-supported library that integrates seamlessly with ASP.NET Core dependency injection, configuration, and middleware.
Feature Flags Philosophy
Feature flags are not optional metadata—they are integrated into REST & gRPC endpoints, use cases, application services, test scenarios, and infrastructure features (telemetry, health checks, caching, metrics). Every new capability should be wrapped in a conditional contract—by default off, but strategically available.
Architecture Overview¶
Feature Flags in ConnectSoft Architecture¶
Configuration (appsettings.json / Azure App Configuration)
↓
FeatureFlagsExtensions.cs
├── Registers IFeatureManager
├── Registers PercentageFilter
└── Registers TimeWindowFilter
↓
Application Code
├── IFeatureManager (injected via DI)
├── FeatureGate Attribute (MVC/gRPC)
├── Use Cases & Application Services
├── Background Jobs
└── Infrastructure Components
Key Integration Points¶
| Layer | Component | Responsibility |
|---|---|---|
| ApplicationModel | FeatureFlagsExtensions.cs |
Service registration and configuration |
| Application | Program.cs |
Azure App Configuration integration (optional) |
| ServiceModel | Controllers, gRPC Services | Route-level gating with FeatureGate attribute |
| DomainModel | Use Cases, Processors | Runtime feature evaluation with IFeatureManager |
| Infrastructure | Background Jobs, Telemetry | Conditional feature execution |
Core Components¶
IFeatureManager¶
The primary interface for checking feature flag states:
public interface IFeatureManager
{
Task<bool> IsEnabledAsync(string feature);
Task<bool> IsEnabledAsync<TContext>(string feature, TContext context);
IAsyncEnumerable<string> GetFeatureNamesAsync();
}
IFeatureManagerSnapshot¶
Cached version of IFeatureManager for per-request consistency:
When evaluating a feature multiple times within the same request, use IFeatureManagerSnapshot to ensure consistent results and reduce configuration reads.
Feature Filters¶
Feature filters enable dynamic evaluation of flags based on rules:
| Filter | Purpose | Use Case |
|---|---|---|
PercentageFilter |
Enables flag for X% of evaluations | Gradual rollout, A/B testing |
TimeWindowFilter |
Enables flag during specific UTC time window | Scheduled releases, trials |
TargetingFilter |
Enables flag for users, groups, or environments | Beta testing, role-based access |
Service Registration¶
FeatureFlagsExtensions.cs¶
Feature flags are registered via the AddMicroserviceFeatureManagement() extension method:
// FeatureFlagsExtensions.cs
internal static class FeatureFlagsExtensions
{
/// <summary>
/// Add and configure feature flags management infrastructure.
/// </summary>
internal static IServiceCollection AddMicroserviceFeatureManagement(
this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);
services.AddFeatureManagement()
.AddFeatureFilter<PercentageFilter>()
.AddFeatureFilter<TimeWindowFilter>();
return services;
}
}
Registration in Startup¶
Feature flags are conditionally registered based on template options:
// MicroserviceRegistrationExtensions.cs
#if FeatureFlags
services.AddMicroserviceFeatureManagement();
#endif
This ensures feature flags are only registered when explicitly enabled during template generation.
Configuration¶
appsettings.json Configuration¶
Feature flags are configured in the FeatureManagement section:
{
"FeatureManagement": {
"Expose Features Api": true,
"UseSemanticKernel": false,
"EnableMetrics": {
"EnabledFor": [
{
"Name": "Percentage",
"Parameters": {
"Value": 50
}
}
]
}
}
}
Flag Types¶
Boolean Flags¶
Simple on/off flags:
true: Feature is always enabledfalse: Feature is always disabled
Filter-Based Flags¶
Flags with conditional evaluation:
{
"FeatureManagement": {
"EnableMetrics": {
"EnabledFor": [
{
"Name": "Percentage",
"Parameters": {
"Value": 25
}
}
]
}
}
}
PercentageFilter Configuration¶
Gradually roll out features to a percentage of requests:
{
"FeatureManagement": {
"EnableMetrics": {
"EnabledFor": [
{
"Name": "Percentage",
"Parameters": {
"Value": 50
}
}
]
}
}
}
- Enables the flag for 50% of evaluations
- Uses hashing to ensure consistent evaluation per user/session (when context provided)
- Useful for A/B testing and gradual rollouts
TimeWindowFilter Configuration¶
Enable features during specific time windows:
{
"FeatureManagement": {
"UseSemanticKernel": {
"EnabledFor": [
{
"Name": "TimeWindow",
"Parameters": {
"Start": "2025-06-01T00:00:00Z",
"End": "2025-07-01T23:59:59Z"
}
}
]
}
}
}
- Times must be in UTC
- Feature is only enabled during the specified window
- Useful for scheduled releases, trials, and time-limited promotions
Multiple Filters¶
Multiple filters can be combined (all must return true):
{
"FeatureManagement": {
"EnableMetrics": {
"EnabledFor": [
{
"Name": "Percentage",
"Parameters": {
"Value": 50
}
},
{
"Name": "TimeWindow",
"Parameters": {
"Start": "2025-06-01T00:00:00Z",
"End": "2025-07-01T23:59:59Z"
}
}
]
}
}
}
The feature is only enabled if:
- The time window is active AND
- The percentage filter evaluates to true
Environment-Specific Configuration¶
Different flags per environment:
// appsettings.Development.json
{
"FeatureManagement": {
"Expose Features Api": true,
"UseSemanticKernel": true
}
}
// appsettings.Production.json
{
"FeatureManagement": {
"Expose Features Api": false,
"UseSemanticKernel": false
}
}
Configuration hierarchy:
1. appsettings.json (base)
2. appsettings.{Environment}.json (overrides base)
3. Azure App Configuration (if configured, highest precedence)
4. Environment variables
5. Command-line arguments
Usage in Code¶
Injecting IFeatureManager¶
Feature manager is injected via dependency injection:
public class MyService
{
private readonly IFeatureManager featureManager;
public MyService(IFeatureManager featureManager)
{
this.featureManager = featureManager ??
throw new ArgumentNullException(nameof(featureManager));
}
}
Basic Feature Check¶
Simple conditional logic:
public async Task<Result> ProcessAsync(Input input)
{
if (await this.featureManager.IsEnabledAsync("UseSemanticKernel"))
{
return await this.aiService.ProcessWithKernel(input);
}
else
{
return await this.aiService.ProcessLegacy(input);
}
}
Guard Pattern¶
Throw exception if feature is disabled:
public async Task<Result> ProcessAsync(Input input)
{
if (!await this.featureManager.IsEnabledAsync("UseSemanticKernel"))
{
throw new FeatureDisabledException(
"UseSemanticKernel feature is currently disabled.");
}
return await this.aiService.ProcessWithKernel(input);
}
Using IFeatureManagerSnapshot¶
For consistent evaluation within a single request:
// Register snapshot
services.AddScoped<IFeatureManagerSnapshot, FeatureManagerSnapshot>();
// Use in service
public class MyService
{
private readonly IFeatureManagerSnapshot featureManagerSnapshot;
public MyService(IFeatureManagerSnapshot featureManagerSnapshot)
{
this.featureManagerSnapshot = featureManagerSnapshot;
}
public async Task ProcessAsync()
{
// Multiple evaluations within same request are consistent
var isEnabled1 = await this.featureManagerSnapshot.IsEnabledAsync("FeatureX");
var isEnabled2 = await this.featureManagerSnapshot.IsEnabledAsync("FeatureX");
// isEnabled1 == isEnabled2 (guaranteed)
}
}
Using FeatureGate Attribute¶
Gate entire controllers or actions:
[ApiController]
[Route("api/[controller]")]
public class FeaturesController : ControllerBase
{
[HttpGet]
[FeatureGate("Expose Features Api")]
public async Task<IActionResult> GetFeatures()
{
// This action is only accessible if "Expose Features Api" is enabled
return Ok(await GetEnabledFeaturesAsync());
}
[HttpPost]
[FeatureGate("AllowBackgroundProcessing")]
public async Task<IActionResult> StartBackgroundJob()
{
// This action requires "AllowBackgroundProcessing" flag
await this.jobService.StartJobAsync();
return Ok();
}
}
When a feature is disabled, FeatureGate returns 404 NotFound by default.
Using FeatureGate with Multiple Flags¶
Require multiple flags:
[FeatureGate("FeatureA", "FeatureB")]
public async Task<IActionResult> RequiresBothFeatures()
{
// Both FeatureA and FeatureB must be enabled
return Ok();
}
Require any flag:
[FeatureGate(RequirementType.Any, "FeatureA", "FeatureB")]
public async Task<IActionResult> RequiresEitherFeature()
{
// Either FeatureA or FeatureB must be enabled
return Ok();
}
Background Jobs and Hosted Services¶
Feature flags in background processing:
public class BackgroundJobService : BackgroundService
{
private readonly IFeatureManager featureManager;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
if (await this.featureManager.IsEnabledAsync("AllowBackgroundProcessing"))
{
await this.ProcessJobsAsync(stoppingToken);
}
else
{
this.logger.LogInformation("Background processing is disabled via feature flag");
}
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
}
}
Azure App Configuration Integration¶
Overview¶
Azure App Configuration provides centralized feature flag management with:
- Dynamic Updates: Change flags without redeployment
- Centralized Control: Manage flags across multiple services
- Environment Labels: Different flags per environment
- Governance: Role-based access control and audit logs
Configuration Setup¶
Enable Azure App Configuration as a feature flags provider:
// Program.cs
#if UseAzureAppConfigurationAsAdditionalConfigurationProvider
configurationBuilder.AddAzureAppConfiguration(options =>
{
string? connectionString = configurationRoot.GetConnectionString("AzureAppConfiguration");
options.Connect(connectionString)
// Load all keys that start with ConnectSoft.MicroserviceTemplate:
.Select("ConnectSoft.MicroserviceTemplate:*", LabelFilter.Null)
// Configure refresh
.ConfigureRefresh(refresh =>
{
refresh.Register("ConnectSoft.MicroserviceTemplate:Settings:Sentinel", refreshAll: true);
refresh.SetRefreshInterval(TimeSpan.FromMinutes(30));
});
#if UseAzureAppConfigurationAsFeatureFlagsProvider
// Configure feature flags
options.UseFeatureFlags(featureFlagsOptions =>
{
featureFlagsOptions.Select("ConnectSoft.MicroserviceTemplate:*", LabelFilter.Null);
featureFlagsOptions.SetRefreshInterval(TimeSpan.FromMinutes(30));
});
#endif
});
#endif
Middleware for Dynamic Refresh¶
Enable dynamic refresh middleware:
// MicroserviceRegistrationExtensions.cs
#if UseAzureAppConfigurationAsAdditionalConfigurationProvider
application.UseMicroserviceAzureAppConfiguration();
#endif
This middleware refreshes configuration (including feature flags) periodically or when the sentinel key changes.
Environment Labels¶
Use labels for environment-specific flags:
options.UseFeatureFlags(featureFlagsOptions =>
{
featureFlagsOptions.Select("ConnectSoft.MicroserviceTemplate:*",
LabelFilter.Null); // Production
// Or use environment-specific labels
featureFlagsOptions.Select("ConnectSoft.MicroserviceTemplate:*",
environment.EnvironmentName); // Development, Staging, etc.
});
Connection String Configuration¶
Store connection string in configuration:
{
"ConnectionStrings": {
"AzureAppConfiguration": "Endpoint=https://{store}.azconfig.io;Id={id};Secret={secret}"
}
}
For production, use managed identity or Key Vault references:
{
"ConnectionStrings": {
"AzureAppConfiguration": "@Microsoft.KeyVault(SecretUri=https://{vault}.vault.azure.net/secrets/{secret})"
}
}
Best Practices¶
Naming Conventions¶
-
Use Descriptive Names
-
Use Domain Prefixes
-
Use PascalCase or Dot Notation
Flag Lifecycle¶
| Stage | Duration | Action |
|---|---|---|
| Temporary Rollout | ≤ 3 months | Remove after full rollout |
| Emergency Kill Switch | ≤ 1 year | Review quarterly |
| Permanent Config | Long-term | Document and maintain |
Do's¶
-
Check Flags Early
-
Log Flag Evaluations
-
Use IFeatureManagerSnapshot for Per-Request Consistency
-
Document Flag Purpose
-
Remove Unused Flags
- Remove from configuration
- Remove from code
- Remove from tests
- Update documentation
Don'ts¶
-
Don't Hide Exceptions Behind Flags
-
Don't Use Flags for A/B Testing Data Collection
// ❌ BAD - Flag controls data collection if (await this.featureManager.IsEnabledAsync("CollectMetrics")) { this.telemetry.TrackEvent(...); } // ✅ GOOD - Always collect, flag controls feature this.telemetry.TrackEvent(...); // Always collect if (await this.featureManager.IsEnabledAsync("UseNewAlgorithm")) { await this.newAlgorithm(); } -
Don't Expose Internal Flags
-
Don't Use Flags for Security
-
Don't Create Too Many Flags
- Keep flag count manageable
- Remove flags after they're no longer needed
- Group related flags under domain prefixes
Testing¶
Unit Testing¶
Mock IFeatureManager in unit tests:
[TestMethod]
public async Task ProcessAsync_FeatureEnabled_ShouldUseNewAlgorithm()
{
// Arrange
var mockFeatureManager = new Mock<IFeatureManager>();
mockFeatureManager
.Setup(m => m.IsEnabledAsync("UseNewAlgorithm", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
var service = new MyService(mockFeatureManager.Object);
// Act
var result = await service.ProcessAsync(input);
// Assert
Assert.IsTrue(result.UsedNewAlgorithm);
}
Integration Testing¶
Use configuration overrides:
[TestMethod]
public async Task Controller_FeatureDisabled_ShouldReturn404()
{
// Arrange
var factory = new WebApplicationFactory<Program>();
var client = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureAppConfiguration((context, config) =>
{
config.AddInMemoryCollection(new Dictionary<string, string>
{
["FeatureManagement:Expose Features Api"] = "false"
});
});
}).CreateClient();
// Act
var response = await client.GetAsync("/api/features");
// Assert
Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode);
}
BDD/Reqnroll Testing¶
Test feature flags in BDD scenarios:
Feature: Feature Flag Behavior
Scenario: When feature is disabled, fallback is used
Given feature "UseSemanticKernel" is disabled
When I send a valid AI request
Then the fallback AI processor is called
Step definition:
[Given(@"feature ""(.*)"" is (enabled|disabled)")]
public void GivenFeatureFlagState(string flagName, string state)
{
var flagState = state == "enabled";
// Override configuration in test context
this.testConfiguration[$"FeatureManagement:{flagName}"] = flagState.ToString();
}
Observability¶
Logging Feature Flag Evaluations¶
Log flag evaluations for debugging:
public async Task<Result> ProcessAsync(Input input)
{
var isEnabled = await this.featureManager.IsEnabledAsync("UseSemanticKernel");
this.logger.LogInformation(
"Feature flag 'UseSemanticKernel' evaluated: {IsEnabled}",
isEnabled);
if (isEnabled)
{
return await this.aiService.ProcessWithKernel(input);
}
return await this.aiService.ProcessLegacy(input);
}
Metrics and Tracing¶
Tag telemetry with feature flag states:
using (var activity = ActivitySource.StartActivity("ProcessRequest"))
{
var isEnabled = await this.featureManager.IsEnabledAsync("UseSemanticKernel");
activity?.SetTag("feature.flag.UseSemanticKernel", isEnabled);
activity?.SetTag("feature.enabled", isEnabled);
if (isEnabled)
{
this.metrics.IncrementCounter("feature.semantickernel.enabled");
return await this.aiService.ProcessWithKernel(input);
}
this.metrics.IncrementCounter("feature.semantickernel.disabled");
return await this.aiService.ProcessLegacy(input);
}
This enables: - Dashboard filtering by feature flag state - Performance comparison between enabled/disabled paths - Error rate analysis per flag state
Common Scenarios¶
Scenario 1: Gradual Feature Rollout¶
{
"FeatureManagement": {
"NewFeature": {
"EnabledFor": [
{
"Name": "Percentage",
"Parameters": {
"Value": 10
}
}
]
}
}
}
Start at 10%, monitor metrics, gradually increase to 50%, then 100%.
Scenario 2: Kill Switch¶
Quickly disable problematic features without redeployment.
Scenario 3: Environment-Specific Features¶
// appsettings.Development.json
{
"FeatureManagement": {
"Expose Features Api": true
}
}
// appsettings.Production.json
{
"FeatureManagement": {
"Expose Features Api": false
}
}
Enable debugging features only in development.
Scenario 4: Time-Limited Promotion¶
{
"FeatureManagement": {
"HolidayPromotion": {
"EnabledFor": [
{
"Name": "TimeWindow",
"Parameters": {
"Start": "2025-12-01T00:00:00Z",
"End": "2025-12-31T23:59:59Z"
}
}
]
}
}
}
Automatically enable/disable features during specific time windows.
Troubleshooting¶
Issue: Feature Flag Not Working¶
Symptoms: Flag always returns false or doesn't change behavior.
Solutions: 1. Verify flag is registered in configuration:
2. Check flag name matches exactly (case-sensitive) 3. VerifyAddMicroserviceFeatureManagement() is called in startup
4. Check Azure App Configuration connection (if used)
Issue: Percentage Filter Always Returns Same Result¶
Symptoms: Percentage filter doesn't distribute correctly.
Solutions: 1. Provide context to ensure consistent hashing:
2. UseIFeatureManagerSnapshot for per-request consistency
Issue: Time Window Not Activating¶
Symptoms: Time window filter doesn't enable feature.
Solutions:
1. Verify times are in UTC
2. Check system clock accuracy
3. Verify time format matches ISO 8601: "2025-06-01T00:00:00Z"
Issue: Azure App Configuration Not Refreshing¶
Symptoms: Flag changes in Azure don't reflect in application.
Solutions:
1. Verify UseMicroserviceAzureAppConfiguration() middleware is registered
2. Check refresh interval configuration
3. Verify sentinel key is being updated
4. Check Azure App Configuration connection string
Summary¶
Feature flags in the ConnectSoft Microservice Template provide:
- ✅ Runtime Control: Enable/disable features without redeployment
- ✅ Gradual Rollout: Percentage-based and time-window filters
- ✅ Centralized Management: Azure App Configuration integration
- ✅ Type Safety: Strongly-typed
IFeatureManagerinterface - ✅ DI Integration: Fully integrated with ASP.NET Core dependency injection
- ✅ Testability: Easy to mock and test with configuration overrides
- ✅ Observability: Structured logging and metrics integration
- ✅ Flexibility: Support for simple boolean and complex filter-based flags
By following these patterns, teams can:
- Deploy Safely: Separate deployment from feature activation
- Roll Out Gradually: Test features with subsets of users before full rollout
- Respond Quickly: Disable problematic features instantly
- Experiment: A/B test features and measure impact
- Maintain Quality: Keep flag count manageable and remove unused flags
Feature flags are a fundamental capability for building resilient, adaptable microservices that can evolve safely in production environments.