Leaky abstraction and clean architecture template

According to Wikipedia in software development, a leaky abstraction is an abstraction that leaks details that it is supposed to abstract away. The term "leaky abstraction" was popularized in 2002 by Joel Spolsky. An earlier paper by Kiczales describes some of the issues with imperfect abstractions and presents a potential solution to the problem by allowing for the customization of the abstraction itself.

As systems become more complex, software developers must rely upon more abstractions. Each abstraction tries to hide complexity, letting a developer write software that "handles" the many variations of modern computing.
However, this law claims that developers of reliable software must learn the abstraction's underlying details anyway.

During the past year, I've seen several implementations of clean architecture that inspired form Jason Taylor Clean Architecture Solution Template and all of them have a common interface, IApplicationDbContext. This interface aims to hide the underlying data access technology that is being used but when you look at the interface you notice that the interface is coupled to Entity Framework Core. I also can guess Jason Taylor assumed that you are always using EF Core and it never going to change.

Let's see leaked abstraction in action. I want to implement a unit test for UpdateTodoListCommandHandler and assume it is a real-world project and we some logic inside the Handle method but we don't implement integration for each scenario. Here is the actual implementation of :

namespace CleanArchitecture.Application.TodoLists.Commands.UpdateTodoList
{
    public class UpdateTodoListCommand : IRequest
    {
        public int Id { get; set; }

        public string Title { get; set; }
    }

    public class UpdateTodoListCommandHandler : IRequestHandler<UpdateTodoListCommand>
    {
        private readonly IApplicationDbContext _context;

        public UpdateTodoListCommandHandler(IApplicationDbContext context)
        {
            _context = context;
        }

        public async Task<Unit> Handle(UpdateTodoListCommand request, CancellationToken cancellationToken)
        {
            var entity = await _context.TodoLists.FindAsync(request.Id);

            if (entity == null)
            {
                throw new NotFoundException(nameof(TodoList), request.Id);
            }

            entity.Title = request.Title;

            await _context.SaveChangesAsync(cancellationToken);

            return Unit.Value;
        }
    }
}

I want to ensure when the entity does not exist, NotFoundException is thrown.

[Test]
public void Update_TodoList_Throws_Exception_When_Entity_Does_Not_Exist()
{
    // Arrange
    var list = new List<TodoList>();
    var queryable = list.AsQueryable();

    var dbSet = new Mock<DbSet<TodoList>>();
    dbSet.As<IQueryable<TodoList>>().Setup(m => m.Provider).Returns(queryable.Provider);
    dbSet.As<IQueryable<TodoList>>().Setup(m => m.Expression).Returns(queryable.Expression);
    dbSet.As<IQueryable<TodoList>>().Setup(m => m.ElementType).Returns(queryable.ElementType);
    dbSet.As<IQueryable<TodoList>>().Setup(m => m.GetEnumerator()).Returns(() => queryable.GetEnumerator());
    dbSet.Setup(d => d.FindAsync(It.IsAny<object[]>())).ReturnsAsync((object[] id) => list.SingleOrDefault(t => t.Id == (int)id[0]));

    var dbContext = new Mock<IApplicationDbContext>();
    dbContext.SetupGet(d => d.TodoLists).Returns(dbSet.Object);

    var sut = new UpdateTodoListCommandHandler(dbContext.Object);
    var command = new UpdateTodoListCommand { Id = 1, Title = "Test" };

    // Act
    var exception = Assert.ThrowsAsync<NotFoundException>(() => sut.Handle(command, new CancellationToken()));

    // Assert
    Assert.NotNull(exception);
}

As you can see I tried to mock IApplicationDbContext and the interface should help to hide underlying implementation but to implement such a test you should know how to mock DbSet. If you use the ApplicationDbContext class instead of IApplicationDbContext interface, the result will be the same and same amount of code is need to mock ApplicationDbContext class.

Instead, we can use repository pattern and hide underlying technology and mock it easily.
Lets replace IApplicationDbContext with ITodoListRepository.

public interface ITodoListRepository
{
    Task<TodoList> GetByIdAsync(int id);

    Task UpdateAsync(TodoList todoList);
}

And the handler:

public class UpdateTodoListCommandHandler : IRequestHandler<UpdateTodoListCommand>
{
    private readonly ITodoListRepository _repository;

    public UpdateTodoListCommandHandler(ITodoListRepository repository)
    {
        _repository = repository;
    }

    public async Task<Unit> Handle(UpdateTodoListCommand request, CancellationToken cancellationToken)
    {
        var entity = await _repository.GetByIdAsync(request.Id);

        if (entity == null)
        {
            throw new NotFoundException(nameof(TodoList), request.Id);
        }

        entity.Title = request.Title;

        await _repository.UpdateAsync(entity);

        return Unit.Value;
    }
}

Now we can refactor the test method:

[Test]
public void Test1()
{
    // Arrange
    var list = new List<TodoList>();

    var repository = new Mock<ITodoListRepository>();
    repository.Setup(r => r.GetByIdAsync(It.IsAny<int>())).ReturnsAsync((int id) => list.SingleOrDefault(t => t.Id == id));

    var command = new UpdateTodoListCommand { Title = "Test", Id = 1 };
    var sut = new UpdateTodoListCommandHandler(repository.Object);

    // Act
    var exception = Assert.ThrowsAsync<NotFoundException>(async () => await sut.Handle(command, new CancellationToken()));

    // Act
    Assert.NotNull(exception);
    Assert.AreEqual(typeof(NotFoundException), exception.GetType());
}

16