Cómo funciona la configuración en .NET Core

Dejemos de hablar sobre DDD y la reflexión por un tiempo. Propongo hablar sobre lo simple, sobre la organización de la configuración de la aplicación.


Después de que mis colegas y yo decidimos cambiarnos a .NET Core, surgió la pregunta de cómo organizar los archivos de configuración, cómo realizar transformaciones, etc. en el nuevo entorno. El siguiente código se encuentra en muchos ejemplos, y muchos lo han utilizado con éxito.


public IConfiguration Configuration { get; set; } public IHostingEnvironment Environment { get; set; } public Startup(IConfiguration configuration, IHostingEnvironment environment) {   Environment = environment;   Configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .AddJsonFile($"appsettings.{Environment.EnvironmentName}.json") .Build(); } 

Pero veamos cómo funciona la configuración, y en qué casos usar este enfoque, y en qué confiar en los desarrolladores de .NET Core. Pido gato.


Como era antes


Como cualquier historia, este artículo tiene un comienzo. Uno de los primeros problemas después de cambiar a ASP.NET Core fue la transformación de los archivos de configuración.


Recuerde cómo era antes con web.config


La configuración consistió en varios archivos. El archivo principal era web.config , y las transformaciones ( web.Development.config , etc.) ya se le aplicaron, según la configuración del ensamblado. Al mismo tiempo, los atributos xml se utilizaron activamente para buscar y transformar la sección del documento xml .


Pero como sabemos en ASP.NET Core, el archivo web.config ha sido reemplazado por appsettings.json y ya no existe el mecanismo de transformación habitual.


¿Qué nos dice google?

El resultado de búsqueda para "Transformar en ASP.NET Core" en Google resultó en el siguiente código:


 public IConfiguration Configuration { get; set; } public IHostingEnvironment Environment { get; set; } public Startup(IConfiguration configuration, IHostingEnvironment environment) {  Environment = environment;  Configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .AddJsonFile($"appsettings.{Environment.EnvironmentName}.json") .Build(); } 

En el constructor de la clase Startup , creamos un objeto de configuración usando ConfigurationBuilder . En este caso, indicamos explícitamente qué fuentes de configuración queremos usar.


Y tal:


 public IConfiguration Configuration { get; set; } public IHostingEnvironment Environment { get; set; } public Startup(IConfiguration configuration, IHostingEnvironment environment) {  Environment = environment;  Configuration = new ConfigurationBuilder() .AddJsonFile($"appsettings.{Environment.EnvironmentName}.json") .Build(); } 

Dependiendo de la variable de entorno, se selecciona una u otra fuente de configuración.


Estas respuestas a menudo se encuentran en SO y otros recursos menos populares. Pero el sentimiento no se fue. que nos vamos mal ¿Qué sucede si quiero usar variables de entorno o argumentos de línea de comando en la configuración? ¿Por qué necesito escribir este código en cada proyecto?


En busca de la verdad, tuve que profundizar en la documentación y el código fuente. Y quiero compartir el conocimiento adquirido en este artículo.


Veamos cómo funciona la configuración en .NET Core.


Configuracion


La configuración en .NET Core está representada por un objeto de interfaz IConfiguration .


 public interface IConfiguration {  string this[string key] { get; set; }  IConfigurationSection GetSection(string key);  IEnumerable<IConfigurationSection> GetChildren();  IChangeToken GetReloadToken(); } 

  • indexador [clave de cadena] , que permite obtener el valor del parámetro de configuración por clave
  • GetSection (cadena clave) devuelve la sección de configuración que corresponde a la tecla clave
  • GetChildren () devuelve un conjunto de subsecciones de la sección de configuración actual
  • GetReloadToken () devuelve una instancia de IChangeToken que se puede usar para recibir notificaciones cuando los cambios de configuración

Una configuración es una colección de pares clave-valor. Al leer desde una fuente de configuración (archivo, variables de entorno), los datos jerárquicos se reducen a una estructura plana. Por ejemplo, json es un objeto de la forma


 { "Settings": { "Key": "I am options" } } 

se reducirá a una vista plana:


 Settings:Key = I am options 

Aquí, la clave es Configuración: Clave , y el valor es I am options .
Los proveedores de configuración se utilizan para completar la configuración.


Proveedores de configuración


Un objeto de interfaz es responsable de leer los datos de la fuente de configuración.
IConfigurationProvider :


 public interface IConfigurationProvider {  bool TryGet(string key, out string value);  void Set(string key, string value);  IChangeToken GetReloadToken();  void Load();  IEnumerable<string> GetChildKeys(IEnumerable<string> earlierKeys, string parentPath); } 

  • TryGet (clave de cadena, valor de cadena de salida) permite obtener el valor del parámetro de configuración por clave
  • Set (clave de cadena, valor de cadena) se utiliza para establecer el valor del parámetro de configuración
  • GetReloadToken () devuelve una instancia de IChangeToken que se puede usar para recibir notificaciones cuando cambia una fuente de configuración
  • Método Load () que se encarga de leer la fuente de configuración
  • GetChildKeys (IEnumerable <string> previousKeys, string parentPath) le permite obtener una lista de todas las claves que proporciona este proveedor de configuración

Los siguientes proveedores están disponibles en la caja:


  • Json
  • Ini
  • Xml
  • Variables de entorno
  • Memoria
  • Azur
  • Proveedor de configuración personalizada

Se aceptan las siguientes convenciones para usar proveedores de configuración.


  1. Las fuentes de configuración se leen en el orden en que se especificaron.
  2. Si las mismas claves están presentes en diferentes fuentes de configuración (la comparación no distingue entre mayúsculas y minúsculas), entonces se utiliza el último valor agregado.

Si creamos una instancia de un servidor web utilizando CreateDefaultBuilder , los siguientes proveedores de configuración están conectados de forma predeterminada:



  • ChainedConfigurationProvider a través de este proveedor puede obtener valores y claves de configuración que otros proveedores de configuración han agregado
  • JsonConfigurationProvider utiliza archivos json como fuente de configuración. Como puede ver, se agregan tres proveedores de este tipo a la lista de proveedores. El primero usa appsettings.json como fuente, el segundo usa appsettings. {Environment} .json . El tercero lee datos de secrets.json . Si crea la aplicación en la configuración de lanzamiento , el tercer proveedor no estará conectado, ya que no se recomienda usar secretos en el entorno de producción
  • EnvironmentVariablesConfigurationProvider recupera los parámetros de configuración de las variables de entorno
  • CommandLineConfigurationProvider permite agregar argumentos de línea de comando a la configuración

Dado que la configuración se almacena como un diccionario, es necesario garantizar la unicidad de las claves. Por defecto, esto funciona así.


Si el proveedor CommandLineConfigurationProvider tiene un elemento con la clave clave y el proveedor JsonConfigurationProvider tiene un elemento con la clave clave, el elemento de JsonConfigurationProvider será reemplazado por el elemento de CommandLineConfigurationProvider, ya que está registrado en último lugar y tiene una prioridad más alta.


Recordemos un ejemplo desde el principio del artículo.
 public IConfiguration Configuration { get; set; } public IHostingEnvironment Environment { get; set; } public Startup(IConfiguration configuration, IHostingEnvironment environment) {  Environment = environment;  Configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .AddJsonFile($"appsettings.{Environment.EnvironmentName}.json") .Build(); } 

No necesitamos crear IConfiguration nosotros mismos para transformar los archivos de configuración, ya que esto está habilitado de forma predeterminada. Este enfoque es necesario cuando queremos limitar el número de fuentes de configuración.


Proveedor de configuración personalizada


Para escribir su proveedor de configuración, debe implementar las interfaces IConfigurationProvider e IConfigurationSource . IConfigurationSource es una nueva interfaz que aún no hemos considerado en este artículo.


 public interface IConfigurationSource { IConfigurationProvider Build(IConfigurationBuilder builder); } 

La interfaz consta de un único método de compilación que toma IConfigurationBuilder como parámetro y devuelve una nueva instancia de IConfigurationProvider .


Para implementar nuestros proveedores de configuración, las clases abstractas ConfigurationProvider y FileConfigurationProvider están disponibles para nosotros. En estas clases, la lógica de los métodos TryGet , Set , GetReloadToken , GetChildKeys ya está implementada y queda por implementar solo el método Load .


Veamos un ejemplo. Es necesario implementar la lectura de la configuración del archivo yaml , y también es necesario que podamos cambiar la configuración sin reiniciar nuestra aplicación.


Cree la clase YamlConfigurationProvider y conviértala en heredera de FileConfigurationProvider .


 public class YamlConfigurationProvider : FileConfigurationProvider { private readonly string _filePath; public YamlConfigurationProvider(FileConfigurationSource source) : base(source) { } public override void Load(Stream stream) { throw new NotImplementedException(); } } 

En el fragmento de código anterior, puede observar algunas características de la clase FileConfigurationProvider . El constructor acepta una instancia de FileConfigurationSource , que contiene IFileProvider . IFileProvider se utiliza para leer un archivo y suscribirse a un evento de cambio de archivo. También puede observar que el método Load acepta una secuencia en la que el archivo de configuración está abierto para lectura. Este es un método de la clase FileConfigurationProvider y no está en la interfaz IConfigurationProvider .


Agregue una implementación simple que nos permita leer el archivo yaml . Para leer el archivo, usaré el paquete YamlDotNet .


Implementación YamlConfigurationProvider
  public class YamlConfigurationProvider : FileConfigurationProvider { private readonly string _filePath; public YamlConfigurationProvider(FileConfigurationSource source) : base(source) { } public override void Load(Stream stream) { if (stream.CanSeek) { stream.Seek(0L, SeekOrigin.Begin); using (StreamReader streamReader = new StreamReader(stream)) { var fileContent = streamReader.ReadToEnd(); var yamlObject = new DeserializerBuilder() .Build() .Deserialize(new StringReader(fileContent)) as IDictionary<object, object>; Data = new Dictionary<string, string>(); foreach (var pair in yamlObject) { FillData(String.Empty, pair); } } } } private void FillData(string prefix, KeyValuePair<object, object> pair) { var key = String.IsNullOrEmpty(prefix) ? pair.Key.ToString() : $"{prefix}:{pair.Key}"; switch (pair.Value) { case string value: Data.Add(key, value); break; case IDictionary<object, object> section: { foreach (var sectionPair in section) FillData(pair.Key.ToString(), sectionPair); break; } } } } 

Para crear una instancia de nuestro proveedor de configuración, debe implementar FileConfigurationSource .


Implementación de YamlConfigurationSource
 public class YamlConfigurationSource : FileConfigurationSource { public YamlConfigurationSource(string fileName) { Path = fileName; ReloadOnChange = true; } public override IConfigurationProvider Build(IConfigurationBuilder builder) { this.EnsureDefaults(builder); return new YamlConfigurationProvider(this); } } 

Es importante tener en cuenta aquí que para inicializar las propiedades de la clase base, debe llamar al método this.EnsureDefaults (constructor) .


Para registrar un proveedor de configuración personalizado en la aplicación, debe agregar la instancia del proveedor a IConfigurationBuilder . Puede llamar al método Add desde IConfigurationBuilder , pero inmediatamente sacaré la lógica de inicialización YamlConfigurationProvider en el método de extensión .


Implemente YamlConfigurationExtensions
 public static class YamlConfigurationExtensions { public static IConfigurationBuilder AddYaml( this IConfigurationBuilder builder, string filePath) { if (builder == null) throw new ArgumentNullException(nameof(builder)); if (string.IsNullOrEmpty(filePath)) throw new ArgumentNullException(nameof(filePath)); return builder .Add(new YamlConfigurationSource(filePath)); } } 

Llame al método AddYaml
 public class Program { public static void Main(string[] args) { CreateWebHostBuilder(args).Build().Run(); } public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .ConfigureAppConfiguration((context, builder) => { builder.AddYaml("appsettings.yaml"); }) .UseStartup<Startup>(); } 

Seguimiento de cambios


En la nueva configuración de la API , se hizo posible volver a leer la fuente de configuración cuando se cambia. Al mismo tiempo, la aplicación no se reinicia.
Cómo funciona


  • El proveedor de configuración supervisa los cambios en la fuente de configuración
  • Si se produce un cambio de configuración, se crea un nuevo IChangeToken.
  • Cuando se cambia IChangeToken , se llama una recarga de configuración

Veamos cómo se implementa el seguimiento de cambios en FileConfigurationProvider .


 ChangeToken.OnChange( //producer () => Source.FileProvider.Watch(Source.Path), //consumer () => { Thread.Sleep(Source.ReloadDelay); Load(reload: true); }); 

Se pasan dos parámetros al método OnChange de la clase estática ChangeToken . El primer parámetro es una función que devuelve un nuevo IChangeToken cuando cambia la fuente de configuración (en este caso el archivo), este es el llamado productor . El segundo parámetro es la función de devolución de llamada (o consumidor ), que se llamará cuando se cambie la fuente de configuración.
Obtenga más información sobre la clase ChangeToken .


No todos los proveedores de configuración implementan el seguimiento de cambios. Este mecanismo está disponible para los descendientes de FileConfigurationProvider y AzureKeyVaultConfigurationProvider .


Conclusión


En .NET Core, tenemos un mecanismo fácil y conveniente para administrar la configuración de la aplicación. Muchos complementos están disponibles de fábrica, muchas cosas se usan de forma predeterminada.
Por supuesto, cada persona decide de qué manera usarlo, pero estoy de acuerdo con el hecho de que las personas conocen sus herramientas.


Este artículo solo cubre los conceptos básicos. Además de lo básico, IOptions, scripts posteriores a la configuración, validación de configuraciones y mucho más están disponibles para nosotros. Pero esa es otra historia.


Puede encontrar el proyecto de aplicación con ejemplos de este artículo en el repositorio de Github .
Comparta en los comentarios quién usa qué enfoques de gestión de configuración.
Gracias por su atencion


upd .: como AdAbsurdum sugirió correctamente, cuando se trabaja con matrices, los elementos no siempre se reemplazarán al fusionar una configuración de dos fuentes.
Considera un ejemplo. Al leer una matriz de appsettings.json obtenemos esta vista plana:


 array:0=valueA 

Al leer desde appsettings.Development.json :


 array:0=valueB array:1=value 

Como resultado, la configuración será:


 array:0=valueB array:1=value 

Todos los elementos con índices únicos ( matriz: 1 en el ejemplo) se agregarán a la matriz resultante. Los elementos de diferentes fuentes de configuración, pero que tengan el mismo índice ( matriz: 0 en el ejemplo) se fusionarán, y se utilizará el elemento que se agregó por última vez.

Source: https://habr.com/ru/post/453416/


All Articles