EF Code First с репозиторием, UnitOfWork и DbContextFactory
Я собираюсь> взорвать <:) из-за количества чтения на эту тему ... У меня болит голова, и мне нужны честные мнения ... Существует аналогичный вопрос /обзор , который я заметил, но я считаю, что мой подход немного другой, поэтому я хотел спросить.
Мы приступаем к действительно большому проекту, который будет использовать ASP.NET MVC 4 с Web API и хотел бы использовать Entity Framework Code First. Прежде чем отправлять код для проверки, , пожалуйста, рассмотрите
- Мой проект будет большим (возможно, более 100 доменов)
- Нужен SOLID подход к архитектуре
- В настоящее время я понимаю, что Entity Framework
DbContext
(полностью или частично) реализует шаблоны Unit of Work и Repository, поэтому я мог бы пропустить все это;) - Я действительно обеспокоен использованием EF, введенным непосредственно в конструкторы Controller (тестируемость, разделение проблем и т. д. и т. д.).
- Я также знаю, что существует несколько способов реализации шаблонов UoW и Repository.
- Я не беспокоюсь или не хочу создавать абстракции, которые позволят мне «обменивать» ORM (например, своп Entity Framework для NHiberante или такой).
Мой подход
- Репозитории являются универсальными и имеют базовый класс, который реализует большую часть стандартной логики.
- Репозитории нужны
DbContext
через конструктор (который предоставляетсяUnitOfWork
) -
UnitOfWork
отвечает за управление доступом ко всем репозиториям, и между ними существует общий контекст защиты. -
UnitOfWork
является одноразовым, репозитории не ... - Чтобы «скрыть»
DbContext
,UnitOfWork
создается с помощьюIDbContextFactory
.
Вопросы
- Кажется, это работает для меня, и я вижу, что каждый контроллер просто нуждается в инъекции UoW, что приятно. Некоторым контроллерам требуется 2-3 репозитория в дополнение к сервисам домена, поэтому это делает вещи приятными ... Я думаю ...
- Со временем UoW будет расти вместе с репозиториями (может быть 65+ совокупных корней, каждый из которых имеет репо). Как и как лучше управлять этим? Должен ли я каким-то образом вводить репозитории вместо od new (), используя их в
UnitOfWork
? Мне бы хотелось создать модуль IoC (Autofac - мой яд), чтобы связать все репозитории (как-то) - Является ли использование
IDbContextFactory
излишним, или я должен просто добавитьDbContext
в конструкторUnitOfWork
вместо этого? Прямо сейчас, у моего веб-приложения нет прямой зависимости от Entity Framework, он зависит только от n DAL (что, в свою очередь, зависит от EF). С другой стороны, DbContextFactory new () es upMyAppDbContext
и не обрабатывается IoC - Кто-нибудь замечает какой-либо другой «запах кода»?
- Некоторые вопросы содержатся в комментариях к коду, чтобы сделать их более релевантными ...
Хорошо вот код с двумя репозиториями и использованием примера (все пространства имен опущены для краткости)
IDbContextFactory и DbContextFactory
/// <summary>
/// Creates instance of specific DbContext
/// </summary>
public interface IDbContextFactory //: IDisposable //NOTE: Since UnitOfWork is disposable I am not sure if context factory has to be also...
{
DbContext GetDbContext();
}
public class DbContextFactory : IDbContextFactory
{
private readonly DbContext _context;
public DbContextFactory()
{
// the context is new()ed up instead of being injected to avoid direct dependency on EF
// not sure if this is good approach...but it removes direct dependency on EF from web tier
_context = new MyAppDbContext();
}
public DbContext GetDbContext()
{
return _context;
}
// see comment in IDbContextFactory inteface...
//public void Dispose()
//{
// if (_context != null)
// {
// _context.Dispose();
// GC.SuppressFinalize(this);
// }
//}
}
IRepository, репозиторий и 2 конкретных репозитория с дополнительными интерфейсами (Vehicle and Inventory)
public interface IRepository<T> where T : class
{
/// <summary>
/// Get the total objects count.
/// </summary>
int Count { get; }
/// <summary>
/// Gets all objects from database
/// </summary>
IQueryable<T> All();
/// <summary>
/// Gets object by primary key.
/// </summary>
/// <param name="id"> primary key </param>
/// <returns> </returns>
T GetById(object id);
/// <summary>
/// Gets objects via optional filter, sort order, and includes
/// </summary>
/// <param name="filter"> </param>
/// <param name="orderBy"> </param>
/// <param name="includeProperties"> </param>
/// <returns> </returns>
IQueryable<T> Get(Expression<Func<T, bool>> filter = null, Func<IQueryable<T>, IOrderedQueryable<T>> orderBy = null, string includeProperties = "");
/// <summary>
/// Gets objects from database by filter.
/// </summary>
/// <param name="predicate"> Specified a filter </param>
IQueryable<T> Filter(Expression<Func<T, bool>> predicate);
/// <summary>
/// Gets objects from database with filting and paging.
/// </summary>
/// <param name="filter"> Specified a filter </param>
/// <param name="total"> Returns the total records count of the filter. </param>
/// <param name="index"> Specified the page index. </param>
/// <param name="size"> Specified the page size </param>
IQueryable<T> Filter(Expression<Func<T, bool>> filter, out int total, int index = 0, int size = 50);
/// <summary>
/// Gets the object(s) is exists in database by specified filter.
/// </summary>
/// <param name="predicate"> Specified the filter expression </param>
bool Contains(Expression<Func<T, bool>> predicate);
/// <summary>
/// Find object by keys.
/// </summary>
/// <param name="keys"> Specified the search keys. </param>
T Find(params object[] keys);
/// <summary>
/// Find object by specified expression.
/// </summary>
/// <param name="predicate"> </param>
T Find(Expression<Func<T, bool>> predicate);
/// <summary>
/// Create a new object to database.
/// </summary>
/// <param name="entity"> Specified a new object to create. </param>
T Create(T entity);
/// <summary>
/// Deletes the object by primary key
/// </summary>
/// <param name="id"> </param>
void Delete(object id);
/// <summary>
/// Delete the object from database.
/// </summary>
/// <param name="entity"> Specified a existing object to delete. </param>
void Delete(T entity);
/// <summary>
/// Delete objects from database by specified filter expression.
/// </summary>
/// <param name="predicate"> </param>
void Delete(Expression<Func<T, bool>> predicate);
/// <summary>
/// Update object changes and save to database.
/// </summary>
/// <param name="entity"> Specified the object to save. </param>
void Update(T entity);
}
public class Repository<T> : IRepository<T> where T : class
{
protected readonly DbContext _dbContext;
protected readonly DbSet<T> _dbSet;
public Repository(DbContext dbContext)
{
_dbContext = dbContext;
_dbSet = _dbContext.Set<T>();
}
public virtual int Count
{
get { return _dbSet.Count(); }
}
public virtual IQueryable<T> All()
{
return _dbSet.AsQueryable();
}
public virtual T GetById(object id)
{
return _dbSet.Find(id);
}
public virtual IQueryable<T> Get(Expression<Func<T, bool>> filter = null, Func<IQueryable<T>, IOrderedQueryable<T>> orderBy = null, string includeProperties = "")
{
IQueryable<T> query = _dbSet;
if (filter != null)
{
query = query.Where(filter);
}
if (!String.IsNullOrWhiteSpace(includeProperties))
{
foreach (var includeProperty in includeProperties.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProperty);
}
}
if (orderBy != null)
{
return orderBy(query).AsQueryable();
}
else
{
return query.AsQueryable();
}
}
public virtual IQueryable<T> Filter(Expression<Func<T, bool>> predicate)
{
return _dbSet.Where(predicate).AsQueryable();
}
public virtual IQueryable<T> Filter(Expression<Func<T, bool>> filter, out int total, int index = 0, int size = 50)
{
int skipCount = index*size;
var resetSet = filter != null ? _dbSet.Where(filter).AsQueryable() : _dbSet.AsQueryable();
resetSet = skipCount == 0 ? resetSet.Take(size) : resetSet.Skip(skipCount).Take(size);
total = resetSet.Count();
return resetSet.AsQueryable();
}
public bool Contains(Expression<Func<T, bool>> predicate)
{
return _dbSet.Count(predicate) > 0;
}
public virtual T Find(params object[] keys)
{
return _dbSet.Find(keys);
}
public virtual T Find(Expression<Func<T, bool>> predicate)
{
return _dbSet.FirstOrDefault(predicate);
}
public virtual T Create(T entity)
{
var newEntry = _dbSet.Add(entity);
return newEntry;
}
public virtual void Delete(object id)
{
var entityToDelete = _dbSet.Find(id);
Delete(entityToDelete);
}
public virtual void Delete(T entity)
{
if (_dbContext.Entry(entity).State == EntityState.Detached)
{
_dbSet.Attach(entity);
}
_dbSet.Remove(entity);
}
public virtual void Delete(Expression<Func<T, bool>> predicate)
{
var entitiesToDelete = Filter(predicate);
foreach (var entity in entitiesToDelete)
{
if (_dbContext.Entry(entity).State == EntityState.Detached)
{
_dbSet.Attach(entity);
}
_dbSet.Remove(entity);
}
}
public virtual void Update(T entity)
{
var entry = _dbContext.Entry(entity);
_dbSet.Attach(entity);
entry.State = EntityState.Modified;
}
}
public class VehicleRepository : Repository<Vehicle>, IVehicleRepository
{
public VehicleRepository(DbContext dbContext) : base(dbContext)
{
}
}
public interface IVehicleRepository : IRepository<Vehicle>
{
//RFU
}
public interface IInventoryRepository : IRepository<InventoryItem>
{
IList<InventoryItem> GetByVehicleId(string vehicleId); // NOTE: InventoryItem.VehicleId != InventoryItem.Id
}
public class InventoryItemRepository : Repository<InventoryItem>, IInventoryItemRepository
{
public InventoryItemRepository(DbContext dbContext) : base(dbContext)
{
}
public IList<InventoryItem> GetByVehicleId(string vehicleId)
{
return Filter(vii => vii.Vehicle.Id == vehicleId).ToList();
}
}
IUnitOfWork, UnitOfWork
public interface IUnitOfWork : IDisposable
{
InventoryItemRepository InventoryItemRepository { get; }
VehicleRepository VehicleRepository { get; }
void Save();
}
public class UnitOfWork : IUnitOfWork
{
private readonly DbContext _dbContext;
private bool _disposed;
private InventoryItemRepository _inventoryItemRepository;
private VehicleRepository _vehicleRepository;
/// <summary>
/// NOTE: repository getters instantiate repositories as needed (lazily)...
/// i wish I knew of IoC "way" of wiring up repository getters...
/// </summary>
/// <param name="dbContextFactory"></param>
public UnitOfWork(IDbContextFactory dbContextFactory)
{
_dbContext = dbContextFactory.GetDbContext();
}
public void Save()
{
if (_dbContext.GetValidationErrors().Any())
{
// TODO: move validation errors into domain level exception and then throw it instead of EF related one
}
_dbContext.SaveChanges();
}
public InventoryItemRepository InventoryItemRepository
{
get { return _inventoryItemRepository ?? (_inventoryItemRepository = new InventoryItemRepository(_dbContext)); }
}
public VehicleRepository VehicleRepository
{
get { return _vehicleRepository ?? (_vehicleRepository = new VehicleRepository(_dbContext)); }
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_dbContext.Dispose();
}
}
_disposed = true;
}
}
Пример использования в ASP.NET MVC4 + веб-API
Global.asax.cs (Application_Start)
// relevant registration
builder.RegisterType<UnitOfWork>().As<IUnitOfWork>()
.WithParameter("dbContextFactory", new DbContextFactory())
.InstancePerHttpRequest()
.InstancePerApiRequest();
InventoryController
public class InventoryController : ApiController
{
private readonly InventoryItemMapper _mapper; // NOTE: maps viewModel to domain entities and vice versa using ValueInjector
private readonly IUnitOfWork _unitOfWork;
public InventoryController(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork; // UoW (with all repos) is injected and ready for use...
_mapper = new VehicleInventoryItemMapper(); //TODO: this will be injected also...
}
public IEnumerable<InventoryViewModel> Get()
{
var inventoryItems = _unitOfWork.InventoryItemRepository.All().ToList();
var inventory = _mapper.MapToModel(inventoryItems);
return inventory;
}
}
4 ответа
Если вы, вероятно, будете расти до 100+ объектов домена, я исхожу из предположения, что у вас будет много логики.
Я бы не обращался к репозиториям напрямую через контроллеры, я бы добавил уровень сервиса, который извлекается контроллерами через UnitOfWork.
причина:
-
Действия вашего контроллера, вероятно, раздуваются и становятся неуправляемыми. Поскольку у вас будет много кода котла (например, Get, Update, Save).
-
Уровень сервиса заставит вас не нарушать принцип DRY, потому что вы не будете повторять типичный код репозитория.
-
Вы можете использовать шаблон «Фасад» для группировки общих задач, требующих нескольких репозиториев http://en.wikipedia.org/wiki/Facade_pattern
-
Ваша бизнес-логика будет находиться в Уровне обслуживания, позволяя им повторно использовать в другом месте, плюс ваши контроллеры будут слабо связаны с вашей бизнес-логикой.
пример Я построил: https://gist.github.com/3025099
Я согласен с аргументами @Mike о том, чтобы избежать раздувания контроллера через службы.
Обертка DbContext - это нечеткая абстракция. Независимо от того, вы окажетесь в какой-то зависимости от EF в своем сервисе /уровне контроллера.
Кроме того, я бы избегал лишнего слоя UnitOfWork
и Repository
просто потому, что DbContext
обертывает это для вас уже.
В MSDN :
Класс DbContext
Представляет комбинацию шаблонов Unit-Of-Work и Repository и позволяет вам запрашивать базу данных и группировать изменения, которые затем будет записана обратно в хранилище в виде единицы.
Если вы используете любые рамки DI, вы можете легко управлять временем жизни DbContext и сервисами.
Дальнейшее чтение: Архитектура в яме гибели : Зло уровня абстракции хранилища
Он выглядит хорошо, за исключением того, что фильтр для пейджинга будет проходить через ошибку.
Метод «Пропустить» поддерживается только для отсортированного ввода в LINQ to Entities. Метод «OrderBy» должен быть вызван перед методом «Пропустить».
Возможно, это была опечатка, но я, вероятно, предположил бы, что IUnitOfWork
выставляет интерфейсы вместо конкретных реализаций, например. IVehicleRepository
вместо VehicleRepository
.
Мне очень нравится использование IDBContextFactory
в этом отношении, это не полностью перебор, но красиво абстрагирует DBContext
. Однако при реализации этого вы не могли бы затем передать завод в конструкторы Repository<T>
, а не DBContext
)?
Я всегда задавался вопросом, как обеспечить, чтобы контроллеры имели доступ к количеству или репозиториям и т. д. Мне интересно видеть комментарии других людей об экспонировании всех репозиториев в UnitOfWork
.