Skip to content

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.

  • 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
PackageRoleDepends on
Granit.SettingsISettingProvider, ISettingManager, cascading resolutionGranit.Caching, Granit.Encryption, Granit.Users
Granit.Settings.EntityFrameworkCoreSettingsDbContext (isolated, multi-tenant + soft-delete)Granit.Settings, Granit.Persistence
Granit.Settings.EndpointsUser/Global/Tenant HTTP endpoints, SettingsCultureMiddlewareGranit.Settings, Granit.Authorization
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 { }
Program.cs
builder.AddGranitSettingsEntityFrameworkCore(opt =>
opt.UseNpgsql(connectionString));

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
});
}
}
PropertyDefaultDescription
Name(required)Unique setting key
DefaultValuenullFallback when no provider supplies a value
IsEncryptedfalseEncrypt at rest via IStringEncryptionService
IsVisibleToClientsfalseExpose via user-scoped API endpoints
IsInheritedtrueLower-priority scopes inherit from higher-priority ones
Providers[] (all)Allow-list of provider names ("U", "T", "G")

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
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;
}
}
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);
}
}
ScopeMethodRoutePermission
UserGET/settings/userAuthenticated
UserGET/settings/user/{name}Authenticated
UserPUT/settings/user/{name}Authenticated
UserDELETE/settings/user/{name}Authenticated
GlobalGET/settings/globalSettings.Global.Read
GlobalPUT/settings/global/{name}Settings.Global.Manage
TenantGET/settings/tenantSettings.Tenant.Read
TenantPUT/settings/tenant/{name}Settings.Tenant.Manage

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.

Program.cs
app.UseAuthentication();
app.UseMiddleware<SettingsCultureMiddleware>();
app.UseAuthorization();
{
"Settings": {
"CacheExpiration": "00:30:00"
}
}
PropertyDefaultDescription
CacheExpiration00:30:00Cache entry TTL for resolved setting values

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.

Encrypted setting values are never exposed via HTTP:

  • GET endpoints return "***" for encrypted settings (all scopes: User, Global, Tenant)
  • SettingChangedEvent masks OldValue and NewValue for encrypted settings, preventing sensitive values from leaking into audit log handlers or event consumers

Setting values are limited to 4000 characters (enforced both by endpoint validation and a database column constraint).

SettingsMetrics emits three counters via IMeterFactory (meter: Granit.Settings):

MetricDescription
granit.settings.value.changedSetting values created or updated
granit.settings.value.deletedSetting values deleted
granit.settings.cache.invalidatedCache entries invalidated after a write

All counters include tenant_id, provider_name, and setting_name tags.

CategoryKey typesPackage
ModulesGranitSettingsModule, GranitSettingsEntityFrameworkCoreModule, GranitSettingsEndpointsModule
AbstractionsISettingProvider (GetOrNullAsync, GetAllAsync), ISettingManager (SetGlobalAsync, SetForTenantAsync, SetForUserAsync)Granit.Settings
DefinitionsSettingDefinition, ISettingDefinitionProvider, SettingDefinitionManagerGranit.Settings
OptionsSettingsOptions (section "Settings", CacheExpiration)Granit.Settings
EndpointsMapGranitUserSettings(), MapGranitGlobalSettings(), MapGranitTenantSettings(), SettingsCultureMiddlewareGranit.Settings.Endpoints
  • 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