Saltar a contenido

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

  1. Instalar en el proyecto Application la dependencia con Core.Relational
    dotnet add package Andreani.Arq.Core.Relational
    
  2. Instalar en el proyecto Infreastructure la dependencia con CQRS.SqlServer

    dotnet add package Andreani.Arq.Cqrs.SqlServer
    

  3. Tener presente que si se está trabajando con clean architecture, se debe instalar en el project de application.

  4. 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();
En el ejemplo, se espera una función que devuelva un string a la cual se le va a inyectar una instancia de la entidad principal de la consulta.

            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();
De la misma manera podemos agregar multiples paramatros de ordenamiento.

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();
El objetivo de nuestro test va a ser verificar que, en efecto, estamos recibiendo 3 objetos.

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);
Para más información sobre testing, consultar la documentación de Unit Testing en .Net.