介绍

业务篇章先从客户端开始写,另外补充一下我给项目起名的时候没多想起的太随意了,结果后面有些地方命名冲突了需要通过手动using不过问题不大。

开工

应用层

根据第三章分层架构里面讲到的现在我们模型已经创建好了,下一步应该是去Application.Contracts层创建我们的业务接口和Dto.


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

五、Abp vNext 基础篇丨博客聚合功能的更多相关文章

  1. Abp vNext 基础篇丨领域构建

    介绍 我们将通过例⼦介绍和解释⼀些显式规则.在实现领域驱动设计时,应该遵循这些规则并将其应⽤到解决⽅案中. 领域划分 首先我们先对比下Blog.Core和本次重构设计上的偏差,可以看到多了一个博客管理 ...

  2. 六、Abp vNext 基础篇丨文章聚合功能上

    介绍 9月开篇讲,前面几章群里已经有几个小伙伴跟着做了一遍了,遇到的问题和疑惑也都在群里反馈和解决好了,9月咱们保持保持更新.争取10月份更新完基础篇. 另外番外篇属于 我在abp群里和日常开发的问题 ...

  3. 八、Abp vNext 基础篇丨标签聚合功能

    介绍 本章节先来把上一章漏掉的上传文件处理下,然后实现Tag功能. 上传文件 上传文件其实不含在任何一个聚合中,它属于一个独立的辅助性功能,先把抽象接口定义一下,在Bcvp.Blog.Core.App ...

  4. 十一、Abp vNext 基础篇丨测试

    前言 祝大家国庆快乐,本来想国庆之前更新完的,结果没写完,今天把剩下的代码补了一下总算ok了. 本章节也是我们后端日常开发中最重要的一步就是测试,我们经常听到的单元测试.集成测试.UI测试.系统测试, ...

  5. Abp vNext 基础篇丨分层架构

    介绍 本章节对 ABP 框架进行一个简单的介绍,摘自ABP官方,后面会在使用过程中对各个知识点进行细致的讲解. 领域驱动设计 领域驱动设计(简称:DDD)是一种针对复杂需求的软件开发方法.将软件实现与 ...

  6. 七、Abp vNext 基础篇丨文章聚合功能下

    介绍 不好意思这篇文章应该早点更新的,这几天在忙CICD的东西没顾得上,等后面整好了CICD我也发2篇文章讲讲,咱们进入正题,这一章来补全剩下的 2个接口和将文章聚合进行完善. 开工 上一章大部分业务 ...

  7. 十、Abp vNext 基础篇丨权限

    介绍 本章节来把接口的权限加一下 权限配置和使用 官方地址:https://docs.abp.io/en/abp/latest/Authorization 下面这种代码可能我们日常开发都写过,ASP. ...

  8. 九、Abp vNext 基础篇丨评论聚合功能

    介绍 评论本来是要放到标签里面去讲的,但是因为上一章东西有点多了,我就没放进去,这一章单独拿出来,内容不多大家自己写写就可以,也算是对前面讲解的一个小练习吧. 相关注释我也加在代码上面了,大家看看代码 ...

  9. 我的第一篇Markdown博客

    我的第一篇Markdown博客 这是我第一次用Markdown写博客,发现还是比较好用的,加上Marsedit也支持了Markdown的博客预览,博客园也加了Markdown的格式支持,就更加方便了, ...

随机推荐

  1. python 15篇 面向对象

    1.面向对象编程概念 面向对象是包含面向过程 面向过程编程 买车: 1.4s看车,买车 2.上保险 保险公司 3.交税 地税局 4.交管所 上牌面向对象编程 卖车处: 1.4s 2.保险 3.交税 4 ...

  2. C语言的编译与链接

    C语言源文件要经过编译.链接才能生成可执行程序:编译(Compile)会将源文件(.c文件)转换为目标文件.对于 VC/VS,目标文件后缀为.obj:对于GCC,目标文件后缀为.o.编译是针对单个源文 ...

  3. 使用BeautifulSoup自动爬取微信公众号图片

    爬取微信分享的图片,根据不同的页面自行修改,使用BeautifulSoup爬取,自行格局HTML修改要爬取图片的位置 import re import time import requests imp ...

  4. Python3.7 lxml引入etree

    用xml代替lxml,Python3.7中已经没有etree这个模块了 import xml.etree.ElementTree as etree from lxml import etree 这种方 ...

  5. java集合(3)-Java8新增的Predicate操作集合

    Java8起为Collection集合新增了一个removeIf(Predicate filter)方法,该方法将批量删除符合filter条件的所有元素.该方法需要一个Predicate(谓词)对象作 ...

  6. javascript学习(二)--函数

    一.在JavaScript中,定义函数的方式如下: 1.第一种方式: function abs(x) { if (x >= 0) { return x; } else { return -x; ...

  7. Spring-初见

    目录 组成 Spring Boot与Spring Cloud IOC DI 1.常量注入 2.Bean注入 3.Properties注入 p命名和c命名注入 Bean Singleton Protot ...

  8. tp3常量

    __MODULE__ __CONTROLLER__

  9. 微信小程序 -- 英语词典 (小程序插件)

    英语词典小程序 基于英语词典小程序插件 - 提供开源地址 项目地址 英语词典小程序插件: 微信小程序 词典 真题基础服务插件(gitee.com) 功能特色 [x] 全面详实的经典词库,详细释义覆盖约 ...

  10. Mac搭建Vue开发环境

    1.安装Homebrew /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/ ...