You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何修剪Dapper查询返回复杂对象中的所有字符串(遗留数据库场景)

解决Dapper查询自动修剪字符串末尾空格的问题

你遇到的核心问题在于:扩展方法命名不匹配原生调用、dynamic类型无法通过常规反射处理,以及现有代码未递归处理嵌套实体的字符串属性。下面一步步帮你解决:

一、修正你的Dapper扩展方法

先把扩展方法名改成你实际调用的QueryTrim,同时处理dynamic(本质是ExpandoObject)和嵌套实体的情况:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Dynamic;
using System.Reflection;
using Dapper;

public static class DapperExtensions
{
    public static IEnumerable<T> QueryTrim<T>(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, bool buffered = true, int? commandTimeout = null, CommandType? commandType = null)
    {
        var dapperResult = SqlMapper.Query<T>(cnn, sql, param, transaction, buffered, commandTimeout, commandType);
        TrimAllStrings(dapperResult);
        return dapperResult;
    }

    private static void TrimAllStrings<T>(IEnumerable<T> items)
    {
        foreach (var item in items)
        {
            if (item == null) continue;
            
            // 处理dynamic/ExpandoObject类型
            if (item is ExpandoObject expando)
            {
                TrimExpandoObject(expando);
            }
            else
            {
                // 处理普通实体类,递归遍历所有属性
                TrimObjectProperties(item);
            }
        }
    }

    private static void TrimExpandoObject(ExpandoObject expando)
    {
        var dict = (IDictionary<string, object>)expando;
        var keys = new List<string>(dict.Keys);
        
        foreach (var key in keys)
        {
            var value = dict[key];
            if (value is string strValue)
            {
                dict[key] = strValue.SafeTrim();
            }
            // 递归处理嵌套的ExpandoObject
            else if (value is ExpandoObject nestedExpando)
            {
                TrimExpandoObject(nestedExpando);
            }
            // 处理嵌套集合
            else if (value is IEnumerable enumerable && !(value is string))
            {
                foreach (var nestedItem in enumerable)
                {
                    if (nestedItem is ExpandoObject nested)
                        TrimExpandoObject(nested);
                    else
                        TrimObjectProperties(nestedItem);
                }
            }
            else
            {
                TrimObjectProperties(value);
            }
        }
    }

    private static void TrimObjectProperties(object obj)
    {
        if (obj == null) return;
        
        var type = obj.GetType();
        // 修剪当前对象的字符串属性
        var stringProperties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
            .Where(p => p.PropertyType == typeof(string) && p.CanRead && p.CanWrite);
        
        foreach (var prop in stringProperties)
        {
            var value = (string)prop.GetValue(obj);
            prop.SetValue(obj, value.SafeTrim());
        }

        // 递归处理嵌套对象和集合属性
        var nestedProperties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
            .Where(p => !p.PropertyType.IsValueType && p.PropertyType != typeof(string) && p.CanRead);
        
        foreach (var prop in nestedProperties)
        {
            var value = prop.GetValue(obj);
            if (value is IEnumerable enumerable && !(value is string))
            {
                foreach (var item in enumerable)
                {
                    TrimObjectProperties(item);
                }
            }
            else
            {
                TrimObjectProperties(value);
            }
        }
    }

    private static string SafeTrim(this string source)
    {
        return source?.Trim() ?? source;
    }
}

修正点说明:

  1. 命名匹配:把方法名改成QueryTrim,和你调用的_db.QueryTrim<dynamic>(...)对应,避免和Dapper原生Query方法冲突。
  2. dynamic类型处理:dynamic返回的是ExpandoObject,需转换成字典来遍历修改属性值。
  3. 递归嵌套处理:不管是Person里的Address这类嵌套对象,还是Payments集合,都会递归处理其中的字符串属性。
  4. 原地修改:直接修改原对象属性,避免额外内存开销。

二、其他更简洁的替代方案

如果不想写复杂的扩展方法,这些方案更省心:

1. 全局注册Dapper字符串类型处理器

一次性注册,所有Dapper查询的字符串字段都会自动修剪:

public class TrimmedStringTypeHandler : SqlMapper.TypeHandler<string>
{
    public override void SetValue(IDbDataParameter parameter, string value)
    {
        parameter.Value = value?.Trim() ?? value;
    }

    public override string Parse(object value)
    {
        return value?.ToString().Trim() ?? null;
    }
}

// 程序启动时注册(比如Program.cs或Startup.cs)
SqlMapper.AddTypeHandler(new TrimmedStringTypeHandler());

优点是一劳永逸,无需修改任何查询代码。

2. 在实体属性Setter中自动修剪

针对特定实体属性做修剪,灵活可控:

public class Address : BaseEntity
{
    private string _street;
    public string Street
    {
        get => _street;
        set => _street = value.SafeTrim();
    }

    private string _boxNumber;
    public string BoxNumber
    {
        get => _boxNumber;
        set => _boxNumber = value.SafeTrim();
    }

    // 其他属性同理...
}

public static class StringExtensions
{
    public static string SafeTrim(this string source)
    {
        return source?.Trim() ?? source;
    }
}

3. 结合AutoMapper做修剪

如果已经用AutoMapper映射实体,可以全局配置字符串修剪规则:

var config = new MapperConfiguration(cfg =>
{
    // 全局规则:所有字符串映射自动修剪
    cfg.ForAllMaps((map, exp) =>
    {
        exp.ForAllMembers(opt =>
        {
            if (opt.DestinationMember.PropertyType == typeof(string))
            {
                opt.MapFrom(src => ((string)opt.SourceMember.GetValue(src)).SafeTrim());
            }
        });
    });

    // 你的实体映射规则
    cfg.CreateMap<dynamic, Identification>().ConvertUsing(IdentificationMapper.MapToIdentificationEntity);
    cfg.CreateMap<dynamic, Address>().ConvertUsing(AddressMapper.MapToAddressEntity);
});

内容的提问来源于stack exchange,提问作者Gilles Vercammen

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.12 03:56:53