Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/CsvHelper/CsvHelper.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@
<None Include="Icon.png" Pack="true" PackagePath="\" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Zomp.SyncMethodGenerator" Version="2.0.33">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<!-- .NET 4.6.2 -->
<ItemGroup Condition="'$(TargetFramework)' == 'net462'">
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" />
Expand Down
37 changes: 2 additions & 35 deletions src/CsvHelper/CsvParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace CsvHelper;
/// <summary>
/// Parses a CSV file.
/// </summary>
public class CsvParser : IParser, IDisposable
public partial class CsvParser : IParser, IDisposable
{
private readonly IParserConfiguration configuration;
private readonly FieldCache fieldCache = new FieldCache();
Expand Down Expand Up @@ -204,42 +204,9 @@ public CsvParser(TextReader reader, IParserConfiguration configuration, bool lea
processedFields = new string[128];
}

/// <inheritdoc/>
public bool Read()
{
isRecordProcessed = false;
rowStartPosition = bufferPosition;
fieldStartPosition = rowStartPosition;
fieldsPosition = 0;
quoteCount = 0;
row++;
rawRow++;
var c = '\0';
var cPrev = c;

while (true)
{
if (bufferPosition >= charsRead)
{
if (!FillBuffer())
{
return ReadEndOfFile();
}

if (row == 1 && detectDelimiter)
{
DetectDelimiter();
}
}

if (ReadLine(ref c, ref cPrev) == ReadLineResult.Complete)
{
return true;
}
}
}

/// <inheritdoc/>
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public async Task<bool> ReadAsync()
{
isRecordProcessed = false;
Expand Down
36 changes: 2 additions & 34 deletions src/CsvHelper/CsvReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace CsvHelper;
/// <summary>
/// Reads data that was parsed from <see cref="IParser" />.
/// </summary>
public class CsvReader : IReader
public partial class CsvReader : IReader
{
private readonly Lazy<RecordManager> recordManager;
private readonly bool detectColumnCountChanges;
Expand Down Expand Up @@ -237,41 +237,9 @@ protected virtual void ValidateHeader(ClassMap map, List<InvalidHeader> invalidH
}
}

/// <inheritdoc/>
public virtual bool Read()
{
// Don't forget about the async method below!

bool hasMoreRecords;
do
{
hasMoreRecords = parser.Read();
hasBeenRead = true;
}
while (hasMoreRecords && (shouldSkipRecord?.Invoke(new ShouldSkipRecordArgs(this)) ?? false));

currentIndex = -1;

if (detectColumnCountChanges && hasMoreRecords)
{
if (prevColumnCount > 0 && prevColumnCount != parser.Count)
{
var csvException = new BadDataException(string.Empty, parser.RawRecord, context, "An inconsistent number of columns has been detected.");

var args = new ReadingExceptionOccurredArgs(csvException);
if (readingExceptionOccurred?.Invoke(args) ?? true)
{
throw csvException;
}
}

prevColumnCount = parser.Count;
}

return hasMoreRecords;
}

/// <inheritdoc/>
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public virtual async Task<bool> ReadAsync()
{
bool hasMoreRecords;
Expand Down
145 changes: 15 additions & 130 deletions src/CsvHelper/CsvWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ namespace CsvHelper;
/// <summary>
/// Used to write CSV files.
/// </summary>
public class CsvWriter : IWriter
public partial class CsvWriter : IWriter
{
private readonly TextWriter writer;
private readonly CsvContext context;
Expand Down Expand Up @@ -346,119 +346,9 @@ public virtual void WriteRecord<T>(T? record)
}
}

/// <inheritdoc/>
public virtual void WriteRecords(IEnumerable records)
{
// Changes in this method require changes in method WriteRecords<T>(IEnumerable<T> records) also.

var enumerator = records.GetEnumerator();

try
{
if (!enumerator.MoveNext())
{
return;
}

if (WriteHeaderFromRecord(enumerator.Current))
{
NextRecord();
}

Action<object>? write = null;
RecordTypeInfo writeType = default;

do
{
var record = enumerator.Current;

if (record == null)
{
// Since every record could be a different type, just write a blank line.
NextRecord();
continue;
}

if (write == null || writeType.RecordType != record.GetType())
{
writeType = GetTypeInfoForRecord(record);
write = recordManager.Value.GetWriteDelegate<object>(writeType);
}

write(record);
NextRecord();
}
while (enumerator.MoveNext());
}
catch (Exception ex) when (ex is not CsvHelperException)
{
throw new WriterException(context, "An unexpected error occurred. See inner exception for details.", ex);
}
finally
{
if (enumerator is IDisposable en)
{
en.Dispose();
}
}
}

/// <inheritdoc/>
public virtual void WriteRecords<T>(IEnumerable<T> records)
{
// Changes in this method require changes in method WriteRecords(IEnumerable records) also.

var enumerator = records.GetEnumerator() ?? throw new InvalidOperationException("Enumerator is null.");

try
{
if (WriteHeaderFromType<T>())
{
NextRecord();
}

if (!enumerator.MoveNext())
{
return;
}

if (WriteHeaderFromRecord(enumerator.Current))
{
NextRecord();
}

Action<T>? write = null;
RecordTypeInfo writeType = default;

do
{
var record = enumerator.Current;

if (write == null || (record != null && writeType.RecordType != typeof(T)))
{
writeType = GetTypeInfoForRecord(record);
write = recordManager.Value.GetWriteDelegate<T>(writeType);
}

write(record);
NextRecord();
}
while (enumerator.MoveNext());
}
catch (Exception ex) when (ex is not CsvHelperException)
{
throw new WriterException(context, "An unexpected error occurred. See inner exception for details.", ex);
}
finally
{
if (enumerator is IDisposable en)
{
en.Dispose();
}
}
}

/// <inheritdoc/>
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public virtual async Task WriteRecordsAsync(IEnumerable records, CancellationToken cancellationToken = default)
{
// These methods should all be the same;
Expand Down Expand Up @@ -489,7 +379,14 @@ public virtual async Task WriteRecordsAsync(IEnumerable records, CancellationTok

var record = enumerator.Current;

if (write == null || (record != null && writeType.RecordType != record.GetType()))
if (record == null)
{
// Since every record could be a different type, just write a blank line.
await NextRecordAsync().ConfigureAwait(false);
continue;
}

if (write == null || writeType.RecordType != record.GetType())
{
writeType = GetTypeInfoForRecord(record);
write = recordManager.Value.GetWriteDelegate<object?>(writeType);
Expand All @@ -514,6 +411,7 @@ public virtual async Task WriteRecordsAsync(IEnumerable records, CancellationTok
}

/// <inheritdoc/>
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public virtual async Task WriteRecordsAsync<T>(IEnumerable<T> records, CancellationToken cancellationToken = default)
{
// These methods should all be the same;
Expand Down Expand Up @@ -574,6 +472,8 @@ public virtual async Task WriteRecordsAsync<T>(IEnumerable<T> records, Cancellat
}

/// <inheritdoc/>
[Zomp.SyncMethodGenerator.CreateSyncVersion]
[Zomp.SyncMethodGenerator.SkipSyncVersion]
public virtual async Task WriteRecordsAsync<T>(IAsyncEnumerable<T> records, CancellationToken cancellationToken = default)
{
// These methods should all be the same;
Expand Down Expand Up @@ -631,16 +531,7 @@ public virtual async Task WriteRecordsAsync<T>(IAsyncEnumerable<T> records, Canc
}

/// <inheritdoc/>
public virtual void NextRecord()
{
WriteToBuffer(newLine);
FlushBuffer();

index = 0;
row++;
}

/// <inheritdoc/>
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public virtual async Task NextRecordAsync()
{
WriteToBuffer(newLine);
Expand All @@ -651,13 +542,7 @@ public virtual async Task NextRecordAsync()
}

/// <inheritdoc/>
public virtual void Flush()
{
FlushBuffer();
writer.Flush();
}

/// <inheritdoc/>
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public virtual async Task FlushAsync()
{
await FlushBufferAsync().ConfigureAwait(false);
Expand Down
53 changes: 53 additions & 0 deletions tests/CsvHelper.Tests/Writing/WriteNullTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Threading.Tasks;
using Xunit;

namespace CsvHelper.Tests.Writing
Expand Down Expand Up @@ -80,6 +81,58 @@ public void WriteRecord_RecordIsNull_WritesEmptyRecord()
}
}

[Fact]
public async Task WriteRecordsEnumerableGenericAsync_RecordIsNull_WritesEmptyRecord()
{
var records = new List<Foo?>
{
new Foo { Id = 1, Name = "one"},
null,
new Foo { Id = 2, Name = "two" },
};
var config = new CsvConfiguration(CultureInfo.InvariantCulture);
using (var writer = new StringWriter())
using (var csv = new CsvWriter(writer, config))
{
await csv.WriteRecordsAsync(records);
await csv.FlushAsync();

var expected = new TestStringBuilder(config.NewLine);
expected.AppendLine("Id,Name");
expected.AppendLine("1,one");
expected.AppendLine(",");
expected.AppendLine("2,two");

Assert.Equal(expected, writer.ToString());
}
}

[Fact]
public async Task WriteRecordsEnumerableAsync_RecordIsNull_WritesEmptyRecord()
{
IEnumerable records = new List<Foo?>
{
new Foo { Id = 1, Name = "one"},
null,
new Foo { Id = 2, Name = "two" },
};
var config = new CsvConfiguration(CultureInfo.InvariantCulture);
using (var writer = new StringWriter())
using (var csv = new CsvWriter(writer, config))
{
await csv.WriteRecordsAsync(records);
await csv.FlushAsync();

var expected = new TestStringBuilder(config.NewLine);
expected.AppendLine("Id,Name");
expected.AppendLine("1,one");
expected.AppendLine("");
expected.AppendLine("2,two");

Assert.Equal(expected, writer.ToString());
}
}

private class Foo
{
public int Id { get; set; }
Expand Down