Manage application settings
Granit.Settings provides a dynamic settings system with a five-level resolution
cascade (User -> Tenant -> Global -> Configuration -> Default). Settings can be
changed at runtime without redeployment, optionally encrypted at rest, and cached
with automatic invalidation.
Prerequisites
Section titled “Prerequisites”- A .NET 10 project referencing
Granit.Core Granit.Settingsfor setting definitions and resolutionGranit.Settings.EntityFrameworkCorefor database persistence (production)Granit.Encryptionif you need encrypted settings
Step 1 — Install packages
Section titled “Step 1 — Install packages”dotnet add package Granit.Settingsdotnet add package Granit.Settings.EntityFrameworkCoreStep 2 — Register the module
Section titled “Step 2 — Register the module”[DependsOn(typeof(GranitSettingsModule))]public sealed class AppModule : GranitModule { }Uses InMemorySettingStore — suitable for tests and local development only.
[DependsOn( typeof(GranitSettingsModule), typeof(GranitSettingsEntityFrameworkCoreModule))]public sealed class AppModule : GranitModule { }Step 3 — Define settings
Section titled “Step 3 — Define settings”Settings are declared via ISettingDefinitionProvider. Create one provider per
functional module:
using Granit.Settings.Definitions;
namespace MyApp.Settings;
public sealed class AppSettingDefinitionProvider : ISettingDefinitionProvider{ public void Define(ISettingDefinitionContext context) { // User-visible setting with a default value context.Add(new SettingDefinition("App.Theme") { DefaultValue = "dark", DisplayName = "Interface theme", IsVisibleToClients = true });
// Encrypted setting (ISO 27001), not exposed to clients context.Add(new SettingDefinition("App.ExternalApiKey") { IsEncrypted = true, IsVisibleToClients = false, Providers = { GlobalSettingValueProvider.ProviderName } });
// Numeric setting restricted to Global and Tenant scopes context.Add(new SettingDefinition("App.MaxUploadMb") { DefaultValue = "10", Providers = { GlobalSettingValueProvider.ProviderName, TenantSettingValueProvider.ProviderName } }); }}Register the provider:
services.AddSingleton<ISettingDefinitionProvider, AppSettingDefinitionProvider>();SettingDefinition properties
Section titled “SettingDefinition properties”| Property | Type | Default | Description |
|---|---|---|---|
Name | string | — | Unique setting key |
DefaultValue | string? | null | Fallback when no provider has a value |
IsEncrypted | bool | false | Encrypt at rest via IStringEncryptionService |
IsVisibleToClients | bool | false | Expose via user-facing API endpoints |
IsInherited | bool | true | Cascade to next level when null |
Providers | IList<string> | [] | Allowed providers (empty = all) |
Step 4 — Read settings
Section titled “Step 4 — Read settings”Inject ISettingProvider to read settings. The cascade runs automatically:
using Granit.Settings.Services;
namespace MyApp.Services;
public sealed class ThemeService(ISettingProvider settings){ public async Task<string> GetThemeAsync( CancellationToken cancellationToken) { var theme = await settings.GetOrNullAsync( "App.Theme", cancellationToken); return theme ?? "dark"; }}The resolution cascade:
User (U, order=100) -- per-user preference | null ->Tenant (T, order=200) -- tenant-wide default | null ->Global (G, order=300) -- application-wide | null ->Config (C, order=400) -- appsettings.json / environment variables | null ->Default(D, order=500) -- SettingDefinition.DefaultValueThe first non-null value wins. Setting IsInherited = false stops the cascade
after the first applicable provider, even if it returns null.
Step 5 — Write settings
Section titled “Step 5 — Write settings”Inject ISettingManager to create or update setting values at any scope:
using Granit.Settings.Services;
namespace MyApp.Services;
public sealed class AdminSettingsService(ISettingManager settings){ public Task SetGlobalThemeAsync( string theme, CancellationToken cancellationToken) => settings.SetGlobalAsync("App.Theme", theme, cancellationToken);
public Task SetTenantThemeAsync( Guid tenantId, string theme, CancellationToken cancellationToken) => settings.SetForTenantAsync( tenantId, "App.Theme", theme, cancellationToken);
public Task SetUserThemeAsync( string userId, string theme, CancellationToken cancellationToken) => settings.SetForUserAsync( userId, "App.Theme", theme, cancellationToken);}Cache invalidation happens automatically on writes via ISettingManager.
Step 6 — Configure EF Core persistence
Section titled “Step 6 — Configure EF Core persistence”Implement ISettingsDbContext
Section titled “Implement ISettingsDbContext”Your application DbContext must implement ISettingsDbContext and call
ConfigureSettingsModule():
using Granit.Settings.EntityFrameworkCore;using Microsoft.EntityFrameworkCore;
namespace MyApp.Persistence;
public sealed class AppDbContext( DbContextOptions<AppDbContext> options) : DbContext(options), ISettingsDbContext{ public DbSet<SettingRecord> SettingRecords { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.ConfigureSettingsModule(); modelBuilder.ApplyGranitConventions(currentTenant, dataFilter); }}Register the store and run the migration
Section titled “Register the store and run the migration”// Program.cs -- replaces InMemorySettingStore with EfCoreSettingStorebuilder.AddGranitSettingsEfCore<AppDbContext>();dotnet ef migrations add InitSettings \ --project src/MyApp \ --startup-project src/MyAppThe migration creates the core_setting_records table with a unique index on
(Name, ProviderName, ProviderKey).
Step 7 — Override via appsettings.json
Section titled “Step 7 — Override via appsettings.json”The Configuration provider (C, order=400) reads values from appsettings.json or
environment variables. These have priority over DefaultValue but are overridden
by Global, Tenant, and User values in the database:
{ "Settings": { "App.Theme": "light", "App.MaxUploadMb": "50" }}Step 8 — Expose settings via REST endpoints (optional)
Section titled “Step 8 — Expose settings via REST endpoints (optional)”Granit.Settings.Endpoints provides Minimal API endpoints for user preferences
and admin management:
// User preferences (authenticated, IsVisibleToClients = true only)app.MapGranitUserSettings();
// Global admin (requires Settings.Global.Read/Manage permissions)app.MapGranitGlobalSettings();
// Tenant admin (requires Settings.Tenant.Read/Manage permissions)app.MapGranitTenantSettings();| Scope | GET | PUT | DELETE |
|---|---|---|---|
| User | GET /settings/user | PUT /settings/user/{name} | DELETE /settings/user/{name} |
| Global | GET /settings/global | PUT /settings/global/{name} | — |
| Tenant | GET /settings/tenant | PUT /settings/tenant/{name} | — |
Deleting a user-level setting causes the cascade to fall through to Tenant, then Global, then Configuration, then Default.
Next steps
Section titled “Next steps”- Encrypt sensitive data — encrypt settings with
IsEncrypted = true - Add feature flags — SaaS feature management with quota guards
- Settings and Features reference — full API and configuration details