В 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?
Если пропустить промежуточные стадии, то вариантов два (с половиной):
Если вы не пишете тесты, то вам вообще не нужна абстракция IRepository в таком виде. Создавайте контекст на самом верху, в методе, который у вас соответствует одной бизнес-операции, связывайте объекты друг с другом, потом один раз вызывайте SaveChanges - и EF сам разберется.
Это идеология, заложенная в EF, и попытки пойти против нее вызывают много кода и боль.
Вариант, привычный со времен NHibernate - сделать контекст на запрос (от пользователя), положить в HttpContext (напрямую или через IoC) и более-менее споконо жить до момента, когда вам придется провести две раздельных операции за один запрос. Или записать ошибку операции в лог. Когда момент наступит - переписать на [ThreadStatic] / <AsyncLocal> и жить дальше.
Если вам нужны и 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
Можно передавать в конструктор DbContext который будет возвращать какой-нибудь DI-контейнер. В самом контейнере уже настроить жизненный цикл объекта
private DbContext db;
public EntityService(DbContext context)
{
db = context;
}
В контейнерах регистрируем MyContext для DbContext. Если уверенны, что тип вашего зарегистрированного контекста не поменяется, то можно еще и приведение сделать
private MyContext db;
public EntityService(DbContext context)
{
db = (MyContext)context;
}
Сборка персонального компьютера от Artline: умный выбор для современных пользователей