Dependency Scope Issues with MediatR and SimpleInjector

一笑奈何 提交于 2019-12-13 14:12:25

问题


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;
        }
    }
}

回答1:


The problem is that your decoratee (with its DbContext dependency) is created at the time the decorator is created, and at that time there is no active scope (since you create it at a later point in time). You should use a decoratee factory as decribed here. In other words, your LifetimeScopeDecorator should be implemented as follows:

public class LifetimeScopeDecorator<TRequest, TResponse> :
    IRequestHandler<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly Func<IRequestHandler<TRequest, TResponse>> _decorateeFactory;
    private readonly Container _container;

    public LifetimeScopeDecorator(
        Func<IRequestHandler<TRequest, TResponse>> decorateeFactory,
        Container container)
    {
        _decorateeFactory = decorateeFactory;
        _container = container;
    }

    public TResponse Handle(TRequest message)
    {
        using (_container.BeginLifetimeScope())
        {
            var result = _decorateeFactory.Invoke().Handle(message);
            return result;
        }
    }
}

The difference with your original implementation is that a Func<IRequestHandler<TRequest, TResponse>> is injected instead of an IRequestHandler<TRequest, TResponse>. This allows Simple Injector to postpone the creation after the scope has been created.



来源:https://stackoverflow.com/questions/38615432/dependency-scope-issues-with-mediatr-and-simpleinjector

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!