< Summary

Information
Class: ArturRios.Data.Export.Exporters.ExporterBase<T>
Assembly: ArturRios.Data.Export
File(s): /home/ci/agent/workspace/dotnet-data-docs_main/app/src/ArturRios.Data.Export/Exporters/ExporterBase.cs
Line coverage
92%
Covered lines: 39
Uncovered lines: 3
Coverable lines: 42
Total lines: 132
Line coverage: 92.8%
Branch coverage
71%
Covered branches: 10
Total branches: 14
Branch coverage: 71.4%
Method coverage
100%
Covered methods: 7
Fully covered methods: 5
Total methods: 7
Method coverage: 100%
Full method coverage: 71.4%

Metrics

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

File(s)

/home/ci/agent/workspace/dotnet-data-docs_main/app/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>
 10818public 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) =>
 14434        GuardedWriteAsync(data, destination, stream => WriteCoreAsync(data, stream, ct));
 35
 36    /// <inheritdoc />
 37    public Task<ProcessOutput> WriteToFileAsync(IEnumerable<T> data, string path, CancellationToken ct = default) =>
 2238        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    {
 7844        if (data is null)
 445            return ProcessOutput.New.WithError(NullDataMessage);
 7446        if (destination is null)
 247            return ProcessOutput.New.WithError(NullDestinationMessage);
 48
 49        try
 50        {
 7251            await write(destination).ConfigureAwait(false);
 6652            return ProcessOutput.New;
 53        }
 454        catch (OperationCanceledException) { throw; }
 855        catch (Exception ex) { return Fail(ex, destination: null); }
 7656    }
 57
 58    /// <summary>
 59    ///     Guards a file write: writes through <paramref name="write" /> to a temporary file beside
 60    ///     <paramref name="path" />, then moves it over <paramref name="path" />. A write that fails or is
 61    ///     cancelled leaves an existing file untouched and no partial file behind.
 62    /// </summary>
 63    protected async Task<ProcessOutput> GuardedFileAsync(IEnumerable<T> data, string path, Func<Stream, Task> write)
 64    {
 1265        if (data is null)
 066            return ProcessOutput.New.WithError(NullDataMessage);
 1267        if (string.IsNullOrEmpty(path))
 068            return ProcessOutput.New.WithError(EmptyPathMessage);
 69
 1270        string? temporaryPath = null;
 71        try
 72        {
 1273            var fullPath = Path.GetFullPath(path);
 1274            temporaryPath = Path.Combine(Path.GetDirectoryName(fullPath)!,
 1275                $".{Path.GetFileName(fullPath)}.{Guid.NewGuid():N}.tmp");
 76
 1277            var stream = new FileStream(temporaryPath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
 1078            await using (stream.ConfigureAwait(false))
 79            {
 1080                await write(stream).ConfigureAwait(false);
 81            }
 82
 483            File.Move(temporaryPath, fullPath, overwrite: true);
 484            return ProcessOutput.New;
 85        }
 286        catch (OperationCanceledException)
 87        {
 288            DeleteQuietly(temporaryPath);
 289            throw;
 90        }
 691        catch (Exception ex)
 92        {
 693            DeleteQuietly(temporaryPath);
 694            return Fail(ex, path);
 95        }
 1096    }
 97
 98    private static void DeleteQuietly(string? path)
 99    {
 8100        if (path is null)
 101        {
 0102            return;
 103        }
 104
 105        try
 106        {
 8107            File.Delete(path);
 6108        }
 2109        catch
 110        {
 111            // Best effort: the temporary file is hidden and uniquely named, so a leftover never collides.
 2112        }
 8113    }
 114
 115    /// <summary>
 116    ///     Logs the failure when a logger is configured, and returns the caller-safe envelope.
 117    ///     Exception text embeds absolute paths and OS error detail, so it goes to the log and
 118    ///     never to the caller.
 119    /// </summary>
 120    /// <param name="ex">The exception caught by a guard.</param>
 121    /// <param name="destination">The file path being written, or <see langword="null" /> for a stream write.</param>
 122    protected ProcessOutput Fail(Exception ex, string? destination)
 123    {
 10124        logger?.LogError(ex, "Export failed. Exporter: {Exporter}, record: {Record}, destination: {Destination}",
 10125            GetType().Name, typeof(T).Name, destination ?? "<stream>");
 126
 10127        return ProcessOutput.New.WithError(ExportFailedMessage);
 128    }
 129
 130    /// <summary>Format-specific write. Implementations must honor <paramref name="ct" /> and not dispose the stream.</s
 131    protected abstract Task WriteCoreAsync(IEnumerable<T> data, Stream destination, CancellationToken ct);
 132}