简体   繁体   中英

DDD: Repository, unit of work, ORMs and dependency injection

Edit

What happens if you put 10 domain experts in a room? Right, you got 11 opinions. (Of which 10 of them are declared as anti-patterns )

Thanks everybody for the detailed answers. I'll study them and consider how they help me solving particular problems.


I have a hard time getting my head around repositories & unit of work when together used with ORMs and dependency injection. Consider the following pretty standard interfaces:

public interface IRepository<TAggregateRoot> : ITransientDependency
{

    void Add(TAggregateRoot aggregateRoot);
    void Delete(TAggregateRoot aggregateRoot);
    IEnumerable<TAggregateRoot> GetAll();
}

public interface IUnitOfWork : ITransientDependency
{
    void Commit();
    void Rollback();
}

I have a few scenarios in my mind that I would like to cover with those approaches.

  • Inserting a single entity into a repository
  • Deleting a aggregate where linked entities should be deleted too
  • Doing a transaction over more than 2 repositories

The default implementation using NHibernate might look like this:

public abstract class NHibernateRepository<TAggregateRoot> : IRepository<TAggregate>
{
    protected NHibernateRepository(ISession session) {}
}

public sealed NHibernateUnitOfWork : IUnitOfWork
{
    public NHibernateUnitOfWork(ISession session)
    {}

    public void Commit() {
        _session.Flush();
    }
}

1.Scenario: Inserting a single entity into a repository

// ASP.NET MVC controller, but valid for any
// other arbitary application service
public class MyController : Controller {

    private readonly IPeopleRepository _repository;

    // di -> declaring IPeopleRepository dependency
    public MyController(IPeopleRepository repository) {
        _repository = repository;
    }

    public void AddPerson(Person person) {
        _repository.Add(person);
    }

}

Now, what happens after I added the person to the repository? Right, nothing. Even if a single insert is not exactly a unit of work (a transaction, technical), ORM frameworks like EF and NHibernate still require to Commit the changes to the database, since their fancy sessions and DBContexts are technically unit of work and repositories .

How do I overcome this first problem? Starting a unit of work for everything I do?

2.Scenario: Deleting a aggregate where linked entities should be deleted too

Check out the following aggregate:

public class Person : IAggregateRoot {

    private readonly List<Cat> _cats = new List<Cat>();

    public IEnumerable<Cat> Cats {
        get { return _cats; }
    }

    public void AddCat(Cat cat) {
    //

}

Let's remove the aggregate using it's root via a repository:

IPersonRepository.Remove(person);

Now, all the entities the Person aggregates are technically deleted to. Since there aren't anymore references to them in code, the garbage collector acted as a database manager and removed the Cats from the memory.

But how does this might look like in the ORM repository implementation? Where does Unit of Work come into play?

3.Scenario: Doing a transaction over more than 2 repositories

Okay, here I got my fancy SomethingService. He has to do some stuff over multiple repositories, hence there is clearly a transaction necessary which calls for a unit of work .

public SomethingService : ISomethingService {

    public ISomethingService(IFirstRepo repo1, ISecondRepo repo2, IUnitOfWork uow)
    {
    ...
    }

    public void DoSomething() {
        repo1.AddThis();
        repo2.GetThisOne();
        repo2.BecauseOfTheOneAboveDeleteThis();
        uow.Commit();
    }

}

Looks fine to me, but considering the NHibernate repository and Unit of Work implementations above, this won't work, simply because each (the unit of work, the 2 repositories) have different instances of the NHibernate session!

I considered aspect oriented programming using Interceptors , however this only works partially because the time the IoC intercepts a service method, the repositories have already been created with their own session, thus unable to share the session of the Unit of Work .

How to overcome this problems? Are there any full fledged working examples that run without any dirty hacks? (Eg singleton unit of works)

And just to say: Yes, I want to use repositories along with a ORM. They are a nice way to abstract the framework away and let me design my domain the way I (or my customer) wants it, not like the framework would like to have it.

Thank you very much for reading this wall of text.

Scenario 1:

Suppose that your Controllers are hosted in a web application, and that a single web request should cause an entity to be inserted in the repository. The trick is then to align your IUnitOfWork so that it is created when you start processing the request and Committed when you finish processing the request.

I'm not sure which DI framework you're using, but Unity has an elegant solution for ASP.NET MVC in the form of a PerRequestLifetimeManager . All types registered using this lifetime manager are cached only within the scope of a single web request, and are automatically Disposed when the web request ends. Hence, if you use this lifetime manager and make your unit of work implement IDisposable so that it Commits on a Dispose, you've got that taken care of.

You might have to do a little trickery for not committing when an error occurs, though - possibly in NHibernateUnitOfWork (see below).

Scenario 2:

I'd say that IPersonRepository.Remove(person) should know to explicitly delete all Cats as well. This means that you cannot use a generic repository, which is perfectly fine since it is considered (at least by some) a bit of an anti-pattern .

Scenario 3:

Again, the solution is to use the correct lifetime manager. The reason each repository now gets its own version of ISession is because the DI container considers ISession a transient type. If you use something like PerResolve , or better yet the PerRequest lifetime manager again, it will reuse the same ISession instance for each of your repositories.

Btw, I noticed that your NHibernateRepository depends directly on ISession rather than NHibernateUnitOfWork - is there a specific reason for this? I think I would have NHibernateUnitOfWork expose an ISession property (or maybe even re-expose all of its members), and have all the repositories depend on NHibernateUnitOfWork instead. For one, because the repositories don't just "do something with a session", they're actually part of the unit of work you're doing. For another, it makes it easier to use NHibernateUnitOfWork as a facade in which you can prevent the NHibernate session being actually committed if there was an error or if no modifications were made.

In this case I would have the NHibernateUnitOfWork use the PerRequest lifetime rather than ISession .

Generally speaking, your problems can be solved by placing control of the Unit of Work into the hands of an object that knows about the application's current execution context (Controller, in your case).

Unit of Work is your business/applicative transaction, a lower-level persistence object such as a Repository doesn't know about the overarching context and shouldn't decide when a transaction is finished. Practically speaking, Repos should just have a reference to a UoW in order to be able to add/remove things from it, but not conclude it.

There's this extremely well explained blog post about DBContext and Unit of Work subtleties. It's about Entity Framework's DBContext, but you can easily translate it in terms of NHibernate's Session.

Your services must be the sole components in your application responsible for calling the DbContext.SaveChanges() method at the end of a business transaction. Should other parts of the application call the SaveChanges() method (eg repository methods), you will end up with partially committed changes, leaving your data in an inconsistent state.

As for Scenario 3, you should avoid business transactions that span across multiple Aggregates as much as possible. It may be a good idea to design your Aggregates precisely as transactional consistency boundaries .

If you still need to affect one Aggregate as a result of changes in another, Eventual Consistency can come to the rescue. It can be achieved, for instance, by having the first Aggregate emit a Domain Event, then an event handler (synchronously or asynchronously) gets it and calls the second Aggregate in a separate UoW.

1. The unit of work handling can be done bit differently,


According to what you have proposed the session is injected into the repository and I would not go on that path, instead I would use the sessionFactory

public interface IUnitOfWork : IDisposable
{
    ISession CurrentSession { get; }
    void Commit();
    void Rollback();
}

public class NHibernateUnitOfWork : IUnitOfWork
{
    private readonly ISessionFactory _sessionFactory;

    [ThreadStatic]
    private ISession _session;

    [ThreadStatic]
    private ITransaction _transaction;

    public NHibernateUnitOfWork(ISessionFactory sessionFactory)
    {
        _sessionFactory = sessionFactory;
        _session = _sessionFactory.OpenSession();
        _transaction = _session.BeginTransaction();
    }

    public static ISession CurrentSession { get { return _session; } }

    public void Dispose()
    {
        _transaction = null;
        _session.Close();
        _session = null;
    }

    public void Commit()
    {
        _transaction.Commit();
    }

    public void Rollback()
    {
        if (_transaction.IsActive) _transaction.Rollback();
    }
}

public class Repository : IRepository
{
    public void Add(IObj obj)
    {
        if (NHibernateUnitOfWork.CurrentSession == null)
            throw new Exception("No unit of work present");

        NHibernateUnitOfWork.CurrentSession.Save(obj);         
    }
}

// ASP.NET MVC controller, but valid for any
// other arbitary application service
public class MyController : Controller 
{
    private readonly IPeopleRepository _repository;

    // di -> declaring IPeopleRepository dependency
    public MyController(IPeopleRepository repository) {
        _repository = repository;
    }

    public void AddPerson(Person person) 
    {
        using (IUnitOfWork uow = new NHibernateUnitOfWork())
        {
            try
            { 
                _repository.Add(person);
                uow.Commit();
            }
            catch(Exception ex)
            {
               uow.RollBack();
            }
        }
    }
 }

Although this is one way to deal with the issue there are ways to do it smarter, one is to use a ActionFilter which starts transaction before action and commits if all successful, or you can go for a HttpModule which takes care of the transaction handling..

Or

You could go for a total different path and implement the command pattern where every action is a command regardless how complex it is and handler should start and commit transaction have a look at https://fnhmvc.codeplex.com/

2. If proper mapping used and proper UOW used to delete the parent entity child entities will be deleted automatically

3. If above mentioned Unit of work pattern used, this would not be a problem

public SomethingService : ISomethingService { public ISomethingService(IFirstRepo repo1, ISecondRepo repo2, IUnitOfWork uow) { ... }

public void DoSomething() 
{
    using (IUnitOfWork uow = new NHibernateUnitOfWork())
    {
        try
        { 
            repo1.AddThis();
            repo2.GetThisOne();
            repo2.BecauseOfTheOneAboveDeleteThis();
            uow.Commit();
        }
        catch(Exception ex)
        {
           uow.RollBack();
        }
    }
}

I'll try to answer.

1.Scenario: Inserting a single entity into a repository

You may add method void SaveChanges() to your repository interface, that's ok.

2.Deleting a aggregate where linked entities should be deleted to

There are many approaches to do that. First, you should decide is aggregatre root or repository responsible for deletion of associated objects?

If it is aggregate root responsibility, than

  1. You need to populate IAggregateRoot with void OnDelete()
  2. Call it in repository before deletion.
  3. Implementations of OnDelete should manually delete all associated objects, thus you need to
  4. Define IDeleteStrategy which will work like Only-Delete-Repository. Difference is in IDeleteStrategy methods argument restrictions - it must handle Entities (!), not only aggregate roots. And repositories can reuse it by delegating delete operation to it.
  5. Finally you should incorporate IDeleteStrategy into aggregate root for every entity type you want to delete. Yes, it is dependency injection in aggregate root.

This solution is good because:

  1. Aggregate root manages aggregate insides, thus no aggregate encapsulation violations.
  2. Aggregate root can throw exception if some inner aggregate conditions are met, and thus deletion will not be completed.
  3. Probably, you can hold your repositories more simple and generic (as for me and according to DDD, it is very good)

If it is repository responsibility, than you should create non-generic repositories and realize custom deletion logic for every aggregate.

And last one. There is a principle which tells us that creator of object is responsible for it's lifetime. Thus, probably Factory is a good place to clear aggregate inside.

3.Scenario: Doing a transaction over more than 2 repositories

There is a techincal limitation, which requires to use single DbConnection to perform transactions/UoW. Solutions is strongly depends on technologies you use. So, the only way I can quickly propose is to force your environment use single context (EF?)/connection/session per request/user session.

Another one possibility is in implementation of some kind of Transactions level (between domain and DAL), but also it depends on technology you use.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM