【发布时间】: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 中的Id 或Name。据我所知,没有办法在特定情况下扩展此表达式以返回 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 docs:
创建一个 MethodCallExpression,表示对带有参数的方法的调用。
但是我想要的表达式不包含任何方法调用 - 所以我删除了它并且它起作用了。现在我只需使用委托来提取所需属性的名称并使用另一个MemberAccessExpression 获取它。
这只是我的解释。很高兴得到纠正,如果我错了。
备注:我建议将typeof 调用缓存在私有字段中,而不是每次构建表达式时调用它们。这也适用于两个以上的派生类(在我的情况下为InternalCourseInstance 和OpenCourseInstance),您只需要一个额外的ConditionalExpression(s)。
编辑
我已经编辑了代码部分 - EntityFramework 似乎不支持 Expression.Convert,但 Expression.TypeAs 的工作原理相同。
【问题讨论】:
-
我不确定这是否适用于 EF,但您可以选择
dynamic将您的表达式转换为Expression<Func<dynamic, Course>>之类的东西吗?不理想,但无论如何你的设计都被打破了。事实上,我什至讨厌自己提出这个建议...... -
@HimBromBeere 我想到了这一点,但它并不能解决概念层面的问题。如果需要,我仍然无法获得
Course的Id。而更多情况下,在 LINQ 表达式和 AutoMapper 配置中不需要这样做,这意味着我不能简单地获得Id和后续Select语句,例如。请不要自暴自弃,我已经对这个话题表达了足够的仇恨:) -
请将您的解决方案移至自己的答案,谢谢。
标签: c#