I've been struggling around a DDD-related issue with Specifications and I've read much into DDD and specifications and repositories.
However, there is an issue if trying to combine all 3 of these without breaking the domain-driven design. It boils down to how to apply filters with performance in mind.
First a few obvious facts:
- Repositories to got DataAccess/Infrastructure Layer
- Domain Models represent Business Logic and go to the Domain layer
- Data Access Models represent the Persistence layer and go to the Persistence/Infrastructure/DataAccess layer
- Business Logic goes to Domain Layer
- Specifications are Business Logic, so they belong to the Domain layer too.
- In all these examples, an ORM Framework and SQL Server is used inside the Repository
- Persistance Models may not leak into Domain Layer
So far, so easy. The problem arises when/if we try to apply Specifications to the Repository and not breaking the DDD pattern or having performance issues.
The possible ways to apply Specifications:
1) Classic way: Specifications using Domain Model in Domain Layer
Apply the traditional Specification Pattern, with a IsSatisfiedBy
method, returning a bool
and Composite Specifications to combine multiple Specifications.
This let us keep specifications in Domain Layer, but...
- It has to work with Domain Models, while the repository uses Persistence Models which represent the data structure of the persistence layer. This one is easy to fix with the usage of mappers such as
AutoMapper
.
- However, the problem which can't be solved: All the specifications would have to be performed in memory. In a big table/database this means a huge impact if you have to iterate through ALL Entities only to filter out the one which meets your specifications
2) Specifications using Persistence Model
This is similar to 1), but using Persistence Models in the specification. This allows direct use of the Specification as part of our .Where
predicate which will be translated into a query (i.e. TSQL) and the filtering will be performed on the Persistence storage (i.e. SQL Server).
- While this gives us good performance, it clearly violates the DDD pattern. Our Persistence model leaks into the Domain layer, making the Domain Layer depend on the Persistence Layer instead of the other way around.
3) Like 2), but make Specifications Part of the Persistence Layer
- This doesn't work, because Domain Layer needs to reference the Specifications. It would still depend on the persistence layer.
- We would have business logic inside the Persistence layer. Which also violates the DDD pattern
4) Like 3, but use abstract the Specifications as Interfaces
We would have Specification interfaces in our Domain layer, our concrete implementations of the Specifications in the Persistence Layer. Now our Domain Layer would only interact with the interfaces and not depend on the Persistence layer.
- This still violates #2 from 3). We would have business logic in the persistence layer, which is bad.
5) Translate the Expression Tree from Domain Model into Persistence Model
This certainly solves the problem, but it's a non-trivial task but it would keep the Specifications inside our Domain Layer while still benefiting from SQL optimization because the Specifications becomes part of the Repositories Where clause and translates into TSQL
I tried going this approach and there are several issues (from the implementation side):
- We would need to know the Configuration from the Mapper (if we use one) or keep our own mapping system. This can be partly done (reading Mapper configuration) with i.e. AutoMapper, but further issues exist
- It's acceptable for one where one Property of Model A maps to one Property of Model B. It becomes more difficult if the types are different (i.e. due to persistence types, for example, Enums being saved as strings or key/value pairs in another table and we need to do conversions inside the resolver.
- It gets pretty complicated if multiple fields get mapped into one destination field. I believe this is non an issue for Domain Model -> Persistence Model mappings
6) Query Builder like API
The last one is making some kind of query API which is passed into the specification and from whom the Repository/Persistence layer would generate an Expression Tree to be passed to .Where
clause and which uses an Interface to declare all filterable fields.
I did a few attempts in that direction too but wasn't too happy about the results. Something like
public interface IQuery<T>
{
IQuery<T> Where(Expression<Func<T, T>> predicate);
}
public interface IQueryFilter<TFilter>
{
TFilter And(TFilter other);
TFilter Or(TFilter other);
TFilter Not(TFilter other);
}
public interface IQueryField<TSource, IQueryFilter>
{
IQueryFilter Equal(TSource other);
IQueryFilter GreaterThan(TSource other);
IQueryFilter Greater(TSource other);
IQueryFilter LesserThan(TSource other);
IQueryFilter Lesser(TSource other);
}
public interface IPersonQueryFilter : IQueryFilter<IPersonQueryFilter>
{
IQueryField<int, IPersonQueryFilter> ID { get; }
IQueryField<string, IPersonQueryFilter> Name { get; }
IQueryField<int, IPersonQueryFilter> Age { get; }
}
and in the specification, we would pass an IQuery<IPersonQueryFilter> query
to the specifications constructor and then apply the specifications to it when using or combining it.
IQuery<IGridQueryFilter> query = null;
query.Where(f => f.Name.Equal("Bob") );
I don't like this approach much, as it makes handling complex specifications somewhat hard (like and or if chaining) and I don't like the way the And/Or/Not would work, especially creating expression trees from this "API".
I have been looking for weeks all over the Internet read dozens of articles on DDD and Specification, but they always only handle simple cases and don't take the performance into consideration or they violate the DDD pattern.
How do you solve this in a real-world application without doing in-memory filtering or leaking Persistence into Domain Layer??
Are there any frameworks that solve the issues above with one of the two ways (Query Builder like syntax to Expression Trees or an Expression Tree translator)?
See Question&Answers more detail:
os