Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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,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,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();
}
17 changes: 17 additions & 0 deletions examples/GettingFeaturesFromDatabase/Database/SqliteDbContext.cs
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>
23 changes: 23 additions & 0 deletions examples/GettingFeaturesFromDatabase/Program.cs
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();
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();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
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; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
9 changes: 9 additions & 0 deletions examples/GettingFeaturesFromDatabase/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}