Options Binding¶
The [Options] attribute provides automatic configuration binding for your options classes, integrating with Microsoft's Options pattern.
Quick Start¶
using NexusLabs.Needlr.Generators;
[Options]
public class CacheSettings
{
public int TimeoutSeconds { get; set; } = 300;
public int MaxItems { get; set; } = 1000;
}
The generator automatically:
1. Binds your options class to the configuration section
2. Registers it with the DI container
3. Makes it available via IOptions<CacheSettings>, IOptionsSnapshot<CacheSettings>, or IOptionsMonitor<CacheSettings>
Section Name¶
Convention-Based (Default)¶
By default, the section name is inferred from the class name:
| Class Name | Section Name |
|---|---|
CacheSettings |
CacheSettings |
DatabaseOptions |
DatabaseOptions |
SmtpConfiguration |
SmtpConfiguration |
Explicit Section Name¶
Specify a custom section name:
[Options("MyApp:Database")]
public class DatabaseOptions
{
public string ConnectionString { get; set; } = "";
}
Named Options¶
For multiple configurations of the same type, use named options:
[Options("Databases:Primary", Name = "Primary")]
public class DatabaseOptions
{
public string ConnectionString { get; set; } = "";
}
[Options("Databases:Replica", Name = "Replica")]
public class ReplicaDatabaseOptions : DatabaseOptions { }
Retrieve named options using IOptionsSnapshot<T> or IOptionsMonitor<T>:
public class MyService
{
public MyService(IOptionsSnapshot<DatabaseOptions> options)
{
var primary = options.Get("Primary");
var replica = options.Get("Replica");
}
}
Validation¶
Data Annotations¶
Use standard System.ComponentModel.DataAnnotations attributes:
using System.ComponentModel.DataAnnotations;
[Options(ValidateOnStart = true)]
public class ApiSettings
{
[Required]
public string ApiKey { get; set; } = "";
[Range(1, 300)]
public int TimeoutSeconds { get; set; } = 30;
[Url]
public string BaseUrl { get; set; } = "";
}
When ValidateOnStart = true, the application will fail to start if validation fails.
Custom Validation Method¶
Add a Validate() method to your options class:
[Options(ValidateOnStart = true)]
public class StripeOptions
{
public string ApiKey { get; set; } = "";
public bool IsProduction { get; set; }
// Convention: method named "Validate" is auto-discovered
public IEnumerable<ValidationError> Validate()
{
if (IsProduction && !ApiKey.StartsWith("sk_live_"))
yield return "Production requires a live API key";
if (!IsProduction && !ApiKey.StartsWith("sk_test_"))
yield return "Test mode requires a test API key";
}
}
Custom Method Name¶
Use a different method name with ValidateMethod:
[Options(ValidateOnStart = true, ValidateMethod = nameof(CheckConfiguration))]
public class EmailOptions
{
public string SmtpHost { get; set; } = "";
public IEnumerable<ValidationError> CheckConfiguration()
{
if (string.IsNullOrEmpty(SmtpHost))
yield return "SMTP host is required";
}
}
External Validator¶
Delegate validation to a separate class:
[Options(ValidateOnStart = true, Validator = typeof(PaymentOptionsValidator))]
public class PaymentOptions
{
public string MerchantId { get; set; } = "";
public decimal MinAmount { get; set; }
public decimal MaxAmount { get; set; }
}
public class PaymentOptionsValidator : IOptionsValidator<PaymentOptions>
{
public IEnumerable<ValidationError> Validate(PaymentOptions options)
{
if (string.IsNullOrEmpty(options.MerchantId))
yield return "MerchantId is required";
if (options.MinAmount >= options.MaxAmount)
yield return "MinAmount must be less than MaxAmount";
}
}
FluentValidation Integration¶
If you're using FluentValidation, validators are automatically discovered:
[Options("Database", ValidateOnStart = true, Validator = typeof(DatabaseOptionsValidator))]
public class DatabaseOptions
{
public string ConnectionString { get; set; } = "";
public int MaxPoolSize { get; set; } = 100;
}
public class DatabaseOptionsValidator : AbstractValidator<DatabaseOptions>
{
public DatabaseOptionsValidator()
{
RuleFor(x => x.ConnectionString)
.NotEmpty()
.WithMessage("Connection string is required");
RuleFor(x => x.MaxPoolSize)
.InclusiveBetween(1, 1000);
}
}
For more details on FluentValidation integration, see the Advanced Usage guide.
Attribute Properties¶
| Property | Type | Default | Description |
|---|---|---|---|
SectionName |
string? |
Class name | Configuration section to bind |
Name |
string? |
null |
Named options identifier |
ValidateOnStart |
bool |
false |
Validate at application startup |
ValidateMethod |
string? |
"Validate" |
Custom validation method name |
Validator |
Type? |
null |
External validator type |
Generated Code¶
The [Options] attribute causes the generator to emit registration code in TypeRegistry.RegisterOptions():
// Generated code (simplified)
public static void RegisterOptions(IServiceCollection services, IConfiguration configuration)
{
// Without validation
services.Configure<CacheSettings>(configuration.GetSection("CacheSettings"));
// With validation
services.AddOptions<ApiSettings>()
.BindConfiguration("ApiSettings")
.ValidateDataAnnotations()
.ValidateOnStart();
}
Consuming Options¶
Inject options using standard Microsoft.Extensions.Options patterns:
public class MyService
{
private readonly CacheSettings _settings;
// Option 1: IOptions<T> - singleton, doesn't reload
public MyService(IOptions<CacheSettings> options)
{
_settings = options.Value;
}
// Option 2: IOptionsSnapshot<T> - scoped, reloads per request
public MyService(IOptionsSnapshot<CacheSettings> options)
{
_settings = options.Value;
}
// Option 3: IOptionsMonitor<T> - singleton, live reload
public MyService(IOptionsMonitor<CacheSettings> options)
{
_settings = options.CurrentValue;
options.OnChange(newValue => { /* handle reload */ });
}
}
Immutable Options¶
The [Options] attribute supports immutable options patterns, including init-only properties and records with init properties.
Init-Only Properties¶
[Options("Cache")]
public class CacheOptions
{
public required string Host { get; init; }
public int Port { get; init; } = 6379;
public int TimeoutMs { get; init; } = 5000;
}
Records with Init Properties¶
[Options("Redis")]
public record RedisOptions
{
public string Host { get; init; } = "localhost";
public int Port { get; init; } = 6379;
}
Configuration Reload with Immutable Types¶
Immutable types work correctly with IOptionsMonitor<T> for configuration reload. When the configuration file changes:
- A new instance is created with the updated values
IOptionsMonitor<T>.CurrentValuereturns the new instanceOnChange()callbacks receive the new instance
public class CacheService
{
private readonly IOptionsMonitor<CacheOptions> _options;
public CacheService(IOptionsMonitor<CacheOptions> options)
{
_options = options;
// React to configuration changes
options.OnChange(newOptions =>
{
// newOptions is a new immutable instance
Console.WriteLine($"Cache timeout changed to {newOptions.TimeoutMs}ms");
});
}
public CacheOptions CurrentConfig => _options.CurrentValue;
}
Positional Records¶
Positional records (records with primary constructor parameters) are supported when declared as partial:
// ✅ WORKS - partial positional record
[Options("Redis")]
public partial record RedisOptions(string Host, int Port);
// ✅ WORKS - init-only properties (no partial needed)
[Options("Redis")]
public record RedisOptions
{
public string Host { get; init; } = "";
public int Port { get; init; } = 6379;
}
When you use partial, Needlr generates a parameterless constructor that chains to the primary constructor:
// Generated code:
public partial record RedisOptions
{
public RedisOptions() : this(string.Empty, default) { }
}
This enables Microsoft's configuration binder to instantiate the record.
⚠️ Non-partial positional records will emit warning NDLRGEN021 because they cannot work with configuration binding at runtime.
AOT Compatibility¶
⚠️ Important: The
[Options]attribute is not compatible with Native AOT.
The generated code uses Configure<T>() and BindConfiguration() which rely on reflection for property binding. These APIs have [RequiresDynamicCode] attributes, making them incompatible with AOT compilation.
If your project has PublishAot=true or IsAotCompatible=true, the generator will emit error NDLRGEN020.
Workaround for AOT projects: Bind configuration manually:
public class CacheSettings
{
public int TimeoutSeconds { get; set; }
public static CacheSettings FromConfiguration(IConfiguration config)
{
var section = config.GetSection("CacheSettings");
return new CacheSettings
{
TimeoutSeconds = section.GetValue<int>("TimeoutSeconds")
};
}
}
// In startup:
var settings = CacheSettings.FromConfiguration(configuration);
services.AddSingleton(Microsoft.Extensions.Options.Options.Create(settings));
Analyzers¶
The following analyzers help catch common mistakes:
| Analyzer | Severity | Description |
|---|---|---|
| NDLRGEN014 | Error | Validator type has no validation method |
| NDLRGEN015 | Error | Validator type mismatch |
| NDLRGEN016 | Error | Validation method not found |
| NDLRGEN017 | Error | Validation method has wrong signature |
| NDLRGEN018 | Warning | Validator won't run (ValidateOnStart is false) |
| NDLRGEN019 | Warning | Validation method won't run (ValidateOnStart is false) |
| NDLRGEN020 | Error | [Options] is not compatible with Native AOT |
| NDLRGEN021 | Warning | Positional record must be partial for [Options] |
Best Practices¶
-
Always validate in production: Use
ValidateOnStart = trueto fail fast on misconfiguration -
Use strongly-typed sections: Prefer explicit section names over relying on conventions when your config structure is complex
-
Separate validation logic: Use external validators for complex validation rules to keep options classes clean
-
Document required settings: Use
[Required]and XML comments to make configuration requirements clear -
Provide sensible defaults: Initialize properties with default values for optional settings