This blog post is the last of the series Plug-and-play architecture in Asp.NET 5 and we will see how we can actually implement the plug-and-play execution pipeline.
Execution context vs execution pipeline?
Execution context is the metadata that every request comes with. As we discussed in the previous blog post the context can hold data for a request id, principal, and everything else that we might need as part of each request.
Execution pipeline on the other hand is a series of steps that each request goes through. Depending on the configuration we usually see a pipeline in the MVC architecture as described in the image above. We need to implement a pipeline for both queries and commands that implicitly will do some things for us, like logging exceptions or logging commands/events.
First let's configure a pipeline interface that we will use to create the pipeline middlewares:
using PlugAndPlayExample.Infrastructure.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
namespace PlugAndPlayExample.Services.Infrastructure
{
public interface IPipeline
{
TResponse Execute<TResponse>(Func<TResponse> inner) where TResponse : Response;
IPipeline Next { get; set; }
}
}
And lets create two different middlewares that we will use:
using PlugAndPlayExample.Infrastructure.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
namespace PlugAndPlayExample.Services.Infrastructure
{
public class LoggingPipeline : IPipeline
{
public IPipeline Next { get; set; }
private readonly ILogger logger;
public LoggingPipeline(ILogger logger)
{
this.logger = logger;
}
public TResponse Execute<TResponse>(Func<TResponse> inner) where TResponse : Response
{
try
{
return Next.Execute(inner);
}
catch (Exception ex)
{
logger.Error(ex.Message, ex);
throw;
}
}
}
public class RunnerPipeline : IPipeline
{
public IPipeline Next { get; set; }
public TResponse Execute<TResponse>(Func<TResponse> inner) where TResponse : Response
{
return inner();
}
}
}
The logging middleware executes a function and if it throws an exception, it logs it. The runner middleware just runs the function and we always use it as the last middleware with no Next property.
Now we need to implement the command and query middleware configuration and order:
using PlugAndPlayExample.Infrastructure.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
namespace PlugAndPlayExample.Services.Infrastructure
{
public interface ICommandPipeline
{
TResponse Execute<TResponse>(Func<TResponse> inner) where TResponse : Response;
}
public interface IQueryPipeline
{
TResponse Execute<TResponse>(Func<TResponse> inner) where TResponse : Response;
}
public class CommandPipeline : ICommandPipeline
{
private List<IPipeline> pipelines;
public CommandPipeline(IServiceProvider serviceProvider)
{
pipelines = new List<IPipeline>();
pipelines.Add(serviceProvider.GetService(typeof(LoggingPipeline)) as LoggingPipeline);
pipelines.Add(serviceProvider.GetService(typeof(RunnerPipeline)) as RunnerPipeline);
for (int i = 0; i < pipelines.Count - 1; i++)
{
pipelines[i].Next = pipelines[i + 1];
}
}
public TResponse Execute<TResponse>(Func<TResponse> inner) where TResponse : Response
{
return pipelines.FirstOrDefault()?.Execute(inner);
}
}
public class QueryPipeline : IQueryPipeline
{
private List<IPipeline> pipelines;
public QueryPipeline(IServiceProvider serviceProvider)
{
pipelines = new List<IPipeline>();
pipelines.Add(serviceProvider.GetService(typeof(RunnerPipeline)) as RunnerPipeline);
for (int i = 0; i < pipelines.Count - 1; i++)
{
pipelines[i].Next = pipelines[i + 1];
}
}
public TResponse Execute<TResponse>(Func<TResponse> inner) where TResponse : Response
{
return pipelines.FirstOrDefault()?.Execute(inner);
}
}
}
We are finally ready to start using this middlewares in our mediator and have our plug and play architecture:
using System;
namespace PlugAndPlayExample.Services.Infrastructure
{
public class Mediator
{
private readonly IServiceProvider serviceProvider;
private readonly IQueryPipeline queryPipeline;
private readonly ICommandPipeline commandPipeline;
public Mediator(IServiceProvider serviceProvider, IQueryPipeline queryPipeline, ICommandPipeline commandPipeline)
{
this.serviceProvider = serviceProvider;
this.queryPipeline = queryPipeline;
this.commandPipeline = commandPipeline;
}
public TResult Dispatch<TCommand, TResult>(TCommand command)
where TCommand : ICommand
where TResult : Response
{
var commandHandler = serviceProvider.GetService(typeof(ICommandHandler<TCommand, TResult>)) as ICommandHandler<TCommand, TResult>;
return commandPipeline.Execute(() => commandHandler.Handle(command));
}
public TResult Get<TQuery, TResult>(TQuery query)
where TQuery : IQuery
where TResult : Response
{
var queryHandler = serviceProvider.GetService(typeof(IQueryHandler<TQuery, TResult>)) as IQueryHandler<TQuery, TResult>;
return queryPipeline.Execute(() => queryHandler.Handle(query));
}
}
}
Finally let's not forget to register everything in the service container:
using Microsoft.Extensions.DependencyInjection;
using PlugAndPlayExample.Infrastructure.Caching;
using PlugAndPlayExample.Infrastructure.Logging;
using PlugAndPlayExample.Services.Infrastructure;
using System.IO;
namespace PlugAndPlayExample.Configuration
{
public static class RegisterInfrastructureAspectsExtension
{
public static IServiceCollection RegisterInfrastructureAspects(this IServiceCollection services)
{
services.AddSingleton<ICache, InMemoryCache>();
services.AddSingleton<ILogger, FileLogger>(provider => new FileLogger(new FileInfo("my_log_file.log")));
services.AddSingleton<IQueryPipeline, QueryPipeline>();
services.AddSingleton<ICommandPipeline, CommandPipeline>();
services.AddSingleton<RunnerPipeline>();
services.AddSingleton<LoggingPipeline>();
return services;
}
}
}
Hope you found this blog post helpful. 😄
Happy coding,
DotNetGuru