Andreani.ARQ.CQRS.SqlServer¶
Librería de extensión de CQRS, incluye operaciones básicas de consultas con dapper pseudo similares a LINQ.
Instalación¶
Debemos instalar la librería Andreani.ARQ.CQRS.SqlServer
.
- Tener presente que si se está trabajando con clean architecture, se debe instalar en el project de application.
- Se debe desinstalar la librería
Andreani.ARQ.CQRS
ya que la extensión la poseé internamente.
Configuración¶
Para configurar la extensión debemos ir a nuestro archivo de DependencyInjection, o donde hemos configurado la implementación de CQRS y llamar al m�todo UseSqlServer()
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
...
services.AddCQRS<ApplicationDbContext>(configuration)
.UseSqlServer();
...
return services;
}
Uso¶
Para utilizar los métodos debemos inyectar la interface IReadOnlyQuery
y utilizar el método From
private readonly IReadOnlyQuery _query;
public GetByIdPedidoHandler(IReadOnlyQuery query)
{
_query = query;
}
Ejemplo de uso
var pedido = await _query
.From<Pedidos>()
.Join<EstadoDelPedido>(
pe => nameof(pe.EstadoDelPedido),
ep => nameof(ep.Id)
)
.Execute();
En este ejemplo la query trae todos los pedidos con la información de sus estados.
La interface IQueryBuilder contiene los siguientes métodos
IQueryBuilder<TEntity> From(string table = null);
IQueryBuilder<TEntity> Join<TInner>(Func<TEntity, string> entityKey, Func<TInner, string> innerKey);
IQueryBuilder<TEntity> ThenJoin<TInner, TPreviusProperty>(Func<TPreviusProperty, string> previusInnerKey, Func<TInner, string> innerKey);
IQueryBuilder<TEntity> Where(Func<TEntity, string> predicate, object parameters = null, Enums.TypeWhere where = Enums.TypeWhere.AND);
Task<IEnumerable<TEntity>> Execute();
Task<TEntity> FirstOrDefault();
string ShowQuery();
Estos métodos tienen por objetivo armar la query de sql server correspondiente.
From¶
El método From(string table = null)
inicializa la query, se puede asignar un nombre de tabla en caso que la entidad no se corresponda con el nombre de la tabla en base de datos.
Where¶
El método Where(Func<TEntity, string> predicate, object parameters = null, Enums.TypeWhere where = Enums.TypeWhere.AND)
agrega un filtro a la consulta, este filtro puede ser concatenado por condicionales AND
o OR
Ejemplo:
Simple
var pedido = await _query
.From<Pedidos>()
.Where(p => $"{nameof(p.Id)} = @id", new {id = "1"})
.FirstOrDefault();
var pedido = await _query
.From<Pedidos>()
.Where(p => $"{nameof(p.Id)} <= 1")
.FirstOrDefault();
var pedido = await _query
.From<Pedidos>()
.Where(_ => "id <= 1")
.FirstOrDefault();
Concatenación de filtros
var usuario = await _query
.From<User>()
.Where(u => $"{nameof(u.Id)} = @id", new {id = "1"})
.Where(u => $"{nameof(u.Username)} = @value", new {value = "mail@andreani"}, TypeWhere.OR)
.FirstOrDefault();
El parámetro TypeWhere
define el tipo de where que vamos a concatenar puede optar por AND
o OR
Join¶
El método Join<TInner>(Func<TEntity, string> entityKey, Func<TInner, string> innerKey);
nos permite mapear la entidad con sus relaciones, algo similar al Include
de LINQ
pero más rustico.
EJemplo:
var pedidos = await _query
.From<Pedidos>()
.Join<EstadoDelPedido>(
pe => nameof(pe.EstadoDelPedido),
ep => nameof(ep.Id)
)
.Execute();
El método necesita que se indique como está configurado la relación entre las dos tablas, por lo que debemos pasar el nombre de esas columnas que estan relacionadas con una FK.
En el ejemplo, pe
es la entidad de Pedidos
que es la consulta principal. nameof(pe.EstadoDelPedido)
nos indica que la FK se encuentra en el atributo EstadoDelPedido
. Por otra parte, ep
representa a EstadoDelPedido
y nameof(ep.Id)
indica que es su PK es ese atributo.
El Join solo necesita que se le pasen los nombres de los atributos que van a estar uniendose.
El resultado de esta consulta es una lista de Pedidos, que contendrá mapeado los valores de la tabla EstadoDelPedido correspondiente a cada pedido
Otro Ejemplo:
var pedidos = await _query
.From<EstadoDelPedido>()
.Join<Pedidos>(
ep => nameof(ep.Id),
pe => nameof(pe.EstadoDelPedido)
)
.Execute();
El resultado de esta consulta es una lista de todos los EstadosDelPedido con todos los Pedidos que cumplen con el estado
ThenJoin¶
El método ThenJoin<TInner, TPreviusProperty>(Func<TPreviusProperty, string> previusInnerKey, Func<TInner, string> innerKey);
nos permite mapear la entidad con sus relaciones en segundo orden, algo similar al ThenInclude
de LINQ
pero más rustico.
EJemplo:
var pedidos = await _query
.From<Pedidos>()
.Join<EstadoDelPedido>(
pe => nameof(pe.EstadoDelPedido),
ep => nameof(ep.Id)
)
.ThenJoin<OtroEstado, EstadoDelPedido>(
ep => nameof(ep.Description),
oe => nameof(oe.Id)
)
.Execute();
Al igual que el método Join
el método mapea objetos internos. Pero a diferencia necesita que se le indique cual es el join previo y debe ser ejecutado despues del join.
OrderBy¶
El método OrderBy(Func<TEntity, string> predicate)
permite ordenar los resultados de nuestra consulta según un parámetro a determinar. De forma predeterminada, el método ordena los objetos de forma ascendente (de menor a mayor).
Ejemplo:
var pedidos = await _query
.From<Pedidos>()
.OrderBy(pe => nameof(pe.NumeroDePedido))
.Execute();
var pedidos = await _query
.From<Pedidos>()
.OrderBy("NumeroDePedido")
.Execute();
La función OrderBy tiene una sobrecarga a la cual se puede incorporar directamente el string a incorporar en la Query.
El resultado de estas consulta es una lista de Pedidos, ordenada de forma ascendente de acuerdo al número de pedido.
var pedidos = await _query
.From<Pedidos>()
.OrderBy("Cuando DESC, NumeroDePedido ASC")
.Execute();
OrderByDescending¶
La librería también cuenta con el método OrderByDescending(Func<TEntity, string> predicate)
, que realiza la misma función de ordenamiento pero en el sentido inverso al OrderBy (de mayor a menor).
Ejemplo con OrderByDescending:
var pedidos = await _query
.From<EstadoDelPedido>()
.OrderByDescending(pe => nameof(pe.PedidoId))
.Execute();
var pedidos = await _query
.From<EstadoDelPedido>()
.OrderByDescending("PedidoId")
.Execute();
El resultado de esta consulta es una lista de Pedidos, ordenada de forma descendente.
Limit y Offset¶
El método Limit(int predicate)
nos permite controlar la cantidad de registros que traera la query y el método Offset(int predicate)
nos permite controlar la posición en la tabla desde donde comenzaremos a traer registros. Combinados ambos metodos podemos tener la funcionalidad de paginado en nuestras consultas.
Ejemplo:
var pedidos = await _query
.From<Pedidos>()
.OrderBy("PedidoId")
.Offset(5)
.Limit(3)
.Execute();
Warning
Hay que recordar que tanto Limit y Offset son métodos que deben usarse después de llamar al método OrderBy. De otra manera, obtendremos un error. A su vez, para poder usar Limit tenemos que definir primero el Offset. Si no queremos definir un Offset, simplemente le enviamos 0 como parámetro.
El resultado de esta consulta es una lista de Pedidos, que comienza en el id 6 y devuelve 3 resultados.
Execute y FirstOrDefault¶
La query se ejecuta cuando se invoque el método Execute
o FirstOrDefault
, la diferencia entre uno o otro es más que clara, uno devuelve un solo objeto mientras que el otro devuelve una collection.
ShowQuery¶
El método show query fue incluido solo a fines de debugging, permitiendo al dev ver la query que esta armando la librería y con posibilidades de ejecutarla en alguna terminal de Sql Server.
Cómo generar tests de las consultas¶
Para realizar tests de las consultas, vamos a utilizar la librería Moq, generando un cliente mockeado y su setup correspondiente. En otras palabras, vamos a realizar unit testing como lo hacemos de costumbre.
Digamos que queremos realizar el test de la siguiente query:
var student = await _query
.From<Student>()
.OrderBy(_ => "id")
.Limit(3)
.Execute();
Primero mockeamos las dependencias en el constructor:
private readonly Mock<IReadOnlyQuery> _query;
private readonly Mock<IMapper> _mapper;
private readonly GetListStudentHandler _handler;
Las instanciamos:
_query = new Mock<IReadOnlyQuery>();
_mapper = new Mock<IMapper>();
_handler = new GetListStudentHandler(_query.Object, _mapper.Object);
Y, siguiendo el patrón AAA, nuestro test se va a ver así:
//Arrange
var limit = 3;
var fixture = new Fixture();
var response = fixture.CreateMany<Student>(limit);
var content = fixture.CreateMany<StudentDto>(limit);
_query.SetupSequence(x => x.From<Student>(It.IsAny<string>()).OrderBy(It.IsAny<Func<Student, string>>()).Limit(limit).Execute())
.ReturnsAsync(response);
_mapper.Setup(x => x.Map(It.IsAny<IEnumerable<Student>>(), It.IsAny<List<StudentDto>>())).Returns(content.ToList());
//Act
var result = await _handler.Handle(new GetListStudent(), CancellationToken.None);
//Assert
Assert.NotNull(result);
Assert.True(result.IsValid);
Assert.True(HttpStatusCode.OK == result.StatusCode);
Assert.True(result.Content.Count == limit);