Универсальный репозиторий ADO.NET Entity

Уважаемые программисты!
Не было печалиПонадобилось мне плотно поработать с ASP.NET MVC2 + Entity Framework, однако базовая функциональность работы с БД меня совсем не впечатлила, потому что нужно было каждый раз выбирать нужную коллекцию объектов из списка. Как избежать написания нескольких классов и использовать всего один — пойдет дальше речь.

Требования

Для начала определимся, что же собственно хотелось бы получиться на выходе.

Хочется удобно работать с объектами:
  • Добавлять.
  • Удалять.
  • Редактировать.
  • Получать по ID.
  • Получать список всех объектов.


используя конструкции вида:
    Unit a = new Unit();
    BaseRepository<Unit> unitRepository = new BaseRepository<Unit>();
    ...........
    unitRepository.AddItem(a);
    unitRepository.ChangeItem(a);
    unitRepository.DeleteItem(a.ID);


* This source code was highlighted with Source Code Highlighter.


Причем, чтобы не требовалось явно указывать с каким именно набором (ObjectSet<...>) следует работать ObjectContext'у.

Немного теории о рефлексии


Так как весь мой код будет основывать на этом принципе, следует понять, что же это такое.

Рефлексия — это процесс, во время которого программа может отслеживать и модифицировать собственную структуру и поведение во время выполнения.

Вкратце — базовый класс Object имеет метод GetType(), который возвращает структуру класса (все его свойства, поля, методы). Следовательно, мы можем использовать его для вызова функция и задания значений свойствам во время выполнения, даже не зная на этапе написания кода о наличии этих полей и функций в классе.

Метод GetType() возвращает объект типа Type, который собственно нам и нужен. Пройдя по ссылке, Вы можете воочию убедиться о количестве и разнообразии методов и свойств этого объекта. Пока что нам понадобиться GetMethod() и GetProperty().

Собственно реализация

Для начала опишу принцип функционирования.

В двух словах:
  1. Получить тип переданного объекта.
  2. Получить из типа имя.
  3. Выбрать нужный набор сущностей из ObjectContext.
  4. «Дернуть» нужный метод (Add, Delete).


  /// <summary>
  /// Base repository for all sets.
  /// </summary>
  /// <typeparam name="T">Class from Business Model, should be EntityType</typeparam>
  public class BaseRepository<T> where T : EntityObject


* This source code was highlighted with Source Code Highlighter.


При передаче объекта в угловых скобках мы уже сможем получить из него все нужные данные (в частности название, что немаловажно). Так как по умолчанию объекты в ObjectContext носят название ******Set, где ****** — имя сущности (entity) в визуальном редакторе. Воспользовавшись рефлексией, мы получаем возможность достучаться к любому набору объектов, не указывая это название в коде. Таким образом и производятся все манипуляции.

Например, во всех ObjectSet'ах содержится метод DeleteObject.

Далее, нам понадобиться сам ObjectContext (куда, собственно, мы и будет писать и получать данные):

//DataBase container
    DBContainer db = new DBContainer();


* This source code was highlighted with Source Code Highlighter.


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

Ещё несколько строк кода просто улучшают читабельность:
/// <summary>
    /// Reflection - get name of T.
    /// </summary>
    private string name
    {
      get { return typeof(T).Name; }
    }


* This source code was highlighted with Source Code Highlighter.


Это получение имени типа Т (выше) и небольшой «макрос» получение типа ObjectContext :
//Simple macros
    Type dbT = typeof(DBContainer);


* This source code was highlighted with Source Code Highlighter.


В целом, вступление закончено и можно переходить ко всему коду. Далее я расскажу о не совсем очевидных кусках кода.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Data.Objects;
using System.Data.Objects.DataClasses;

namespace Chib.Lib
{
  /// <summary>
  /// Base repository for all sets.
  /// </summary>
  /// <typeparam name="T">Class from Business Model, should be EntityType</typeparam>
  public class BaseRepository<T> where T : EntityObject
  {
    /// <summary>
    /// Reflection - get name of T.
    /// </summary>
    private string name
    {
      get { return typeof(T).Name; }
    }

    //DataBase container
    DBContainer db = new DBContainer();
    //Simple macros
    Type dbT = typeof(DBContainer);

    /// <summary>
    /// Get all items in Set as IQueryable, making easy to operate with data.
    /// </summary>
    /// <returns>Items in Set</returns>
    public IQueryable<T> AllItems
    {
      get
      {
        return (IQueryable<T>)AllItemsAsObj;
      }
    }

    /// <summary>
    /// Get the items (ObjectSet) as Object. For internal use only.
    /// </summary>
    /// <returns>Object</returns>
    private object AllItemsAsObj
    {
      get
      {
        PropertyInfo mi = dbT.GetProperty(name + "Set");
        object obj = mi.GetValue(db, null);
        return obj;
      }
    }

    /// <summary>
    /// Add item to collection and save changes.
    /// </summary>
    /// <typeparam name="T">The type of item</typeparam>
    /// <param name="item">Added item</param>
    /// <returns>True if no errors.</returns>
    public bool AddItem(T item)
    {
      try
      {
        object obj = AllItemsAsObj;
        obj.GetType().GetMethod("AddObject").Invoke(obj, new object[] { item });
        db.SaveChanges();
        return true;
      }
      catch
      { return false; }
    }

    /// <summary>
    /// Get the single T item by it's ID
    /// </summary>
    /// <param name="id">Guid ID</param>
    /// <returns>Null if nothing found.</returns>
    public T GetItem(Guid id)
    {
      foreach (var item in AllItems)
      {
        if (new Guid(item.GetType().GetProperty("ID").GetValue(item, null).ToString()) == id)
          return (T)item;
      }
      return null;
    }

    /// <summary>
    /// Delets an item by it's ID.
    /// </summary>
    /// <param name="id">ID of item</param>
    /// <returns>True if no errors.</returns>
    public bool DeleteItem(Guid id)
    {
      try
      {
        T item = GetItem(id);
        object set = AllItemsAsObj;
        set.GetType().GetMethod("DeleteObject").Invoke(set, new object[] { item });
        db.SaveChanges();
        return true;
      }
      catch
      {
        return false;
      }
    }

    public bool ChangeItem(T item)
    {
      try
      {
        var guid = new Guid(item.GetType().GetProperty("ID").GetValue(item, null).ToString());
        T modyfying = AllItems.Single(x => x.GetType().GetProperty("ID").GetValue(null, null).ToString() == guid.ToString());
        modyfying = item;
        db.SaveChanges();
        return true;
      }
      catch
      {
        return false;
      }
    }

    /// <summary>
    /// Force save changes to DB.
    /// </summary>
    /// <returns>True if no errors.</returns>
    public bool SaveChanges()
    {
      try
      {
        db.SaveChanges();
        return true;
      }
      catch
      {
        return false;
      }
    }
  }
}


* This source code was highlighted with Source Code Highlighter.


Подводные камни

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

Функция private object AllItemsAsObj() используется для получения самого набора объектов (ObjectSet) и доступна только внутри класса. Нужна она практически во всех случаях, так как это наш базовый объект, надо которым будут проводиться манипуляции.

Функция AddItem(T item) была написана первой.
С ходу не хотел метода Invoke метода GetMethod. Оказалось, что нужно обязательно передать в качестве параметра объект, над которым мы выполняем действия. Поставив null (как я видел в примерах на неизвестных сайтах) метод не вызывался. Обратите на это внимание!

    /// <summary>
    /// Get the single T item by it's ID
    /// </summary>
    /// <param name="id">Guid ID</param>
    /// <returns>Null if nothing found.</returns>
    public T GetItem(Guid id)
    {
      foreach (var item in AllItems)
      {
        if (new Guid(item.GetType().GetProperty("ID").GetValue(item, null).ToString()) == id)
          return (T)item;
      }
      return null;
    }


* This source code was highlighted with Source Code Highlighter.


Эта часть кода осуществляет выборку одного объекта из набора по его ID. У меня в проекте у каждого объекта есть поле ID типа Guid. Если у вас оно другое — просто замените все упоминания «ID» на свое название.
В данной функции происходит перебор всех элементов коллекции, и при совпадении значения поля ID с входным параметром происходит возврат нужного объекта.

В конце каждой функции стоит строка:
db.SaveChanges();

* This source code was highlighted with Source Code Highlighter.

Она сохраняет все изменения, внесенные в базу данных.

Пример использования

Предположим, у Вас есть два класса: Unit и City.
Для объявления двух репозиториев работы с БД следует поступить так:
BaseRepository<Unit> unitRepository = new BaseRepository<Unit>();
BaseRepository<City> cityRepository = new BaseRepository<City>();


* This source code was highlighted with Source Code Highlighter.

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

Комментарии 17

    0
    Эммм… Транзакции?

    (В сторону: ну и гадость же этот ваш EF)
      0
      не совсем транзакции. но согласен, слегка похоже!
        0
        Вы вопрос не поняли.
          0
          Как уже отметили, вы не поняли вопроса. Где и как у вас организуются транзакции?
            0
            И unitRepository и cityRepository имеют внутри себя каждый свой экземпляр DBContainer. При вызове BaseRepository.SaveChanges() каждого из них, по порядку, будет: открыто свое соединение -> начата транзакция -> выполнены CRUD команды SQL -> комит/ролбек -> закрытие соединения.
            В итоге: получаем отдельные «транзакции» отдельно для каждого объекта типа BaseRepository, или так: в результате можем иметь сохраненные изменения unitRepository и «обломавшийся» cityRepository.SaveChanges().
            Не уверен, поможет ли выполнение обоих SaveChanges() внутри using (TransactionScope ts = new TransactionScope()) {...}.
            Или я неправильно понял вопрос.
              0
              Да, правильно поняли. Что неправильно — так это подобное поведение. А использование TransactionScope вполне себе может эскалировать транзакцию до распределенной.

                0
                How to run two Entity Framework Contexts inside TransactionScope without MSDTC?
                Что неправильно — так это подобное поведение. — Мне кажется что все дискусии решаютя бизнес-задачами. Если именно_такое нужно для конткретного_проекта и требований — велком! Ну и да, нужно понимать какие ограничения возможны в применении данного подхода.
                  0
                  А чем именно неправильно подобное поведение? Эскалацию до распределенной можно побороть через ручное управление Enlistment-ом. Будет гарантированно одно соединение в пределах TransactionScope (и одна SqlTransaction), расшаренное для всех контекстов.
                    0
                    Потому что вообще говоря транзакции — дело клиентского по отношению к DAL/репозиторию/следующему-баззвордному-паттерну кода.
                      0
                      Эммм… Транзакции?
                      вообще говоря транзакции — дело клиентского по отношению к DAL/репозиторию/следующему-баззвордному-паттерну кода.

                      Я тоже не понял вопроса. И вашего же ответа.

                      DAL должен поддерживать транзакции, а не организовывать их. Репозиторий [в дописанном до совместимости с TransactionScope варианте, гарантированно не делающий эскалацию внутри] будет поддерживать и Explicit и Implicit транзакции. Если автор решил не реализовать поддержку транзакций в рамках статью — это его выбор. Это же статья, а не дамп кода живого проекта с комментариями.
          +2
          Производительность на нескольких процессорах с ГГц-ами — конечно не главное, но… Рефлексия на каждый чих достаточно жестокая вещь.

          А потом я не понял особой разницы между new BaseRepository().AllItems и new DBContainer().UnitSet. Конструкции по моему равнозначные (без полиморфизма же). По длине и по сложности очень похожие.
            0
            Да, производительность возможно несколько пострадает. Но по моему скромному мнение удобство повышается…
            Разницы особой и нету. Только BaseRepository не требует явного задания названия контейнера, вот и всё.
              0
              Производительность — однозначна спорная вещь, я на ней не настаиваю. К тому же при выполнении sql-запросов. Просто замечание. Факт просадки может определить только профайлер.

              А вот с интерфейсами — выигрыш по моему спорен. У вас упоминание о типе объектов просто переехало в другое место.
                +1
                Рефлексию можно заменить на компилируемые лямбды. Работать будет… быстрее. И выглядеть — намного круче.
              +1
              1. Посмотрите еще на один очень интересный проект расширения возможностей Entity Framework 1.0/4.0:
              ADO.NET Entity Framework Extensions. Лично я уже даже не представляю себе проект на EF без использования функционала этих «расширений».
              2. А для получения сущности по ключу я использую метод-расширение в классе DBContainer:

              public T GetObjectByKey(object id) where T : EntityObject
              {
              string entitySetName = this.MetadataWorkspace.GetEntityContainer(this.DefaultContainerName, DataSpace.CSpace)
              .BaseEntitySets.Where(bes => bes.ElementType.Name == typeof(T).Name)
              .FirstOrDefault().Name;
              string entitySetKeyName = string.Format("{0}.{1}", this.DefaultContainerName, entitySetName);
              EntityKey key = new EntityKey(entitySetKeyName, "ID", id);
              return (T)this.GetObjectByKey(key);
              }

              3. Получается, что у Вас unitRepository и cityRepository каждый имеет СВОИ/РАЗЛИЧНЫЕ экземпляры DBContainer(). Это, конечно, вариант и все зависит от требований конкретной задачи, но я бы еще добавил бы в конструктор BaseRepository параметра типа DBContainer: вдруг понадобится работать с уже ранее созданным экземпляром DBContainer. Имхо.
                +2
                Хорошая попытка, но попробуите почитать на эту тему побольше. Если нет проблем с англииским, то можете начать с этои статьи: huyrua.wordpress.com/2010/07/13/entity-framework-4-poco-repository-and-specification-pattern/ где подробно изложена схема создания репозитория. Там используется замечательныи метод ObjectContext.CreateObjectSet<TEntity>() вместо рефлексии.
                  0
                  С английским проблем никаких нет:) Спасибо! Очень занимательная статья! Я правда сейчас работаю с Code-First, хочу посмотреть на что он способен.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое