Strongly-Typed Configuration
Reading config with Configuration["JWT:Key"] scattered across the codebase means every typo is a silent null discovered at runtime. The Options pattern binds a config section to a real class once, validates it at startup, and injects it wherever it's needed.
Define the shape
Options/JwtOptions.cs
public class JwtOptions
{
public const string SectionName = "JWT";
public required string Key { get; set; }
public required string Issuer { get; set; }
public double ExpiryMinutes { get; set; } = 60;
}
appsettings.json
{
"JWT": {
"Key": "replace-with-a-real-secret",
"Issuer": "YourApp",
"ExpiryMinutes": 60
}
}
Bind and validate at startup
ValidateDataAnnotations() plus ValidateOnStart() means a missing or malformed section fails the app immediately on boot โ not the first time a request happens to hit the code path that reads it.
Dependencies.cs
services.AddOptions<JwtOptions>()
.Bind(configuration.GetSection(JwtOptions.SectionName))
.ValidateDataAnnotations()
.ValidateOnStart();
Inject it โ no config lookups at the call site
IOptions<T> for values that never change during the app's lifetime; IOptionsSnapshot<T> (scoped) if you want it to re-read on each request in a reloadable-config scenario.
TokenService.cs
public class TokenService : ITokenService
{
private readonly JwtOptions _options;
public TokenService(IOptions<JwtOptions> options)
{
_options = options.Value;
}
public Token GenerateToken(User user)
{
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.Key));
var expiresOn = DateTime.UtcNow.AddMinutes(_options.ExpiryMinutes);
// ... _options.Issuer, no ["JWT:Issuer"] string lookup anywhere
}
}
Compare to the manualConfiguration["JWT:Key"] ?? throw ...checks in JWT Authentication โ those work, but the validation happens lazily on first use. Binding to a class withValidateOnStart()moves that same check to app startup, which is worth it once a project has more than one or two config values.