Skip to content

Add getting features from database example #389

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Microsoft.FeatureManagement.sln
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TargetingConsoleApp", "exam
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazorServerApp", "examples\BlazorServerApp\BlazorServerApp.csproj", "{12BAB5A6-4EEB-4917-B5D9-4AFB6253008E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GettingFeaturesFromDatabase", "examples\GettingFeaturesFromDatabase\GettingFeaturesFromDatabase.csproj", "{C58C3CF1-756A-4A3B-9591-99F0D372B208}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -67,6 +69,10 @@ Global
{12BAB5A6-4EEB-4917-B5D9-4AFB6253008E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{12BAB5A6-4EEB-4917-B5D9-4AFB6253008E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{12BAB5A6-4EEB-4917-B5D9-4AFB6253008E}.Release|Any CPU.Build.0 = Release|Any CPU
{C58C3CF1-756A-4A3B-9591-99F0D372B208}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C58C3CF1-756A-4A3B-9591-99F0D372B208}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C58C3CF1-756A-4A3B-9591-99F0D372B208}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C58C3CF1-756A-4A3B-9591-99F0D372B208}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -79,6 +85,7 @@ Global
{DACAB624-4611-42E8-844C-529F93A54980} = {FB5C34DF-695C-4DF9-8AED-B3EA2516EA72}
{283D3EBB-4716-4F1D-BA51-A435F7E2AB82} = {FB5C34DF-695C-4DF9-8AED-B3EA2516EA72}
{12BAB5A6-4EEB-4917-B5D9-4AFB6253008E} = {FB5C34DF-695C-4DF9-8AED-B3EA2516EA72}
{C58C3CF1-756A-4A3B-9591-99F0D372B208} = {FB5C34DF-695C-4DF9-8AED-B3EA2516EA72}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {84DA6C54-F140-4518-A1B4-E4CF42117FBD}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace GettingFeaturesFromDatabase.Constants;

public static class FeatureConstants
{
public const string Weather = "Weather";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using GettingFeaturesFromDatabase.Database;
using GettingFeaturesFromDatabase.Database.Services;
using Microsoft.AspNetCore.Mvc;

namespace GettingFeaturesFromDatabase.Controllers;

[ApiController]
[Route("feature")]
public class FeatureController : ControllerBase
{
private readonly IFeatureService _featureService;

public FeatureController(IFeatureService featureService)
{
_featureService = featureService;
}

[HttpGet]
public async Task<IReadOnlyCollection<Feature>> GetFeatures()
{
return await _featureService.GetFeatureAsync();
}

[HttpPut]
public async Task UpdateFeature(string featureName, bool isEnabled)
{
await _featureService.UpdateFeatureAsync(featureName, isEnabled);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using GettingFeaturesFromDatabase.Constants;
using Microsoft.AspNetCore.Mvc;
using Microsoft.FeatureManagement.Mvc;

namespace GettingFeaturesFromDatabase.Controllers;

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

private readonly ILogger<WeatherForecastController> _logger;

public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}

[HttpGet(Name = "GetWeatherForecast")]
[FeatureGate(FeatureConstants.Weather)]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using GettingFeaturesFromDatabase.Database;
using GettingFeaturesFromDatabase.Database.Services;
using Microsoft.FeatureManagement;

namespace GettingFeaturesFromDatabase;

public class CustomFeatureDefinitionProvider : IFeatureDefinitionProvider
{
private readonly IFeatureService _featureService;

public CustomFeatureDefinitionProvider(IFeatureService featureService)
{
_featureService = featureService;
}

public async Task<FeatureDefinition> GetFeatureDefinitionAsync(string featureName)
{
var feature = await _featureService.GetFeatureAsync(featureName);

return GenerateFeatureDefinition(feature);
}

public async IAsyncEnumerable<FeatureDefinition> GetAllFeatureDefinitionsAsync()
{
var features = await _featureService.GetFeatureAsync();

foreach (var feature in features)
{
yield return GenerateFeatureDefinition(feature);
}
}

private FeatureDefinition GenerateFeatureDefinition(Feature? feature)
{
if (feature is null)
{
return new FeatureDefinition();
}

if (feature.IsEnabled)
{
return new FeatureDefinition
{
Name = feature.Name,
EnabledFor = new[]
{
new FeatureFilterConfiguration
{
Name = "AlwaysOn",
},
},
};
}

return new FeatureDefinition
{
Name = feature.Name,
};
}
}
8 changes: 8 additions & 0 deletions examples/GettingFeaturesFromDatabase/Database/Feature.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace GettingFeaturesFromDatabase.Database;

public class Feature
{
public string Name { get; set; }

public bool IsEnabled { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using GettingFeaturesFromDatabase.Constants;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace GettingFeaturesFromDatabase.Database;

internal sealed class FeatureConfiguration : IEntityTypeConfiguration<Feature>
{
public void Configure(EntityTypeBuilder<Feature> builder)
{
builder.Property(x => x.Name).IsRequired();
builder.Property(x => x.IsEnabled).IsRequired();

/* Populate with feature data. */
builder.HasData(new List<Feature>()
{
new Feature { Name = FeatureConstants.Weather, IsEnabled = true },
});

builder.ToTable("Features").HasKey(x => x.Name);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using GettingFeaturesFromDatabase.Database.Services;
using Microsoft.EntityFrameworkCore;

namespace GettingFeaturesFromDatabase.Database;

public static class ServiceCollectionExtensions
{
public static void AddDatabase(this IServiceCollection services)
{
services.AddDbContext<SqliteDbContext>(options =>
{
options.UseSqlite("Data Source=example.db");
});

/* Create Database On Start */
var databaseContext = services.BuildServiceProvider().GetRequiredService<SqliteDbContext>();
databaseContext.Database.EnsureDeleted();
databaseContext.Database.EnsureCreated();
}

public static void AddFeatureService(this IServiceCollection services)
{
services.AddScoped<IFeatureService, FeatureService>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Microsoft.EntityFrameworkCore;

namespace GettingFeaturesFromDatabase.Database.Services;

public class FeatureService : IFeatureService
{
private readonly SqliteDbContext _sqliteDbContext;

public FeatureService(SqliteDbContext sqliteDbContext)
{
_sqliteDbContext = sqliteDbContext;
}

public async Task<Feature?> GetFeatureAsync(string featureName)
{
var feature = await _sqliteDbContext.Set<Feature>().FindAsync(featureName);

return feature;
}

public async Task<IReadOnlyCollection<Feature>> GetFeatureAsync()
{
var features = await _sqliteDbContext.Set<Feature>().ToListAsync();

return features;
}

public async Task UpdateFeatureAsync(string featureName, bool isEnabled)
{
var feature = await _sqliteDbContext.Set<Feature>().FindAsync(featureName);
if (feature != null) feature.IsEnabled = isEnabled;

await _sqliteDbContext.SaveChangesAsync();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace GettingFeaturesFromDatabase.Database.Services;

public interface IFeatureService
{
Task<Feature?> GetFeatureAsync(string featureName);

Task<IReadOnlyCollection<Feature>> GetFeatureAsync();

Task UpdateFeatureAsync(string featureName, bool isEnabled);
}
19 changes: 19 additions & 0 deletions examples/GettingFeaturesFromDatabase/Database/SqliteDbContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Microsoft.EntityFrameworkCore;

namespace GettingFeaturesFromDatabase.Database;

public class SqliteDbContext : DbContext
{
public DbSet<Feature> Features { get; set; } = null!;

public SqliteDbContext(DbContextOptions<SqliteDbContext> options) : base(options)
{
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);

modelBuilder.ApplyConfiguration(new FeatureConfiguration());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.16" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.16" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="6.5.0" />
<ProjectReference Include="..\..\src\Microsoft.FeatureManagement\Microsoft.FeatureManagement.csproj" />
<ProjectReference Include="..\..\src\Microsoft.FeatureManagement.AspNetCore\Microsoft.FeatureManagement.AspNetCore.csproj" />
</ItemGroup>

</Project>
30 changes: 30 additions & 0 deletions examples/GettingFeaturesFromDatabase/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using GettingFeaturesFromDatabase;
using GettingFeaturesFromDatabase.Database;
using Microsoft.FeatureManagement;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDatabase();
builder.Services.AddFeatureService();

/* Add feature management */
builder.Services.AddScoped<IFeatureDefinitionProvider, CustomFeatureDefinitionProvider>().AddScopedFeatureManagement();
Copy link
Contributor

@zhiyuanliang-ms zhiyuanliang-ms Mar 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, @fffffatah Maybe this is a stupid question. Would you mind explaining why CustomFeatureDefinitionProvider is registered as scoped here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @zhiyuanliang-ms 👋, CustomFeatureDefinitionProvider is scoped here because the life-time of DbContext and the service which is used to read the features from DB are scoped. If I'm not wrong, setting the life time of DbContext as singleton might lead to issues when many concurrent calls are made. Although, DbContext can be registered as singleton and the Feature set can be marked AsNoTracking while querying, but it felt a bit risky.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!


var app = builder.Build();

if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
47 changes: 47 additions & 0 deletions examples/GettingFeaturesFromDatabase/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Getting features from database (Web API Example)

This example demonstrates how to get features from a database and use them with Microsoft Feature Management.

## Quickstart

To get started,

1. Simply run the project `GettingFeaturesFromDatabase` and you should see **Swagger UI** open in a new browser window.
2. Try out the `GET /WeatherForecast` endpoint and see the response. It should return a successful response (200 OK).
3. Now, try out `PUT /feature` endpoint to set the state of 'Weather' feature flag to 'false'.
4. Try out the `GET /WeatherForecast` endpoint again and see the response. It should return not found response (404 NotFound).

_Note: You can get the list of features using `GET /feature` endpoint._

## About the example project

This example is a simple Web API project with Entity Framework Core and SQLite database.

The endpoints are,

1. `GET /WeatherForecast` - This is the endpoint where `FeatureGate` attribute is used and access to this endpoint is controlled by the state of 'Weather' feature flag.
2. `GET /feature` - This is the endpoint to get all the feature flags from the database.
3. `PUT /feature` - This is the endpoint to update the state of a feature flag in the database. The query parameters are 'featureName' and 'isEnabled'.

## Seeding the database

During the initial run of the project, an SQLite database file named `example.db` will be created in the root of `GettingFeaturesFromDatabase` directory.
Initial run would also create a table named 'Features' and populate it with a feature flag named 'Weather'.

You can find the database seeding logic inside `/Database/FeatureConfiguration.cs` file.

```csharp
public void Configure(EntityTypeBuilder<Feature> builder)
{
builder.Property(x => x.Name).IsRequired();
builder.Property(x => x.IsEnabled).IsRequired();

/* Populate with feature data. */
builder.HasData(new List<Feature>()
{
new Feature { Name = FeatureConstants.Weather, IsEnabled = true },
});

builder.ToTable("Features").HasKey(x => x.Name);
}
```
12 changes: 12 additions & 0 deletions examples/GettingFeaturesFromDatabase/WeatherForecast.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace GettingFeaturesFromDatabase;

public class WeatherForecast
{
public DateOnly Date { get; set; }

public int TemperatureC { get; set; }

public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);

public string? Summary { get; set; }
}
Loading