Options pattern en C#

En el post anterior vimos como leer configuración para nuestras aplicaciones de .NET a través de IConfiguration. En este post vamos a aprender sobre el “Options Pattern” un patrón para leer dicha configuración. 

 

 

Un gran beneficio del options pattern es que nos obligamos a definir clases que representan parte de la aplicación.

 

1 - Qué es Options pattern en C#?

El beneficio de introducir un patrón es que así podemos encapsular y separar la lógica de nuestra configuración del resto de los componentes. 

 

Por ejemplo, vimos que para leer la configuración utilizaremos el método .bind(), pero qué pasa si tenemos que validar dicha configuración? comprobar que el servidor es una IP (o un dominio) y no un texto aleatorio, el campo no esta nulo, etc.

Para este post vamos utilizar el mismo código que hemos ido utilizando durante toda la serie, pero lo vamos a extender para que implemente una funcionalidad para enviar un email. 

 

Para ello creamos una clase llamada EmailConfiguration, que va a ser la que va a tener una relación 1 a 1 con la información que vamos a definir en nuestro appsettings.json

public class EmailConfiguration
{
    public string SmtpServer { get; set; }
    public string From { get; set; }
}

 

Y junto a ella un pequeño servicio, el cual va a simular el envío de un 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;
    }
    
}

 

Como podemos observar vamos a inyectar el tipo EmailConfiguration dentro de nuestro servicio. Por cierto, el propio servicio EmailSender tambíen podrá ser inyectado a traves de su interfaz IEmailSender.

 

Tal y como tenemos el código ahora, podríamos emplear el método bind, como vimos en el post anterior para que nuestra configuración se mapee a nuestro objeto:

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>();
}

 

Pero hasta aquí no hemos visto nada nuevo que no viéramos en el post anterior, en que cambia? 

 

 

2 - Cómo configurar el options pattern en C#?

Para configurar el Options Pattern debemos cambiar el tipo que inyectamos en nuestro servicio de EmailConfiguration a IOptions<EmailConfiguration>.

 

Para Utilizar IOptions<T> debemos utilizar el paquete Microsoft.Extensions.Options, y como en el caso anterior, cuando nuestro servicio es invocado en el código, el contenedor de dependencias inyectara nuestra instancia de la configuración.

 

Y como utilizamos generics debemos acceder al valor de T, que lo haremos a través de .Value en el constructor:

public class EmailSender : IEmailSender
{
    private readonly EmailConfiguration _emailConfiguration;

    public EmailSender(IOptions<EmailConfiguration> options)
    {
        _emailConfiguration = options.Value;
    }
    /*más código*/
}

 

Si intentamos ejecutamos ahora el código, no funcionara, esto es debido a que no hemos inyectado dicho IOptions<T>, para añadir nuestra configuración al contenedor de dependencias debemos hacerlo a través del método .Configure<T>() del ServiceCollection.

 

El método .Configure<T> nos va ha pedir que introducimos como parámetro un tipo IConfigurationSection como el que vimos en el post anterior, para acceder al mismo, únicamente debemos acceder a la configuración e invocar .GetSection(“EmailService”):

services.Configure<EmailConfiguration>(Configuration.GetSection("EmailService"));
services.AddScoped<IEmailSender, EmailSender>();

 

Antes de continuar, decir que el “Options Pattern” nos provee de tres interfaces, IOptions<T>, que acabamos de ver, IOptionsSnapshot<T>  y finalmente IOptionsMonitor<T>.

 

 

3 - Por qué necesito IOptions en C#?

Como acabamos de ver el funcionamiento de IOptions<EmailConfiguration> y el que teníamos anteriormente inyectando únicamente EmailConfiguration parece ser el mismo, y así es, mientras trabajamos no encontraremos ninguna diferencia entre uno o el otro.

Pero hay que tener en cuenta un par de cosas:

  1. Cuando utilizamos Options Pattern,  nuestras configuraciones se crean en el contenedor de dependencias como singleton (cuando usamos IOptions), por lo que no tenemos que crearlas nosotros mismos y pueden ser utilizadas en cualquier servicio.
  2. Utilizarlo, nos fuera a tener nuestra configuración fuertemente tipada y así, evitar errores.
  3. Cuando revisamos código, o simplemente lo leemos después de un tiempo, es mucho más sencillo de entender si nuestro tipo de configuración se llama IOptions<T>, así sabemos de dónde viene.

Además, cualquiera de las tres interfaces nos provee de un método llamado .PostConfigure() el cual nos permite ejecutar instrucciones de código una vez la configuración esta cargada. 

Aquí podemos comprobar o validar la información de dicha configuración.

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");
    }
});

Pero la forma correcta de validar la configuración la veremos en el siguiente post.

 

 

4 - IOptionsSnapshot en C#

La segunda de nuestras interfaces es la interfaz es IOptionsSnapshot<T> la cual inicializa una instancia al inicio de nuestra request (scoped) la cual va a ser inmutable hasta el final de la misma.

 

La ventaja de IOptionsSnapthot es que nos permite cambiar el valor de la configuración dentro del fichero, y esta será inyectada en el código sin tener que volver a desplegar la aplicación.

ioptions snapshot

La forma de acceder a la configuración es similar a la anterior, únicamente debemos cambiar el tipo que inyectamos en nuestro servicio.

  • Nota: no tienes que cambiar nada en la configuración del contenedor de dependencias, ya que el .configure() se encarga de todo.
public class EmailSender : IEmailSender
{
    private readonly EmailConfiguration _emailConfiguration;

    public EmailSender(IOptionsSnapshot<EmailConfiguration> options)
    {
        _emailConfiguration = options.Value;
    }
    /*más código*/
}

 

 

5 - IOptionsMonitor en C#

La tercera y última de nuestras interfaces es IOptionsMontor la cual es un pelín diferente a las dos versiones anteriores. 

 

IOptionsMonitor<T> se inyecta en nuestro servicio como Singleton y funciona de una manera especial, ya que en vez de acceder a .Value para acceder a T como hacíamos anteriormente, ahora realizamos .CurrentValue el cual nos devuelve el valor en el momento.

Esto quiere decir que podemos inyectar IOptionsMonitor<T> y calculará el valor correcto cada vez que lo ejecutemos.

 

Esto quiere decir que si cambias el valor a mitad de la request o mitad de un proceso, este obtendrá el valor actualizado:

Y el código también será un poco diferente, ya que en nuestro constructor almacenaremos IOptionsMonitor<T> en vez de únicamente T, para así poder aprovecharnos del beneficio de utilizar 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 - Cuándo utilizar el patrón Options?

Cuando programamos en C#, debemos utilizar el Options Pattern siempre, ya que es considerado una buena práctica. 

La elección entre las diferentes interfaces del patrón IOptions dependerá de tu caso de uso.

Si no vas a cambiar la configuración, puedes utilizar IOptions.

Si vas a cambiar la configuración, tendrás que usar IOptionsSnapshot.

Y si necesitas cambiar la configuración constantemente o detectas que ese cambio puede pasar en el medio de un proceso lo recomendable será IOptionsMonitor.

 


Uso del bloqueador de anuncios adblock

Hola!

Primero de todo bienvenido a la web de NetMentor donde podrás aprender programación en C# y .NET desde un nivel de principiante hasta más avanzado.


Yo entiendo que utilices un bloqueador de anuncios como AdBlock, Ublock o el propio navegador Brave. Pero te tengo que pedir por favor que desactives el bloqueador para esta web.


Intento personalmente no poner mucha publicidad, la justa para pagar el servidor y por supuesto que no sea intrusiva; Si pese a ello piensas que es intrusiva siempre me puedes escribir por privado o por Twitter a @NetMentorTW.


Si ya lo has desactivado, por favor recarga la página.


Un saludo y muchas gracias por tu colaboración

© copyright 2024 NetMentor | Todos los derechos reservados | RSS Feed

Buy me a coffee Invitame a un café