Specifications¶
Specifications are a way to abstract part of a query into a reusable type. Currently, there are four main kinds of specifications: Filter, Sort, Projection and Aggregate.
To declare your own specifications, you always extend from one of the base
classes. Never implement ISpecification
, or its derivate interfaces, directly.
Filter Specification¶
Create a specification class in the Domain
module of your service, make it
extends from the FilterSpecification<TEntity>
class and set the condition to
follow as the Criteria
field.
C# | |
---|---|
Important
Domain layer specifications cannot reference any DB specific logic. It is about the Domain Model, not about the infrastructure layer. Since the specification can be used in memory, as well as in DB queries, its expression must be able to be compiled, which means it must not involve logic that can not be used outside of an IQueryable
The criterion of each specification should be as granular as the rule it represents to be as concise as possible.
Avoid expressions involving different conditions that are loosely related, instead, separate each condition into their own specification. This way it makes it more apparent which rule the specification represents, eases the test of it and could prevent unwanted behaviors when they are translated and applied to the queries on the repositories.
Avoid:
C# | |
---|---|
Do:
Specifications can be easily combined by using an Aggregate Specification.
DateRange Specification¶
This is a particular FilterSpecification that easily allows us to set the
condition that a date entity property value is included into a DateRange
.
A DateRange
can be built using an specific StartDate and EndDate, or an amount
of days and a direction (x days before the current date, x days after the
current date, or both).
To create a new specification class, just extend from
DateRangeSpecification<TEntity>
. For example:
C# | |
---|---|
As Filter Specifications, they can be combined by using an Aggregate Specification.
Sort Specification¶
Specifications can be used to define a sorting criteria.
Create a new class per sorting criteria, extending from
SortSpecification<TEntity>
.
C# | |
---|---|
Filter and Sort Specifications can be easily combined by using an Aggregate Specification.
Projection Specification¶
When you'd like to SELECT a subset of your entity fields, or compose them, or execute aggregation functions, a Projection Specification can be used.
Create a new class extending from
ProjectionSpecification<TEntity, TProjection>
C# | |
---|---|
Grouping Specification¶
When we need to group data by a given property or field, it is highly recommended to use a grouping specification. This way we avoid performing the grouping in-memory and leverage the whole translation of the query and posterior calculation to EF.
The preferred way to apply grouping is through an AggregateSpecification<T>
Note that grouping specifications return a IGrouping<TKey, T>
which expose two
fields:
- The
name
field contains the grouping key. - The
values
field holds all the items under that specified key.
AutoMapper Projections¶
When mapping entities to DTOs, we can use the
AutoMapperProjectionSpecification
to use Auto Mapper's ProjectTo
.
This specification does not require a custom implementation and can just be
instantiated with the proper generic parameters, and an instance of IMapper
which must be retrieved from DI.
C# | |
---|---|
Important
When working with GraphQL, if you need to return a projection of an entity
over a DTO you should use AutoMapperHotChocolateProjectionSpecification
,
see this.
Aggregate Specification¶
The Aggregate can be used to easily combine multiple specifications into one in order to execute a query against a repository, for example.
Aggregates can be created on the fly by simply instantiating a new one:
C# | |
---|---|
Important
Aggregates are immutables. Calling its methods returns a new instance, hence
it's important to always assign its results; just like with IEnumerable
s.
Combining two Aggregates¶
Aggregates can be composed as well by using the AndAggregate
and OrAggregate
methods.
Executing Specifications in-memory¶
Any of the classes implementing ISpecification
can be applied to IQueryable
s
or IEnumerable
s:
C# | |
---|---|
Executing Specifications against the Database¶
The IRepository.SearchAsync
receives an ISpecification<TEntity>
so any of
the provided Specifications can be used. It is most convenient to provide an
AggregateSpecification
so that you can combine multiples to perform a complex
query.
The convention to perform searches in the Application
layer, taking as example
a method on the AppService which takes a FilterDto as parameter, is as
follows:
EFCore Includes¶
When we need to .Include
relations into our query, we can use the
IncludeSpecification<TEntity>
to define them:
C# | |
---|---|
If we need to .Include
more than one relation of the same entity into our
query:
C# | |
---|---|
Polymorphic Child Inheritance¶
Some times we have root entities, that have children which may be of different types inheriting from a base class, and the entity has a list of the base class.
When creating includes for the root entity's children, we need to use this hackish wizardry:
Info
In this example we are mapping the abstract class BaseChild
that can be a
ConcreteChild
which has the property Relation
and we also need to
include the children of this property. If the abstract class had more
implementations, just add another builder with the same approach.
You can combine an IncludeSpecification
with the AggregateSpecification
to
generate a complex query:
C# | |
---|---|
Validations on application layer¶
To execute an specification in memory, against an Enumerable or Queryable, you
can simply use the Apply
method. For example:
C# | |
---|---|
Unit tests¶
As long as we keep our specifications' criterion simple, our testing will often only imply to check if the rule it represents is truly the one we expected, this means, that the criterion it has is met by the models which should satisfy it, and not met by those models who shouldn't.
For example, for the following specification:
C# | |
---|---|
The tests we will need to define will be the following: