From db06c62f76ce13dcbd6e5cba230d93dfac0b7c13 Mon Sep 17 00:00:00 2001 From: Yevhen Cherkes Date: Mon, 24 Aug 2026 10:47:10 +0200 Subject: [PATCH] Add source-only projection factory with inferred result type --- README.md | 11 ++++++++ .../Projections/ProjectionFactory.cs | 22 ++++++++++++++++ .../Raffinert.Expressions.csproj | 2 +- .../EfCoreExpressionTests.cs | 7 +++--- .../ProjectionTests.cs | 25 +++++++++++++++++++ 5 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 src/Raffinert.Expressions/Projections/ProjectionFactory.cs diff --git a/README.md b/README.md index 5b2ac0c..9aa85dd 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,17 @@ bool matches = wanted.Invoke(product); ## Projections +When the result is an anonymous type, specify only the source type; the result type is inferred from the +lambda: + +```csharp +var summary = Projection.Create(product => new +{ + product.Name, + Total = product.Price * 2 +}); +``` + ```csharp var summary = Projection.Create(product => new ProductSummary { diff --git a/src/Raffinert.Expressions/Projections/ProjectionFactory.cs b/src/Raffinert.Expressions/Projections/ProjectionFactory.cs new file mode 100644 index 0000000..626b031 --- /dev/null +++ b/src/Raffinert.Expressions/Projections/ProjectionFactory.cs @@ -0,0 +1,22 @@ +using System.Linq.Expressions; + +namespace Raffinert.Expressions; + +/// Provides projection creation with a source type and an inferred result type. +/// The type of value supplied to the projection. +/// +/// This factory is useful when the result is an anonymous type. The result type is inferred from the +/// expression, so only the source type needs to be written: +/// Projection<Order>.Create(order => new { order.Id }). +/// +public static class Projection +{ + /// Creates a projection whose result type is inferred from the expression. + /// The inferred type produced by the projection. + /// The transformation expression represented by the projection. + /// A projection that represents . + /// is null. + public static Projection Create( + Expression> expression) => + Projection.Create(expression); +} diff --git a/src/Raffinert.Expressions/Raffinert.Expressions.csproj b/src/Raffinert.Expressions/Raffinert.Expressions.csproj index ab531a8..3485f57 100644 --- a/src/Raffinert.Expressions/Raffinert.Expressions.csproj +++ b/src/Raffinert.Expressions/Raffinert.Expressions.csproj @@ -3,7 +3,7 @@ netstandard2.0 Raffinert.Expressions Raffinert.Expressions - 1.0.0 + 1.0.1 true true true diff --git a/tests/Raffinert.Expressions.IntegrationTests/EfCoreExpressionTests.cs b/tests/Raffinert.Expressions.IntegrationTests/EfCoreExpressionTests.cs index 7bd5240..5fd6271 100644 --- a/tests/Raffinert.Expressions.IntegrationTests/EfCoreExpressionTests.cs +++ b/tests/Raffinert.Expressions.IntegrationTests/EfCoreExpressionTests.cs @@ -193,11 +193,12 @@ public async Task StructuralAdaptationTranslates() var adaptedCondition = condition.AdaptSource(); var adaptedProjection = projection.Adapt(); - var rows = await _db.Products + var query = _db.Products .Where(adaptedCondition) .Select(adaptedProjection) - .OrderBy(product => product.Name) - .ToArrayAsync(); + .OrderBy(product => product.Name); + + var rows = await query.ToListAsync(); Assert.Equal( "product => (product.PriceCents > 1000) && (product.Name != \"Hidden\")", diff --git a/tests/Raffinert.Expressions.UnitTests/ProjectionTests.cs b/tests/Raffinert.Expressions.UnitTests/ProjectionTests.cs index 5097aba..1ac7033 100644 --- a/tests/Raffinert.Expressions.UnitTests/ProjectionTests.cs +++ b/tests/Raffinert.Expressions.UnitTests/ProjectionTests.cs @@ -20,6 +20,31 @@ public void InlineSubclassEnumerableAndQueryableProjectionWork() Assert.Equal("Book", products.AsQueryable().Select(inline).Single()); } + [Fact] + public void SourceOnlyFactoryInfersAnonymousResultType() + { + var projection = Projection.Create(product => new + { + product.Name, + Total = product.Price * 2 + }); + + var result = projection.Invoke(new Product { Name = "Book", Price = 12m }); + + Assert.Equal("Book", result.Name); + Assert.Equal(24m, result.Total); + Assert.Equal( + """ + product => new + { + Name = product.Name, + Total = product.Price * 2m + } + """, + projection.GetExpandedExpression().ToReadableString(), + ignoreLineEndingDifferences: true); + } + [Fact] public void NullSafeNestedProjectionUsesConditionalAndDefault() {