CsvHelper icon indicating copy to clipboard operation
CsvHelper copied to clipboard

DynamicObject WriteDynamicHeader issue

Open ghost opened this issue 3 months ago • 0 comments

Describe the bug CsvWriter.WriteRecords doesn't call WriteDynamicHeader

To Reproduce

using CsvHelper;
using CsvHelper.Configuration;
using System.Dynamic;
using System.Globalization;
using System.Text;

namespace DynCsv
{
    public class DynamicCsvRecord : DynamicObject
    {
        public string FirstFixedProp { get; set; }
        public string SecondFixedProp { get; set; }

        public override DynamicMetaObject GetMetaObject(System.Linq.Expressions.Expression parameter)
        {
            return base.GetMetaObject(parameter);
        }

        private Dictionary<string, object?> _Dict = new();

        public bool TryAdd(string key, object? value) => _Dict.TryAdd(key, value);

        public override IEnumerable<string> GetDynamicMemberNames()
        {
            List<string> names = new();

            foreach (var prop in GetType().GetProperties())
            {
                names.Add(prop.Name);
            }

            foreach (var key in _Dict.Keys)
            {
                names.Add(key);
            }

            return names;
        }

        public override bool TrySetMember(SetMemberBinder binder, object? value) => _Dict.TryAdd(binder.Name, value);

        public override bool TryGetMember(GetMemberBinder binder, out object? result)
        {
            result = _Dict[binder.Name];
            return true;
        }

    }

    public class Program
    {
        static void Main(string[] args)
        {
            List<DynamicCsvRecord> records = new();

            var rec = new DynamicCsvRecord();
            rec.FirstFixedProp = "1st Fixed Value";
            rec.SecondFixedProp = "2nd Fixed Value";
            rec.TryAdd("FirstDynamicProp", "1st Dynamic Value");
            rec.TryAdd("SecondDynamicProp", "2nd Dynamic Value");

            records.Add(rec);

            var config = new CsvConfiguration(CultureInfo.InvariantCulture)
            {
                HasHeaderRecord = true,
            };

            using (var stream = File.Open("Out.csv", FileMode.Create))
            using (var writer = new StreamWriter(stream, Encoding.UTF8))
            using (var csv = new CsvWriter(writer, config))
            {
                if (records?.ElementAtOrDefault(0) is IDynamicMetaObjectProvider provider)
                {
                    // unless I explicitly call this method, dynamic column names won't be written
                    csv.WriteDynamicHeader(provider);
                    csv.NextRecord();
                }

                csv.WriteRecords(records);
            }
        }
    }
}

Expected behavior WriteDynamicHeader should be called from WriteRecords

Screenshots

Image

Additional context I think the problem is in CsvWriter.cs at line 415-425. At line 425 WriteHeaderFromRecord(enumerator.Current) would call the WriteDynamicHeader(dynamicObject) but at line 415 WriteHeaderFromType<T>() writes the fixed members header and sets hasHeaderBeenWritten true, so WriteDynamicHeader won't be called from WriteHeaderFromRecord

ghost avatar Sep 06 '25 20:09 ghost