Skip to content

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:

public interface IFeatureManagerSnapshot : IFeatureManager
{
}

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:

{
  "FeatureManagement": {
    "Expose Features Api": true,
    "UseSemanticKernel": false
  }
}
  • true: Feature is always enabled
  • false: 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

  1. Use Descriptive Names

    // ✅ GOOD
    "Expose Features Api"
    "UseSemanticKernel"
    "EnableMetrics"
    
    // ❌ BAD
    "Feature1"
    "NewFeature"
    "TestFlag"
    

  2. Use Domain Prefixes

    // ✅ GOOD
    "AI.UseSemanticKernel"
    "Auth.PasswordlessEnabled"
    "Billing.EnableFraudDetection"
    

  3. Use PascalCase or Dot Notation

    // ✅ GOOD
    "UseSemanticKernel"
    "AI.FeatureX.Enabled"
    

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

  1. Check Flags Early

    // ✅ GOOD - Check before expensive operations
    if (!await this.featureManager.IsEnabledAsync("FeatureX"))
        return;
    
    await this.expensiveOperation();
    

  2. Log Flag Evaluations

    // ✅ GOOD - Log for observability
    var isEnabled = await this.featureManager.IsEnabledAsync("FeatureX");
    this.logger.LogInformation("FeatureX evaluated: {IsEnabled}", isEnabled);
    

  3. Use IFeatureManagerSnapshot for Per-Request Consistency

    // ✅ GOOD - Consistent within request
    private readonly IFeatureManagerSnapshot featureManagerSnapshot;
    

  4. Document Flag Purpose

    /// <summary>
    /// Enables Semantic Kernel AI processing.
    /// Temporary flag for gradual rollout.
    /// </summary>
    "UseSemanticKernel": false
    

  5. Remove Unused Flags

  6. Remove from configuration
  7. Remove from code
  8. Remove from tests
  9. Update documentation

Don'ts

  1. Don't Hide Exceptions Behind Flags

    // ❌ BAD - Silent failure
    if (await this.featureManager.IsEnabledAsync("FeatureX"))
    {
        try { await DoThing(); } catch { }
    }
    
    // ✅ GOOD - Flag controls behavior, not error suppression
    if (await this.featureManager.IsEnabledAsync("FeatureX"))
    {
        await DoThing(); // Let exceptions bubble up
    }
    

  2. 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();
    }
    

  3. Don't Expose Internal Flags

    // ❌ BAD - Exposing internal flag in public API
    [HttpGet("features")]
    public async Task<IActionResult> GetFeatures()
    {
        // Don't expose "EnableInternalAuditMode"
    }
    

  4. Don't Use Flags for Security

    // ❌ BAD - Security via feature flag
    [FeatureGate("AllowAdminAccess")]
    public async Task<IActionResult> DeleteUser()
    
    // ✅ GOOD - Security via authorization
    [Authorize(Roles = "Admin")]
    public async Task<IActionResult> DeleteUser()
    

  5. Don't Create Too Many Flags

  6. Keep flag count manageable
  7. Remove flags after they're no longer needed
  8. 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

{
  "FeatureManagement": {
    "EnableAsyncProcessing": false
  }
}

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:

{
  "FeatureManagement": {
    "MyFeature": true
  }
}
2. Check flag name matches exactly (case-sensitive) 3. Verify AddMicroserviceFeatureManagement() 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:

var isEnabled = await this.featureManager
    .IsEnabledAsync("Feature", context);
2. Use IFeatureManagerSnapshot 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 IFeatureManager interface
  • 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.