Casos de uso de configuración en ASP.NET Core

Para obtener la configuración de la aplicación, es común utilizar el método de acceso por palabra clave (clave-valor). Pero esto no siempre es conveniente. a veces es necesario utilizar objetos listos para usar en su código con valores ya establecidos y con la capacidad de actualizar valores sin reiniciar la aplicación. Este ejemplo proporciona una plantilla para usar la configuración como middleware para aplicaciones ASP.NET Core.



Le recomendamos que se familiarice con el material: Metanit - Configuración , Cómo funciona la configuración en .NET Core .



Formulación del problema



Debe implementar una aplicación ASP NET Core con la capacidad de actualizar la configuración en formato JSON en tiempo de ejecución. Durante una actualización de configuración, las sesiones que se estén ejecutando actualmente deberían seguir funcionando con las opciones de configuración anteriores. Después de actualizar la configuración, los objetos usados ​​deben actualizarse / reemplazarse por otros nuevos.



La configuración debe estar deserializada, no debe haber acceso directo a los objetos IConfiguration desde los controladores. Los valores leídos deben verificarse para verificar que sean correctos, si no están presentes, deben reemplazarse con los valores predeterminados. La implementación debería funcionar en un contenedor Docker.



Trabajo de configuración clásico



GitHub: ConfigurationTemplate_1



El proyecto se basa en la plantilla ASP NET Core MVC. El proveedor de configuración JsonConfigurationProvider se utiliza para trabajar con archivos de configuración JSON . Para agregar la capacidad de recargar la configuración de la aplicación durante la operación, agregue el parámetro: "reloadOnChange: true".



En el archivo Startup.cs, reemplace:



public Startup(IConfiguration configuration)
 {
   Configuration = configuration;
 }


En



public Startup(IConfiguration configuration)
 {         
   var builder = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
   configuration = builder.Build();
   Configuration = configuration;
  }


.AddJsonFile : agrega un archivo JSON, reloadOnChange: true indica que cuando se cambian los parámetros del archivo de configuración, se volverán a cargar sin necesidad de volver a cargar la aplicación.



Contenido del archivo appsettings.json :



{
  "AppSettings": {
    "Parameter1": "Parameter1 ABC",
    "Parameter2": "Parameter2 ABC"  
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}


Los controladores de aplicaciones usarán el servicio: ServiceABC en lugar de acceder directamente a la configuración. ServiceABC es una clase que toma valores iniciales del archivo de configuración. En este ejemplo, la clase ServiceABC contiene solo una propiedad de título . Contenido del



archivo ServiceABC.cs :



public class ServiceABC
{
  public string Title;
  public ServiceABC(string title)
  {
     Title = title;
  }
  public ServiceABC()
  { }
}


Para utilizar ServiceABC, debe agregarlo como un servicio de middleware a su aplicación. Agregue el servicio como AddTransient, que se crea cada vez que accede a él, usando la expresión:
services.AddTransient<IYourService>(o => new YourService(param));
Excelente para servicios ligeros que no consumen memoria ni recursos. La lectura de los parámetros de configuración en Startup.cs se realiza mediante IConfiguration , que utiliza una cadena de consulta que indica la ruta completa de la ubicación del valor, por ejemplo: AppSettings: Parameter1.



En el archivo Startup.cs, agregue:



public void ConfigureServices(IServiceCollection services)
{
  //  "Parameter1"    ServiceABC
  var settingsParameter1 = Configuration["AppSettings:Parameter1"];
  //  "Parameter1"            
  services.AddScoped(s=> new ServiceABC(settingsParameter1));
  //next
  services.AddControllersWithViews();
}


Un ejemplo del uso del servicio ServiceABC en un controlador, el valor de Parameter1 se mostrará en la página html.



Para usar el servicio en controladores, agréguelo al constructor, archivo HomeController.cs



public class HomeController : Controller
{
  private readonly ILogger<HomeController> _logger;
  private readonly ServiceABC _serviceABC;
  public HomeController(ILogger<HomeController> logger, ServiceABC serviceABC)
    {
      _logger = logger;
      _serviceABC = serviceABC;
    }
  public IActionResult Index()
    {
      return View(_serviceABC);
    }


Agregue el archivo ServiceABC de visibilidad del servicio _ViewImports.cshtml



@using ConfigurationTemplate_1.Services


Cambie Index.cshtml para mostrar las opciones de la página Parameter1 .



@model ServiceABC
@{
    ViewData["Title"] = "Home Page";
}
    <div class="text-center">
        <h1>   ASP.NET Core</h1>
        <h4>   </h4>
    </div>
<div>        
    <p> ServiceABC,  
          Parameter1 = @Model.Title</p>
</div>


Comencemos la aplicación:







Salir



Este enfoque resuelve parcialmente el problema. Esta solución no permite aplicar cambios de configuración mientras la aplicación está en ejecución. el servicio recibe el valor del archivo de configuración solo al inicio y luego funciona solo con esta instancia. Como resultado, los cambios posteriores al archivo de configuración no darán lugar a cambios en la aplicación.



Usando IConfiguration como singleton



GitHub: ConfigurationTemplate_2 La



segunda opción es poner IConfiguration (como Singleton) en los servicios. Como resultado, IConfiguration se puede llamar desde controladores y otros servicios. Cuando se usa AddSingleton, el servicio se crea una vez, y cuando se usa la aplicación, la llamada va a la misma instancia. Utilice este método con extrema precaución, ya que pueden producirse pérdidas de memoria y problemas de subprocesos múltiples.



Reemplacemos el código del ejemplo anterior en Startup.cs por uno nuevo, donde

services.AddSingleton<IConfiguration>(Configuration);
agrega IConfiguration como Singleton a los servicios.



public void ConfigureServices(IServiceCollection services)
{
  //  IConfiguration     
  services.AddSingleton<IConfiguration>(Configuration);
  //  "ServiceABC"                          
  services.AddScoped<ServiceABC>();
  //next
  services.AddControllersWithViews();
}


Cambie el constructor del servicio ServiceABC para aceptar IConfiguration



public class ServiceABC
{        
  private readonly IConfiguration _configuration;
  public string Title => _configuration["AppSettings:Parameter1"];        
  public ServiceABC(IConfiguration Configuration)
    {
      _configuration = Configuration;
    }
  public ServiceABC()
    { }
}


Como en la versión anterior, agregue el servicio al constructor y agregue un enlace al espacio de nombres
, HomeController.cs



public class HomeController : Controller
{
  private readonly ILogger<HomeController> _logger;
  private readonly ServiceABC _serviceABC;
  public HomeController(ILogger<HomeController> logger, ServiceABC serviceABC)
    {
      _logger = logger;
      _serviceABC = serviceABC;
    }
  public IActionResult Index()
    {
      return View(_serviceABC);
    }


ServiceABC _ViewImports.cshtml:



@using ConfigurationTemplate_2.Services;


Index.cshtml Parameter1 .



@model ServiceABC
@{
    ViewData["Title"] = "Home Page";
}
<div class="text-center">
    <h1>   ASP.NET Core</h1>
    <h4> IConfiguration  Singleton</h4>
</div>
<div>
    <p>
         ServiceABC,  
          Parameter1 = @Model.Title
    </p>
</div>




Comencemos la aplicación:







el servicio ServiceABC agregado al contenedor usando AddScoped significa que se creará una instancia de la clase en cada solicitud de página. Como resultado, se creará una instancia de la clase ServiceABC en cada solicitud http junto con la recarga de la configuración de IConfiguration , y se aplicarán nuevos cambios en appsettings.json.

Por lo tanto, si durante el funcionamiento de la aplicación, cambie el parámetro Parameter1 a “NEW !!! Parameter1 ABC ”, la próxima vez que acceda a la página de inicio, se mostrará el nuevo valor del parámetro. Actualicemos



la página después de cambiar el archivo appsettings.json :







Salir



La desventaja de este enfoque es la lectura manual de cada parámetro. Y si agrega la validación de parámetros, la verificación se realizará no después de cambiar el archivo appsettings.json, sino cada vez que use ServiceABC , que es una acción innecesaria. En el mejor de los casos, los parámetros deben validarse solo una vez después de cada cambio de archivo.



Deserialización de la configuración con validación (opción IOptions)



GitHub: ConfigurationTemplate_3

Lea acerca de las opciones aquí .



Esta opción elimina la necesidad de utilizar ServiceABC . En su lugar, se utiliza la clase AppSettings , que contiene la configuración del archivo de configuración y el objeto ClientConfig . El objeto ClientConfig debe inicializarse después de cambiar la configuración, porque un objeto prefabricado se utiliza en controladores.

ClientConfig es una clase que interactúa con sistemas externos, cuyo código no se puede cambiar. Si solo deserializa los datos de la clase AppSettings , ClientConfigserá nulo. Por lo tanto, es necesario suscribirse al evento de configuración de lectura e inicializar el objeto ClientConfig en el controlador .



Para transferir la configuración no en forma de pares clave-valor, sino como objetos de ciertas clases, usaremos la interfaz IOptions . Además, IOptions, a diferencia de ConfigurationManager, le permite deserializar secciones individuales. Para crear el objeto ClientConfig , deberá usar IPostConfigureOptions , que se ejecuta después de que se haya procesado toda la configuración. IPostConfigureOptions se ejecutará cada vez que se lea la configuración, la más reciente.



Vamos a crear ClientConfig.cs :



public class ClientConfig
{
  private string _parameter1;
  private string _parameter2;
  public string Value => _parameter1 + " " + _parameter2;
  public ClientConfig(ClientConfigOptions configOptions)
    {
      _parameter1 = configOptions.Parameter1;
      _parameter2 = configOptions.Parameter2;
    }
}


Tomará parámetros como constructor en forma de un objeto ClientConfigOptions :



public class ClientConfigOptions
{
  public string Parameter1;
  public string Parameter2;
} 


Creemos la clase de configuración AppSettings y definamos el método ClientConfigBuild () en ella , que creará el objeto ClientConfig .



Archivo AppSettings.cs :



public class AppSettings
{        
  public string Parameter1 { get; set; }
  public string Parameter2 { get; set; }        
  public ClientConfig clientConfig;
  public void ClientConfigBuild()
    {
      clientConfig = new ClientConfig(new ClientConfigOptions()
        {
          Parameter1 = this.Parameter1,
          Parameter2 = this.Parameter2
        }
        );
      }
}


Creemos un controlador de configuración que se procesará en último lugar. Para hacer esto, debe heredarse de IPostConfigureOptions . El último PostConfigure llamado ejecutará ClientConfigBuild () , que creará ClientConfig .



Archivo ConfigureAppSettingsOptions.cs :



public class ConfigureAppSettingsOptions: IPostConfigureOptions<AppSettings>
{
  public ConfigureAppSettingsOptions()
    { }
  public void PostConfigure(string name, AppSettings options)
    {            
      options.ClientConfigBuild();
    }
}


Ahora queda hacer cambios solo en Startup.cs , los cambios afectarán solo la función ConfigureServices (servicios IServiceCollection) .



Primero, leamos la sección AppSettings en appsettings.json



// configure strongly typed settings objects
var appSettingsSection = Configuration.GetSection("AppSettings");
services.Configure<AppSettings>(appSettingsSection);


Además, para cada solicitud, se creará una copia de AppSettings para que se pueda llamar al posprocesamiento:



services.AddScoped(sp => sp.GetService<IOptionsSnapshot<AppSettings>>().Value);


Agreguemos un posprocesamiento de la clase AppSettings como servicio:



services.AddSingleton<IPostConfigureOptions<AppSettings>, ConfigureAppSettingsOptions>();


Código agregado a Startup.cs



public void ConfigureServices(IServiceCollection services)
{
  // configure strongly typed settings objects
  var appSettingsSection = Configuration.GetSection("AppSettings");
  services.Configure<AppSettings>(appSettingsSection);
  services.AddScoped(sp => sp.GetService<IOptionsSnapshot<AppSettings>>().Value);                                    
  services.AddSingleton<IPostConfigureOptions<AppSettings>, ConfigureAppSettingsOptions>();            
  //next
  services.AddControllersWithViews();
}


Para acceder a la configuración, bastará con inyectar AppSettings desde el controlador .



Archivo HomeController.cs :



public class HomeController : Controller
{
  private readonly ILogger<HomeController> _logger;
  private readonly AppSettings _appSettings;
  public HomeController(ILogger<HomeController> logger, AppSettings appSettings)
    {
      _logger = logger;
      _appSettings = appSettings;
    }


Cambiemos Index.cshtml para mostrar el parámetro Value del objeto lientConfig



@model AppSettings
@{
    ViewData["Title"] = "Home Page";
}
<div class="text-center">
    <h1>   ASP.NET Core</h1>
    <h4>    ( IOptions)</h4>
</div>
<div>
    <p>
         ClientConfig,  
         = @Model.clientConfig.Value
    </p>
</div>










Iniciemos la aplicación: Si durante el funcionamiento de la aplicación, cambiamos el parámetro Parameter1 a “NEW !!! Parámetro1 ABC "y Parámetro2 a" ¡¡¡NUEVO !!! Parameter2 ABC ", la próxima vez que acceda a la página inicial, se mostrará la nueva propiedad Value :







Salir



Este enfoque le permite deserializar todos los valores de configuración sin iterar manualmente sobre los parámetros. Cada solicitud http funciona con su propia instancia de AppSettings y lientConfig, lo que elimina la situación de colisiones. IPostConfigureOptions garantiza que se ejecute en último lugar cuando se hayan vuelto a leer todas las opciones. La desventaja de esta solución es la creación constante de una instancia de ClientConfig para cada solicitud, lo cual no es práctico porque de hecho, ClientConfig solo debe volver a crearse después de cambios en la configuración.



Deserialización de configuración con validación (sin usar IOptions)



GitHub: El enfoque de uso de ConfigurationTemplate_4



usando IPostConfigureOptions conduce a la creación del objeto ClientConfig cada vez que recibe una solicitud del cliente. Esto no es lo suficientemente racional porque cada solicitud funciona con un estado ClientConfig inicial, que cambia solo cuando se cambia el archivo de configuración appsettings.json. Para hacer esto, abandonaremos IPostConfigureOptions y crearemos un controlador de configuración al que se llamará solo cuando cambie appsettings.json, como resultado, ClientConfig se creará solo una vez, y luego se proporcionará la instancia ClientConfig ya creada para cada solicitud.



Crear una clase SingletonAppSettingsconfiguración (Singleton) a partir de la cual se creará una instancia de configuración para cada solicitud.



Archivo SingletonAppSettings.cs :



public class SingletonAppSettings
{
  public AppSettings appSettings;  
  private static readonly Lazy<SingletonAppSettings> lazy = new Lazy<SingletonAppSettings>(() => new SingletonAppSettings());
  private SingletonAppSettings()
    { }
  public static SingletonAppSettings Instance => lazy.Value;
}


Regresemos a la clase Startup y agreguemos una referencia a la interfaz IServiceCollection .

Se utilizará en el método de manejo de la configuración.



public IServiceCollection Services { get; set; }


Cambiemos ConfigureServices (servicios IServiceCollection) y pasemos una referencia a IServiceCollection .



Archivo Startup.cs :



public void ConfigureServices(IServiceCollection services)
{
  Services = services;
  //  AppSettings  
  var appSettings = Configuration.GetSection("AppSettings").Get<AppSettings>();
  appSettings.ClientConfigBuild();


Creemos una configuración Singleton y agréguela a la colección de servicios:



SingletonAppSettings singletonAppSettings = SingletonAppSettings.Instance;
singletonAppSettings.appSettings = appSettings;
services.AddSingleton(singletonAppSettings);     


Agreguemos el objeto AppSettings como Scoped, con cada solicitud se creará una copia del Singleton:



services.AddScoped(sp => sp.GetService<SingletonAppSettings>().appSettings);


ConfigureServices completamente (servicios IServiceCollection) :



public void ConfigureServices(IServiceCollection services)
{
  Services = services;
  //  AppSettings  
  var appSettings = Configuration.GetSection("AppSettings").Get<AppSettings>();
  appSettings.ClientConfigBuild();
  SingletonAppSettings singletonAppSettings = SingletonAppSettings.Instance;
  singletonAppSettings.appSettings = appSettings;
  services.AddSingleton(singletonAppSettings);             
  services.AddScoped(sp => sp.GetService<SingletonAppSettings>().appSettings);
  //next
  services.AddControllersWithViews();
}


Ahora agregue un controlador para la configuración en Configure (aplicación IApplicationBuilder, IWebHostEnvironment env) . Se usa un token para rastrear el cambio en el archivo appsettings.json. OnChange es la función que se llamará cuando cambie el archivo. Controlador de configuración OnChange () :



ChangeToken.OnChange(() => Configuration.GetReloadToken(), onChange);


Primero, leemos el archivo appsettings.json y deserializamos la clase AppSettings . Luego, de la colección de servicios, obtenemos una referencia al Singleton que almacena el objeto AppSettings y lo reemplazamos por uno nuevo.



private void onChange()
{                        
  var newAppSettings = Configuration.GetSection("AppSettings").Get<AppSettings>();
  newAppSettings.ClientConfigBuild();
  var serviceAppSettings = Services.BuildServiceProvider().GetService<SingletonAppSettings>();
  serviceAppSettings.appSettings = newAppSettings;
  Console.WriteLine($"AppSettings has been changed! {DateTime.Now}");
}


En el HomeController, inyectaremos un enlace a AppSettings, como en la versión anterior (ConfigurationTemplate_3)
HomeController.cs:



public class HomeController : Controller
{
  private readonly ILogger<HomeController> _logger;
  private readonly AppSettings _appSettings;
  public HomeController(ILogger<HomeController> logger, AppSettings appSettings)
    {
      _logger = logger;
      _appSettings = appSettings;
    }


Index.cshtml Value lientConfig:



@model AppSettings
@{
    ViewData["Title"] = "Home Page";
}
<div class="text-center">
    <h1>   ASP.NET Core</h1>
    <h4>    (  IOptions)</h4>
</div>
<div>
    <p>
         ClientConfig,  
        = @Model.clientConfig.Value
    </p>
</div>




Iniciemos







la aplicación: Habiendo seleccionado el modo de lanzamiento como una aplicación de consola, en la ventana de la aplicación puede ver un mensaje sobre la activación del evento de cambio del archivo de configuración:







Y los nuevos valores:







Salir



Esta opción es mejor que usar IPostConfigureOptions porque le permite construir un objeto solo después de cambiar el archivo de configuración, y no en cada solicitud. El resultado es una reducción del tiempo de respuesta del servidor. Una vez que se activa el token, se restablece el estado del token.



Agregar valores predeterminados y validar la configuración



GitHub: ConfigurationTemplate_5



En los ejemplos anteriores, si falta el archivo appsettings.json, la aplicación lanzará una excepción, así que hagamos que el archivo de configuración sea opcional y agreguemos la configuración predeterminada. Cuando publica una aplicación de proyecto creada a partir de una plantilla en Visula Studio, el archivo appsettings.json se ubicará en la misma carpeta junto con todos los binarios, lo cual es inconveniente cuando se implementa en Docker. El archivo appsettings.json se ha movido a config / :



.AddJsonFile("config/appsettings.json")


Para poder iniciar la aplicación sin appsettings.json, cambie el parámetro optiona l a true , que en este caso significa que la presencia de appsettings.json es opcional.



Archivo Startup.cs :



public Startup(IConfiguration configuration)
{
  var builder = new ConfigurationBuilder()
     .AddJsonFile("config/appsettings.json", optional: true, reloadOnChange: true);
  configuration = builder.Build();
  Configuration = configuration;
}


Agregue a ConfigureServices public void (servicios IServiceCollection) a la línea de deserialización de la configuración el caso de manejar la ausencia del archivo appsettings.json:



 var appSettings = Configuration.GetSection("AppSettings").Get<AppSettings>() ?? new AppSettings();


Agreguemos la validación de la configuración basada en la interfaz IValidatableObject . Si faltan parámetros de configuración, se utilizará el valor predeterminado. Heredemos la



clase AppSettings de IValidatableObject e implementemos el método:



public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)


Archivo AppSettings.cs :



public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
  List<ValidationResult> errors = new List<ValidationResult>();
  if (string.IsNullOrWhiteSpace(this.Parameter1))
    {
      errors.Add(new ValidationResult("   Parameter1.  " +
        "   DefaultParameter1 ABC"));
      this.Parameter1 = "DefaultParameter1 ABC";
    }
    if (string.IsNullOrWhiteSpace(this.Parameter2))
    {
      errors.Add(new ValidationResult("   Parameter2.  " +
        "   DefaultParameter2 ABC"));
      this.Parameter2 = "DefaultParameter2 ABC";
    }
    return errors;
}


Agregue un método para llamar a la verificación de configuración que se llamará desde el archivo Startup.cs de la clase de inicio :





private void ValidateAppSettings(AppSettings appSettings)
{
  var resultsValidation = new List<ValidationResult>();
  var context = new ValidationContext(appSettings);
  if (!Validator.TryValidateObject(appSettings, context, resultsValidation, true))
    {
      resultsValidation.ForEach(
        error => Console.WriteLine($" : {error.ErrorMessage}"));
      }
    }


Agreguemos una llamada al método de validación de la configuración en ConfigureServices (servicios IServiceCollection). Si no hay ningún archivo appsettings.json, debe inicializar el objeto AppSettings con los valores predeterminados.



Archivo Startup.cs :



var appSettings = Configuration.GetSection("AppSettings").Get<AppSettings>() ?? new AppSettings();


Comprobación de parámetros. Si se utiliza el valor predeterminado, se mostrará un mensaje que indica el parámetro en la consola.



 //Validate            
this.ValidateAppSettings(appSettings);            
appSettings.ClientConfigBuild();


Cambiemos la verificación de configuración en onChange ()



private void onChange()
{                        
  var newAppSettings = Configuration.GetSection("AppSettings").Get<AppSettings>() ?? new AppSettings();
  //Validate            
  this.ValidateAppSettings(newAppSettings);            
  newAppSettings.ClientConfigBuild();
  var serviceAppSettings = Services.BuildServiceProvider().GetService<SingletonAppSettings>();
  serviceAppSettings.appSettings = newAppSettings;
  Console.WriteLine($"AppSettings has been changed! {DateTime.Now}");
}


Si elimina la clave Parameter1 del archivo appsettings.json , luego de guardar el archivo, aparecerá un mensaje sobre la ausencia del parámetro en la ventana de la aplicación de la consola:







Salir



Cambiar la ruta para la ubicación de las configuraciones en la carpeta de configuración es una buena solución. le permite no mezclar todos los archivos en un montón. La carpeta de configuración está definida solo para almacenar archivos de configuración. Simplificó la tarea de implementar y configurar la aplicación para administradores mediante la validación de la configuración. Si agrega la salida de los errores de configuración al registro, el administrador, si se especifican parámetros incorrectos, recibirá información precisa sobre el problema, y ​​no como los programadores recientemente comenzaron a escribir en cualquier excepción: "Algo salió mal" .



No existe una opción ideal para trabajar con la configuración, todo depende de la tarea a realizar, cada opción tiene sus pros y sus contras.



Todas las plantillas de configuración están disponibles aquí .



Literatura:



  1. Corregir ASP.NET Core
  2. METANIT - Configuración. Conceptos básicos de configuración
  3. Patrón de diseño singleton C # .net core
  4. Recarga de la configuración en .NET core
  5. Recarga de opciones fuertemente tipadas en cambios de archivo en ASP.NET Core RC2
  6. Configuración de la aplicación ASP.NET Core a través de IOptions
  7. METANIT - Pasando la configuración a través de IOptions
  8. Configuración de la aplicación ASP.NET Core a través de IOptions
  9. METANIT - Autovalidación del modelo



All Articles