五、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的格式支持,就更加方便了, ...
随机推荐
- 「CF446C」 DZY Loves Fibonacci Numbers
「CF446C」 DZY Loves Fibonacci Numbers 这里提供一种优美的根号分治做法. 首先,我们考虑一种不太一样的暴力.对于一个区间加斐波那契数的操作 \([a,b]\),以及一 ...
- 嵌入式Redis服务器在Spring Boot测试中的使用
1.概述 Spring Data Redis提供了一种与Redis实例集成的简单方法. 但是,在某些情况下,使用嵌入式服务器比使用真实服务器创建开发和测试环境更方便. 因此,我们将学习如何设置和使用嵌 ...
- 【排序+模拟】魔法照片 luogu-1583
题目描述 一共有n(n≤20000)个人(以1--n编号)向佳佳要照片,而佳佳只能把照片给其中的k个人.佳佳按照与他们的关系好坏的程度给每个人赋予了一个初始权值W[i].然后将初始权值从大到小进行排序 ...
- sentry_sdk 错误日志监控 Flask配置
https://www.cnblogs.com/sui776265233/p/11348169.html 开源的平台,为小服务日志监控统一管理 pip install --upgrade sentry ...
- 自制车速记录仪「GitHub 热点速览 v.21.31」
作者:HelloGitHub-小鱼干 如果你有一辆普通的自行车,那么就可以使用下 X-TRACK 这个项目制作一个自己的测速器,记录你的行驶轨迹还有车速,体验一把硬件发烧友的乐趣.如果你有一个非 ma ...
- SpringCloud升级之路2020.0.x版-2.微服务框架需要考虑的问题
本系列为之前系列的整理重启版,随着项目的发展以及项目中的使用,之前系列里面很多东西发生了变化,并且还有一些东西之前系列并没有提到,所以重启这个系列重新整理下,欢迎各位留言交流,谢谢!~ 上图中演示了一 ...
- Tomcat7+ 弱口令 && 后台getshell漏洞
打开tomcat管理页面http://192.168.49.2:8080/manager/html,输入弱密码tomcat:tomcat,即可访问后台 先将jsp大马压缩为zip,再将zip后缀改名为 ...
- @FeignClient常用属性
@FeignClient(name = "gateway-test", value = "gateway-test", url = "localhos ...
- Elsevier(爱思唯尔)期刊模板的使用
1.爱思唯尔(Elsevier)期刊模板的使用 2.sci论文(以Elsevier为例) latex文件从小白到投稿 3.初次使用爱斯维尔(Elsevier)论文模板遇到的坑 4.latex学习 da ...
- 小技巧 | Get 到一个 Web 自动化方案,绝了!
1. 前言 大家好,我是安果! 无论是 Chrome,还是 Firefox 浏览器,它们的强大性在很大程度上都是依赖于海量的插件,让我们能高效办公 那我们是否可以编写一个插件,让浏览器自动化完成一些日 ...