Validation Specification

For more complex, validation scenarios, there is IValidationSpecification<T> interface from FluentSpecification.Abstractions package.
IValidationSpecification<T> inherit from ISpecification<T>.

public class ItemDiscountSpecification :
    IValidationSpecification<Item>
{
    public bool IsSatisfiedBy(Item candidate)
    {
        return candidate.Paid == false &&
               candidate.Name.StartsWith("Sale") &&
               candidate.Price < 20.0;
    }

    public bool IsSatisfiedBy(Item candidate, out SpecificationResult result)
    {
        bool overall = IsSatisfiedBy(candidate);

        if (overall)
        {
            result = new SpecificationResult();
        }
        else
        {
            result = new SpecificationResult(false, 
                new FailedSpecification(typeof(ItemDiscountSpecification), 
                    candidate, "Item is not 'discountable'."));
        }

        return overall;
    }
}

It's good practice to provide FailedSpecification object, when overall Specification result is false.

ValidationSpecification inheritance

In FluentSpecification.Core package there is a special abstract class - ValidationSpecification<T>.

public class ItemDiscountSpecification :
    ValidationSpecification<Item>
{
    public override bool IsSatisfiedBy(Item candidate)
    {
        return candidate.Paid == false &&
               candidate.Name.StartsWith("Sale") &&
               candidate.Price < 20.0;
    }

    protected override string CreateFailedMessage(Item candidate)
    {
        return $"Item [{candidate.Name}] is not 'discountable'.";
    }
}

Base class will provide proper SpecificationResult object, depends on IsSatisfiedBy result.
When your Specification use some externall parameters, you can return them from GetParameters method. This parameters will be passed to SpecificationResult object.

public class ItemDiscountSpecification :
    NegatableValidationSpecification<Item>
{
    private readonly double _itemPrice;

    public ItemDiscountSpecification(double itemPrice)
    {
        _itemPrice = itemPrice;
    }

    public override bool IsSatisfiedBy(Item candidate)
    {
        return candidate.Paid == false &&
               candidate.Name.StartsWith("Sale") &&
               candidate.Price < _itemPrice;
    }

    protected override string CreateFailedMessage(Item candidate)
    {
        return $"Item [{candidate.Name}] is NOT 'discountable'.";
    }

    protected override string CreateNegationFailedMessage(Item candidate)
    {
        // Item is discountable and we don't want to
        return $"Item [{candidate.Name}] is 'discountable'.";
    }

    protected override IReadOnlyDictionary<string, object> GetParameters()
    {
        return new Dictionary<string, object>
        {
            { "MaxItemPrice", _itemPrice }
        };
    }
}
GitHub