Мы все ищем тот же IRepository?

Я пытаюсь создать способ создания общих хранилищ, которые работают с различными хранилищами данных:

public interface IRepository
{
    IQueryable<T> GetAll<T>();
    void Save<T>(T item);
    void Delete<T>(T item);
}
public class MemoryRepository : IRepository {...}
public class SqlRepository : IRepository {...}

Я хотел бы работать против тех же классов домена POCO в каждом. Я также рассматриваю аналогичный подход, где каждый класс домена имеет свой собственный репозиторий:

public interface IRepository<T>
{
    IQueryable<T> GetAll();
    void Save(T item);
    void Delete(T item);
}
public class MemoryCustomerRepository : IRepository {...}
public class SqlCustomerRepository : IRepository {...}

Мои вопросы: 1) Возможно ли, что первый подход возможен? 2) Есть ли какое-либо преимущество для второго подхода.

Ответ 1

  • Первый подход возможен, я сделал что-то подобное в прошлом, когда я написал свою собственную структуру отображения, которая предназначалась для РСУБД и XmlWriter/XmlReader. Вы можете использовать такой подход для упрощения модульного тестирования, хотя я думаю, что теперь у нас есть превосходные инструменты OSS для этого.

  • Второй подход - это то, что я сейчас использую сейчас с IBATIS.NET mappers. У каждого картографа есть интерфейс, и каждый картограф [может] предоставляет ваши основные операции CRUD. Преимущество каждого картографа для класса домена также имеет определенные функции (такие как SelectByLastName или DeleteFromParent), которые выражаются интерфейсом и определяются в конкретном картографе. Из-за этого мне не нужно создавать отдельные репозитории, как вы предлагаете, - наши конкретные картографы нацелены на базу данных. Для выполнения модульных тестов я использую StructureMap и Moq для создания репозиториев в памяти, которые работают как ваш Memory*Repository. Его меньшие классы для реализации и управления и снижения производительности в целом для очень проверяемого подхода. Для данных, общих для модульных тестов, я использую шаблон построителя для каждого класса домена, который имеет методы WithXXX и AsSomeProfile (AsSomeProfile просто возвращает экземпляр компоновщика с предварительно сконфигурированными тестовыми данными).

Вот пример того, что я обычно получаю в своих модульных тестах:

// Moq mocking the concrete PersonMapper through the IPersonMapper interface
var personMock = new Mock<IPersonMapper>(MockBehavior.Strict);
personMock.Expect(pm => pm.Select(It.IsAny<int>())).Returns(
    new PersonBuilder().AsMike().Build()
);

// StructureMap ObjectFactory
ObjectFactory.Inject(personMock.Object);

// now anywhere in my actual code where an IPersonMapper instance is requested from
// ObjectFactory, Moq will satisfy the requirement and return a Person instance
// set with the PersonBuilder Mike profile unit test data

Ответ 2

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

Некоторые репозитории доступны только для чтения, некоторые из них только вставляются (без обновления, без удаления), некоторые из них имеют только определенные поисковые запросы...

Используя GetAll return IQueryable, ваша логика запроса просочится в ваш код, возможно, на прикладной уровень.

Но все же интересно использовать тот интерфейс, который вы предоставляете для инкапсуляции объектов Linq Table<T>, чтобы вы могли заменить его реализацией памяти в целях тестирования.

Поэтому я предлагаю называть его ITable<T>, дать ему тот же интерфейс, что и объект linq Table<T>, и использовать его внутри для ваших репозиториев домена (а не вместо).

Затем вы можете использовать определенные репозитории в памяти с помощью реализации ITable<T> в памяти.

Самый простой способ реализовать ITable<T> в памяти - использовать List<T> и получить интерфейс IQueryable<T>, используя метод расширения .AsQueryable().

public class InMemoryTable<T> : ITable<T>
{
    private List<T> list;
    private IQueryable<T> queryable;

   public InMemoryTable<T>(List<T> list)
   { 
      this.list = list;
      this.queryable = list.AsQueryable();
   }

   public void Add(T entity) { list.Add(entity); }
   public void Remove(T entity) { list.Remove(entity); }

   public IEnumerator<T> GetEnumerator() { return list.GetEnumerator(); }

   public Type ElementType { get { return queryable.ElementType; } }
   public IQueryProvider Provider {     get { return queryable.Provider; } }
   ...
}

Вы можете работать изолированно от базы данных для тестирования, но с истинными конкретными репозиториями, которые дают больше информации о домене.

Ответ 3

Это немного поздно... но посмотрите на реализацию IRepository на CommonLibrary.NET на codeplex. Он получил довольно хороший набор функций.

Что касается вашей проблемы, я вижу много людей, использующих такие методы, как GetAllProducts(), GetAllEmployees() в их реализации репозитория. Это избыточно и не позволяет вашему репозиторию быть общим. Все, что вам нужно - это GetAll() или All(). Решение, приведенное выше, действительно решает проблему именования.

Это взято из документации CommonLibrary.NET онлайн:

0.9.4 Beta 2 имеет мощную реализацию репозитория.

* Supports all CRUD methods ( Create, Retrieve, Update, Delete )
* Supports aggregate methods Min, Max, Sum, Avg, Count
* Supports Find methods using ICriteria<T>
* Supports Distinct, and GroupBy
* Supports interface IRepository<T> so you can use an In-Memory table for unit-testing
* Supports versioning of your entities
* Supports paging, eg. Get(page, pageSize)
* Supports audit fields ( CreateUser, CreatedDate, UpdateDate etc )
* Supports the use of Mapper<T> so you can map any table record to some entity
* Supports creating entities only if it isn't there already, by checking for field values.