Is there a way to combine Specification classes in the Ardalis.Specification library?

2.7k Views Asked by At

I am new to the Ardalis.Specification library and to the Clean Architecture in general. I was wondering if there might be a way to group multiple specification classes to make a composable query. It would be helpful for complex queries involving a large set of variables to sortBy and avoid excessive use of conditional statements.

I may need for example to sort a DateTime property either by Superior than or Inferior than, to add multiple Where clauses depending on the configuration of the request by the client and to include different related entities depending on the nature of the request (which, themselves, might be queried based on a criteria).

The following example is not a good illustration of what i need to do but that might help.

1/ Gets a list of Contact based on the state of Completion of their profile.

public ContactByCompletion(bool IsComplete)
{
    Query
        .Where(prop => prop.IsComplete == IsComplete)
        .OrderByDescending(ord => ord.CreatedAt);
}

2/ Gets a list of Contacts with a creation Date later than the parameter

public ContactByCreationDateSuperiorThan(DateTime dateRef)
{
    Query
        .Where(prop => prop.CreatedAt > dateRef.Date);
}

3/ Gets a list of Contacts affiliated to a given country

public ContactByCountry(string countryCode)
{
    Guard.Against.NullOrEmpty(countryCode, nameof(countryCode));

    Query
        .Where(prop => prop.CountryCode == countryCode);
}

So in this very simplistic example, the idea would be to combine these queries to form one that gets you the Contacts with a completed state that reside on a given country and that were created after the date in question. Is there a way to achieve that?

1

There are 1 best solutions below

1
On

I needed this so I came up with a solution. Hope others also get use out of this.

public class MergedSpecification<T> : Specification<T>, ISpecification<T>
{
    private readonly ISpecification<T>[] _specs;

    /// <summary>
    /// Initializes a new instance of the <see cref="MergedSpecification{T}"/> class.
    /// </summary>
    /// <param name="spec">The spec.</param>
    /// <param name="specs">The specs.</param>
    public MergedSpecification(params ISpecification<T>[] specs)
    {
        _specs = specs;
    }

    public TSpec? GetSpec<TSpec>() where TSpec : ISpecification<T>
    {
        return _specs.OfType<TSpec>().FirstOrDefault();
    }

    public override IEnumerable<T> Evaluate(IEnumerable<T> entities)
    {
        entities = entities.ToArray();
        foreach (var spec in _specs)
        {
            entities = Evaluator.Evaluate(entities, spec);
        }

        return entities;
    }

    IEnumerable<WhereExpressionInfo<T>> ISpecification<T>.WhereExpressions => _specs.SelectMany(spec => spec.WhereExpressions);
    IEnumerable<OrderExpressionInfo<T>> ISpecification<T>.OrderExpressions => _specs.SelectMany(spec => spec.OrderExpressions);
    IEnumerable<IncludeExpressionInfo> ISpecification<T>.IncludeExpressions => _specs.SelectMany(spec => spec.IncludeExpressions);
    IEnumerable<string> ISpecification<T>.IncludeStrings => _specs.SelectMany(spec => spec.IncludeStrings);
    IEnumerable<SearchExpressionInfo<T>> ISpecification<T>.SearchCriterias => _specs.SelectMany(spec => spec.SearchCriterias);

    int? ISpecification<T>.Take => _specs.FirstOrDefault(s => s.Take != null)?.Take;
    int? ISpecification<T>.Skip => _specs.FirstOrDefault(s => s.Skip != null)?.Skip;

    Func<IEnumerable<T>, IEnumerable<T>>? ISpecification<T>.PostProcessingAction => results =>
    {
        foreach (var func in _specs.Select(s => s.PostProcessingAction))
        {
            if (func != null)
            {
                results = func(results);
            }
        }

        return results;
    };

    string? ISpecification<T>.CacheKey => $"MergedSpecification:{string.Join(':', _specs.Select(s => s.CacheKey))}";
    bool ISpecification<T>.CacheEnabled => _specs.Any(s => s.CacheEnabled);
    bool ISpecification<T>.AsNoTracking => _specs.Any(s => s.AsNoTracking);
    bool ISpecification<T>.AsSplitQuery => _specs.Any(s => s.AsSplitQuery);
    bool ISpecification<T>.AsNoTrackingWithIdentityResolution => _specs.Any(s => s.AsNoTrackingWithIdentityResolution);
    bool ISpecification<T>.IgnoreQueryFilters => _specs.Any(s => s.IgnoreQueryFilters);
}