Settings — Tenant-Scoped Configuration
Cascading key-value settings engine with user/tenant/global scopes, encrypted storage, and distributed cache. Each scope is resolved through a provider chain — the first non-null value wins.
Package structure
Section titled “Package structure”DirectoryGranit.Settings/ Cascading settings engine (User, Tenant, Global, Configuration, Default)
- Granit.Settings.EntityFrameworkCore Isolated SettingsDbContext
- Granit.Settings.Endpoints User, Global, and Tenant setting HTTP endpoints
| Package | Role | Depends on |
|---|---|---|
Granit.Settings | ISettingProvider, ISettingManager, cascading resolution | Granit.Caching, Granit.Encryption, Granit.Users |
Granit.Settings.EntityFrameworkCore | SettingsDbContext (isolated, multi-tenant + soft-delete) | Granit.Settings, Granit.Persistence |
Granit.Settings.Endpoints | User/Global/Tenant HTTP endpoints, SettingsCultureMiddleware | Granit.Settings, Granit.Authorization |
Dependency graph
Section titled “Dependency graph”graph TD
S[Granit.Settings] --> CA[Granit.Caching]
S --> EN[Granit.Encryption]
S --> SE[Granit.Users]
SEF[Granit.Settings.EntityFrameworkCore] --> S
SEF --> P[Granit.Persistence]
SEP[Granit.Settings.Endpoints] --> S
SEP --> A[Granit.Authorization]
[DependsOn( typeof(GranitSettingsModule), typeof(GranitSettingsEntityFrameworkCoreModule))]public class AppModule : GranitModule { }builder.AddGranitSettingsEntityFrameworkCore(opt => opt.UseNpgsql(connectionString));[DependsOn( typeof(GranitSettingsModule), typeof(GranitSettingsEntityFrameworkCoreModule), typeof(GranitSettingsEndpointsModule))]public class AppModule : GranitModule { }// Program.cs — map endpointsapp.MapGranitUserSettings(); // GET/PUT/DELETE /settings/user/{name}app.MapGranitGlobalSettings(); // GET/PUT /settings/global/{name}app.MapGranitTenantSettings(); // GET/PUT /settings/tenant/{name}Defining settings
Section titled “Defining settings”Implement ISettingDefinitionProvider in any module. Providers are auto-discovered at startup.
public sealed class AcmeSettingDefinitionProvider : ISettingDefinitionProvider{ public void Define(ISettingDefinitionContext context) { context.Add(new SettingDefinition("Acme.DefaultPageSize") { DefaultValue = "25", IsVisibleToClients = true, IsInherited = true, Description = "Default number of items per page" });
context.Add(new SettingDefinition("Acme.SmtpPassword") { DefaultValue = null, IsEncrypted = true, IsVisibleToClients = false, Providers = { "G" } // Global scope only }); }}SettingDefinition properties
Section titled “SettingDefinition properties”| Property | Default | Description |
|---|---|---|
Name | (required) | Unique setting key |
DefaultValue | null | Fallback when no provider supplies a value |
IsEncrypted | false | Encrypt at rest via IStringEncryptionService |
IsVisibleToClients | false | Expose via user-scoped API endpoints |
IsInherited | true | Lower-priority scopes inherit from higher-priority ones |
Providers | [] (all) | Allow-list of provider names ("U", "T", "G") |
Value cascade
Section titled “Value cascade”Settings are resolved through a provider chain. The first provider that returns a non-null value wins:
User (U) → Tenant (T) → Global (G) → Configuration (appsettings.json) → Default (code)When IsInherited = false, each scope is independent and does not fall through.
graph LR
U[User] -->|null?| T[Tenant]
T -->|null?| G[Global]
G -->|null?| C[Configuration]
C -->|null?| D[Default]
style U fill:#4CAF50,color:white
style D fill:#9E9E9E,color:white
Reading settings
Section titled “Reading settings”public class ReportService(ISettingProvider settings){ public async Task<int> GetPageSizeAsync(CancellationToken ct) { string? value = await settings .GetOrNullAsync("Acme.DefaultPageSize", ct) .ConfigureAwait(false);
return int.TryParse(value, out int size) ? size : 25; }}Writing settings
Section titled “Writing settings”public class AdminService(ISettingManager settingManager){ public async Task SetGlobalPageSizeAsync(int size, CancellationToken ct) { await settingManager .SetGlobalAsync("Acme.DefaultPageSize", size.ToString(), ct) .ConfigureAwait(false); }
public async Task SetTenantThemeAsync(Guid tenantId, string theme, CancellationToken ct) { await settingManager .SetForTenantAsync(tenantId, "Acme.Theme", theme, ct) .ConfigureAwait(false); }
public async Task SetUserLocaleAsync(string userId, string locale, CancellationToken ct) { await settingManager .SetForUserAsync(userId, "Granit.PreferredCulture", locale, ct) .ConfigureAwait(false); }}Endpoints
Section titled “Endpoints”| Scope | Method | Route | Permission |
|---|---|---|---|
| User | GET | /settings/user | Authenticated |
| User | GET | /settings/user/{name} | Authenticated |
| User | PUT | /settings/user/{name} | Authenticated |
| User | DELETE | /settings/user/{name} | Authenticated |
| Global | GET | /settings/global | Settings.Global.Read |
| Global | PUT | /settings/global/{name} | Settings.Global.Manage |
| Tenant | GET | /settings/tenant | Settings.Tenant.Read |
| Tenant | PUT | /settings/tenant/{name} | Settings.Tenant.Manage |
SettingsCultureMiddleware
Section titled “SettingsCultureMiddleware”Hydrates CultureInfo.CurrentUICulture and ICurrentTimezoneProvider from the
authenticated user’s Granit.PreferredCulture and Granit.PreferredTimezone settings.
Runs after authentication, before endpoint handlers. No-op for anonymous requests.
app.UseAuthentication();app.UseMiddleware<SettingsCultureMiddleware>();app.UseAuthorization();Configuration
Section titled “Configuration”{ "Settings": { "CacheExpiration": "00:30:00" }}| Property | Default | Description |
|---|---|---|
CacheExpiration | 00:30:00 | Cache entry TTL for resolved setting values |
Security
Section titled “Security”Encryption at rest
Section titled “Encryption at rest”Settings declared with IsEncrypted = true are encrypted by IStringEncryptionService
(AES-256 or Vault Transit) in the EfCoreSettingStore layer before database persistence.
The FusionCache layer stores plaintext to avoid double encryption — Redis is protected
by AesCacheValueEncryptor independently.
Value masking
Section titled “Value masking”Encrypted setting values are never exposed via HTTP:
- GET endpoints return
"***"for encrypted settings (all scopes: User, Global, Tenant) SettingChangedEventmasksOldValueandNewValuefor encrypted settings, preventing sensitive values from leaking into audit log handlers or event consumers
Input validation
Section titled “Input validation”Setting values are limited to 4000 characters (enforced both by endpoint validation and a database column constraint).
Observability
Section titled “Observability”SettingsMetrics emits three counters via IMeterFactory (meter: Granit.Settings):
| Metric | Description |
|---|---|
granit.settings.value.changed | Setting values created or updated |
granit.settings.value.deleted | Setting values deleted |
granit.settings.cache.invalidated | Cache entries invalidated after a write |
All counters include tenant_id, provider_name, and setting_name tags.
Public API summary
Section titled “Public API summary”| Category | Key types | Package |
|---|---|---|
| Modules | GranitSettingsModule, GranitSettingsEntityFrameworkCoreModule, GranitSettingsEndpointsModule | — |
| Abstractions | ISettingProvider (GetOrNullAsync, GetAllAsync), ISettingManager (SetGlobalAsync, SetForTenantAsync, SetForUserAsync) | Granit.Settings |
| Definitions | SettingDefinition, ISettingDefinitionProvider, SettingDefinitionManager | Granit.Settings |
| Options | SettingsOptions (section "Settings", CacheExpiration) | Granit.Settings |
| Endpoints | MapGranitUserSettings(), MapGranitGlobalSettings(), MapGranitTenantSettings(), SettingsCultureMiddleware | Granit.Settings.Endpoints |
See also
Section titled “See also”- Features — SaaS feature flags with plan-based activation
- Reference Data — i18n lookup tables
- Caching — used by Settings for value caching
- Persistence — isolated DbContext pattern
- Security — permission-based access for admin endpoints