Search code examples
c#entity-framework-coreprojectionnavigation-properties

Searching Against Projected EF Core Navigation Properties


Using EF Core 3.1, I'm trying to perform a search against a nullable navigation property (a.k.a. left join) after it has been projected to a different type.

For example:

var dtos = await query
    .Select(x => new RootEntityDto
    {
        // ...

        Nested = x.NestedId.HasValue
            ? new NestedEntityDto
            {
                // ...

                Description = x.Nested.Description
            }
            : null
    })
    .Where(x => x.Nested.Description.Contains("2"))
    .ToArrayAsync();

The problem is, it gives me an error over the ternary that may conditionally result in null:

The LINQ expression 'DbSet<RootEntity>
    .LeftJoin(
        outer: DbSet<NestedEntity>, 
        inner: f => EF.Property<Nullable<int>>(f, "NestedId"), 
        outerKeySelector: o => EF.Property<Nullable<int>>(o, "Id"), 
        innerKeySelector: (o, i) => new TransparentIdentifier<RootEntity, NestedEntity>(
            Outer = o, 
            Inner = i
        ))
    .Where(f => EF.Property<Nullable<int>>(f.Inner, "Id") != null ? new NestedEntityDto{ 
        Description = f.Inner.Description, 
    }
     : null.Description.Contains("2"))' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.

If I try precisely the same thing before projecting the entity to a DTO, I have no problems:

var entities = await query
    .Where(x => x.Nested.Description.Contains("2"))
    .ToArrayAsync();

I realize the simple solution is to perform the Where statement before the projection occurs, but that's not an option. The above examples are simplified in order to illustrate the intent. I'm also not interested in evaluating the query client-side just because I don't know a better alternative to the ternary.

Ideally, I'm just looking for the best practice for conditionally projecting a left join navigation property, such that I can still perform searches on it.

Edit: I decided to try AutoMapper 10.1.1 and received a similar error:

var dtos = await query
    .ProjectTo<RootEntityDto>(_mapper.ConfigurationProvider, x => x.Nested)
    .Where(x => x.Nested.Description.Contains("2"))
    .ToArrayAsync();
The LINQ expression 'DbSet<RootEntity>
    .LeftJoin(
        outer: DbSet<NestedEntity>, 
        inner: f => EF.Property<Nullable<int>>(f, "NestedId"), 
        outerKeySelector: o => EF.Property<Nullable<int>>(o, "Id"), 
        innerKeySelector: (o, i) => new TransparentIdentifier<RootEntity, NestedEntity>(
            Outer = o, 
            Inner = i
        ))
    .Where(f => EF.Property<Nullable<int>>(f.Inner, "Id") == null ? null : new NestedEntityDto{ 
        Description = f.Inner.Description
    }
    .Description.Contains("2"))' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.

Solution

  • I did some research into what AutoMapper was doing and discovered a convention that works:

    var dtos = await query
        .Select(x => new RootEntityDto
        {
            // ...
    
            Nested = x.Nested == null
                ? null
                : new NestedEntityDto
                {
                    // ...
    
                    Description = x.Nested.Description
                }
        })
        .Where(x => x.Nested.Description.Contains("2"))
        .ToArrayAsync();
    

    Notice that I inverted the ternary and switched what would have been !x.NestedId.HasValue to x.Nested == null. But even with those changes, I still had to upgrade to EF Core 5.