Skip to main content

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...
}
}

Global specifications

As the name implies, global specifications are specifications that are applied globally across the application. They are useful for scenarios where you want to enforce a consistent filtering logic that applies to all queries of a specific entity type, regardless of the execution context. Any specification can be made global by decorating it with the [GlobalSpecification] Attribute.

Given this property, global specifications can be used to enforce security rules, data access policies, or any other business logic that should apply universally across the application.

In the example below, we define a global specification that filters Person entities to return only those that belong to the same area as the currently logged-in user. This will apply to all queries involving Person entities made through the repositories using the GetAll() method.

[GlobalSpecification]
public class MyUnitPersonsSpecification : ShaSpecification<Person>
{
private readonly ICurrentUser _currentUser;
public MyUnitPersonsSpecification(
ICurrentUser currentUser)
{
_currentUser = currentUser;
}

public override Expression<Func<Person, bool>> BuildExpression()
{
// Fetch current person. Note: all specifications are disabled here
var personRepo = IocManager.Resolve<IRepository<Person, Guid>>();
var currentPerson = personRepo.GetAll().FirstOrDefault(p => p.User != null && p.User.Id == AbpSession.UserId);

// Return only persons from the same area as the current user
return person => person.AreaLevel1 == currentPerson.AreaLevel1;
}
}

Disabling specifications

In certain scenarios, it may be necessary to temporarily disable all specifications. The SpecificationManager provides the DisableSpecifications() method to achieve this. Below is an example:

public Expression<Func<T, bool>> ToExpression()
{
// Temporarily disable all specifications before building the expression
using (SpecificationManager.DisableSpecifications())
{
return BuildExpression();
}
}

On the action level, you can disable all specifications by applying the [DisableSpecifications] Attribute to the method. This ensures that no specifications are applied during the execution of the method.

[DisableSpecifications]
public async Task GetUnfilteredAsync()
{
var persons = await AsyncQueryableExecuter.ToListAsync(Repository.GetAll());
}

Important Note: Specifications are automatically disabled within Specification classes to avoid potential infinite loops during execution. This ensures that specifications do not inadvertently reference themselves while building expressions.

Front-end Support

The Query Builder provides a user-friendly interface for applying back-end-defined specifications as filter parameters in queries. This feature simplifies the process of creating complex filters, allowing users to leverage predefined specifications without needing to understand the underlying code. Specifications are displayed in the property list, as illustrated in the image, and the Query Builder supports two operations for specifications:

  1. Is satisfied: The specification is directly integrated into the final query, meaning the expression returned by ToExpression evaluates to true.
  2. Is satisfied when: The specification is included in the final query only if certain client-side logic (pre-condition) evaluates to true.

image

By integrating specifications into the Query Builder, Shesha enables the application of advanced filtering logic when configuring data sources on the front-end. This approach goes beyond basic property-based filtering, allowing users to leverage predefined specifications for more complex and dynamic queries.