Использование простого инжектора с шаблоном работы и репозитория в форме Windows

Я пытаюсь реализовать IoC в своем приложении для форм Windows. Мой выбор упал на Simple Injector, потому что он быстрый и легкий. Я также реализую единицу работы и шаблон хранилища в своих приложениях. Вот структура:

DbContext

public class MemberContext : DbContext
{
    public MemberContext()
        : base("Name=MemberContext")
    { }

    public DbSet<Member> Members { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();\
    }
}

Model

public class Member
{
    public int MemberID { get; set; }
    public string Name { get; set; }
}

GenericRepository

public abstract class GenericRepository<TEntity> : IGenericRepository<TEntity> 
    where TEntity : class
{
    internal DbContext context;
    internal DbSet<TEntity> dbSet;

    public GenericRepository(DbContext context)
    {
        this.context = context;
        this.dbSet = context.Set<TEntity>();
    }

    public virtual void Insert(TEntity entity)
    {
        dbSet.Add(entity);
    }
}

MemberRepository

public class MemberRepository : GenericRepository<Member>, IMemberRepository
{
    public MemberRepository(DbContext context)
        : base(context)
    { }
}

UnitOfWork

public class UnitOfWork : IUnitOfWork
{
    public DbContext context;

    public UnitOfWork(DbContext context)
    {
        this.context = context;
    }

    public void SaveChanges()
    {
        context.SaveChanges();
    }

    private bool disposed = false;

    protected virtual void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            if (disposing)
            {
                context.Dispose();
            }
        }

        this.disposed = true;
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

MemberService

public class MemberService : IMemberService
{
    private readonly IUnitOfWork unitOfWork;
    private readonly IMemberRepository memberRepository;

    public MemberService(IUnitOfWork unitOfWork, IMemberRepository memberRepository)
    {
        this.unitOfWork = unitOfWork;
        this.memberRepository = memberRepository;
    }

    public void Save(Member member)
    {
        Save(new List<Member> { member });
    }

    public void Save(List<Member> members)
    {
        members.ForEach(m =>
            {
                if (m.MemberID == default(int))
                {
                    memberRepository.Insert(m);
                }
            });
        unitOfWork.SaveChanges();
    }
}

В форме участника я добавляю только текстовое поле для ввода имени элемента и кнопки для сохранения в базе данных. Это код в форме участника:

frmMember

public partial class frmMember : Form
{
    private readonly IMemberService memberService;

    public frmMember(IMemberService memberService)
    {
        InitializeComponent();

        this.memberService = memberService;
    }

    private void btnSave_Click(object sender, EventArgs e)
    {
        Member member = new Member();
        member.Name = txtName.Text;
        memberService.Save(member);
    }
}

Я реализую SimpleInjector (см. http://simpleinjector.readthedocs.org/en/latest/windowsformsintegration.html) в Program.cs, как показано в коде ниже

static class Program
{
    private static Container container;

    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Bootstrap();
        Application.Run(new frmMember((MemberService)container.GetInstance(typeof(IMemberService))));
    }

    private static void Bootstrap()
    {
        container = new Container();

        container.RegisterSingle<IMemberRepository, MemberRepository>();
        container.Register<IMemberService, MemberService>();
        container.Register<DbContext, MemberContext>();
        container.Register<IUnitOfWork, UnitOfWork>();

        container.Verify();
    }
}

Когда я запускаю программу и добавляю член, она не сохраняется в базе данных. Если я изменил container.Register на container.RegisterSingle, он сохранит базу данных. Из документации RegisterSingle мой класс станет Singleton. Я не могу использовать RegisterLifeTimeScope, потому что он будет генерировать ошибку

"Зарегистрированный делегат для типа IMemberService сделал исключение. IUnitOfWork зарегистрирован как образ жизни" Срок службы ", но экземпляр запрашивается вне контекста" Срок службы "

1) Как использовать SimpleInjector в Windows Form с шаблоном UnitOfWork и Repository?
2) Правильно ли реализую шаблоны?

Ответ 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 и (конкретной) службы.