In a very simple application
In some applications, the domain model and the database entities are identical, and there is no need to do any data mapping between them. Let's call them "domain entities". In such applications, the DbContext
can act both as a repository and a unit of work simultaneously. Instead of doing some complicated patterns, we can simply use the context:
public class CustomerController : Controller
{
private readonly CustomerContext context; // injected
[HttpPost]
public IActionResult Update(CustomerUpdateDetails viewmodel)
{
// [Repository] acting like an in-memory domain object collection
var person = context.Person.Find(viewmodel.Id);
// [UnitOfWork] keeps track of everything you do during a business transaction
person.Name = viewmodel.NewName;
person.AnotherComplexOperationWithBusinessRequirements();
// [UnitOfWork] figures out everything that needs to be done to alter the database
context.SaveChanges();
}
}
Complex queries on larger apps
If your application gets more complex, you'll start writing some large Linq queries in order to access your data. In that situation, you'll probably need to introduce a new layer that handle these queries, in order to prevent yourself from copy pasting them across your controllers. In that situation, you'll end up having two different layers, the unit of work pattern implemented by the DbContext
, and the repository pattern that will simply provide some Linq results executing over the former. Your controller is expected to call the repository to get the entities, change their state and then call the DbContext to persist the changes to the database, but proxying the DbContext.SaveChanges()
through the repository object is an acceptable approximation:
public class PersonRepository
{
private readonly PersonDbContext context;
public Person GetClosestTo(GeoCoordinate location) {} // redacted
}
public class PersonController
{
private readonly PersonRepository repository;
private readonly PersonDbContext context; // requires to Equals repository.context
public IActionResult Action()
{
var person = repository.GetClosestTo(new GeoCoordinate());
person.DoSomething();
context.SaveChanges();
// repository.SaveChanges(); would save the injection of the DbContext
}
}
DDD applications
It gets more interesting when domain models and entities are two different group of classes. This will happen when you will start implementing DDD, as this requires you to define some aggregates, which are clusters of domain objects that can be treated as a single unit. The structure of aggregates does not always perfectly map to your relational database schema, as it can provides multiple level of abstractions depending on the use case you're dealing with.
For instance, an aggregate may allow a user to manage multiple addresses, but in another business context you'll might want to flatten the model and limit the modeling of the person's address to the latest value only:
public class PersonEntity
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public bool IsValid { get; set; }
public ICollection<AddressEntity> Addresses { get; set; }
}
public class AddressEntity
{
[Key]
public int Id { get; set; }
public string Value { get; set; }
public DateTime Since { get; set; }
public PersonEntity Person { get; set; }
}
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public string CurrentAddressValue { get; private set; }
}
Implementing the unit of work pattern
First let's get back to the definition:
A unit of work keeps track of everything you do during a business transaction that can affect the database. When you're done, it figures out everything that needs to be done to alter the database as a result of your work.
The DbContext
keeps tracks of every modification that happens to entities and will persist them to the database once you call the SaveChanges()
method. Like in the simpler example, unit of work is exactly what the DbContext
does, and using it as a unit of work is actually how Microsoft suggest you'd structure a .NET application using DDD.
Implementing the repository pattern
Once again, let's get back to the definition:
A repository mediates between the domain and data mapping layers, acting like an in-memory domain object collection.
The DbContext
, cannot act as a repository. Although it behaves as an in-memory collection of entities, it does not act as an in-memory collection of domain objects. In that situation, we must implement another class for the repository, that will act as our in-memory collection of domain models, and will map data from entities to domain models. However, you will find a lot of implementations that are simply a projection of the DbSet in the domain model and provide IList
-like methods that simply maps entities back and reproduce the operations on the DbSet<T>
.
Although this implementation might be valid in multiple situations, it overemphasizes over the collection part, and not enough on the mediator part of the definition.
A repository is a mediator between the domain layer and the infrastructure layer, which means its interface is defined in the domain layer. Methods described in the interface are defined in the domain layer, and they all must have a meaning in the business context of the program. Ubiquitous language being a central concept of DDD, these methods must provide a meaningful name, and perhaps "adding a person" is not the right business way to name this operation.
Also, all persistence-related concepts are strictly limited to the implementation of the repository. The implementation defines how a given business operation translates in the infrastructure layer, as a series of entities manipulation that will eventually be persisted to the database through an atomic database transaction. Also note that the Add
operation on a domain model does not necessarily implies an INSERT
statement in the database and a Remove
will sometimes end up in an UPDATE
or even multiple INSERT
statements !
Actually, here is a pretty valid implementation of a repository pattern:
public class Person
{
public void EnsureEnrollable(IPersonRepository repository)
{
if(!repository.IsEnrollable(this))
{
throw new BusinessException<PersonError>(PersonError.CannotEnroll);
}
}
}
public class PersonRepository : IPersonRepository
{
private readonly PersonDbContext context;
public IEnumerable<Person> GetAll()
{
return context.Persons.AsNoTracking()
.Where(person => person.Active)
.ProjectTo<Person>().ToList();
}
public Person Enroll(Person person)
{
person.EnsureEnrollable(this);
context.Persons.Find(person.Id).Active = true;
context.SaveChanges(); // UPDATE statement
return person;
}
public bool IsEnrollable(Person person)
{
return context.Persons.Any(entity => entity.Id == person.Id && !entity.Active);
}
}
Business transaction
You're saying a purpose of using unit of work is to form a Business Transaction, which is wrong. The purpose of the unit of work class is to keeps track of everything you do during a business transaction that can affect the database, to alter the database as a result of your work in an atomic operation. The repositories do share the unit of work instances, but bear in mind that dependency injection usually uses a scoped lifetime manager when injecting dbcontext. This means that instances are only shared within the same http request context, and different requests will not share changes tracking. Using a singleton lifetime manager will share instances among different http request which will provoke havoc in your application.
Calling the unit of work save changes method from a repository is actually how you are expected to implementation a DDD application. The repository is the class that knows about the actual implementation of the persistence layer, and that will orchestrate all database operations to commit/rollback at the end of transaction. Saving changes from another repository when calling save changes is also the expected behavior of the unit of work pattern. The unit of work accumulates all changes made by all repositories until someone calls a commit or a rollback. If a repository makes changes to the context that are not expected to be persisted in the database, then the problem is not the unit of work persisting these changes, but the repository doing these changes.
However, if your application does one atomic save changes that persists change operations from multiple repositories, it probably violates one of the DDD design principles. A repository is a one-to-one mapping with an aggregate, and an aggregate is a cluster of domain objects that can be treated as a single unit. If you are using multiple repositories, then you are trying to modify multiple units of data in a single transaction.
Either your aggregate is designed too small, and you need to make a larger one that holds all data for your single transaction, with a repository that will handle all that data in a single transaction ; either you're trying to make a complex transaction that spans over a wide part of your model, and you will need to implement this transaction with eventual consistency.