五、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的格式支持,就更加方便了, ...
随机推荐
- Python - 基本数据类型_str 字符串
前言 字符串是编程中最重要的数据类型,也是最常见的 字符串的表示方式 单引号 ' ' 双引号 " " 多引号 """ ""&quo ...
- C#版Nebula客户端编译
一.需求背景 从Nebula的Github上可以发现,Nebula为以下语言提供了客户端SDK: nebula-cpp nebula-java nebula-go nebula-python nebu ...
- asp.net 简明代码
<asp:RadioButton ID="daadaa" runat="server" GroupName="dada" OnChec ...
- Requests方法 -- Blog流程类进行关联
1.接口封装关联 1.有些接口经常会用到比如登录的接口,这时候我们可以每个接口都封装成一个方法,如:登录.保存草稿.发帖.删帖,这四个接口就可以写成四个方法2.接口封装好了后,后面我们写用例那就直接调 ...
- 【Mysql】一个简易的索引方案
一.没有索引的时候如何查找 先忽略掉索引这个概念,如果现在直接要查某条记录,要如何查找呢? 在一个页中查找 如果表中的记录很少,一个页就够放,那么这时候有 2 种情况: 用主键为搜索条件:这时就是之前 ...
- springMVC-7-数据处理转换
我们为什么要对数据进行处理? 需求:在上个crud中我们如果需要每次修改的时候都要把时间也记录下来 解决:在jsp中新增一个input,在employee中新增一个Data字段 问题:input输出来 ...
- springMVC-4-处理模型数据
返回模型数据(Model) index.jsp中 <h1>获取模型数据</h1> <a href="/model/test1">ModelAnd ...
- 【洛谷P1962 斐波那契数列】矩阵快速幂+数学推导
来提供两个正确的做法: 斐波那契数列双倍项的做法(附加证明) 矩阵快速幂 一.双倍项做法 在偶然之中,在百度中翻到了有关于斐波那契数列的词条(传送门),那么我们可以发现一个这个规律$ \frac{F_ ...
- router-link与router-view的对应关系和映射特点
router-link对应的router-view规律为: 1.根据to的值而定,值为一层(如 /child)则对应app.vue中的router-view: 值为两层,如 /second/child ...
- Mac终端美化(iterm2+zsh+oh-my-zsh+powerlevel10k)
iterm2+zsh+oh-my-zsh+powerlevel10k 一.下载iterm2 方式一:官网下载 https://iterm2.com/ 方式二:百度云下载 链接: https://pan ...