Specification pattern async
Asked Answered
B

3

7

I'm trying to apply Specification pattern to my validation logic. But I have some problems with async validation.

Let's say I have an entity AddRequest (has 2 string property FileName and Content) that need to be validated.

I need to create 3 validators:

  1. Validate if FileName doesn't contains invalid characters

  2. Validate if Content is correct

  3. Async validate if file with FileName is exists on the database. In this case I should have something like Task<bool> IsSatisfiedByAsync

But how can I implement both IsSatisfiedBy and IsSatisfiedByAsync? Should I create 2 interfaces like ISpecification and IAsyncSpecification or can I do that in one?

My version of ISpecification (I need only And)

    public interface ISpecification
    {
        bool IsSatisfiedBy(object candidate);
        ISpecification And(ISpecification other);
    }

AndSpecification

public class AndSpecification : CompositeSpecification 
{
    private ISpecification leftCondition;
    private ISpecification rightCondition;

    public AndSpecification(ISpecification left, ISpecification right) 
    {
        leftCondition = left;
        rightCondition = right;
    }

    public override bool IsSatisfiedBy(object o) 
    {
        return leftCondition.IsSatisfiedBy(o) && rightCondition.IsSatisfiedBy(o);
    }
}

To validate if file exists I should use:

 await _fileStorage.FileExistsAsync(addRequest.FileName);

How can I write IsSatisfiedBy for that check if I really need do that async?

For example here my validator (1) for FileName

public class FileNameSpecification : CompositeSpecification 
{
    private static readonly char[] _invalidEndingCharacters = { '.', '/' };
    
    public override bool IsSatisfiedBy(object o) 
    {
        var request = (AddRequest)o;
        if (string.IsNullOrEmpty(request.FileName))
        {
            return false;
        }
        if (request.FileName.Length > 1024)
        {
            return false;
        }
        if (request.FileName.Contains('\\') || _invalidEndingCharacters.Contains(request.FileName.Last()))
        {
            return false;
        }

        return true
    }
}

I need to create FileExistsSpecification and use like:

var validations = new FileNameSpecification().And(new FileExistsSpecification());
if(validations.IsSatisfiedBy(addRequest)) 
{ ... }

But how can I create FileExistsSpecification if I need async?

Bouzoun answered 14/8, 2017 at 14:27 Comment(8)
If you absolutely need to support asynchronous specifications then I'd generalize on asynchronicity and drop synchronous contracts. Given that specifications can be arbitrarily composed together and that the client doesn't know whether a spec is async or not it seems more natural to treat everything as if it was asynchronous.Painkiller
Isn't string.IsNullOrEmpty(request.FileName) have to be executed before request.FileName.Length > 1024? It cannot be asyncSaiva
@Saiva What are you talking about? string.IsNullOrEmpty(request.FileName) executed before request.FileName.Length > 1024 look at the codeBouzoun
@Painkiller I like your idea. Could you please show me how can I make my IsSatisfiedBy async in my case for FileNameSpecification ? ThxBouzoun
I tought that you want asynchronously invoke each ifSaiva
@Painkiller There's no need to treat everything as async, and in fact that'd be a fairly lousy approach for situations where all rules would be fast-running and/or synchronous in nature.Drops
@Mr.Potkin , did you read and understand my answer below? Allow for differences between async and sync, but not using an interface--because the answer for a composite specification will depend on its children. I.e. if at least one is async, the composite is as well, and additionally the composite should be behave differently when only one child is async compared to when both are.Drops
@Stephen Cleary seems to have posted an answer that does what I described already.Painkiller
E
4

But how can I implement both IsSatisfiedBy and IsSatisfiedByAsync? Should I create 2 interfaces like ISpecification and IAsyncSpecification or can I do that in one?

You can define both synchronous and asynchronous interfaces, but any general-purpose composite implementations would have to only implement the asynchronous version.

Since asynchronous methods on interfaces mean "this might be asynchronous" whereas synchronous methods mean "this must be synchronous", I'd go with an asynchronous-only interface, as such:

public interface ISpecification
{
  Task<bool> IsSatisfiedByAsync(object candidate);
}

If many of your specifications are synchronous, you can help out with a base class:

public abstract class SynchronousSpecificationBase : ISpecification
{
  public virtual Task<bool> IsSatisfiedByAsync(object candidate)
  {
    return Task.FromResult(IsSatisfiedBy(candidate));
  }
  protected abstract bool IsSatisfiedBy(object candidate);
}

The composites would then be:

public class AndSpecification : ISpecification 
{
  ...

  public async Task<bool> IsSatisfiedByAsync(object o) 
  {
    return await leftCondition.IsSatisfiedByAsync(o) && await rightCondition.IsSatisfiedByAsync(o);
  }
}

public static class SpecificationExtensions
{
  public static ISpecification And(ISpeicification @this, ISpecification other) =>
      new AndSpecification(@this, other);
}

and individual specifications as such:

public class FileExistsSpecification : ISpecification
{
  public async Task<bool> IsSatisfiedByAsync(object o)
  {
    return await _fileStorage.FileExistsAsync(addRequest.FileName);
  }
}

public class FileNameSpecification : SynchronousSpecification 
{
  private static readonly char[] _invalidEndingCharacters = { '.', '/' };

  public override bool IsSatisfiedBy(object o) 
  {
    var request = (AddRequest)o;
    if (string.IsNullOrEmpty(request.FileName))
      return false;
    if (request.FileName.Length > 1024)
      return false;
    if (request.FileName.Contains('\\') || _invalidEndingCharacters.Contains(request.FileName.Last()))
      return false;
    return true;
  }
}

Usage:

var validations = new FileNameSpecification().And(new FileExistsSpecification());
if (await validations.IsSatisfiedByAsync(addRequest))
{ ... }
Etsukoetta answered 14/8, 2017 at 20:39 Comment(0)
A
-1

I don't know, why you need async operations in a sync driven pattern.

Imagine, if the first result is false and you have two or more async checks, it would be a waste in performance.

If you want to know, how to get an async request back in sync, you can try to use the following:

public class FileExistsSpecification : CompositeSpecification
{
    public override bool IsSatisfiedBy(object o)
    {
        var addRequest = (AddRequest)o
        Task<bool> fileExistsResult = _fileStorage.FileExistsAsync(addRequest.FileName);
        fileExistsResult.Wait();

        return fileExistsResult.Result;
    }
}

You should also use the generics approach.

Anelace answered 14/8, 2017 at 15:1 Comment(2)
if the first result is false and you have two or more async checks, it would be a waste <= "Performance" can mean different things; time to completion is obviously prioritized here (or else he would simply serially execute any long-running specification evaluations). Nor is some extra processing when the system is below peak usage really a waste of anything but electricity. Also, returning early from the earliest-finishing of two tasks means that the other can cease, avoiding waste. In fact, if the first of two sync operations is successful, it's always "wasted" if the other fails.Drops
There might be situations where for example a detailed and composite error message might be useful though - we did this with a 'Result' type (that implicitely overloads as a bool) instead of returning a boolConvertible
D
-1

I think your main goal here is to make sure that code finishes as soon as possible for evaluating a composite specification, when executing child specifications and one or more may take a while, yes? It's always possible for calling code outside of your pattern implementation to invoke a specification asynchronously; it's not really your concern at that point.

So, in light of that, how about giving your ISpecification an extra property?

public interface ISpecification 
{
    bool IsAsynchronous { get; }
    bool IsSatisfiedBy(object o);
}

Then, for non-composite synchronous or asynchronous-type specifications, hard-code the return value for IsAsynchronous. But in composite ones, base it on the children, to wit:

public class AndSpecification : ISpecification
{
    private ISpecification left;
    private ISpecification right;

    public AndSpecification(ISpecification _left, ISpecification _right) 
    {
        if (_left == null || _right == null) throw new ArgumentNullException();        
        left = _left;
        right = _right;
    }

    public bool IsAsynchronous { get { return left.IsAsynchronous || right.IsAsynchronous; }

    public override bool IsSatisfiedBy(object o) 
    {
        if (!this.IsAsynchronous)            
            return leftCondition.IsSatisfiedBy(o) && rightCondition.IsSatisfiedBy(o);

        Parallel.Invoke(
            () => {
                if (!left.IsSatisfiedBy(o)) return false;
            },
            () => {
                if (!right.IsSatisfiedBy(o)) return false;
            }
        );

        return true;
    }
}

But taking this a bit further, you don't want to waste performance. Hence why not evaluate the fast, synchronous child first when there's one sync and one async? Here's a closer-to-finished version of the basic idea:

public class AndSpecification : ISpecification
{
    private ISpecification left;
    private ISpecification right;

    public AndSpecification(ISpecification _left, ISpecification _right) 
    {
        if (_left == null || _right == null) throw new ArgumentNullException();        
        left = _left;
        right = _right;
    }

    public bool IsAsynchronous { get { return left.IsAsynchronous || right.IsAsynchronous; }

    public override bool IsSatisfiedBy(object o) 
    {
        if (!left.IsAsynchronous) 
        {
            if (!right.IsAsynchronous) 
            {
                return left.IsSatisfiedBy(o) && right.IsSatisfiedBy(o);
            }
            else
            {
                if (!left.IsSatisfiedBy(o)) return false;
                return right.IsSatisfiedBy(o);
            } 
        }
        else if (!right.IsAsynchronous) 
        {
            if (!right.IsSatisfiedBy(o)) return false;
            return left.IsSatisfiedBy(o);
        }
        else
        {
            Parallel.Invoke(
                () => {
                    if (!left.IsSatisfiedBy(o)) return false;
                },
                () => {
                    if (!right.IsSatisfiedBy(o)) return false;
                }
            );

            return true;
        }
    }
}
Drops answered 14/8, 2017 at 15:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.