Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
```json
```json
//[doc-seo]
{
"Description": "Learn how to implement application services in the ABP Framework to expose domain logic and streamline presentation layer interactions."
Expand Down Expand Up @@ -444,6 +444,7 @@ These methods are low level methods that can control how to query entities from
* `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.
* `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.
* `GetEntityByIdAsync` is used to get an entity by id, which calls `Repository.GetAsync(id)` by default.
* `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.
* `DeleteByIdAsync` is used to delete an entity by id, which calls `Repository.DeleteAsync(id)` by default.

#### Object to Object Mapping
Expand All @@ -456,6 +457,56 @@ These methods are used to convert Entities to DTOs and vice verse. They use the
* `MapToEntityAsync(TCreateInput)` is used to create an entity from `TCreateInput`.
* `MapToEntityAsync(TUpdateInput, TEntity)` is used to update an existing entity from `TUpdateInput`.

#### Query Projection

`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.

Implement the `IQueryProjectionMapper<TEntity, TDto>` interface to define a projection:

````csharp
using System.Linq;
using Volo.Abp.ObjectMapping;

namespace MyProject.Books;

public class BookProjector : IQueryProjectionMapper<Book, BookDto>
{
public IQueryable<BookDto> ProjectTo(IQueryable<Book> source)
{
return source.Select(book => new BookDto
{
Id = book.Id,
Name = book.Name
});
}
}
````

[Mapperly](https://mapperly.riok.app/) can generate that method for you:

````csharp
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
public partial class BookProjector : IQueryProjectionMapper<Book, BookDto>
{
public partial IQueryable<BookDto> ProjectTo(IQueryable<Book> source);
}
````

> `RequiredMappingStrategy.Target` tells Mapperly to only require the DTO members to be mapped. Without it, it reports a warning for every entity property that the DTO doesn't have.

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.

> 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`:

````csharp
public class BookAppService : CrudAppService<Book, BookDto, Guid>
{
protected override IQueryProjectionMapper<Book, BookDto>? GetProjectionMapper => null;

//...
}
````

## Miscellaneous

### Working with Streams
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,20 @@ public abstract class AbstractKeyReadOnlyAppService<TEntity, TGetOutputDto, TGet

protected virtual string? GetListPolicyName { get; set; }

/// <summary>
/// <see cref="GetEntityByIdAsync"/> and <see cref="MapToGetOutputDtoAsync"/> are not used
/// while a projection mapper is available. Override and return null to keep using them.
/// </summary>
protected virtual IQueryProjectionMapper<TEntity, TGetOutputDto>? GetProjectionMapper
=> LazyServiceProvider.LazyGetService<IQueryProjectionMapper<TEntity, TGetOutputDto>>();

/// <summary>
/// <see cref="MapToGetListOutputDtosAsync"/> is not used while a projection mapper is
/// available. Override and return null to keep using it.
/// </summary>
protected virtual IQueryProjectionMapper<TEntity, TGetListOutputDto>? GetListProjectionMapper
=> LazyServiceProvider.LazyGetService<IQueryProjectionMapper<TEntity, TGetListOutputDto>>();

protected AbstractKeyReadOnlyAppService(IReadOnlyRepository<TEntity> repository)
{
ReadOnlyRepository = repository;
Expand All @@ -53,6 +67,17 @@ public virtual async Task<TGetOutputDto> GetAsync(TKey id)
{
await CheckGetPolicyAsync();

var projectionMapper = GetProjectionMapper;
if (projectionMapper != null)
{
var query = await GetEntityByIdQueryOrNullAsync(id);
if (query != null)
{
return await AsyncExecuter.FirstOrDefaultAsync(projectionMapper.ProjectTo(query))
?? throw new EntityNotFoundException<TEntity>(id);
}
}

var entity = await GetEntityByIdAsync(id);

return await MapToGetOutputDtoAsync(entity);
Expand All @@ -65,16 +90,23 @@ public virtual async Task<PagedResultDto<TGetListOutputDto>> GetListAsync(TGetLi
var query = await CreateFilteredQueryAsync(input);
var totalCount = await AsyncExecuter.CountAsync(query);

var entities = new List<TEntity>();
var entityDtos = new List<TGetListOutputDto>();

if (totalCount > 0)
{
query = ApplySorting(query, input);
query = ApplyPaging(query, input);

entities = await AsyncExecuter.ToListAsync(query);
entityDtos = await MapToGetListOutputDtosAsync(entities);
var projectionMapper = GetListProjectionMapper;
if (projectionMapper != null)
{
entityDtos = await AsyncExecuter.ToListAsync(projectionMapper.ProjectTo(query));
}
else
{
var entities = await AsyncExecuter.ToListAsync(query);
entityDtos = await MapToGetListOutputDtosAsync(entities);
}
}

return new PagedResultDto<TGetListOutputDto>(
Expand All @@ -85,6 +117,15 @@ public virtual async Task<PagedResultDto<TGetListOutputDto>> GetListAsync(TGetLi

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

/// <summary>
/// Returns null if this application service can not create a query for a single entity.
/// <see cref="GetEntityByIdAsync"/> is used in that case.
/// </summary>
protected virtual Task<IQueryable<TEntity>?> GetEntityByIdQueryOrNullAsync(TKey id)
{
return Task.FromResult<IQueryable<TEntity>?>(null);
}

protected virtual async Task CheckGetPolicyAsync()
{
await CheckPolicyAsync(GetPolicyName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,13 @@ protected override async Task<TEntity> GetEntityByIdAsync(TKey id)
return await Repository.GetAsync(id);
}

protected override async Task<IQueryable<TEntity>?> GetEntityByIdQueryOrNullAsync(TKey id)
{
var query = await Repository.GetQueryableAsync();

return query.Where(EntityHelper.CreateEqualityExpressionForId<TEntity, TKey>(id));
}

protected override void MapToEntity(TUpdateInput updateInput, TEntity entity)
{
if (updateInput is IEntityDto<TKey> entityDto)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ protected override async Task<TEntity> GetEntityByIdAsync(TKey id)
return await Repository.GetAsync(id);
}

protected override async Task<IQueryable<TEntity>?> GetEntityByIdQueryOrNullAsync(TKey id)
{
var query = await Repository.GetQueryableAsync();

return query.Where(EntityHelper.CreateEqualityExpressionForId<TEntity, TKey>(id));
}

protected override IQueryable<TEntity> ApplyDefaultSorting(IQueryable<TEntity> query)
{
if (typeof(TEntity).IsAssignableTo<ICreationAuditedObject>())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ public override void PreConfigureServices(ServiceConfigurationContext context)
typeof(IObjectMapper<,>)
).ConvertAll(t => new ServiceIdentifier(t))
);

//Register types for IQueryProjectionMapper<TSource, TDestination> if implements
onServiceExposingContext.ExposedTypes.AddRange(
ReflectionHelper.GetImplementedGenericTypes(
onServiceExposingContext.ImplementationType,
typeof(IQueryProjectionMapper<,>)
).ConvertAll(t => new ServiceIdentifier(t))
);
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Linq;
using Volo.Abp.DependencyInjection;

namespace Volo.Abp.ObjectMapping;

/// <summary>
/// Projects a query of <typeparamref name="TSource"/> objects to a query of
/// <typeparamref name="TDestination"/> objects.
/// Implement this interface to let the query provider translate the projection into the data
/// store's own query language, instead of loading the source objects into the memory.
/// </summary>
/// <typeparam name="TSource">Type of the source objects</typeparam>
/// <typeparam name="TDestination">Type of the destination objects</typeparam>
public interface IQueryProjectionMapper<TSource, TDestination> : ITransientDependency
{
/// <summary>
/// Projects the given query to a query of <typeparamref name="TDestination"/> objects.
/// The returned query must be built on top of <paramref name="source"/>, so the query
/// provider can still translate it.
/// </summary>
/// <param name="source">The query to project</param>
IQueryable<TDestination> ProjectTo(IQueryable<TSource> source);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System;
using Volo.Abp.Domain.Entities;

namespace Volo.Abp.Application.Services.QueryProjection;

public class Book : Entity<Guid>
{
public string Name { get; set; } = default!;

public int Price { get; set; }

public Book()
{

}

public Book(Guid id, string name, int price)
: base(id)
{
Name = name;
Price = price;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.Domain.Repositories;

namespace Volo.Abp.Application.Services.QueryProjection;

public class BookAbstractKeyAppService : AbstractKeyReadOnlyAppService<Book, BookDto, Guid>
{
public BookAbstractKeyAppService(IReadOnlyRepository<Book> repository)
: base(repository)
{

}

protected override async Task<Book> GetEntityByIdAsync(Guid id)
{
var query = await ReadOnlyRepository.GetQueryableAsync();

return await AsyncExecuter.FirstAsync(query, book => book.Id == id);
}

protected override IQueryable<Book> ApplyDefaultSorting(IQueryable<Book> query)
{
return query.OrderBy(book => book.Id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;
using Volo.Abp.Domain.Repositories;

namespace Volo.Abp.Application.Services.QueryProjection;

public class BookAppService : CrudAppService<Book, BookDto, Guid>
{
public BookAppService(IRepository<Book, Guid> repository)
: base(repository)
{

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
using System.Threading.Tasks;
using Volo.Abp.Domain.Repositories;

namespace Volo.Abp.Application.Services.QueryProjection;

public class BookCustomizedAppService : CrudAppService<Book, BookDto, Guid>
{
public const string Marker = "-customized";

public BookCustomizedAppService(IRepository<Book, Guid> repository)
: base(repository)
{

}

protected override async Task<Book> GetEntityByIdAsync(Guid id)
{
var book = await base.GetEntityByIdAsync(id);
book.Name += Marker;
return book;
}

protected override Task<BookDto> MapToGetOutputDtoAsync(Book entity)
{
return Task.FromResult(new BookDto { Id = entity.Id, Name = entity.Name + Marker });
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System;
using Volo.Abp.Application.Dtos;

namespace Volo.Abp.Application.Services.QueryProjection;

public class BookDto : EntityDto<Guid>
{
public string Name { get; set; } = default!;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;
using Volo.Abp.Domain.Repositories;

namespace Volo.Abp.Application.Services.QueryProjection;

public class BookLiteAppService : CrudAppService<Book, BookLiteDto, Guid>
{
public BookLiteAppService(IRepository<Book, Guid> repository)
: base(repository)
{

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System;
using Volo.Abp.Application.Dtos;

namespace Volo.Abp.Application.Services.QueryProjection;

public class BookLiteDto : EntityDto<Guid>
{
public string Name { get; set; } = default!;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Volo.Abp.DependencyInjection;
using Volo.Abp.ObjectMapping;

namespace Volo.Abp.Application.Services.QueryProjection;

public class BookObjectMapper :
IObjectMapper<Book, BookDto>,
IObjectMapper<Book, BookLiteDto>,
ITransientDependency
{
public const string Marker = "-mapped";

public BookDto Map(Book source)
{
return new BookDto { Id = source.Id, Name = source.Name + Marker };
}

public BookDto Map(Book source, BookDto destination)
{
destination.Id = source.Id;
destination.Name = source.Name + Marker;
return destination;
}

BookLiteDto IObjectMapper<Book, BookLiteDto>.Map(Book source)
{
return new BookLiteDto { Id = source.Id, Name = source.Name + Marker };
}

BookLiteDto IObjectMapper<Book, BookLiteDto>.Map(Book source, BookLiteDto destination)
{
destination.Id = source.Id;
destination.Name = source.Name + Marker;
return destination;
}
}
Loading
Loading