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:
- Configuration File: configura el host para recibir las configuraciones por archivo
appsettings.json
oappsettings.yml
- Logging: configura el serilog. Dispone de la configuración enviada a través de el archivo appsettigs.
- Configuración WebHost: tiene la configuración estándar para levantar un web host
- Elastic APM: Configura por defecto Elastic APM
- 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:
- Configuración de Controllers ASPNET
- Configuración de Compresión GZIP a las respuestas de las API.
- Configuración Swagger - Autodocumentación
- Configuración de HealthCheck - Urls:
/health
y/healthcheck
- Configuración de Versionado de APIs.
- Configuración de Middleware para manejo de error de validación fluentValidation.
- Configuración Routing, Authenticación y Authorization.
- Configuración RateLimit
Host Worker¶
builder.Services.ConfigureAndreaniWorkerServices();
//...
var app = builder.Build();
app.ConfigureAndreaniWorker();
- Configuración de HealthCheck - Urls:
/health
y/healthcheck
- 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"
}));
}));
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:
- Usar builder.Services.AddDefaultCors(builder.Configuration) antes de llamar a builder.Build().
- Llamar a .WithOrigins() y .Build() en la configuración CORS.
- 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:
- Utiliza builder.Services.ActionCorsConfiguration(options => ...) antes de llamar a builder.Build().
- 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¶
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:
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.
Headers:
Add:
- "x-api-key=1234567890"
Eliminar headers¶
Si necesitamos remover headers globales podemos realizarlo agregando.
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.
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.