ASP.NET Core: Options Pattern in Microservices
In modern microservice architectures, managing configuration is crucial for building flexible and maintainable applications. The Options Pattern in ASP.NET Core provides a robust way to handle configuration data, making your applications more modular and easier to test. Let’s dive deep into how this pattern works and how you can implement it effectively in your microservices.
What is the Options Pattern?
The Options Pattern is a configuration approach that provides the following benefits:
- Strong typing of configuration settings
- Validation of configuration data
- Multiple configuration versions support
- Reloadable configuration
- Dependency injection ready
Implementation Guide
First, let’s create a simple configuration class that represents our application settings. In a microservice, this might include database connections, API endpoints, or service-specific settings.
public class EmailServiceOptions
{
public const string SectionName = "EmailService";
public string SmtpServer { get; set; }
public int Port { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public bool UseSSL { get; set; }
public int RetryCount { get; set; }
}
Your configuration values should be defined in your appsettings.json file:
{
"EmailService": {
"SmtpServer": "iqboss.example.com",
"Port": 587,
"Username": "iqboss@example.com",
"Password": "securePassword",
"UseSSL": true,
"RetryCount": 3
}
}
Register your options in the service collection during startup:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.RegisterOptions<EmailServiceOptions>(configuration, EmailServiceOptions.SectionName);
}
}
public static class ApplicationOptionRegister
{
public static T RegisterOptions<T>(
this IServiceCollection services,
IConfiguration configuration,
string sectionName) where T : class
{
IConfigurationSection section = configuration.GetSection(sectionName);
services.AddOptions<T>().Bind(section)
.ValidateDataAnnotations()
.ValidateOnStart();
return section.Get<T>();
}
}
Here’s how to use the configured options in a service:
public class EmailService : IEmailService
{
private readonly EmailServiceOptions _options;
public EmailService(IOptions<EmailServiceOptions> options)
{
_options = options.Value;
}
public async Task SendEmailAsync(string to, string subject, string body)
{
using var client = new SmtpClient(_options.SmtpServer, _options.Port)
{
EnableSsl = _options.UseSSL,
Credentials = new NetworkCredential(_options.Username, _options.Password)
};
var retryPolicy = Policy
.Handle<SmtpException>()
.WaitAndRetryAsync(_options.RetryCount,retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
await retryPolicy.ExecuteAsync(async () =>
{
await client.SendMailAsync(
_options.Username,
to,
subject,
body);
});
}
}
Advanced Features
Named Options: Sometimes you need multiple configurations of the same type. Named options allow you to handle this:
services.Configure<EmailServiceOptions>("Primary",
configuration.GetSection("PrimaryEmailService"));
services.Configure<EmailServiceOptions>("Backup",
configuration.GetSection("BackupEmailService"));
public class EmailService
{
public EmailService(IOptionsMonitor<EmailServiceOptions> options)
{
var primaryConfig = options.Get("Primary");
var backupConfig = options.Get("Backup");
}
}
Reloadable Options: For configurations that might change during runtime:
services.Configure<EmailServiceOptions>(
configuration.GetSection(EmailServiceOptions.SectionName))
.AddOptions<EmailServiceOptions>()
.Bind(configuration.GetSection(EmailServiceOptions.SectionName))
.ValidateDataAnnotations()
.ValidateOnStart();
// Usage
public class EmailService
{
private readonly IOptionsMonitor<EmailServiceOptions> _options;
public EmailService(IOptionsMonitor<EmailServiceOptions> options)
{
_options = options;
options.OnChange(config =>
{
// Handle configuration changes
Console.WriteLine("Config changed: New SMTP server is " + config.SmtpServer);
});
}
}
Best Practices
- Separation of Concerns: Keep your options classes focused and specific to a particular feature or service.
- Validation: Always implement validation for your options to catch configuration errors early.
- Security: Never store sensitive information like passwords in plain text in your configuration files. Use secret management solutions like Azure Key Vault or AWS Secrets Manager.
- Documentation: Document all configuration options, including their purpose, acceptable values, and default settings.
- Testing: Create unit tests for your options validation and service implementation:
public class EmailServiceTests
{
[Fact]
public void ValidateOptions_WithInvalidPort_ReturnsFailure()
{
// Arrange
var options = new EmailServiceOptions
{
SmtpServer = "smtp.example.com",
Port = -1,
Username = "test",
Password = "test"
};
var validator = new EmailServiceOptionsValidator();
// Act
var result = validator.Validate("", options);
// Assert
Assert.False(result.Succeeded);
Assert.Contains(result.Failures,
f => f.Contains("Port must be between 1 and 65535"));
}
}
Conclusion
The Options Pattern in ASP.NET Core is a powerful tool for managing configuration in microservices. It provides type safety, validation, and flexibility while maintaining clean separation of concerns. By following the best practices outlined above and leveraging features like validation and named options, you can create more maintainable and robust microservices.
Remember that while the Options Pattern is excellent for application configuration, it should be used alongside other configuration management best practices, especially when dealing with sensitive information in a microservices environment.
Thank you for reading if you have any doubts. drop the message in comments.
Follow me on Medium or LinkedIn to read more about Microservices.