五、Abp vNext 基础篇丨博客聚合功能

  • 2021 年 8 月 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

公众号