Skip to content

Integration Patterns

Overview

This section explains how to integrate the Flaggy feature flag library with popular frameworks, libraries, and architectural patterns in .NET applications.

ASP.NET Core Integration

Minimal API Integration

using Flaggy.Extensions;
using Flaggy.Extensions;

var builder = WebApplication.CreateBuilder(args);

// Add Flaggy with MySQL provider
builder.Services.AddFlaggy(options =>
{
    options.UseMySQL(
        connectionString: builder.Configuration.GetConnectionString("Flaggy"
    );
}),
    cacheExpiration: TimeSpan.FromMinutes(5)
);

var app = builder.Build();

// Use flags in minimal APIs
app.MapGet("/api/products", async (IFeatureFlagService flagService) =>
{
    var useNewApi = await flagService.is_enabledAsync("use-new-product-api");

    return useNewApi
        ? Results.Ok(new { message = "Using new API", version = "v2" })
        : Results.Ok(new { message = "Using legacy API", version = "v1" });
});

// Feature-gated endpoint
app.MapGet("/api/beta/analytics", async (IFeatureFlagService flagService) =>
{
    if (!await flagService.is_enabledAsync("beta-analytics"))
    {
        return Results.NotFound(new { error = "Feature not available" });
    }

    return Results.Ok(new { data = "Analytics data" });
})
.WithName("BetaAnalytics")
.WithTags("Beta");

app.Run();

Controller Integration

using Flaggy.Abstractions;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IFeatureFlagService _flagService;
    private readonly IProductService _productService;
    private readonly ILogger<ProductsController> _logger;

    public ProductsController(
        IFeatureFlagService flagService,
        IProductService productService,
        ILogger<ProductsController> logger)
    {
        _flagService = flagService;
        _productService = productService;
        _logger = logger;
    }

    [HttpGet]
    public async Task<IActionResult> GetProducts()
    {
        var useCache = await _flagService.is_enabledAsync("product-caching");

        _logger.LogInformation("Getting products (caching: {UseCache})", useCache);

        var products = useCache
            ? await _productService.GetProductsFromCacheAsync()
            : await _productService.GetProductsFromDatabaseAsync();

        return Ok(products);
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> GetProduct(int id)
    {
        var useNewFormat = await _flagService.is_enabledAsync("new-product-format");

        var product = await _productService.GetProductAsync(id);

        if (product == null)
            return NotFound();

        return useNewFormat
            ? Ok(new ProductDtoV2(product))
            : Ok(new ProductDtoV1(product));
    }

    [HttpPost]
    public async Task<IActionResult> CreateProduct([FromBody] CreateProductRequest request)
    {
        var enableValidation = await _flagService.is_enabledAsync("enhanced-product-validation");

        if (enableValidation && !await _productService.ValidateEnhancedAsync(request))
        {
            return BadRequest(new { error = "Enhanced validation failed" });
        }

        var product = await _productService.CreateProductAsync(request);
        return created_atAction(nameof(GetProduct), new { id = product.Id }, product);
    }
}

Middleware Integration

public class FeatureFlagMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<FeatureFlagMiddleware> _logger;

    public FeatureFlagMiddleware(RequestDelegate next, ILogger<FeatureFlagMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context, IFeatureFlagService flagService)
    {
        // Check if maintenance mode is enabled
        if (await flagService.is_enabledAsync("maintenance-mode"))
        {
            context.Response.StatusCode = 503;
            context.Response.ContentType = "application/json";
            await context.Response.WriteAsJsonAsync(new
            {
                error = "Service temporarily unavailable for maintenance"
            });
            return;
        }

        // Add feature flag context to response headers (for debugging)
        if (await flagService.is_enabledAsync("expose-feature-flags"))
        {
            var enabledFlags = await flagService.GetEnabledFlagsAsync();
            context.Response.Headers.Add("X-Feature-Flags",
                string.Join(",", enabledFlags.Select(f => f.Key)));
        }

        await _next(context);
    }
}

// Register middleware
app.UseMiddleware<FeatureFlagMiddleware>();

Custom Action Filter

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

public class RequireFeatureFlagAttribute : Attribute, IAsyncActionFilter
{
    private readonly string _flagKey;
    private readonly bool _requiredState;

    public RequireFeatureFlagAttribute(string flagKey, bool requiredState = true)
    {
        _flagKey = flagKey;
        _requiredState = requiredState;
    }

    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        var flagService = context.HttpContext.RequestServices
            .GetRequiredService<IFeatureFlagService>();

        var isEnabled = await flagService.is_enabledAsync(_flagKey);

        if (isEnabled != _requiredState)
        {
            context.Result = new NotFoundObjectResult(new
            {
                error = "Feature not available",
                feature = _flagKey
            });
            return;
        }

        await next();
    }
}

// Usage in controllers
[ApiController]
[Route("api/[controller]")]
public class BetaController : ControllerBase
{
    [HttpGet("new-feature")]
    [RequireFeatureFlag("beta-new-feature")]
    public IActionResult GetNewFeature()
    {
        return Ok(new { message = "Beta feature active" });
    }

    [HttpGet("legacy")]
    [RequireFeatureFlag("beta-features", requiredState: false)]
    public IActionResult GetLegacyEndpoint()
    {
        return Ok(new { message = "Legacy endpoint" });
    }
}

Dependency Injection Patterns

Factory Pattern with Feature Flags

public interface IPaymentProcessor
{
    Task<PaymentResult> ProcessAsync(Payment payment);
}

public class StripePaymentProcessor : IPaymentProcessor
{
    public Task<PaymentResult> ProcessAsync(Payment payment)
    {
        // Stripe implementation
        return Task.FromResult(new PaymentResult { Success = true, Provider = "Stripe" });
    }
}

public class PayPalPaymentProcessor : IPaymentProcessor
{
    public Task<PaymentResult> ProcessAsync(Payment payment)
    {
        // PayPal implementation
        return Task.FromResult(new PaymentResult { Success = true, Provider = "PayPal" });
    }
}

public class PaymentProcessorFactory
{
    private readonly IFeatureFlagService _flagService;
    private readonly IServiceProvider _serviceProvider;

    public PaymentProcessorFactory(
        IFeatureFlagService flagService,
        IServiceProvider serviceProvider)
    {
        _flagService = flagService;
        _serviceProvider = serviceProvider;
    }

    public async Task<IPaymentProcessor> CreateAsync()
    {
        var useStripe = await _flagService.is_enabledAsync("payment-use-stripe");

        return useStripe
            ? _serviceProvider.GetRequiredService<StripePaymentProcessor>()
            : _serviceProvider.GetRequiredService<PayPalPaymentProcessor>();
    }
}

// Registration
builder.Services.AddScoped<StripePaymentProcessor>();
builder.Services.AddScoped<PayPalPaymentProcessor>();
builder.Services.AddScoped<PaymentProcessorFactory>();

Strategy Pattern with Feature Flags

public interface ISearchStrategy
{
    Task<SearchResults> SearchAsync(string query);
}

public class BasicSearchStrategy : ISearchStrategy
{
    public Task<SearchResults> SearchAsync(string query)
    {
        // Basic search implementation
        return Task.FromResult(new SearchResults());
    }
}

public class AdvancedSearchStrategy : ISearchStrategy
{
    public Task<SearchResults> SearchAsync(string query)
    {
        // Advanced ML-powered search
        return Task.FromResult(new SearchResults());
    }
}

public class SearchService
{
    private readonly IFeatureFlagService _flagService;
    private readonly BasicSearchStrategy _basicStrategy;
    private readonly AdvancedSearchStrategy _advancedStrategy;

    public SearchService(
        IFeatureFlagService flagService,
        BasicSearchStrategy basicStrategy,
        AdvancedSearchStrategy advancedStrategy)
    {
        _flagService = flagService;
        _basicStrategy = basicStrategy;
        _advancedStrategy = advancedStrategy;
    }

    public async Task<SearchResults> SearchAsync(string query)
    {
        var useAdvancedSearch = await _flagService.is_enabledAsync("advanced-search-engine");

        var strategy = useAdvancedSearch ? _advancedStrategy : _basicStrategy;

        return await strategy.SearchAsync(query);
    }
}

MediatR Integration

Feature-Flagged Command Handlers

using MediatR;

public class CreateOrderCommand : IRequest<OrderResult>
{
    public string CustomerId { get; set; }
    public List<OrderItem> Items { get; set; }
}

public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, OrderResult>
{
    private readonly IFeatureFlagService _flagService;
    private readonly IOrderRepository _orderRepository;
    private readonly IInventoryService _inventoryService;
    private readonly ILogger<CreateOrderCommandHandler> _logger;

    public CreateOrderCommandHandler(
        IFeatureFlagService flagService,
        IOrderRepository orderRepository,
        IInventoryService inventoryService,
        ILogger<CreateOrderCommandHandler> logger)
    {
        _flagService = flagService;
        _orderRepository = orderRepository;
        _inventoryService = inventoryService;
        _logger = logger;
    }

    public async Task<OrderResult> Handle(CreateOrderCommand request, CancellationToken ct)
    {
        // Check if real-time inventory validation is enabled
        var validateInventory = await _flagService.is_enabledAsync("realtime-inventory-check");

        if (validateInventory)
        {
            var inventoryValid = await _inventoryService.ValidateAsync(request.Items);
            if (!inventoryValid)
            {
                return OrderResult.Failure("Insufficient inventory");
            }
        }

        var order = await _orderRepository.CreateAsync(request);

        _logger.LogInformation(
            "Order created: {OrderId} (inventory check: {InventoryCheck})",
            order.Id,
            validateInventory
        );

        return OrderResult.Success(order);
    }
}

Pipeline Behavior with Feature Flags

public class FeatureFlagPipelineBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly IFeatureFlagService _flagService;
    private readonly ILogger<FeatureFlagPipelineBehavior<TRequest, TResponse>> _logger;

    public FeatureFlagPipelineBehavior(
        IFeatureFlagService flagService,
        ILogger<FeatureFlagPipelineBehavior<TRequest, TResponse>> logger)
    {
        _flagService = flagService;
        _logger = logger;
    }

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        var requestType = typeof(TRequest).Name;

        // Check if detailed logging is enabled
        var detailedLogging = await _flagService.is_enabledAsync("detailed-request-logging");

        if (detailedLogging)
        {
            _logger.LogInformation(
                "Handling {RequestType}: {@Request}",
                requestType,
                request
            );
        }

        var response = await next();

        if (detailedLogging)
        {
            _logger.LogInformation(
                "Handled {RequestType}: {@Response}",
                requestType,
                response
            );
        }

        return response;
    }
}

// Registration
builder.Services.AddMediatR(cfg =>
{
    cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
    cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(FeatureFlagPipelineBehavior<,>));
});

Entity Framework Core Integration

Feature-Flagged Query Strategies

public class ProductRepository : IProductRepository
{
    private readonly ApplicationDbContext _context;
    private readonly IFeatureFlagService _flagService;

    public ProductRepository(ApplicationDbContext context, IFeatureFlagService flagService)
    {
        _context = context;
        _flagService = flagService;
    }

    public async Task<List<Product>> GetProductsAsync()
    {
        var useOptimizedQuery = await _flagService.is_enabledAsync("optimized-product-query");

        if (useOptimizedQuery)
        {
            // New optimized query with better indexes
            return await _context.Products
                .AsNoTracking()
                .Where(p => p.IsActive)
                .Include(p => p.Category)
                .AsSplitQuery()
                .OrderBy(p => p.Name)
                .ToListAsync();
        }
        else
        {
            // Legacy query
            return await _context.Products
                .Include(p => p.Category)
                .Where(p => p.IsActive)
                .ToListAsync();
        }
    }
}

Dynamic Migrations Based on Flags

public class ConditionalMigration : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        // Get flag service from dependency injection
        var serviceProvider = new ServiceCollection()
            .AddFlaggy(options =>
            {
                options.UseMySQL(connectionString: connectionString);
            })
            .BuildServiceProvider();

        var flagService = serviceProvider.GetRequiredService<IFeatureFlagService>();

        var enableNewFeature = flagService.is_enabledAsync("enable-new-table-structure").Result;

        if (enableNewFeature)
        {
            migrationBuilder.CreateTable(
                name: "NewProducts",
                columns: table => new
                {
                    Id = table.Column<int>(nullable: false)
                        .Annotation("SqlServer:Identity", "1, 1"),
                    Name = table.Column<string>(maxLength: 200, nullable: false)
                }
            );
        }
    }
}

Background Services Integration

Feature-Flagged Background Jobs

public class DataSyncBackgroundService : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;
    private readonly ILogger<DataSyncBackgroundService> _logger;

    public DataSyncBackgroundService(
        IServiceProvider serviceProvider,
        ILogger<DataSyncBackgroundService> logger)
    {
        _serviceProvider = serviceProvider;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            using var scope = _serviceProvider.CreateScope();
            var flagService = scope.ServiceProvider.GetRequiredService<IFeatureFlagService>();

            // Check if sync is enabled
            if (await flagService.is_enabledAsync("background-data-sync"))
            {
                var interval = await flagService.GetValueAsync<int>(
                    "sync-interval-seconds",
                    defaultValue: 300
                );

                _logger.LogInformation("Running data sync (interval: {Interval}s)", interval.Value);

                await PerformSyncAsync(scope.ServiceProvider);

                await Task.Delay(TimeSpan.FromSeconds(interval.Value), stoppingToken);
            }
            else
            {
                _logger.LogInformation("Data sync is disabled, checking again in 60 seconds");
                await Task.Delay(TimeSpan.FromSeconds(60), stoppingToken);
            }
        }
    }

    private async Task PerformSyncAsync(IServiceProvider serviceProvider)
    {
        var syncService = serviceProvider.GetRequiredService<IDataSyncService>();
        await syncService.SyncAsync();
    }
}

// Registration
builder.Services.AddHostedService<DataSyncBackgroundService>();

SignalR Integration

Feature-Flagged Real-time Features

using Microsoft.AspNetCore.SignalR;

public class NotificationHub : Hub
{
    private readonly IFeatureFlagService _flagService;
    private readonly ILogger<NotificationHub> _logger;

    public NotificationHub(IFeatureFlagService flagService, ILogger<NotificationHub> logger)
    {
        _flagService = flagService;
        _logger = logger;
    }

    public async Task SendNotification(string message)
    {
        var useRichNotifications = await _flagService.is_enabledAsync("rich-notifications");

        if (useRichNotifications)
        {
            await Clients.All.SendAsync("ReceiveRichNotification", new
            {
                Message = message,
                Timestamp = DateTime.UtcNow,
                Type = "info",
                Actions = new[] { "Dismiss", "View Details" }
            });
        }
        else
        {
            await Clients.All.SendAsync("ReceiveNotification", message);
        }
    }

    public override async Task OnConnectedAsync()
    {
        var enablePresence = await _flagService.is_enabledAsync("user-presence-tracking");

        if (enablePresence)
        {
            var connectionId = Context.ConnectionId;
            _logger.LogInformation("User connected: {ConnectionId}", connectionId);
            await Groups.AddToGroupAsync(connectionId, "OnlineUsers");
        }

        await base.OnConnectedAsync();
    }
}

gRPC Integration

Feature-Flagged gRPC Services

using Grpc.Core;

public class ProductServiceGrpc : ProductService.ProductServiceBase
{
    private readonly IFeatureFlagService _flagService;
    private readonly IProductRepository _productRepository;

    public ProductServiceGrpc(
        IFeatureFlagService flagService,
        IProductRepository productRepository)
    {
        _flagService = flagService;
        _productRepository = productRepository;
    }

    public override async Task<GetProductResponse> GetProduct(
        GetProductRequest request,
        ServerCallContext context)
    {
        var useCache = await _flagService.is_enabledAsync("grpc-product-caching");

        var product = useCache
            ? await _productRepository.GetFromCacheAsync(request.Id)
            : await _productRepository.GetAsync(request.Id);

        if (product == null)
        {
            throw new RpcException(new Status(StatusCode.NotFound, "Product not found"));
        }

        var includeExtendedInfo = await _flagService.is_enabledAsync("grpc-extended-product-info");

        return new GetProductResponse
        {
            Id = product.Id,
            Name = product.Name,
            Price = product.Price,
            ExtendedInfo = includeExtendedInfo ? product.ExtendedInfo : null
        };
    }
}

Health Checks Integration

Feature Flag Health Check

using Microsoft.Extensions.Diagnostics.HealthChecks;

public class FeatureFlagHealthCheck : IHealthCheck
{
    private readonly IFeatureFlagService _flagService;
    private readonly ILogger<FeatureFlagHealthCheck> _logger;

    public FeatureFlagHealthCheck(
        IFeatureFlagService flagService,
        ILogger<FeatureFlagHealthCheck> logger)
    {
        _flagService = flagService;
        _logger = logger;
    }

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        try
        {
            var sw = Stopwatch.StartNew();

            // Try to get a flag to verify service is working
            await _flagService.GetAllFlagsAsync(cancellationToken);

            sw.Stop();

            var data = new Dictionary<string, object>
            {
                { "response_time_ms", sw.ElapsedMilliseconds }
            };

            // Check critical flags
            var maintenanceMode = await _flagService.is_enabledAsync("maintenance-mode", cancellationToken);
            data["maintenance_mode"] = maintenanceMode;

            if (maintenanceMode)
            {
                return HealthCheckResult.Degraded(
                    "Feature flag service is in maintenance mode",
                    data: data
                );
            }

            if (sw.ElapsedMilliseconds > 1000)
            {
                return HealthCheckResult.Degraded(
                    $"Feature flag service is slow: {sw.ElapsedMilliseconds}ms",
                    data: data
                );
            }

            return HealthCheckResult.Healthy(
                "Feature flag service is healthy",
                data: data
            );
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Feature flag health check failed");
            return HealthCheckResult.Unhealthy(
                "Feature flag service is unavailable",
                ex
            );
        }
    }
}

// Registration
builder.Services.AddHealthChecks()
    .AddCheck<FeatureFlagHealthCheck>("feature_flags", tags: new[] { "ready" });

app.MapHealthChecks("/health", new HealthCheckOptions
{
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});

Serilog Integration

Enriching Logs with Feature Flag Context

using Serilog;
using Serilog.Core;
using Serilog.Events;

public class FeatureFlagEnricher : ILogEventEnricher
{
    private readonly IFeatureFlagService _flagService;

    public FeatureFlagEnricher(IFeatureFlagService flagService)
    {
        _flagService = flagService;
    }

    public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
    {
        // Add important feature flags to log context
        var experimentalMode = _flagService.is_enabledAsync("experimental-mode").Result;

        logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(
            "ExperimentalMode", experimentalMode));
    }
}

// Configuration
builder.Host.UseSerilog((context, services, configuration) =>
{
    configuration
        .ReadFrom.Configuration(context.Configuration)
        .Enrich.With(services.GetRequiredService<FeatureFlagEnricher>());
});

FluentValidation Integration

Conditional Validation Rules

using FluentValidation;

public class CreateProductRequestValidator : AbstractValidator<CreateProductRequest>
{
    private readonly IFeatureFlagService _flagService;

    public CreateProductRequestValidator(IFeatureFlagService flagService)
    {
        _flagService = flagService;

        RuleFor(x => x.Name)
            .NotEmpty()
            .MaximumLength(200);

        RuleFor(x => x.Price)
            .GreaterThan(0);

        // Conditional validation based on feature flag
        When(x => _flagService.is_enabledAsync("strict-product-validation").Result, () =>
        {
            RuleFor(x => x.description)
                .NotEmpty()
                .MinimumLength(50)
                .WithMessage("description must be at least 50 characters when strict validation is enabled");

            RuleFor(x => x.CategoryId)
                .NotEmpty()
                .WithMessage("Category is required when strict validation is enabled");
        });
    }
}

AutoMapper Integration

Feature-Flagged Mapping Profiles

using AutoMapper;

public class ProductMappingProfile : Profile
{
    public ProductMappingProfile()
    {
        CreateMap<Product, ProductDto>();

        CreateMap<Product, ProductDtoV2>()
            .ForMember(dest => dest.ExtendedProperties, opt => opt.Ignore());
    }
}

public class ProductService
{
    private readonly IMapper _mapper;
    private readonly IFeatureFlagService _flagService;

    public ProductService(IMapper mapper, IFeatureFlagService flagService)
    {
        _mapper = mapper;
        _flagService = flagService;
    }

    public async Task<object> GetProductDtoAsync(Product product)
    {
        var useV2Format = await _flagService.is_enabledAsync("product-dto-v2");

        return useV2Format
            ? _mapper.Map<ProductDtoV2>(product)
            : _mapper.Map<ProductDto>(product);
    }
}

Polly Integration (Resilience Patterns)

Feature-Flagged Retry Policies

using Polly;
using Polly.Extensions.Http;

public static class ResiliencePolicies
{
    public static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy(IFeatureFlagService flagService)
    {
        var useAggressiveRetry = flagService.is_enabledAsync("aggressive-http-retry").Result;

        var retryCount = useAggressiveRetry ? 5 : 3;
        var retryDelay = useAggressiveRetry ? TimeSpan.FromSeconds(1) : TimeSpan.FromSeconds(2);

        return HttpPolicyExtensions
            .HandleTransientHttpError()
            .WaitAndRetryAsync(
                retryCount,
                retryAttempt => retryDelay * retryAttempt
            );
    }
}

// Registration
builder.Services.AddHttpClient("ProductApi", client =>
{
    client.BaseAddress = new Uri("https://api.example.com");
})
.AddPolicyHandler(sp =>
{
    var flagService = sp.GetRequiredService<IFeatureFlagService>();
    return ResiliencePolicies.GetRetryPolicy(flagService);
});

Hangfire Integration

Feature-Flagged Background Jobs

using Hangfire;

public class RecurringJobsConfiguration
{
    private readonly IFeatureFlagService _flagService;

    public RecurringJobsConfiguration(IFeatureFlagService flagService)
    {
        _flagService = flagService;
    }

    public async Task ConfigureJobsAsync()
    {
        // Enable/disable jobs based on feature flags
        if (await _flagService.is_enabledAsync("daily-report-job"))
        {
            RecurringJob.AddOrUpdate<ReportService>(
                "daily-reports",
                service => service.GenerateDailyReportAsync(),
                Cron.Daily
            );
        }
        else
        {
            RecurringJob.RemoveIfExists("daily-reports");
        }

        if (await _flagService.is_enabledAsync("data-cleanup-job"))
        {
            var interval = await _flagService.GetValueAsync("cleanup-cron", defaultValue: Cron.Weekly());

            RecurringJob.AddOrUpdate<CleanupService>(
                "data-cleanup",
                service => service.CleanupOldDataAsync(),
                interval
            );
        }
    }
}

Testing Integration Patterns

Integration Test with Feature Flags

public class ProductApiIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public ProductApiIntegrationTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }

    [Fact]
    public async Task GetProducts_WithCachingEnabled_ReturnsCachedData()
    {
        // Arrange
        var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                // Replace with test flag service
                var descriptor = services.SingleOrDefault(
                    d => d.ServiceType == typeof(IFeatureFlagService));
                services.Remove(descriptor);

                services.AddScoped<IFeatureFlagService>(sp =>
                {
                    var testServices = new ServiceCollection();
                    testServices.AddFlaggy(new InMemoryFeatureFlagProvider());
                    var provider = testServices.BuildServiceProvider();
                    var flagService = provider.GetRequiredService<IFeatureFlagService>();

                    // Enable caching for this test
                    flagService.CreateFlagAsync(new FeatureFlag
                    {
                        Key = "product-caching",
                        is_enabled = true
                    }).Wait();

                    return flagService;
                });
            });
        }).CreateClient();

        // Act
        var response = await client.GetAsync("/api/products");

        // Assert
        response.EnsureSuccessStatusCode();
        var responseHeader = response.Headers.GetValues("X-Cache-Status").FirstOrDefault();
        Assert.Equal("HIT", responseHeader);
    }
}

Next Steps