mirror of https://github.com/abpframework/abp.git
71 changed files with 1676 additions and 312 deletions
@ -0,0 +1,144 @@ |
|||
name: Auto-merge forward |
|||
|
|||
# Push to a rel-x.y branch opens a merge PR into the next newer rel-* line, |
|||
# or into dev when this line is the newest. Merge of that PR retriggers the |
|||
# next hop, so a bug-fix on rel-1.0 flows rel-1.0 -> rel-1.1 -> ... -> dev. |
|||
on: |
|||
push: |
|||
branches: |
|||
- 'rel-*' |
|||
workflow_dispatch: |
|||
|
|||
concurrency: |
|||
group: auto-merge-forward-${{ github.ref_name }} |
|||
cancel-in-progress: false |
|||
|
|||
permissions: |
|||
contents: read |
|||
|
|||
jobs: |
|||
forward: |
|||
runs-on: ubuntu-latest |
|||
permissions: |
|||
contents: write |
|||
pull-requests: write |
|||
steps: |
|||
- uses: actions/checkout@v4 |
|||
with: |
|||
fetch-depth: 0 |
|||
|
|||
- name: Resolve forward target |
|||
id: target |
|||
run: | |
|||
set -euo pipefail |
|||
SOURCE="${GITHUB_REF_NAME}" |
|||
if [[ ! "$SOURCE" =~ ^rel-[0-9]+\.[0-9]+$ ]]; then |
|||
echo "Not a rel-x.y branch ($SOURCE); skipping." |
|||
echo "skip=true" >> "$GITHUB_OUTPUT" |
|||
exit 0 |
|||
fi |
|||
|
|||
git fetch origin --prune |
|||
|
|||
mapfile -t RELS < <( |
|||
git ls-remote --heads origin 'rel-*' \ |
|||
| awk '{print $2}' \ |
|||
| sed 's|refs/heads/||' \ |
|||
| grep -E '^rel-[0-9]+\.[0-9]+$' \ |
|||
| sort -t. -k1.5,1n -k2,2n |
|||
) |
|||
|
|||
TARGET="dev" |
|||
found=0 |
|||
for branch in "${RELS[@]}"; do |
|||
if [[ "$found" -eq 1 ]]; then |
|||
TARGET="$branch" |
|||
break |
|||
fi |
|||
if [[ "$branch" == "$SOURCE" ]]; then |
|||
found=1 |
|||
fi |
|||
done |
|||
|
|||
if [[ "$found" -eq 0 ]]; then |
|||
echo "::error::Source branch $SOURCE was not listed among origin rel-* heads." |
|||
exit 1 |
|||
fi |
|||
|
|||
if ! git rev-parse --verify "origin/$TARGET" >/dev/null 2>&1; then |
|||
echo "::error::Target branch origin/$TARGET does not exist." |
|||
exit 1 |
|||
fi |
|||
|
|||
if git merge-base --is-ancestor "origin/$SOURCE" "origin/$TARGET"; then |
|||
echo "origin/$SOURCE is already an ancestor of origin/$TARGET; nothing to forward." |
|||
echo "skip=true" >> "$GITHUB_OUTPUT" |
|||
exit 0 |
|||
fi |
|||
|
|||
echo "skip=false" >> "$GITHUB_OUTPUT" |
|||
echo "source=$SOURCE" >> "$GITHUB_OUTPUT" |
|||
echo "target=$TARGET" >> "$GITHUB_OUTPUT" |
|||
echo "Auto-merge forward: $SOURCE -> $TARGET" |
|||
|
|||
- name: Merge into forward branch |
|||
if: steps.target.outputs.skip != 'true' |
|||
id: merge |
|||
run: | |
|||
set -euo pipefail |
|||
SOURCE="${{ steps.target.outputs.source }}" |
|||
TARGET="${{ steps.target.outputs.target }}" |
|||
FORWARD_BRANCH="auto-merge-forward/${SOURCE}-to-${TARGET}-${{ github.run_number }}" |
|||
|
|||
git config user.name "github-actions[bot]" |
|||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com" |
|||
|
|||
git checkout -B "$FORWARD_BRANCH" "origin/$TARGET" |
|||
if git merge --no-edit "origin/$SOURCE"; then |
|||
echo "conflict=false" >> "$GITHUB_OUTPUT" |
|||
else |
|||
git merge --abort |
|||
git checkout -B "$FORWARD_BRANCH" "origin/$SOURCE" |
|||
echo "conflict=true" >> "$GITHUB_OUTPUT" |
|||
echo "::warning::Merge conflict forwarding ${SOURCE} to ${TARGET}. PR left open for manual resolution." |
|||
fi |
|||
|
|||
git push origin "$FORWARD_BRANCH" |
|||
echo "branch=$FORWARD_BRANCH" >> "$GITHUB_OUTPUT" |
|||
|
|||
- name: Create pull request |
|||
if: steps.target.outputs.skip != 'true' |
|||
id: pr |
|||
env: |
|||
GH_TOKEN: ${{ github.token }} |
|||
run: | |
|||
set -euo pipefail |
|||
SOURCE="${{ steps.target.outputs.source }}" |
|||
TARGET="${{ steps.target.outputs.target }}" |
|||
FORWARD_BRANCH="${{ steps.merge.outputs.branch }}" |
|||
CONFLICT="${{ steps.merge.outputs.conflict }}" |
|||
|
|||
BODY="Automated forward merge of \`${SOURCE}\` into \`${TARGET}\`." |
|||
if [[ "$CONFLICT" == "true" ]]; then |
|||
BODY+=$'\n\n**Merge conflict:** this branch is \`${SOURCE}\` as-is. Resolve against \`${TARGET}\` before merging.' |
|||
fi |
|||
|
|||
URL="$(gh pr create \ |
|||
--base "$TARGET" \ |
|||
--head "$FORWARD_BRANCH" \ |
|||
--title "Auto-merge forward ${SOURCE} → ${TARGET}" \ |
|||
--body "$BODY")" |
|||
echo "url=$URL" >> "$GITHUB_OUTPUT" |
|||
echo "Created $URL" |
|||
|
|||
# BOT_SECRET, not github.token: a merge performed with the default token produces a push |
|||
# that triggers no workflow, which would stop the chain at the first hop. |
|||
- name: Approve and auto-merge |
|||
if: steps.target.outputs.skip != 'true' && steps.merge.outputs.conflict != 'true' |
|||
env: |
|||
GH_TOKEN: ${{ secrets.BOT_SECRET }} |
|||
run: | |
|||
set -euo pipefail |
|||
FORWARD_BRANCH="${{ steps.merge.outputs.branch }}" |
|||
gh pr review "$FORWARD_BRANCH" --approve |
|||
gh pr merge "$FORWARD_BRANCH" --merge --auto --delete-branch |
|||
@ -1,37 +0,0 @@ |
|||
name: Merge branch dev with rel-10.7 |
|||
on: |
|||
push: |
|||
branches: |
|||
- rel-10.7 |
|||
permissions: |
|||
contents: read |
|||
|
|||
jobs: |
|||
merge-dev-with-rel-10-7: |
|||
permissions: |
|||
contents: write # for peter-evans/create-pull-request to create branch |
|||
pull-requests: write # for peter-evans/create-pull-request to create a PR |
|||
runs-on: ubuntu-latest |
|||
steps: |
|||
- uses: actions/checkout@v2 |
|||
with: |
|||
ref: dev |
|||
- name: Reset promotion branch |
|||
run: | |
|||
git fetch origin rel-10.7:rel-10.7 |
|||
git reset --hard rel-10.7 |
|||
- name: Create Pull Request |
|||
uses: peter-evans/create-pull-request@v3 |
|||
with: |
|||
branch: auto-merge/rel-10-7/${{github.run_number}} |
|||
title: Merge branch dev with rel-10.7 |
|||
body: This PR generated automatically to merge dev with rel-10.7. Please review the changed files before merging to prevent any errors that may occur. |
|||
draft: true |
|||
token: ${{ github.token }} |
|||
- name: Merge Pull Request |
|||
env: |
|||
GH_TOKEN: ${{ secrets.BOT_SECRET }} |
|||
run: | |
|||
gh pr ready |
|||
gh pr review auto-merge/rel-10-7/${{github.run_number}} --approve |
|||
gh pr merge auto-merge/rel-10-7/${{github.run_number}} --merge --auto --delete-branch |
|||
@ -1,12 +0,0 @@ |
|||
using System.Collections.Generic; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Bundling; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.MalihuCustomScrollbar; |
|||
|
|||
public class MalihuCustomScrollbarPluginScriptBundleContributor : BundleContributor |
|||
{ |
|||
public override void ConfigureBundle(BundleConfigurationContext context) |
|||
{ |
|||
context.Files.AddIfNotContains("/libs/malihu-custom-scrollbar-plugin/jquery.mCustomScrollbar.concat.min.js"); |
|||
} |
|||
} |
|||
@ -1,12 +0,0 @@ |
|||
using System.Collections.Generic; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Bundling; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.MalihuCustomScrollbar; |
|||
|
|||
public class MalihuCustomScrollbarPluginStyleBundleContributor : BundleContributor |
|||
{ |
|||
public override void ConfigureBundle(BundleConfigurationContext context) |
|||
{ |
|||
context.Files.AddIfNotContains("/libs/malihu-custom-scrollbar-plugin/jquery.mCustomScrollbar.css"); |
|||
} |
|||
} |
|||
@ -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"); |
|||
} |
|||
} |
|||
@ -1 +1 @@ |
|||
{"version":3,"sourceRoot":"","sources":["vs.scss"],"names":[],"mappings":"AAGA;EACI;EACA;;AAEA;EACI;EACA;;AAIJ;EACI;;AAGJ;EACI;EACA;;AAGJ;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;;AAIA;EACI;;AAGJ;AAAA;AAAA;AAAA;EAII;;AAIR;AAAA;EAEI;;AAGJ;EACI;;AAEA;EACI;;AAEJ;EACI;;AAIR;AAAA;EAEI;EACA;EACA;;AAGJ;EACI;;AAKA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EAEI;EACA;;AAIJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EAEI;EACA;EACA;EACA;EACA;;AAIJ;EACI;;AAGJ;EACI;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEA;EACI;;AAIR;EACI;EACA;EACA;EACA;;AAEA;EACI;;AAIR;EA5GJ;IA6GQ;IACA;IACA;;EAEA;IACI;IACA;IACA;IACA;IACA;;EAGJ;IACI;IACA;IACA;IACA;;;AAOR;EAEI;;AAGJ;EACI;IAEI;;;;AAOpB;EACI;;AAEA;EACI;EACA;EACA,KAtOK;EAuOL,QAvOK;EAwOL;EACA;EACA;EACA;EACA;EACA;EACA;;AAIQ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAMhB;EACI;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;;AAGJ;EACI;EACA;;AAEJ;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAIR;AAAA;EAEI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;AAAA;AAAA;AAAA;AAAA;EAII;;AAIR;EACI;EACA;;AAEJ;EACI;EACA;;AAEA;EACI;;AAIR;EACI;;AAEA;EAII;;AAIR;EACI;EACA;EACA;;AAIR;EACI;;AAIR;EAEI;EACA;;AAEA;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;;AAKZ;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGI;EACI;;AAIR;EACI;;AAIR;EACI;;AAGI;EACI;;AAGJ;EACI;;AAGI;EACI;;AAQhB;EACI;EACA;;AAIA;EACI;EACA;;AAMA;EACI;;AAOpB;EACI;;AAKhB;EACI;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AACA;EACI;EACA;EACA;EACA;EACA;;AAMA;EACI;EACA;EACA;EACA;EACA;;AACA;EACI;;AAEJ;EACI;EACA;;AAMhB;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAEJ;EACI;;AAGJ;EACI;EACA;;AAKZ;EACI;EACA;;AAEA;EACI;;AAEA;EACI;;AAGJ;EACI;EACA;;AAIR;EACI;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;;AAKJ;EACI;EACA;EACA;;AAGJ;EACI;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAMA;EACI;;AAQhB;EACI;;AAGJ;EACI;EACA;;AAEA;EACI;EACA;EACA;;AAGJ;EACI;EACA;EACA;;AAGJ;AAAA;AAAA;AAAA;EAII;EACA;EACA;;AAGJ;AAAA;AAAA;AAAA;AAAA;AAAA;EAMI;EACA;;AAEA;AAAA;AAAA;AAAA;AAAA;AAAA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAIA;AAAA;AAAA;AAAA;AAAA;AAAA;EACI;;AAKZ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;;AAGI;EACI;;AAIR;EACI;;AAGJ;AAAA;EAEI;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;;AAGJ;EACI;;AAKZ;EACI;EACA;EACA;EACA;;AACA;EACI;EACA;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AACA;EACI;;AAKZ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAMR;EACI;EACA;;AAEA;EACI;;AAIR;EACI;EACA;EACA;;AAKZ;EACI;EACA;;AAEA;EACI;EACA;EACA,KA90BC;EA+0BD;EACA;EACA;;AAGI;EACI;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;;AAMA;EACI;;AAMA;EACI;;AAQxB;EACI;;AAIR;EACI;;AAGJ;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAIR;EACI;EACA;EACA;EACA;EACA,eA15BC;EA25BD;;AAEA;EACI;EACA;EACA;;AACA;EACI;;AAGR;EACI;EACA,SAx6BH;;AA06BD;EACI;EACA;EACA;;AAGJ;EACI;;;AAMhB;EACI;IACI;;EAEA;IACI;;EAGJ;IACI;;EAGJ;AAAA;IAEI;IACA;IACA;IACA;IACA;IACA;IACA;;EAQR;IACI;;EAEA;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAGI;IACI;IACA;IACA;IACA;;EAEA;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEA;IACI;;EAIR;IACI;;EAGJ;IACI;IACA;;EAGJ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEA;IACI;IACA;IACA;;EAEA;IACI;;EAKZ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAIR;IACI;IACA;IACA;;EAIR;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEA;IACI;;EAMZ;IACI;IACA;IACA;IACA;IACA;;EAEA;IACI;;EAGJ;IACI;;EAIA;IACI;;EAEJ;IACI;;EAGJ;IACI;IACA;IACA;IACA;;EAGJ;IACI;IACA;IACA;IACA;;EAKZ;IACI;IACA;IACA;;EAEA;IACI;IACA;;EAIR;IACI;;EAEA;IACI;;;AAMhB;EAIgB;IACI;;EAEJ;IACI;;EAMI;IACI;IACA;;;AAS5B;EAEQ;IACI;IACA;;EAEJ;IAII;;EAHA;IACI;;;AAOhB;EAEQ;IACI;IACA;IACA;IACA;;EAEA;IACI;IACA;IACA","file":"vs.css"} |
|||
{"version":3,"sourceRoot":"","sources":["vs.scss"],"names":[],"mappings":"AAGA;EACI;EACA;;AAEA;EACI;EACA;;AAIJ;EACI;;AAGJ;EACI;EACA;;AAGJ;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;;AAIA;EACI;;AAGJ;AAAA;AAAA;AAAA;EAII;;AAIR;AAAA;EAEI;;AAGJ;EACI;;AAEA;EACI;;AAEJ;EACI;;AAMR;EACI;;AAGJ;EACI;;AAMA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGA;EACI;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGA;EACI;;AAGJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAIJ;EACI;EACA;;AAMZ;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;;AAGA;EACI;EACA;;AAOZ;EAGI;EACA;EACA;;AAGJ;EACI;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEA;EACI;;AAIR;EA5HJ;IA6HQ;IACA;IACA;;EAEA;IACI;IACA;IACA;IACA;;EAEA;IACI;;;AAOhB;EACI;;AAEA;EAHJ;IAIQ;;;;AAMhB;EACI;;AAEA;EACI;EACA;EACA,KA5OK;EA6OL,QA7OK;EA8OL;EACA;EACA;EACA;EACA;EACA;EACA;;AAIQ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAMhB;EACI;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;;AAGJ;EACI;EACA;;AAEJ;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAIR;AAAA;EAEI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;AAAA;AAAA;AAAA;AAAA;EAII;;AAIR;EACI;EACA;;AAEJ;EACI;EACA;;AAEA;EACI;;AAIR;EACI;;AAEA;EAII;;AAIR;EACI;EACA;EACA;;AAIR;EACI;;AAIR;EAEI;EACA;;AAEA;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;;AAKZ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGI;EACI;;AAIR;EACI;;AAIR;EACI;;AAGI;EACI;;AAGJ;EACI;;AAGI;EACI;;AAQhB;EACI;EACA;;AAIA;EACI;EACA;;AAMA;EACI;;AAUhC;EACI;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AACA;EACI;EACA;EACA;EACA;EACA;EACA;;AAMA;EACI;EACA;EACA;EACA;EACA;;AACA;EACI;;AAEJ;EACI;EACA;;AAMhB;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAEJ;EACI;;AAGJ;EACI;EACA;;AAKZ;EACI;EACA;;AAEA;EACI;;AAEA;EACI;;AAGJ;EACI;EACA;;AAIR;EACI;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;;AAKJ;EACI;EACA;EACA;;AAGJ;EACI;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAMA;EACI;;AAQhB;EACI;;AAGJ;EACI;EACA;;AAEA;EACI;EACA;EACA;;AAGJ;EACI;EACA;EACA;;AAGJ;AAAA;AAAA;AAAA;EAII;EACA;EACA;;AAGJ;AAAA;AAAA;AAAA;AAAA;AAAA;EAMI;EACA;;AAEA;AAAA;AAAA;AAAA;AAAA;AAAA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAIA;AAAA;AAAA;AAAA;AAAA;AAAA;EACI;;AAKZ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;;AAGI;EACI;;AAIR;EACI;;AAGJ;AAAA;EAEI;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;;AAGJ;EACI;;AAKZ;EACI;EACA;EACA;EACA;;AACA;EACI;EACA;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AACA;EACI;;AAKZ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAMR;EACI;EACA;;AAEA;EACI;;AAIR;EACI;EACA;EACA;;AAKZ;EACI;EACA;;AAEA;EACI;EACA;EACA,KAp1BC;EAq1BD;EACA;EACA;;AAGI;EACI;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;;AAMA;EACI;;AAMA;EACI;;AAQxB;EACI;;AAIR;EACI;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAIR;EACI;EACA;EACA;EACA;EACA,eAl6BC;EAm6BD;;AAEA;EACI;EACA;EACA;;AACA;EACI;;AAGR;EACI;EACA,SAh7BH;;AAk7BD;EACI;EACA;EACA;;AAGJ;EACI;;;AAMhB;EACI;IACI;;EAEA;IACI;;EAGJ;IACI;;EAGJ;AAAA;IAEI;IACA;IACA;IACA;IACA;IACA;IACA;;EAQR;IACI;;EAEA;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAGI;IACI;IACA;IACA;IACA;;EAEA;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEA;IACI;;EAIR;IACI;;EAGJ;IACI;IACA;;EAGJ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEA;IACI;IACA;IACA;;EAEA;IACI;;EAKZ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAIR;IACI;IACA;IACA;;EAIR;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEA;IACI;;EAMZ;IACI;IACA;IACA;IACA;IACA;;EAEA;IACI;;EAGJ;IACI;;EAIA;IACI;;EAEJ;IACI;;EAGJ;IACI;IACA;IACA;IACA;;EAGJ;IACI;IACA;IACA;IACA;;EAKZ;IACI;IACA;IACA;;EAEA;IACI;IACA;;EAIR;IACI;;EAEA;IACI;;;AAMhB;EAIgB;IACI;;EAEJ;IACI;;EAMI;IACI;IACA;;;AAS5B;EAEQ;IACI;IACA;;EAEJ;IAII;;EAHA;IACI;;;AAOhB;EAEQ;IACI;IACA;IACA;IACA;;EAEA;IACI;IACA;IACA","file":"vs.css"} |
|||
@ -1,129 +0,0 @@ |
|||
## ℹ️ Description |
|||
|
|||
ABP Framework is a complete open-source infrastructure to create modern web applications by following the best practices and conventions of software development. This package is a part of the [ABP Framework](https://abp.io) and contains client-side files. |
|||
For more information, check out the below links: |
|||
|
|||
🔗Official Website: https://abp.io |
|||
|
|||
🔗Commercial Website: https://commercial.abp.io |
|||
|
|||
🔗Commercial Demo: https://commercial.abp.io/demo |
|||
|
|||
🔗GitHub Repository: https://github.com/abpframework/abp |
|||
|
|||
🔗Official Theme: https://www.LeptonTheme.com |
|||
|
|||
🔗Documentation: https://docs.abp.io |
|||
|
|||
🔗Community: https://community.abp.io |
|||
|
|||
🔗Blog: https://blog.abp.io |
|||
|
|||
🔗Books: https://abp.io/books |
|||
|
|||
🔗Twitter: https://twitter.com/abpframework |
|||
|
|||
🔗Discord: https://community.abp.io/discord |
|||
|
|||
🔗Stackoverflow: https://stackoverflow.com/questions/tagged/abp |
|||
|
|||
🔗YouTube: https://www.youtube.com/@Volosoft |
|||
|
|||
|
|||
## 🤔 Why ABP Platform? |
|||
|
|||
Why should you use the ABP.IO Platform instead of creating a new solution from scratch? |
|||
|
|||
You can find the answer here 👉🏻 [Why ABP Platform?](https://docs.abp.io/en/commercial/latest/why-abp-io-platform) |
|||
|
|||
## 🚀 Key Features of the ABP Framework |
|||
|
|||
🟡 Modularity |
|||
|
|||
🟡 Multi-Tenancy |
|||
|
|||
🟡 Bootstrap Tag Helpers |
|||
|
|||
🟡 Dynamic Forms |
|||
|
|||
🟡 Authentication |
|||
|
|||
🟡 Authorization |
|||
|
|||
🟡 Distributed Event Bus |
|||
|
|||
🟡 BLOB Storing |
|||
|
|||
🟡 Text Templating |
|||
|
|||
🟡 Tooling: ABP CLI |
|||
|
|||
🟡 Cross-Cutting Concerns |
|||
|
|||
🟡 Bundling & Minification |
|||
|
|||
🟡 Virtual File System |
|||
|
|||
🟡 Theming |
|||
|
|||
🟡 Background Jobs |
|||
|
|||
🟡 DDD Infrastructure |
|||
|
|||
🟡 Auto REST APIs |
|||
|
|||
🟡 Dynamic Client Proxies |
|||
|
|||
🟡 Multiple Database Providers |
|||
|
|||
🟡 Data filtering |
|||
|
|||
🟡 Test Infrastructure |
|||
|
|||
🟡 Audit Logging |
|||
|
|||
🟡 Object to Object Mapping |
|||
|
|||
🟡 Email & SMS Abstractions |
|||
|
|||
🟡 Localization |
|||
|
|||
🟡 Setting Management |
|||
|
|||
🟡 Extension Methods |
|||
|
|||
🟡 Aspect Oriented Programming |
|||
|
|||
🟡 Dependency Injection |
|||
|
|||
|
|||
## 🧐 How It Works? |
|||
|
|||
The following page explains how you use the ABP.IO Platform as a .NET developer 👉 [How it works?](https://commercial.abp.io/how-it-works) |
|||
|
|||
|
|||
### 📘 Supported Database Providers |
|||
|
|||
🔵 Entity Framework Core |
|||
|
|||
🔵 MongoDB |
|||
|
|||
🔵 Dapper |
|||
|
|||
|
|||
### 🎴 Supported UI Frameworks |
|||
|
|||
🔵 Angular |
|||
|
|||
🔵 Razor Pages |
|||
|
|||
🔵 Blazor Web Assembly |
|||
|
|||
🔵 Blazor Server |
|||
|
|||
🔵 MAUI with Blazor Hybrid |
|||
|
|||
|
|||
## 📫 Bug & Support |
|||
|
|||
Support for open-source ABP Framework client-side packages is available at [GitHub Issues](https://github.com/abpframework/abp/issues), and the commercial support is available at [support.abp.io](https://support.abp.io). |
|||
@ -1,5 +0,0 @@ |
|||
module.exports = { |
|||
mappings: { |
|||
"@node_modules/malihu-custom-scrollbar-plugin/*.*": "@libs/malihu-custom-scrollbar-plugin/" |
|||
} |
|||
} |
|||
@ -1,32 +0,0 @@ |
|||
{ |
|||
"version": "10.7.0-rc.3", |
|||
"name": "@abp/malihu-custom-scrollbar-plugin", |
|||
"repository": { |
|||
"type": "git", |
|||
"url": "https://github.com/abpframework/abp.git", |
|||
"directory": "npm/packs/malihu-custom-scrollbar-plugin" |
|||
}, |
|||
"publishConfig": { |
|||
"access": "public" |
|||
}, |
|||
"dependencies": { |
|||
"@abp/core": "~10.7.0-rc.3", |
|||
"malihu-custom-scrollbar-plugin": "^3.1.5" |
|||
}, |
|||
"gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", |
|||
"homepage": "https://abp.io", |
|||
"license": "LGPL-3.0", |
|||
"keywords": [ |
|||
"aspnetcore", |
|||
"boilerplate", |
|||
"framework", |
|||
"web", |
|||
"best-practices", |
|||
"angular", |
|||
"maui", |
|||
"blazor", |
|||
"mvc", |
|||
"csharp", |
|||
"webapp" |
|||
] |
|||
} |
|||
Loading…
Reference in new issue