# The settings pattern

> Add strongly-typed, validated configuration with the settings pattern and bind it from appsettings and environment variables.

## Why typed settings

The API never reads `IConfiguration` directly. Configuration binds from `appsettings.json`
(plus environment-specific layers and environment variables) into typed records under
`api/src/Slicekit.Core/Settings/`, and consumers inject `IOptions` (or `IOptionsSnapshot` /
`IOptionsMonitor` when they need reload semantics).

Every section is validated at boot. A missing connection string or a malformed URL fails the host
before it accepts traffic, not on the first request that touches the setting.

> **Scope: API only.** The frontend reads `VITE_*` env vars at build time. See
> [Frontend overview](/docs/frontend-overview).

## Layout

One settings class per logical group, all in `api/src/Slicekit.Core/Settings/`:

```
api/src/Slicekit.Core/Settings/
├── AppSettings.cs        Root composition: every section as a property.
├── DatabaseSettings.cs
├── AuthSettings.cs
├── OAuthSettings.cs
├── AdminSettings.cs
├── MessagingSettings.cs
├── CachingSettings.cs
├── EmailSettings.cs
├── SmtpSettings.cs
├── FrontendSettings.cs
├── ApiKeySettings.cs
├── AuditingSettings.cs
└── StorageSettings.cs
```

A settings class is a plain class with init-only or settable properties and data-annotation
attributes for validation. Here is the whole of `FrontendSettings.cs`:

```csharp
using System.ComponentModel.DataAnnotations;

namespace Slicekit.Core.Settings;

public sealed class FrontendSettings
{
    [Required] public string BaseUrl { get; set; } = string.Empty;
}
```

## 1. Define the record

Create `api/src/Slicekit.Core/Settings/Settings.cs`. Use `[Required]`, `[Range]`, `[Url]`, and
the rest of `System.ComponentModel.DataAnnotations` for single-field rules:

```csharp
using System.ComponentModel.DataAnnotations;

namespace Slicekit.Core.Settings;

public sealed class WidgetSettings
{
    [Required, Url] public string Endpoint { get; init; } = string.Empty;

    [Range(1, 100)] public int MaxBatchSize { get; init; } = 10;
}
```

A handy convention used by several classes is a static `SectionName` property, so the section name
lives next to the type that owns it:

```csharp
public static string SectionName => "Widget";
```

## 2. Nest it under the root

Add a property to `AppSettings.cs` so the section nests under the composed root:

```csharp
public sealed class AppSettings
{
    public DatabaseSettings Database { get; set; } = new();
    // ...
    public WidgetSettings Widget { get; set; } = new();
}
```

## 3. Add placeholder values to appsettings.json

`appsettings.json` is committed and holds placeholder values only, never real secrets. Add your
section with values safe for local dev:

```json
{
  "Widget": {
    "Endpoint": "http://localhost:9999",
    "MaxBatchSize": 10
  }
}
```

## 4. Register it

Add one line to
`api/src/Slicekit.Core/Configuration/Settings.cs`, inside `ConfigureSettings`:

```csharp
builder.AddSettings(WidgetSettings.SectionName);
```

That private `AddSettings` helper wires up binding, `ValidateDataAnnotations()` and
`ValidateOnStart()` in one shot:

```csharp
private static OptionsBuilder AddSettings(this IHostApplicationBuilder builder, string section)
    where T : class =>
    builder.Services.AddOptions()
        .BindConfiguration(section)
        .ValidateDataAnnotations()
        .ValidateOnStart();
```

The API project picks up the whole set through a single `.ConfigureSettings()` call in
`api/src/Slicekit.Api/Program.cs`. No per-section registration in the host.

## 5. Inject it

Depend on `IOptions` wherever you need the values. Reach for `IOptionsSnapshot`
when you want per-request reload, or `IOptionsMonitor` for change notifications:

```csharp
public sealed class WidgetClient(IOptions options)
{
    private readonly WidgetSettings _settings = options.Value;
    // ...
}
```

## Cross-field rules: implement IValidatableObject

Data annotations cover one field at a time. For invariants that span fields ("B is required when
flag A is on") or to validate nested settings objects (which `ValidateDataAnnotations()` does not
walk by default), implement `IValidatableObject` on the settings class. The same
`AddSettings` registration picks it up automatically.

`EmailSettings` is the worked example in the repo. It carries a nested `SmtpSettings` and only
demands a host and a from-address once email is switched on:

```csharp
public sealed class EmailSettings : IValidatableObject
{
    public static string SectionName => "Email";

    public bool Enabled { get; init; }
    public SmtpSettings Smtp { get; init; } = new();

    public IEnumerable Validate(ValidationContext validationContext)
    {
        if (Smtp.Port is <= 0 or > 65535)
            yield return new ValidationResult(
                "Email:Smtp:Port must be between 1 and 65535",
                [$"{nameof(Smtp)}.{nameof(SmtpSettings.Port)}"]);

        if (!Enabled) yield break;

        if (string.IsNullOrWhiteSpace(Smtp.Host))
            yield return new ValidationResult(
                "Email:Smtp:Host is required when Email:Enabled is true",
                [$"{nameof(Smtp)}.{nameof(SmtpSettings.Host)}"]);

        if (string.IsNullOrWhiteSpace(Smtp.FromEmail))
            yield return new ValidationResult(
                "Email:Smtp:FromEmail is required when Email:Enabled is true",
                [$"{nameof(Smtp)}.{nameof(SmtpSettings.FromEmail)}"]);
    }
}
```

Each yielded `ValidationResult` becomes a separate startup error, with the member name(s)
attributing the violation. Keeping the rules in the settings class means a single file describes both
the shape and its invariants.

## Secrets and overrides

`appsettings.json` ships placeholders. Real values arrive from environment variables at runtime,
using ASP.NET's `__` separator to walk into nested sections:

| Setting                     | Env var                      |
|-----------------------------|------------------------------|
| `Database:ConnectionString` | `Database__ConnectionString` |
| `Auth:Jwt:SigningKey`       | `Auth__Jwt__SigningKey`      |
| `Widget:Endpoint`           | `Widget__Endpoint`           |

The repo ships no Key Vault, AWS Secrets Manager, or similar scaffolding. Host operators wire env
vars however their platform prefers (Docker secrets, Kubernetes secrets, GitHub Actions, and so on).
`.env.prod.example` at the repo root lists every variable a production deploy needs and is consumed
by `docker-compose.prod.yml`. When you add a new section, list its env-var name(s) there. See
[Deployment](/docs/deployment) for how those values flow into the production stack.

## Validation at boot

Combined with `ValidateOnStart()`, both data annotations and `IValidatableObject` run when the host
starts. Malformed config fails the API at boot rather than at first use:

```sh
dotnet run --project api/src/Slicekit.Api
```

If a required setting is missing or invalid, the host throws before it accepts traffic, and the
error names the offending section and member.

## Conventions to keep

- **One class per section.** Group related keys; nest the property on `AppSettings`.
- **Placeholders, never secrets, in `appsettings.json`.** Real values come from env vars at runtime.
- **Register once in `Settings.cs`.** A single `AddSettings` line wires binding plus validation.
- **Inject `IOptions`, not `IConfiguration`.** Consumers never read raw configuration.
- **Cross-field rules live on the settings class** via `IValidatableObject`, beside the shape.
- **Document new env vars in `.env.prod.example`** so deploy operators know what to set.

New here? Start with [Getting started](/docs/getting-started) to run the API locally before changing
its configuration.
