使用.NET 6开发TodoList应用(17)——实现数据塑形


系列导航及源代码

需求

在查询的场景中,还有一类需求不是很常见,就是在前端请求中指定返回的字段,所以关于搜索的最后一个主题我们就来演示一下关于数据塑形(Data Shaping)。

目标

实现数据塑形搜索请求。

原理与思路

对于数据塑形来说,我们需要定义一些接口和泛型类实现来完成通用的功能,然后修改对应的查询请求,实现具体的功能。

实现

定义通用接口和泛型类实现

  • IDataShaper.cs
using System.Dynamic;

namespace TodoList.Application.Common.Interfaces;

public interface IDataShaper<T>
{
    IEnumerable<ExpandoObject> ShapeData(IEnumerable<T> entities, string fieldString);
    ExpandoObject ShapeData(T entity, string fieldString);
}

并实现通用的功能:

  • DataShaper.cs
using System.Dynamic;
using System.Reflection;
using TodoList.Application.Common.Interfaces;

namespace TodoList.Application.Common;

public class DataShaper<T> : IDataShaper<T> where T : class
{
    public PropertyInfo[] Properties { get; set; }

    public DataShaper()
    {
        Properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
    }

    public IEnumerable<ExpandoObject> ShapeData(IEnumerable<T> entities, string? fieldString)
    {
        var requiredProperties = GetRequiredProperties(fieldString);

        return GetData(entities, requiredProperties);
    }

    public ExpandoObject ShapeData(T entity, string? fieldString)
    {
        var requiredProperties = GetRequiredProperties(fieldString);

        return GetDataForEntity(entity, requiredProperties);
    }

    private IEnumerable<PropertyInfo> GetRequiredProperties(string? fieldString)
    {
        var requiredProperties = new List<PropertyInfo>();

        if (!string.IsNullOrEmpty(fieldString))
        {
            var fields = fieldString.Split(',', StringSplitOptions.RemoveEmptyEntries);
            foreach (var field in fields)
            {
                var property = Properties.FirstOrDefault(pi => pi.Name.Equals(field.Trim(), StringComparison.InvariantCultureIgnoreCase));
                if (property == null)
                {
                    continue;
                }

                requiredProperties.Add(property);
            }
        }
        else
        {
            requiredProperties = Properties.ToList();
        }

        return requiredProperties;
    }

    private IEnumerable<ExpandoObject> GetData(IEnumerable<T> entities, IEnumerable<PropertyInfo> requiredProperties)
    {
        return entities.Select(entity => GetDataForEntity(entity, requiredProperties)).ToList();
    }

    private ExpandoObject GetDataForEntity(T entity, IEnumerable<PropertyInfo> requiredProperties)
    {
        var shapedObject = new ExpandoObject();
        foreach (var property in requiredProperties)
        {
            var objectPropertyValue = property.GetValue(entity);
            shapedObject.TryAdd(property.Name, objectPropertyValue);
        }

        return shapedObject;
    }
}

定义扩展方法

为了使我们的Handle方法调用链能够直接应用,我们在Application/Extensions中新增一个DataShaperExtensions

  • DataShaperExtensions.cs
using System.Dynamic;
using TodoList.Application.Common.Interfaces;

namespace TodoList.Application.Common.Extensions;

public static class DataShaperExtensions
{
    public static IEnumerable<ExpandoObject> ShapeData<T>(this IEnumerable<T> entities, IDataShaper<T> shaper, string? fieldString)
    {
        return shaper.ShapeData(entities, fieldString);
    }
}

然后再对我们之前写的MappingExtensions静态类中添加一个方法:

  • MappingExtensions.cs
// 省略其他...
public static PaginatedList<TDestination> PaginatedListFromEnumerable<TDestination>(this IEnumerable<TDestination> entities, int pageNumber, int pageSize)
{
    return PaginatedList<TDestination>.Create(entities, pageNumber, pageSize);   
}

添加依赖注入

ApplicationDependencyInjection.cs中添加依赖注入:

  • DependencyInjection.cs
// 省略其他
services.AddScoped(typeof(IDataShaper<>), typeof(DataShaper<>));

修改查询请求和Controller接口

我们在上一篇文章实现排序的基础上增加一个字段用于指明数据塑形字段并对应修改Handle方法:

  • GetTodoItemsWithConditionQuery.cs
using System.Dynamic;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using MediatR;
using TodoList.Application.Common.Extensions;
using TodoList.Application.Common.Interfaces;
using TodoList.Application.Common.Mappings;
using TodoList.Application.Common.Models;
using TodoList.Application.TodoItems.Specs;
using TodoList.Domain.Entities;
using TodoList.Domain.Enums;

namespace TodoList.Application.TodoItems.Queries.GetTodoItems;

public class GetTodoItemsWithConditionQuery : IRequest<PaginatedList<ExpandoObject>>
{
    public Guid ListId { get; set; }
    public bool? Done { get; set; }
    public string? Title { get; set; }
    // 前端指明需要返回的字段
    public string? Fields { get; set; }
    public PriorityLevel? PriorityLevel { get; set; }
    public string? SortOrder { get; set; } = "title_asc";
    public int PageNumber { get; set; } = 1;
    public int PageSize { get; set; } = 10;
}

public class GetTodoItemsWithConditionQueryHandler : IRequestHandler<GetTodoItemsWithConditionQuery, PaginatedList<ExpandoObject>>
{
    private readonly IRepository<TodoItem> _repository;
    private readonly IMapper _mapper;
    private readonly IDataShaper<TodoItemDto> _shaper;

    public GetTodoItemsWithConditionQueryHandler(IRepository<TodoItem> repository, IMapper mapper, IDataShaper<TodoItemDto> shaper)
    {
        _repository = repository;
        _mapper = mapper;
        _shaper = shaper;
    }

    public Task<PaginatedList<ExpandoObject>> Handle(GetTodoItemsWithConditionQuery request, CancellationToken cancellationToken)
    {
        var spec = new TodoItemSpec(request);
        return Task.FromResult(
            _repository
                .GetAsQueryable(spec)
                .ProjectTo<TodoItemDto>(_mapper.ConfigurationProvider)
                .AsEnumerable()
                // 进行数据塑形和分页返回
                .ShapeData(_shaper, request.Fields)
                .PaginatedListFromEnumerable(request.PageNumber, request.PageSize)
            );
    }
}

对应修改Controller:

  • TodoItemController.cs
[HttpGet]
public async Task<ApiResponse<PaginatedList<ExpandoObject>>> GetTodoItemsWithCondition([FromQuery] GetTodoItemsWithConditionQuery query)
{
    return ApiResponse<PaginatedList<ExpandoObject>>.Success(await _mediator.Send(query));
}

验证

启动Api项目,执行查询TodoItem的请求:

  • 请求
    image

  • 响应
    image

我们再把之前讲到的过滤和搜索添加到请求里来:

  • 请求
    image

  • 响应
    image

总结

对于数据塑形的请求,关键步骤就是使用反射获取待返回对象的所有配置的可以返回的属性,再通过前端传入的属性名称进行过滤和值的重组进行返回。实现起来是比较简单的。但是在实际的使用过程中我不推荐这样用,除了某些非常适用的特殊场景。个人更偏向于向前端提供明确的接口定义。

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

版权声明:本文为YES开发框架网发布内容,转载请附上原文出处连接
管理员
上一篇:.NET Core 利用委托实现动态流程组装
下一篇:如何提升.NET控制台应用体验?
评论列表

发表评论

评论内容
昵称:
关联文章

使用.NET 6开发TodoList应用(17)——实现数据
使用.NET 6开发TodoList应用(16)——实现查询排序
使用.NET 6开发TodoList应用(15)——实现查询搜索
使用.NET 6开发TodoList应用(25)——实现RefreshToken
使用.NET 6开发TodoList应用(22)——实现缓存
使用.NET 6开发TodoList应用(12)——实现ActionFilter
使用.NET 6开发TodoList应用(9)——实现PUT请求
使用.NET 6开发TodoList应用(14)——实现查询过滤
使用.NET 6开发TodoList应用(6)——使用MediatR实现POST请求
使用.NET 6开发TodoList应用(7)——使用AutoMapper实现GET请求
使用.NET 6开发TodoList应用(30)——实现Docker打包和部署
使用.NET 6开发TodoList应用(13)——实现查询分页
使用.NET 6开发TodoList应用(填坑1)——实现当前登录用户获取
使用.NET 6开发TodoList应用(27)——实现API的Swagger文档化
使用.NET 6开发TodoList应用(24)——实现基于JWT的Identity功能
使用.NET 6开发TodoList应用(21)——实现API版本控制
使用.NET 6开发TodoList应用(28)——实现应用程序健康检查
使用.NET 6开发TodoList应用(31)——实现基于Github Actions和ACI的CI/CD
使用.NET 6开发TodoList应用(11)——使用FluentValidation和MediatR实现接口请求验证
使用.NET 6开发TodoList应用(23)——实现请求限流

联系我们
联系电话:15090125178(微信同号)
电子邮箱:garson_zhang@163.com
站长微信二维码
微信二维码