Skip to content

Settings

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.Security
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.Security]
    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
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