.NET 6开发TodoList应用之使用AutoMapper实现GET请求

目录
  • 需求
  • 目标
  • 原理与思路
  • 实现
    • 引入AutoMapper
    • 实现GET请求
  • 验证
    • 获取所有TodoList列表
    • 获取单个TodoList详情
    • 填一个POST文章里的坑
  • 总结

需求

需求很简单:实现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列表

请求

响应

获取单个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,并且在Header中location字段表明了创建资源的位置。

总结

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

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

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

以上就是.NET 6开发TodoList应用之使用AutoMapper实现GET请求的详细内容,更多关于.NET 6 AutoMapper实现GET请求的资料请关注我们其它相关文章!

(0)

相关推荐

  • .NET 6开发TodoList应用实现系列背景

    目录 1.列说明 2.系列导航 2.1 使用.NET 6开发TodoList应用文章索引 2.1.1创建项目 2.1.2.NET 6 WebAPI Program.cs的变更 2.1.3Change 1: Top-level statements 2.1.4Change 2: Implicit using directives 2.1.5Change 3: No Startup class 2.2 关于Pipeline的一些知识点 2.2.1Pipeline Sequence 2.2.2app.

  • 使用.NET 6开发TodoList应用之领域实体创建原理和思路

    需求 上一篇文章中我们完成了数据存储服务的接入,从这一篇开始将正式进入业务逻辑部分的开发. 首先要定义和解决的问题是,根据TodoList项目的需求,我们应该设计怎样的数据实体,如何去进行操作? 长文预警!包含大量代码 目标 在本文中,我们希望达到以下几个目标: 定义领域实体: 通过数据库操作领域实体: 原理和思路 虽然TodoList是一个很简单的应用,业务逻辑并不复杂,至少在这个系列文章中我并不想使其过度复杂.但是我还是打算借此简单地涉及领域驱动开发(DDD)的基础概念. 首先比较明确的是,

  • .NET 6开发TodoList应用之实现Repository模式

    目录 需求 目标 原理和思路 实现 通用Repository实现 引入使用 验证 总结 需求 经常写CRUD程序的小伙伴们可能都经历过定义很多Repository接口,分别做对应的实现,依赖注入并使用的场景.有的时候会发现,很多分散的XXXXRepository的逻辑都是基本一致的,于是开始思考是否可以将这些操作抽象出去,当然是可以的,而且被抽象出去的部分是可以不加改变地在今后的任何有此需求的项目中直接引入使用. 那么我们本文的需求就是:如何实现一个可重用的Repository模块. 长文预警,

  • .NET 6开发TodoList应用引入第三方日志库

    目录 1.需求 2.目标 3.原理和思路 4.实现 4.1日志配置实现 4.2主程序配置 4.3注入使用 5.验证 1.需求 在我们项目开发的过程中,使用.NET 6自带的日志系统有时是不能满足实际需求的,比如有的时候我们需要将日志输出到第三方平台上,最典型的应用就是在各种云平台上,为了集中管理日志和查询日志,通常会选择对应平台的日志SDK进行集成.使用Serilog提供的多种Sink,可以实现将日志写入不同云平台或者是非云平台的日志存储中去,这是我们这篇文章讲要研究的内容. 2.目标 我们将为

  • .NET 6开发TodoList应用之使用MediatR实现POST请求

    目录 需求 目标 原理与思路 CQRS模式 中介者Mediator模式 MediatR 实现 引入MediatR 实现Post请求 验证 创建TodoList验证 创建TodoItem验证 总结 参考资料 需求 需求很简单:如何创建新的TodoList和TodoItem并持久化. 初学者按照教程去实现的话,应该分成以下几步:创建Controller并实现POST方法:实用传入的请求参数new一个数据库实体对象:调用IRepository<T>完成数据库的写入,最多会在中间加一层Service.

  • .NET 6开发TodoList应用引入数据存储

    目录 一.需求 二.目标 三.原理和思路 四.实现 1. 引入Nuget包并进行配置 2. 添加DBContext对象并进行配置# 3. 配置文件修改 4. 主程序配置 5. 本地运行MSSQL Server容器及数据持久化 五.验证 一.需求 作为后端CRUD程序员(bushi,数据存储是开发后端服务一个非常重要的组件.对我们的TodoList项目来说,自然也需要配置数据存储. 目前的需求很简单: 需要能持久化TodoList对象并对其进行操作: 需要能持久化TodoItem对象并对其进行操作

  • .NET 6开发TodoList应用实现结构搭建

    目录 1.TodoList需求简介 2.开发工具 2.1.NET 6 2.2Visual Studio Code 2.3Hoppscotch 3.Clean Architecture简介 4.搭建解决方案结构 5.运行 往期学习: .NET 6开发TodoList应用实现系列背景 1.TodoList需求简介 首先明确一下我们即将开发的这个TodoList应用都需要完成什么功能,我不会一次性把所有的特性诸如允许用户登陆之类的需求全部写上,只是先列出最基本的功能性需求: 我们可以维护一个TodoL

  • .NET 6开发TodoList应用之使用AutoMapper实现GET请求

    目录 需求 目标 原理与思路 实现 引入AutoMapper 实现GET请求 验证 获取所有TodoList列表 获取单个TodoList详情 填一个POST文章里的坑 总结 需求 需求很简单:实现GET请求获取业务数据.在这个阶段我们经常使用的类库是AutoMapper. 目标 合理组织并使用AutoMapper,完成GET请求. 原理与思路 首先来简单地介绍一下这这个类库. 关于AutoMapper 在业务侧代码和数据库实体打交道的过程中,一个必不可少的部分就是返回的数据类型转换.对于不同的

  • .NET 6开发TodoList应用之实现查询分页

    目录 需求 目标 原理与思路 实现 定义分页结果数据结构 添加对于分页结果的Mapping Profile 创建分页查询请求 创建查询Controller 验证 总结 需求 查询中有个非常常见的需求就是后端分页,实现的方式也不算复杂,所以我们本文仅仅演示一个后端查询分页的例子. 目标 实现分页查询返回. 原理与思路 对于分页查询而言,我们需要在请求中获取当前请求的是第几页,每页请求多少项数据.在返回值中需要告诉前端,当前这一页的所有数据项列表,总共的数据项有多少.为此我们可以定义一个包装类型,供

  • .NET 6开发TodoList应用之实现查询排序

    目录 需求 目标 原理与思路 实现 验证 总结 需求 关于查询的另一个需求是要根据前端请求的排序字段进行对结果相应的排序. 目标 实现根据排序要求返回排序后的结果 原理与思路 要实现根据前端请求的进行相应排序,结合我们之前写好的Specification,可以比较简单地做到. 实现 我们还是用TodoItem请求来举例,再添加一个排序字段到查询请求中: GetTodoItemsWithConditionQuery.cs using AutoMapper; using AutoMapper.Que

  • .NET 6开发TodoList应用之实现数据塑形

    目录 需求 目标 原理与思路 实现 定义通用接口和泛型类实现 定义扩展方法 添加依赖注入 修改查询请求和Controller接口 验证 总结 需求 在查询的场景中,还有一类需求不是很常见,就是在前端请求中指定返回的字段,所以关于搜索的最后一个主题我们就来演示一下关于数据塑形(Data Shaping). 目标 实现数据塑形搜索请求. 原理与思路 对于数据塑形来说,我们需要定义一些接口和泛型类实现来完成通用的功能,然后修改对应的查询请求,实现具体的功能. 实现 定义通用接口和泛型类实现 IData

  • 使用.NET 6开发TodoList应用之引入数据存储的思路详解

    需求 作为后端CRUD程序员(bushi,数据存储是开发后端服务一个非常重要的组件.对我们的TodoList项目来说,自然也需要配置数据存储.目前的需求很简单: 需要能持久化TodoList对象并对其进行操作: 需要能持久化TodoItem对象并对其进行操作: 问题是,我们打算如何存储数据? 存储组件的选择非常多:以MSSQL Server/Postgres/MySql/SQLite等为代表的关系型数据库,以MongoDB/ElasticSearch等为代表的非关系型数据库,除此之外,我们还可以

随机推荐