Skip to main content

Specifications

A specification is a named, reusable, combinable, and testable filter for an entity or other business object. The Specification Pattern lets you wrap a piece of filtering logic in its own class so you can apply it to queries and operations across your application instead of repeating the same where clause everywhere.

Specifications can be combined using logical operators such as And, Or, and Not, which makes them useful when you need complex filtering in 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

Define a specification by inheriting from ShaSpecification<T> and implementing the BuildExpression method, which returns an Expression<Func<T, bool>>. This expression is used to filter entities of type T.

The example below defines two specifications, Age18PlusSpecification and HasNoAccountSpecification, which filter Person entities by age and account status.

// 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 ISpecificationManager makes it easy to apply specifications by integrating them automatically into repositories created through dependency injection. This keeps filtering logic consistent across repositories without manual configuration.

private readonly ISpecificationManager _specificationManager;

public async Task SpecificationUsageExample()
{
using (_specificationManager.Use<Age18PlusSpecification, Person>())
{
// GetAll() is filtered automatically by Age18PlusSpecification
var personsQuery = Repository.GetAll();
var persons = await AsyncQueryableExecuter.ToListAsync(personsQuery);
}

using (_specificationManager.Use(
typeof(Age18PlusSpecification),
typeof(HasNoAccountSpecification)))
{
// GetAll() is filtered automatically by both specifications
var personsQuery = Repository.GetAll();
var persons = await AsyncQueryableExecuter.ToListAsync(personsQuery);
}
}

In the example above, specifications are activated manually, and GetAll() automatically appends the matching LINQ expressions to the IQueryable. The specification manager is thread-safe, so it is suitable for use in asynchronous methods. When multiple specifications are applied to the same entity type, the manager combines them using a logical And.


Action-level specifications

You can also apply specifications directly at the action level using the [ApplySpecifications] attribute. This lets you state which specifications should apply when a particular method in an application service runs.

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

A global specification is applied automatically across the whole application. Global specifications are useful when you want a consistent filter to apply to every query of a given entity type, regardless of the execution context. Make any specification global by decorating it with the [GlobalSpecification] attribute.

Because they apply everywhere, global specifications are well suited to enforcing security rules, data access policies, or any other business logic that should apply universally.

The example below defines a global specification that filters Person entities to only those in the same area as the currently logged-in user. It applies to all queries involving Person made through repositories using GetAll().

[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

Sometimes you need to temporarily turn off all specifications. The specification manager provides DisableSpecifications() for this:

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

At the action level, apply the [DisableSpecifications] attribute to a method so that no specifications run during its execution.

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

Specifications are automatically disabled inside specification classes to avoid infinite loops. This stops a specification from accidentally referencing itself while building its expression.


Front-end support

The Query Builder provides a user-friendly way to apply back-end specifications as filter parameters in queries. This lets configurators use predefined specifications without needing to understand the underlying code. Specifications appear in the property list, and the Query Builder supports two operations for them:

  1. Is satisfied - The specification is included directly in the final query, so the expression returned by BuildExpression must evaluate to true.
  2. Is satisfied when - The specification is included in the final query only if a client-side pre-condition evaluates to true.

image

By integrating specifications into the Query Builder, Shesha lets you apply advanced filtering when configuring data sources on the front-end. This goes beyond basic property-based filtering and lets configurators use predefined specifications for more complex, dynamic queries.