In the previous post we saw how to read configuration for our .NET applications using IConfiguration
. In this post, we're going to learn about the Options Pattern, a pattern for reading such configuration.
Index
A great benefit of the options pattern is that it forces us to define classes representing parts of the application.
1 - What is the Options pattern in C#?
The benefit of introducing a pattern is it allows us to encapsulate and separate our configuration logic from the rest of the components.
For example, we saw that to read the configuration we use the .bind()
method, but what if we have to validate that configuration? Checking if the server is an IP address (or a domain) and not just random text, or making sure the field isn't null, etc.
For this post, we'll use the same code as in the rest of the series, but we'll extend it to implement some functionality for sending an email.
For this, we create a class called EmailConfiguration
, which will have a one to one relationship with the information we'll define in our appsettings.json
.
public class EmailConfiguration
{
public string SmtpServer { get; set; }
public string From { get; set; }
}
And with it, a small service which will simulate sending an email:
public interface IEmailSender
{
Task<Result<bool>> SendEmail(string to, string subject, string body);
}
public class EmailSender : IEmailSender
{
private readonly EmailConfiguration _emailConfiguration;
public EmailSender(EmailConfiguration emailConfiguration)
{
_emailConfiguration = emailConfiguration;
}
public async Task<Result<bool>> SendEmail(string to, string subject, string body)
{
Console.WriteLine("this simulates the an email being Sent");
Console.WriteLine($"Email configuration Server: {_emailConfiguration.SmtpServer}, From: {_emailConfiguration.From}");
Console.WriteLine($"Email data To: {to}, subject: {subject}, body: {body}");
return true;
}
}
As you can see, we will inject the EmailConfiguration
type into our service. By the way, the EmailSender
service itself can also be injected through its interface IEmailSender
.
As the code stands now, we could use the bind
method, like we saw in the previous post, to map our configuration to our object:
public void ConfigureServices(IServiceCollection services)
{
/*más código*/
services.AddSingleton(x =>
{
EmailConfiguration emailConfiguration = new EmailConfiguration();
Configuration.Bind("emailService", emailConfiguration);
return emailConfiguration;
} )
.AddScoped<IEmailSender, EmailSender>();
services.AddScoped<IEmailSender, EmailSender>();
}
But up to now, there's nothing new compared to the previous post. So what's different?
2 - How to configure the options pattern in C#?
To configure the Options Pattern, we need to change the type we inject into our service from EmailConfiguration
to IOptions<EmailConfiguration>
.
To use IOptions<T>
, you need the Microsoft.Extensions.Options
package, and as in the previous example, when our service is invoked in the code, the dependency container will inject our instance of the configuration.
Since we're using generics, we need to access the value of T
, which we'll do via .Value
in the constructor:
public class EmailSender : IEmailSender
{
private readonly EmailConfiguration _emailConfiguration;
public EmailSender(IOptions<EmailConfiguration> options)
{
_emailConfiguration = options.Value;
}
/*más código*/
}
If you try to run the code now, it won't work. This is because we haven't injected IOptions<T>
. To add our configuration to the dependency container, we need to use the .Configure<T>()
method of the ServiceCollection
.
The .Configure<T>
method requires as a parameter an IConfigurationSection
, as we saw in the previous post. To access it, we only need to access the configuration and invoke .GetSection("EmailService")
:
services.Configure<EmailConfiguration>(Configuration.GetSection("EmailService"));
services.AddScoped<IEmailSender, EmailSender>();
Before continuing, keep in mind that the Options Pattern provides three interfaces: IOptions<T>
(which we just saw), IOptionsSnapshot<T>
, and finally IOptionsMonitor<T>
.
3 - Why do I need IOptions in C#?
As we've just seen, using IOptions<EmailConfiguration>
and previously injecting EmailConfiguration
seem to work the same way, and they do, while we're developing, you won't notice any difference between them.
But keep a few things in mind:
- When you use the Options Pattern, your configurations are created in the dependency container as singletons (when using
IOptions
). This means you don't need to create them yourself, and they can be used in any service. - Using it keeps your configuration strongly typed, which helps prevent errors.
- When reviewing code, or simply reading it after some time, it's much easier to understand if your configuration type is called
IOptions<T>
. That way, you know where it comes from.
Additionally, any of the three interfaces provide a method called .PostConfigure()
, which lets you run code once the configuration is loaded.
Here, you can check or validate the configuration information.
services.Configure<EmailConfiguration>(Configuration.GetSection("EmailService"));
services.PostConfigure<EmailConfiguration>(emailConfiguration =>
{
if ( string.IsNullOrWhiteSpace(emailConfiguration.SmtpServer))
{
throw new ApplicationException("el campo SmtpServer debe contner información");
}
});
But the proper way to validate configuration will be discussed in the next post.
4 - IOptionsSnapshot in C#
The second of our interfaces is IOptionsSnapshot<T>
, which initializes an instance at the start of each request (scoped
) and keeps it immutable for the duration of that request.
The advantage of IOptionsSnapshot
is that it lets you change the value of the configuration in the file, and it will be injected into your code without having to redeploy the application.
The way to access the configuration is similar to before, you only need to change the type injected into your service.
- Note: you do not need to change anything in the dependency container configuration,
.configure()
takes care of everything.
public class EmailSender : IEmailSender
{
private readonly EmailConfiguration _emailConfiguration;
public EmailSender(IOptionsSnapshot<EmailConfiguration> options)
{
_emailConfiguration = options.Value;
}
/*más código*/
}
5 - IOptionsMonitor in C#
The third and last interface is IOptionsMonitor
, and it's a bit different from the previous two versions.
IOptionsMonitor<T>
is injected into the service as a Singleton
and works in a special way: instead of accessing .Value
to reach T
like before, now you'll use .CurrentValue
, which returns the value at that moment.
This means you can inject IOptionsMonitor<T>
and it will compute the correct value every time you use it.
This means that if you change the value during a request or in the middle of a process, you'll get the updated value:
And the code will be a bit different too, since in your constructor you'll store the IOptionsMonitor<T>
instead of just T
, fully benefiting from using IOptionsMonitor
.
public class EmailSender : IEmailSender
{
private readonly IOptionsMonitor<EmailConfiguration> _options;
private EmailConfiguration EmailConfiguration => _options.CurrentValue;
public EmailSender(IOptionsMonitor<EmailConfiguration> options)
{
_options = options;
}
public async Task<Result<bool>> SendEmail(string to, string subject, string body)
{
Console.WriteLine("this simulates the an email being Sent");
Console.WriteLine($"Email configuration Server: {EmailConfiguration.SmtpServer}, From: {EmailConfiguration.From}");
Console.WriteLine($"Email data To: {to}, subject: {subject}, body: {body}");
return true;
}
}
6 - When to use the Options pattern?
When programming in C#, you should always use the Options Pattern, as it is considered a best practice.
Choosing among the different interfaces of the IOptions
pattern will depend on your specific use case.
If you're not going to change the configuration, you can use IOptions
.
If you are going to change the configuration, you should use IOptionsSnapshot
.
And if you need to change the configuration constantly or you know that the value might change in the middle of a process, it's better to use IOptionsMonitor
.
If there is any problem you can add a comment bellow or contact me in the website's contact form