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