Domain Driven Design with Web API revisited Part 8: the abstract repository
August 31, 2015 1 Comment
Introduction
In the previous post we finished working on our domain layer. We have built the aggregate root of our load test aggregate, i.e. the Timetable object. It is meant to act as an umbrella for Loadtest-related operations. We can also consider it as a domain service. External callers that wish to interact with load tests must do so through the Timetable object.
However, that is not the entire story as far as data storage is concerned. In this post we’ll start discussing repositories.
Repositories and the repository pattern
The repository pattern helps us hide the implementation details of the concrete data access layer behind an abstraction. It is also a means of expressing what a domain – an aggregate root – will allow the external users to perform on it.
We must abstract away the implementation details of the data access technology we use so that we can easily switch strategies later if necessary. We cannot let any technology-specific implementation bubble up from the data access layer into other layers of the solution. We’ll soon discover, however, that this rule is not always possible to maintain to 100%.
Another benefit of the pattern is that other layers in the solution won’t simply bypass the validation rules and other logic built into the domain classes.
These data storage implementation details include the object context in EntityFramework, MongoClient and MongoServer in MongoDb .NET, the objects related to the file system in a purely file based data access solution etc., you probably get the idea. We must therefore make sure that no other layer will be dependent on the concrete data access solution.
For a more detailed example of how a technology-centred solution can go wrong in practice you can view the first part of the original DDD series available here.
We built quite a complex abstract – and concrete for that matter – repository in the original DDD series. We had abstractions for the following elements:
- The unit of work
- The unit of work repository
- The repository
Revised abstract repository for the Loadtest demo
You can find the post I’m referring to here.
In this revised series we’ll go for a much more simplified case. We’ll only abstract away the repository. The concrete repository implementation will be based on EntityFramework (EF) which already has a powerful unit of work, i.e. the object context, or database context.
What is the unit of work?
The purpose of a Unit of Work is to maintain a list of objects that have been modified by a transaction. These modifications include insertions, updates and deletions. The Unit of Work co-ordinates the persistence of these changes and also checks for concurrency problems, such as the same object being modified by different threads. Here’s a possible abstraction for the unit of work:
public interface IUnitOfWork { void RegisterUpdate(IAggregateRoot aggregateRoot); void RegisterInsertion(IAggregateRoot aggregateRoot); void RegisterDeletion(IAggregateRoot aggregateRoot); void Commit(); }
EF offers a great implementation for all of those methods.
What is the unit of work repository?
Notice that we have separated the registration and commit actions in the above abstraction. In the case of EF and similar ORM technologies the object that registers and persists the changes will often be the same: the object context or a similar object.
However, this is not always the case. It is perfectly reasonable that you are not fond of these automation tools and want to use something more basic where you have the freedom of specifying how you track changes and how you persist them: you may register the changes in memory and persist them in a file. Or you may still have a lot of legacy ADO.NET where you want to move to a more modern layered architecture. ADO.NET lacks a DbContext object so you may have to solve the registration of changes in a different way. Here’s a possible abstraction for this purpose:
public interface IUnitOfWorkRepository { void PersistInsertion(IAggregateRoot aggregateRoot); void PersistUpdate(IAggregateRoot aggregateRoot); void PersistDeletion(IAggregateRoot aggregateRoot); }
Again, the EF object context can handle these operations easily. EF will be the #1 concrete repository choice for many .NET developers so hopefully this demo will be more useful for them than what we saw in the original DDD demo. Admittedly the original solution was cumbersome for an EF repository implementation.
Normally technological questions should not be central in a DDD project but selecting the repository technology is an important design choice. We mentioned above that the repository pattern can help you switch between concrete data store implementations, like switching between MS SQL and MongoDB. However, that is usually an unlikely scenario in practice for a software already in production. Transferring millions of rows from one data store to another and preserve the validity of all the data is a difficult task. However, switching between data store solutions can be handy in the beginning of a new project if you’re not sure which data store type to go for. You might want to test the parameters of the candidate solutions before deciding which one to go for.
Therefore, even if we go for a simpler solution in the demo we’ll try to design the abstractions and the implementations so that we can still switch between concrete implementations at ease using an IoC (inversion of control) container.
Repository abstractions
Abstract repositories are part of the Domain layer. As we said, they express what external callers are allowed to perform on a domain object as far as data storage is concerned. Here comes an example of a read-only repository which doesn’t allow any modifications:
public interface IReadOnlyRepository<AggregateType, IdType> where AggregateType : IAggregateRoot { AggregateType FindBy(IdType id); IEnumerable<AggregateType> FindAll(); }
And here’s a full CRUD repository:
public interface IRepository<AggregateType, IdType> : IReadOnlyRepository<AggregateType, IdType> where AggregateType : IAggregateRoot { void Update(AggregateType aggregate); void Insert(AggregateType aggregate); void Delete(AggregateType aggregate); }
The above interfaces will be very handy in an administration type of bounded context. The administrative application would be responsible for viewing, adding, removing and modifying the domain objects. Also, keep in mind that these interfaces only expose the most common actions that are applicable across all aggregate roots. You’ll be able to specify domain-specific actions in domain-specific implementations of these repositories, such as FindByName, or InsertMany etc. The domain-specific repositories will only be valid for the domain and not for all of them.
However, our bounded context is not an administrative one. We want to carefully control the access to the load testing data store through the Timetable repository. The Loadtest object won’t have any repository. It’s not the aggregate root and we want to force the callers to go through its umbrella object which contains the necessary validation logic for entries and updates.
Insert the following interface into the Domain layer:
public interface ITimetableRepository { IList<Loadtest> GetLoadtestsForTimePeriod(DateTime searchStartDateUtc, DateTime searchEndDateUtc); void AddOrUpdateLoadtests(AddOrUpdateLoadtestsValidationResult addOrUpdateLoadtestsValidationResult); void DeleteById(Guid guid); }
First of all note that this interface is part of the Domain layer, and not the Repository layer that we haven’t yet added. The abstract repository describes what data store operations the external callers will be able to perform on an aggregate root.
The GetLoadtestsForTimePeriod will retrieve the load tests for a given time period. The implementation will need to find all overlapping load tests, not just the ones that are exactly between those two dates.
The AddOrUpdateLoadtests method is the repository equivalent of the Timetable.AddOrUpdateLoadtests method. Recall that the Timetable method returns an AddOrUpdateLoadtestsValidationResult object which will then be supplied to the repository method.
Finally, we have a delete by ID method as well which will delete a load test by its ID.
We’ll need to implement the above abstraction in the concrete repository layer. However, we have to build our data store first.
In the next post we’ll start building our database context using EntityFramework.
View the list of posts on Architecture and Patterns here.
Awesome!