Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CsvHelper and immutable types

Tags:

c#

csv

I have an immutable class that I want to write to and read from a CSV file. The issue is I am getting an exception when reading the CSV despite having mapped the object and set up a configuration that should allow this to work.

To do this I am using CsvHelper. The immutable class looks like the following.

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;
    }
}

I have no issue writing this to a CSV file, but when I try to read it from a file, I get the following exception.

No members are mapped for type 'CsvTest.Program+ImmutableTest'

However, I have mapped the members for this class in the map class below.

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));
    }
}

I have also tried to configure the reader to use the constructor to build the object by using the following configuration.

Configuration config = new Configuration
{
    IgnoreBlankLines = true
};

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

No of this seems to be working. Where am I going wrong?


Complete MCVE .NET Framework Console Example

Install the packages

Install-Package CsvHelper
Install-Package morelinq

Sample console program

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;
            }
        }
    }
}
like image 635
Dan Avatar asked Nov 06 '22 15:11

Dan


1 Answers

If the type is immutable, it will use constructor mapping instead. Your constructor variable names need to match the header names. You can do this using Configuration.PrepareHeaderForMatch.

void Main()
{
    var s = new StringBuilder();
    s.AppendLine("Id,Name");
    s.AppendLine($"{Guid.NewGuid()},one");
    using (var reader = new StringReader(s.ToString()))
    using (var csv = new CsvReader(reader))
    {
        csv.Configuration.PrepareHeaderForMatch = (header, indexer) => header.ToLower();
        csv.GetRecords<ImmutableTest>().ToList().Dump();
    }
}

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;
    }
}
like image 144
Josh Close Avatar answered Nov 09 '22 23:11

Josh Close