Una de las debilidades de una aplicación Typescript, que no deja de ser una aplicación Javascript es como mantener la configuración de la aplicación.

Un sistema configurable debería cumplir los siguientes requisitos:

  1. Que la configuración pueda ser modificada en runtime.
  2. Que la configuración esté centralizada, es decir que no haya múltiples archivos de configuración que mantienen las mismas settings.
  3. Que los cambios realizados sean reconocidos por el sistema sin que sea necesario volver a compilar.
  4. Que los cambios puedan ser aplicados sin necesidad de un despliegue de todo o parte del sistema.

Algunos de estos requisitos ya los cumple Asp.NET Core, sobre todo en la versión 2.1. Y otros pueden ser solventados fácilmente desde el lado del servidor, que es lo que vamos a ver en este artículo.

Existen otras alternativas posibles, desde un clase estática con las settings «hard coded» (esto nos obligaría a compilar), hasta utilizar un fichero JSON que podemos modificar y que es leído en runtime (aunque entonces podríamos tener más de un archivo que mantener, si sumamos el archivo de configuración del lado servidor).

También con un fichero JSON estático siempre nos encontraríamos con algún problemilla, como el cache del navegador u otros inconvenientes, que impedirían que pudiéramos cumplir con los requisitos de una configuración fácilmente mantenible.

En este artículo, propongo un solución muy interesante y muy fácil de implementar, aplicable a aplicaciones Asp.NET Core combinadas con aplicaciones cliente escritas con Typescript o Javascript.

A continuación, vamos a ver paso a paso como implementar esta alternativa.

1. En primer lugar: tenemos que definir, en el fichero de configuración “appSettings.json”, las settings que queremos cargar en nuestra aplicación cliente. El siguiente ejemplo de código muestra algunas  “settings” que utilizaremos en el resto de los pasos.

"AppSettings": {
  "ProjectsApiUrl": "",
  "UsersApiUrl": "",
  "EditorSettings": {
    "DefProjectName": "New Project",
    "DefProjectTitle": "Nuevo proyecto",
    "DefUsername": "Anonymous",
    "ZoomSliderMinvalue": "0.1",
    "ZoomSliderMaxValue": "5",
    "ZoomSliderInitialValue": "1.0",
    "ZoomSliderStepValue": "0.1",
    "ScaleIncrementStepValue": "0.2",
    "InitialCanvasWidth": "2000",
    "InitialCanvasHeight": "2000",
    "EnabledDateCalculations": "false",
    "ReleaseVersion": "0.7.4",
    "SelectionLayersLimit": "2"
   }
}

2. En segundo lugar: tenemos que definir la clase sobre la que mapear las “settings” que se inyectarán desde el lado servidor. Lógicamente esta clase debe tener una propiedad pública por cada “setting” a mapear y exactamente el mismo nombre:

export class EditorSettings {
    ProjectName: string;
    ProjectTitle: string;
    Username: string;
    ZoomSliderMinvalue: number;
    ZoomSliderMaxValue: number;
    ZoomSliderInitialValue: number;
    ZoomSliderStepValue: number;
    ScaleIncrementStepValue: number;
    InitialCanvasWidth: number;
    InitialCanvasHeight: number;
    EnabledDateCalculations: boolean;
    ReleaseVersion: number;
    SelectionLayersLimit: number;

}

3. En tercer lugar: configuramos, en el “Controller” correspondiente, el «Action«, que lanza nuestra aplicación cliente, para que reciba inyectado el objeto «Configuration«. Nos valdremos de la DI nativa  (Dependency Injection) que nos proporciona el «Model Binding System» de Asp.NET Core usando el atributo «[FromServices]«:

public class HomeController : Controller
{
    public IActionResult Index([FromServices] IConfiguration configuration)
    {
        return View();
    }
}

Esto nos va a permitir utilizar después, en la vista que realiza la carga y ejecución de nuestra aplicación cliente (en este caso es la vista “_layout.html”), la directiva “@inject” para inyectar en la vista el objeto “Configuration”, que ha sido inyectado así mismo en el lado servidor como hemos visto  en el código anterior.

4. La directiva “@inject”  inyecta en la vista el objeto “Configuration”, lo que nos permite leer el archivo de configuración “appSettings.json” desde la propia vista. A continuación, aprovechamos que ya tenemos acceso a las settings, para construir un objeto JSON con las settings,  intentando respetar (recomendable) el grafo jerárquico que tienen en el archivo de configuración “appSettings.json”. Recordemos que dentro de la sección “AppSettings” teníamos otra sección “EditorSettings” que contenía a su vez más settings:

@using Microsoft.Extensions.Configuration;
@inject IConfiguration config;
<script type="text/javascript">
    const appSettings = {
        ProjectsApiUrl: "@config["AppSettings:ProjectsApiUrl"]",
        UsersApiUrl: "@config["AppSettings:UsersApiUrl"]",
        EditorSettings: {
            @foreach(var setting in config.GetSection("AppSettings:EditorSettings").GetChildren()) {
            @Html.Raw("\"" + setting.Key + "\":\"" + setting.Value + "\",");
         }
        }
    }
    //Bioreg App EntryPoint.
    $(Start(appSettings));
</script>

5. Una vez que ya tenemos un objeto JSON, con nuestras settings, en el lado cliente, sólo tenemos que mapearlo o mejor dicho deserializarlo sobre la clase “EditorSettings” que habíamos preparado en el paso 2. En el siguiente código, vemos como se hace utilizando la función “JSON.parse” que forma parte de la implementación nativa de Javascript en la mayoría de navegadores.

Start(appSettings: any): void {
    //(1) Load application settings.
    this.LoadSettings(appSettings);
    ...
}
//Load appsetings provide by the server into typed settings at runtime.
LoadSettings(appSettings: any) {
    this.Settings = JSON.parse(JSON.stringify(appSettings),
        function (k, v) {
            if (!(typeof v === "object" || isNaN(v))) {
                return parseFloat(v);
            } else {
                switch(v) {
                    case "true":
                        return true;
                    case "false":
                        return false;
                    default:
                        return v;
                }
             
            }
        }
    );
    
}

La función “JSON.parse” no puede realizar por si misma la conversión de los valores numéricos o booleanos desde un formato “string”. Pero, sí permite pasar como segundo parámetro una función que realice esta conversión. Observando el código anterior en el depurador de Chrome podemos ver que los valores con validación numérica se han convertido a valores numéricos  y los valores “true/false” se han convertido a sus correspondientes valores booleanos:

image

Y por último un ejemplo de código que utiliza nuestras settings inyectadas y dentro de lo que cabe más o menos tipadas. En el ejemplo utilizado para extraer las muestras de código, hay una clase “Runtime” que contiene la propiedad “Settings” sobre la que se deserializa el objeto JSON inyectado y que es accesible desde la instancia “AppRuntime”:

GetProjects(): Projects.Project[] {
    try {
        let projects = new Array<Projects.Project>();
        fetch(AppRuntime.Settings.ProjectsApiUrl + "api/projects") 
            .then(function (response) {
                ...
            })
            .catch(function (error) {
                ...
            })
    }
    catch{
        return null;
    }
}
...
const zoomSlider: any = {
    ...
    value: AppRuntime.Settings.EditorSettings.ZoomSliderInitialValue,
    step: AppRuntime.Settings.EditorSettings.ZoomSliderStepValue,
    min: AppRuntime.Settings.EditorSettings.ZoomSliderMinvalue,
    max: AppRuntime.Settings.EditorSettings.ZoomSliderMaxValue,
    ...
}

Esta es, a mi juicio, una más que interesante alternativa de configuración para aplicaciones Typescript o Javascript en el lado cliente. Permite centralizar el sitio en el que se encuentra la configuración tanto de nuestra aplicación servidora como de nuestra aplicación cliente y permite utilizar esta configuración de forma tipada, si no de forma estricta si por lo menos en cuanto al acceso usando notación de objetos. Eso sí, hay que tener mucho cuidado de no incluir información sensible en las settings que vamos a inyectar al lado cliente, porque serán visibles mediante las herramientas de depuración del navegador.

Espero que esta solución os guste tanto como a mí.