五、Abp vNext 基礎篇丨部落格聚合功能

初久的私房菜發表於2021-08-27

介紹

業務篇章先從客戶端開始寫,另外補充一下我給專案起名的時候沒多想起的太隨意了,結果後面有些地方命名衝突了需要通過手動using不過問題不大。

開工

應用層

根據第三章分層架構裡面講到的現在我們模型已經建立好了,下一步應該是去Application.Contracts層建立我們的業務介面和Dto.

Blog業務介面


    public interface IBlogAppService : IApplicationService
    {
        Task<ListResultDto<BlogDto>> GetListAsync();

        Task<BlogDto> GetByShortNameAsync(string shortName);

        Task<BlogDto> GetAsync(Guid id);
    }


    public class BlogDto : FullAuditedEntityDto<Guid>
    {
        public string Name { get; set; }

        public string ShortName { get; set; }

        public string Description { get; set; }
    }

介面寫完之後,我們去Application層實現 Application.Contracts 中定義的服務接⼝,應⽤服務是⽆狀態服務,實現應⽤程式⽤例。⼀個應⽤服務通常使⽤領域物件實現⽤例,獲取或返回數 據傳輸物件DTOs,被展示層調⽤。

應⽤服務通⽤原則:

  • 實現特定⽤例的應⽤邏輯,不能在應⽤服務中實現領域邏輯(需要理清應⽤邏輯和領域邏輯⼆者的 區別)。
  • 應⽤服務⽅法不能返回實體,因為這樣會打破領域層的封裝性,始終只返回DTO。

大家先看下面的程式碼有什麼問題

public class BlogAppService : CoreAppService, IBlogAppService
    {
        private readonly IRepository<Blog> _blogRepository;

        public BlogAppService(IRepository<Blog> blogRepository)
        {
            _blogRepository = blogRepository;
        }
        public async Task<ListResultDto<BlogDto>> GetListAsync()
        {
            var blogs = await _blogRepository.GetListAsync();

            return new ListResultDto<BlogDto>(
                ObjectMapper.Map<List<Blog>, List<BlogDto>>(blogs)
            );
        }

        public async Task<BlogDto> 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, BlogDto>(blog);
        }

        public async Task<BlogDto> GetAsync(Guid id)
        {
            var blog = await _blogRepository.GetAsync(x=>x.Id == id);

            return ObjectMapper.Map<Blog, BlogDto>(blog);
        }
    }

錯誤:上面程式碼違反了應用層原則將特定⽤例的應⽤邏輯寫在了應⽤服務層。

倉儲

解決上面的問題就要用到倉儲,ABP預設提供的泛型倉儲無法滿足業務需要的時候就需要我們自定義倉儲,倉儲應該只針對聚合根,⽽不是所有實體。因為⼦集合實體(聚合)應該通過聚合根訪問。

倉儲定義寫在領域層,倉儲實現寫在基礎層,參照第三章:ABP專案分層解析關於資料庫獨⽴性原則的討論

倉儲的通⽤原則

  • 在領域層中定義倉儲接⼝,在基礎層中實現倉儲接⼝(⽐如: EntityFrameworkCore 項⽬ 或 MongoDB 項⽬)
  • 倉儲不包含業務邏輯,專注資料處理。
  • 倉儲接⼝應該保持 資料提供程式/ORM 獨⽴性。舉個例⼦,倉儲接⼝定義的⽅法不能返回 DbSet 物件,因為該物件由 EF Core 提供,如果使⽤ MongoDB 資料庫則⽆法實現該接⼝。
  • 為聚合根建立對應倉儲,⽽不是所有實體。因為⼦集合實體(聚合)應該通過聚合根訪問。

專案結構

    public interface IBlogRepository : IBasicRepository<Blog, Guid>
    {
        Task<Blog> FindByShortNameAsync(string shortName, CancellationToken cancellationToken = default);
    }



    public class EfCoreBlogRepository : EfCoreRepository<CoreDbContext, Blog, Guid>, IBlogRepository
    {
        public EfCoreBlogRepository(IDbContextProvider<CoreDbContext> dbContextProvider)
            : base(dbContextProvider)
        {

        }

        public async Task<Blog> 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<ListResultDto<BlogDto>> GetListAsync()
        {
            var blogs = await _blogRepository.GetListAsync();

            return new ListResultDto<BlogDto>(
                ObjectMapper.Map<List<Blog>, List<BlogDto>>(blogs)
            );
        }

        public async Task<BlogDto> 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, BlogDto>(blog);
        }

        public async Task<BlogDto> GetAsync(Guid id)
        {
            var blog = await _blogRepository.GetAsync(id);

            return ObjectMapper.Map<Blog, BlogDto>(blog);
        }
    }

對映Domain物件

上面完成後我們就可以啟動系統看到我們定義的介面了,但是我們還少了一步那就是對映 Domain 物件(實體和值型別)到資料庫表。

演示

CoreDbContext上下文中加入我們的實體,然後在 CoreEfCoreEntityExtensionMappings 中新建一個靜態ConfigureBcvpBlogCore方法寫FluentApi,這裡有幾個疑惑我說下,因為我目前使用的版本是4.4也就是ABP剛釋出的新版本,這個版本中它移除了一些類比如ModelBuilderConfigurationOptionsDbContextModelBuilderExtensions,我就直接把ConfigureBcvpBlogCore寫在CoreEfCoreEntityExtensionMappings裡面了,可能後面我會在找合理的地方去單獨放,另外可以看到PostTag沒有出現在這裡,這是因為PostTag是一個值物件作為實體的私有型別處理了,這裡就能充分感受到模型建立與資料庫對映抽離。


----------------------------- CoreDbContext.cs

        public DbSet<BlogCore.Blogs.Blog> Blogs { get; set; }

        public DbSet<Post> Posts { get; set; }

        public DbSet<Tag> Tags { get; set; }

        public DbSet<Comment> 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<BlogCore.Blogs.Blog>(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<Post>(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<BlogCore.Blogs.Blog>().WithMany().IsRequired().HasForeignKey(p => p.BlogId);

                b.ApplyObjectExtensionMappings();
            });

            builder.Entity<Tag>(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<Comment>(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<Comment>().WithMany().HasForeignKey(p => p.RepliedCommentId);
                b.HasOne<Post>().WithMany().IsRequired().HasForeignKey(p => p.PostId);

                b.ApplyObjectExtensionMappings();
            });


         

            builder.TryConfigureObjectExtensions<CoreDbContext>();

        }

接下來就是生成遷移和執行遷移了

建立專案

結語

本節知識點:

  • 1.根據前面4章講的知識完成部落格建模
  • 2.完成業務部落格業務程式碼
  • 3.自定義倉儲

聯絡作者:加群:867095512 @MrChuJiu

公眾號

相關文章