0
0

Working implementation

This commit is contained in:
Rhys Ickeringill
2025-12-06 01:58:57 +11:00
parent 3505e44e89
commit 30cd4249b4
14 changed files with 1078 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
namespace RAIC.Extensions.Configuration.EntityFrameworkCore;
internal interface IEntityFrameworkCoreDbSetConfigurationProvider : IConfigurationProvider
{
void OnReload();
bool Remove(string key);
}
internal class EntityFrameworkCoreDbSetConfigurationProvider<TDbContext, TSetting> : ConfigurationProvider, IEntityFrameworkCoreDbSetConfigurationProvider
where TDbContext : DbContext, ISettingsDbContext<DbSet<TSetting>, TSetting>
where TSetting : class, ISetting
{
private readonly IEntityFrameworkCoreDbSetConfigurationSource<TDbContext> _configurationSource;
internal EntityFrameworkCoreDbSetConfigurationProvider(IEntityFrameworkCoreDbSetConfigurationSource<TDbContext> configurationSource) : base()
{
_configurationSource = configurationSource;
}
public override void Load()
{
using var dbContext = _configurationSource.DbContextFactory!.CreateDbContext();
Data = dbContext.Settings.ToDictionary(s => s.Key, s => (string?)s.Value);
}
public bool Remove(string key) => Data.Remove(key);
public new void OnReload() => base.OnReload();
}
internal interface IEntityFrameworkCoreDbSetConfigurationSource<TDbContext> where TDbContext : DbContext
{
internal IDbContextFactory<TDbContext> DbContextFactory { get; }
}
internal class EntityFrameworkCoreDbSetConfigurationSource<TDbContext, TSetting> : IConfigurationSource, IEntityFrameworkCoreDbSetConfigurationSource<TDbContext>
where TDbContext : DbContext, ISettingsDbContext<DbSet<TSetting>, TSetting>
where TSetting : class, ISetting
{
public required IDbContextFactory<TDbContext> DbContextFactory { get; init; }
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new EntityFrameworkCoreDbSetConfigurationProvider<TDbContext, TSetting>(this);
}
}

View File

@@ -0,0 +1,35 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.Extensions.Configuration;
namespace RAIC.Extensions.Configuration.EntityFrameworkCore.Extensions;
public static class ConfigurationBuilderExtensions
{
public delegate DbContextOptionsBuilder<T> DbContextOptionsTransformer<T>(DbContextOptionsBuilder<T> transform) where T : DbContext;
/// <summary>
/// Adds a <see cref="DbSet{}"/> off <typeparamref name="TDbContext"/> as a configuration provider to the <see cref="IConfigurationBuilder"/>.
/// </summary>
/// <typeparam name="TDbContext">Type of the <see cref="DbContext"/> which implements <see cref="ISettingsDbContext{,}"/></typeparam>
/// <typeparam name="TSetting">Concrete type which implements <see cref="ISetting"/></typeparam>
/// <param name="optionsTransformer">
/// a <see cref="DbContextOptionsTransformer{}"/> which configures your <see cref="DbContextOptions{}"/>. eg.
/// <code>
/// dbContextOptions => dbContextOptions.UseNpgsql(builder.Configuration.GetConnectionString("Default"))
/// </code>
/// </param>
/// <returns>The <see cref="IConfigurationBuilder"/></returns>
public static IConfigurationBuilder AddDbSet<TDbContext, TSetting>(this IConfigurationBuilder builder, DbContextOptionsTransformer<TDbContext> optionsTransformer)
where TDbContext : DbContext, ISettingsDbContext<DbSet<TSetting>, TSetting>
where TSetting : class, ISetting
{
// DEBT: Find way to create non-pooled DbContextFactory since this is only a short lived usage
var configurationSource = new EntityFrameworkCoreDbSetConfigurationSource<TDbContext, TSetting>()
{
DbContextFactory = new PooledDbContextFactory<TDbContext>(optionsTransformer(new DbContextOptionsBuilder<TDbContext>()).Options, poolSize: 1)
};
return builder.Add(configurationSource);
}
}

View File

@@ -0,0 +1,22 @@
using System;
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
namespace RAIC.Extensions.Configuration.EntityFrameworkCore;
public interface ISettingsDbContext<out TSettingDbSet, out TSetting> : IDisposable
where TSettingDbSet : DbSet<TSetting>
where TSetting : class, ISetting
{
TSettingDbSet Settings { get; }
}
public interface ISetting
{
[Key]
string Key { get; }
[Required]
string Value { get; }
}

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="RAIC.Extensions.Configuration.EntityFrameworkCore.PostgreSQL" />
<InternalsVisibleTo Include="RAIC.Extensions.Configuration.EntityFrameworkCore.SqlServer" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.11" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,56 @@
# RAIC.Extensions.Configuration.EntityFrameworkCore
This library is a `Microsoft.Extensions.Configuration.IConfigurationProvider` that reads settings from a `DbSet<ISetting> Settings` property
present on your Entity Framework Core `DbContext`.
## Goals
1. Usable with minimal constraints on your entity model
1. Follows conventional configuration patterns
1. `IOptionsMonitor` update support through optional opt-in services (see `RAIC.Extensions.Configuration.EntityFrameworkCore.PostgreSQL` & `RAIC.Extensions.Configuration.EntityFrameworkCore.SqlServer`)
## Requirements
* .NET 8
## Gotchas
* Setting values cannot be `null` (as signified by the `RequiredAttribute` on `ISetting.Value`)
* Setting keys should not contain the `=` character (similar to `CommandLineConfigurationProvider` & `EnvironmentVariablesConfigurationProvider`)
## Usage Example
```csharp
using RAIC.Extensions.Configuration.EntityFrameworkCore.Extensions;
public record Setting : ISetting
{
[Key]
public required string Key { get; set; }
[Required]
public required string Value { get; set; }
}
public class MyDbContext(DbContextOptions<MyDbContext> options) : DbContext(options), ISettingsDbContext<DbSet<Setting>, Setting>
{
public DbSet<Setting> Settings { get; set; }
}
var builder = Host.CreateApplicationBuilder(args) // or WebApplication.CreateBuilder(args);
// build an initial configuration
builder.Configuration.AddJsonFile("appsettings.json")
.AddUserSecrets<Program>(); // or whereever your connection string lives
// obtain connection string from preliminary config so can initialise other settings from DbSet
builder.Configuration.AddDbSet<MyDbContext, Setting>(dbContextOptions => dbContextOptions.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
...
await builder.Build().RunAsync(); // use config as normal
```
Read more about [Configuration](https://docs.microsoft.com/en-us/dotnet/core/extensions/configuration) on the Microsoft Docs site.