diff --git a/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbstractKeyReadOnlyAppService.cs b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbstractKeyReadOnlyAppService.cs index e4980074ba..fa8aafc76a 100644 --- a/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbstractKeyReadOnlyAppService.cs +++ b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbstractKeyReadOnlyAppService.cs @@ -92,7 +92,7 @@ public abstract class AbstractKeyReadOnlyAppService(); @@ -104,11 +104,11 @@ public abstract class AbstractKeyReadOnlyAppService GetEntityByIdAsync(TKey id); - protected virtual CancellationToken GetCancellationToken(CancellationToken preferredValue = default) + private CancellationToken GetCancellationToken() { - return CancellationTokenProvider.FallbackToProvider(preferredValue); + return LazyServiceProvider + .LazyGetService(NullCancellationTokenProvider.Instance) + .FallbackToProvider(); } /// diff --git a/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/ApplicationService.cs b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/ApplicationService.cs index 01e9e81354..4eb285df04 100644 --- a/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/ApplicationService.cs +++ b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/ApplicationService.cs @@ -19,7 +19,6 @@ using Volo.Abp.Localization; using Volo.Abp.MultiTenancy; using Volo.Abp.ObjectMapping; using Volo.Abp.Settings; -using Volo.Abp.Threading; using Volo.Abp.Timing; using Volo.Abp.Uow; using Volo.Abp.Users; @@ -49,8 +48,6 @@ public abstract class ApplicationService : protected IAsyncQueryableExecuter AsyncExecuter => LazyServiceProvider.LazyGetRequiredService(); - protected ICancellationTokenProvider CancellationTokenProvider => LazyServiceProvider.LazyGetService(NullCancellationTokenProvider.Instance); - protected Type? ObjectMapperContext { get; set; } protected IObjectMapper ObjectMapper => LazyServiceProvider.LazyGetService(provider => ObjectMapperContext == null diff --git a/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/AbpObjectMappingModule.cs b/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/AbpObjectMappingModule.cs index 87e0c43b56..2f1b8830ac 100644 --- a/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/AbpObjectMappingModule.cs +++ b/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/AbpObjectMappingModule.cs @@ -21,7 +21,6 @@ public class AbpObjectMappingModule : AbpModule ); //Register types for IQueryProjector if implements - //The class name convention may have already exposed them, so they are not added twice foreach (var serviceType in ReflectionHelper.GetImplementedGenericTypes( onServiceExposingContext.ImplementationType, typeof(IQueryProjector<,>))) diff --git a/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/IQueryProjector.cs b/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/IQueryProjector.cs index 1abf5667e6..89652e5f40 100644 --- a/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/IQueryProjector.cs +++ b/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/IQueryProjector.cs @@ -7,7 +7,8 @@ namespace Volo.Abp.ObjectMapping; /// Maps a query to another. /// Implement this interface to project a query on the data store side, instead of loading the /// source objects into the memory and mapping them one by one. -/// Implement it once for a source and destination pair, the last registered one is used otherwise. +/// Implement it once for a source and destination pair. Use the ReplaceServices option of the +/// DependencyAttribute to replace an existing implementation. /// /// Type of the source objects /// Type of the destination objects diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookCustomizedAppService.cs b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookCustomizedAppService.cs index b4c756d6b2..f5df26baaa 100644 --- a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookCustomizedAppService.cs +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookCustomizedAppService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using Volo.Abp.Domain.Repositories; @@ -25,4 +26,9 @@ public class BookCustomizedAppService : CrudAppService { return Task.FromResult(new BookDto { Id = entity.Id, Name = entity.Name + Marker }); } + + protected override Task> MapToGetListOutputDtosAsync(List entities) + { + return Task.FromResult(entities.ConvertAll(entity => new BookDto { Id = entity.Id, Name = entity.Name + Marker })); + } } diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookWithoutProjectionAppService.cs b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookWithoutProjectionAppService.cs index 9ad4680d45..928c8fee66 100644 --- a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookWithoutProjectionAppService.cs +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookWithoutProjectionAppService.cs @@ -1,5 +1,7 @@ #nullable enable using System; +using System.Collections.Generic; +using System.Threading.Tasks; using Volo.Abp.Domain.Repositories; using Volo.Abp.ObjectMapping; @@ -7,6 +9,8 @@ namespace Volo.Abp.Application.Services.QueryProjection; public class BookWithoutProjectionAppService : CrudAppService { + public const string Marker = "-not-projected"; + protected override IQueryProjector? GetOutputDtoQueryProjector => null; protected override IQueryProjector? GetListOutputDtoQueryProjector => null; @@ -16,4 +20,14 @@ public class BookWithoutProjectionAppService : CrudAppService MapToGetOutputDtoAsync(Book entity) + { + return Task.FromResult(new BookDto { Id = entity.Id, Name = entity.Name + Marker }); + } + + protected override Task> MapToGetListOutputDtosAsync(List entities) + { + return Task.FromResult(entities.ConvertAll(entity => new BookDto { Id = entity.Id, Name = entity.Name + Marker })); + } } diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/QueryProjection_Tests.cs b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/QueryProjection_Tests.cs index 3823a07a56..425021fb03 100644 --- a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/QueryProjection_Tests.cs +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/QueryProjection_Tests.cs @@ -66,13 +66,13 @@ public class QueryProjection_Tests : AbpDddApplicationTestBase } [Fact] - public async Task Should_Use_The_Object_Mapper_If_The_Projection_Was_Disabled() + public async Task Should_Use_The_Entity_Based_Overrides_If_The_Projection_Was_Disabled() { var appService = GetRequiredService(); - (await appService.GetAsync(_bookId)).Name.ShouldEndWith(BookObjectMapper.Marker); + (await appService.GetAsync(_bookId)).Name.ShouldEndWith(BookWithoutProjectionAppService.Marker); (await appService.GetListAsync(new PagedAndSortedResultRequestDto())) - .Items[0].Name.ShouldEndWith(BookObjectMapper.Marker); + .Items[0].Name.ShouldEndWith(BookWithoutProjectionAppService.Marker); } [Fact] @@ -84,6 +84,11 @@ public class QueryProjection_Tests : AbpDddApplicationTestBase dto.Name.ShouldEndWith(BookProjector.Marker); dto.Name.ShouldNotContain(BookCustomizedAppService.Marker); + + var items = (await appService.GetListAsync(new PagedAndSortedResultRequestDto())).Items; + + items[0].Name.ShouldEndWith(BookProjector.Marker); + items[0].Name.ShouldNotContain(BookCustomizedAppService.Marker); } [Fact] diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/PersonWithCityDto.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/PersonWithCityDto.cs index db62825b8e..dac278c7b6 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/PersonWithCityDto.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/PersonWithCityDto.cs @@ -1,3 +1,4 @@ +#nullable enable using System; using Volo.Abp.Application.Dtos; @@ -7,5 +8,5 @@ public class PersonWithCityDto : EntityDto { public string Name { get; set; } - public string CityName { get; set; } + public string? CityName { get; set; } } 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 e2d2f45d7c..d4410c15e7 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 @@ -129,8 +129,9 @@ public class QueryProjection_Tests : EntityFrameworkCoreTestBase } //the application service must run the projection itself, not materialize the entities first - var select = commands.Last(x => x.Contains("FROM \"People\"") && !x.Contains("COUNT")); + var selects = commands.Where(x => x.Contains("FROM \"People\"") && !x.Contains("COUNT")).ToList(); + var select = selects.ShouldHaveSingleItem(); select.ShouldContain("\"Name\""); select.ShouldNotContain("\"Birthday\""); select.ShouldNotContain("\"ExtraProperties\""); @@ -151,26 +152,46 @@ public class QueryProjection_Tests : EntityFrameworkCoreTestBase var cancellationTokenProvider = GetRequiredService(); var appService = GetRequiredService(); + //the entity path passes it through Repository.GetAsync, so the projected path must not regress using (cancellationTokenProvider.Use(new CancellationToken(canceled: true))) { await Should.ThrowAsync(async () => await appService.GetAsync(TestDataBuilder.UserDouglasId)); + } + } - await Should.ThrowAsync(async () => - await appService.GetListAsync(new PagedAndSortedResultRequestDto())); + [Fact] + public async Task Should_Execute_The_Projected_Query_From_The_Application_Service_For_A_Single_Entity() + { + System.Collections.Concurrent.ConcurrentQueue commands; + using (SqlCommandCapture.Begin(out commands)) + { + await GetRequiredService().GetAsync(TestDataBuilder.UserDouglasId); } + + var selects = commands.Where(x => x.Contains("FROM \"People\"")).ToList(); + + var select = selects.ShouldHaveSingleItem(); + select.ShouldContain("\"Name\""); + select.ShouldNotContain("\"Birthday\""); + select.ShouldNotContain("\"ExtraProperties\""); } [Fact] - public async Task Should_Use_The_Ambient_Cancellation_Token_Without_A_Projector() + public async Task Should_Apply_The_Data_Filters_Of_The_Joined_Aggregate() { - var cancellationTokenProvider = GetRequiredService(); - var appService = GetRequiredService(); + var cityRepository = GetRequiredService>(); - using (cancellationTokenProvider.Use(new CancellationToken(canceled: true))) + await WithUnitOfWorkAsync(async () => { - await Should.ThrowAsync(async () => - await appService.GetListAsync(new PagedAndSortedResultRequestDto())); - } + var london = await cityRepository.GetAsync(TestDataBuilder.LondonCityId); + await cityRepository.DeleteAsync(london, autoSave: true); + }); + + var result = await GetRequiredService() + .GetListAsync(new PagedAndSortedResultRequestDto()); + + //the soft deleted city is filtered out, the left join still keeps the person + result.Items.ShouldContain(x => x.Name == "Douglas" && x.CityName == null); } }