Skip to content

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

  1. Navigate to dashboard (e.g., /flaggy)
  2. Create or edit a flag
  3. Enter value in the "Value" field
  4. Toggle "Enabled" to activate
  5. 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);