Upgrade newest standards#67
Conversation
…, and ExtractorBase Co-authored-by: Chris-Wolfgang <210299580+Chris-Wolfgang@users.noreply.github.com>
Co-authored-by: Chris-Wolfgang <210299580+Chris-Wolfgang@users.noreply.github.com>
Co-authored-by: Chris-Wolfgang <210299580+Chris-Wolfgang@users.noreply.github.com>
Co-authored-by: Chris-Wolfgang <210299580+Chris-Wolfgang@users.noreply.github.com>
…tractor.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…Transformer.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…s-errors Fix static analysis errors in Example6-ReducingDuplicateCode (Net8.0)
|
PS C:\Source\GitHub\ETL-Framework\ETL-Abstractions> del .\Wolfgang.Etl.Abstractions.sln Restore failed with 1 error(s) in 17.4s |
|
@Chris-Wolfgang I've opened a new pull request, #68, to work on those changes. Once the pull request is ready, I'll request review from you. |
There was a problem hiding this comment.
Pull request overview
Updates core ETL abstraction base classes and the .NET 8 example to address code scanning/analyzer findings (exception types/docs, argument validation, and API usage patterns), plus a small repo hygiene tweak.
Changes:
- Updated XML docs/guards to prefer
ArgumentOutOfRangeExceptionand added null-argument exception documentation in base classes. - Adjusted
MaximumItemCountvalidation (now>= 1for Loader/Transformer) and applied small style tweaks (e.g., namedstate:inTimerctor). - Refactored Example6 types into separate files and added
ConfigureAwait(false)in several awaits.
Reviewed changes
Copilot reviewed 9 out of 10 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Wolfgang.Etl.Abstractions/TransformerBase.cs | Doc/guard updates; MaximumItemCount validation tweak; timer callback arg naming. |
| src/Wolfgang.Etl.Abstractions/LoaderBase.cs | Doc/guard updates; MaximumItemCount validation tweak; timer callback arg naming. |
| src/Wolfgang.Etl.Abstractions/ExtractorBase.cs | Doc/guard updates; timer callback arg naming. |
| examples/Net8.0/Example6-ReducingDuplicateCode/Program.cs | Added ConfigureAwait(false); moved nested helper types out. |
| examples/Net8.0/Example6-ReducingDuplicateCode/EtlProgress.cs | New file for EtlProgress record. |
| examples/Net8.0/Example6-ReducingDuplicateCode/ConsoleColors.cs | New file for ConsoleColors. |
| examples/Net8.0/Example6-ReducingDuplicateCode/ETL/IntToStringTransformer.cs | Updated null guards/arg naming; timer + delay changes. |
| examples/Net8.0/Example6-ReducingDuplicateCode/ETL/FibonacciExtractor.cs | Updated null guards/arg naming; timer + delay changes. |
| examples/Net8.0/Example6-ReducingDuplicateCode/ETL/ConsoleLoader.cs | Updated null guards/arg naming; timer + delay changes. |
| .gitignore | Ignored .nuget/nuget.exe. |
Comments suppressed due to low confidence (3)
src/Wolfgang.Etl.Abstractions/ExtractorBase.cs:82
CurrentSkippedItemCount’s setter never assigns the backing field when the value is valid, so the property can’t be updated. Assign_currentSkippedItemCount = valueafter validation (or remove the setter if it isn’t intended to be set).
get => _currentSkippedItemCount;
protected set
{
if (value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value), "value cannot be less than 0.");
}
src/Wolfgang.Etl.Abstractions/LoaderBase.cs:238
LoadAsync(items, progress)returns the workerTaskwithout awaiting it, but the progressTimeris in ausing var, so it will be disposed immediately when the method returns, preventing progress from being reported. Make the methodasyncandawaitthe worker (or otherwise keep the timer alive until the load completes).
using var timer = new Timer
(
_ => progress.Report(CreateProgressReport()),
state: null,
TimeSpan.Zero,
TimeSpan.FromMilliseconds(ReportingInterval)
);
return LoadWorkerAsync(items, CancellationToken.None);
src/Wolfgang.Etl.Abstractions/ExtractorBase.cs:273
- Same timer-lifetime issue as the other
ExtractAsyncoverload: disposing theTimerin the method scope means it will be gone before the returnedIAsyncEnumerableis enumerated, so progress reporting won’t occur. Ensure the timer lifetime is tied to enumeration (e.g., via an async iterator wrapper).
using var timer = new Timer
(
_ => progress.Report(CreateProgressReport()),
state: null,
TimeSpan.Zero,
TimeSpan.FromMilliseconds(ReportingInterval)
);
| _ => progress?.Report(new EtlProgress(Volatile.Read(ref count))), | ||
| null, | ||
| state: null, | ||
| TimeSpan.Zero, | ||
| TimeSpan.FromMilliseconds(_progressInterval) // Use the configured progress interval | ||
| ); | ||
| ).ConfigureAwait(false); |
There was a problem hiding this comment.
System.Threading.Timer is IDisposable (not IAsyncDisposable) and the constructor doesn’t return a task, so await using and the trailing .ConfigureAwait(false) on the timer creation won’t compile. Use a normal using var for the timer (or switch to PeriodicTimer with an async loop) and keep it scoped to the async iterator so it’s disposed when enumeration completes.
| using var timer = new Timer | ||
| ( | ||
| _ => progress.Report(CreateProgressReport()), | ||
| null, | ||
| state: null, | ||
| TimeSpan.Zero, | ||
| TimeSpan.FromMilliseconds(ReportingInterval) | ||
| ); |
There was a problem hiding this comment.
The Timer is created in a method that returns a deferred IAsyncEnumerable. Because it’s in a using var, it will be disposed as soon as TransformAsync returns (before enumeration starts), so progress reporting won’t run. Wrap the worker enumeration in an async iterator (or otherwise tie timer lifetime to enumeration) so the timer lives until the sequence completes/cancels.
| using var timer = new Timer | ||
| ( | ||
| _ => progress.Report(CreateProgressReport()), | ||
| null, | ||
| state: null, | ||
| TimeSpan.Zero, | ||
| TimeSpan.FromMilliseconds(ReportingInterval) | ||
| ); |
There was a problem hiding this comment.
Same timer-lifetime issue as the other TransformAsync overload: returning a deferred IAsyncEnumerable while the timer is in a using var disposes it immediately, preventing progress reports. Ensure the timer is disposed only after enumeration completes (e.g., by wrapping the worker in an async iterator that owns the timer).
| Console.WriteLine($"Transforming integer {item} to string."); | ||
| await Task.Delay(50); // Simulate some delay for transformation | ||
| await Task.Delay(50, CancellationToken.None).ConfigureAwait(false); // Simulate some delay for transformation | ||
| yield return item.ToString(); |
There was a problem hiding this comment.
This delay is using CancellationToken.None, which makes cancellation less responsive (the iterator won’t observe cancellation until after the delay completes). Pass the method’s token to Task.Delay so cancellation can interrupt the delay promptly.
| using var timer = new Timer | ||
| ( | ||
| _ => progress.Report(CreateProgressReport()), | ||
| null, | ||
| state: null, | ||
| TimeSpan.Zero, | ||
| TimeSpan.FromMilliseconds(ReportingInterval) | ||
| ); |
There was a problem hiding this comment.
ExtractAsync(progress) returns a deferred IAsyncEnumerable, but the progress Timer is in a using var, so it will be disposed as soon as the method returns (before enumeration starts). Wrap the worker enumeration in an async iterator that owns the timer (or otherwise keep the timer alive for the duration of enumeration) so progress reports are actually emitted.
| using var timer = new Timer | ||
| ( | ||
| _ => progress.Report(CreateProgressReport()), | ||
| null, | ||
| state: null, | ||
| TimeSpan.Zero, | ||
| TimeSpan.FromMilliseconds(ReportingInterval) | ||
| ); |
There was a problem hiding this comment.
Same timer-lifetime issue as the other LoadAsync overload: disposing the Timer via using var before the returned Task completes prevents progress reporting. Ensure the timer is disposed only after LoadWorkerAsync finishes/cancels (e.g., by making this method async and awaiting the worker).
| /// <summary> | ||
| /// Gets the current number of records skipped | ||
| /// </summary> | ||
| /// <exception cref="ArgumentOutOfRangeException">Value cannot be less than 0</exception> | ||
| public int CurrentSkippedItemCount | ||
| { | ||
| get => _currentSkippedItemCount; |
There was a problem hiding this comment.
CurrentSkippedItemCount has a backing field and a protected set, but the setter body never assigns _currentSkippedItemCount when the value is valid (it only throws for negatives). That means derived classes can’t actually update this property via the setter. Assign the backing field after validation (or remove the setter if it’s intentionally not settable).
| /// <summary> | ||
| /// Gets the current number of records skipped | ||
| /// </summary> | ||
| /// <exception cref="ArgumentOutOfRangeException">Value cannot be less than 0</exception> | ||
| public int CurrentSkippedItemCount | ||
| { | ||
| get => _currentSkippedItemCount; |
There was a problem hiding this comment.
CurrentSkippedItemCount exposes a protected set, but the setter implementation never assigns _currentSkippedItemCount when the value is valid (it only throws for negatives). This makes the property effectively write-only via IncrementCurrentSkippedItemCount and can break derived implementations that set the property directly. Assign the backing field after validation (or remove the setter if that’s the intent).
| if (value < 1) | ||
| { | ||
| throw new ArgumentOutOfRangeException(nameof(value), "Maximum item count cannot be less than 0."); | ||
| throw new ArgumentOutOfRangeException(nameof(value), "Maximum item count cannot be less than 1."); | ||
| } | ||
| _maximumItemCount = value; |
There was a problem hiding this comment.
This changes MaximumItemCount validation to disallow 0, while ExtractorBase.MaximumItemCount still allows 0. That makes the base-class APIs inconsistent and can be a breaking behavior change for consumers. Consider aligning all three base classes on the same allowed range (either allow 0 everywhere or disallow it everywhere) and update the docs/tests accordingly.
| if (value < 1) | ||
| { | ||
| throw new ArgumentOutOfRangeException(nameof(value), "Maximum item count cannot be less than 0."); | ||
| throw new ArgumentOutOfRangeException(nameof(value), "Maximum item count cannot be less than 1."); | ||
| } | ||
| _maximumItemCount = value; |
There was a problem hiding this comment.
This changes MaximumItemCount validation to disallow 0, but ExtractorBase.MaximumItemCount still allows 0. That inconsistency in the public base-class API can surprise consumers. Consider aligning the allowed range across Extractor/Transformer/Loader base classes and adjusting docs/tests to match.
Description
Modified code and examples to comply with code scan results
Fixes/Complete # (issue)