Specifications
The Specification Pattern is a design approach used to define named, reusable, combinable, and testable filters for entities and other business objects. They can encapsulate complex filtering logic into reusable components that can be applied to queries and operations.
Specifications enable the combination of filtering logic using logical operators such as And, Or, and Not, making them particularly useful for scenarios requiring complex filtering across different parts of an application.
In the Shesha framework, specifications play a critical role in the back-end by filtering data, enforcing business rules, and managing access control.
Defining Specifications
A Specification can be defined by inheriting from the ShaSpecification<T>
class and implementing the BuildExpression
method, which returns an Expression<Func<T, bool>>
. This expression will be used to filter entities of type T
.
In the example below, we define two specifications: Age18PlusSpecification
and HasNoAccountSpecification
. These specifications can be used to filter Person
entities based on their age and account status, respectively.
// Specification that filters persons who are 18 years or older
public class Age18PlusSpecification : ShaSpecification<Person>
{
public override Expression<Func<Person, bool>> BuildExpression()
{
return p => p.DateOfBirth != null && p.DateOfBirth <= DateTime.Now.AddYears(-18);
}
}
// Specification that filters persons who have no associated user account
public class HasNoAccountSpecification : ShaSpecification<Person>
{
public override Expression<Func<Person, bool>> BuildExpression()
{
return p => p.User == null;
}
}
Applying Specifications
Specification Manager
The Specification Manager simplifies the application of specifications by automatically integrating them into repositories created via dependency injection (IoC). This ensures consistent filtering logic across repositories without manual configuration.
private readonly ISpecificationManager _specificationManager;
public async Task SpecificationUsageExample()
{
using (_specificationManager.Use<Age18PlusSpecification, Person>())
{
// GetAll() injected automatically to filter by Age18PlusSpecification
var personsQuery = Repository.GetAll();
var persons = await AsyncQueryableExecuter.ToListAsync(personsQuery);
}
using (_specificationManager.Use(
typeof(Age18PlusSpecification),
typeof(HasNoAccountSpecification)))
{
// GetAll() injected automatically to filter by both Age18PlusSpecification and HasNoAccountSpecification
var personsQuery = Repository.GetAll();
var persons = await AsyncQueryableExecuter.ToListAsync(personsQuery);
}
}
In the example above, specifications are activated manually, and the GetAll() method automatically appends the corresponding LINQ expressions to the IQueryable. The SpecificationManager is thread-safe, making it suitable for use in asynchronous methods. When multiple specifications are applied to the same entity type, the SpecificationManager combines them using logical And operators.
Action-level Specifications
In addition to using the Specification Manager, specifications can also be applied directly at the action level using the [ApplySpecifications]
attribute. This allows you to specify which specifications should be applied when executing a particular method in an application service.
public class PersonAppService : DynamicCrudAppService<Person, DynamicDto<Person, Guid>, Guid, Guid>, ITransientDependency
{
[ApplySpecifications(typeof(Age18PlusSpecification), typeof(HasNoAccountSpecification))]
public async Task GetFilteredAsync()
{
var persons = await AsyncQueryableExecuter.ToListAsync(Repository.GetAll());
// do something...
}
}