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 */);