9
\$\begingroup\$

AN UPDATED VERSION with GitHub link is avalable HERE.


I am trying to define a plug and play architecture which leverages stereotypical role implementations (Validator, Reader, Writer, Logger, Query, etc) by automatically instantiating associated services through a very limited use of Service Locator.

Demo code: let's say we have a query object to define some sequence of integers:

class IntRange : Query<IntRange, int>
{
    public IntRange(int start, int count)
    {
        Start = start;
        Count = count;
    }

    public int Start { get; }
    public int Count { get; }
}

We also have couple services to be instantiated and invoked by inversion of control container. One to validate the query before execution:

class RangeValidator : IValidator<IntRange>
{
    public Task HandleAsync(IntRange subject) =>
        subject.Count < 0 || subject.Count > 10 ?
            Task.FromException(new ArgumentOutOfRangeException()) :
            Task.CompletedTask;
}

And one more for actual sequence materialization:

class IntSequence : IReader<IntRange, int>
{
    public Task<IEnumerable<int>> HandleAsync(IntRange subject) =>
        Task.FromResult(
            Enumerable.Range(subject.Start, subject.Count));        
}

This code should magically print 10 11 12 13 14 after successful validation with matching service instantiation and execution:

foreach (var i in await new IntRange(10,5))
    Console.WriteLine(i);  

The following snippet reports ArgumentOutOfRangeException:

try
{
    await new IntRange(10, 100);
}
catch(Exception ex)
{
    Console.WriteLine(ex);
}

Autofac configuration used for the demo code above:

var builder = new ContainerBuilder();
builder.RegisterType<IntSequence>().AsImplementedInterfaces();
builder.RegisterType<RangeValidator>().AsImplementedInterfaces();

var container = builder.Build();
var csl = new AutofacServiceLocator(container);
ServiceLocator.SetLocatorProvider(() => csl);

Here are the library interfaces and classes:

public interface IHandler<in TSubject, out TTask>
    where TTask : Task
{
    TTask HandleAsync(TSubject subject);
}

And its specialization:

public interface IValidator<in TSubject>
    : IHandler<TSubject, Task>
{
}

I have this helper class which aggregates all the registered validation services into one (insures all the matching validators to report):

public class Validator<TSubject> : IValidator<TSubject>
{
    public Task HandleAsync(TSubject subject) =>
        Task.WhenAll(
            ServiceLocator.Current
                .GetAllInstances<IValidator<TSubject>>()
                .Select(validator => validator.HandleAsync(subject)));
}    

One more specialization:

public interface IReader<in TQuery, TResult>
    : IHandler<TQuery, Task<IEnumerable<TResult>>>
{
}

And an aggregating helper implementation (concatenates outputs of all the compatible readers):

public class Reader<TQuery, TResult> : IReader<TQuery, TResult>
{
    public async Task<IEnumerable<TResult>> HandleAsync(TQuery subject) =>
        (await Task.WhenAll(
            ServiceLocator.Current
                .GetAllInstances<IReader<TQuery, TResult>>()
                .Select(reader => reader.HandleAsync(subject))))
        .SelectMany(results => results);
}

Now the base class for the query to define validation, reading, and caching strategy:

public abstract class Query<TQuery, TResult>
    where TQuery : Query<TQuery, TResult>
{

    protected Query()
    {
        Data = new Lazy<Task<IEnumerable<TResult>>>(ExecuteAsync);
    }

    public TaskAwaiter<IEnumerable<TResult>> GetAwaiter() =>
        Data.Value.GetAwaiter();

    Lazy<Task<IEnumerable<TResult>>> Data { get; }

    protected virtual async Task<IEnumerable<TResult>> ExecuteAsync()
    {
        await new Validator<TQuery>()
            .HandleAsync((TQuery)this);

        return await new Reader<TQuery, TResult>()
            .HandleAsync((TQuery)this);            
    }
}

PROS:

  • Reusability (minimum amount of code for query invocation)
  • Extensibility (easy to add more handling services)
  • Traceability (ExecuteAsync defines a central point to add logging)
  • Query objects are serializable/model-bindable DTO
  • Required dependencies are injectable to handling services by IoC container.
  • Testable through LocatorService mocking (empty locator provides a reasonable default behavior).

CONS:

  • It is a ServiceLocator.
\$\endgroup\$
  • 1
    \$\begingroup\$ It's an interesting concept, as usual ;-) but I have difficulties to imagine its practical usage. If I had multiple ranges and wanted to apply different validators to each range... would it be possible? \$\endgroup\$ – t3chb0t Dec 26 '16 at 8:40
  • 1
    \$\begingroup\$ I wrote a lot of data access API and I have never been validating the same request differently :) It supposed to be used in, let's say, ASP.NET web api Controller action: [HttpGet] public async Task<IEnumerable<int>> Sequence(IntRange query) => await query; \$\endgroup\$ – Dmitry Nogin Dec 26 '16 at 8:49
  • \$\begingroup\$ One could subtype query to customize handling though. \$\endgroup\$ – Dmitry Nogin Dec 26 '16 at 8:52
  • \$\begingroup\$ @t3chb0t I have cleaned up the solution - it looks a way better now: github.com/dmitrynogin/Cqrs.Base \$\endgroup\$ – Dmitry Nogin Dec 27 '16 at 5:12
  • \$\begingroup\$ Wouldn't it be a better idea to edit the question and post the improved code? \$\endgroup\$ – t3chb0t Dec 27 '16 at 13:42
7
\$\begingroup\$

I'm going to do my usual complaining about basic issues, things that as a good programmer you should never get away with. (I'm looking at you Dmitry and t3chb0t. >.>)

Lazy<Task<IEnumerable<TResult>>> Data { get; }

Come on man, get those explicit access modifiers in there! Label that thing private!

public class Reader<TQuery, TResult> : IReader<TQuery, TResult>
{
    public async Task<IEnumerable<TResult>> HandleAsync(TQuery subject) =>
        (await Task.WhenAll(
            ServiceLocator.Current
                .GetAllInstances<IReader<TQuery, TResult>>()
                .Select(reader => reader.HandleAsync(subject))))
        .SelectMany(results => results);
}

That's just ugly. I get wanting to use expression-bodied members, they're really cool, but this just looks bad. Let's rewrite that as a non-expression-bodied method and create some intermediate variables, because I don't like seeing )))) anywhere in my code.

Back to this:

Lazy<Task<IEnumerable<TResult>>> Data { get; }

I don't like the name Data, I think Result would be a little more informative. Data acts like it'll be a more simple type, but you have to call Data.Value which just doesn't look right.


Now I'm going to complain about this idea in general.

I wrote a lot of data access API and I have never been validating the same request differently :) It supposed to be used in, let's say, ASP.NET web api Controller action: [HttpGet] public async Task<IEnumerable<int>> Sequence(IntRange query) => await query;

Here's the issue I find with this structure: if I want to change how things validate for an IntRange then I have to create different classes that work off different validators, add them to the configuration and hope for the best. I really fail to see where this would save any time/effort, especially if I want to validate something like a UserRange where you cannot select an Id larger than the max user ID, I'd have to either add it to the constructor (which means load from the database each call) or add it to the Validator (which means load from the database, and I have no idea how many times that will happen, I'd hope Autofac would only use one instance of the Validator, but who knows. Certainly not I) and hope for the best. This just seems like a lot of overhead for largely simple tasks. Oh, and I have to register my Validator and Sequence with Autofac.

\$\endgroup\$
  • \$\begingroup\$ 1) private - Common, it is year 2017. We do not write code which is not necessary to write anymore. Actually, Principles behind the Agile Manifesto: “Simplicity--the art of maximizing the amount of work not done--is essential” (c) 2001 :) 2) It is a way easy to refactor by extracting methods when there are no local variables (Refactoring: Improving the Design of Existing Code by Martin Fowler). One could say that these is how functional code looks like – when one does write code, it is more generally condensed than it used to be in days of old – in imperative era. \$\endgroup\$ – Dmitry Nogin Jun 6 '17 at 17:25
  • \$\begingroup\$ 3) Agree, Query.Result.Value is more readable. 4) I have never been changing how things been validated. Really :) It means that I have different query or, at least, different query parameter values – so it works fine. The same time, I have a lot of query reuse, so this what needs to be syntactically cheap. Multiple validators are totally OK – they all cooperate on the subject as intended – it is an extension point. And, in year 2017, I use registration by convention, so it is hardly to be an issue :) \$\endgroup\$ – Dmitry Nogin Jun 6 '17 at 17:25
  • 1
  • \$\begingroup\$ I have explicitly recommended myself to use Scala as an example of common sense ;) \$\endgroup\$ – Dmitry Nogin Jun 6 '17 at 17:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.