Algunas pinceladas sobre como arquitecturar tu WEB/API

El otro día Antíoco Llanos lanzaba el siguiente tweet:

(Siempre las mismas dudas. Que dependa mi capa de negocio de EF para usar sus IDbSet o no… ¿abstraer la abstracción?)

Contesté yo con algunas sugerencias y eso derivó en otra conversación paralela, así que me parece una buena idea poner algunas pinceladas sobre como podemos abordar ese aspecto. Por supuesto y como digo siempe: no hay balas de plata y no existe la arquitectura para todo. Cada proyecto debe analizarse para valorar la arquitectura a abordar, o arquitecturas porque se pueden usar distintas en un mismo proyecto. Así, este post no tiene más pretensión que contarte algunas ideas, pero las conclusiones que saques de ellas son cosa tuya 😉

Nota: Todos los ejemplos de código estarán en netcore y usando EF Core como ORM. Pero vamos, muchas de las cosas que se cuenten (por no decir todo) son aplicables a MVC5 con EF 6.x o NHibernate. No te quedes con que eso “solo sirve para netcore”, pero algún framework hay que usar para el código 😛

1. Usar el contexto directamente en los controladores

Antíoco se preguntaba si abstraer la abstracción. Personalmente soy enemigo de abstraer abstracciones porque toda abstracción fuga así que la abstracción de una abstracción o bien fuga doblemente o bien fuga abstractamente y ninguna de las dos cosas parece muy buena.

Si el proyecto es un proyecto muy CRUD, con operaciones CRUD simples, porqué no inyectar el contexto de EF en el controlador directamente?

    [Route("api/[controller]")]
    public class BeersController : Controller
    {
        private readonly BeersDbContext _db;
        public BeersController(BeersDbContext ctx) => _db = ctx;

        // GET api/values
        [HttpGet]
        public async Task<IActionResult> Get()
        {
            var beers = await _db.Beers.ToListAsync();
            return Ok(beers);
        }

        [HttpGet("{id}")]
        public async Task<IActionResult> Get(int id)
        {
            var beer = await _db.Beers.SingleOrDefaultAsync(b => b.Id == id);
            return beer != null ? (IActionResult)Ok(beer) : (IActionResult)NotFound();
        }

        [HttpPost]
        public async Task<IActionResult> Post([FromBody]Beer newBeer)
       {
            if (string.IsNullOrWhiteSpace(newBeer.Name))
            {
                return BadRequest();
            }
            _db.Beers.Add(newBeer);
            await _db.SaveChangesAsync();
            return NoContent();
        }
    }
}

Hay varios argumentos que se esgrimen en contra de esa aproximación:

  1. El controlador está a atado a EF. ¿Qué ocurre si algún día quiero irme de EF? Efectivamente, el controlador está atado a EF… y también a MVC Core. No sé si me explico: Si te planteas el abstraerte de EF solo “por si en un futuro quiero cambiarlo por otro framework”, también deberías plantearte abstraerte de MVC Core, no vayas a querer usar NancyFx en un futuro. Hazte un favor y sigue el principio YAGNI.
  2. Eso no es testeable. Para probar eso necesitas tener EF sí. Pero veamos que probamos en esos casos tan CRUD. No hay casi lógica, en todo caso la poca lógica que hay es la de las validaciones (p. ej. en el método Post validamos que el nombre no esté vacío). Eso es lo primero que deberías separar del controlador. Puedes mover esas validaciones fuera (directamente en la clase Beer a través de IValidatableObject o usando FluentValidation o similar. Eso te permite probar las validaciones mediante pruebas unitarias de forma sencilla. ¿No te convence solo probar las validaciones? Claro, quieres probar las consultas y los inserts…

1.1 Intentando probar las consultas

Como quieres probar las consultas, decides que lo suyo es meter una capa de abstracción entre el controlador y EF. Y te creas una interfaz que tiene un nombre parecido a IBeersService o IBeersRepository depende de cual sea el blog al que te haya mandado Google:

public interface IBeersService
{
    Task<IEnumerable<Beer>> GetAll();
    Task<Beer> GetById(int id);
    Task Add(Beer beer);
}

Haces tu implementación (que usa BeersDbContext) y en el controlador le inyectas tu servicio. ¡Genial! Ya está todo listo para que pruebes las consultas… Pues no. Deja de engañarte. Veamos como quedaría el método Get/id del controlador:

[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
{
    var beer = await _svc.GetById(id);
    return beer != null ? (IActionResult)Ok(beer) : (IActionResult)NotFound();
}

Estamos usando el servicio. Fantástico, ahora ya podemos crear un test unitario, para este método del controlador:

[Fact]
public async Task Get_Beer_By_Id_Should_Return_The_Beer()
{
    var svc = new Mock<IBeersService>();
    svc.Setup(s => s.GetById(1)).
        Returns(async () => new Beer() { Id = 1, Name = "mocked beer" });
    var ctlr = new BeersController(svc.Object);
    var result = await ctlr.Get(1) as OkObjectResult;
    var beer = result.Value as Beer;
    Assert.Equal(1, beer?.Id);
}

¿Ves cual es el problema? ¡Este test NO comprueba absolutamente nada! Lo único que estás comprobando es que efectivamente el controlador llama al método GetById del servicio. Pero, dado que el servicio es un mock del servicio, no estás probando realmente ninguna consulta. Oh, puedes complicar el mock todo lo que quieras (hay gente que se hace bonitas implementaciones a base de List<T>) pero… estarás probando tu Mock.

¿Estoy diciendo con eso que siempre debes usar EF desde el controlador? No. Digo que en este caso tan CRUD, donde no hay lógica o es solo de validaciones, lo que debes separar son las validaciones, no EF. En este caso, usar EF directamente desde el controlador es lo más sencillo posible. Y por supuesto, no me he olvidado, quieres probar las consultas: hazlo a través de tests de integración. Pero que esas pruebas usen EF, no ningun invento que te hayas hecho tu para “mockear EF”.

2. Proyectos con (relativamente) poca lógica y/o proyectos pequeños

Antes que nada, ojo en decidir un arquitectura en base al “tamaño” del proyecto. El mundo está lleno de “ConsoleApplication1.exe” que han terminado en producción… y de proyectos que eran sencillos (nah… un par de semanas) y que varios meses y múltiples cárnicas, allá siguen.

Usar EF en los controladores directamente, puede funcionar para casos muy CRUD, donde no hay muchas consultas y esas son muy simples. Si en un controlador empezamos a usar LINQ a lo loco, la cosa seguramente no terminará muy bien. En este momento te puede ir bien encapsular y sacar EF fuera del controlador. Aunque solo sea por la claridad del código. Por supuesto esto puede ser parte de un refactoring en un proyecto donde se usaba EF en los controladores.

2.1 Tu enemigo, el repositorio

Cuando eso ocurre, mucha gente termina a un viejo enemigo muy jodido: el repositorio genérico. El repositorio genérico es como Annatar: luce muy bien y promete regalos en forma de reutilización de código. Pero creéme… no tardarás en verle su verdadera cara.

Hay varios tipos de repositorios genéricos (muta como los virus) pero en general todos siguen ese patrón:

public interface IRepository<T, K> where T : class
{
    IEnumerable<T> GetAll();
    T GetById(K id);
    bool Update();
    void Delete(T entity);
    void Add(T entity);
}

Hay gente que incluso, en un alarde de generelización, hasta hacen una implementación genérica del repositorio, algún engendro parecido a eso:

public class Repository<T, K> : IRepository<T, K>
    where T : class
{
    private readonly DbSet<T> _set;
    private readonly DbContext _db;
    public Repository(DbContext context)
    {
        _set = context.Set<T>();
        _db = context;

    }
    public bool Add(T entity)
    {
        _set.Add(entity);
        return _db.SaveChanges() > 0;
    }
    public bool Delete(T entity)
    {
        _set.Remove(entity);
        return _db.SaveChanges() > 0;
    }
    public IEnumerable<T> GetAll()
    {
        return _set.ToList();
    }
    public T GetById(K id)
    {
        return _set.Find(id);
    }
    public bool Update(T entity)
    {
        var entry = _db.Entry(entity);
        _set.Attach(entity);
        entry.State = EntityState.Modified;
        var rows = _db.SaveChanges();
        return rows > 0;
    }
}

Así luego crear repositorios es tan sencillo como:

public class BeersRepository : Repository<Beer, int>
{
    public BeersRepository(BeersDbContext ctx) : base(ctx)
    {
    }
}

¿Qué puede salir mal con aliados como estos? Pues… todo. Para empezar, a pesar de su nombre, eso no es un repositorio: es un DAO, pues lo único que hace es consultar y modificar elementos de la base de datos. Luego… ¿qué abstracción aporta eso? Pero bueno, aceptamos pulpo, lo usamos y luego nos encontramos el gran problema: Tienes un método en un controlador para agregar una cerveza de una cervecería nueva. Así que usas BreweryRepository para agregar la cervecería y luego BeerRepository para agregar la cerveza. Y entonces te das cuenta que debes usar una transacción porque el método Add de la clase Repository llama al SaveChanges. Debes abrir una transacción para algo que no sería necesario: usando el contexto podrías agregar el objeto Beer a un DbSet, el objeto Brewery a otro DbSet y llamar a SaveChanges una sola vez. EF ya se encarga de que los cambios se realicen todos o ninguno. También te das cuenta de que añadir 3 cervezas implica llamar a SaveChanges 3 veces. Además hay otros problemas: ¿qué pasa si una entidad no puede borrarse? ¿O el borrado es lógico? Liskov todavía se está riendo…

Lo chungo de eso es que te sueles dar cuenta demasiado tarde. Por supuesto podrías quitar el SaveChanges automático que se hace en cada método y añadir un método Save() en el repositorio (dejando así, más claro si cabe, que nunca ha sido un repositorio de verdad). Esto te soluciona el tema de añadir 3 cervezas, pero en el caso de añadir una cerveza y una brewery… ¿Qué método Save llamas? Al del BeerRepository o al del BreweryRepository? Y da gracias que, al menos, nuestro repositorio acepta el DbContext como parámetro, que si lo crease él, entonces sí que date por jodido. Y como digo, te das cuenta demasiado tarde, cuando ya tienes mucho código que depende de ese comportamiento: Annatar se ha quitado la careta y te ha arrastrado a Dol Guldur.

2.2 DAOs o Servicios

Olvidémonos del repositorio y por supuesto del repositorio genérico y centrémonos un poco. La idea que hemos comentado antes en 1.1 de usar un servicio no es mala del todo. En el punto 1.1 lo erróneo era el motivo que exibíamos para su creación, no el propio servicio en sí. Por supuesto si el código tiene que ser como el IBeersService que hemos presentado antes, poco nos aporta, pero su existencia nos permite tener un punto donde colocar esa lógica de negocio y/o consultas que se repiten varias veces.

Tenemos un servicio IBeersService que contiene todos los métodos necesarios para actualizar, borrar, crear y consultar cervezas, junto con la lógica de negocio para asegurar que todo funciona. Vale, el ejemplo de las cervezas se nos queda un poco corto aquí, pero imagina que en lugar de cervezas tratamos con el apasionante mundo de los pedidos (de cervezas, por supuesto). En este caso crear un pedido, supone crear el pedido junto con determinadas líneas de pedido. Las clases Order y OrderLine son como sigue:

public class Order
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Total { get; set; }

    public ICollection<OrderLine> Items { get; set; }
}
public class OrderLine
{
    public int Id { get; set; }
    public int OrderId { get; set; }
    public Order Order { get; set; }
    public string Name { get; set; }

    public int BeerId { get; set; }
    public Beer Beer { get; set; }
    public int Qty { get; set; }

    public decimal LinePrice { get; set; }
}

Y ahora, veamos como puede ser el método AddOrderLine del IOrderService:

public void AddOrderLine(Order order, OrderLine line)
{
    if (line.Order != null)
    {
        throw new ArgumentException("line");
    }
    line.LinePrice = line.Beer.Price * line.Qty;
    if (line.Qty > NeededItemsForDiscount)
    {
        line.LinePrice = line.LinePrice * DiscountPercent;
    }
    order.Items.Add(line);
}

Este método ya gestiona una determinada lógica (en este caso descuentos si el número de elementos comprados sobrepasa una cierta cantidad). Este método además es totalmente testeable, ya que no depende de la base de datos para nada (trabaja solo con las entidades). Así, mientras agreguemos líneas a los pedidos usando el método AddOrderLine todo funcionará correctamente.

Es cierto que en términos de DDD tenemos lo que se llama un modelo anémico, pero si somos conscientes de ello y nos está bien pues adelante.

A nivel de dependencias, por supuesto que el IOrdersService depende del contexto de EF y en algunos métodos hará uso de él. P. ej. en un supuesto método que nos devuelva todos los pedidos que incluyan una determinada cerveza (en este caso _db) es el BeersDbContext:

public async Task<IEnumerable<Order>> GetOrdersByBeer(int beerId)
{
    var data = await _db.OrderLines
        .Where(ol => ol.BeerId == beerId)
        .Select(ol => ol.Order)
        .Distinct()
        .ToListAsync();
    return data;
}

A nivel de pruebas unitarias, lo mismo que antes: para probar este método (consulta) lo mejor es un test de integración, con EF “de verdad” atacando una BBDD “de verdad”. Es una consulta… poca lógica hay en las consultas.

2.3 Query objects y comandos

Vamos ahora a dar un pequeño paso más, conceptualmente simple, pero a la práctica muy potente. Vamos a separar nuestro servicio en un conjunto de objetos.

De esos objetos unos se encargarán de las consultas y los otros de las modificaciones. A los primeros los llamaremos query objects (QO) y a los segundos comandos.

2.3.1 Query Objects

A la práctica usar QO significa que en lugar de tener el método GetOrdersByBeer tienes un objeto que se encarga de esa consulta:

public class OrdersByBeerQuery : IOrdersByBeerQuery
{

    private readonly BeersDbContext _db;
    public OrdersByBeerQuery(BeersDbContext db) => _db = db;
    public async Task<IEnumerable<Order>> GetOrdersByBeer(int beerId)
    {
        var data = await _db.OrderLines
            .Where(ol => ol.BeerId == beerId)
            .Select(ol => ol.Order)
            .Distinct()
            .ToListAsync();
        return data;
    }
}

(La interfaz no sería estrictamente necesaria).

Luego puedo tener un controlador como el siguiente:

[Route("api/[controller]")]
public class BeersController : Controller
{
    private readonly BeersDbContext _db;
    public BeersController(BeersDbContext db) =>  _db = db;
    [HttpGet("{beerid}/orders")]
    public async Task<IActionResult> GetOrders(int beerid)
    {
        var query = new OrdersByBeerQuery(_db);
        var data = await query.GetOrdersByBeer(beerid);
        return Ok(data);
    }
}

Todo muy bonito y encapsulado. Excepto por el hecho de que debemos inyectar el contexto al controlador para pasarlo a cada query object que se crea. Si no quieres hacer esto puedes inyectar en el controlador el query object y inyectar el contexto en cada query object. El único problema es que si un controlador hace 5 consultas a lo mejor requiere 5 query objects y eso son 5 parámetros en el constructor. Hay dos alternativas para eso:

  1. Agrupas todos los query objects en uno (un OrdersQueries que tenga varios métodos, uno por consulta). En este caso, en el controlador inyectas el OrderQueries y en el OrderQueries inyectas el contexto.
  2. Creas una factoría que te devuelva los query objects. Inyectas la factoría en el controlador y el contexto en la factoría. La factoría usa este contexto para crear los query objects.

Y sobre testing… pues lo de siempre: son consultas. No hay lógica. Tests de integración.

2.3.1.1 ¿Qué devolver? Entidades? DTOs?

Esa pregunta es otro clásico. ¿Qué debemos devolver desde los query objects al controlador? Una respuesta rápida es las entidades de base de datos  (como en nuestro ejemplo) pero debemos tener presente que podemos tener consultas que requieran solo parte de los datos y en este caso igual deberíamos proyectar solo los datos necesarios. P. ej. podemos tener una consulta BeersForHomePage que devuelva solo nombre e Id, otra consulta BeersForCheckinPage que devuelva más detalles y otra consulta BeersForShopping que devuelva solo Id, Name y Price.

Si devolvemos siempre un IEnumerable<Beer> para todas esas consultas significa que de la BBDD nos hemos traído todas las columnas de la entidad. Si nos queremos traer de la BBDD solo las columnas requeridas entonces podemos usar un DTO:

public class OrdersForListPageQuery
{
    private readonly BeersDbContext _db;
    public OrdersForListPageQuery(BeersDbContext db) => _db = db;

    public IEnumerable<OrderForListDto> GetAll()
    {
        return _db.Orders.Select(o => new OrderForListDto
        {
            Name = o.Name,
            Id = o.Id,
            Price = o.Total
        }).ToList();
    }
}

El hándicap es que puedes terminar con muchos DTOs si tienes muchas consultas que solo muestran un subconjunto de los campos de la entidad.

Otra opción que evita crear esos DTOs es usar dynamic como tipo de retorno de tus query objects. Así, el método GetAll puede devolver IEnumerable<dynamic> y en el Select proyectas sobre un objeto anónimo. El problema es que luego el compilador no puede ayudarte más.

Es una decisión de esas en la que es fácil tener la sensación de que hagas lo que hagas deberías haberlo hecho distinto 😛

2.3.2 Comandos

Los comandos incluyen todo lo que no son QOs. Es decir cualquier modificación del estado del sistema. La idea a priori es sencilla:

public class AddBeerCommand
{
    private readonly BeersDbContext _db;
    public AddBeerCommand(BeersDbContext ctx) => _db = ctx;

    public Task Execute(Beer beer)
    {
        _db.Beers.Add(beer);
        return _db.SaveChangesAsync();
    }
}

El problema principal es que estamos mezclando dos cosas en esta clase: los datos del comando y la ejecución del comando. En este caso los datos del comando son la cerveza que añadimos (modelada con el parámetro a Execute) y la ejecución del método es el método Execute. Es mucho mejor separarlo y clarificar los conceptos: por un lado el comando y por otro el manejador del comando. El primero es un mero contenedor de datos. El segundo ejecuta la lógica para unos determinado comando.

Por lo tanto, el código nos quedaría como:

public class AddBeerCommand
{
    public Beer BeerToAdd { get; }
    public AddBeerCommand(Beer beerToAdd)
    {
        BeerToAdd = beerToAdd;
    }
}
public class AddBeerCommandHandler
{
    private readonly BeersDbContext _db;
    public AddBeerCommandHandler(BeersDbContext ctx) => _db = ctx;

    public async Task Execute(AddBeerCommand cmd)
    {
        _db.Beers.Add(cmd.BeerToAdd);
        await _db.SaveChangesAsync();
    }
}

Ahora nos falta por ver como sería la acción del controlador:

[Route("api/[controller]")]
public class BeersController : Controller
{
    private readonly BeersDbContext _db;
    public BeersController(BeersDbContext db) =>  _db = db;

    [HttpPost]
    public async Task<IActionResult> AddBeer([FromBody] Beer beer)
    {
        var cmd = new AddBeerCommand(beer);
        var handler = new AddBeerCommandHandler(_db);
        await handler.Execute(cmd);
        return NoContent();
    }
}

Observa que desde el manejador del comando se llama a SaveChanges. Eso puede parecer que nos acarrea el mismo problema que antes con el repositorio (si quiero agregar tres cervezas o una cerveza y una cerveceria debo usar una transacción) pero la diferencia es que los comandos no pretenden ser operaciones unitarias. Es decir, puedes tener un comando que sea “Agregar N cervezas” y otro comando que sea “Agregar cerveza y cervecería”. Así lo lógico es que el guardado final lo realice el manejador del comando cuando haya ejecutado todas las tareas. En general (aunque, por supuesto todo es debatible) suele ser buena idea que una acción del controlador de tu API, termine invocando un solo comando que envuelve toda la lógica de negocio asociada.

Ahora el tema aquí es que tenemos un acoplamiento entre el controlador y el manejador del comando, por lo que el controlador debe conocer qué manejador maneja cada comando. Veamos como solucionar esto. Observa además que en nuestro caso hemos inyectado al controlador el contexto de EF, a pesar de que realmente el controlador no lo usa (pero lo necesita para crear los manejadores de comandos).

2.3.2.1 El bus de comandos (command bus)

El bus de comandos es una pieza que nos permite desacoplar el controlador de los manejadores de comandos. Básicamente es una infrastructura que permite algo parecido a publicación/suscripción: los controladores publican comandos y los manejadores de comando están suscritos a un tipo de comando. Cuando se publica un comando, el bus automáticamente ejecuta el manejador correspondiente. Suena complicado, pero es una infrastructura muy sencilla (aunque como todo, se puede complicar). Hay implementaciones ya hechas como MediatR.

Si usamos un bus de comandos entonces el código en el controlador nos queda más o menos así:

[Route("api/[controller]")]
public class BeersController : Controller
{
    private readonly ICommandBus _bus;
    public BeersController(ICommandBus bus)
    {
        _bus = bus;
    }
    [HttpPost]
    public async Task<IActionResult> AddBeer([FromBody] Beer beer)
    {
        var cmd = new AddBeerCommand(beer);
        await _bus.Publish(cmd);
        await _db.SaveChangesAsync();
        return NoContent();
    }
}

La clave aquí es entender que le pasemos el comando que le pasemos, el bus invocará a su correspondiente manejador. El controlador no debe saber que manejador maneja cada comando.

A nivel de inyección de dependencias, a los manejadores de comandos les inyectamos el contexto, ya que lo necesitan, y al controlador ya no necesitamos inyectarle el contexto de EF: solo el bus de comandos. Eso deja claras las dependencias: el controlador depende del bus de comandos y son los manejadores de comandos quienes dependen del contexto de EF (pues hacen cosas con la BBDD).

A nivel de unit testing, probar los manejadores de comando es relativamente sencillo excepto por un problema: dependen del contexto. Observa el método “Execute” del AddBeerCommandHandler anterior. Este método hace su lógica de negocio y termina llamando a SaveChangesAsync. Esta llamada me impide testear fácilmente el método porque requiere EF. El problema ahora es que en el método Execute del manejador de comandos tenemos mezclado el acceso a datos mediante el contexto de EF y la lógica de negocio asociada (que es lo que queremos probar mediante pruebas unitarias). Debemos desacoplar esas dos partes…

2.3.2.2 Tu amigo, el repositorio

La última pieza que nos falta para solucionar el puzzle es el repositorio. Pero ahora hablamos de un repositorio de verdad, no un DAO camuflado como el infame repositorio que hemos visto antes. Veamos que nos soluciona. Vamos a modificar el manejador de comandos AddBeerCommandHandler para que antes de añadir la cerveza verifique que no exista (y así tenemos una lógica de negocio muy básica que probar):

public class AddBeerCommandHandler
{
    private readonly IBeersRepository _repo;

    public AddBeerCommandHandler(IBeersRepository repo)
    {
        _repo = repo;
    }

    public async Task Execute(AddBeerCommand cmd)
    {
        var beer = await _repo.FindByName(cmd.BeerToAdd.Name);
        if (beer != null)
        {
            throw new Exception($"Beer with name {cmd.BeerToAdd.Name} already exists");
        }
        await _repo.AddBeer(beer);
        await _repo.UnitOfWork.SaveChangesAsync();
    }
}

Ojo a los cambios:

  • Ya no inyectamos el contexto en el manejador de comandos. En su lugar inyectamos un IBeersRepository
  • Usamos métodos del repositorio (FindByName, AddBeer). Es el repositorio quien accede a la BBDD.
  • Llamamos al método SaveChangesAsync de la propiedad UnitOfWork del repositorio.

Para que te hagas una idea, la interfaz IBeersRepository es como sigue:

public interface IBeersRepository
{
    Task AddBeer(Beer beer);
    Task<Beer> FindById(int id);
    Task<Beer> FindByName(string name);
    IUnitOfWork UnitOfWork { get; }
}

El repositorio contiene todas aquellas consultas que son necesarios para los manejadores de comandos y todas las actualizaciones necesarias. Por norma general un manejador de comandos debería apañárselas con métodos FindByXX (en nuestro caso FindByName). El IBeersRepository no está pensado para consultas, de ahí que no haya un GetAll p.ej.

Este repositorio es amigable porque por un lado la implementación de sus métodos nunca llama a SaveChanges. Veamos como estaría implementado:

public class BeersRepository : IBeersRepository
{
    private readonly BeersDbContext _db;
    public BeersRepository(BeersDbContext ctx) => _db = ctx;
    public IUnitOfWork UnitOfWork => _db;
    public Task AddBeer(Beer beer)
    {
        return _db.AddAsync(beer);
    }
    public Task<Beer> FindById(int id)
    {
        return _db.Beers.SingleAsync(b => b.Id == id);
    }
    public Task<Beer> FindByName(string name)
    {
        return _db.Beers.SingleAsync(b => b.Name == name);
    }
}

Aquí no hay llamada alguna SaveChanges. Observa la propiedad UnitOfWork que devuelve… el contexto. En efecto hemos hecho que el contexto implemente la interfaz IUnitOfWork que solo define el SaveChanges:

public interface IUnitOfWork
{
    Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken));
    int SaveChanges();
}

(Observa que hemos definido los métodos con el mismo nombre y firma que los métodos de DbContext. Así solo basta declarar que nuestro contexto implementa IUnitOfWork y no tenemos que añadirle método alguno).

Ahora a nivel de inyección de dependencias, el contexto solo inyecta en los repositorios (y los repositorios a los manejadores de comandos).

¿Qué nos permiten esos cambios? Pues testear fácilmente nuestro manejador de comandos. Para ello tan solo debemos pasarle mocks de los repositorios, que devuelvan los datos que queremos y podemos probar la lógica de negocio:

[Fact]
public async Task Add_Beer_With_Existing_Name_Must_Throw_Exception()
{
    const string name = "Estrella";
    var uow = new Mock<IUnitOfWork>();
    uow.Setup(m => m.SaveChangesAsync(default(CancellationToken))).
        Returns(() => Task.FromResult(0));
    var repo = new Mock<IBeersRepository>();
    repo.Setup(r => r.UnitOfWork).Returns(uow.Object);
    repo.Setup(r => r.FindByName(name)).
        Returns(async () => new Beer()
        {
            Id = 1,
            Name = name,
            Price = 0.5M
        });
    var handler = new AddBeerCommandHandler(repo.Object);
    var ex = await Assert.ThrowsAsync<Exception>(() => handler.Execute(new AddBeerCommand(new Beer() { Name = name })));
}

Observa la diferencia de este test con el que se presentó al principio: en este test estamos validando la lógica de negocio del manejador de comandos. No queremos probar que el repositorio funciona.

La clave aquí está en que el repositorio ahora sí que actúa como tal modificando solo datos en memoria. Es el manejador de comandos quien, al final si todo es correcto, llama a SaveChanges de la “unit of work” (a la práctica, el contexto). Y es normal que lo haga el manejador de comandos porque las operaciones del repositorio son operaciones básicas CRUD, pero el manejador de comandos no representa una operación básica CRUD: representa una operación de negocio. Así que el manejador actúa con sus repositorios (observa que un manejador de comandos podría usar más de un repositorio si fuese necesario) y solo cuando ha efectuado toda la lógica, consolida los datos en base de datos llamando a SaveChanges.

Nota: En nuestro caso hemos hecho que la “unit of work” se obtenga a partir de una propiedad del repositorio, pero se podría inyectar en los manejadores de comando si se quisiera.

Para probar los repositorios en si mismos… pues con tests de integración

3. En el tintero…

Se nos han quedado algunas cosas en el tintero. A partir de lo que tenemos ahora podemos empezar a plantearnos si queremos aplicar más técnicas de DDD. Eso pasaría primero por evitar un modelo anémico (p. ej. el método AddOrderLine podría ser un método de la clase Order). En este caso la lógica de negocio que afecta a una entidad se encuentra en la propia entidad, mientras que la lógica de negocio que afecta a distintos tipos de entidades estaría en los manejadores de comandos.

También podemos empezar a usar agregados. En este caso, los repositorios deberían ser uno por agregado (trabajando siempre con la raíz del agregado). En nuestro caso, no tendríamos nunca un repositorio de OrderLines  porque Order y OrderLines forman un agregado cuya raíz es Order. Así el repositorio trabajaría solo con Order por lo que los manejadores de comando nunca trabajarían con OrderLines y accederían a ellas solo a través de los métodos de la raíz del agregado. Usando agregados podemos querer evitar las propiedades de navegación entre agregados. P. ej. si Beer y OrderLine no son parte del mismo agregado, quizá nos interesa que no exista la propiedad Beer en OrderLine para así, evitar que se pueda actualizar más de un agregado a la vez y garanticemos que un comando actúa solo sobre un agregado.

Si un manejador de comandos actúa solo sobre un agregado y una acción de la API lanza un solo comando, ¿como podemos modelar acciones que se dan sobre varios agregados? Aquí nos puede venir bien usar eventos de dominio, que permiten encadenar acciones que afectan a varios agregados. Y si hay un estado global, compartido por varias acciones (que afecten a uno o más agregados) entonces lo podemos modelar como una saga.

Quizá te interese echar un vistazo a eShop on Containers, que contiene muchas de esas técnicas en acción. Es cierto que quizá la aplicación no es lo suficientemente compleja para justificar algunas de las técnicas aplicadas, pero el objetivo es proporcionar implementaciones “de demo” de esas técnicas, para que puedas ver como funcionan y decidir si quieres aplicarlas o no.

En fin… hay muchas cosas que se nos han quedado en el tintero, pero creo que en el post hemos cubierto varias opciones. Ahora te toca a ti decidir cuales prefieres aplicar en tus proyectos y recuerda: no hay balas de plata.

Saludos!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *