从父级获取派生类属性,而无需重复进行转换

时间:2018-11-09 13:11:44

标签: c#

我正在尝试解决由数据模型结构中的设计失败引起的烦恼。重构不是一种选择,因为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及其导航属性放在派生类型上,而不是放在父类上。现在,每次我想从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中的NameCourse。据我所知,在特定情况下无法扩展此表达式以返回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行之内。根据{{​​3}}:

  

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

但是,我想要的表达式不包含任何方法调用-因此我将其删除并成功运行。现在,我仅使用委托来提取所需属性的名称,然后使用另一个MemberAccessExpression来获取该名称。

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

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

编辑

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

1 个答案:

答案 0 :(得分: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的委托。这意味着您需要在您的expression-tree中添加对该委托的调用:

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