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();
var query = await CreateFilteredQueryAsync(input);
var totalCount = await AsyncExecuter.CountAsync(query, GetCancellationToken());
var totalCount = await AsyncExecuter.CountAsync(query);
var entityDtos = new List<TGetListOutputDto>();
@ -104,11 +104,11 @@ public abstract class AbstractKeyReadOnlyAppService<TEntity, TGetOutputDto, TGet
var dtoQuery = await CreateGetListOutputDtoQueryOrNullAsync(query);
if (dtoQuery != null)
{
entityDtos = await AsyncExecuter.ToListAsync(dtoQuery, GetCancellationToken());
entityDtos = await AsyncExecuter.ToListAsync(dtoQuery);
}
else
{
var entities = await AsyncExecuter.ToListAsync(query, GetCancellationToken());
var entities = await AsyncExecuter.ToListAsync(query);
entityDtos = await MapToGetListOutputDtosAsync(entities);
}
}
@ -121,9 +121,11 @@ public abstract class AbstractKeyReadOnlyAppService<TEntity, TGetOutputDto, TGet
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>

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.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<IAsyncQueryableExecuter>();
protected ICancellationTokenProvider CancellationTokenProvider => LazyServiceProvider.LazyGetService<ICancellationTokenProvider>(NullCancellationTokenProvider.Instance);
protected Type? ObjectMapperContext { get; set; }
protected IObjectMapper ObjectMapper => LazyServiceProvider.LazyGetService<IObjectMapper>(provider =>
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
//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<,>)))

3
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.
/// </summary>
/// <typeparam name="TSource">Type of the source 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.Collections.Generic;
using System.Threading.Tasks;
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 });
}
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
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<Book, BookDto, Guid>
{
public const string Marker = "-not-projected";
protected override IQueryProjector<Book, BookDto>? GetOutputDtoQueryProjector => 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]
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>();
(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]

3
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<Guid>
{
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
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<ICancellationTokenProvider>();
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)))
{
await Should.ThrowAsync<OperationCanceledException>(async () =>
await appService.GetAsync(TestDataBuilder.UserDouglasId));
}
}
await Should.ThrowAsync<OperationCanceledException>(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<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]
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 appService = GetRequiredService<IPeopleAppService>();
var cityRepository = GetRequiredService<IRepository<City, Guid>>();
using (cancellationTokenProvider.Use(new CancellationToken(canceled: true)))
await WithUnitOfWorkAsync(async () =>
{
await Should.ThrowAsync<OperationCanceledException>(async () =>
await appService.GetListAsync(new PagedAndSortedResultRequestDto()));
}
var london = await cityRepository.GetAsync(TestDataBuilder.LondonCityId);
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