Skip to content

Commit 022e720

Browse files
committed
Add sections on IOptionsSnapshot vs IOptionsMonitor, configuration testing patterns, and secrets management best practices
1 parent 8e21cac commit 022e720

File tree

1 file changed

+379
-0
lines changed

1 file changed

+379
-0
lines changed

slides/Slides.md

Lines changed: 379 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,10 +573,389 @@ public class FileOptions
573573

574574
---
575575

576+
# Performance: IOptionsSnapshot vs IOptionsMonitor
577+
578+
<div class="columns">
579+
<div>
580+
581+
## <i class="fa fa-camera"></i> IOptionsSnapshot&lt;T&gt;
582+
583+
- **Scoped lifetime** (per request)
584+
- **Recomputed each request**
585+
- **Supports named options**
586+
- **Best for web apps**
587+
588+
```csharp
589+
public class MyController : Controller
590+
{
591+
public MyController(IOptionsSnapshot<MyOptions> options)
592+
{
593+
// Fresh config per request
594+
_options = options.Value;
595+
}
596+
}
597+
```
598+
599+
</div>
600+
<div>
601+
602+
## <i class="fa fa-tv"></i> IOptionsMonitor&lt;T&gt;
603+
604+
- **Singleton lifetime**
605+
- **Real-time change notifications**
606+
- **Supports named options**
607+
- **Best for background services**
608+
609+
```csharp
610+
public class MyService : BackgroundService
611+
{
612+
public MyService(IOptionsMonitor<MyOptions> monitor)
613+
{
614+
// React to config changes
615+
monitor.OnChange(OnConfigChanged);
616+
}
617+
}
618+
```
619+
620+
</div>
621+
</div>
622+
623+
---
624+
625+
# Testing Configuration
626+
627+
---
628+
629+
# Configuration Testing Patterns
630+
631+
<div class="columns">
632+
<div>
633+
634+
## <i class="fa fa-flask"></i> Unit Testing
635+
636+
```csharp
637+
[Test]
638+
public void Service_Uses_Configuration_Correctly()
639+
{
640+
// Arrange
641+
var config = new ConfigurationBuilder()
642+
.AddInMemoryCollection(new[]
643+
{
644+
new KeyValuePair<string, string>("ApiUrl", "https://test.api"),
645+
new KeyValuePair<string, string>("Timeout", "30")
646+
})
647+
.Build();
648+
649+
var options = Options.Create(config.Get<ApiOptions>());
650+
var service = new ApiService(options);
651+
652+
// Act & Assert
653+
Assert.That(service.BaseUrl, Is.EqualTo("https://test.api"));
654+
}
655+
```
656+
657+
</div>
658+
<div>
659+
660+
## <i class="fa fa-cog"></i> Integration Testing
661+
662+
```csharp
663+
public class TestWebApplicationFactory<TProgram>
664+
: WebApplicationFactory<TProgram> where TProgram : class
665+
{
666+
protected override void ConfigureWebHost(IWebHostBuilder builder)
667+
{
668+
builder.ConfigureAppConfiguration(config =>
669+
{
670+
config.AddInMemoryCollection(new[]
671+
{
672+
new KeyValuePair<string, string>("Database:ConnectionString",
673+
"Server=localhost;Database=TestDb;"),
674+
new KeyValuePair<string, string>("ExternalApi:BaseUrl",
675+
"https://mock-api.test")
676+
});
677+
});
678+
}
679+
}
680+
```
681+
682+
</div>
683+
</div>
684+
685+
---
686+
687+
# Configuration Validation
688+
689+
<div class="columns">
690+
<div>
691+
692+
## <i class="fa fa-check-circle"></i> Data Annotations
693+
694+
```csharp
695+
public class DatabaseOptions
696+
{
697+
[Required]
698+
[Url]
699+
public string ConnectionString { get; set; } = "";
700+
701+
[Range(1, 300)]
702+
public int CommandTimeoutSeconds { get; set; } = 30;
703+
704+
[Required]
705+
[RegularExpression(@"^[a-zA-Z0-9_]+$")]
706+
public string DatabaseName { get; set; } = "";
707+
}
708+
709+
// Register with validation
710+
services.AddOptions<DatabaseOptions>()
711+
.Bind(configuration.GetSection("Database"))
712+
.ValidateDataAnnotations()
713+
.ValidateOnStart();
714+
```
715+
716+
</div>
717+
<div>
718+
719+
## <i class="fa fa-shield-alt"></i> Custom Validation
720+
721+
```csharp
722+
public class DatabaseOptionsValidator : IValidateOptions<DatabaseOptions>
723+
{
724+
public ValidateOptionsResult Validate(string name, DatabaseOptions options)
725+
{
726+
var failures = new List<string>();
727+
728+
if (string.IsNullOrEmpty(options.ConnectionString))
729+
failures.Add("ConnectionString is required");
730+
731+
if (options.CommandTimeoutSeconds <= 0)
732+
failures.Add("CommandTimeoutSeconds must be positive");
733+
734+
if (!IsValidDatabaseName(options.DatabaseName))
735+
failures.Add("Invalid database name format");
736+
737+
return failures.Count > 0
738+
? ValidateOptionsResult.Fail(failures)
739+
: ValidateOptionsResult.Success;
740+
}
741+
}
742+
743+
// Register validator
744+
services.AddSingleton<IValidateOptions<DatabaseOptions>, DatabaseOptionsValidator>();
745+
```
746+
747+
</div>
748+
</div>
749+
750+
---
751+
752+
# Validation at Startup
753+
754+
<div class="columns">
755+
<div>
756+
757+
## <i class="fa fa-rocket"></i> Fail Fast Pattern
758+
759+
```csharp
760+
// Program.cs
761+
var builder = WebApplication.CreateBuilder(args);
762+
763+
// Configure options with validation
764+
builder.Services.AddOptions<ApiOptions>()
765+
.Bind(builder.Configuration.GetSection("Api"))
766+
.ValidateDataAnnotations()
767+
.ValidateOnStart(); // Validates during app startup
768+
769+
builder.Services.AddOptions<DatabaseOptions>()
770+
.Bind(builder.Configuration.GetSection("Database"))
771+
.Validate(options => !string.IsNullOrEmpty(options.ConnectionString),
772+
"Connection string cannot be empty")
773+
.ValidateOnStart();
774+
775+
var app = builder.Build();
776+
// App fails to start if validation fails
777+
```
778+
779+
</div>
780+
<div>
781+
782+
## <i class="fa fa-exclamation-triangle"></i> Benefits
783+
784+
- **Early Detection**: Catch configuration errors at startup
785+
- **Clear Error Messages**: Know exactly what's wrong
786+
- **Prevents Runtime Failures**: No surprises in production
787+
- **Better DevEx**: Immediate feedback during development
788+
789+
```csharp
790+
// Custom validation method
791+
services.AddOptions<MyOptions>()
792+
.Bind(configuration.GetSection("MySection"))
793+
.Validate(options =>
794+
{
795+
return options.ApiKey?.Length >= 10;
796+
}, "ApiKey must be at least 10 characters")
797+
.ValidateOnStart();
798+
```
799+
800+
</div>
801+
</div>
802+
803+
---
804+
576805
# DEMOS
577806

578807
---
579808

809+
# Secrets Management Best Practices
810+
811+
<div class="columns">
812+
<div>
813+
814+
## <i class="fa fa-exclamation-triangle"></i> Don't
815+
816+
- Store secrets in appsettings.json
817+
- Commit secrets to source control
818+
- Use production secrets in development
819+
- Log configuration values containing secrets
820+
821+
</div>
822+
<div>
823+
824+
## <i class="fa fa-check-circle"></i> Do
825+
826+
- Use User Secrets for development
827+
- Use Azure Key Vault for production
828+
- Use environment variables for containers
829+
- Implement proper secret rotation
830+
- Validate secrets at startup
831+
832+
</div>
833+
</div>
834+
835+
---
836+
837+
# Secrets by Environment
838+
839+
<div class="columns3">
840+
<div>
841+
842+
## <i class="fa fa-laptop-code"></i> Development
843+
844+
- **User Secrets**
845+
- Per-project secrets
846+
- Stored outside source control
847+
- Easy to manage locally
848+
849+
```bash
850+
dotnet user-secrets set "ApiKey" "dev-key-123"
851+
```
852+
853+
</div>
854+
<div>
855+
856+
## <i class="fa fa-server"></i> Staging/Production
857+
858+
- **Azure Key Vault**
859+
- Centralized secret management
860+
- Access policies and RBAC
861+
- Audit logging
862+
- Automatic rotation
863+
864+
```csharp
865+
builder.Configuration.AddAzureKeyVault(
866+
keyVaultUrl, credential);
867+
```
868+
869+
</div>
870+
<div>
871+
872+
## <i class="fa fa-cube"></i> Containers
873+
874+
- **Environment Variables**
875+
- Kubernetes secrets
876+
- Docker secrets
877+
- Service connection strings
878+
879+
```bash
880+
docker run -e "ConnectionString=..." myapp
881+
```
882+
883+
</div>
884+
</div>
885+
886+
---
887+
888+
# Environment-Specific Configuration Strategies
889+
890+
<div class="columns">
891+
<div>
892+
893+
## <i class="fa fa-layer-group"></i> Layered Configuration
894+
895+
```csharp
896+
builder.Configuration
897+
.AddJsonFile("appsettings.json")
898+
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", true)
899+
.AddEnvironmentVariables()
900+
.AddCommandLine(args);
901+
```
902+
903+
**Order matters!** Later sources override earlier ones.
904+
905+
</div>
906+
<div>
907+
908+
## <i class="fa fa-code-branch"></i> Environment Patterns
909+
910+
- **Development**: User Secrets + local files
911+
- **Staging**: Environment variables + Key Vault
912+
- **Production**: Key Vault + minimal env vars
913+
- **Testing**: In-memory configuration
914+
915+
```csharp
916+
if (env.IsDevelopment())
917+
{
918+
builder.Configuration.AddUserSecrets<Program>();
919+
}
920+
```
921+
922+
</div>
923+
</div>
924+
925+
---
926+
927+
# Configuration Security Considerations
928+
929+
<div class="columns">
930+
<div>
931+
932+
## <i class="fa fa-shield-alt"></i> Prevent Secret Leakage
933+
934+
- **Never log IConfiguration directly**
935+
- **Redact sensitive values in logs**
936+
- **Use IOptionsSnapshot/IOptionsMonitor**
937+
- **Implement custom configuration providers for sensitive data**
938+
939+
</div>
940+
<div>
941+
942+
## <i class="fa fa-eye-slash"></i> Secure Logging
943+
944+
```csharp
945+
// ❌ DON'T - Exposes all configuration
946+
logger.LogInformation("Config: {Config}",
947+
JsonSerializer.Serialize(configuration));
948+
949+
// ✅ DO - Log specific, non-sensitive values
950+
logger.LogInformation("Database timeout: {Timeout}s",
951+
dbOptions.CommandTimeout);
952+
```
953+
954+
</div>
955+
</div>
956+
957+
---
958+
580959
# Questions?
581960

582961
---

0 commit comments

Comments
 (0)