From cd2d18107c8e3c5e0607cc4b18e968a38618981a Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 23 May 2026 16:32:04 -0300 Subject: [PATCH 1/4] adds impl & tests --- RecordParser.Test/FileReaderTest.cs | 18 ++-- RecordParser.Test/FileWriterTest.cs | 55 ++++++++++- .../Extensions/FileWriter/WriterExtensions.cs | 96 ++++++++++++++++++- 3 files changed, 158 insertions(+), 11 deletions(-) diff --git a/RecordParser.Test/FileReaderTest.cs b/RecordParser.Test/FileReaderTest.cs index d6e78fe..c4404df 100644 --- a/RecordParser.Test/FileReaderTest.cs +++ b/RecordParser.Test/FileReaderTest.cs @@ -39,7 +39,7 @@ public void Given_record_is_too_large_for_default_buffer_size_then_exception_sho // construct a CSV with a header row // and a *single* data row, where the 4th column contains a large inlined, CSV file enclosed in quotes. // this is an extreme case, but is a valid CSV according to the spec. - var tw = new StringWriter(); + using var tw = new StringWriter(); tw.WriteLine("A,B,C,D"); tw.Write("1,2,3,\""); @@ -51,7 +51,7 @@ public void Given_record_is_too_large_for_default_buffer_size_then_exception_sho tw.WriteLine("\""); var fileContent = tw.ToString(); - var reader = new StringReader(fileContent); + using var reader = new StringReader(fileContent); // Act @@ -138,7 +138,7 @@ public void Given_record_is_too_large_for_custom_buffer_size_then_exception_shou (88,89,90,91), }; - var reader = new StringReader(fileContent); + using var reader = new StringReader(fileContent); var parser = new VariableLengthReaderSequentialBuilder<(int A, int B, int C, int D)>() .Map(x => x.A) @@ -639,7 +639,7 @@ public void Read_csv_file_with_autobind_should_support_additional_configuration( 88,89,90,91 """; - var reader = new StringReader(fileContent); + using var reader = new StringReader(fileContent); var expected = new RegularCaseRecord[] { new(10,20,30,40), @@ -679,7 +679,7 @@ public void Read_csv_file_with_autobind_should_support_nested_fields() 2020.05.23 ; son name ; 1980.01.15 ; mother name """; - var reader = new StringReader(fileContent); + using var reader = new StringReader(fileContent); var expected = new Person[] { new Person @@ -721,7 +721,7 @@ public void Read_csv_file_with_autobind_should_support_inherited_properties() 99 ; 2020.05.23 ; son name ; 100 ; 1980.01.15 ; mother name """; - var reader = new StringReader(fileContent); + using var reader = new StringReader(fileContent); var expected = new PersonDerivated[] { new () @@ -769,7 +769,7 @@ public void Read_csv_file_with_autobind_should_throw_when_missing_header(string 99 ; 2020.05.23 ; son name ; 100 ; 1980.01.15 ; mother name """; - var reader = new StringReader(fileContent); + using var reader = new StringReader(fileContent); var expected = new Person[] { new () @@ -824,7 +824,7 @@ public void Read_csv_file_with_autobind_should_detect_common_separators(string s """ .Replace(",", separator); - var reader = new StringReader(fileContent); + using var reader = new StringReader(fileContent); var expected = new RegularCaseRecord[] { new(1,2,3,4), @@ -888,7 +888,7 @@ public void Read_csv_file_with_autobind_should_support_explict_separators(string """ .Replace(",", fileSeparator); - var reader = new StringReader(fileContent); + using var reader = new StringReader(fileContent); var expected = new RegularCaseRecord[] { new(1,2,3,4), diff --git a/RecordParser.Test/FileWriterTest.cs b/RecordParser.Test/FileWriterTest.cs index 408176c..53a4274 100644 --- a/RecordParser.Test/FileWriterTest.cs +++ b/RecordParser.Test/FileWriterTest.cs @@ -9,6 +9,7 @@ using System.IO; using System.Linq; using Xunit; +using static RecordParser.Test.FileReaderTest; namespace RecordParser.Test { @@ -95,6 +96,58 @@ public void Write_csv_file(int repeat, bool parallel, bool ordered) items.Should().BeEquivalentTo(expectedItems, cfg => cfg.WithStrictOrdering()); else items.Should().BeEquivalentTo(expectedItems); - } + } + + [Fact] + public void Write_csv_file_with_autobind_should_support_regular_properties() + { + // Arrange + + var items = new RegularCaseRecord[] + { + new(1,2,3,4), + new(5,6,7,8), + new(9,10,11,12), + new(13,14,15,16), + new(87,88,89,100), + new(89,99,100,101), + new(88,89,90,91), + }; + + var expected = $""" + Aaa;Bbb;Ccc;Ddd + 1;2;3;4 + 5;6;7;8 + 9;10;11;12 + 13;14;15;16 + 87;88;89;100 + 89;99;100;101 + 88;89;90;91 + + """; + + // Act + + using var memory = new MemoryStream(); + using var textWriter = new StreamWriter(memory); + + var parallelOptions = new ParallelismOptions() + { + Enabled = false, + EnsureOriginalOrdering = false, + MaxDegreeOfParallelism = MaxParallelism, + }; + + textWriter.WriteRecords(items, parallelOptions); + textWriter.Flush(); + + // Assert + + memory.Seek(0, SeekOrigin.Begin); + using var textReader = new StreamReader(memory); + var content = textReader.ReadToEnd(); + + content.Should().Be(expected); + } } } \ No newline at end of file diff --git a/RecordParser/Extensions/FileWriter/WriterExtensions.cs b/RecordParser/Extensions/FileWriter/WriterExtensions.cs index 04f8345..f0434e4 100644 --- a/RecordParser/Extensions/FileWriter/WriterExtensions.cs +++ b/RecordParser/Extensions/FileWriter/WriterExtensions.cs @@ -1,8 +1,11 @@ -using System; +using RecordParser.Builders.Writer; +using System; using System.Buffers; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Linq.Expressions; +using System.Reflection; namespace RecordParser.Extensions { @@ -118,5 +121,96 @@ private static void WriteSequential(TextWriter textWriter, IEnumerable ite ArrayPool.Shared.Return(buffer); } } + + private class NodeState + { + public Type Type { get; set; } + public Expression CurrentExpression { get; set; } + public int Depth { get; set; } + } + + /// + /// Writes the elements of a sequence into the as well the header of file. + /// + /// Type of items in the sequence. + /// The TextWriter where the items will be written into. + /// Sequence of the elements. + /// Options to configure parallel processing. + public static void WriteRecords(this TextWriter textWriter, IEnumerable items, ParallelismOptions options) + { + const string separator = ";"; + var members = GetPropertyExpressions(typeof(T), 64); + var builder = new VariableLengthWriterSequentialBuilder(); + + foreach (dynamic item in members.Select(x => x.exp)) + builder.Map(item); + + var parser = builder.Build(separator); + var header = string.Join(separator, members.Select(x => x.column)); + + textWriter.WriteLine(header); + WriteRecords(textWriter, items, parser.TryFormat, options); + } + + private static IReadOnlyList<(LambdaExpression exp, string column)> GetPropertyExpressions(Type type, int maxDepth) + { + var expressions = new List<(LambdaExpression, string)>(); + + if (maxDepth < 1) + return expressions; + + var paramText = Guid.NewGuid().ToString(); + var rootParameter = Expression.Parameter(type, paramText); + + var queue = new Queue(); + + queue.Enqueue(new NodeState + { + Type = type, + CurrentExpression = rootParameter, + Depth = 1 + }); + + // loop BFS + while (queue.Count > 0) + { + var currentState = queue.Dequeue(); + + if (currentState.Depth > maxDepth) + continue; + + var properties = currentState.Type.GetProperties(BindingFlags.Public | BindingFlags.Instance); + + foreach (var prop in properties) + { + if (prop.CanRead == false) + continue; + + var propertyAccess = Expression.Property(currentState.CurrentExpression, prop); + var pType = prop.PropertyType; + + if (pType.IsPrimitive || pType.IsEnum + || pType == typeof(string) + || pType == typeof(decimal) + || pType == typeof(DateTime)) + { + var lambda = Expression.Lambda(propertyAccess, rootParameter); + var column = propertyAccess.ToString().Replace(paramText + ".", string.Empty); + expressions.Add((lambda, column)); + } + else + { + queue.Enqueue(new NodeState + { + Type = prop.PropertyType, + CurrentExpression = propertyAccess, + Depth = currentState.Depth + 1 + }); + } + } + } + + return expressions; + } } } From 336d6f80446ce35d0216e2970784760e981987fd Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 4 Jun 2026 16:17:21 -0300 Subject: [PATCH 2/4] adds nested properties --- RecordParser.Test/FileWriterTest.cs | 51 +++++++++++++++++++ .../Reader/PrimitiveTypeReaderEngine.cs | 3 ++ .../Extensions/FileWriter/WriterExtensions.cs | 15 +++--- 3 files changed, 62 insertions(+), 7 deletions(-) diff --git a/RecordParser.Test/FileWriterTest.cs b/RecordParser.Test/FileWriterTest.cs index 53a4274..808881b 100644 --- a/RecordParser.Test/FileWriterTest.cs +++ b/RecordParser.Test/FileWriterTest.cs @@ -149,5 +149,56 @@ public void Write_csv_file_with_autobind_should_support_regular_properties() content.Should().Be(expected); } + + [Fact] + public void Write_csv_file_with_autobind_should_support_nested_properties() + { + // Arrange + + var items = new Person[] + { + new("Bob",22,new("CPF", "123")), + new("Carla",26, new("Other", "ABC")), + }; + + var expected = $""" + Name;Age;Document.Type;Document.Value + Bob;22;CPF;123 + Carla;26;Other;ABC + + """; + + // Act + + using var memory = new MemoryStream(); + using var textWriter = new StreamWriter(memory); + + var parallelOptions = new ParallelismOptions() + { + Enabled = false, + EnsureOriginalOrdering = false, + MaxDegreeOfParallelism = MaxParallelism, + }; + + textWriter.WriteRecords(items, parallelOptions); + textWriter.Flush(); + + // Assert + + memory.Seek(0, SeekOrigin.Begin); + using var textReader = new StreamReader(memory); + var content = textReader.ReadToEnd(); + + content.Should().Be(expected); + + memory.Seek(0, SeekOrigin.Begin); + var opt = new VariableLengthReaderAutoBindOptions() { HasHeader = true }; + var readItems = textReader.ReadRecords(opt); + + readItems.Should().BeEquivalentTo(items); + } + + public record class Person(string Name, int Age, Documento Document); + public record class Documento(string Type, string Value); } } \ No newline at end of file diff --git a/RecordParser/Engines/Reader/PrimitiveTypeReaderEngine.cs b/RecordParser/Engines/Reader/PrimitiveTypeReaderEngine.cs index 1f1c0b1..7610e3f 100644 --- a/RecordParser/Engines/Reader/PrimitiveTypeReaderEngine.cs +++ b/RecordParser/Engines/Reader/PrimitiveTypeReaderEngine.cs @@ -47,6 +47,9 @@ static PrimitiveTypeReaderEngine() dic = mapping; } + public static bool IsPrimitiveType(Type type) => + type.IsPrimitive || type.IsEnum || dic.Any(kvp => kvp.Key.to == type); + private static char ToChar(ReadOnlySpan span) => span[0]; private static void AddMapForReadOnlySpan( diff --git a/RecordParser/Extensions/FileWriter/WriterExtensions.cs b/RecordParser/Extensions/FileWriter/WriterExtensions.cs index f0434e4..07e4917 100644 --- a/RecordParser/Extensions/FileWriter/WriterExtensions.cs +++ b/RecordParser/Extensions/FileWriter/WriterExtensions.cs @@ -1,4 +1,6 @@ using RecordParser.Builders.Writer; +using RecordParser.Engines.Reader; +using RecordParser.Parsers; using System; using System.Buffers; using System.Collections.Generic; @@ -142,8 +144,11 @@ public static void WriteRecords(this TextWriter textWriter, IEnumerable it var members = GetPropertyExpressions(typeof(T), 64); var builder = new VariableLengthWriterSequentialBuilder(); - foreach (dynamic item in members.Select(x => x.exp)) - builder.Map(item); + foreach (var item in members.Select(x => x.exp)) + if (item.ReturnType == typeof(string)) + builder.Map((dynamic)item, converter: default(FuncSpanTIntBool)); + else + builder.Map((dynamic)item); var parser = builder.Build(separator); var header = string.Join(separator, members.Select(x => x.column)); @@ -187,12 +192,8 @@ public static void WriteRecords(this TextWriter textWriter, IEnumerable it continue; var propertyAccess = Expression.Property(currentState.CurrentExpression, prop); - var pType = prop.PropertyType; - if (pType.IsPrimitive || pType.IsEnum - || pType == typeof(string) - || pType == typeof(decimal) - || pType == typeof(DateTime)) + if (PrimitiveTypeReaderEngine.IsPrimitiveType(prop.PropertyType)) { var lambda = Expression.Lambda(propertyAccess, rootParameter); var column = propertyAccess.ToString().Replace(paramText + ".", string.Empty); From 3c4cf53abfb14ffb5b07009e19615c737766428a Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 4 Jun 2026 17:18:21 -0300 Subject: [PATCH 3/4] adds option parameter --- RecordParser.Test/FileWriterTest.cs | 21 ++--------- .../Extensions/FileWriter/WriterExtensions.cs | 36 ++++++++++++++++--- 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/RecordParser.Test/FileWriterTest.cs b/RecordParser.Test/FileWriterTest.cs index 808881b..b55ea34 100644 --- a/RecordParser.Test/FileWriterTest.cs +++ b/RecordParser.Test/FileWriterTest.cs @@ -131,14 +131,7 @@ public void Write_csv_file_with_autobind_should_support_regular_properties() using var memory = new MemoryStream(); using var textWriter = new StreamWriter(memory); - var parallelOptions = new ParallelismOptions() - { - Enabled = false, - EnsureOriginalOrdering = false, - MaxDegreeOfParallelism = MaxParallelism, - }; - - textWriter.WriteRecords(items, parallelOptions); + textWriter.WriteRecords(items); textWriter.Flush(); // Assert @@ -146,7 +139,6 @@ public void Write_csv_file_with_autobind_should_support_regular_properties() memory.Seek(0, SeekOrigin.Begin); using var textReader = new StreamReader(memory); var content = textReader.ReadToEnd(); - content.Should().Be(expected); } @@ -173,14 +165,7 @@ public void Write_csv_file_with_autobind_should_support_nested_properties() using var memory = new MemoryStream(); using var textWriter = new StreamWriter(memory); - var parallelOptions = new ParallelismOptions() - { - Enabled = false, - EnsureOriginalOrdering = false, - MaxDegreeOfParallelism = MaxParallelism, - }; - - textWriter.WriteRecords(items, parallelOptions); + textWriter.WriteRecords(items); textWriter.Flush(); // Assert @@ -188,13 +173,11 @@ public void Write_csv_file_with_autobind_should_support_nested_properties() memory.Seek(0, SeekOrigin.Begin); using var textReader = new StreamReader(memory); var content = textReader.ReadToEnd(); - content.Should().Be(expected); memory.Seek(0, SeekOrigin.Begin); var opt = new VariableLengthReaderAutoBindOptions() { HasHeader = true }; var readItems = textReader.ReadRecords(opt); - readItems.Should().BeEquivalentTo(items); } diff --git a/RecordParser/Extensions/FileWriter/WriterExtensions.cs b/RecordParser/Extensions/FileWriter/WriterExtensions.cs index 07e4917..daae5e3 100644 --- a/RecordParser/Extensions/FileWriter/WriterExtensions.cs +++ b/RecordParser/Extensions/FileWriter/WriterExtensions.cs @@ -23,6 +23,19 @@ namespace RecordParser.Extensions /// public delegate bool TryFormat(T instance, Span destination, out int charsWritten); + public record class VariableLengthWriterAutoBindOptions + { + /// + /// Maximum depth to search for nested properties. + /// + public int MaxDepth { get; set; } = 3; + + /// + /// Options to configure parallel processing + /// + public ParallelismOptions ParallelismOptions { get; set; } + } + public static class WriterExtensions { private const int initialPow = 10; @@ -132,16 +145,28 @@ private class NodeState } /// - /// Writes the elements of a sequence into the as well the header of file. + /// Writes each item of the sequence as a csv record into the as well the header. /// /// Type of items in the sequence. /// The TextWriter where the items will be written into. /// Sequence of the elements. - /// Options to configure parallel processing. - public static void WriteRecords(this TextWriter textWriter, IEnumerable items, ParallelismOptions options) + public static void WriteRecords(this TextWriter textWriter, IEnumerable items) => + WriteRecords(textWriter, items, default(VariableLengthWriterAutoBindOptions)); + + /// + /// Writes each item of the sequence as a csv record into the as well the header. + /// + /// Type of items in the sequence. + /// The TextWriter where the items will be written into. + /// Sequence of the elements. + /// Options to configure autobind processing. + public static void WriteRecords(this TextWriter textWriter, IEnumerable items, VariableLengthWriterAutoBindOptions options) { + options ??= new(); + var parallel = options.ParallelismOptions ?? new(); + const string separator = ";"; - var members = GetPropertyExpressions(typeof(T), 64); + var members = GetPropertyExpressions(typeof(T), options.MaxDepth); var builder = new VariableLengthWriterSequentialBuilder(); foreach (var item in members.Select(x => x.exp)) @@ -154,7 +179,8 @@ public static void WriteRecords(this TextWriter textWriter, IEnumerable it var header = string.Join(separator, members.Select(x => x.column)); textWriter.WriteLine(header); - WriteRecords(textWriter, items, parser.TryFormat, options); + + WriteRecords(textWriter, items, parser.TryFormat, parallel); } private static IReadOnlyList<(LambdaExpression exp, string column)> GetPropertyExpressions(Type type, int maxDepth) From 4b7da9019868eb6be5b8bb79ed6a6fa713f67ca2 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 31 Jul 2026 12:23:31 -0300 Subject: [PATCH 4/4] adds max depth unit test --- RecordParser.Test/FileWriterTest.cs | 39 +++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/RecordParser.Test/FileWriterTest.cs b/RecordParser.Test/FileWriterTest.cs index b55ea34..8d10b2d 100644 --- a/RecordParser.Test/FileWriterTest.cs +++ b/RecordParser.Test/FileWriterTest.cs @@ -142,6 +142,45 @@ public void Write_csv_file_with_autobind_should_support_regular_properties() content.Should().Be(expected); } + [Fact] + public void Write_csv_file_with_autobind_should_respect_max_depth_configuration() + { + // Arrange + + var items = new Person[] + { + new("Bob",22,new("CPF", "123")), + new("Carla",26, new("Other", "ABC")), + }; + + var expected = $""" + Name;Age + Bob;22 + Carla;26 + + """; + + // Act + + using var memory = new MemoryStream(); + using var textWriter = new StreamWriter(memory); + var options = new VariableLengthWriterAutoBindOptions + { + MaxDepth = 1 + }; + + textWriter.WriteRecords(items, options); + textWriter.Flush(); + + // Assert + + memory.Seek(0, SeekOrigin.Begin); + using var textReader = new StreamReader(memory); + var content = textReader.ReadToEnd(); + content.Should().Be(expected); + } + + [Fact] public void Write_csv_file_with_autobind_should_support_nested_properties() {