mirror of https://github.com/abpframework/abp.git
committed by
GitHub
46 changed files with 1507 additions and 4 deletions
@ -0,0 +1,24 @@ |
|||
using System.Linq; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.ObjectMapping; |
|||
|
|||
/// <summary>
|
|||
/// Maps a query to another.
|
|||
/// Implement this interface to project a query on the data store side, instead of loading the
|
|||
/// source objects into the memory and mapping them one by one.
|
|||
/// Implement it once for a source and destination pair. Use the ReplaceServices option of the
|
|||
/// DependencyAttribute to replace an existing implementation.
|
|||
/// </summary>
|
|||
/// <typeparam name="TSource">Type of the source objects</typeparam>
|
|||
/// <typeparam name="TDestination">Type of the destination objects</typeparam>
|
|||
public interface IQueryProjector<TSource, TDestination> : ITransientDependency |
|||
{ |
|||
/// <summary>
|
|||
/// Projects the given query. The returned query must be built on top of it and must keep its order,
|
|||
/// with a single destination object for each source object, using expressions the query provider can
|
|||
/// translate. The caller may have already sorted, paged or counted the source query.
|
|||
/// </summary>
|
|||
/// <param name="source">The query to project</param>
|
|||
IQueryable<TDestination> ProjectTo(IQueryable<TSource> source); |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
#nullable enable |
|||
using System; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Domain.Repositories; |
|||
|
|||
namespace Volo.Abp.Application.Services.QueryProjection; |
|||
|
|||
public class BookAbstractKeyProjectingAppService : AbstractKeyReadOnlyAppService<Book, BookDto, Guid> |
|||
{ |
|||
public BookAbstractKeyProjectingAppService(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 async Task<IQueryable<Book>?> CreateEntityQueryOrNullAsync(Guid id) |
|||
{ |
|||
var query = await ReadOnlyRepository.GetQueryableAsync(); |
|||
|
|||
return query.Where(book => book.Id == id); |
|||
} |
|||
|
|||
protected override IQueryable<Book> ApplyDefaultSorting(IQueryable<Book> query) |
|||
{ |
|||
return query.OrderBy(book => book.Id); |
|||
} |
|||
} |
|||
@ -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) |
|||
{ |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
#nullable enable |
|||
using System; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Volo.Abp.Domain.Repositories; |
|||
|
|||
namespace Volo.Abp.Application.Services.QueryProjection; |
|||
|
|||
public class BookAsyncProjectionAppService : CrudAppService<Book, BookDto, Guid> |
|||
{ |
|||
public const string Marker = "-async"; |
|||
|
|||
private readonly IBookNameSuffixProvider _suffixProvider; |
|||
|
|||
public BookAsyncProjectionAppService( |
|||
IRepository<Book, Guid> repository, |
|||
IBookNameSuffixProvider suffixProvider) |
|||
: base(repository) |
|||
{ |
|||
_suffixProvider = suffixProvider; |
|||
} |
|||
|
|||
protected override async Task<IQueryable<BookDto>?> CreateGetOutputDtoQueryOrNullAsync(Guid id) |
|||
{ |
|||
var query = await Repository.GetQueryableAsync(); |
|||
|
|||
return await ProjectAsync(query.Where(book => book.Id == id)); |
|||
} |
|||
|
|||
protected override async Task<IQueryable<BookDto>?> CreateGetListOutputDtoQueryOrNullAsync(IQueryable<Book> query) |
|||
{ |
|||
return await ProjectAsync(query); |
|||
} |
|||
|
|||
private async Task<IQueryable<BookDto>> ProjectAsync(IQueryable<Book> query) |
|||
{ |
|||
var suffix = await _suffixProvider.GetAsync(); |
|||
|
|||
return query.Select(book => new BookDto |
|||
{ |
|||
Id = book.Id, |
|||
Name = book.Name + suffix |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
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 }); |
|||
} |
|||
|
|||
protected override Task<List<BookDto>> MapToGetListOutputDtosAsync(List<Book> entities) |
|||
{ |
|||
return Task.FromResult(entities.ConvertAll(entity => new BookDto { Id = entity.Id, Name = entity.Name + Marker })); |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
using System; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Volo.Abp.Domain.Repositories; |
|||
|
|||
namespace Volo.Abp.Application.Services.QueryProjection; |
|||
|
|||
public class BookDetailAppService : |
|||
ReadOnlyAppService<Book, BookDetailDto, BookDto, Guid, PagedAndSortedResultRequestDto> |
|||
{ |
|||
public BookDetailAppService(IReadOnlyRepository<Book, Guid> repository) |
|||
: base(repository) |
|||
{ |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using System; |
|||
using Volo.Abp.Application.Dtos; |
|||
|
|||
namespace Volo.Abp.Application.Services.QueryProjection; |
|||
|
|||
public class BookDetailDto : EntityDto<Guid> |
|||
{ |
|||
public string Name { get; set; } = default!; |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
using System.Linq; |
|||
using Volo.Abp.ObjectMapping; |
|||
|
|||
namespace Volo.Abp.Application.Services.QueryProjection; |
|||
|
|||
public class BookDetailProjector : IQueryProjector<Book, BookDetailDto> |
|||
{ |
|||
public const string Marker = "-detail"; |
|||
|
|||
public IQueryable<BookDetailDto> ProjectTo(IQueryable<Book> source) |
|||
{ |
|||
return source.Select(book => new BookDetailDto |
|||
{ |
|||
Id = book.Id, |
|||
Name = book.Name + Marker |
|||
}); |
|||
} |
|||
} |
|||
@ -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!; |
|||
} |
|||
@ -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) |
|||
{ |
|||
|
|||
} |
|||
} |
|||
@ -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!; |
|||
} |
|||
@ -0,0 +1,48 @@ |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.ObjectMapping; |
|||
|
|||
namespace Volo.Abp.Application.Services.QueryProjection; |
|||
|
|||
public class BookObjectMapper : |
|||
IObjectMapper<Book, BookDto>, |
|||
IObjectMapper<Book, BookLiteDto>, |
|||
IObjectMapper<BookDto, Book>, |
|||
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; |
|||
} |
|||
|
|||
Book IObjectMapper<BookDto, Book>.Map(BookDto source) |
|||
{ |
|||
return new Book(source.Id, source.Name, 0); |
|||
} |
|||
|
|||
Book IObjectMapper<BookDto, Book>.Map(BookDto source, Book destination) |
|||
{ |
|||
destination.Name = source.Name; |
|||
return destination; |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Domain.Repositories; |
|||
|
|||
namespace Volo.Abp.Application.Services.QueryProjection; |
|||
|
|||
public class BookPolicyCheckedException : Exception |
|||
{ |
|||
|
|||
} |
|||
|
|||
public class BookPolicyCheckedAppService : CrudAppService<Book, BookDto, Guid> |
|||
{ |
|||
public BookPolicyCheckedAppService(IRepository<Book, Guid> repository) |
|||
: base(repository) |
|||
{ |
|||
|
|||
} |
|||
|
|||
protected override Task CheckGetPolicyAsync() |
|||
{ |
|||
throw new BookPolicyCheckedException(); |
|||
} |
|||
|
|||
protected override Task CheckGetListPolicyAsync() |
|||
{ |
|||
throw new BookPolicyCheckedException(); |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
using System.Linq; |
|||
using Volo.Abp.ObjectMapping; |
|||
|
|||
namespace Volo.Abp.Application.Services.QueryProjection; |
|||
|
|||
public class BookProjector : IQueryProjector<Book, BookDto> |
|||
{ |
|||
public const string Marker = "-projected"; |
|||
|
|||
public IQueryable<BookDto> ProjectTo(IQueryable<Book> source) |
|||
{ |
|||
return source.Select(book => new BookDto |
|||
{ |
|||
Id = book.Id, |
|||
Name = book.Name + Marker |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
using System; |
|||
using Volo.Abp.Domain.Repositories; |
|||
|
|||
namespace Volo.Abp.Application.Services.QueryProjection; |
|||
|
|||
public class BookReadOnlyAppService : ReadOnlyAppService<Book, BookDto, Guid> |
|||
{ |
|||
public BookReadOnlyAppService(IReadOnlyRepository<Book, Guid> repository) |
|||
: base(repository) |
|||
{ |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,106 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Linq.Expressions; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Domain.Entities; |
|||
using Volo.Abp.Domain.Repositories; |
|||
|
|||
namespace Volo.Abp.Application.Services.QueryProjection; |
|||
|
|||
[ExposeServices( |
|||
typeof(IRepository<Book, Guid>), |
|||
typeof(IReadOnlyRepository<Book, Guid>), |
|||
typeof(IReadOnlyRepository<Book>))] |
|||
public class BookRepository : RepositoryBase<Book, Guid>, ISingletonDependency |
|||
{ |
|||
private readonly List<Book> _books = new(); |
|||
|
|||
public BookRepository() |
|||
: base("InMemory") |
|||
{ |
|||
|
|||
} |
|||
|
|||
public override Task<IQueryable<Book>> GetQueryableAsync() |
|||
{ |
|||
return Task.FromResult(_books.AsQueryable()); |
|||
} |
|||
|
|||
[Obsolete("Use GetQueryableAsync method.")] |
|||
protected override IQueryable<Book> GetQueryable() |
|||
{ |
|||
return _books.AsQueryable(); |
|||
} |
|||
|
|||
public override Task<Book> GetAsync(Guid id, bool includeDetails = true, CancellationToken cancellationToken = default) |
|||
{ |
|||
var book = _books.FirstOrDefault(x => x.Id == id); |
|||
if (book == null) |
|||
{ |
|||
throw new EntityNotFoundException(typeof(Book), id); |
|||
} |
|||
|
|||
return Task.FromResult(book); |
|||
} |
|||
|
|||
public override Task<Book> FindAsync(Guid id, bool includeDetails = true, CancellationToken cancellationToken = default) |
|||
{ |
|||
return Task.FromResult(_books.FirstOrDefault(x => x.Id == id)); |
|||
} |
|||
|
|||
public override Task<Book> FindAsync(Expression<Func<Book, bool>> predicate, bool includeDetails = true, CancellationToken cancellationToken = default) |
|||
{ |
|||
return Task.FromResult(_books.AsQueryable().FirstOrDefault(predicate)); |
|||
} |
|||
|
|||
public override Task<List<Book>> GetListAsync(bool includeDetails = false, CancellationToken cancellationToken = default) |
|||
{ |
|||
return Task.FromResult(_books.ToList()); |
|||
} |
|||
|
|||
public override Task<List<Book>> GetListAsync(Expression<Func<Book, bool>> predicate, bool includeDetails = false, CancellationToken cancellationToken = default) |
|||
{ |
|||
return Task.FromResult(_books.AsQueryable().Where(predicate).ToList()); |
|||
} |
|||
|
|||
public override Task<List<Book>> GetPagedListAsync(int skipCount, int maxResultCount, string sorting, bool includeDetails = false, CancellationToken cancellationToken = default) |
|||
{ |
|||
return Task.FromResult(_books.Skip(skipCount).Take(maxResultCount).ToList()); |
|||
} |
|||
|
|||
public override Task<long> GetCountAsync(CancellationToken cancellationToken = default) |
|||
{ |
|||
return Task.FromResult((long)_books.Count); |
|||
} |
|||
|
|||
public override Task<Book> InsertAsync(Book entity, bool autoSave = false, CancellationToken cancellationToken = default) |
|||
{ |
|||
_books.Add(entity); |
|||
return Task.FromResult(entity); |
|||
} |
|||
|
|||
public override Task<Book> UpdateAsync(Book entity, bool autoSave = false, CancellationToken cancellationToken = default) |
|||
{ |
|||
return Task.FromResult(entity); |
|||
} |
|||
|
|||
public override Task DeleteAsync(Book entity, bool autoSave = false, CancellationToken cancellationToken = default) |
|||
{ |
|||
_books.Remove(entity); |
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
public override Task DeleteAsync(Expression<Func<Book, bool>> predicate, bool autoSave = false, CancellationToken cancellationToken = default) |
|||
{ |
|||
_books.RemoveAll(new Predicate<Book>(predicate.Compile())); |
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
public override Task DeleteDirectAsync(Expression<Func<Book, bool>> predicate, CancellationToken cancellationToken = default) |
|||
{ |
|||
return DeleteAsync(predicate); |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
using System; |
|||
using Volo.Abp.Domain.Repositories; |
|||
|
|||
namespace Volo.Abp.Application.Services.QueryProjection; |
|||
|
|||
public class BookStructAppService : ReadOnlyAppService<Book, BookStructDto, Guid> |
|||
{ |
|||
public BookStructAppService(IReadOnlyRepository<Book, Guid> repository) |
|||
: base(repository) |
|||
{ |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using System; |
|||
|
|||
namespace Volo.Abp.Application.Services.QueryProjection; |
|||
|
|||
public struct BookStructDto |
|||
{ |
|||
public Guid Id { get; set; } |
|||
|
|||
public string Name { get; set; } |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
using System.Linq; |
|||
using Volo.Abp.ObjectMapping; |
|||
|
|||
namespace Volo.Abp.Application.Services.QueryProjection; |
|||
|
|||
public class BookStructProjector : IQueryProjector<Book, BookStructDto> |
|||
{ |
|||
public IQueryable<BookStructDto> ProjectTo(IQueryable<Book> source) |
|||
{ |
|||
return source.Select(book => new BookStructDto |
|||
{ |
|||
Id = book.Id, |
|||
Name = book.Name |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
#nullable enable |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Domain.Repositories; |
|||
using Volo.Abp.ObjectMapping; |
|||
|
|||
namespace Volo.Abp.Application.Services.QueryProjection; |
|||
|
|||
public class BookWithoutProjectionAppService : CrudAppService<Book, BookDto, Guid> |
|||
{ |
|||
public const string Marker = "-not-projected"; |
|||
|
|||
protected override IQueryProjector<Book, BookDto>? GetOutputDtoQueryProjector => null; |
|||
|
|||
protected override IQueryProjector<Book, BookDto>? GetListOutputDtoQueryProjector => null; |
|||
|
|||
public BookWithoutProjectionAppService(IRepository<Book, Guid> repository) |
|||
: base(repository) |
|||
{ |
|||
|
|||
} |
|||
|
|||
protected override Task<BookDto> MapToGetOutputDtoAsync(Book entity) |
|||
{ |
|||
return Task.FromResult(new BookDto { Id = entity.Id, Name = entity.Name + Marker }); |
|||
} |
|||
|
|||
protected override Task<List<BookDto>> MapToGetListOutputDtosAsync(List<Book> entities) |
|||
{ |
|||
return Task.FromResult(entities.ConvertAll(entity => new BookDto { Id = entity.Id, Name = entity.Name + Marker })); |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.Application.Services.QueryProjection; |
|||
|
|||
public interface IBookNameSuffixProvider |
|||
{ |
|||
Task<string> GetAsync(); |
|||
} |
|||
|
|||
public class BookNameSuffixProvider : IBookNameSuffixProvider, ITransientDependency |
|||
{ |
|||
public async Task<string> GetAsync() |
|||
{ |
|||
await Task.Yield(); |
|||
|
|||
return BookAsyncProjectionAppService.Marker; |
|||
} |
|||
} |
|||
@ -0,0 +1,196 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Shouldly; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Volo.Abp.Domain.Entities; |
|||
using Volo.Abp.Domain.Repositories; |
|||
using Volo.Abp.ObjectMapping; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.Application.Services.QueryProjection; |
|||
|
|||
public class QueryProjection_Tests : AbpDddApplicationTestBase |
|||
{ |
|||
private readonly Guid _bookId = Guid.NewGuid(); |
|||
|
|||
public QueryProjection_Tests() |
|||
{ |
|||
var repository = GetRequiredService<IRepository<Book, Guid>>(); |
|||
repository.InsertAsync(new Book(_bookId, "Hitchhiker's Guide", 42)).GetAwaiter().GetResult(); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Resolve_The_Projector_Independent_From_The_Class_Name() |
|||
{ |
|||
ServiceProvider.GetService<IQueryProjector<Book, BookDto>>() |
|||
.ShouldBeOfType<BookProjector>(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Project_On_The_Query_For_CrudAppService() |
|||
{ |
|||
var appService = GetRequiredService<BookAppService>(); |
|||
|
|||
(await appService.GetAsync(_bookId)).Name.ShouldEndWith(BookProjector.Marker); |
|||
(await appService.GetListAsync(new PagedAndSortedResultRequestDto())) |
|||
.Items[0].Name.ShouldEndWith(BookProjector.Marker); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Project_On_The_Query_For_ReadOnlyAppService() |
|||
{ |
|||
var appService = GetRequiredService<BookReadOnlyAppService>(); |
|||
|
|||
(await appService.GetAsync(_bookId)).Name.ShouldEndWith(BookProjector.Marker); |
|||
(await appService.GetListAsync(new PagedAndSortedResultRequestDto())) |
|||
.Items[0].Name.ShouldEndWith(BookProjector.Marker); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Throw_EntityNotFoundException_If_The_Projected_Entity_Does_Not_Exist() |
|||
{ |
|||
var appService = GetRequiredService<BookAppService>(); |
|||
|
|||
await Should.ThrowAsync<EntityNotFoundException>(async () => await appService.GetAsync(Guid.NewGuid())); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Use_The_Object_Mapper_If_No_Projector_Was_Registered() |
|||
{ |
|||
var appService = GetRequiredService<BookLiteAppService>(); |
|||
|
|||
(await appService.GetAsync(_bookId)).Name.ShouldEndWith(BookObjectMapper.Marker); |
|||
(await appService.GetListAsync(new PagedAndSortedResultRequestDto())) |
|||
.Items[0].Name.ShouldEndWith(BookObjectMapper.Marker); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Use_The_Entity_Based_Overrides_If_The_Projection_Was_Disabled() |
|||
{ |
|||
var appService = GetRequiredService<BookWithoutProjectionAppService>(); |
|||
|
|||
(await appService.GetAsync(_bookId)).Name.ShouldEndWith(BookWithoutProjectionAppService.Marker); |
|||
(await appService.GetListAsync(new PagedAndSortedResultRequestDto())) |
|||
.Items[0].Name.ShouldEndWith(BookWithoutProjectionAppService.Marker); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Not_Use_The_Entity_Based_Overrides_While_Projecting() |
|||
{ |
|||
var appService = GetRequiredService<BookCustomizedAppService>(); |
|||
|
|||
var dto = await appService.GetAsync(_bookId); |
|||
|
|||
dto.Name.ShouldEndWith(BookProjector.Marker); |
|||
dto.Name.ShouldNotContain(BookCustomizedAppService.Marker); |
|||
|
|||
var items = (await appService.GetListAsync(new PagedAndSortedResultRequestDto())).Items; |
|||
|
|||
items[0].Name.ShouldEndWith(BookProjector.Marker); |
|||
items[0].Name.ShouldNotContain(BookCustomizedAppService.Marker); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Use_The_Object_Mapper_If_The_Entity_Query_Can_Not_Be_Created() |
|||
{ |
|||
var appService = GetRequiredService<BookAbstractKeyAppService>(); |
|||
|
|||
//GetAsync has no query to project, it falls back to GetEntityByIdAsync
|
|||
(await appService.GetAsync(_bookId)).Name.ShouldEndWith(BookObjectMapper.Marker); |
|||
|
|||
//GetListAsync always has a query, so it is still projected
|
|||
(await appService.GetListAsync(new PagedAndSortedResultRequestDto())) |
|||
.Items[0].Name.ShouldEndWith(BookProjector.Marker); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Use_The_Asynchronously_Created_Projection_Query() |
|||
{ |
|||
var appService = GetRequiredService<BookAsyncProjectionAppService>(); |
|||
|
|||
(await appService.GetAsync(_bookId)).Name.ShouldEndWith(BookAsyncProjectionAppService.Marker); |
|||
(await appService.GetListAsync(new PagedAndSortedResultRequestDto())) |
|||
.Items[0].Name.ShouldEndWith(BookAsyncProjectionAppService.Marker); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Throw_EntityNotFoundException_From_An_Overridden_Projection_Query() |
|||
{ |
|||
var appService = GetRequiredService<BookAsyncProjectionAppService>(); |
|||
|
|||
await Should.ThrowAsync<EntityNotFoundException>(async () => await appService.GetAsync(Guid.NewGuid())); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Throw_EntityNotFoundException_For_A_Value_Type_Dto() |
|||
{ |
|||
var appService = GetRequiredService<BookStructAppService>(); |
|||
|
|||
(await appService.GetAsync(_bookId)).Name.ShouldBe("Hitchhiker's Guide"); |
|||
|
|||
await Should.ThrowAsync<EntityNotFoundException>(async () => await appService.GetAsync(Guid.NewGuid())); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Not_Project_On_The_Create_And_Update_Paths() |
|||
{ |
|||
var appService = GetRequiredService<BookAppService>(); |
|||
|
|||
var created = await appService.CreateAsync(new BookDto { Name = "New Book" }); |
|||
created.Name.ShouldEndWith(BookObjectMapper.Marker); |
|||
|
|||
var updated = await appService.UpdateAsync(created.Id, new BookDto { Name = "Updated Book" }); |
|||
updated.Name.ShouldEndWith(BookObjectMapper.Marker); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Use_A_Different_Projector_For_The_Get_And_The_List() |
|||
{ |
|||
var appService = GetRequiredService<BookDetailAppService>(); |
|||
|
|||
(await appService.GetAsync(_bookId)).Name.ShouldEndWith(BookDetailProjector.Marker); |
|||
(await appService.GetListAsync(new PagedAndSortedResultRequestDto())) |
|||
.Items[0].Name.ShouldEndWith(BookProjector.Marker); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Apply_Paging_And_Sorting_Before_Projecting() |
|||
{ |
|||
var repository = GetRequiredService<IRepository<Book, Guid>>(); |
|||
await repository.InsertAsync(new Book(Guid.NewGuid(), "A Book", 1)); |
|||
await repository.InsertAsync(new Book(Guid.NewGuid(), "Z Book", 2)); |
|||
|
|||
var appService = GetRequiredService<BookAppService>(); |
|||
|
|||
var firstPage = await appService.GetListAsync( |
|||
new PagedAndSortedResultRequestDto { MaxResultCount = 1, Sorting = "Name" }); |
|||
|
|||
firstPage.TotalCount.ShouldBe(3); |
|||
firstPage.Items.Count.ShouldBe(1); |
|||
firstPage.Items[0].Name.ShouldBe("A Book" + BookProjector.Marker); |
|||
|
|||
var secondPage = await appService.GetListAsync( |
|||
new PagedAndSortedResultRequestDto { MaxResultCount = 1, SkipCount = 1, Sorting = "Name" }); |
|||
|
|||
secondPage.Items[0].Name.ShouldBe("Hitchhiker's Guide" + BookProjector.Marker); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Project_A_Single_Entity_From_An_AbstractKey_Application_Service() |
|||
{ |
|||
var appService = GetRequiredService<BookAbstractKeyProjectingAppService>(); |
|||
|
|||
(await appService.GetAsync(_bookId)).Name.ShouldEndWith(BookProjector.Marker); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Check_The_Policies_While_Projecting() |
|||
{ |
|||
var appService = GetRequiredService<BookPolicyCheckedAppService>(); |
|||
|
|||
await Should.ThrowAsync<BookPolicyCheckedException>(async () => await appService.GetAsync(_bookId)); |
|||
await Should.ThrowAsync<BookPolicyCheckedException>(async () => |
|||
await appService.GetListAsync(new PagedAndSortedResultRequestDto())); |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
using Volo.Abp.Application.Services; |
|||
using Volo.Abp.Domain.Repositories; |
|||
using Volo.Abp.TestApp.Domain; |
|||
|
|||
namespace Volo.Abp.EntityFrameworkCore.Applications; |
|||
|
|||
public class EntityWithIntPkProjectionAppService : ReadOnlyAppService<EntityWithIntPk, EntityWithIntPkProjectionDto, int> |
|||
{ |
|||
public EntityWithIntPkProjectionAppService(IReadOnlyRepository<EntityWithIntPk, int> repository) |
|||
: base(repository) |
|||
{ |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
using Volo.Abp.Application.Dtos; |
|||
|
|||
namespace Volo.Abp.EntityFrameworkCore.Applications; |
|||
|
|||
public class EntityWithIntPkProjectionDto : EntityDto<int> |
|||
{ |
|||
public string Name { get; set; } |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
using System.Linq; |
|||
using Volo.Abp.ObjectMapping; |
|||
using Volo.Abp.TestApp.Domain; |
|||
|
|||
namespace Volo.Abp.EntityFrameworkCore.Applications; |
|||
|
|||
public class EntityWithIntPkProjector : IQueryProjector<EntityWithIntPk, EntityWithIntPkProjectionDto> |
|||
{ |
|||
public IQueryable<EntityWithIntPkProjectionDto> ProjectTo(IQueryable<EntityWithIntPk> source) |
|||
{ |
|||
return source.Select(entity => new EntityWithIntPkProjectionDto |
|||
{ |
|||
Id = entity.Id, |
|||
Name = entity.Name |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
using System; |
|||
using Volo.Abp.Application.Services; |
|||
using Volo.Abp.Domain.Repositories; |
|||
using Volo.Abp.TestApp.Domain; |
|||
|
|||
namespace Volo.Abp.EntityFrameworkCore.Applications; |
|||
|
|||
public class PersonProjectionAppService : ReadOnlyAppService<Person, PersonProjectionDto, Guid> |
|||
{ |
|||
public PersonProjectionAppService(IReadOnlyRepository<Person, Guid> repository) |
|||
: base(repository) |
|||
{ |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using System; |
|||
using Volo.Abp.Application.Dtos; |
|||
|
|||
namespace Volo.Abp.EntityFrameworkCore.Applications; |
|||
|
|||
public class PersonProjectionDto : EntityDto<Guid> |
|||
{ |
|||
public string Name { get; set; } |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
using System.Linq; |
|||
using Volo.Abp.ObjectMapping; |
|||
using Volo.Abp.TestApp.Domain; |
|||
|
|||
namespace Volo.Abp.EntityFrameworkCore.Applications; |
|||
|
|||
public class PersonProjector : IQueryProjector<Person, PersonProjectionDto> |
|||
{ |
|||
public IQueryable<PersonProjectionDto> ProjectTo(IQueryable<Person> source) |
|||
{ |
|||
return source.Select(person => new PersonProjectionDto |
|||
{ |
|||
Id = person.Id, |
|||
Name = person.Name |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
#nullable enable |
|||
using System; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Services; |
|||
using Volo.Abp.Domain.Repositories; |
|||
using Volo.Abp.TestApp.Domain; |
|||
|
|||
namespace Volo.Abp.EntityFrameworkCore.Applications; |
|||
|
|||
//City is another aggregate root, so Person has no City navigation property to project.
|
|||
public class PersonWithCityAppService : ReadOnlyAppService<Person, PersonWithCityDto, Guid> |
|||
{ |
|||
private readonly IReadOnlyRepository<City, Guid> _cityRepository; |
|||
|
|||
public PersonWithCityAppService( |
|||
IReadOnlyRepository<Person, Guid> repository, |
|||
IReadOnlyRepository<City, Guid> cityRepository) |
|||
: base(repository) |
|||
{ |
|||
_cityRepository = cityRepository; |
|||
} |
|||
|
|||
protected override async Task<IQueryable<PersonWithCityDto>?> CreateGetOutputDtoQueryOrNullAsync(Guid id) |
|||
{ |
|||
var people = await CreateEntityQueryOrNullAsync(id); |
|||
|
|||
return people == null ? null : await JoinCitiesAsync(people); |
|||
} |
|||
|
|||
protected override async Task<IQueryable<PersonWithCityDto>?> CreateGetListOutputDtoQueryOrNullAsync(IQueryable<Person> query) |
|||
{ |
|||
return await JoinCitiesAsync(query); |
|||
} |
|||
|
|||
//left join, an inner join would drop the people without a city and break the total count
|
|||
private async Task<IQueryable<PersonWithCityDto>> JoinCitiesAsync(IQueryable<Person> people) |
|||
{ |
|||
var cities = await _cityRepository.GetQueryableAsync(); |
|||
|
|||
return from person in people |
|||
join city in cities on person.CityId equals city.Id into personCities |
|||
from personCity in personCities.DefaultIfEmpty() |
|||
select new PersonWithCityDto |
|||
{ |
|||
Id = person.Id, |
|||
Name = person.Name, |
|||
CityName = personCity != null ? personCity.Name : null |
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
#nullable enable |
|||
using System; |
|||
using Volo.Abp.Application.Dtos; |
|||
|
|||
namespace Volo.Abp.EntityFrameworkCore.Applications; |
|||
|
|||
public class PersonWithCityDto : EntityDto<Guid> |
|||
{ |
|||
public string Name { get; set; } |
|||
|
|||
public string? CityName { get; set; } |
|||
} |
|||
@ -0,0 +1,197 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.EntityFrameworkCore; |
|||
using Shouldly; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Volo.Abp.Domain.Entities; |
|||
using Volo.Abp.Domain.Repositories; |
|||
using Volo.Abp.ObjectMapping; |
|||
using Volo.Abp.TestApp; |
|||
using Volo.Abp.TestApp.Application; |
|||
using Volo.Abp.Threading; |
|||
using Volo.Abp.TestApp.Domain; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.EntityFrameworkCore.Applications; |
|||
|
|||
public class QueryProjection_Tests : EntityFrameworkCoreTestBase |
|||
{ |
|||
private readonly PersonProjectionAppService _personProjectionAppService; |
|||
|
|||
public QueryProjection_Tests() |
|||
{ |
|||
_personProjectionAppService = GetRequiredService<PersonProjectionAppService>(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Get_A_Projected_Entity() |
|||
{ |
|||
var dto = await _personProjectionAppService.GetAsync(TestDataBuilder.UserDouglasId); |
|||
|
|||
dto.Id.ShouldBe(TestDataBuilder.UserDouglasId); |
|||
dto.Name.ShouldBe("Douglas"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Throw_EntityNotFoundException_For_A_Missing_Entity() |
|||
{ |
|||
(await Should.ThrowAsync<EntityNotFoundException<Person>>( |
|||
async () => await _personProjectionAppService.GetAsync(Guid.NewGuid())) |
|||
).EntityType.ShouldBe(typeof(Person)); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Get_A_Projected_List() |
|||
{ |
|||
var result = await _personProjectionAppService.GetListAsync(new PagedAndSortedResultRequestDto()); |
|||
|
|||
result.TotalCount.ShouldBeGreaterThan(0); |
|||
result.Items.Count.ShouldBe((int)result.TotalCount); |
|||
result.Items.ShouldContain(x => x.Name == "Douglas"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Get_A_Projected_Entity_With_A_Non_Guid_Key() |
|||
{ |
|||
var appService = GetRequiredService<EntityWithIntPkProjectionAppService>(); |
|||
var entity = await WithUnitOfWorkAsync( |
|||
async () => await GetRequiredService<IRepository<EntityWithIntPk, int>>().FirstAsync()); |
|||
|
|||
(await appService.GetAsync(entity.Id)).Name.ShouldBe(entity.Name); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Apply_The_Soft_Delete_Filter_While_Projecting() |
|||
{ |
|||
var result = await _personProjectionAppService.GetListAsync(new PagedAndSortedResultRequestDto()); |
|||
|
|||
result.Items.ShouldNotContain(x => x.Id == TestDataBuilder.UserJohnDeletedId); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Only_Select_The_Projected_Columns() |
|||
{ |
|||
await WithUnitOfWorkAsync(async () => |
|||
{ |
|||
var repository = GetRequiredService<IReadOnlyRepository<Person, Guid>>(); |
|||
var projector = GetRequiredService<IQueryProjector<Person, PersonProjectionDto>>(); |
|||
|
|||
var sql = projector.ProjectTo(await repository.GetQueryableAsync()).ToQueryString(); |
|||
|
|||
sql.ShouldContain("\"Name\""); |
|||
sql.ShouldNotContain("\"Birthday\""); |
|||
sql.ShouldNotContain("\"ExtraProperties\""); |
|||
|
|||
//the data filters are still a part of the query
|
|||
sql.ShouldContain("Is_Deleted"); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Join_Another_Aggregate_While_Projecting() |
|||
{ |
|||
var appService = GetRequiredService<PersonWithCityAppService>(); |
|||
|
|||
var dto = await appService.GetAsync(TestDataBuilder.UserDouglasId); |
|||
dto.CityName.ShouldBe("London"); |
|||
|
|||
var result = await appService.GetListAsync(new PagedAndSortedResultRequestDto()); |
|||
result.Items.ShouldContain(x => x.CityName == "London"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Keep_The_Total_Count_While_Joining_Another_Aggregate() |
|||
{ |
|||
await WithUnitOfWorkAsync(async () => |
|||
{ |
|||
await GetRequiredService<IRepository<Person, Guid>>() |
|||
.InsertAsync(new Person(Guid.NewGuid(), "PersonWithoutCity", 30), autoSave: true); |
|||
}); |
|||
|
|||
var result = await GetRequiredService<PersonWithCityAppService>() |
|||
.GetListAsync(new PagedAndSortedResultRequestDto()); |
|||
|
|||
result.Items.Count.ShouldBe((int)result.TotalCount); |
|||
result.Items.ShouldContain(x => x.Name == "PersonWithoutCity" && x.CityName == null); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Execute_The_Projected_Query_From_The_Application_Service() |
|||
{ |
|||
System.Collections.Concurrent.ConcurrentQueue<string> commands; |
|||
using (SqlCommandCapture.Begin(out commands)) |
|||
{ |
|||
await GetRequiredService<PersonProjectionAppService>() |
|||
.GetListAsync(new PagedAndSortedResultRequestDto()); |
|||
} |
|||
|
|||
//the application service must run the projection itself, not materialize the entities first
|
|||
var selects = commands.Where(x => x.Contains("FROM \"People\"") && !x.Contains("COUNT")).ToList(); |
|||
|
|||
var select = selects.ShouldHaveSingleItem(); |
|||
select.ShouldContain("\"Name\""); |
|||
select.ShouldNotContain("\"Birthday\""); |
|||
select.ShouldNotContain("\"ExtraProperties\""); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Apply_The_Multi_Tenancy_Filter_While_Projecting() |
|||
{ |
|||
var result = await GetRequiredService<PersonProjectionAppService>() |
|||
.GetListAsync(new PagedAndSortedResultRequestDto()); |
|||
|
|||
result.Items.ShouldNotContain(x => x.Name.StartsWith(TestDataBuilder.TenantId1.ToString())); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Use_The_Ambient_Cancellation_Token_While_Projecting() |
|||
{ |
|||
var cancellationTokenProvider = GetRequiredService<ICancellationTokenProvider>(); |
|||
var appService = GetRequiredService<PersonProjectionAppService>(); |
|||
|
|||
//the entity path passes it through Repository.GetAsync, so the projected path must not regress
|
|||
using (cancellationTokenProvider.Use(new CancellationToken(canceled: true))) |
|||
{ |
|||
await Should.ThrowAsync<OperationCanceledException>(async () => |
|||
await appService.GetAsync(TestDataBuilder.UserDouglasId)); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Execute_The_Projected_Query_From_The_Application_Service_For_A_Single_Entity() |
|||
{ |
|||
System.Collections.Concurrent.ConcurrentQueue<string> commands; |
|||
using (SqlCommandCapture.Begin(out commands)) |
|||
{ |
|||
await GetRequiredService<PersonProjectionAppService>().GetAsync(TestDataBuilder.UserDouglasId); |
|||
} |
|||
|
|||
var selects = commands.Where(x => x.Contains("FROM \"People\"")).ToList(); |
|||
|
|||
var select = selects.ShouldHaveSingleItem(); |
|||
select.ShouldContain("\"Name\""); |
|||
select.ShouldNotContain("\"Birthday\""); |
|||
select.ShouldNotContain("\"ExtraProperties\""); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Apply_The_Data_Filters_Of_The_Joined_Aggregate() |
|||
{ |
|||
var cityRepository = GetRequiredService<IRepository<City, Guid>>(); |
|||
|
|||
await WithUnitOfWorkAsync(async () => |
|||
{ |
|||
var london = await cityRepository.GetAsync(TestDataBuilder.LondonCityId); |
|||
await cityRepository.DeleteAsync(london, autoSave: true); |
|||
}); |
|||
|
|||
var result = await GetRequiredService<PersonWithCityAppService>() |
|||
.GetListAsync(new PagedAndSortedResultRequestDto()); |
|||
|
|||
//the soft deleted city is filtered out, the left join still keeps the person
|
|||
result.Items.ShouldContain(x => x.Name == "Douglas" && x.CityName == null); |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
using System; |
|||
using System.Collections.Concurrent; |
|||
using System.Collections.Generic; |
|||
using System.Data.Common; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.EntityFrameworkCore.Diagnostics; |
|||
|
|||
namespace Volo.Abp.EntityFrameworkCore.Applications; |
|||
|
|||
public class SqlCommandCapture : DbCommandInterceptor |
|||
{ |
|||
private static readonly AsyncLocal<ConcurrentQueue<string>> Commands = new(); |
|||
|
|||
public static IDisposable Begin(out ConcurrentQueue<string> commands) |
|||
{ |
|||
commands = new ConcurrentQueue<string>(); |
|||
Commands.Value = commands; |
|||
return new DisposeAction(() => Commands.Value = null); |
|||
} |
|||
|
|||
public override InterceptionResult<DbDataReader> ReaderExecuting( |
|||
DbCommand command, |
|||
CommandEventData eventData, |
|||
InterceptionResult<DbDataReader> result) |
|||
{ |
|||
Commands.Value?.Enqueue(command.CommandText); |
|||
return base.ReaderExecuting(command, eventData, result); |
|||
} |
|||
|
|||
public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync( |
|||
DbCommand command, |
|||
CommandEventData eventData, |
|||
InterceptionResult<DbDataReader> result, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
Commands.Value?.Enqueue(command.CommandText); |
|||
return base.ReaderExecutingAsync(command, eventData, result, cancellationToken); |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Shouldly; |
|||
using Volo.Abp.Mapperly.SampleClasses; |
|||
using Volo.Abp.ObjectMapping; |
|||
using Volo.Abp.Testing; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.Mapperly; |
|||
|
|||
public class AbpMapperlyQueryProjection_Tests : AbpIntegratedTest<MapperlyTestModule> |
|||
{ |
|||
[Fact] |
|||
public void Should_Project_A_Queryable() |
|||
{ |
|||
var queryProjector = ServiceProvider.GetRequiredService<IQueryProjector<MyEntity, MyEntityDto>>(); |
|||
|
|||
var entities = new List<MyEntity> |
|||
{ |
|||
new MyEntity { Id = Guid.NewGuid(), Number = 42 } |
|||
}.AsQueryable(); |
|||
|
|||
var dtos = queryProjector.ProjectTo(entities).ToList(); |
|||
|
|||
dtos.Count.ShouldBe(1); |
|||
dtos[0].Id.ShouldBe(entities.First().Id); |
|||
dtos[0].Number.ShouldBe(42); |
|||
} |
|||
|
|||
|
|||
[Fact] |
|||
public void Should_Register_A_Projector_Only_Once() |
|||
{ |
|||
//MyEntityQueryProjector also matches the class name convention of ExposeServicesAttribute
|
|||
ServiceProvider.GetServices<IQueryProjector<MyEntity, MyEntityDto>>().ShouldHaveSingleItem(); |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
using System.Linq; |
|||
using Riok.Mapperly.Abstractions; |
|||
using Volo.Abp.ObjectMapping; |
|||
|
|||
namespace Volo.Abp.Mapperly.SampleClasses; |
|||
|
|||
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] |
|||
public partial class MyEntityQueryProjector : IQueryProjector<MyEntity, MyEntityDto> |
|||
{ |
|||
public partial IQueryable<MyEntityDto> ProjectTo(IQueryable<MyEntity> source); |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
using System; |
|||
using Volo.Abp.Application.Services; |
|||
using Volo.Abp.Domain.Repositories; |
|||
using Volo.Abp.TestApp.Domain; |
|||
|
|||
namespace Volo.Abp.MongoDB.Applications; |
|||
|
|||
public class PersonProjectionAppService : ReadOnlyAppService<Person, PersonProjectionDto, Guid> |
|||
{ |
|||
public PersonProjectionAppService(IReadOnlyRepository<Person, Guid> repository) |
|||
: base(repository) |
|||
{ |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using System; |
|||
using Volo.Abp.Application.Dtos; |
|||
|
|||
namespace Volo.Abp.MongoDB.Applications; |
|||
|
|||
public class PersonProjectionDto : EntityDto<Guid> |
|||
{ |
|||
public string Name { get; set; } |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
using System.Linq; |
|||
using Volo.Abp.ObjectMapping; |
|||
using Volo.Abp.TestApp.Domain; |
|||
|
|||
namespace Volo.Abp.MongoDB.Applications; |
|||
|
|||
public class PersonProjector : IQueryProjector<Person, PersonProjectionDto> |
|||
{ |
|||
public IQueryable<PersonProjectionDto> ProjectTo(IQueryable<Person> source) |
|||
{ |
|||
return source.Select(person => new PersonProjectionDto |
|||
{ |
|||
Id = person.Id, |
|||
Name = person.Name |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Shouldly; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Volo.Abp.Domain.Entities; |
|||
using Volo.Abp.TestApp; |
|||
using Volo.Abp.TestApp.Domain; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.MongoDB.Applications; |
|||
|
|||
[Collection(MongoTestCollection.Name)] |
|||
public class QueryProjection_Tests : MongoDbTestBase |
|||
{ |
|||
private readonly PersonProjectionAppService _personProjectionAppService; |
|||
|
|||
public QueryProjection_Tests() |
|||
{ |
|||
_personProjectionAppService = GetRequiredService<PersonProjectionAppService>(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Get_A_Projected_Entity() |
|||
{ |
|||
var dto = await _personProjectionAppService.GetAsync(TestDataBuilder.UserDouglasId); |
|||
|
|||
dto.Id.ShouldBe(TestDataBuilder.UserDouglasId); |
|||
dto.Name.ShouldBe("Douglas"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Throw_EntityNotFoundException_For_A_Missing_Entity() |
|||
{ |
|||
await Should.ThrowAsync<EntityNotFoundException<Person>>( |
|||
async () => await _personProjectionAppService.GetAsync(Guid.NewGuid())); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Get_A_Projected_List() |
|||
{ |
|||
var result = await _personProjectionAppService.GetListAsync(new PagedAndSortedResultRequestDto()); |
|||
|
|||
result.TotalCount.ShouldBeGreaterThan(0); |
|||
result.Items.ShouldContain(x => x.Name == "Douglas"); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue