mercredi 28 octobre 2020

How register a transaction decorator for database Command with simpleinjector

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> factory)
    {
        _factory = factory ?? throw new ArgumentNullException(nameof(factory));
    }

    public WpfRadDispenserDbContext GetDbContext()
    {
        return _context ?? (_context = _factory.CreateDbContext(null));
    }

    public IDbContextTransaction GetTransaction()
    {
        if (_transaction == null)
            _transaction = GetDbContext().Database.BeginTransaction();

        return _transaction;
    }

    public void CreateTransaction(IsolationLevel isolationLevel)
    {
        GetTransaction();
    }

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

    public void Commit()
    {
        _transaction?.Commit();
    }

    public void Persist()
    {
        _context.SaveChanges();
    }

    public void Dispose()
    {
        _transaction?.Dispose();
        _context?.Dispose();
    }
}


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
  HResult=0x80131500
  Messaggio=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.
  Origine=SimpleInjector
  Analisi dello stack:
   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 simpleInjector

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.

Could you give me some advice on this?

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.

Aucun commentaire:

Enregistrer un commentaire