在C#数据处理场景中,行转列是常见需求,比如将多行用户属性记录转换为按属性分列的表结构。下面我们来看具体的通用实现方式。

行转列需求分析
假设我们有一个用户实体类,包含用户ID、属性名、属性值三个字段,原有的List数据是按行存储的,每行对应一个用户的一个属性,现在需要转换为每行对应一个用户,每个属性作为单独的列。
比如原始数据结构如下:
| UserId | PropertyName | PropertyValue |
|---|---|---|
| 1 | Name | 张三 |
| 1 | Age | 20 |
| 2 | Name | 李四 |
| 2 | Age | 22 |
转换后需要的表结构为:
| UserId | Name | Age |
|---|---|---|
| 1 | 张三 | 20 |
| 2 | 李四 | 22 |
通用实现思路
实现通用行转列方法需要解决几个核心问题:
- 如何动态识别需要作为列的属性名,这里我们可以通过传入的属性名列和属性值列来指定
- 如何根据分组键(比如UserId)对原始数据进行分组
- 如何动态创建转换后的结果对象,并赋值对应的属性
我们使用泛型和反射来实现通用逻辑,这样不管实体类型是什么,都可以复用同一套转换方法。
完整代码示例
首先定义原始数据实体类:
public class SourceData
{
public int UserId { get; set; }
public string PropertyName { get; set; }
public string PropertyValue { get; set; }
}然后定义行转列通用方法:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
public static class ListRowToColumnHelper
{
/// <summary>
/// List行转列通用方法
/// </summary>
/// <typeparam name="TSource">原始数据类型</typeparam>
/// <typeparam name="TResult">转换后结果类型</typeparam>
/// <param name="sourceList">原始数据集合</param>
/// <param name="groupKeySelector">分组键选择委托</param>
/// <param name="propertyNameSelector">属性名选择委托</param>
/// <param name="propertyValueSelector">属性值选择委托</param>
/// <returns>转换后的结果集合</returns>
public static List<TResult> RowToColumn<TSource, TResult>(
List<TSource> sourceList,
Func<TSource, object> groupKeySelector,
Func<TSource, string> propertyNameSelector,
Func<TSource, object> propertyValueSelector)
{
// 按分组键分组
var groups = sourceList.GroupBy(groupKeySelector).ToList();
// 获取结果类型的所有属性
var resultProperties = typeof(TResult).GetProperties(BindingFlags.Public | BindingFlags.Instance);
List<TResult> resultList = new List<TResult>();
foreach (var group in groups)
{
// 创建结果对象实例
TResult resultObj = Activator.CreateInstance<TResult>();
// 先设置分组键对应的属性,假设结果类型有和分组键同名的属性
var groupKeyProp = resultProperties.FirstOrDefault(p => p.Name == "UserId");
if (groupKeyProp != null && groupKeyProp.CanWrite)
{
// 类型转换,这里简单处理,实际使用时可以根据需要完善类型转换逻辑
var groupKeyValue = Convert.ChangeType(group.Key, groupKeyProp.PropertyType);
groupKeyProp.SetValue(resultObj, groupKeyValue);
}
// 遍历分组内的所有数据,设置对应的属性值
foreach (var item in group)
{
string propertyName = propertyNameSelector(item);
object propertyValue = propertyValueSelector(item);
var prop = resultProperties.FirstOrDefault(p => p.Name == propertyName);
if (prop != null && prop.CanWrite)
{
// 类型转换,处理属性值类型和目标属性类型不匹配的情况
try
{
var typedValue = Convert.ChangeType(propertyValue, prop.PropertyType);
prop.SetValue(resultObj, typedValue);
}
catch
{
// 转换失败可以记录日志或者设置默认值,这里简单跳过
}
}
}
resultList.Add(resultObj);
}
return resultList;
}
}接着定义转换后的结果实体类:
public class TargetData
{
public int UserId { get; set; }
public string Name { get; set; }
public string Age { get; set; }
}最后是使用方法的示例:
class Program
{
static void Main(string[] args)
{
// 准备原始数据
List<SourceData> sourceList = new List<SourceData>
{
new SourceData { UserId = 1, PropertyName = "Name", PropertyValue = "张三" },
new SourceData { UserId = 1, PropertyName = "Age", PropertyValue = "20" },
new SourceData { UserId = 2, PropertyName = "Name", PropertyValue = "李四" },
new SourceData { UserId = 2, PropertyName = "Age", PropertyValue = "22" }
};
// 调用行转列方法
List<TargetData> resultList = ListRowToColumnHelper.RowToColumn<SourceData, TargetData>(
sourceList,
item => item.UserId,
item => item.PropertyName,
item => item.PropertyValue
);
// 输出结果验证
foreach (var item in resultList)
{
Console.WriteLine($"UserId:{item.UserId}, Name:{item.Name}, Age:{item.Age}");
}
}
}注意事项
这个通用方法目前做了简单的类型转换,实际使用中如果遇到复杂的类型转换场景,可以根据需求扩展类型转换的逻辑。另外如果结果类型的属性名和原始数据的属性名不是完全匹配,也可以传入额外的映射关系来处理,让方法更灵活。
通过这种泛型加反射的方式,我们只需要维护这一个通用方法,就可以适配各种不同实体的行转列需求,不需要每次遇到新的实体都重新写一套转换逻辑,大大提升了开发效率。