Skip to content

Commit 977f547

Browse files
authored
Fixes and Docs update (#10)
- Fix nullables features - Updated packages - Reorganized and fix some tests - Updated CONTRIBUTING.md and README.md
2 parents 3eea26a + 9bbcf78 commit 977f547

16 files changed

+303
-45
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -397,3 +397,6 @@ FodyWeavers.xsd
397397
# JetBrains Rider
398398
*.sln.iml
399399
/CodeMaid.config
400+
401+
# Others
402+
*.xcf

CONTRIBUTING.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,15 @@ This project and everyone participating in it are governed by our [Code of Condu
3737
- Be responsive to comments and feedback on your pull request.
3838

3939
## Reporting Bugs and Issues
40-
If you find a bug or have a question, please open an issue on GitHub. When reporting a bug, provide as much detail as possible, including:
40+
If you find a bug or have a question, please open an [issue](https://github.com/teociaps/FeatureManagement.Database/issues/new/choose) on GitHub. When reporting a bug, provide as much detail as possible, including:
4141

4242
- A clear and concise description of the bug.
4343
- Steps to reproduce the issue.
4444
- Expected behavior and actual behavior.
4545
- Screenshots or code snippets, if applicable.
4646

4747
## Feature Requests
48-
We welcome suggestions for new features or improvements. Please open an issue to propose your ideas.
48+
We welcome suggestions for new features or improvements. Please open an [issue](https://github.com/teociaps/FeatureManagement.Database/issues/new?template=feature-request.yml) to propose your ideas.
4949

5050
## License
5151
By contributing to this project, you agree that your contributions will be licensed under the [License](LICENSE) file.

README.md

+245-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,245 @@
1-
# FeatureManagement.Database
2-
An extension library of .NET Feature Management for implementing feature flags in a .NET or ASP.NET Core application through database.
1+
# <img height="55" src="\build\icon.png" align="center"> .NET Database Feature Management
2+
3+
4+
[![Build Status](https://github.com/teociaps/FeatureManagement.Database/actions/workflows/build.yml/badge.svg)](https://github.com/teociaps/FeatureManagement.Database/actions/workflows/build.yml)
5+
[![Test Status](https://github.com/teociaps/FeatureManagement.Database/actions/workflows/test.yml/badge.svg)](https://github.com/teociaps/FeatureManagement.Database/actions/workflows/test.yml)
6+
7+
**.NET Feature Management Database** extends [Feature Management] for retrieving feature definitions from various databases.
8+
It includes abstractions and default implementations to facilitate easy integration with your .NET applications.
9+
10+
11+
## Supported .NET Versions
12+
13+
| Version | Status |
14+
| ------- | ------ |
15+
| .NET 6 | ![Badge](https://img.shields.io/badge/Status-Supported-brightgreen) |
16+
| .NET 7 | ![Badge](https://img.shields.io/badge/Status-Supported-brightgreen) |
17+
| .NET 8 | ![Badge](https://img.shields.io/badge/Status-Supported-brightgreen) |
18+
19+
20+
## Index
21+
22+
* [Features](#features)
23+
* [Packages](#packages)
24+
* [Getting Started](#getting-started)
25+
* [Feature Store](#feature-store)
26+
* [Service Registration](#service-registration)
27+
* [Configure Cache](#configure-cache)
28+
* [Consumption](#consumption)
29+
* [ASP.NET Core Integration](#asp.net-core-integration)
30+
31+
32+
## Features
33+
34+
- **Database Integration**: store and retrieve feature definitions from various databases.
35+
- **Caching**: built-in support for caching feature definitions to enhance performance and reduce database load.
36+
- **Customizable**: easily extend and customize to support additional storage solutions and unique requirements.
37+
- **Seamless Integration**: integrates smoothly with Microsoft Feature Management, enabling efficient database-backed feature flag management.
38+
39+
40+
## Packages
41+
42+
| Package | NuGet Version |
43+
| ------- | ------------- |
44+
| [FeatureManagement.Database.Abstractions](https://www.nuget.org/packages/FeatureManagement.Database.Abstractions/) | [![NuGet Version](https://img.shields.io/nuget/v/FeatureManagement.Database.svg?style=flat)](https://www.nuget.org/packages/FeatureManagement.Database.Abstractions/)
45+
46+
**Package Purposes**
47+
48+
* _FeatureManagement.Database.Abstractions_
49+
* Standard functionalities for managing feature flags across various databases
50+
51+
52+
## Getting Started
53+
54+
.NET Feature Management Database allows you to manage feature definitions stored in a database.
55+
The built-in `DatabaseFeatureDefinitionProvider` retrieves these definitions and converts them into `FeatureDefinition` objects used by the feature management system.
56+
This setup relies on an implementation of the `IFeatureStore` interface to access the database.
57+
58+
### Entities
59+
60+
Two primary entities are pre-configured for database feature management:
61+
62+
- **Feature**: represents a feature with its associated settings. Each feature has a unique name, a requirement type, and a collection of settings that define how the feature is enabled or disabled.
63+
64+
- **FeatureSettings**: contains the settings for a feature and these define the conditions under which a feature is enabled.
65+
The parameters are stored in JSON format and based on Feature Management [built-in feature filter][Feature Management built-in filters] or [contextual feature filter][Feature Management contextual filters] configuration, and can include [custom feature filter][Feature Management custom filters] configuration.
66+
67+
#### Example
68+
69+
Suppose you want to define a feature that is enabled for 50% of the users.
70+
Here is an example of how you can define such a feature and its settings:
71+
72+
```csharp
73+
74+
var newFeature = new Feature
75+
{
76+
Id = Guid.NewGuid(),
77+
Name = "NewFeature",
78+
RequirementType = RequirementType.Any,
79+
Settings = new List<FeatureSettings>
80+
{
81+
new FeatureSettings
82+
{
83+
Id = Guid.NewGuid(),
84+
FilterType = FeatureFilterType.Percentage,
85+
Parameters = "{\"Value\":50}"
86+
}
87+
}
88+
}
89+
90+
```
91+
92+
### Feature Store
93+
94+
The `IFeatureStore` interface is the core abstraction for retrieving feature data from a database.
95+
Implement this interface to connect to your specific database (e.g., SQL Server, MongoDB, etc.).
96+
97+
```csharp
98+
public class MyFeatureStore : IFeatureStore
99+
{
100+
// Implementation to fetch feature definitions from your database
101+
}
102+
```
103+
104+
### Service Registration
105+
106+
Database feature management relies on .NET Core dependency injection.
107+
Registering the feature management services can be done using the following approaches:
108+
109+
* #### Register Feature Store and Feature Management Separately
110+
111+
First, register your custom `IFeatureStore` implementation and then add database feature management:
112+
113+
```csharp
114+
services.AddFeatureStore<MyFeatureStore>();
115+
services.AddDatabaseFeatureManagement();
116+
```
117+
118+
This approach allows for more modular and explicit registration of services.
119+
120+
* #### Register Feature Store and Feature Management in a Single Call
121+
122+
For a more streamlined setup, you can register your custom `IFeatureStore` and add database feature management in one step:
123+
124+
```csharp
125+
services.AddDatabaseFeatureManagement<MyFeatureStore>();
126+
```
127+
128+
This method simplifies the configuration by combining both registrations into a single call.
129+
130+
> [!NOTE]
131+
> In the context of database solutions, the feature management services will be added as scoped services.
132+
133+
> [!IMPORTANT]
134+
> To use database feature management, you need to register an **IFeatureStore**.
135+
136+
### Configure Cache
137+
138+
To improve performance and reduce database load, you can configure caching for the feature store.
139+
The `WithCacheService` method provides several ways to configure caching:
140+
141+
* #### Using Default Options
142+
143+
To register the cache service with default options:
144+
145+
```csharp
146+
services.AddDatabaseFeatureManagement<MyFeatureStore>()
147+
.WithCacheService();
148+
```
149+
150+
> By default, the inactive cache will be removed after 30 minutes.
151+
152+
* #### Using Custom Configuration Action
153+
154+
To register the cache service with custom options:
155+
156+
```csharp
157+
services.AddDatabaseFeatureManagement<MyFeatureStore>()
158+
.WithCacheService(options =>
159+
{
160+
options.SlidingExpiration = TimeSpan.FromMinutes(10);
161+
options.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1);
162+
});
163+
```
164+
165+
* #### Using Configuration Section
166+
167+
To register the cache service using settings from a configuration section:
168+
169+
```csharp
170+
var cacheConfiguration = Configuration.GetSection(FeatureCacheOptions.Name);
171+
services.AddDatabaseFeatureManagement<MyFeatureStore>()
172+
.WithCacheService(cacheConfiguration);
173+
```
174+
And in your `appsettings.json`:
175+
176+
```json
177+
{
178+
"FeatureCacheOptions": {
179+
"AbsoluteExpirationRelativeToNow": "01:00:00",
180+
"SlidingExpiration": "00:30:00"
181+
}
182+
}
183+
```
184+
185+
#### Advanced Cache Configuration
186+
187+
The cache keys have a prefix defined in the options (`FeatureCacheOptions.CachePrefix`).
188+
189+
- For a single feature, the default cache key will be the name of that feature (prefix included).
190+
Example:
191+
192+
```text
193+
MyFeature => FMDb_MyFeature
194+
```
195+
196+
- For all features, the default cache key will be "features" combined with the prefix:
197+
198+
```text
199+
All features => FMDb_features
200+
```
201+
202+
That _"features"_ can be overridden using one of the methods above. So you can have `"FMDb_your-own-cache-key"`.
203+
204+
See the `FeatureCacheOptions` class for more cache-related settings.
205+
206+
> [!WARNING]
207+
> When a feature value is updated in the database, the cache does not automatically clean up or refresh.
208+
> Ensure to handle cache invalidation appropriately in such scenarios to keep the cache in sync with the database.
209+
210+
211+
## Consumption
212+
213+
The basic form of feature management is checking if a feature flag is enabled and then performing actions based on the result.
214+
This is done through the `IFeatureManager`'s `IsEnabledAsync` method.
215+
216+
```csharp
217+
218+
IFeatureManager featureManager;
219+
220+
if (await featureManager.IsEnabledAsync("MyFeature"))
221+
{
222+
// Do something
223+
}
224+
```
225+
226+
See more [here][Feature Management Consumption].
227+
228+
### ASP.NET Core Integration
229+
230+
The database feature management library provides support in ASP.NET Core and MVC to enable common feature flag scenarios in web applications.
231+
232+
See more [here][Feature Management ASP.NET Core].
233+
234+
235+
## Contributing
236+
237+
Please see our [Contribution Guidelines](CONTRIBUTING.md) for more information.
238+
239+
240+
[Feature Management]: https://github.com/microsoft/FeatureManagement-Dotnet
241+
[Feature Management built-in filters]: https://github.com/microsoft/FeatureManagement-Dotnet?tab=readme-ov-file#built-in-feature-filters
242+
[Feature Management contextual filters]: https://github.com/microsoft/FeatureManagement-Dotnet?tab=readme-ov-file#contextual-feature-filters
243+
[Feature Management custom filters]: https://github.com/microsoft/FeatureManagement-Dotnet?tab=readme-ov-file#implementing-a-feature-filter
244+
[Feature Management Consumption]: https://github.com/microsoft/FeatureManagement-Dotnet?tab=readme-ov-file#consumption
245+
[Feature Management ASP.NET Core]: https://github.com/microsoft/FeatureManagement-Dotnet?tab=readme-ov-file#aspnet-core-integration

build/NuGet.props

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<RepositoryType>git</RepositoryType>
1111
<PackageProjectUrl>https://github.com/teociaps/FeatureManagement.Database</PackageProjectUrl>
1212
<PackageLicenseExpression>MIT</PackageLicenseExpression>
13-
<PackageIcon>icon.png</PackageIcon> <!-- TODO: change icon (64x64)-->
13+
<PackageIcon>icon.png</PackageIcon>
1414
<GenerateDocumentationFile>true</GenerateDocumentationFile>
1515
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
1616
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>

build/icon.png

55.5 KB
Loading

samples/WebApiApp/WebApiApp.csproj

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10-
<PackageReference Include="Microsoft.FeatureManagement.AspNetCore" Version="3.2.0" />
11-
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
10+
<PackageReference Include="Microsoft.FeatureManagement.AspNetCore" Version="3.3.1" />
11+
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
1212
</ItemGroup>
1313

1414
<ItemGroup>

src/Directory.Packages.props

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<ItemGroup>
66
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
77
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
8-
<PackageVersion Include="Microsoft.FeatureManagement" Version="3.2.0" />
8+
<PackageVersion Include="Microsoft.FeatureManagement" Version="3.3.1" />
99
<PackageVersion Include="System.Text.Json" Version="8.0.3" />
1010
</ItemGroup>
1111
</Project>

src/FeatureManagement.Database.Abstractions/FeatureManagement/Database/CachedFeatureStore.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public async Task<IReadOnlyCollection<Feature>> GetFeaturesAsync()
6262

6363
private async Task<TData> GetCacheAsync<TData>(string key) where TData : class
6464
{
65-
var cachedData = await _cache.GetAsync(key);
65+
var cachedData = await _cache.GetAsync(FeatureCacheOptions.CachePrefix + key);
6666

6767
if (cachedData is null)
6868
return null;
@@ -72,7 +72,7 @@ private async Task<TData> GetCacheAsync<TData>(string key) where TData : class
7272

7373
private async Task SetCacheAsync<TData>(string key, TData data) where TData : class
7474
{
75-
await _cache.SetAsync(key, JsonSerializer.SerializeToUtf8Bytes(data), new DistributedCacheEntryOptions
75+
await _cache.SetAsync(FeatureCacheOptions.CachePrefix + key, JsonSerializer.SerializeToUtf8Bytes(data), new DistributedCacheEntryOptions
7676
{
7777
AbsoluteExpiration = _cacheOptions.AbsoluteExpiration,
7878
AbsoluteExpirationRelativeToNow = _cacheOptions.AbsoluteExpirationRelativeToNow,

src/FeatureManagement.Database.Abstractions/FeatureManagement/Database/DatabaseFeatureDefinitionProvider.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public async IAsyncEnumerable<FeatureDefinition> GetAllFeatureDefinitionsAsync()
4242
var features = await _featureStore.GetFeaturesAsync();
4343
foreach (var feature in features)
4444
{
45-
if (string.IsNullOrWhiteSpace(feature.Name))
45+
if (string.IsNullOrWhiteSpace(feature?.Name))
4646
continue;
4747

4848
yield return GetDefinitionFromFeature(feature);

src/FeatureManagement.Database.Abstractions/FeatureManagement/Database/FeatureCacheOptions.cs

+5
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ public class FeatureCacheOptions
1313
/// </summary>
1414
public const string Name = nameof(FeatureCacheOptions);
1515

16+
/// <summary>
17+
/// The prefix for the cache keys.
18+
/// </summary>
19+
internal const string CachePrefix = "FMDb_";
20+
1621
/// <summary>
1722
/// Gets or sets an absolute expiration date for the cache entry.
1823
/// </summary>

src/FeatureManagement.Database.Abstractions/FeatureManagement/Database/FeatureManagementBuilderExtensions.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
using Microsoft.Extensions.Configuration;
1+
// Copyright (c) Matteo Ciapparelli.
2+
// Licensed under the MIT license.
3+
4+
using Microsoft.Extensions.Configuration;
25
using Microsoft.Extensions.DependencyInjection;
36
using Microsoft.FeatureManagement;
47

tests/Directory.Packages.props

+3-4
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@
44
</PropertyGroup>
55
<ItemGroup>
66
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
7-
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
87
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
9-
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
8+
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
109
<PackageVersion Include="NSubstitute" Version="5.1.0" />
1110
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
12-
<PackageVersion Include="xunit" Version="2.7.1" />
13-
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.8" />
11+
<PackageVersion Include="xunit" Version="2.8.0" />
12+
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.0" />
1413
</ItemGroup>
1514
</Project>

tests/FeatureManagement.Database.Tests/FeatureManagement.Database.Tests.csproj

-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
</PropertyGroup>
1111

1212
<ItemGroup>
13-
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
1413
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
1514
<PackageReference Include="NSubstitute" />
1615
<PackageReference Include="System.Linq.Async" />

tests/FeatureManagement.Database.Tests/FeatureManagement/Database/Tests/CachedFeatureStoreTests.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public async Task GetFeatureFromCachedStore()
2626

2727
// Act
2828
var feature = await cachedFeatureStore.GetFeatureAsync(FirstFeature);
29-
var cachedFeature = JsonSerializer.Deserialize<Feature>(await cache.GetAsync(FirstFeature));
29+
var cachedFeature = JsonSerializer.Deserialize<Feature>(await cache.GetAsync(FeatureCacheOptions.CachePrefix + FirstFeature));
3030

3131
// Assert
3232
Assert.True(feature is not null);
@@ -58,7 +58,7 @@ public async Task GetAllFeaturesFromCachedStore()
5858

5959
// Act
6060
var features = await cachedFeatureStore.GetFeaturesAsync();
61-
var cachedFeatures = JsonSerializer.Deserialize<IReadOnlyCollection<Feature>>(await cache.GetAsync(cacheOptions.KeyNames.AllFeatures));
61+
var cachedFeatures = JsonSerializer.Deserialize<IReadOnlyCollection<Feature>>(await cache.GetAsync(FeatureCacheOptions.CachePrefix + cacheOptions.KeyNames.AllFeatures));
6262

6363
// Assert
6464
Assert.True(features is not null);

0 commit comments

Comments
 (0)