I've been experimenting with the mediator pattern and CQRS using the MediatR library in a WinForms application that uses the Entity Framework for data access. The application is used in a batch manufacturing plant, and allows users to see a list of active and completed batches, and if necessary make updates to batch information. Each batch has a large amount of information associated with it, such as quality and process measurements. Reading and writing data is organized into Queries and Commands, based on these articles:
Meanwhile... on the query side of my architecture
CQRS with MediatR and AutoMapper
Here is a simple example of a query and query handler. DataContext
is injected into the query handler using SimpleInjector.
public class GetAllBatchesQuery: IRequest<IEnumerable<Batch>> { }
public class GetAllBatchesQueryHandler :
IRequestHandler<GetAllBatchesQuery, IEnumerable<Batch>>
{
private readonly DataContext _context;
public GetAllBatchesQueryHandler(DataContext context)
{
_context= context;
}
public IEnumerable<Batch> Handle(GetAllBatchesQueryrequest)
{
return _db.Batches.ToList();
}
}
This would be called from the presenter as follows:
var batches = mediator.Send(new GetAllBatchesQuery());
The problem that I'm running into is with the lifetime of the DbContext. Ideally, I'd like to use a single instance per isolated transaction, which in this case would include such things as:
- Retrieving the list of batches from the database
- Retrieving a list of quality metrics for a batch (these are stored in a different database and accessed through a stored procedure)
- Updating a batch, which may include updating multiple entities in the database
This would lead me towards a scoped or transient lifestyle for DbContext. However, when using a transient lifestyle, SimpleInjector raises the following error, which is thrown when registering the type as follows:
container.Register<DataContext>();
An unhandled exception of type 'SimpleInjector.DiagnosticVerificationException' occurred in SimpleInjector.dll
Additional information: The configuration is invalid. The following diagnostic warnings were reported:
-[Disposable Transient Component] DataContext is registered as transient, but implements IDisposable.
Researching this issue on the SimpleInjector website reveals the following note:
Warning: Transient instances are not tracked by the container. This means that Simple Injector will not dispose transient instances.
This led me down the path of using a Lifetime Scope lifestyle for the DataContext. To achieve this, I created a new decorator class for my queries and registered it as follows:
public class LifetimeScopeDecorator<TRequest, TResponse> :
IRequestHandler<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IRequestHandler<TRequest, TResponse> _decorated;
private readonly Container _container;
public LifetimeScopeDecorator(
IRequestHandler<TRequest, TResponse> decorated,
Container container)
{
_decorated = decorated;
_container = container;
}
public TResponse Handle(TRequest message)
{
using (_container.BeginLifetimeScope())
{
var result = _decorated.Handle(message);
return result;
}
}
}
...
container.RegisterDecorator(
typeof(IRequestHandler<,>),
typeof(ExecutionContextScopeDecorator<,>));
However, making that change causes a different exception, this time thrown at the following line:
var batches = mediator.Send(new GetAllBatchesQuery());
An unhandled exception of type 'System.InvalidOperationException' occurred in MediatR.dll
Additional information: Handler was not found for request of type MediatorTest.GetAllBatchesQuery.
Container or service locator not configured properly or handlers not registered with your container.
After debugging and looking through the MediatR code, it appears that when the mediator.Send(...)
method is called, a new instance of the GetAllBatchesQueryHandler
class is created by calling container.GetInstance()
. However, since DataContext
is not within an execution scope at this point, it may not be properly initialized, causing the exception.
I believe I understand the root cause of the issue, but am at a loss as to how to effectively resolve it. To help illustrate this problem better, I developed the following minimal example. Any class that implements IDisposable
will result in the same issue that I am having with DataContext
.
using System;
using System.Collections.Generic;
using System.Reflection;
using MediatR;
using SimpleInjector;
using SimpleInjector.Extensions.LifetimeScoping;
namespace MediatorTest
{
public class GetRandomQuery : IRequest<int>
{
public int Min { get; set; }
public int Max { get; set; }
}
public class GetRandomQueryHandler : IRequestHandler<GetRandomQuery, int>
{
private readonly RandomNumberGenerator _r;
public GetRandomQueryHandler(RandomNumberGenerator r)
{
_r = r;
}
public int Handle(GetRandomQuery request)
{
return _r.Next(request.Min, request.Max);
}
}
public class RandomNumberGenerator : IDisposable
{
private Random _random = new Random();
public RandomNumberGenerator() { }
public void Dispose() { }
public int Next(int min, int max)
{
var result = _random.Next(min, max);
return result;
}
}
public class LifetimeScopeDecorator<TRequest, TResponse> :
IRequestHandler<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IRequestHandler<TRequest, TResponse> _decorated;
private readonly Container _container;
public LifetimeScopeDecorator(
IRequestHandler<TRequest, TResponse> decorated,
Container container)
{
_decorated = decorated;
_container = container;
}
public TResponse Handle(TRequest message)
{
using (_container.BeginLifetimeScope())
{
var result = _decorated.Handle(message);
return result;
}
}
}
class Program
{
static void Main(string[] args)
{
var assemblies = GetAssemblies();
var container = new Container();
container.Options.DefaultScopedLifestyle = new LifetimeScopeLifestyle();
container.RegisterSingleton<IMediator, Mediator>();
container.Register<RandomNumberGenerator>(Lifestyle.Scoped);
container.Register(typeof(IRequestHandler<,>), assemblies);
container.RegisterSingleton(new SingleInstanceFactory(container.GetInstance));
container.RegisterSingleton(new MultiInstanceFactory(container.GetAllInstances));
container.RegisterDecorator(
typeof(IRequestHandler<,>),
typeof(LifetimeScopeDecorator<,>));
container.Verify();
var mediator = container.GetInstance<IMediator>();
var value = mediator.Send(new GetRandomQuery() { Min = 1, Max = 100 });
Console.WriteLine("Value = " + value);
Console.ReadKey();
}
private static IEnumerable<Assembly> GetAssemblies()
{
yield return typeof(IMediator).GetTypeInfo().Assembly;
yield return typeof(GetRandomQuery).GetTypeInfo().Assembly;
}
}
}