问题
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 providedScope
. - The execute method now creates a new
Scope
usingnew Scope(Container)
instead of relying on the ambient scoping ofAsyncScopedLifestyle
. - The
Func<Scope, T>
factory is provided with the created scope. - The
IDesignTimeDbContextFactory<T>
is resolved from theScope
instance, instead of using theContainer
.
来源:https://stackoverflow.com/questions/64575662/decorator-for-creating-scope-with-scopedlifestyle-flowing-in-simple-injector