五、Abp vNext 基础篇丨博客聚合功能
介绍
业务篇章先从客户端开始写,另外补充一下我给项目起名的时候没多想起的太随意了,结果后面有些地方命名冲突了需要通过手动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刚发布的新版本,这个版本中它移除了一些类比如ModelBuilderConfigurationOptions和DbContextModelBuilderExtensions,我就直接把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 基础篇丨博客聚合功能的更多相关文章
- Abp vNext 基础篇丨领域构建
介绍 我们将通过例⼦介绍和解释⼀些显式规则.在实现领域驱动设计时,应该遵循这些规则并将其应⽤到解决⽅案中. 领域划分 首先我们先对比下Blog.Core和本次重构设计上的偏差,可以看到多了一个博客管理 ...
- 六、Abp vNext 基础篇丨文章聚合功能上
介绍 9月开篇讲,前面几章群里已经有几个小伙伴跟着做了一遍了,遇到的问题和疑惑也都在群里反馈和解决好了,9月咱们保持保持更新.争取10月份更新完基础篇. 另外番外篇属于 我在abp群里和日常开发的问题 ...
- 八、Abp vNext 基础篇丨标签聚合功能
介绍 本章节先来把上一章漏掉的上传文件处理下,然后实现Tag功能. 上传文件 上传文件其实不含在任何一个聚合中,它属于一个独立的辅助性功能,先把抽象接口定义一下,在Bcvp.Blog.Core.App ...
- 十一、Abp vNext 基础篇丨测试
前言 祝大家国庆快乐,本来想国庆之前更新完的,结果没写完,今天把剩下的代码补了一下总算ok了. 本章节也是我们后端日常开发中最重要的一步就是测试,我们经常听到的单元测试.集成测试.UI测试.系统测试, ...
- Abp vNext 基础篇丨分层架构
介绍 本章节对 ABP 框架进行一个简单的介绍,摘自ABP官方,后面会在使用过程中对各个知识点进行细致的讲解. 领域驱动设计 领域驱动设计(简称:DDD)是一种针对复杂需求的软件开发方法.将软件实现与 ...
- 七、Abp vNext 基础篇丨文章聚合功能下
介绍 不好意思这篇文章应该早点更新的,这几天在忙CICD的东西没顾得上,等后面整好了CICD我也发2篇文章讲讲,咱们进入正题,这一章来补全剩下的 2个接口和将文章聚合进行完善. 开工 上一章大部分业务 ...
- 十、Abp vNext 基础篇丨权限
介绍 本章节来把接口的权限加一下 权限配置和使用 官方地址:https://docs.abp.io/en/abp/latest/Authorization 下面这种代码可能我们日常开发都写过,ASP. ...
- 九、Abp vNext 基础篇丨评论聚合功能
介绍 评论本来是要放到标签里面去讲的,但是因为上一章东西有点多了,我就没放进去,这一章单独拿出来,内容不多大家自己写写就可以,也算是对前面讲解的一个小练习吧. 相关注释我也加在代码上面了,大家看看代码 ...
- 我的第一篇Markdown博客
我的第一篇Markdown博客 这是我第一次用Markdown写博客,发现还是比较好用的,加上Marsedit也支持了Markdown的博客预览,博客园也加了Markdown的格式支持,就更加方便了, ...
随机推荐
- spring boot框架相关知识
1.spring:一个轻量级的控制反转和面向切面的容器,专业的开发Web项目的开源框架. spring mvc:是基于spring的mvc框架,属于一个企业WEB开发的MVC框架,涵盖面包括前端 ...
- luogu P2710 数列
(这是个双倍经验呀! 题目描述 维护一个可以支持插入.删除.翻转.区间赋值.求和.求值和求最大子段和操作的序列.(真·简洁) solution 基本不用什么神奇操作,平衡树硬上就行.(我用的 Spla ...
- 深度解析 Lucene 轻量级全文索引实现原理
一.Lucene简介 1.1 Lucene是什么? Lucene是Apache基金会jakarta项目组的一个子项目: Lucene是一个开放源码的全文检索引擎工具包,提供了完整的查询引擎和索引引擎, ...
- 00JAVA语法基础 原码、反码、补码
记得之前学C语言的时候老师课上讲过一些,不过当时觉得考试不考,也就上课听了下,下课也没怎么多做了解.这次,Java课上再次提出来了,自己也超越了些资料,对这三种概念算是有所初步了解. 1.原码 数据储 ...
- Java基础00-IO流27
1. File 1.1 File类概述和构造方法 File的构造方法:这三个构造方法可以做同样的事情 代码示例: public class File1 { public static void mai ...
- Java中的基本数据类型和引用数据类型的区别
一.数据类型 Java中的数据类型分为两大类,基本数据类型和引用数据类型. 1.基本数据类型 基本数据类型只有8种,可按照如下分类 ①整数类型:long.int.short.byte ②浮点类型:fl ...
- [006] - JavaSE面试题(六):泛型
第一期:Java面试 - 100题,梳理各大网站优秀面试题.大家可以跟着我一起来刷刷Java理论知识 [006] - JavaSE面试题(六):泛型 第1问:什么是泛型? Java泛型( generi ...
- 关于如何在mysql中插入一条数据后,返回这条数据的id
简单的总结一下如何在mysql中出入一条数据后,返回该条数据的id ,假如之后代码需要这个id,这样做起来就变得非常方便,内容如下: <insert id="insertAndGetI ...
- Rowid和Rownum
Rowid和Rownum对于数据库开发人员来说基本很少用到,因为在企业数据库开发中大多都是进行数据批处理,但是对于其他数据库人员来说还是会用到的. rowid和rownum都是虚列,但含义完全不同.r ...
- Java中Integer类型的整数值的大小比较
如果比较两个数值相等的Integer类型的整数,我们可能会发现,用"=="比较(首先你必须明确"=="比较的是地址),有的时候返回true,而有的时候,返回fa ...