Skip to content

Commit 7ac1bc6

Browse files
committed
Document the query projection
1 parent 3da29b2 commit 7ac1bc6

1 file changed

Lines changed: 44 additions & 7 deletions

File tree

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

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
```json
1+
```json
22
//[doc-seo]
33
{
44
"Description": "Learn how to implement application services in the ABP Framework to expose domain logic and streamline presentation layer interactions."
@@ -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-
* `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.
447+
* `CreateEntityQueryOrNullAsync` 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 `IQueryableMapper<TEntity, TDto>` interface to define a projection:
464+
Implement the `IQueryProjector<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 : IQueryableMapper<Book, BookDto>
472+
public class BookProjector : IQueryProjector<Book, BookDto>
473473
{
474474
public IQueryable<BookDto> ProjectTo(IQueryable<Book> source)
475475
{
@@ -484,14 +484,51 @@ public class BookProjector : IQueryableMapper<Book, BookDto>
484484

485485
You don't have to write the `Select` by hand. Both [Mapperly](https://mapperly.riok.app/) and [AutoMapper](https://docs.automapper.org) can project an `IQueryable`, refer to their own documentation for it and to the [object to object mapping document](../../infrastructure/object-to-object-mapping.md) for their ABP integrations.
486486

487-
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.
487+
ABP registers the projectors by convention, you don't need to configure anything else. Implement a projector once for an entity and DTO pair, the last registered one is used otherwise. 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 `GetQueryableMapper` or `GetListQueryableMapper` property and return `null`:
489+
> A projection must return one row per entity. The total count and the paging are calculated on the entity query before the projection runs, so a projection that filters out rows (an inner join to an optional relation) or multiplies them (a join to a collection) returns a page that doesn't match the reported total count. Use a left join for optional relations.
490+
491+
The projector is synchronous, so it can not obtain the query of another aggregate root, which is only
492+
available through the asynchronous `GetQueryableAsync`. Override `CreateGetOutputDtoQueryOrNullAsync` or
493+
`CreateGetListOutputDtoQueryOrNullAsync` for that. They replace the projector for that application service:
494+
495+
````csharp
496+
public class BookAppService : ReadOnlyAppService<Book, BookDto, Guid>
497+
{
498+
private readonly IReadOnlyRepository<Author, Guid> _authorRepository;
499+
500+
//...
501+
502+
protected override async Task<IQueryable<BookDto>?> CreateGetListOutputDtoQueryOrNullAsync(IQueryable<Book> query)
503+
{
504+
var authors = await _authorRepository.GetQueryableAsync();
505+
506+
return from book in query
507+
join author in authors on book.AuthorId equals author.Id into bookAuthors
508+
from bookAuthor in bookAuthors.DefaultIfEmpty()
509+
select new BookDto
510+
{
511+
Id = book.Id,
512+
Name = book.Name,
513+
AuthorName = bookAuthor != null ? bookAuthor.Name : null
514+
};
515+
}
516+
}
517+
````
518+
519+
Both queries must come from the same database context, otherwise they can not be executed as a single query,
520+
and the provider has to be able to translate the join. The one row per entity rule above applies here too,
521+
that's why the example uses a left join. A joined column can not be used for the sorting and the paging,
522+
since they are already applied to the entity query before this method is called.
523+
524+
> The projection replaces the entity based extension points. `GetAsync` doesn't use `GetEntityByIdAsync` and `MapToGetOutputDtoAsync`, `GetListAsync` doesn't use `MapToGetListOutputDtosAsync` anymore. Projectors are resolved by the `(entity, DTO)` type pair, so registering one enables the projection for every application service using that pair. If an application service needs to keep using the entity based extension points, override the `GetOutputDtoQueryProjector` or `GetListOutputDtoQueryProjector` property and return `null`:
490525
491526
````csharp
492527
public class BookAppService : CrudAppService<Book, BookDto, Guid>
493528
{
494-
protected override IQueryableMapper<Book, BookDto>? GetQueryableMapper => null;
529+
protected override IQueryProjector<Book, BookDto>? GetOutputDtoQueryProjector => null;
530+
531+
protected override IQueryProjector<Book, BookDto>? GetListOutputDtoQueryProjector => null;
495532

496533
//...
497534
}

0 commit comments

Comments
 (0)