Skip to content

Commit a9064c4

Browse files
authored
Merge PR #799: Replace Task.Run wrappers with real asynchronous methods - first exclusive commit to future v2.0.0 release
Base commit for v2.0 version: - Implements real async queries returning IAsyncEnumerable instead of Task.Run wrappers of IEnumerables - Removes all synchronous methods in favour of source generated methods by library [sync-method-generator](https://github.com/zompinc/sync-method-generator) - Adds dependency to aforementioned library package - Drops support of .NET Framework 4.5 in favour of .NET Framework 4.6.2 and removes unnecessary preprocessor directives - Converts all public methods and majority of remaining to asynchronous implementation - Adds ConfigureAwait(false) and CancellationTokens where missing - Exposes AddPictureAsync - Enforces calling async methods in async context, ConfigureAwait(false), passing CancellationToken and disposal of disposable resources through .editorconfig - Fixes async tests
1 parent 1d06e96 commit a9064c4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1771
-2634
lines changed

.editorconfig

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,18 @@ dotnet_style_qualification_for_property = false
1313
dotnet_style_qualification_for_method = false
1414

1515
# IDE0065: Misplaced using directive
16-
csharp_using_directive_placement = outside_namespace
16+
csharp_using_directive_placement = outside_namespace
17+
18+
# CA1835: Prefer the memory-based overloads of ReadAsync/WriteAsync methods in stream-based classes
19+
dotnet_diagnostic.CA1835.severity = error
20+
21+
# CA1849: Call async methods when in an async method
22+
dotnet_diagnostic.CA1849.severity = error
23+
24+
dotnet_diagnostic.CA2000.severity = error
25+
26+
# CA2007: Do not directly await a Task
27+
dotnet_diagnostic.CA2007.severity = error
28+
29+
# CA2016: Forward the CancellationToken parameter to methods that take one
30+
dotnet_diagnostic.CA2016.severity = error

benchmarks/MiniExcel.Benchmarks/MiniExcel.Benchmarks.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<ImplicitUsings>enable</ImplicitUsings>
88
<LangVersion>latest</LangVersion>
99
<RootNamespace>MiniExcelLibs.Benchmarks</RootNamespace>
10+
<NoWarn>$(NoWarn);CA2000;CA2007</NoWarn>
1011
</PropertyGroup>
1112

1213
<ItemGroup>

src/MiniExcel/Csv/CsvReader.cs

Lines changed: 32 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@
88
using System.Text.RegularExpressions;
99
using System.Threading;
1010
using System.Threading.Tasks;
11+
using System.Runtime.CompilerServices;
1112

1213
namespace MiniExcelLibs.Csv
1314
{
14-
internal class CsvReader : IExcelReader
15+
internal partial class CsvReader : IExcelReader
1516
{
1617
private Stream _stream;
1718
private CsvConfiguration _config;
@@ -22,8 +23,11 @@ public CsvReader(Stream stream, IConfiguration configuration)
2223
_config = configuration == null ? CsvConfiguration.DefaultConfiguration : (CsvConfiguration)configuration;
2324
}
2425

25-
public IEnumerable<IDictionary<string, object>> Query(bool useHeaderRow, string sheetName, string startCell)
26+
[Zomp.SyncMethodGenerator.CreateSyncVersion]
27+
public async IAsyncEnumerable<IDictionary<string, object>> QueryAsync(bool useHeaderRow, string sheetName, string startCell, [EnumeratorCancellation] CancellationToken cancellationToken = default)
2628
{
29+
cancellationToken.ThrowIfCancellationRequested();
30+
2731
if (startCell != "A1")
2832
throw new NotImplementedException("CSV does not implement parameter startCell");
2933

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

3741
string row;
38-
for (var rowIndex = 1; (row = reader.ReadLine()) != null; rowIndex++)
42+
for (var rowIndex = 1; (row = await reader.ReadLineAsync(
43+
#if NET7_0_OR_GREATER
44+
cancellationToken
45+
#endif
46+
).ConfigureAwait(false)) != null; rowIndex++)
3947
{
4048
string finalRow = row;
4149
if (_config.ReadLineBreaksWithinQuotes)
4250
{
4351
while (finalRow.Count(c => c == '"') % 2 != 0)
4452
{
45-
var nextPart = reader.ReadLine();
53+
var nextPart = await reader.ReadLineAsync(
54+
#if NET7_0_OR_GREATER
55+
cancellationToken
56+
#endif
57+
).ConfigureAwait(false);
4658
if (nextPart == null)
4759
{
4860
break;
@@ -107,62 +119,37 @@ public IEnumerable<IDictionary<string, object>> Query(bool useHeaderRow, string
107119
}
108120
}
109121

110-
public IEnumerable<T> Query<T>(string sheetName, string startCell, bool hasHeader) where T : class, new()
122+
[Zomp.SyncMethodGenerator.CreateSyncVersion]
123+
public IAsyncEnumerable<T> QueryAsync<T>(string sheetName, string startCell, bool hasHeader, CancellationToken cancellationToken = default) where T : class, new()
111124
{
112-
var dynamicRecords = Query(false, sheetName, startCell);
113-
return ExcelOpenXmlSheetReader.QueryImpl<T>(dynamicRecords, startCell, hasHeader, _config);
125+
var dynamicRecords = QueryAsync(false, sheetName, startCell, cancellationToken);
126+
return ExcelOpenXmlSheetReader.QueryImplAsync<T>(dynamicRecords, startCell, hasHeader, _config, cancellationToken);
114127
}
115128

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

121-
public IEnumerable<T> QueryRange<T>(string sheetName, string startCell, string endCell, bool hasHeader) where T : class, new()
135+
[Zomp.SyncMethodGenerator.CreateSyncVersion]
136+
public IAsyncEnumerable<T> QueryRangeAsync<T>(string sheetName, string startCell, string endCell, bool hasHeader, CancellationToken cancellationToken = default) where T : class, new()
122137
{
123-
var dynamicRecords = QueryRange(false, sheetName, startCell, endCell);
124-
return ExcelOpenXmlSheetReader.QueryImpl<T>(dynamicRecords, startCell, hasHeader, this._config);
138+
var dynamicRecords = QueryRangeAsync(false, sheetName, startCell, endCell, cancellationToken);
139+
return ExcelOpenXmlSheetReader.QueryImplAsync<T>(dynamicRecords, startCell, hasHeader, this._config, cancellationToken);
125140
}
126141

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

132-
public IEnumerable<T> QueryRange<T>(string sheetName, int startRowIndex, int startColumnIndex, int? endRowIndex, int? endColumnIndex, bool hasHeader) where T : class, new()
133-
{
134-
var dynamicRecords = QueryRange(false, sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex);
135-
return ExcelOpenXmlSheetReader.QueryImpl<T>(dynamicRecords, ReferenceHelper.ConvertXyToCell(startRowIndex, startColumnIndex), hasHeader, this._config);
136-
}
137-
138-
public Task<IEnumerable<IDictionary<string, object>>> QueryAsync(bool useHeaderRow, string sheetName, string startCell, CancellationToken cancellationToken = default)
139-
{
140-
return Task.Run(() => Query(useHeaderRow, sheetName, startCell), cancellationToken);
141-
}
142-
143-
public async Task<IEnumerable<T>> QueryAsync<T>(string sheetName, string startCell, bool hasHeader, CancellationToken cancellationToken = default) where T : class, new()
144-
{
145-
return await Task.Run(() => Query<T>(sheetName, startCell, hasHeader), cancellationToken).ConfigureAwait(false);
146-
}
147-
148-
public Task<IEnumerable<IDictionary<string, object>>> QueryRangeAsync(bool useHeaderRow, string sheetName, string startCell, string endCel, CancellationToken cancellationToken = default)
149-
{
150-
return Task.Run(() => QueryRange(useHeaderRow, sheetName, startCell, endCel), cancellationToken);
151-
}
152-
153-
public async Task<IEnumerable<T>> QueryRangeAsync<T>(string sheetName, string startCell, string endCel, bool hasHeader, CancellationToken cancellationToken = default) where T : class, new()
154-
{
155-
return await Task.Run(() => QueryRange<T>(sheetName, startCell, endCel, hasHeader), cancellationToken).ConfigureAwait(false);
156-
}
157-
158-
public Task<IEnumerable<IDictionary<string, object>>> QueryRangeAsync(bool useHeaderRow, string sheetName, int startRowIndex, int startColumnIndex, int? endRowIndex, int? endColumnIndex, CancellationToken cancellationToken = default)
159-
{
160-
return Task.Run(() => QueryRange(useHeaderRow, sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex), cancellationToken);
161-
}
162-
163-
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()
148+
[Zomp.SyncMethodGenerator.CreateSyncVersion]
149+
public IAsyncEnumerable<T> QueryRangeAsync<T>(string sheetName, int startRowIndex, int startColumnIndex, int? endRowIndex, int? endColumnIndex, bool hasHeader, CancellationToken cancellationToken = default) where T : class, new()
164150
{
165-
return await Task.Run(() => QueryRange<T>(sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex, hasHeader), cancellationToken).ConfigureAwait(false);
151+
var dynamicRecords = QueryRangeAsync(false, sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex, cancellationToken);
152+
return ExcelOpenXmlSheetReader.QueryImplAsync<T>(dynamicRecords, ReferenceHelper.ConvertXyToCell(startRowIndex, startColumnIndex), hasHeader, this._config, cancellationToken);
166153
}
167154

168155
private string[] Split(string row)

src/MiniExcel/Csv/CsvWriter.cs

Lines changed: 60 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -115,22 +115,38 @@ private async Task<int> WriteValuesAsync(StreamWriter writer, object values, str
115115
{
116116
writeAdapter = MiniExcelWriteAdapterFactory.GetWriteAdapter(values, _configuration);
117117
}
118-
var props = writeAdapter != null ? writeAdapter.GetColumns() : await asyncWriteAdapter.GetColumnsAsync();
118+
var props = writeAdapter != null ? writeAdapter.GetColumns() : await asyncWriteAdapter.GetColumnsAsync().ConfigureAwait(false);
119119
#else
120120
IMiniExcelWriteAdapter writeAdapter = MiniExcelWriteAdapterFactory.GetWriteAdapter(values, _configuration);
121121
var props = writeAdapter.GetColumns();
122122
#endif
123123
if (props == null)
124124
{
125-
await _writer.WriteAsync(_configuration.NewLine);
126-
await _writer.FlushAsync();
125+
await _writer.WriteAsync(_configuration.NewLine
126+
#if NET5_0_OR_GREATER
127+
.AsMemory(), cancellationToken
128+
#endif
129+
).ConfigureAwait(false);
130+
await _writer.FlushAsync(
131+
#if NET8_0_OR_GREATER
132+
cancellationToken
133+
#endif
134+
).ConfigureAwait(false);
127135
return 0;
128136
}
129137

130138
if (_printHeader)
131139
{
132-
await _writer.WriteAsync(GetHeader(props));
133-
await _writer.WriteAsync(newLine);
140+
await _writer.WriteAsync(GetHeader(props)
141+
#if NET5_0_OR_GREATER
142+
.AsMemory(), cancellationToken
143+
#endif
144+
).ConfigureAwait(false);
145+
await _writer.WriteAsync(newLine
146+
#if NET5_0_OR_GREATER
147+
.AsMemory(), cancellationToken
148+
#endif
149+
).ConfigureAwait(false);
134150
}
135151

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

150166
RemoveTrailingSeparator(rowBuilder);
151-
await _writer.WriteAsync(rowBuilder.ToString());
152-
await _writer.WriteAsync(newLine);
167+
await _writer.WriteAsync(rowBuilder.ToString()
168+
#if NET5_0_OR_GREATER
169+
.AsMemory(), cancellationToken
170+
#endif
171+
).ConfigureAwait(false);
172+
await _writer.WriteAsync(newLine
173+
#if NET5_0_OR_GREATER
174+
.AsMemory(), cancellationToken
175+
#endif
176+
).ConfigureAwait(false);
153177

154178
rowsWritten++;
155179
}
156180
}
157181
#if NETSTANDARD2_0_OR_GREATER || NET
158182
else
159183
{
160-
await foreach (var row in asyncWriteAdapter.GetRowsAsync(props, cancellationToken))
184+
await foreach (var row in asyncWriteAdapter.GetRowsAsync(props, cancellationToken).ConfigureAwait(false))
161185
{
162186
cancellationToken.ThrowIfCancellationRequested();
163187
rowBuilder.Clear();
164188

165-
await foreach (var column in row)
189+
await foreach (var column in row.ConfigureAwait(false))
166190
{
167191
cancellationToken.ThrowIfCancellationRequested();
168192
AppendColumn(rowBuilder, column);
169193
}
170194

171195
RemoveTrailingSeparator(rowBuilder);
172-
await _writer.WriteAsync(rowBuilder.ToString());
173-
await _writer.WriteAsync(newLine);
196+
await _writer.WriteAsync(rowBuilder.ToString()
197+
#if NET5_0_OR_GREATER
198+
.AsMemory(), cancellationToken
199+
#endif
200+
).ConfigureAwait(false);
201+
await _writer.WriteAsync(newLine
202+
#if NET5_0_OR_GREATER
203+
.AsMemory(), cancellationToken
204+
#endif
205+
).ConfigureAwait(false);
174206

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

189221
if (_value == null)
190222
{
191-
await _writer.WriteAsync("");
192-
await _writer.FlushAsync();
223+
await _writer.WriteAsync(""
224+
#if NET5_0_OR_GREATER
225+
.AsMemory(), cancellationToken
226+
#endif
227+
).ConfigureAwait(false);
228+
await _writer.FlushAsync(
229+
#if NET8_0_OR_GREATER
230+
cancellationToken
231+
#endif
232+
).ConfigureAwait(false);
193233
return new int[0];
194234
}
195235

196-
var rowsWritten = await WriteValuesAsync(_writer, _value, seperator, newLine, cancellationToken);
197-
await _writer.FlushAsync();
236+
var rowsWritten = await WriteValuesAsync(_writer, _value, seperator, newLine, cancellationToken).ConfigureAwait(false);
237+
await _writer.FlushAsync(
238+
#if NET8_0_OR_GREATER
239+
cancellationToken
240+
#endif
241+
).ConfigureAwait(false);
198242

199243
return new[] { rowsWritten };
200244
}
201245

202246
public async Task<int> InsertAsync(bool overwriteSheet = false, CancellationToken cancellationToken = default)
203247
{
204-
var rowsWritten = await SaveAsAsync(cancellationToken);
248+
var rowsWritten = await SaveAsAsync(cancellationToken).ConfigureAwait(false);
205249
return rowsWritten.FirstOrDefault();
206250
}
207251

src/MiniExcel/ExcelFactory.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,20 @@
55
using MiniExcelLibs.OpenXml.SaveByTemplate;
66
using System;
77
using System.IO;
8+
using System.Threading;
9+
using System.Threading.Tasks;
810

9-
internal static class ExcelReaderFactory
11+
internal static partial class ExcelReaderFactory
1012
{
11-
internal static IExcelReader GetProvider(Stream stream, ExcelType excelType, IConfiguration configuration)
13+
[Zomp.SyncMethodGenerator.CreateSyncVersion]
14+
internal static async Task<IExcelReader> GetProviderAsync(Stream stream, ExcelType excelType, IConfiguration configuration, CancellationToken cancellationToken = default)
1215
{
1316
switch (excelType)
1417
{
1518
case ExcelType.CSV:
1619
return new CsvReader(stream, configuration);
1720
case ExcelType.XLSX:
18-
return new ExcelOpenXmlSheetReader(stream, configuration);
21+
return await ExcelOpenXmlSheetReader.CreateAsync(stream, configuration, cancellationToken: cancellationToken).ConfigureAwait(false);
1922
default:
2023
throw new NotSupportedException("Something went wrong. Please report this issue you are experiencing with MiniExcel.");
2124
}

src/MiniExcel/IExcelReader.cs

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,22 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Threading;
4-
using System.Threading.Tasks;
54

65
namespace MiniExcelLibs
76
{
8-
internal interface IExcelReader : IDisposable
7+
internal partial interface IExcelReader : IDisposable
98
{
10-
IEnumerable<IDictionary<string, object>> Query(bool useHeaderRow, string sheetName, string startCell);
11-
IEnumerable<T> Query<T>(string sheetName, string startCell, bool hasHeader) where T : class, new();
12-
Task<IEnumerable<IDictionary<string, object>>> QueryAsync(bool useHeaderRow, string sheetName, string startCell, CancellationToken cancellationToken = default);
13-
Task<IEnumerable<T>> QueryAsync<T>(string sheetName, string startCell, bool hasHeader, CancellationToken cancellationToken = default) where T : class, new();
14-
IEnumerable<IDictionary<string, object>> QueryRange(bool useHeaderRow, string sheetName, string startCell, string endCell);
15-
IEnumerable<T> QueryRange<T>(string sheetName, string startCell, string endCell, bool hasHeader) where T : class, new();
16-
Task<IEnumerable<IDictionary<string, object>>> QueryRangeAsync(bool useHeaderRow, string sheetName, string startCell, string endCell, CancellationToken cancellationToken = default);
17-
Task<IEnumerable<T>> QueryRangeAsync<T>(string sheetName, string startCell, string endCell, bool hasHeader, CancellationToken cancellationToken = default) where T : class, new();
18-
IEnumerable<IDictionary<string, object>> QueryRange(bool useHeaderRow, string sheetName, int startRowIndex, int startColumnIndex, int? endRowIndex, int? endColumnIndex);
19-
IEnumerable<T> QueryRange<T>(string sheetName, int startRowIndex, int startColumnIndex, int? endRowIndex, int? endColumnIndex, bool hasHeader) where T : class, new();
20-
Task<IEnumerable<IDictionary<string, object>>> QueryRangeAsync(bool useHeaderRow, string sheetName, int startRowIndex, int startColumnIndex, int? endRowIndex, int? endColumnIndex, CancellationToken cancellationToken = default);
21-
Task<IEnumerable<T>> QueryRangeAsync<T>(string sheetName, int startRowIndex, int startColumnIndex, int? endRowIndex, int? endColumnIndex, bool hasHeader, CancellationToken cancellationToken = default) where T : class, new();
9+
[Zomp.SyncMethodGenerator.CreateSyncVersion]
10+
IAsyncEnumerable<IDictionary<string, object>> QueryAsync(bool useHeaderRow, string sheetName, string startCell, CancellationToken cancellationToken = default);
11+
[Zomp.SyncMethodGenerator.CreateSyncVersion]
12+
IAsyncEnumerable<T> QueryAsync<T>(string sheetName, string startCell, bool hasHeader, CancellationToken cancellationToken = default) where T : class, new();
13+
[Zomp.SyncMethodGenerator.CreateSyncVersion]
14+
IAsyncEnumerable<IDictionary<string, object>> QueryRangeAsync(bool useHeaderRow, string sheetName, string startCell, string endCell, CancellationToken cancellationToken = default);
15+
[Zomp.SyncMethodGenerator.CreateSyncVersion]
16+
IAsyncEnumerable<T> QueryRangeAsync<T>(string sheetName, string startCell, string endCell, bool hasHeader, CancellationToken cancellationToken = default) where T : class, new();
17+
[Zomp.SyncMethodGenerator.CreateSyncVersion]
18+
IAsyncEnumerable<IDictionary<string, object>> QueryRangeAsync(bool useHeaderRow, string sheetName, int startRowIndex, int startColumnIndex, int? endRowIndex, int? endColumnIndex, CancellationToken cancellationToken = default);
19+
[Zomp.SyncMethodGenerator.CreateSyncVersion]
20+
IAsyncEnumerable<T> QueryRangeAsync<T>(string sheetName, int startRowIndex, int startColumnIndex, int? endRowIndex, int? endColumnIndex, bool hasHeader, CancellationToken cancellationToken = default) where T : class, new();
2221
}
2322
}

0 commit comments

Comments
 (0)