Diciembre de 2017

Volumen 32, número 12

Tecnología de vanguardia: configuración de aplicaciones de ASP.NET Core

Por Dino Esposito | Diciembre de 2017

Dino EspositoLa lógica de cualquier aplicación de software realista depende de algunos datos de configuración externos que, cuando se capturan, dirigen el comportamiento general de la aplicación. En términos generales, existen tres tipos de datos de configuración: datos que se capturan una vez y se usan en todas partes, datos que se capturan con frecuencia y se usan en todas partes, y datos que se capturan a petición justo antes de usarlos. La implementación de los dos últimos tipos de datos de configuración es, en gran parte, específica de la aplicación. El primer tipo, los datos que se capturan una sola vez para la duración de la aplicación, tienden a dirigir la compilación de una API de contenedor que oculta el almacén de datos real en la medida de lo posible.

En ASP.NET clásico, el archivo web.config era, entre muchas otras cosas, el repositorio favorito para los datos de configuración de toda la aplicación. En ASP.NET Core, ya no existe ningún archivo web.config, pero la API de configuración es más completa y flexible que nunca.

Proveedores de datos de configuración

La API de configuración se centra en el concepto del proveedor de datos. El proveedor de datos recupera los datos de un origen determinado y los expone en la aplicación en forma de pares nombre-valor. ASP.NET Core incluye algunos proveedores de configuración predefinidos capaces de leer archivos de texto (especialmente archivos JSON), variables de entorno y diccionarios en memoria. Los proveedores de configuración se conectan al sistema al iniciar la aplicación por medio de los servicios de la clase ConfigurationBuilder. Todos los proveedores vinculados al compilador contribuyen con sus propios pares nombre-valor y la colección resultante se expone como un objeto IConfigurationRoot.

Mientras que los datos siempre se proporcionan como un par nombre-valor, la estructura general del objeto de configuración resultante también podría ser jerárquica. Todo depende de la naturaleza y la estructura reales del valor asociado al nombre. Extraído del constructor de una clase de inicio, el siguiente fragmento de código muestra cómo compilar un árbol de configuración combinando dos proveedores de configuración diferentes:

// Env is a reference to the IHostingEnvironment instance
// that you might want to inject in the class via ctor
var dom = new ConfigurationBuilder()
  .SetBasePath(env.ContentRootPath)
  .AddJsonFile("MyAppSettings.json")
  .AddInMemoryCollection(new Dictionary<string, string> {{"Timezone", "+1"}})
  .Build();

El método de extensión AddJsonFile agrega propiedades nombre-valor de las propiedades almacenadas en el archivo JSON especificado. Observe que el archivo JSON se enumera a través de una ruta de acceso relativa. De hecho, el método SetBasePath, establece el directorio raíz donde el sistema empezará a buscar estos archivos de referencia. Todos los archivos JSON se pueden usar como un origen de datos de configuración. La decisión sobre la estructura de los archivos es cosa suya y puede incluir cualquier nivel de anidamiento. Se pueden vincular varios archivos JSON al mismo tiempo.

El método AddInMemoryCollection agrega el contenido del diccionario especificado. Observe que ambos tipos de clave y valor del diccionario deben ser de cadena. A primera vista, un método de extensión de este tipo podría parecer poco útil, ya que solo agrega datos estáticos que únicamente pueden establecerse en tiempo de compilación. No obstante, una colección en memoria sigue permitiendo aislar datos paramétricos, y el modelo de proveedor de la API de configuración de ASP.NET Core desacopla los datos del cuerpo principal de la aplicación y los inserta por usted al iniciar el sistema, de la siguiente manera:

new ConfigurationBuilder()
  .AddInMemoryCollection(
    new Dictionary<string, string> {{"Timezone", "+1"}})
  .Build();

Por ejemplo, en el fragmento de código anterior, un valor que representa la zona horaria que se va a usar se anexa al compilador de configuración, y el resto de la aplicación lo recibe a través de la interfaz unificada de la API de configuración. De esta manera, no tiene que cambiar nada excepto el proveedor y el patrón de almacenamiento real para leer la zona horaria (además de cualquier otro dato que inserte de la memoria) de otros orígenes de datos.

Por último, el método AddEnvironmentVariables agrega al árbol de configuración todas las variables de entorno definidas en la instancia de servidor. Observe que todas las variables de entorno definidas se agregan al árbol como un solo bloque. Si necesita realizar un filtrado, lo mejor es que opte por un proveedor en memoria y copie solo las variables seleccionadas en el diccionario.

Observe que en ASP.NET Core 2.0 también puede insertar la interfaz IConfiguration directamente en el constructor de la clase de inicio, y hacer que el árbol de configuración se configure automáticamente con las variables de entorno y el contenido de dos archivos JSON: appsettings.json y appsettings.development.json. Si quiere archivos JSON con un nombre diferente u otra lista de proveedores, solo tiene que compilar el árbol de configuración desde cero de la siguiente manera:

var dom = new ConfigurationBuilder()
  .SetBasePath(env.ContentRootPath)
  .AddJsonFile("MyAppSettings.json")
  .AddInMemoryCollection(new Dictionary<string, 
    string> {{"Timezone", "+1"}})
  .Build();

Proveedores de configuración personalizados

Además de usar proveedores predefinidos, también puede crear su propio proveedor de configuración. Para crear un proveedor de configuración personalizado, debe iniciar una clase de origen de configuración de contenedor (una clase sin formato que implementa IConfigurationSource). Aquí se muestra un ejemplo:

public class MyDatabaseConfigSource : IConfigurationSource
{
  public IConfigurationProvider Build(IConfigurationBuilder builder)
  {
    return new MyDatabaseConfigProvider();
  }
}

En la implementación del método Build, como se muestra en el fragmento de código anterior, termina haciendo referencia al proveedor real, especialmente una clase que se hereda de la clase Configuration­Provider definida por el sistema (consulte la Figura 1).

Figura 1 Proveedor de configuración controlada por base de datos de ejemplo

public class MyDatabaseConfigProvider : ConfigurationProvider
{
  private const string ConnectionString = "...";
  public override void Load()
  {
    using (var db = new MyDatabaseContext(ConnectionString))
    {
      db.Database.EnsureCreated();
      Data = !db.Values.Any()
        ? GetDefaultValues(db)
        : db.Values.ToDictionary(c => c.Id, c => c.Value);
    }
  }
  private IDictionary<string, string> GetDefaultValues (MyDatabaseContext db)
  {
    // Pseudo code for determining default values to use
    var values = DetermineDefaultValues();
    // Save default values to the store
    // NOTE: Values is a DbSet<T> in the DbContext being used
    db.Values.AddRange(values);
    db.SaveChanges();
    // Return configuration values
    return values;
  }
}

Un ejemplo común es un proveedor de configuración que lee datos de una tabla de base de datos ad hoc. El proveedor podría, en última instancia, ocultar el esquema de la tabla y el diseño de la base de datos implicada. Por ejemplo, la cadena de conexión podría ser un valor constante privado. Probablemente, este proveedor de configuración usaría Entity Framework (EF) Core para realizar tareas de acceso a datos y, por tanto, debe tener a su disposición una clase DbContext dedicada y un conjunto de clases de entidad dedicado para capturar valores que más tarde se convertirán en una cadena o un diccionario de cadenas. Como toque distintivo, quizás quiera definir valores predeterminados para cualquiera de los valores previstos que se encontrarán y que rellenarán la base de datos, si está vacía.

El proveedor controlado por base de datos que hemos explicado aquí está cerrado en torno a una tabla de base de datos conocida. No obstante, si encuentra un modo de pasar un objeto DbContextOptions como un argumento al proveedor, puede ingeniárselas para trabajar con un proveedor basado en EF genérico. Se puede encontrar un ejemplo de esta técnica en bit.ly/2uQBJmK.

Compilación del árbol de configuración

El árbol de configuración resultante (personalmente, me gusta llamarlo modelo de objetos de documento de configuración) se compila generalmente en el constructor de la clase de inicio. La salida que genera el método Build de la clase ConfigurationBuilder es un objeto de tipo IConfigurationRoot. La clase de inicio proporcionará un miembro para guardar la instancia para su uso posterior en toda la pila de aplicaciones, como se muestra a continuación:

public class Startup
{
  public IConfigurationRoot Configuration { get; }
  public Startup(IHostingEnvironment env)
  {
    var dom = new ConfigurationBuilder()
      .SetBasePath(env.ContentRootPath)
      .AddJsonFile("MyAppSettings.json")
      .Build();
     Configuration = dom;
  }
}

El objeto IConfigurationRoot es el punto de conexión para que los componentes de la aplicación lean los valores individuales del árbol de configuración.

Lectura de datos de configuración

Para leer datos de configuración mediante programación, debe usar el método GetSection en el objeto IConfigurationRoot. El valor se lee como una cadena sin formato. Para identificar el valor exacto que quiere leer, debe proporcionar una cadena de ruta de acceso donde el signo de dos puntos (:) se usa para delimitar las propiedades en un esquema jerárquico. Supongamos que su proyecto de ASP.NET Core incluye un archivo JSON como el de la Figura 2.

Figura 2 Archivo JSON de ejemplo

{
  "ApplicationTitle" : "Programming ASP.NET Core",
  "GeneralSettings" : {
    "CopyrightYears" : [2017, 2018],
    "Paging" : {
      "PageSize" : 20,
      "FreezeHeaders" : true
    },
    "Sorting" : {
      "Enabled" : true
    }
  }
}

Para leer la configuración, puede proceder de muchas maneras diferentes, pero es indispensable que sepa cómo navegar al valor real del árbol de configuración. Por ejemplo, para localizar el lugar donde está almacenado el tamaño predeterminado de la página, la ruta de acceso es la siguiente:

Generalsettings:Paging:PageSize

Observe que una cadena de ruta de acceso nunca distingue entre mayúsculas y minúsculas. Teniendo en cuenta esto, la manera más simple de leer la configuración es la API del indizador, como se muestra a continuación:

var pageSize = Configuration["generalsettings:paging:pagesize"];

Es importante tener en cuenta que la API del indizador devuelve el valor de configuración como una cadena, pero también puede usar una API fuertemente tipada alternativa. Este enfoque presenta el aspecto siguiente:

var pathString = "generalsettings:paging:pagesize";
var pageSize = Configuration.GetValue<int>(pathString);

La API de lectura es independiente del origen de datos real. Para leer contenido jerárquico de un archivo JSON se usa la misma API que para leer contenido plano de diccionarios en memoria.

Además de leer directamente, puede aprovechar la API de posicionamiento, que moverá conceptualmente el cursor de lectura en un subárbol de configuración específico. El método GetSection le permite seleccionar todo un subárbol de configuración, en el que puede actuar mediante la API del indizador y la API fuertemente tipada. El método GetSection es una herramienta de consulta genérica para el árbol de configuración y no es específica de los archivos JSON solamente. A continuación se muestra un ejemplo:

var pageSize = Configuration.GetSection("Paging").GetValue<int>("PageSize");

Observe que también tiene a su disposición un método GetValue y la propiedad Value para la lectura. Ambos devolverían el valor de configuración como una cadena sin formato.

Actualización de la configuración cargada

En ASP.NET Core, la API de configuración está diseñada para ser de solo lectura. Esto solo significa que no puede reescribir en el origen de datos configurado mediante una API oficial. Si tiene una manera de editar el contenido del origen de datos (es decir, sobrescrituras programáticas de archivos de texto, actualizaciones de bases de datos y similares), el sistema le permite recargar el árbol de configuración mediante programación.

Para recargar un árbol de configuración, solo tiene que llamar al método Reload en el objeto raíz de configuración.

Configuration.Reload();

Por lo general, es posible que desee usar este código desde una página de administración, donde se ofrece a los usuarios un formulario para actualizar la configuración almacenada. En lo que concierne a los archivos JSON, también puede habilitar las recargas automáticas del contenido al producirse cambios. Solo tiene que agregar un parámetro adicional al método AddJsonFile, como se indica a continuación:

var dom = new ConfigurationBuilder()
  .AddJsonFile("MyAppSettings.json", optional: true, reloadOnChange: true);

Los archivos JSON son los más populares de los formatos de archivo de texto que ASP.NET Core admite de forma nativa. También puede cargar la configuración de archivos XML e .ini. Solo tiene que agregar una llamada a los métodos AddXmlFile y AddIniFile con la misma firma sofisticada del método AddJsonFile.

Observe que, en ASP.NET Core 2, la configuración también se puede administrar directamente desde el archivo program.cs, como se muestra a continuación:

return new WebHostBuilder()
  .UseKestrel()
  .UseContentRoot(Directory.GetCurrentDirectory())
  .ConfigureAppConfiguration((builderContext, config) =>
  {
    var env = builderContext.HostingEnvironment;
    config.AddJsonFile("appsettings.json")
  });

Si lo hace, luego puede insertar el árbol de configuración en la clase de inicio a través de la interfaz IConfiguration en el constructor.

Circulación de datos de configuración

El objeto raíz de configuración reside en el ámbito de la clase de inicio, pero su contenido debería estar disponible en toda la aplicación. La manera natural de conseguir este objetivo en ASP.NET Core es por medio de la inserción de dependencias (DI). Para compartir el objeto raíz de configuración con el sistema, solo tiene que enlazarlo al sistema de inserción de dependencias como un singleton.

public void ConfigureServices(IServiceCollection services)
{
  services.AddSingleton(Configuration);
  ...
}

Posteriormente, puede insertar una referencia IConfigurationRoot en cualquier constructor de controlador y cualquier vista de Razor. A continuación se incluye un ejemplo de una vista:

@inject IConfigurationRoot Configuration
CURRENT PAGE SIZE IS @Configuration["GeneralSettings:Paging:PageSize"]

Aunque es posible insertar el objeto raíz de configuración, y en gran medida también es fácil, sigue generando una API bastante compleja si el acceso a la configuración es muy frecuente. Por eso, la API de configuración de ASP.NET Core ofrece un mecanismo de serialización que le permite guardar el árbol de configuración, total o parcialmente, en una clase POCO independiente.

Serialización en clases POCO

Si alguna vez ha realizado alguna tarea de programación de ASP.NET MVC, debería estar familiarizado con el concepto de enlace de modelos. Un patrón similar, denominado Opciones, se puede usar en ASP.NET Core para este fin, como se muestra a continuación:

public void ConfigureServices(IServiceCollection services)
{
  // Initializes the Options subsystem
  services.AddOptions();
  // Maps the PAGING section to a distinct dedicated POCO class
  services.Configure<PagingOptions>(
    Configuration.GetSection("generalsettings:paging"));
}

En este ejemplo, primero debe inicializar la capa de enlace de configuración, como se muestra en el fragmento de código anterior, y luego solicitar explícitamente que se intente enlazar el contenido del subárbol especificado a las propiedades públicas de la clase determinada, de la siguiente manera:

public class PagingOptions
{
  public int PageSize { get; set; }
  public bool FreezeHeaders { get; set; }
}

La clase enlazada debe ser una clase POCO con las propiedades de captador o establecedor públicas y un constructor predeterminado. El método Configure intentará capturar y copiar los valores de una sección especificada del árbol de configuración directamente en una instancia recién creada de la clase. No hace falta decir que el enlace generará un error de manera silenciosa si los valores no se pueden convertir en tipos declarados.

Una clase POCO se puede pasar a toda la pila de aplicaciones mediante el sistema de inserción de dependencias integrado. No tiene que hacer nada para que esto suceda. O más bien, cualquier configuración necesaria se aplica al invocar AddOptions. Solo queda un paso para el acceso mediante programación a los datos de configuración serializados en una clase:

public PagingOptions PagingOptions { get; }
public CustomerController(IOptions<PagingOptions> config)
{
  PagingOptions = config.Value;
}

Si usa el patrón Options de manera extensiva en todos sus controladores, debería considerar mover la propiedad de opciones (es decir, Paging­Options) a una clase base y, a continuación, heredar de esta sus clases de controlador. Del mismo modo, puede insertar IOptions<T> en cualquier vista de Razor.

Resumen

En un entorno ASP.NET MVC clásico, el procedimiento recomendado para el tratamiento de datos de configuración exige que cargue todos sus datos una vez al iniciar el sistema en un objeto de contenedor global. A continuación, se puede acceder al objeto global desde métodos de controlador y su contenido se puede insertar como un argumento en clases de back-end, como repositorios, e incluso en vistas. En un entorno ASP.NET MVC clásico, usted asume el costo total de asignación de datos imprecisos basados en cadenas en propiedades fuertemente tipadas del contenedor global.

En ASP.NET Core, tiene una API de bajo nivel para la lectura de valores individuales de manera mucho más precisa que con la API de ConfigurationManager del entorno ASP.NET antiguo, y un método automático para serializar contenido externo en clases POCO de su diseño. Esto es posible porque la introducción del árbol de configuración (rellenado con una lista de proveedores) desacopla la configuración del cuerpo principal de la aplicación.


Dino Esposito es el autor de "Microsoft .NET: Architecting Applications for the Enterprise" (Microsoft Press, 2014) y "Programming ASP.NET Core" (Microsoft Press, 2018). Autor de Pluralsight y defensor de los desarrolladores en JetBrains, Esposito comparte su visión del software en Twitter: @despos.

Gracias al siguiente experto técnico de Microsoft por revisar este artículo: Ugo Lattanzi
Ugo es un programador especializado en el desarrollo de aplicaciones empresariales que usa herramientas y lenguajes diferentes. Su trabajo se centra en aplicaciones web y orientadas a servicios, así como en entornos donde la escalabilidad es una de las principales prioridades. Hace nueve años que Ugo es MVP de Microsoft.


Discuta sobre este artículo en el foro de MSDN Magazine