-
Notifications
You must be signed in to change notification settings - Fork 118
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
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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,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, | ||
}; | ||
} | ||
} |
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,20 @@ | ||
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"); | ||
}); | ||
} | ||
|
||
public static void AddFeatureService(this IServiceCollection services) | ||
{ | ||
services.AddScoped<IFeatureService, FeatureService>(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
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<IEnumerable<Feature>> GetFeatureAsync() | ||
{ | ||
var features = await _sqliteDbContext.Set<Feature>().ToListAsync(); | ||
|
||
return features; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
namespace GettingFeaturesFromDatabase.Database.Services; | ||
|
||
public interface IFeatureService | ||
{ | ||
Task<Feature?> GetFeatureAsync(string featureName); | ||
|
||
Task<IEnumerable<Feature>> GetFeatureAsync(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
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) | ||
{ | ||
modelBuilder.Entity<Feature>().HasKey(x => x.Name); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
<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.Core" Version="7.0.16" /> | ||
<ProjectReference Include="..\..\src\Microsoft.FeatureManagement\Microsoft.FeatureManagement.csproj" /> | ||
<ProjectReference Include="..\..\src\Microsoft.FeatureManagement.AspNetCore\Microsoft.FeatureManagement.AspNetCore.csproj" /> | ||
</ItemGroup> | ||
|
||
</Project> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
using GettingFeaturesFromDatabase; | ||
using GettingFeaturesFromDatabase.Database; | ||
using Microsoft.FeatureManagement; | ||
|
||
var builder = WebApplication.CreateBuilder(args); | ||
|
||
builder.Services.AddControllers(); | ||
builder.Services.AddEndpointsApiExplorer(); | ||
builder.Services.AddDatabase(); | ||
builder.Services.AddFeatureService(); | ||
|
||
/* Add feature management */ | ||
builder.Services.AddScoped<IFeatureDefinitionProvider, CustomFeatureDefinitionProvider>().AddScopedFeatureManagement(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you! |
||
|
||
var app = builder.Build(); | ||
|
||
app.UseHttpsRedirection(); | ||
|
||
app.UseAuthorization(); | ||
|
||
app.MapControllers(); | ||
|
||
app.Run(); |
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; } | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"Logging": { | ||
"LogLevel": { | ||
"Default": "Information", | ||
"Microsoft.AspNetCore": "Warning" | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"Logging": { | ||
"LogLevel": { | ||
"Default": "Information", | ||
"Microsoft.AspNetCore": "Warning" | ||
} | ||
}, | ||
"AllowedHosts": "*" | ||
} |
Uh oh!
There was an error while loading. Please reload this page.