• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

loresoft / FluentCommand / 16327866078

16 Jul 2025 05:23PM UTC coverage: 55.131% (+0.1%) from 54.995%
16327866078

push

github

pwelter34
improve Imports

1735 of 3650 branches covered (47.53%)

Branch coverage included in aggregate %.

16 of 18 new or added lines in 1 file covered. (88.89%)

7 existing lines in 1 file now uncovered.

4384 of 7449 relevant lines covered (58.85%)

230.55 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

63.83
/src/FluentCommand.Csv/CsvCommandExtensions.cs
1
using System.Buffers;
2
using System.Data;
3
using System.Data.Common;
4
using System.Globalization;
5
using System.Text;
6

7
using FluentCommand.Extensions;
8

9
using Microsoft.IO;
10

11
namespace FluentCommand;
12

13
/// <summary>
14
/// Provides extension methods for <see cref="IDataCommand"/> to export query results as CSV data.
15
/// </summary>
16
public static class CsvCommandExtensions
17
{
18
    private static readonly RecyclableMemoryStreamManager _memoryStreamManager = new();
1✔
19
    private static readonly SearchValues<char> SpecialChars = SearchValues.Create(",\"\n\r");
1✔
20

21
    /// <summary>
22
    /// Executes the query and returns a CSV string from the data set returned by the query.
23
    /// </summary>
24
    /// <param name="dataCommand">The data command to execute.</param>
25
    /// <returns>
26
    /// A CSV string representing the <see cref="IDataReader"/> result of the command.
27
    /// </returns>
28
    /// <exception cref="ArgumentNullException"><paramref name="dataCommand"/> is <c>null</c>.</exception>
29
    public static string QueryCsv(this IDataCommand dataCommand)
30
    {
31
        if (dataCommand is null)
1!
32
            throw new ArgumentNullException(nameof(dataCommand));
×
33

34
        using var stream = _memoryStreamManager.GetStream();
1✔
35

36
        QueryCsv(dataCommand, stream);
1✔
37

38
        var bytes = stream.GetReadOnlySequence();
1✔
39

40
#if NET5_0_OR_GREATER
41
        return Encoding.UTF8.GetString(bytes);
1✔
42
#else
43
        return Encoding.UTF8.GetString(bytes.ToArray());
44
#endif
45
    }
1✔
46

47
    /// <summary>
48
    /// Executes the query and writes the CSV data to the specified <paramref name="stream"/>.
49
    /// </summary>
50
    /// <param name="dataCommand">The data command to execute.</param>
51
    /// <param name="stream">The stream to which the CSV data will be written.</param>
52
    /// <exception cref="ArgumentNullException">
53
    /// <paramref name="dataCommand"/> or <paramref name="stream"/> is <c>null</c>.
54
    /// </exception>
55
    public static void QueryCsv(this IDataCommand dataCommand, Stream stream)
56
    {
57
        if (dataCommand is null)
2!
58
            throw new ArgumentNullException(nameof(dataCommand));
×
59
        if (stream is null)
2!
60
            throw new ArgumentNullException(nameof(stream));
×
61

62
        using var streamWriter = new StreamWriter(stream, Encoding.UTF8, 1024, true);
2✔
63

64
        dataCommand.Read(
2✔
65
            readAction: reader => WriteData(streamWriter, reader),
2✔
66
            commandBehavior: CommandBehavior.SequentialAccess | CommandBehavior.SingleResult);
2✔
67

68
        streamWriter.Flush();
2✔
69
    }
4✔
70

71
    /// <summary>
72
    /// Executes the query and returns a CSV string from the data set returned by the query asynchronously.
73
    /// </summary>
74
    /// <param name="dataCommand">The data command to execute.</param>
75
    /// <param name="cancellationToken">The cancellation token to observe.</param>
76
    /// <returns>
77
    /// A task representing the asynchronous operation. The result contains a CSV string representing the <see cref="IDataReader"/> result of the command.
78
    /// </returns>
79
    /// <exception cref="ArgumentNullException"><paramref name="dataCommand"/> is <c>null</c>.</exception>
80
    public static async Task<string> QueryCsvAsync(this IDataCommand dataCommand, CancellationToken cancellationToken = default)
81
    {
82
        if (dataCommand is null)
2!
83
            throw new ArgumentNullException(nameof(dataCommand));
×
84

85
        using var stream = _memoryStreamManager.GetStream();
2✔
86

87
        await QueryCsvAsync(dataCommand, stream, cancellationToken);
2✔
88

89
        var bytes = stream.GetReadOnlySequence();
2✔
90

91
#if NET5_0_OR_GREATER
92
        return Encoding.UTF8.GetString(bytes);
2✔
93
#else
94
        return Encoding.UTF8.GetString(bytes.ToArray());
95
#endif
96
    }
2✔
97

98
    /// <summary>
99
    /// Executes the query and writes the CSV data to the specified <paramref name="stream"/> asynchronously.
100
    /// </summary>
101
    /// <param name="dataCommand">The data command to execute.</param>
102
    /// <param name="stream">The stream to which the CSV data will be written.</param>
103
    /// <param name="cancellationToken">The cancellation token to observe.</param>
104
    /// <returns>
105
    /// A task representing the asynchronous operation.
106
    /// </returns>
107
    /// <exception cref="ArgumentNullException">
108
    /// <paramref name="dataCommand"/> or <paramref name="stream"/> is <c>null</c>.
109
    /// </exception>
110
    public static async Task QueryCsvAsync(this IDataCommand dataCommand, Stream stream, CancellationToken cancellationToken = default)
111
    {
112
        if (dataCommand is null)
3!
113
            throw new ArgumentNullException(nameof(dataCommand));
×
114
        if (stream is null)
3!
115
            throw new ArgumentNullException(nameof(stream));
×
116

117
        using var streamWriter = new StreamWriter(stream, Encoding.UTF8, 1024, true);
3✔
118

119
        await dataCommand.ReadAsync(
3✔
120
            readAction: async (reader, token) =>
3✔
121
            {
3✔
122
                if (reader is DbDataReader dataReader)
3!
123
                    await WriteDataAsync(streamWriter, dataReader, token);
3✔
124
                else
3✔
125
                    WriteData(streamWriter, reader);
×
126
            },
3✔
127
            commandBehavior: CommandBehavior.SequentialAccess | CommandBehavior.SingleResult,
3✔
128
            cancellationToken: cancellationToken);
3✔
129

130
        await streamWriter.FlushAsync(cancellationToken);
3✔
131
    }
3✔
132

133

134
    private static void WriteData(TextWriter writer, IDataReader reader)
135
    {
136
        var wroteHeader = false;
2✔
137
        Type[] rowTypes = null;
2✔
138

139
        while (reader.Read())
2,002✔
140
        {
141
            if (!wroteHeader)
2,000✔
142
            {
143
                WriteHeader(writer, reader);
2✔
144
                wroteHeader = true;
2✔
145
            }
146

147
            if (rowTypes is null)
2,000✔
148
            {
149
                rowTypes = new Type[reader.FieldCount];
2✔
150
                for (int i = 0; i < reader.FieldCount; i++)
80✔
151
                    rowTypes[i] = reader.GetFieldType(i);
38✔
152
            }
153

154
            WriteRow(writer, reader, rowTypes);
2,000✔
155
        }
156
    }
2✔
157

158
    private static async Task WriteDataAsync(TextWriter writer, DbDataReader reader, CancellationToken cancellationToken = default)
159
    {
160
        var wroteHeader = false;
3✔
161
        Type[] rowTypes = null;
3✔
162

163
        while (await reader.ReadAsync(cancellationToken))
2,009✔
164
        {
165
            if (!wroteHeader)
2,006✔
166
            {
167
                WriteHeader(writer, reader);
3✔
168
                wroteHeader = true;
3✔
169
            }
170

171
            if (rowTypes is null)
2,006✔
172
            {
173
                rowTypes = new Type[reader.FieldCount];
3✔
174
                for (int i = 0; i < reader.FieldCount; i++)
102✔
175
                    rowTypes[i] = reader.GetFieldType(i);
48✔
176
            }
177

178
            WriteRow(writer, reader, rowTypes);
2,006✔
179
        }
180
    }
3✔
181

182
    private static void WriteHeader(TextWriter writer, IDataReader reader)
183
    {
184
        for (int index = 0; index < reader.FieldCount; index++)
182✔
185
        {
186
            if (index > 0)
86✔
187
                writer.Write(',');
81✔
188

189
            var name = reader.GetName(index);
86✔
190
            WriteValue(writer, name);
86✔
191
        }
192

193
        writer.WriteLine();
5✔
194
    }
5✔
195

196
    private static void WriteRow(TextWriter writer, IDataReader reader, Type[] rowTypes)
197
    {
198
        for (int index = 0; index < reader.FieldCount; index++)
160,132✔
199
        {
200
            if (index > 0)
76,060✔
201
                writer.Write(',');
72,054✔
202

203
            WriteValue(writer, reader, index, rowTypes);
76,060✔
204
        }
205
        writer.WriteLine();
4,006✔
206
    }
4,006✔
207

208
    private static void WriteValue(TextWriter writer, IDataReader reader, int index, Type[] rowTypes)
209
    {
210
        if (reader.IsDBNull(index))
76,060✔
211
        {
212
            writer.Write(string.Empty);
24,012✔
213
            return;
24,012✔
214
        }
215

216
        var type = rowTypes[index];
52,048✔
217
        if (type == typeof(string))
52,048✔
218
        {
219
            var value = reader.GetString(index);
20,012✔
220
            WriteValue(writer, value);
20,012✔
221
            return;
20,012✔
222
        }
223
        if (type == typeof(char))
32,036!
224
        {
225
            var value = reader.GetChar(index);
×
226
            writer.Write(value);
×
227
            return;
×
228
        }
229

230
        if (type == typeof(char[]))
32,036!
231
        {
232
            var value = reader.GetString(index);
×
233
            WriteValue(writer, value);
×
234
            return;
×
235
        }
236

237
        if (type == typeof(bool))
32,036✔
238
        {
239
            var value = reader.GetBoolean(index);
12,006✔
240
            writer.Write(value);
12,006✔
241
            return;
12,006✔
242
        }
243

244
        if (type == typeof(byte))
20,030!
245
        {
246
            var value = reader.GetByte(index);
×
247
            writer.Write(value);
×
248
            return;
×
249
        }
250

251
        if (type == typeof(short))
20,030!
252
        {
253
            var value = reader.GetInt16(index);
×
254
            writer.Write(value);
×
255
            return;
×
256
        }
257

258
        if (type == typeof(int))
20,030✔
259
        {
260
            var value = reader.GetInt32(index);
4,012✔
261
            writer.Write(value);
4,012✔
262
            return;
4,012✔
263
        }
264

265
        if (type == typeof(long))
16,018!
266
        {
267
            var value = reader.GetInt64(index);
×
268
            writer.Write(value);
×
269
            return;
×
270
        }
271

272
        if (type == typeof(float))
16,018!
273
        {
274
            var value = reader.GetFloat(index);
×
275
            writer.Write(value);
×
276
            return;
×
277
        }
278

279
        if (type == typeof(double))
16,018!
280
        {
281
            var value = reader.GetDouble(index);
×
282
            writer.Write(value);
×
283
            return;
×
284
        }
285

286
        if (type == typeof(decimal))
16,018!
287
        {
288
            var value = reader.GetDecimal(index);
×
289
            writer.Write(value);
×
290
            return;
×
291
        }
292

293
#if NET6_0_OR_GREATER
294
        if (type == typeof(DateOnly))
16,018!
295
        {
296
            var value = reader.GetValue<DateOnly>(index);
×
297
            var formatted = value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
×
298

299
            writer.Write(formatted);
×
300
            return;
×
301
        }
302

303
        if (type == typeof(TimeOnly))
16,018!
304
        {
305
            var value = reader.GetValue<TimeOnly>(index);
×
306
            var formatted = value.Second == 0 && value.Millisecond == 0
×
307
                ? value.ToString("HH:mm", CultureInfo.InvariantCulture)
×
308
                : value.ToString("HH:mm:ss.fffffff", CultureInfo.InvariantCulture);
×
309

310
            writer.Write(formatted);
×
311
            return;
×
312
        }
313
#endif
314

315
        if (type == typeof(TimeSpan))
16,018!
316
        {
317
            var value = reader.GetValue<TimeSpan>(index);
×
318
            var formatted = value.Seconds == 0 && value.Milliseconds == 0
×
319
                ? value.ToString(@"hh\:mm", CultureInfo.InvariantCulture)
×
320
                : value.ToString(@"hh\:mm\:ss\.fffffff", CultureInfo.InvariantCulture);
×
321

322
            writer.Write(formatted);
×
323
            return;
×
324
        }
325

326
        if (type == typeof(DateTime))
16,018!
327
        {
328
            var value = reader.GetDateTime(index);
×
329
            var formatted = value.TimeOfDay == TimeSpan.Zero
×
330
                ? value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)
×
331
                : value.ToString("yyyy-MM-dd'T'HH:mm:ss.fffffff", CultureInfo.InvariantCulture);
×
332

333
            writer.Write(formatted);
×
334
            return;
×
335
        }
336

337
        if (type == typeof(DateTimeOffset))
16,018✔
338
        {
339
            var value = reader.GetValue<DateTimeOffset>(index);
8,012✔
340
            var formatted = value.ToString("yyyy-MM-dd'T'HH:mm:ss.fffffffK", CultureInfo.InvariantCulture);
8,012✔
341

342
            writer.Write(formatted);
8,012✔
343
            return;
8,012✔
344
        }
345

346
        if (type == typeof(Guid))
8,006✔
347
        {
348
            var value = reader.GetGuid(index);
4,000✔
349
            var formatted = value.ToString("D", CultureInfo.InvariantCulture);
4,000✔
350

351
            writer.Write(formatted);
4,000✔
352
            return;
4,000✔
353
        }
354

355
        if (type == typeof(byte[]))
4,006!
356
        {
357
            var value = reader.GetBytes(index);
4,006✔
358
            var hex = Convert.ToHexString(value);
4,006✔
359

360
            writer.Write(hex);
4,006✔
361
            return;
4,006✔
362
        }
363

364
        // fallback
365
        var fieldValue = reader.GetValue(index);
×
366
        var formattedValue = Convert.ToString(fieldValue, CultureInfo.InvariantCulture);
×
367
        WriteValue(writer, formattedValue);
×
368
    }
×
369

370
    private static void WriteValue(TextWriter writer, string value)
371
    {
372
        if (string.IsNullOrEmpty(value))
20,098!
373
            return;
×
374

375
        var span = value.AsSpan();
20,098✔
376
        var needsQuotes = span.ContainsAny(SpecialChars);
20,098✔
377

378
        if (!needsQuotes)
20,098!
379
        {
380
            // Fast path: no special chars, write directly
381
            writer.Write(value);
20,098✔
382
            return;
20,098✔
383
        }
384

385
        // write with quotes and escape any quotes within the value
UNCOV
386
        writer.Write('"');
×
UNCOV
387
        for (var i = 0; i < value.Length; i++)
×
388
        {
UNCOV
389
            var ch = value[i];
×
390

UNCOV
391
            if (ch == '"')
×
392
                writer.Write("\"\"");
×
393
            else
UNCOV
394
                writer.Write(ch);
×
395
        }
UNCOV
396
        writer.Write('"');
×
UNCOV
397
    }
×
398
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc