Получить свойство производного класса от родителя без повторного приведения - PullRequest
0 голосов
/ 09 ноября 2018

Я пытаюсь обойти раздражение, вызванное ошибкой проектирования в структуре модели данных. Рефакторинг не вариант, потому что EF сходит с ума. ASP.NET 4.6 Framework.

Структура выглядит следующим образом:

class Course
{
     // properties defining a Course object. Example: Marketing course
     public string Name { get; set; }
}

class CourseInstance
{
    // properties that define an Instance of course. Example: Marketing course, January
    public DateTime StartDate { get; set; }
}

class InternalCourseInstance : CourseInstance
{
    // Additional business logic properties. Example : Entry course - Marketing program
    public bool IsEntry { get; set; }

    public int CourseId { get; set; }

    public Course Course { get; set; }
}

class OpenCourseInstance : CourseInstance
{
    // Separate branch of instance. Example - Marketing course instance
    public int Price { get; set; }    

    public int CourseId { get; set; }

    public Course Course { get; set; }
}

Могу поспорить, ты уже видишь недостаток? Действительно, по неизвестной причине кто-то решил поместить CourseId и его навигационное свойство в производные типы вместо родительских. Теперь каждый раз, когда я хочу получить доступ к Course из CourseInstance, я делаю что-то вроде:

x.course => courseInstance is InternalCourseInstance
    ? (courseInstance as InternalCourseInstance).Course
    : (courseInstance as OpenCourseInstance).Course;

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

Я ищу способ сократить это, по сути, создать метод или выражение, которое делает это внутренне. Однако есть еще одна проблема - она ​​должна быть переведена на SQL, поскольку чаще всего не * это приведение используется на IQueryable.

Самое близкое, что я нашел к решению:

// CourseInstance.cs
public static Expression<Func<CourseInstance, Course>> GetCourseExpression =>
    t => t is OpenCourseInstance
        ? (t as OpenCourseInstance).Course
        : (t as InternalCrouseInstance).Course

Это должно работать, однако иногда мне нужно Id или Name из Course. И, насколько я могу судить, нет никакого способа - расширить это Выражение при определенных обстоятельствах, чтобы вернуть Id или Name.

Я легко могу сделать это внутри метода, но тогда, по понятным причинам, он не работает на LINQ to Entities.

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


Решение

Во-первых, спасибо HimBromBeere за его ответ и терпение. Я не мог заставить его общую перегрузку сработать, в моем случае это было сбрасывание, как вы можете видеть в обсуждении ниже его ответа. Вот как я решил это в итоге:

CourseInstance.cs

public static Expression<Func<CourseInstance, TProperty> GetCourseProperty<TProperty>(
    Expression<Func<Course, TProperty>> propertySelector)
{
    var parameter = Expression.Parameter(typeof(CourseInstance), "ci");

    var isInternalCourseInstance = Expression.TypeIs(parameter, typeof(InternalCourseInstance);

    // 1) Cast to InternalCourseInstance and get Course property
    var getInternalCourseInstanceCourse = Expression.MakeMemberAccess(
        Expression.TypeAs(parameter, typeof(InternalCourseInstance)), typeof(InternalCourseInstance).GetProperty(nameof(InternalCourseInstance.Course)));

    var propertyName = ((MemberExpression)propertySelector.Body).Member.Name;

    // 2) Get value of <propertyName> in <Course> object.
    var getInternalCourseInstanceProperty = Expression.MakeMemberAccess(
        getInternalCourseInstanceCourse, typeof(Course).GetProperty(propertyName);

    // Repeat steps 1) and 2) for OpenCourseInstance ...

    var expression = Expression.Condition(isInternalCourseInstance, getInternalCourseInstanceProperty, getOpenCourseInstanceProperty);

    return Expression.Lambda<Func<CourseInstance, TProperty(expression, parameter);

Использование

// his first suggestion - it works, retrieving the `Course` property of `CourseInstance`
var courses = courseInstancesQuery.Select(GetCourse()) 

// My modified overload above. 
var courseNames = courseInstancesQuery.Select(GetCourseProperty<string>(c => c.Name));

Мысли

Проблема с предложенной реализацией, на мой взгляд, находится в пределах строки Expression.Call. За MS документов :

Создает выражение MethodCallExpression, представляющее вызов метода, который принимает аргументы.

Однако мое желаемое выражение не содержит вызовов методов - поэтому я удалил его, и оно сработало. Теперь я просто использую делегат, чтобы извлечь имя нужного свойства и получить его с другим MemberAccessExpression.

Это только моя интерпретация. Рад поправиться, если я ошибаюсь.

Замечания : Я рекомендую кэшировать вызовы typeof в приватных полях, а не вызывать их каждый раз, когда вы строите выражение. Также это может работать для более чем двух производных классов (в моем случае InternalCourseInstance и OpenCourseInstance), вам просто нужны дополнительные ConditionalExpression (s).

Редактировать

Я отредактировал раздел кода - кажется, Expression.Convert не поддерживается EntityFramework, однако Expression.TypeAs работает точно так же.

1 Ответ

0 голосов
/ 09 ноября 2018

Вы должны создать выражение, используя дерево выражений:

Expression<Func<CourseInstance, Course>> CreateExpression()
{
    // (CourseInstance x) => x is InternalCourseInstance ? ((InternalCourseInstance)x).Course : ((OpenCourseInstance).x).Course

    ParameterExpression param = Expression.Parameter(typeof(CourseInstance), "x");
    Expression expr = Expression.TypeIs(param, typeof(InternalCourseInstance));
    var cast1Expr = Expression.MakeMemberAccess(Expression.Convert(param, typeof(InternalCourseInstance)), typeof(InternalCourseInstance).GetProperty(nameof(InternalCourseInstance.Course)));
    var cast2Expr = Expression.MakeMemberAccess(Expression.Convert(param, typeof(OpenCourseInstance)), typeof(OpenCourseInstance).GetProperty(nameof(OpenCourseInstance.Course)));
    expr = Expression.Condition(expr, cast1Expr, cast2Expr);

    return Expression.Lambda<Func<CourseInstance, Course>>(expr, param);
}

Теперь вы можете использовать это выражение, скомпилировав его и вызвав его:

var func = CreateExpression().Compile();
var courseInstance = new InternalCourseInstance { Course = new Course { Name = "MyCourse" } };
var result = func(courseInstance);

Чтобы получить CourseId или Name из экземпляра, вы должны ввести делегата, который ожидает экземпляр Course и возвращает любой произвольный тип T. Это означает, что вам нужно добавить вызов к этому делегату в вашем дереве выражений:

expr = Expression.Call(null, func.Method, expr);

Значение null важно, поскольку ваш делегат, указывающий на анонимный метод, транслируется статическим методом из вашего компилятора. Если делегат, с другой стороны, указывает на именованный нестатический метод, вы, конечно, должны предоставить экземпляр, для которого этот метод затем вызывается:

expr = Expression.Call(instanceExpression, func.Method, expr);

Имейте в виду, что ваш скомпилированный метод теперь возвращает T, а не Course, поэтому ваш последний метод выглядит следующим образом:

Expression<Func<CourseInstance, T>> CreateExpression<T>(Func<Course, T> func)
{
    // x => func(x is InternalCourseInstance ? ((InternalCourseInstance)x).Course : ((OpenCourseInstance).x).Course)

    ParameterExpression param = Expression.Parameter(typeof(CourseInstance), "x");
    Expression expr = Expression.TypeIs(param, typeof(InternalCourseInstance));
    var cast1Expr = Expression.MakeMemberAccess(Expression.Convert(param, typeof(InternalCourseInstance)), typeof(InternalCourseInstance).GetProperty(nameof(InternalCourseInstance.Course)));
    var cast2Expr = Expression.MakeMemberAccess(Expression.Convert(param, typeof(OpenCourseInstance)), typeof(OpenCourseInstance).GetProperty(nameof(OpenCourseInstance.Course)));
    expr = Expression.Condition(expr, cast1Expr, cast2Expr);
    expr = Expression.Call(null, func.Method, expr);

    return Expression.Lambda<Func<CourseInstance, T>>(expr, param);
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...