Дизайн базового класса сущности в C # для модели хранилища - PullRequest
6 голосов
/ 09 апреля 2009

Я пытаюсь стать хорошим гражданином в программировании, узнав больше о Dependency Injection / IoC и других передовых методах. Для этого у меня есть проект, в котором я пытаюсь сделать правильный выбор и спроектировать все «правильным» образом, что бы это ни значило. Ninject, Moq и ASP.NET MVC помогают в тестировании и выводе приложения на улицу.

Однако у меня есть вопрос о том, как разработать базовый класс сущности для объектов, из которых состоит мое приложение. У меня есть простая библиотека классов, на которой построено веб-приложение. Эта библиотека предоставляет интерфейс IRepository, а реализация по умолчанию (та, которую использует приложение) использует Linq-to-SQL под оболочкой (DataContext и т. Д. Не предоставляется веб-приложению) и просто содержит способы извлечения этих объектов. Хранилище в основном выглядит так (упрощенно):

public interface IRepository
{
    IEnumerable<T> FindAll<T>() where T : Entity
    T Get<T>(int id) where T : Entity
}

Реализация по умолчанию использует метод GetTable () в DataContext для предоставления правильных данных.

Однако для этого необходимо, чтобы базовый класс "Entity" имел некоторые функции. Достаточно легко заставить мои объекты наследоваться от него, создав частичный класс с тем же именем, что и сопоставленный объект, который мне дает Linq-to-SQL, но каков «правильный» способ сделать это?

Например, вышеприведенный интерфейс имеет функцию для получения сущности по ее идентификатору - у всех различных классов, производных от сущности, действительно есть поле «id» типа int (сопоставленное с первичным ключом их соответствующего таблицы), но как я могу указать это таким образом, чтобы я мог реализовать IRepository, как это?

public class ConcreteRepository : IRepository
{
    private SomeDataContext db = new SomeDataContext();

    public IEnumerable<T> FindAll<T>() where T : Entity
    {
        return db.GetTable<T>().ToList();
    }

    public T Get(int id) where T : Entity
    {
        return db.GetTable<T>().Where(ent => ent.id == id).FirstOrDefault();
    }
}

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

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

И я могу создать абстрактное поле или обычное поле, которое «скрыто» полем id, которое Linq-to-SQL вставляет в сгенерированные классы.

Но все это похоже на чит и даже выдает предупреждения компилятора.

Должно ли "Entity" действительно быть "IEntity" (вместо интерфейса), и я должен попытаться определить поле id таким образом, чтобы Linq-to-SQL выполнял? Это также упростит указание других интерфейсов, которые необходимо реализовать разработчикам сущностей.

Или «Entity» должен быть абстрактным базовым классом с абстрактным полем id, и должен ли он также реализовывать необходимые интерфейсы абстрактным образом, чтобы другие могли его переопределить?

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

Спасибо!

РЕДАКТИРОВАТЬ 10 ​​апреля:

Я вижу, что упустил что-то важное здесь. В моем IRepository есть две конкретные реализации - одна «настоящая», использующая LINQ to SQL, другая - InMemoryRepository, которая сейчас использует только List, который используется для модульного тестирования и т. Д.

Оба добавленных решения будут работать для одной из этих ситуаций, а не для другой. Поэтому, если возможно, мне нужен способ определить, что все, что наследуется от Entity, будет иметь свойство «id», и что это будет работать с LINQ to SQL DataContext без «мошенничества».

Ответы [ 6 ]

5 голосов
/ 09 апреля 2009

Если вы используете «Id» в качестве первичных ключей во всех таблицах, чтобы все сгенерированные классы имели открытое свойство, например:

public int Id {}

Затем создайте интерфейс

public interface IIdentifiable {
    Id { get; }
}

Тогда самая утомительная часть :(, для всех сущностей создайте частичный класс и заставьте его реализовать IIdentifiable.

Класс хранилища может выглядеть так:

public class Repository<T> : IRepository where T : IIdentifiable {
}

И будет работать следующий код:

db.GetTable<T>().SingleOrDefault(ent => ent.Id.Equals(id));

Если вы не используете сгенерированные классы и создаете свои, еще проще с этой точки зрения.

EDIT:
Вместо ent => ent.Id == id используйте ent => ent.Id.Equals (id). Только что протестировано, следующий полный пример:

public interface IIdentifiable {
    int Id { get; }
}

public class Repository<T> where T : class, IIdentifiable {
    TestDataContext dataContext = new TestDataContext();

    public T GetById(int id) {
        T t = dataContext.GetTable<T>().SingleOrDefault(elem => elem.Id.Equals(id));
        return t;
    }
}

public partial class Item : IIdentifiable {
}

class Program {
    static void Main(string[] args) {
        Repository<Item> itemRepository = new Repository<Item>();

        Item item = itemRepository.GetById(1);

        Console.WriteLine(item.Text);
    }
}
3 голосов
/ 09 апреля 2009

«FindAll» - плохая идея, и она заставляет ее извлекать все данные. Возвращение IQueryable<T> было бы лучше, но все же не идеально, ИМО.

Поиск по идентификатору - см. здесь для ответа LINQ-to-SQL ; здесь используется метамодель LINQ-to-SQL для поиска первичного ключа, так что вам не нужно это делать. Он также работает как для атрибутивных, так и для ресурсных моделей.

Я бы не стал форсировать базовый класс. Это было не очень популярно в Entity Framework, и вряд ли оно станет более популярным ...

1 голос
/ 12 октября 2009

У меня есть похожий проект, в основе которого я лежал Идея магазина Роба Конери , где, как и в приведенных выше предложениях, я использую интерфейс для указания полей int ID:

public interface IModelObject
{
    int ID { get; set; }
}

Но вместо того, чтобы заставлять мои репозитории реализовывать IRepository, я выбрал методы расширения для типа IQueryable<T>. Поэтому я использую фильтры:

public static class ModelObjectFilters
{
    /// <summary>
    /// Filters the query by ID
    /// </summary>
    /// <typeparam name="T">The IModelObject being queried</typeparam>
    /// <param name="qry">The IQueryable being extended</param>
    /// <param name="ID">The ID to filter by</param>
    /// <returns></returns>
    public static IQueryable<T> WithID<T>(this IQueryable<T> qry,
        int ID) where T : IModelObject
    {
        return qry.Where(result => result.ID == ID);
    }
}

.. таким образом, я могу получить тип IQueryable<T>, чтобы вернуться из моего репо и просто использовать .WithID(x), чтобы вытащить один объект.

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

0 голосов
/ 28 января 2010

У вас есть 2 варианта для поля Id:

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

  2. Просто используйте int Id. (Это упрощает работу с вашими сущностями и становится вашим соглашением для ваших сущностей / хранилищ.

Теперь о том, как реализовать вашу сущность. Ваш класс сущности может быть абстрактным. Взгляните на реализацию базового класса Entity в Sharp-Architechture или из CommonLibrary.NET или SubSonic

Как правило, у вас есть поля аудита и, возможно, методы проверки. Я использую CommonLibrary.NET

Сущность: IEntity

  • Id
  • CreateDate
  • UpdateDate
  • CreateUser
  • UpdateUser
  • Validate (...)
  • IsValid
  • Ошибка

Интерфейс IEntity представляет собой комбинацию полей аудита (CreateDate, UpdateUser и т. Д.) И методов проверки для сущности.

0 голосов
/ 16 мая 2009

Вы делаете то же, что и мы. У нас есть универсальный интерфейс Persist и две реализации: одна с LinqToSql, другая для насмешек.

Мы столкнулись с той же проблемой, что и GetById. Наше решение состоит в том, чтобы все объекты наследовали от одного базового класса. Этот базовый класс имеет виртуальный метод

protected object GetId()

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

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

0 голосов
/ 10 апреля 2009

Взгляните на следующие методы, которые были опубликованы Денис Троллер в качестве ответа на мой вопрос :

public virtual T GetById(short id)
            {
                var itemParameter = Expression.Parameter(typeof(T), "item");
                var whereExpression = Expression.Lambda<Func<T, bool>>
                    (
                    Expression.Equal(
                        Expression.Property(
                            itemParameter,
                            GetPrimaryKeyName<T>()
                            ),
                        Expression.Constant(id)
                        ),
                    new[] { itemParameter }
                    );
                var table = DB.GetTable<T>();
                return table.Where(whereExpression).Single();
            }


public string GetPrimaryKeyName<T>()
            {
                var type = Mapping.GetMetaType(typeof(T));

                var PK = (from m in type.DataMembers
                          where m.IsPrimaryKey
                          select m).Single();
                return PK.Name;
            }
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...