【问题标题】:Get derived class property from parent without repetitive casting从父类获取派生类属性,无需重复转换
【发布时间】:2018-12-31 16:35:25
【问题描述】:

我正在尝试解决由数据模型结构中的设计失败引起的烦恼。重构不是一种选择,因为 EF 发疯了。 ASP.NET 4.6 框架。

结构如下:

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 及其导航属性放在派生类型上,而不是父类型上。现在每次我想从CourseInstance 访问Course 时,我都会这样做:

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

这应该可以,但有时我需要Course 中的IdName。据我所知,没有办法在特定情况下扩展此表达式以返回 IdName

我可以在一个方法中轻松地做到这一点,但是在 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 docs

创建一个 MethodCallExpression,表示对带有参数的方法的调用。

但是我想要的表达式不包含任何方法调用 - 所以我删除了它并且它起作用了。现在我只需使用委托来提取所需属性的名称并使用另一个MemberAccessExpression 获取它。

这只是我的解释。很高兴得到纠正,如果我错了。

备注:我建议将typeof 调用缓存在私有字段中,而不是每次构建表达式时调用它们。这也适用于两个以上的派生类(在我的情况下为InternalCourseInstanceOpenCourseInstance),您只需要一个额外的ConditionalExpression(s)。

编辑

我已经编辑了代码部分 - EntityFramework 似乎不支持 Expression.Convert,但 Expression.TypeAs 的工作原理相同。

【问题讨论】:

  • 我不确定这是否适用于 EF,但您可以选择 dynamic 将您的表达式转换为 Expression&lt;Func&lt;dynamic, Course&gt;&gt; 之类的东西吗?不理想,但无论如何你的设计都被打破了。事实上,我什至讨厌自己提出这个建议......
  • @HimBromBeere 我想到了这一点,但它并不能解决概念层面的问题。如果需要,我仍然无法获得CourseId。而更多情况下,在 LINQ 表达式和 AutoMapper 配置中不需要这样做,这意味着我不能简单地获得 Id 和后续 Select 语句,例如。请不要自暴自弃,我已经对这个话题表达了足够的仇恨:)
  • 请将您的解决方案移至自己的答案,谢谢。

标签: c#


【解决方案1】:

您必须使用表达式树来创建表达式:

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);

为了从实例中获取CourseIdName,您必须引入一个期望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);
}

【讨论】:

  • 感谢您的回复。如果我理解正确 - result 将包含 Course 对象,无论我是否通过 CourseInstanceInternalCourseInstanceOpenCourseInstance?这可以在 LINQ to Entity 中使用吗?这比我的经验要深一些。会尽快尝试。
  • @Alex Me 也不是,但我认为 EF 是甚至考虑表达式树的原因之一。否则,您可以创建已经在您的问题中编写的方法。是的,result 包含 Course 的一个实例。
  • 仍然无法让Expression.Call 工作。它看起来像这样:Expression.Call(null, propertySelector.Method, outerExpression) 并抛出 静态方法需要空实例,非静态方法需要非空实例。参数名称:方​​法异常
  • 我不确定static 是什么,什么不是。我的整个表达式被定义为一个静态方法,附加到我的数据模型 - CourseInstance,但这不应该是一个问题,事实上它不是。根据编译器 - propertySelector.Method 应该是 null,但我不明白为什么。
  • 您的非通用建议也会抛出:无法将类型“CourseInstance”转换为类型“InternalCourseInstance”。 LINQ to Entities 仅支持转换 EDM 基元或枚举类型。
猜你喜欢
  • 2017-11-05
  • 2022-06-10
  • 2022-01-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-09-15
相关资源
最近更新 更多