介绍

业务篇章先从客户端开始写,另外补充一下我给项目起名的时候没多想起的太随意了,结果后面有些地方命名冲突了需要通过手动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. ADB打开快手APP

    aa="adb -s {0} shell am start -n com.kuaishou.nebula/com.yxcorp.gifshow.HomeActivity".form ...

  2. [刘阳Java]_BeanNameViewResolver视图解析器_第8讲

    BeanNameViewResolver:这个视图解析器跟XmlViewResolver有点类似,也是通过把返回的逻辑视图名称去匹配定义好的视图bean对象.它要求视图bean对象都定义在Spring ...

  3. 【记录】如何造一个vite插件(1)

    在看文章前,先做个定位,这不是一篇纯粹的技术性文章,可以把它理解成一个叙述文章,记录我开发插件的过程. 开始前简单的吹个牛 vue2 也写了很多年了,多人合作始终避不开用到别人的组件.关键是有些组件没 ...

  4. 【LeetCode】28. 实现 strStr()

    28. 实现 strStr() 知识点:字符串:KMP算法 题目描述 实现 strStr() 函数. 给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 ne ...

  5. react-router 基本使用

    # 1. 理解react-router react的一个插件库 专门用来实现一个SPA应用 基于react的项目基本都会用到此库 # 2. 几个重要问题## 1). SPA应用 单页Web应用(sin ...

  6. Requests方法 -- 参数关联

    一.删除草稿箱1.参数这篇https://www.cnblogs.com/Teachertao/p/11144726.html 2.删除刚才保存的草稿 3.用 fiddler 抓包,抓到删除帖子的请求 ...

  7. Java多线程(下)

    线程同步 当多个线程访问一个对象时,有可能会发生污读,即读取到未及时更新的数据,这个时候就需要线程同步. 线程同步: 即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线 ...

  8. 两万字Vue.js基础学习笔记(二)

    Vue.js学习笔记(二) 4.模块化开发 ES6模块化的导入和导出 我们使用export指令导出了模块对外提供的接口,下面我们就可以通过import命令来加载对应的这个模块了 首先,我们需要在HTM ...

  9. 记录一个SQL语句 case select 1

    select Code, CodeName, CodeAlias, ComCode, OtherSign from ldcode where codetype = 'edorapptype' and ...

  10. .NET5控制台程序使用EF连接MYSQL

    .NET5控制台程序使用EF连接MYSQL 1.使用nuget下载: Microsoft.Extensions.Configuration.Json Pomelo.EntityFrameworkCor ...