2

There is a question here: LEFT OUTER JOIN in LINQ, but that was specifically asked about Linq-to-Objects. This question is about Linq-to-Entities.

I have an inner join that I'd like to make into an outer join:

        alumni = alumni.Join(_context.AlumniSurvey, a => a.Uid, s => s.Uid, (a, s) => new { a, s })
                    .Where(x => x.s.UniversityNumber != x.s.Uid)
                    .Select(x => x.a);

Not that I am only returning alumni; I include AlumniSurvey because I will perform a variety of where clauses based upon what the user selects in the index view

General Grievance
  • 4,555
  • 31
  • 31
  • 45
Ron Isaac
  • 109
  • 2
  • 10
  • Have you tried to use Google? This is most common question in the .NET world. https://stackoverflow.com/questions/3404975/left-outer-join-in-linq – Svyatoslav Danyliv Feb 01 '21 at 19:59
  • I'm not sure. I'd rather not to have to reference each property. as shown in that example – Ron Isaac Feb 01 '21 at 20:44
  • I am so happy the person answered my question. The other question to which I was referred was not nearly as helpful as this answer. I saw my mistake quickly with this. – Ron Isaac Feb 01 '21 at 21:15
  • Related: https://stackoverflow.com/questions/19356439/left-join-in-linq-to-entities – General Grievance Aug 23 '21 at 17:43

1 Answers1

5

This should translate to a LEFT JOIN in SQL - note that this will cause duplicate alumni rows if there are more than one matching AlumniSurvey rows.

alumni = alumni.GroupJoin(_context.AlumniSurvey, a => a.Uid, s => s.Uid, (a, sj) => new { a, sj })
               .SelectMany(asj => asj.sj.DefaultIfEmpty(), (asj, s) => new { asj.a, s })
               .Where(x => x.s.UniversityNumber != x.s.Uid)
               .Select(x => x.a);

You can create an extension method to simplify this:

private static Expression<Func<TOuter, TInner, TResult>> CastSMLambda<TOuter, TInner, TResult>(LambdaExpression ex, TOuter _1, TInner _2, TResult _3) => (Expression<Func<TOuter, TInner, TResult>>)ex;

public static IQueryable<TResult> LeftOuterJoin<TOuter, TInner, TKey, TResult>(
    this IQueryable<TOuter> outer,
    IQueryable<TInner> inner,
    Expression<Func<TOuter, TKey>> outerKeyExpr,
    Expression<Func<TInner, TKey>> innerKeyExpr,
    Expression<Func<TOuter, TInner, TResult>> resExpr) {

    var gjResTemplate = new { outer = default(TOuter), innerj = default(IEnumerable<TInner>) };
    // typeof(new { outer, innerj }) oij
    var oijParm = Expression.Parameter(gjResTemplate.GetType(), "oij");
    // TInner inner
    var iParm = Expression.Parameter(typeof(TInner), "inner");
    // oij.outer
    var oijOuter = Expression.PropertyOrField(oijParm, "outer");
    // (oij,inner) => resExpr(oij.outer, inner)
    var selectResExpr = CastSMLambda(Expression.Lambda(resExpr.Apply(oijOuter, iParm), oijParm, iParm), gjResTemplate, default(TInner), default(TResult));

    return outer.GroupJoin(inner, outerKeyExpr, innerKeyExpr, (outer, innerj) => new { outer, innerj })
                .SelectMany(r => r.innerj.DefaultIfEmpty(), selectResExpr);
}

// Apply: (x => f).Apply(args)
/// <summary>
/// Substitutes an array of Expression args for the parameters of a lambda, returning a new Expression
/// </summary>
/// <param name="e">The original LambdaExpression to "call".</param>
/// <param name="args">The Expression[] of values to substitute for the parameters of e.</param>
/// <returns>Expression representing e.Body with args substituted in</returns>
public static Expression Apply(this LambdaExpression e, params Expression[] args) {
    var b = e.Body;

    foreach (var pa in e.Parameters.Zip(args, (p, a) => (p, a)))
        b = b.Replace(pa.p, pa.a);

    return b.PropagateNull();
}

/// <summary>
/// Replaces an Expression (reference Equals) with another 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 T Replace<T>(this T orig, Expression from, Expression to) where T : Expression => (T)new ReplaceVisitor(from, to).Visit(orig);

/// <summary>
/// ExpressionVisitor to replace an Expression (that is Equals) with another 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);
}

public static T PropagateNull<T>(this T orig) where T : Expression => (T)new NullVisitor().Visit(orig);

/// <summary>
/// ExpressionVisitor to replace a null.member Expression with a null
/// </summary>
public class NullVisitor : System.Linq.Expressions.ExpressionVisitor {
    public override Expression Visit(Expression node) {
        if (node is MemberExpression nme && nme.Expression is ConstantExpression nce && nce.Value == null)
            return Expression.Constant(null, nce.Type.GetMember(nme.Member.Name).Single().GetMemberType());
        else
            return base.Visit(node);
    }
}
NetMage
  • 26,163
  • 3
  • 34
  • 55
  • Thanks! I added .Distinct() after the .Select (...) to get distinct records. – Ron Isaac Feb 01 '21 at 21:07
  • @RonIsaac Be careful with that - at least in EF 5.x, you can get a SQL exception if any of the columns are of the `TEXT` type since you can't do `DISTINCT` on a `TEXT` column... – NetMage Feb 01 '21 at 21:16
  • yes - thanks - I did get that, but for the grid, I don't need the text type. I am tempted to just use ADO.NET or something other than EF. Or maybe EF with views. Do you have anything thoughts on the subject? – Ron Isaac Feb 01 '21 at 22:16
  • @RonIsaac I vastly prefer LINQ over ADO.Net. I would only use Views when needing queries EF can't handle (e.g. full text search). I think EF Core 5.x is close to usable, so I would try to do that. – NetMage Feb 01 '21 at 22:21
  • thank your - I will continue forward with it. But because my existing database has some wackiness, I cannot use Foreign keys, so I cannot use navigation in EF. Thus, I have to peel back the covers and learn a bit more. – Ron Isaac Feb 02 '21 at 11:03
  • @RonIsaac What kind of wackiness? If your database has foreign keys of any sort, you should be able to teach EF how to use them... – NetMage Feb 02 '21 at 17:05
  • I'm sure you are right, but I'm switching to Dapper because I prefer to think in SQL. – Ron Isaac Feb 03 '21 at 21:27
  • @RonIsaac Thinking in SQL is fine, but brings with it the problems LINQ tries to prevent automatically with strong type checking (see [BobbyTables](https://xkcd.com/327/)). – NetMage Feb 03 '21 at 21:40
  • I would love to know more about what LINQ will solve, but the Bobby Tables link was to site xkcd. – Ron Isaac Feb 04 '21 at 14:42
  • @RonIsaac Yes, it is a commonly referenced explanation of the primary problem with using SQL from another language - using string substitution instead of parameters to put values into SQL statements leads to horrendous security holes. – NetMage Feb 04 '21 at 18:09
  • I believe Dapper uses parameterized queries. I will use EF for simple editing, but for complex queires to grids Dapper seenms easier – Ron Isaac Feb 05 '21 at 10:44