Подтвердить что ты не робот

Entity Framework 6 Code First - Реализация репозитория хорошая?

Я собираюсь реализовать проект Entity Framework 6 с репозиторием и единицей работы.

Есть так много статей, и я не уверен, что лучший совет: например, я действительно так же, как и шаблон, реализованный здесь: по причинам, предложенным в статье здесь

Однако Tom Dykstra (Senior Programming Writer on Microsoft Web Platform & Tools Content Team) предполагает, что это должно быть сделано в другой статье: здесь

Я подписываюсь на Pluralsight, и он реализуется несколько иначе, почти каждый раз, когда он используется в курсе, поэтому выбор дизайна затруднен.

Некоторые люди, похоже, предполагают, что часть работы уже реализована DbContext, как в этой статье, поэтому нам не нужно ее вообще реализовывать.

Я понимаю, что этот вопрос задавался раньше, и это может быть субъективным, но мой вопрос прямо:

Мне нравится подход в первой статье (Code Fizzle) и хотел бы знать, может ли он быть более удобным и легко проверяемым, как другие подходы и безопасно продолжать?

Любые другие взгляды более чем приветствуются.

4b9b3361

Ответ 1

@Крис Харди прав, EF реализует UoW из коробки. Однако многие люди упускают из виду тот факт, что EF также реализует общий шаблон репозитория из коробки:

var repos1 = _dbContext.Set<Widget1>();
var repos2 = _dbContext.Set<Widget2>();
var reposN = _dbContext.Set<WidgetN>();

... и это довольно хорошая универсальная реализация репозитория, встроенная в сам инструмент.

Почему возникает проблема создания тонны других интерфейсов и свойств, когда DbContext дает вам все, что вам нужно? Если вы хотите отвлечь DbContext за интерфейсами уровня приложения и хотите применить сегрегацию запроса команд, вы можете сделать что-то простое:

public interface IReadEntities
{
    IQueryable<TEntity> Query<TEntity>();
}

public interface IWriteEntities : IReadEntities, IUnitOfWork
{
    IQueryable<TEntity> Load<TEntity>();
    void Create<TEntity>(TEntity entity);
    void Update<TEntity>(TEntity entity);
    void Delete<TEntity>(TEntity entity);
}

public interface IUnitOfWork
{
    int SaveChanges();
}

Вы можете использовать эти 3 интерфейса для всего доступа к вашей сущности и не беспокоиться о том, чтобы вводить 3 или более разных репозитория в бизнес-код, который работает с 3 или более наборами сущностей. Разумеется, вы все равно будете использовать IoC, чтобы убедиться, что на веб-запрос имеется только один экземпляр DbContext, но все три интерфейса реализованы одним и тем же классом, что упрощает его.

public class MyDbContext : DbContext, IWriteEntities
{
    public IQueryable<TEntity> Query<TEntity>()
    {
        return Set<TEntity>().AsNoTracking(); // detach results from context
    }

    public IQueryable<TEntity> Load<TEntity>()
    {
        return Set<TEntity>();
    }

    public void Create<TEntity>(TEntity entity)
    {
        if (Entry(entity).State == EntityState.Detached)
            Set<TEntity>().Add(entity);
    }

    ...etc
}

Теперь вам нужно только ввести один отдельный интерфейс в свою зависимость, независимо от того, сколько различных объектов ему нужно для работы:

// NOTE: In reality I would never inject IWriteEntities into an MVC Controller.
// Instead I would inject my CQRS business layer, which consumes IWriteEntities.
// See @MikeSW answer for more info as to why you shouldn't consume a
// generic repository like this directly by your web application layer.
// See http://www.cuttingedge.it/blogs/steven/pivot/entry.php?id=91 and
// http://www.cuttingedge.it/blogs/steven/pivot/entry.php?id=92 for more info
// on what a CQRS business layer that consumes IWriteEntities / IReadEntities
// (and is consumed by an MVC Controller) might look like.
public class RecipeController : Controller
{
    private readonly IWriteEntities _entities;

    //Using Dependency Injection 
    public RecipeController(IWriteEntities entities)
    {
        _entities = entities;
    }

    [HttpPost]
    public ActionResult Create(CreateEditRecipeViewModel model)
    {
        Mapper.CreateMap<CreateEditRecipeViewModel, Recipe>()
            .ForMember(r => r.IngredientAmounts, opt => opt.Ignore());

        Recipe recipe = Mapper.Map<CreateEditRecipeViewModel, Recipe>(model);
        _entities.Create(recipe);
        foreach(Tag t in model.Tags) {
            _entities.Create(tag);
        }
        _entities.SaveChanges();
        return RedirectToAction("CreateRecipeSuccess");
    }
}

Одна из моих любимых вещей в этом дизайне заключается в том, что он минимизирует зависимости хранилища объектов от потребителя. В этом примере RecipeController является потребителем, но в реальном приложении потребитель будет обработчиком команд. (Для обработчика запросов вы обычно потребляете IReadEntities только потому, что хотите просто вернуть данные, а не мутировать какое-либо состояние.) Но для этого примера позвольте просто использовать RecipeController в качестве потребителя для изучения зависимостей зависимостей:

Скажем, у вас есть набор модульных тестов, написанных для вышеупомянутого действия. В каждом из этих модульных тестов вы обновляете контроллер, передавая макет в конструктор. Затем скажите, что ваш клиент решает, что они хотят, чтобы люди могли создавать новую Поваренную книгу или добавлять к существующему при создании нового рецепта.

С шаблоном интерфейса репозитория для каждого объекта или репозитория за совокупность вам нужно будет добавить новый экземпляр репозитория IRepository<Cookbook> в свой конструктор контроллера (или используя ответ @Chris Hardie, напишите код, чтобы прикрепить еще один репозиторий к экземпляру UoW). Это немедленно приведет к разрыву всех ваших других модульных тестов, и вам нужно будет вернуться к изменению кода построения во всех них, передав еще один макетный экземпляр и расширив ваш массив зависимостей. Однако с вышесказанным все ваши другие модульные тесты будут по-прежнему компилироваться. Все, что вам нужно сделать, это написать дополнительный тест для покрытия новых функций поваренной книги.

Ответ 2

Мне (не) жаль говорить, что Codefizzle, статья Dyksta и предыдущие ответы неверны. Для простого факта, что они используют сущности EF в качестве предметных (бизнес) объектов, что является большим WTF.

Обновление: для менее технического объяснения (в простых словах) прочитайте Шаблон репозитория для чайников

Короче говоря, ЛЮБОЙ интерфейс репозитория не должен быть связан с ЛЮБОЙ деталью персистентности (ORM). Интерфейс репо работает ТОЛЬКО с объектами, которые имеют смысл для остальной части приложения (домен, может быть, пользовательский интерфейс, как в презентации). Множество людей (с MS, возглавляющими пакет, с намерением, которое я подозреваю) делают ошибку, полагая, что они могут повторно использовать свои объекты EF или что они могут быть бизнес-объектом поверх них.

Хотя это может случиться, это довольно редко. На практике у вас будет много доменных объектов, "спроектированных" по правилам базы данных, т.е. плохое моделирование. Цель репозитория состоит в том, чтобы отделить остальную часть приложения (главным образом бизнес-уровень) от его формы постоянства.

Как вы развязываете его, когда ваше репо имеет дело с сущностями EF (подробности персистентности) или его методы возвращают IQueryable, утечку абстракции с неправильной семантикой для этой цели (IQueryable позволяет вам построить запрос, таким образом, подразумевая, что вам необходимо знать детали персистентности, таким образом, отрицание цели и функциональности хранилища)?

Доминирующий объект никогда не должен знать о постоянстве, EF, объединениях и т.д. Он не должен знать, какой механизм БД вы используете или используете ли вы. То же самое с остальной частью приложения, если вы хотите, чтобы оно было отделено от деталей персистентности.

Интерфейс хранилища знает только то, что знает верхний уровень. Это означает, что универсальный интерфейс хранилища доменов выглядит следующим образом

public interface IStore<TDomainObject> //where TDomainObject != Ef (ORM) entity
{
   void Save(TDomainObject entity);
   TDomainObject Get(Guid id);
   void Delete(Guid id);
 }

Реализация будет находиться в DAL и будет использовать EF для работы с БД. Однако реализация выглядит так

public class UsersRepository:IStore<User>
 {
   public UsersRepository(DbContext db) {}


    public void Save(User entity)
    {
       //map entity to one or more ORM entities
       //use EF to save it
    }
           //.. other methods implementation ...

 }

У вас нет конкретного общего хранилища. Единственное использование конкретного универсального репозитория - это когда ЛЮБОЙ объект домена хранится в сериализованной форме в виде значения ключа, такого как таблица. Это не относится к ORM.

Как насчет запросов?

 public interface IQueryUsers
 {
       PagedResult<UserData> GetAll(int skip, int take);
       //or
       PagedResult<UserData> Get(CriteriaObject criteria,int skip, int take); 
 }

UserData - это модель чтения/просмотра, подходящая для использования в контексте запроса.

Вы можете напрямую использовать EF для запросов в обработчике запросов, если вы не возражаете, что ваш DAL знает о моделях представлений, и в этом случае вам не понадобится репо.

Заключение

  • Ваш бизнес-объект не должен знать о сущностях EF.
  • В репозитории будет использоваться ORM, но он никогда не предоставляет ORM остальной части приложения, поэтому интерфейс репо будет использовать только объекты домена или модели представления (или любой другой объект контекста приложения, который не является подробностью сохранения)
  • Вы не говорите репо, как выполнять свою работу, т.е. НИКОГДА не используйте IQueryable с интерфейсом репо.
  • Если вы просто хотите использовать базу данных более простым и крутым способом и имеете дело с простым приложением CRUD, в котором вам не нужно (будьте уверены в этом) поддерживать разделение задач, пропустите все хранилище вместе, используйте напрямую EF для всех данных. Приложение будет тесно связано с EF, но, по крайней мере, вы порежете посредника, и это будет сделано не случайно.

Обратите внимание, что неправильное использование репозитория сделает его использование недействительным, и ваше приложение будет по-прежнему тесно связано с постоянством (ORM).

Если вы считаете, что ORM существует для магического хранения ваших доменных объектов, это не так. Целью ORM является моделирование ООП-хранилища поверх реляционных таблиц. Он связан с постоянством и не имеет отношения к домену, поэтому не используйте ORM вне постоянства.

Ответ 3

DbContext действительно построен с шаблоном Unit of Work. Он позволяет всем своим сущностям совместно использовать тот же контекст, с которым мы работаем с ними. Эта реализация является внутренней для DbContext.

Однако следует отметить, что если вы создаете экземпляр двух объектов DbContext, ни один из них не увидит других объектов, которые они отслеживают. Они изолированы друг от друга, что может быть проблематичным.

Когда я создаю приложение MVC, я хочу убедиться, что во время запроса весь код доступа к данным работает от одного DbContext. Для этого я применяю Единицу работы как шаблон, внешний по отношению к DbContext.

Вот мой объект Unit of Work из приложения рецепта для барбекю, которое я создаю:

public class UnitOfWork : IUnitOfWork
{
    private BarbecurianContext _context = new BarbecurianContext();
    private IRepository<Recipe> _recipeRepository;
    private IRepository<Category> _categoryRepository;
    private IRepository<Tag> _tagRepository;

    public IRepository<Recipe> RecipeRepository
    {
        get
        {
            if (_recipeRepository == null)
            {
                _recipeRepository = new RecipeRepository(_context);
            }
            return _recipeRepository;
        }
    }

    public void Save()
    {
        _context.SaveChanges();
    }
    **SNIP**

Я прикрепляю все мои репозитории, которые все вводятся с тем же DbContext, к моему объекту Unit of Work. Пока требуются любые репозитории из объекта Unit of Work, мы можем быть уверены, что весь наш код доступа к данным будет управляться с помощью того же DbContext - удивительного соуса!

Если бы я использовал это в приложении MVC, я бы удостоверился, что Единица работы используется во всем запросе, создав ее в контроллере и используя ее во всех своих действиях:

public class RecipeController : Controller
{
    private IUnitOfWork _unitOfWork;
    private IRepository<Recipe> _recipeService;
    private IRepository<Category> _categoryService;
    private IRepository<Tag> _tagService;

    //Using Dependency Injection 
    public RecipeController(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
        _categoryRepository = _unitOfWork.CategoryRepository;
        _recipeRepository = _unitOfWork.RecipeRepository;
        _tagRepository = _unitOfWork.TagRepository;
    }

Теперь в нашем действии мы можем быть уверены, что весь наш код доступа к данным будет использовать тот же DbContext:

    [HttpPost]
    public ActionResult Create(CreateEditRecipeViewModel model)
    {
        Mapper.CreateMap<CreateEditRecipeViewModel, Recipe>().ForMember(r => r.IngredientAmounts, opt => opt.Ignore());

        Recipe recipe = Mapper.Map<CreateEditRecipeViewModel, Recipe>(model);
        _recipeRepository.Create(recipe);
        foreach(Tag t in model.Tags){
             _tagRepository.Create(tag); //I'm using the same DbContext as the recipe repo!
        }
        _unitOfWork.Save();

Ответ 5

Репозиторий с реализацией единичного шаблона работы является плохим ответом на ваш вопрос.

DbContext структуры сущностей реализуется Microsoft в соответствии с шаблоном работы. Это означает, что context.SaveChanges транзакционно сохраняет ваши изменения за один раз.

DbSet также является реализацией шаблона репозитория. Не создавайте репозитории, которые вы можете просто сделать:

void Add(Customer c)
{
   _context.Customers.Add(c);
}

Создайте однострочный метод для того, что вы можете сделать внутри службы в любом случае

Нет никакой выгоды, и никто не меняет EF ORM на другой ORM в наши дни...

Вам не нужна эта свобода...

Крис Харди утверждает, что могут быть созданы несколько объектов контекста, но уже делает это, вы делаете это неправильно...

Просто используйте инструмент IOC, который вам нравится, и настройте MyContext для запроса на Http, и все в порядке.

Возьмите ninject, например:

kernel.Bind<ITeststepService>().To<TeststepService>().InRequestScope().WithConstructorArgument("context", c => new ITMSContext());

Служба, выполняющая бизнес-логику, получает контекст.

Просто держи это просто глупо: -)

Ответ 6

Вы должны рассмотреть "объекты команды/запроса" в качестве альтернативы, вы можете найти кучу интересных статей в этой области, но вот хорошая:

https://rob.conery.io/2014/03/03/repositories-and-unitofwork-are-not-a-good-idea/

Когда вам требуется транзакция для нескольких объектов БД, используйте один объект команды на команду, чтобы избежать сложности шаблона UOW.

Объект запроса на запрос, вероятно, не нужен для большинства проектов. Вместо этого вы можете начать с объекта 'FooQueries' ... я имею в виду, что вы можете начать с шаблона Repository для READS, но назовите его "Queries", чтобы явно указать, что он не выполняет и не должен выполнять никаких вставок/обновлений. ,

Позже вы можете найти целесообразным разделение отдельных объектов запроса, если вы хотите добавить такие вещи, как авторизация и ведение журнала, вы можете передать объект запроса в конвейер.

Ответ 7

Я всегда использую UoW с EF-кодом. Я считаю, что он более эффективен и проще управлять вашими контекстами, чтобы предотвратить утечку памяти и тому подобное. Вы можете найти пример моего обходного пути для моего github: http://www.github.com/stefchri в проекте RADAR.

Если у вас есть какие-либо вопросы по этому поводу, не стесняйтесь спрашивать их.