Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Specification pattern async

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?

like image 439
Mr.Potkin Avatar asked Aug 14 '17 14:08

Mr.Potkin


Video Answer


1 Answers

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))
{ ... }
like image 95
Stephen Cleary Avatar answered Sep 25 '22 03:09

Stephen Cleary