Settify.Dapper
1.3.0
dotnet add package Settify.Dapper --version 1.3.0
NuGet\Install-Package Settify.Dapper -Version 1.3.0
<PackageReference Include="Settify.Dapper" Version="1.3.0" />
<PackageVersion Include="Settify.Dapper" Version="1.3.0" />
<PackageReference Include="Settify.Dapper" />
paket add Settify.Dapper --version 1.3.0
#r "nuget: Settify.Dapper, 1.3.0"
#:package Settify.Dapper@1.3.0
#addin nuget:?package=Settify.Dapper&version=1.3.0
#tool nuget:?package=Settify.Dapper&version=1.3.0
Settify
Database-backed, strongly-typed settings with entity-scoped cascade resolution for .NET.
Features
- Strongly-typed settings — define settings as plain C# classes
- Typed DI injection —
ISettings<T>with scope metadata from[Settings]attribute - Source generator — compile-time validation (SETKIT001-003), AOT-friendly auto-registration
- Cascade resolution — Entity → Global → Default fallback chain
- Object-level migrations — versioned schema evolution applied on read
- Multiple providers — In-Memory, Dapper, EF Core
- Caching — IMemoryCache and HybridCache decorators with tag-based invalidation
- Validation — DataAnnotations support with custom validators
- Encryption —
[Secret]attribute with pluggable encryption for sensitive properties - Auto-detect SQL dialect — Dapper provider auto-detects from IDbConnection type
Quick Start
// Define your settings with the [Settings] attribute
[Settings(Scope = SettingsScope.Global)]
public class AppSettings : ISettings
{
public string SiteName { get; set; } = "My App";
public int MaxUploadSize { get; set; } = 10485760;
}
[Settings(Scope = SettingsScope.Entity, EntityType = "Project")]
public class ProjectSettings : ISettings
{
public string TicketPrefix { get; set; } = "PROJ";
public bool EnableDigest { get; set; }
}
// Register in DI — settings types are auto-discovered by the source generator
builder.Services.AddSettify(opts => opts.ValidateOnSave = true)
.UseInMemory();
// Inject typed settings
app.MapGet("/settings", async (ISettings<AppSettings> settings) =>
await settings.GetAsync());
app.MapGet("/project/{id}/settings", async (string id, ISettings<ProjectSettings> settings) =>
await settings.GetAsync(id));
app.MapPut("/project/{id}/settings", async (string id, ISettings<ProjectSettings> settings) =>
{
await settings.UpdateAsync(s => s.TicketPrefix = "ACME", id);
return Results.Ok();
});
The ISettingsManager API is still fully supported for cases where you need direct control:
app.MapGet("/settings", async (ISettingsManager manager) =>
await manager.GetGlobalAsync<AppSettings>());
[Settings] Attribute
The [Settings] attribute provides scope metadata and enables compile-time validation via the built-in source generator.
// Global settings — one instance per type
[Settings(Scope = SettingsScope.Global)]
public class AppSettings : ISettings { ... }
// Entity-scoped settings — one instance per entity
[Settings(Scope = SettingsScope.Entity, EntityType = "Project")]
public class ProjectSettings : ISettings { ... }
// Custom group key override
[Settings(Scope = SettingsScope.Global, GroupKey = "app-config")]
public class AppConfig : ISettings { ... }
Compile-Time Diagnostics
| ID | Severity | Rule |
|---|---|---|
| SETKIT001 | Error | Entity-scoped settings must specify EntityType |
| SETKIT002 | Error | Settings class must have a public parameterless constructor |
| SETKIT003 | Error | [Settings] class must implement ISettings |
ISettings<T> — Typed Injection
ISettings<T> is automatically registered for each [Settings]-annotated class. It resolves scope and entity type from the attribute.
public interface ISettings<T> where T : class, ISettings, new()
{
Task<T?> GetAsync(string? entityId = null, CancellationToken ct = default);
Task SaveAsync(T settings, string? entityId = null, CancellationToken ct = default);
Task<bool> DeleteAsync(string? entityId = null, CancellationToken ct = default);
Task<T> ResolveMergedAsync(string entityId, CancellationToken ct = default);
Task UpdateAsync(Action<T> mutator, string? entityId = null, CancellationToken ct = default);
}
- Global scope:
GetAsync()— noentityIdneeded - Entity scope:
GetAsync("123")—entityIdis required (throwsArgumentExceptionif null) - UpdateAsync: reads current value, applies your mutator, saves back
Migrations
Settify supports object-level migrations for schema evolution. Migrations run automatically on read when a stored version is behind the latest.
public class AddDigestSetting : SettingsMigration<ProjectSettings>
{
public override int Version => 2;
public override void Up(ProjectSettings settings)
{
settings.EnableDigest = false;
}
}
Register migrations at startup:
builder.Services.AddSettify(opts =>
{
opts.AddMigrationsFromAssembly(typeof(Program).Assembly);
})
Or register individually:
opts.AddMigration<AddDigestSetting>();
How It Works
- Provider reads
(json, version)from storage - Manager deserializes → runs pending
Up()methods in version order - Manager serializes back and saves with the new version
- Version is stored as an
INT DEFAULT 0column — not inside the JSON
Providers that don't implement IVersionedGlobalSettingsProvider / IVersionedEntitySettingsProvider will skip migrations automatically (backward compatible).
Providers
In-Memory
builder.Services.AddSettify().UseInMemory();
Dapper
builder.Services.AddSettify()
.UseDapper(cfg =>
{
cfg.AutoCreateTables = true;
cfg.Global.TableName = "global_settings";
cfg.Entity.Strategy = EntityTableStrategy.SharedTable;
});
SQL dialect is auto-detected from IDbConnection type (SQL Server, PostgreSQL, SQLite).
EF Core
builder.Services.AddSettify()
.UseEntityFrameworkCore<AppDbContext>();
In your DbContext:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplySettifyConfiguration();
}
Caching
IMemoryCache
builder.Services.AddSettify()
.UseDapper(cfg => { ... })
.UseMemoryCache(opts => opts.Expiration = TimeSpan.FromMinutes(10));
HybridCache
builder.Services.AddSettify()
.UseDapper(cfg => { ... })
.UseHybridCache(opts =>
{
opts.Expiration = TimeSpan.FromMinutes(5);
opts.Tag = "settingskit";
});
HybridCache supports L1 (in-process) + L2 (distributed) caching. Tag-based invalidation is used on save/delete — entries are tagged with group:{groupKey} and entity:{entityType}:{entityId} for bulk eviction via RemoveByTagAsync.
Encryption
Mark sensitive properties with [Secret] to automatically encrypt them at rest. Non-secret properties remain as plaintext in the stored JSON.
[Settings(Scope = SettingsScope.Global)]
public class IntegrationSettings : ISettings
{
public string ServiceUrl { get; set; } = "";
[Secret]
public string ApiKey { get; set; } = "";
[Secret]
public string WebhookSecret { get; set; } = "";
}
Quick Setup
The simplest option — uses AES-256-GCM with an auto-generated key stored at ~/.settingskit/encryption.key:
builder.Services.AddSettify()
.UseEncryption()
.UseDapper(cfg => { ... });
Custom Key File Path
builder.Services.AddSettify()
.UseEncryption("/etc/myapp/settings.key");
Explicit Key
builder.Services.AddSettify()
.UseEncryption(new AesSettingsEncryptor("base64-encoded-32-byte-key"));
Custom Encryptor
Implement ISettingsEncryptor to use DPAPI, Azure Key Vault, AWS KMS, or any other provider:
public class KeyVaultEncryptor : ISettingsEncryptor
{
public byte[] Encrypt(byte[] plaintext) { ... }
public byte[] Decrypt(byte[] ciphertext) { ... }
}
builder.Services.AddSettify()
.UseEncryption<KeyVaultEncryptor>();
How It Works
- On save,
[Secret]property values are encrypted and stored as$enc$<base64>strings in the JSON - On read,
$enc$-prefixed values are decrypted back to their original types - Non-secret properties are unaffected
- Existing plaintext data is read as-is (backward compatible) and encrypted on the next save
- Cascade merging, migrations, and validation all work transparently with encrypted properties
- If no encryptor is registered,
[Secret]is ignored and values are stored as plaintext
Stored JSON Example
{
"serviceUrl": "https://api.example.com",
"apiKey": "$enc$SGVsbG8gV29ybGQ...",
"webhookSecret": "$enc$dGVzdCBkYXRh..."
}
Packages
| Package | Description |
|---|---|
Settify |
Core interfaces, manager, in-memory providers, source generator |
Settify.Dapper |
Dapper global + entity providers |
Settify.EntityFrameworkCore |
EF Core global + entity providers |
Settify.Caching |
IMemoryCache and HybridCache decorators |
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Dapper (>= 2.1.35)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.3)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.3)
- Microsoft.Extensions.Options (>= 10.0.3)
- Settify (>= 1.3.0)
-
net8.0
- Dapper (>= 2.1.35)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.3)
- Microsoft.Extensions.Hosting.Abstractions (>= 9.0.3)
- Microsoft.Extensions.Options (>= 9.0.3)
- Settify (>= 1.3.0)
-
net9.0
- Dapper (>= 2.1.35)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.3)
- Microsoft.Extensions.Hosting.Abstractions (>= 9.0.3)
- Microsoft.Extensions.Options (>= 9.0.3)
- Settify (>= 1.3.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.