0

Using .net-6 and EF Core 6, I want to define reusable projections using expressions so that I can centralize DTO mappings in one place.

Given an entity with relationships:

class Property {
  public int Id {get; set;}
  public List<Amenity> Amenities {get; set;}
  public Address Address {get; set;}
}

class Amenity {
  public int Id {get; set;}
  public string Name {get; set;}
  public string Value {get; set;}
}

class Address {
  public int Id {get; set;}
  public string Country {get; set;}
  public string City {get; set;}
  public string Street {get; set;}
}

And their DTOs:

class PropertyDto {
  public int Id {get; set;}
  public List<AmenityDto> Amenities {get; set;}
  public AddressDto Address {get; set;}
}

class AmenityDto{
  public int Id {get; set;}
  public string Name {get; set;}
  public string Value {get; set;}
}

class AddressDto{
  public int Id {get; set;}
  public string Country {get; set;}
  public string City {get; set;}
  public string Street {get; set;}
}

I can create a reusable projection expression:

public class PropertyDto {
  ...
  public static Expression<Func<Property, PropertyDto>> Projection =
    property => new PropertyDto{
      Id = property.Id,
    };
  ...
}

That I can use in any query's Select() call as the projection expression, which EF will "visit" and translate into SQL to fetch only those columns I need:

DbContext.Set<Property>()
  .Select(Property.Projection)
  .ToListAsync();

If I want to reuse projections for Amenities I can create a Projection expression for AmenityDto and do the following:

public static Expression<Func<Property, PropertyDto>> Projection =
    property => new PropertyDto{
      Id = property.Id,
      Amenities = property.Amenities.AsQueryable().Select(Amenity.Dto).ToList(),
    };

But if I want to do the same for Address I can't use .Select() to project it because it's not a collection.

public static Expression<Func<Property, PropertyDto>> Projection =
    property => new PropertyDto{
      Id = property.Id,
      Amenities = property.Amenities.AsQueryable().Select(Amenity.Dto).ToList(),
      Address = // how can I use AddressDto.Projection here?
    };

The Address field expects an AddressDto. If I use a callback e.g. AddressDto.Projection(address) EF will load the whole entity because it can't translate the method to SQL. After a lot of google I have only come across some articles discussing the use of .AsExpandable() or [ReplaceWithExpression] attribute to instruct EF to replace a method with an expression. As far as I can tell, none of these longer work in EF Core 6.0

Is there any way I can reuse projection expressions when projecting a single entity?

Gert Arnold
  • 105,341
  • 31
  • 202
  • 291
leuquim
  • 636
  • 6
  • 16
  • 1
    Check [this my answer](https://stackoverflow.com/a/66386142/10646316). Maybe it will be more useful for you. Anyway with LINQKit we can also correct your code. – Svyatoslav Danyliv Nov 10 '22 at 16:26
  • @SvyatoslavDanyliv Thank you for pointing me in the right direction! I had run into LINQKit but wasn't aware it was capable of doing this. I'm also surprised none of my Google searches brought up that SO thread. Appreciate it! – leuquim Nov 10 '22 at 20:25

1 Answers1

0

Just to answer my own question, and thanks to the comment by @SvyatoslavDanyliv, here is what I did to solve my issue:

  1. I installed LinqKit.Microsoft.EntityFrameworkCore
  2. I created a method that returns the expression for projecting my domain model to the Dto equivalent:
public static Expression<Func<Address, AddressDto>> Projection() =>
   address => new AddressDto
   {
       Id = address .Id,
       Country = address.Country,
       City = address.City,
       Street = address.Street,
   };
  1. I created the method that will be called from within other projections, and which LinqKit will replace with the projection expression above.
[Expandable(nameof(Projection))]
public static AddressDto FromEntity(Address address)
{
    // If you want to use this method outside of projections
    // return Projection().Compile().Invoke(address);
    throw new NotImplementedException();
}

I decided to throw new NotImplementedException() because I will only be calling this method from within projections, but if you wish to reuse this method to create DTOs elsewhere in your codebase you can compile & invoke your projection.

  1. I used the replaceable Dto projection method in my parent EF projections:
...
.Select(property => new PropertyDto{
   Id = property.Id,
   Address = AddressDto.FromEntity(property.Address),
   ...
})
...

The resulting EF SQL properly selects only those columns used by the child Dto.

leuquim
  • 636
  • 6
  • 16