< Summary

Information
Class: ArturRios.Data.Export.Exporters.CsvExporter<T>
Assembly: ArturRios.Data.Export
File(s): /home/ci/agent/workspace/dotnet-data-docs_main/app/src/ArturRios.Data.Export/Exporters/CsvExporter.cs
Line coverage
100%
Covered lines: 21
Uncovered lines: 0
Coverable lines: 21
Total lines: 67
Line coverage: 100%
Branch coverage
88%
Covered branches: 23
Total branches: 26
Branch coverage: 88.4%
Method coverage
100%
Covered methods: 5
Fully covered methods: 5
Total methods: 5
Method coverage: 100%
Full method coverage: 100%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
WriteCoreAsync()100%44100%
.cctor()100%11100%
Render(...)78.57%1414100%
Escape(...)100%88100%

File(s)

/home/ci/agent/workspace/dotnet-data-docs_main/app/src/ArturRios.Data.Export/Exporters/CsvExporter.cs

#LineLine coverage
 1using ArturRios.Data.Export.Abstractions;
 2using ArturRios.Data.Export.Configuration;
 3using Microsoft.Extensions.Logging;
 4
 5namespace ArturRios.Data.Export.Exporters;
 6
 7/// <summary>Writes records as delimited text using the shared column map and RFC 4180 quoting.</summary>
 8/// <remarks>
 9///     Quoting follows RFC 4180: a field is quoted when it holds the delimiter, a double quote or a line
 10///     break, and an embedded quote is doubled. The delimiter and the line terminator are not fixed to the
 11///     RFC's comma and CRLF - the delimiter comes from the options and lines end with the platform's
 12///     newline - so the output is RFC 4180 only when those happen to match.
 13///     Text that a spreadsheet would evaluate as a formula is prefixed with <c>'</c> unless
 14///     <see cref="CsvOptions.EscapeFormulas" /> is off.
 15/// </remarks>
 16/// <typeparam name="T">The record type.</typeparam>
 17/// <param name="options">CSV options.</param>
 18/// <param name="logger">Optional logger; see <see cref="ExporterBase{T}" />.</param>
 19public class CsvExporter<T>(CsvOptions options, ILogger<CsvExporter<T>>? logger = null)
 3620    : ExporterBase<T>(logger) where T : class
 21{
 22    /// <inheritdoc />
 23    protected override async Task WriteCoreAsync(IEnumerable<T> data, Stream destination, CancellationToken ct)
 24    {
 3225        var columns = ColumnMap.For<T>();
 3226        var writer = new StreamWriter(destination, options.Encoding, leaveOpen: true);
 27
 3228        await using var writerScope = writer.ConfigureAwait(false);
 29
 3230        if (options.IncludeHeader)
 31        {
 1032            await writer.WriteLineAsync(string.Join(options.Delimiter,
 4033                columns.Select(c => Escape(c.Header, options.Delimiter)))).ConfigureAwait(false);
 34        }
 35
 12436        foreach (var item in data)
 37        {
 3038            ct.ThrowIfCancellationRequested();
 3039            await writer.WriteLineAsync(string.Join(options.Delimiter,
 12040                columns.Select(c => Escape(Render(c.Getter(item)), options.Delimiter)))).ConfigureAwait(false);
 41        }
 42
 3243        await writer.FlushAsync(ct).ConfigureAwait(false);
 3244    }
 45
 46    // Characters that make a spreadsheet treat a cell as a formula (OWASP "CSV Injection").
 447    private static readonly char[] FormulaTriggers = ['=', '+', '-', '@', '\t', '\r'];
 48
 49    private string Render(object? value)
 50    {
 9051        var text = ValueRenderer.Render(value);
 52
 53        // Only text is neutralized: a formatted value (number, date, enum, Guid) is produced by the runtime,
 54        // not by whoever supplied the data, and a negative number must stay a number.
 9055        var isText = value is string or char || value is not null and not IFormattable;
 56
 9057        return options.EscapeFormulas && isText && text.Length > 0 && FormulaTriggers.Contains(text[0])
 9058            ? "'" + text
 9059            : text;
 60    }
 61
 62    private static string Escape(string field, char delimiter)
 63    {
 12064        var mustQuote = field.Contains(delimiter) || field.Contains('"') || field.Contains('\n') || field.Contains('\r')
 12065        return mustQuote ? $"\"{field.Replace("\"", "\"\"")}\"" : field;
 66    }
 67}