CsvHelper 和不可变类型

Dan*_*Dan 5 c# csv

我有一个不可变的类,我想写入和读取 CSV 文件。问题是我在读取 CSV 时遇到了一个异常,尽管已经映射了对象并设置了一个应该允许它工作的配置。

为此,我使用CsvHelper。不可变类如下所示。

public class ImmutableTest
{
    public Guid Id { get; }

    public string Name { get; }

    public ImmutableTest(string name) : this(Guid.NewGuid(), name)
    {
    }

    public ImmutableTest(Guid id, string name)
    {
        Id = id;
        Name = name;
    }
}
Run Code Online (Sandbox Code Playgroud)

我将此写入 CSV 文件没有问题,但是当我尝试从文件中读取它时,出现以下异常。

没有成员映射到类型“CsvTest.Program+ImmutableTest”

但是,我已经在下面的映射类中映射了此类的成员。

public sealed class ImmutableTestMap : ClassMap<ImmutableTest>
{
    public ImmutableTestMap()
    {
        Map(immutableTest => immutableTest.Id)
            .Index(0)
            .Name(nameof(ImmutableTest.Id).ToUpper());

        Map(immutableTest => immutableTest.Name)
            .Index(1)
            .Name(nameof(ImmutableTest.Name));
    }
}
Run Code Online (Sandbox Code Playgroud)

我还尝试使用以下配置将阅读器配置为使用构造函数来构建对象。

Configuration config = new Configuration
{
    IgnoreBlankLines = true
};

config.RegisterClassMap<ImmutableTestMap>();
config.ShouldUseConstructorParameters = type => true;
config.GetConstructor = type => type.GetConstructors()
    .MaxBy(constructor => constructor.GetParameters().Length)
    .FirstOrDefault();
Run Code Online (Sandbox Code Playgroud)

这一切似乎都没有奏效。我哪里错了?


完整的 MCVE .NET Framework 控制台示例

安装软件包

Install-Package CsvHelper
Install-Package morelinq
Run Code Online (Sandbox Code Playgroud)

示例控制台程序

using System;
using System.IO;
using CsvHelper;
using CsvHelper.Configuration;
using MoreLinq;

namespace CsvTest
{
    class Program
    {
        static void Main()
        {
            Configuration config = new Configuration
            {
                IgnoreBlankLines = true
            };

            config.RegisterClassMap<ImmutableTestMap>();
            config.ShouldUseConstructorParameters = type => true;
            config.GetConstructor = type => type.GetConstructors()
                .MaxBy(constructor => constructor.GetParameters().Length)
                .FirstOrDefault();

            const string filePath = "Test.csv";
            using (FileStream file = new FileStream(filePath, FileMode.Create))
            using (StreamWriter fileWriter = new StreamWriter(file))
            using (CsvSerializer csvSerializer = new CsvSerializer(fileWriter, config))
            using (CsvWriter csvWriter = new CsvWriter(csvSerializer))
            {
                csvWriter.WriteHeader<ImmutableTest>();
                csvWriter.NextRecord();
                csvWriter.WriteRecord(new ImmutableTest("Test 1"));
                csvWriter.NextRecord();
                csvWriter.WriteRecord(new ImmutableTest("Test 2"));
                csvWriter.NextRecord();
            }

            using (FileStream file = new FileStream(filePath, FileMode.Open))
            using (StreamReader fileReader = new StreamReader(file))
            using (CsvReader csvReader = new CsvReader(fileReader, config))
            {
                foreach (ImmutableTest record in csvReader.GetRecords<ImmutableTest>())
                {
                    Console.WriteLine(record.Id);
                    Console.WriteLine(record.Name);
                    Console.WriteLine();
                }
            }
        }

        public sealed class ImmutableTestMap : ClassMap<ImmutableTest>
        {
            public ImmutableTestMap()
            {
                Map(immutableTest => immutableTest.Id)
                    .Index(0)
                    .Name(nameof(ImmutableTest.Id).ToUpper());

                Map(immutableTest => immutableTest.Name)
                    .Index(1)
                    .Name(nameof(ImmutableTest.Name));
            }
        }

        public class ImmutableTest
        {
            public Guid Id { get; }

            public string Name { get; }

            public ImmutableTest(string name) : this(Guid.NewGuid(), name)
            {
            }

            public ImmutableTest(Guid id, string name)
            {
                Id = id;
                Name = name;
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Dan*_*Dan 0

ParameterMaps所以这里的问题是类映射中没有。

解决这个问题的简单方法,如上面的示例,是编写类似的内容

public ImmutableTestMap()
{
    AutoMap();

    Map(immutableTest => immutableTest.Id)
        .Index(0)
        .Name(nameof(ImmutableTest.Id).ToUpper());
}
Run Code Online (Sandbox Code Playgroud)

但这会导致一个问题,CSV文件中的列标题是IDName并且生成的参数映射是idname。它们彼此不相等,因此读取器会抛出错误,指出无法找到名为 的列ID。它还表示您可以将 header validated 设置为 null。在尝试了这个之后,我最终得到了配置

Configuration config = new Configuration
{
    IgnoreBlankLines = true,
    ShouldUseConstructorParameters = type => true,
    GetConstructor = type => type.GetConstructors()
        .MaxBy(constructor => constructor.GetParameters().Length)
        .FirstOrDefault(),
    HeaderValidated = null,
    MissingFieldFound = null
};

config.RegisterClassMap<ImmutableTestMap>();
Run Code Online (Sandbox Code Playgroud)

尝试解析空字段但无法将其转换为 GUID。所以看起来那条路已经死了。

为了解决这个问题,我检查了自动映射生成的每个参数映射的“id”值,然后将其替换为“ID”。但是,这也不起作用,因为自动生成的地图是使用空名称生成的。仅当在配置中注册地图期间调用 SetMapDefaults 时才会分配该名称。

因此,我进行了一个盲循环,将参数映射的名称设置为显式定义的成员映射。

public ImmutableTestMap()
{
    AutoMap();
    Map(immutableTest => immutableTest.Id)
        .Index(0)
        .Name(nameof(ImmutableTest.Id).ToUpper());

    Map(immutableTest => immutableTest.Name)
        .Index(0)
        .Name(nameof(ImmutableTest.Id));

    if (MemberMaps.Count != ParameterMaps.Count)
    {
        return;
    }

    for (int i = 0; i < MemberMaps.Count; i++)
    {
        ParameterMaps[i].Data.Name = MemberMaps[i].Data.Names[0];
    }
}
Run Code Online (Sandbox Code Playgroud)

然而,我想说这更像是一种困境,而不是解决办法,并且肯定会对其他答案持开放态度。