我正在尝试解决由数据模型结构中的设计失败引起的烦恼。重构不是一种选择,因为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
中的Name
或Course
。据我所知,在特定情况下无法扩展此表达式以返回Id
或Name
。
我可以轻松地在一个方法中完成此操作,但是可以理解,它在LINQ to Entities上失败了。
我知道这是一个特定于项目的问题,但是在此阶段无法解决,因此我试图找到一个不错的解决方法。
首先,感谢 HimBromBeere 的回答和耐心。我无法让他的通用重载正常工作,就我而言,它就像您在他的答案下面的讨论中看到的那样正在抛出。这是我最终解决的方法:
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
调用,而不是每次构建表达式时都调用它们。同样,这可以用于两个以上的派生类(在我的情况下为InternalCourseInstance
和OpenCourseInstance
),您只需要一个额外的ConditionalExpression
。
我已经编辑了代码部分-EntityFramework不支持Expression.Convert
,但是Expression.TypeAs
的工作原理相同。
答案 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);
为了从实例中获取CourseId
或Name
,您必须引入一个期望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);
}