Dependency Injection & Common App Settings
Hola cracks, como andan? Después de un tiempo decidí hacer los posts tanto en inglés como en español ya que varias personas me lo venían sugiriendo, espero que les guste:D
Ok, hoy vamos hablar de una situación que suele suceder cuando comenzamos un proyecto: los parametros de configuración de la aplicación o settings, en la mayoria de los escenarios estos no cambiaran, serán inmutables por ejemplo:
- Nombre de la aplicación
- Email que utilizara la app para envió de correos
- Cantidad de registros por paginas que mostraran las grillas de la app.
- El path de la carpeta en la cual se guardaran los attachments de la app.
- Algunas Keys de plugins que usamos en la app, etc
Tenemos varias alternativas para guardar y recuperar esta información. Veamos algunas 😉
Business scenario 😀
Bien, estamos desarrollado una app para una Agencia de Viajes y contamos con algunos requerimientos no funcionales:
- Todas las grillas de la aplicación deben ser refrescadas cada 2 minutos automáticamente si el usuario se encuentra en una.
- Todas las grillas soportan paginación, la cantidad de registros de cada página es de 25 filas.
- Los usuarios pueden subir archivos a una ubicación particular en el storage server.
Ok, no vamos a hardcodear valores en el código, vamos a necesitar 3 variables para alojar estos valores:
- RefreshGridTimerInSeconds
- RowsPerPage
- StorageAddress
Estos parámetros serán consultados por la aplicación todo el tiempo para cargar las grillas o subir archivos.
Alternativa 1 – Usar AppSettings & Options pattern
Primero, declaramos una sección : “AppSettings” en el appSettings.json:
{
...
"AppSettings": {
"RefreshGridTimerInSeconds": 120,
"RowsPerPage": 25,
"StorageAddress": "E:\\Storage"
}
}
Luego, necesitamos crear una clase para mappear la sección:
public class AppSettings
{
public int RefreshGridTimerInSeconds { get; set; }
public int RowsPerPage { get; set; }
public string StorageAddress { get; set; }
}
Finalmente, registramos nuestra clase en el contenedor. Utilizaremos Options Pattern para inyectar las settings en las clases que lo necesiten. Registramos nuestras settings:
//Startup class
public void ConfigureServices(IServiceCollection services)
{
//Configure DI - Add AppSettings in the container
services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
...
Y ahora? que teneos que hacer? Bien, para inyectar la clase AppSettings en las clases que necesiten usarla utilizaremos Options Patterns. Net Core nos da la posibilidad de inyectar: IOption<T> o IOptionsSnapshot<T> o tal vez IOptionsMonitor<T> de acuerdo a nuestras necesidades.
En este caso utilizaremos IOptionsSnapshot<T> ya que de este modo en cada request obtendremos el ultimo valor del appSettings.json. Ya que IOptionSnapshot es Scoped! Más adelante haré un post hablando de estas interesantes interfaces y sus features 😉
Bien, inyectemos el appSettings en Destination Controller.
[Route("api/[controller]")]
[ApiController]
public class DestinationsController : ControllerBase
{
private readonly AppSettings _appSettings;
public DestinationsController(IOptionsSnapshot<AppSettings> appSettings)
{
_appSettings = appSettings.Value;
}
[HttpGet]
public IActionResult Get()
{
return Ok(new
{
_appSettings.RefreshGridTimerInSeconds,
_appSettings.RowsPerPage,
_appSettings.StorageAddress
});
}
}
Cuando enviamos un request a api/destinations [GET]
Excelente, podemos usar esta alternativa para recuperar los valores del appSettings.json utilizando inyección de dependencias 😉
Alternativa 2 – Usando appSettings & Singleton 😀
Otra alternativa es utilizar un servicio Singleton, él cual contenga las settings de la aplicación.
En este escenario, las settings son inmutables, es decir se mantienen iguales en toda la vida del sistema por lo tanto podríamos tranquilamente colocarlas en un singleton y luego inyectarla en las clases que necesiten consultar las settings.
Seguiremos utilizando la sección “AppSettings” que esta en el appSettings.json pero esta vez NO accederemos a los valores mediante Options Pattern sino utilizando un Singleton.
Primero, necesitamos crear nuestro AppSettingService que será nuestro singleton. El mismo contendrá las settings de nuestra aplicación.
public class AppSettingService: IAppSettingService
{
public int RefreshGridTimerInSeconds { get; private set; }
public int RowsPerPage { get; private set; }
public string StorageAddress { get; private set; }
public AppSettingService(int refreshGridTimerInSeconds, int rowsPerPage, string storageAddress)
{
RefreshGridTimerInSeconds = refreshGridTimerInSeconds;
RowsPerPage = rowsPerPage;
StorageAddress = storageAddress;
}
}
Luego, tenemos que registrar nuestro service como Singleton. En este caso, usaremos la sobrecarga que utiliza un Factory para registrar el servicio. De este forma nos haremos responsable de la creación de instancias de nuestro servicio:
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IAppSettingService, AppSettingService>(sp =>
{
return new AppSettingService(
Convert.ToInt32(Configuration["AppSettings:RefreshGridTimerInSeconds"]),
Convert.ToInt32(Configuration["AppSettings:RowsPerPage"]),
Configuration["AppSettings:StorageAddress"]);
});
...
Finalmente, inyectamos el servicio en DestinationController:
[Route("api/[controller]")]
[ApiController]
public class DestinationsController : ControllerBase
{
private IAppSettingService _appSettingService;
public DestinationsController(IAppSettingService appSettingService)
{
_appSettingService = appSettingService;
}
[HttpGet]
public IActionResult Get()
{
return Ok(new
{
_appSettingService.RefreshGridTimerInSeconds,
_appSettingService.RowsPerPage,
_appSettingService.StorageAddress
});
}
...
Veamos que sucede cuando enviamos un request a api/destinations [GET]:
Perfecto, obtuvimos los mismos resultados que en la Alternativa 1. 🙂
Personalmente prefiero la alternativa 1 en escenarios donde los settings no cambian, de esta forma aprovechamos las ventajas de Options Pattern, sin embargo, en algunos escenarios no solo será leer un config sino que tendremos que aplicar alguna lógica para calcular variables, etc en esos casos utilizo la alternativa 2: Singleton pattern.
Alternativa 3 – settings modificables, singleton service & scoped repository & IServiceScopeFactory 😮
Ok, nuestro cliente nos llama cada mes para que cambiemos los parámetros que pusimos en la sección “AppSettings” . Resulta ser que no son tan inmutables como habíamos pensando a tal punto que el cliente nos pide un modulo para que los usuarios puedan modificar los settings cuando lo necesiten.
Y ahora que hacemos ? bien tenemos que hacer una estrategia para que los users puedan actualizar los settings: RefreshGridTimerInSeconds, RowsPerPage, and StorageAddress desde la app.
Ok, obviamente ya no podemos utilizar el appSettings.json ya que ahora el user puede actualizar estos valores desde la app, ensu lugar generaremos una tabla en la base de datos denominada: AppSettings. Dicha tabla tendra las columnas: Id | Code | Value. Además utilizaremos repository pattern para acceder a la base de datos.
Veramos nuestra clase AppSettings que utilizaremos para mappear los valores de los registros de la tabla.
public class AppSettings
{
public int Id { get; set; }
public string Code { get; set; }
public string Value { get; set; }
}
Luego, generaremos la interface IAppSettingRepository y la clase concreta AppSettingRepository:
public interface IAppSettingRepository
{
IEnumerable<AppSettings> GetList();
}
Finalmente, necesitamos registrar nuestro Repository como Scoped:
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IAppSettingRepository, AppSettingRepository>();
...
Ok, ahora tenemos dos opciones:
- Inyectar IAppSettingRepository en las clases que necesiten obtener los settings desde la base de datos.
- Crear un Singleton Service que contenga los settings. Por supuesto que este servicio deberá utilizar el AppSettingRepository para acceder a la db, inicializar sus variables y actualizarlas cuando los usuarios modifiquen las settings desde la app.
Ok, cual es el mejor enfoque ? Desde mi punto de vista, la opción 2. Por qué?
- Accedemos a la DB solamente en el primer request que necesite obtener los settings de la aplicación, es decir, el primer request que quiera utilizar el Singleton. En ese request se carga los valores de los settings en el Singleton.
- En todos los request posteriores que se reciban que necesiten acceder a los settings, simplemente se consultara al singleton sin acceder a la db.
- Cuando los usuarios modifiquen los Settings, la app actualizara los valores en la db y luego el singleton debera recargar sus valores también para mantenerlos actualizados.
Perfecto tenemos una buena estrategia 😉 sin embargo nos falta resolver un último problema: tenemos un Singleton Service ( AppSettingService ) y un Scoped service ( AppSettingRepository ). Deberíamos inyectar AppSettingRepository en el singleton para poder acceder a la db e inicializarlo pero esto no es posible! No podemos inyectar un scoped service en un singleton, que hacemos ahora !?
Net Core nos provee la interface: IServiceScopeFactory que nos permite crear un Scope y utilizar el provider para obtener instancias del container 😉 Perfecto, ahora tenemos una alternativa para obtener AppSettingRepository 😀
public class AppSettingService: IAppSettingService
{
private readonly IServiceScopeFactory _serviceScopeFactory;
//Settings
public int RefreshGridTimerInSeconds { get; private set; }
public int RowsPerPage { get; private set; }
public string StorageAddress { get; private set; }
//IServiceScopeFactory allows create scope and use the provider to retrieve instances from the container manually ;)
public AppSettingService(IServiceScopeFactory serviceScopeFactory)
{
_serviceScopeFactory = serviceScopeFactory;
this.ReloadParameters();
}
//Using this method to init and refresh the settings
public void ReloadParameters()
{
using(var scope = _serviceScopeFactory.CreateScope())
{
var repository = scope.ServiceProvider.GetService<IAppSettingRepository>();
var settings = repository.GetList();
RefreshGridTimerInSeconds = Convert.ToInt32(settings.First(s => s.Code == "RefreshGridTimerInSeconds").Value);
RowsPerPage = Convert.ToInt32(settings.First(s => s.Code == "RowsPerPage").Value);
StorageAddress = settings.First(s => s.Code == "StorageAddress").Value;
}
}
}
Perfecto, lo logramos 😉
Ok, veamos nuestro DestinationController, el mismo tiene dos metodos:
- [Get] recupera los valores del Singleton.
- [UpdateSettings] actualiza los settings en la db y refresca los valores en el singleton.
[Route("api/[controller]")]
[ApiController]
public class DestinationsController : ControllerBase
{
private readonly IAppSettingService _appSettingService;
private readonly IAppSettingRepository _appSettingRepository;
public DestinationsController(IAppSettingService appSettingService,
IAppSettingRepository appSettingRepository)
{
_appSettingService = appSettingService;
_appSettingRepository = appSettingRepository;
}
[HttpGet]
public IActionResult Get()
{
//retrieve values from the singleton without accessing to databse
return Ok(new
{
_appSettingService.RefreshGridTimerInSeconds,
_appSettingService.RowsPerPage,
_appSettingService.StorageAddress
});
}
[HttpPut]
public IActionResult UpdateSettings()
{
//Update values in the database
foreach (var setting in _appSettingRepository.GetList())
{
if( setting.Code == "RefreshGridTimerInSeconds" ||
setting.Code == "RowsPerPage")
{
setting.Value = "500";
}
else
{
setting.Value = @"C:\Storage\Test";
}
}
//Refresh singleton settings
_appSettingService.ReloadParameters();
return NoContent();
}
}
Hagamos un test rápido 😉
1- Inicia la app, enviamos un request a app/destinations, se inicializa el Singleton con los valores de la db, luego recuperamos los valores del singleton y los devolvemos:
Excelente, nuestro singleton funciona 😀
Luego, enviamos un PUT request para actualizar los valores de los settings en la db y hacer un refresh en los valores del singleton.
Finalmente, enviamos un nuevo request a app/destinations para chequear que se hayan actualizados los valores en la db y en el singleton:
Genial! Todo funciono 10 puntos 😉 tenemos un singleton funcional que guarda las settings y lo podemos actualizar cuando el usuario modifica sus valores 😀
Eso es todo por el momento cracks 😉 Espero que les haya gustado 😀
El código completo lo encuentran en mi github
Abrazo 😀