Ответ 1
У вас есть разница в образе жизни между вашим сервисом, репозиторием, unitofwork и dbcontext.
Поскольку MemberRepository
имеет стиль Singleton, Simple Injector создаст один экземпляр, который будет использоваться повторно на протяжении всего приложения, и может быть дни, даже недели или месяцы с приложением WinForms. Прямым следствием регистрации MemberRepository
как Singleton является то, что все зависимости этого класса также станут Singletons, независимо от того, какой образ жизни используется при регистрации. Это общая проблема, называемая Captive Dependency.
В качестве дополнительной заметки: диагностические службы Simple Injector могут обнаружить эту ошибку конфигурации и показать/выбросить Предупреждение о возможном несоответствии образа жизни.
Таким образом, MemberRepository
является Singleton и имеет один и тот же DbContext
на протяжении всей жизни приложения. Но UnitOfWork
, который имеет зависимость также от DbContext
, получит другой экземпляр DbContext
, потому что регистрация для DbContext
является Transient. Этот контекст в вашем примере никогда не сохранит вновь созданный Member
, потому что этот DbContext
не имеет вновь созданного Member
, член создается в другом DbContext
.
При изменении регистрации DbContext
на RegisterSingleton
он начнет работать, потому что теперь каждая служба, класс или что-то другое в зависимости от DbContext
получит тот же экземпляр.
Но это, безусловно, не решение, потому что наличие одного DbContext
для срока службы приложения вызовет у вас проблемы, как вы, вероятно, уже знаете. Это подробно объясняется в этом сообщении .
Вам нужно использовать экземпляр Scoped для DbContext
, который вы уже пробовали. Вам не хватает информации о том, как использовать функцию видимости в течение всей жизни Simple Injector (и большинство других контейнеров). При использовании образцового образа жизни должен быть активен, поскольку сообщение об исключении четко указано. Начало жизненного цикла довольно просто:
using (ThreadScopedLifestyle.BeginScope(container))
{
// all instances resolved within this scope
// with a ThreadScopedLifestyleLifestyle
// will be the same instance
}
Вы можете подробно прочитать здесь.
Изменение регистраций на:
var container = new Container();
container.Options.DefaultScopedLifestyle = new ThreadScopedLifestyle();
container.Register<IMemberRepository, MemberRepository>(Lifestyle.Scoped);
container.Register<IMemberService, MemberService>(Lifestyle.Scoped);
container.Register<DbContext, MemberContext>(Lifestyle.Scoped);
container.Register<IUnitOfWork, UnitOfWork>(Lifestyle.Scoped);
и сменив код с btnSaveClick()
на:
private void btnSave_Click(object sender, EventArgs e)
{
Member member = new Member();
member.Name = txtName.Text;
using (ThreadScopedLifestyle.BeginScope(container))
{
var memberService = container.GetInstance<IMemberService>();
memberService.Save(member);
}
}
- это в основном то, что вам нужно.
Но мы ввели новую проблему. Теперь мы используем антивирусное свойство Locator, чтобы получить экземпляр с областью действия IMemberService
. Поэтому нам нужен какой-то инфраструктурный объект, который будет обрабатывать это для нас как Cross-Cutting Concern в приложении. A Decorator - это идеальный способ реализовать это. См. Также здесь. Это будет выглядеть так:
public class ThreadScopedMemberServiceDecorator : IMemberService
{
private readonly Func<IMemberService> decorateeFactory;
private readonly Container container;
public ThreadScopedMemberServiceDecorator(Func<IMemberService> decorateeFactory,
Container container)
{
this.decorateeFactory = decorateeFactory;
this.container = container;
}
public void Save(List<Member> members)
{
using (ThreadScopedLifestyle.BeginScope(container))
{
IMemberService service = this.decorateeFactory.Invoke();
service.Save(members);
}
}
}
Теперь вы зарегистрируете это как (Singleton) Decorator в Simple Injector Container
следующим образом:
container.RegisterDecorator(
typeof(IMemberService),
typeof(ThreadScopedMemberServiceDecorator),
Lifestyle.Singleton);
Контейнер предоставит класс, который зависит от IMemberService
с этим ThreadScopedMemberServiceDecorator
. В этом случае контейнер будет вводить Func<IMemberService>
, который при вызове возвращает экземпляр из контейнера с использованием настроенного образа жизни.
Добавление этого Decorator (и его регистрация) и изменение образа жизни устранит проблему из вашего примера.
Я ожидаю, однако, что ваше приложение, в конце концов, имеет IMemberService
, IUserService
, ICustomerService
и т.д. Итак, вам нужен декоратор для каждого IXXXService
, не очень DRY, если вы спросите меня. Если все службы будут реализовывать Save(List<T> items)
, вы можете рассмотреть возможность создания открытого общего интерфейса:
public interface IService<T>
{
void Save(List<T> items);
}
public class MemberService : IService<Member>
{
// same code as before
}
Вы регистрируете все реализации в одной строке, используя Batch-Registration:
container.Register(typeof(IService<>),
new[] { Assembly.GetExecutingAssembly() },
Lifestyle.Scoped);
И вы можете обернуть все эти экземпляры в единую открытую общую реализацию вышеупомянутого ThreadScopedServiceDecorator
.
Было бы даже лучше использовать шаблон команды/обработчика (вы действительно должны прочитать ссылку!) для этого типа работы, Очень коротко: в этом шаблоне каждый use case переводится в объект сообщения (команду), который обрабатывается одним командным обработчиком, который могут быть украшены, например, a SaveChangesCommandHandlerDecorator
и a ThreadScopedCommandHandlerDecorator
и LoggingDecorator
и т.д.
Ваш пример будет выглядеть следующим образом:
public interface ICommandHandler<TCommand>
{
void Handle(TCommand command);
}
public class CreateMemberCommand
{
public string MemberName { get; set; }
}
Со следующими обработчиками:
public class CreateMemberCommandHandler : ICommandHandler<CreateMemberCommand>
{
//notice that the need for MemberRepository is zero IMO
private readonly IGenericRepository<Member> memberRepository;
public CreateMemberCommandHandler(IGenericRepository<Member> memberRepository)
{
this.memberRepository = memberRepository;
}
public void Handle(CreateMemberCommand command)
{
var member = new Member { Name = command.MemberName };
this.memberRepository.Insert(member);
}
}
public class SaveChangesCommandHandlerDecorator<TCommand>
: ICommandHandler<TCommand>
{
private ICommandHandler<TCommand> decoratee;
private DbContext db;
public SaveChangesCommandHandlerDecorator(
ICommandHandler<TCommand> decoratee, DbContext db)
{
this.decoratee = decoratee;
this.db = db;
}
public void Handle(TCommand command)
{
this.decoratee.Handle(command);
this.db.SaveChanges();
}
}
Теперь форма теперь может зависеть от ICommandHandler<T>
:
public partial class frmMember : Form
{
private readonly ICommandHandler<CreateMemberCommand> commandHandler;
public frmMember(ICommandHandler<CreateMemberCommand> commandHandler)
{
InitializeComponent();
this.commandHandler = commandHandler;
}
private void btnSave_Click(object sender, EventArgs e)
{
this.commandHandler.Handle(
new CreateMemberCommand { MemberName = txtName.Text });
}
}
Все это можно зарегистрировать следующим образом:
container.Register(typeof(IGenericRepository<>),
typeof(GenericRepository<>));
container.Register(typeof(ICommandHandler<>),
new[] { Assembly.GetExecutingAssembly() });
container.RegisterDecorator(typeof(ICommandHandler<>),
typeof(SaveChangesCommandHandlerDecorator<>));
container.RegisterDecorator(typeof(ICommandHandler<>),
typeof(ThreadScopedCommandHandlerDecorator<>),
Lifestyle.Singleton);
Этот проект полностью устранит необходимость UnitOfWork
и (конкретной) службы.