1

Not sure if I put the proper title for my problem.

In simple words, I'm trying to extract a projection from the query into a static class, but when I do that, EF queries for all columns.

await dbQuery
          .Select(pl => new PackingListDto
                            {
                                Id = pl.Id,
                                Name = pl.Name
                            })
          .AsNoTracking()
          .ToListAsync();

await dbQuery
          .Select(pl => pl.AsDto())
          .AsNoTracking()
          .ToListAsync();

In the 2nd query, the mapping is extracted into this static class:

internal static class Extensions
{
    public static PackingListDto AsDto(this PackingListReadModel readModel)
        => new()
        {
            Id = readModel.Id,
            Name = readModel.Name
        };
}

Which for me should generate exactly the same SQL query. However the queries generated are:

SELECT p."Id", p."Name"
FROM packing."PackingLists" AS p

and

SELECT p."Id", p."Localization", p."Name", p."Version", p0."Id", p0."IsPacked", p0."Name", p0."PackingListId", p0."Quantity"
FROM packing."PackingLists" AS p
LEFT JOIN packing."PackingItems" AS p0 ON p."Id" = p0."PackingListId"
ORDER BY p."Id"

The whole code for the project can be found here, just simplified the DTO.

https://github.com/devmentors/PackIT

Any ideas?

Thanks

PS: I'm not sure if that changes anything, but using postgresql as the database.

UPDATE Tried to use LinqKit as suggested in comment.

            await dbQuery
          .Select(pl => LinqKitExample.AsDto(pl))
          .AsExpandable()
          .ToListAsync();

And the class:

    internal static class LinqKitExample
{
    [Expandable(nameof(AsDtoImpl))]
    public static ExampleDto AsDto(PackingListReadModel model)
    {
        _asDtoImpl ??= AsDtoImpl.Compile();
        return _asDtoImpl(model);
    }

    private static Func<PackingListReadModel, ExampleDto> _asDtoImpl;

    private static Expression<Func<PackingListReadModel, ExampleDto>> AsDtoImpl =>
        model => new ExampleDto
        {
            Id = model.Id,
            Name = model.Name,
        };
}

However that still selected all fields from the database. The log from console

info: Microsoft.EntityFrameworkCore.Infrastructure[10403] Entity Framework Core 6.0.7 initialized 'ReadDbContext' using provider 'Npgsql.EntityFrameworkCore.PostgreSQL:6.0.6+6fa8f3c27a7c241a66e72a6c09e0b252509215d0' with options: LINQKitExpandable info: Microsoft.EntityFrameworkCore.Database.Command[20101] Executed DbCommand (4ms) [Parameters=[], CommandType='Text', CommandTimeout='30'] SELECT p."Id", p."Localization", p."Name", p."Version", p0."Id", p0."IsPacked", p0."Name", p0."PackingListId", p0."Quantity" FROM packing."PackingLists" AS p LEFT JOIN packing."PackingItems" AS p0 ON p."Id" = p0."PackingListId" ORDER BY p."Id"

  • Does this answer your question? [Can I reuse code for selecting a custom DTO object for a child property with EF Core?](https://stackoverflow.com/questions/66378438/can-i-reuse-code-for-selecting-a-custom-dto-object-for-a-child-property-with-ef) – Svyatoslav Danyliv Aug 07 '22 at 13:14
  • I've tried to use LinqKit but either I did something wrong wrong or the query was not affected. await dbQuery .AsExpandable() .Select(pl => pl.TestLinq()) .ToListAsync(); And TestLinq being: public static Expression> TestLinq(this PackingListReadModel readModel) { Expression> output = output => new PackingListDto() { Id = readModel.Id, Name = readModel.Name}; return output; } – Lukasz Pomianowski Aug 07 '22 at 15:52
  • It is wrong. Where is `Expandable` attributes? – Svyatoslav Danyliv Aug 07 '22 at 17:18
  • Sorry, done it properly this time ( I believe ) however it still selected all fields. I will paste it properly for clarity – Lukasz Pomianowski Aug 07 '22 at 20:47

2 Answers2

2

Your pl => pl.AsDto() statement is delegate. EF Core provider can not translate it to SQL.

Method must return Expression.

internal static class Extensions
{
    public static Expression<Func<PackingList, PackingListDto>> ToDto()
        => packingList => new PackingListDto
        {
            Id = packingList.Id,
            Name = packingList.Name
        };
}

Use it like this:

await dbQuery
    .Select(Extensions.ToDto())
    .AsNoTracking()
    .ToListAsync();

You must use the method exactly like this: Select(Extensions.ToDto()).
You must not use it like this: Select(x => Extensions.ToDto()).

The method Extensions.ToDto() returns Expression that can be translated by EF into SQL.
The lambda x => Extensions.ToDto() converts it into a delegate that EF doesn't know what to do with.


Other approach. Method returns IQueryable.

internal static class Extensions
{
    public static IQueryable<PackingListDto> ToDto(this IQueryable<PackingList> packingLists)
        => packingLists.Select(packingList => new PackingListDto
        {
            Id = packingList.Id,
            Name = packingList.Name
        });
}

Use it like this:

await dbQuery
    .ToDto()
    .AsNoTracking()
    .ToListAsync();
Alexander Petrov
  • 13,457
  • 2
  • 20
  • 49
  • A method returning IQueryable works. However when tried to use Expression I got the following error: System.InvalidOperationException: The LINQ expression 'packingList => new ExampleDto{ Id = packingList.Id, Name = packingList.Name } ' 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 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'. – Lukasz Pomianowski Aug 08 '22 at 12:06
  • 1
    @LukaszPomianowski - see update – Alexander Petrov Aug 08 '22 at 12:54
  • Perfect. That has worked. Just before I accept it as the answer any ideas why the code I posted does not work ? I like the statement Select(pl => pl.AsDto()) as I find it very neat / explicit. Thank you for the help and in advance for the answer. – Lukasz Pomianowski Aug 08 '22 at 14:31
  • @LukaszPomianowski - added more explanation. I'm not native English speaker, so me hard to proper explain. – Alexander Petrov Aug 08 '22 at 14:57
-1

First - you don't map your query result to a static class, you map it to a (just) class using a static method extension.

Second - Postgres (or any DB engine for this case) knows nothing about method AsDto and how to translate it to SQL. Thus it gives up and fetches everything to be processed on the client within your method's body later.

Nick Farsi
  • 366
  • 4
  • 19