WithProjectionOf Feature

The WithProjectionOf extension method allows you to create a new specification by reusing an existing specification’s filtering, ordering, and other query logic, but apply a projection from a different specification (e.g., a different Select or SelectMany clause). It returns a new combined specification while the input specifications remain unchanged.

This is useful in various scenarios:

  • You want to project to a different shapes of data (DTOs, VMs, etc.) from the same base query logic, without duplicating the logic.
  • You want to project to a specific shape given different base query logic, without duplicating the logic.

Usage 1

Suppose you have a specification that defines filtering, but you want to project the results into different DTO models:

public class Customer
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public string? Address { get; set; }
}

public class CustomerDto1
{
    public int Id { get; set; }
    public string? Name { get; set; }
}

public class CustomerDto2
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public string? Address { get; set; }
}

public class CustomerSpec : Specification<Customer>
{
    public CustomerSpec(string name)
    {
        Query.Where(x => x.Name == name);
    }
}

public class CustomerToCustomerDto1Spec : Specification<Customer, CustomerDto1>
{
    public CustomerToCustomerDto1Spec()
    {
        Query.Select(x => new CustomerDto1
        {
            Id = x.Id,
            Name = x.Name
        });
    }
}

public class CustomerToCustomerDto2Spec : Specification<Customer, CustomerDto2>
{
    public CustomerToCustomerDto2Spec()
    {
        Query.Select(x => new CustomerDto2
        {
            Id = x.Id,
            Name = x.Name,
            Address = x.Address
        });
    }
}

We may combine them as follows. It creates new Specification<Customer, CustomerDto1> and Specification<Customer, CustomerDto2> specifications, with the state of CustomerSpec and only the projection of CustomerDto specifications. The input specifications remain unchanged.

var customerSpec = new CustomerSpec("John");
var customerDto1Spec = new CustomerToCustomerDto1Spec();
var customerDto2Spec = new CustomerToCustomerDto2Spec();

var newSpec1 = customerSpec.WithProjectionOf(customerDto1Spec);
var newSpec2 = customerSpec.WithProjectionOf(customerDto2Spec);

Usage 2

Suppose you have two specifications that defines different filtering criteria, but you want to project the results into a specific DTO model.

public class Customer
{
    public int Id { get; set; }
    public string? FirstName { get; set; }
    public string? LastName { get; set; }
    public string? Address { get; set; }
}

public class CustomerDto
{
    public int Id { get; set; }
    public string? Name { get; set; }
}

public class CustomerSpec1 : Specification<Customer>
{
    public CustomerSpec1(string name)
    {
        Query.Where(x => x.FirstName == name);
    }
}

public class CustomerSpec2 : Specification<Customer>
{
    public CustomerSpec2(string lastName)
    {
        Query.Where(x => x.LastName == lastName);
    }
}

public class CustomerToCustomerDtoSpec : Specification<Customer, CustomerDto>
{
    public CustomerToCustomerDtoSpec()
    {
        Query.Select(x => new CustomerDto
        {
            Id = x.Id,
            Name = x.FirstName
        });
    }
}

Now, we may combine them as follows. It creates new Specification<Customer, CustomerDto> specifications, with the state of Customer specifications and only the projection of CustomerToCustomerDtoSpec. The input specifications remain unchanged.

var customerSpec1 = new CustomerSpec1("John");
var customerSpec2 = new CustomerSpec2("Smith");
var customerDtoSpec = new CustomerToCustomerDtoSpec();

var newSpec1 = customerSpec1.WithProjectionOf(customerDtoSpec);
var newSpec2 = customerSpec2.WithProjectionOf(customerDtoSpec);

See also: