Как использовать один экземпляр DbContext?(IUnitOfWork)

319
19 мая 2017, 12:54

В EF DbContext и DbSet, вообще говоря, реализуют из коробки соответственно UnitOfWork и Repository. В интернете тысячи примеров как люди следуя четко по букварям оборачивает их руками в свои классы, которые реализуют свои интерфейсы.Что-то вроде этого:

public interface IUnitOfWork : IDisposable
{
    IRepository<T> GetRepository<T>() where T : class;
    void SaveAllChanges();
}
 public interface IRepository<T> : IDisposable
    where T : class
{
    IQueryable<T> Entities();
    void Update(T entity);
    void Add(T entity);
    void Remove(T entity);
    bool Contains(T entity);
}

Получается своего рода абстракция над абстракцией, во многих местах пишут что этого не следует делать, но нигде примера реализации как правильно нет. Я имею проект на трехслойке, изначально сделал так же через repo и uow, позже убрал репозитории и uow, но при добавлении объектов из разных классов к друг другу ловлю: System.InvalidOperationException: "Не удалось определить связь между двумя объектами, поскольку они привязаны к разным объектам ObjectContext." Как я понимаю, это из за того что я каждый раз объявляю новый экземпляр контекста данных, вот что у меня щас в БЛЛ:

 public class EntityService : IEntityService
 {
    private MyContext db;
    public EntityService(string connectionString)
    {
        db = new MyContext(connectionString);
     }
    ...
}

Как мне правильно передавать один экземпляр контекста данных вовсе подобные сервисы без явной повторной реализации UoW?

Answer 1

Если пропустить промежуточные стадии, то вариантов два (с половиной):

Вариант 1

Если вы не пишете тесты, то вам вообще не нужна абстракция IRepository в таком виде. Создавайте контекст на самом верху, в методе, который у вас соответствует одной бизнес-операции, связывайте объекты друг с другом, потом один раз вызывайте SaveChanges - и EF сам разберется.

Это идеология, заложенная в EF, и попытки пойти против нее вызывают много кода и боль.

Вариант 1.01

Вариант, привычный со времен NHibernate - сделать контекст на запрос (от пользователя), положить в HttpContext (напрямую или через IoC) и более-менее споконо жить до момента, когда вам придется провести две раздельных операции за один запрос. Или записать ошибку операции в лог. Когда момент наступит - переписать на [ThreadStatic] / <AsyncLocal> и жить дальше.

Вариант 2

Если вам нужны и UoW, и тесты, и репозиторий (ради тестов и ради локализации запросов), то придется наворачивать что-то вроде:

Интерфейс IoW в качестве точки доступа к репозиториям:

public interface IUnitOfWork : IDisposable
{
    IEntity1Repository Entity1Repository { get; }
    IEntity2Repository Entity2Repository { get; }
    void Save();
}

Его реализацию в виде

public class UnitOfWork : IUnitOfWork
{
    static AsyncLocal<UnitOfWork> _root = new AsyncLocal<UnitOfWork>();
    private readonly SomeModel _context;
    public UnitOfWork()
    {
        if (_root.Value == null)
        {
            this._context = new SomeModel();
            _root.Value = this;
        }
        else
        {
            this._context = _root.Value._context;
        }
    }
    public void Save()
    {
        if (_root.Value == this)
        {
            _context.SaveChanges();
        }
    }
    public IEntity1Repository Entity1Repository => new Entity1Repository(_context);
    public void Dispose()
    {
        if (_root.Value == this)
        {
            _context.Dispose();
            _root.Value = null;
        }
    }
}

Т.к. вам захочется мокать репозитории и контекст, то придется добавить интерфейс для создания UoW:

public interface IUnitOfWorkFactory
{
    IUnitOfWork Create();
}

вставлять его в виде зависимостей в сервисы и использовать примерно так:

public class SomeService : ISomeService
{
    [Dependency]
    public IUnitOfWorkFactory UoWFactory { get; set; }
    public List<Entity1> GetAllEntitiesList()
    {
        using (var uow = UoWFactory.Create())
        {
            // добавить вызов Save в тех методах, которые действительно что-то меняют
            return uow.Entity1Repository.GetAll();
        }
    }
}

и мокать примерно так:

var list = new List<Entity1> { new Entity1() };
var repo = Mock.Of<IEntity1Repository>(repo => repo.GetAll() == list);
var uow = Mock.Of<IUnitOfWork>(u => u.Entity1Repository == repo);
var uowFactory = Mock.Of<IUnitOfWorkFactory>(f => f.Create() == uow);
var service = new SomeService() { UoWFactory = uowFactory };
var result = service.GetAllEntitiesList();
CollectionAssert.AreEqual(list, result);

Ссылка по теме, с кучей других вариантов вставки: Survey of Entity Framework Unit of Work Patterns

Answer 2

Можно передавать в конструктор DbContext который будет возвращать какой-нибудь DI-контейнер. В самом контейнере уже настроить жизненный цикл объекта

private DbContext db;
public EntityService(DbContext context)
{
   db = context;
}

В контейнерах регистрируем MyContext для DbContext. Если уверенны, что тип вашего зарегистрированного контекста не поменяется, то можно еще и приведение сделать

private MyContext db;
public EntityService(DbContext context)
{
   db = (MyContext)context;
}
READ ALSO
xaml верхний и нижний отступы у button

xaml верхний и нижний отступы у button

Делаю приложение для Windows Phone 81

180
Как определить, из какой формы была открыта текущая?

Как определить, из какой формы была открыта текущая?

Из одной формы запускается другая при помощи подобного кода (по сути, стандартного):

276
Как объединить две кнопки используя if

Как объединить две кнопки используя if

Есть button Play и button PauseХочу объединить их в одну кнопку, чтобы когда композиция уже играет при нажатии ставилась пауза, а если стоит пауза то при...

208