DbContext Lifestyle per Screen/ViewModel (WPF + Simple Injector)

牧云@^-^@ 提交于 2019-12-04 18:43:58

If you are applying the command/handler and query/handler patterns as described here and here, the most logical thing to do is to scope the lifetime of a DbContext around the execution of a command and query.

This can be achieved by defining a single generic decorator that allows applying scoping:

using SimpleInjector;
using SimpleInjector.Extensions.LifetimeScoping;

public class LifetimeScopedCommandHandlerDecorator<T> : ICommandHandler<T>
{
    private readonly Container container;
    private readonly Func<ICommandHandler<T>> decorateeProvider;
    public LifetimeScopedCommandHandlerDecorator(Container container,
        Func<ICommandHandler<T>> decorateeProvider) {
        this.container = container;
        this.decorateeProvider = decorateeProvider;
    }

    public void Handle(T command) {
        using (container.BeginLifetimeScope()) {
            this.decorateeProvider().Handle(command);
        }
    }
}

This decorator can be registered as last decorator as follows:

container.RegisterDecorator(typeof(ICommandHandler<>),
    typeof(LifetimeScopedCommandHandlerDecorator<>),
    Lifestyle.Singleton);

After doing this, you can register your DbContext using the LifetimeScopeLifestyle.

You can do the same trick with query handlers.

The great advantage of this is that you allow your force strict isolation of your command handlers and query handlers, minimizing the risk of influencing each other through a shared DbContext and it makes it easier later on to move your handlers to a different tier, by sending your command and query messages over the wire, as explained here.

Finally I managed to get it working with help of container.RegisterInitializer method and custom ScopedLifeStyle:

public class PerScreenScopedLifestyle : ScopedLifestyle
{
    public PerScreenScopedLifestyle()
            : this(disposeInstanceWhenScopeEnds: true)
    {
    }

    public PerScreenScopedLifestyle(bool disposeInstanceWhenScopeEnds) : base("Per Screen Scope", disposeInstanceWhenScopeEnds)
    {
    }

    protected override Registration CreateRegistrationCore<TService, TImplementation>(Container container)
    {
        // any time a IQueryBus is requested, new scope should be created..
        container.RegisterInitializer<IQueryBus>(QueryBusInitializer);

        return base.CreateRegistrationCore<TService, TImplementation>(container);
    }

    protected override Registration CreateRegistrationCore<TService>(Func<TService> instanceCreator, Container container)
    {
        // any time a IQueryBus is requested, new scope should be created..
        container.RegisterInitializer<IQueryBus>(QueryBusInitializer);

        return base.CreateRegistrationCore<TService>(instanceCreator, container);
    }

    void QueryBusInitializer(IQueryBus obj)
    {
        // any scope used before must be disposed
        if (scope != null)
        {
            scope.Dispose();
        }

        // create new scope
        scope = new Scope();
    }

    protected override Scope GetCurrentScopeCore(Container container)
    {
        return scope;
    }

    protected override Func<Scope> CreateCurrentScopeProvider(Container container)
    {
        return () =>
        {
            var result = scope == null ? new Scope() : scope;
            return result;
        };
    }

    Scope scope;
}

and DbContext is registered as:

Container.Register<DbContext, MyDbContext>(Lifestyle.Scoped);

and container is configured as:

Container.Options.DefaultScopedLifestyle = new PerScreenScopedLifestyle();

How it works:

Everytime an IQueryBus is created, new Scope is created and previous Scope is disposed in the RegisterInitializer method of IQueryBus. When DbContext is requested, CreateCurrentScopeProvider returns the cached Scope containing cached DbContext. This means that DbContext will be shared for the lifetime of IQueryBus - and IQueryBus is injected into the ViewModel as Transient, so I will get always the same instance of DbContext until next new ViewModel gets injected with new IQueryBus.

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