2

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.


Solution

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:

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

Usage

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

Thoughts

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

Edit

I've edited the code section - it seems Expression.Convert is not supported by EntityFramework, however Expression.TypeAs works just the same.

3
  • I´m not sure if this works for EF, but could you go for dynamic turning your expression into something like Expression<Func<dynamic, Course>>? Not ideal, but as your design is broken anyway. In fact I hate myself for even suggesting it... Commented Nov 9, 2018 at 13:15
  • @HimBromBeere I thought of that, however it does not solve the problem on conceptual level. I still don't have a way of getting the Id of Course, if I need. And more often then not this is required inLINQ expressions and AutoMapper configurations, which means I cannot simply get the Id with follow up Select statement, for example. Please don't have yourself, I've expressed enough hate on this subject already :) Commented Nov 9, 2018 at 14:21
  • Please move your solution to an answer of its own, thank you. Commented Dec 31, 2018 at 8:35

1 Answer 1

1

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);
}
Sign up to request clarification or add additional context in comments.

13 Comments

Thanks for the reply. If I understand correctly - result will contain the Course object, regardless if I pass CourseInstance, InternalCourseInstance or OpenCourseInstance? Would that work in LINQ to Entities? This goes a bit deeper then my experience. Will give it a go ASAP.
@Alex Me neither, however I assume EF is one of the reasons to even think about expression-trees at all. Otherwise you could just create the method as written in your question already. And yes, result contains an instance of Course.
Still can't get Expression.Call to work. It looks like this: Expression.Call(null, propertySelector.Method, outerExpression) and throws Static method requires null instance, non-static method requires non-null instance. Parameter name: method exception
I am not sure what is static and what is not. My whole Expression was defined as a static method, attached to my Data model - CourseInstance, but this shouldn't be a problem and indeed it is not. According to the compiler - propertySelector.Method is supposed to be null, but I don't understand why.
Your non-generic suggestion also throws: Unable to cast the type 'CourseInstance' to type 'InternalCourseInstance'. LINQ to Entities only supports casting EDM primitive or enumeration types.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.