< Summary

Information
Class: ArturRios.Data.Export.Exporters.ExporterBase<T>
Assembly: ArturRios.Data.Export
File(s): /home/runner/work/dotnet-data/dotnet-data/src/ArturRios.Data.Export/Exporters/ExporterBase.cs
Line coverage
91%
Covered lines: 21
Uncovered lines: 2
Coverable lines: 23
Total lines: 92
Line coverage: 91.3%
Branch coverage
75%
Covered branches: 9
Total branches: 12
Branch coverage: 75%
Method coverage
100%
Covered methods: 6
Fully covered methods: 5
Total methods: 6
Method coverage: 100%
Full method coverage: 83.3%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
WriteAsync(...)100%11100%
WriteToFileAsync(...)100%11100%
GuardedWriteAsync()100%44100%
GuardedFileAsync()50%4480%
Fail(...)75%44100%

File(s)

/home/runner/work/dotnet-data/dotnet-data/src/ArturRios.Data.Export/Exporters/ExporterBase.cs

#LineLine coverage
 1using ArturRios.Data.Export.Interfaces;
 2using ArturRios.Output;
 3using Microsoft.Extensions.Logging;
 4
 5namespace ArturRios.Data.Export.Exporters;
 6
 7/// <summary>
 8///     Base for exporters: handles null-guarding, envelope conversion, cancellation propagation, and
 9///     file-stream lifetime. Concrete exporters implement <see cref="WriteCoreAsync" />.
 10/// </summary>
 11/// <typeparam name="T">The record type.</typeparam>
 12/// <param name="logger">
 13///     Optional logger. Envelopes carry no exception text, so a write failure is otherwise
 14///     undiagnosable: supply a logger and the full exception, plus the destination path for file
 15///     writes, is written at <see cref="LogLevel.Error" />. Record contents are never logged.
 16///     Resolved from DI when logging is registered.
 17/// </param>
 7018public abstract class ExporterBase<T>(ILogger? logger = null) : IExporter<T> where T : class
 19{
 20    /// <summary>Message returned when a write fails.</summary>
 21    protected const string ExportFailedMessage = "An export error occurred.";
 22
 23    /// <summary>Message returned when the caller passes no records.</summary>
 24    protected const string NullDataMessage = "An export error occurred: data is null.";
 25
 26    /// <summary>Message returned when the caller passes no destination stream.</summary>
 27    protected const string NullDestinationMessage = "An export error occurred: destination is null.";
 28
 29    /// <summary>Message returned when the caller passes no destination path.</summary>
 30    protected const string EmptyPathMessage = "An export error occurred: path is null or empty.";
 31
 32    /// <inheritdoc />
 33    public Task<ProcessOutput> WriteAsync(IEnumerable<T> data, Stream destination, CancellationToken ct = default) =>
 8434        GuardedWriteAsync(data, destination, stream => WriteCoreAsync(data, stream, ct));
 35
 36    /// <inheritdoc />
 37    public Task<ProcessOutput> WriteToFileAsync(IEnumerable<T> data, string path, CancellationToken ct = default) =>
 838        GuardedFileAsync(data, path, stream => WriteCoreAsync(data, stream, ct));
 39
 40    /// <summary>Guards a stream write: null checks, envelope conversion, cancellation propagation.</summary>
 41    protected async Task<ProcessOutput> GuardedWriteAsync(IEnumerable<T> data, Stream destination,
 42        Func<Stream, Task> write)
 43    {
 5244        if (data is null) return ProcessOutput.New.WithError(NullDataMessage);
 4645        if (destination is null) return ProcessOutput.New.WithError(NullDestinationMessage);
 46
 47        try
 48        {
 4249            await write(destination).ConfigureAwait(false);
 3650            return ProcessOutput.New;
 51        }
 452        catch (OperationCanceledException) { throw; }
 853        catch (Exception ex) { return Fail(ex, destination: null); }
 4654    }
 55
 56    /// <summary>Guards a file write: opens/truncates the file, then delegates to <paramref name="write" />.</summary>
 57    protected async Task<ProcessOutput> GuardedFileAsync(IEnumerable<T> data, string path, Func<Stream, Task> write)
 58    {
 459        if (data is null) return ProcessOutput.New.WithError(NullDataMessage);
 460        if (string.IsNullOrEmpty(path)) return ProcessOutput.New.WithError(EmptyPathMessage);
 61
 62        try
 63        {
 464            var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
 65
 466            await using var streamScope = stream.ConfigureAwait(false);
 67
 468            await write(stream).ConfigureAwait(false);
 269            return ProcessOutput.New;
 070        }
 071        catch (OperationCanceledException) { throw; }
 472        catch (Exception ex) { return Fail(ex, path); }
 473    }
 74
 75    /// <summary>
 76    ///     Logs the failure when a logger is configured, and returns the caller-safe envelope.
 77    ///     Exception text embeds absolute paths and OS error detail, so it goes to the log and
 78    ///     never to the caller.
 79    /// </summary>
 80    /// <param name="ex">The exception caught by a guard.</param>
 81    /// <param name="destination">The file path being written, or <see langword="null" /> for a stream write.</param>
 82    protected ProcessOutput Fail(Exception ex, string? destination)
 83    {
 684        logger?.LogError(ex, "Export failed. Exporter: {Exporter}, record: {Record}, destination: {Destination}",
 685            GetType().Name, typeof(T).Name, destination ?? "<stream>");
 86
 687        return ProcessOutput.New.WithError(ExportFailedMessage);
 88    }
 89
 90    /// <summary>Format-specific write. Implementations must honor <paramref name="ct" /> and not dispose the stream.</s
 91    protected abstract Task WriteCoreAsync(IEnumerable<T> data, Stream destination, CancellationToken ct);
 92}