ardalis / Specification

Base class with tests for adding specifications to a DDD model

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Concat specifications

diegomodolo opened this issue · comments

Is there a way to concat two or more specifications together?
I have the scenario where I have a status specification and a customer id, and in some cases I want to use them together, without writing a new one for each combination.

It's not advisable typically to try to combine specifications. That moves the logic of composing the query out of the specification (and domain model) and into the calling code (probably in another less testable part of the app).

In your case why not have one that takes in a status and an id in its constructor?

In my case, I would have a method at my service like GetOpenOrdersByCustomer, and I would only use the specifications inside my method, I would not make the specs visible to the UI, for example.
I was trying to achieve the concat so that I wouldn't need to create one spec for Status, one for Customer, and one for both.

Again, we don't have plans to add support for combining specs. Personally I would create 3 specs and if there were logic between them that you really didn't want to duplicate, I'd do something like reference the expression in question from a shared constant value. But more likely I would just create 3 independent specs. If I really wanted one super-flexible spec, I'd create one that took in Customer and Status, but allowed for either one (but not both) of them to be omitted. That, too, seems kind of a poor design, so again I'd probably go the 3 spec route. Sorry to not be more help.

I understand, and I thank you for all the info you've provided me.

I think I just did this. If there is any problem with this design, please let me know, this is a copy of my work.

using Ardalis.Specification;

namespace Domain.Abstraction.Specifications;

/// <summary>
/// Domain Specification interface
/// </summary>
/// <typeparam name="T">The type being queried against.</typeparam>
/// <remarks>You have to call <see cref="Build"/> method to build the query logic before using.</remarks>
public interface IDomainSpecification<T> : ISpecification<T>
{
    /// <summary>
    /// Whether the current query model is an unconditional query model
    /// </summary>
    /// <returns><see langword="true"/> if it's an unconditional query model, otherwise <see langword="false"/></returns>
    bool IsPredicateLess() =>  !WhereExpressions.Any();

    /// <summary>
    /// build the query logic
    /// </summary>
    /// <remarks>In the case of multiple inheritance, the subclass must ensure that the implementation of the parent class is called in this method. 
    /// The order of calling the implementation of the parent class is determined according to the situation, and it is not necessary to call the implementation of the parent class first.Although the order in which query conventions are 
    /// constructed is not important it does not preclude the order being considered necessary in the new Ardalis.Specification package. 
    /// Under normal circumstances, the implementation of the parent class is called at the end, so that the query logic of the subclass will be constructed first</remarks>
    void Build();
}

/// <summary>
/// Pagination specification
/// </summary>
/// <typeparam name="T">The type being queried against.</typeparam>
/// <remarks>You have to call <see cref="Build"/> method to build the query logic before using.</remarks>
public interface IPaginationSpecification<T> : IDomainSpecification<T>
{
}


public class CompanySpec : Specification<Company> , IDomainSpecification<Company>
{
    private bool built;

    public void Build()
    {
       // make sure it only build once.
        if(built)
            return;
        //Do something before sub class build
        PreBuild();
        //Do something after sub class build
        built = true;
    }

    /// <summary>
    /// build query
    /// </summary>
    /// <remarks>make sure the implements are call the base <see cref="PreBuild"/>method</remarks>
    protected virtual void PreBuild()
    {
        Query.Where(e => !e.Deleted);
    }
}

public class CompanyPaginationSpec : CompanySpec, IPaginationSpecification<Company>
{
    ///<summary>
    /// Page index begin with 1
    ///</summary>
    public int PageIndex { get; }
    public int PageSize { get; }

    public CompanyPaginationSpec(int pageIndex, int pageSize)
    {
        PageIndex = pageIndex;
        PageSize = pageSize;
    }

    protected override void PreBuild()
    {
        base.PreBuild();
        Query.Skip(PageSize * (PageIndex - 1)).Take(PageSize);
    }
}

CompanySpec is an example class, you can create a common abstract class, it build the all the base entity or DTO query logic. the real entity or DTO implement this abstract class gets the base query logic.
I do not like call virtual method in the constructor, this may cause NullReferenceException. call virtual method in constructor need to make sure the sub class is not access its instance member in the override method.

@diegomodolo example updated.