系列导航

需求

需求很简单:实现GET请求获取业务数据。在这个阶段我们经常使用的类库是AutoMapper

目标

合理组织并使用AutoMapper,完成GET请求。

原理与思路

首先来简单地介绍一下这这个类库。

关于AutoMapper

在业务侧代码和数据库实体打交道的过程中,一个必不可少的部分就是返回的数据类型转换。对于不同的请求来说,希望得到的返回值是数据库实体的一部分/组合/计算等情形。我们就经常需要手写用于数据对象转换的代码,但是转换前后可能大部分情况下有着相同名称的字段或属性。这部分工作能避免手写冗长的代码吗?可以。

我们希望接受的请求和返回的值(统一称为model)具有以下两点需要遵循的原则:

  1. 每个model被且只被一个API消费;
  2. 每个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请求

在本章中我们只实现TodoListQuery接口(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对象:

  • 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));
}
}

创建一个根据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返回的,推荐的做法是下面这样:

// 省略其他...
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateTodoListCommand command)
{
var createdTodoList = await _mediator.Send(command);
// 创建成功返回201
return CreatedAtRoute("TodListById", new { id = createdTodoList.Id }, createdTodoList);
}
  • 请求

  • 返回

    Content部分

以及Header部分

我们主要观察返回的HTTPStatusCode是201,并且在Headerlocation字段表明了创建资源的位置。

总结

其他和查询请求相关的例子我就不多举了,通过两个简单的例子想说明如何组织CQRS模式下的代码逻辑。我们可以直观地看出,CQRS操作是通过IRequestIRequestHandler实现的,其中IRequest部分直接和API接口的请求参数直接或间接相关联,将请求参数通过注入的_mediator对象进行处理。

同时我们在实现两个查询接口的过程中也可以发现,查询语句中的Select部分现在已经被AutoMapper的相关功能替代掉了,所以在调用Repository时,可能并不经常用到SelectXXXX相关的具有数据类型转换的接口,更多的还是使用返回IQueryable对象的接口。这和我在使用.NET 6开发TodoList应用(5.1)——实现Repository模式中实践的有一点出入,在那篇文章中,我之所以把Repository的抽象层次做的很高的原因是,我希望顺便把类似的类库实现思路也梳理一下。就像评论中有朋友提出的那样,其实更多的场合下,因为会配合系统里其他组件的使用,比如这里的AutoMapper,那么对于Repository的实际需求就变成了只需要给我一个IQueryable对象即可。这也是我在那篇文章中试图强调的那样:关于Repository,每个人的理解和实现都有差别,因为取决于抽象程度和应用场合。

这一篇文章处理了关于GET的请求,有一个小的知识点没有讲到:后台分页返回,这部分内容会在后面专门再回到查询的场景里来说。然后又留了一个小坑下一篇文章来说:全局异常处理和统一返回类型。

使用.NET 6开发TodoList应用(7)——使用AutoMapper实现GET请求的更多相关文章

  1. 使用.NET 6开发TodoList应用(6)——使用MediatR实现POST请求

    需求 需求很简单:如何创建新的TodoList和TodoItem并持久化. 初学者按照教程去实现的话,应该分成以下几步:创建Controller并实现POST方法:实用传入的请求参数new一个数据库实 ...

  2. 使用.NET 6开发TodoList应用(19)——处理OPTION和HEAD请求

    系列导航及源代码 使用.NET 6开发TodoList应用文章索引 需求 在HTTP请求中,我们还剩下两类不常使用的请求没有讲到,本文就来实现以下关于OPTIONS和HEAD请求.OPTIONS请求用 ...

  3. 使用.NET 6开发TodoList应用文章索引

    系列导航 使用.NET 6开发TodoList应用(1)--系列背景 使用.NET 6开发TodoList应用(2)--项目结构搭建 使用.NET 6开发TodoList应用(3)--引入第三方日志 ...

  4. 使用.NET 6开发TodoList应用(3)——引入第三方日志库

    需求 在我们项目开发的过程中,使用.NET 6自带的日志系统有时是不能满足实际需求的,比如有的时候我们需要将日志输出到第三方平台上,最典型的应用就是在各种云平台上,为了集中管理日志和查询日志,通常会选 ...

  5. 使用.NET 6开发TodoList应用(1)——系列背景

    前言 想到要写这样一个系列博客,初衷有两个:一是希望通过一个实践项目,将.NET 6 WebAPI开发的基础知识串联起来,帮助那些想要入门.NET 6服务端开发的朋友们快速上手,对使用.NET 6开发 ...

  6. 使用.NET 6开发TodoList应用(2)——项目结构搭建

    为了不影响阅读的体验,我把系列导航放到文章最后了,有需要的小伙伴可以直接通过导航跳转到对应的文章 : P TodoList需求简介 首先明确一下我们即将开发的这个TodoList应用都需要完成什么功能 ...

  7. 使用.NET 6开发TodoList应用(4)——引入数据存储

    需求 作为后端CRUD程序员(bushi,数据存储是开发后端服务一个非常重要的组件.对我们的TodoList项目来说,自然也需要配置数据存储.目前的需求很简单: 需要能持久化TodoList对象并对其 ...

  8. 使用.NET 6开发TodoList应用(5)——领域实体创建

    需求 上一篇文章中我们完成了数据存储服务的接入,从这一篇开始将正式进入业务逻辑部分的开发. 首先要定义和解决的问题是,根据TodoList项目的需求,我们应该设计怎样的数据实体,如何去进行操作? 长文 ...

  9. 使用.NET 6开发TodoList应用(5.1)——实现Repository模式

    需求 经常写CRUD程序的小伙伴们可能都经历过定义很多Repository接口,分别做对应的实现,依赖注入并使用的场景.有的时候会发现,很多分散的XXXXRepository的逻辑都是基本一致的,于是 ...

随机推荐

  1. Codeforces 679E - Bear and Bad Powers of 42(线段树+势能分析)

    Codeforces 题目传送门 & 洛谷题目传送门 这个 \(42\) 的条件非常奇怪,不过注意到本题 \(a_i\) 范围的最大值为 \(10^{14}\),而在值域范围内 \(42\) ...

  2. AT695 マス目

    AT695 マス目 本题选自 DP 优化方法大杂烩 状压部分. 这个题很 nb.下文记 \(n=H\),\(m=W\). 对于每一列,如果只记录一个格子是否为黑色,那么发现它无法处理从右边绕到左边再绕 ...

  3. mysql-加密函数AES_DECRYPT函数

    向user表插入数据age字段值为888,并用AES_DECRYPT函数进行加密,key为age(可以自己随意设置,记住就行) insert into user(name,sex,age) value ...

  4. LVS-原理

    一. 集群的概念 服务器集群简称集群是一种服务器系统,它通过一组松散集成的服务器软件和/或硬件连接起来高度紧密地协作完成计算工作.在某种意义上,他们可以被看作是一台服务器.集群系统中的单个服务器通常称 ...

  5. 1 — 第一个springboot

    1.什么是springboot? 老规矩:百度百科一下 2.对springboot快速上手 1).第一种方式:通过官网来创建springboot项目 ---- 了解即可 这里面的创建方式不做过多说明, ...

  6. [Emlog主题] Monkey V3.0 优化修改

    原作者博客:https://blog.dyboy.cn/ Monkey V3.0 优化修改版 修改说明: 背景颜色修改(按个人喜好可自行修改,仿PCQQ午夜巴黎皮肤) 搜索框按钮样式优化,不那么突兀了 ...

  7. day11 序列化组件、批量出入、自定义分页器

    day11 序列化组件.批量出入.自定义分页器 今日内容详细 ajax实现删除二次提醒(普通版本) ajax结合第三方插件sweetalert实现二次提醒(样式好看些) ajax如何发送文件数据 aj ...

  8. 安全相关,xss

    XSS XSS,即 Cross Site Script,中译是跨站脚本攻击:其原本缩写是 CSS,但为了和层叠样式表(Cascading Style Sheet)有所区分,因而在安全领域叫做 XSS. ...

  9. 转 Android中Activity的启动模式(LaunchMode)和使用场景

    转载请注明出处:http://blog.csdn.net/sinat_14849739/article/details/78072401本文出自Shawpoo的专栏我的简书:简书 一.为什么需要启动模 ...

  10. java多线程 并发编程

    一.多线程 1.操作系统有两个容易混淆的概念,进程和线程. 进程:一个计算机程序的运行实例,包含了需要执行的指令:有自己的独立地址空间,包含程序内容和数据:不同进程的地址空间是互相隔离的:进程拥有各种 ...