Skip to content

Commit db94bd8

Browse files
committed
Minimized memory allocations when writing large strings
1 parent fec56fb commit db94bd8

1 file changed

Lines changed: 75 additions & 20 deletions

File tree

CsvExport/CsvExport.cs

Lines changed: 75 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,12 @@ public class CsvExport
4444
/// <summary>
4545
/// The list of rows
4646
/// </summary>
47-
List<List<string>> _rows = new();
47+
List<List<object>> _rows = new();
4848

4949
/// <summary>
5050
/// The current row
5151
/// </summary>
52-
List<string> _currentRow = null;
52+
List<object> _currentRow = null;
5353

5454
/// <summary>
5555
/// The string used to separate columns in the output
@@ -113,7 +113,7 @@ public object this[string field]
113113
while (num >= _currentRow.Count) //fill the current row with nulls until we have the right size
114114
_currentRow.Add(null);
115115

116-
_currentRow[num] = MakeValueCsvFriendly(value, _columnSeparator); //set the value at position
116+
_currentRow[num] = value; //set the raw value at position
117117
}
118118
}
119119

@@ -158,19 +158,61 @@ public void AddRows<T>(IEnumerable<T> list)
158158
/// </param>
159159
public static string MakeValueCsvFriendly(object value, string columnSeparator = ",")
160160
{
161-
if (value == null) return "";
162-
if (value is INullable && ((INullable)value).IsNull) return "";
161+
var sb = new StringBuilder();
162+
WriteCsvFriendlyValue(value, new StringBuilderCsvWriter(sb), columnSeparator);
163+
return sb.ToString();
164+
}
165+
166+
/// <summary>
167+
/// Interface for abstracting write operations to different output targets
168+
/// </summary>
169+
private interface ICsvWriter
170+
{
171+
void Write(string value);
172+
void Write(char value);
173+
}
174+
175+
/// <summary>
176+
/// StringBuilder wrapper for ICsvWriter
177+
/// </summary>
178+
private class StringBuilderCsvWriter : ICsvWriter
179+
{
180+
private readonly StringBuilder _sb;
181+
public StringBuilderCsvWriter(StringBuilder sb) => _sb = sb;
182+
public void Write(string value) => _sb.Append(value);
183+
public void Write(char value) => _sb.Append(value);
184+
}
185+
186+
/// <summary>
187+
/// StreamWriter wrapper for ICsvWriter
188+
/// </summary>
189+
private class StreamWriterCsvWriter : ICsvWriter
190+
{
191+
private readonly StreamWriter _sw;
192+
public StreamWriterCsvWriter(StreamWriter sw) => _sw = sw;
193+
public void Write(string value) => _sw.Write(value);
194+
public void Write(char value) => _sw.Write(value);
195+
}
196+
197+
/// <summary>
198+
/// Converts a value to CSV-friendly format and writes it directly to an ICsvWriter
199+
/// </summary>
200+
private static void WriteCsvFriendlyValue(object value, ICsvWriter writer, string columnSeparator = ",")
201+
{
202+
if (value == null) return;
203+
if (value is INullable && ((INullable)value).IsNull) return;
163204

164205
if (value is DateTime date)
165206
{
166207
if (date.TimeOfDay.TotalSeconds == 0)
167208
{
168-
return date.ToString("yyyy-MM-dd");
209+
writer.Write(date.ToString("yyyy-MM-dd"));
169210
}
170211
else
171212
{
172-
return date.ToString("yyyy-MM-dd HH:mm:ss");
213+
writer.Write(date.ToString("yyyy-MM-dd HH:mm:ss"));
173214
}
215+
return;
174216
}
175217

176218
var output = value.ToString().Trim();
@@ -179,20 +221,26 @@ public static string MakeValueCsvFriendly(object value, string columnSeparator =
179221
output = output.Substring(0, 30000);
180222

181223
if (output.Contains(columnSeparator) || output.Contains('\"') || output.Contains('\n') || output.Contains('\r'))
182-
output = '"' + output.Replace("\"", "\"\"") + '"';
183-
184-
return output;
224+
{
225+
writer.Write('"');
226+
writer.Write(output.Replace("\"", "\"\""));
227+
writer.Write('"');
228+
}
229+
else
230+
{
231+
writer.Write(output);
232+
}
185233
}
186234

187235
/// <summary>
188236
/// Outputs all rows as a CSV, returning one "line" at a time
189-
/// Where "line" is a IEnumerable of string values
237+
/// Where "line" is a IEnumerable of object values
190238
/// </summary>
191-
private IEnumerable<IEnumerable<string>> ExportToLines()
239+
private IEnumerable<IEnumerable<object>> ExportToLines()
192240
{
193241
// The header
194242
if (_includeHeaderRow)
195-
yield return _fields.OrderBy(f => f.Value).Select(f => MakeValueCsvFriendly(f.Key, _columnSeparator));
243+
yield return _fields.OrderBy(f => f.Value).Select(f => f.Key);
196244

197245
// The rows
198246
foreach (var row in _rows)
@@ -207,18 +255,22 @@ private IEnumerable<IEnumerable<string>> ExportToLines()
207255
public string Export()
208256
{
209257
StringBuilder sb = new StringBuilder();
258+
ICsvWriter writer = new StringBuilderCsvWriter(sb);
210259

211260
if (_includeColumnSeparatorDefinitionPreamble)
212261
sb.Append("sep=" + _columnSeparator + "\r\n");
213262

214263
foreach (var line in ExportToLines())
215264
{
265+
bool first = true;
216266
foreach (var value in line)
217267
{
218-
sb.Append(value);
219-
sb.Append(_columnSeparator);
268+
if (!first)
269+
sb.Append(_columnSeparator);
270+
271+
WriteCsvFriendlyValue(value, writer, _columnSeparator);
272+
first = false;
220273
}
221-
sb.Length = sb.Length - _columnSeparator.Length; //remove the trailing comma (shut up)
222274
sb.Append("\r\n");
223275
}
224276

@@ -254,18 +306,21 @@ public MemoryStream ExportAsMemoryStream(Encoding encoding = null)
254306

255307
using (var sw = new StreamWriter(ms, encoding, 1024, leaveOpen: true))
256308
{
309+
ICsvWriter writer = new StreamWriterCsvWriter(sw);
310+
257311
if (_includeColumnSeparatorDefinitionPreamble)
258312
sw.Write("sep=" + _columnSeparator + "\r\n");
259313

260314
foreach (var line in ExportToLines())
261315
{
262-
int i = 0;
316+
bool first = true;
263317
foreach (var value in line)
264318
{
265-
sw.Write(value);
266-
267-
if (++i != _fields.Count)
319+
if (!first)
268320
sw.Write(_columnSeparator);
321+
322+
WriteCsvFriendlyValue(value, writer, _columnSeparator);
323+
first = false;
269324
}
270325
sw.Write("\r\n");
271326
}

0 commit comments

Comments
 (0)