LINQ to Entity Framework — базовый класс для работы с БД? (C#)?

Доброго времени суток. Большую часть времени я писал на PHP, но вот возникла необходимость разбираться с ASP.NET MVC



А проблема у меня в следующем.



Работая с EF, мы вполне можем создавать подобные методы в классе:



public class SiteContext : DbContext
    {
        public DbSet<Problem> Problems { get; set; }
    }

    public class BaseDBProxy 
    {
        protected SiteContext db = new SiteContext();
    }

    public class ProblemsProxy : BaseDBProxy
    {
        public Problem GetById(int id)
        {
            return db.Problems.Where(e => e.Id == id).FirstOrDefault<Problem>();
        }
   }


Но как на уровень BaseDBProxy вынести более общие методы? Например, как сделать (в псевдокоде)?



public class BaseDBProxy 
    {
        protected SiteContext db = new SiteContext();

        public АВТОПОДСТАНОВКА_НУЖНОЙ_МОДЕЛИ GetById(int id)
        {
            return db.АВТОПОДСТАНОВКА_НУЖНОЙ_МОДЕЛИ.Where(e => e.Id == id).FirstOrDefault<[АВТОПОДСТАНОВКА_НУЖНОЙ_МОДЕЛИ]>();
        }
    }


Цель — вынести типовые действия на верхний уровень, дабы не копипастить (пусть и через шаблоны и автогенерацию кода) идентичные действия. Пока способа нормально не нашел.
  • Вопрос задан
  • 8642 просмотра
Решения вопроса 1
lam0x86
@lam0x86
Давно не использовал EF, но думаю, должно получиться:
    public class BaseDBProxy 
    {
        protected SiteContext db = new SiteContext();

        public TEntity GetById<TEntity>(int id)
        {
            return db.Set<TEntity>.Where(e => e.Id == id).FirstOrDefault();
        }
    }


Но вообще, лучше этот метод вынести в базовый класс репозитория:

    public abstract class RepositoryBase<TEntity> where TEntity : class
    {
        private Set<TEntity> _set;

        public RepositoryBase(DbContext context)
        {
            _set = context.Set<TEntity>();
        }

        public TEntity GetById(int id)
        {
            return _set.Where(e => e.Id == id).First();
        }

        public TEntity TryGetById(int id)
        {
            return _set.Where(e => e.Id == id).FirstOrDefault();
        }
    }


А в наследниках можно дописывать методы специфичные для конкретных типов сущностей.
Ответ написан
Пригласить эксперта
Ответы на вопрос 3
Paulskit правильно сказал, что автор пытается написать паттерн Repository. А Lam правильно написал как сделать GenericRepository.
Пример обобщённого репозитория, кстати, есть здесь www.asp.net/mvc/tutorials/getting-started-with-ef-...
Я в своём первом проекте его использовал. На самом деле, лично в моём понимании, это не очень хорошо.
Сейчас ваш метод возвращает объект по его ID. Предвижу, что следующим методом будет получение группы объектов и возвращение IQueryable. Такой подход невольно заставляет писать код выборки объектов где-нибудь (в контроллере, в слое сервиса), но не в репозитории, что приводит к путанице. На мой взляд, лучше делать не обобщённый репозиторий, а уникальный репозиторий для каждой сущности, необходимой для работы программы.
Предположим, что у вас есть класс BlogPost. Вы хотите иметь возможность загружать этот объект по ID, загружать вообще все эти объекты в память, загружать только часть этих объектов в память, загружать объекты с определённым условием и т.д. Для этого мы можете создать BlogPostRepository и написать в нём эти методы. Тогда ваша программа станет логичнее и понятнее. При работе с блогами вам не нужно будет каждый объявлять обобщённый репозиторий и городить поверх него запросы типа
var repository = new GenericRepository<BlogPost>();
var blogs = repository.GetAll().Skip(100).Take(10).OrderByDescending(c => c.Created);

Ну или как-то так (код примерный). Причём такие конструкции придётся городить в любом месте, где понадобится получить 10 последних блогов.
Проще сделать не обобщённый репозиторий, а специальный репозиторий для нужного объекта, тогда получится примерно так:
var repository = new BlogPostRepository();
var blogs = repository.GetTenTopBlogs();

В методе GetTenTopBlogs, соответственно, инкапсулируете логику выборки последних 10 блогов. Разумеется GetTenTopBlogs может иметь параметры (например количество блогов, которые нужно получить). Тут уже всё зависит от задач. И GetTopBlogs Должен возвращать IEnumerable, чтобы вызывающий код уже оперировал с сущностями в памяти, а не строил новые запросы к БД.
В целом код становится лаконичнее. Вам не нужно теперь каждый раз, чтобы что-то получить, объявлять обощённый репозиторий, тащить из него Queryable коллекцию и делать на ней выборку. Вы только вызываете метод и уже получаете результат. Теперь в вызывающих классах нет логики выборки.
Примерно так... :)
У Александра Бындю есть хорошие статьи на эту тему.
blog.byndyu.ru/2011/08/repository.html

Кстати, только заметил. Насколько я могу судить, репозиторий не должен знать об объекте DbContext. Это нужно для того, чтобы реализовать паттерн Unit Of Work. Представьте, что вы хотите совершить несколько транзакий (операций с репозиторием). Причём если хотя бы одна из них прошла неудачно, нужно всё откатить обратно. Предположим, что вы хотите сохранить 3 разных сущности в базу. Для этого вы написали свой обобщённый GenericRepository и объявили три переменных для каждой сущности. Причём у вас в этом GenericRepository будет функция Save (иначе как вы будете сохранять свои сущности).
Выходит, что каждый экземпляр класса GenericRepository будет содержать свою копию DbContext, и каждый раз после добавления сущности вы будете сохранять базу.
Единой транзакции не получится.
Чтобы получилась, нужно передавать DbContext в конструктор репозитория и ничего не сохранять в репозиториях. Т.е. вы где-то объявляете свой контекст, затем создаёте три репозитория и в каждый из них передаёте этот контекст. Делаете все операции, после чего сохраняете контекст в вызыывающем класе. Получается UoW. Хотя DbContext - это уже UnitOfWork.
По ссылке выше это тоже обсуждается (смотрите там рекоммендованные ссылки).

p.s. Paulskit дал хорошую ссылку, я как раз её пытался найти.
Lam тоже говорит правильно о том, что общие методы можно, конечно, вынести в обобщённый репозиторий. Только зачастую там этих методов - 1-2, а доброй трети сущностей эти методы вообще не понадобятся, так что я бы не стал делать лишнюю иерархию только для того, чтобы всем репозиториям назначить базовый класс.
Ответ написан
Комментировать
@Paulskit
Не понятно чего Вы добиваетесь. Каким образом программа должна узнать что по ID Вы хотите получить Problem, а не что-то другое?
Мне кажется, Вы пытаетесь реализовать паттерн репозиторий
Ответ написан
Комментировать
Newbilius
@Newbilius Автор вопроса
Я добиваюсь сокращения кода, вынесением общих действий в предка. Во всех встреченных мною примерах создавался репозиторий, но он играл роль интерфейса — все методы во всех потомках приходилось заново реализовывать.

В PHP можно было бы сделать примерно так:

public class BaseDBProxy 
    {
        protected SiteContext db = new SiteContext();
        protected $class_name="Problems";

        public GetById(int id)
        {
            return db.$class_name.Where(e => e.Id == id).FirstOrDefault<$class_name>();
        }
    }


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

Статью посмотрел, есть над чем подумать (не с решением этой задачи, а с постановкой задачи вообще).
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы