Есть ли способ использовать `dynamic` в лямбда-выражении?

Во-первых, spec. Мы используем MVC5,.NET 4.5.1 и Entity framework 6.1.

В нашем бизнес-приложении MVC5 у нас много повторяющегося кода CRUD. Моя задача - "автоматизировать" большую часть, что означает извлечение ее на базовые классы и ее повторное использование. Прямо сейчас у меня есть базовые классы для контроллеров, моделей представлений и моделей сущностей EF6.

Мой абстрактный базовый класс, который наследует все объекты EF6:

public abstract class BaseEntity<TSubclass>
    where TSubclass : BaseEntity<TSubclass>
{
    public abstract Expression<Func<TSubclass, object>> UpdateCriterion();
}

UpdateCriterion метод используется в AddOrUpdate методе контекста базы данных. У меня есть общий параметр для подклассов, потому что UpdateCriterion должен возвращать лямбда-выражение, которое использует точный тип подкласса, а не интерфейс или базовый класс. Очень упрощенный подкласс, реализующий этот абстрактный базовый класс, будет выглядеть так:

public class Worker : BaseEntity<Worker>
{
    public int ID { get; set; }
    public int Name { get; set; }

    public override Expression<Func<Worker, object>> UpdateCriterion()
    {
        return worker => worker.ID;
    }
}

После этого, в SaveOrUpdate действии моего базового контроллера, у меня был бы такой код:

public ActionResult Save(TViewModel viewModel)
{
    if (ModelState.IsValid)
    {
        var entityModel = viewModel.ConstructEntityModel();
        db.Set<TEntityModel>().AddOrUpdate<TEntityModel>(entityModel.UpdateCriterion(), entityModel);
        db.SaveChanges();
    }
}

Благодаря этому подклассам базового контроллера не нужно внедрять метод Save, как это было раньше. Теперь все это работает, и на самом деле он работает очень хорошо, несмотря на фанковый синтаксис (я имею в виду class BaseEntity<TSubclass> where TSubclass : BaseEntity<TSubclass>, серьезно?).

Вот моя проблема. Для большинства объектов поле ID является ключом, но для некоторых это не так, поэтому я не могу правильно обобщить реализацию суперкласса. Итак, на данный момент каждый подкласс объекта реализует его собственный UpdateCriterion. Но, поскольку для большинства (90% +) объектов e => e.ID является правильной реализацией, у меня много дублирования. Поэтому я хочу переписать базовый класс сущности на что-то вроде этого:

public abstract class BaseEntity<TSubclass> 
    where TSubclass : BaseEntity<TSubclass>
{
    public virtual Expression<Func<TSubclass, object>> UpdateCriterion()
    {
        return entity => ((dynamic)entity).ID;
    }
}

Цель состоит в том, чтобы обеспечить реализацию по умолчанию, использующую ID как ключ, и позволить подклассам переопределять ее, если они используют другой ключ. Я не могу использовать интерфейс или базовый класс с полем ID, потому что не все его имеют. Я решил использовать dynamic для вывода ID поля, но я получаю следующую ошибку: Error: An expression tree may not contain a dynamic operation.

Итак, любая идея о том, как это сделать? Будет ли отражение работать в базе UpdateCriterion?

Ответы

Ответ 1

Нет, вы не можете использовать динамический запрос Linq to Entities. Но вы можете построить выражение Lambda во время выполнения.

public virtual Expression<Func<TSubclass, object>> UpdateCriterion()
{
    var param = Expression.Parameter(typeof(TSubclass));
    var body = Expression.Convert(Expression.Property(param, "ID"), typeof(object));

    return Expression.Lambda<Func<TSubclass, object>>(body, param);
}

Если тип TSubclass не имеет свойства ID Expression.Property(param, "ID" ), генерирует исключение.

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

Ответ 2

Если вы определяете класс BaseEntity, вы можете добавить свойство виртуального чтения, которое возвращает фактическое свойство идентификатора. Я верю, что EF рассматривает свойства только для чтения как "вычисленные", поэтому они не сохраняются в db.

public abstract class BaseEntity<TSubclass> where TSubclass : BaseEntity<TSubclass>
{
    public abstract object ID { get; }
    public virtual Expression<Func<TSubclass, object>> UpdateCriterion()
    {
        return entity => entity.ID;
    }
}

public partial class Foo : BaseEntity<Foo>
{
    public Int32 FooId { get; set; }
    public override object ID { get { return FooId; } } 
}

Просто мысль - я только пытался компилировать в LinqPad и проверять значение вызова UpdateCriterion.:)

Ответ 3

Посмотрите этот ответ. Он использует отражение, чтобы получить свойство ID. Я думаю, это решает вашу проблему:

public static object GetPropValue(object src, string propName)
{
    return src.GetType().GetProperty(propName).GetValue(src, null);
}

Затем вы заменяете выражение лямбда на

return entity => GetPropValue(entity, "ID");

Я не тестировал, так как у меня нет полноценного кода для его проверки. Если это работает, сообщите нам.