Browse Source

Keep the ambient cancellation token out of the entity paths

* Only the projected GetAsync passes it, that is the path Repository.GetAsync already covered
* Assert a single data query so a materialize-then-project implementation can not pass
* Opting out of the projection brings the entity based overrides back
pull/26016/head
maliming 2 weeks ago
parent
commit
be8f078ea9
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 12
      framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbstractKeyReadOnlyAppService.cs
  2. 3
      framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/ApplicationService.cs
  3. 1
      framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/AbpObjectMappingModule.cs
  4. 3
      framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/IQueryProjector.cs
  5. 6
      framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookCustomizedAppService.cs
  6. 14
      framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookWithoutProjectionAppService.cs
  7. 11
      framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/QueryProjection_Tests.cs
  8. 3
      framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/PersonWithCityDto.cs
  9. 41
      framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/QueryProjection_Tests.cs

12
framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbstractKeyReadOnlyAppService.cs

@ -92,7 +92,7 @@ public abstract class AbstractKeyReadOnlyAppService<TEntity, TGetOutputDto, TGet
await CheckGetListPolicyAsync(); await CheckGetListPolicyAsync();
var query = await CreateFilteredQueryAsync(input); var query = await CreateFilteredQueryAsync(input);
var totalCount = await AsyncExecuter.CountAsync(query, GetCancellationToken()); var totalCount = await AsyncExecuter.CountAsync(query);
var entityDtos = new List<TGetListOutputDto>(); var entityDtos = new List<TGetListOutputDto>();
@ -104,11 +104,11 @@ public abstract class AbstractKeyReadOnlyAppService<TEntity, TGetOutputDto, TGet
var dtoQuery = await CreateGetListOutputDtoQueryOrNullAsync(query); var dtoQuery = await CreateGetListOutputDtoQueryOrNullAsync(query);
if (dtoQuery != null) if (dtoQuery != null)
{ {
entityDtos = await AsyncExecuter.ToListAsync(dtoQuery, GetCancellationToken()); entityDtos = await AsyncExecuter.ToListAsync(dtoQuery);
} }
else else
{ {
var entities = await AsyncExecuter.ToListAsync(query, GetCancellationToken()); var entities = await AsyncExecuter.ToListAsync(query);
entityDtos = await MapToGetListOutputDtosAsync(entities); entityDtos = await MapToGetListOutputDtosAsync(entities);
} }
} }
@ -121,9 +121,11 @@ public abstract class AbstractKeyReadOnlyAppService<TEntity, TGetOutputDto, TGet
protected abstract Task<TEntity> GetEntityByIdAsync(TKey id); protected abstract Task<TEntity> GetEntityByIdAsync(TKey id);
protected virtual CancellationToken GetCancellationToken(CancellationToken preferredValue = default) private CancellationToken GetCancellationToken()
{ {
return CancellationTokenProvider.FallbackToProvider(preferredValue); return LazyServiceProvider
.LazyGetService<ICancellationTokenProvider>(NullCancellationTokenProvider.Instance)
.FallbackToProvider();
} }
/// <summary> /// <summary>

3
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.MultiTenancy;
using Volo.Abp.ObjectMapping; using Volo.Abp.ObjectMapping;
using Volo.Abp.Settings; using Volo.Abp.Settings;
using Volo.Abp.Threading;
using Volo.Abp.Timing; using Volo.Abp.Timing;
using Volo.Abp.Uow; using Volo.Abp.Uow;
using Volo.Abp.Users; using Volo.Abp.Users;
@ -49,8 +48,6 @@ public abstract class ApplicationService :
protected IAsyncQueryableExecuter AsyncExecuter => LazyServiceProvider.LazyGetRequiredService<IAsyncQueryableExecuter>(); protected IAsyncQueryableExecuter AsyncExecuter => LazyServiceProvider.LazyGetRequiredService<IAsyncQueryableExecuter>();
protected ICancellationTokenProvider CancellationTokenProvider => LazyServiceProvider.LazyGetService<ICancellationTokenProvider>(NullCancellationTokenProvider.Instance);
protected Type? ObjectMapperContext { get; set; } protected Type? ObjectMapperContext { get; set; }
protected IObjectMapper ObjectMapper => LazyServiceProvider.LazyGetService<IObjectMapper>(provider => protected IObjectMapper ObjectMapper => LazyServiceProvider.LazyGetService<IObjectMapper>(provider =>
ObjectMapperContext == null ObjectMapperContext == null

1
framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/AbpObjectMappingModule.cs

@ -21,7 +21,6 @@ public class AbpObjectMappingModule : AbpModule
); );
//Register types for IQueryProjector<TSource, TDestination> if implements //Register types for IQueryProjector<TSource, TDestination> if implements
//The class name convention may have already exposed them, so they are not added twice
foreach (var serviceType in ReflectionHelper.GetImplementedGenericTypes( foreach (var serviceType in ReflectionHelper.GetImplementedGenericTypes(
onServiceExposingContext.ImplementationType, onServiceExposingContext.ImplementationType,
typeof(IQueryProjector<,>))) typeof(IQueryProjector<,>)))

3
framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/IQueryProjector.cs

@ -7,7 +7,8 @@ namespace Volo.Abp.ObjectMapping;
/// Maps a query to another. /// Maps a query to another.
/// Implement this interface to project a query on the data store side, instead of loading the /// 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. /// 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.
/// </summary> /// </summary>
/// <typeparam name="TSource">Type of the source objects</typeparam> /// <typeparam name="TSource">Type of the source objects</typeparam>
/// <typeparam name="TDestination">Type of the destination objects</typeparam> /// <typeparam name="TDestination">Type of the destination objects</typeparam>

6
framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookCustomizedAppService.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
@ -25,4 +26,9 @@ public class BookCustomizedAppService : CrudAppService<Book, BookDto, Guid>
{ {
return Task.FromResult(new BookDto { Id = entity.Id, Name = entity.Name + Marker }); return Task.FromResult(new BookDto { Id = entity.Id, Name = entity.Name + Marker });
} }
protected override Task<List<BookDto>> MapToGetListOutputDtosAsync(List<Book> entities)
{
return Task.FromResult(entities.ConvertAll(entity => new BookDto { Id = entity.Id, Name = entity.Name + Marker }));
}
} }

14
framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookWithoutProjectionAppService.cs

@ -1,5 +1,7 @@
#nullable enable #nullable enable
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
using Volo.Abp.ObjectMapping; using Volo.Abp.ObjectMapping;
@ -7,6 +9,8 @@ namespace Volo.Abp.Application.Services.QueryProjection;
public class BookWithoutProjectionAppService : CrudAppService<Book, BookDto, Guid> public class BookWithoutProjectionAppService : CrudAppService<Book, BookDto, Guid>
{ {
public const string Marker = "-not-projected";
protected override IQueryProjector<Book, BookDto>? GetOutputDtoQueryProjector => null; protected override IQueryProjector<Book, BookDto>? GetOutputDtoQueryProjector => null;
protected override IQueryProjector<Book, BookDto>? GetListOutputDtoQueryProjector => null; protected override IQueryProjector<Book, BookDto>? GetListOutputDtoQueryProjector => null;
@ -16,4 +20,14 @@ public class BookWithoutProjectionAppService : CrudAppService<Book, BookDto, Gui
{ {
} }
protected override Task<BookDto> MapToGetOutputDtoAsync(Book entity)
{
return Task.FromResult(new BookDto { Id = entity.Id, Name = entity.Name + Marker });
}
protected override Task<List<BookDto>> MapToGetListOutputDtosAsync(List<Book> entities)
{
return Task.FromResult(entities.ConvertAll(entity => new BookDto { Id = entity.Id, Name = entity.Name + Marker }));
}
} }

11
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] [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<BookWithoutProjectionAppService>(); var appService = GetRequiredService<BookWithoutProjectionAppService>();
(await appService.GetAsync(_bookId)).Name.ShouldEndWith(BookObjectMapper.Marker); (await appService.GetAsync(_bookId)).Name.ShouldEndWith(BookWithoutProjectionAppService.Marker);
(await appService.GetListAsync(new PagedAndSortedResultRequestDto())) (await appService.GetListAsync(new PagedAndSortedResultRequestDto()))
.Items[0].Name.ShouldEndWith(BookObjectMapper.Marker); .Items[0].Name.ShouldEndWith(BookWithoutProjectionAppService.Marker);
} }
[Fact] [Fact]
@ -84,6 +84,11 @@ public class QueryProjection_Tests : AbpDddApplicationTestBase
dto.Name.ShouldEndWith(BookProjector.Marker); dto.Name.ShouldEndWith(BookProjector.Marker);
dto.Name.ShouldNotContain(BookCustomizedAppService.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] [Fact]

3
framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/PersonWithCityDto.cs

@ -1,3 +1,4 @@
#nullable enable
using System; using System;
using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Dtos;
@ -7,5 +8,5 @@ public class PersonWithCityDto : EntityDto<Guid>
{ {
public string Name { get; set; } public string Name { get; set; }
public string CityName { get; set; } public string? CityName { get; set; }
} }

41
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 //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.ShouldContain("\"Name\"");
select.ShouldNotContain("\"Birthday\""); select.ShouldNotContain("\"Birthday\"");
select.ShouldNotContain("\"ExtraProperties\""); select.ShouldNotContain("\"ExtraProperties\"");
@ -151,26 +152,46 @@ public class QueryProjection_Tests : EntityFrameworkCoreTestBase
var cancellationTokenProvider = GetRequiredService<ICancellationTokenProvider>(); var cancellationTokenProvider = GetRequiredService<ICancellationTokenProvider>();
var appService = GetRequiredService<PersonProjectionAppService>(); var appService = GetRequiredService<PersonProjectionAppService>();
//the entity path passes it through Repository.GetAsync, so the projected path must not regress
using (cancellationTokenProvider.Use(new CancellationToken(canceled: true))) using (cancellationTokenProvider.Use(new CancellationToken(canceled: true)))
{ {
await Should.ThrowAsync<OperationCanceledException>(async () => await Should.ThrowAsync<OperationCanceledException>(async () =>
await appService.GetAsync(TestDataBuilder.UserDouglasId)); await appService.GetAsync(TestDataBuilder.UserDouglasId));
}
}
await Should.ThrowAsync<OperationCanceledException>(async () => [Fact]
await appService.GetListAsync(new PagedAndSortedResultRequestDto())); public async Task Should_Execute_The_Projected_Query_From_The_Application_Service_For_A_Single_Entity()
{
System.Collections.Concurrent.ConcurrentQueue<string> commands;
using (SqlCommandCapture.Begin(out commands))
{
await GetRequiredService<PersonProjectionAppService>().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] [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<ICancellationTokenProvider>(); var cityRepository = GetRequiredService<IRepository<City, Guid>>();
var appService = GetRequiredService<IPeopleAppService>();
using (cancellationTokenProvider.Use(new CancellationToken(canceled: true))) await WithUnitOfWorkAsync(async () =>
{ {
await Should.ThrowAsync<OperationCanceledException>(async () => var london = await cityRepository.GetAsync(TestDataBuilder.LondonCityId);
await appService.GetListAsync(new PagedAndSortedResultRequestDto())); await cityRepository.DeleteAsync(london, autoSave: true);
} });
var result = await GetRequiredService<PersonWithCityAppService>()
.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);
} }
} }

Loading…
Cancel
Save