Reputation: 893
I am trying to work-around an annoyance, caused by design failure in the data model structure. Refactoring is not an option, because EF goes crazy. ASP.NET 4.6 framework.
The structure is as follows:
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; }
}
I bet you can already see the flaw? Indeed, for an unknown reason, someone decided to put CourseId
and its navigational property on the derived types, instead of parent. Now every time I want to access the Course
from CourseInstance
, I have do do something like:
x.course => courseInstance is InternalCourseInstance
? (courseInstance as InternalCourseInstance).Course
: (courseInstance as OpenCourseInstance).Course;
You can see how this can become really ugly with several more course instance types that derive from CourseInstance
.
I am looking for a way to short-hand that, essentially create a method or expression which does it internally. There is one more problem however - it has to be translatable to SQL, since more often then not this casting is used on IQueryable
.
The closest I came to the solution is:
// CourseInstance.cs
public static Expression<Func<CourseInstance, Course>> GetCourseExpression =>
t => t is OpenCourseInstance
? (t as OpenCourseInstance).Course
: (t as InternalCrouseInstance).Course
This should work, however sometimes I need Id
or Name
of Course
. And there is no way, as far as I can tell - to expand this Expression in specific circumstances to return Id
or Name
.
I can easily do it inside a method, but then it fails on LINQ to Entities, understandably.
I know it's a project-specific problem, however at this stage it cannot be fixed, so I am trying to find a decent work around.
Firstly, thanks to HimBromBeere for his answer and patience. I couldn't get his generic overload to work, in my case it was throwing as you might see in the discussion below his answer. Here is how I solved it eventually:
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));
The problem with the suggested implementation in my opinion is within the Expression.Call
line. Per MS docs:
Creates a MethodCallExpression that represents a call to a method that takes arguments.
However my desired expression contains no method calls - so I removed it and it worked. Now I simply use the delegate to extract the desired property's name and get that with another MemberAccessExpression
.
This is only my interpretation though. Happy to get corrected, if I am wrong.
Remarks: I recommend caching the typeof
calls in private fields, instead of calling them every time you build the expression. Also this can work for more then two derived classes (in my case InternalCourseInstance
and OpenCourseInstance
), you just need an extra ConditionalExpression
(s).
I've edited the code section - it seems Expression.Convert
is not supported by EntityFramework, however Expression.TypeAs
works just the same.
Upvotes: 2
Views: 123
Reputation: 37000
You have to create the expression using an expression-tree:
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);
}
ow you can use this expression by compiling it and call it:
var func = CreateExpression().Compile();
var courseInstance = new InternalCourseInstance { Course = new Course { Name = "MyCourse" } };
var result = func(courseInstance);
In order to get the CourseId
or the Name
from the instance you have to introduce a delegate that expects an instance of Course
and returns any arbitrary type T
. This means you´d need to add a call to that delegate in yoour expression-tree:
expr = Expression.Call(null, func.Method, expr);
The null
is important as your delegate that points to an anonymous method is translated to a static method from your compiler. If the delegate on the other hand points to a named non-static method, you should of course provide an instance for which this method is then called:
expr = Expression.Call(instanceExpression, func.Method, expr);
Be aware that your compiled method now returns a T
, not a Course
, so your final method looks like this:
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);
}
Upvotes: 1