Onion Architecture, Unit of Work and a generic Repository pattern

六月ゝ 毕业季﹏ 提交于 2019-11-28 17:17:43
Ilya Palkin

I can say that this code is good enough for the first time try but it does have some places to improve.

Let's go through some of them.

1. Dependency injection (DI) and usage of IoC.

You use the simplest version of Service Locator pattern - container instance itself.

I suggest you use 'constructor injection'. You can find more information here (ASP.NET MVC 4 Dependency Injection).

public class CustomerController : Controller
{
    private readonly IUnitOfWork unitOfWork;
    private readonly ICustomerRepository customerRepository;

    public CustomerController(
        IUnitOfWork unitOfWork, 
        ICustomerRepository customerRepository)
    {
        this.unitOfWork = unitOfWork;
        this.customerRepository = customerRepository;
    }

    public ActionResult List()
    {
        return View(customerRepository.Query());
    }

    [HttpPost]
    public ActionResult Create(Customer customer)
    {
        customerRepository.Add(customer);
        unitOfWork.SaveChanges();
        return RedirectToAction("List");
    }
}

2. Unit of Work (UoW) scope.

I can't find lifestyle of IUnitOfWork and ICustomerRepository. I am not familiar with Unity but msdn says that TransientLifetimeManager is used by default. It means that you'll get a new instance every time when you resolve type.

So, the following test fails:

[Test]
public void MyTest()
{
    var target = new UnityContainer();
    target.RegisterType<IUnitOfWork, EntityFrameworkUnitOfWork>();
    target.RegisterType<ICustomerRepository, CustomerRepository>();

    //act
    var unitOfWork1 = target.Resolve<IUnitOfWork>();
    var unitOfWork2 = target.Resolve<IUnitOfWork>();

    // assert
    // This Assert fails!
    unitOfWork1.Should().Be(unitOfWork2);
} 

And I expect that instance of UnitOfWork in your controller differs from the instance of UnitOfWork in your repository. Sometimes it may be resulted in bugs. But it is not highlighted in the ASP.NET MVC 4 Dependency Injection as an issue for Unity.

In Castle Windsor PerWebRequest lifestyle is used to share the same instance of type within single http request.

It is common approach when UnitOfWork is a PerWebRequest component. Custom ActionFilter can be used in order to invoke Commit() during invocation of OnActionExecuted() method.

I would also rename the SaveChanges() method and call it simply Commit as it is called in the example and in the PoEAA.

public interface IUnitOfWork : IDisposable
{
    void Commit();
}

3.1. Dependencies on repositories.

If your repositories are going to be 'empty' it is not needed to create specific interfaces for them. It is possible to resolve IRepository<Customer> and have the following code in your controller

public CustomerController(
    IUnitOfWork unitOfWork, 
    IRepository<Customer> customerRepository)
{
    this.unitOfWork = unitOfWork;
    this.customerRepository = customerRepository;
}

There is a test that tests it.

[Test]
public void MyTest()
{
    var target = new UnityContainer();
    target.RegisterType<IRepository<Customer>, CustomerRepository>();

    //act
    var repository = target.Resolve<IRepository<Customer>>();

    // assert
    repository.Should().NotBeNull();
    repository.Should().BeOfType<CustomerRepository>();
}

But if you would like to have repositories that are 'layer of abstraction over the mapping layer where query construction code is concentrated.' (PoEAA, Repository)

A Repository mediates between the domain and data mapping layers, acting like an in-memory domain object collection. Client objects construct query specifications declaratively and submit them to Repository for satisfaction.

3.2. Inheritance on EntityFrameworkRepository.

In this case I would create a simple IRepository

public interface IRepository
{
    void Add(object item);

    void Remove(object item);

    IQueryable<T> Query<T>() where T : class;
}

and its implementation that knows how to work with EntityFramework infrastructure and can be easily replaced by another one (e.g. AzureTableStorageRepository).

public class EntityFrameworkRepository : IRepository
{
    public readonly EntityFrameworkUnitOfWork unitOfWork;

    public EntityFrameworkRepository(IUnitOfWork unitOfWork)
    {
        var entityFrameworkUnitOfWork = unitOfWork as EntityFrameworkUnitOfWork;

        if (entityFrameworkUnitOfWork == null)
        {
            throw new ArgumentOutOfRangeException("Must be of type EntityFrameworkUnitOfWork");
        }

        this.unitOfWork = entityFrameworkUnitOfWork;
    }

    public void Add(object item)
    {
        unitOfWork.GetDbSet(item.GetType()).Add(item);
    }

    public void Remove(object item)
    {
        unitOfWork.GetDbSet(item.GetType()).Remove(item);
    }

    public IQueryable<T> Query<T>() where T : class
    {
        return unitOfWork.GetDbSet<T>();
    }
}

public interface IUnitOfWork : IDisposable
{
    void Commit();
}

public class EntityFrameworkUnitOfWork : IUnitOfWork
{
    private readonly DbContext context;

    public EntityFrameworkUnitOfWork()
    {
        this.context = new CustomerContext();
    }

    internal DbSet<T> GetDbSet<T>()
        where T : class
    {
        return context.Set<T>();
    }

    internal DbSet GetDbSet(Type type)
    {
        return context.Set(type);
    }

    public void Commit()
    {
        context.SaveChanges();
    }

    public void Dispose()
    {
        context.Dispose();
    }
}

And now CustomerRepository can be a proxy and refer to it.

public interface IRepository<T> where T : class
{
    void Add(T item);

    void Remove(T item);
}

public abstract class RepositoryBase<T> : IRepository<T> where T : class
{
    protected readonly IRepository Repository;

    protected RepositoryBase(IRepository repository)
    {
        Repository = repository;
    }

    public void Add(T item)
    {
        Repository.Add(item);
    }

    public void Remove(T item)
    {
        Repository.Remove(item);
    }
}

public interface ICustomerRepository : IRepository<Customer>
{
    IList<Customer> All();

    IList<Customer> FindByCriteria(Func<Customer, bool> criteria);
}

public class CustomerRepository : RepositoryBase<Customer>, ICustomerRepository
{
    public CustomerRepository(IRepository repository)
        : base(repository)
    { }

    public IList<Customer> All()
    {
        return Repository.Query<Customer>().ToList();
    }

    public IList<Customer> FindByCriteria(Func<Customer, bool> criteria)
    {
        return Repository.Query<Customer>().Where(criteria).ToList();
    }
}

The only con I see is that you are highly dependent on your IOC tool, so make sure your implementation is solid. However, this is not unique to Onion designs. I have used Onion on a number of projects and have not run into any real 'gotchas".

I see couple of serious problems in code.

The 1st problem is relashionship between repositories and UoW.

    var unitOfWork = container.Resolve<IUnitOfWork>();
    var customerRepository = container.Resolve<ICustomerRepository>();

Here is implicit dependency. Repository will not work itself without UoW! Not all repositories needs to be connected with UoW. For example what about stored procedures? You have stored procedure and you hide it behind repository. Stored procedure invokation uses separate transaction! At least not in all cases. So if I resolve the only repository and add item then it will not work. Moreover this code will not work if I set Transient life license because repository will have another UoW instance. So we have tight implicit coupling.

The 2nd problem you create tight coupling between DI container engine and use it as service locator! Service locator is not good approach to implement IoC and aggregation. In some case it is anti pattern. DI container should be used

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!