前言
首先非常感谢老哥提出的问题
@落叶子
IdentityServer、用户、角色、组织 你都没用到 然后生成那么多没用的表,这点感觉不怎么好
首先回答一下上一章,大家提出的疑问,首先我本次搭建的这个项目,在安排上其实需要用到以上的东西。
但是如果说你在用的时候发现abp默认生成的很多表你不想要你该怎么办呢。
针对IdentityServer你可以移除掉改成Jwt
针对组织架构,这一点这里强调说明,在我们开发项目的时候,组织架构是需要我们根据项目要求做出调整的,在真实业务场景是需要重写的,abp提供的组织模块仅供参考。
我最近又思考了一下,我觉得上次的设计在后面会给我自己挖坑,我只是做技术教程,不想写太多业务上的代码,所以我将Entity做出了一点点的调整,将创建问答和文章的支持匿名,相应的问答就不存在采纳,而是改为赞同。
对应的也省掉了修改和删除的接口,我真是太机智了!
开始
上一节 我们简单的创建了下Entity 和 实体映射配置,这一节来吧问答业务写一下。
创捷接口抽象和Dto
根据Abp的规范,我们先要在 Application.Contracts 层创建(Contracts翻译过来就是合约,用于约束具体业务的实现行为),接口定义和Dto
创建 Questions 文件夹 内部创建 IQuestionAppService 接口
public interface IQuestionAppService : IApplicationService
{
/// <summary>
/// 获取所有问答
/// </summary>
/// <returns></returns>
Task<ListResultDto<QuestionDto>> GetListAllAsync();
/// <summary>
/// 获取问答列表
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
Task<PagedResultDto<QuestionDto>> GetListAsync(GetQuestionInputDto input);
/// <summary>
/// 获取问答详情
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
Task<QuestionDetailsDto> GetAsync(Guid id);
/// <summary>
/// 创建问答
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
Task CreateAsync(CreateQuestionInputDto input);
}
并创建 CreateQuestionInputDto、GetQuestionInputDto、QuestionDetailsDto、QuestionDto 这里因为我们没有修改所以没写修改的Dto,根据我目前的使用经验
一个Entity业务基本上对应以上 5个 Dto 就能够满足正常的CRUD业务需要。
public class CreateQuestionInputDto
{
/// <summary>
/// 标题
/// </summary>
public string Title { get; set; }
/// <summary>
/// 内容
/// </summary>
public string Content { get; set; }
/// <summary>
/// 类别
/// </summary>
public string Tag { get; set; }
}
public class GetQuestionInputDto : PagedAndSortedResultRequestDto
{
public string Filter { get; set; }
}
public class QuestionDetailsDto : FullAuditedEntityDto<Guid>
{
/// <summary>
/// 标题
/// </summary>
public string Title { get; set; }
/// <summary>
/// 内容
/// </summary>
public string Content { get; set; }
/// <summary>
/// 类别
/// </summary>
public string Tag { get; set; }
/// <summary>
/// 访问量
/// </summary>
public int Traffic { get; set; }
/// <summary>
/// 回答
/// </summary>
public List<QuestionCommentDto> QuestionComment = new List<QuestionCommentDto>();
}
public class QuestionDto : FullAuditedEntityDto<Guid>
{
/// <summary>
/// 标题
/// </summary>
public string Title { get; set; }
/// <summary>
/// 内容
/// </summary>
public string Content { get; set; }
/// <summary>
/// 类别
/// </summary>
public string Tag { get; set; }
/// <summary>
/// 访问量
/// </summary>
public int Traffic { get; set; }
/// <summary>
/// 回答数量
/// </summary>
public int QuestionCommentsCount { get; set; }
}
另外因为问答有个评论,我们在内部创建一个 Comments 文件夹,创建 CreateQuestionCommentInputDto、QuestionCommentDto 2个Dto 这里因为我们回答不牵扯修改和指定查询动作所以就2个Dto
public class CreateQuestionCommentInputDto
{
/// <summary>
/// 内容
/// </summary>
public string Content { get; set; }
}
public class QuestionCommentDto : FullAuditedEntityDto<Guid>
{
/// <summary>
/// 内容
/// </summary>
public string Content { get; set; }
/// <summary>
/// 赞同数
/// </summary>
public bool ApproveOf { get; set; }
}
完成后的结构如下图所示
实现接口业务
在 Application 创建 QuestionAppService 继承 AbpvNextAppService 和 刚才创建的 IQuestionAppService 接口。
先列出一个我们最初级的写法,当我们开始用Abp的时候,你不会写你就算用下面的写法你的代码也是很优秀的,因为它可以随着你对Abp的深入了解,非常简单的就能改造完成,
用我经常说的一句话,你已经在这么优秀的框架上进行开发了,你写的代码在烂又能烂到哪去。
public class QuestionAppService: AbpvNextAppService, IQuestionAppService
{
protected readonly IRepository<Question> _questionsRepository;
protected readonly IRepository<QuestionComment> _questionCommentsRepository;
public QuestionAppService(IRepository<Question> questionsRepository, IRepository<QuestionComment> questionCommentsRepository)
{
_questionsRepository = questionsRepository;
_questionCommentsRepository = questionCommentsRepository;
}
/// <summary>
/// 获取所有问答
/// </summary>
/// <returns></returns>
public async Task<ListResultDto<QuestionDto>> GetListAllAsync()
{
var entityList = await _questionsRepository.GetListAsync();
return new ListResultDto<QuestionDto>(ObjectMapper.Map<List<Question>, List<QuestionDto>>(entityList));
}
/// <summary>
/// 获取问答列表
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public async Task<PagedResultDto<QuestionDto>> GetListAsync(GetQuestionInputDto input)
{
// .WhereIf(!input.Filter.IsNullOrEmpty(), x => x.Title.Contains(input.Filter) || x.Content.Contains(input.Filter))
var entityList = await _questionsRepository.GetPagedListAsync(input.SkipCount, input.MaxResultCount, input.Sorting);
var result = ObjectMapper.Map<List<Question>, List<QuestionDto>>(entityList);
return new PagedResultDto<QuestionDto>(100, result);
}
/// <summary>
/// 获取问答详情
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public async Task<QuestionDetailsDto> GetAsync(Guid id)
{
var entity = await _questionsRepository.FindAsync(x=>x.Id == id);
return ObjectMapper.Map<Question, QuestionDetailsDto>(entity);
}
/// <summary>
/// 创建问答
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public async Task CreateAsync(CreateQuestionInputDto input)
{
var entity = ObjectMapper.Map<CreateQuestionInputDto, Question>(input);
await _questionsRepository.InsertAsync(entity);
}
}
改造
上面的代码你会看到我把筛选条件给注释了,因为Abp提供的GetPagedListAsync 并不是基于IQueryable而是封装的仓储方法,当然你也可以通过_questionsRepository.WithDetailsAsync()来写linq 完成,但是Abp提供了另一种方案自定义仓储
自定义仓储
在领域层的问答文件夹新建仓储接口 IQuestionRepository,这里传递一些开发经验Abp本身的那个PageList有个非常不好的点,它官方的写法是通过调用2次接口一次返回数据,一次返回Count,对于这么简单的类型返回其实用元组也可以,但是我比较偏向于定于类型所以我封装了一个EntityPageCount来数据,另外根据经验我的仓储都会出现这3个接口,如果业务需要做字段验证我会外加一个 ExistsAsync,如果还需要根据某某编码或者特定字段查询我会加一个 GetByXXXAsync。
public interface IQuestionRepository : IBasicRepository<Question, Guid>
{
Task<EntityPageCount<Question>> GetListAsync(string filter,
bool includeDetails = true, string sorting = null,
int maxResultCount = Int32.MaxValue, int skipCount = 0, CancellationToken cancellationToken = default);
Task<Question> GetByIdAsync(Guid id, bool includeDetails = true,
CancellationToken cancellationToken = default);
Task<List<Question>> GetListByIdsAsync(List<Guid> ids, bool includeDetails = true,
CancellationToken cancellationToken = default);
}
在持久化层 增加 EFCoreRepository 文件夹实现自定义仓储接口,这里有2点经验我说一下,首先是ApplyFilterForGetAll方法该方法应做到所有参数都可以传递null,方便提供给其他方法进行调用,这个可能需要一个复杂业务才能体会到。第二点就是 IncludeDetails 我个人不推荐在使用EF的时候开启懒加载,所以我会扩展一个IncludeDetails方法,通过传递参数来选择是否加载子表
(什么你问我多个子表怎么办?你不会吧bool改成Flag枚举嘛)
public class EfCoreQuestionRepository : EfCoreRepository<AbpvNextDbContext, Question, Guid>, IQuestionRepository
{
public EfCoreQuestionRepository(IDbContextProvider<AbpvNextDbContext> dbContextProvider) : base(dbContextProvider)
{
}
public async Task<EntityPageCount<Question>> GetListAsync(string filter, bool includeDetails = true,
string sorting = null, int maxResultCount = Int32.MaxValue, int skipCount = 0,
CancellationToken cancellationToken = default)
{
IQueryable<Question> query = ApplyFilterForGetAll(await GetDbSetAsync(), filter);
query = query.OrderBy(string.IsNullOrWhiteSpace(sorting) ? nameof(Question.CreationTime) : sorting).AsNoTracking();
var count = query.Count();
var entityList = await query.IncludeDetails(includeDetails).PageBy(skipCount, maxResultCount).ToListAsync(cancellationToken);
return new EntityPageCount<Question>(count, entityList);
}
public async Task<Question> GetByIdAsync(Guid id, bool includeDetails = true, CancellationToken cancellationToken = default)
{
return await(await GetDbSetAsync()).Where(x => x.Id == id).IncludeDetails(includeDetails).FirstOrDefaultAsync(GetCancellationToken(cancellationToken));
}
public async Task<List<Question>> GetListByIdsAsync(List<Guid> ids, bool includeDetails = true, CancellationToken cancellationToken = default)
{
return await(await GetDbSetAsync()).Where(x => ids.Contains(x.Id)).IncludeDetails(includeDetails).ToListAsync(GetCancellationToken(cancellationToken));
}
protected virtual IQueryable<Question> ApplyFilterForGetAll(IQueryable<Question> query, string filter)
{
return query.Include(x => x.QuestionComments)
.WhereIf(!filter.IsNullOrEmpty(), x => x.Title.Contains(filter) || x.Content.Contains(filter));
}
}
采用自定义仓储改完之后接口
public class QuestionAppService: AbpvNextAppService, IQuestionAppService
{
protected readonly IQuestionRepository _questionsRepository;
public QuestionAppService(IQuestionRepository questionsRepository)
{
_questionsRepository = questionsRepository;
}
/// <summary>
/// 获取所有问答
/// </summary>
/// <returns></returns>
public async Task<ListResultDto<QuestionDto>> GetListAllAsync()
{
var entityList = await _questionsRepository.GetListAsync();
return new ListResultDto<QuestionDto>(ObjectMapper.Map<List<Question>, List<QuestionDto>>(entityList));
}
/// <summary>
/// 获取问答列表
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public async Task<PagedResultDto<QuestionDto>> GetListAsync(GetQuestionInputDto input)
{
var entityList = await _questionsRepository.GetListAsync(input.Filter, false, input.Sorting, input.MaxResultCount, input.SkipCount);
var result = ObjectMapper.Map<List<Question>, List<QuestionDto>>(entityList.EntityList);
return new PagedResultDto<QuestionDto>(entityList.Count, result);
}
/// <summary>
/// 获取问答详情
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public async Task<QuestionDetailsDto> GetAsync(Guid id)
{
var entity = await _questionsRepository.FindAsync(id);
return ObjectMapper.Map<Question, QuestionDetailsDto>(entity);
}
/// <summary>
/// 创建问答
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public async Task CreateAsync(CreateQuestionInputDto input)
{
var entity = ObjectMapper.Map<CreateQuestionInputDto, Question>(input);
await _questionsRepository.InsertAsync(entity);
}
}
AutoMap映射
接口改完了,接下来需要把数据映射搞一下在 Application 层有一个 xxxApplicationAutoMapperProfile类
public class AbpvNextApplicationAutoMapperProfile : Profile
{
public AbpvNextApplicationAutoMapperProfile()
{
/* You can configure your AutoMapper mapping configuration here.
* Alternatively, you can split your mapping configurations
* into multiple profile classes for a better organization. */
// 问答
CreateMap<Question, QuestionDto>();
CreateMap<Question, QuestionDetailsDto>();
}
}
结语
本节知识点:
1.业务接口
2.泛型仓储介绍
3.如何自定义仓储
4.配置AutoMap映射
就先讲到这里吧,运行代码你会发现创建接口报错,因为我们没有加AutoMap映射,这里不写主要是偷懒不想写多余的代码,因为下一节我们讲DDD实践,现在这样写代码太糙了。
请各位一起指出文章中错误的讲解点 谢谢,如果你也在使用Abp有好的设计思路和想法希望你能共享给我,我们一起交流 加油!
该项目存放仓库在 https://github.com/BaseCoreVueProject/Blog.Core.AbpvNext
联系作者:加群:867095512 @MrChuJiu