2.1 Configuration POCOs (Mqtt, Storage, UrlFetch, ApiKey)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 14:15:34 +02:00
parent 79039623e8
commit 20e6aafaa1
6 changed files with 112 additions and 1 deletions

View File

@@ -71,7 +71,7 @@ Each type lives in `src/FrameProcessor/Domain/`. Tests in `tests/FrameProcessor.
## Phase 2 — Configuration binding ## Phase 2 — Configuration binding
### [ ] 2.1 `MqttOptions`, `StorageOptions`, `UrlFetchOptions`, `ApiKeyOptions` ### [x] 2.1 `MqttOptions`, `StorageOptions`, `UrlFetchOptions`, `ApiKeyOptions`
- POCOs in `src/FrameProcessor/Configuration/`. - POCOs in `src/FrameProcessor/Configuration/`.
- Bound from `appsettings.json` via `builder.Services.Configure<T>(...)`. - Bound from `appsettings.json` via `builder.Services.Configure<T>(...)`.
- Validate on startup (`ValidateOnStart` + `IValidateOptions<T>` or DataAnnotations). - Validate on startup (`ValidateOnStart` + `IValidateOptions<T>` or DataAnnotations).

View File

@@ -0,0 +1,20 @@
using System.ComponentModel.DataAnnotations;
using FrameProcessor.Domain;
namespace FrameProcessor.Configuration;
/// <summary>
/// Wraps the shared API key from <c>appsettings.json</c>. The configured value is bound
/// from the top-level <c>ApiKey</c> key (which is a plain string in the JSON schema —
/// see SPEC.md §6.1) into <see cref="Value"/>; consumers should call <see cref="ToApiKey"/>
/// to obtain the constant-time-comparing <see cref="ApiKey"/> value type.
/// </summary>
public sealed class ApiKeyOptions
{
public const string SectionName = "ApiKey";
[Required(AllowEmptyStrings = false)]
public string Value { get; set; } = string.Empty;
public ApiKey ToApiKey() => new(Value);
}

View File

@@ -0,0 +1,35 @@
using System.ComponentModel.DataAnnotations;
namespace FrameProcessor.Configuration;
/// <summary>
/// Bound from the <c>Mqtt</c> section of <c>appsettings.json</c>. See SPEC.md §6.1.
/// </summary>
public sealed class MqttOptions
{
public const string SectionName = "Mqtt";
[Required(AllowEmptyStrings = false)]
public string Host { get; set; } = string.Empty;
[Range(1, 65535)]
public int Port { get; set; } = 1883;
[Required(AllowEmptyStrings = false)]
public string ClientId { get; set; } = string.Empty;
public string? Username { get; set; }
public string? Password { get; set; }
public bool UseTls { get; set; }
[Required(AllowEmptyStrings = false)]
public string BaseTopic { get; set; } = "frames";
[Range(0, 2)]
public int PublishQos { get; set; } = 1;
[MinLength(1)]
public int[] RetryBackoffSeconds { get; set; } = Array.Empty<int>();
}

View File

@@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
namespace FrameProcessor.Configuration;
/// <summary>
/// Bound from the <c>Storage</c> section of <c>appsettings.json</c>. See SPEC.md §6.1, §7.
/// </summary>
public sealed class StorageOptions
{
public const string SectionName = "Storage";
[Required(AllowEmptyStrings = false)]
public string ImageDirectory { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,20 @@
using System.ComponentModel.DataAnnotations;
namespace FrameProcessor.Configuration;
/// <summary>
/// Bound from the <c>UrlFetch</c> section of <c>appsettings.json</c>. See SPEC.md §6.1, §8.
/// </summary>
public sealed class UrlFetchOptions
{
public const string SectionName = "UrlFetch";
[Range(1, long.MaxValue)]
public long MaxBytes { get; set; } = 52_428_800L;
[Range(1, int.MaxValue)]
public int TimeoutSeconds { get; set; } = 30;
[Range(0, int.MaxValue)]
public int MaxRedirects { get; set; } = 3;
}

View File

@@ -1,7 +1,29 @@
using FrameProcessor.Configuration;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.Services.AddOptions<MqttOptions>()
.Bind(builder.Configuration.GetSection(MqttOptions.SectionName))
.ValidateDataAnnotations()
.ValidateOnStart();
builder.Services.AddOptions<StorageOptions>()
.Bind(builder.Configuration.GetSection(StorageOptions.SectionName))
.ValidateDataAnnotations()
.ValidateOnStart();
builder.Services.AddOptions<UrlFetchOptions>()
.Bind(builder.Configuration.GetSection(UrlFetchOptions.SectionName))
.ValidateDataAnnotations()
.ValidateOnStart();
builder.Services.AddOptions<ApiKeyOptions>()
.Configure<IConfiguration>((opts, cfg) => opts.Value = cfg[ApiKeyOptions.SectionName] ?? string.Empty)
.ValidateDataAnnotations()
.ValidateOnStart();
var app = builder.Build(); var app = builder.Build();
app.MapControllers(); app.MapControllers();