3

I have the following LINQ query:

using (var context = new EUContext())
        {
            var tmp = context.Terms.Include(x => x.StudentCourses)
                .Where(x => x.StudentID == studentId && x.DepartmentID == departmentId)
                .OrderBy(x => x.AcademicYear)
                .ThenBy(x=> x.TermRegistered == "Fall" ? 1 :
                            x.TermRegistered == "Spring" ? 2 : 3));

            return tmp.ToList();
        }

I am trying to move the OrdyBy in the ThenBy clause to clean up the code. I am trying to use an expression as following:

private static Expression<Func<string, int>> TermsOrder(string x)
        {
            return (x == "Fall" ? 1 :
                    x == "Spring" ? 2 : 3);
        }

and my code should look like this:

using (var context = new EUContext())
            {
                var tmp = context.Terms.Include(x => x.StudentCourses)
                    .Where(x => x.StudentID == studentId && x.DepartmentID == departmentId)
                    .OrderBy(x => x.AcademicYear)
                    .ThenBy(x=> TermsOrder(x.TermRegistered));

                return tmp.ToList();
            }

Unfortunately the expression doesn't work there is a long squiggly line in the body of the expression with the following error message:

Cannot implicitly convert type 'int' to 'System.Linq.Expressions.Expression>

What am I doing wrong? This is my first try on using expressions and I know that I am missing something obvious due to not fully understanding how expressions work.

Thanks

  • I would suggest not to keep such data as string in database. If I were you I would change into into enum instead of string. You may have much more troubles later – miechooy Nov 27 '18 at 23:23
  • The database is a production database and I am writing code to provide additional functionality. So altering the database is not an option for now. – Sulieman Mansouri Nov 27 '18 at 23:40
  • Look at the type of the `return` statement - what are you returning? Look at the type of the `TermsOrder` function - what do you want to return? – NetMage Nov 28 '18 at 00:12

1 Answers1

3

This isn't as simple as it seems. You need to combine Expressions or build Expressions to generate what you want, and unfortunately C# doesn't include a lot of help in that area.

The easiest approach is to use an extension method for LambdaExpression composition. It depends on some Expression extension methods for replacing one Expression with another in an Expression:

public static class ExpressionExt {
    // Compose: f.Compose(g) => x => f(g(x))
    /// <summary>
    /// Composes two LambdaExpression into a new LambdaExpression: f.Compose(g) => x => f(g(x))
    /// </summary>
    /// <param name="fFn">The outer LambdaExpression.</param>
    /// <param name="gFn">The inner LambdaExpression.</param>
    /// <returns>LambdaExpression representing outer composed with inner</returns>
    public static Expression<Func<T, TResult>> Compose<T, TIntermediate, TResult>(this Expression<Func<TIntermediate, TResult>> fFn, Expression<Func<T, TIntermediate>> gFn) =>
        Expression.Lambda<Func<T, TResult>>(fFn.Body.Replace(fFn.Parameters[0], gFn.Body), gFn.Parameters[0]);    

    /// <summary>
    /// Replaces a sub-Expression with another Expression inside an Expression
    /// </summary>
    /// <param name="orig">The original Expression.</param>
    /// <param name="from">The from Expression.</param>
    /// <param name="to">The to Expression.</param>
    /// <returns>Expression with all occurrences of from replaced with to</returns>
    public static Expression Replace(this Expression orig, Expression from, Expression to) => new ReplaceVisitor(from, to).Visit(orig);
}

/// <summary>
/// Standard ExpressionVisitor to replace an Expression with another in an Expression.
/// </summary>
public class ReplaceVisitor : ExpressionVisitor {
    readonly Expression from;
    readonly Expression to;

    public ReplaceVisitor(Expression from, Expression to) {
        this.from = from;
        this.to = to;
    }

    public override Expression Visit(Expression node) => node == from ? to : base.Visit(node);
}

Now you can create your method that takes a lambda representing the field you want to test. It uses a local LambdaExpression as a template for the final result:

public static class Util {
    static Expression<Func<string, int>> TermOrderTemplateFn = p => (p == "Fall" ? 1 : p == "Spring" ? 2 : 3);
    public static Expression<Func<TRec, int>> TermsOrder<TRec>(Expression<Func<TRec, string>> selectorFn) =>
        TermOrderTemplateFn.Compose(selectorFn);
}

Now you can call the method in your expression, passing in a lambda representing the desired field (or field expression) to test:

var tmp = context.Terms.Include(x => x.StudentCourses).AsQueryable()
                .Where(x => x.StudentID == studentId && x.DepartmentID == departmentId)
                .OrderBy(x => x.AcademicYear)
                .ThenBy(Util.TermsOrder<Term>(p => p.TermRegistered));

Note: I am calling the type of context.Terms.First() Term but you would need to use the actual correct type name in the call to TermsOrder. You could also do TermsOrder((Term p) => ...) instead.

I would probably prefer to create a special version of ThenBy so you can use type inference to determine the record type:

public static class EFExt {
    static Expression<Func<string, int>> TermThenOrderTemplateFn = p => (p == "Fall" ? 1 : p == "Spring" ? 2 : 3);
    public static IOrderedQueryable<T> ThenByTerm<T>(this IOrderedQueryable<T> src, Expression<Func<T, string>> selectorFn) =>
        src.ThenBy(TermThenOrderTemplateFn.Compose(selectorFn));
}

Then you can use it directly:

var tmp = context.Terms.Include(x => x.StudentCourses).AsQueryable()
                .Where(x => x.StudentID == studentId && x.DepartmentID == departmentId)
                .OrderBy(x => x.AcademicYear)
                .ThenByTerm(p => p.TermRegistered);
NetMage
  • 26,163
  • 3
  • 34
  • 55
  • BTW, I am still working on how to create a useful `Expressions` helper library that would make this stuff easier. – NetMage Nov 28 '18 at 00:49
  • 1
    Is there any advantage to write `(Expression>)Expression.Lambda` instead of `Expression.Lambda>`? – Antonín Lejsek Nov 28 '18 at 01:46
  • @AntonínLejsek Absolutely no. I have no idea why people continue using cast when this overload exists. Probably because they don't know about it :) – Ivan Stoev Nov 28 '18 at 01:59
  • What you call `Swap` is really `Replace` and usually is enough just to handle lambda parameter replacing (hence called `ParameterReplacer`). Also what you do here is referred as composing expressions by [several posts](https://stackoverflow.com/search?q=user%3A1159478+Compose) by Servy, for instance [Convert Linq expression “obj => obj.Prop” into “parent => parent.obj.Prop”](https://stackoverflow.com/questions/37602729/convert-linq-expression-obj-obj-prop-into-parent-parent-obj-prop/37602870#37602870). Also there are already many libraries for doing that - LinqKit, NeinLinq, even AutoMapper. – Ivan Stoev Nov 28 '18 at 02:05
  • But.. my previous comments doesn't mean this answer is not correct. Indeed it is and does solve the OP issue. – Ivan Stoev Nov 28 '18 at 02:14
  • @IvanStoev I know about it, but it doesn't come to mind - I think because I expect generic parameters to be inferred, and am often annoyed when they are not. I don't expect to treat them like regular parameters. – NetMage Nov 28 '18 at 21:23
  • 1
    @IvanStoev I feel like those libraries are too heavy weight for this and similar issues - look at what LINQKit would require in the query, for example. I like the name `Replace` though. – NetMage Nov 28 '18 at 21:42
  • 1
    @IvanStoev Updated my answer to use functional programming compose terminology and suggest an `IOrderedQueryable` extension. – NetMage Nov 28 '18 at 22:59