Andreani.ARQ.CQRS¶
Overview¶
Se desarrolló la librería Andreani.ARQ.CQRS
para implementar EntityFramework y Dapper. Su objetivo es abstraer las configuraciones a lxs desarrolladorxs y proveer interfaces comunes para facilitar la adopción y uso de estas tecnologías.
Instalación¶
- Instalar en el proyecto
Application
la dependencia conCore.Relational
:dotnet add package Andreani.Arq.Core.Relational
- Instalar en el proyecto
Infrastructure
la dependencia conCQRS
:dotnet add package Andreani.Arq.Cqrs
Configuración¶
Para configurar, primero instala como paquete nuget la librería Andreani.ARQ.CQRS
.
En appsettings
, añade una sección para la configuración de CQRS:
DataAccessRegistry:
ReadOnlyConnection: "Data Source=DBFileName.db"
TransactionalConnection: "Data Source=DBFileName.db"
ProviderName: Microsoft.Data.Sqlite
{
"DataAccessRegistry":{
"ReadOnlyConnection": "Data Source=DBFileName.db",
"TransactionalConnection": "Data Source=DBFileName.db",
"ProviderName": "Microsoft.Data.Sqlite",
}
}
Nombre | Descripción | Requerido | Default |
---|---|---|---|
Name | Nombre de la conexión de base de datos | No | default |
ReadOnlyConnection | Connection string para consultas | Sí | |
TransactionalConnection | Connection string para operaciones de escritura | Sí | |
ProviderName | Proveedor de base de datos | Sí | |
Version | Versión (solo para MySQL) | No | null |
AutoMigrate | Ejecución de migración automática | No | false |
CQRS permite usar dos bases de datos: una para transacciones y otra para consultas. Puedes elegir usar la misma base de datos o dos diferentes.
Agrega la siguiente línea en el bootstrap de la capa de Infrastructure para incluir la configuración:
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
//...
services.AddCQRS(configuration)
.Configure<ApplicationDbContext>();
//...
}
Breacking Change: Uso de ITransactionalRepository y IReadOnlyQuery
Las interfaces IReadOnlyQuery
y ITransactionalRepository
ya no están disponibles para uso directo. Crea una interfaz propia que extienda de estas y genera el objeto concreto para cada una.
Providers¶
Se soportan las siguientes bases de datos y sus respectivos ProviderName
:
Base de Datos | ProviderName |
---|---|
Sql Server | System.Data.SqlClient |
Oracle | Oracle.ManagedDataAccess.Client |
MySql | MySql.Data.MySqlClient |
Sqlite | Microsoft.Data.Sqlite |
PostgreSql | Npgsql |
MySql
MySql: desde Net 6, agrega el parámetro Version
en la configuración.
DataAccessRegistry:
ReadOnlyConnection: "Data Source=..."
TransactionalConnection: "Data Source=..."
ProviderName: MySql.Data.MySqlClient
Version: 5.5.X
Healthcheck¶
La librería incluye healthcheck, que se complementa con Andreani.ARQ.WebHost
. Accede a los healthchecks mediante http://*:*/health
o http://*:*/healthcheck
.
Interfaces Disponibles¶
La librería CQRS implementa dos interfaces con funcionalidades comunes para interactuar con la base de datos.
ITransactionalRepository¶
Implementa Ef core y ofrece operaciones de escritura.
public interface ITransactionalRepository
{
// Métodos de la interfaz...
}
Esta interfaz permite persistir cualquier entidad mapeada en el contexto.
IReadOnlyQuery¶
Implementa Dapper para consultas de lectura, con métodos comunes para consultas simples y generales.
public interface IReadOnlyQuery
{
// Métodos de la interfaz...
}
Recomendamos crear objetos Query propios para consultas complejas y no abusar de los métodos genéricéricos para múltiples consultas. Utiliza ExecuteQueryAsync
o FirstOrDefaultQueryAsync
, o crea tus propias consultas con Dapper.
Uso de Operaciones de Escritura¶
Crea una interfaz que extienda ITransactionalRepository
.
interface ICommandSqlServer : ITransactionalRepository { }
En Infrastructure/Repository
, crea las implementaciones de la interfaz.
public class CommandSqlServer : TransactionalRepository, ICommandSqlServer
{
public CommandSqlServer([FromKeyedServices("default")] ITransactionalConfiguration config) : base(config)
{
}
}
KeyedServices
- FromKeyedServices: Define qué configuración de base de datos usar. Más en CQRS Multiple base de datos.
Incluye esta clase en la inyección de dependencias:
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
//...
services.AddCQRS(configuration)
.Configure<ApplicationDbContext>();
services.AddScoped<ICommandSqlServer, CommandSqlServer>();
//...
}
public class MyHandler
{
private readonly ICommandSqlServer _repository;
public MyHandler(ICommandSqlServer repository)
{
_repository = repository;
}
public async Task MyMethod(){
var entity = new ObjectEntity
{
AnyValue = "ejemplo"
};
_repository.Insert(entity);
await _repository.SaveChangeAsync();
}
}
Herencia
Al heredar de TransactionalRepository
, tu clase implementada contiene los métodos de ITransactionalRepository
y puedes extender su funcionalidad agregando métodos a tu nueva interfaz y utilizando el Contexto.
Ejemplo de Extensión
interface ICommandSqlServer : ITransactionalRepository
{
Task<List<Person>> AddMultiPerson(List<PersonDto> people);
}
public class CommandSqlServer : TransactionalRepository, ICommandSqlServer
{
public CommandSqlServer([FromKeyedServices("default")] ITransactionalConfiguration config) : base(config)
{
}
public async Task<List<Person>> AddMultiPerson(List<PersonDto> people)
{
List<Person> peopleEntity = people.Select(x => new Person()).ToList();
context.AddRange(peopleEntity);
await context.SaveChangesAsync();
return peopleEntity;
}
}
Uso de Operaciones de Lectura¶
Crea una interfaz que extienda IReadOnlyQuery
.
interface IQuerySqlServer : IReadOnlyQuery { }
En Infrastructure/Query
, crea las implementaciones de la interfaz.
public class QuerySqlServer : ReadOnlyQuery, IQuerySqlServer
{
public QuerySqlServer([FromKeyedServices("default")] IReadOnlyQueryConfiguration config) : base(config)
{
}
}
KeyedServices
- FromKeyedServices: Define qué configuración de base de datos usar. Más en CQRS Multiple base de datos.
Incluye esta clase en la inyección de dependencias:
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
//...
services.AddCQRS(configuration)
.Configure<ApplicationDbContext>();
services.AddScoped<IQuerySqlServer, QuerySqlServer>();
//...
}
Ejemplo de uso
public class MyHandler
{
private readonly IQuerySqlServer _query;
public MyHandler(IQuerySqlServer query)
{
_query = query;
}
public async Task<List<Person>> Handle(ListPerson request, CancellationToken cancellationToken)
{
var result = await _query.GetAllAsync<Person>();
return result;
}
}
Herencia
Al heredar de ReadOnlyQuery
, tu clase implementada contiene los métodos de IReadOnlyQuery
y puedes extender su funcionalidad agregando métodos a tu nueva interfaz y utilizando el Contexto.
Ejemplo de creación de un método personalizado:
Declara el método en tu Interface
:
public interface IQuerySqlServer : IReadOnlyQuery
{
Task<List<Person>> GetAllByNameAsync(string name);
}
En la implementación, usa UseConnection
para ejecutar tu consulta:
public class QuerySqlServer : ReadOnlyQuery, IQuerySqlServer
{
private const string FINDBYNAME = "SELECT * FROM Person WHERE Name = @name";
public QuerySqlServer([FromKeyedServices("default")] IReadOnlyQueryConfiguration config) : base(config)
{
}
public async Task<List<Person>> GetAllByNameAsync(string name)
{
return await UseConnectionAsync(async x => await x.QueryAsync<Person>(FINDBYNAME, new { name }));
}
}
UseConnection¶
El método UseConnection
inyecta la conexión de base de datos en la consulta. Construye e inyecta la conexión siguiendo las buenas prácticas, permitiendo el uso completo de Dapper y sus funcionalidades.
Ejemplo de uso sincrónico:
var result = _query.UseConnection(connection => {
var sql = "SELECT * FROM Board";
var resultSet = connection.Query<Board>(sql);
return resultSet;
});
Para consultas asíncronas, utiliza UseConnectionAsync
:
var results = await _query.UseConnectionAsync(async connection => {
var sql = "SELECT * FROM Board";
var resultSet = await connection.QueryAsync<Board>(sql);
return resultSet;
});
Recomendación: Uso de Constantes o Variables Estáticas para Query Strings
Para optimizar el rendimiento, es altamente recomendable almacenar las query strings en constantes o variables estáticas. Esta práctica reduce el tiempo de ejecución ya que evita la recreación repetitiva de la misma cadena de consulta. Al utilizar constantes o variables estáticas, se garantiza que la cadena de consulta se compile una sola vez y se reutilice en todas las instancias donde se necesite. Esto no solo mejora la eficiencia en términos de procesamiento y uso de memoria, sino que también contribuye a un código más limpio y mantenible, facilitando las actualizaciones y la gestión de las queries en un solo lugar.
Implementación Multi Base de Datos¶
CQRS
permite tener múltiples data stores, con configuraciones y objetos independientes.
Configuración¶
Ejemplo con dos configuraciones, default
para SqlServer y cache
para SqLite:
DataAccessRegistry:
Name: "default"
ReadOnlyConnection: "Data Source={sqlserver}..."
TransactionalConnection: "Data Source={sqlserver}..."
ProviderName: System.Data.SqlClient
DataAccessRegistryCache:
Name: "cache"
ReadOnlyConnection: "Data Source=DBFileName.db"
TransactionalConnection: "Data Source=DBFileName.db"
ProviderName: Microsoft.Data.Sqlite
Agrega esto en el bootstrap de la capa de Infrastructure:
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
//...
services.AddCQRS(configuration)
.Configure<ApplicationDbContext>()
.Configure<ApplicationCacheDbContext>("DataAccessRegistryCache");
//...
}
Atención
Necesitarás un nuevo DbContext para la nueva conexión.
Genera las implementaciones para esta nueva conexión siguiendo la documentación en Uso de Operaciones de Escritura.
Ejemplo:
public class QuerySqLite : ReadOnlyQuery, IQuerySqLite
{
public QuerySqLite([FromKeyedServices("cache")] IReadOnlyQueryConfiguration config) : base(config)
{
}
}
FromKeyedServices
Usa el nombre dado en la configuración de la base de datos para que los recursos inyectados tengan la configuración adecuada.
Healthchecks¶
{
"status": "Healthy",
"totalDuration": "00:00:01.5170060",
"entries": {
"defaultTransactional": {
"data": {
},
"duration": "00:00:01.5007649",
"status": "Healthy",
"tags": [
"db"
]
},
"defaultReadOnly": {
"data": {
},
"duration": "00:00:01.5007314",
"status": "Healthy",
"tags": [
"db"
]
},
"cacheTransactional": {
"data": {
},
"duration": "00:00:00.4042012",
"status": "Healthy",
"tags": [
"db"
]
},
"cacheReadOnly": {
"data": {
},
"duration": "00:00:00.4041246",
"status": "Healthy",
"tags": [
"db"
]
}
}
}
Lectura con Ef Core¶
En caso de tener separado las base de lectura y escritura y sea necesario realizar consultas con Ef Core, puede configurar un contexto para las operaciones de solo lectura con la conexión a la base de solo lectura.
Configuración¶
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
//...
services.AddCQRS(configuration)
.Configure<ApplicationDbContext, ApplicationReadOnlyDbContext>();
//...
}
En este caso, necesitamos pasar como argumento el contexto que debera ser configurado como ReadOnly
FAQ¶
Problemas con los tiempos de vida(LifeTime)
Error con Query o Command:¶
Este error sucede cuando intentamos inyectar un servicio Scoped (IQuerySqlServer) en un servicio que está inyectado como Singleton (WorkerService).
Solución:
Para abordar este problema, contamos con dos soluciones posibles que dependen de cómo esté implementado nuestro servicio y de lo que necesitemos lograr:
- Si estamos utilizando únicamente el servicio IQuerySqlServer en toda la aplicación y no realiza consultas con el DbContext de EF Core, podemos ajustar el tiempo de vida (Lifetime) del servicio Cqrs mediante configuraciones. Esto se puede lograr de la siguiente manera:
- Si no cumplimos con las condiciones del caso 1, debemos inyectar el servicio (ya sea IQuerySqlServer o ICommandSqlServer) de manera similar a la solución proporcionada para el
Mediator
.
Warning
Es crucial que el DbContext esté configurado como Scoped en la configuración de Cqrs, ya que de lo contrario podría ocasionar problemas al intentar establecer múltiples conexiones a la base de datos.
Problema de consumo alto de memoria
Problema de consumo alto de memoria debido al contexto de EFCORE:¶
El problema se puede registrar en procesos que tienen un uso de Entity Framework Core, habitualmente aquellos sistemas
que son consumidores o workers ya que la instancia del contexto de EF CORE se instancia como scoped
lo que proboca para estos sistemas que actue como singleton
.
Vemos sintomas relacionados con el consumo de memoria y la no liberación de esta en un periodo de tiempo lo que puede provovar reinicios de nuestros sistemas constantemente.
Solución
- Actualización a la versión de Platform
v8.5.0
o superior - Desactivar el trackeo de entidades del DbContext
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
ChangeTracker.AutoDetectChangesEnabled = false;
}
- Validar que las querys que usamos con el contexto tengan el metodo
AsNotTracking()
.
public async Task<Users> GetUsers()
{
return context.Users
.AsNotTranking()
.Include<Orders>()
.ToListAsync();
}
Tip
Platform al leer la configuración de AutoDetectChangesEnabled
realiza la limpieza del Tracker. Puedes forzar la limipieza de la siguiente manera luego de un SaveChange
:
await context.SaveChangesAsync();
context.ChangeTracker.Clear(); // Clear the change tracker