JoshClose / CsvHelper

Library to help reading and writing CSV files
http://joshclose.github.io/CsvHelper/
Other
4.79k stars 1.07k forks source link

Write of simple List<dynamic> throws TypeConverterException #2024

Open chriswill opened 2 years ago

chriswill commented 2 years ago

I'm following the examples for writing a CSV using a list of dynamic by following the steps here. I'm executing my code in a net 6.0 Web API controller.

My model is extremely simple, as shown in the JSON representation below.

[
     {"department":"Engineering","itemCount":1}
]

I don't know the schema of this model in advance.

Whenever my code gets to the csv.WriteRecords method it throws a CsvHelper.TypeConversion.TypeConverterException.

Converting IEnumerable types is not supported for a single field. If you want to do this, create your own ITypeConverter and register it in the TypeConverterFactory by calling AddConverter.
IWriter state:
   Row: 2
   Index: 0
   HeaderRecord:
2

   at CsvHelper.CsvWriter.<WriteRecordsAsync>d__63`1.MoveNext()
   at CloudScope.Concierge.Api.Controllers.api.ReportController.<ExportToCsv>d__8.MoveNext() in C:\src\CloudScope.Concierge\source\CloudScope.Concierge.Api\Controllers\api\ReportController.cs:line 662

The only time I've found this error message in past issues is #455, but it doesn't seem related to my model.

JoshClose commented 2 years ago

If one of the properties is an IEnumerable such as a List<T> or Array, you'll get this exception.

chriswill commented 2 years ago

When you query against Azure Cosmos db you get a List in return, and the dynamic objects are actually Newtonsoft.Json.Linq.JObject that you get by deserializing the returned json using the Cosmos client library.

None of the returned properties are actually IEnumerable, but it seems the CsvHelper lib gets confused by the JToken that is the value part of the returned JObject.

I'll likely end up writing the ITypeConverter since there are only two required methods and handling this should be easy given that I don't return any actual IEnumerables in the model, just the JTokens that inherit from them.

Where do you access the TypeConverterFactory to add in this ITypeConverter?

chriswill commented 2 years ago

The TypeConverterCache mentioned in #1064 does not seem to be available in the instance of CsvConfiguration now and the example in #1838 requires a pre-defined class.

chriswill commented 2 years ago

Just to help anyone who visits this post in the future, an example of the failing code is:

                        var records = new List<dynamic>();
            JObject record = JObject.Parse("{\"department\":\"Engineering\",\"itemCount\":1}");
            records.Add(record);

            using (var writer = new StringWriter())
            using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
            {
                csv.WriteRecords(records);

                var s = writer.ToString();
            }

Until this is resolved in the main release, you have to create a JValueTypeConverter that implements ITypeConverter.

You can then register it with the TypeConverterCache like this:

                        using (var csv = new CsvWriter(writer, cultureInfo))
                        {
                            try
                            {
                                csv.Context.TypeConverterCache.AddConverter<JValue>(new JTypeTypeConverter());
                                await csv.WriteRecordsAsync(entities);
                                await csv.FlushAsync();
                            }
                            catch (Exception e)
                            {
                                Console.WriteLine(e);
                                throw;
                            }

                        }
Anu666 commented 1 year ago

Hi @chriswill , Thank you for raising this issue and the workaround you provided. I have been banging my head to find what was wrong with my code. The conversation above helps a lot.

I am new to using the CsvHelper library. Could you please provide some idea into how this JValueTypeConverter needs to be implemented. I would help me greatly. Thanks.

chriswill commented 1 year ago

@Anu666

This is the implementation in my controller:

using (MemoryStream stream = new MemoryStream())
                {
                    using (StreamWriter writer = new StreamWriter(stream))
                    {
                        using (var csv = new CsvWriter(writer, cultureInfo))
                        {
                            csv.Context.TypeConverterCache.AddConverter<JValue>(new JValueTypeConverter());
                            await csv.WriteRecordsAsync(entities);
                            await writer.FlushAsync();
                            stream.Seek(0, SeekOrigin.Begin);
                            bytes = stream.ToArray();
                        }
                    }
                }

                return File(bytes, "text/csv", fileName);

and the JValueTypeConverter class looks like this:

public class JValueTypeConverter : ITypeConverter
    {
        public object ConvertFromString(string? text, IReaderRow row, MemberMapData memberMapData)
        {
            throw new NotImplementedException();
        }

        public string ConvertToString(object? value, IWriterRow row, MemberMapData memberMapData)
        {
            if (value == null)
            {
                if (memberMapData.TypeConverterOptions.NullValues.Count > 0)
                {
                    return memberMapData.TypeConverterOptions.NullValues.First();
                }

                return string.Empty;
            }

            if (value is IFormattable formattable)
            {
                string? format = memberMapData.TypeConverterOptions.Formats?.FirstOrDefault();
                return formattable.ToString(format, memberMapData.TypeConverterOptions.CultureInfo);
            }

#pragma warning disable CS8603 // Possible null reference return.
            return value.ToString();
#pragma warning restore CS8603 // Possible null reference return.
        }
    }

The #pragma notations are to disable Resharper warnings.

Anu666 commented 1 year ago

I have utilized the DefaultTypeConverter class to bypass the issue for the time being. I see that your implementation of the ITypeConverter is similar to the DefaultTypeConverter class, but I believe your approach would be better to add any custom handling of the values in the future.

csv.Context.TypeConverterCache.AddConverter<JValue>(new DefaultTypeConverter());

Thank you for taking out some time to help me with this. @chriswill