Decorator for creating Scope with ScopedLifestyle.Flowing in Simple Injector

可紊 提交于 2021-01-29 22:27:11

问题


I need some help to understand what it's wrong in my configuration of the container.

I based this implementation by using this example.

Basically i need to implement some use case as database command based on that interface

public interface IDatabaseCommand<TResult, TParam>
{
    TResult Execute(TParam commandParam);
}

and i want to use a decorator that add the transaction safe functionality.

Every command need to use a dedicated DbContext and the transaction has to be executed on that context

To do this i have implemented

Transactional Decorator:

public class TransactionDatabaseCommandDecorator 
    : IDatabaseCommand<DatabaseResult, BusinessCommandParams1>
{
    private readonly Container _container;
    private readonly Func<IDatabaseCommand<DatabaseResult, BusinessCommandParams1>>
        _decorateeFactory;

    public TransactionDatabaseCommandDecorator(
        Container container,
        Func<IDatabaseCommand<DatabaseResult, BusinessCommandParams1>> decorateeFactory)
    {
        _container = container;
        _decorateeFactory = decorateeFactory;
    }

    public DatabaseResult Execute(BusinessCommandParams1 commandParam)
    {
        DatabaseResult res;
        using (AsyncScopedLifestyle.BeginScope(_container))
        {
            var _command = _decorateeFactory.Invoke();

            var factory = _container
                .GetInstance<IDesignTimeDbContextFactory<WpfRadDispenserDbContext>>();

            using (var transaction = factory.CreateDbContext(
                new[] { "" }).Database.BeginTransaction())
            {
                try
                {
                    res = _command.Execute(commandParam);
                    transaction.Commit();
                }
                catch (Exception e)
                {
                    Console.WriteLine(e);
                    transaction.Rollback();
                    throw;
                }
            }
        }

        return res;
    }
}

Example of implementation:

public class WpfRadDispenserUOW : IUnitOfWork<WpfRadDispenserDbContext>
{
    private readonly IDesignTimeDbContextFactory<WpfRadDispenserDbContext> _factory;
    private WpfRadDispenserDbContext _context;
    private IDbContextTransaction _transaction;
    public bool IsTransactionPresent => _transaction != null;

    public WpfRadDispenserUOW(IDesignTimeDbContextFactory<WpfRadDispenserDbContext> fact)
    {
        _factory = fact ?? throw new ArgumentNullException(nameof(fact));
    }

    public WpfRadDispenserDbContext GetDbContext() =>
         _context ?? (_context = _factory.CreateDbContext(null));

    public IDbContextTransaction GetTransaction() =>
        _transaction ?? (_transaction = GetDbContext().Database.BeginTransaction());

    public void RollBack()
    {
        _transaction?.Rollback();
        _transaction?.Dispose();
    }

    public void CreateTransaction(IsolationLevel isolationLevel) => GetTransaction();
    public void Commit() => _transaction?.Commit();
    public void Persist() => _context.SaveChanges();
    
    public void Dispose()
    {
        _transaction?.Dispose();
        _context?.Dispose();
    }
}

Some command:

public class BusinessCommand1 : IDatabaseCommand<DatabaseResult, BusinessCommandParams1>
{
    private readonly IUnitOfWork<WpfRadDispenserDbContext> _context;

    public BusinessCommand1(IUnitOfWork<WpfRadDispenserDbContext> context)
    {
       _context = context;
    }

    public DatabaseResult Execute(BusinessCommandParams1 commandParam)
    {
        //ToDo: use context
        return new DatabaseResult();
    }
}

Registration of container:

var container = new Container();
container.Options.DefaultScopedLifestyle = ScopedLifestyle.Flowing;

container.Register<IDesignTimeDbContextFactory<WpfRadDispenserDbContext>>(() =>
{
    var factory = new WpfRadDispenserDbContextFactory();
    factory.ConnectionString =
        "Server=.\\SqlExpress;Database=Test;Trusted_Connection=True";
    return factory;
});

container.Register<IUnitOfWork<WpfRadDispenserDbContext>, WpfRadDispenserUOW>(
    Lifestyle.Scoped);
container
    .Register<IUnitOfWorkFactory<WpfRadDispenserDbContext>, WpfRadDispenserUOWFactory>();

//Command registration
container.Register<
    IDatabaseCommand<DatabaseResult, BusinessCommandParams1>,
    BusinessCommand1>();

//Command Decorator registration
container.RegisterDecorator(
    typeof(IDatabaseCommand<DatabaseResult, BusinessCommandParams1>),
    typeof(TransactionDatabaseCommandDecorator),Lifestyle.Singleton);

The problem is that when i try to execute

var transactionCommandHandler =
    _container.GetInstance<IDatabaseCommand<DatabaseResult, BusinessCommandParams1>>();
usecase.Execute(new BusinessCommandParams1());

i receive correctly an instance of TransactionDatabaseCommandDecorator but when the i try to get the instance from the factory i receive this error

SimpleInjector.ActivationException: WpfRadDispenserUOW is registered using the 'Scoped' lifestyle, but the instance is requested outside the context of an active (Scoped) scope. Please see https://simpleinjector.org/scoped for more information about how apply lifestyles and manage scopes.

in SimpleInjector.Scope.GetScopelessInstance(ScopedRegistration registration)
in SimpleInjector.Scope.GetInstance[TImplementation](ScopedRegistration registration, Scope scope)
in SimpleInjector.Advanced.Internal.LazyScopedRegistration`1.GetInstance(Scope scope)
in WpfRadDispenser.DataLayer.Decorator.TransactionDatabaseCommandDecorator.Execute(BusinessCommandParams1 commandParam) in C:\Work\Git\AlphaProject\WpfRadDispenser\WpfRadDispenser.DataLayer\Decorator\TransactionDatabaseCommandDecorator.cs: riga 29
in WpfRadDispenser.Program.Main() in C:\Work\Git\AlphaProject\WpfRadDispenser\WpfRadDispenser\Program.cs: riga 47

The problem here is that i want to use a dbcontext that it's created and controlled by his decorator. But the constructor injection it's handled by container so how i can inject the context created by the decorator inside the command?

Basically i want to having something like that made by the decorator of the command

var context = ContextFactory.GetContext();

try
{
    var transaction = context.database.GetTransaction();
    var command = new Command(context);
    var commandParams = new CommandParams();
    var ret = command.Execute(commandParams);
    
    if (!ret.Success)
    {
        transaction.Discard();
        return;
    }
    
    transaction.Commit();
}
catch
{
    transaction.Discard();
}

but made with DI and Simple Injector

Maybe there is some issue or several issue on my design but i'm new on DI and i want to understand better how the things works.

Just to recap i need to use a lot of command database in which every command has to have an isolated context and the functionality of transaction has to be controlled by an extra layer inside the decorator.


回答1:


The problem is caused by the mixture of both flowing/closure scoping vs ambient scoping. Since you are writing a WPF application, you choose to use Simple Injector's Flowing scopes feature. This allows you to resolve instances directly from a scope (e.g. calling Scope.GetInstnace).

This, however, doesn't mix with Ambient Scoping, as is what AsyncScopedLifestyle.BeginScope does.

To fix this, you will have to change the implementation of your decorator to the following:

public class TransactionDatabaseCommandDecorator 
    : IDatabaseCommand<DatabaseResult, BusinessCommandParams1>
{
    private readonly Container _container;
    private readonly Func<IDatabaseCommand<DatabaseResult, BusinessCommandParams1>>
        _decorateeFactory;

    public TransactionDatabaseCommandDecorator(
        Container container,
        Func<IDatabaseCommand<DatabaseResult, BusinessCommandParams1>> decorateeFactory)
    {
        _container = container;
        _decorateeFactory = decorateeFactory;
    }

    public DatabaseResult Execute(BusinessCommandParams1 commandParam)
    {
        DatabaseResult res;
        using (Scope scope = new Scope(_container))
        {
            var command = _decorateeFactory.Invoke(scope);

            var factory = scope
                .GetInstance<IDesignTimeDbContextFactory<WpfRadDispenserDbContext>>();

            ...
        }

        return res;
    }
}

Note the following about the decorator above:

  • It gets injected with a Func<Scope, T> factory. This factory will create the decoratee using the provided Scope.
  • The execute method now creates a new Scope using new Scope(Container) instead of relying on the ambient scoping of AsyncScopedLifestyle.
  • The Func<Scope, T> factory is provided with the created scope.
  • The IDesignTimeDbContextFactory<T> is resolved from the Scope instance, instead of using the Container.


来源:https://stackoverflow.com/questions/64575662/decorator-for-creating-scope-with-scopedlifestyle-flowing-in-simple-injector

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