Saltar a contenido

Andreani.ARQ.WebHost

Resumen

Esta librería tiene el objetivo de contener las configuraciones estándar e iniciales para cualquier proyecto .Net que requiera un Host.

Configuración WebHost

// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.ConfigureAndreaniWebHost(args);

el método ConfigureAndreaniWebHost realiza las siguientes configuraciones:

  1. Configuration File: configura el host para recibir las configuraciones por archivo appsettings.json o appsettings.yml
  2. Logging: configura el serilog. Dispone de la configuración enviada a través de el archivo appsettigs.
  3. Configuración WebHost: tiene la configuración estándar para levantar un web host
  4. Elastic APM: Configura por defecto Elastic APM
  5. Configuración Kestrel: Se deshabilita la exposición del servicio de kestrel en los headers.

Configure Services

Web API

builder.Services.ConfigureAndreaniServices();
//...

var app = builder.Build();

app.ConfigureAndreani();

Los método ConfigureAndreaniServices y ConfigureAndreani realizan las siguientes configuraciones:

  1. Configuración de Controllers ASPNET
  2. Configuración de Compresión GZIP a las respuestas de las API.
  3. Configuración Swagger - Autodocumentación
  4. Configuración de HealthCheck - Urls: /health y /healthcheck
  5. Configuración de Versionado de APIs.
  6. Configuración de Middleware para manejo de error de validación fluentValidation.
  7. Configuración Routing, Authenticación y Authorization.
  8. Configuración RateLimit

Host Worker

builder.Services.ConfigureAndreaniWorkerServices();
//...

var app = builder.Build();

app.ConfigureAndreaniWorker();
Para un worker no necesitamos de configuraciones extras, por lo que los métodos solo configuran:

  1. Configuración de HealthCheck - Urls: /health y /healthcheck
  2. Configuración RateLimit

Endpoints:

El metodo ConfigureAndreani permite configurar endpoints extras fuera de los controllers de ASPNET.

app.ConfigureAndreani(configEndopint: (endpoint) => endpoint.MapGet("/test", async context =>
{
    await context.Response.WriteAsync(JsonSerializer.Serialize(new
    {
        Test = "esto es una api de test"
    }));
}));
En este ejemplo tendremos disponible el endpoint "/test".

Warning

Esta funcionalidad no debe ser usada para exponer endpoints de negocio.

Middleware:

El método ConfigureAndreani nos permite configurar middleware. Para eso podemos seguir las especificaciones de Microsoft de configuración de middleware

app.ConfigureAndreani(
    middleware: (opt) => {
        opt.Use(async (context, next) =>
        {
            // Do work that can write to the Response.
            await next.Invoke();
            // Do logging or other work that doesn't write to the Response.
        });
    });

HealthChecks:

Como crear CustomHealthChecks en un proyecto de platform:

Para crear un custom HealthCheck que apunte a otro servicio ( por ejemplo una API ) se debe crear una nueva clase en Infrastructure dentro de una carpeta que se va a llamar HealtChecks, la misma clase tiene que implementar la interfaz IHealthCheck, luego a la clase le creamos un constructor que reciba un string (este se crea para recibir la url del servicio del que dependemos, y que se pueda configurar en el appsettings), y dentro del metodo que nos pide implementar la interfaz CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) , hacemos un request al "/health" del servicio y en función del StatusCode que devuelva, retornamos Healthy o Unhealthy, como en el ejemplo:

public class HealthCheckApiWithArgs : IHealthCheck
    {
        private readonly string _url;

        public HealthCheckApiWithArgs(string url)
        {
            _url = url;
        }

        public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
        {
            var client = new HttpClient();

            client.BaseAddress = new Uri(_url);

            HttpResponseMessage response = await client.GetAsync("");

            return response.StatusCode == HttpStatusCode.OK ?
                await Task.FromResult(new HealthCheckResult(
                      status: HealthStatus.Healthy,
                      description: "The API is healthy :)"
                       )) :
                await Task.FromResult(new HealthCheckResult(
                      status: context.Registration.FailureStatus,
                      description: $"API does not work because {response.ReasonPhrase} :("));
        }
    }

Una vez creado el custom Health Check pasamos a configurarlo, creamos dentro de Infrastructure una carpeta que se va a llamar Extensions, dentro de esa carpeta creamos una clase static con el nombre HealthChecksExtensions, luego creamos un metodo static void AddHealthChecks y dentro del mismo configuramos el healthcheck como se muestra en el ejemplo:

 public static class HealthChecksExtensions
    {
        public static void AddHealthChecks(this IServiceCollection services, IConfiguration configuration)
        {
            var url = configuration["HealthChecks:Url"];

            services.AddHealthChecks()
                .AddTypeActivatedCheck<HealthCheckApiWithArgs>(
                "Custom Health Check with Args",
                failureStatus: HealthStatus.Unhealthy,
                tags: new[] { $"EndPoint Services: {url}" },
                args: new object[] { url }
                );
        }
    }

Warning

No nos podemos olvidar de agregar el valor de "HealthChecks:Url" en el appsettings.Development.yml o en el appsettings.yml

y por ultimo vamos a DependencyInjection de Infrastructure y agregamos la linea services.AddHealthChecks(configuration);.

Tip

El parametro args de `AddTypeActivatedCheck` puede recibe un array de object, pero cada parametro que se pase por la función tiene que estar en el constructor de nuestra clase HealthCheck.

Para más información de creación de custom Health Checks ver Create health checks.

WebHost Authentication

La librería WebHost ofrece un módulo de autenticación personalizable y flexible que permite autenticar a los usuarios utilizando diferentes tokens y proveedores de identidad. Esta funcionalidad de autenticación ha sido mejorada para que ya no dependa de ninguna tecnología en particular. Ahora es posible autenticar con diferentes Tokens de diferentes Identity Providers o Custom Tokens, y todo se configura mediante las variables de entorno.

Configuracion:

Para configurar la autenticación en nuestra app, simplemente debemos agregar el siguiente servicio:

builder.Services.AddOpenIdConnectAuthentication(builder.Configuration);

La configuración de la autenticación se realiza a través de la sección "OpenIdConnect" del archivo de configuración de la aplicación. En esta sección se pueden especificar diferentes proveedores de autenticación y sus configuraciones correspondientes. A continuación, se muestra un ejemplo de cómo podría verse la sección "OpenIdConnect" en el archivo de configuración:

OpenIdConnect:
  - AuthScheme: "Cognito"
    Issuer: "https://test-issuer1.com"
    ClientID: "testclientid1"
    ValidateAudience: true
    ValidateIssuer: true
    ValidateIssuerSingingKey: true
  - AuthScheme: "AzureADB2C"
    Issuer: "https://test-issuer2.com"
    ClientID: "testclientid2"
    ValidateAudience: false
    ValidateIssuer: true
    ValidateIssuerSingingKey: false
    Flow: "B2C_A1_SignIn_SignUpAAD"
  - AuthScheme: "CustomToken"
    ClientID: "testclientid3"
    ValidateAudience: false
    ValidateIssuer: true
    ValidateIssuerSingingKey: true
    UseCustomScheme: true
    SecretKey: "test_secret_key"

AuthScheme: Este atributo especifica el nombre del esquema de autenticación que se usará.

Issuer: Este atributo especifica la URL del proveedor de identidad que se utilizará para autenticar a los usuarios. (Opcional)

ClientID: Este atributo especifica el identificador del cliente que se utilizará para autenticar a los usuarios. (Opcional)

ValidateAudience: Este atributo especifica si se deben validar los valores del token de audiencia (aud). Si este valor es verdadero, el valor de la audiencia del token debe coincidir con el valor especificado en el atributo "ClientID". Este valor por defecto es verdadero. (Opcional)

ValidateIssuer: Este atributo especifica si se deben validar los valores del token del emisor (iss). Si este valor es verdadero, el valor del emisor del token debe coincidir con el valor especificado en el atributo "Issuer". Este valor por defecto es verdadero. (Opcional)

ValidateIssuerSingingKey: Este atributo especifica si se deben validar las SigninKeys del proveedor de identidad. Si este valor es verdadero, se valida que la SigninKey en el token corresponda a una de las claves de firma proporcionadas por el proveedor de identidad. Este valor por defecto es verdadero. (Opcional)

Flow: Este atributo especifica el flujo de autenticación que se utilizará. Es un atributo opcional y solo se utiliza en algunos proveedores de identidad. Por ejemplo, en Azure AD B2C se puede especificar el flujo B2C_A1_SignIn_SignUpAAD para habilitar el registro de nuevos usuarios. (Opcional)

UseCustomScheme: Este atributo especifica si se utilizará un esquema de autenticación personalizado en lugar de un proveedor de identidad. Si este valor es verdadero, se utilizará el atributo "SecretKey" como clave secreta para firmar y verificar tokens. Este valor por defecto es falso. (Opcional)

SecretKey: Este atributo especifica la secret key que se utilizará para firmar y verificar tokens si se utiliza un esquema de autenticación personalizado. Este valor solo se utiliza si "UseCustomScheme" es verdadero. (Opcional)

WebHost Authorization

La librería ofrece la posiblidad de configurar politicas de autorización, actualmente solo es utilizable por Azure AD B2C, puede leer más en Authorization Azure B2C

Desactivar Swagger UI:

Con esta funcionalidad existe la posiblidad de desactivar la UI de Swagger a través de variables de entorno, la variable va a ser un boolean con valor true o false, como a continuación:

Swagger:
  Enabled: false

En caso de no usar esta variable, Swagger sigue funcionando.

Configuración de Cors

La configuración de Control de Acceso HTTP (CORS) en una aplicación web es esencial para permitir que los navegadores restrinjan las solicitudes desde diferentes orígenes.

Configuración básica

Para configurar CORS correctamente, debemos seguir estos pasos:

  1. Usar builder.Services.AddDefaultCors(builder.Configuration) antes de llamar a builder.Build().
  2. Llamar a .WithOrigins() y .Build() en la configuración CORS.
  3. Agregar app.UseCors() después de la creación de la aplicación.

A continuación se muestra un ejemplo:

using Andreani.ARQ.WebHost.Extension;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using ProjectName.Application;
using ProjectName.Infrastructure;

var builder = WebApplication.CreateBuilder(args);

builder.ConfigureAndreaniWebHost(args);

builder.Services.ConfigureAndreaniServices();
builder.Services.AddApplication();
builder.Services.AddInfrastructure(builder.Configuration);

builder.Services.AddDefaultCors(builder.Configuration).WithOrigins().Build();

var app = builder.Build();

app.UseCors();
app.ConfigureAndreani();

app.Run();

Además, aquí tienes un ejemplo de cómo configurar los orígenes en tu archivo de configuración (ya sea YAML o JSON):

Cors: 
  Origins: 
  - "localhost:3000"
  - "andreani.com.ar" 
"Cors": {
    "Origins": [
        "localhost:3000",
        "andreani.com.ar"
    ]
}

Warning

- **La configuración los Origins es obligatoría, en caso de no tenerlo en las environment no dejará levantar la aplicación hasta que esté configurado correctamente**
- **También es necesario el uso del `.Buil()` para terminar la configuración**

Notas

-**El método `WithOrigins()` se encarga de configurar los Cors para que acepten todos los origenes que terminen en los valores que le pasamos en las environments**
- **Por defecto si no se configuran los `Headers` o `Methods`, los origenes configurados van a aceptar cualquier header y cualquier método**

Configuración con Headers

Si deseas configurar los Headers CORS, sigue los pasos de la configuración básica y agrega .WithHeaders() antes de .Build(). A continuación, se muestra un ejemplo:

using Andreani.ARQ.WebHost.Extension;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using ProjectName.Application;
using ProjectName.Infrastructure;

var builder = WebApplication.CreateBuilder(args);

builder.ConfigureAndreaniWebHost(args);

builder.Services.ConfigureAndreaniServices();
builder.Services.AddApplication();
builder.Services.AddInfrastructure(builder.Configuration);

builder.Services.AddDefaultCors(builder.Configuration).WithOrigins().WithHeaders().Build();

var app = builder.Build();

app.UseCors();
app.ConfigureAndreani();

app.Run();
Cors: 
  Origins: 
  - "localhost:3000"
  - "andreani.com.ar"
  Headers:
  - "Content-Type"
  - "Accept"  
"Cors": {
    "Origins": [
        "localhost:3000",
        "andreani.com.ar"
    ],
    "Headers": [
        "Content-Type",
        "Accept"
    ]
}

Configuración de Methods

Para configurar los Methods CORS, sigue los pasos de la configuración básica y agrega .WithMethods() antes de .Build(). Aquí tienes un ejemplo:

using Andreani.ARQ.WebHost.Extension;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using ProjectName.Application;
using ProjectName.Infrastructure;

var builder = WebApplication.CreateBuilder(args);

builder.ConfigureAndreaniWebHost(args);

builder.Services.ConfigureAndreaniServices();
builder.Services.AddApplication();
builder.Services.AddInfrastructure(builder.Configuration);

builder.Services.AddDefaultCors(builder.Configuration).WithOrigins().WithMethods().Build();

var app = builder.Build();

app.UseCors();
app.ConfigureAndreani();

app.Run();
Cors: 
  Origins: 
  - "localhost:3000"
  - "andreani.com.ar"
  Methods:
  - "DELETE"
  - "PUT"  
"Cors": {
    "Origins": [
        "localhost:3000",
        "andreani.com.ar"
    ],
    "Methods": [
        "DELETE",
        "PUT"
    ]
}

Notas

**Por defecto, los métodos ``POST`` y ``GET`` estarán habilitados aunque no se configuren explícitamente.**

Configuración de ExposedHeaders

Si deseas configurar los ExposedHeaders CORS, sigue los pasos de la configuración básica y agrega .WithExposedHeaders() antes de .Build(). Aquí tienes un ejemplo:

using Andreani.ARQ.WebHost.Extension;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using ProjectName.Application;
using ProjectName.Infrastructure;

var builder = WebApplication.CreateBuilder(args);

builder.ConfigureAndreaniWebHost(args);

builder.Services.ConfigureAndreaniServices();
builder.Services.AddApplication();
builder.Services.AddInfrastructure(builder.Configuration);

builder.Services.AddDefaultCors(builder.Configuration).WithOrigins().WithExposedHeaders().Build();

var app = builder.Build();

app.UseCors();
app.ConfigureAndreani();

app.Run();
Cors: 
  Origins: 
  - "localhost:3000"
  - "andreani.com.ar"
  ExposedHeaders:
  - "Content-Type"
  - "Accept"  
"Cors": {
    "Origins": [
        "localhost:3000",
        "andreani.com.ar"
    ],
    "ExposedHeaders": [
        "Content-Type",
        "Accept"
    ]
}

Configuración de MaxAge

Si deseas configurar la propiedad MaxAge de CORS, sigue los pasos de la configuración básica y agrega .WithMaxAge() antes de .Build(). Aquí tienes un ejemplo:

using Andreani.ARQ.WebHost.Extension;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using ProjectName.Application;
using ProjectName.Infrastructure;

var builder = WebApplication.CreateBuilder(args);

builder.ConfigureAndreaniWebHost(args);

builder.Services.ConfigureAndreaniServices();
builder.Services.AddApplication();
builder.Services.AddInfrastructure(builder.Configuration);

builder.Services.AddDefaultCors(builder.Configuration).WithOrigins().WithMaxAge().Build();

var app = builder.Build();

app.UseCors();
app.ConfigureAndreani();

app.Run();
Cors: 
  Origins: 
  - "localhost:3000"
  - "andreani.com.ar"
  MaxAge: "00:00:30"  
"Cors": {
    "Origins": [
        "localhost:3000",
        "andreani.com.ar"
    ],
    "MaxAge": "00:00:30"
}

Configuración con ActionCorsConfiguration

Para configurar los CORS utilizando el método ActionCorsConfiguration, sigue estos pasos:

  1. Utiliza builder.Services.ActionCorsConfiguration(options => ...) antes de llamar a builder.Build().
  2. Llama a app.UseCors("CustomPolicy") después de crear la aplicación.

Aquí tienes un ejemplo:

using Andreani.ARQ.WebHost.Extension;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using ProjectName.Application;
using ProjectName.Infrastructure;

var builder = WebApplication.CreateBuilder(args);

builder.ConfigureAndreaniWebHost(args);

builder.Services.ConfigureAndreaniServices();
builder.Services.AddApplication();
builder.Services.AddInfrastructure(builder.Configuration);

builder.Services.AddDefaultCors(options =>
options.AddPolicy("CustomPolicy", policy =>
policy.WithOrigins("http://localhost:3000").AllowAnyHeader().AllowAnyMethod()
));

var app = builder.Build();

app.UseCors("CustomPolicy");
app.ConfigureAndreani();

app.Run();

Esta configuración te permite definir una política CORS personalizada llamada CustomPolicy que acepta solicitudes desde http://localhost:3000, permite cualquier encabezado y cualquier método.

Security Headers

Web Host tiene la capacidad de incorporar headers por defecto según los lineamientos del equipo de Ciberseguridad de la compañia.

Configuración

Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.ConfigureAndreaniWebHost(args);
//...

var app = builder.Build();

app.UseSecurityHeader(builder.Configuration);
app.ConfigureAndreani();

app.Run();

La función UseSecurityHeader incorpora a cada petición los siguientes headers

Key Value Descripción
X-XSS-Protection 1; mode=block Ayuda a proteger contra ataques de tipo cross-site scripting (XSS).
X-Content-Type-Options nosniff Previene que los navegadores intenten adivinar ("sniff") los tipos MIME, lo que ayuda a reducir ciertos tipos de ataques.
Strict-Transport-Security max-age=31536000; includeSubdomains; preload Garantiza que los navegadores solo se comuniquen con el servidor a través de HTTPS, lo que mejora la seguridad de la transmisión de datos.
Referrer-Policy no-referrer-when-downgrade Controla la información que se envía en los encabezados Referer para mejorar la privacidad y seguridad.

Custom Default Header

Si necesitamos modificar los header por defecto podemos hacerlo desde la siguientes variables:

appsettings.yml
Headers:
    DefaultHeaders:
        XXSSProtection: "1; mode=block"
        XContentTypeOptions: "nosniff"
        ReferrerPolicy: "no-referrer-when-downgrade"
        StrictTransportSecurity: "max-age=31536000; includeSubdomains; preload"

Agregar headers

Si necesitamos añadir headers globales podemos realizarlo agregando.

appsettings.yml
Headers:
  Add:
    - "x-api-key=1234567890"

Eliminar headers

Si necesitamos remover headers globales podemos realizarlo agregando.

appsettings.yml
Headers:
  Remove:
    - "x-api-key"

LimitRate

WebHost incorpora una politica global de limite de request concurrente. Puede ver el detalle del middleware en .NET RateLiming

Limites por defecto

Se realizo la configuración según la politica de Concurrencia por IP con la siguiente configuración

key value
PermitLimit 100
QueueLimit 500

Actualización Limites por defecto

La configuración esta implicita en la configuración estandar de web host, es posible actualizar la configuración por defecto desde las variables de ambiente.

appsettings.yml
RateLimit:
 PermitLimit: 1
 QueueLimit: 1

FAQ

Problemas con los tiempos de vida(LifeTime)

Error de Mediator:

Este error sucede porque el WorkerService está inyectado con el AddHostedService de .Net, el mismo inyecta el servicio como Singleton, y nosotros estamos queriendo inyectar un mediator que está como Transient y entra en conflicto por los diferentes tiempos de vida(Lifetime).

Solución: Para resolver este problema, necesitamos crear un nuevo servicio con un tiempo de vida adecuado para nuestras necesidades. Podemos hacer esto definiendo un ServiceScoped y luego utilizar el IServiceScopeFactory para solicitar instancias de este servicio según sea necesario.

El ServiceScoped nos permite controlar el tiempo de vida del servicio de manera más granular, lo que nos permite evitar el conflicto de tiempos de vida entre el WorkerService y el mediator. Al utilizar el IServiceScopeFactory, podemos solicitar instancias del ServiceScoped dentro del contexto adecuado, asegurando que se respeten los tiempos de vida esperados y evitando problemas de Singleton versus Transient.

Aquí hay un ejemplo de cómo implementar esto:

Warning

Toda la implementación se creo en un mismo archivo con el objetivo de que el ejemplo sea fácil de entender, pero se recomienda que la Interfaz IScopedService se cree en la capa de Aplicación y la implementación (ScopedService) en la capa de Infraestructura, con el objetivo de respetar los principios de la arquitectura.