使用.NET 6开发TodoList应用(7)——使用AutoMapper实现GET请求
系列导航#
需求#
需求很简单:实现GET
请求获取业务数据。在这个阶段我们经常使用的类库是AutoMapper。
目标#
合理组织并使用AutoMapper,完成GET
请求。
原理与思路#
首先来简单地介绍一下这这个类库。
关于AutoMapper#
在业务侧代码和数据库实体打交道的过程中,一个必不可少的部分就是返回的数据类型转换。对于不同的请求来说,希望得到的返回值是数据库实体的一部分/组合/计算等情形。我们就经常需要手写用于数据对象转换的代码,但是转换前后可能大部分情况下有着相同名称的字段或属性。这部分工作能避免手写冗长的代码吗?可以。
我们希望接受的请求和返回的值(统一称为model)具有以下两点需要遵循的原则:
- 每个model被且只被一个API消费;
- 每个model里仅仅包含API发起方希望包含的必要字段或属性。
AutoMapper库就是为了实现这个需求而存在的,它的具体用法请参考官方文档,尤其是关于Convention
的部分,避免重复劳动。
实现#
所有需要使用AutoMapper
的地方都集中在Application
项目中。
引入AutoMapper#
$ dotnet add src/TodoList.Application/TodoList.Application.csproj package AutoMapper.Extensions.Microsoft.DependencyInjection
然后在Application/Common/Mappings
下添加配置,提供接口的原因是我们后面就可以在DTO
里实现各自对应的Mapping规则,方便查找。
IMapFrom.cs
using AutoMapper;
namespace TodoList.Application.Common.Mappings;
public interface IMapFrom<T>
{
void Mapping(Profile profile) => profile.CreateMap(typeof(T), GetType());
}
MappingProfile.cs
using System.Reflection;
using AutoMapper;
namespace TodoList.Application.Common.Mappings;
public class MappingProfile : Profile
{
public MappingProfile() => ApplyMappingsFromAssembly(Assembly.GetExecutingAssembly());
private void ApplyMappingsFromAssembly(Assembly assembly)
{
var types = assembly.GetExportedTypes()
.Where(t => t.GetInterfaces().Any(i =>
i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IMapFrom<>)))
.ToList();
foreach (var type in types)
{
var instance = Activator.CreateInstance(type);
var methodInfo = type.GetMethod("Mapping")
?? type.GetInterface("IMapFrom`1")!.GetMethod("Mapping");
methodInfo?.Invoke(instance, new object[] { this });
}
}
}
在DependencyInjection.cs
进行依赖注入:
DependencyInjection.cs
// 省略其他...
services.AddAutoMapper(Assembly.GetExecutingAssembly());
services.AddMediatR(Assembly.GetExecutingAssembly());
return services;
实现GET请求#
在本章中我们只实现TodoList
的Query
接口(GET),并且在结果中包含TodoItem
集合,剩下的接口后面的文章中逐步涉及。
GET All TodoLists#
在Application/TodoLists/Queries/
下新建一个目录GetTodos
用于存放创建一个TodoList
相关的所有逻辑:
定义TodoListBriefDto
对象:
TodoListBriefDto.cs
using TodoList.Application.Common.Mappings;
namespace TodoList.Application.TodoLists.Queries.GetTodos;
// 实现IMapFrom<T>接口,因为此Dto不涉及特殊字段的Mapping规则
// 并且属性名称与领域实体保持一致,根据Convention规则默认可以完成Mapping,不需要额外实现
public class TodoListBriefDto : IMapFrom<Domain.Entities.TodoList>
{
public Guid Id { get; set; }
public string? Title { get; set; }
public string? Colour { get; set; }
}
GetTodosQuery.cs
using AutoMapper;
using AutoMapper.QueryableExtensions;
using MediatR;
using Microsoft.EntityFrameworkCore;
using TodoList.Application.Common.Interfaces;
namespace TodoList.Application.TodoLists.Queries.GetTodos;
public class GetTodosQuery : IRequest<List<TodoListBriefDto>>
{
}
public class GetTodosQueryHandler : IRequestHandler<GetTodosQuery, List<TodoListBriefDto>>
{
private readonly IRepository<Domain.Entities.TodoList> _repository;
private readonly IMapper _mapper;
public GetTodosQueryHandler(IRepository<Domain.Entities.TodoList> repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
public async Task<List<TodoListBriefDto>> Handle(GetTodosQuery request, CancellationToken cancellationToken)
{
return await _repository
.GetAsQueryable()
.AsNoTracking()
.ProjectTo<TodoListBriefDto>(_mapper.ConfigurationProvider)
.OrderBy(t => t.Title)
.ToListAsync(cancellationToken);
}
}
最后实现Controller
层的逻辑:
TodoListController.cs
// 省略其他...
[HttpGet]
public async Task<ActionResult<List<TodoListBriefDto>>> Get()
{
return await _mediator.Send(new GetTodosQuery());
}
GET Single TodoList#
首先在Application/TodoItems/Queries/
下新建目录GetTodoItems
用于存放获取TodoItem
相关的所有逻辑:
定义TodoItemDto
和TodoListDto
对象:
TodoItemDto.cs
using AutoMapper;
using TodoList.Application.Common.Mappings;
using TodoList.Domain.Entities;
namespace TodoList.Application.TodoItems.Queries.GetTodoItems;
// 实现IMapFrom<T>接口
public class TodoItemDto : IMapFrom<TodoItem>
{
public Guid Id { get; set; }
public Guid ListId { get; set; }
public string? Title { get; set; }
public bool Done { get; set; }
public int Priority { get; set; }
// 实现接口定义的Mapping方法,并提供除了Convention之外的特殊字段的转换规则
public void Mapping(Profile profile)
{
profile.CreateMap<TodoItem, TodoItemDto>()
.ForMember(d => d.Priority, opt => opt.MapFrom(s => (int)s.Priority));
}
}
TodoListDto.cs
using TodoList.Application.Common.Mappings;
using TodoList.Application.TodoItems.Queries.GetTodoItems;
namespace TodoList.Application.TodoLists.Queries.GetSingleTodo;
// 实现IMapFrom<T>接口,因为此Dto不涉及特殊字段的Mapping规则
// 并且属性名称与领域实体保持一致,根据Convention规则默认可以完成Mapping,不需要额外实现
public class TodoListDto : IMapFrom<Domain.Entities.TodoList>
{
public Guid Id { get; set; }
public string? Title { get; set; }
public string? Colour { get; set; }
public IList<TodoItemDto> Items { get; set; } = new List<TodoItemDto>();
}
创建一个根据ListId
来获取包含TodoItems
子项的spec:
TodoListSpec.cs
using Microsoft.EntityFrameworkCore;
using TodoList.Application.Common;
namespace TodoList.Application.TodoLists.Specs;
public sealed class TodoListSpec : SpecificationBase<Domain.Entities.TodoList>
{
public TodoListSpec(Guid id, bool includeItems = false) : base(t => t.Id == id)
{
if (includeItems)
{
AddInclude(t => t.Include(i => i.Items));
}
}
}
我们仍然为这个查询新建一个GetSingleTodo
目录,并实现GetSIngleTodoQuery
:
GetSingleTodoQuery.cs
using AutoMapper;
using AutoMapper.QueryableExtensions;
using MediatR;
using Microsoft.EntityFrameworkCore;
using TodoList.Application.Common.Interfaces;
using TodoList.Application.TodoLists.Specs;
namespace TodoList.Application.TodoLists.Queries.GetSingleTodo;
public class GetSingleTodoQuery : IRequest<TodoListDto?>
{
public Guid ListId { get; set; }
}
public class ExportTodosQueryHandler : IRequestHandler<GetSingleTodoQuery, TodoListDto?>
{
private readonly IRepository<Domain.Entities.TodoList> _repository;
private readonly IMapper _mapper;
public ExportTodosQueryHandler(IRepository<Domain.Entities.TodoList> repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
public async Task<TodoListDto?> Handle(GetSingleTodoQuery request, CancellationToken cancellationToken)
{
var spec = new TodoListSpec(request.ListId, true);
return await _repository
.GetAsQueryable(spec)
.AsNoTracking()
.ProjectTo<TodoListDto>(_mapper.ConfigurationProvider)
.FirstOrDefaultAsync(cancellationToken);
}
}
添加Controller逻辑,这里的Name
是为了完成之前遗留的201返回的问题,后文会有使用。
TodoListController.cs
// 省略其他...
[HttpGet("{id:Guid}", Name = "TodListById")]
public async Task<ActionResult<TodoListDto>> GetSingleTodoList(Guid id)
{
return await _mediator.Send(new GetSingleTodoQuery
{
ListId = id
}) ?? throw new InvalidOperationException();
}
验证#
运行Api
项目
获取所有TodoList列表#
获取单个TodoList详情#
填一个POST文章里的坑#
在使用.NET 6开发TodoList应用(6)——使用MediatR实现POST请求中我们留了一个问题,即创建TodoList
后的返回值当时我们是临时使用Id
返回的,推荐的做法是下面这样:
需要修改CreateTodoListCommand
的定义,现在我们需要返回实体对象而不是原先的Id:
CreateTodoListCommand.cs
using MediatR;
using TodoList.Application.Common.Interfaces;
using TodoList.Domain.ValueObjects;
namespace TodoList.Application.TodoLists.Commands.CreateTodoList;
public class CreateTodoListCommand : IRequest<Domain.Entities.TodoList>
{
public string? Title { get; set; }
public string? Colour { get; set; }
}
public class CreateTodoListCommandHandler : IRequestHandler<CreateTodoListCommand, Domain.Entities.TodoList>
{
private readonly IRepository<Domain.Entities.TodoList> _repository;
public CreateTodoListCommandHandler(IRepository<Domain.Entities.TodoList> repository)
{
_repository = repository;
}
public async Task<Domain.Entities.TodoList> Handle(CreateTodoListCommand request, CancellationToken cancellationToken)
{
var entity = new Domain.Entities.TodoList
{
Title = request.Title,
Colour = Colour.From(request.Colour ?? string.Empty)
};
await _repository.AddAsync(entity, cancellationToken);
return entity;
}
}
// 省略其他...
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateTodoListCommand command)
{
var createdTodoList = await _mediator.Send(command);
// 创建成功返回201
return CreatedAtRoute("TodListById", new { id = createdTodoList.Id }, createdTodoList);
}
我们主要观察返回的HTTPStatusCode
是201,并且在Header
中location
字段表明了创建资源的位置。
总结#
其他和查询请求相关的例子我就不多举了,通过两个简单的例子想说明如何组织CQRS模式下的代码逻辑。我们可以直观地看出,CQRS操作是通过IRequest
和IRequestHandler
实现的,其中IRequest
部分直接和API接口的请求参数直接或间接相关联,将请求参数通过注入的_mediator
对象进行处理。
同时我们在实现两个查询接口的过程中也可以发现,查询语句中的Select
部分现在已经被AutoMapper
的相关功能替代掉了,所以在调用Repository
时,可能并不经常用到SelectXXXX
相关的具有数据类型转换的接口,更多的还是使用返回IQueryable
对象的接口。这和我在使用.NET 6开发TodoList应用(5.1)——实现Repository模式中实践的有一点出入,在那篇文章中,我之所以把Repository
的抽象层次做的很高的原因是,我希望顺便把类似的类库实现思路也梳理一下。就像评论中有朋友提出的那样,其实更多的场合下,因为会配合系统里其他组件的使用,比如这里的AutoMapper
,那么对于Repository
的实际需求就变成了只需要给我一个IQueryable
对象即可。这也是我在那篇文章中试图强调的那样:关于Repository
,每个人的理解和实现都有差别,因为取决于抽象程度和应用场合。
这一篇文章处理了关于GET
的请求,有一个小的知识点没有讲到:后台分页返回,这部分内容会在后面专门再回到查询的场景里来说。然后又留了一个小坑下一篇文章来说:全局异常处理和统一返回类型。
作者:CODE4NOTHING
出处:https://www.cnblogs.com/code4nothing/p/build-todolist-7.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
欢迎转载,转载请注明出处
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架