0

I am working on the re-usable projection methods for my EFC queries & manual model-to-DTO convertions.

My expectation is to have single-defined model-to-DTO projection (in a static class) and be able to use it as EFC Select(...) Expression, and be able to manually invoke such method.

I did a lot of attempts to solve that problem, but they all have unexpected disadvantages - I have to manually write sub-model projection, or Select query includes all model fields instead of expected ones, or it totally does not work, throws exceptions instead.

Example code:

    // Models
    public class Bar
    {
        public int Id { get; set; }
        public int Something { get; set; }
        public int Something2 { get; set; }
        public int Secret { get; set; }
    }

    public record BarDto(int Something, int Something2);

    public class Foo
    {
        public int Id { get; set; }
        public int Something { get; set; }
        public int Secret { get; set; }
        public ICollection<Bar> Bars { get; set; }
        public Bar TestBar { get; set; }
    }

    public record FooDto(int Something, List<BarDto> Bars);

    public record FooTestDto(int Something, BarDto TestDto);

Dream code:

    public static class ExpectedCodeExtensions
    {
        public static Func<Bar, BarDto> BarToDtoProjection = x => x == null ? null : new BarDto(x.Something, x.Something2);
        public static Func<Foo, FooDto> FooToDtoProjection = x => x == null ? null : new FooDto(x.Something, x.Bars.AsQueryable().Select(y => BarToDtoProjection.Invoke(y)).ToList());
        public static Func<Foo, FooTestDto> FooToTestDtoProjection = x => x == null ? null : new FooTestDto(x.Something, x.TestBar == null ? null : BarToDtoProjection.Invoke(x.TestBar));

        public static Expression<Func<Bar, BarDto>> BarToDtoExpr = x => BarToDtoProjection.Invoke(x);
        public static Expression<Func<Foo, FooDto>> FooToDtoExpr = x => FooToDtoProjection.Invoke(x);
        public static Expression<Func<Foo, FooTestDto>> FooToTestDtoExpr = x => FooToTestDtoProjection.Invoke(x);
    }

But that code has large disadvantage -> Query built with those two included all Foo fields, instead of expected ones, and does not include ICollection, or just Bar (just TestBarId):

SELECT "f"."Id", "f"."Secret", "f"."Something", "f"."TestBarId"
FROM "Foos" AS "f"
SELECT "f"."Id", "f"."Secret", "f"."Something", "f"."TestBarId"
FROM "Foos" AS "f"

Manually-typed query produces "expected" SQL query:

        public static Expression<Func<Foo, FooDto>> ManuallyTypedQuery = x => x == null ? null : new FooDto(x.Something, x.Bars.Select(y => new BarDto(y.Something, y.Something2)).ToList());
SELECT 0, "f"."Something", "f"."Id", "b"."Something", "b"."Something2", "b"."Id"
FROM "Foos" AS "f"
LEFT JOIN "Bars" AS "b" ON "f"."Id" = "b"."FooId"
ORDER BY "f"."Id", "b"."Id"

But it's disadvantage is repeating same code a lot amount of times (in target project, I have to map same DTO a lot amount of times).

Is there a way to have a single-defined projection Func<T, T2>, Expression that uses that Func, and performs high-performance queries (that selects only expected rows)?

This is how I got queries mentioned above:

        [HttpGet]
        public void Tests()
        {
            var q = db.Foos.Select(ExpectedCodeExtensions.FooToDtoExpr);
            Console.WriteLine(q.ToQueryString());

            var q2 = db.Foos.Select(ExpectedCodeExtensions.FooToTestDtoExpr);
            Console.WriteLine(q2.ToQueryString());

            var q3 = db.Foos.Select(ExpectedCodeExtensions.ManuallyTypedQuery);
            Console.WriteLine(q3.ToQueryString());
        }

Edit 1 I did some research about Expression itself, and I tried to reverse Expressions with Func content from methods above, here is a result:

    public static class ExpectedCodeExtensions
    {
        public static Expression<Func<Bar, BarDto>> BarToDtoExpr = x => new BarDto(x.Something, x.Something2); 
        public static Expression<Func<Foo, FooDto>> FooToDtoExpr = x => new FooDto(x.Something, x.Bars.AsQueryable().Select(BarToDtoExpr).ToList());
        public static Expression<Func<Foo, FooTestDto>> FooToTestDtoExpr = x => new FooTestDto(x.Something, BarToDtoProjection.Invoke(x.TestBar));

        public static Func<Bar, BarDto> BarToDtoProjection = BarToDtoExpr.Compile();
        public static Func<Foo, FooDto> FooToDtoProjection = FooToDtoExpr.Compile();
        public static Func<Foo, FooTestDto> FooToTestDtoProjection = FooToTestDtoExpr.Compile();

        public static Expression<Func<Foo, FooDto>> ManuallyTypedQuery = x => x == null ? null : new FooDto(x.Something, x.Bars.Select(y => new BarDto(y.Something, y.Something2)).ToList());
    }

And, interesting (for me) thing happened -> FooToDtoExpr and ManuallyTypedQuery returned exactly the same query, but I have a problem with FooToTestDtoExpr -> It includes all fields from Foo. I have tried removing BarToDtoProjection.Invoke(x.TestBar) and hard-typed DTO constructor and it worked fine.

The remaining problem is how to parse single model field using Expression BarToDtoExpr, without making query larger, or in other words how to put this:

public static Expression<Func<Bar, BarDto>> BarToDtoExpr = x => new BarDto(x.Something, x.Something2);

There:

public static Expression<Func<Foo, FooTestDto>> FooToTestDtoExpr = x => new FooTestDto(x.Something, /* HERE */);
LambdaTheDev
  • 160
  • 1
  • 9
  • Side note: you can avoid all this by using AutoMapper. – Gert Arnold Apr 15 '22 at 14:36
  • What LINQ are you using: LINQ to Objects / SQL / EF 6.x / EF Core 2.0 / 2.1 / 3.x / 5.x / 6.x? What database provider? Note for example if you are using EF Core 2.0 it will automatically convert to client side processing when it can't convert the expression to SQL (e.g. when you use a `Func<>`) effectively putting a `AsEnumerable()` before that expression. – NetMage Apr 15 '22 at 16:08
  • Consider using [LINQKit](https://github.com/scottksmith95/LINQKit). – NetMage Apr 15 '22 at 16:08
  • @NetMage I use EFCore 5.X, and I’d like to use full potential of EFC (I don’t wanna queries that select whole object, when I need 3-4 fields). – LambdaTheDev Apr 15 '22 at 17:04
  • 1
    @LambdaTheDev From the [documentation](https://learn.microsoft.com/en-us/ef/core/querying/client-eval) even later EF Core supports client side evaluation in the final `Select`, so if you pass in a `Func` (calling `Enumerable.Select` instead of `Queryable.Select`) then you it will be exactly like you did `AsEnumerable().Select`, causing server side evaluation of the query before the final projection only. You must use `Expression` to get SQL translation of LINQ methods. – NetMage Apr 15 '22 at 19:26
  • Your `FooToTextDtoExpr` creates an `Expression` tree with a method call to `BarToDtoProjection`, which is compiled code, which is not translatable to SQL (EF Core doesn't decompile machine code to figure out what you are doing). LINQKit really is your best bet, unless you want to roll your own custom expansion code for `Invoke` - see [my answer](https://stackoverflow.com/a/49418695/2557128) here. – NetMage Apr 15 '22 at 19:33
  • @NetMage How to do it using LINQKit? Library seems kinda complex. – LambdaTheDev Apr 16 '22 at 06:45
  • See [this documentation](https://github.com/scottksmith95/LINQKit#combining-expressions) on combining expressions. – NetMage Apr 18 '22 at 17:51

0 Answers0