Flag Values¶
Flaggy supports typed values for feature flags, allowing you to store configuration data alongside simple on/off states. This makes flags more versatile, enabling use cases like A/B testing, configuration management, and gradual rollouts.
Overview¶
Flag values provide:
- Typed Values: Store strings, integers, doubles, booleans, etc.
- Default Values: Fallback when flag doesn't exist or is disabled
- Type Conversion: Automatic conversion to desired types
- Configuration Management: Use flags for dynamic configuration
- A/B Testing: Store variant identifiers
- Gradual Rollouts: Store percentages or cohort identifiers
Flag Model¶
Each flag has these properties:
public class FeatureFlag
{
public string Key { get; set; } // Unique identifier
public bool IsEnabled { get; set; } // On/off state
public string? Value { get; set; } // Optional value (can be null)
public string? Description { get; set; } // Human-readable description
public DateTime? CreatedAt { get; set; } // Creation timestamp
public DateTime? UpdatedAt { get; set; } // Last update timestamp
}
How Flag Values Work¶
The Value property stores data as a string, with automatic type conversion:
- Enabled = true: Flag is active, returns the stored value
- Enabled = false: Flag is inactive, returns the default value
- Flag doesn't exist: Returns the default value
- Value is null: Returns the default value
// Example flag
{
"Key": "max-users",
"IsEnabled": true,
"Value": "1000",
"Description": "Maximum concurrent users"
}
// Getting the value
var maxUsers = await flagService.GetValueAsync<int>("max-users", defaultValue: 100);
// Returns: 1000 (converted to int)
// If IsEnabled = false
// Returns: 100 (default value)
Getting Flag Values¶
String Values¶
using Flaggy.Abstractions;
public class ConfigService
{
private readonly IFeatureFlagService _flagService;
public ConfigService(IFeatureFlagService flagService)
{
_flagService = flagService;
}
public async Task<string> GetWelcomeMessage()
{
// Returns "Welcome!" if flag doesn't exist or is disabled
var message = await _flagService.GetValueAsync(
"welcome-message",
defaultValue: "Welcome!"
);
return message;
}
public async Task<string> GetTheme()
{
// Returns "light" as default
var theme = await _flagService.GetValueAsync(
"theme",
defaultValue: "light"
);
return theme; // Could return "dark", "light", "auto"
}
}
Integer Values¶
public async Task<int> GetMaxUsers()
{
// Type conversion happens automatically
var maxUsers = await _flagService.GetValueAsync<int>(
"max-users",
defaultValue: 100
);
return maxUsers.Value; // Returns int
}
public async Task<int> GetPageSize()
{
var pageSize = await _flagService.GetValueAsync<int>(
"page-size",
defaultValue: 20
);
return pageSize ?? 20; // Returns int? (nullable)
}
public async Task<int> GetCacheExpiration()
{
// Cache expiration in seconds
var seconds = await _flagService.GetValueAsync<int>(
"cache-expiration-seconds",
defaultValue: 300 // 5 minutes
);
return seconds ?? 300;
}
Double Values¶
public async Task<double> GetDiscountRate()
{
// Discount rate as decimal (0.15 = 15%)
var rate = await _flagService.GetValueAsync<double>(
"discount-rate",
defaultValue: 0.0
);
return rate ?? 0.0;
}
public async Task<decimal> CalculatePrice(decimal basePrice)
{
var discountRate = await _flagService.GetValueAsync<double>(
"discount-rate",
defaultValue: 0.0
);
var finalPrice = basePrice * (decimal)(1 - discountRate.Value);
return finalPrice;
}
public async Task<double> GetRolloutPercentage()
{
// Feature rollout percentage (0.0 to 1.0)
var percentage = await _flagService.GetValueAsync<double>(
"new-feature-rollout",
defaultValue: 0.0 // Start with 0% rollout
);
return percentage ?? 0.0;
}
Boolean Values¶
public async Task<bool> IsStrictModeEnabled()
{
// Boolean stored as string "true" or "false"
var strictMode = await _flagService.GetValueAsync<bool>(
"strict-mode",
defaultValue: false
);
return strictMode ?? false;
}
public async Task<bool> UseNewAlgorithm()
{
var useNew = await _flagService.GetValueAsync<bool>(
"use-new-algorithm",
defaultValue: false
);
return useNew ?? false;
}
Complex Values (JSON)¶
Store complex data as JSON strings:
using System.Text.Json;
public class FeatureConfig
{
public string Version { get; set; }
public List<string> EnabledModules { get; set; }
public Dictionary<string, string> Settings { get; set; }
}
public async Task<FeatureConfig> GetFeatureConfig()
{
var json = await _flagService.GetValueAsync(
"feature-config",
defaultValue: "{\"Version\":\"1.0\",\"EnabledModules\":[],\"Settings\":{}}"
);
return JsonSerializer.Deserialize<FeatureConfig>(json);
}
Setting Flag Values¶
Through Programmatic API¶
using Flaggy.Models;
// Create flag with value
await flagService.CreateFlagAsync(new FeatureFlag
{
Key = "max-upload-size",
IsEnabled = true,
Value = "10485760", // 10MB in bytes
Description = "Maximum file upload size in bytes"
});
// Update value only
await flagService.UpdateFlagValueAsync("max-upload-size", "20971520"); // 20MB
// Update entire flag
var flag = await flagService.GetFlagAsync("max-upload-size");
flag.Value = "52428800"; // 50MB
await flagService.UpdateFlagAsync(flag);
Through Fluent API¶
using Flaggy.Helpers;
var initializer = new FeatureFlagInitializer(flagService);
await initializer.CreateFlag("api-rate-limit")
.Enabled()
.WithValue("100") // 100 requests per minute
.WithDescription("API rate limit per user per minute")
.CreateIfNotExistsAsync();
await initializer.CreateFlag("discount-rate")
.Enabled()
.WithValue("0.15") // 15% discount
.WithDescription("Current discount rate")
.UpsertAsync();
Through Dashboard¶
- Navigate to dashboard (e.g.,
/flaggy) - Create or edit a flag
- Enter value in the "Value" field
- Toggle "Enabled" to activate
- Save
Common Use Cases¶
1. Configuration Management¶
Use flags for dynamic configuration:
public class AppConfiguration
{
private readonly IFeatureFlagService _flagService;
public AppConfiguration(IFeatureFlagService flagService)
{
_flagService = flagService;
}
public async Task<int> GetMaxConcurrentConnections()
{
return (await _flagService.GetValueAsync<int>(
"max-concurrent-connections",
defaultValue: 100
)) ?? 100;
}
public async Task<int> GetRequestTimeoutSeconds()
{
return (await _flagService.GetValueAsync<int>(
"request-timeout-seconds",
defaultValue: 30
)) ?? 30;
}
public async Task<string> GetLogLevel()
{
return await _flagService.GetValueAsync(
"log-level",
defaultValue: "Information"
); // "Debug", "Information", "Warning", "Error"
}
public async Task<int> GetCacheDurationMinutes()
{
return (await _flagService.GetValueAsync<int>(
"cache-duration-minutes",
defaultValue: 5
)) ?? 5;
}
}
2. A/B Testing¶
Store variant identifiers:
public class ABTestingService
{
private readonly IFeatureFlagService _flagService;
public ABTestingService(IFeatureFlagService flagService)
{
_flagService = flagService;
}
public async Task<string> GetCheckoutVariant()
{
// Returns "variant-a", "variant-b", or "variant-c"
return await _flagService.GetValueAsync(
"checkout-page-variant",
defaultValue: "variant-a"
);
}
public async Task<string> GetHomepageLayout()
{
// Returns "classic", "modern", or "minimal"
return await _flagService.GetValueAsync(
"homepage-layout",
defaultValue: "classic"
);
}
public async Task<bool> ShowNewFeature(string userId)
{
// Get rollout percentage
var rolloutPercentage = await _flagService.GetValueAsync<double>(
"new-feature-rollout",
defaultValue: 0.0
);
// Determine if user is in rollout
var userHash = Math.Abs(userId.GetHashCode()) % 100;
return userHash < (rolloutPercentage ?? 0.0) * 100;
}
}
3. Gradual Rollout¶
Implement percentage-based rollouts:
public class FeatureRolloutService
{
private readonly IFeatureFlagService _flagService;
public FeatureRolloutService(IFeatureFlagService flagService)
{
_flagService = flagService;
}
public async Task<bool> IsUserInRollout(string featureKey, string userId)
{
// Check if feature is enabled
var isEnabled = await _flagService.IsEnabledAsync(featureKey);
if (!isEnabled)
return false;
// Get rollout percentage (0.0 to 1.0)
var percentage = await _flagService.GetValueAsync<double>(
featureKey,
defaultValue: 0.0
);
if (percentage == null || percentage == 0.0)
return false;
if (percentage >= 1.0)
return true; // 100% rollout
// Deterministic hash-based selection
var userHash = Math.Abs(userId.GetHashCode()) % 10000;
return userHash < percentage.Value * 10000;
}
}
// Usage
var showNewDashboard = await rolloutService.IsUserInRollout("new-dashboard", userId);
if (showNewDashboard)
{
// Show new dashboard
}
else
{
// Show old dashboard
}
4. Feature Versioning¶
Track feature versions:
public class VersionService
{
private readonly IFeatureFlagService _flagService;
public VersionService(IFeatureFlagService flagService)
{
_flagService = flagService;
}
public async Task<string> GetApiVersion()
{
return await _flagService.GetValueAsync(
"api-version",
defaultValue: "v1"
); // Returns "v1", "v2", "v3"
}
public async Task<string> GetDatabaseSchemaVersion()
{
return await _flagService.GetValueAsync(
"db-schema-version",
defaultValue: "1.0.0"
);
}
public async Task<int> GetProtocolVersion()
{
return (await _flagService.GetValueAsync<int>(
"protocol-version",
defaultValue: 1
)) ?? 1;
}
}
5. Resource Limits¶
Configure resource limits dynamically:
public class ResourceLimitService
{
private readonly IFeatureFlagService _flagService;
public ResourceLimitService(IFeatureFlagService flagService)
{
_flagService = flagService;
}
public async Task<int> GetMaxUploadSizeBytes()
{
return (await _flagService.GetValueAsync<int>(
"max-upload-size-bytes",
defaultValue: 10485760 // 10MB
)) ?? 10485760;
}
public async Task<int> GetMaxRequestsPerMinute()
{
return (await _flagService.GetValueAsync<int>(
"max-requests-per-minute",
defaultValue: 60
)) ?? 60;
}
public async Task<int> GetMaxDatabaseConnections()
{
return (await _flagService.GetValueAsync<int>(
"max-db-connections",
defaultValue: 100
)) ?? 100;
}
public async Task<TimeSpan> GetRequestTimeout()
{
var seconds = await _flagService.GetValueAsync<int>(
"request-timeout-seconds",
defaultValue: 30
);
return TimeSpan.FromSeconds(seconds ?? 30);
}
}
6. Multi-Valued Flags¶
Store comma-separated lists:
public class FeatureAccessService
{
private readonly IFeatureFlagService _flagService;
public FeatureAccessService(IFeatureFlagService flagService)
{
_flagService = flagService;
}
public async Task<List<string>> GetEnabledModules()
{
// Value: "module-a,module-b,module-c"
var modules = await _flagService.GetValueAsync(
"enabled-modules",
defaultValue: ""
);
return modules
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(m => m.Trim())
.ToList();
}
public async Task<bool> HasAccessToModule(string moduleName)
{
var enabledModules = await GetEnabledModules();
return enabledModules.Contains(moduleName);
}
public async Task<List<string>> GetAllowedIpRanges()
{
// Value: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
var ranges = await _flagService.GetValueAsync(
"allowed-ip-ranges",
defaultValue: "0.0.0.0/0"
);
return ranges
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(r => r.Trim())
.ToList();
}
}
Type Conversion¶
Flaggy uses TypeDescriptor for type conversion:
// Supported types (examples)
int intValue = await flagService.GetValueAsync<int>("key", 0);
long longValue = await flagService.GetValueAsync<long>("key", 0L);
double doubleValue = await flagService.GetValueAsync<double>("key", 0.0);
float floatValue = await flagService.GetValueAsync<float>("key", 0.0f);
decimal decimalValue = await flagService.GetValueAsync<decimal>("key", 0m);
bool boolValue = await flagService.GetValueAsync<bool>("key", false);
DateTime dateValue = await flagService.GetValueAsync<DateTime>("key", DateTime.UtcNow);
Guid guidValue = await flagService.GetValueAsync<Guid>("key", Guid.Empty);
Error Handling¶
If conversion fails, the default value is returned:
// Flag value: "not-a-number"
var value = await flagService.GetValueAsync<int>(
"max-users",
defaultValue: 100
);
// Returns: 100 (default, because conversion failed)
Best Practices¶
1. Always Provide Defaults¶
// Good - has default
var maxUsers = await flagService.GetValueAsync<int>("max-users", defaultValue: 100);
// Risky - null if flag doesn't exist
var maxUsers = await flagService.GetValueAsync<int>("max-users");
2. Use Semantic Keys¶
// Good - clear and descriptive
"max-upload-size-bytes"
"api-rate-limit-per-minute"
"discount-rate-percentage"
"new-feature-rollout-percent"
// Bad - unclear
"max-size"
"rate"
"discount"
"rollout"
3. Document Units¶
Include units in descriptions:
await initializer.CreateFlag("request-timeout")
.Enabled()
.WithValue("30")
.WithDescription("Request timeout in seconds") // Clear unit
.CreateAsync();
await initializer.CreateFlag("max-file-size")
.Enabled()
.WithValue("10485760")
.WithDescription("Maximum file upload size in bytes") // Clear unit
.CreateAsync();
4. Use Enums for Limited Options¶
public enum Theme { Light, Dark, Auto }
public async Task<Theme> GetTheme()
{
var themeStr = await _flagService.GetValueAsync("theme", defaultValue: "Light");
if (Enum.TryParse<Theme>(themeStr, out var theme))
{
return theme;
}
return Theme.Light; // Default
}
5. Validate Values¶
public async Task<int> GetPageSize()
{
var pageSize = await _flagService.GetValueAsync<int>(
"page-size",
defaultValue: 20
);
// Validate range
if (pageSize < 1 || pageSize > 100)
{
_logger.LogWarning("Invalid page size {Size}, using default", pageSize);
return 20;
}
return pageSize.Value;
}
6. Cache Frequently Used Values¶
public class CachedConfigService
{
private readonly IFeatureFlagService _flagService;
private readonly IMemoryCache _cache;
public CachedConfigService(IFeatureFlagService flagService, IMemoryCache cache)
{
_flagService = flagService;
_cache = cache;
}
public async Task<int> GetMaxUsers()
{
return await _cache.GetOrCreateAsync("config:max-users", async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
return (await _flagService.GetValueAsync<int>("max-users", defaultValue: 100)) ?? 100;
});
}
}
7. Use Type-Safe Wrappers¶
public class AppConfig
{
private readonly IFeatureFlagService _flagService;
public AppConfig(IFeatureFlagService flagService)
{
_flagService = flagService;
}
public async Task<int> MaxUsers() =>
(await _flagService.GetValueAsync<int>("max-users", defaultValue: 100)) ?? 100;
public async Task<double> DiscountRate() =>
(await _flagService.GetValueAsync<double>("discount-rate", defaultValue: 0.0)) ?? 0.0;
public async Task<TimeSpan> CacheExpiration()
{
var minutes = await _flagService.GetValueAsync<int>("cache-expiration-minutes", defaultValue: 5);
return TimeSpan.FromMinutes(minutes ?? 5);
}
}
Troubleshooting¶
Value Not Converting¶
Problem: Type conversion returns default value.
Solution: Check the stored value format:
// For int, store: "123"
// For double, store: "1.5" or "1,5" (depends on culture)
// For bool, store: "true" or "false"
// For DateTime, store ISO format: "2025-01-15T10:30:00Z"
Null Values¶
Problem: Getting null when expecting a value.
Solution: Check flag enabled state and provide defaults:
var value = await flagService.GetValueAsync<int>("key", defaultValue: 100);
var result = value ?? 100; // Handle nullable
Culture-Specific Parsing¶
Problem: Decimal parsing fails in different cultures.
Solution: Use invariant culture for storage:
// Store
var value = 1.5;
flag.Value = value.ToString(CultureInfo.InvariantCulture);
// Retrieve
var parsed = double.Parse(flag.Value, CultureInfo.InvariantCulture);
Related Topics¶
- Programmatic API - Managing flags and values
- Dashboard - Setting values through the UI
- Caching - Understanding value caching
- Providers - Value storage in different databases