Skip to content

Commit d6876b8

Browse files
committed
Rename the query projection API
* IQueryableMapper pairs with IObjectMapper, CreateEntityQueryAsync with CreateFilteredQueryAsync
1 parent e3127db commit d6876b8

16 files changed

Lines changed: 57 additions & 58 deletions

File tree

docs/en/framework/architecture/domain-driven-design/application-services.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ These methods are low level methods that can control how to query entities from
444444
* `ApplyPaging` is used to make paging on the query. If your `TGetListInput` already implements `IPagedResultRequest`, you don't need to override this since the ABP automatically understands it and performs the paging.
445445
* `ApplySorting` is used to sort (order by...) the query. If your `TGetListInput` already implements the `ISortedResultRequest`, ABP automatically sorts the query. If not, it fallbacks to the `ApplyDefaultSorting` which tries to sort by creation time, if your entity implements the standard `IHasCreationTime` interface.
446446
* `GetEntityByIdAsync` is used to get an entity by id, which calls `Repository.GetAsync(id)` by default.
447-
* `GetEntityByIdQueryOrNullAsync` is used to create a query for a single entity by id, which is only needed for the *Query Projection* explained below. It returns `null` if the application service can not create such a query, then `GetEntityByIdAsync` is used.
447+
* `CreateEntityQueryAsync` is used to create a query for a single entity by id, which is only needed for the *Query Projection* explained below. It returns `null` if the application service can not create such a query, then `GetEntityByIdAsync` is used.
448448
* `DeleteByIdAsync` is used to delete an entity by id, which calls `Repository.DeleteAsync(id)` by default.
449449

450450
#### Object to Object Mapping
@@ -461,15 +461,15 @@ These methods are used to convert Entities to DTOs and vice verse. They use the
461461

462462
`GetAsync` and `GetListAsync` get the entities from the database, then map them to DTOs in the memory. If your DTO uses only a few properties of a large entity, you can project the query to the DTO instead, so the database returns only the columns you need.
463463

464-
Implement the `IQueryProjectionMapper<TEntity, TDto>` interface to define a projection:
464+
Implement the `IQueryableMapper<TEntity, TDto>` interface to define a projection:
465465

466466
````csharp
467467
using System.Linq;
468468
using Volo.Abp.ObjectMapping;
469469

470470
namespace MyProject.Books;
471471

472-
public class BookProjector : IQueryProjectionMapper<Book, BookDto>
472+
public class BookProjector : IQueryableMapper<Book, BookDto>
473473
{
474474
public IQueryable<BookDto> ProjectTo(IQueryable<Book> source)
475475
{
@@ -486,12 +486,12 @@ You don't have to write the `Select` by hand. Both [Mapperly](https://mapperly.r
486486

487487
ABP registers the projection mappers by convention, you don't need to configure anything else. Filters (like soft delete and multi-tenancy), sorting and paging are still applied to the query before the projection.
488488

489-
> The projection replaces the entity based extension points. `GetAsync` doesn't use `GetEntityByIdAsync` and `MapToGetOutputDtoAsync`, `GetListAsync` doesn't use `MapToGetListOutputDtosAsync` anymore. If an application service needs to keep using them, override the `GetProjectionMapper` or `GetListProjectionMapper` property and return `null`:
489+
> The projection replaces the entity based extension points. `GetAsync` doesn't use `GetEntityByIdAsync` and `MapToGetOutputDtoAsync`, `GetListAsync` doesn't use `MapToGetListOutputDtosAsync` anymore. If an application service needs to keep using them, override the `GetQueryableMapper` or `GetListQueryableMapper` property and return `null`:
490490
491491
````csharp
492492
public class BookAppService : CrudAppService<Book, BookDto, Guid>
493493
{
494-
protected override IQueryProjectionMapper<Book, BookDto>? GetProjectionMapper => null;
494+
protected override IQueryableMapper<Book, BookDto>? GetQueryableMapper => null;
495495

496496
//...
497497
}

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

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -45,18 +45,20 @@ public abstract class AbstractKeyReadOnlyAppService<TEntity, TGetOutputDto, TGet
4545
protected virtual string? GetListPolicyName { get; set; }
4646

4747
/// <summary>
48-
/// <see cref="GetEntityByIdAsync"/> and <see cref="MapToGetOutputDtoAsync"/> are not used
49-
/// while a projection mapper is available. Override and return null to keep using them.
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.
5051
/// </summary>
51-
protected virtual IQueryProjectionMapper<TEntity, TGetOutputDto>? GetProjectionMapper
52-
=> LazyServiceProvider.LazyGetService<IQueryProjectionMapper<TEntity, TGetOutputDto>>();
52+
protected virtual IQueryableMapper<TEntity, TGetOutputDto>? GetQueryableMapper
53+
=> LazyServiceProvider.LazyGetService<IQueryableMapper<TEntity, TGetOutputDto>>();
5354

5455
/// <summary>
55-
/// <see cref="MapToGetListOutputDtosAsync"/> is not used while a projection mapper is
56-
/// available. Override and return null to keep using it.
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.
5759
/// </summary>
58-
protected virtual IQueryProjectionMapper<TEntity, TGetListOutputDto>? GetListProjectionMapper
59-
=> LazyServiceProvider.LazyGetService<IQueryProjectionMapper<TEntity, TGetListOutputDto>>();
60+
protected virtual IQueryableMapper<TEntity, TGetListOutputDto>? GetListQueryableMapper
61+
=> LazyServiceProvider.LazyGetService<IQueryableMapper<TEntity, TGetListOutputDto>>();
6062

6163
protected AbstractKeyReadOnlyAppService(IReadOnlyRepository<TEntity> repository)
6264
{
@@ -67,10 +69,10 @@ public virtual async Task<TGetOutputDto> GetAsync(TKey id)
6769
{
6870
await CheckGetPolicyAsync();
6971

70-
var projectionMapper = GetProjectionMapper;
72+
var projectionMapper = GetQueryableMapper;
7173
if (projectionMapper != null)
7274
{
73-
var query = await GetEntityByIdQueryOrNullAsync(id);
75+
var query = await CreateEntityQueryAsync(id);
7476
if (query != null)
7577
{
7678
return await AsyncExecuter.FirstOrDefaultAsync(projectionMapper.ProjectTo(query))
@@ -97,7 +99,7 @@ public virtual async Task<PagedResultDto<TGetListOutputDto>> GetListAsync(TGetLi
9799
query = ApplySorting(query, input);
98100
query = ApplyPaging(query, input);
99101

100-
var projectionMapper = GetListProjectionMapper;
102+
var projectionMapper = GetListQueryableMapper;
101103
if (projectionMapper != null)
102104
{
103105
entityDtos = await AsyncExecuter.ToListAsync(projectionMapper.ProjectTo(query));
@@ -118,10 +120,11 @@ public virtual async Task<PagedResultDto<TGetListOutputDto>> GetListAsync(TGetLi
118120
protected abstract Task<TEntity> GetEntityByIdAsync(TKey id);
119121

120122
/// <summary>
121-
/// Returns null if this application service can not create a query for a single entity.
122-
/// <see cref="GetEntityByIdAsync"/> is used in that case.
123+
/// 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.
123125
/// </summary>
124-
protected virtual Task<IQueryable<TEntity>?> GetEntityByIdQueryOrNullAsync(TKey id)
126+
/// <param name="id">The id of the entity.</param>
127+
protected virtual Task<IQueryable<TEntity>?> CreateEntityQueryAsync(TKey id)
125128
{
126129
return Task.FromResult<IQueryable<TEntity>?>(null);
127130
}

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

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

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

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

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

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

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ public override void PreConfigureServices(ServiceConfigurationContext context)
1919
).ConvertAll(t => new ServiceIdentifier(t))
2020
);
2121

22-
//Register types for IQueryProjectionMapper<TSource, TDestination> if implements
22+
//Register types for IQueryableMapper<TSource, TDestination> if implements
2323
onServiceExposingContext.ExposedTypes.AddRange(
2424
ReflectionHelper.GetImplementedGenericTypes(
2525
onServiceExposingContext.ImplementationType,
26-
typeof(IQueryProjectionMapper<,>)
26+
typeof(IQueryableMapper<,>)
2727
).ConvertAll(t => new ServiceIdentifier(t))
2828
);
2929
});

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

Lines changed: 0 additions & 23 deletions
This file was deleted.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System.Linq;
2+
using Volo.Abp.DependencyInjection;
3+
4+
namespace Volo.Abp.ObjectMapping;
5+
6+
/// <summary>
7+
/// Maps a query to another.
8+
/// Implement this interface to project a query on the data store side, instead of loading the
9+
/// source objects into the memory and mapping them one by one.
10+
/// </summary>
11+
/// <typeparam name="TSource">Type of the source objects</typeparam>
12+
/// <typeparam name="TDestination">Type of the destination objects</typeparam>
13+
public interface IQueryableMapper<TSource, TDestination> : ITransientDependency
14+
{
15+
/// <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.
18+
/// </summary>
19+
/// <param name="source">The query to project</param>
20+
IQueryable<TDestination> ProjectTo(IQueryable<TSource> source);
21+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
namespace Volo.Abp.Application.Services.QueryProjection;
55

6-
public class BookProjector : IQueryProjectionMapper<Book, BookDto>
6+
public class BookProjector : IQueryableMapper<Book, BookDto>
77
{
88
public const string Marker = "-projected";
99

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ namespace Volo.Abp.Application.Services.QueryProjection;
66

77
public class BookWithoutProjectionAppService : CrudAppService<Book, BookDto, Guid>
88
{
9-
protected override IQueryProjectionMapper<Book, BookDto> GetProjectionMapper => null;
9+
protected override IQueryableMapper<Book, BookDto> GetQueryableMapper => null;
1010

11-
protected override IQueryProjectionMapper<Book, BookDto> GetListProjectionMapper => null;
11+
protected override IQueryableMapper<Book, BookDto> GetListQueryableMapper => null;
1212

1313
public BookWithoutProjectionAppService(IRepository<Book, Guid> repository)
1414
: base(repository)

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public QueryProjection_Tests()
2323
[Fact]
2424
public void Should_Resolve_Projection_Mapper_Independent_From_The_Class_Name()
2525
{
26-
ServiceProvider.GetService<IQueryProjectionMapper<Book, BookDto>>()
26+
ServiceProvider.GetService<IQueryableMapper<Book, BookDto>>()
2727
.ShouldBeOfType<BookProjector>();
2828
}
2929

@@ -80,8 +80,6 @@ public async Task Should_Not_Use_The_Entity_Based_Overrides_While_Projecting()
8080
{
8181
var appService = GetRequiredService<BookCustomizedAppService>();
8282

83-
//A projection mapper is registered for <Book, BookDto>, so GetEntityByIdAsync and
84-
//MapToGetOutputDtoAsync of this application service are skipped by design.
8583
var dto = await appService.GetAsync(_bookId);
8684

8785
dto.Name.ShouldEndWith(BookProjector.Marker);

0 commit comments

Comments
 (0)