CsvHelper icon indicating copy to clipboard operation
CsvHelper copied to clipboard

Nested objects list is not working for GetRecords

Open LasVegasIs opened this issue 3 years ago • 3 comments

Describe the bug Nested objects list is not working for GetRecords error: Property 'System.String Id' is not defined for type 'System.Collections.Generic.List`1[csvHelperTest.Program+B]' (Parameter 'property')

To Reproduce run this code:

using System.IO;
using System.Linq;
using CsvHelper.Configuration;
using CsvHelper;
using System.Globalization;
using System.Collections.Generic;

namespace csvHelperTest
{
    internal class Program
    {
        static void Main(string[] args)
        {
            using (var stream = new MemoryStream())
            using (var reader = new StreamReader(stream))
            using (var writer = new StreamWriter(stream))
            using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
            {
                csv.Context.RegisterClassMap<AMap>();
                csv.Context.RegisterClassMap<BMap>();

                List<A> listA = new List<A>();
                A a1 = new A() { Id = "a1" };
                a1.B = new List<B>();
                a1.B.Add(new B() { Id = "b1" });
                listA.Add(a1);

                writer.WriteLine("AId,BId");

                foreach (var a in listA)
                {
                    foreach (var b in a.B)
                    {
                        writer.WriteLine(a);
                        writer.WriteLine(b);
                    }
                }
                writer.Flush();
                stream.Position = 0;

                var list = csv.GetRecords<A>().ToList();


                for (var i = 0; i < 4; i++)
                {
                    var rowId = i + 1;
                    var row = list[i];
                    System.Console.WriteLine(row);
                }

                System.Console.ReadLine();
            }
        }

        private class A
        {
            public string Id { get; set; }

            public List<B> B { get; set; }
        }

        private class B
        {
            public string Id { get; set; }
        }

        private sealed class AMap : ClassMap<A>
        {
            public AMap()
            {
                Map(m => m.Id).Name("AId");
                References<BMap>(m => m.B);
            }
        }

        private sealed class BMap : ClassMap<B>
        {
            public BMap()
            {
                Map(m => m.Id).Name("BId");
            }
        }
    }
}

Expected behavior GetRecords without errors I will see the filled object with nested list

Additional context I saw the similar issue there and I think it is still not working.

LasVegasIs avatar Aug 16 '22 16:08 LasVegasIs

A year later, Is this still not working? I am also unable to serialize a list navigation property when calling GetRecords. Same exception is thrown. Thanks.

pagner avatar Jul 30 '23 22:07 pagner

Unfortunately, you are not able to use nested lists of objects. CSV does not lend itself to flattening lists of objects. @LasVegasIs example is way too simple. It gets much more complicated when you have classes with more properties and multiple levels of nesting.

You can have an A class with a List of primitives like List<int> or List<string>.

private class A
{
	public string Id { get; set; }

	public List<string> B { get; set; }
}

private sealed class AMap : ClassMap<A>
{
	public AMap()
	{
		Map(m => m.Id).Name("AId");
		Map(m => m.B).Index(1);
	}
}

void Main()
{
	var input = @"AId,B1,B2,B3
a1,b1,b2,b3
a2,b4,b5,b6";
	using var reader = new StringReader(input);
	using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);
	csv.Context.RegisterClassMap<AMap>();
	var records = csv.GetRecords<A>().ToList();
	records.Dump();
}

You can have a single B class in your A class

private class A
{
	public string Id { get; set; }

	public B B { get; set; }
}

private class B 
{
	public string Id { get; set; }
	public string Name { get; set; }
}

private sealed class AMap : ClassMap<A>
{
	public AMap()
	{
		Map(m => m.Id).Name("A_Id");
		References<BMap>(m => m.B);
	}
}

private sealed class BMap : ClassMap<B>
{
	public BMap()
	{
		Map(m => m.Id).Name("B_Id");
		Map(m => m.Name).Name("B_Name");
	}
}

void Main()
{
	var input = @"A_Id,B_Id,B_Name
a1,b1,Bee 1
a2,b2,Bee 2";
	using var reader = new StringReader(input);
	using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);
	csv.Context.RegisterClassMap<AMap>();
	var records = csv.GetRecords<A>().ToList();
	records.Dump();
}

If you know that B is simple and there is a set number of B, you could manually read List<B>.

private class A
{
	public string Id { get; set; }	
	public string Name { get; set; }

	public List<B> B { get; set; }
}

private class B 
{
	public string Id { get; set; }
	public string Name { get; set; }
}

private sealed class AMap : ClassMap<A>
{
	public AMap()
	{
		Map(m => m.Id).Name("A_Id");
		Map(m => m.Name).Name("A_Name");
	}
}

void Main()
{
	var input = @"A_Id,A_Name,B_Id1,B_Name1,B_Id2,B_Name2
a1,Alpha 1,b1,Bee 1,b2,Bee 2
a2,Alpha 2,b3,Bee 3,b4,Bee 4";
	using var reader = new StringReader(input);
	using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);
	csv.Context.RegisterClassMap<AMap>();
	
	csv.Read();
	csv.ReadHeader();
	var records = new List<A>();
	
	while (csv.Read())
	{
		var record = csv.GetRecord<A>();
		record.B = new List<B>
		{
			new B {
				Id = csv.GetField("B_Id1"),
				Name = csv.GetField("B_Name1")
			},
			new B {
				Id = csv.GetField("B_Id2"),
				Name = csv.GetField("B_Name2")
			}
		};
		
		records.Add(record);
	}
	
	records.Dump();
}

AltruCoder avatar Aug 01 '23 18:08 AltruCoder

Thanks @AltruCoder, I will give this a try. However in my case the main type A isn't known at compile time. I am using reflection to invoke the generic method that kicks off the read. Type B in my case is a constant from a base class. Your example is a good starting point for me though.

pagner avatar Aug 01 '23 22:08 pagner