Skip to content

Commit 3da29b2

Browse files
committed
Add asynchronous hooks and rename the query projection API
* IQueryProjector replaces IQueryableMapper, the hooks can await other queries to join them * A projection must return one row per entity, the total count and the paging come before it * Pass the ambient cancellation token and detect the missing entity for value type DTOs
1 parent d6876b8 commit 3da29b2

29 files changed

Lines changed: 573 additions & 51 deletions

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

Lines changed: 64 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
using System.Collections.Generic;
33
using System.Linq;
44
using System.Linq.Dynamic.Core;
5+
using System.Threading;
56
using System.Threading.Tasks;
67
using Volo.Abp.Application.Dtos;
78
using Volo.Abp.Auditing;
89
using Volo.Abp.Domain.Entities;
910
using Volo.Abp.Domain.Repositories;
1011
using Volo.Abp.ObjectMapping;
12+
using Volo.Abp.Threading;
1113

1214
namespace Volo.Abp.Application.Services;
1315

@@ -45,20 +47,18 @@ public abstract class AbstractKeyReadOnlyAppService<TEntity, TGetOutputDto, TGet
4547
protected virtual string? GetListPolicyName { get; set; }
4648

4749
/// <summary>
48-
/// Used by the <see cref="GetAsync"/> to project the query to the <typeparamref name="TGetOutputDto"/>.
49-
/// It returns the registered mapper or null by default.
50-
/// The <see cref="GetEntityByIdAsync"/> and the <see cref="MapToGetOutputDtoAsync"/> are not used when it returns a mapper.
50+
/// Used by the <see cref="CreateGetOutputDtoQueryOrNullAsync"/> to project the query to the <typeparamref name="TGetOutputDto"/>.
51+
/// The <see cref="GetEntityByIdAsync"/> and the <see cref="MapToGetOutputDtoAsync"/> are not used while the query is projected.
5152
/// </summary>
52-
protected virtual IQueryableMapper<TEntity, TGetOutputDto>? GetQueryableMapper
53-
=> LazyServiceProvider.LazyGetService<IQueryableMapper<TEntity, TGetOutputDto>>();
53+
protected virtual IQueryProjector<TEntity, TGetOutputDto>? GetOutputDtoQueryProjector
54+
=> LazyServiceProvider.LazyGetService<IQueryProjector<TEntity, TGetOutputDto>>();
5455

5556
/// <summary>
56-
/// Used by the <see cref="GetListAsync"/> to project the query to the <typeparamref name="TGetListOutputDto"/>.
57-
/// It returns the registered mapper or null by default.
58-
/// The <see cref="MapToGetListOutputDtosAsync"/> is not used when it returns a mapper.
57+
/// Used by the <see cref="CreateGetListOutputDtoQueryOrNullAsync"/> to project the query to the <typeparamref name="TGetListOutputDto"/>.
58+
/// The <see cref="MapToGetListOutputDtosAsync"/> is not used while the query is projected.
5959
/// </summary>
60-
protected virtual IQueryableMapper<TEntity, TGetListOutputDto>? GetListQueryableMapper
61-
=> LazyServiceProvider.LazyGetService<IQueryableMapper<TEntity, TGetListOutputDto>>();
60+
protected virtual IQueryProjector<TEntity, TGetListOutputDto>? GetListOutputDtoQueryProjector
61+
=> LazyServiceProvider.LazyGetService<IQueryProjector<TEntity, TGetListOutputDto>>();
6262

6363
protected AbstractKeyReadOnlyAppService(IReadOnlyRepository<TEntity> repository)
6464
{
@@ -69,15 +69,17 @@ public virtual async Task<TGetOutputDto> GetAsync(TKey id)
6969
{
7070
await CheckGetPolicyAsync();
7171

72-
var projectionMapper = GetQueryableMapper;
73-
if (projectionMapper != null)
72+
var dtoQuery = await CreateGetOutputDtoQueryOrNullAsync(id);
73+
if (dtoQuery != null)
7474
{
75-
var query = await CreateEntityQueryAsync(id);
76-
if (query != null)
75+
//TGetOutputDto has no class constraint, so a default value can not be used to detect the missing entity
76+
var dtos = await AsyncExecuter.ToListAsync(dtoQuery.Take(1), GetCancellationToken());
77+
if (dtos.Count == 0)
7778
{
78-
return await AsyncExecuter.FirstOrDefaultAsync(projectionMapper.ProjectTo(query))
79-
?? throw new EntityNotFoundException<TEntity>(id);
79+
throw new EntityNotFoundException<TEntity>(id);
8080
}
81+
82+
return dtos[0];
8183
}
8284

8385
var entity = await GetEntityByIdAsync(id);
@@ -90,7 +92,7 @@ public virtual async Task<PagedResultDto<TGetListOutputDto>> GetListAsync(TGetLi
9092
await CheckGetListPolicyAsync();
9193

9294
var query = await CreateFilteredQueryAsync(input);
93-
var totalCount = await AsyncExecuter.CountAsync(query);
95+
var totalCount = await AsyncExecuter.CountAsync(query, GetCancellationToken());
9496

9597
var entityDtos = new List<TGetListOutputDto>();
9698

@@ -99,14 +101,14 @@ public virtual async Task<PagedResultDto<TGetListOutputDto>> GetListAsync(TGetLi
99101
query = ApplySorting(query, input);
100102
query = ApplyPaging(query, input);
101103

102-
var projectionMapper = GetListQueryableMapper;
103-
if (projectionMapper != null)
104+
var dtoQuery = await CreateGetListOutputDtoQueryOrNullAsync(query);
105+
if (dtoQuery != null)
104106
{
105-
entityDtos = await AsyncExecuter.ToListAsync(projectionMapper.ProjectTo(query));
107+
entityDtos = await AsyncExecuter.ToListAsync(dtoQuery, GetCancellationToken());
106108
}
107109
else
108110
{
109-
var entities = await AsyncExecuter.ToListAsync(query);
111+
var entities = await AsyncExecuter.ToListAsync(query, GetCancellationToken());
110112
entityDtos = await MapToGetListOutputDtosAsync(entities);
111113
}
112114
}
@@ -119,16 +121,55 @@ public virtual async Task<PagedResultDto<TGetListOutputDto>> GetListAsync(TGetLi
119121

120122
protected abstract Task<TEntity> GetEntityByIdAsync(TKey id);
121123

124+
protected virtual CancellationToken GetCancellationToken(CancellationToken preferredValue = default)
125+
{
126+
return CancellationTokenProvider.FallbackToProvider(preferredValue);
127+
}
128+
122129
/// <summary>
123130
/// Should create a query that selects the entity with the given <paramref name="id"/>.
124-
/// It returns null by default, then the <see cref="GetEntityByIdAsync"/> is used instead of the projection.
131+
/// It returns null by default, then the entity is not projected.
125132
/// </summary>
126133
/// <param name="id">The id of the entity.</param>
127-
protected virtual Task<IQueryable<TEntity>?> CreateEntityQueryAsync(TKey id)
134+
protected virtual Task<IQueryable<TEntity>?> CreateEntityQueryOrNullAsync(TKey id)
128135
{
129136
return Task.FromResult<IQueryable<TEntity>?>(null);
130137
}
131138

139+
/// <summary>
140+
/// Projects the query of the entity with the given <paramref name="id"/> to the <typeparamref name="TGetOutputDto"/>.
141+
/// It uses the <see cref="GetOutputDtoQueryProjector"/> and the <see cref="CreateEntityQueryOrNullAsync"/> by default,
142+
/// and the <see cref="GetEntityByIdAsync"/> is used when it returns null.
143+
/// Override it to await other queries, like the query of another aggregate root to join.
144+
/// </summary>
145+
/// <param name="id">The id of the entity.</param>
146+
protected virtual async Task<IQueryable<TGetOutputDto>?> CreateGetOutputDtoQueryOrNullAsync(TKey id)
147+
{
148+
var queryProjector = GetOutputDtoQueryProjector;
149+
if (queryProjector == null)
150+
{
151+
return null;
152+
}
153+
154+
var query = await CreateEntityQueryOrNullAsync(id);
155+
156+
return query == null ? null : queryProjector.ProjectTo(query);
157+
}
158+
159+
/// <summary>
160+
/// Projects the given entity query to the <typeparamref name="TGetListOutputDto"/>.
161+
/// It uses the <see cref="GetListOutputDtoQueryProjector"/> by default,
162+
/// and the <see cref="MapToGetListOutputDtosAsync"/> is used when it returns null.
163+
/// Override it to await other queries, like the query of another aggregate root to join.
164+
/// The projection must return one row per entity: the total count is already calculated and the paging is
165+
/// already applied, so adding or removing rows makes the page inconsistent with the total count.
166+
/// </summary>
167+
/// <param name="query">The sorted and paged entity query.</param>
168+
protected virtual Task<IQueryable<TGetListOutputDto>?> CreateGetListOutputDtoQueryOrNullAsync(IQueryable<TEntity> query)
169+
{
170+
return Task.FromResult(GetListOutputDtoQueryProjector?.ProjectTo(query));
171+
}
172+
132173
protected virtual async Task CheckGetPolicyAsync()
133174
{
134175
await CheckPolicyAsync(GetPolicyName);

framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/ApplicationService.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using Volo.Abp.MultiTenancy;
2020
using Volo.Abp.ObjectMapping;
2121
using Volo.Abp.Settings;
22+
using Volo.Abp.Threading;
2223
using Volo.Abp.Timing;
2324
using Volo.Abp.Uow;
2425
using Volo.Abp.Users;
@@ -48,6 +49,8 @@ public abstract class ApplicationService :
4849

4950
protected IAsyncQueryableExecuter AsyncExecuter => LazyServiceProvider.LazyGetRequiredService<IAsyncQueryableExecuter>();
5051

52+
protected ICancellationTokenProvider CancellationTokenProvider => LazyServiceProvider.LazyGetService<ICancellationTokenProvider>(NullCancellationTokenProvider.Instance);
53+
5154
protected Type? ObjectMapperContext { get; set; }
5255
protected IObjectMapper ObjectMapper => LazyServiceProvider.LazyGetService<IObjectMapper>(provider =>
5356
ObjectMapperContext == null

framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/CrudAppService.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,11 @@ protected override async Task<TEntity> GetEntityByIdAsync(TKey id)
8484
return await Repository.GetAsync(id);
8585
}
8686

87-
protected override async Task<IQueryable<TEntity>?> CreateEntityQueryAsync(TKey id)
87+
protected override async Task<IQueryable<TEntity>?> CreateEntityQueryOrNullAsync(TKey id)
8888
{
8989
var query = await Repository.GetQueryableAsync();
9090

91-
return query.Where(EntityHelper.CreateEqualityExpressionForId<TEntity, TKey>(id));
91+
return query.Where(e => e.Id!.Equals(id));
9292
}
9393

9494
protected override void MapToEntity(TUpdateInput updateInput, TEntity entity)

framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/ReadOnlyAppService.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,11 @@ protected override async Task<TEntity> GetEntityByIdAsync(TKey id)
4747
return await Repository.GetAsync(id);
4848
}
4949

50-
protected override async Task<IQueryable<TEntity>?> CreateEntityQueryAsync(TKey id)
50+
protected override async Task<IQueryable<TEntity>?> CreateEntityQueryOrNullAsync(TKey id)
5151
{
5252
var query = await Repository.GetQueryableAsync();
5353

54-
return query.Where(EntityHelper.CreateEqualityExpressionForId<TEntity, TKey>(id));
54+
return query.Where(e => e.Id!.Equals(id));
5555
}
5656

5757
protected override IQueryable<TEntity> ApplyDefaultSorting(IQueryable<TEntity> query)

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

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using Microsoft.Extensions.DependencyInjection;
1+
using System.Collections.Generic;
2+
using Microsoft.Extensions.DependencyInjection;
23
using Volo.Abp.DependencyInjection;
34
using Volo.Abp.Modularity;
45
using Volo.Abp.Reflection;
@@ -19,13 +20,14 @@ public override void PreConfigureServices(ServiceConfigurationContext context)
1920
).ConvertAll(t => new ServiceIdentifier(t))
2021
);
2122

22-
//Register types for IQueryableMapper<TSource, TDestination> if implements
23-
onServiceExposingContext.ExposedTypes.AddRange(
24-
ReflectionHelper.GetImplementedGenericTypes(
25-
onServiceExposingContext.ImplementationType,
26-
typeof(IQueryableMapper<,>)
27-
).ConvertAll(t => new ServiceIdentifier(t))
28-
);
23+
//Register types for IQueryProjector<TSource, TDestination> if implements
24+
//The class name convention may have already exposed them, so they are not added twice
25+
foreach (var serviceType in ReflectionHelper.GetImplementedGenericTypes(
26+
onServiceExposingContext.ImplementationType,
27+
typeof(IQueryProjector<,>)))
28+
{
29+
onServiceExposingContext.ExposedTypes.AddIfNotContains(new ServiceIdentifier(serviceType));
30+
}
2931
});
3032
}
3133

framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/IQueryableMapper.cs renamed to framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/IQueryProjector.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@ namespace Volo.Abp.ObjectMapping;
77
/// Maps a query to another.
88
/// Implement this interface to project a query on the data store side, instead of loading the
99
/// source objects into the memory and mapping them one by one.
10+
/// Implement it once for a source and destination pair, the last registered one is used otherwise.
1011
/// </summary>
1112
/// <typeparam name="TSource">Type of the source objects</typeparam>
1213
/// <typeparam name="TDestination">Type of the destination objects</typeparam>
13-
public interface IQueryableMapper<TSource, TDestination> : ITransientDependency
14+
public interface IQueryProjector<TSource, TDestination> : ITransientDependency
1415
{
1516
/// <summary>
16-
/// Projects the given query. The returned query must be built on top of it, otherwise the
17-
/// query provider can not translate the projection.
17+
/// Projects the given query. The returned query must be built on top of it and must keep its order,
18+
/// with a single destination object for each source object, using expressions the query provider can
19+
/// translate. The caller may have already sorted, paged or counted the source query.
1820
/// </summary>
1921
/// <param name="source">The query to project</param>
2022
IQueryable<TDestination> ProjectTo(IQueryable<TSource> source);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#nullable enable
2+
using System;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using Volo.Abp.Domain.Repositories;
6+
7+
namespace Volo.Abp.Application.Services.QueryProjection;
8+
9+
public class BookAbstractKeyProjectingAppService : AbstractKeyReadOnlyAppService<Book, BookDto, Guid>
10+
{
11+
public BookAbstractKeyProjectingAppService(IReadOnlyRepository<Book> repository)
12+
: base(repository)
13+
{
14+
15+
}
16+
17+
protected override async Task<Book> GetEntityByIdAsync(Guid id)
18+
{
19+
var query = await ReadOnlyRepository.GetQueryableAsync();
20+
21+
return await AsyncExecuter.FirstAsync(query, book => book.Id == id);
22+
}
23+
24+
protected override async Task<IQueryable<Book>?> CreateEntityQueryOrNullAsync(Guid id)
25+
{
26+
var query = await ReadOnlyRepository.GetQueryableAsync();
27+
28+
return query.Where(book => book.Id == id);
29+
}
30+
31+
protected override IQueryable<Book> ApplyDefaultSorting(IQueryable<Book> query)
32+
{
33+
return query.OrderBy(book => book.Id);
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#nullable enable
2+
using System;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using Volo.Abp.Application.Dtos;
6+
using Volo.Abp.Domain.Repositories;
7+
8+
namespace Volo.Abp.Application.Services.QueryProjection;
9+
10+
public class BookAsyncProjectionAppService : CrudAppService<Book, BookDto, Guid>
11+
{
12+
public const string Marker = "-async";
13+
14+
private readonly IBookNameSuffixProvider _suffixProvider;
15+
16+
public BookAsyncProjectionAppService(
17+
IRepository<Book, Guid> repository,
18+
IBookNameSuffixProvider suffixProvider)
19+
: base(repository)
20+
{
21+
_suffixProvider = suffixProvider;
22+
}
23+
24+
protected override async Task<IQueryable<BookDto>?> CreateGetOutputDtoQueryOrNullAsync(Guid id)
25+
{
26+
var query = await Repository.GetQueryableAsync();
27+
28+
return await ProjectAsync(query.Where(book => book.Id == id));
29+
}
30+
31+
protected override async Task<IQueryable<BookDto>?> CreateGetListOutputDtoQueryOrNullAsync(IQueryable<Book> query)
32+
{
33+
return await ProjectAsync(query);
34+
}
35+
36+
private async Task<IQueryable<BookDto>> ProjectAsync(IQueryable<Book> query)
37+
{
38+
var suffix = await _suffixProvider.GetAsync();
39+
40+
return query.Select(book => new BookDto
41+
{
42+
Id = book.Id,
43+
Name = book.Name + suffix
44+
});
45+
}
46+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System;
2+
using Volo.Abp.Application.Dtos;
3+
using Volo.Abp.Domain.Repositories;
4+
5+
namespace Volo.Abp.Application.Services.QueryProjection;
6+
7+
public class BookDetailAppService :
8+
ReadOnlyAppService<Book, BookDetailDto, BookDto, Guid, PagedAndSortedResultRequestDto>
9+
{
10+
public BookDetailAppService(IReadOnlyRepository<Book, Guid> repository)
11+
: base(repository)
12+
{
13+
14+
}
15+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using System;
2+
using Volo.Abp.Application.Dtos;
3+
4+
namespace Volo.Abp.Application.Services.QueryProjection;
5+
6+
public class BookDetailDto : EntityDto<Guid>
7+
{
8+
public string Name { get; set; } = default!;
9+
}

0 commit comments

Comments
 (0)