Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
fb4e066
Implement real async instead of Task.Run wrappers
virzak Jun 10, 2025
fe8a036
Use CreateSyncVersion in interface
virzak Jun 12, 2025
3f25658
Fix Async tests
virzak Jun 12, 2025
d9d0791
Decorate with ConfigureAsync(false) and remove all Task<IEnumerable
virzak Jun 12, 2025
7e2049a
Fix source generation for ConfigureAwait after #endif
virzak Jun 12, 2025
38d460a
Add ConfigureAwait where missed
virzak Jun 13, 2025
295b691
Convert everything in ExcelOpenXmlSheetReader to Async
virzak Jun 13, 2025
20e74d3
Convert a bunch of stuff to Async
virzak Jun 14, 2025
0873d0d
Generate WriteCell from WriteCellAsync
virzak Jun 14, 2025
00f07f0
Remove existing duplicate sync code
virzak Jun 14, 2025
61c3239
Factor out ExcelOpenXmlSheetWriter where sync and async do not diverge
virzak Jun 14, 2025
39b2b1e
Use ExcelOpenXmlSheetReader.CreateAsync
virzak Jun 15, 2025
e3087d7
Rewrite all public methods to Async and eliminate the need for MiniEx…
virzak Jun 15, 2025
4a368f3
Expose AddPictureAsync
virzak Jun 15, 2025
f3285ac
Fix some diverging methods and remove sync version
virzak Jun 15, 2025
8128822
Remove preprocessor directives made unnecessary by NET462 upgrade
virzak Jun 15, 2025
14236b5
Remove WriteValues for WriteValuesAsync. Do the best to account for d…
virzak Jun 15, 2025
3c176a9
Change ct to cancellationToken everywhere
virzak Jun 15, 2025
9b3993d
Remove async code from generated sync code
virzak Jun 15, 2025
dd630fe
Pass all tests and clean up
virzak Jun 15, 2025
8a7f175
Enforce .ConfigureAwait(false) and passing CancellationToken through …
virzak Jun 16, 2025
c04890f
Enforce calling async methods in async context
virzak Jun 16, 2025
3ed0c61
Enforce disposing disposable objects
virzak Jun 16, 2025
e05dc99
Fix test failures from previous commit
virzak Jun 16, 2025
38551a3
Remove redundant WithCancellation
virzak Jun 16, 2025
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
16 changes: 15 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,18 @@ dotnet_style_qualification_for_property = false
dotnet_style_qualification_for_method = false

# IDE0065: Misplaced using directive
csharp_using_directive_placement = outside_namespace
csharp_using_directive_placement = outside_namespace

# CA1835: Prefer the memory-based overloads of ReadAsync/WriteAsync methods in stream-based classes
dotnet_diagnostic.CA1835.severity = error

# CA1849: Call async methods when in an async method
dotnet_diagnostic.CA1849.severity = error

dotnet_diagnostic.CA2000.severity = error

# CA2007: Do not directly await a Task
dotnet_diagnostic.CA2007.severity = error

# CA2016: Forward the CancellationToken parameter to methods that take one
dotnet_diagnostic.CA2016.severity = error
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
<RootNamespace>MiniExcelLibs.Benchmarks</RootNamespace>
<NoWarn>$(NoWarn);CA2000;CA2007</NoWarn>
</PropertyGroup>

<ItemGroup>
Expand Down
77 changes: 32 additions & 45 deletions src/MiniExcel/Csv/CsvReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;

namespace MiniExcelLibs.Csv
{
internal class CsvReader : IExcelReader
internal partial class CsvReader : IExcelReader
{
private Stream _stream;
private CsvConfiguration _config;
Expand All @@ -22,8 +23,11 @@ public CsvReader(Stream stream, IConfiguration configuration)
_config = configuration == null ? CsvConfiguration.DefaultConfiguration : (CsvConfiguration)configuration;
}

public IEnumerable<IDictionary<string, object>> Query(bool useHeaderRow, string sheetName, string startCell)
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public async IAsyncEnumerable<IDictionary<string, object>> QueryAsync(bool useHeaderRow, string sheetName, string startCell, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

if (startCell != "A1")
throw new NotImplementedException("CSV does not implement parameter startCell");

Expand All @@ -35,14 +39,22 @@ public IEnumerable<IDictionary<string, object>> Query(bool useHeaderRow, string
var headRows = new Dictionary<int, string>();

string row;
for (var rowIndex = 1; (row = reader.ReadLine()) != null; rowIndex++)
for (var rowIndex = 1; (row = await reader.ReadLineAsync(
#if NET7_0_OR_GREATER
cancellationToken
#endif
).ConfigureAwait(false)) != null; rowIndex++)
{
string finalRow = row;
if (_config.ReadLineBreaksWithinQuotes)
{
while (finalRow.Count(c => c == '"') % 2 != 0)
{
var nextPart = reader.ReadLine();
var nextPart = await reader.ReadLineAsync(
#if NET7_0_OR_GREATER
cancellationToken
#endif
).ConfigureAwait(false);
if (nextPart == null)
{
break;
Expand Down Expand Up @@ -107,62 +119,37 @@ public IEnumerable<IDictionary<string, object>> Query(bool useHeaderRow, string
}
}

public IEnumerable<T> Query<T>(string sheetName, string startCell, bool hasHeader) where T : class, new()
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public IAsyncEnumerable<T> QueryAsync<T>(string sheetName, string startCell, bool hasHeader, CancellationToken cancellationToken = default) where T : class, new()
{
var dynamicRecords = Query(false, sheetName, startCell);
return ExcelOpenXmlSheetReader.QueryImpl<T>(dynamicRecords, startCell, hasHeader, _config);
var dynamicRecords = QueryAsync(false, sheetName, startCell, cancellationToken);
return ExcelOpenXmlSheetReader.QueryImplAsync<T>(dynamicRecords, startCell, hasHeader, _config, cancellationToken);
}

public IEnumerable<IDictionary<string, object>> QueryRange(bool useHeaderRow, string sheetName, string startCell, string endCell)
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public IAsyncEnumerable<IDictionary<string, object>> QueryRangeAsync(bool useHeaderRow, string sheetName, string startCell, string endCell, CancellationToken cancellationToken = default)
{
throw new NotImplementedException("CSV does not implement QueryRange");
}

public IEnumerable<T> QueryRange<T>(string sheetName, string startCell, string endCell, bool hasHeader) where T : class, new()
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public IAsyncEnumerable<T> QueryRangeAsync<T>(string sheetName, string startCell, string endCell, bool hasHeader, CancellationToken cancellationToken = default) where T : class, new()
{
var dynamicRecords = QueryRange(false, sheetName, startCell, endCell);
return ExcelOpenXmlSheetReader.QueryImpl<T>(dynamicRecords, startCell, hasHeader, this._config);
var dynamicRecords = QueryRangeAsync(false, sheetName, startCell, endCell, cancellationToken);
return ExcelOpenXmlSheetReader.QueryImplAsync<T>(dynamicRecords, startCell, hasHeader, this._config, cancellationToken);
}

public IEnumerable<IDictionary<string, object>> QueryRange(bool useHeaderRow, string sheetName, int startRowIndex, int startColumnIndex, int? endRowIndex, int? endColumnIndex)
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public IAsyncEnumerable<IDictionary<string, object>> QueryRangeAsync(bool useHeaderRow, string sheetName, int startRowIndex, int startColumnIndex, int? endRowIndex, int? endColumnIndex, CancellationToken cancellationToken = default)
{
throw new NotImplementedException("CSV does not implement QueryRange");
}

public IEnumerable<T> QueryRange<T>(string sheetName, int startRowIndex, int startColumnIndex, int? endRowIndex, int? endColumnIndex, bool hasHeader) where T : class, new()
{
var dynamicRecords = QueryRange(false, sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex);
return ExcelOpenXmlSheetReader.QueryImpl<T>(dynamicRecords, ReferenceHelper.ConvertXyToCell(startRowIndex, startColumnIndex), hasHeader, this._config);
}

public Task<IEnumerable<IDictionary<string, object>>> QueryAsync(bool useHeaderRow, string sheetName, string startCell, CancellationToken cancellationToken = default)
{
return Task.Run(() => Query(useHeaderRow, sheetName, startCell), cancellationToken);
}

public async Task<IEnumerable<T>> QueryAsync<T>(string sheetName, string startCell, bool hasHeader, CancellationToken cancellationToken = default) where T : class, new()
{
return await Task.Run(() => Query<T>(sheetName, startCell, hasHeader), cancellationToken).ConfigureAwait(false);
}

public Task<IEnumerable<IDictionary<string, object>>> QueryRangeAsync(bool useHeaderRow, string sheetName, string startCell, string endCel, CancellationToken cancellationToken = default)
{
return Task.Run(() => QueryRange(useHeaderRow, sheetName, startCell, endCel), cancellationToken);
}

public async Task<IEnumerable<T>> QueryRangeAsync<T>(string sheetName, string startCell, string endCel, bool hasHeader, CancellationToken cancellationToken = default) where T : class, new()
{
return await Task.Run(() => QueryRange<T>(sheetName, startCell, endCel, hasHeader), cancellationToken).ConfigureAwait(false);
}

public Task<IEnumerable<IDictionary<string, object>>> QueryRangeAsync(bool useHeaderRow, string sheetName, int startRowIndex, int startColumnIndex, int? endRowIndex, int? endColumnIndex, CancellationToken cancellationToken = default)
{
return Task.Run(() => QueryRange(useHeaderRow, sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex), cancellationToken);
}

public async Task<IEnumerable<T>> QueryRangeAsync<T>(string sheetName, int startRowIndex, int startColumnIndex, int? endRowIndex, int? endColumnIndex, bool hasHeader, CancellationToken cancellationToken = default) where T : class, new()
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public IAsyncEnumerable<T> QueryRangeAsync<T>(string sheetName, int startRowIndex, int startColumnIndex, int? endRowIndex, int? endColumnIndex, bool hasHeader, CancellationToken cancellationToken = default) where T : class, new()
{
return await Task.Run(() => QueryRange<T>(sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex, hasHeader), cancellationToken).ConfigureAwait(false);
var dynamicRecords = QueryRangeAsync(false, sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex, cancellationToken);
return ExcelOpenXmlSheetReader.QueryImplAsync<T>(dynamicRecords, ReferenceHelper.ConvertXyToCell(startRowIndex, startColumnIndex), hasHeader, this._config, cancellationToken);
}

private string[] Split(string row)
Expand Down
76 changes: 60 additions & 16 deletions src/MiniExcel/Csv/CsvWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,22 +115,38 @@ private async Task<int> WriteValuesAsync(StreamWriter writer, object values, str
{
writeAdapter = MiniExcelWriteAdapterFactory.GetWriteAdapter(values, _configuration);
}
var props = writeAdapter != null ? writeAdapter.GetColumns() : await asyncWriteAdapter.GetColumnsAsync();
var props = writeAdapter != null ? writeAdapter.GetColumns() : await asyncWriteAdapter.GetColumnsAsync().ConfigureAwait(false);
#else
IMiniExcelWriteAdapter writeAdapter = MiniExcelWriteAdapterFactory.GetWriteAdapter(values, _configuration);
var props = writeAdapter.GetColumns();
#endif
if (props == null)
{
await _writer.WriteAsync(_configuration.NewLine);
await _writer.FlushAsync();
await _writer.WriteAsync(_configuration.NewLine
#if NET5_0_OR_GREATER
.AsMemory(), cancellationToken
#endif
).ConfigureAwait(false);
await _writer.FlushAsync(
#if NET8_0_OR_GREATER
cancellationToken
#endif
).ConfigureAwait(false);
return 0;
}

if (_printHeader)
{
await _writer.WriteAsync(GetHeader(props));
await _writer.WriteAsync(newLine);
await _writer.WriteAsync(GetHeader(props)
#if NET5_0_OR_GREATER
.AsMemory(), cancellationToken
#endif
).ConfigureAwait(false);
await _writer.WriteAsync(newLine
#if NET5_0_OR_GREATER
.AsMemory(), cancellationToken
#endif
).ConfigureAwait(false);
}

var rowBuilder = new StringBuilder();
Expand All @@ -148,29 +164,45 @@ private async Task<int> WriteValuesAsync(StreamWriter writer, object values, str
}

RemoveTrailingSeparator(rowBuilder);
await _writer.WriteAsync(rowBuilder.ToString());
await _writer.WriteAsync(newLine);
await _writer.WriteAsync(rowBuilder.ToString()
#if NET5_0_OR_GREATER
.AsMemory(), cancellationToken
#endif
).ConfigureAwait(false);
await _writer.WriteAsync(newLine
#if NET5_0_OR_GREATER
.AsMemory(), cancellationToken
#endif
).ConfigureAwait(false);

rowsWritten++;
}
}
#if NETSTANDARD2_0_OR_GREATER || NET
else
{
await foreach (var row in asyncWriteAdapter.GetRowsAsync(props, cancellationToken))
await foreach (var row in asyncWriteAdapter.GetRowsAsync(props, cancellationToken).ConfigureAwait(false))
{
cancellationToken.ThrowIfCancellationRequested();
rowBuilder.Clear();

await foreach (var column in row)
await foreach (var column in row.ConfigureAwait(false))
{
cancellationToken.ThrowIfCancellationRequested();
AppendColumn(rowBuilder, column);
}

RemoveTrailingSeparator(rowBuilder);
await _writer.WriteAsync(rowBuilder.ToString());
await _writer.WriteAsync(newLine);
await _writer.WriteAsync(rowBuilder.ToString()
#if NET5_0_OR_GREATER
.AsMemory(), cancellationToken
#endif
).ConfigureAwait(false);
await _writer.WriteAsync(newLine
#if NET5_0_OR_GREATER
.AsMemory(), cancellationToken
#endif
).ConfigureAwait(false);

rowsWritten++;
}
Expand All @@ -188,20 +220,32 @@ public async Task<int[]> SaveAsAsync(CancellationToken cancellationToken = defau

if (_value == null)
{
await _writer.WriteAsync("");
await _writer.FlushAsync();
await _writer.WriteAsync(""
#if NET5_0_OR_GREATER
.AsMemory(), cancellationToken
#endif
).ConfigureAwait(false);
await _writer.FlushAsync(
#if NET8_0_OR_GREATER
cancellationToken
#endif
).ConfigureAwait(false);
return new int[0];
}

var rowsWritten = await WriteValuesAsync(_writer, _value, seperator, newLine, cancellationToken);
await _writer.FlushAsync();
var rowsWritten = await WriteValuesAsync(_writer, _value, seperator, newLine, cancellationToken).ConfigureAwait(false);
await _writer.FlushAsync(
#if NET8_0_OR_GREATER
cancellationToken
#endif
).ConfigureAwait(false);

return new[] { rowsWritten };
}

public async Task<int> InsertAsync(bool overwriteSheet = false, CancellationToken cancellationToken = default)
{
var rowsWritten = await SaveAsAsync(cancellationToken);
var rowsWritten = await SaveAsAsync(cancellationToken).ConfigureAwait(false);
return rowsWritten.FirstOrDefault();
}

Expand Down
9 changes: 6 additions & 3 deletions src/MiniExcel/ExcelFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@
using MiniExcelLibs.OpenXml.SaveByTemplate;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

internal static class ExcelReaderFactory
internal static partial class ExcelReaderFactory
{
internal static IExcelReader GetProvider(Stream stream, ExcelType excelType, IConfiguration configuration)
[Zomp.SyncMethodGenerator.CreateSyncVersion]
internal static async Task<IExcelReader> GetProviderAsync(Stream stream, ExcelType excelType, IConfiguration configuration, CancellationToken cancellationToken = default)
{
switch (excelType)
{
case ExcelType.CSV:
return new CsvReader(stream, configuration);
case ExcelType.XLSX:
return new ExcelOpenXmlSheetReader(stream, configuration);
return await ExcelOpenXmlSheetReader.CreateAsync(stream, configuration, cancellationToken: cancellationToken).ConfigureAwait(false);
default:
throw new NotSupportedException("Something went wrong. Please report this issue you are experiencing with MiniExcel.");
}
Expand Down
27 changes: 13 additions & 14 deletions src/MiniExcel/IExcelReader.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace MiniExcelLibs
{
internal interface IExcelReader : IDisposable
internal partial interface IExcelReader : IDisposable
{
IEnumerable<IDictionary<string, object>> Query(bool useHeaderRow, string sheetName, string startCell);
IEnumerable<T> Query<T>(string sheetName, string startCell, bool hasHeader) where T : class, new();
Task<IEnumerable<IDictionary<string, object>>> QueryAsync(bool useHeaderRow, string sheetName, string startCell, CancellationToken cancellationToken = default);
Task<IEnumerable<T>> QueryAsync<T>(string sheetName, string startCell, bool hasHeader, CancellationToken cancellationToken = default) where T : class, new();
IEnumerable<IDictionary<string, object>> QueryRange(bool useHeaderRow, string sheetName, string startCell, string endCell);
IEnumerable<T> QueryRange<T>(string sheetName, string startCell, string endCell, bool hasHeader) where T : class, new();
Task<IEnumerable<IDictionary<string, object>>> QueryRangeAsync(bool useHeaderRow, string sheetName, string startCell, string endCell, CancellationToken cancellationToken = default);
Task<IEnumerable<T>> QueryRangeAsync<T>(string sheetName, string startCell, string endCell, bool hasHeader, CancellationToken cancellationToken = default) where T : class, new();
IEnumerable<IDictionary<string, object>> QueryRange(bool useHeaderRow, string sheetName, int startRowIndex, int startColumnIndex, int? endRowIndex, int? endColumnIndex);
IEnumerable<T> QueryRange<T>(string sheetName, int startRowIndex, int startColumnIndex, int? endRowIndex, int? endColumnIndex, bool hasHeader) where T : class, new();
Task<IEnumerable<IDictionary<string, object>>> QueryRangeAsync(bool useHeaderRow, string sheetName, int startRowIndex, int startColumnIndex, int? endRowIndex, int? endColumnIndex, CancellationToken cancellationToken = default);
Task<IEnumerable<T>> QueryRangeAsync<T>(string sheetName, int startRowIndex, int startColumnIndex, int? endRowIndex, int? endColumnIndex, bool hasHeader, CancellationToken cancellationToken = default) where T : class, new();
[Zomp.SyncMethodGenerator.CreateSyncVersion]
IAsyncEnumerable<IDictionary<string, object>> QueryAsync(bool useHeaderRow, string sheetName, string startCell, CancellationToken cancellationToken = default);
[Zomp.SyncMethodGenerator.CreateSyncVersion]
IAsyncEnumerable<T> QueryAsync<T>(string sheetName, string startCell, bool hasHeader, CancellationToken cancellationToken = default) where T : class, new();
[Zomp.SyncMethodGenerator.CreateSyncVersion]
IAsyncEnumerable<IDictionary<string, object>> QueryRangeAsync(bool useHeaderRow, string sheetName, string startCell, string endCell, CancellationToken cancellationToken = default);
[Zomp.SyncMethodGenerator.CreateSyncVersion]
IAsyncEnumerable<T> QueryRangeAsync<T>(string sheetName, string startCell, string endCell, bool hasHeader, CancellationToken cancellationToken = default) where T : class, new();
[Zomp.SyncMethodGenerator.CreateSyncVersion]
IAsyncEnumerable<IDictionary<string, object>> QueryRangeAsync(bool useHeaderRow, string sheetName, int startRowIndex, int startColumnIndex, int? endRowIndex, int? endColumnIndex, CancellationToken cancellationToken = default);
[Zomp.SyncMethodGenerator.CreateSyncVersion]
IAsyncEnumerable<T> QueryRangeAsync<T>(string sheetName, int startRowIndex, int startColumnIndex, int? endRowIndex, int? endColumnIndex, bool hasHeader, CancellationToken cancellationToken = default) where T : class, new();
}
}
Loading
Loading