From a2ab505ff13fd3b3c1be70755e21e54153fae7a5 Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Mon, 14 Sep 2026 15:51:40 +0200 Subject: [PATCH] Read from CSV: retarget net10.0, fix the discarded configuration, add the missing data file and real tests - ReadFromCsv and ReadFromCsvTests move from net6.0 to net10.0 - CsvHelper 27.2.1 to 33.1.0, Microsoft.NET.Test.Sdk 16.11.0 to 18.10.0, MSTest 2.2.7 to 4.4.0, coverlet.collector 3.1.0 to 10.0.1 - ReadPersons() now passes its CsvConfiguration to the CsvReader, so HasHeaderRecord = false takes effect and the first data row is no longer consumed as a header, and materialises the records with ToList() so the file is actually read - Add the missing filePersons.csv and copy it to the output folder, so the sample runs instead of throwing FileNotFoundException - File-scoped namespaces, and PersonMap moves into the ReadFromCsv namespace - Tests assert field values instead of a count that cannot fail, with the expected data matching the input and dates parsed with InvariantCulture, plus new tests for the headerless read, a quoted comma, comments with and without AllowComments, TextFieldParser against string.Split, and ReadingExceptionOccurred skipping a bad row --- .../ReadFromCsv/ReadFromCsv/Person.cs | 25 +- .../ReadFromCsv/ReadFromCsv/PersonMap.cs | 23 +- .../ReadFromCsv/ReadFromCsv/Program.cs | 7 +- .../ReadFromCsv/ReadFromCsv.csproj | 10 +- .../ReadFromCsv/ReadFromCsv/ReadMethods.cs | 32 ++- .../ReadFromCsv/ReadFromCsv/filePersons.csv | 3 + .../ReadFromCsvTests/ReadFromCsvTests.cs | 218 +++++++++++++++--- .../ReadFromCsvTests/ReadFromCsvTests.csproj | 12 +- 8 files changed, 250 insertions(+), 80 deletions(-) create mode 100644 files-csharp/ReadFromCsv/ReadFromCsv/filePersons.csv diff --git a/files-csharp/ReadFromCsv/ReadFromCsv/Person.cs b/files-csharp/ReadFromCsv/ReadFromCsv/Person.cs index 143100388b..d95e47dd80 100644 --- a/files-csharp/ReadFromCsv/ReadFromCsv/Person.cs +++ b/files-csharp/ReadFromCsv/ReadFromCsv/Person.cs @@ -1,16 +1,15 @@ -using CsvHelper.Configuration.Attributes; +using CsvHelper.Configuration.Attributes; -namespace ReadFromCsv +namespace ReadFromCsv; + +public class Person { - public class Person - { - [Index(0)] - public int Id { get; set; } - [Index(1)] - public string? Name { get; set; } - [Index(2)] - public bool IsLiving { get; set; } - [Index(3)] - public DateTime DateOfBirth { get; set; } - } + [Index(0)] + public int Id { get; set; } + [Index(1)] + public string? Name { get; set; } + [Index(2)] + public bool IsLiving { get; set; } + [Index(3)] + public DateTime DateOfBirth { get; set; } } diff --git a/files-csharp/ReadFromCsv/ReadFromCsv/PersonMap.cs b/files-csharp/ReadFromCsv/ReadFromCsv/PersonMap.cs index 06575cc163..276fba56d3 100644 --- a/files-csharp/ReadFromCsv/ReadFromCsv/PersonMap.cs +++ b/files-csharp/ReadFromCsv/ReadFromCsv/PersonMap.cs @@ -1,12 +1,13 @@ -using CsvHelper.Configuration; -using ReadFromCsv; +using CsvHelper.Configuration; -public class PersonMap : ClassMap -{ - public PersonMap() - { - Map(p => p.Id).Index(0); - Map(p => p.Name).Index(1); - Map(p => p.IsLiving).Index(2); - } -} \ No newline at end of file +namespace ReadFromCsv; + +public class PersonMap : ClassMap +{ + public PersonMap() + { + Map(p => p.Id).Index(0); + Map(p => p.Name).Index(1); + Map(p => p.IsLiving).Index(2); + } +} diff --git a/files-csharp/ReadFromCsv/ReadFromCsv/Program.cs b/files-csharp/ReadFromCsv/ReadFromCsv/Program.cs index c6999f0586..8c52308b24 100644 --- a/files-csharp/ReadFromCsv/ReadFromCsv/Program.cs +++ b/files-csharp/ReadFromCsv/ReadFromCsv/Program.cs @@ -1,3 +1,6 @@ -using ReadFromCsv; +using ReadFromCsv; -ReadMethods.ReadPersons(); \ No newline at end of file +foreach (var person in ReadMethods.ReadPersons()) +{ + Console.WriteLine($"{person.Id} {person.Name} {person.IsLiving}"); +} diff --git a/files-csharp/ReadFromCsv/ReadFromCsv/ReadFromCsv.csproj b/files-csharp/ReadFromCsv/ReadFromCsv/ReadFromCsv.csproj index 0658f11724..4402dfd6a2 100644 --- a/files-csharp/ReadFromCsv/ReadFromCsv/ReadFromCsv.csproj +++ b/files-csharp/ReadFromCsv/ReadFromCsv/ReadFromCsv.csproj @@ -2,13 +2,19 @@ Exe - net6.0 + net10.0 enable enable - + + + + + + PreserveNewest + diff --git a/files-csharp/ReadFromCsv/ReadFromCsv/ReadMethods.cs b/files-csharp/ReadFromCsv/ReadFromCsv/ReadMethods.cs index 46a8d3e422..d861efea29 100644 --- a/files-csharp/ReadFromCsv/ReadFromCsv/ReadMethods.cs +++ b/files-csharp/ReadFromCsv/ReadFromCsv/ReadMethods.cs @@ -1,27 +1,25 @@ -using CsvHelper; +using CsvHelper; using CsvHelper.Configuration; using System.Globalization; -namespace ReadFromCsv +namespace ReadFromCsv; + +public static class ReadMethods { - public static class ReadMethods + public static List ReadPersons() { - public static void ReadPersons() + var configuration = new CsvConfiguration(CultureInfo.InvariantCulture) { - var configuration = new CsvConfiguration(CultureInfo.InvariantCulture) - { - Delimiter = ",", - Comment = '#', - HasHeaderRecord = false - }; + Delimiter = ",", + Comment = '#', + HasHeaderRecord = false + }; + + using var reader = new StreamReader("filePersons.csv"); + using var csv = new CsvReader(reader, configuration); - using (var reader = new StreamReader("filePersons.csv")) - using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture)) - { - csv.Context.RegisterClassMap(); - var persons = csv.GetRecords(); - } + csv.Context.RegisterClassMap(); - } + return csv.GetRecords().ToList(); } } diff --git a/files-csharp/ReadFromCsv/ReadFromCsv/filePersons.csv b/files-csharp/ReadFromCsv/ReadFromCsv/filePersons.csv new file mode 100644 index 0000000000..7ca8ae4fe6 --- /dev/null +++ b/files-csharp/ReadFromCsv/ReadFromCsv/filePersons.csv @@ -0,0 +1,3 @@ +1,John,True,03/05/2006 +2,Steve,False,03/05/2006 +3,James,True,03/05/2006 diff --git a/files-csharp/ReadFromCsv/ReadFromCsvTests/ReadFromCsvTests.cs b/files-csharp/ReadFromCsv/ReadFromCsvTests/ReadFromCsvTests.cs index 16fadf3aef..80f405643d 100644 --- a/files-csharp/ReadFromCsv/ReadFromCsvTests/ReadFromCsvTests.cs +++ b/files-csharp/ReadFromCsv/ReadFromCsvTests/ReadFromCsvTests.cs @@ -1,5 +1,7 @@ using CsvHelper; using CsvHelper.Configuration; +using CsvHelper.TypeConversion; +using Microsoft.VisualBasic.FileIO; using Microsoft.VisualStudio.TestTools.UnitTesting; using ReadFromCsv; using System; @@ -9,41 +11,199 @@ using System.Linq; using System.Text; -namespace ReadFromCsvTests +namespace ReadFromCsvTests; + +[TestClass] +public class ReadFromCsvTests { - [TestClass] - public class ReadFromCsvTests + private static CsvReader CreateReader(string csvContent, CsvConfiguration configuration, out StreamReader reader) + { + var stream = new MemoryStream(Encoding.UTF8.GetBytes(csvContent)); + + reader = new StreamReader(stream); + + return new CsvReader(reader, configuration); + } + + [TestMethod] + public void GivenHeaderlessCsvFile_WhenReadingPersons_ThenEveryFieldIsMapped() { - [TestMethod] - public void GivenCsvFile_WhenReadingPersonsFromCsv_ThenListOfPersonsIsPopulated() + var csvFile = "1,John,True,03/05/2006 00:00:00\n" + + "2,Steve,False,03/05/2006 00:00:00\n" + + "3,James,True,03/05/2006 00:00:00"; + + var expectedPersons = new List { - string csvFile = - @"1, John, True, 03/05/2006 00:00:00 -2, Steve, False, 03/05/2006 00:00:00 -3, James, True, 03/05/2006 00:00:00"; + new() { Id = 1, Name = "John", IsLiving = true, DateOfBirth = DateTime.Parse("03/05/2006", CultureInfo.InvariantCulture) }, + new() { Id = 2, Name = "Steve", IsLiving = false, DateOfBirth = DateTime.Parse("03/05/2006", CultureInfo.InvariantCulture) }, + new() { Id = 3, Name = "James", IsLiving = true, DateOfBirth = DateTime.Parse("03/05/2006", CultureInfo.InvariantCulture) } + }; - var expectedPersons = new List() - { - new Person { Id = 1, IsLiving = true, Name = "John", DateOfBirth = Convert.ToDateTime("03/05/2006") }, - new Person { Id = 2, IsLiving = true, Name = "Steve", DateOfBirth = Convert.ToDateTime("03/09/1998") }, - new Person { Id = 3, IsLiving = true, Name = "James", DateOfBirth = Convert.ToDateTime("03/08/1994") } - }; + var configuration = new CsvConfiguration(CultureInfo.InvariantCulture) + { + HasHeaderRecord = false + }; - using (var test_Stream = new MemoryStream(Encoding.UTF8.GetBytes(csvFile))) + using var csv = CreateReader(csvFile, configuration, out var reader); + using (reader) + { + var persons = csv.GetRecords().ToList(); + + Assert.AreEqual(expectedPersons.Count, persons.Count); + + for (var i = 0; i < expectedPersons.Count; i++) { - var config = new CsvConfiguration(CultureInfo.InvariantCulture) - { - HasHeaderRecord = false, - }; - - using (var reader = new StreamReader(test_Stream)) - using (var csv = new CsvReader(reader, config)) - { - var records = csv.GetRecords(); - - Assert.AreEqual(expectedPersons.Count, records.Count()); - } + Assert.AreEqual(expectedPersons[i].Id, persons[i].Id); + Assert.AreEqual(expectedPersons[i].Name, persons[i].Name); + Assert.AreEqual(expectedPersons[i].IsLiving, persons[i].IsLiving); + Assert.AreEqual(expectedPersons[i].DateOfBirth, persons[i].DateOfBirth); } } } -} \ No newline at end of file + + [TestMethod] + public void GivenHeaderlessCsvFile_WhenTheConfigurationIsNotPassedToTheReader_ThenTheFirstRowIsLost() + { + var csvFile = "1,John,True,03/05/2006 00:00:00\n2,Steve,False,03/05/2006 00:00:00"; + + var configuration = new CsvConfiguration(CultureInfo.InvariantCulture) + { + HasHeaderRecord = false + }; + + using (var csv = CreateReader(csvFile, configuration, out var withConfiguration)) + using (withConfiguration) + { + Assert.AreEqual(2, csv.GetRecords().ToList().Count); + } + + var cultureOnly = new CsvConfiguration(CultureInfo.InvariantCulture); + + using (var csv = CreateReader(csvFile, cultureOnly, out var withCulture)) + using (withCulture) + { + var persons = csv.GetRecords().ToList(); + + Assert.AreEqual(1, persons.Count); + Assert.AreEqual(2, persons[0].Id); + } + } + + [TestMethod] + public void GivenAQuotedComma_WhenReadingTheRow_ThenTheFieldSurvivesIntact() + { + var csvFile = "Id,Name,IsLiving,DateOfBirth\n1,\"Close, Josh\",true,03/05/2006 00:00:00"; + + var configuration = new CsvConfiguration(CultureInfo.InvariantCulture); + + using var csv = CreateReader(csvFile, configuration, out var reader); + using (reader) + { + var persons = csv.GetRecords().ToList(); + + Assert.AreEqual(1, persons.Count); + Assert.AreEqual("Close, Josh", persons[0].Name); + } + } + + [TestMethod] + public void GivenACommentLine_WhenAllowCommentsIsFalse_ThenTheCommentIsReadAsData() + { + var csvFile = "1;John;True;03/05/2006 00:00:00\n% a comment line\n2;Steve;False;03/05/2006 00:00:00"; + + var configuration = new CsvConfiguration(CultureInfo.InvariantCulture) + { + HasHeaderRecord = false, + Delimiter = ";", + Comment = '%' + }; + + Assert.IsFalse(configuration.AllowComments); + + using var csv = CreateReader(csvFile, configuration, out var reader); + using (reader) + { + Assert.ThrowsExactly(() => csv.GetRecords().ToList()); + } + } + + [TestMethod] + public void GivenACommentLine_WhenAllowCommentsIsTrue_ThenTheCommentIsSkipped() + { + var csvFile = "1;John;True;03/05/2006 00:00:00\n% a comment line\n2;Steve;False;03/05/2006 00:00:00"; + + var configuration = new CsvConfiguration(CultureInfo.InvariantCulture) + { + HasHeaderRecord = false, + Delimiter = ";", + Comment = '%', + AllowComments = true + }; + + using var csv = CreateReader(csvFile, configuration, out var reader); + using (reader) + { + var persons = csv.GetRecords().ToList(); + + Assert.AreEqual(2, persons.Count); + Assert.AreEqual("Steve", persons[1].Name); + } + } + + [TestMethod] + public void GivenAQuotedComma_WhenSplittingTheLine_ThenSplitBreaksItAndTextFieldParserDoesNot() + { + var row = "1,\"Close, Josh\",true,03/05/2006 00:00:00"; + + Assert.AreEqual(5, row.Split(',').Length); + + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(row)); + using var parser = new TextFieldParser(stream) + { + TextFieldType = FieldType.Delimited, + HasFieldsEnclosedInQuotes = true + }; + + parser.SetDelimiters(","); + + var fields = parser.ReadFields(); + + Assert.IsNotNull(fields); + Assert.AreEqual(4, fields.Length); + Assert.AreEqual("Close, Josh", fields[1]); + } + + [TestMethod] + public void GivenAnUnconvertibleField_WhenReadingExceptionOccurredReturnsFalse_ThenTheBadRowIsSkipped() + { + var csvFile = "1,John,True,03/05/2006 00:00:00\n" + + "2,Steve,notabool,03/05/2006 00:00:00\n" + + "3,James,True,03/05/2006 00:00:00"; + + var strict = new CsvConfiguration(CultureInfo.InvariantCulture) + { + HasHeaderRecord = false + }; + + using (var csv = CreateReader(csvFile, strict, out var strictReader)) + using (strictReader) + { + Assert.ThrowsExactly(() => csv.GetRecords().ToList()); + } + + var tolerant = new CsvConfiguration(CultureInfo.InvariantCulture) + { + HasHeaderRecord = false, + ReadingExceptionOccurred = args => false + }; + + using (var csv = CreateReader(csvFile, tolerant, out var tolerantReader)) + using (tolerantReader) + { + var persons = csv.GetRecords().ToList(); + + Assert.AreEqual(2, persons.Count); + Assert.AreEqual(3, persons[1].Id); + } + } +} diff --git a/files-csharp/ReadFromCsv/ReadFromCsvTests/ReadFromCsvTests.csproj b/files-csharp/ReadFromCsv/ReadFromCsvTests/ReadFromCsvTests.csproj index 935d103d3b..3b2b9c0eca 100644 --- a/files-csharp/ReadFromCsv/ReadFromCsvTests/ReadFromCsvTests.csproj +++ b/files-csharp/ReadFromCsv/ReadFromCsvTests/ReadFromCsvTests.csproj @@ -1,18 +1,18 @@ - net6.0 + net10.0 enable false - - - - - + + + + +