22using System . Collections . Generic ;
33using System . Linq ;
44using System . Linq . Dynamic . Core ;
5+ using System . Threading ;
56using System . Threading . Tasks ;
67using Volo . Abp . Application . Dtos ;
78using Volo . Abp . Auditing ;
89using Volo . Abp . Domain . Entities ;
910using Volo . Abp . Domain . Repositories ;
1011using Volo . Abp . ObjectMapping ;
12+ using Volo . Abp . Threading ;
1113
1214namespace 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 ) ;
0 commit comments