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
.
[HttpGet] public async Task<IEnumerable<int>> Sequence(IntRange query) => await query;
\$\endgroup\$