From f00d7df56cbb3c516373fa4ae615604888e67b76 Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 20 Aug 2026 10:53:53 +0800 Subject: [PATCH] Assert the projected SQL and fix the Mapperly sample in the docs * RequiredMappingStrategy.Target avoids an RMG020 warning per unmapped entity property --- .../application-services.md | 4 +++- .../Applications/QueryProjection_Tests.cs | 22 +++++++++++++++++++ .../SampleClasses/MyEntityQueryProjector.cs | 2 +- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/docs/en/framework/architecture/domain-driven-design/application-services.md b/docs/en/framework/architecture/domain-driven-design/application-services.md index 74ae2adf50..d1c0186f6d 100644 --- a/docs/en/framework/architecture/domain-driven-design/application-services.md +++ b/docs/en/framework/architecture/domain-driven-design/application-services.md @@ -485,13 +485,15 @@ public class BookProjector : IQueryProjectionMapper [Mapperly](https://mapperly.riok.app/) can generate that method for you: ````csharp -[Mapper] +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] public partial class BookProjector : IQueryProjectionMapper { public partial IQueryable ProjectTo(IQueryable source); } ```` +> `RequiredMappingStrategy.Target` tells Mapperly to only require the DTO members to be mapped. Without it, it reports a warning for every entity property that the DTO doesn't have. + ABP registers the projection mappers by convention, you don't need to configure anything else. Filters (like soft delete and multi-tenancy), sorting and paging are still applied to the query before the projection. > The projection replaces the entity based extension points. `GetAsync` doesn't use `GetEntityByIdAsync` and `MapToGetOutputDtoAsync`, `GetListAsync` doesn't use `MapToGetListOutputDtosAsync` anymore. If an application service needs to keep using them, override the `GetProjectionMapper` or `GetListProjectionMapper` property and return `null`: diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/QueryProjection_Tests.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/QueryProjection_Tests.cs index 832a1502e4..c833a2515a 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/QueryProjection_Tests.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/QueryProjection_Tests.cs @@ -1,9 +1,12 @@ using System; +using System.Linq; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; using Shouldly; using Volo.Abp.Application.Dtos; using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Repositories; +using Volo.Abp.ObjectMapping; using Volo.Abp.TestApp; using Volo.Abp.TestApp.Domain; using Xunit; @@ -63,4 +66,23 @@ public class QueryProjection_Tests : EntityFrameworkCoreTestBase result.Items.ShouldNotContain(x => x.Id == TestDataBuilder.UserJohnDeletedId); } + + [Fact] + public async Task Should_Only_Select_The_Projected_Columns() + { + await WithUnitOfWorkAsync(async () => + { + var repository = GetRequiredService>(); + var projector = GetRequiredService>(); + + var sql = projector.ProjectTo(await repository.GetQueryableAsync()).ToQueryString(); + + sql.ShouldContain("\"Name\""); + sql.ShouldNotContain("\"Birthday\""); + sql.ShouldNotContain("\"ExtraProperties\""); + + //the data filters are still a part of the query + sql.ShouldContain("Is_Deleted"); + }); + } } diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityQueryProjector.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityQueryProjector.cs index 80d984e11b..da57936cf6 100644 --- a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityQueryProjector.cs +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityQueryProjector.cs @@ -4,7 +4,7 @@ using Volo.Abp.ObjectMapping; namespace Volo.Abp.Mapperly.SampleClasses; -[Mapper] +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] public partial class MyEntityQueryProjector : IQueryProjectionMapper { public partial IQueryable ProjectTo(IQueryable source);