Pull to refresh

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

Reading time 9 min
Views 6.7K
Уважаемые программисты!
Не было печалиПонадобилось мне плотно поработать с 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.

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

Articles