Building Custom Storage Providers¶
Overview¶
This comprehensive guide covers how to build custom storage providers for Flaggy, allowing you to integrate with any data storage system including NoSQL databases, cloud services, or custom backends.
Understanding the Provider Architecture¶
Core Interface¶
All storage providers implement the IFeatureFlagProvider interface:
using Flaggy.Models;
namespace Flaggy.Abstractions;
public interface IFeatureFlagProvider
{
Task<FeatureFlag?> GetFlagAsync(string key, CancellationToken cancellationToken = default);
Task<IEnumerable<FeatureFlag>> GetAllFlagsAsync(CancellationToken cancellationToken = default);
Task<bool> CreateFlagAsync(FeatureFlag flag, CancellationToken cancellationToken = default);
Task<bool> UpdateFlagAsync(FeatureFlag flag, CancellationToken cancellationToken = default);
Task<bool> DeleteFlagAsync(string key, CancellationToken cancellationToken = default);
}
FeatureFlag Model¶
namespace Flaggy.Models;
public class FeatureFlag
{
public required string Key { get; set; }
public bool IsEnabled { get; set; }
public string? Value { get; set; }
public string? Description { get; set; }
public DateTime? CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
}
Building a Basic Provider¶
Example: MongoDB Provider¶
using Flaggy.Abstractions;
using Flaggy.Models;
using MongoDB.Driver;
namespace Flaggy.Provider.MongoDB;
public class MongoDbFeatureFlagProvider : IFeatureFlagProvider
{
private readonly IMongoCollection<FeatureFlag> _collection;
private readonly ILogger<MongoDbFeatureFlagProvider> _logger;
public MongoDbFeatureFlagProvider(
string connectionString,
string databaseName = "flaggy",
string collectionName = "feature_flags")
{
var client = new MongoClient(connectionString);
var database = client.GetDatabase(databaseName);
_collection = database.GetCollection<FeatureFlag>(collectionName);
// Create index on Key field for fast lookups
var keyIndex = Builders<FeatureFlag>.IndexKeys.Ascending(f => f.Key);
_collection.Indexes.CreateOne(new CreateIndexModel<FeatureFlag>(keyIndex, new CreateIndexOptions { Unique = true }));
}
public async Task<FeatureFlag?> GetFlagAsync(string key, CancellationToken cancellationToken = default)
{
var filter = Builders<FeatureFlag>.Filter.Eq(f => f.Key, key);
return await _collection.Find(filter).FirstOrDefaultAsync(cancellationToken);
}
public async Task<IEnumerable<FeatureFlag>> GetAllFlagsAsync(CancellationToken cancellationToken = default)
{
return await _collection.Find(_ => true).ToListAsync(cancellationToken);
}
public async Task<bool> CreateFlagAsync(FeatureFlag flag, CancellationToken cancellationToken = default)
{
try
{
flag.CreatedAt = DateTime.UtcNow;
flag.UpdatedAt = DateTime.UtcNow;
await _collection.InsertOneAsync(flag, null, cancellationToken);
return true;
}
catch (MongoWriteException ex) when (ex.WriteError.Category == ServerErrorCategory.DuplicateKey)
{
// Key already exists
return false;
}
}
public async Task<bool> UpdateFlagAsync(FeatureFlag flag, CancellationToken cancellationToken = default)
{
flag.UpdatedAt = DateTime.UtcNow;
var filter = Builders<FeatureFlag>.Filter.Eq(f => f.Key, flag.Key);
var update = Builders<FeatureFlag>.Update
.Set(f => f.IsEnabled, flag.IsEnabled)
.Set(f => f.Value, flag.Value)
.Set(f => f.Description, flag.Description)
.Set(f => f.UpdatedAt, flag.UpdatedAt);
var result = await _collection.UpdateOneAsync(filter, update, null, cancellationToken);
return result.ModifiedCount > 0;
}
public async Task<bool> DeleteFlagAsync(string key, CancellationToken cancellationToken = default)
{
var filter = Builders<FeatureFlag>.Filter.Eq(f => f.Key, key);
var result = await _collection.DeleteOneAsync(filter, cancellationToken);
return result.DeletedCount > 0;
}
}
Extension Methods for Registration¶
using Flaggy.Provider.MongoDB;
using Microsoft.Extensions.DependencyInjection;
namespace Flaggy.Provider.MongoDB.Extensions;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddFlaggyMongoDB(
this IServiceCollection services,
string connectionString,
string databaseName = "flaggy",
string collectionName = "feature_flags",
TimeSpan? cacheExpiration = null)
{
services.AddSingleton<IFeatureFlagProvider>(sp =>
new MongoDbFeatureFlagProvider(connectionString, databaseName, collectionName));
services.AddFlaggy(
sp => sp.GetRequiredService<IFeatureFlagProvider>(),
cacheExpiration: cacheExpiration
);
return services;
}
}
Usage¶
using Flaggy.Provider.MongoDB.Extensions;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFlaggyMongoDB(
connectionString: "mongodb://localhost:27017",
databaseName: "myapp",
collectionName: "feature_flags",
cacheExpiration: TimeSpan.FromMinutes(5)
);
var app = builder.Build();
app.Run();
Advanced Provider Examples¶
Example: Redis Provider¶
using Flaggy.Abstractions;
using Flaggy.Models;
using StackExchange.Redis;
using System.Text.Json;
namespace Flaggy.Provider.Redis;
public class RedisFeatureFlagProvider : IFeatureFlagProvider
{
private readonly IDatabase _database;
private readonly string _keyPrefix;
private readonly JsonSerializerOptions _jsonOptions;
public RedisFeatureFlagProvider(
string connectionString,
string keyPrefix = "flaggy:",
int database = 0)
{
var redis = ConnectionMultiplexer.Connect(connectionString);
_database = redis.GetDatabase(database);
_keyPrefix = keyPrefix;
_jsonOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
}
private string GetRedisKey(string flagKey) => $"{_keyPrefix}{flagKey}";
private string GetAllKeysPattern() => $"{_keyPrefix}*";
public async Task<FeatureFlag?> GetFlagAsync(string key, CancellationToken cancellationToken = default)
{
var redisKey = GetRedisKey(key);
var value = await _database.StringGetAsync(redisKey);
if (value.IsNullOrEmpty)
return null;
return JsonSerializer.Deserialize<FeatureFlag>(value!, _jsonOptions);
}
public async Task<IEnumerable<FeatureFlag>> GetAllFlagsAsync(CancellationToken cancellationToken = default)
{
var server = _database.Multiplexer.GetServer(_database.Multiplexer.GetEndPoints().First());
var keys = server.Keys(pattern: GetAllKeysPattern()).ToArray();
if (keys.Length == 0)
return Enumerable.Empty<FeatureFlag>();
var values = await _database.StringGetAsync(keys);
return values
.Where(v => !v.IsNullOrEmpty)
.Select(v => JsonSerializer.Deserialize<FeatureFlag>(v!, _jsonOptions))
.Where(f => f != null)
.Cast<FeatureFlag>();
}
public async Task<bool> CreateFlagAsync(FeatureFlag flag, CancellationToken cancellationToken = default)
{
flag.CreatedAt = DateTime.UtcNow;
flag.UpdatedAt = DateTime.UtcNow;
var redisKey = GetRedisKey(flag.Key);
var json = JsonSerializer.Serialize(flag, _jsonOptions);
// Use SetNX to only create if it doesn't exist
return await _database.StringSetAsync(redisKey, json, when: When.NotExists);
}
public async Task<bool> UpdateFlagAsync(FeatureFlag flag, CancellationToken cancellationToken = default)
{
flag.UpdatedAt = DateTime.UtcNow;
var redisKey = GetRedisKey(flag.Key);
// Check if key exists first
if (!await _database.KeyExistsAsync(redisKey))
return false;
var json = JsonSerializer.Serialize(flag, _jsonOptions);
return await _database.StringSetAsync(redisKey, json);
}
public async Task<bool> DeleteFlagAsync(string key, CancellationToken cancellationToken = default)
{
var redisKey = GetRedisKey(key);
return await _database.KeyDeleteAsync(redisKey);
}
}
Example: Azure Cosmos DB Provider¶
using Flaggy.Abstractions;
using Flaggy.Models;
using Microsoft.Azure.Cosmos;
namespace Flaggy.Provider.CosmosDb;
public class CosmosDbFeatureFlagProvider : IFeatureFlagProvider
{
private readonly Container _container;
private readonly ILogger<CosmosDbFeatureFlagProvider> _logger;
public CosmosDbFeatureFlagProvider(
string connectionString,
string databaseName = "flaggy",
string containerName = "feature_flags")
{
var client = new CosmosClient(connectionString);
_container = client.GetContainer(databaseName, containerName);
}
public async Task<FeatureFlag?> GetFlagAsync(string key, CancellationToken cancellationToken = default)
{
try
{
var response = await _container.ReadItemAsync<FeatureFlag>(
key,
new PartitionKey(key),
cancellationToken: cancellationToken
);
return response.Resource;
}
catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return null;
}
}
public async Task<IEnumerable<FeatureFlag>> GetAllFlagsAsync(CancellationToken cancellationToken = default)
{
var query = new QueryDefinition("SELECT * FROM c");
var iterator = _container.GetItemQueryIterator<FeatureFlag>(query);
var results = new List<FeatureFlag>();
while (iterator.HasMoreResults)
{
var response = await iterator.ReadNextAsync(cancellationToken);
results.AddRange(response);
}
return results;
}
public async Task<bool> CreateFlagAsync(FeatureFlag flag, CancellationToken cancellationToken = default)
{
try
{
flag.CreatedAt = DateTime.UtcNow;
flag.UpdatedAt = DateTime.UtcNow;
await _container.CreateItemAsync(
flag,
new PartitionKey(flag.Key),
cancellationToken: cancellationToken
);
return true;
}
catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Conflict)
{
// Item already exists
return false;
}
}
public async Task<bool> UpdateFlagAsync(FeatureFlag flag, CancellationToken cancellationToken = default)
{
try
{
flag.UpdatedAt = DateTime.UtcNow;
await _container.UpsertItemAsync(
flag,
new PartitionKey(flag.Key),
cancellationToken: cancellationToken
);
return true;
}
catch (CosmosException)
{
return false;
}
}
public async Task<bool> DeleteFlagAsync(string key, CancellationToken cancellationToken = default)
{
try
{
await _container.DeleteItemAsync<FeatureFlag>(
key,
new PartitionKey(key),
cancellationToken: cancellationToken
);
return true;
}
catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return false;
}
}
}
Example: AWS DynamoDB Provider¶
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.DataModel;
using Amazon.DynamoDBv2.DocumentModel;
using Flaggy.Abstractions;
using Flaggy.Models;
namespace Flaggy.Provider.DynamoDB;
public class DynamoDbFeatureFlagProvider : IFeatureFlagProvider
{
private readonly DynamoDBContext _context;
private readonly string _tableName;
public DynamoDbFeatureFlagProvider(
string awsAccessKeyId,
string awsSecretAccessKey,
string region,
string tableName = "FeatureFlags")
{
var config = new AmazonDynamoDBConfig
{
RegionEndpoint = Amazon.RegionEndpoint.GetBySystemName(region)
};
var client = new AmazonDynamoDBClient(awsAccessKeyId, awsSecretAccessKey, config);
_context = new DynamoDBContext(client);
_tableName = tableName;
}
public async Task<FeatureFlag?> GetFlagAsync(string key, CancellationToken cancellationToken = default)
{
return await _context.LoadAsync<FeatureFlag>(key, cancellationToken);
}
public async Task<IEnumerable<FeatureFlag>> GetAllFlagsAsync(CancellationToken cancellationToken = default)
{
var search = _context.ScanAsync<FeatureFlag>(null);
return await search.GetRemainingAsync(cancellationToken);
}
public async Task<bool> CreateFlagAsync(FeatureFlag flag, CancellationToken cancellationToken = default)
{
try
{
// Check if item exists
var existing = await GetFlagAsync(flag.Key, cancellationToken);
if (existing != null)
return false;
flag.CreatedAt = DateTime.UtcNow;
flag.UpdatedAt = DateTime.UtcNow;
await _context.SaveAsync(flag, cancellationToken);
return true;
}
catch
{
return false;
}
}
public async Task<bool> UpdateFlagAsync(FeatureFlag flag, CancellationToken cancellationToken = default)
{
try
{
// Check if item exists
var existing = await GetFlagAsync(flag.Key, cancellationToken);
if (existing == null)
return false;
flag.CreatedAt = existing.CreatedAt; // Preserve creation time
flag.UpdatedAt = DateTime.UtcNow;
await _context.SaveAsync(flag, cancellationToken);
return true;
}
catch
{
return false;
}
}
public async Task<bool> DeleteFlagAsync(string key, CancellationToken cancellationToken = default)
{
try
{
await _context.DeleteAsync<FeatureFlag>(key, cancellationToken);
return true;
}
catch
{
return false;
}
}
}
Provider Best Practices¶
1. Thread Safety¶
Ensure your provider is thread-safe for concurrent access:
public class ThreadSafeFeatureFlagProvider : IFeatureFlagProvider
{
private readonly SemaphoreSlim _semaphore = new(1, 1);
private readonly Dictionary<string, FeatureFlag> _flags = new();
public async Task<bool> CreateFlagAsync(FeatureFlag flag, CancellationToken cancellationToken = default)
{
await _semaphore.WaitAsync(cancellationToken);
try
{
if (_flags.ContainsKey(flag.Key))
return false;
_flags[flag.Key] = flag;
return true;
}
finally
{
_semaphore.Release();
}
}
// Implement other methods with proper locking...
}
2. Connection Pooling and Resource Management¶
public class OptimizedDatabaseProvider : IFeatureFlagProvider, IDisposable
{
private readonly DbConnection _connection;
private bool _disposed = false;
public OptimizedDatabaseProvider(string connectionString)
{
// Use connection pooling
_connection = new NpgsqlConnection(connectionString);
}
public async Task<FeatureFlag?> GetFlagAsync(string key, CancellationToken cancellationToken = default)
{
if (_disposed)
throw new ObjectDisposedException(nameof(OptimizedDatabaseProvider));
// Reuse connection for multiple queries
await EnsureConnectionOpenAsync(cancellationToken);
// Implement query logic...
}
private async Task EnsureConnectionOpenAsync(CancellationToken cancellationToken)
{
if (_connection.State != ConnectionState.Open)
{
await _connection.OpenAsync(cancellationToken);
}
}
public void Dispose()
{
if (!_disposed)
{
_connection?.Dispose();
_disposed = true;
}
}
}
3. Error Handling and Logging¶
public class RobustFeatureFlagProvider : IFeatureFlagProvider
{
private readonly ILogger<RobustFeatureFlagProvider> _logger;
private readonly IFeatureFlagProvider _innerProvider;
public RobustFeatureFlagProvider(
IFeatureFlagProvider innerProvider,
ILogger<RobustFeatureFlagProvider> logger)
{
_innerProvider = innerProvider;
_logger = logger;
}
public async Task<FeatureFlag?> GetFlagAsync(string key, CancellationToken cancellationToken = default)
{
try
{
_logger.LogDebug("Getting flag: {Key}", key);
return await _innerProvider.GetFlagAsync(key, cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting flag: {Key}", key);
return null; // Or rethrow based on requirements
}
}
public async Task<bool> CreateFlagAsync(FeatureFlag flag, CancellationToken cancellationToken = default)
{
try
{
_logger.LogInformation("Creating flag: {Key}", flag.Key);
var result = await _innerProvider.CreateFlagAsync(flag, cancellationToken);
if (result)
_logger.LogInformation("Successfully created flag: {Key}", flag.Key);
else
_logger.LogWarning("Failed to create flag (may already exist): {Key}", flag.Key);
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating flag: {Key}", flag.Key);
return false;
}
}
// Implement other methods with similar error handling...
}
4. Performance Optimization¶
public class CachedFeatureFlagProvider : IFeatureFlagProvider
{
private readonly IFeatureFlagProvider _innerProvider;
private readonly IMemoryCache _localCache;
private readonly TimeSpan _cacheDuration;
public CachedFeatureFlagProvider(
IFeatureFlagProvider innerProvider,
IMemoryCache localCache,
TimeSpan? cacheDuration = null)
{
_innerProvider = innerProvider;
_localCache = localCache;
_cacheDuration = cacheDuration ?? TimeSpan.FromMinutes(1);
}
public async Task<FeatureFlag?> GetFlagAsync(string key, CancellationToken cancellationToken = default)
{
var cacheKey = $"flag:{key}";
if (_localCache.TryGetValue<FeatureFlag>(cacheKey, out var cachedFlag))
{
return cachedFlag;
}
var flag = await _innerProvider.GetFlagAsync(key, cancellationToken);
if (flag != null)
{
_localCache.Set(cacheKey, flag, _cacheDuration);
}
return flag;
}
public async Task<bool> UpdateFlagAsync(FeatureFlag flag, CancellationToken cancellationToken = default)
{
var result = await _innerProvider.UpdateFlagAsync(flag, cancellationToken);
if (result)
{
// Invalidate cache
_localCache.Remove($"flag:{flag.Key}");
}
return result;
}
// Implement other methods with cache invalidation...
}
5. Retry and Circuit Breaker Patterns¶
using Polly;
public class ResilientFeatureFlagProvider : IFeatureFlagProvider
{
private readonly IFeatureFlagProvider _innerProvider;
private readonly IAsyncPolicy _retryPolicy;
private readonly IAsyncPolicy _circuitBreakerPolicy;
public ResilientFeatureFlagProvider(IFeatureFlagProvider innerProvider)
{
_innerProvider = innerProvider;
_retryPolicy = Policy
.Handle<Exception>()
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))
);
_circuitBreakerPolicy = Policy
.Handle<Exception>()
.CircuitBreakerAsync(
exceptionsAllowedBeforeBreaking: 5,
durationOfBreak: TimeSpan.FromMinutes(1)
);
}
public async Task<FeatureFlag?> GetFlagAsync(string key, CancellationToken cancellationToken = default)
{
return await _circuitBreakerPolicy.ExecuteAsync(async () =>
await _retryPolicy.ExecuteAsync(async () =>
await _innerProvider.GetFlagAsync(key, cancellationToken)
)
);
}
// Implement other methods with resilience patterns...
}
Testing Custom Providers¶
Unit Tests¶
using Xunit;
using FluentAssertions;
public class MongoDbFeatureFlagProviderTests
{
private readonly MongoDbFeatureFlagProvider _provider;
public MongoDbFeatureFlagProviderTests()
{
// Use test database
_provider = new MongoDbFeatureFlagProvider(
"mongodb://localhost:27017",
databaseName: "flaggy_test"
);
}
[Fact]
public async Task CreateFlagAsync_WhenValid_CreatesFlag()
{
// Arrange
var flag = new FeatureFlag
{
Key = $"test-flag-{Guid.NewGuid()}",
IsEnabled = true,
Value = "test"
};
// Act
var result = await _provider.CreateFlagAsync(flag);
// Assert
result.Should().BeTrue();
var retrieved = await _provider.GetFlagAsync(flag.Key);
retrieved.Should().NotBeNull();
retrieved!.IsEnabled.Should().BeTrue();
}
[Fact]
public async Task CreateFlagAsync_WhenDuplicateKey_ReturnsFalse()
{
// Arrange
var flag = new FeatureFlag { Key = $"duplicate-{Guid.NewGuid()}", IsEnabled = true };
await _provider.CreateFlagAsync(flag);
// Act
var result = await _provider.CreateFlagAsync(flag);
// Assert
result.Should().BeFalse();
}
[Fact]
public async Task UpdateFlagAsync_WhenExists_UpdatesFlag()
{
// Arrange
var flag = new FeatureFlag { Key = $"update-test-{Guid.NewGuid()}", IsEnabled = false };
await _provider.CreateFlagAsync(flag);
flag.IsEnabled = true;
flag.Value = "updated";
// Act
var result = await _provider.UpdateFlagAsync(flag);
// Assert
result.Should().BeTrue();
var retrieved = await _provider.GetFlagAsync(flag.Key);
retrieved!.IsEnabled.Should().BeTrue();
retrieved.Value.Should().Be("updated");
}
[Fact]
public async Task DeleteFlagAsync_WhenExists_DeletesFlag()
{
// Arrange
var flag = new FeatureFlag { Key = $"delete-test-{Guid.NewGuid()}", IsEnabled = true };
await _provider.CreateFlagAsync(flag);
// Act
var result = await _provider.DeleteFlagAsync(flag.Key);
// Assert
result.Should().BeTrue();
var retrieved = await _provider.GetFlagAsync(flag.Key);
retrieved.Should().BeNull();
}
[Fact]
public async Task GetAllFlagsAsync_ReturnsAllFlags()
{
// Arrange
var prefix = Guid.NewGuid().ToString();
await _provider.CreateFlagAsync(new FeatureFlag { Key = $"{prefix}-1", IsEnabled = true });
await _provider.CreateFlagAsync(new FeatureFlag { Key = $"{prefix}-2", IsEnabled = false });
// Act
var allFlags = await _provider.GetAllFlagsAsync();
// Assert
var testFlags = allFlags.Where(f => f.Key.StartsWith(prefix)).ToList();
testFlags.Should().HaveCount(2);
}
}
Integration Tests¶
public class ProviderIntegrationTests : IAsyncLifetime
{
private readonly IFeatureFlagService _flagService;
private readonly IFeatureFlagProvider _provider;
public ProviderIntegrationTests()
{
var services = new ServiceCollection();
_provider = new MongoDbFeatureFlagProvider("mongodb://localhost:27017", "flaggy_integration_test");
services.AddSingleton(_provider);
services.AddFlaggy(sp => sp.GetRequiredService<IFeatureFlagProvider>());
var serviceProvider = services.BuildServiceProvider();
_flagService = serviceProvider.GetRequiredService<IFeatureFlagService>();
}
public async Task InitializeAsync()
{
// Clean up any existing test data
var allFlags = await _provider.GetAllFlagsAsync();
foreach (var flag in allFlags)
{
await _provider.DeleteFlagAsync(flag.Key);
}
}
public Task DisposeAsync() => Task.CompletedTask;
[Fact]
public async Task EndToEnd_CreateCheckAndDelete_WorksCorrectly()
{
// Create
await _flagService.CreateFlagAsync(new FeatureFlag
{
Key = "integration-test-flag",
IsEnabled = true,
Value = "test-value"
});
// Check
var isEnabled = await _flagService.IsEnabledAsync("integration-test-flag");
Assert.True(isEnabled);
var value = await _flagService.GetValueAsync("integration-test-flag");
Assert.Equal("test-value", value);
// Delete
await _flagService.DeleteFlagAsync("integration-test-flag");
var deleted = await _flagService.GetFlagAsync("integration-test-flag");
Assert.Null(deleted);
}
}
Common Pitfalls and Solutions¶
Pitfall 1: Not Handling Timestamps Correctly¶
// Bad - Missing timestamp updates
public async Task<bool> CreateFlagAsync(FeatureFlag flag, CancellationToken cancellationToken = default)
{
await _collection.InsertOneAsync(flag, cancellationToken);
return true;
}
// Good - Always set timestamps
public async Task<bool> CreateFlagAsync(FeatureFlag flag, CancellationToken cancellationToken = default)
{
flag.CreatedAt = DateTime.UtcNow; // Always use UTC
flag.UpdatedAt = DateTime.UtcNow;
await _collection.InsertOneAsync(flag, cancellationToken);
return true;
}
Pitfall 2: Not Handling Duplicate Keys¶
// Bad - Throws exception on duplicate
public async Task<bool> CreateFlagAsync(FeatureFlag flag, CancellationToken cancellationToken = default)
{
await _collection.InsertOneAsync(flag, cancellationToken);
return true; // May throw if key exists
}
// Good - Returns false on duplicate
public async Task<bool> CreateFlagAsync(FeatureFlag flag, CancellationToken cancellationToken = default)
{
try
{
await _collection.InsertOneAsync(flag, cancellationToken);
return true;
}
catch (DuplicateKeyException)
{
return false;
}
}
Pitfall 3: Not Cancelling Long Operations¶
// Bad - Ignores cancellation token
public async Task<IEnumerable<FeatureFlag>> GetAllFlagsAsync(CancellationToken cancellationToken = default)
{
return await _collection.Find(_ => true).ToListAsync(); // Wrong
}
// Good - Passes cancellation token
public async Task<IEnumerable<FeatureFlag>> GetAllFlagsAsync(CancellationToken cancellationToken = default)
{
return await _collection.Find(_ => true).ToListAsync(cancellationToken);
}
Documentation and Examples¶
Creating Provider Documentation¶
# MongoDB Provider for Flaggy
## Installation
```bash
dotnet add package Flaggy.Provider.MongoDB
Configuration¶
using Flaggy.Provider.MongoDB.Extensions;
builder.Services.AddFlaggyMongoDB(
connectionString: "mongodb://localhost:27017",
databaseName: "myapp",
collectionName: "feature_flags"
);
Schema¶
The provider automatically creates an index on the Key field for optimal performance.
Performance¶
- Average read latency: ~2-5ms
- Average write latency: ~5-10ms
- Supports horizontal scaling via MongoDB sharding ```