Intro

Probably you have often seen messy controllers logic with hundreds of lines and dependencies, where each action is implemented slightly (or totally) different and there are a few distinct approaches of doing validation, executing logic and returning values.

Today I will show you how to do it in an elegant way with a couple of principles:

  1. Readability
    • Reading a logic should be as fast as possible and clear, I see an action code – I understand a logic
  2. Consistency
    • Making sure that every programmer would write a logic in the same way
  3. Exhaustive
    • All basic paths of action logic should be supported – validation, logic execution, data returning.

Expected result

[ApiController]
[Route("api/[controller]")]
public class ChainTestController : Controller
{
    private readonly ISampleLogic _sampleLogic;

    public ChainTestController(IChainBuilder chain, ISampleLogic sampleLogic)
    {
        _sampleLogic = sampleLogic;
        
        Chain = chain;
    }

    public IChainBuilder Chain { get; }

    [HttpGet]
    public async Task<IActionResult> Get(SampleData data)
        => await Chain.HasValidator(new SampleInputValidator(), data)
            .Catch<SampleDataInputErrorState>()
            .WithResult(BadRequest())
            .Act(async () =>
                    await _sampleLogic.DoAsync(data))
            .HasValidator(new SampleLogicOutputValidator(), result => result)
            .Catch<SampleDataLogicErrorState>()
            .WithResult(Problem())
            .WithFinalResult(Ok);
}

So we are going to make fluent style logic building. This includes: adding multiple validators for inputs, adding catches for expected failures, adding pure business logic, adding multiple validators for output and catches for failures respectively, and the last – our success path output result.


Prerequisites


Components

Building chaining is not quite easy and we need to think what components are needed to achieve this approach.

We have 4 phases: pre-validation, acting, post-validation, and final result return

Here are the components which handle the process:

Chain builder – the root component. It provides entry point for our algorithm – we want to add some validators, then execute second phase – act section

Catch builder – we need to interact for expected failures, in our case we need to return specific result

Act chain builder – after executing our pure logic, we want to validate it again. This is our third phase.

Act catch builder – in a case of failure of act logic data validation we need to interact somehow – return a specific fail result.

In our case Return blocks are WithResult or WithFinalResult, but we can modify our catch builders, so we can get even different fallback paths.


Coding

Let’s take a look on our root Chain Builder implementation:

public class ChainBuilder : IChainBuilder
{
    private readonly ICollection<Func<Task<ValidationResult>>> _preValidatorFunctions;
    private readonly ICollection<Catch> _catches;
    private Func<Task<object>> _actFunction;

    public ChainBuilder()
    {
        _preValidatorFunctions = new List<Func<Task<ValidationResult>>>();
        _catches = new List<Catch>();
    }

    public IChainBuilder HasValidator<TValidatee>(IValidator<TValidatee> validator, TValidatee validatee)
    {
        _preValidatorFunctions.Add(() => validator.ValidateAsync(validatee));

        return this;
    }

    public IActChainBuilder<TResult> Act<TResult>(Func<Task<TResult>> action)
        where TResult : class
    {
        if (action == null)
        {
            throw new ArgumentNullException(nameof(action));
        }

        _actFunction = async () => await action();

        return new ActChainBuilder<TResult>(_preValidatorFunctions, _catches, _actFunction);
    }

    public ICatchChainBuilder Catch<TCatched>()
    {
        var catchChainBuilder = new CatchChainBuilder(this);

        _catches.Add(
            new(
                typeof(TCatched),
                () => catchChainBuilder.GetResultResolver(),
                _actFunction != null));

        return catchChainBuilder;
    }
}

Here we are collecting validators and doing fallback with catch clause. In this implementation we are doing some validation which result is a custom state of validation result which helps us to correlate an error which our catch clause. Details of this process will be shown in the next components.

After we are done with attaching validators, the catch clause returns catch chain builder for details of fallback. We just want to return a specific fail result. Additionally there is internal method which gets our result resolver, which is needed in the next component.

class CatchChainBuilder : ICatchChainBuilder
{
    private readonly IChainBuilder _chainBuilder;
    private Func<IActionResult> _resultResolver;

    public CatchChainBuilder(IChainBuilder chainBuilder)
    {
        _chainBuilder = chainBuilder;
    }

    public IChainBuilder WithResult(IActionResult result = default)
    {
        _resultResolver = () => result;

        return _chainBuilder;
    }
    
    internal Func<IActionResult> GetResultResolver()
        => _resultResolver;
}

Now, the biggest part of our code – act chain builder:

public class ActChainBuilder<TResult> : IActChainBuilder<TResult>
    where TResult : class
{
    private readonly ICollection<Func<Task<ValidationResult>>> _preValidatorFunctions;
    private readonly Func<Task<object>> _actFunction;
    private readonly ICollection<Func<TResult, Task<ValidationResult>>> _postValidatorFunctions;
    private readonly ICollection<Catch> _catches;

    public ActChainBuilder(ICollection<Func<Task<ValidationResult>>> preValidatorFunctions,
        ICollection<Catch> catches,
        Func<Task<object>> actFunction)
    {
        _preValidatorFunctions = preValidatorFunctions;
        _actFunction = actFunction;
        _postValidatorFunctions = new List<Func<TResult, Task<ValidationResult>>>();
        _catches = catches;
    }

    public IActCatchChainBuilder<TResult> Catch<TException>()
    {
        var catchChainBuilder = new ActCatchChainBuilder<TResult>(this);

        _catches.Add(
            new(
                typeof(TException),
                () => catchChainBuilder.GetResultResolver(),
                true));

        return catchChainBuilder;
    }
    
    public IActChainBuilder<TResult> HasValidator<TValidatee>(IValidator<TValidatee> validator,
        Func<TResult, TValidatee> validatee)
    {
        _postValidatorFunctions.Add(
            result => validator.ValidateAsync(
                validatee(result)));

        return this;
    }
    
    public async Task<IActionResult> WithFinalResult(Func<TResult, IActionResult> resultResolver)
    {
        IActionResult preValidationFaultedResult = await HandlePreValidationAsync();

        if (preValidationFaultedResult != null)
        {
            return preValidationFaultedResult;
        }

        TResult result = null;

        try
        {
            result = (TResult)await _actFunction();

            IActionResult postValidationFaultedResult = await HandlePostValidationAsync(result);

            if (postValidationFaultedResult != null)
            {
                return postValidationFaultedResult;
            }
        }
        catch (Exception e)
        {
            var @catch = _catches.SingleOrDefault(
                x => x.AfterAct
                     && x.CatchedType == e.GetType());

            if (@catch != default)
            {
                return @catch.CatchResult()();
            }
        }

        return resultResolver(result);
    }

    // private --------------------------------------------------------------
    
    private async Task<IActionResult> HandlePostValidationAsync(TResult result)
    {
        foreach (Func<TResult, Task<ValidationResult>> validatorFunction in _postValidatorFunctions)
        {
            var validationResult = await validatorFunction(result);

            if (!validationResult.IsValid)
            {
                var @catch = _catches.SingleOrDefault(
                    x => x.AfterAct
                         && validationResult.Errors.Any(
                             error => error.CustomState.GetType() == x.CatchedType));

                if (@catch != default)
                {
                    return @catch.CatchResult()();
                }
            }
        }

        return null;
    }

    private async Task<IActionResult> HandlePreValidationAsync()
    {
        foreach (Func<Task<ValidationResult>> validatorFunction in _preValidatorFunctions)
        {
            var validationResult = await validatorFunction();

            if (!validationResult.IsValid)
            {
                var @catch = _catches
                    .SingleOrDefault(
                        x => !x.AfterAct
                             && validationResult.Errors.Any(
                                 error => error.CustomState.GetType() == x.CatchedType));

                if (@catch != default)
                {
                    return @catch.CatchResult()();
                }
            }
        }

        return null;
    }
}

Again, like in our root chain builder we are providing methods for adding validators and execute some fallback with catch clause. Moreover we have WithFinalResult which is used on the end of the success path to return a specific success result. Also here we have core of our execution, so handling pre-validation, executing logic, and performing post-validation.

Let’s talk about the validation – we trying to validate inputs at pre-validation, then correlate thrown errors with our stored catch clauses. If the error matches the custom state of validation result error and phase matches, so it’s before acting then it’s the right fallback which we should execute, more preciously, we need to execute stored catch result.

# Tip #
As you see there is a double lambda function invocation - it's needed for lazy result evaluation, because we store catch clause before the fallback is executed, so WithResult method assigns a value after we have a catch clause stored.

Now, the last part for complete the implementation – act catch builder:

public class ActCatchChainBuilder<TResult> : IActCatchChainBuilder<TResult>
    where TResult : class
{
    private readonly IActChainBuilder<TResult> _chainBuilder;
    private Func<IActionResult> _resultResolver;

    public ActCatchChainBuilder(IActChainBuilder<TResult> chainBuilder)
    {
        _chainBuilder = chainBuilder;
    }

    internal Func<IActionResult> GetResultResolver()
        => _resultResolver;

    public IActChainBuilder<TResult> WithResult(IActionResult result = default)
    {
        _resultResolver = () => result;
         
        return _chainBuilder;
    }

}

It’s exactly the same as our root catch builder, but it returns act chain builder instead of root one, so we are on the next phase.

Let’s also look on sample input validator, so how we should apply the state, to catch it then in our logic:

public class SampleInputValidator : AbstractValidator<SampleData>
{
    public SampleInputValidator()
        => RuleFor(x => x.Input)
            .NotNull()
            .WithState(_ => new SampleDataInputErrorState());
}

Just on certain validation errors cases assign a custom state, which will be catched then.

That’s all for basic usage purposes. In the next parts we will continue to extend our logic for more complex scenarios.

Let's get in

Touch!