diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..24739b1 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,57 @@ +name: Docker Image Core CI/CD + +on: + push: + branches: [main] +env: + IMAGE_NAME: ${{ secrets.DOCKER_REPOSITORY }} + IMAGE_NAME_TAG: ${{ secrets.DOCKER_REPOSITORY }}:v${{ github.run_id }}.${{ github.run_number }} + + +jobs: + build-net-core: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Build .Net Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 5.0.x + - name: dotnet restore + run: dotnet restore ./aspnet-core/src/Bcvp.Blog.Core.HttpApi.Host/Bcvp.Blog.Core.HttpApi.Host.csproj + - name: dotnet publish + run: dotnet publish ./aspnet-core/src/Bcvp.Blog.Core.HttpApi.Host/Bcvp.Blog.Core.HttpApi.Host.csproj --configuration -c Release --no-restore -o ./aspnet-core/app + - name: Docker Image + run: ls + - name: Copy DockerFile + run: cp ./aspnet-core/Dockerfile ./aspnet-core/app + - name: Login to registry + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + registry: registry.us-west-1.aliyuncs.com + - name: Build Image + run: docker build -t ${{ env.IMAGE_NAME_TAG }} ./aspnet-core/app + - name: Push Image + run: docker push ${{ env.IMAGE_NAME_TAG }} + + pull-docker: + needs: [build-net-core] + name: Pull Docker + runs-on: ubuntu-latest + steps: + - name: Deploy + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.HOST }} + username: ${{ secrets.HOST_USERNAME }} + password: ${{ secrets.HOST_PASSWORD }} + port: ${{ secrets.HOST_PORT }} + script: | + docker stop $(docker ps -a | grep ${{ env.IMAGE_NAME }} | awk '{print $1}') + docker rm -f $(docker ps -a | grep ${{ env.IMAGE_NAME }} | awk '{print $1}') + docker rmi -f $(docker images | grep ${{ env.IMAGE_NAME }} | awk '{print $3}') + docker login --username=${{ secrets.DOCKER_USERNAME }} --password ${{ secrets.DOCKER_PASSWORD }} registry.cn-hangzhou.aliyuncs.com + docker pull ${{ env.IMAGE_NAME_TAG }} + docker run -d -p 8080:80 ${{ env.IMAGE_NAME_TAG }} diff --git a/README.md b/README.md index 3d8cf58..065283f 100644 --- a/README.md +++ b/README.md @@ -1 +1,35 @@ -# ABPvNext.Blog.Core \ No newline at end of file +# ABP vNext Blog.Core +Bcvp社区的Abp vNext教程,该教程适用于有过1-2年的.Net Core开发者学习,编程新人可以在学习完老张博客文章后,进行阅读。 + + + +## 从零开始学Abp vNext + +[01 | 介绍](https://www.cnblogs.com/MrChuJiu/p/15146662.html) + +[02 | 框架搭建](https://www.cnblogs.com/MrChuJiu/p/15146662.html) + +[03 | 分层架构](https://www.cnblogs.com/MrChuJiu/p/15160110.html) + +[04 | 领域构建](https://www.cnblogs.com/MrChuJiu/p/15174460.html) + +[05 | 博客聚合](https://www.cnblogs.com/MrChuJiu/p/15192406.html) + +[06 | 文章聚合上](https://www.cnblogs.com/MrChuJiu/p/15213166.html) + +[07 | 文章聚合下](https://www.cnblogs.com/MrChuJiu/p/15233997.html) + +[08 | 标签聚合功能](https://www.cnblogs.com/MrChuJiu/p/15261007.html) + +[09 | 评论聚合功能](https://www.cnblogs.com/MrChuJiu/p/15307159.html) + +[10 | 权限](https://www.cnblogs.com/MrChuJiu/p/15322738.html) + +[11 | 测试](https://www.cnblogs.com/MrChuJiu/p/15386460.html) + +[疑难杂症 | 认证授权](https://www.cnblogs.com/MrChuJiu/p/15166090.html) + +[疑难杂症 | Nginx反向代理ids4](https://www.cnblogs.com/MrChuJiu/p/15262001.html) + +[疑难杂症 | 浅谈扩展属性与多用户设计](https://www.cnblogs.com/MrChuJiu/p/15341992.html) + diff --git a/aspnet-core/.dockerignore b/aspnet-core/.dockerignore new file mode 100644 index 0000000..3729ff0 --- /dev/null +++ b/aspnet-core/.dockerignore @@ -0,0 +1,25 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/aspnet-core/Dockerfile b/aspnet-core/Dockerfile new file mode 100644 index 0000000..7bb2844 --- /dev/null +++ b/aspnet-core/Dockerfile @@ -0,0 +1,9 @@ +#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base + +WORKDIR /app +EXPOSE 80 +EXPOSE 443 +COPY . . +ENTRYPOINT ["dotnet", "Bcvp.Blog.Core.HttpApi.Host.dll"] diff --git a/aspnet-core/src/Bcvp.Blog.Core.Application.Contracts/BlogCore/Posts/IPostAppService.cs b/aspnet-core/src/Bcvp.Blog.Core.Application.Contracts/BlogCore/Posts/IPostAppService.cs index e68f3fc..dd32292 100644 --- a/aspnet-core/src/Bcvp.Blog.Core.Application.Contracts/BlogCore/Posts/IPostAppService.cs +++ b/aspnet-core/src/Bcvp.Blog.Core.Application.Contracts/BlogCore/Posts/IPostAppService.cs @@ -9,7 +9,7 @@ namespace Bcvp.Blog.Core.BlogCore.Posts { public interface IPostAppService : IApplicationService { - Task> GetListByBlogIdAndTagName(Guid blogId, string tagName); + Task> GetListByBlogIdAndTagNameAsync(Guid blogId, string tagName); Task> GetTimeOrderedListAsync(Guid blogId); diff --git a/aspnet-core/src/Bcvp.Blog.Core.Application.Contracts/BlogCore/Tagging/ITagAppService.cs b/aspnet-core/src/Bcvp.Blog.Core.Application.Contracts/BlogCore/Tagging/ITagAppService.cs index c135393..156f026 100644 --- a/aspnet-core/src/Bcvp.Blog.Core.Application.Contracts/BlogCore/Tagging/ITagAppService.cs +++ b/aspnet-core/src/Bcvp.Blog.Core.Application.Contracts/BlogCore/Tagging/ITagAppService.cs @@ -8,7 +8,7 @@ namespace Bcvp.Blog.Core.BlogCore.Tagging { public interface ITagAppService : IApplicationService { - Task> GetPopularTags(Guid blogId, GetPopularTagsInput input); + Task> GetPopularTagsAsync(Guid blogId, GetPopularTagsInput input); } } diff --git a/aspnet-core/src/Bcvp.Blog.Core.Application/BlogCore/Comments/CommentAppService.cs b/aspnet-core/src/Bcvp.Blog.Core.Application/BlogCore/Comments/CommentAppService.cs index fc48d64..fca30f0 100644 --- a/aspnet-core/src/Bcvp.Blog.Core.Application/BlogCore/Comments/CommentAppService.cs +++ b/aspnet-core/src/Bcvp.Blog.Core.Application/BlogCore/Comments/CommentAppService.cs @@ -4,6 +4,7 @@ using System.Text; using System.Threading.Tasks; using Bcvp.Blog.Core.BlogCore.Posts; +using Bcvp.Blog.Core.BlogCore.Users; using Microsoft.AspNetCore.Authorization; using Volo.Abp.Guids; using Volo.Abp.Identity; @@ -13,12 +14,12 @@ namespace Bcvp.Blog.Core.BlogCore.Comments { public class CommentAppService : CoreAppService, ICommentAppService { - private IUserLookupService UserLookupService { get; } + protected IBlogUserLookupService UserLookupService; private readonly ICommentRepository _commentRepository; private readonly IGuidGenerator _guidGenerator; - public CommentAppService(ICommentRepository commentRepository, IGuidGenerator guidGenerator, IUserLookupService userLookupService) + public CommentAppService(ICommentRepository commentRepository, IGuidGenerator guidGenerator, IBlogUserLookupService userLookupService) { _commentRepository = commentRepository; _guidGenerator = guidGenerator; @@ -42,7 +43,7 @@ public async Task> GetHierarchicalListOfPostAsync(Gu if (creatorUser != null && !userDictionary.ContainsKey(creatorUser.Id)) { - userDictionary.Add(creatorUser.Id, ObjectMapper.Map(creatorUser)); + userDictionary.Add(creatorUser.Id, ObjectMapper.Map(creatorUser)); } } } diff --git a/aspnet-core/src/Bcvp.Blog.Core.Application/BlogCore/Posts/PostAppService.cs b/aspnet-core/src/Bcvp.Blog.Core.Application/BlogCore/Posts/PostAppService.cs index dc8825e..9a17923 100644 --- a/aspnet-core/src/Bcvp.Blog.Core.Application/BlogCore/Posts/PostAppService.cs +++ b/aspnet-core/src/Bcvp.Blog.Core.Application/BlogCore/Posts/PostAppService.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Bcvp.Blog.Core.BlogCore.Comments; using Bcvp.Blog.Core.BlogCore.Tagging; +using Bcvp.Blog.Core.BlogCore.Users; using Bcvp.Blog.Core.Permissions; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Caching.Distributed; @@ -18,7 +19,7 @@ namespace Bcvp.Blog.Core.BlogCore.Posts { public class PostAppService : CoreAppService, IPostAppService { - private IUserLookupService UserLookupService { get; } + protected IBlogUserLookupService UserLookupService { get; } private readonly IPostRepository _postRepository; private readonly ILocalEventBus _localEventBus; @@ -26,7 +27,7 @@ public class PostAppService : CoreAppService, IPostAppService private readonly ICommentRepository _commentRepository; private readonly IDistributedCache> _postsCache; - public PostAppService(IPostRepository postRepository, ILocalEventBus localEventBus, ITagRepository tagRepository, IUserLookupService userLookupService, ICommentRepository commentRepository, IDistributedCache> postsCache) + public PostAppService(IPostRepository postRepository, ILocalEventBus localEventBus, ITagRepository tagRepository, IBlogUserLookupService userLookupService, ICommentRepository commentRepository, IDistributedCache> postsCache) { _postRepository = postRepository; _localEventBus = localEventBus; @@ -37,7 +38,7 @@ public PostAppService(IPostRepository postRepository, ILocalEventBus localEventB } - public async Task> GetListByBlogIdAndTagName(Guid id, string tagName) + public async Task> GetListByBlogIdAndTagNameAsync(Guid id, string tagName) { // 根据blogId查询文章数据 var posts = await _postRepository.GetPostsByBlogId(id); @@ -67,7 +68,7 @@ public async Task> GetListByBlogIdAndTagName(G var creatorUser = await UserLookupService.FindByIdAsync(postDto.CreatorId.Value); if (creatorUser != null) { - userDictionary[creatorUser.Id] = ObjectMapper.Map(creatorUser); + userDictionary[creatorUser.Id] = ObjectMapper.Map(creatorUser); } } @@ -102,7 +103,7 @@ public async Task> GetTimeOrderedListAsync(Gui var creatorUser = await UserLookupService.FindByIdAsync(post.CreatorId.Value); if (creatorUser != null) { - post.Writer = ObjectMapper.Map(creatorUser); + post.Writer = ObjectMapper.Map(creatorUser); } } } @@ -125,7 +126,7 @@ public async Task GetForReadingAsync(GetPostInput input) { var creatorUser = await UserLookupService.FindByIdAsync(postDto.CreatorId.Value); - postDto.Writer = ObjectMapper.Map(creatorUser); + postDto.Writer = ObjectMapper.Map(creatorUser); } return postDto; @@ -143,7 +144,7 @@ public async Task GetAsync(Guid id) { var creatorUser = await UserLookupService.FindByIdAsync(postDto.CreatorId.Value); - postDto.Writer = ObjectMapper.Map(creatorUser); + postDto.Writer = ObjectMapper.Map(creatorUser); } return postDto; diff --git a/aspnet-core/src/Bcvp.Blog.Core.Application/BlogCore/Tagging/TagAppService.cs b/aspnet-core/src/Bcvp.Blog.Core.Application/BlogCore/Tagging/TagAppService.cs index f555a46..bbe66aa 100644 --- a/aspnet-core/src/Bcvp.Blog.Core.Application/BlogCore/Tagging/TagAppService.cs +++ b/aspnet-core/src/Bcvp.Blog.Core.Application/BlogCore/Tagging/TagAppService.cs @@ -15,7 +15,7 @@ public TagAppService(ITagRepository tagRepository) _tagRepository = tagRepository; } - public async Task> GetPopularTags(Guid blogId, GetPopularTagsInput input) + public async Task> GetPopularTagsAsync(Guid blogId, GetPopularTagsInput input) { var postTags = (await _tagRepository.GetListAsync(blogId)).OrderByDescending(t=>t.UsageCount) .WhereIf(input.MinimumPostCount != null, t=>t.UsageCount >= input.MinimumPostCount) diff --git a/aspnet-core/src/Bcvp.Blog.Core.Application/CoreApplicationModule.cs b/aspnet-core/src/Bcvp.Blog.Core.Application/CoreApplicationModule.cs index b8b406e..a7f68e3 100644 --- a/aspnet-core/src/Bcvp.Blog.Core.Application/CoreApplicationModule.cs +++ b/aspnet-core/src/Bcvp.Blog.Core.Application/CoreApplicationModule.cs @@ -33,6 +33,7 @@ public override void ConfigureServices(ServiceConfigurationContext context) options.AddMaps(); }); + // 注册 Configure(options => { options.AddPolicy("BloggingUpdatePolicy", policy => policy.Requirements.Add(CommonOperations.Update)); diff --git a/aspnet-core/src/Bcvp.Blog.Core.Domain/BlogCore/Users/BlogUser.cs b/aspnet-core/src/Bcvp.Blog.Core.Domain/BlogCore/Users/BlogUser.cs new file mode 100644 index 0000000..1738bcd --- /dev/null +++ b/aspnet-core/src/Bcvp.Blog.Core.Domain/BlogCore/Users/BlogUser.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Volo.Abp.Domain.Entities; +using Volo.Abp.Users; + +namespace Bcvp.Blog.Core.BlogCore.Users +{ + public class BlogUser : AggregateRoot, IUser, IUpdateUserData + { + public virtual Guid? TenantId { get; protected set; } + + public virtual string UserName { get; protected set; } + + public virtual string Email { get; protected set; } + + public virtual string Name { get; set; } + + public virtual string Surname { get; set; } + + public virtual bool EmailConfirmed { get; protected set; } + + public virtual string PhoneNumber { get; protected set; } + + public virtual bool PhoneNumberConfirmed { get; protected set; } + + protected BlogUser() + { + + } + + public BlogUser(IUserData user) + : base(user.Id) + { + TenantId = user.TenantId; + UpdateInternal(user); + } + + public virtual bool Update(IUserData user) + { + if (Id != user.Id) + { + throw new ArgumentException($"Given User's Id '{user.Id}' does not match to this User's Id '{Id}'"); + } + + if (TenantId != user.TenantId) + { + throw new ArgumentException($"Given User's TenantId '{user.TenantId}' does not match to this User's TenantId '{TenantId}'"); + } + + if (Equals(user)) + { + return false; + } + + UpdateInternal(user); + return true; + } + + protected virtual bool Equals(IUserData user) + { + return Id == user.Id && + TenantId == user.TenantId && + UserName == user.UserName && + Name == user.Name && + Surname == user.Surname && + Email == user.Email && + EmailConfirmed == user.EmailConfirmed && + PhoneNumber == user.PhoneNumber && + PhoneNumberConfirmed == user.PhoneNumberConfirmed; + } + + protected virtual void UpdateInternal(IUserData user) + { + Email = user.Email; + Name = user.Name; + Surname = user.Surname; + EmailConfirmed = user.EmailConfirmed; + PhoneNumber = user.PhoneNumber; + PhoneNumberConfirmed = user.PhoneNumberConfirmed; + UserName = user.UserName; + } + } +} diff --git a/aspnet-core/src/Bcvp.Blog.Core.Domain/BlogCore/Users/BlogUserLookupService.cs b/aspnet-core/src/Bcvp.Blog.Core.Domain/BlogCore/Users/BlogUserLookupService.cs new file mode 100644 index 0000000..3628aa8 --- /dev/null +++ b/aspnet-core/src/Bcvp.Blog.Core.Domain/BlogCore/Users/BlogUserLookupService.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Volo.Abp.Identity; +using Volo.Abp.Uow; +using Volo.Abp.Users; + +namespace Bcvp.Blog.Core.BlogCore.Users +{ + public class BlogUserLookupService : UserLookupService, IBlogUserLookupService + { + public BlogUserLookupService( + IBlogUserRepository userRepository, + IUnitOfWorkManager unitOfWorkManager) + : base( + userRepository, + unitOfWorkManager) + { + + } + + protected override BlogUser CreateUser(IUserData externalUser) + { + return new BlogUser(externalUser); + } + } +} diff --git a/aspnet-core/src/Bcvp.Blog.Core.Domain/BlogCore/Users/IBlogUserLookupService.cs b/aspnet-core/src/Bcvp.Blog.Core.Domain/BlogCore/Users/IBlogUserLookupService.cs new file mode 100644 index 0000000..12c14ef --- /dev/null +++ b/aspnet-core/src/Bcvp.Blog.Core.Domain/BlogCore/Users/IBlogUserLookupService.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Volo.Abp.Identity; +using Volo.Abp.Users; + +namespace Bcvp.Blog.Core.BlogCore.Users +{ + public interface IBlogUserLookupService : IUserLookupService + { + + } +} diff --git a/aspnet-core/src/Bcvp.Blog.Core.Domain/BlogCore/Users/IBlogUserRepository.cs b/aspnet-core/src/Bcvp.Blog.Core.Domain/BlogCore/Users/IBlogUserRepository.cs new file mode 100644 index 0000000..6a78f13 --- /dev/null +++ b/aspnet-core/src/Bcvp.Blog.Core.Domain/BlogCore/Users/IBlogUserRepository.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Users; + +namespace Bcvp.Blog.Core.BlogCore.Users +{ + public interface IBlogUserRepository : IBasicRepository, IUserRepository + { + Task> GetUsersAsync(int maxCount, string filter, CancellationToken cancellationToken = default); + } +} diff --git a/aspnet-core/src/Bcvp.Blog.Core.EntityFrameworkCore/BlogCore/Users/EfCoreBlogUserRepository.cs b/aspnet-core/src/Bcvp.Blog.Core.EntityFrameworkCore/BlogCore/Users/EfCoreBlogUserRepository.cs new file mode 100644 index 0000000..7224d0e --- /dev/null +++ b/aspnet-core/src/Bcvp.Blog.Core.EntityFrameworkCore/BlogCore/Users/EfCoreBlogUserRepository.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Bcvp.Blog.Core.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; +using Volo.Abp.Users.EntityFrameworkCore; + +namespace Bcvp.Blog.Core.BlogCore.Users +{ + public class EfCoreBlogUserRepository : EfCoreUserRepositoryBase, IBlogUserRepository + { + public EfCoreBlogUserRepository(IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + + } + + public async Task> GetUsersAsync(int maxCount, string filter, CancellationToken cancellationToken = default) + { + return await DbSet + .WhereIf(!string.IsNullOrWhiteSpace(filter), x => x.UserName.Contains(filter)) + .Take(maxCount).ToListAsync(cancellationToken); + } + } +} diff --git a/aspnet-core/src/Bcvp.Blog.Core.EntityFrameworkCore/EntityFrameworkCore/CoreEfCoreEntityExtensionMappings.cs b/aspnet-core/src/Bcvp.Blog.Core.EntityFrameworkCore/EntityFrameworkCore/CoreEfCoreEntityExtensionMappings.cs index 134ea40..4692bcc 100644 --- a/aspnet-core/src/Bcvp.Blog.Core.EntityFrameworkCore/EntityFrameworkCore/CoreEfCoreEntityExtensionMappings.cs +++ b/aspnet-core/src/Bcvp.Blog.Core.EntityFrameworkCore/EntityFrameworkCore/CoreEfCoreEntityExtensionMappings.cs @@ -4,6 +4,7 @@ using Bcvp.Blog.Core.BlogCore.Comments; using Bcvp.Blog.Core.BlogCore.Posts; using Bcvp.Blog.Core.BlogCore.Tagging; +using Bcvp.Blog.Core.BlogCore.Users; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using Volo.Abp; @@ -63,6 +64,15 @@ public static void ConfigureBcvpBlogCore([NotNull] this ModelBuilder builder) return; } + builder.Entity(b => + { + b.ToTable(CoreConsts.DbTablePrefix + "Users", CoreConsts.DbSchema); + + b.ConfigureByConvention(); + b.ConfigureAbpUser(); + + b.ApplyObjectExtensionMappings(); + }); builder.Entity(b => { diff --git a/aspnet-core/src/Bcvp.Blog.Core.EntityFrameworkCore/EntityFrameworkCore/CoreEntityFrameworkCoreModule.cs b/aspnet-core/src/Bcvp.Blog.Core.EntityFrameworkCore/EntityFrameworkCore/CoreEntityFrameworkCoreModule.cs index 051cb6b..78be6c3 100644 --- a/aspnet-core/src/Bcvp.Blog.Core.EntityFrameworkCore/EntityFrameworkCore/CoreEntityFrameworkCoreModule.cs +++ b/aspnet-core/src/Bcvp.Blog.Core.EntityFrameworkCore/EntityFrameworkCore/CoreEntityFrameworkCoreModule.cs @@ -1,4 +1,8 @@ using Bcvp.Blog.Core.BlogCore.Blogs; +using Bcvp.Blog.Core.BlogCore.Comments; +using Bcvp.Blog.Core.BlogCore.Posts; +using Bcvp.Blog.Core.BlogCore.Tagging; +using Bcvp.Blog.Core.BlogCore.Users; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.AuditLogging.EntityFrameworkCore; using Volo.Abp.BackgroundJobs.EntityFrameworkCore; @@ -40,6 +44,10 @@ public override void ConfigureServices(ServiceConfigurationContext context) /* Remove "includeAllEntities: true" to create * default repositories only for aggregate roots */ options.AddRepository(); + options.AddRepository(); + options.AddRepository(); + options.AddRepository(); + options.AddRepository(); options.AddDefaultRepositories(includeAllEntities: true); }); diff --git a/aspnet-core/src/Bcvp.Blog.Core.EntityFrameworkCore/Migrations/20211009064551_AddBlogUser.Designer.cs b/aspnet-core/src/Bcvp.Blog.Core.EntityFrameworkCore/Migrations/20211009064551_AddBlogUser.Designer.cs new file mode 100644 index 0000000..b8692d8 --- /dev/null +++ b/aspnet-core/src/Bcvp.Blog.Core.EntityFrameworkCore/Migrations/20211009064551_AddBlogUser.Designer.cs @@ -0,0 +1,2689 @@ +// +using System; +using Bcvp.Blog.Core.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Volo.Abp.EntityFrameworkCore; + +namespace Bcvp.Blog.Core.Migrations +{ + [DbContext(typeof(CoreDbContext))] + [Migration("20211009064551_AddBlogUser")] + partial class AddBlogUser + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.SqlServer) + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("ProductVersion", "5.0.10") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Bcvp.Blog.Core.BlogCore.Blogs.Blog", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)") + .HasColumnName("Description"); + + b.Property("ExtraProperties") + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("Name"); + + b.Property("ShortName") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("nvarchar(32)") + .HasColumnName("ShortName"); + + b.HasKey("Id"); + + b.ToTable("AppBlogs"); + }); + + modelBuilder.Entity("Bcvp.Blog.Core.BlogCore.Comments.Comment", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("PostId") + .HasColumnType("uniqueidentifier") + .HasColumnName("PostId"); + + b.Property("RepliedCommentId") + .HasColumnType("uniqueidentifier") + .HasColumnName("RepliedCommentId"); + + b.Property("Text") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)") + .HasColumnName("Text"); + + b.HasKey("Id"); + + b.HasIndex("PostId"); + + b.HasIndex("RepliedCommentId"); + + b.ToTable("AppComments"); + }); + + modelBuilder.Entity("Bcvp.Blog.Core.BlogCore.Posts.Post", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("BlogId") + .HasColumnType("uniqueidentifier") + .HasColumnName("BlogId"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("Content") + .HasMaxLength(1048576) + .HasColumnType("nvarchar(max)") + .HasColumnName("Content"); + + b.Property("CoverImage") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("CoverImage"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)") + .HasColumnName("Description"); + + b.Property("ExtraProperties") + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("ReadCount") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("Title"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("Url"); + + b.HasKey("Id"); + + b.ToTable("AppPosts"); + }); + + modelBuilder.Entity("Bcvp.Blog.Core.BlogCore.Tagging.Tag", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("BlogId") + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("Description"); + + b.Property("ExtraProperties") + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("Name"); + + b.Property("UsageCount") + .HasColumnType("int") + .HasColumnName("UsageCount"); + + b.HasKey("Id"); + + b.ToTable("AppTags"); + }); + + modelBuilder.Entity("Bcvp.Blog.Core.BlogCore.Users.BlogUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("Email"); + + b.Property("EmailConfirmed") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("EmailConfirmed"); + + b.Property("ExtraProperties") + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("Name") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("Name"); + + b.Property("PhoneNumber") + .HasMaxLength(16) + .HasColumnType("nvarchar(16)") + .HasColumnName("PhoneNumber"); + + b.Property("PhoneNumberConfirmed") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("PhoneNumberConfirmed"); + + b.Property("Surname") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("Surname"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("UserName"); + + b.HasKey("Id"); + + b.ToTable("AppUsers"); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApplicationName") + .HasMaxLength(96) + .HasColumnType("nvarchar(96)") + .HasColumnName("ApplicationName"); + + b.Property("BrowserInfo") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("BrowserInfo"); + + b.Property("ClientId") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("ClientId"); + + b.Property("ClientIpAddress") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("ClientIpAddress"); + + b.Property("ClientName") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("ClientName"); + + b.Property("Comments") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("Comments"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("CorrelationId"); + + b.Property("Exceptions") + .HasColumnType("nvarchar(max)"); + + b.Property("ExecutionDuration") + .HasColumnType("int") + .HasColumnName("ExecutionDuration"); + + b.Property("ExecutionTime") + .HasColumnType("datetime2"); + + b.Property("ExtraProperties") + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("HttpMethod") + .HasMaxLength(16) + .HasColumnType("nvarchar(16)") + .HasColumnName("HttpMethod"); + + b.Property("HttpStatusCode") + .HasColumnType("int") + .HasColumnName("HttpStatusCode"); + + b.Property("ImpersonatorTenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("ImpersonatorTenantId"); + + b.Property("ImpersonatorUserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("ImpersonatorUserId"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.Property("TenantName") + .HasColumnType("nvarchar(max)"); + + b.Property("Url") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("Url"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("UserId"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("UserName"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ExecutionTime"); + + b.HasIndex("TenantId", "UserId", "ExecutionTime"); + + b.ToTable("AbpAuditLogs"); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.AuditLogAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AuditLogId") + .HasColumnType("uniqueidentifier") + .HasColumnName("AuditLogId"); + + b.Property("ExecutionDuration") + .HasColumnType("int") + .HasColumnName("ExecutionDuration"); + + b.Property("ExecutionTime") + .HasColumnType("datetime2") + .HasColumnName("ExecutionTime"); + + b.Property("ExtraProperties") + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("MethodName") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("MethodName"); + + b.Property("Parameters") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("Parameters"); + + b.Property("ServiceName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("ServiceName"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AuditLogId"); + + b.HasIndex("TenantId", "ServiceName", "MethodName", "ExecutionTime"); + + b.ToTable("AbpAuditLogActions"); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.EntityChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AuditLogId") + .HasColumnType("uniqueidentifier") + .HasColumnName("AuditLogId"); + + b.Property("ChangeTime") + .HasColumnType("datetime2") + .HasColumnName("ChangeTime"); + + b.Property("ChangeType") + .HasColumnType("tinyint") + .HasColumnName("ChangeType"); + + b.Property("EntityId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("EntityId"); + + b.Property("EntityTenantId") + .HasColumnType("uniqueidentifier"); + + b.Property("EntityTypeFullName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("EntityTypeFullName"); + + b.Property("ExtraProperties") + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AuditLogId"); + + b.HasIndex("TenantId", "EntityTypeFullName", "EntityId"); + + b.ToTable("AbpEntityChanges"); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.EntityPropertyChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("EntityChangeId") + .HasColumnType("uniqueidentifier"); + + b.Property("NewValue") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("NewValue"); + + b.Property("OriginalValue") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("OriginalValue"); + + b.Property("PropertyName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("PropertyName"); + + b.Property("PropertyTypeFullName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("PropertyTypeFullName"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("EntityChangeId"); + + b.ToTable("AbpEntityPropertyChanges"); + }); + + modelBuilder.Entity("Volo.Abp.BackgroundJobs.BackgroundJobRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("ExtraProperties") + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("IsAbandoned") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("JobArgs") + .IsRequired() + .HasMaxLength(1048576) + .HasColumnType("nvarchar(max)"); + + b.Property("JobName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("LastTryTime") + .HasColumnType("datetime2"); + + b.Property("NextTryTime") + .HasColumnType("datetime2"); + + b.Property("Priority") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint") + .HasDefaultValue((byte)15); + + b.Property("TryCount") + .ValueGeneratedOnAdd() + .HasColumnType("smallint") + .HasDefaultValue((short)0); + + b.HasKey("Id"); + + b.HasIndex("IsAbandoned", "NextTryTime"); + + b.ToTable("AbpBackgroundJobs"); + }); + + modelBuilder.Entity("Volo.Abp.FeatureManagement.FeatureValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ProviderKey") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ProviderName") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.HasKey("Id"); + + b.HasIndex("Name", "ProviderName", "ProviderKey"); + + b.ToTable("AbpFeatureValues"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityClaimType", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("ExtraProperties") + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("IsStatic") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("Regex") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.Property("RegexDescription") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("Required") + .HasColumnType("bit"); + + b.Property("ValueType") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("AbpClaimTypes"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityLinkUser", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("SourceTenantId") + .HasColumnType("uniqueidentifier"); + + b.Property("SourceUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("TargetTenantId") + .HasColumnType("uniqueidentifier"); + + b.Property("TargetUserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("SourceUserId", "SourceTenantId", "TargetUserId", "TargetTenantId") + .IsUnique() + .HasFilter("[SourceTenantId] IS NOT NULL AND [TargetTenantId] IS NOT NULL"); + + b.ToTable("AbpLinkUsers"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ExtraProperties") + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("IsDefault") + .HasColumnType("bit") + .HasColumnName("IsDefault"); + + b.Property("IsPublic") + .HasColumnType("bit") + .HasColumnName("IsPublic"); + + b.Property("IsStatic") + .HasColumnType("bit") + .HasColumnName("IsStatic"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName"); + + b.ToTable("AbpRoles"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("ClaimType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("ClaimValue") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AbpRoleClaims"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentitySecurityLog", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("Action") + .HasMaxLength(96) + .HasColumnType("nvarchar(96)"); + + b.Property("ApplicationName") + .HasMaxLength(96) + .HasColumnType("nvarchar(96)"); + + b.Property("BrowserInfo") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.Property("ClientId") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ClientIpAddress") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CreationTime") + .HasColumnType("datetime2"); + + b.Property("ExtraProperties") + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("Identity") + .HasMaxLength(96) + .HasColumnType("nvarchar(96)"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.Property("TenantName") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Action"); + + b.HasIndex("TenantId", "ApplicationName"); + + b.HasIndex("TenantId", "Identity"); + + b.HasIndex("TenantId", "UserId"); + + b.ToTable("AbpSecurityLogs"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("AccessFailedCount") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0) + .HasColumnName("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("Email"); + + b.Property("EmailConfirmed") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("EmailConfirmed"); + + b.Property("ExtraProperties") + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsExternal") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsExternal"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("LockoutEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("LockoutEnabled"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("Name"); + + b.Property("NormalizedEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("NormalizedEmail"); + + b.Property("NormalizedUserName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("NormalizedUserName"); + + b.Property("PasswordHash") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("PasswordHash"); + + b.Property("PhoneNumber") + .HasMaxLength(16) + .HasColumnType("nvarchar(16)") + .HasColumnName("PhoneNumber"); + + b.Property("PhoneNumberConfirmed") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("PhoneNumberConfirmed"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("SecurityStamp"); + + b.Property("Surname") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("Surname"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.Property("TwoFactorEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("TwoFactorEnabled"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("UserName"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("NormalizedEmail"); + + b.HasIndex("NormalizedUserName"); + + b.HasIndex("UserName"); + + b.ToTable("AbpUsers"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("ClaimType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("ClaimValue") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AbpUserClaims"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserLogin", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("LoginProvider") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ProviderDisplayName") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ProviderKey") + .IsRequired() + .HasMaxLength(196) + .HasColumnType("nvarchar(196)"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.HasKey("UserId", "LoginProvider"); + + b.HasIndex("LoginProvider", "ProviderKey"); + + b.ToTable("AbpUserLogins"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserOrganizationUnit", b => + { + b.Property("OrganizationUnitId") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.HasKey("OrganizationUnitId", "UserId"); + + b.HasIndex("UserId", "OrganizationUnitId"); + + b.ToTable("AbpUserOrganizationUnits"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId", "UserId"); + + b.ToTable("AbpUserRoles"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("LoginProvider") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Name") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AbpUserTokens"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnit", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(95) + .HasColumnType("nvarchar(95)") + .HasColumnName("Code"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("DisplayName"); + + b.Property("ExtraProperties") + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("ParentId") + .HasColumnType("uniqueidentifier"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("Code"); + + b.HasIndex("ParentId"); + + b.ToTable("AbpOrganizationUnits"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnitRole", b => + { + b.Property("OrganizationUnitId") + .HasColumnType("uniqueidentifier"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.HasKey("OrganizationUnitId", "RoleId"); + + b.HasIndex("RoleId", "OrganizationUnitId"); + + b.ToTable("AbpOrganizationUnitRoles"); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.ApiResources.ApiResource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AllowedAccessTokenSigningAlgorithms") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("DisplayName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Enabled") + .HasColumnType("bit"); + + b.Property("ExtraProperties") + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ShowInDiscoveryDocument") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("IdentityServerApiResources"); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.ApiResources.ApiResourceClaim", b => + { + b.Property("ApiResourceId") + .HasColumnType("uniqueidentifier"); + + b.Property("Type") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("ApiResourceId", "Type"); + + b.ToTable("IdentityServerApiResourceClaims"); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.ApiResources.ApiResourceProperty", b => + { + b.Property("ApiResourceId") + .HasColumnType("uniqueidentifier"); + + b.Property("Key") + .HasMaxLength(250) + .HasColumnType("nvarchar(250)"); + + b.Property("Value") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.HasKey("ApiResourceId", "Key", "Value"); + + b.ToTable("IdentityServerApiResourceProperties"); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.ApiResources.ApiResourceScope", b => + { + b.Property("ApiResourceId") + .HasColumnType("uniqueidentifier"); + + b.Property("Scope") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("ApiResourceId", "Scope"); + + b.ToTable("IdentityServerApiResourceScopes"); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.ApiResources.ApiResourceSecret", b => + { + b.Property("ApiResourceId") + .HasColumnType("uniqueidentifier"); + + b.Property("Type") + .HasMaxLength(250) + .HasColumnType("nvarchar(250)"); + + b.Property("Value") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Expiration") + .HasColumnType("datetime2"); + + b.HasKey("ApiResourceId", "Type", "Value"); + + b.ToTable("IdentityServerApiResourceSecrets"); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.ApiScopes.ApiScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("DisplayName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Emphasize") + .HasColumnType("bit"); + + b.Property("Enabled") + .HasColumnType("bit"); + + b.Property("ExtraProperties") + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Required") + .HasColumnType("bit"); + + b.Property("ShowInDiscoveryDocument") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("IdentityServerApiScopes"); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.ApiScopes.ApiScopeClaim", b => + { + b.Property("ApiScopeId") + .HasColumnType("uniqueidentifier"); + + b.Property("Type") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("ApiScopeId", "Type"); + + b.ToTable("IdentityServerApiScopeClaims"); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.ApiScopes.ApiScopeProperty", b => + { + b.Property("ApiScopeId") + .HasColumnType("uniqueidentifier"); + + b.Property("Key") + .HasMaxLength(250) + .HasColumnType("nvarchar(250)"); + + b.Property("Value") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.HasKey("ApiScopeId", "Key", "Value"); + + b.ToTable("IdentityServerApiScopeProperties"); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.Client", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AbsoluteRefreshTokenLifetime") + .HasColumnType("int"); + + b.Property("AccessTokenLifetime") + .HasColumnType("int"); + + b.Property("AccessTokenType") + .HasColumnType("int"); + + b.Property("AllowAccessTokensViaBrowser") + .HasColumnType("bit"); + + b.Property("AllowOfflineAccess") + .HasColumnType("bit"); + + b.Property("AllowPlainTextPkce") + .HasColumnType("bit"); + + b.Property("AllowRememberConsent") + .HasColumnType("bit"); + + b.Property("AllowedIdentityTokenSigningAlgorithms") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("AlwaysIncludeUserClaimsInIdToken") + .HasColumnType("bit"); + + b.Property("AlwaysSendClientClaims") + .HasColumnType("bit"); + + b.Property("AuthorizationCodeLifetime") + .HasColumnType("int"); + + b.Property("BackChannelLogoutSessionRequired") + .HasColumnType("bit"); + + b.Property("BackChannelLogoutUri") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("ClientClaimsPrefix") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ClientName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ClientUri") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ConsentLifetime") + .HasColumnType("int"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("DeviceCodeLifetime") + .HasColumnType("int"); + + b.Property("EnableLocalLogin") + .HasColumnType("bit"); + + b.Property("Enabled") + .HasColumnType("bit"); + + b.Property("ExtraProperties") + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("FrontChannelLogoutSessionRequired") + .HasColumnType("bit"); + + b.Property("FrontChannelLogoutUri") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("IdentityTokenLifetime") + .HasColumnType("int"); + + b.Property("IncludeJwtId") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("LogoUri") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("PairWiseSubjectSalt") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ProtocolType") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("RefreshTokenExpiration") + .HasColumnType("int"); + + b.Property("RefreshTokenUsage") + .HasColumnType("int"); + + b.Property("RequireClientSecret") + .HasColumnType("bit"); + + b.Property("RequireConsent") + .HasColumnType("bit"); + + b.Property("RequirePkce") + .HasColumnType("bit"); + + b.Property("RequireRequestObject") + .HasColumnType("bit"); + + b.Property("SlidingRefreshTokenLifetime") + .HasColumnType("int"); + + b.Property("UpdateAccessTokenClaimsOnRefresh") + .HasColumnType("bit"); + + b.Property("UserCodeType") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UserSsoLifetime") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("IdentityServerClients"); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientClaim", b => + { + b.Property("ClientId") + .HasColumnType("uniqueidentifier"); + + b.Property("Type") + .HasMaxLength(250) + .HasColumnType("nvarchar(250)"); + + b.Property("Value") + .HasMaxLength(250) + .HasColumnType("nvarchar(250)"); + + b.HasKey("ClientId", "Type", "Value"); + + b.ToTable("IdentityServerClientClaims"); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientCorsOrigin", b => + { + b.Property("ClientId") + .HasColumnType("uniqueidentifier"); + + b.Property("Origin") + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.HasKey("ClientId", "Origin"); + + b.ToTable("IdentityServerClientCorsOrigins"); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientGrantType", b => + { + b.Property("ClientId") + .HasColumnType("uniqueidentifier"); + + b.Property("GrantType") + .HasMaxLength(250) + .HasColumnType("nvarchar(250)"); + + b.HasKey("ClientId", "GrantType"); + + b.ToTable("IdentityServerClientGrantTypes"); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientIdPRestriction", b => + { + b.Property("ClientId") + .HasColumnType("uniqueidentifier"); + + b.Property("Provider") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("ClientId", "Provider"); + + b.ToTable("IdentityServerClientIdPRestrictions"); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientPostLogoutRedirectUri", b => + { + b.Property("ClientId") + .HasColumnType("uniqueidentifier"); + + b.Property("PostLogoutRedirectUri") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.HasKey("ClientId", "PostLogoutRedirectUri"); + + b.ToTable("IdentityServerClientPostLogoutRedirectUris"); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientProperty", b => + { + b.Property("ClientId") + .HasColumnType("uniqueidentifier"); + + b.Property("Key") + .HasMaxLength(250) + .HasColumnType("nvarchar(250)"); + + b.Property("Value") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.HasKey("ClientId", "Key", "Value"); + + b.ToTable("IdentityServerClientProperties"); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientRedirectUri", b => + { + b.Property("ClientId") + .HasColumnType("uniqueidentifier"); + + b.Property("RedirectUri") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.HasKey("ClientId", "RedirectUri"); + + b.ToTable("IdentityServerClientRedirectUris"); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientScope", b => + { + b.Property("ClientId") + .HasColumnType("uniqueidentifier"); + + b.Property("Scope") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("ClientId", "Scope"); + + b.ToTable("IdentityServerClientScopes"); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientSecret", b => + { + b.Property("ClientId") + .HasColumnType("uniqueidentifier"); + + b.Property("Type") + .HasMaxLength(250) + .HasColumnType("nvarchar(250)"); + + b.Property("Value") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("Expiration") + .HasColumnType("datetime2"); + + b.HasKey("ClientId", "Type", "Value"); + + b.ToTable("IdentityServerClientSecrets"); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Devices.DeviceFlowCodes", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("Data") + .IsRequired() + .HasMaxLength(50000) + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("DeviceCode") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Expiration") + .IsRequired() + .HasColumnType("datetime2"); + + b.Property("ExtraProperties") + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UserCode") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DeviceCode") + .IsUnique(); + + b.HasIndex("Expiration"); + + b.HasIndex("UserCode"); + + b.ToTable("IdentityServerDeviceFlowCodes"); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Grants.PersistedGrant", b => + { + b.Property("Key") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ConsumedTime") + .HasColumnType("datetime2"); + + b.Property("CreationTime") + .HasColumnType("datetime2"); + + b.Property("Data") + .IsRequired() + .HasMaxLength(50000) + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Expiration") + .HasColumnType("datetime2"); + + b.Property("ExtraProperties") + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Key"); + + b.HasIndex("Expiration"); + + b.HasIndex("SubjectId", "ClientId", "Type"); + + b.HasIndex("SubjectId", "SessionId", "Type"); + + b.ToTable("IdentityServerPersistedGrants"); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.IdentityResources.IdentityResource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("DisplayName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Emphasize") + .HasColumnType("bit"); + + b.Property("Enabled") + .HasColumnType("bit"); + + b.Property("ExtraProperties") + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Required") + .HasColumnType("bit"); + + b.Property("ShowInDiscoveryDocument") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("IdentityServerIdentityResources"); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.IdentityResources.IdentityResourceClaim", b => + { + b.Property("IdentityResourceId") + .HasColumnType("uniqueidentifier"); + + b.Property("Type") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("IdentityResourceId", "Type"); + + b.ToTable("IdentityServerIdentityResourceClaims"); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.IdentityResources.IdentityResourceProperty", b => + { + b.Property("IdentityResourceId") + .HasColumnType("uniqueidentifier"); + + b.Property("Key") + .HasMaxLength(250) + .HasColumnType("nvarchar(250)"); + + b.Property("Value") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.HasKey("IdentityResourceId", "Key", "Value"); + + b.ToTable("IdentityServerIdentityResourceProperties"); + }); + + modelBuilder.Entity("Volo.Abp.PermissionManagement.PermissionGrant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ProviderKey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ProviderName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("Name", "ProviderName", "ProviderKey"); + + b.ToTable("AbpPermissionGrants"); + }); + + modelBuilder.Entity("Volo.Abp.SettingManagement.Setting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ProviderKey") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ProviderName") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.HasKey("Id"); + + b.HasIndex("Name", "ProviderName", "ProviderKey"); + + b.ToTable("AbpSettings"); + }); + + modelBuilder.Entity("Volo.Abp.TenantManagement.Tenant", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("AbpTenants"); + }); + + modelBuilder.Entity("Volo.Abp.TenantManagement.TenantConnectionString", b => + { + b.Property("TenantId") + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.HasKey("TenantId", "Name"); + + b.ToTable("AbpTenantConnectionStrings"); + }); + + modelBuilder.Entity("Bcvp.Blog.Core.BlogCore.Comments.Comment", b => + { + b.HasOne("Bcvp.Blog.Core.BlogCore.Posts.Post", null) + .WithMany() + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bcvp.Blog.Core.BlogCore.Comments.Comment", null) + .WithMany() + .HasForeignKey("RepliedCommentId"); + }); + + modelBuilder.Entity("Bcvp.Blog.Core.BlogCore.Posts.Post", b => + { + b.OwnsMany("Bcvp.Blog.Core.BlogCore.Posts.PostTag", "Tags", b1 => + { + b1.Property("PostId") + .HasColumnType("uniqueidentifier"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b1.Property("TagId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TagId"); + + b1.HasKey("PostId", "Id"); + + b1.ToTable("AppPostTags"); + + b1.WithOwner() + .HasForeignKey("PostId"); + }); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.AuditLogAction", b => + { + b.HasOne("Volo.Abp.AuditLogging.AuditLog", null) + .WithMany("Actions") + .HasForeignKey("AuditLogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.EntityChange", b => + { + b.HasOne("Volo.Abp.AuditLogging.AuditLog", null) + .WithMany("EntityChanges") + .HasForeignKey("AuditLogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.EntityPropertyChange", b => + { + b.HasOne("Volo.Abp.AuditLogging.EntityChange", null) + .WithMany("PropertyChanges") + .HasForeignKey("EntityChangeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityRoleClaim", b => + { + b.HasOne("Volo.Abp.Identity.IdentityRole", null) + .WithMany("Claims") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserClaim", b => + { + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("Claims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserLogin", b => + { + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("Logins") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserOrganizationUnit", b => + { + b.HasOne("Volo.Abp.Identity.OrganizationUnit", null) + .WithMany() + .HasForeignKey("OrganizationUnitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("OrganizationUnits") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserRole", b => + { + b.HasOne("Volo.Abp.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("Roles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserToken", b => + { + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("Tokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnit", b => + { + b.HasOne("Volo.Abp.Identity.OrganizationUnit", null) + .WithMany() + .HasForeignKey("ParentId"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnitRole", b => + { + b.HasOne("Volo.Abp.Identity.OrganizationUnit", null) + .WithMany("Roles") + .HasForeignKey("OrganizationUnitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Volo.Abp.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.ApiResources.ApiResourceClaim", b => + { + b.HasOne("Volo.Abp.IdentityServer.ApiResources.ApiResource", null) + .WithMany("UserClaims") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.ApiResources.ApiResourceProperty", b => + { + b.HasOne("Volo.Abp.IdentityServer.ApiResources.ApiResource", null) + .WithMany("Properties") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.ApiResources.ApiResourceScope", b => + { + b.HasOne("Volo.Abp.IdentityServer.ApiResources.ApiResource", null) + .WithMany("Scopes") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.ApiResources.ApiResourceSecret", b => + { + b.HasOne("Volo.Abp.IdentityServer.ApiResources.ApiResource", null) + .WithMany("Secrets") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.ApiScopes.ApiScopeClaim", b => + { + b.HasOne("Volo.Abp.IdentityServer.ApiScopes.ApiScope", null) + .WithMany("UserClaims") + .HasForeignKey("ApiScopeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.ApiScopes.ApiScopeProperty", b => + { + b.HasOne("Volo.Abp.IdentityServer.ApiScopes.ApiScope", null) + .WithMany("Properties") + .HasForeignKey("ApiScopeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientClaim", b => + { + b.HasOne("Volo.Abp.IdentityServer.Clients.Client", null) + .WithMany("Claims") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientCorsOrigin", b => + { + b.HasOne("Volo.Abp.IdentityServer.Clients.Client", null) + .WithMany("AllowedCorsOrigins") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientGrantType", b => + { + b.HasOne("Volo.Abp.IdentityServer.Clients.Client", null) + .WithMany("AllowedGrantTypes") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientIdPRestriction", b => + { + b.HasOne("Volo.Abp.IdentityServer.Clients.Client", null) + .WithMany("IdentityProviderRestrictions") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientPostLogoutRedirectUri", b => + { + b.HasOne("Volo.Abp.IdentityServer.Clients.Client", null) + .WithMany("PostLogoutRedirectUris") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientProperty", b => + { + b.HasOne("Volo.Abp.IdentityServer.Clients.Client", null) + .WithMany("Properties") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientRedirectUri", b => + { + b.HasOne("Volo.Abp.IdentityServer.Clients.Client", null) + .WithMany("RedirectUris") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientScope", b => + { + b.HasOne("Volo.Abp.IdentityServer.Clients.Client", null) + .WithMany("AllowedScopes") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientSecret", b => + { + b.HasOne("Volo.Abp.IdentityServer.Clients.Client", null) + .WithMany("ClientSecrets") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.IdentityResources.IdentityResourceClaim", b => + { + b.HasOne("Volo.Abp.IdentityServer.IdentityResources.IdentityResource", null) + .WithMany("UserClaims") + .HasForeignKey("IdentityResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.IdentityResources.IdentityResourceProperty", b => + { + b.HasOne("Volo.Abp.IdentityServer.IdentityResources.IdentityResource", null) + .WithMany("Properties") + .HasForeignKey("IdentityResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.TenantManagement.TenantConnectionString", b => + { + b.HasOne("Volo.Abp.TenantManagement.Tenant", null) + .WithMany("ConnectionStrings") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.AuditLog", b => + { + b.Navigation("Actions"); + + b.Navigation("EntityChanges"); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.EntityChange", b => + { + b.Navigation("PropertyChanges"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityRole", b => + { + b.Navigation("Claims"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUser", b => + { + b.Navigation("Claims"); + + b.Navigation("Logins"); + + b.Navigation("OrganizationUnits"); + + b.Navigation("Roles"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnit", b => + { + b.Navigation("Roles"); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.ApiResources.ApiResource", b => + { + b.Navigation("Properties"); + + b.Navigation("Scopes"); + + b.Navigation("Secrets"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.ApiScopes.ApiScope", b => + { + b.Navigation("Properties"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.Client", b => + { + b.Navigation("AllowedCorsOrigins"); + + b.Navigation("AllowedGrantTypes"); + + b.Navigation("AllowedScopes"); + + b.Navigation("Claims"); + + b.Navigation("ClientSecrets"); + + b.Navigation("IdentityProviderRestrictions"); + + b.Navigation("PostLogoutRedirectUris"); + + b.Navigation("Properties"); + + b.Navigation("RedirectUris"); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.IdentityResources.IdentityResource", b => + { + b.Navigation("Properties"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("Volo.Abp.TenantManagement.Tenant", b => + { + b.Navigation("ConnectionStrings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/aspnet-core/src/Bcvp.Blog.Core.EntityFrameworkCore/Migrations/20211009064551_AddBlogUser.cs b/aspnet-core/src/Bcvp.Blog.Core.EntityFrameworkCore/Migrations/20211009064551_AddBlogUser.cs new file mode 100644 index 0000000..0b13f3e --- /dev/null +++ b/aspnet-core/src/Bcvp.Blog.Core.EntityFrameworkCore/Migrations/20211009064551_AddBlogUser.cs @@ -0,0 +1,59 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Bcvp.Blog.Core.Migrations +{ + public partial class AddBlogUser : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_AppPosts_AppBlogs_BlogId", + table: "AppPosts"); + + migrationBuilder.DropIndex( + name: "IX_AppPosts_BlogId", + table: "AppPosts"); + + migrationBuilder.CreateTable( + name: "AppUsers", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + TenantId = table.Column(type: "uniqueidentifier", nullable: true), + UserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false), + Email = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false), + Name = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: true), + Surname = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: true), + EmailConfirmed = table.Column(type: "bit", nullable: false, defaultValue: false), + PhoneNumber = table.Column(type: "nvarchar(16)", maxLength: 16, nullable: true), + PhoneNumberConfirmed = table.Column(type: "bit", nullable: false, defaultValue: false), + ExtraProperties = table.Column(type: "nvarchar(max)", nullable: true), + ConcurrencyStamp = table.Column(type: "nvarchar(40)", maxLength: 40, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AppUsers", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AppUsers"); + + migrationBuilder.CreateIndex( + name: "IX_AppPosts_BlogId", + table: "AppPosts", + column: "BlogId"); + + migrationBuilder.AddForeignKey( + name: "FK_AppPosts_AppBlogs_BlogId", + table: "AppPosts", + column: "BlogId", + principalTable: "AppBlogs", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/aspnet-core/src/Bcvp.Blog.Core.EntityFrameworkCore/Migrations/CoreDbContextModelSnapshot.cs b/aspnet-core/src/Bcvp.Blog.Core.EntityFrameworkCore/Migrations/CoreDbContextModelSnapshot.cs index c373d71..95bf630 100644 --- a/aspnet-core/src/Bcvp.Blog.Core.EntityFrameworkCore/Migrations/CoreDbContextModelSnapshot.cs +++ b/aspnet-core/src/Bcvp.Blog.Core.EntityFrameworkCore/Migrations/CoreDbContextModelSnapshot.cs @@ -18,7 +18,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.SqlServer) .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("ProductVersion", "5.0.9") + .HasAnnotation("ProductVersion", "5.0.10") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); modelBuilder.Entity("Bcvp.Blog.Core.BlogCore.Blogs.Blog", b => @@ -237,8 +237,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.HasIndex("BlogId"); - b.ToTable("AppPosts"); }); @@ -310,6 +308,70 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AppTags"); }); + modelBuilder.Entity("Bcvp.Blog.Core.BlogCore.Users.BlogUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("Email"); + + b.Property("EmailConfirmed") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("EmailConfirmed"); + + b.Property("ExtraProperties") + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("Name") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("Name"); + + b.Property("PhoneNumber") + .HasMaxLength(16) + .HasColumnType("nvarchar(16)") + .HasColumnName("PhoneNumber"); + + b.Property("PhoneNumberConfirmed") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("PhoneNumberConfirmed"); + + b.Property("Surname") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("Surname"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("UserName"); + + b.HasKey("Id"); + + b.ToTable("AppUsers"); + }); + modelBuilder.Entity("Volo.Abp.AuditLogging.AuditLog", b => { b.Property("Id") @@ -2232,12 +2294,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Bcvp.Blog.Core.BlogCore.Posts.Post", b => { - b.HasOne("Bcvp.Blog.Core.BlogCore.Blogs.Blog", null) - .WithMany() - .HasForeignKey("BlogId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - b.OwnsMany("Bcvp.Blog.Core.BlogCore.Posts.PostTag", "Tags", b1 => { b1.Property("PostId") diff --git a/aspnet-core/src/Bcvp.Blog.Core.HttpApi.Host/Bcvp.Blog.Core.HttpApi.Host.csproj b/aspnet-core/src/Bcvp.Blog.Core.HttpApi.Host/Bcvp.Blog.Core.HttpApi.Host.csproj index bc09bf6..fb48ba0 100644 --- a/aspnet-core/src/Bcvp.Blog.Core.HttpApi.Host/Bcvp.Blog.Core.HttpApi.Host.csproj +++ b/aspnet-core/src/Bcvp.Blog.Core.HttpApi.Host/Bcvp.Blog.Core.HttpApi.Host.csproj @@ -7,9 +7,12 @@ Bcvp.Blog.Core true Bcvp.Blog.Core-4681b4fd-151f-4221-84a4-929d86723e4c + Linux + ..\.. + diff --git a/aspnet-core/src/Bcvp.Blog.Core.HttpApi.Host/Properties/launchSettings.json b/aspnet-core/src/Bcvp.Blog.Core.HttpApi.Host/Properties/launchSettings.json index 651a73a..4768128 100644 --- a/aspnet-core/src/Bcvp.Blog.Core.HttpApi.Host/Properties/launchSettings.json +++ b/aspnet-core/src/Bcvp.Blog.Core.HttpApi.Host/Properties/launchSettings.json @@ -1,6 +1,6 @@ -{ +{ "iisSettings": { - "windowsAuthentication": false, + "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "https://localhost:44315", @@ -18,10 +18,17 @@ "Bcvp.Blog.Core.HttpApi.Host": { "commandName": "Project", "launchBrowser": true, - "applicationUrl": "https://localhost:44315", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - } + }, + "applicationUrl": "https://localhost:44315" + }, + "Docker": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", + "publishAllPorts": true, + "useSSL": true } } } \ No newline at end of file diff --git a/aspnet-core/src/Bcvp.Blog.Core.IdentityServer/Bcvp.Blog.Core.IdentityServer.csproj b/aspnet-core/src/Bcvp.Blog.Core.IdentityServer/Bcvp.Blog.Core.IdentityServer.csproj index 4c7e052..212e699 100644 --- a/aspnet-core/src/Bcvp.Blog.Core.IdentityServer/Bcvp.Blog.Core.IdentityServer.csproj +++ b/aspnet-core/src/Bcvp.Blog.Core.IdentityServer/Bcvp.Blog.Core.IdentityServer.csproj @@ -13,6 +13,8 @@ false true Bcvp.Blog.Core-4681b4fd-151f-4221-84a4-929d86723e4c + Linux + ..\.. @@ -32,6 +34,7 @@ + diff --git a/aspnet-core/src/Bcvp.Blog.Core.IdentityServer/Properties/launchSettings.json b/aspnet-core/src/Bcvp.Blog.Core.IdentityServer/Properties/launchSettings.json index 05c40bc..dd186c9 100644 --- a/aspnet-core/src/Bcvp.Blog.Core.IdentityServer/Properties/launchSettings.json +++ b/aspnet-core/src/Bcvp.Blog.Core.IdentityServer/Properties/launchSettings.json @@ -1,6 +1,6 @@ -{ +{ "iisSettings": { - "windowsAuthentication": false, + "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "https://localhost:44362", @@ -18,10 +18,17 @@ "Bcvp.Blog.Core.IdentityServer": { "commandName": "Project", "launchBrowser": true, - "applicationUrl": "https://localhost:44362", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - } + }, + "applicationUrl": "https://localhost:44362" + }, + "Docker": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", + "publishAllPorts": true, + "useSSL": true } } } \ No newline at end of file diff --git a/aspnet-core/test/Bcvp.Blog.Core.Application.Tests/BlogCore/BlogAppService_Tests.cs b/aspnet-core/test/Bcvp.Blog.Core.Application.Tests/BlogCore/BlogAppService_Tests.cs new file mode 100644 index 0000000..3c8efd7 --- /dev/null +++ b/aspnet-core/test/Bcvp.Blog.Core.Application.Tests/BlogCore/BlogAppService_Tests.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Bcvp.Blog.Core.BlogCore.Blogs; +using Shouldly; +using Volo.Abp.Identity; +using Xunit; + +namespace Bcvp.Blog.Core.BlogCore +{ + public class BlogAppService_Tests : CoreApplicationTestBase + { + private readonly IBlogAppService _blogAppService; + public BlogAppService_Tests() + { + _blogAppService = GetRequiredService(); + } + + [Fact] + public async Task Should_Get_List_Of_Blogs() + { + var blogs = await _blogAppService.GetListAsync(); + + blogs.Items.Count.ShouldBeGreaterThan(0); + } + + [Fact] + public async Task Should_Get_Blog_By_Shortname() + { + var targetBlog = (await _blogAppService.GetListAsync()).Items.First(); + + var blog = await _blogAppService.GetByShortNameAsync(targetBlog.ShortName); + + blog.ShouldNotBeNull(); + blog.Name.ShouldBe(targetBlog.Name); + } + + } +} diff --git a/aspnet-core/test/Bcvp.Blog.Core.Application.Tests/BlogCore/CommentAppService_Tests.cs b/aspnet-core/test/Bcvp.Blog.Core.Application.Tests/BlogCore/CommentAppService_Tests.cs new file mode 100644 index 0000000..6151783 --- /dev/null +++ b/aspnet-core/test/Bcvp.Blog.Core.Application.Tests/BlogCore/CommentAppService_Tests.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Bcvp.Blog.Core.BlogCore.Blogs; +using Bcvp.Blog.Core.BlogCore.Comments; +using Bcvp.Blog.Core.BlogCore.Posts; +using Shouldly; +using Xunit; + +namespace Bcvp.Blog.Core.BlogCore +{ + public class CommentAppService_Tests : CoreApplicationTestBase + { + private readonly ICommentAppService _commentAppService; + private readonly ICommentRepository _commentRepository; + private readonly IPostRepository _postRepository; + private readonly IBlogRepository _blogRepository; + + public CommentAppService_Tests() + { + _commentAppService = GetRequiredService(); + _commentRepository = GetRequiredService(); + _postRepository = GetRequiredService(); + _blogRepository = GetRequiredService(); + } + + [Fact] + public async Task Should_Get_List_Of_Comments() + { + var post = (await _postRepository.GetListAsync()).FirstOrDefault(); + await _commentRepository.InsertAsync(new Comment(Guid.NewGuid(), post.Id, null, "qweasd")); + await _commentRepository.InsertAsync(new Comment(Guid.NewGuid(), post.Id, null, "qweasd")); + + var comments = + await _commentAppService.GetHierarchicalListOfPostAsync(post.Id); + + comments.Count.ShouldBeGreaterThan(2); + } + + [Fact] + public async Task Should_Create_A_Comment() + { + var postId = (await _postRepository.GetListAsync()).First().Id; + var content = "test content"; + + var commentWithDetailsDto = await _commentAppService.CreateAsync(new CreateCommentDto() + { PostId = postId, Text = content }); + + UsingDbContext(context => + { + var comment = context.Comments.FirstOrDefault(q => q.Id == commentWithDetailsDto.Id); + comment.ShouldNotBeNull(); + comment.Text.ShouldBe(commentWithDetailsDto.Text); + }); + } + + [Fact] + public async Task Should_Update_A_Comment() + { + var newContent = "new content"; + + var oldComment = (await _commentRepository.GetListAsync()).FirstOrDefault(); ; + + await _commentAppService.UpdateAsync(oldComment.Id, new UpdateCommentDto() + { Text = newContent }); + + UsingDbContext(context => + { + var comment = context.Comments.FirstOrDefault(q => q.Id == oldComment.Id); + comment.Text.ShouldBe(newContent); + }); + } + + [Fact] + public async Task Should_Delete_A_Comment() + { + var comment = (await _commentRepository.GetListAsync()).First(); + + await _commentAppService.DeleteAsync(comment.Id); + } + } +} diff --git a/aspnet-core/test/Bcvp.Blog.Core.Application.Tests/BlogCore/PostAppService_Tests.cs b/aspnet-core/test/Bcvp.Blog.Core.Application.Tests/BlogCore/PostAppService_Tests.cs new file mode 100644 index 0000000..d44506b --- /dev/null +++ b/aspnet-core/test/Bcvp.Blog.Core.Application.Tests/BlogCore/PostAppService_Tests.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Bcvp.Blog.Core.BlogCore.Blogs; +using Bcvp.Blog.Core.BlogCore.Posts; +using Shouldly; +using Xunit; + +namespace Bcvp.Blog.Core.BlogCore +{ + public class PostAppService_Tests : CoreApplicationTestBase + { + private readonly IPostAppService _postAppService; + private readonly IBlogRepository _blogRepository; + private readonly IPostRepository _postRepository; + + public PostAppService_Tests() + { + _postAppService = GetRequiredService(); + _blogRepository = GetRequiredService(); + _postRepository = GetRequiredService(); + } + + [Fact] + public async Task Should_Get_List_Of_Posts() + { + var blogId = (await _blogRepository.GetListAsync()).First().Id; + var posts = await _postAppService.GetListByBlogIdAndTagNameAsync(blogId, null); + posts.Items.Count.ShouldBeGreaterThan(0); + } + + [Fact] + public async Task Should_Get_For_Reading() + { + var blogId = (await _blogRepository.GetListAsync()).First().Id; + var post = (await _postRepository.GetListAsync()).First(p => p.BlogId == blogId); + var postToRead = await _postAppService.GetForReadingAsync(new GetPostInput() { BlogId = blogId, Url = post.Url }); + + postToRead.ShouldNotBeNull(); + } + + [Fact] + public async Task Should_Create_A_Post() + { + var blogId = (await _blogRepository.GetListAsync()).First().Id; + var title = "test title"; + var content = "test content"; + var coverImage = "new.jpg"; + + var newPostDto = await _postAppService.CreateAsync(new CreatePostDto() + { + BlogId = blogId, + Title = title, + Content = content, + CoverImage = coverImage, + Url = title.Replace(" ", "-") + }); + + newPostDto.Id.ShouldNotBe(Guid.Empty); + + UsingDbContext(context => + { + var post = context.Posts.FirstOrDefault(q => q.Title == title); + post.ShouldNotBeNull(); + post.Id.ShouldBe(newPostDto.Id); + post.Title.ShouldBe(newPostDto.Title); + post.Content.ShouldBe(newPostDto.Content); + post.CoverImage.ShouldBe(newPostDto.CoverImage); + post.BlogId.ShouldBe(blogId); + post.Url.ShouldBe(newPostDto.Url); + }); + } + + [Fact] + public async Task Should_Update_A_Post() + { + var blogId = (await _blogRepository.GetListAsync()).First().Id; + var newTitle = "new title"; + var newContent = "content"; + + var oldPost = (await _postRepository.GetListAsync()).FirstOrDefault(); ; + + await _postAppService.UpdateAsync(oldPost.Id, new UpdatePostDto() + { + BlogId = blogId, + Title = newTitle, + CoverImage = oldPost.CoverImage, + Content = newContent, + Url = newTitle.Replace(" ", "-") + }); + + UsingDbContext(context => + { + var post = context.Posts.FirstOrDefault(q => q.Id == oldPost.Id); + post.Title.ShouldBe(newTitle); + post.Content.ShouldBe(newContent); + }); + } + + [Fact] + public async Task Should_Delete_A_Post() + { + var post = (await _postRepository.GetListAsync()).First(); + + await _postAppService.DeleteAsync(post.Id); + } + } +} diff --git a/aspnet-core/test/Bcvp.Blog.Core.Application.Tests/BlogCore/TagAppService_Tests.cs b/aspnet-core/test/Bcvp.Blog.Core.Application.Tests/BlogCore/TagAppService_Tests.cs new file mode 100644 index 0000000..4986765 --- /dev/null +++ b/aspnet-core/test/Bcvp.Blog.Core.Application.Tests/BlogCore/TagAppService_Tests.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Bcvp.Blog.Core.BlogCore.Tagging; +using Shouldly; +using Xunit; + +namespace Bcvp.Blog.Core.BlogCore +{ + public class TagAppService_Tests : CoreApplicationTestBase + { + private readonly ITagAppService _tagAppService; + private readonly ITagRepository _tagRepository; + private readonly CoreTestData _bloggingTestData; + + public TagAppService_Tests() + { + _tagAppService = GetRequiredService(); + _tagRepository = GetRequiredService(); + _bloggingTestData = GetRequiredService(); + } + + [Fact] + public async Task Should_Get_Popular_Tags() + { + var tags = await _tagAppService.GetPopularTagsAsync(_bloggingTestData.Blog1Id, + new GetPopularTagsInput() { ResultCount = 5, MinimumPostCount = 0 }); + + tags.Count.ShouldBeGreaterThan(0); + } + } +} diff --git a/aspnet-core/test/Bcvp.Blog.Core.Application.Tests/CoreApplicationTestBase.cs b/aspnet-core/test/Bcvp.Blog.Core.Application.Tests/CoreApplicationTestBase.cs index 8cc8c37..e321a84 100644 --- a/aspnet-core/test/Bcvp.Blog.Core.Application.Tests/CoreApplicationTestBase.cs +++ b/aspnet-core/test/Bcvp.Blog.Core.Application.Tests/CoreApplicationTestBase.cs @@ -1,7 +1,24 @@ -namespace Bcvp.Blog.Core +using System; +using Bcvp.Blog.Core.EntityFrameworkCore; + +namespace Bcvp.Blog.Core { public abstract class CoreApplicationTestBase : CoreTestBase { + protected virtual void UsingDbContext(Action action) + { + using (var dbContext = GetRequiredService()) + { + action.Invoke(dbContext); + } + } + protected virtual T UsingDbContext(Func action) + { + using (var dbContext = GetRequiredService()) + { + return action.Invoke(dbContext); + } + } } } diff --git a/aspnet-core/test/Bcvp.Blog.Core.Application.Tests/CoreApplicationTestModule.cs b/aspnet-core/test/Bcvp.Blog.Core.Application.Tests/CoreApplicationTestModule.cs index 497281e..a6fdf31 100644 --- a/aspnet-core/test/Bcvp.Blog.Core.Application.Tests/CoreApplicationTestModule.cs +++ b/aspnet-core/test/Bcvp.Blog.Core.Application.Tests/CoreApplicationTestModule.cs @@ -1,13 +1,19 @@ -using Volo.Abp.Modularity; +using Bcvp.Blog.Core.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Modularity; namespace Bcvp.Blog.Core { [DependsOn( typeof(CoreApplicationModule), - typeof(CoreDomainTestModule) + typeof(CoreEntityFrameworkCoreTestModule), + typeof(CoreTestBaseModule) )] public class CoreApplicationTestModule : AbpModule { - + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddAlwaysAllowAuthorization(); + } } } \ No newline at end of file diff --git a/aspnet-core/test/Bcvp.Blog.Core.Domain.Tests/BlogCore/Blogs/Blog_Tests.cs b/aspnet-core/test/Bcvp.Blog.Core.Domain.Tests/BlogCore/Blogs/Blog_Tests.cs new file mode 100644 index 0000000..adba250 --- /dev/null +++ b/aspnet-core/test/Bcvp.Blog.Core.Domain.Tests/BlogCore/Blogs/Blog_Tests.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Shouldly; +using Xunit; + +namespace Bcvp.Blog.Core.BlogCore.Blogs +{ + public class Blog_Tests + { + [Theory] + [InlineData("aaa")] + [InlineData("bbb")] + public void SetName(string name) + { + var blog = new Blog(Guid.NewGuid(), "test blog", "test"); + blog.SetName(name); + blog.Name.ShouldBe(name); + } + + [Theory] + [InlineData("aaa")] + [InlineData("bbb")] + public void SetShortName(string name) + { + var blog = new Blog(Guid.NewGuid(), "test blog", "test"); + blog.SetShortName(name); + blog.ShortName.ShouldBe(name); + } + } +} diff --git a/aspnet-core/test/Bcvp.Blog.Core.Domain.Tests/BlogCore/Comments/Comment_Tests.cs b/aspnet-core/test/Bcvp.Blog.Core.Domain.Tests/BlogCore/Comments/Comment_Tests.cs new file mode 100644 index 0000000..423603f --- /dev/null +++ b/aspnet-core/test/Bcvp.Blog.Core.Domain.Tests/BlogCore/Comments/Comment_Tests.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Shouldly; +using Xunit; + +namespace Bcvp.Blog.Core.BlogCore.Comments +{ + public class Comment_Tests + { + [Theory] + [InlineData("aaa")] + [InlineData("bbb")] + public void SetText(string text) + { + var comment = new Comment(Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid(), "good"); + comment.SetText(text); + comment.Text.ShouldBe(text); + } + } +} diff --git a/aspnet-core/test/Bcvp.Blog.Core.Domain.Tests/BlogCore/Posts/Post_Tests.cs b/aspnet-core/test/Bcvp.Blog.Core.Domain.Tests/BlogCore/Posts/Post_Tests.cs new file mode 100644 index 0000000..7102624 --- /dev/null +++ b/aspnet-core/test/Bcvp.Blog.Core.Domain.Tests/BlogCore/Posts/Post_Tests.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Shouldly; +using Xunit; + +namespace Bcvp.Blog.Core.BlogCore.Posts +{ + public class Post_Tests + { + [Fact] + public void IncreaseReadCount() + { + var post = new Post(Guid.NewGuid(), Guid.NewGuid(), "abp", "⊙o⊙", "abp.io"); + post.IncreaseReadCount(); + post.ReadCount.ShouldBe(1); + } + + [Theory] + [InlineData("aaa")] + [InlineData("bbb")] + public void SetTitle(string title) + { + var post = new Post(Guid.NewGuid(), Guid.NewGuid(), "abp", "⊙o⊙", "abp.io"); + post.SetTitle(title); + post.Title.ShouldBe(title); + } + + [Theory] + [InlineData("aaa")] + [InlineData("bbb")] + public void SetUrl(string url) + { + var post = new Post(Guid.NewGuid(), Guid.NewGuid(), "abp", "⊙o⊙", "abp.io"); + post.SetUrl(url); + post.Url.ShouldBe(url); + } + + [Fact] + public void AddTag() + { + var post = new Post(Guid.NewGuid(), Guid.NewGuid(), "abp", "⊙o⊙", "abp.io"); + var tagId = Guid.NewGuid(); + post.AddTag(tagId); + post.Tags.ShouldContain(x => x.TagId == tagId); + } + + + [Fact] + public void RemoveTag() + { + var post = new Post(Guid.NewGuid(), Guid.NewGuid(), "abp", "⊙o⊙", "abp.io"); + var tagId = Guid.NewGuid(); + post.AddTag(tagId); + + post.Tags.ShouldContain(x => x.TagId == tagId); + + post.RemoveTag(tagId); + post.Tags.ShouldNotContain(x => x.TagId == tagId); + } + + } +} diff --git a/aspnet-core/test/Bcvp.Blog.Core.Domain.Tests/BlogCore/Tagging/Tag_Tests.cs b/aspnet-core/test/Bcvp.Blog.Core.Domain.Tests/BlogCore/Tagging/Tag_Tests.cs new file mode 100644 index 0000000..48e804e --- /dev/null +++ b/aspnet-core/test/Bcvp.Blog.Core.Domain.Tests/BlogCore/Tagging/Tag_Tests.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Shouldly; +using Xunit; + +namespace Bcvp.Blog.Core.BlogCore.Tagging +{ + public class Tag_Tests + { + [Theory] + [InlineData("aaa")] + [InlineData("bbb")] + public void SetName(string name) + { + var tag = new Tag(Guid.NewGuid(), Guid.NewGuid(), "abp", 0, "abp tag"); + tag.SetName(name); + tag.Name.ShouldBe(name); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public void IncreaseUsageCount(int number) + { + var tag = new Tag(Guid.NewGuid(), Guid.NewGuid(), "abp", 0, "abp tag"); + tag.IncreaseUsageCount(number); + tag.UsageCount.ShouldBe(number); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public void DecreaseUsageCount(int number) + { + var tag = new Tag(Guid.NewGuid(), Guid.NewGuid(), "abp", 10, "abp tag"); + tag.DecreaseUsageCount(number); + tag.UsageCount.ShouldBe(10 - number); + } + + [Fact] + public void DecreaseUsageCount_Should_Greater_ThanOrEqual_Zero() + { + var tag = new Tag(Guid.NewGuid(), Guid.NewGuid(), "abp", 10, "abp tag"); + tag.DecreaseUsageCount(100); + tag.UsageCount.ShouldBe(0); + } + + [Theory] + [InlineData("aaa")] + [InlineData("bbb")] + public void SetDescription(string description) + { + var tag = new Tag(Guid.NewGuid(), Guid.NewGuid(), "abp", 0, "abp tag"); + tag.SetDescription(description); + tag.Description.ShouldBe(description); + } + + + } +} diff --git a/aspnet-core/test/Bcvp.Blog.Core.Domain.Tests/BlogCore/Users/BlogUser_Test.cs b/aspnet-core/test/Bcvp.Blog.Core.Domain.Tests/BlogCore/Users/BlogUser_Test.cs new file mode 100644 index 0000000..fc4d00b --- /dev/null +++ b/aspnet-core/test/Bcvp.Blog.Core.Domain.Tests/BlogCore/Users/BlogUser_Test.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Users; +using Xunit; + +namespace Bcvp.Blog.Core.BlogCore.Users +{ + public class BlogUser_Test + { + [Fact] + public void Update() + { + var userId = Guid.NewGuid(); + var tenantId = Guid.NewGuid(); + var blogUser = new BlogUser(new UserData(userId, "bob lee", "boblee@volosoft.com", "lee", "bob", true, + "123456", true, tenantId)); + var userData = new UserData(userId, "lee bob", "leebob@volosoft.com", "bob", "lee", false, + "654321", false, tenantId); + + blogUser.Update(userData); + + blogUser.EntityEquals(new BlogUser(userData)).ShouldBeTrue(); + } + + [Fact] + public void Update_User_Id_Must_Equals() + { + var userId = Guid.NewGuid(); + var tenantId = Guid.NewGuid(); + var blogUser = new BlogUser(new UserData(userId, "bob lee", "boblee@volosoft.com", "lee", "bob", true, + "123456", true, tenantId)); + + var userData = new UserData(Guid.NewGuid(), "lee bob", "leebob@volosoft.com", "bob", "lee", false, + "654321", false, tenantId); + + Assert.Throws(() => blogUser.Update(userData)); + } + + [Fact] + public void Update_Tenant_Id_Must_Equals() + { + var userId = Guid.NewGuid(); + var tenantId = Guid.NewGuid(); + var blogUser = new BlogUser(new UserData(userId, "bob lee", "boblee@volosoft.com", "lee", "bob", true, + "123456", true, tenantId)); + + var userData = new UserData(userId, "lee bob", "leebob@volosoft.com", "bob", "lee", false, + "654321", false, Guid.NewGuid()); + + Assert.Throws(() => blogUser.Update(userData)); + } + + [Fact] + public void BlogUserEquals() + { + var userId = Guid.NewGuid(); + var tenantId = Guid.NewGuid(); + var blogUser = new BlogUser(new UserData(userId, "bob lee", "john@volosoft.com", "lee", "bob", true, + "123456", true, tenantId)); + + var blogUser2 = new BlogUser(new UserData(userId, "bob lee", "john@volosoft.com", "lee", "bob", true, + "123456", true, tenantId)); + + blogUser.EntityEquals(blogUser2).ShouldBeTrue(); + } + } +} diff --git a/aspnet-core/test/Bcvp.Blog.Core.Domain.Tests/CoreDomainTestModule.cs b/aspnet-core/test/Bcvp.Blog.Core.Domain.Tests/CoreDomainTestModule.cs index b958dce..4b82903 100644 --- a/aspnet-core/test/Bcvp.Blog.Core.Domain.Tests/CoreDomainTestModule.cs +++ b/aspnet-core/test/Bcvp.Blog.Core.Domain.Tests/CoreDomainTestModule.cs @@ -4,7 +4,8 @@ namespace Bcvp.Blog.Core { [DependsOn( - typeof(CoreEntityFrameworkCoreTestModule) + typeof(CoreEntityFrameworkCoreTestModule), + typeof(CoreTestBaseModule) )] public class CoreDomainTestModule : AbpModule { diff --git a/aspnet-core/test/Bcvp.Blog.Core.Domain.Tests/Samples/SampleDomainTests.cs b/aspnet-core/test/Bcvp.Blog.Core.Domain.Tests/Samples/SampleDomainTests.cs deleted file mode 100644 index 3373eef..0000000 --- a/aspnet-core/test/Bcvp.Blog.Core.Domain.Tests/Samples/SampleDomainTests.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Threading.Tasks; -using Shouldly; -using Volo.Abp.Identity; -using Xunit; - -namespace Bcvp.Blog.Core.Samples -{ - /* This is just an example test class. - * Normally, you don't test code of the modules you are using - * (like IdentityUserManager here). - * Only test your own domain services. - */ - public class SampleDomainTests : CoreDomainTestBase - { - private readonly IIdentityUserRepository _identityUserRepository; - private readonly IdentityUserManager _identityUserManager; - - public SampleDomainTests() - { - _identityUserRepository = GetRequiredService(); - _identityUserManager = GetRequiredService(); - } - - [Fact] - public async Task Should_Set_Email_Of_A_User() - { - IdentityUser adminUser; - - /* Need to manually start Unit Of Work because - * FirstOrDefaultAsync should be executed while db connection / context is available. - */ - await WithUnitOfWorkAsync(async () => - { - adminUser = await _identityUserRepository - .FindByNormalizedUserNameAsync("ADMIN"); - - await _identityUserManager.SetEmailAsync(adminUser, "newemail@abp.io"); - await _identityUserRepository.UpdateAsync(adminUser); - }); - - adminUser = await _identityUserRepository.FindByNormalizedUserNameAsync("ADMIN"); - adminUser.Email.ShouldBe("newemail@abp.io"); - } - } -} diff --git a/aspnet-core/test/Bcvp.Blog.Core.EntityFrameworkCore.Tests/EntityFrameworkCore/Blogging/Blogs/BlogRepository_Tests.cs b/aspnet-core/test/Bcvp.Blog.Core.EntityFrameworkCore.Tests/EntityFrameworkCore/Blogging/Blogs/BlogRepository_Tests.cs new file mode 100644 index 0000000..33ad88f --- /dev/null +++ b/aspnet-core/test/Bcvp.Blog.Core.EntityFrameworkCore.Tests/EntityFrameworkCore/Blogging/Blogs/BlogRepository_Tests.cs @@ -0,0 +1,13 @@ +using Bcvp.Blog.Core.BlogCore.Blogs; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bcvp.Blog.Core.EntityFrameworkCore.Blogging.Blogs +{ + public class BlogRepository_Tests : BlogRepository_Tests + { + } +} diff --git a/aspnet-core/test/Bcvp.Blog.Core.EntityFrameworkCore.Tests/EntityFrameworkCore/Blogging/Comments/CommentRepository_Tests.cs b/aspnet-core/test/Bcvp.Blog.Core.EntityFrameworkCore.Tests/EntityFrameworkCore/Blogging/Comments/CommentRepository_Tests.cs new file mode 100644 index 0000000..d23a8d5 --- /dev/null +++ b/aspnet-core/test/Bcvp.Blog.Core.EntityFrameworkCore.Tests/EntityFrameworkCore/Blogging/Comments/CommentRepository_Tests.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Bcvp.Blog.Core.BlogCore.Comments; + +namespace Bcvp.Blog.Core.EntityFrameworkCore.Blogging.Comments +{ + public class CommentRepository_Tests : CommentRepository_Tests + { + } +} diff --git a/aspnet-core/test/Bcvp.Blog.Core.EntityFrameworkCore.Tests/EntityFrameworkCore/Blogging/Posts/PostRepository_Tests.cs b/aspnet-core/test/Bcvp.Blog.Core.EntityFrameworkCore.Tests/EntityFrameworkCore/Blogging/Posts/PostRepository_Tests.cs new file mode 100644 index 0000000..e689c81 --- /dev/null +++ b/aspnet-core/test/Bcvp.Blog.Core.EntityFrameworkCore.Tests/EntityFrameworkCore/Blogging/Posts/PostRepository_Tests.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Bcvp.Blog.Core.BlogCore.Posts; + +namespace Bcvp.Blog.Core.EntityFrameworkCore.Blogging.Posts +{ + public class PostRepository_Tests : PostRepository_Tests + { + } +} diff --git a/aspnet-core/test/Bcvp.Blog.Core.EntityFrameworkCore.Tests/EntityFrameworkCore/Blogging/Tagging/TagRepository_Tests.cs b/aspnet-core/test/Bcvp.Blog.Core.EntityFrameworkCore.Tests/EntityFrameworkCore/Blogging/Tagging/TagRepository_Tests.cs new file mode 100644 index 0000000..e56c6c6 --- /dev/null +++ b/aspnet-core/test/Bcvp.Blog.Core.EntityFrameworkCore.Tests/EntityFrameworkCore/Blogging/Tagging/TagRepository_Tests.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Bcvp.Blog.Core.BlogCore.Tagging; + +namespace Bcvp.Blog.Core.EntityFrameworkCore.Blogging.Tagging +{ + public class TagRepository_Tests : TagRepository_Tests + { + } +} diff --git a/aspnet-core/test/Bcvp.Blog.Core.TestBase/BlogCore/Blogs/BlogRepository_Tests.cs b/aspnet-core/test/Bcvp.Blog.Core.TestBase/BlogCore/Blogs/BlogRepository_Tests.cs new file mode 100644 index 0000000..d179642 --- /dev/null +++ b/aspnet-core/test/Bcvp.Blog.Core.TestBase/BlogCore/Blogs/BlogRepository_Tests.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Modularity; +using Xunit; + +namespace Bcvp.Blog.Core.BlogCore.Blogs +{ + public abstract class BlogRepository_Tests : CoreTestBase + where TStartupModule : IAbpModule + { + protected IBlogRepository BlogRepository { get; } + + protected BlogRepository_Tests() + { + BlogRepository = GetRequiredService(); + } + + [Fact] + public async Task Should_Find_By_ShortName() + { + var blogFromRepository = (await BlogRepository.GetListAsync()).First(); + var blog = await BlogRepository.FindByShortNameAsync(blogFromRepository.ShortName); + blog.ShouldNotBeNull(); + } + } +} diff --git a/aspnet-core/test/Bcvp.Blog.Core.TestBase/BlogCore/Comments/CommentRepository_Tests.cs b/aspnet-core/test/Bcvp.Blog.Core.TestBase/BlogCore/Comments/CommentRepository_Tests.cs new file mode 100644 index 0000000..0823663 --- /dev/null +++ b/aspnet-core/test/Bcvp.Blog.Core.TestBase/BlogCore/Comments/CommentRepository_Tests.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Modularity; +using Xunit; + +namespace Bcvp.Blog.Core.BlogCore.Comments +{ + public abstract class CommentRepository_Tests : CoreTestBase + where TStartupModule : IAbpModule + { + protected ICommentRepository CommentRepository { get; } + protected CoreTestData BloggingTestData { get; } + + protected CommentRepository_Tests() + { + CommentRepository = GetRequiredService(); + BloggingTestData = GetRequiredService(); + } + + [Fact] + public async Task GetListOfPostAsync() + { + var comments = await CommentRepository.GetListOfPostAsync(BloggingTestData.Blog1Post1Id); + comments.ShouldNotBeNull(); + comments.Count.ShouldBe(2); + comments.ShouldAllBe(x => x.PostId == BloggingTestData.Blog1Post1Id); + } + + [Fact] + public async Task GetCommentCountOfPostAsync() + { + var count = await CommentRepository.GetCommentCountOfPostAsync(BloggingTestData.Blog1Post1Id); + count.ShouldBe(2); + } + + [Fact] + public async Task GetRepliesOfComment() + { + var comment = await CommentRepository.GetRepliesOfComment(BloggingTestData.Blog1Post1Comment1Id); + comment.ShouldNotBeNull(); + comment.ShouldContain(x => x.Id == BloggingTestData.Blog1Post1Comment2Id); + } + + [Fact] + public async Task DeleteOfPost() + { + await CommentRepository.DeleteOfPost(BloggingTestData.Blog1Post1Id); + (await CommentRepository.GetListAsync()).ShouldBeEmpty(); + } + } +} diff --git a/aspnet-core/test/Bcvp.Blog.Core.TestBase/BlogCore/Posts/PostRepository_Tests.cs b/aspnet-core/test/Bcvp.Blog.Core.TestBase/BlogCore/Posts/PostRepository_Tests.cs new file mode 100644 index 0000000..f67d856 --- /dev/null +++ b/aspnet-core/test/Bcvp.Blog.Core.TestBase/BlogCore/Posts/PostRepository_Tests.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Modularity; +using Xunit; + +namespace Bcvp.Blog.Core.BlogCore.Posts +{ + public abstract class PostRepository_Tests : CoreTestBase + where TStartupModule : IAbpModule + { + protected IPostRepository PostRepository { get; } + protected CoreTestData BloggingTestData { get; } + + protected PostRepository_Tests() + { + PostRepository = GetRequiredService(); + BloggingTestData = GetRequiredService(); + } + + [Fact] + public async Task GetListOfPostAsync() + { + var posts = await PostRepository.GetPostsByBlogId(BloggingTestData.Blog1Id); + posts.ShouldNotBeNull(); + posts.Count.ShouldBe(2); + posts.ShouldContain(x => x.Id == BloggingTestData.Blog1Post1Id); + posts.ShouldContain(x => x.Id == BloggingTestData.Blog1Post2Id); + } + + [Fact] + public async Task GetPostByUrl() + { + var post = await PostRepository.GetPostByUrl(BloggingTestData.Blog1Id, "url"); + post.ShouldNotBeNull(); + post.Url.ShouldBe("url"); + } + } +} diff --git a/aspnet-core/test/Bcvp.Blog.Core.TestBase/BlogCore/Tagging/TagRepository_Tests.cs b/aspnet-core/test/Bcvp.Blog.Core.TestBase/BlogCore/Tagging/TagRepository_Tests.cs new file mode 100644 index 0000000..81b5a6f --- /dev/null +++ b/aspnet-core/test/Bcvp.Blog.Core.TestBase/BlogCore/Tagging/TagRepository_Tests.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Modularity; +using Xunit; + +namespace Bcvp.Blog.Core.BlogCore.Tagging +{ + public abstract class TagRepository_Tests : CoreTestBase + where TStartupModule : IAbpModule + { + protected ITagRepository TagRepository { get; } + protected CoreTestData BloggingTestData { get; } + + protected TagRepository_Tests() + { + TagRepository = GetRequiredService(); + BloggingTestData = GetRequiredService(); + } + + [Fact] + public async Task GetListAsync() + { + var tags = await TagRepository.GetListAsync(BloggingTestData.Blog1Id); + tags.ShouldNotBeNull(); + tags.Count.ShouldBe(2); + } + + [Fact] + public async Task GetByNameAsync() + { + var tag = await TagRepository.GetByNameAsync(BloggingTestData.Blog1Id, BloggingTestData.Tag1Name); + tag.ShouldNotBeNull(); + tag.Name.ShouldBe(BloggingTestData.Tag1Name); + } + + [Fact] + public async Task FindByNameAsync() + { + var tag = await TagRepository.FindByNameAsync(BloggingTestData.Blog1Id, BloggingTestData.Tag1Name); + tag.ShouldNotBeNull(); + tag.Name.ShouldBe(BloggingTestData.Tag1Name); + } + + [Fact] + public async Task GetListAsync2() + { + var tagIds = (await TagRepository.GetListAsync()).Select(x => x.Id).ToList(); + var tags = await TagRepository.GetListAsync(tagIds); + tags.ShouldNotBeNull(); + tags.Count.ShouldBe(tagIds.Count); + } + + [Fact] + public async Task DecreaseUsageCountOfTags() + { + var tag = await TagRepository.FindByNameAsync(BloggingTestData.Blog1Id, BloggingTestData.Tag1Name); + var usageCount = tag.UsageCount; + + await TagRepository.DecreaseUsageCountOfTagsAsync(new List() + { + tag.Id + }); + + await TagRepository.FindByNameAsync(BloggingTestData.Blog1Id, BloggingTestData.Tag1Name); + (await TagRepository.FindByNameAsync(BloggingTestData.Blog1Id, BloggingTestData.Tag1Name)).UsageCount + .ShouldBe(usageCount - 1); + } + } +} diff --git a/aspnet-core/test/Bcvp.Blog.Core.TestBase/BlogCore/Users/BlogUserRepository_Tests.cs b/aspnet-core/test/Bcvp.Blog.Core.TestBase/BlogCore/Users/BlogUserRepository_Tests.cs new file mode 100644 index 0000000..7717fc6 --- /dev/null +++ b/aspnet-core/test/Bcvp.Blog.Core.TestBase/BlogCore/Users/BlogUserRepository_Tests.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Modularity; +using Xunit; + +namespace Bcvp.Blog.Core.BlogCore.Users +{ + public abstract class BlogUserRepository_Tests : CoreTestBase + where TStartupModule : IAbpModule + { + + protected IBlogUserRepository UserRepository { get; } + + protected BlogUserRepository_Tests() + { + UserRepository = GetRequiredService(); + } + + [Fact] + public async Task GetUsersAsync() + { + var users = await UserRepository.GetUsersAsync(10, null); + users.ShouldNotBeNull(); + users.Count.ShouldBe(0); + } + + + } +} diff --git a/aspnet-core/test/Bcvp.Blog.Core.TestBase/CoreTestData.cs b/aspnet-core/test/Bcvp.Blog.Core.TestBase/CoreTestData.cs new file mode 100644 index 0000000..5945e61 --- /dev/null +++ b/aspnet-core/test/Bcvp.Blog.Core.TestBase/CoreTestData.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace Bcvp.Blog.Core +{ + public class CoreTestData : ISingletonDependency + { + public Guid Blog1Id { get; } = Guid.NewGuid(); + public Guid Blog1Post1Id { get; } = Guid.NewGuid(); + public Guid Blog1Post2Id { get; } = Guid.NewGuid(); + public Guid Blog1Post1Comment1Id { get; } = Guid.NewGuid(); + public Guid Blog1Post1Comment2Id { get; } = Guid.NewGuid(); + public string Tag1Name { get; } = "Tag1Name"; + public string Tag2Name { get; } = "Tag2Name"; + } +} diff --git a/aspnet-core/test/Bcvp.Blog.Core.TestBase/CoreTestDataSeedContributor.cs b/aspnet-core/test/Bcvp.Blog.Core.TestBase/CoreTestDataSeedContributor.cs index d76ae0b..abe1c35 100644 --- a/aspnet-core/test/Bcvp.Blog.Core.TestBase/CoreTestDataSeedContributor.cs +++ b/aspnet-core/test/Bcvp.Blog.Core.TestBase/CoreTestDataSeedContributor.cs @@ -1,16 +1,76 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; +using Bcvp.Blog.Core.BlogCore.Blogs; +using Bcvp.Blog.Core.BlogCore.Comments; +using Bcvp.Blog.Core.BlogCore.Posts; +using Bcvp.Blog.Core.BlogCore.Tagging; using Volo.Abp.Data; using Volo.Abp.DependencyInjection; +using Volo.Abp.MultiTenancy; namespace Bcvp.Blog.Core { public class CoreTestDataSeedContributor : IDataSeedContributor, ITransientDependency { - public Task SeedAsync(DataSeedContext context) + + private readonly CoreTestData _testData; + private readonly IBlogRepository _blogRepository; + private readonly IPostRepository _postRepository; + private readonly ICommentRepository _commentRepository; + private readonly ITagRepository _tagRepository; + private readonly ICurrentTenant _currentTenant; + + public CoreTestDataSeedContributor( + CoreTestData testData, + IBlogRepository blogRepository, + IPostRepository postRepository, + ICommentRepository commentRepository, + ITagRepository tagRepository, + ICurrentTenant currentTenant) + { + _testData = testData; + _blogRepository = blogRepository; + _postRepository = postRepository; + _commentRepository = commentRepository; + _tagRepository = tagRepository; + _currentTenant = currentTenant; + } + + public async Task SeedAsync(DataSeedContext context) { /* Seed additional test data... */ + using (_currentTenant.Change(context?.TenantId)) + { + await SeedBlogsAsync(); + await SeedPostsAsync(); + await SeedCommentsAsync(); + await SeedTagsAsync(); + + } + } + + + private async Task SeedBlogsAsync() + { + await _blogRepository.InsertAsync(new BlogCore.Blogs.Blog(_testData.Blog1Id, "The First Blog", "blog-1")); + } + + private async Task SeedPostsAsync() + { + await _postRepository.InsertAsync(new Post(_testData.Blog1Post1Id, _testData.Blog1Id, "title", "coverImage", "url")); + await _postRepository.InsertAsync(new Post(_testData.Blog1Post2Id, _testData.Blog1Id, "title2", "coverImage2", "url2")); + } - return Task.CompletedTask; + public async Task SeedCommentsAsync() + { + await _commentRepository.InsertAsync(new Comment(_testData.Blog1Post1Comment1Id, _testData.Blog1Post1Id, null, "text")); + await _commentRepository.InsertAsync(new Comment(_testData.Blog1Post1Comment2Id, _testData.Blog1Post1Id, _testData.Blog1Post1Comment1Id, "text")); + } + + public async Task SeedTagsAsync() + { + await _tagRepository.InsertAsync(new Tag(Guid.NewGuid(), _testData.Blog1Id, _testData.Tag1Name, 10)); + await _tagRepository.InsertAsync(new Tag(Guid.NewGuid(), _testData.Blog1Id, _testData.Tag2Name)); } } } \ No newline at end of file diff --git "a/dosc/1.Abp vNext \345\237\272\347\241\200\347\257\207\344\270\250\344\273\213\347\273\215.md" "b/dosc/1.Abp vNext \345\237\272\347\241\200\347\257\207\344\270\250\344\273\213\347\273\215.md" deleted file mode 100644 index 782500b..0000000 --- "a/dosc/1.Abp vNext \345\237\272\347\241\200\347\257\207\344\270\250\344\273\213\347\273\215.md" +++ /dev/null @@ -1,85 +0,0 @@ - - -## 缘起 - -在.Net行也目前工作5年,前年认识了`老张的哲学`,也跟着一起学习了非常多的知识,非常感谢老张鼓励我来做ABP这个系列的教程,我也努努力带着我那小小的海漂梦(上海漂流)来搞起。 - -## ABP vNext 简要介绍 - -ABP vNext 框架是一个基于ASP.NET核心的完整基础设施,通过遵循软件开发最佳实践和最新技术来创建现代web应用程序和API,不同于老的ABP框架新的 ABP vNext 框架核心库更加精简,因为将原有许多的组件从其核心库抽离成独立的组件。这样开发人员可以更加灵活的选择自己需要的功能进行集成,使项目远离臃肿的库,比起原有的 ABP 框架 ABP vNext 完全基于 ASP.NET Core 丢掉了历史包袱,设计更加合理,更加细粒度的模块化设计。 - -Abp vNext 官方文档提供了非常全面的功能介绍,官方提供了启动模板,模板遵循了领域驱动设计的最佳实践来进行项目分层,引入了常用的功能模块。 - -如果你有不错的.Net基础那么Abp你用起来会的心用手,使用过程中遇到的问题几乎可以在官方文档和Issues中找到并解决。 - -但如果你是一个 .Net 新手你不知道什么是依赖注入、模块化、DDD 推荐先去 `https://www.cnblogs.com/laozhang-is-phi/p/9495618.html#autoid-1-0-0`学习一下。 - -后续文章中出现的ABP都是指ABP vNext。 - -## 开篇简介 - -`Bcvp.Blog.Core`是基于`老张的哲学`Blog.Core项目采用ABP vNext框架和DDD思想进行重构的项目,教程面向.Net Core初中级开发人员,从基础项目搭建开始一步步学习使用ABP vNext框架并在开发中融入DDD思想。 - -整篇文章目前会分为3个阶段分别是。 - -- 基础篇(学习ABP vNext框架和DDD)。 -- 中级篇(学习模块化和部分源代码)。 -- 高级篇(ABP vNext微服务)。 - -组织地址: -https://github.com/BaseCoreVueProject - -作者博客: -https://www.cnblogs.com/MrChuJiu/ - - -## 框架功能 - -整体教程会设计的功能介绍如下,下图为ABP官方商业版,我们只抽取部分功能进行实现。 - -前端框架目前采用Bcvp组织的:https://github.com/BaseCoreVueProject/angular-template 作为前端教程框架,暂时不推荐将该框架应用生产(不是技术问题),ABP官方的目前正在推行LeptonX应该会有新的进展 - -![Abp官方商业版图](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/8.png) - -ABP 自带功能 -- Saas管理 -- 组织管理 -- 角色管理 -- 审计日志 -- 系统设置 - -业务功能 -- 博客管理 -- 文章管理 - -前端站点 -- 发布文章 -- 用户登录/注册 -- 文章评论 - -## 项目分层依赖关系 - - -![项目依赖关系](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/6.png) - -Domain.Shared 其他项⽬直接或间接引⽤,项⽬中定义的类型在所有项⽬中共享。 - -Domain 只引⽤ Domain.Shared ,⽐如:在 Domain.Shared 中定义的 IssuType 枚举类型需要 在 Domain 项⽬中 Issue 实体中⽤到。 - -Application.Contracts 依赖 Domain.Shared ,这样我们可以在 DTOs 中使⽤这些共享类型。 ⽐如: CreateIssueDto 中可以直接使⽤ IssueType 枚举。 - -Application 依赖 Application.Contracts ,因为 Application 实现 Application.Contracts 中定义的服务接⼝和使⽤ DTO 对象。同时,引⽤ Domain 项⽬,在应 ⽤服务中使⽤仓储接⼝或领域对象。 - -EntiryFrameworkCore 依赖 Domain ,映射 Domain 对象(实体和值类型)到数据库表 (ORM)并实现在 Domain 中定义的仓储接⼝。 - -HttpApi 依赖 Application.Contract ,在控制器在内部对 应⽤服务接⼝ 进⾏依赖注⼊。 - -HttpApi.Client 依赖 Application.Contract 消费应⽤服务 Web 依赖 HttpApi ,发布⾥⾯定义的 HTTP APIs 。另外,通过这种⽅式,它间接地依赖于 Application.Contracts 项⽬,可以在⻚⾯/组件中使⽤应⽤服务 - - -## 结语 -本节只是作为一个开篇讲解希望各位持续关注 - -联系作者:加群:867095512 @MrChuJiu - -![公众号](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/wxgzh.png) diff --git "a/dosc/10.Abp vNext \345\237\272\347\241\200\347\257\207\344\270\250\346\235\203\351\231\220.md" "b/dosc/10.Abp vNext \345\237\272\347\241\200\347\257\207\344\270\250\346\235\203\351\231\220.md" deleted file mode 100644 index 6fe76a5..0000000 --- "a/dosc/10.Abp vNext \345\237\272\347\241\200\347\257\207\344\270\250\346\235\203\351\231\220.md" +++ /dev/null @@ -1,496 +0,0 @@ -## 介绍 - -本章节来把接口的权限加一下 - -## 权限配置和使用 - -官方地址:https://docs.abp.io/en/abp/latest/Authorization - -下面这种代码可能我们日常开发都写过,ASP.NET Core 提供的Authorize特性来帮我们做授权,但是`BookStore_Author_Create`策略,需要我们去手动声明。 - -![权限配置](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/32.png) - -Abp定义了一个叫`Permission System`叫权限系统啥的都可以,来帮助我们轻松的搞定授权,具体怎么玩看下面代码就三部。 - -![权限配置](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/31.png) - -#### 一、定义常量 -```cs - public static class CorePermissions - { - public const string GroupName = "Bvcp.Core"; - - public static class Blogs - { - public const string Default = GroupName + ".Blog"; - public const string Management = Default + ".Management"; - public const string Delete = Default + ".Delete"; - public const string Update = Default + ".Update"; - public const string Create = Default + ".Create"; - public const string ClearCache = Default + ".ClearCache"; - } - - public static class Posts - { - public const string Default = GroupName + ".Post"; - public const string Delete = Default + ".Delete"; - public const string Update = Default + ".Update"; - public const string Create = Default + ".Create"; - } - - public static class Tags - { - public const string Default = GroupName + ".Tag"; - public const string Delete = Default + ".Delete"; - public const string Update = Default + ".Update"; - public const string Create = Default + ".Create"; - } - - public static class Comments - { - public const string Default = GroupName + ".Comment"; - public const string Delete = Default + ".Delete"; - public const string Update = Default + ".Update"; - public const string Create = Default + ".Create"; - } - - public static string[] GetAll() - { - return ReflectionHelper.GetPublicConstantsRecursively(typeof(CorePermissions)); - } - } -``` - -#### 二、添加权限配置 -```cs - public class CorePermissionDefinitionProvider : PermissionDefinitionProvider - { - public override void Define(IPermissionDefinitionContext context) - { - var bloggingGroup = context.AddGroup(CorePermissions.GroupName, L("Permission:Core")); - - var blogs = bloggingGroup.AddPermission(CorePermissions.Blogs.Default, L("Permission:Blogs")); - blogs.AddChild(CorePermissions.Blogs.Management, L("Permission:Management")); - blogs.AddChild(CorePermissions.Blogs.Update, L("Permission:Edit")); - blogs.AddChild(CorePermissions.Blogs.Delete, L("Permission:Delete")); - blogs.AddChild(CorePermissions.Blogs.Create, L("Permission:Create")); - blogs.AddChild(CorePermissions.Blogs.ClearCache, L("Permission:ClearCache")); - - var posts = bloggingGroup.AddPermission(CorePermissions.Posts.Default, L("Permission:Posts")); - posts.AddChild(CorePermissions.Posts.Update, L("Permission:Edit")); - posts.AddChild(CorePermissions.Posts.Delete, L("Permission:Delete")); - posts.AddChild(CorePermissions.Posts.Create, L("Permission:Create")); - - var tags = bloggingGroup.AddPermission(CorePermissions.Tags.Default, L("Permission:Tags")); - tags.AddChild(CorePermissions.Tags.Update, L("Permission:Edit")); - tags.AddChild(CorePermissions.Tags.Delete, L("Permission:Delete")); - tags.AddChild(CorePermissions.Tags.Create, L("Permission:Create")); - - var comments = bloggingGroup.AddPermission(CorePermissions.Comments.Default, L("Permission:Comments")); - comments.AddChild(CorePermissions.Comments.Update, L("Permission:Edit")); - comments.AddChild(CorePermissions.Comments.Delete, L("Permission:Delete")); - comments.AddChild(CorePermissions.Comments.Create, L("Permission:Create")); - } - - private static LocalizableString L(string name) - { - return LocalizableString.Create(name); - } - } -``` - -#### 三、使用 -```cs -Authorize(CorePermissions.Posts.Delete)] -``` -![使用权限](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/33.png) - - - - - - - - - - - - - - - -## 资源授权方案 - -资源授权可能用过的人不多,代码都会在`整体的修改代码`一节这里先看就行,可以参考微软官方文档 - -https://docs.microsoft.com/zh-cn/aspnet/core/security/authorization/resourcebased?view=aspnetcore-5.0 - - -有些场景下,授权需要依赖于要访问的资源,例如:每个资源通常会有一个创建者属性,我们只允许该资源的创建者才可以对其进行编辑,删除等操作,这就无法通过[Authorize]特性来指定授权了。因为授权过滤器会在我们的应用代码之前执行,无法确定所访问的资源。此时,我们需要使用基于资源的授权,下面就来演示一下具体是如何操作的。 - -#### 定义资源Requirement -在基于资源的授权中,我们要判断的是用户是否具有针对该资源的某项操作,因此,我们先定义一个代表操作的Requirement: -```cs - public static class CommonOperations - { - public static OperationAuthorizationRequirement Update = new OperationAuthorizationRequirement { Name = nameof(Update) }; - public static OperationAuthorizationRequirement Delete = new OperationAuthorizationRequirement { Name = nameof(Delete) }; - } -``` - -#### 实现资源授权Handler -每一个 Requirement 都需要有一个对应的 Handler,来完成授权逻辑,可以直接让 Requirement 实现IAuthorizationHandler接口。 - -我们是根据资源的创建者来判断用户是否具有操作权限,实现我们的授权Handler: -```cs - public class CommentAuthorizationHandler : AuthorizationHandler - { - private readonly IPermissionChecker _permissionChecker; - - public CommentAuthorizationHandler(IPermissionChecker permissionChecker) - { - _permissionChecker = permissionChecker; - } - - protected override async Task HandleRequirementAsync( - AuthorizationHandlerContext context, - OperationAuthorizationRequirement requirement, - Comment resource) - { - if (requirement.Name == CommonOperations.Delete.Name && await HasDeletePermission(context, resource)) - { - context.Succeed(requirement); - return; - } - - if (requirement.Name == CommonOperations.Update.Name && await HasUpdatePermission(context, resource)) - { - context.Succeed(requirement); - return; - } - } - - private async Task HasDeletePermission(AuthorizationHandlerContext context, Comment resource) - { - // 判断创建人是否是登陆人 - if (resource.CreatorId != null && resource.CreatorId == context.User.FindUserId()) - { - return true; - } - // 判断当前用户是否满足资源操作策略 - if (await _permissionChecker.IsGrantedAsync(context.User, CorePermissions.Comments.Delete)) - { - return true; - } - - return false; - } - - private async Task HasUpdatePermission(AuthorizationHandlerContext context, Comment resource) - { - // 判断创建人是否是登陆人 - if (resource.CreatorId != null && resource.CreatorId == context.User.FindUserId()) - { - return true; - } - // 判断当前用户是否满足资源操作策略 - if (await _permissionChecker.IsGrantedAsync(context.User, CorePermissions.Comments.Update)) - { - return true; - } - - return false; - } - } -``` - - -注册Handler,这一点不要忘了 - -```cs - public class CoreApplicationModule : AbpModule - { - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - options.AddMaps(); - }); - - Configure(options => - { - options.AddPolicy("BloggingUpdatePolicy", policy => policy.Requirements.Add(CommonOperations.Update)); - options.AddPolicy("BloggingDeletePolicy", policy => policy.Requirements.Add(CommonOperations.Delete)); - }); - - context.Services.AddSingleton(); - context.Services.AddSingleton(); - - - } - } -``` - - -#### 使用策略授权 -```cs - [Authorize] - public async Task UpdateAsync(Guid id, UpdateCommentDto input) - { - - var comment = await _commentRepository.GetAsync(id); - // 检测是否有权限 - await AuthorizationService.CheckAsync(comment, CommonOperations.Update); - - comment.SetText(input.Text); - - comment = await _commentRepository.UpdateAsync(comment); - - return ObjectMapper.Map(comment); - } -``` - - - - - - -## 整体的修改代码 - - -![整体结构](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/34.png) - - -```cs - public static class CommonOperations - { - public static OperationAuthorizationRequirement Update = new OperationAuthorizationRequirement { Name = nameof(Update) }; - public static OperationAuthorizationRequirement Delete = new OperationAuthorizationRequirement { Name = nameof(Delete) }; - } -``` - -```cs - public class CommentAuthorizationHandler : AuthorizationHandler - { - private readonly IPermissionChecker _permissionChecker; - - public CommentAuthorizationHandler(IPermissionChecker permissionChecker) - { - _permissionChecker = permissionChecker; - } - - protected override async Task HandleRequirementAsync( - AuthorizationHandlerContext context, - OperationAuthorizationRequirement requirement, - Comment resource) - { - if (requirement.Name == CommonOperations.Delete.Name && await HasDeletePermission(context, resource)) - { - context.Succeed(requirement); - return; - } - - if (requirement.Name == CommonOperations.Update.Name && await HasUpdatePermission(context, resource)) - { - context.Succeed(requirement); - return; - } - } - - private async Task HasDeletePermission(AuthorizationHandlerContext context, Comment resource) - { - if (resource.CreatorId != null && resource.CreatorId == context.User.FindUserId()) - { - return true; - } - - if (await _permissionChecker.IsGrantedAsync(context.User, CorePermissions.Comments.Delete)) - { - return true; - } - - return false; - } - - private async Task HasUpdatePermission(AuthorizationHandlerContext context, Comment resource) - { - if (resource.CreatorId != null && resource.CreatorId == context.User.FindUserId()) - { - return true; - } - - if (await _permissionChecker.IsGrantedAsync(context.User, CorePermissions.Comments.Update)) - { - return true; - } - - return false; - } - } -``` - -```cs - public class PostAuthorizationHandler : AuthorizationHandler - { - private readonly IPermissionChecker _permissionChecker; - - public PostAuthorizationHandler(IPermissionChecker permissionChecker) - { - _permissionChecker = permissionChecker; - } - - protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement, - Post resource) - { - - if (requirement.Name == CommonOperations.Delete.Name && await HasDeletePermission(context, resource)) - { - context.Succeed(requirement); - return; - } - - if (requirement.Name == CommonOperations.Update.Name && await HasUpdatePermission(context, resource)) - { - context.Succeed(requirement); - return; - } - - } - - private async Task HasDeletePermission(AuthorizationHandlerContext context, Post resource) - { - if (resource.CreatorId != null && resource.CreatorId == context.User.FindUserId()) - { - return true; - } - - if (await _permissionChecker.IsGrantedAsync(context.User, CorePermissions.Posts.Delete)) - { - return true; - } - - return false; - } - - private async Task HasUpdatePermission(AuthorizationHandlerContext context, Post resource) - { - if (resource.CreatorId != null && resource.CreatorId == context.User.FindUserId()) - { - return true; - } - - if (await _permissionChecker.IsGrantedAsync(context.User, CorePermissions.Posts.Update)) - { - return true; - } - - return false; - } - } -``` - -CoreApplicationModule.cs - -```cs - public class CoreApplicationModule : AbpModule - { - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - options.AddMaps(); - }); - - // 注册 - Configure(options => - { - options.AddPolicy("BloggingUpdatePolicy", policy => policy.Requirements.Add(CommonOperations.Update)); - options.AddPolicy("BloggingDeletePolicy", policy => policy.Requirements.Add(CommonOperations.Delete)); - }); - - context.Services.AddSingleton(); - context.Services.AddSingleton(); - - - } - } -``` - -CommentAppService.cs - -```cs - [Authorize] - public async Task UpdateAsync(Guid id, UpdateCommentDto input) - { - - var comment = await _commentRepository.GetAsync(id); - - await AuthorizationService.CheckAsync(comment, CommonOperations.Update); - - comment.SetText(input.Text); - - comment = await _commentRepository.UpdateAsync(comment); - - return ObjectMapper.Map(comment); - } - - [Authorize] - public async Task DeleteAsync(Guid id) - { - var comment = await _commentRepository.GetAsync(id); - - await AuthorizationService.CheckAsync(comment, CommonOperations.Delete); - - var replies = await _commentRepository.GetRepliesOfComment(id); - - foreach (var reply in replies) - { - await _commentRepository.DeleteAsync(reply.Id); - } - } -``` - -PostAppService.cs - -```cs - [Authorize(CorePermissions.Posts.Delete)] - public async Task DeleteAsync(Guid id) - { - // 查找文章 - var post = await _postRepository.GetAsync(id); - // 判断是否有资源操作权 - await AuthorizationService.CheckAsync(post, CommonOperations.Delete); - // 根据文章获取Tags - var tags = await GetTagsOfPost(id); - // 减少Tag引用数量 - await _tagRepository.DecreaseUsageCountOfTagsAsync(tags.Select(t => t.Id).ToList()); - // 删除评论 - await _commentRepository.DeleteOfPost(id); - // 删除文章 - await _postRepository.DeleteAsync(id); - await PublishPostChangedEventAsync(post.BlogId); - } - - [Authorize(CorePermissions.Posts.Update)] - public async Task UpdateAsync(Guid id, UpdatePostDto input) - { - var post = await _postRepository.GetAsync(id); - - input.Url = await RenameUrlIfItAlreadyExistAsync(input.BlogId, input.Url, post); - - await AuthorizationService.CheckAsync(post, CommonOperations.Update); - - post.SetTitle(input.Title); - post.SetUrl(input.Url); - post.Content = input.Content; - post.Description = input.Description; - post.CoverImage = input.CoverImage; - - post = await _postRepository.UpdateAsync(post); - - var tagList = SplitTags(input.Tags); - await SaveTags(tagList, post); - await PublishPostChangedEventAsync(post.BlogId); - return ObjectMapper.Map(post); - } -``` \ No newline at end of file diff --git "a/dosc/2.Abp vNext \345\237\272\347\241\200\347\257\207\344\270\250\346\241\206\346\236\266\346\220\255\345\273\272.md" "b/dosc/2.Abp vNext \345\237\272\347\241\200\347\257\207\344\270\250\346\241\206\346\236\266\346\220\255\345\273\272.md" deleted file mode 100644 index 1ebeff3..0000000 --- "a/dosc/2.Abp vNext \345\237\272\347\241\200\347\257\207\344\270\250\346\241\206\346\236\266\346\220\255\345\273\272.md" +++ /dev/null @@ -1,85 +0,0 @@ - -## 介绍 - -ABP目前的最新版本是`4.4`也是最近才发布的,文章目前采用的是Angular作为UI框架,使用Entity Framework Core作为数据库提供者,如果你想用其他UI框架需要自己完成欢迎提交(pr) - -## 创建项目 - -在 https://abp.io/ 首页,点击开始创建项目,项目名称`Bcvp.Blog.Core`,勾选Tiered,ABP默认采用Ids4授权,勾选后他会将Ids4单独分离一层出来。 - -![创建项目](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/1.png) - -## 启动项目 - -项目下载下来后打开项目修改`appsettings.json`的字符串连接,这里有三处要改分别是DbMigrator、HttpApi.Host、IdentityServer. - -![创建项目](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/2.png) - - -另外Abp项目默认采用Redis作为缓存提供者,如果你不想使用Redis可以直接删掉或者加一个`"IsEnabled":"false"`来关闭redis。 - -上面的完成后将`DbMigrator`设为启动项目,在程序包管理控制台选择`Bcvp.Blog.Core.EntityFrameworkCore`输入`Add-Migration Init`生成迁移文件。然后启动`DbMigrator`运行项目该项目会执行迁移并添加种子数据,这里我说一下种子数据,ABP默认生成的种子数据是HOST理解为最高管理员,具体代码可以看`Bcvp.Blog.Core.Domain`下的`CoreDbMigrationService.cs`,至于怎么自己写一个等后面业务用到的时候在单独讲。 -```cs - private async Task SeedDataAsync(Tenant tenant = null) - { - Logger.LogInformation($"Executing {(tenant == null ? "host" : tenant.Name + " tenant")} database seed..."); - - await _dataSeeder.SeedAsync(new DataSeedContext(tenant?.Id) - .WithProperty(IdentityDataSeedContributor.AdminEmailPropertyName, IdentityDataSeedContributor.AdminEmailDefaultValue) - .WithProperty(IdentityDataSeedContributor.AdminPasswordPropertyName, IdentityDataSeedContributor.AdminPasswordDefaultValue) - ); - } -``` - -数据都搞定了那就直接启动项目,因为创建项目的时候勾选了Tiered所以会生成2个Web项目,可以在解决方案上右键属性,多项目启动,启动后默认用户名:Admin 密码:1q2w3E* - -![设置运行](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/4.png) - -![运行情况](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/5.png) - - -## 修改配置 - -我本人脑子有时候不好使Abp密码我记不住所以很难受,对于有相同问题的朋友可以参照下面这样修改密码配置。 - -在`Domain.Shared`层下面新建`CoreIdentityConsts`用于用于更换Abp的默认HOST账号配置信息(这里我们要记住一个开发规范,聚合内的常量和枚举要放在`Domain.Shared`层)。 - -```cs - public static class CoreIdentityConsts - { - public const string AdminEmailDefaultValue = "mrchujiu@abp.io"; - public const string AdminPasswordDefaultValue = "123456"; - } - - - await _dataSeeder.SeedAsync(new DataSeedContext(tenant?.Id) - .WithProperty(IdentityDataSeedContributor.AdminEmailPropertyName, CoreIdentityConsts.AdminEmailDefaultValue) - .WithProperty(IdentityDataSeedContributor.AdminPasswordPropertyName, CoreIdentityConsts.AdminPasswordDefaultValue) - ); - - -``` - -在`Bcvp.Blog.Core.Domain`层中在`CoreDomainModule.cs`的`ConfigureServices`方法中加入如下代码修改`Identity`配置,虽然关闭了限制但是因为我们没修改密码的页面暂时也只好删除数据库重新跑一下`DbMigrator`迁移来做了,以后就`123456`登录吧。 - -```cs - Configure(options => - { - options.Password.RequireNonAlphanumeric = false; - options.Password.RequireLowercase = false; - options.Password.RequireUppercase = false; - options.Password.RequireDigit = false; - }); -``` - - -## 结语 -本节知识点: -- 1.一个基础的ABP框架 -- 2.修改种子数据配置 - -联系作者:加群:867095512 @MrChuJiu - -![公众号](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/wxgzh.png) - - diff --git "a/dosc/3.Abp vNext \345\237\272\347\241\200\347\257\207\344\270\250\345\210\206\345\261\202\346\236\266\346\236\204.md" "b/dosc/3.Abp vNext \345\237\272\347\241\200\347\257\207\344\270\250\345\210\206\345\261\202\346\236\266\346\236\204.md" deleted file mode 100644 index b459c31..0000000 --- "a/dosc/3.Abp vNext \345\237\272\347\241\200\347\257\207\344\270\250\345\210\206\345\261\202\346\236\266\346\236\204.md" +++ /dev/null @@ -1,177 +0,0 @@ - -## 介绍 - -本章节对 ABP 框架进行一个简单的介绍,摘自ABP官方,后面会在使用过程中对各个知识点进行细致的讲解。 - -## 领域驱动设计 - -领域驱动设计(简称:DDD)是一种针对复杂需求的软件开发方法。将软件实现与不断发展的模型联系起来,专注于核心领域逻辑,而不是基础设施细节。DDD适用于复杂领域和大规模应用,而不是简单的CRUD应用。它有助于建立一个灵活、模块化和可维护的代码库。 - -一个基于领域驱动的解决方案有四个基本层: - -领域层:实现领域(或系统)中的用例独立的核心业务逻辑。 -应用层:基于领域的应用程序用例,应用程序用例可以看作是用户界面上的用户交互。 -展示层:包含应用程序UI元素(页面、组件等)。 -基础层:支持层,通过对第三方类库的调用或系统的抽象和集成来实现对其他层的支持。 - -#### 核心构件 - -DDD主要关注领域层和应用层,展示层和基础层被看作是细节,业务层不应该依赖于它们,但这并不意味着展示层和基础层不重要,它们也非常重要。展示层中的UI框架和基础层中的数据提供程序有他们自己的实现规则和最佳实践,需要了解和应用。然而,这些并不在DDD的主题中,我们重点来看领域层和应用层的基本构件。 - -###### 领域层构件 - -实体(Entity):一个实体是一个对象,该对象包含自己的属性和方法,属性用于存储数据和描述状态;方法结合属性实现业务逻辑。一个实体使用唯一标识(ID)来表示,两个实体对象ID不同则是为不同的实体。 - -值对象(Value Object):值对象是另一种类型的领域对象,该对象由其属性而不是唯一ID来标识。意思是说,只有全部属性相同才会被认为是同一个对象。值对象通常被实现为不可变的,而且大多比实体简单得多。 - -聚合和聚合根:聚合根是一个特定类型的实体,具有额外的职责。聚合是以聚合根为中心绑定在一起的一组对象,对象包括实体和值对象。 - -仓储(接口):仓储是一个类似集合的接口,被领域层和应用层用来访问数据持久化系统(数据库)。它将数据库的复杂性从业务代码中隐藏起来。领域层包含仓储接口。 - -领域服务:领域服务是无状态服务,实现核心领域业务规则。用于实现依赖于多个聚合(实体)或外部服务的领域逻辑。 - -规约:用于为实体和其他业务对象定义可命名的、可重用的和可组合的过滤器。 - -领域事件:领域事件是一种低耦合的通知方式,当一个特定的领域事件发生时,会通知其他服务。 - -###### 应用层构件 - -应用服务:应用服务是无状态服务,实现应用程序用例。一个应用服务通常获取和返回数据传输对象(DTOs),用于展示层。调用领域对象来实现用例。一个用例通常被认为是一个工作单元。 - -数据传输对象(DTO):DTO是简单对象,不包含任何业务逻辑,只用于在应用层和展示层传递数据。 - -工作单元:一个工作单元是一个原子工作。在工作单元中的所有操作统一提交,要么全部成功,失败则全部回滚。 - - -## ABP项目分层解析 - -![项目结构](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/7.png) - -#### 领域层 - -领域层拆分为两个项目: - -Bcvp.Blog.Core.Domain:领域层,该项目包含所有领域层构件,比如:实体、值对象、领域服务、规约、仓储接口等。 - -Bcvp.Blog.Core.Domain.Shared:领域共享层,包含属于领域层,但是与其他层共享的类型。举个例子:定义的常量和枚举,既在领域对象中使用,也要在其他层中使用,放在该项目中。 - -#### 应用层 - -应用层拆分为两个项目: - -Bcvp.Blog.Core.Application.Contracts:应用契约层,包含应用服务接口和数据传输对象(用于接口),该项目被应用程序客户端引用,比如:WEB项目、API客户端项目。 - -Bcvp.Blog.Core.Application:应用层,实现在 Contracts 项目中定义的接口。 - -#### 展示层 - -Bcvp.Blog.Core.HttpApi.Host 项目作为一个独立的端点提供 HTTP API 服务,供客户端调用。 - -#### 远程服务层 - -Bcvp.Blog.Core.HttpApi:远程服务层,该项目用于定义 HTTP APIs,通常包含 MVC Controller 及相关的模型。 - -Bcvp.Blog.Core.HttpApi.Client:远程服务代理层,客户端应用程序引用该项目,将直接通过依赖注入使用远程应用服务,该项目基于ABP Framework动态C#客户端API代理系统实现。在C#项目中需要调用HTTP APIs时,会非常有用。 - -#### 基础层 - -实现DDD时,可以使用一个基础层项目来实现所有的集成和抽象,当然也可以为不同依赖创建不同项目。 - -建议折中处理,为核心基础依赖创建单独项目,比如:Entity Framework Core;另外创建一个公共基础项目存放其他基础设施。 - -启动模板中包含两个项目对 Entity Framework Core 进行集成: - -Bcvp.Blog.Core.EntityFrameworkCore:EF Core核心基础依赖项目,包含:数据上下文、数据库映射、EF Core仓储实现等。 - -#### 其他项目 - -还有一个项目 Bcvp.Blog.Core.DbMigrator,一个简单的控制台应用程序,当你执行它时,会迁移数据库结构并初始化种子数据。这是一个有用的实用程序,可以在开发和生产环境中使用它。 - -## 项目依赖关系 - -![项目依赖关系](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/6.png) - -Domain.Shared 其他项⽬直接或间接引⽤,项⽬中定义的类型在所有项⽬中共享。 - -Domain 只引⽤ Domain.Shared ,⽐如:在 Domain.Shared 中定义的 IssuType 枚举类型需要 在 Domain 项⽬中 Issue 实体中⽤到。 - -Application.Contracts 依赖 Domain.Shared ,这样我们可以在 DTOs 中使⽤这些共享类型。 ⽐如: CreateIssueDto 中可以直接使⽤ IssueType 枚举。 - -Application 依赖 Application.Contracts ,因为 Application 实现 Application.Contracts 中定义的服务接⼝和使⽤ DTO 对象。同时,引⽤ Domain 项⽬,在应 ⽤服务中使⽤仓储接⼝或领域对象。 - -EntiryFrameworkCore 依赖 Domain ,映射 Domain 对象(实体和值类型)到数据库表 (ORM)并实现在 Domain 中定义的仓储接⼝。 - -HttpApi 依赖 Application.Contract ,在控制器在内部对 应⽤服务接⼝ 进⾏依赖注⼊。 - -HttpApi.Client 依赖 Application.Contract 消费应⽤服务 Web 依赖 HttpApi ,发布⾥⾯定义的 HTTP APIs 。另外,通过这种⽅式,它间接地依赖于 Application.Contracts 项⽬,可以在⻚⾯/组件中使⽤应⽤服务 - -## DDD通用原则 - -在正式开始之前我们在梳理一下DDD的通用原则。 - -#### 数据库(Database Provider / ORM)独⽴性原则 - -领域层和应⽤层不知道项⽬中使⽤的 ORM 和 Database Provider。只依赖于仓储接⼝,并且仓储接⼝ 不适合使⽤⽤任何 ORM 特殊对象 - -这⼀原则的主要原因是: - -1. 使领域层和应⽤层与基础层独⽴,因为基础层将来可能更改,或者你可能需要⽀持其他类型数据库。 - -2. 使领域和应⽤聚焦在业务代码上,通过将基础设施实现细节隐藏于仓储之后,使您的领域和应⽤服 务专注于业务代码。 - -3. 易于⾃动化测试,因为可以通过仓储接⼝模拟仓储数据。 - - -#### 关于数据库独⽴性原则的讨论 - -假设你当前使⽤ Entity Framework Core 操作关系型数据库,后期希望切换为 MongoDB,这就决定你不能使⽤ EF Core 中独 有功能,因为在MongoDB中不被⽀持. - -``` -举个例⼦: - - 不能使⽤更改跟踪(Change Tacking),因为 MongoDB 不⽀持。所以,需要显式更改实体。 - - 不能在实体中使⽤导航属性(Navigation Properties) 或集合关联其他聚合,因为可能在⽂档数 据库中不⽀持。 -``` - - -那么如何解决实体关联的问题?记住规则:`仅通过Id引⽤其他聚合`。 - -如果你认为这些功能对你很重要,⽽且你永远不会弃⽤ EF Core,我们认为这个原则是可以有弹性的, 但是我们仍然建议使⽤仓储模式来隐藏基础设施的实现细节 - -``` -ABP Framework 为仓储接⼝ IRepository 提供获取 IQueryable 对象的扩展⽅法 GetQueryableAsync() ,使我们在使⽤仓储时可以直接使⽤标准LINQ扩展⽅法。 -``` - - -#### 展示技术⽆关性原则 - -展示层技术(UI框架)是应⽤程序中变化最多的部分,将领域层和应⽤层设计成完全不知道展示层技术的框架⾮常重要的。 - -这⼀原则相对容易实现,⽽ABP的启动模板使其更加容易实现,选择不同UI框架⾃动⽣成对应的启动模板项⽬。 - -在某些场景下,你可能需要在应⽤层和展示层使⽤相同的逻辑。举例,你可能需要在两个层中进⾏验证和授权。在UI层检测是为了提⾼⽤户体验,在应⽤层和领域层是出安全和数据有效性考虑。这是⾮常正常和必要的。 - - -#### 聚焦状态变化,⽽不是性能优化 - -DDD聚焦领域对象如何变化和如何交互;如何创建实体和改变属性,并且保持数据的完整性、有效性; 如何创建⽅法,实现业务规则。 - -DDD没有考虑报表和⼤规模查询等需要⾼性能的业务场景,如果你的应⽤程序中没有花哨的仪表盘或报表功能,谁会去考虑呢?意思是我们需要⾃⼰考虑性能问题。 - -性能优化或技术选型,只要不影响到业务逻辑,可以⾃由使⽤ SQL Server 全部功能。 - - - - - - -## 结语 -本节知识点: -- 1.讲解Abp和DDD的分层架构介绍 -- 2.很重要的知识点DDD聚焦状态变化而非性能优化 - -联系作者:加群:867095512 @MrChuJiu - -![公众号](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/wxgzh.png) - diff --git "a/dosc/4.Abp vNext \345\237\272\347\241\200\347\257\207\344\270\250\351\242\206\345\237\237\346\236\204\345\273\272.md" "b/dosc/4.Abp vNext \345\237\272\347\241\200\347\257\207\344\270\250\351\242\206\345\237\237\346\236\204\345\273\272.md" deleted file mode 100644 index f3a80b0..0000000 --- "a/dosc/4.Abp vNext \345\237\272\347\241\200\347\257\207\344\270\250\351\242\206\345\237\237\346\236\204\345\273\272.md" +++ /dev/null @@ -1,425 +0,0 @@ -## 介绍 - -我们将通过例⼦介绍和解释⼀些显式规则。在实现领域驱动设计时,应该遵循这些规则并将其应⽤到解决⽅案中。 - -## 领域划分 - -首先我们先对比下Blog.Core和本次重构设计上的偏差,可以看到多了一个博客管理和类别管理。 - -![业务脑图](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/3.png) - -根据上面得到的业务脑图我们可以看到包含Blog(博客),Post(文章),Comment(评论),Tag(标签),User(用户),根据脑图画出领域图来指明关系。 - -![领域图](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/9.png) - -领域图连接地址:https://www.processon.com/view/link/611365c00e3e7407d39727ee - -## 聚合根最佳实践 - -#### 只通过ID引⽤其他聚合 - -⼀个聚合应该只通过其他聚合的ID引⽤聚合,这意味着你不能添加导航属性到其他聚合。 - -- 这条规则使得实现可序列化原则得以实现。 - -- 可以防⽌不同聚合相互操作,以及将聚合的业务逻辑泄露给另⼀个聚合。 - -来看下面的2个聚合根 Blog 和 Post. - -- Blog 没有包含 Post集合,因为他们是不同聚合 -- Post 使用 BlogId 关联 Blog - -当你有一个 Post 需要关联 Blog的时候 你可以从数据库通过 BlogId 进行获取 - -```cs - public class Blog:FullAuditedAggregateRoot - { - [NotNull] - public virtual string Name { get; set; } - - [NotNull] - public virtual string ShortName { get; set; } - - [CanBeNull] - public virtual string Description { get; set; } - } - - public class Post : FullAuditedAggregateRoot - { - public virtual Guid BlogId { get; protected set; } - - [NotNull] - public virtual string Url { get; protected set; } - - [NotNull] - public virtual string CoverImage { get; set; } - - [NotNull] - public virtual string Title { get; protected set; } - - [CanBeNull] - public virtual string Content { get; set; } - - [CanBeNull] - public virtual string Description { get; set; } - - public virtual int ReadCount { get; protected set; } - - public virtual Collection Tags { get; protected set; } - } -``` - -#### 聚合根/实体中的主键 - -⼀个聚合根通常有⼀个ID属性作为其标识符(主键,Primark Key: PK)。推荐使⽤ Guid 作为聚合,聚合中的实体(不是聚合根)可以使⽤复合主键(后面讲),主键ABP已经帮我们做好了参阅文档:https://docs.abp.io/en/abp/latest/Entities。 - -```cs - public class Blog:FullAuditedAggregateRoot - { - [NotNull] - public virtual string Name { get; set; } - - [NotNull] - public virtual string ShortName { get; set; } - - [CanBeNull] - public virtual string Description { get; set; } - } -``` - - -#### 聚合根/实体构造函数 - -构造函数是实体的⽣命周期开始的地⽅。⼀个设计良好的构造函数,担负以下职责: - -- 获取所需的实体属性参数,来创建⼀个有效的实体。应该强制只传递必要的参数,并可以将⾮必要 的属性作为可选参数。 -- 检查参数的有效性。 -- 初始化⼦集合。 - -```cs - - public Blog(Guid id, [NotNull] string name, [NotNull] string shortName) - { - //属性赋值 - Id = id; - //有效性检测 - Name = Check.NotNullOrWhiteSpace(name, nameof(name)); - //有效性检测 - ShortName = Check.NotNullOrWhiteSpace(shortName, nameof(shortName)); - } - -``` - -- Blog 类通过构造函数参数、获得属性所需的值,以此创建一个正确有效的实体 -- 在构造函数中验证输⼊参数的有效性,⽐如: Check.NotNullOrWhiteSpace(...) 当传递的值为空 时,抛出异常 ArgumentException -- 构造函数将参数 id 传递给 base 类,不在构造函数中⽣成 Guid,可以将其委托给另⼀个 Guid⽣成 服务,作为参数传递进来 -- ⽆参构造函数对于ORM是必要的。我们将其设置为私有,以防⽌在代码中意外地使⽤它 - -#### 实体属性访问器和⽅法 - -上⾯的示例代码,看起来可能很奇怪。⽐如:在构造函数中,我们强制传递⼀个不为 null 的 Name 。 但是,我们可以将 Name 属性设置为 null ,⽽对其没有进⾏任何有效性控制。 - -如果我们⽤ public 设置器声明所有的属性,就像上⾯的 Blog 类中的属性例⼦,我们就不能在实体的⽣命周期中强制保持其有效性和完整性。所以: - -- 当需要在设置属性时,执⾏任何逻辑,请将属性设置为私有 private 。 -- 定义公共⽅法来操作这些属性。 - -```cs - public class Blog:FullAuditedAggregateRoot - { - [NotNull] - public virtual string Name { get; protected set; } - - [NotNull] - public virtual string ShortName { get; protected set; } - - [CanBeNull] - public virtual string Description { get; set; } - - protected Blog() - { - /*反序列化或ORM 需要*/ - } - - public Blog(Guid id, [NotNull] string name, [NotNull] string shortName) - { - //属性赋值 - Id = id; - //有效性检测 - Name = Check.NotNullOrWhiteSpace(name, nameof(name)); - //有效性检测 - ShortName = Check.NotNullOrWhiteSpace(shortName, nameof(shortName)); - } - - public virtual Blog SetName([NotNull] string name) - { - Name = Check.NotNullOrWhiteSpace(name, nameof(name)); - return this; - } - - public virtual Blog SetShortName(string shortName) - { - ShortName = Check.NotNullOrWhiteSpace(shortName, nameof(shortName)); - return this; - } - - } -``` - - - -#### 业务逻辑和实体中的异常处理 - -当你在实体中进⾏验证和实现业务逻辑,经常需要管理异常: - -- 创建特定领域异常。 -- 必要时在实体⽅法中抛出这些异常 - -ABP框架 Exception Handing 系统处理了这些问题。 - - - -## 完成聚合的实体创建 - -根据 最佳实践的讲解完成,把其他实体创建出来,代码粘在这里了。 - -![实体](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/11.png) - -```cs -public class Blog:FullAuditedAggregateRoot - { - [NotNull] - public virtual string Name { get; protected set; } - - [NotNull] - public virtual string ShortName { get; protected set; } - - [CanBeNull] - public virtual string Description { get; set; } - - protected Blog() - { - - } - - public Blog(Guid id, [NotNull] string name, [NotNull] string shortName) - { - //属性赋值 - Id = id; - //有效性检测 - Name = Check.NotNullOrWhiteSpace(name, nameof(name)); - //有效性检测 - ShortName = Check.NotNullOrWhiteSpace(shortName, nameof(shortName)); - } - - public virtual Blog SetName([NotNull] string name) - { - Name = Check.NotNullOrWhiteSpace(name, nameof(name)); - return this; - } - - public virtual Blog SetShortName(string shortName) - { - ShortName = Check.NotNullOrWhiteSpace(shortName, nameof(shortName)); - return this; - } - - } - - - - - - public class Comment : FullAuditedAggregateRoot - { - public virtual Guid PostId { get; protected set; } - - public virtual Guid? RepliedCommentId { get; protected set; } - - public virtual string Text { get; protected set; } - - protected Comment() - { - - } - - public Comment(Guid id, Guid postId, Guid? repliedCommentId, [NotNull] string text) - { - Id = id; - PostId = postId; - RepliedCommentId = repliedCommentId; - Text = Check.NotNullOrWhiteSpace(text, nameof(text)); - } - - public void SetText(string text) - { - Text = Check.NotNullOrWhiteSpace(text, nameof(text)); - } - } - - - - - - - public class Post : FullAuditedAggregateRoot - { - public virtual Guid BlogId { get; protected set; } - - [NotNull] - public virtual string Url { get; protected set; } - - [NotNull] - public virtual string CoverImage { get; set; } - - [NotNull] - public virtual string Title { get; protected set; } - - [CanBeNull] - public virtual string Content { get; set; } - - [CanBeNull] - public virtual string Description { get; set; } - - public virtual int ReadCount { get; protected set; } - - public virtual Collection Tags { get; protected set; } - - - protected Post() - { - - } - - public Post(Guid id, Guid blogId, [NotNull] string title, [NotNull] string coverImage, [NotNull] string url) - { - Id = id; - BlogId = blogId; - Title = Check.NotNullOrWhiteSpace(title, nameof(title)); - Url = Check.NotNullOrWhiteSpace(url, nameof(url)); - CoverImage = Check.NotNullOrWhiteSpace(coverImage, nameof(coverImage)); - - Tags = new Collection(); - Comments = new Collection(); - } - - public virtual Post IncreaseReadCount() - { - ReadCount++; - return this; - } - - public virtual Post SetTitle([NotNull] string title) - { - Title = Check.NotNullOrWhiteSpace(title, nameof(title)); - return this; - } - - public virtual Post SetUrl([NotNull] string url) - { - Url = Check.NotNullOrWhiteSpace(url, nameof(url)); - return this; - } - - public virtual void AddTag(Guid tagId) - { - Tags.Add(new PostTag(Id, tagId)); - } - - public virtual void RemoveTag(Guid tagId) - { - Tags.RemoveAll(t => t.TagId == tagId); - } - - } - - - - - - - public record PostTag - { - public virtual Guid TagId { get; init; } //主键 - - protected PostTag() - { - - } - - public PostTag( Guid tagId) - { - TagId = tagId; - } - } - - - - public class Tag : FullAuditedAggregateRoot - { - public virtual Guid BlogId { get; protected set; } - - public virtual string Name { get; protected set; } - - public virtual string Description { get; protected set; } - - public virtual int UsageCount { get; protected internal set; } - - - protected Tag() - { - - } - - public Tag(Guid id, Guid blogId, [NotNull] string name, int usageCount = 0, string description = null) - { - Id = id; - Name = Check.NotNullOrWhiteSpace(name, nameof(name)); - BlogId = blogId; - Description = description; - UsageCount = usageCount; - } - - public virtual void SetName(string name) - { - Name = Check.NotNullOrWhiteSpace(name, nameof(name)); - } - - public virtual void IncreaseUsageCount(int number = 1) - { - UsageCount += number; - } - - public virtual void DecreaseUsageCount(int number = 1) - { - if (UsageCount <= 0) - { - return; - } - - if (UsageCount - number <= 0) - { - UsageCount = 0; - return; - } - - UsageCount -= number; - } - - public virtual void SetDescription(string description) - { - Description = description; - } - - } - -``` - -## 结语 -本节知识点: -- 1.根据脑图划分聚合 -- 2.根据领域图在遵守DDD的聚合根规范的情况下创建聚合 - -联系作者:加群:867095512 @MrChuJiu - -![公众号](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/wxgzh.png) diff --git "a/dosc/5.Abp vNext \345\237\272\347\241\200\347\257\207\344\270\250\345\215\232\345\256\242\350\201\232\345\220\210\345\212\237\350\203\275.md" "b/dosc/5.Abp vNext \345\237\272\347\241\200\347\257\207\344\270\250\345\215\232\345\256\242\350\201\232\345\220\210\345\212\237\350\203\275.md" deleted file mode 100644 index 7ddb7d4..0000000 --- "a/dosc/5.Abp vNext \345\237\272\347\241\200\347\257\207\344\270\250\345\215\232\345\256\242\350\201\232\345\220\210\345\212\237\350\203\275.md" +++ /dev/null @@ -1,301 +0,0 @@ -## 介绍 - -业务篇章先从客户端开始写,另外补充一下我给项目起名的时候没多想起的太随意了,结果后面有些地方命名冲突了需要通过手动using不过问题不大。 - -## 开工 - -#### 应用层 - -根据第三章分层架构里面讲到的现在我们模型已经创建好了,下一步应该是去`Application.Contracts`层创建我们的业务接口和Dto. - -![Blog业务接口](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/12.png) - -```cs - - public interface IBlogAppService : IApplicationService - { - Task> GetListAsync(); - - Task GetByShortNameAsync(string shortName); - - Task GetAsync(Guid id); - } - - - public class BlogDto : FullAuditedEntityDto - { - public string Name { get; set; } - - public string ShortName { get; set; } - - public string Description { get; set; } - } -``` - -接口写完之后,我们去`Application`层实现 Application.Contracts 中定义的服务接⼝,应⽤服务是⽆状态服务,实现应⽤程序⽤例。⼀个应⽤服务通常使⽤领域对象实现⽤例,获取或返回数 据传输对象DTOs,被展示层调⽤。 - -应⽤服务通⽤原则: -- 实现特定⽤例的应⽤逻辑,不能在应⽤服务中实现领域逻辑(需要理清应⽤逻辑和领域逻辑⼆者的 区别)。 -- 应⽤服务⽅法不能返回实体,因为这样会打破领域层的封装性,始终只返回DTO。 - -大家先看下面的代码有什么`问题`。 -```cs -public class BlogAppService : CoreAppService, IBlogAppService - { - private readonly IRepository _blogRepository; - - public BlogAppService(IRepository blogRepository) - { - _blogRepository = blogRepository; - } - public async Task> GetListAsync() - { - var blogs = await _blogRepository.GetListAsync(); - - return new ListResultDto( - ObjectMapper.Map, List>(blogs) - ); - } - - public async Task GetByShortNameAsync(string shortName) - { - Check.NotNullOrWhiteSpace(shortName, nameof(shortName)); - - var blog = await _blogRepository.GetAsync(x=>x.ShortName == shortName); - - if (blog == null) - { - throw new EntityNotFoundException(typeof(Blog), shortName); - } - - return ObjectMapper.Map(blog); - } - - public async Task GetAsync(Guid id) - { - var blog = await _blogRepository.GetAsync(x=>x.Id == id); - - return ObjectMapper.Map(blog); - } - } -``` -错误:上面代码违反了应用层原则将特定⽤例的应⽤逻辑写在了应⽤服务层。 - -#### 仓储 - -解决上面的问题就要用到`仓储`,ABP默认提供的泛型仓储无法满足业务需要的时候就需要我们自定义仓储,仓储应该只针对聚合根,⽽不是所有实体。因为⼦集合实体(聚合)应该通过聚合根访问。 - -仓储定义写在领域层,仓储实现写在基础层,参照第三章:`ABP项目分层解析`和`关于数据库独⽴性原则的讨论`。 - -仓储的通⽤原则 -- 在领域层中定义仓储接⼝,在基础层中实现仓储接⼝(⽐如: EntityFrameworkCore 项⽬ 或 MongoDB 项⽬) -- 仓储不包含业务逻辑,专注数据处理。 -- 仓储接⼝应该保持 数据提供程序/ORM 独⽴性。举个例⼦,仓储接⼝定义的⽅法不能返回 DbSet 对象,因为该对象由 EF Core 提供,如果使⽤ MongoDB 数据库则⽆法实现该接⼝。 -- 为聚合根创建对应仓储,⽽不是所有实体。因为⼦集合实体(聚合)应该通过聚合根访问。 - - -![项目结构](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/13.png) - -```cs - public interface IBlogRepository : IBasicRepository - { - Task FindByShortNameAsync(string shortName, CancellationToken cancellationToken = default); - } - - - - public class EfCoreBlogRepository : EfCoreRepository, IBlogRepository - { - public EfCoreBlogRepository(IDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - - } - - public async Task FindByShortNameAsync(string shortName, CancellationToken cancellationToken = default) - { - return await (await GetDbSetAsync()).FirstOrDefaultAsync(p => p.ShortName == shortName, GetCancellationToken(cancellationToken)); - } - } - - - - public class BlogAppService : CoreAppService, IBlogAppService - { - private readonly IBlogRepository _blogRepository; - - public BlogAppService(IBlogRepository blogRepository) - { - _blogRepository = blogRepository; - } - public async Task> GetListAsync() - { - var blogs = await _blogRepository.GetListAsync(); - - return new ListResultDto( - ObjectMapper.Map, List>(blogs) - ); - } - - public async Task GetByShortNameAsync(string shortName) - { - Check.NotNullOrWhiteSpace(shortName, nameof(shortName)); - - var blog = await _blogRepository.FindByShortNameAsync(shortName); - - if (blog == null) - { - throw new EntityNotFoundException(typeof(Blog), shortName); - } - - return ObjectMapper.Map(blog); - } - - public async Task GetAsync(Guid id) - { - var blog = await _blogRepository.GetAsync(id); - - return ObjectMapper.Map(blog); - } - } - -``` - - -## 映射Domain对象 - -上面完成后我们就可以启动系统看到我们定义的接口了,但是我们还少了一步那就是映射 Domain 对象(实体和值类型)到数据库表。 - -![演示](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/14.png) - - -在`CoreDbContext`上下文中加入我们的实体,然后在 CoreEfCoreEntityExtensionMappings 中新建一个静态`ConfigureBcvpBlogCore`方法写`FluentApi`,这里有几个疑惑我说下,因为我目前使用的版本是4.4也就是ABP刚发布的新版本,这个版本中它移除了一些类比如`ModelBuilderConfigurationOptions`和`DbContextModelBuilderExtensions`,我就直接把ConfigureBcvpBlogCore写在`CoreEfCoreEntityExtensionMappings`里面了,可能后面我会在找合理的地方去单独放,另外可以看到PostTag没有出现在这里,这是因为PostTag是一个值对象作为实体的私有类型处理了,这里就能充分感受到模型建立与数据库映射抽离。 - -```cs - ------------------------------ CoreDbContext.cs - - public DbSet Blogs { get; set; } - - public DbSet Posts { get; set; } - - public DbSet Tags { get; set; } - - public DbSet Comments { get; set; } - - - protected override void OnModelCreating(ModelBuilder builder) - { - - // 这里是追加不是删掉原来的 - builder.ConfigureBcvpBlogCore(); - - } - - - ------------------------------ CoreEfCoreEntityExtensionMappings.cs - - public static void ConfigureBcvpBlogCore([NotNull] this ModelBuilder builder) - { - Check.NotNull(builder, nameof(builder)); - - if (builder.IsTenantOnlyDatabase()) - { - return; - } - - - builder.Entity(b => - { - b.ToTable(CoreConsts.DbTablePrefix + "Blogs", CoreConsts.DbSchema); - - b.ConfigureByConvention(); - - b.Property(x => x.Name).IsRequired().HasMaxLength(BlogConsts.MaxNameLength).HasColumnName(nameof(BlogCore.Blogs.Blog.Name)); - b.Property(x => x.ShortName).IsRequired().HasMaxLength(BlogConsts.MaxShortNameLength).HasColumnName(nameof(BlogCore.Blogs.Blog.ShortName)); - b.Property(x => x.Description).IsRequired(false).HasMaxLength(BlogConsts.MaxDescriptionLength).HasColumnName(nameof(BlogCore.Blogs.Blog.Description)); - - b.ApplyObjectExtensionMappings(); - }); - - builder.Entity(b => - { - b.ToTable(CoreConsts.DbTablePrefix + "Posts", CoreConsts.DbSchema); - - b.ConfigureByConvention(); - - b.Property(x => x.BlogId).HasColumnName(nameof(Post.BlogId)); - b.Property(x => x.Title).IsRequired().HasMaxLength(PostConsts.MaxTitleLength).HasColumnName(nameof(Post.Title)); - b.Property(x => x.CoverImage).IsRequired().HasColumnName(nameof(Post.CoverImage)); - b.Property(x => x.Url).IsRequired().HasMaxLength(PostConsts.MaxUrlLength).HasColumnName(nameof(Post.Url)); - b.Property(x => x.Content).IsRequired(false).HasMaxLength(PostConsts.MaxContentLength).HasColumnName(nameof(Post.Content)); - b.Property(x => x.Description).IsRequired(false).HasMaxLength(PostConsts.MaxDescriptionLength).HasColumnName(nameof(Post.Description)); - - b.OwnsMany(p => p.Tags, pd => - { - pd.ToTable(CoreConsts.DbTablePrefix + "PostTags", CoreConsts.DbSchema); - - pd.Property(x => x.TagId).HasColumnName(nameof(PostTag.TagId)); - - }); - - b.HasOne().WithMany().IsRequired().HasForeignKey(p => p.BlogId); - - b.ApplyObjectExtensionMappings(); - }); - - builder.Entity(b => - { - b.ToTable(CoreConsts.DbTablePrefix + "Tags", CoreConsts.DbSchema); - - b.ConfigureByConvention(); - - b.Property(x => x.Name).IsRequired().HasMaxLength(TagConsts.MaxNameLength).HasColumnName(nameof(Tag.Name)); - b.Property(x => x.Description).HasMaxLength(TagConsts.MaxDescriptionLength).HasColumnName(nameof(Tag.Description)); - b.Property(x => x.UsageCount).HasColumnName(nameof(Tag.UsageCount)); - - b.ApplyObjectExtensionMappings(); - }); - - - builder.Entity(b => - { - b.ToTable(CoreConsts.DbTablePrefix + "Comments", CoreConsts.DbSchema); - - b.ConfigureByConvention(); - - b.Property(x => x.Text).IsRequired().HasMaxLength(CommentConsts.MaxTextLength).HasColumnName(nameof(Comment.Text)); - b.Property(x => x.RepliedCommentId).HasColumnName(nameof(Comment.RepliedCommentId)); - b.Property(x => x.PostId).IsRequired().HasColumnName(nameof(Comment.PostId)); - - b.HasOne().WithMany().HasForeignKey(p => p.RepliedCommentId); - b.HasOne().WithMany().IsRequired().HasForeignKey(p => p.PostId); - - b.ApplyObjectExtensionMappings(); - }); - - - - - builder.TryConfigureObjectExtensions(); - - } -``` - - -接下来就是生成迁移和执行迁移了 - - -![创建项目](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/15.png) - - -## 结语 -本节知识点: -- 1.根据前面4章讲的知识完成博客建模 -- 2.完成业务博客业务代码 -- 3.自定义仓储 - -联系作者:加群:867095512 @MrChuJiu - -![公众号](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/wxgzh.png) \ No newline at end of file diff --git "a/dosc/6.Abp vNext \345\237\272\347\241\200\347\257\207\344\270\250\346\226\207\347\253\240\350\201\232\345\220\210\345\212\237\350\203\275\344\270\212.md" "b/dosc/6.Abp vNext \345\237\272\347\241\200\347\257\207\344\270\250\346\226\207\347\253\240\350\201\232\345\220\210\345\212\237\350\203\275\344\270\212.md" deleted file mode 100644 index 8803204..0000000 --- "a/dosc/6.Abp vNext \345\237\272\347\241\200\347\257\207\344\270\250\346\226\207\347\253\240\350\201\232\345\220\210\345\212\237\350\203\275\344\270\212.md" +++ /dev/null @@ -1,582 +0,0 @@ -## 介绍 - -9月开篇讲,前面几章群里已经有几个小伙伴跟着做了一遍了,遇到的问题和疑惑也都在群里反馈和解决好了,9月咱们保持保持更新。争取10月份更新完基础篇。 - -另外番外篇属于 我在abp群里和日常开发的问题记录,如果各位在使用abp的过程中发现什么问题也可以及时反馈给我。 - -上一章已经把所有实体的迁移都做好了,这一章我们进入到文章聚合,文章聚合涉及接口比较多。 - -## 开工 - -先来看下需要定义那些应用层接口,Dto我也在下面定义好了,关于里面的`BlogUserDto`这个是作者目前打算采用ABP Identtiy中的User来做到时候通过权限控制,另外就是`TagDto`属于Posts领域的Dto. - -![项目结构](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/16.png) - -```cs - public interface IPostAppService : IApplicationService - { - Task> GetListByBlogIdAndTagName(Guid blogId, string tagName); - - Task> GetTimeOrderedListAsync(Guid blogId); - - Task GetForReadingAsync(GetPostInput input); - - Task GetAsync(Guid id); - - Task DeleteAsync(Guid id); - - Task CreateAsync(CreatePostDto input); - - Task UpdateAsync(Guid id, UpdatePostDto input); - } - - - - public class BlogUserDto : EntityDto - { - public Guid? TenantId { get; set; } - - public string UserName { get; set; } - - public string Email { get; set; } - - public bool EmailConfirmed { get; set; } - - public string PhoneNumber { get; set; } - - public bool PhoneNumberConfirmed { get; set; } - - public Dictionary ExtraProperties { get; set; } - } - - - - - public class CreatePostDto - { - public Guid BlogId { get; set; } - - [Required] - [DynamicStringLength(typeof(PostConsts), nameof(PostConsts.MaxTitleLength))] - public string Title { get; set; } - - [Required] - public string CoverImage { get; set; } - - [Required] - [DynamicStringLength(typeof(PostConsts), nameof(PostConsts.MaxUrlLength))] - public string Url { get; set; } - - [Required] - [DynamicStringLength(typeof(PostConsts), nameof(PostConsts.MaxContentLength))] - public string Content { get; set; } - - public string Tags { get; set; } - - [DynamicStringLength(typeof(PostConsts), nameof(PostConsts.MaxDescriptionLength))] - public string Description { get; set; } - - } - - - - public class GetPostInput - { - [Required] - public string Url { get; set; } - - public Guid BlogId { get; set; } - } - - - - public class UpdatePostDto - { - public Guid BlogId { get; set; } - - [Required] - public string Title { get; set; } - - [Required] - public string CoverImage { get; set; } - - [Required] - public string Url { get; set; } - - [Required] - public string Content { get; set; } - - public string Description { get; set; } - - public string Tags { get; set; } - } - - - - public class PostWithDetailsDto : FullAuditedEntityDto - { - public Guid BlogId { get; set; } - - public string Title { get; set; } - - public string CoverImage { get; set; } - - public string Url { get; set; } - - public string Content { get; set; } - - public string Description { get; set; } - - public int ReadCount { get; set; } - - public int CommentCount { get; set; } - - [CanBeNull] - public BlogUserDto Writer { get; set; } - - public List Tags { get; set; } - } - - - - - public class TagDto : FullAuditedEntityDto - { - public string Name { get; set; } - - public string Description { get; set; } - - public int UsageCount { get; set; } - } - - -``` - -根据上上面的接口我想,就应该明白ABP自带的仓储无法满足我们业务需求,我们需要自定义仓储,在大多数场景下我们不会采用ABP提供的泛型仓储,除非业务足够简单泛型仓储完全满足(个人意见)。 - -另外我们重写了`WithDetailsAsync`通过扩展`IncludeDetails`方法实现`Include`包含⼦集合对象,其实这个也可以作为可选参数我们可以在使用ABP提供的泛型仓储`GetAsync`方法中看到他有一个可选参数`includeDetails`,来指明查询是否包含⼦集合对象。 - -```cs - public interface IPostRepository : IBasicRepository - { - Task> GetPostsByBlogId(Guid id, CancellationToken cancellationToken = default); - - Task IsPostUrlInUseAsync(Guid blogId, string url, Guid? excludingPostId = null, CancellationToken cancellationToken = default); - - Task GetPostByUrl(Guid blogId, string url, CancellationToken cancellationToken = default); - - Task> GetOrderedList(Guid blogId, bool descending = false, CancellationToken cancellationToken = default); - } - - - - - public class EfCorePostRepository : EfCoreRepository, IPostRepository - { - public EfCorePostRepository(IDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - - } - - public async Task> GetPostsByBlogId(Guid id, CancellationToken cancellationToken = default) - { - return await (await GetDbSetAsync()).Where(p => p.BlogId == id).OrderByDescending(p => p.CreationTime).ToListAsync(GetCancellationToken(cancellationToken)); - } - - public async Task IsPostUrlInUseAsync(Guid blogId, string url, Guid? excludingPostId = null, CancellationToken cancellationToken = default) - { - var query = (await GetDbSetAsync()).Where(p => blogId == p.BlogId && p.Url == url); - - if (excludingPostId != null) - { - query = query.Where(p => excludingPostId != p.Id); - } - - return await query.AnyAsync(GetCancellationToken(cancellationToken)); - } - - public async Task GetPostByUrl(Guid blogId, string url, CancellationToken cancellationToken = default) - { - var post = await (await GetDbSetAsync()).FirstOrDefaultAsync(p => p.BlogId == blogId && p.Url == url, GetCancellationToken(cancellationToken)); - - if (post == null) - { - throw new EntityNotFoundException(typeof(Post), nameof(post)); - } - - return post; - } - - public async Task> GetOrderedList(Guid blogId, bool descending = false, CancellationToken cancellationToken = default) - { - if (!descending) - { - return await (await GetDbSetAsync()).Where(x => x.BlogId == blogId).OrderByDescending(x => x.CreationTime).ToListAsync(GetCancellationToken(cancellationToken)); - } - else - { - return await (await GetDbSetAsync()).Where(x => x.BlogId == blogId).OrderBy(x => x.CreationTime).ToListAsync(GetCancellationToken(cancellationToken)); - } - - } - - public override async Task> WithDetailsAsync() - { - return (await GetQueryableAsync()).IncludeDetails(); - } - } - - - - - public static class CoreEntityFrameworkCoreQueryableExtensions - { - public static IQueryable IncludeDetails(this IQueryable queryable, bool include = true) - { - if (!include) - { - return queryable; - } - - return queryable - .Include(x => x.Tags); - } - } - - -``` - - -#### 应用层 - -新建`PostAppService`继承`IPostAppService`然后开始第一个方法`GetListByBlogIdAndTagName`该方法根据blogId 和 tagName 查询相关的文章数据。我们有`IPostRepository`的`GetPostsByBlogId`方法可以根据blogId获取文章,那么如何在根据tagName筛选呢,这里就需要我们新增一个`ITagRepository`,先不着急先实现先把业务逻辑跑通。 -```cs - public interface ITagRepository : IBasicRepository - { - - Task> GetListAsync(Guid blogId, CancellationToken cancellationToken = default); - - Task FindByNameAsync(Guid blogId, string name, CancellationToken cancellationToken = default); - - Task> GetListAsync(IEnumerable ids, CancellationToken cancellationToken = default); - - } -``` - -现在进行下一步,文章已经查询出来了,文章上的作者和Tag还没处理,下面代码我写了注释代码意思应该都能看明白,这里可能会比较疑问的事这样写代码for循环去跑数据库是不是不太合理,因为Tags这个本身就不会存在很多数据,这块如果要调整其实完全可以讲TagName存在Tasg值对象中。 - -```cs - public async Task> GetListByBlogIdAndTagName(Guid id, string tagName) - { - // 根据blogId查询文章数据 - var posts = await _postRepository.GetPostsByBlogId(id); - var postDtos = new List(ObjectMapper.Map, List>(posts)); - - // 根据tagName筛选tag - var tag = tagName.IsNullOrWhiteSpace() ? null : await _tagRepository.FindByNameAsync(id, tagName); - - // 给文章Tags赋值 - foreach (var postDto in postDtos) - { - postDto.Tags = await GetTagsOfPost(postDto.Id); - } - - // 筛选掉不符合要求的文章 - if (tag != null) - { - postDtos = await FilterPostsByTag(postDtos, tag); - } - - } - - private async Task> GetTagsOfPost(Guid id) - { - var tagIds = (await _postRepository.GetAsync(id)).Tags; - - var tags = await _tagRepository.GetListAsync(tagIds.Select(t => t.TagId)); - - return ObjectMapper.Map, List>(tags); - } - - private Task> FilterPostsByTag(IEnumerable allPostDtos, Tag tag) - { - var filteredPostDtos = allPostDtos.Where(p => p.Tags?.Any(t => t.Id == tag.Id) ?? false).ToList(); - - return Task.FromResult(filteredPostDtos); - } - - -``` - -继续向下就是赋值作者信息,对应上面Tasg最多十几个,但是系统有多少用户就不好说了所以这里使用`userDictionary`就是省掉重复查询数据。 - -```cs - public async Task> GetListByBlogIdAndTagName(Guid id, string tagName) - { - - // 前面的代码就不重复粘贴了 - - var userDictionary = new Dictionary(); - // 赋值作者信息 - foreach (var postDto in postDtos) - { - if (postDto.CreatorId.HasValue) - { - if (!userDictionary.ContainsKey(postDto.CreatorId.Value)) - { - var creatorUser = await UserLookupService.FindByIdAsync(postDto.CreatorId.Value); - if (creatorUser != null) - { - userDictionary[creatorUser.Id] = ObjectMapper.Map(creatorUser); - } - } - - if (userDictionary.ContainsKey(postDto.CreatorId.Value)) - { - postDto.Writer = userDictionary[(Guid)postDto.CreatorId]; - } - } - } - - return new ListResultDto(postDtos); - - } - -``` - -目前删除和修改接口做不了因为这里牵扯评论的部分操作,除去这两个,其他的接口直接看代码应该都没有什么问题,这一章的东西已经很多了剩下的我们下集。 - -```cs - public async Task> GetListByBlogIdAndTagName(Guid id, string tagName) - { - // 根据blogId查询文章数据 - var posts = await _postRepository.GetPostsByBlogId(id); - // 根据tagName筛选tag - var tag = tagName.IsNullOrWhiteSpace() ? null : await _tagRepository.FindByNameAsync(id, tagName); - var userDictionary = new Dictionary(); - var postDtos = new List(ObjectMapper.Map, List>(posts)); - - // 给文章Tags赋值 - foreach (var postDto in postDtos) - { - postDto.Tags = await GetTagsOfPost(postDto.Id); - } - // 筛选掉不符合要求的文章 - if (tag != null) - { - postDtos = await FilterPostsByTag(postDtos, tag); - } - - // 赋值作者信息 - foreach (var postDto in postDtos) - { - if (postDto.CreatorId.HasValue) - { - if (!userDictionary.ContainsKey(postDto.CreatorId.Value)) - { - var creatorUser = await UserLookupService.FindByIdAsync(postDto.CreatorId.Value); - if (creatorUser != null) - { - userDictionary[creatorUser.Id] = ObjectMapper.Map(creatorUser); - } - } - - if (userDictionary.ContainsKey(postDto.CreatorId.Value)) - { - postDto.Writer = userDictionary[(Guid)postDto.CreatorId]; - } - } - } - - return new ListResultDto(postDtos); - - } - - public async Task> GetTimeOrderedListAsync(Guid blogId) - { - var posts = await _postRepository.GetOrderedList(blogId); - - var postsWithDetails = ObjectMapper.Map, List>(posts); - - foreach (var post in postsWithDetails) - { - if (post.CreatorId.HasValue) - { - var creatorUser = await UserLookupService.FindByIdAsync(post.CreatorId.Value); - if (creatorUser != null) - { - post.Writer = ObjectMapper.Map(creatorUser); - } - } - } - - return new ListResultDto(postsWithDetails); - - } - - public async Task GetForReadingAsync(GetPostInput input) - { - var post = await _postRepository.GetPostByUrl(input.BlogId, input.Url); - - post.IncreaseReadCount(); - - var postDto = ObjectMapper.Map(post); - - postDto.Tags = await GetTagsOfPost(postDto.Id); - - if (postDto.CreatorId.HasValue) - { - var creatorUser = await UserLookupService.FindByIdAsync(postDto.CreatorId.Value); - - postDto.Writer = ObjectMapper.Map(creatorUser); - } - - return postDto; - } - - public async Task GetAsync(Guid id) - { - var post = await _postRepository.GetAsync(id); - - var postDto = ObjectMapper.Map(post); - - postDto.Tags = await GetTagsOfPost(postDto.Id); - - if (postDto.CreatorId.HasValue) - { - var creatorUser = await UserLookupService.FindByIdAsync(postDto.CreatorId.Value); - - postDto.Writer = ObjectMapper.Map(creatorUser); - } - - return postDto; - } - - - public async Task CreateAsync(CreatePostDto input) - { - input.Url = await RenameUrlIfItAlreadyExistAsync(input.BlogId, input.Url); - - var post = new Post( - id: GuidGenerator.Create(), - blogId: input.BlogId, - title: input.Title, - coverImage: input.CoverImage, - url: input.Url - ) - { - Content = input.Content, - Description = input.Description - }; - - await _postRepository.InsertAsync(post); - - var tagList = SplitTags(input.Tags); - await SaveTags(tagList, post); - - - return ObjectMapper.Map(post); - } - - private async Task RenameUrlIfItAlreadyExistAsync(Guid blogId, string url, Post existingPost = null) - { - if (await _postRepository.IsPostUrlInUseAsync(blogId, url, existingPost?.Id)) - { - return url + "-" + Guid.NewGuid().ToString().Substring(0, 5); - } - - return url; - } - - private async Task SaveTags(ICollection newTags, Post post) - { - await RemoveOldTags(newTags, post); - - await AddNewTags(newTags, post); - } - - private async Task RemoveOldTags(ICollection newTags, Post post) - { - foreach (var oldTag in post.Tags.ToList()) - { - var tag = await _tagRepository.GetAsync(oldTag.TagId); - - var oldTagNameInNewTags = newTags.FirstOrDefault(t => t == tag.Name); - - if (oldTagNameInNewTags == null) - { - post.RemoveTag(oldTag.TagId); - - tag.DecreaseUsageCount(); - await _tagRepository.UpdateAsync(tag); - } - else - { - newTags.Remove(oldTagNameInNewTags); - } - } - } - - private async Task AddNewTags(IEnumerable newTags, Post post) - { - var tags = await _tagRepository.GetListAsync(post.BlogId); - - foreach (var newTag in newTags) - { - var tag = tags.FirstOrDefault(t => t.Name == newTag); - - if (tag == null) - { - tag = await _tagRepository.InsertAsync(new Tag(GuidGenerator.Create(), post.BlogId, newTag, 1)); - } - else - { - tag.IncreaseUsageCount(); - tag = await _tagRepository.UpdateAsync(tag); - } - - post.AddTag(tag.Id); - } - } - - private List SplitTags(string tags) - { - if (tags.IsNullOrWhiteSpace()) - { - return new List(); - } - return new List(tags.Split(",").Select(t => t.Trim())); - } - - private async Task> GetTagsOfPost(Guid id) - { - var tagIds = (await _postRepository.GetAsync(id)).Tags; - - var tags = await _tagRepository.GetListAsync(tagIds.Select(t => t.TagId)); - - return ObjectMapper.Map, List>(tags); - } - - private Task> FilterPostsByTag(IEnumerable allPostDtos, Tag tag) - { - var filteredPostDtos = allPostDtos.Where(p => p.Tags?.Any(t => t.Id == tag.Id) ?? false).ToList(); - - return Task.FromResult(filteredPostDtos); - } -``` - - - -## 结语 -本节知识点: -- 1.我们梳理了一个聚合的开发过程 - -因为该聚合东西太多了我们就拆成2章来搞一章的话太长了 - -联系作者:加群:867095512 @MrChuJiu - -![公众号](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/wxgzh.png) - - - diff --git "a/dosc/7.Abp vNext \345\237\272\347\241\200\347\257\207\344\270\250\346\226\207\347\253\240\350\201\232\345\220\210\345\212\237\350\203\275\344\270\213.md" "b/dosc/7.Abp vNext \345\237\272\347\241\200\347\257\207\344\270\250\346\226\207\347\253\240\350\201\232\345\220\210\345\212\237\350\203\275\344\270\213.md" deleted file mode 100644 index 365516c..0000000 --- "a/dosc/7.Abp vNext \345\237\272\347\241\200\347\257\207\344\270\250\346\226\207\347\253\240\350\201\232\345\220\210\345\212\237\350\203\275\344\270\213.md" +++ /dev/null @@ -1,297 +0,0 @@ -## 介绍 - -这一章来补全剩下的 2个接口和将文章聚合进行完善。 - -## 开工 - -上一章大部分业务都完成了,这一章专门讲删除和修改,首先是删除,文章被删除评论肯定也要同步被删掉掉,另外评论因为也会存在子集所以也要同步删除。 - -#### 业务接口 - -首先根据上面的分析创建评论自定义仓储接口。 - -```cs - public interface ICommentRepository : IBasicRepository - { - Task DeleteOfPost(Guid id, CancellationToken cancellationToken = default); - } - - - public interface ITagRepository : IBasicRepository - { - Task> GetListAsync(Guid blogId, CancellationToken cancellationToken = default); - - Task FindByNameAsync(Guid blogId, string name, CancellationToken cancellationToken = default); - - Task> GetListAsync(IEnumerable ids, CancellationToken cancellationToken = default); - - // 新加入的 - Task DecreaseUsageCountOfTagsAsync(List id, CancellationToken cancellationToken = default); - } -``` - -完成删除业务接口 - -```cs - - public async Task DeleteAsync(Guid id) - { - // 查找文章 - var post = await _postRepository.GetAsync(id); - // 根据文章获取Tags - var tags = await GetTagsOfPost(id); - // 减少Tag引用数量 - await _tagRepository.DecreaseUsageCountOfTagsAsync(tags.Select(t => t.Id).ToList()); - // 删除评论 - await _commentRepository.DeleteOfPost(id); - // 删除文章 - await _postRepository.DeleteAsync(id); - - } - -``` - -上面的删除接口完成后,就剩下修改接口了。 - -```cs - public async Task UpdateAsync(Guid id, UpdatePostDto input) - { - var post = await _postRepository.GetAsync(id); - - input.Url = await RenameUrlIfItAlreadyExistAsync(input.BlogId, input.Url, post); - - post.SetTitle(input.Title); - post.SetUrl(input.Url); - post.Content = input.Content; - post.Description = input.Description; - post.CoverImage = input.CoverImage; - - post = await _postRepository.UpdateAsync(post); - - var tagList = SplitTags(input.Tags); - await SaveTags(tagList, post); - - return ObjectMapper.Map(post); - } - -``` - -![整体效果](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/17.png) - - -#### 补充 - -文章整体业务完成了,现在需要把其他使用到的仓储接口实现补全一下了。 - -```cs - public class EfCoreBlogRepository : EfCoreRepository, IBlogRepository - { - public EfCoreBlogRepository(IDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - - } - - public async Task FindByShortNameAsync(string shortName, CancellationToken cancellationToken = default) - { - return await (await GetDbSetAsync()).FirstOrDefaultAsync(p => p.ShortName == shortName, GetCancellationToken(cancellationToken)); - } - } - - - public class EfCoreTagRepository : EfCoreRepository, ITagRepository - { - public EfCoreTagRepository(IDbContextProvider dbContextProvider) : base(dbContextProvider) - { - } - - public async Task> GetListAsync(Guid blogId, CancellationToken cancellationToken = default) - { - return await (await GetDbSetAsync()).Where(t => t.BlogId == blogId).ToListAsync(GetCancellationToken(cancellationToken)); - } - - public async Task GetByNameAsync(Guid blogId, string name, CancellationToken cancellationToken = default) - { - return await (await GetDbSetAsync()).FirstAsync(t => t.BlogId == blogId && t.Name == name, GetCancellationToken(cancellationToken)); - } - - public async Task FindByNameAsync(Guid blogId, string name, CancellationToken cancellationToken = default) - { - return await (await GetDbSetAsync()).FirstOrDefaultAsync(t => t.BlogId == blogId && t.Name == name, GetCancellationToken(cancellationToken)); - } - - public async Task DecreaseUsageCountOfTagsAsync(List ids, CancellationToken cancellationToken = default) - { - var tags = await (await GetDbSetAsync()) - .Where(t => ids.Any(id => id == t.Id)) - .ToListAsync(GetCancellationToken(cancellationToken)); - - foreach (var tag in tags) - { - tag.DecreaseUsageCount(); - } - } - } - - -``` - - - -#### 缓存与事件 - -将`GetTimeOrderedListAsync`的文章列表数据用缓存处理。 - -缓存用法可以直接参照官方文档:https://docs.abp.io/en/abp/latest/Caching,另外缓存如果你开启了redis就是我们第二章讲的那么数据就会进入redis,如果没开启就是内存。 - -`ICreationAuditedObject`我们会在中级篇进行讲解,名字一看就懂创建对象审核。 - -![整体效果](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/18.png) - -```cs - private readonly IDistributedCache> _postsCache; - - - public async Task> GetTimeOrderedListAsync(Guid blogId) - { - var postCacheItems = await _postsCache.GetOrAddAsync( - blogId.ToString(), - async () => await GetTimeOrderedPostsAsync(blogId), - () => new DistributedCacheEntryOptions - { - AbsoluteExpiration = DateTimeOffset.Now.AddHours(1) - } - ); - - var postsWithDetails = ObjectMapper.Map, List>(postCacheItems); - - foreach (var post in postsWithDetails) - { - if (post.CreatorId.HasValue) - { - var creatorUser = await UserLookupService.FindByIdAsync(post.CreatorId.Value); - if (creatorUser != null) - { - post.Writer = ObjectMapper.Map(creatorUser); - } - } - } - - return new ListResultDto(postsWithDetails); - - } - - private async Task> GetTimeOrderedPostsAsync(Guid blogId) - { - var posts = await _postRepository.GetOrderedList(blogId); - - return ObjectMapper.Map, List>(posts); - } - - - - - - [Serializable] - public class PostCacheItem : ICreationAuditedObject - { - public Guid Id { get; set; } - - public Guid BlogId { get; set; } - - public string Title { get; set; } - - public string CoverImage { get; set; } - - public string Url { get; set; } - - public string Content { get; set; } - - public string Description { get; set; } - - public int ReadCount { get; set; } - - public int CommentCount { get; set; } - - public List Tags { get; set; } - - public Guid? CreatorId { get; set; } - - public DateTime CreationTime { get; set; } - } - - - -``` - - - -我们现在将`根据时间排序获取文章列表`接口的文章数据缓存了,但是如果文章被删除或者修改或者创建了新的文章,怎么办,这个时候我们缓存中的数据是有问题的,我们需要刷新缓存内容。 - -领域事件使用文档:https://docs.abp.io/en/abp/latest/Local-Event-Bus - -引入`ILocalEventBus`并新增`PublishPostChangedEventAsync`方法发布事件,在删除、新增、修改接口上调用发布事件方法。 - -`PostChangedEvent`放在领域层,可以参考第三章的架构讲解 - -![调用发布事件](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/19.png) - -```cs - - private readonly ILocalEventBus _localEventBus; - - - private async Task PublishPostChangedEventAsync(Guid blogId) - { - await _localEventBus.PublishAsync( - new PostChangedEvent - { - BlogId = blogId - }); - } - - - public class PostChangedEvent - { - public Guid BlogId { get; set; } - } - -``` - -事件发布出去了,谁来处理呢,这个业务是文章的肯定文章自己处理,LocalEvent属于本地事件和接口属于同一个事务单元,可以去上面的文档说明。 - -```cs - public class PostCacheInvalidator : ILocalEventHandler, ITransientDependency - { - protected IDistributedCache> Cache { get; } - - public PostCacheInvalidator(IDistributedCache> cache) - { - Cache = cache; - } - - public virtual async Task HandleEventAsync(PostChangedEvent post) - { - await Cache.RemoveAsync(post.BlogId.ToString()); - } - } -``` - -![领域事件](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/20.png) - - -## 结语 -本节知识点: -- 1.业务开发方式 -- 2.ABP缓存的使用 -- 3.领域事件的使用 - -最麻烦的文章聚合算是讲完了,有一个ABP如何做文件上传没讲那就是那个文章封面下期再说,还剩下Comment、Tag,另外我说下我这边写代码暂时不给演示测试效果,大家也是先学ABP用法和DDD理论实践。 - -后面单元测试的时候我在把接口一把梭,加油请持续关注。 - -联系作者:加群:867095512 @MrChuJiu - -![公众号](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/wxgzh.png) - - diff --git "a/dosc/8.Abp vNext \345\237\272\347\241\200\347\257\207\344\270\250\346\240\207\347\255\276\350\201\232\345\220\210\345\212\237\350\203\275.md" "b/dosc/8.Abp vNext \345\237\272\347\241\200\347\257\207\344\270\250\346\240\207\347\255\276\350\201\232\345\220\210\345\212\237\350\203\275.md" deleted file mode 100644 index d9a20e7..0000000 --- "a/dosc/8.Abp vNext \345\237\272\347\241\200\347\257\207\344\270\250\346\240\207\347\255\276\350\201\232\345\220\210\345\212\237\350\203\275.md" +++ /dev/null @@ -1,354 +0,0 @@ -## 介绍 - -本章节先来把上一章漏掉的上传文件处理下,然后实现Tag功能。 - -## 上传文件 - -上传文件其实不含在任何一个聚合中,它属于一个独立的辅助性功能,先把抽象接口定义一下,在`Bcvp.Blog.Core.Application.Contracts`层Blog内新建File文件夹。 - -一个是根据文件name获取文件,一个是创建文件,另外`BlogWebConsts`是对上传文件的约束。 - -```cs - public interface IFileAppService : IApplicationService - { - Task GetAsync(string name); - - Task CreateAsync(FileUploadInputDto input); - } - - - public class RawFileDto - { - public byte[] Bytes { get; set; } - - public bool IsFileEmpty => Bytes == null || Bytes.Length == 0; - - public RawFileDto() - { - - } - - public static RawFileDto EmptyResult() - { - return new RawFileDto() { Bytes = new byte[0] }; - } - } - - - public class FileUploadInputDto - { - [Required] - public byte[] Bytes { get; set; } - - [Required] - public string Name { get; set; } - } - - - public class FileUploadOutputDto - { - public string Name { get; set; } - - public string WebUrl { get; set; } - } - - - public class BlogWebConsts - { - public class FileUploading - { - /// - /// Default value: 5242880 - /// - public static int MaxFileSize { get; set; } = 5242880; //5MB - - public static int MaxFileSizeAsMegabytes => Convert.ToInt32((MaxFileSize / 1024f) / 1024f); - } - } -``` - - -在`Bcvp.Blog.Core.Application`层实现抽象接口 -```cs - public class FileAppService : CoreAppService, IFileAppService - { - // Nuget: Volo.Abp.BlobStoring https://docs.abp.io/en/abp/latest/Blob-Storing - protected IBlobContainer BlobContainer { get; } - - public FileAppService( - IBlobContainer blobContainer) - { - BlobContainer = blobContainer; - } - - public virtual async Task GetAsync(string name) - { - Check.NotNullOrWhiteSpace(name, nameof(name)); - - return new RawFileDto - { - Bytes = await BlobContainer.GetAllBytesAsync(name) - }; - } - - public virtual async Task CreateAsync(FileUploadInputDto input) - { - if (input.Bytes.IsNullOrEmpty()) - { - ThrowValidationException("上传文件为空!", "Bytes"); - } - - if (input.Bytes.Length > BlogWebConsts.FileUploading.MaxFileSize) - { - throw new UserFriendlyException($"文件大小超出上限 ({BlogWebConsts.FileUploading.MaxFileSizeAsMegabytes} MB)!"); - } - - if (!ImageFormatHelper.IsValidImage(input.Bytes, FileUploadConsts.AllowedImageUploadFormats)) - { - throw new UserFriendlyException("无效的图片格式!"); - } - - var uniqueFileName = GenerateUniqueFileName(Path.GetExtension(input.Name)); - - await BlobContainer.SaveAsync(uniqueFileName, input.Bytes); - - return new FileUploadOutputDto - { - Name = uniqueFileName, - WebUrl = "/api/blog/files/www/" + uniqueFileName - }; - } - - private static void ThrowValidationException(string message, string memberName) - { - throw new AbpValidationException(message, - new List - { - new ValidationResult(message, new[] {memberName}) - }); - } - - protected virtual string GenerateUniqueFileName(string extension, string prefix = null, string postfix = null) - { - return prefix + GuidGenerator.Create().ToString("N") + postfix + extension; - } - } - - - - public class FileUploadConsts - { - public static readonly ICollection AllowedImageUploadFormats = new Collection - { - ImageFormat.Jpeg, - ImageFormat.Png, - ImageFormat.Gif, - ImageFormat.Bmp - }; - - public static string AllowedImageFormatsJoint => string.Join(",", AllowedImageUploadFormats.Select(x => x.ToString())); - } - - - public class ImageFormatHelper - { - public static ImageFormat GetImageRawFormat(byte[] fileBytes) - { - using (var memoryStream = new MemoryStream(fileBytes)) - { - return System.Drawing.Image.FromStream(memoryStream).RawFormat; - } - } - - public static bool IsValidImage(byte[] fileBytes, ICollection validFormats) - { - var imageFormat = GetImageRawFormat(fileBytes); - return validFormats.Contains(imageFormat); - } - } -``` - -结构目录如下 - -![项目结构](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/21.png) - - -#### 思考 - -这个接口的创建文件和返回都是用的byte这个适用于服务间调用,但是如果我们是前端调用根本没法用,我们传统开发的上传文件都是通过`IFormFile`来做的这里咋办? - -ABP为我们提供`Bcvp.Blog.Core.HttpApi`远程服务层,用于定义 HTTP APIs,在Controllers文件夹下创建BlogFilesController控制器,简单点理解就是文件创建还是由上面的`FileAppService`来完成,我们通过`BlogFilesController`扩展了远程服务传输文件的方式。 - -```cs - [RemoteService(Name = "blog")] // 远程服务的组名 - [Area("blog")]// Mvc里的区域 - [Route("api/blog/files")] //Api路由 - public class BlogFilesController : AbpController, IFileAppService - { - private readonly IFileAppService _fileAppService; - - public BlogFilesController(IFileAppService fileAppService) - { - _fileAppService = fileAppService; - } - - [HttpGet] - [Route("{name}")] - public Task GetAsync(string name) - { - return _fileAppService.GetAsync(name); - } - - [HttpGet] - [Route("www/{name}")] - public async Task GetForWebAsync(string name) - { - var file = await _fileAppService.GetAsync(name); - return File( - file.Bytes, - MimeTypes.GetByExtension(Path.GetExtension(name)) - ); - } - - [HttpPost] - public Task CreateAsync(FileUploadInputDto input) - { - return _fileAppService.CreateAsync(input); - } - - - [HttpPost] - [Route("images/upload")] - public async Task UploadImage(IFormFile file) - { - //TODO: localize exception messages - - if (file == null) - { - throw new UserFriendlyException("没找到文件"); - } - - if (file.Length <= 0) - { - throw new UserFriendlyException("上传文件为空"); - } - - if (!file.ContentType.Contains("image")) - { - throw new UserFriendlyException("文件不是图片类型"); - } - - var output = await _fileAppService.CreateAsync( - new FileUploadInputDto - { - Bytes = file.GetAllBytes(), - Name = file.FileName - } - ); - - return Json(new FileUploadResult(output.WebUrl)); - } - - } - - - - public class FileUploadResult - { - public string FileUrl { get; set; } - - public FileUploadResult(string fileUrl) - { - FileUrl = fileUrl; - } - } -``` - - - - - - -## 标签聚合 - -标签应该是最简单的了,它就一个功能,获取当前博客下的标签列表,在之前我们写文章聚合的时候已经把标签的仓储接口和实现都完成了,这里补一下业务接口。 - -```cs - public interface ITagAppService : IApplicationService - { - Task> GetPopularTags(Guid blogId, GetPopularTagsInput input); - - } - - public class GetPopularTagsInput - { - public int ResultCount { get; set; } = 10; - - public int? MinimumPostCount { get; set; } - } - - - public class TagAppService : CoreAppService, ITagAppService - { - private readonly ITagRepository _tagRepository; - - public TagAppService(ITagRepository tagRepository) - { - _tagRepository = tagRepository; - } - - public async Task> GetPopularTags(Guid blogId, GetPopularTagsInput input) - { - var postTags = (await _tagRepository.GetListAsync(blogId)).OrderByDescending(t=>t.UsageCount) - .WhereIf(input.MinimumPostCount != null, t=>t.UsageCount >= input.MinimumPostCount) - .Take(input.ResultCount).ToList(); - - return new List( - ObjectMapper.Map, List>(postTags)); - } - } - - - -``` - - - - - -## 查缺补漏 - -前面写了这么多结果忘了配置实体映射了,我们在AutoMapper的时候是需要配置Dto和实体的映射才是使用的,在`Bcvp.Blog.Core.Application`层有一个CoreApplicationAutoMapperProfile.cs,把漏掉的映射配置补一下。 - -```cs - public CoreApplicationAutoMapperProfile() - { - /* You can configure your AutoMapper mapping configuration here. - * Alternatively, you can split your mapping configurations - * into multiple profile classes for a better organization. */ - - CreateMap(); - CreateMap(); - - CreateMap().Ignore(x => x.CommentCount).Ignore(x => x.Tags); - CreateMap().Ignore(x => x.Writer).Ignore(x => x.CommentCount).Ignore(x => x.Tags); - CreateMap() - .Ignore(x => x.Writer) - .Ignore(x => x.CommentCount) - .Ignore(x => x.Tags); - - CreateMap(); - - } - -``` - -## 结语 -本节知识点: -- 1.远程服务层的使用 -- 2.标签聚合的完成 -- 3.AutoMapper配置 - -联系作者:加群:867095512 @MrChuJiu - -![公众号](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/wxgzh.png) diff --git "a/dosc/9.Abp vNext \345\237\272\347\241\200\347\257\207\344\270\250\350\257\204\350\256\272\350\201\232\345\220\210\345\212\237\350\203\275.md" "b/dosc/9.Abp vNext \345\237\272\347\241\200\347\257\207\344\270\250\350\257\204\350\256\272\350\201\232\345\220\210\345\212\237\350\203\275.md" deleted file mode 100644 index 852cda5..0000000 --- "a/dosc/9.Abp vNext \345\237\272\347\241\200\347\257\207\344\270\250\350\257\204\350\256\272\350\201\232\345\220\210\345\212\237\350\203\275.md" +++ /dev/null @@ -1,272 +0,0 @@ -## 介绍 - -评论本来是要放到标签里面去讲的,但是因为上一章东西有点多了,我就没放进去,这一章单独拿出来,内容不多大家自己写写就可以,也算是对前面讲解的一个小练习吧。 - -相关注释我也加在代码上面了,大家看看代码都可以理解。 - -## 评论仓储接口和实现 - -```cs - public interface ICommentRepository : IBasicRepository - { - /// - /// 根据文章Id 获取评论 - /// - /// - /// - /// - Task> GetListOfPostAsync(Guid postId, CancellationToken cancellationToken = default); - /// - /// 根据文章Id 获取评论数量 - /// - /// - /// - /// - Task GetCommentCountOfPostAsync(Guid postId, CancellationToken cancellationToken = default); - /// - /// 根据评论Id 下面的子获取评论 - /// - /// - /// - /// - Task> GetRepliesOfComment(Guid id, CancellationToken cancellationToken = default); - - Task DeleteOfPost(Guid id, CancellationToken cancellationToken = default); - } - - - - public class EfCoreCommentRepository:EfCoreRepository,ICommentRepository - { - public EfCoreCommentRepository(IDbContextProvider dbContextProvider) : base(dbContextProvider) - { - } - - public async Task> GetListOfPostAsync(Guid postId, CancellationToken cancellationToken = default) - { - return await (await GetDbSetAsync()) - .Where(a => a.PostId == postId) - .OrderBy(a => a.CreationTime) - .ToListAsync(GetCancellationToken(cancellationToken)); - } - - public async Task GetCommentCountOfPostAsync(Guid postId, CancellationToken cancellationToken = default) - { - return await (await GetDbSetAsync()) - .CountAsync(a => a.PostId == postId, GetCancellationToken(cancellationToken)); - } - - public async Task> GetRepliesOfComment(Guid id, CancellationToken cancellationToken = default) - { - return await (await GetDbSetAsync()) - .Where(a => a.RepliedCommentId == id).ToListAsync(GetCancellationToken(cancellationToken)); - } - - public async Task DeleteOfPost(Guid id, CancellationToken cancellationToken = default) - { - var recordsToDelete = await (await GetDbSetAsync()).Where(pt => pt.PostId == id).ToListAsync(GetCancellationToken(cancellationToken)); - (await GetDbSetAsync()).RemoveRange(recordsToDelete); - } - } - - -``` - -## 评论接口和Dto - -```cs - public interface ICommentAppService : IApplicationService - { - Task> GetHierarchicalListOfPostAsync(Guid postId); - - Task CreateAsync(CreateCommentDto input); - - Task UpdateAsync(Guid id, UpdateCommentDto input); - - Task DeleteAsync(Guid id); - } - - public class CommentWithRepliesDto - { - public CommentWithDetailsDto Comment { get; set; } - - public List Replies { get; set; } = new List(); - } - - public class CommentWithDetailsDto : FullAuditedEntityDto - { - public Guid? RepliedCommentId { get; set; } - - public string Text { get; set; } - - public BlogUserDto Writer { get; set; } - } - - public class CreateCommentDto - { - public Guid? RepliedCommentId { get; set; } - - public Guid PostId { get; set; } - - [Required] - public string Text { get; set; } - } - - public class UpdateCommentDto - { - [Required] - public string Text { get; set; } - } - -``` - -## 接口实现 - -```cs - - public class CommentAppService : CoreAppService, ICommentAppService - { - private IUserLookupService UserLookupService { get; } - - private readonly ICommentRepository _commentRepository; - private readonly IGuidGenerator _guidGenerator; - - public CommentAppService(ICommentRepository commentRepository, IGuidGenerator guidGenerator, IUserLookupService userLookupService) - { - _commentRepository = commentRepository; - _guidGenerator = guidGenerator; - UserLookupService = userLookupService; - } - - public async Task> GetHierarchicalListOfPostAsync(Guid postId) - { - // 获取评论数据 - var comments = await GetListOfPostAsync(postId); - - #region 对评论的作者进行赋值 - - var userDictionary = new Dictionary(); - - foreach (var commentDto in comments) - { - if (commentDto.CreatorId.HasValue) - { - var creatorUser = await UserLookupService.FindByIdAsync(commentDto.CreatorId.Value); - - if (creatorUser != null && !userDictionary.ContainsKey(creatorUser.Id)) - { - userDictionary.Add(creatorUser.Id, ObjectMapper.Map(creatorUser)); - } - } - } - - foreach (var commentDto in comments) - { - if (commentDto.CreatorId.HasValue && userDictionary.ContainsKey((Guid)commentDto.CreatorId)) - { - commentDto.Writer = userDictionary[(Guid)commentDto.CreatorId]; - } - } - - #endregion - - var hierarchicalComments = new List(); - - #region 包装评论数据格式 - - // 评论包装成2级(ps:前面的查询根据时间排序,这里不要担心子集在父级前面) - - foreach (var commentDto in comments) - { - var parent = hierarchicalComments.Find(c => c.Comment.Id == commentDto.RepliedCommentId); - - if (parent != null) - { - parent.Replies.Add(commentDto); - } - else - { - hierarchicalComments.Add(new CommentWithRepliesDto() { Comment = commentDto }); - } - } - - hierarchicalComments = hierarchicalComments.OrderByDescending(c => c.Comment.CreationTime).ToList(); - - - #endregion - - - return hierarchicalComments; - - } - - public async Task CreateAsync(CreateCommentDto input) - { - // 也可以使用这种方式(这里只是介绍用法) GuidGenerator.Create() - var comment = new Comment(_guidGenerator.Create(), input.PostId, input.RepliedCommentId, input.Text); - - comment = await _commentRepository.InsertAsync(comment); - - await CurrentUnitOfWork.SaveChangesAsync(); - - return ObjectMapper.Map(comment); - } - - public async Task UpdateAsync(Guid id, UpdateCommentDto input) - { - var comment = await _commentRepository.GetAsync(id); - - comment.SetText(input.Text); - - comment = await _commentRepository.UpdateAsync(comment); - - return ObjectMapper.Map(comment); - } - - public async Task DeleteAsync(Guid id) - { - await _commentRepository.DeleteAsync(id); - - var replies = await _commentRepository.GetRepliesOfComment(id); - - foreach (var reply in replies) - { - await _commentRepository.DeleteAsync(reply.Id); - } - } - - - private async Task> GetListOfPostAsync(Guid postId) - { - var comments = await _commentRepository.GetListOfPostAsync(postId); - - return new List( - ObjectMapper.Map, List>(comments)); - } - - } - -``` - -```cs - - CreateMap().Ignore(x => x.Writer); - -``` - - - - - -## 结语 -说明: -- 1.整个评论的实现非常简单,我们只是实现了一个2层的嵌套。 -- 2.下一章我们讲授权和策略大家应该会比较喜欢,加油 - -联系作者:加群:867095512 @MrChuJiu - -![公众号](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/wxgzh.png) - - - - diff --git "a/dosc/Abp vNext \347\225\252\345\244\226\347\257\207\344\270\250\346\216\210\346\235\203.md" "b/dosc/Abp vNext \347\225\252\345\244\226\347\257\207\344\270\250\346\216\210\346\235\203.md" deleted file mode 100644 index ab9fc00..0000000 --- "a/dosc/Abp vNext \347\225\252\345\244\226\347\257\207\344\270\250\346\216\210\346\235\203.md" +++ /dev/null @@ -1,96 +0,0 @@ -## 缘起 - -该问题来自于`ABP Framework 研习社`的`Avatar`,聊天记录和文章已经和本人沟通过了,发这篇文章是因为今天他升级`ABP 4.4`不小心写出了点问题找到我,我才想可以把这个事写一下。老哥在群里提问ABP如何走角色授权,当时看到这个问题并没有在意因为ABP默认只是扩展了策略授权,角色授权直接走微软的就可以了。 - -但是:当我看到这个老哥的role验证的时候,我的第一直觉是这个人是个憨憨,怎么可以这样写角色授权,官方明明提供了。 - -![Jwt](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/24.png) - -后面这位老哥发出了他的Jwt解析,成功引起了我的注意,他说他用角色授权一直不通过,无奈才用了上面的方案 - -![Jwt](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/25.png) - -![Jwt](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/22.png) - -当时群里很多大佬都在提出解决方案,当时我也觉得这个应该可以授权成功,但是我还是抱有丢丢的怀疑,我觉得去帮这个老哥解决这个问题。 - -![方案讨论](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/23.png) - - - -## 解密 - -带着疑惑我开始一探究竟,首先官方提供了`IsInRole`方法来验证角色是否有权限,然后根据`IAuthorizationRequirement`引用我找到了`RolesAuthorizationRequirement`, -代码里已经很明显了。 - -![官方说明](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/26.png) - -![源码](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/27.png) - -请注意下面这个`RoleClaimType`,微软要求我们的角色key是`"http://schemas.microsoft.com/ws/2008/06/identity/claims/role"`而这个老哥用的是`Role`,这就直接找到问题原因了。 - -```cs - public virtual bool IsInRole(string role) - { - for (int index = 0; index < this._identities.Count; ++index) - { - if (this._identities[index] != null && this._identities[index].HasClaim(this._identities[index].RoleClaimType, role)) - return true; - } - return false; - } -``` - - -```cs - private string _roleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"; -``` - - -## 验证设想 - -我将我的猜想发给这个老哥,让他进行实验,实验证明我的猜想对了,接下来就是解决问题了,他的这个token是授权服务器颁发的,授权服务器颁发下来key就是`role`,但是微软的`RolesAuthorizationRequirement`要求你必须是`"http://schemas.microsoft.com/ws/2008/06/identity/claims/role"`. - -![测试](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/28.png) - - -## 安全落地 - -接下来就是给老哥出方案了,虽然老哥的基础有点让我懵圈了但是这不是问题! - -![教育](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/29.png) - - -我们在认证中间和和授权中间件加入如下代码,针对`Role`进行替换。 - -```cs -app.Use(async (ctx, next) => - { - var currentPrincipalAccessor = ctx.RequestServices.GetRequiredService(); - var map = new Dictionary() - { - { "role", ClaimTypes.Role }, - }; - var mapClaims = currentPrincipalAccessor.HttpContext.User.Claims.Where(p => map.Keys.Contains(p.Type)).ToList(); - currentPrincipalAccessor.HttpContext.User.AddIdentity(new ClaimsIdentity(mapClaims.Select(p => - - new Claim(map[p.Type], p.Value, p.ValueType, p.Issuer)) - - )); - await next(); - }); -``` - -![方案落地](https://git.imweb.io/hdong/ImageBed/raw/master/BlogVnextCore/30.png) - - - -## 反思 - -ABP框架值得学习,但是.Net Core官方文档也要记得经常去看一下巩固复习。不过这个地方也应该有其他更好的解决方案,希望大佬如果有更好的方案给我留言蟹蟹! - -https://github.com/abpframework/abp/commit/d8bdcffd4c69787e69fb7ae3c3375a44a0067e7c#diff-536faff86021c0373581d67458a5bdf291c1df099523e119493c8211b541c27e - -联系作者:加群:867095512 @MrChuJiu - -非常欢迎各位针对日常开发中使用ABP遇到的疑难杂症找我提问(ps:我不一定能解决,哈哈哈)。 diff --git "a/dosc/Abp vNext \347\225\252\345\244\226\347\257\207\344\270\250\347\275\221\347\273\234\350\260\203\347\224\250.md" "b/dosc/Abp vNext \347\225\252\345\244\226\347\257\207\344\270\250\347\275\221\347\273\234\350\260\203\347\224\250.md" deleted file mode 100644 index e69de29..0000000 diff --git a/dosc/Blog.emmx b/dosc/Blog.emmx deleted file mode 100644 index dc2eef4..0000000 Binary files a/dosc/Blog.emmx and /dev/null differ diff --git a/dosc/image/1.png b/dosc/image/1.png deleted file mode 100644 index 1499ee0..0000000 Binary files a/dosc/image/1.png and /dev/null differ diff --git a/dosc/image/10.png b/dosc/image/10.png deleted file mode 100644 index ba0aad3..0000000 Binary files a/dosc/image/10.png and /dev/null differ diff --git a/dosc/image/11.png b/dosc/image/11.png deleted file mode 100644 index f96fbf5..0000000 Binary files a/dosc/image/11.png and /dev/null differ diff --git a/dosc/image/12.png b/dosc/image/12.png deleted file mode 100644 index 634835f..0000000 Binary files a/dosc/image/12.png and /dev/null differ diff --git a/dosc/image/13.png b/dosc/image/13.png deleted file mode 100644 index 52fb7b6..0000000 Binary files a/dosc/image/13.png and /dev/null differ diff --git a/dosc/image/14.png b/dosc/image/14.png deleted file mode 100644 index 86ae3cb..0000000 Binary files a/dosc/image/14.png and /dev/null differ diff --git a/dosc/image/15.png b/dosc/image/15.png deleted file mode 100644 index 0df8ac6..0000000 Binary files a/dosc/image/15.png and /dev/null differ diff --git a/dosc/image/16.png b/dosc/image/16.png deleted file mode 100644 index def72bb..0000000 Binary files a/dosc/image/16.png and /dev/null differ diff --git a/dosc/image/17.png b/dosc/image/17.png deleted file mode 100644 index 87e1606..0000000 Binary files a/dosc/image/17.png and /dev/null differ diff --git a/dosc/image/18.png b/dosc/image/18.png deleted file mode 100644 index df0bf27..0000000 Binary files a/dosc/image/18.png and /dev/null differ diff --git a/dosc/image/19.png b/dosc/image/19.png deleted file mode 100644 index dc815e0..0000000 Binary files a/dosc/image/19.png and /dev/null differ diff --git a/dosc/image/2.png b/dosc/image/2.png deleted file mode 100644 index 06881f5..0000000 Binary files a/dosc/image/2.png and /dev/null differ diff --git a/dosc/image/20.png b/dosc/image/20.png deleted file mode 100644 index 04fc630..0000000 Binary files a/dosc/image/20.png and /dev/null differ diff --git a/dosc/image/21.png b/dosc/image/21.png deleted file mode 100644 index f61d48a..0000000 Binary files a/dosc/image/21.png and /dev/null differ diff --git a/dosc/image/22.png b/dosc/image/22.png deleted file mode 100644 index db00055..0000000 Binary files a/dosc/image/22.png and /dev/null differ diff --git a/dosc/image/23.png b/dosc/image/23.png deleted file mode 100644 index 62b2c45..0000000 Binary files a/dosc/image/23.png and /dev/null differ diff --git a/dosc/image/24.png b/dosc/image/24.png deleted file mode 100644 index e8097ca..0000000 Binary files a/dosc/image/24.png and /dev/null differ diff --git a/dosc/image/25.png b/dosc/image/25.png deleted file mode 100644 index aab8e8c..0000000 Binary files a/dosc/image/25.png and /dev/null differ diff --git a/dosc/image/26.png b/dosc/image/26.png deleted file mode 100644 index f1989a1..0000000 Binary files a/dosc/image/26.png and /dev/null differ diff --git a/dosc/image/27.png b/dosc/image/27.png deleted file mode 100644 index cfdbf5d..0000000 Binary files a/dosc/image/27.png and /dev/null differ diff --git a/dosc/image/28.png b/dosc/image/28.png deleted file mode 100644 index f78a2a1..0000000 Binary files a/dosc/image/28.png and /dev/null differ diff --git a/dosc/image/29.png b/dosc/image/29.png deleted file mode 100644 index f4149e0..0000000 Binary files a/dosc/image/29.png and /dev/null differ diff --git a/dosc/image/3.png b/dosc/image/3.png deleted file mode 100644 index 0e98afa..0000000 Binary files a/dosc/image/3.png and /dev/null differ diff --git a/dosc/image/30.png b/dosc/image/30.png deleted file mode 100644 index f90c5e5..0000000 Binary files a/dosc/image/30.png and /dev/null differ diff --git a/dosc/image/31.png b/dosc/image/31.png deleted file mode 100644 index ceaf7e5..0000000 Binary files a/dosc/image/31.png and /dev/null differ diff --git a/dosc/image/32.png b/dosc/image/32.png deleted file mode 100644 index 61207ab..0000000 Binary files a/dosc/image/32.png and /dev/null differ diff --git a/dosc/image/33.png b/dosc/image/33.png deleted file mode 100644 index dc5ed97..0000000 Binary files a/dosc/image/33.png and /dev/null differ diff --git a/dosc/image/34.png b/dosc/image/34.png deleted file mode 100644 index b55b6a1..0000000 Binary files a/dosc/image/34.png and /dev/null differ diff --git a/dosc/image/4.png b/dosc/image/4.png deleted file mode 100644 index 6280da7..0000000 Binary files a/dosc/image/4.png and /dev/null differ diff --git a/dosc/image/5.png b/dosc/image/5.png deleted file mode 100644 index 105a717..0000000 Binary files a/dosc/image/5.png and /dev/null differ diff --git a/dosc/image/6.png b/dosc/image/6.png deleted file mode 100644 index 8873ebb..0000000 Binary files a/dosc/image/6.png and /dev/null differ diff --git a/dosc/image/7.png b/dosc/image/7.png deleted file mode 100644 index 6901f49..0000000 Binary files a/dosc/image/7.png and /dev/null differ diff --git a/dosc/image/8.png b/dosc/image/8.png deleted file mode 100644 index e380881..0000000 Binary files a/dosc/image/8.png and /dev/null differ diff --git a/dosc/image/9.png b/dosc/image/9.png deleted file mode 100644 index 52006df..0000000 Binary files a/dosc/image/9.png and /dev/null differ diff --git a/dosc/image/wxgzh.png b/dosc/image/wxgzh.png deleted file mode 100644 index 59a4ca3..0000000 Binary files a/dosc/image/wxgzh.png and /dev/null differ diff --git "a/dosc/\351\242\235\345\244\226\350\241\245\345\205\205.md" "b/dosc/\351\242\235\345\244\226\350\241\245\345\205\205.md" deleted file mode 100644 index c6b73f2..0000000 --- "a/dosc/\351\242\235\345\244\226\350\241\245\345\205\205.md" +++ /dev/null @@ -1,20 +0,0 @@ -,下面有一句`await AuthorizationService.CheckAsync(post, CommonOperations.Delete);`可以参照微软文档: https://docs.microsoft.com/zh-cn/aspnet/core/security/authorization/resourcebased?view=aspnetcore-5.0 - - // 判断是否有资源操作权 - await AuthorizationService.CheckAsync(post, CommonOperations.Delete); - - - - - public static class CommonOperations - { - public static OperationAuthorizationRequirement Update = new OperationAuthorizationRequirement { Name = nameof(Update) }; - public static OperationAuthorizationRequirement Delete = new OperationAuthorizationRequirement { Name = nameof(Delete) }; - } - - - await AuthorizationService.CheckAsync(post, CommonOperations.Update); - - - - diff --git a/github-actions-demo.yml b/github-actions-demo.yml deleted file mode 100644 index 711d62e..0000000 --- a/github-actions-demo.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: GitHub Actions Demo -on: [push] -jobs: - Explore-GitHub-Actions: - runs-on: ubuntu-latest - steps: - - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event." - - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!" - - run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}." - - name: Check out repository code - uses: actions/checkout@v2 - - run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner." - - run: echo "🖥️ The workflow is now ready to test your code on the runner." - - name: List files in the repository - run: | - ls ${{ github.workspace }} - - run: echo "🍏 This job's status is ${{ job.status }}."