使用.NET 6开发TodoList应用(7)——使用AutoMapper实现GET请求


系列导航

需求

需求很简单:实现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相关的所有逻辑:

定义TodoItemDtoTodoListDto对象:

  • 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列表

  • 请求
    image

  • 响应
    image

获取单个TodoList详情

  • 请求
    image

  • 响应
    image

填一个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);
}
  • 请求
    image

  • 返回
    Content部分
    image

以及Header部分
image

我们主要观察返回的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的请求,有一个小的知识点没有讲到:后台分页返回,这部分内容会在后面专门再回到查询的场景里来说。然后又留了一个小坑下一篇文章来说:全局异常处理和统一返回类型。

文章来源:https://www.cnblogs.com/code4nothing/p/build-todolist-7.html

版权声明:本文为YES开发框架网发布内容,转载请附上原文出处连接
管理员
上一篇:浅析.netcore中的Configuration
下一篇:[WPF] 抄一个 CSS3 实现的按钮
评论列表

发表评论

评论内容
昵称:
验证码:
验证码
关联文章

使用.NET 6开发TodoList应用(7)——使用AutoMapper实现GET请求
使用.NET 6开发TodoList应用(9)——实现PUT请求
使用.NET 6开发TodoList应用(6)——使用MediatR实现POST请求
使用.NET 6开发TodoList应用(16)——实现查询排序
使用.NET 6开发TodoList应用(15)——实现查询搜索
使用.NET 6开发TodoList应用(10)——实现DELETE请求以及HTTP请求幂等性
使用.NET 6开发TodoList应用(14)——实现查询过滤
使用.NET 6开发TodoList应用(25)——实现RefreshToken
使用.NET 6开发TodoList应用(22)——实现缓存
使用.NET 6开发TodoList应用(11)——使用FluentValidation和MediatR实现接口请求验证
使用.NET 6开发TodoList应用(23)——实现请求限流
使用.NET 6开发TodoList应用(13)——实现查询分页
使用.NET 6开发TodoList应用(12)——实现ActionFilter
使用.NET 6开发TodoList应用(17)——实现数据塑形
使用.NET 6开发TodoList应用(19)——处理OPTION和HEAD请求
使用.NET 6开发TodoList应用(28)——实现应用程序健康检查
使用.NET 6开发TodoList应用(30)——实现Docker打包和部署
使用.NET 6开发TodoList应用(31)——实现基于Github Actions和ACI的CI/CD
使用.NET 6开发TodoList应用(8)——实现全局异常处理
使用.NET 6开发TodoList应用(填坑1)——实现当前登录用户获取

热门标签
.NET Core .NET Reactor ag-grid AI发布 api安全 ASP.NET Core C#DLL加密 C#播放声音 C#代码混淆 C#代码加密 ChromeDriver Codex DateTime DBeaver devexpress devTool DLL混淆 edge.js EF EFCore Electron element-ui el-form el-table excel FastReport FileStream FolderBrowerDialog FolderSelectDialog form提交 git gridcontrol gridview input javascript json字符串 JS转换对象JSON jwt JWT授权 linq log Math MCP mitmproxy MVC MySQL Navicat netstat nginx node_modules NSwag Nuget Nuget镜像 number PowerShell pyinstaller python pythoncom python爬虫 python抓包 pywin32 redis Requests-html RestSharp Selenium sql SQL Server Swagger to-cms Visual Studio VSCode vue VueRouter vue路由 VUE页面通讯 Webpack Windows Windows服务 winform wmi xlrd yaml YESCMS YESWEB开发框架 白象 表单提交 播放声音 打开URL 代码混淆 弹窗提醒 端口占用 对象转换 分布式 公共字典 机器码 进程排查 静态资源 开发指南 路由参数 密钥 配置教程 配置文件 权限 人工智能 任务 任务调度 日期间隔 日志 日志记录 省市区 授权验证 数据库 四舍五入 文案 文件读取 文件夹选择 文件目录选择 问题排查 行政区域数据 页面通讯 中间件 CSharp 事务锁 工单系统 并发控制 重复提交 CMS Markdig Markdown markdown-it marked 技术选型 VS Code 开发工具 源代码管理 版本控制 Docker PostgreSQL 时区 部署排查 CMS架构 EF Core 主题系统 二次开发 插件系统 容器 运维命令 镜像清理 Linux NAS 远程挂载 飞牛 fnOS S/4HANA SAP GUI SAP HANA SAP R/3 SAP入门 SAP版本 ERP SAP SAP MM 库存管理 物料管理 采购管理 入门教程 SAP S/4HANA SPRO 企业结构 采购组织 MM01 物料主数据 物料类型 BP分组 业务伙伴 供应商主数据 ME41 RFQ 库存物料 采购流程 ME51 消耗性物料 科目分配 采购申请 AC03 ML81N 外部服务 服务主数据 Business Partner SAP培训 ME51N MM模块 Lean Services MM-SRV 外部服务采购 PIR 供应来源 采购主数据 采购信息记录 ME31K 框架协议 计划协议 采购合同 ME01 供应来源确定 货源清单 MEQ1 供应源确定 配额安排 配额评分 MD04 MD21 MRP 计划文件 需求计划 批量程序 MD01N MD02 MRP Live MD05 MM 物料计划 优化采购 供应源 采购订单
联系我们
联系电话:15090125178(微信同号)
电子邮箱:garson_zhang@163.com
站长微信二维码
微信二维码