Skip to content
Merged
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
36 changes: 30 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ Relative ordering matches the lower-level writers above (XLS fastest, then XLSB,
| `struct` (`ExcelParser<T>`) | 15.10 ms | 1.59 MB |
| `ref struct` + span binding (`RefParser.ParseNamed<T>`) | 12.91 ms | 17.17 KB |

Parsing into a `ref struct` with a `ReadOnlySpan<byte>` text column removes essentially all per-row allocation — ~99.6% less than the `class` baseline — and is ~15% faster, since there's no per-row model allocation and no per-row `string` allocation for the text column. It is not AOT/trim-safe (reflection-based, same tradeoff as `ExcelParser<T>`) and is sync-only: a `ref struct` element type cannot appear in `IAsyncEnumerable<T>`, so this has no async counterpart, permanently.
Parsing into a `ref struct` with a `ReadOnlySpan<byte>` text column removes essentially all per-row allocation — ~99.6% less than the `class` baseline — and is ~15% faster, since there's no per-row model allocation and no per-row `string` allocation for the text column. It is not AOT/trim-safe (reflection-based, same tradeoff as `ExcelParser<T>`). It can be consumed with `foreach` or `await foreach` but not through `IEnumerable<T>`/`IAsyncEnumerable<T>`/LINQ — a `ref struct` element can't be boxed through those interfaces.

### Cold start

Expand Down Expand Up @@ -189,12 +189,24 @@ CSV is exposed as a single, unnamed sheet (`SheetCount == 1`, `SheetName == ""`)

## Read asynchronously

`Row` and `Cell` are `ref struct` types, so async reading uses a manual loop instead of `await foreach`.
For XLSX files, the async reader buffers one row at a time and uses the same row parser as the sync reader, so sync and async reads stay behaviorally aligned while awaits happen only when more bytes are needed.
Every reader supports `await foreach`. For XLSX files, the async reader buffers one row at a time and uses the same row parser as the sync reader, so sync and async reads stay behaviorally aligned while awaits happen only when more bytes are needed.

```csharp
using ExcelReader.Core.Reader;

await using var reader = await Excel.FromFileAsync("report.xlsx", cancellationToken);

await foreach (var row in reader)
{
Console.WriteLine(row[0].GetString());
}
```

`await foreach` binds to the reader's `GetAsyncEnumerator()` by pattern — the sheet is opened synchronously and only each row advance is awaited. Because `Row` and `Cell` are `ref struct` types, the current row cannot be held across an `await` inside the loop body: read its cells (or copy the values out) before awaiting anything else.

When you need the sheet *opened* asynchronously too (e.g. the first read touches the network), or you need to `await` while a row is in scope, drive the enumerator manually via `GetAsyncEnumeratorAsync`, which awaits the open and threads the cancellation token:

```csharp
await using var reader = await Excel.FromFileAsync("report.xlsx", cancellationToken);
await using var rows = await reader.GetAsyncEnumeratorAsync(cancellationToken);

Expand All @@ -205,6 +217,8 @@ while (await rows.MoveNextAsync())
}
```

`await foreach` does not accept `.WithCancellation(ct)`: `Row` being a `ref struct` rules out `IAsyncEnumerable<Row>`, so the loop binds to the pattern rather than the interface. Pass the token at open time (as above), or use the manual `GetAsyncEnumeratorAsync(ct)` loop.

## Parse typed rows

`ExcelParser<T>` maps worksheet columns to the public settable properties of `T`. Columns match on the property name, or on `[ExcelColumn("header")]` aliases — repeat the attribute to accept several headers. The first row is the header by default.
Expand Down Expand Up @@ -330,11 +344,21 @@ foreach (ChangeRowRef item in RefParser.ParseNamed<ChangeRowRef>(reader))
}
```

The sequence also supports `await foreach`, so a `ref struct` model can be parsed asynchronously — the rows are streamed via `MoveNextAsync` while the model stays a zero-copy `ref struct`:

```csharp
await using var reader = await Excel.FromFileAsync("changes.xlsx");

await foreach (ChangeRowRef item in RefParser.ParseNamed<ChangeRowRef>(reader))
{
Console.WriteLine($"{Encoding.UTF8.GetString(item.File)}: +{item.LinesAdded}");
}
```

A few differences from `ExcelParser<T>`:

- **Span fields alias the reader's row buffer** — valid only until the next row. Copy them out (e.g. `Encoding.UTF8.GetString(span)`) if you need to keep the value past the loop body.
- **Sync only, permanently.** `IAsyncEnumerable<T>` cannot have a `ref struct` element type, so there is no async counterpart — not a missing feature, a language limitation.
- **`foreach` only.** The returned sequence cannot be consumed through `IEnumerable<T>`/LINQ — a `ref struct` element can't be boxed through that interface — so iterate it directly.
- **Span fields alias the reader's row buffer** — valid only until the next row. Copy them out (e.g. `Encoding.UTF8.GetString(span)`) if you need to keep the value past the loop body. Under `await foreach`, the same rule means the model can't be held across an `await` in the loop body.
- **`foreach` / `await foreach` only.** Consumption is pattern-based — the sequence cannot be surfaced through `IEnumerable<T>`, `IAsyncEnumerable<T>`, or LINQ, because a `ref struct` element can't be boxed through those interfaces (`IAsyncEnumerable<T>` in particular forbids a `ref struct` element type — CS9267). Iterate it directly.
- **Not AOT/trim-safe**, same tradeoff as `ExcelParser<T>` (both reflect over `T`'s properties and compile setters at runtime).
- A regular `struct`/`class` model works with `ParseNamed` too — only a genuine `ref struct` model gets the extra zero-copy span-property binding.

Expand Down
2 changes: 2 additions & 0 deletions src/ExcelReader.Core/Parser/Internal/CsvEnumerable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ IAsyncEnumerator<T> IAsyncEnumerable<T>.GetAsyncEnumerator(CancellationToken can
return GetAsyncEnumerator(cancellationToken);
}

[SuppressMessage("Performance", "HLQ006:GetAsyncEnumerator should return a value type",
Justification = "Async enumerator requires a class to host the async state machine.")]
public AsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default)
{
TypeMapInfo<T> info = TypeMapper<T>.GetCsvInfo();
Expand Down
28 changes: 25 additions & 3 deletions src/ExcelReader.Core/Parser/Internal/NamedRefRowEnumerable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,33 @@ internal NamedRefRowEnumerable(
_context = new ExcelRowContext(reader.IsDate1904, formatProvider ?? CultureInfo.InvariantCulture);
}

// Zero-allocation struct enumerator — the supported way to consume this sequence.
// The supported way to consume this sequence via foreach.
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "HLQ006:GetEnumerator should return a value type",
Justification = "The enumerator is a class so the same type can also expose MoveNextAsync for the async path.")]
public NamedRefRowEnumerator<TModel, TEnumerator> GetEnumerator()
{
return new NamedRefRowEnumerator<TModel, TEnumerator>(
_reader.GetEnumerator(), _context, _typeInfo, _comparer, _normalization, _headerRow);
return new(_reader.GetEnumerator(), _context, _typeInfo, _comparer, _normalization, _headerRow);
}

// The 'await foreach' entry point: C#'s pattern-based async binding picks up this parameterless
// GetAsyncEnumerator() (the enumerator it returns has MoveNextAsync/Current/DisposeAsync). Opens
// the sheet synchronously — the reader's GetAsyncEnumerator() is a sync open (no I/O await), the
// async work is per-row via MoveNextAsync. A ref-struct TModel can't be surfaced through
// IAsyncEnumerable<TModel> (CS9267), so this stays a pattern match, never the interface.
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "HLQ006:GetAsyncEnumerator should return a value type",
Justification = "The enumerator is a class so the same type can also expose MoveNextAsync for the async path.")]
public NamedRefRowEnumerator<TModel, TEnumerator> GetAsyncEnumerator()
{
return new(_reader.GetAsyncEnumerator(), _context, _typeInfo, _comparer, _normalization, _headerRow);
}

// Manual-use alternative that opens the sheet asynchronously (awaits the reader's async open).
// Not reachable by 'await foreach' — its shape (returning a ValueTask of the enumerator) doesn't
// match the pattern. Await it, then drive the returned enumerator with MoveNextAsync in a loop.
public async ValueTask<NamedRefRowEnumerator<TModel, TEnumerator>> GetAsyncEnumeratorAsync(CancellationToken ct = default)
{
var enumerator = await _reader.GetAsyncEnumeratorAsync(ct).ConfigureAwait(false);
return new(enumerator, _context, _typeInfo, _comparer, _normalization, _headerRow);
}

IEnumerator<TModel> IEnumerable<TModel>.GetEnumerator()
Expand Down
95 changes: 84 additions & 11 deletions src/ExcelReader.Core/Parser/Internal/NamedRefRowEnumerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,17 @@ namespace ExcelReader.Core.Parser.Internal
// call, immediately writing into a caller-supplied `ref T model`. Every existing caller stores
// that result in a class FIELD (SyncRowEnumerator<T,TRows>.CurrentValue) — illegal for a
// ref-struct-constrained T (CS8345: a ref struct field is only legal inside another ref struct).
// So this type splits the same two responsibilities instead: MoveNext() only advances/classifies
// (skip pre-header rows, build the column map once at the header row), and Current's getter parses
// the row into a fresh LOCAL model on every access — never stored as a field, safe to call
// repeatedly (idempotent) for the same row.
public struct NamedRefRowEnumerator<TModel, TEnumerator> : IDisposable
// So this type splits the same two responsibilities instead: MoveNext()/MoveNextAsync() only
// advance/classify (skip pre-header rows, build the column map once at the header row), and Current's
// getter parses the row into a fresh LOCAL model on every access — never stored as a field, safe to
// call repeatedly (idempotent) for the same row.
//
// A reference type (not a struct): MoveNextAsync's awaiting slow path mutates enumeration state
// (_rowNumber, and the one-time column map) across an await, which a struct would silently lose to the
// async state machine's by-value `this` copy. A class shares the one instance, exactly like
// AsyncRowEnumerator<T,TReader,TRows>. The lazy-Current design above is still required regardless:
// a ref-struct TModel can never be stored in a field (CS8345), class or struct.
public sealed class NamedRefRowEnumerator<TModel, TEnumerator> : IDisposable, IAsyncDisposable
where TModel : allows ref struct
where TEnumerator : class, IExcelRowEnumerator
{
Expand Down Expand Up @@ -55,7 +61,7 @@ internal NamedRefRowEnumerator(
}

// Recomputed on every access (see class remarks) — never cached in a field.
public readonly TModel Current
public TModel Current
{
get
{
Expand Down Expand Up @@ -85,6 +91,63 @@ public bool MoveNext()
return false;
}

// Async twin of MoveNext, mirroring AsyncRowEnumerator<T,TReader,TRows>.MoveNextAsync: a non-async
// fast path that stays synchronous whenever the underlying row-enumerator resolves synchronously
// (the common case — no second state machine on top of _rows' own), only falling to an awaiting
// continuation on a genuine buffer miss. Every state mutation (ClassifyRow's ref _rowNumber,
// BuildColumnMap) runs on the shared class instance, so it survives the await (see class remarks).
[SuppressMessage("SharpSource", "SS034:Use await to get the result of a Task",
Justification = "The .Result access is guarded by IsCompletedSuccessfully immediately above it — never blocks.")]
[SuppressMessage("VisualStudio.Threading", "VSTHRD103:Result synchronously blocks",
Justification = "The .Result access is guarded by IsCompletedSuccessfully immediately above it — never blocks.")]
public ValueTask<bool> MoveNextAsync()
{
while (true)
{
ValueTask<bool> moveTask = _rows.MoveNextAsync();
if (!moveTask.IsCompletedSuccessfully)
{
return AwaitThenContinueAsync(moveTask);
}
if (!moveTask.Result)
{
return new ValueTask<bool>(false);
}
switch (ProjectionRules.ClassifyRow(ref _rowNumber, _headerRow, _bindings is not null))
{
case ProjectionStep.Yield:
return new ValueTask<bool>(true);
case ProjectionStep.BuildMap:
BuildColumnMap(_rows.Current);
break;
case ProjectionStep.Stop:
return new ValueTask<bool>(false);
// Skip: loop again, still synchronous.
}
}
}

private async ValueTask<bool> AwaitThenContinueAsync(ValueTask<bool> pendingMoveNext)
{
if (!await pendingMoveNext.ConfigureAwait(false))
{
return false;
}
ProjectionStep step = ProjectionRules.ClassifyRow(ref _rowNumber, _headerRow, _bindings is not null);
switch (step)
{
case ProjectionStep.Yield:
return true;
case ProjectionStep.BuildMap:
BuildColumnMap(_rows.Current);
break; // map built at the header row — resume the fast path for the next row.
case ProjectionStep.Stop:
return false;
// Skip: resume the fast path.
}
return await MoveNextAsync().ConfigureAwait(false);
}

private void BuildColumnMap(Row row)
{
int propertyCount = _typeInfo.PropertyCount;
Expand Down Expand Up @@ -143,7 +206,7 @@ private void BuildColumnMap(Row row)
_seen = requireValueCount > 0 ? new bool[bindings.Length] : [];
}

private readonly void ParseCurrentRow(Row row, ref TModel model)
private void ParseCurrentRow(Row row, ref TModel model)
{
NamedColumnBinding<TModel>[] bindings = _bindings!;
bool track = _requireValueCount > 0;
Expand Down Expand Up @@ -187,21 +250,31 @@ private readonly void ParseCurrentRow(Row row, ref TModel model)
}
}

private readonly void ValidateRowValues(NamedColumnBinding<TModel>[] bindings)
private void ValidateRowValues(NamedColumnBinding<TModel>[] bindings)
{
for (int i = 0; i < bindings.Length; i++)
{
if (bindings[i].RequireValue && !_seen[i])
ref readonly var binding = ref bindings[i];
if (binding.RequireValue && !_seen[i])
{
throw ProjectionRules.MissingRequiredValue(bindings[i].Name, _rowNumber);
throw ProjectionRules.MissingRequiredValue(binding.Name, _rowNumber);
}
}
}

public readonly void Dispose()
[SuppressMessage("IDisposableAnalyzers.Correctness", "IDISP007:Don't dispose injected",
Justification = "_rows is created for this enumerator alone by NamedRefRowEnumerable.Get(Async)Enumerator() — owned here, not injected.")]
public void Dispose()
{
_rows.Dispose();
}

[SuppressMessage("IDisposableAnalyzers.Correctness", "IDISP007:Don't dispose injected",
Justification = "_rows is created for this enumerator alone by NamedRefRowEnumerable.Get(Async)Enumerator() — owned here, not injected.")]
public ValueTask DisposeAsync()
{
return _rows.DisposeAsync();
}
}
}
#endif
2 changes: 2 additions & 0 deletions src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@
// (a base class can never be less accessible than its derived type).
[SuppressMessage("Design", "CA1034:Nested types should not be visible",
Justification = "Base class of the public nested Enumerator types; not itself meant for direct external use.")]
public abstract class SyncRowEnumerator<T, TRows> : IEnumerator<T>

Check warning on line 16 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Build & Test (macos-latest)

Fix this implementation of 'IDisposable' to conform to the dispose pattern.

Check warning on line 16 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Build & Test (macos-latest)

Check warning on line 16 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Build & Test (macos-latest)

Provide an overridable implementation of Dispose(bool) on 'SyncRowEnumerator' or mark the type as sealed. A call to Dispose(false) should only clean up native resources. A call to Dispose(true) should clean up both managed and native resources. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1063)

Check warning on line 16 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Run (coldstart)

Fix this implementation of 'IDisposable' to conform to the dispose pattern.

Check warning on line 16 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Run (coldstart)

Check warning on line 16 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Run (coldstart)

Provide an overridable implementation of Dispose(bool) on 'SyncRowEnumerator' or mark the type as sealed. A call to Dispose(false) should only clean up native resources. A call to Dispose(true) should clean up both managed and native resources. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1063)

Check warning on line 16 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Build & Test (ubuntu-latest)

Fix this implementation of 'IDisposable' to conform to the dispose pattern.

Check warning on line 16 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Build & Test (ubuntu-latest)

Check warning on line 16 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Build & Test (ubuntu-latest)

Provide an overridable implementation of Dispose(bool) on 'SyncRowEnumerator' or mark the type as sealed. A call to Dispose(false) should only clean up native resources. A call to Dispose(true) should clean up both managed and native resources. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1063)

Check warning on line 16 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Run (realdata)

Fix this implementation of 'IDisposable' to conform to the dispose pattern.

Check warning on line 16 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Run (realdata)

Check warning on line 16 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Run (realdata)

Provide an overridable implementation of Dispose(bool) on 'SyncRowEnumerator' or mark the type as sealed. A call to Dispose(false) should only clean up native resources. A call to Dispose(true) should clean up both managed and native resources. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1063)

Check warning on line 16 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Run (read)

Fix this implementation of 'IDisposable' to conform to the dispose pattern.

Check warning on line 16 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Run (read)

Check warning on line 16 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Run (read)

Provide an overridable implementation of Dispose(bool) on 'SyncRowEnumerator' or mark the type as sealed. A call to Dispose(false) should only clean up native resources. A call to Dispose(true) should clean up both managed and native resources. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1063)

Check warning on line 16 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Build & Test (windows-latest)

Fix this implementation of 'IDisposable' to conform to the dispose pattern.

Check warning on line 16 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Build & Test (windows-latest)

Check warning on line 16 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Build & Test (windows-latest)

Provide an overridable implementation of Dispose(bool) on 'SyncRowEnumerator' or mark the type as sealed. A call to Dispose(false) should only clean up native resources. A call to Dispose(true) should clean up both managed and native resources. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1063)

Check warning on line 16 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Run (parse)

Provide an overridable implementation of Dispose(bool) on 'SyncRowEnumerator' or mark the type as sealed. A call to Dispose(false) should only clean up native resources. A call to Dispose(true) should clean up both managed and native resources. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1063)

Check warning on line 16 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Analyze (C#)

Fix this implementation of 'IDisposable' to conform to the dispose pattern.

Check warning on line 16 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Analyze (C#)

Check warning on line 16 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Analyze (C#)

Provide an overridable implementation of Dispose(bool) on 'SyncRowEnumerator' or mark the type as sealed. A call to Dispose(false) should only clean up native resources. A call to Dispose(true) should clean up both managed and native resources. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1063)

Check warning on line 16 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Run (write)

Check warning on line 16 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Run (write)

Provide an overridable implementation of Dispose(bool) on 'SyncRowEnumerator' or mark the type as sealed. A call to Dispose(false) should only clean up native resources. A call to Dispose(true) should clean up both managed and native resources. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1063)
where TRows : class, IExcelRowEnumerator
{
[SuppressMessage("Performance", "HLQ011:ReadOnlyEnumeratorField",
Justification = "TRows is constrained to `class` here, so it is always a reference type — no copy-on-mutate risk from a readonly field.")]
protected readonly TRows Rows;

Check warning on line 21 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Build & Test (macos-latest)

Do not declare visible instance fields (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1051)

Check warning on line 21 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Run (coldstart)

Do not declare visible instance fields (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1051)

Check warning on line 21 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Build & Test (ubuntu-latest)

Do not declare visible instance fields (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1051)

Check warning on line 21 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Run (realdata)

Do not declare visible instance fields (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1051)

Check warning on line 21 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Run (read)

Do not declare visible instance fields (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1051)

Check warning on line 21 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Build & Test (windows-latest)

Do not declare visible instance fields (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1051)

Check warning on line 21 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Analyze (C#)

Do not declare visible instance fields (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1051)

Check warning on line 21 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Run (write)

Do not declare visible instance fields (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1051)
protected T CurrentValue = default!;

Check warning on line 22 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Build & Test (macos-latest)

Do not declare visible instance fields (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1051)

Check warning on line 22 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Run (coldstart)

Do not declare visible instance fields (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1051)

Check warning on line 22 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Build & Test (ubuntu-latest)

Do not declare visible instance fields (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1051)

Check warning on line 22 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Run (realdata)

Do not declare visible instance fields (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1051)

Check warning on line 22 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Run (read)

Do not declare visible instance fields (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1051)

Check warning on line 22 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Build & Test (windows-latest)

Do not declare visible instance fields (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1051)

Check warning on line 22 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Run (parse)

Do not declare visible instance fields (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1051)

Check warning on line 22 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Run (write)

Do not declare visible instance fields (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1051)

protected SyncRowEnumerator(TRows rows)
{
Expand Down Expand Up @@ -47,6 +47,8 @@

private protected abstract ProjectionStep Project();

[SuppressMessage("IDisposableAnalyzers.Correctness", "IDISP007:Don't dispose injected",
Justification = "Rows is created for this enumerator alone by the enclosing enumerable's GetEnumerator (reader.GetEnumerator()) — owned here, not injected.")]
public void Dispose()
{
Rows.Dispose();
Expand All @@ -66,7 +68,7 @@
// the row-enumerator's own (e.g. XlsxReader.Enumerator.MoveNextAsync / CsvReader.Enumerator.MoveNextAsync).
[SuppressMessage("Design", "CA1034:Nested types should not be visible",
Justification = "Base class of the public nested AsyncEnumerator types; not itself meant for direct external use.")]
public abstract class AsyncRowEnumerator<T, TReader, TRows> : IAsyncEnumerator<T>

Check warning on line 71 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Build & Test (macos-latest)

Class with no virtual DisposeAsyncCore method should be sealed (https://github.com/DotNetAnalyzers/IDisposableAnalyzers/blob/master/documentation/IDISP026.md)

Check warning on line 71 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Run (coldstart)

Class with no virtual DisposeAsyncCore method should be sealed (https://github.com/DotNetAnalyzers/IDisposableAnalyzers/blob/master/documentation/IDISP026.md)

Check warning on line 71 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Build & Test (ubuntu-latest)

Class with no virtual DisposeAsyncCore method should be sealed (https://github.com/DotNetAnalyzers/IDisposableAnalyzers/blob/master/documentation/IDISP026.md)

Check warning on line 71 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Run (realdata)

Class with no virtual DisposeAsyncCore method should be sealed (https://github.com/DotNetAnalyzers/IDisposableAnalyzers/blob/master/documentation/IDISP026.md)

Check warning on line 71 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Build & Test (windows-latest)

Class with no virtual DisposeAsyncCore method should be sealed (https://github.com/DotNetAnalyzers/IDisposableAnalyzers/blob/master/documentation/IDISP026.md)
where TReader : IExcelRowReader<TRows>
where TRows : class, IExcelRowEnumerator
{
Expand All @@ -74,8 +76,8 @@
[SuppressMessage("SharpSource", "SS066:Disposable field is not disposed", Justification = "Borrowed, not owned.")]
private readonly TReader _reader;
private readonly CancellationToken _ct;
protected TRows? Rows;

Check warning on line 79 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Build & Test (macos-latest)

Do not declare visible instance fields (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1051)

Check warning on line 79 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Run (coldstart)

Do not declare visible instance fields (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1051)

Check warning on line 79 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Build & Test (ubuntu-latest)

Do not declare visible instance fields (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1051)

Check warning on line 79 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Run (realdata)

Do not declare visible instance fields (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1051)

Check warning on line 79 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Build & Test (windows-latest)

Do not declare visible instance fields (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1051)
protected T CurrentValue = default!;

Check warning on line 80 in src/ExcelReader.Core/Parser/Internal/RowEnumeratorBase.cs

View workflow job for this annotation

GitHub Actions / Build & Test (macos-latest)

Do not declare visible instance fields (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1051)

protected AsyncRowEnumerator(TReader reader, CancellationToken ct)
{
Expand Down
12 changes: 12 additions & 0 deletions src/ExcelReader.Core/Reader/CsvReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,18 @@ IExcelRowEnumerator IExcelRowReader<IExcelRowEnumerator>.GetEnumerator()
return GetEnumerator();
}

[SuppressMessage("Performance", "HLQ006:GetAsyncEnumerator should return a value type",
Justification = "Enumerator is a class so the same type can also expose MoveNextAsync for the async path.")]
public Enumerator GetAsyncEnumerator()
{
return GetEnumerator();
}

IExcelRowEnumerator IExcelRowReader<IExcelRowEnumerator>.GetAsyncEnumerator()
{
return GetAsyncEnumerator();
}

[SuppressMessage("IDisposableAnalyzers.Correctness", "IDISP001:Dispose created",
Justification = "Enumerator ownership transfers to the caller, who disposes it via await using / DisposeAsync.")]
[SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope",
Expand Down
1 change: 1 addition & 0 deletions src/ExcelReader.Core/Reader/IExcelRowReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public interface IExcelRowReader<TEnumerator>
{
bool IsDate1904 { get; }
TEnumerator GetEnumerator();
TEnumerator GetAsyncEnumerator();
ValueTask<TEnumerator> GetAsyncEnumeratorAsync(CancellationToken ct = default);
}

Expand Down
22 changes: 21 additions & 1 deletion src/ExcelReader.Core/Reader/XlsReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,17 +103,37 @@ IExcelRowEnumerator IExcelRowReader<IExcelRowEnumerator>.GetEnumerator()

[SuppressMessage("Performance", "HLQ006:GetAsyncEnumerator should return a value type",
Justification = "Enumerator is a class so the same type can also expose MoveNextAsync for parity with XlsxReader.")]
public Enumerator GetAsyncEnumerator(CancellationToken ct = default)
public Enumerator GetAsyncEnumerator()
{
return GetEnumerator();
}

// ct overload takes no default value: the parameterless GetAsyncEnumerator() above already covers
// the no-argument call, so a default here would only shadow it.
[SuppressMessage("Performance", "HLQ006:GetAsyncEnumerator should return a value type",
Justification = "Enumerator is a class so the same type can also expose MoveNextAsync for parity with XlsxReader.")]
public Enumerator GetAsyncEnumerator(CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
return new Enumerator(this, _sheets[_current].Offset, ct);
}

IExcelRowEnumerator IExcelRowReader<IExcelRowEnumerator>.GetAsyncEnumerator()
{
return GetAsyncEnumerator();
}

[SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope",
Justification = "Enumerator ownership transfers to the caller, who disposes it via await using / DisposeAsync.")]
public ValueTask<Enumerator> GetAsyncEnumeratorAsync(CancellationToken ct = default)
{
return new ValueTask<Enumerator>(GetAsyncEnumerator(ct));
}

[SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope",
Justification = "Enumerator ownership transfers to the caller, who disposes it via await using / DisposeAsync.")]
[SuppressMessage("Performance", "CA1849:Call async methods when in an async method",
Justification = "XlsReader is fully in-memory; opening the enumerator is synchronous, so there is no async open to await here.")]
ValueTask<IExcelRowEnumerator> IExcelRowReader<IExcelRowEnumerator>.GetAsyncEnumeratorAsync(CancellationToken ct)
{
return new ValueTask<IExcelRowEnumerator>(GetAsyncEnumerator(ct));
Expand Down
Loading
Loading