Advanced Examples¶
This guide covers complex use cases and advanced patterns for using Flaggy in production environments.
Multi-Tenant Feature Flags¶
Manage features differently for each tenant.
public class MultiTenantFeatureService
{
private readonly IFeatureFlagService _flagService;
private readonly IHttpContextAccessor _contextAccessor;
public MultiTenantFeatureService(
IFeatureFlagService flagService,
IHttpContextAccessor contextAccessor)
{
_flagService = flagService;
_contextAccessor = contextAccessor;
}
public async Task<bool> IsEnabledForCurrentTenant(string featureKey)
{
var tenantId = GetCurrentTenantId();
return await IsEnabledForTenant(featureKey, tenantId);
}
public async Task<bool> IsEnabledForTenant(string featureKey, string tenantId)
{
// Priority 1: Check tenant-specific flag
var tenantSpecificKey = $"{featureKey}:tenant:{tenantId}";
var tenantFlag = await _flagService.GetFlagAsync(tenantSpecificKey);
if (tenantFlag != null)
return tenantFlag.IsEnabled;
// Priority 2: Check tier-based flag
var tier = await GetTenantTier(tenantId);
var tierSpecificKey = $"{featureKey}:tier:{tier}";
var tierFlag = await _flagService.GetFlagAsync(tierSpecificKey);
if (tierFlag != null)
return tierFlag.IsEnabled;
// Priority 3: Check global flag
return await _flagService.IsFlagEnabledAsync(featureKey);
}
private string GetCurrentTenantId()
{
return _contextAccessor.HttpContext?.Request.Headers["X-Tenant-Id"].FirstOrDefault()
?? "default";
}
private async Task<string> GetTenantTier(string tenantId)
{
// In real scenario, fetch from database
var tiers = new Dictionary<string, string>
{
["tenant1"] = "enterprise",
["tenant2"] = "professional",
["tenant3"] = "basic"
};
return tiers.TryGetValue(tenantId, out var tier) ? tier : "basic";
}
}
// Usage
app.MapGet("/api/features/analytics", async (MultiTenantFeatureService featureService) =>
{
if (await featureService.IsEnabledForCurrentTenant("advanced-analytics"))
{
return Results.Ok(new { Feature = "Advanced Analytics", Enabled = true });
}
return Results.Forbid();
});
User Segmentation¶
Enable features for specific user groups.
public class UserSegmentationService
{
private readonly IFeatureFlagService _flagService;
public UserSegmentationService(IFeatureFlagService flagService)
{
_flagService = flagService;
}
public async Task<bool> IsEnabledForUser(string featureKey, User user)
{
// Check if feature is globally enabled
if (!await _flagService.IsFlagEnabledAsync(featureKey))
return false;
// Get targeting rules from flag value
var rulesJson = await _flagService.GetValueAsync<string>($"{featureKey}:rules");
if (string.IsNullOrEmpty(rulesJson))
return true; // No rules = enabled for everyone
var rules = JsonSerializer.Deserialize<TargetingRules>(rulesJson);
// Check user whitelist
if (rules.WhitelistedUsers?.Contains(user.Email) == true)
return true;
// Check user attributes
if (rules.RequiredAttributes != null)
{
foreach (var attr in rules.RequiredAttributes)
{
if (!user.Attributes.TryGetValue(attr.Key, out var value) || value != attr.Value)
return false;
}
}
// Check percentage rollout
if (rules.RolloutPercentage.HasValue)
{
var userHash = Math.Abs(user.Id.GetHashCode()) % 100;
return userHash < rules.RolloutPercentage.Value;
}
return true;
}
}
public record User(string Id, string Email, Dictionary<string, string> Attributes);
public record TargetingRules
{
public List<string>? WhitelistedUsers { get; init; }
public Dictionary<string, string>? RequiredAttributes { get; init; }
public int? RolloutPercentage { get; init; }
}
// Setup targeting rules
await flagService.UpsertFlagAsync("beta-features", true,
value: JsonSerializer.Serialize(new TargetingRules
{
WhitelistedUsers = new List<string> { "admin@example.com", "beta-tester@example.com" },
RequiredAttributes = new Dictionary<string, string> { ["country"] = "US" },
RolloutPercentage = 10
}));
Time-Based Flags¶
Enable features based on time windows.
public class TimeBasedFeatureService
{
private readonly IFeatureFlagService _flagService;
public TimeBasedFeatureService(IFeatureFlagService flagService)
{
_flagService = flagService;
}
public async Task<bool> IsEnabledAtTime(string featureKey, DateTime? checkTime = null)
{
if (!await _flagService.IsFlagEnabledAsync(featureKey))
return false;
var scheduleJson = await _flagService.GetValueAsync<string>($"{featureKey}:schedule");
if (string.IsNullOrEmpty(scheduleJson))
return true; // No schedule = always enabled
var schedule = JsonSerializer.Deserialize<FeatureSchedule>(scheduleJson);
var now = checkTime ?? DateTime.UtcNow;
// Check if within active window
if (schedule.StartTime.HasValue && now < schedule.StartTime.Value)
return false;
if (schedule.EndTime.HasValue && now > schedule.EndTime.Value)
return false;
// Check day of week restrictions
if (schedule.AllowedDaysOfWeek?.Any() == true)
{
if (!schedule.AllowedDaysOfWeek.Contains(now.DayOfWeek))
return false;
}
// Check time of day restrictions
if (schedule.AllowedHours?.Any() == true)
{
if (!schedule.AllowedHours.Contains(now.Hour))
return false;
}
return true;
}
}
public record FeatureSchedule
{
public DateTime? StartTime { get; init; }
public DateTime? EndTime { get; init; }
public List<DayOfWeek>? AllowedDaysOfWeek { get; init; }
public List<int>? AllowedHours { get; init; }
}
// Setup time-based feature (Black Friday sale)
await flagService.UpsertFlagAsync("black-friday-sale", true,
value: JsonSerializer.Serialize(new FeatureSchedule
{
StartTime = new DateTime(2024, 11, 29, 0, 0, 0, DateTimeKind.Utc),
EndTime = new DateTime(2024, 12, 2, 23, 59, 59, DateTimeKind.Utc)
}));
Feature Dependency Chains¶
Manage features that depend on other features.
public class FeatureDependencyService
{
private readonly IFeatureFlagService _flagService;
private readonly Dictionary<string, List<string>> _dependencies;
public FeatureDependencyService(IFeatureFlagService flagService)
{
_flagService = flagService;
_dependencies = new Dictionary<string, List<string>>
{
["premium-analytics"] = new() { "basic-analytics", "data-export" },
["ai-recommendations"] = new() { "user-tracking", "ml-service" },
["advanced-reporting"] = new() { "basic-analytics", "premium-analytics" }
};
}
public async Task<bool> IsEnabledWithDependencies(string featureKey)
{
// Check if feature itself is enabled
if (!await _flagService.IsFlagEnabledAsync(featureKey))
return false;
// Check all dependencies
if (_dependencies.TryGetValue(featureKey, out var deps))
{
foreach (var dependency in deps)
{
if (!await IsEnabledWithDependencies(dependency))
{
return false;
}
}
}
return true;
}
public async Task<List<string>> GetMissingDependencies(string featureKey)
{
var missing = new List<string>();
if (_dependencies.TryGetValue(featureKey, out var deps))
{
foreach (var dependency in deps)
{
if (!await _flagService.IsFlagEnabledAsync(dependency))
{
missing.Add(dependency);
}
}
}
return missing;
}
}
// Usage
app.MapGet("/api/features/premium-analytics", async (FeatureDependencyService depService) =>
{
if (await depService.IsEnabledWithDependencies("premium-analytics"))
{
return Results.Ok(GetPremiumAnalytics());
}
var missing = await depService.GetMissingDependencies("premium-analytics");
return Results.BadRequest(new
{
Error = "Feature dependencies not met",
MissingFeatures = missing
});
});
Integration with Middleware¶
Create reusable middleware for feature gating.
public class FeatureGateMiddleware
{
private readonly RequestDelegate _next;
public FeatureGateMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context, IFeatureFlagService flagService)
{
var endpoint = context.GetEndpoint();
var featureGate = endpoint?.Metadata.GetMetadata<FeatureGateAttribute>();
if (featureGate != null)
{
if (!await flagService.IsFlagEnabledAsync(featureGate.FeatureKey))
{
context.Response.StatusCode = 404;
await context.Response.WriteAsJsonAsync(new
{
Error = "Feature not available"
});
return;
}
}
await _next(context);
}
}
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class FeatureGateAttribute : Attribute
{
public string FeatureKey { get; }
public FeatureGateAttribute(string featureKey)
{
FeatureKey = featureKey;
}
}
// Registration
app.UseMiddleware<FeatureGateMiddleware>();
// Usage
app.MapGet("/api/beta/feature")
.WithMetadata(new FeatureGateAttribute("beta-api"))
.WithName("BetaFeature");
Custom Caching Strategy¶
Implement a custom caching strategy with cache warming.
public class WarmCacheFeatureFlagService
{
private readonly IFeatureFlagService _innerService;
private readonly IMemoryCache _cache;
private readonly ILogger<WarmCacheFeatureFlagService> _logger;
public WarmCacheFeatureFlagService(
IFeatureFlagService innerService,
IMemoryCache cache,
ILogger<WarmCacheFeatureFlagService> logger)
{
_innerService = innerService;
_cache = cache;
_logger = logger;
}
public async Task WarmCacheAsync()
{
_logger.LogInformation("Warming feature flag cache...");
var flags = await _innerService.GetAllFlagsAsync();
foreach (var flag in flags)
{
var cacheKey = $"flag:{flag.Key}";
_cache.Set(cacheKey, flag, TimeSpan.FromMinutes(30));
}
_logger.LogInformation("Cache warmed with {Count} flags", flags.Count());
}
public async Task<bool> IsFlagEnabledAsync(string key)
{
var cacheKey = $"flag:{key}";
if (_cache.TryGetValue(cacheKey, out FeatureFlag cachedFlag))
{
return cachedFlag.IsEnabled;
}
// Cache miss - fetch from service
var flag = await _innerService.GetFlagAsync(key);
if (flag != null)
{
_cache.Set(cacheKey, flag, TimeSpan.FromMinutes(30));
return flag.IsEnabled;
}
return false;
}
}
// Warm cache on startup
var warmService = app.Services.GetRequiredService<WarmCacheFeatureFlagService>();
await warmService.WarmCacheAsync();
Circuit Breaker Pattern¶
Combine feature flags with circuit breaker pattern.
public class CircuitBreakerFeatureService
{
private readonly IFeatureFlagService _flagService;
private readonly Dictionary<string, CircuitBreakerState> _states = new();
public async Task<T> ExecuteWithCircuitBreaker<T>(
string featureKey,
Func<Task<T>> operation,
Func<Task<T>> fallback)
{
// Check if feature is enabled
if (!await _flagService.IsFlagEnabledAsync(featureKey))
{
return await fallback();
}
var state = GetOrCreateState(featureKey);
// Check circuit breaker state
if (state.IsOpen)
{
if (DateTime.UtcNow < state.OpenUntil)
{
return await fallback();
}
// Try to half-open
state.IsOpen = false;
}
try
{
var result = await operation();
state.FailureCount = 0; // Reset on success
return result;
}
catch (Exception ex)
{
state.FailureCount++;
// Open circuit if threshold exceeded
if (state.FailureCount >= 5)
{
state.IsOpen = true;
state.OpenUntil = DateTime.UtcNow.AddMinutes(5);
}
return await fallback();
}
}
private CircuitBreakerState GetOrCreateState(string key)
{
if (!_states.TryGetValue(key, out var state))
{
state = new CircuitBreakerState();
_states[key] = state;
}
return state;
}
private class CircuitBreakerState
{
public bool IsOpen { get; set; }
public DateTime OpenUntil { get; set; }
public int FailureCount { get; set; }
}
}
// Usage
var result = await circuitBreaker.ExecuteWithCircuitBreaker(
"external-api-integration",
operation: async () => await CallExternalApi(),
fallback: async () => await GetCachedData()
);
Next Steps¶
- Real World Scenarios - Production use cases
- Best Practices - Production best practices
- Custom Providers - Build your own provider