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

如何使用CSVHelper动态写入动态类型属性至CSV文件?

问题描述

现有如下C#接口与实现类:

public interface IStateInformation
{
    public int TaskId { get; set; }
    public string Name { get; set; }
}

public class ExternalStateInformation : IStateInformation
{
    public int TaskId { get; set; }
    public string Name { get; set; }
    public string External { get; set; }
}

需要写入CSV的SwitchingData类定义如下:

public class SwitchingData
{
    public string DateTime { get; set; }
    public int EpisodeId { get; set; }
    public IStateInformation SourceState { get; set; }
    public IStateInformation TargetState { get; set; }
}

使用CSVHelper写入SwitchingData对象时,即便SourceState或TargetState的实际类型是ExternalStateInformation,也只会写入IStateInformation接口定义的TaskId和Name属性,导致External属性丢失。尝试通过ClassMap解决但未成功,写入代码如下:

...         
using (var stream = File.Open(path, FileMode.Append))
using (var writer = new StreamWriter(stream))
using (var csv = new CsvWriter(writer, config))
{
     if(type != null)
     {
         csv.Context.RegisterClassMap(type);
     }
                
     csv.Context.TypeConverterOptionsCache.AddOptions<DateTime>(options);
     csv.Context.TypeConverterOptionsCache.AddOptions<DateTime?>(options);
     csv.WriteRecords(data);
}
...

需求:实现写入对象实际类型的所有属性,而非静态接口定义的属性。


解决方案

方案1:自定义类型转换器

创建针对IStateInformation的自定义转换器,写入时自动识别对象实际类型,反射获取所有属性并格式化输出:

public class StateInformationConverter : DefaultTypeConverter
{
    public override string ConvertToString(object value, IWriterRow row, MemberMapData memberMapData)
    {
        if (value == null) return string.Empty;
        
        // 获取对象实际类型的所有公共实例属性
        var actualType = value.GetType();
        var properties = actualType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
        
        // 处理属性值,包含逗号或引号时用双引号包裹,同时转义内部双引号
        var formattedValues = properties.Select(p => 
        {
            var propValue = p.GetValue(value)?.ToString() ?? string.Empty;
            return propValue.Contains(',') || propValue.Contains('"') 
                ? $"\"{propValue.Replace("\"", "\"\"")}\"" 
                : propValue;
        });
        
        return string.Join(",", formattedValues);
    }
}

注册转换器后执行写入:

var config = new CsvConfiguration(CultureInfo.InvariantCulture);
// 为IStateInformation接口注册自定义转换器
config.TypeConverterCache.AddConverter<IStateInformation>(new StateInformationConverter());

using (var stream = File.Open(path, FileMode.Append))
using (var writer = new StreamWriter(stream))
using (var csv = new CsvWriter(writer, config))
{
    // 保留原有配置逻辑
    if(type != null)
    {
        csv.Context.RegisterClassMap(type);
    }
    
    csv.Context.TypeConverterOptionsCache.AddOptions<DateTime>(options);
    csv.Context.TypeConverterOptionsCache.AddOptions<DateTime?>(options);
    
    csv.WriteRecords(data);
}

方案2:ClassMap中动态判断类型

通过ClassMap的ConvertUsing方法,针对SourceState和TargetState字段动态判断实际类型,返回包含所有属性的匿名对象:

public class SwitchingDataMap : ClassMap<SwitchingData>
{
    public SwitchingDataMap()
    {
        // 映射基础字段
        Map(m => m.DateTime);
        Map(m => m.EpisodeId);
        
        // 处理SourceState字段
        Map(m => m.SourceState).ConvertUsing(row => 
        {
            var state = row.Source.SourceState;
            if (state is ExternalStateInformation externalState)
            {
                return new 
                {
                    externalState.TaskId,
                    externalState.Name,
                    externalState.External
                };
            }
            // 若有其他IStateInformation实现类,可在此添加对应处理逻辑
            return state;
        });
        
        // 处理TargetState字段,逻辑与SourceState一致
        Map(m => m.TargetState).ConvertUsing(row => 
        {
            var state = row.Source.TargetState;
            if (state is ExternalStateInformation externalState)
            {
                return new 
                {
                    externalState.TaskId,
                    externalState.Name,
                    externalState.External
                };
            }
            return state;
        });
    }
}

注册该Map后执行写入:

using (var stream = File.Open(path, FileMode.Append))
using (var writer = new StreamWriter(stream))
using (var csv = new CsvWriter(writer, config))
{
    csv.Context.RegisterClassMap<SwitchingDataMap>();
    
    // 保留原有配置逻辑
    csv.Context.TypeConverterOptionsCache.AddOptions<DateTime>(options);
    csv.Context.TypeConverterOptionsCache.AddOptions<DateTime?>(options);
    
    csv.WriteRecords(data);
}

方案3:自定义多态引用转换器

通过自定义ReferenceConverter,让CSVHelper自动识别接口字段的实际类型并映射所有属性:

// 自定义多态引用转换器
public class PolymorphicReferenceConverter : ReferenceConverter
{
    public override ITypeConverter GetConverter(Type type, IWriterRow row, MemberMapData memberMapData)
    {
        // 若字段类型是接口,获取对象实际类型并返回对应转换器
        if (type.IsInterface && memberMapData.Member.GetValue(row.Source) is object actualValue)
        {
            return row.Context.TypeConverterCache.GetConverter(actualValue.GetType());
        }
        return base.GetConverter(type, row, memberMapData);
    }
}

配置并使用该转换器:

var config = new CsvConfiguration(CultureInfo.InvariantCulture)
{
    // 指定自定义的多态引用转换器
    ReferenceConverter = new PolymorphicReferenceConverter()
};

using (var stream = File.Open(path, FileMode.Append))
using (var writer = new StreamWriter(stream))
using (var csv = new CsvWriter(writer, config))
{
    // 保留原有配置逻辑
    if(type != null)
    {
        csv.Context.RegisterClassMap(type);
    }
    
    csv.Context.TypeConverterOptionsCache.AddOptions<DateTime>(options);
    csv.Context.TypeConverterOptionsCache.AddOptions<DateTime?>(options);
    
    csv.WriteRecords(data);
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 10:47:08