Skip to content

Options Pattern

The Options pattern structures application configuration as strongly-typed classes validated at startup. Each Granit module exposes an *Options class bound to an appsettings.json section via BindConfiguration, with validation through DataAnnotations and ValidateOnStart().

flowchart LR
    subgraph Config["appsettings.json"]
        JSON["Vault: Address: ..."]
    end

    subgraph Registration["DI Extension"]
        Bind["AddOptions of VaultOptions<br/>.BindConfiguration(SectionName)<br/>.ValidateDataAnnotations()<br/>.ValidateOnStart()"]
    end

    subgraph Runtime["Injection"]
        IO["IOptions of VaultOptions"]
        IOM["IOptionsMonitor of VaultOptions"]
    end

    JSON --> Bind --> IO
    Bind --> IOM

    style Config fill:#f5f5f5,stroke:#666
    style Registration fill:#e8f4fd,stroke:#1a73e8
    style Runtime fill:#e8fde8,stroke:#2d8a4e

Each Options class follows this template:

public sealed class VaultOptions
{
public const string SectionName = "Vault";
[Required]
public string Address { get; set; } = string.Empty;
public string AuthMethod { get; set; } = "kubernetes";
[Range(0.1, 1.0)]
public double LeaseRenewalThreshold { get; set; } = 0.75;
}

Registration in the module’s DI extension:

services.AddOptions<VaultOptions>()
.BindConfiguration(VaultOptions.SectionName)
.ValidateDataAnnotations()
.ValidateOnStart();

Inventory (excerpt — 93 options classes in the framework)

Section titled “Inventory (excerpt — 93 options classes in the framework)”
ModuleClassSectionNotable validation
VaultVaultOptionsVault[Required] Address
CachingCachingOptionsCacheKeyPrefix, EncryptValues
ObservabilityObservabilityOptionsObservabilityServiceName, OtlpEndpoint
Identity.KeycloakKeycloakAdminOptionsKeycloakAdmin[Required] + [Range] + URL helpers
Auth.JwtBearerJwtBearerAuthOptionsAuthenticationAuthority, Audience
BlobStorage.S3S3BlobOptionsinherits BlobStorageOptionsCustom IValidateOptions<T>
WebhooksWebhooksOptionsWebhooks[Range(5, 120)] timeout, [Range(1, 100)] parallelism
NotificationsNotificationsOptionsNotificationsMaxParallelDeliveries
Notifications.BrevoBrevoOptionsNotifications:Brevo[Required] ApiKey, [Range(1, 300)] timeout
MultiTenancyMultiTenancyOptionsMultiTenancyIsEnabled, TenantIdClaimType

Some modules require cross-property validation. They implement IValidateOptions<T>:

internal sealed class S3BlobOptionsValidator : IValidateOptions<S3BlobOptions>
{
public ValidateOptionsResult Validate(string? name, S3BlobOptions options)
{
if (string.IsNullOrWhiteSpace(options.ServiceUrl))
return ValidateOptionsResult.Fail("S3 ServiceUrl is required.");
if (options.ForcePathStyle && options.ServiceUrl.Contains("amazonaws.com"))
return ValidateOptionsResult.Fail("ForcePathStyle should not be used with AWS.");
return ValidateOptionsResult.Success;
}
}
FileRole
src/Granit.Vault/Options/VaultOptions.csCanonical simple example
src/Granit.Identity.Keycloak/Options/KeycloakAdminOptions.csComplex options with helpers
src/Granit.BlobStorage.S3/Options/S3BlobOptions.csCustom IValidateOptions<T>
src/Granit.Webhooks/Options/WebhooksOptions.cs[Range] validation
src/Granit.Observability/Options/ObservabilityOptions.csObservability options
ProblemOptions pattern solution
Configuration read as untyped stringsStrongly-typed classes with IntelliSense
Config errors discovered at runtimeValidateOnStart() — fail-fast at startup
Misspelled JSON sectionsconst string SectionName = single source of truth
Cross-property validation impossible with annotationsIValidateOptions<T> for complex rules
Plaintext secrets in appsettingsCompatible with Vault, User Secrets, env vars
// --- Registration in the module's DI extension ---
public static IServiceCollection AddGranitWebhooks(
this IServiceCollection services)
{
services.AddOptions<WebhooksOptions>()
.BindConfiguration(WebhooksOptions.SectionName)
.ValidateDataAnnotations()
.ValidateOnStart();
services.AddSingleton<IValidateOptions<WebhooksOptions>, WebhooksOptionsValidator>();
return services;
}
// --- Consumption in a service ---
public sealed class WebhookDeliveryService(
IOptions<WebhooksOptions> options,
IHttpClientFactory httpClientFactory)
{
public async Task DeliverAsync(WebhookPayload payload, CancellationToken ct)
{
HttpClient client = httpClientFactory.CreateClient();
client.Timeout = TimeSpan.FromSeconds(options.Value.HttpTimeoutSeconds);
// ...
}
}