diff --git a/.editorconfig b/.editorconfig index e1af6a4..d9bdff6 100644 --- a/.editorconfig +++ b/.editorconfig @@ -32,7 +32,7 @@ csharp_new_line_between_query_expression_clauses = true csharp_indent_block_contents = true csharp_indent_braces = false csharp_indent_case_contents = true -csharp_indent_case_contents_when_block = false +csharp_indent_case_contents_when_block = true csharp_indent_switch_labels = true csharp_indent_labels = one_less_than_current @@ -83,7 +83,7 @@ dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case csharp_using_directive_placement = outside_namespace:suggestion dotnet_sort_system_directives_first = true dotnet_separate_import_directive_groups = true -csharp_prefer_braces = true:refactoring +csharp_prefer_braces = true:error csharp_preserve_single_line_blocks = true:none csharp_preserve_single_line_statements = false:none csharp_prefer_static_local_function = true:suggestion @@ -131,6 +131,7 @@ csharp_style_conditional_delegate_call = true:suggestion csharp_style_prefer_index_operator = false:none csharp_style_prefer_range_operator = false:none csharp_style_pattern_local_over_anonymous_function = false:none +csharp_style_namespace_declarations = file_scoped:warning # Space preferences csharp_space_after_cast = false diff --git a/CsvHelper.FastDynamic.Performance/Internal/CsvReaderExtension.cs b/CsvHelper.FastDynamic.Performance/Internal/CsvReaderExtension.cs index 4e25763..7581938 100644 --- a/CsvHelper.FastDynamic.Performance/Internal/CsvReaderExtension.cs +++ b/CsvHelper.FastDynamic.Performance/Internal/CsvReaderExtension.cs @@ -2,56 +2,55 @@ using System.Collections.Generic; using System.Linq; -namespace CsvHelper.FastDynamic.Performance.Internal +namespace CsvHelper.FastDynamic.Performance.Internal; + +internal static class CsvReaderExtension { - internal static class CsvReaderExtension - { - internal static IReadOnlyList> GetDictionaryRecords(this CsvReader csvReader) - => csvReader.EnumerateDictionaryRecords().ToArray(); + internal static IReadOnlyList> GetDictionaryRecords(this CsvReader csvReader) + => csvReader.EnumerateDictionaryRecords().ToArray(); - internal static IEnumerable> EnumerateDictionaryRecords(this CsvReader csvReader) + internal static IEnumerable> EnumerateDictionaryRecords(this CsvReader csvReader) + { + if (csvReader.Configuration.HasHeaderRecord && csvReader.HeaderRecord == null) { - if (csvReader.Configuration.HasHeaderRecord && csvReader.HeaderRecord == null) + if (!csvReader.Read()) { - if (!csvReader.Read()) - { - yield break; - } - - csvReader.ReadHeader(); + yield break; } - var headerRecord = csvReader.HeaderRecord - .Select((x, i) => csvReader.Configuration.PrepareHeaderForMatch(new PrepareHeaderForMatchArgs(x, i))) - .ToArray(); + csvReader.ReadHeader(); + } + + var headerRecord = csvReader.HeaderRecord + .Select((x, i) => csvReader.Configuration.PrepareHeaderForMatch(new PrepareHeaderForMatchArgs(x, i))) + .ToArray(); - while (csvReader.Read()) + while (csvReader.Read()) + { + Dictionary record; + + try { - Dictionary record; + record = new Dictionary(headerRecord.Length); - try + for (var i = 0; i < headerRecord.Length; i++) { - record = new Dictionary(headerRecord.Length); - - for (var i = 0; i < headerRecord.Length; i++) - { - record[headerRecord[i]] = csvReader.Parser[i]; - } + record[headerRecord[i]] = csvReader.Parser[i]; } - catch (Exception ex) - { - var readerException = new ReaderException(csvReader.Context, "An unexpected error occurred.", ex); - - if (csvReader.Configuration.ReadingExceptionOccurred?.Invoke(new ReadingExceptionOccurredArgs(readerException)) ?? true) - { - throw readerException; - } + } + catch (Exception ex) + { + var readerException = new ReaderException(csvReader.Context, "An unexpected error occurred.", ex); - continue; + if (csvReader.Configuration.ReadingExceptionOccurred?.Invoke(new ReadingExceptionOccurredArgs(readerException)) ?? true) + { + throw readerException; } - yield return record; + continue; } + + yield return record; } } } diff --git a/CsvHelper.FastDynamic.Performance/Program.cs b/CsvHelper.FastDynamic.Performance/Program.cs index fc8b64d..2ad1824 100644 --- a/CsvHelper.FastDynamic.Performance/Program.cs +++ b/CsvHelper.FastDynamic.Performance/Program.cs @@ -1,13 +1,6 @@ using BenchmarkDotNet.Running; -namespace CsvHelper.FastDynamic.Performance -{ - class Program - { - static void Main(string[] args) - { - BenchmarkRunner.Run(); - BenchmarkRunner.Run(); - } - } -} +using CsvHelper.FastDynamic.Performance; + +BenchmarkRunner.Run(); +BenchmarkRunner.Run(); diff --git a/CsvHelper.FastDynamic.Performance/ReaderBenchmark.cs b/CsvHelper.FastDynamic.Performance/ReaderBenchmark.cs index 1d11a47..a009192 100644 --- a/CsvHelper.FastDynamic.Performance/ReaderBenchmark.cs +++ b/CsvHelper.FastDynamic.Performance/ReaderBenchmark.cs @@ -7,42 +7,41 @@ using CsvHelper.FastDynamic.Performance.Internal; -namespace CsvHelper.FastDynamic.Performance +namespace CsvHelper.FastDynamic.Performance; + +[MemoryDiagnoser] +public class ReaderBenchmark { - [MemoryDiagnoser] - public class ReaderBenchmark - { - private const string SampleCsvFile = @".\sampledata\SFO_Airport_Monthly_Utility_Consumption_for_Natural_Gas__Water__and_Electricity.csv"; + private const string SampleCsvFile = @".\sampledata\SFO_Airport_Monthly_Utility_Consumption_for_Natural_Gas__Water__and_Electricity.csv"; - public ReaderBenchmark() - { - _sampleCsvData = File.ReadAllText(SampleCsvFile); - } + public ReaderBenchmark() + { + _sampleCsvData = File.ReadAllText(SampleCsvFile); + } - private readonly string _sampleCsvData; + private readonly string _sampleCsvData; - [Benchmark] - public IReadOnlyList GetRecords() - { - using var csvReader = new CsvReader(new StringReader(_sampleCsvData), CultureInfo.InvariantCulture); + [Benchmark] + public IReadOnlyList GetRecords() + { + using var csvReader = new CsvReader(new StringReader(_sampleCsvData), CultureInfo.InvariantCulture); - return csvReader.GetRecords().ToArray(); - } + return csvReader.GetRecords().ToArray(); + } - [Benchmark] - public IReadOnlyList> GetDictionaryRecords() - { - using var csvReader = new CsvReader(new StringReader(_sampleCsvData), CultureInfo.InvariantCulture); + [Benchmark] + public IReadOnlyList> GetDictionaryRecords() + { + using var csvReader = new CsvReader(new StringReader(_sampleCsvData), CultureInfo.InvariantCulture); - return csvReader.GetDictionaryRecords(); - } + return csvReader.GetDictionaryRecords(); + } - [Benchmark] - public IReadOnlyList GetDynamicRecords() - { - using var csvReader = new CsvReader(new StringReader(_sampleCsvData), CultureInfo.InvariantCulture); + [Benchmark] + public IReadOnlyList GetDynamicRecords() + { + using var csvReader = new CsvReader(new StringReader(_sampleCsvData), CultureInfo.InvariantCulture); - return csvReader.GetDynamicRecords(); - } + return csvReader.GetDynamicRecords(); } } diff --git a/CsvHelper.FastDynamic.Performance/WriterBenchmark.cs b/CsvHelper.FastDynamic.Performance/WriterBenchmark.cs index 6cf8a2b..f53df63 100644 --- a/CsvHelper.FastDynamic.Performance/WriterBenchmark.cs +++ b/CsvHelper.FastDynamic.Performance/WriterBenchmark.cs @@ -4,45 +4,36 @@ using BenchmarkDotNet.Attributes; -using CsvHelper.FastDynamic.Performance.Internal; +namespace CsvHelper.FastDynamic.Performance; -namespace CsvHelper.FastDynamic.Performance +[MemoryDiagnoser] +public class WriterBenchmark { - [MemoryDiagnoser] - public class WriterBenchmark - { - private const string SampleCsvFile = @".\sampledata\SFO_Airport_Monthly_Utility_Consumption_for_Natural_Gas__Water__and_Electricity.csv"; + private const string SampleCsvFile = @".\sampledata\SFO_Airport_Monthly_Utility_Consumption_for_Natural_Gas__Water__and_Electricity.csv"; - public WriterBenchmark() + public WriterBenchmark() + { + using (var csvReader = new CsvReader(new StreamReader(SampleCsvFile), CultureInfo.InvariantCulture)) { - using (var csvReader = new CsvReader(new StreamReader(SampleCsvFile), CultureInfo.InvariantCulture)) - { - _dynamicCsvData = csvReader.GetDynamicRecords(); - } - - using (var csvReader = new CsvReader(new StreamReader(SampleCsvFile), CultureInfo.InvariantCulture)) - { - _dictionaryCsvData = csvReader.GetDictionaryRecords(); - } + _dynamicCsvData = csvReader.GetDynamicRecords(); } + } - private readonly IReadOnlyList _dynamicCsvData; - private readonly IReadOnlyList> _dictionaryCsvData; + private readonly IReadOnlyList _dynamicCsvData; - [Benchmark] - public void WriteRecords_DynamicObject() - { - using var csvWriter = new CsvWriter(new StringWriter(), CultureInfo.InvariantCulture); + [Benchmark] + public void WriteRecords_DynamicObject() + { + using var csvWriter = new CsvWriter(new StringWriter(), CultureInfo.InvariantCulture); - csvWriter.WriteRecords(_dynamicCsvData); - } + csvWriter.WriteRecords(_dynamicCsvData); + } - [Benchmark] - public void WriteDynamicRecords_DynamicObject() - { - using var csvWriter = new CsvWriter(new StringWriter(), CultureInfo.InvariantCulture); + [Benchmark] + public void WriteDynamicRecords_DynamicObject() + { + using var csvWriter = new CsvWriter(new StringWriter(), CultureInfo.InvariantCulture); - csvWriter.WriteDynamicRecords(_dynamicCsvData); - } + csvWriter.WriteDynamicRecords(_dynamicCsvData); } } diff --git a/CsvHelper.FastDynamic.Tests/CsvReaderTests.cs b/CsvHelper.FastDynamic.Tests/CsvReaderTests.cs index 9643e0e..4c4236b 100644 --- a/CsvHelper.FastDynamic.Tests/CsvReaderTests.cs +++ b/CsvHelper.FastDynamic.Tests/CsvReaderTests.cs @@ -6,250 +6,249 @@ using Xunit; -namespace CsvHelper.FastDynamic.Tests +namespace CsvHelper.FastDynamic.Tests; + +public class CsvReaderTests { - public class CsvReaderTests + [Fact] + public void ReadDynamicRecords() { - [Fact] - public void ReadDynamicRecords() - { - var csvReader = CreateInMemoryReader(); + var csvReader = CreateInMemoryReader(); - var records = csvReader.GetDynamicRecords(); + var records = csvReader.GetDynamicRecords(); - Assert.NotNull(records); - Assert.Equal(3, records.Count); + Assert.NotNull(records); + Assert.Equal(3, records.Count); - for (var i = 0; i < 3; i++) - { - Assert.Equal(TestData.CsvRecords[i]["Id"], records[i].Id); - Assert.Equal(TestData.CsvRecords[i]["Name"], records[i].Name); - Assert.Equal(TestData.CsvRecords[i]["Location"], records[i].Location); - } + for (var i = 0; i < 3; i++) + { + Assert.Equal(TestData.CsvRecords[i]["Id"], records[i].Id); + Assert.Equal(TestData.CsvRecords[i]["Name"], records[i].Name); + Assert.Equal(TestData.CsvRecords[i]["Location"], records[i].Location); } + } - [Fact] - public void ReadDynamicRecords_UseIndexer() - { - var csvReader = CreateInMemoryReader(); + [Fact] + public void ReadDynamicRecords_UseIndexer() + { + var csvReader = CreateInMemoryReader(); - var records = csvReader.GetDynamicRecords(); + var records = csvReader.GetDynamicRecords(); - Assert.NotNull(records); - Assert.Equal(3, records.Count); + Assert.NotNull(records); + Assert.Equal(3, records.Count); - for (var i = 0; i < 3; i++) - { - Assert.Equal(TestData.CsvRecords[i]["Id"], records[i]["Id"]); - Assert.Equal(TestData.CsvRecords[i]["Name"], records[i]["Name"]); - Assert.Equal(TestData.CsvRecords[i]["Location"], records[i]["Location"]); - } + for (var i = 0; i < 3; i++) + { + Assert.Equal(TestData.CsvRecords[i]["Id"], records[i]["Id"]); + Assert.Equal(TestData.CsvRecords[i]["Name"], records[i]["Name"]); + Assert.Equal(TestData.CsvRecords[i]["Location"], records[i]["Location"]); } + } - [Fact] - public void ReadDynamicRecords_AsDictionary() - { - var csvReader = CreateInMemoryReader(); + [Fact] + public void ReadDynamicRecords_AsDictionary() + { + var csvReader = CreateInMemoryReader(); - var records = csvReader.GetDynamicRecords() - .Cast>() - .ToArray(); + var records = csvReader.GetDynamicRecords() + .Cast>() + .ToArray(); - Assert.NotNull(records); - Assert.Equal(3, records.Length); + Assert.NotNull(records); + Assert.Equal(3, records.Length); - for (var i = 0; i < 3; i++) - { - Assert.Equal(TestData.CsvRecords[i]["Id"], records[i]["Id"]); - Assert.Equal(TestData.CsvRecords[i]["Name"], records[i]["Name"]); - Assert.Equal(TestData.CsvRecords[i]["Location"], records[i]["Location"]); - } + for (var i = 0; i < 3; i++) + { + Assert.Equal(TestData.CsvRecords[i]["Id"], records[i]["Id"]); + Assert.Equal(TestData.CsvRecords[i]["Name"], records[i]["Name"]); + Assert.Equal(TestData.CsvRecords[i]["Location"], records[i]["Location"]); } + } - [Fact] - public void ReadDynamicRecords_WithMissingHeader() - { - var csvReader = CreateInMemoryReader_WithMissingHeader(); + [Fact] + public void ReadDynamicRecords_WithMissingHeader() + { + var csvReader = CreateInMemoryReader_WithMissingHeader(); - var records = csvReader.GetDynamicRecords() - .Cast>() - .ToArray(); + var records = csvReader.GetDynamicRecords() + .Cast>() + .ToArray(); - Assert.NotNull(records); - Assert.Equal(3, records.Length); + Assert.NotNull(records); + Assert.Equal(3, records.Length); - for (var i = 0; i < 3; i++) - { - Assert.Equal(2, records[i].Count); + for (var i = 0; i < 3; i++) + { + Assert.Equal(2, records[i].Count); - Assert.Equal(TestData.CsvRecords[i]["Id"], records[i]["Id"]); - Assert.Equal(TestData.CsvRecords[i]["Name"], records[i]["Name"]); - } + Assert.Equal(TestData.CsvRecords[i]["Id"], records[i]["Id"]); + Assert.Equal(TestData.CsvRecords[i]["Name"], records[i]["Name"]); } + } - [Fact] - public async Task ReadDynamicRecordsAsync() - { - var csvReader = CreateInMemoryReader(); + [Fact] + public async Task ReadDynamicRecordsAsync() + { + var csvReader = CreateInMemoryReader(); - var records = await csvReader.GetDynamicRecordsAsync(); + var records = await csvReader.GetDynamicRecordsAsync(); - Assert.NotNull(records); - Assert.Equal(3, records.Count); + Assert.NotNull(records); + Assert.Equal(3, records.Count); - for (var i = 0; i < 3; i++) - { - Assert.Equal(TestData.CsvRecords[i]["Id"], records[i].Id); - Assert.Equal(TestData.CsvRecords[i]["Name"], records[i].Name); - Assert.Equal(TestData.CsvRecords[i]["Location"], records[i].Location); - } + for (var i = 0; i < 3; i++) + { + Assert.Equal(TestData.CsvRecords[i]["Id"], records[i].Id); + Assert.Equal(TestData.CsvRecords[i]["Name"], records[i].Name); + Assert.Equal(TestData.CsvRecords[i]["Location"], records[i].Location); } + } - [Fact] - public async Task ReadDynamicRecordsAsync_WithMissingHeader() - { - var csvReader = CreateInMemoryReader_WithMissingHeader(); + [Fact] + public async Task ReadDynamicRecordsAsync_WithMissingHeader() + { + var csvReader = CreateInMemoryReader_WithMissingHeader(); - var records = (await csvReader.GetDynamicRecordsAsync()) - .Cast>() - .ToArray(); + var records = (await csvReader.GetDynamicRecordsAsync()) + .Cast>() + .ToArray(); - Assert.NotNull(records); - Assert.Equal(3, records.Length); + Assert.NotNull(records); + Assert.Equal(3, records.Length); - for (var i = 0; i < 3; i++) - { - Assert.Equal(2, records[i].Count); + for (var i = 0; i < 3; i++) + { + Assert.Equal(2, records[i].Count); - Assert.Equal(TestData.CsvRecords[i]["Id"], records[i]["Id"]); - Assert.Equal(TestData.CsvRecords[i]["Name"], records[i]["Name"]); - } + Assert.Equal(TestData.CsvRecords[i]["Id"], records[i]["Id"]); + Assert.Equal(TestData.CsvRecords[i]["Name"], records[i]["Name"]); } + } - [Fact] - public void EnumerateDynamicRecords() - { - var csvReader = CreateInMemoryReader(); - - var records = csvReader.EnumerateDynamicRecords(); + [Fact] + public void EnumerateDynamicRecords() + { + var csvReader = CreateInMemoryReader(); - var count = 0; + var records = csvReader.EnumerateDynamicRecords(); - foreach (var record in records) - { - Assert.NotNull(record); - Assert.IsAssignableFrom>(record); + var count = 0; - Assert.Equal(TestData.CsvRecords[count]["Id"], record.Id); - Assert.Equal(TestData.CsvRecords[count]["Name"], record.Name); - Assert.Equal(TestData.CsvRecords[count]["Location"], record.Location); + foreach (var record in records) + { + Assert.NotNull(record); + Assert.IsAssignableFrom>(record); - count += 1; - } + Assert.Equal(TestData.CsvRecords[count]["Id"], record.Id); + Assert.Equal(TestData.CsvRecords[count]["Name"], record.Name); + Assert.Equal(TestData.CsvRecords[count]["Location"], record.Location); - Assert.Equal(3, count); + count += 1; } - [Fact] - public async Task EnumerateDynamicRecordsAsync() - { - var csvReader = CreateInMemoryReader(); + Assert.Equal(3, count); + } - var records = csvReader.EnumerateDynamicRecordsAsync(); + [Fact] + public async Task EnumerateDynamicRecordsAsync() + { + var csvReader = CreateInMemoryReader(); - var count = 0; + var records = csvReader.EnumerateDynamicRecordsAsync(); - await foreach (var record in records) - { - Assert.NotNull(record); - Assert.IsAssignableFrom>(record); + var count = 0; - Assert.Equal(TestData.CsvRecords[count]["Id"], record.Id); - Assert.Equal(TestData.CsvRecords[count]["Name"], record.Name); - Assert.Equal(TestData.CsvRecords[count]["Location"], record.Location); + await foreach (var record in records) + { + Assert.NotNull(record); + Assert.IsAssignableFrom>(record); - count += 1; - } + Assert.Equal(TestData.CsvRecords[count]["Id"], record.Id); + Assert.Equal(TestData.CsvRecords[count]["Name"], record.Name); + Assert.Equal(TestData.CsvRecords[count]["Location"], record.Location); - Assert.Equal(3, count); + count += 1; } - [Fact] - public void AddDynamicColumns_Member() - { - var csvReader = CreateInMemoryReader(); - - var records = csvReader.GetDynamicRecords(); + Assert.Equal(3, count); + } - for (var i = 0; i < 3; i++) - { - records[i].Append = $"test-{i}"; - } + [Fact] + public void AddDynamicColumns_Member() + { + var csvReader = CreateInMemoryReader(); - for (var i = 0; i < 3; i++) - { - Assert.Equal(TestData.CsvRecords[i]["Id"], records[i].Id); - Assert.Equal(TestData.CsvRecords[i]["Name"], records[i].Name); - Assert.Equal(TestData.CsvRecords[i]["Location"], records[i].Location); + var records = csvReader.GetDynamicRecords(); - Assert.Equal($"test-{i}", records[i].Append); - } + for (var i = 0; i < 3; i++) + { + records[i].Append = $"test-{i}"; } - [Fact] - public void AddDynamicColumns_Indexer() + for (var i = 0; i < 3; i++) { - var csvReader = CreateInMemoryReader(); + Assert.Equal(TestData.CsvRecords[i]["Id"], records[i].Id); + Assert.Equal(TestData.CsvRecords[i]["Name"], records[i].Name); + Assert.Equal(TestData.CsvRecords[i]["Location"], records[i].Location); - var records = csvReader.GetDynamicRecords(); + Assert.Equal($"test-{i}", records[i].Append); + } + } - for (var i = 0; i < 3; i++) - { - records[i]["Append"] = $"test-{i}"; - } + [Fact] + public void AddDynamicColumns_Indexer() + { + var csvReader = CreateInMemoryReader(); - for (var i = 0; i < 3; i++) - { - Assert.Equal(TestData.CsvRecords[i]["Id"], records[i]["Id"]); - Assert.Equal(TestData.CsvRecords[i]["Name"], records[i]["Name"]); - Assert.Equal(TestData.CsvRecords[i]["Location"], records[i]["Location"]); + var records = csvReader.GetDynamicRecords(); - Assert.Equal($"test-{i}", records[i]["Append"]); - } + for (var i = 0; i < 3; i++) + { + records[i]["Append"] = $"test-{i}"; } - [Fact] - public void AddDynamicColumns_Dictionary() + for (var i = 0; i < 3; i++) { - var csvReader = CreateInMemoryReader(); - - var records = csvReader.GetDynamicRecords() - .Cast>() - .ToArray(); + Assert.Equal(TestData.CsvRecords[i]["Id"], records[i]["Id"]); + Assert.Equal(TestData.CsvRecords[i]["Name"], records[i]["Name"]); + Assert.Equal(TestData.CsvRecords[i]["Location"], records[i]["Location"]); - for (var i = 0; i < 3; i++) - { - records[i]["Append"] = $"test-{i}"; - } + Assert.Equal($"test-{i}", records[i]["Append"]); + } + } - for (var i = 0; i < 3; i++) - { - Assert.Equal(TestData.CsvRecords[i]["Id"], records[i]["Id"]); - Assert.Equal(TestData.CsvRecords[i]["Name"], records[i]["Name"]); - Assert.Equal(TestData.CsvRecords[i]["Location"], records[i]["Location"]); + [Fact] + public void AddDynamicColumns_Dictionary() + { + var csvReader = CreateInMemoryReader(); - Assert.Equal($"test-{i}", records[i]["Append"]); - } - } + var records = csvReader.GetDynamicRecords() + .Cast>() + .ToArray(); - private CsvReader CreateInMemoryReader() + for (var i = 0; i < 3; i++) { - return new CsvReader(new StringReader(TestData.CsvContent), CultureInfo.InvariantCulture); + records[i]["Append"] = $"test-{i}"; } - private CsvReader CreateInMemoryReader_WithMissingHeader() + for (var i = 0; i < 3; i++) { - return new CsvReader(new StringReader(TestData.CsvContentWithMissingHeader), CultureInfo.InvariantCulture); + Assert.Equal(TestData.CsvRecords[i]["Id"], records[i]["Id"]); + Assert.Equal(TestData.CsvRecords[i]["Name"], records[i]["Name"]); + Assert.Equal(TestData.CsvRecords[i]["Location"], records[i]["Location"]); + + Assert.Equal($"test-{i}", records[i]["Append"]); } } + + private CsvReader CreateInMemoryReader() + { + return new CsvReader(new StringReader(TestData.CsvContent), CultureInfo.InvariantCulture); + } + + private CsvReader CreateInMemoryReader_WithMissingHeader() + { + return new CsvReader(new StringReader(TestData.CsvContentWithMissingHeader), CultureInfo.InvariantCulture); + } } diff --git a/CsvHelper.FastDynamic.Tests/CsvWriterTests.cs b/CsvHelper.FastDynamic.Tests/CsvWriterTests.cs index 614aeba..93343b7 100644 --- a/CsvHelper.FastDynamic.Tests/CsvWriterTests.cs +++ b/CsvHelper.FastDynamic.Tests/CsvWriterTests.cs @@ -7,81 +7,80 @@ using Xunit; -namespace CsvHelper.FastDynamic.Tests +namespace CsvHelper.FastDynamic.Tests; + +public class CsvWriterTests { - public class CsvWriterTests + [Fact] + public void WriteDynamicObjects() { - [Fact] - public void WriteDynamicObjects() - { - var (csvWriter, stringWriter) = CreateInMemoryWriter(); + var (csvWriter, stringWriter) = CreateInMemoryWriter(); - var records = TestData.CsvRecords; + var records = TestData.CsvRecords; - var dynamicRecords = records.Select(ConvertToExpandoObject) - .ToArray(); + var dynamicRecords = records.Select(ConvertToExpandoObject) + .ToArray(); - csvWriter.WriteDynamicRecords(dynamicRecords); + csvWriter.WriteDynamicRecords(dynamicRecords); - Assert.Equal(TestData.CsvContent, stringWriter.ToString()); - } + Assert.Equal(TestData.CsvContent, stringWriter.ToString()); + } - [Fact] - public async Task WriteDynamicObjectsAsync() - { - var (csvWriter, stringWriter) = CreateInMemoryWriter(); + [Fact] + public async Task WriteDynamicObjectsAsync() + { + var (csvWriter, stringWriter) = CreateInMemoryWriter(); - var records = TestData.CsvRecords; + var records = TestData.CsvRecords; - var dynamicRecords = records.Select(ConvertToExpandoObject) - .ToArray(); + var dynamicRecords = records.Select(ConvertToExpandoObject) + .ToArray(); - await csvWriter.WriteDynamicRecordsAsync(dynamicRecords); + await csvWriter.WriteDynamicRecordsAsync(dynamicRecords); - Assert.Equal(TestData.CsvContent, stringWriter.ToString()); - } - - [Fact] - public void WriteAnonymousObjects() - { - var (csvWriter, stringWriter) = CreateInMemoryWriter(); + Assert.Equal(TestData.CsvContent, stringWriter.ToString()); + } - var records = TestData.CsvAnonymousRecords; + [Fact] + public void WriteAnonymousObjects() + { + var (csvWriter, stringWriter) = CreateInMemoryWriter(); - csvWriter.WriteDynamicRecords(records); + var records = TestData.CsvAnonymousRecords; - Assert.Equal(TestData.CsvContent, stringWriter.ToString()); - } + csvWriter.WriteDynamicRecords(records); - [Fact] - public async Task WriteAnonymousObjectsAsync() - { - var (csvWriter, stringWriter) = CreateInMemoryWriter(); + Assert.Equal(TestData.CsvContent, stringWriter.ToString()); + } - var records = TestData.CsvAnonymousRecords; + [Fact] + public async Task WriteAnonymousObjectsAsync() + { + var (csvWriter, stringWriter) = CreateInMemoryWriter(); - await csvWriter.WriteDynamicRecordsAsync(records); + var records = TestData.CsvAnonymousRecords; - Assert.Equal(TestData.CsvContent, stringWriter.ToString()); - } + await csvWriter.WriteDynamicRecordsAsync(records); - private (CsvWriter, StringWriter) CreateInMemoryWriter() - { - var stringWriter = new StringWriter(); + Assert.Equal(TestData.CsvContent, stringWriter.ToString()); + } - return (new CsvWriter(stringWriter, CultureInfo.InvariantCulture), stringWriter); - } + private (CsvWriter, StringWriter) CreateInMemoryWriter() + { + var stringWriter = new StringWriter(); - private ExpandoObject ConvertToExpandoObject(IDictionary dictionary) - { - var expandoObject = new ExpandoObject(); + return (new CsvWriter(stringWriter, CultureInfo.InvariantCulture), stringWriter); + } - foreach (var item in dictionary) - { - expandoObject.TryAdd(item.Key, item.Value); - } + private ExpandoObject ConvertToExpandoObject(IDictionary dictionary) + { + var expandoObject = new ExpandoObject(); - return expandoObject; + foreach (var item in dictionary) + { + expandoObject.TryAdd(item.Key, item.Value); } + + return expandoObject; } } diff --git a/CsvHelper.FastDynamic.Tests/TestData.cs b/CsvHelper.FastDynamic.Tests/TestData.cs index c763681..5a085b5 100644 --- a/CsvHelper.FastDynamic.Tests/TestData.cs +++ b/CsvHelper.FastDynamic.Tests/TestData.cs @@ -1,25 +1,24 @@ using System.Collections.Generic; -namespace CsvHelper.FastDynamic.Tests +namespace CsvHelper.FastDynamic.Tests; + +public static class TestData { - public static class TestData - { - public static readonly string CsvContent = $"Id,Name,Location\r\n1,kazuakix,Wakayama\r\n2,daruyanagi,Ehime\r\n3,buchizo,Osaka\r\n"; + public static readonly string CsvContent = "Id,Name,Location\r\n1,kazuakix,Wakayama\r\n2,daruyanagi,Ehime\r\n3,buchizo,Osaka\r\n"; - public static readonly string CsvContentWithMissingHeader = $"Id,Name\r\n1,kazuakix\r\n2,daruyanagi,Ehime\r\n3,buchizo\r\n"; + public static readonly string CsvContentWithMissingHeader = "Id,Name\r\n1,kazuakix\r\n2,daruyanagi,Ehime\r\n3,buchizo\r\n"; - public static readonly IReadOnlyList> CsvRecords = new[] - { - new Dictionary { { "Id", "1" }, { "Name", "kazuakix" }, { "Location", "Wakayama" } }, - new Dictionary { { "Id", "2" }, { "Name", "daruyanagi" }, { "Location", "Ehime" } }, - new Dictionary { { "Id", "3" }, { "Name", "buchizo" }, { "Location", "Osaka" } } - }; + public static readonly IReadOnlyList> CsvRecords = new[] + { + new Dictionary { { "Id", "1" }, { "Name", "kazuakix" }, { "Location", "Wakayama" } }, + new Dictionary { { "Id", "2" }, { "Name", "daruyanagi" }, { "Location", "Ehime" } }, + new Dictionary { { "Id", "3" }, { "Name", "buchizo" }, { "Location", "Osaka" } } + }; - public static readonly IReadOnlyList CsvAnonymousRecords = new[] - { - new { Id = 1, Name = "kazuakix", Location = "Wakayama" }, - new { Id = 2, Name = "daruyanagi", Location = "Ehime" }, - new { Id = 3, Name = "buchizo", Location = "Osaka" } - }; - } + public static readonly IReadOnlyList CsvAnonymousRecords = new[] + { + new { Id = 1, Name = "kazuakix", Location = "Wakayama" }, + new { Id = 2, Name = "daruyanagi", Location = "Ehime" }, + new { Id = 3, Name = "buchizo", Location = "Osaka" } + }; } diff --git a/CsvHelper.FastDynamic/CsvHeader.cs b/CsvHelper.FastDynamic/CsvHeader.cs index 6262dc8..642ec5d 100644 --- a/CsvHelper.FastDynamic/CsvHeader.cs +++ b/CsvHelper.FastDynamic/CsvHeader.cs @@ -1,55 +1,54 @@ using System; using System.Collections.Generic; -namespace CsvHelper.FastDynamic +namespace CsvHelper.FastDynamic; + +// Thanks, from https://github.com/StackExchange/Dapper/blob/master/Dapper/SqlMapper.DapperTable.cs +internal sealed class CsvHeader { - // Thanks, from https://github.com/StackExchange/Dapper/blob/master/Dapper/SqlMapper.DapperTable.cs - internal sealed class CsvHeader + private string[] _fieldNames; + private readonly Dictionary _fieldNameLookup; + + public string[] FieldNames => _fieldNames; + + public CsvHeader(string[] fieldNames) { - private string[] _fieldNames; - private readonly Dictionary _fieldNameLookup; + _fieldNames = fieldNames ?? throw new ArgumentNullException(nameof(fieldNames)); - public string[] FieldNames => _fieldNames; + _fieldNameLookup = new Dictionary(fieldNames.Length, StringComparer.Ordinal); - public CsvHeader(string[] fieldNames) + for (var i = fieldNames.Length - 1; i >= 0; i--) { - _fieldNames = fieldNames ?? throw new ArgumentNullException(nameof(fieldNames)); - - _fieldNameLookup = new Dictionary(fieldNames.Length, StringComparer.Ordinal); + var name = fieldNames[i]; - for (var i = fieldNames.Length - 1; i >= 0; i--) + if (name != null) { - var name = fieldNames[i]; - - if (name != null) - { - _fieldNameLookup[name] = i; - } + _fieldNameLookup[name] = i; } } + } - public int IndexOfName(string name) => name != null && _fieldNameLookup.TryGetValue(name, out var index) ? index : -1; + public int IndexOfName(string name) => name != null && _fieldNameLookup.TryGetValue(name, out var index) ? index : -1; - public int AddField(string name) + public int AddField(string name) + { + if (name == null) { - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } + throw new ArgumentNullException(nameof(name)); + } - if (_fieldNameLookup.ContainsKey(name)) - { - throw new InvalidOperationException($"Field already exists: {name}"); - } + if (_fieldNameLookup.ContainsKey(name)) + { + throw new InvalidOperationException($"Field already exists: {name}"); + } - var oldLength = _fieldNames.Length; + var oldLength = _fieldNames.Length; - Array.Resize(ref _fieldNames, oldLength + 1); + Array.Resize(ref _fieldNames, oldLength + 1); - _fieldNames[oldLength] = name; - _fieldNameLookup[name] = oldLength; + _fieldNames[oldLength] = name; + _fieldNameLookup[name] = oldLength; - return oldLength; - } + return oldLength; } } diff --git a/CsvHelper.FastDynamic/CsvReaderExtensions.cs b/CsvHelper.FastDynamic/CsvReaderExtensions.cs index 200d4d0..beaf10c 100644 --- a/CsvHelper.FastDynamic/CsvReaderExtensions.cs +++ b/CsvHelper.FastDynamic/CsvReaderExtensions.cs @@ -3,114 +3,113 @@ using System.Linq; using System.Threading.Tasks; -namespace CsvHelper.FastDynamic +namespace CsvHelper.FastDynamic; + +public static class CsvReaderExtensions { - public static class CsvReaderExtensions - { - public static IReadOnlyList GetDynamicRecords(this CsvReader csvReader) => csvReader.EnumerateDynamicRecords().ToArray(); + public static IReadOnlyList GetDynamicRecords(this CsvReader csvReader) => csvReader.EnumerateDynamicRecords().ToArray(); - public static IEnumerable EnumerateDynamicRecords(this CsvReader csvReader) + public static IEnumerable EnumerateDynamicRecords(this CsvReader csvReader) + { + if (csvReader.Configuration.HasHeaderRecord && csvReader.HeaderRecord == null) { - if (csvReader.Configuration.HasHeaderRecord && csvReader.HeaderRecord == null) + if (!csvReader.Read()) { - if (!csvReader.Read()) - { - yield break; - } - - csvReader.ReadHeader(); + yield break; } - var csvHeader = new CsvHeader(csvReader.HeaderRecord - .Select((x, i) => csvReader.Configuration.PrepareHeaderForMatch(new PrepareHeaderForMatchArgs(x, i))) - .ToArray()); + csvReader.ReadHeader(); + } - while (csvReader.Read()) - { - CsvRecord record; + var csvHeader = new CsvHeader(csvReader.HeaderRecord + .Select((x, i) => csvReader.Configuration.PrepareHeaderForMatch(new PrepareHeaderForMatchArgs(x, i))) + .ToArray()); - try - { - var values = new object[csvReader.HeaderRecord.Length]; + while (csvReader.Read()) + { + CsvRecord record; - Array.Copy(csvReader.Parser.Record, values, csvReader.HeaderRecord.Length); + try + { + var values = new object[csvReader.HeaderRecord.Length]; - record = new CsvRecord(csvHeader, values); - } - catch (Exception ex) - { - var readerException = new ReaderException(csvReader.Context, "An unexpected error occurred.", ex); + Array.Copy(csvReader.Parser.Record, values, csvReader.HeaderRecord.Length); - if (csvReader.Configuration.ReadingExceptionOccurred?.Invoke(new ReadingExceptionOccurredArgs(readerException)) ?? true) - { - throw readerException; - } + record = new CsvRecord(csvHeader, values); + } + catch (Exception ex) + { + var readerException = new ReaderException(csvReader.Context, "An unexpected error occurred.", ex); - continue; + if (csvReader.Configuration.ReadingExceptionOccurred?.Invoke(new ReadingExceptionOccurredArgs(readerException)) ?? true) + { + throw readerException; } - yield return record; + continue; } + + yield return record; } + } #if NETSTANDARD2_1 - public static async Task> GetDynamicRecordsAsync(this CsvReader csvReader) + public static async Task> GetDynamicRecordsAsync(this CsvReader csvReader) + { + var records = new List(); + + await foreach (var record in csvReader.EnumerateDynamicRecordsAsync()) { - var records = new List(); + records.Add(record); + } - await foreach (var record in csvReader.EnumerateDynamicRecordsAsync()) + return records; + } + + public static async IAsyncEnumerable EnumerateDynamicRecordsAsync(this CsvReader csvReader) + { + if (csvReader.Configuration.HasHeaderRecord && csvReader.HeaderRecord == null) + { + if (!await csvReader.ReadAsync().ConfigureAwait(false)) { - records.Add(record); + yield break; } - return records; + csvReader.ReadHeader(); } - public static async IAsyncEnumerable EnumerateDynamicRecordsAsync(this CsvReader csvReader) - { - if (csvReader.Configuration.HasHeaderRecord && csvReader.HeaderRecord == null) - { - if (!await csvReader.ReadAsync().ConfigureAwait(false)) - { - yield break; - } - - csvReader.ReadHeader(); - } + var csvHeader = new CsvHeader(csvReader.HeaderRecord + .Select((x, i) => csvReader.Configuration.PrepareHeaderForMatch(new PrepareHeaderForMatchArgs(x, i))) + .ToArray()); - var csvHeader = new CsvHeader(csvReader.HeaderRecord - .Select((x, i) => csvReader.Configuration.PrepareHeaderForMatch(new PrepareHeaderForMatchArgs(x, i))) - .ToArray()); + while (await csvReader.ReadAsync().ConfigureAwait(false)) + { + CsvRecord record; - while (await csvReader.ReadAsync().ConfigureAwait(false)) + try { - CsvRecord record; + var values = new object[csvReader.HeaderRecord.Length]; - try - { - var values = new object[csvReader.HeaderRecord.Length]; + Array.Copy(csvReader.Parser.Record, values, csvReader.HeaderRecord.Length); - Array.Copy(csvReader.Parser.Record, values, csvReader.HeaderRecord.Length); + record = new CsvRecord(csvHeader, values); + } + catch (Exception ex) + { + var readerException = new ReaderException(csvReader.Context, "An unexpected error occurred.", ex); - record = new CsvRecord(csvHeader, values); - } - catch (Exception ex) + if (csvReader.Configuration.ReadingExceptionOccurred?.Invoke(new ReadingExceptionOccurredArgs(readerException)) ?? true) { - var readerException = new ReaderException(csvReader.Context, "An unexpected error occurred.", ex); - - if (csvReader.Configuration.ReadingExceptionOccurred?.Invoke(new ReadingExceptionOccurredArgs(readerException)) ?? true) - { - throw readerException; - } - - continue; + throw readerException; } - yield return record; + continue; } + + yield return record; } + } #endif - } } diff --git a/CsvHelper.FastDynamic/CsvRecord.cs b/CsvHelper.FastDynamic/CsvRecord.cs index 9dc3257..3bfb5b6 100644 --- a/CsvHelper.FastDynamic/CsvRecord.cs +++ b/CsvHelper.FastDynamic/CsvRecord.cs @@ -5,215 +5,212 @@ using System.Linq; using System.Linq.Expressions; -namespace CsvHelper.FastDynamic +namespace CsvHelper.FastDynamic; + +// Thanks, from https://github.com/StackExchange/Dapper/blob/master/Dapper/SqlMapper.DapperRow.cs +internal sealed class CsvRecord : IDictionary, IReadOnlyDictionary, IDynamicMetaObjectProvider { - // Thanks, from https://github.com/StackExchange/Dapper/blob/master/Dapper/SqlMapper.DapperRow.cs - internal sealed class CsvRecord : IDictionary, IReadOnlyDictionary, IDynamicMetaObjectProvider + private readonly CsvHeader _header; + private object[] _values; + + internal CsvRecord(CsvHeader header, object[] values) { - private readonly CsvHeader _header; - private object[] _values; + _header = header ?? throw new ArgumentNullException(nameof(header)); + _values = values ?? throw new ArgumentNullException(nameof(values)); + } - internal CsvRecord(CsvHeader header, object[] values) - { - _header = header ?? throw new ArgumentNullException(nameof(header)); - _values = values ?? throw new ArgumentNullException(nameof(values)); - } + private sealed class DeadValue + { + public static readonly DeadValue Default = new(); + private DeadValue() { } + } - private sealed class DeadValue - { - public static readonly DeadValue Default = new DeadValue(); - private DeadValue() { } - } + #region IEnumerable> - #region IEnumerable> + public IEnumerator> GetEnumerator() + { + var names = _header.FieldNames; - public IEnumerator> GetEnumerator() + for (var i = 0; i < names.Length; i++) { - var names = _header.FieldNames; + var value = i < _values.Length ? _values[i] : null; - for (var i = 0; i < names.Length; i++) + if (value is not DeadValue) { - var value = i < _values.Length ? _values[i] : null; - - if (!(value is DeadValue)) - { - yield return new KeyValuePair(names[i], value); - } + yield return new KeyValuePair(names[i], value); } } + } - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - #endregion + #endregion - #region ICollection> + #region ICollection> - void ICollection>.Add(KeyValuePair item) - => ((IDictionary)this).Add(item.Key, item.Value); + void ICollection>.Add(KeyValuePair item) => ((IDictionary)this).Add(item.Key, item.Value); - void ICollection>.Clear() + void ICollection>.Clear() + { + for (var i = 0; i < _values.Length; i++) { - for (var i = 0; i < _values.Length; i++) - { - _values[i] = DeadValue.Default; - } + _values[i] = DeadValue.Default; } + } - bool ICollection>.Contains(KeyValuePair item) - => TryGetValue(item.Key, out var value) && Equals(value, item.Value); + bool ICollection>.Contains(KeyValuePair item) + => TryGetValue(item.Key, out var value) && Equals(value, item.Value); - void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + foreach (var item in this) { - foreach (var item in this) - { - array[arrayIndex++] = item; - } + array[arrayIndex++] = item; } + } - bool ICollection>.Remove(KeyValuePair item) - => ((IDictionary)this).Remove(item.Key); + bool ICollection>.Remove(KeyValuePair item) + => ((IDictionary)this).Remove(item.Key); - int ICollection>.Count => _values.Count(t => !(t is DeadValue)); + int ICollection>.Count => _values.Count(t => t is not DeadValue); - bool ICollection>.IsReadOnly => false; + bool ICollection>.IsReadOnly => false; - #endregion + #endregion - #region IDictionary + #region IDictionary - void IDictionary.Add(string key, object value) => SetValue(key, value, true); + void IDictionary.Add(string key, object value) => SetValue(key, value, true); - bool IDictionary.ContainsKey(string key) => ContainsKey(key); + bool IDictionary.ContainsKey(string key) => ContainsKey(key); - bool IDictionary.Remove(string key) => Remove(_header.IndexOfName(key)); + bool IDictionary.Remove(string key) => Remove(_header.IndexOfName(key)); - bool IDictionary.TryGetValue(string key, out object value) => TryGetValue(key, out value); + bool IDictionary.TryGetValue(string key, out object value) => TryGetValue(key, out value); - object IDictionary.this[string key] - { - get => TryGetValue(key, out var value) ? value : null; - set => SetValue(key, value, false); - } + object IDictionary.this[string key] + { + get => TryGetValue(key, out var value) ? value : null; + set => SetValue(key, value, false); + } - ICollection IDictionary.Keys => this.Select(x => x.Key).ToArray(); + ICollection IDictionary.Keys => this.Select(x => x.Key).ToArray(); - ICollection IDictionary.Values => this.Select(x => x.Value).ToArray(); + ICollection IDictionary.Values => this.Select(x => x.Value).ToArray(); - #endregion + #endregion - #region IReadOnlyDictionary + #region IReadOnlyDictionary - bool IReadOnlyDictionary.ContainsKey(string key) => ContainsKey(key); + bool IReadOnlyDictionary.ContainsKey(string key) => ContainsKey(key); - int IReadOnlyCollection>.Count => _values.Count(x => !(x is DeadValue)); + int IReadOnlyCollection>.Count => _values.Count(x => x is not DeadValue); - object IReadOnlyDictionary.this[string key] - { - get => TryGetValue(key, out var value) ? value : null; - } + object IReadOnlyDictionary.this[string key] + { + get => TryGetValue(key, out var value) ? value : null; + } - bool IReadOnlyDictionary.TryGetValue(string key, out object value) => TryGetValue(key, out value); + bool IReadOnlyDictionary.TryGetValue(string key, out object value) => TryGetValue(key, out value); - IEnumerable IReadOnlyDictionary.Keys => this.Select(x => x.Key); + IEnumerable IReadOnlyDictionary.Keys => this.Select(x => x.Key); - IEnumerable IReadOnlyDictionary.Values => this.Select(x => x.Value); + IEnumerable IReadOnlyDictionary.Values => this.Select(x => x.Value); - #endregion + #endregion - #region IDynamicMetaObjectProvider + #region IDynamicMetaObjectProvider - public DynamicMetaObject GetMetaObject(Expression parameter) - => new CsvRecordMetaObject(parameter, BindingRestrictions.Empty, this); + public DynamicMetaObject GetMetaObject(Expression parameter) => new CsvRecordMetaObject(parameter, BindingRestrictions.Empty, this); - #endregion + #endregion - #region Internal Methods + #region Internal Methods - internal bool ContainsKey(string key) + internal bool ContainsKey(string key) + { + var index = _header.IndexOfName(key); + + return index >= 0 && index < _values.Length && _values[index] is not DeadValue; + } + + internal bool TryGetValue(string key, out object value) => TryGetValue(_header.IndexOfName(key), out value); + + internal bool TryGetValue(int index, out object value) + { + if (index < 0) { - var index = _header.IndexOfName(key); + value = null; - return index >= 0 && index < _values.Length && !(_values[index] is DeadValue); + return false; } - internal bool TryGetValue(string key, out object value) => TryGetValue(_header.IndexOfName(key), out value); + value = index < _values.Length ? _values[index] : null; - internal bool TryGetValue(int index, out object value) + if (value is DeadValue) { - if (index < 0) - { - value = null; + value = null; - return false; - } - - value = index < _values.Length ? _values[index] : null; + return false; + } - if (value is DeadValue) - { - value = null; + return true; + } - return false; - } + public object SetValue(string key, object value) + { + return SetValue(key, value, false); + } - return true; + internal object SetValue(string key, object value, bool isAdd) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); } - public object SetValue(string key, object value) + var index = _header.IndexOfName(key); + + if (index < 0) { - return SetValue(key, value, false); + index = _header.AddField(key); } - - internal object SetValue(string key, object value, bool isAdd) + else if (isAdd && index < _values.Length && _values[index] is not DeadValue) { - if (key == null) - { - throw new ArgumentNullException(nameof(key)); - } - - var index = _header.IndexOfName(key); + throw new ArgumentException("An item with the same name has already been added", nameof(key)); + } - if (index < 0) - { - index = _header.AddField(key); - } - else if (isAdd && index < _values.Length && !(_values[index] is DeadValue)) - { - throw new ArgumentException("An item with the same name has already been added", nameof(key)); - } + return SetValue(index, value); + } - return SetValue(index, value); - } + internal object SetValue(int index, object value) + { + var oldLength = _values.Length; - internal object SetValue(int index, object value) + if (oldLength <= index) { - var oldLength = _values.Length; + Array.Resize(ref _values, _header.FieldNames.Length); - if (oldLength <= index) + for (var i = oldLength; i < _values.Length; i++) { - Array.Resize(ref _values, _header.FieldNames.Length); - - for (var i = oldLength; i < _values.Length; i++) - { - _values[i] = DeadValue.Default; - } + _values[i] = DeadValue.Default; } - - return _values[index] = value; } - internal bool Remove(int index) - { - if (index < 0 || index >= _values.Length || _values[index] is DeadValue) - { - return false; - } - - _values[index] = DeadValue.Default; + return _values[index] = value; + } - return true; + internal bool Remove(int index) + { + if (index < 0 || index >= _values.Length || _values[index] is DeadValue) + { + return false; } - #endregion + _values[index] = DeadValue.Default; + + return true; } + + #endregion } diff --git a/CsvHelper.FastDynamic/CsvRecordMetaObject.cs b/CsvHelper.FastDynamic/CsvRecordMetaObject.cs index b65495c..67b97c1 100644 --- a/CsvHelper.FastDynamic/CsvRecordMetaObject.cs +++ b/CsvHelper.FastDynamic/CsvRecordMetaObject.cs @@ -4,86 +4,85 @@ using System.Linq.Expressions; using System.Reflection; -namespace CsvHelper.FastDynamic +namespace CsvHelper.FastDynamic; + +// Thanks, from https://github.com/StackExchange/Dapper/blob/master/Dapper/SqlMapper.DapperRowMetaObject.cs +internal sealed class CsvRecordMetaObject : DynamicMetaObject { - // Thanks, from https://github.com/StackExchange/Dapper/blob/master/Dapper/SqlMapper.DapperRowMetaObject.cs - internal sealed class CsvRecordMetaObject : DynamicMetaObject - { - private static readonly string[] EmptyArray = Array.Empty(); + private static readonly string[] s_emptyArray = Array.Empty(); - private static readonly MethodInfo GetValueMethod = typeof(IDictionary).GetProperty("Item").GetGetMethod(); - private static readonly MethodInfo SetValueMethod = typeof(CsvRecord).GetMethod(nameof(CsvRecord.SetValue), new[] { typeof(string), typeof(object) }); + private static readonly MethodInfo s_getValueMethod = typeof(IDictionary).GetProperty("Item").GetGetMethod(); + private static readonly MethodInfo s_setValueMethod = typeof(CsvRecord).GetMethod(nameof(CsvRecord.SetValue), new[] { typeof(string), typeof(object) }); - public CsvRecordMetaObject(Expression expression, BindingRestrictions restrictions, object value) - : base(expression, restrictions, value) - { - } + public CsvRecordMetaObject(Expression expression, BindingRestrictions restrictions, object value) + : base(expression, restrictions, value) + { + } - public override DynamicMetaObject BindGetIndex(GetIndexBinder binder, DynamicMetaObject[] indexes) + public override DynamicMetaObject BindGetIndex(GetIndexBinder binder, DynamicMetaObject[] indexes) + { + var parameters = new[] { - var parameters = new Expression[] - { - indexes[0].Expression - }; + indexes[0].Expression + }; - return CallMethod(GetValueMethod, parameters); - } + return CallMethod(s_getValueMethod, parameters); + } - public override DynamicMetaObject BindGetMember(GetMemberBinder binder) + public override DynamicMetaObject BindGetMember(GetMemberBinder binder) + { + var parameters = new Expression[] { - var parameters = new Expression[] - { - Expression.Constant(binder.Name) - }; + Expression.Constant(binder.Name) + }; - return CallMethod(GetValueMethod, parameters); - } + return CallMethod(s_getValueMethod, parameters); + } - public override DynamicMetaObject BindInvokeMember(InvokeMemberBinder binder, DynamicMetaObject[] args) + public override DynamicMetaObject BindInvokeMember(InvokeMemberBinder binder, DynamicMetaObject[] args) + { + var parameters = new Expression[] { - var parameters = new Expression[] - { - Expression.Constant(binder.Name) - }; + Expression.Constant(binder.Name) + }; - return CallMethod(GetValueMethod, parameters); - } + return CallMethod(s_getValueMethod, parameters); + } - public override DynamicMetaObject BindSetIndex(SetIndexBinder binder, DynamicMetaObject[] indexes, DynamicMetaObject value) + public override DynamicMetaObject BindSetIndex(SetIndexBinder binder, DynamicMetaObject[] indexes, DynamicMetaObject value) + { + var parameters = new[] { - var parameters = new Expression[] - { - indexes[0].Expression, - value.Expression - }; + indexes[0].Expression, + value.Expression + }; - return CallMethod(SetValueMethod, parameters); - } + return CallMethod(s_setValueMethod, parameters); + } - public override DynamicMetaObject BindSetMember(SetMemberBinder binder, DynamicMetaObject value) + public override DynamicMetaObject BindSetMember(SetMemberBinder binder, DynamicMetaObject value) + { + var parameters = new[] { - var parameters = new Expression[] - { - Expression.Constant(binder.Name), - value.Expression - }; + Expression.Constant(binder.Name), + value.Expression + }; - return CallMethod(SetValueMethod, parameters); - } + return CallMethod(s_setValueMethod, parameters); + } - public override IEnumerable GetDynamicMemberNames() - { - return HasValue && Value is IDictionary lookup ? lookup.Keys : EmptyArray; - } + public override IEnumerable GetDynamicMemberNames() + { + return HasValue && Value is IDictionary lookup ? lookup.Keys : s_emptyArray; + } - private DynamicMetaObject CallMethod(MethodInfo method, Expression[] parameters) - { - var callMethod = new DynamicMetaObject( - Expression.Call(Expression.Convert(Expression, LimitType), method, parameters), - BindingRestrictions.GetTypeRestriction(Expression, LimitType) - ); + private DynamicMetaObject CallMethod(MethodInfo method, Expression[] parameters) + { + var callMethod = new DynamicMetaObject( + Expression.Call(Expression.Convert(Expression, LimitType), method, parameters), + BindingRestrictions.GetTypeRestriction(Expression, LimitType) + ); - return callMethod; - } + return callMethod; } } diff --git a/CsvHelper.FastDynamic/CsvWriterExtensions.cs b/CsvHelper.FastDynamic/CsvWriterExtensions.cs index 8a99bfc..3d1d78e 100644 --- a/CsvHelper.FastDynamic/CsvWriterExtensions.cs +++ b/CsvHelper.FastDynamic/CsvWriterExtensions.cs @@ -3,124 +3,123 @@ using System.Linq; using System.Threading.Tasks; -namespace CsvHelper.FastDynamic +namespace CsvHelper.FastDynamic; + +public static class CsvWriterExtensions { - public static class CsvWriterExtensions + public static void WriteDynamicRecords(this CsvWriter csvWriter, IEnumerable records) { - public static void WriteDynamicRecords(this CsvWriter csvWriter, IEnumerable records) - { - var context = csvWriter.Context; - var hasHeaderBeenWritten = false; + var context = csvWriter.Context; + var hasHeaderBeenWritten = false; - foreach (var record in records) + foreach (var record in records) + { + if (!hasHeaderBeenWritten && context.Configuration.HasHeaderRecord) { - if (!hasHeaderBeenWritten && context.Configuration.HasHeaderRecord) - { - csvWriter.WriteHeaderInternal(record); - csvWriter.NextRecord(); - - hasHeaderBeenWritten = true; - } - - csvWriter.WriteRecordInternal(record); + csvWriter.WriteHeaderInternal(record); csvWriter.NextRecord(); + + hasHeaderBeenWritten = true; } + + csvWriter.WriteRecordInternal(record); + csvWriter.NextRecord(); } + } - public static async Task WriteDynamicRecordsAsync(this CsvWriter csvWriter, IEnumerable records) - { - var context = csvWriter.Context; - var hasHeaderBeenWritten = false; + public static async Task WriteDynamicRecordsAsync(this CsvWriter csvWriter, IEnumerable records) + { + var context = csvWriter.Context; + var hasHeaderBeenWritten = false; - foreach (var record in records) + foreach (var record in records) + { + if (!hasHeaderBeenWritten && context.Configuration.HasHeaderRecord) { - if (!hasHeaderBeenWritten && context.Configuration.HasHeaderRecord) - { - csvWriter.WriteHeaderInternal(record); - await csvWriter.NextRecordAsync().ConfigureAwait(false); - - hasHeaderBeenWritten = true; - } - - csvWriter.WriteRecordInternal(record); + csvWriter.WriteHeaderInternal(record); await csvWriter.NextRecordAsync().ConfigureAwait(false); + + hasHeaderBeenWritten = true; } + + csvWriter.WriteRecordInternal(record); + await csvWriter.NextRecordAsync().ConfigureAwait(false); } + } #if NETSTANDARD2_1 - public static async Task WriteDynamicRecordsAsync(this CsvWriter csvWriter, IAsyncEnumerable records) - { - var context = csvWriter.Context; - var hasHeaderBeenWritten = false; + public static async Task WriteDynamicRecordsAsync(this CsvWriter csvWriter, IAsyncEnumerable records) + { + var context = csvWriter.Context; + var hasHeaderBeenWritten = false; - await foreach (var record in records.ConfigureAwait(false)) + await foreach (var record in records.ConfigureAwait(false)) + { + if (!hasHeaderBeenWritten && context.Configuration.HasHeaderRecord) { - if (!hasHeaderBeenWritten && context.Configuration.HasHeaderRecord) - { - csvWriter.WriteHeaderInternal(record); - await csvWriter.NextRecordAsync().ConfigureAwait(false); - - hasHeaderBeenWritten = true; - } - - csvWriter.WriteRecordInternal(record); + csvWriter.WriteHeaderInternal(record); await csvWriter.NextRecordAsync().ConfigureAwait(false); + + hasHeaderBeenWritten = true; } + + csvWriter.WriteRecordInternal(record); + await csvWriter.NextRecordAsync().ConfigureAwait(false); } + } #endif - private static void WriteHeaderInternal(this CsvWriter csvWriter, object record) + private static void WriteHeaderInternal(this CsvWriter csvWriter, object record) + { + if (record is IReadOnlyDictionary dictionary) { - if (record is IReadOnlyDictionary dictionary) - { - var fieldNames = dictionary.Keys; - - if (csvWriter.Configuration.DynamicPropertySort != null) - { - fieldNames = fieldNames.OrderBy(x => x, csvWriter.Configuration.DynamicPropertySort); - } + var fieldNames = dictionary.Keys; - foreach (var fieldName in fieldNames) - { - csvWriter.WriteField(fieldName); - } - } - else if (record is IDynamicMetaObjectProvider dynamicObject) + if (csvWriter.Configuration.DynamicPropertySort != null) { - csvWriter.WriteDynamicHeader(dynamicObject); + fieldNames = fieldNames.OrderBy(x => x, csvWriter.Configuration.DynamicPropertySort); } - else + + foreach (var fieldName in fieldNames) { - csvWriter.WriteHeader(record.GetType()); + csvWriter.WriteField(fieldName); } } - - private static void WriteRecordInternal(this CsvWriter csvWriter, object record) + else if (record is IDynamicMetaObjectProvider dynamicObject) { - if (record is IReadOnlyDictionary dictionary) - { - var fieldNames = dictionary.Keys; + csvWriter.WriteDynamicHeader(dynamicObject); + } + else + { + csvWriter.WriteHeader(record.GetType()); + } + } - if (csvWriter.Configuration.DynamicPropertySort != null) - { - fieldNames = fieldNames.OrderBy(x => x, csvWriter.Configuration.DynamicPropertySort); - } + private static void WriteRecordInternal(this CsvWriter csvWriter, object record) + { + if (record is IReadOnlyDictionary dictionary) + { + var fieldNames = dictionary.Keys; - foreach (var fieldName in fieldNames) - { - csvWriter.WriteField(dictionary[fieldName]); - } - } - else if (record is IDynamicMetaObjectProvider) + if (csvWriter.Configuration.DynamicPropertySort != null) { - csvWriter.WriteRecord(record); + fieldNames = fieldNames.OrderBy(x => x, csvWriter.Configuration.DynamicPropertySort); } - else + + foreach (var fieldName in fieldNames) { - csvWriter.WriteRecord(record); + csvWriter.WriteField(dictionary[fieldName]); } } + else if (record is IDynamicMetaObjectProvider) + { + csvWriter.WriteRecord(record); + } + else + { + csvWriter.WriteRecord(record); + } } } diff --git a/README.md b/README.md index 1943dec..f033bbb 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@ [![NuGet](https://badgen.net/nuget/v/CsvHelper.FastDynamic)](https://www.nuget.org/packages/CsvHelper.FastDynamic/) [![License](https://badgen.net/github/license/shibayan/CsvHelper.FastDynamic)](https://github.com/shibayan/CsvHelper.FastDynamic/blob/master/LICENSE) -Fast dynamic records reader and writer extensions for [CsvHelper](https://github.com/JoshClose/CsvHelper) +Fast dynamic CSV records reader and writer extensions for [CsvHelper](https://github.com/JoshClose/CsvHelper) -## Install +## Installation ``` Install-Package CsvHelper.FastDynamic @@ -22,74 +22,68 @@ dotnet add package CsvHelper.FastDynamic ### Simple CSV Reader ```csharp -class Program -{ - static void Main(string[] args) - { - using var csvReader = new CsvReader(new StreamReader("sample.csv"), CultureInfo.InvariantCulture); +using CsvHelper; +using CsvHelper.FastDynamic; + +using var csvReader = new CsvReader(new StreamReader("sample.csv"), CultureInfo.InvariantCulture); - var records = csvReader.GetDynamicRecords(); +var records = csvReader.GetDynamicRecords(); - foreach (var record in records) - { - Console.WriteLine(record); - } - } +foreach (var @record in records) +{ + Console.WriteLine(record); } ``` ### Async CSV Enumerate (.NET Standard 2.1 / C# 8.0) ```csharp -class Program -{ - static async Task Main(string[] args) - { - using var csvReader = new CsvReader(new StreamReader("sample.csv"), CultureInfo.InvariantCulture); +using CsvHelper; +using CsvHelper.FastDynamic; + +using var csvReader = new CsvReader(new StreamReader("sample.csv"), CultureInfo.InvariantCulture); - var records = csvReader.EnumerateDynamicRecordsAsync(); +var records = csvReader.EnumerateDynamicRecordsAsync(); - await foreach (var record in records) - { - Console.WriteLine(record); - } - } +await foreach (var @record in records) +{ + Console.WriteLine(record); } ``` ## Performance -### Reader +### Dynamic record reader ``` -BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19044.1387 (21H2) +BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000 AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores -.NET SDK=6.0.100 - [Host] : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT - DefaultJob : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT +.NET SDK=6.0.200 + [Host] : .NET 6.0.2 (6.0.222.6406), X64 RyuJIT + DefaultJob : .NET 6.0.2 (6.0.222.6406), X64 RyuJIT | Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Allocated | |--------------------- |---------:|--------:|--------:|--------:|--------:|----------:| -| GetRecords | 868.6 us | 3.65 us | 3.23 us | 36.1328 | 17.5781 | 602 KB | -| GetDictionaryRecords | 254.7 us | 1.35 us | 1.19 us | 32.7148 | 16.1133 | 538 KB | -| GetDynamicRecords | 210.5 us | 1.07 us | 1.00 us | 25.6348 | 11.7188 | 420 KB | +| GetRecords | 834.0 us | 3.04 us | 2.85 us | 36.1328 | 17.5781 | 602 KB | +| GetDictionaryRecords | 247.4 us | 0.89 us | 0.78 us | 32.7148 | 16.1133 | 538 KB | +| GetDynamicRecords | 227.5 us | 0.85 us | 0.80 us | 28.3203 | 13.4277 | 464 KB | ``` -### Writer +### Dynamic record writer ``` -BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19044.1387 (21H2) +BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000 AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores -.NET SDK=6.0.100 - [Host] : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT - DefaultJob : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT +.NET SDK=6.0.200 + [Host] : .NET 6.0.2 (6.0.222.6406), X64 RyuJIT + DefaultJob : .NET 6.0.2 (6.0.222.6406), X64 RyuJIT -| Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Allocated | -|------------------------------------- |---------:|--------:|--------:|--------:|-------:|----------:| -| WriteRecords_DynamicObject | 818.5 us | 3.12 us | 2.92 us | 49.8047 | 9.7656 | 822 KB | -| WriteDynamicRecords_DynamicObject | 445.7 us | 2.05 us | 1.82 us | 7.8125 | 1.4648 | 134 KB | +| Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Allocated | +|---------------------------------- |---------:|--------:|--------:|--------:|-------:|----------:| +| WriteRecords_DynamicObject | 819.2 us | 0.85 us | 0.76 us | 49.8047 | 9.7656 | 822 KB | +| WriteDynamicRecords_DynamicObject | 455.0 us | 2.22 us | 1.97 us | 7.8125 | 1.4648 | 134 KB | ``` ## Thanks