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

时光毁灭记忆、已成空白 提交于 2019-12-06 14:40:52

问题


I have an WPF application that adheres command/query pattern and uses EF as an ORM.

In my mind, when new ViewModel is created, new instance of DbContext should be created and that same instance should be reused across (injected into) all command/query handlers, which are created within the scope of that particular ViewModel. At the end of ViewModel's lifetime, DbContext should be disposed.

How to achieve such a setup with Simple Injector?


回答1:


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.




回答2:


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.



来源:https://stackoverflow.com/questions/34181943/dbcontext-lifestyle-per-screen-viewmodel-wpf-simple-injector

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