How to Export a DataTable to CSV in C# and VB.NET (Escaping, UTF-8, Streaming)

3 Comments

If you need to export a DataTable to a CSV file in C# or VB.NET, the tricky part isn't writing the rows  it's escaping the values correctly. A product name with a comma in it, a customer note that contains a quotation mark, or a description with an embedded line break will silently break a naive CSV writer, and you won't notice until someone opens the file in Excel and the columns are scrambled.

This article walks through a small, dependency-free WriteCsvField helper method, in both C# and VB.NET, that handles commas, quotes, and newlines properly, and shows how to wire it up against a DataTable to produce a valid .csv file. 

A DataSet can hold several DataTable objects, but CSV represents a single flat sequence of records, so if you're exporting a DataSet you'll need to export each table as its own CSV file, pick one table to export, or use a multi-sheet format such as .xlsx instead.

Quick answer: to export a DataTable to CSV in C# or VB.NET, iterate over each DataColumn and DataRow and pass every field — including the header row — through a helper that wraps it in double quotes whenever it contains a comma, a double quote, or a line break, doubling any embedded quotes. Convert non-string values with CultureInfo.InvariantCulture so numbers and dates don't shift with the server's locale, write the file as UTF-8 (with a BOM if Excel needs to auto-detect it), and for large exports write directly to a TextWriter instead of building the whole CSV as one in-memory string. The full walkthrough, working C# and VB.NET code, and the edge cases that trip up hand-rolled exporters follow below.

Why CSV Instead of a Native Excel Format

A common scenario: you're pulling a large result set from a stored procedure — say, a year's worth of transaction history — and a client wants it exported for reporting. 

Writing that data out as .xls or .xlsx through an interop library or an in-memory workbook object can be memory-heavy, because many Excel-writing approaches build the entire workbook in memory before saving it. 

On a big enough dataset, that can lead to an OutOfMemoryException, though the exact threshold depends on your available memory, the size of the dataset, and which library you're using — streaming-based XLSX writers can avoid this problem even for large exports.

CSV avoids much of that overhead because it's just text: you can build it line by line with a StringBuilder and write it out in one pass over the data, without holding a workbook object graph in memory. 

That said, this doesn't make the process memory-free — as with any approach that assembles a string first, the complete CSV text is still held in memory before it's written; see the streaming section further down if that's a concern for very large exports. CSV also doesn't preserve cell formatting, formulas, multiple worksheets, charts, or merged cells the way a native workbook format does. 

It's not a replacement for those features, but for raw tabular exports it's usually the simpler and cheaper option.

As a rough guide: reach for CSV when you need a single flat table, maximum interoperability with other systems, and a small, predictable file size. Reach for .xlsx instead when the export needs multiple worksheets, formulas, cell formatting, charts, or other Excel-specific features — CSV can't represent any of those.

Prerequisites

  • Working knowledge of C# or VB.NET and the basics of DataTable/DataRow/DataColumn.
  • A .NET project (console, ASP.NET Core, or WinForms/WPF) where you can write to disk or return a file stream. The examples below use APIs available across modern .NET and .NET Framework alike; nothing here depends on a specific recent language version.
  • No external NuGet packages are required for the approach shown here. If you're doing heavy CSV work across a large codebase, a library like CsvHelper is worth evaluating — it handles quoting, culture-specific formatting, and streaming for you — but understanding the manual approach first makes it easier to reason about what a library is doing under the hood.

Step 1: Build or Retrieve the DataTable

For this example, we'll construct a DataTable in code. In a real project this would usually come back from a stored procedure or a LINQ query projected into a DataTable. 

Note that Price is typed as decimal rather than double — for monetary values, decimal avoids the binary floating-point rounding issues that double can introduce.

C#

csharp
DataTable GetData()
{
    DataTable dt = new DataTable();
    dt.Columns.Add("CustomerId", typeof(int));
    dt.Columns.Add("CustomerName", typeof(string));
    dt.Columns.Add("ProductName", typeof(string));
    dt.Columns.Add("Price", typeof(decimal));

    dt.Rows.Add(1, "Nikunj Satasiya", "Laptop", 37000m);
    dt.Rows.Add(2, "Hiren Dobariya", "Mouse", 820m);
    dt.Rows.Add(3, "Vivek Ghadiya", "Pen, Blue", 250m);
    dt.Rows.Add(4, "Pratik Pansuriya", "Laptop \"Pro\" Model", 42000m);
    dt.Rows.Add(5, "Sneha Patel", "Lip Balm", 130m);
    dt.Rows.Add(6, "John Smith", "Notebook, 200 pages", 150m);

    return dt;
}

VB.NET

vb
Private Function GetData() As DataTable
    Dim dt As New DataTable()
    dt.Columns.Add("CustomerId", GetType(Integer))
    dt.Columns.Add("CustomerName", GetType(String))
    dt.Columns.Add("ProductName", GetType(String))
    dt.Columns.Add("Price", GetType(Decimal))

    dt.Rows.Add(1, "Nikunj Satasiya", "Laptop", 37000D)
    dt.Rows.Add(2, "Hiren Dobariya", "Mouse", 820D)
    dt.Rows.Add(3, "Vivek Ghadiya", "Pen, Blue", 250D)
    dt.Rows.Add(4, "Pratik Pansuriya", "Laptop ""Pro"" Model", 42000D)
    dt.Rows.Add(5, "Sneha Patel", "Lip Balm", 130D)
    dt.Rows.Add(6, "John Smith", "Notebook, 200 pages", 150D)

    Return dt
End Function

Note the deliberately awkward values in rows 3, 4, and 6 — a comma inside ProductName, a quote character inside another one, and a second comma in a different position. These are exactly the cases the escaping logic below needs to handle.

Step 2: Write a Correct CSV-Escaping Helper

RFC 4180 is the closest thing CSV has to a common reference, though it's worth being precise about what that means: it's an Informational RFC that documents a widely used CSV format and registers the text/csv media type, and it says plainly that CSV implementations vary. 

There is no single binding CSV standard, so different applications and consumers can impose their own additional conventions on top of it.

With that caveat, RFC 4180 documents the rule this article follows: a field containing a comma, a double quote, or a line break must be enclosed in double quotes, and an embedded double quote inside such a field is represented as two consecutive double quotes. 

Most real-world CSV consumers, including Excel, honor this rule even where they diverge from the RFC elsewhere.

A subtle bug shows up in a lot of hand-rolled CSV writers: they only quote a field when it contains a comma, and only escape internal quotes when a comma is *also* present. 

That means a value like He said "hello" — no comma, just a quote — gets written out unescaped and unquoted, which produces a malformed CSV. The corrected version below checks for quotes, commas, and newlines independently.

C#

csharp
public static string WriteCsvField(string input)
{
    if (input == null)
        return string.Empty;

    bool needsQuoting = input.Contains(',')
                      || input.Contains('"')
                      || input.Contains('\n')
                      || input.Contains('\r');

    if (!needsQuoting)
        return input;

    string escaped = input.Replace("\"", "\"\"");
    return "\"" + escaped + "\"";
}

VB.NET

vb
Public Shared Function WriteCsvField(ByVal input As String) As String
    If input Is Nothing Then
        Return String.Empty
    End If

    Dim needsQuoting As Boolean =
        input.Contains(",") OrElse
        input.Contains("""") OrElse
        input.Contains(vbLf) OrElse
        input.Contains(vbCr)

    If Not needsQuoting Then
        Return input
    End If

    Dim escaped As String = input.Replace("""", """""")
    Return """" + escaped + """"
End Function

Both versions now do the same three things: check independently for a comma, a quote, or a line break; double any embedded quotes when quoting is needed; and wrap the field in quotes. 

Neither version depends on both conditions being true at once, which is what made the earlier logic incomplete. The VB.NET version also compiles cleanly — it returns a value on every code path, which a working CSV helper has to do.

It's worth seeing what this looks like when a field's own value contains a line break, since it's easy to assume a CSV record is always exactly one physical line. A field with an embedded newline gets quoted like any other field that needs escaping, and the newline inside the quotes stays part of that field's value rather than starting a new record:

csv
CustomerName,Notes
John Smith,"Delivered on time.
Left a positive review."

A CSV-aware parser reads that as a single two-column record, even though it spans two physical lines in the file — the surrounding quotes are what tell it the record hasn't ended yet. 

A naive reader that just splits the file on newlines rather than respecting quoted fields will misread this as two separate rows, so make sure whatever consumes the file back in is CSV-aware rather than doing a plain line-by-line split.

Step 3: Assemble the CSV from the DataTable

With a correct field-level escaping function in place, building the full CSV is just iterating rows and columns and joining fields with commas.

One detail worth being explicit about: StringBuilder.AppendLine() and TextWriter.WriteLine() both write Environment.NewLine, which is \n on Linux/macOS and \r\n on Windows. RFC 4180 describes CRLF as the conventional record separator, and most consumers — including Excel — tolerate a bare \n too, but if you want the output to be the same regardless of which OS generated it, write the separator explicitly instead of relying on the platform default. The examples below do that with an explicit "\r\n" constant rather than AppendLine/WriteLine.

C#

csharp
private const string RecordSeparator = "\r\n";

public static string BuildCsv(DataTable dt)
{
    var sb = new StringBuilder();

    // Header row
    sb.Append(string.Join(",", dt.Columns
        .Cast<DataColumn>()
        .Select(c => WriteCsvField(c.ColumnName))));
    sb.Append(RecordSeparator);

    foreach (DataRow row in dt.Rows)
    {
        var fields = dt.Columns
            .Cast<DataColumn>()
            .Select(col => WriteCsvField(row[col] == DBNull.Value ? string.Empty : Convert.ToString(row[col], CultureInfo.InvariantCulture)));

        sb.Append(string.Join(",", fields));
        sb.Append(RecordSeparator);
    }

    return sb.ToString();
}

VB.NET

vb
Private Const RecordSeparator As String = vbCrLf

Public Shared Function BuildCsv(ByVal dt As DataTable) As String
    Dim sb As New StringBuilder()

    Dim headerFields = dt.Columns.Cast(Of DataColumn)() _
        .Select(Function(c) WriteCsvField(c.ColumnName))
    sb.Append(String.Join(",", headerFields))
    sb.Append(RecordSeparator)

    For Each row As DataRow In dt.Rows
        Dim fields = dt.Columns.Cast(Of DataColumn)() _
            .Select(Function(col) WriteCsvField(If(row(col) Is DBNull.Value, String.Empty, Convert.ToString(row(col), CultureInfo.InvariantCulture)))
        sb.Append(String.Join(",", fields))
        sb.Append(RecordSeparator)
    Next

    Return sb.ToString()
End Function

This writes a header row from the column names and then one line per DataRow, running every field through the same escaping logic. 

The DBNull.Value check is explicit rather than relying on ToString() alone: a database NULL and an intentionally empty string both end up as an empty CSV field here, which is a reasonable default for most exports, but it's a choice your export is making rather than an inherent property of the data — if the distinction matters downstream, write something else (such as the literal text NULL) for the null case instead.

Convert.ToString(..., CultureInfo.InvariantCulture) also keeps number and date formatting consistent regardless of the server's regional settings, rather than relying on the current thread's culture — see the note on culture below. 

For dates and times specifically, prefer an explicit format string such as yyyy-MM-dd or a full ISO 8601 representation when the CSV is meant as an interchange format, since invariant-culture formatting alone doesn't guarantee the exact shape a downstream system expects. 

Using string.Join instead of manually trimming a trailing comma off the StringBuilder (as older versions of this kind of code tend to do) is a small readability win, since it avoids manually managing the delimiter placement between fields.

With the sample data from Step 1, BuildCsv produces:

csv
CustomerId,CustomerName,ProductName,Price
1,Nikunj Satasiya,Laptop,37000
2,Hiren Dobariya,Mouse,820
3,Vivek Ghadiya,"Pen, Blue",250
4,Pratik Pansuriya,"Laptop ""Pro"" Model",42000
5,Sneha Patel,Lip Balm,130
6,John Smith,"Notebook, 200 pages",150

Only the fields that actually contain a comma or a quote end up wrapped in quotes — everything else is left as plain text, which is what makes the output easy to sanity-check by eye.

Step 4: Write the File with the Right Encoding

Once you have the CSV text, writing it to disk is one line — but the encoding is worth getting right.

UTF-8 is a strong default for a modern CSV export, since it can represent any character your data throws at it, but the correct encoding ultimately depends on what the consuming system expects. The wrinkle is the byte-order mark (BOM): a UTF-8 BOM can make Excel more likely to auto-detect a CSV file as UTF-8, rather than misreading it as a legacy code page, particularly when the file contains non-ASCII characters. That behavior isn't guaranteed across every Excel version or every consuming application, and the BOM itself isn't part of CSV's syntax — it's an Excel-specific compatibility concern. 

If you know your file is going into Excel and it may contain non-ASCII characters (accented names, currency symbols, non-Latin scripts), writing a BOM is a reasonable default; if you're feeding a different, more strictly RFC-conformant consumer, test with that consumer and see whether it expects a BOM or chokes on one.

StreamWriter and File.WriteAllText(path, content) both default to UTF-8 *without* a BOM when you don't pass an encoding explicitly, so if you want the BOM, you need to pass a UTF8Encoding configured to emit one, as below.

C#

csharp
DataTable dt = GetData();
string csvContent = BuildCsv(dt);

File.WriteAllText(
    "exported-data.csv",
    csvContent,
    new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));

VB.NET

vb
Dim dt As DataTable = GetData()
Dim csvContent As String = BuildCsv(dt)

File.WriteAllText(
    "exported-data.csv",
    csvContent,
    New UTF8Encoding(encoderShouldEmitUTF8Identifier:=True))

In a web application you'd more likely return this as a file result rather than write to a local path. Here's a simple, buffered ASP.NET Core controller action that does the same thing — it's fine for most exports, but note that at its peak it holds the DataTable, the complete CSV string, and the encoded byte array in memory simultaneously:

csharp
[HttpGet("export")]
public IActionResult Export()
{
    DataTable dt = GetData();
    string csvContent = BuildCsv(dt);
    byte[] bytes = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true).GetBytes(csvContent);

    return File(bytes, "text/csv", "export.csv");
}

If that combined memory footprint is a concern, write straight to the response stream instead, using the streaming WriteCsv(DataTable, TextWriter) method introduced in the next section — this avoids materializing the full CSV string and byte array at all:

csharp
[HttpGet("export-stream")]
public async Task ExportStreaming()
{
    DataTable dt = GetData();

    Response.ContentType = "text/csv";
    Response.Headers.ContentDisposition = "attachment; filename=export.csv";

    await using var writer = new StreamWriter(
        Response.Body,
        new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));

    WriteCsv(dt, writer);
}

This writes each row to Response.Body as it's produced instead of building a full string or byte array first. Response.Body is a Stream, which is why a StreamWriter can wrap it directly — this is different from Response.BodyWriter, covered below, which is a PipeWriter rather than a TextWriter-compatible type. Note that WriteCsv itself is synchronous — the async action method and await using control the request lifecycle and stream disposal, but the row-writing loop does not yield control while it runs. That's fine for most exports; if you need fully non-blocking I/O for very large or slow-to-produce exports, write an async variant that calls TextWriter.WriteAsync instead.

Handling Large Exports: Streaming Instead of Buffering

The StringBuilder-based approach above holds the entire CSV as one in-memory string before it's written anywhere. 

That's fine for small and medium exports, but it adds memory pressure once the row count and average field size are large enough that the resulting string itself becomes significant — there's no fixed row-count threshold where this kicks in; it depends on column count, field lengths, and how much memory is actually available. 

For large or memory-sensitive exports, write each row directly to a TextWriter — a StreamWriter over a file, or over the HTTP response stream — instead of building one large string first. The escaping logic doesn't change at all; only where the bytes end up does, and the same explicit-CRLF reasoning from Step 3 applies here too.

It's worth being precise about what this optimization actually buys you: it means the CSV serialization step itself doesn't need to hold a second, full-size string in memory alongside the data. 

A DataTable already keeps every row it contains in memory, so passing a very large DataTable into WriteCsv doesn't make the export as a whole memory-flat — it just avoids doubling the memory cost with a duplicate CSV string. If the source dataset itself is too large to hold comfortably in a DataTable, stream the underlying query results instead — for example with a DbDataReader (or the more general IDataReader interface) — rather than loading everything into a DataTable first.

C#

csharp
public static void WriteCsv(DataTable dt, TextWriter writer)
{
    const string recordSeparator = "\r\n";

    writer.Write(string.Join(",", dt.Columns
        .Cast<DataColumn>()
        .Select(c => WriteCsvField(c.ColumnName))));
    writer.Write(recordSeparator);

    foreach (DataRow row in dt.Rows)
    {
        var fields = dt.Columns
            .Cast<DataColumn>()
            .Select(col => WriteCsvField(row[col] == DBNull.Value ? string.Empty : Convert.ToString(row[col], CultureInfo.InvariantCulture)));

        writer.Write(string.Join(",", fields));
        writer.Write(recordSeparator);
    }
}

Called against a file, wrapped in a using statement so the StreamWriter is disposed and flushed correctly:

csharp
using (var writer = new StreamWriter("exported-data.csv", false, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true)))
{
    WriteCsv(dt, writer);
}

The same WriteCsv(DataTable, TextWriter) method can be reused with any compatible TextWriter — a StreamWriter over a file, a MemoryStream, or the ASP.NET Core response stream shown in the previous section — which is generally a better long-term shape than duplicating buffering logic for each destination. HttpResponse.BodyWriter, by contrast, is a PipeWriter rather than a TextWriter, so it isn't a drop-in target for this method; wrapping HttpResponse.Body in a StreamWriter, as shown above, is the simpler option for most CSV-streaming scenarios.

Security: CSV Injection is a Different Problem from CSV Escaping

The escaping covered above protects the CSV file's structure — it makes sure a comma inside a value isn't mistaken for a column separator. It does not protect against spreadsheet formula injection, sometimes called CSV injection, which is a separate concern documented by OWASP.

If a field's value starts with a character such as =, +, -, or @ — for example a value like =HYPERLINK("https://example.com","Click") — some spreadsheet applications will interpret it as a formula when the file is opened, rather than as plain text. 

If that value came from user input (a customer-entered product name or note, for instance) rather than from your own trusted data, an attacker could use it to run a formula in whoever opens the exported file's spreadsheet. Quoting and escaping the field correctly, as WriteCsvField does, does not prevent this, because a quoted field can still begin with =.

If you're exporting data that includes untrusted, user-controlled text, treat this as a separate mitigation from CSV escaping — and be aware that the most commonly suggested fix isn't fully reliable. 

Prefixing a value with a single quote (or wrapping and escaping it in double quotes) is the mitigation most often mentioned, but OWASP notes that Microsoft Excel can strip quotes and escape characters from a CSV when the file is saved and reopened, which can silently undo that protection and let a previously neutralized formula become active again.

OWASP's currently documented Excel-resistant alternative is to prefix a formula-triggering value with a tab character (0x09) inside the quoted field, since Excel handles a leading tab differently than a leading apostrophe or quote. 

Any of these mitigations changes the exported data — the tab, like the apostrophe, becomes part of the stored value — and can affect anything downstream that re-parses the file expecting the original text, so treat the choice as an application-specific decision based on how the CSV will actually be opened, rather than something to apply unconditionally to every export.

Note that a simple check on the field's first character, like the one below, only covers the most commonly cited formula-triggering prefixes. 

OWASP's broader guidance also flags a leading tab, carriage return, or line feed, certain full-width Unicode variants in some locales, and the possibility that an attacker manipulates delimiters or quotes to shift a dangerous character to the start of a cell after your own escaping runs. Treat a prefix check like this as one layer of defense against the most common attack pattern, not a complete, universal CSV-injection detector — the right mitigation depends on your specific spreadsheet consumers and threat model.

csharp
// Mitigates the most commonly cited formula-triggering prefixes.
// This is a partial defense, not a complete CSV-injection detector —
// see the discussion above for what it doesn't cover.
public static string NeutralizeSpreadsheetFormula(string value)
{
    if (string.IsNullOrEmpty(value))
        return value;

    char first = value[0];
    return first == '=' || first == '+' || first == '-' || first == '@'
        ? "\t" + value
        : value;
}

Apply it selectively — to fields that hold untrusted, user-supplied text, not to every column — since it alters the literal value stored in the file, and anything that re-parses the CSV expecting the original text will see the extra leading tab. If the export is meant for machine processing rather than opening in a spreadsheet, it's often better to reject or flag values with a formula-triggering prefix instead of silently rewriting them.

How It Works

The whole approach rests on one idea: CSV isn't a binary format, it's plain text with a small set of escaping conventions. WriteCsvField enforces those conventions for a single value — check for the three characters that require quoting (comma, quote, newline), escape internal quotes by doubling them, and wrap the result in quotes if needed. BuildCsv just applies that function to every cell in the table and joins the results with commas and record separators. 

Because each field is escaped independently of its neighbors, you never end up with a comma inside a product name accidentally being read as a column separator when the file is reopened.

It's also worth remembering that CSV has no type system of its own — a DataTable knows that Price is a decimal and CustomerId is an int, but once those values are written out, everything in the file is just text. The producer and consumer need to agree, outside the file itself, on how to interpret each column.

Verifying the Exporter Against Common Edge Cases

Because a CSV exporter is only as trustworthy as the edge cases it's been checked against, it's worth running WriteCsvField against a small set of representative inputs before relying on it:

InputExpected output
LaptopLaptop (unquoted)
Pen, Blue"Pen, Blue"
Laptop "Pro" Model"Laptop ""Pro"" Model"
He said "hello", then left"He said ""hello"", then left" (comma and quote handled together)
Two lines separated by \n or \r\nQuoted field containing a literal newline
Empty string ""Empty, unquoted field
DBNull.ValueEmpty field (per this article's chosen convention — see the DBNull note in Step 3)
=SUM(A1:A2)Quoted like any field starting with a special character; only becomes formula-safe if you additionally apply a mitigation like NeutralizeSpreadsheetFormula
Non-ASCII text (e.g. accented names)Preserved correctly when the file is written as UTF-8
Column name containing a commaHeader field is quoted, since the header goes through WriteCsvField too

These aren't exhaustive, but they cover the failure modes discussed throughout this article — unescaped commas and quotes, embedded newlines, null handling, encoding, and the boundary between CSV escaping and spreadsheet formula injection.

Common Errors and Troubleshooting

Columns shift after opening in Excel. This almost always means a field with an unescaped comma or quote made it into the CSV. Double-check that every value passes through WriteCsvField — a common mistake is escaping the data columns but forgetting to escape the header row, which is why the BuildCsv example above runs the header through the same function. 

If you're opening the file in a European locale where Excel expects ; as the list separator rather than ,, Excel may also misread an otherwise-valid comma-delimited file; that's a regional Excel setting rather than a bug in the CSV itself. 

If you need to target such a locale, the delimiter is only ever used in the string.Join(",", ...) calls above, so replacing the literal , there — or introducing a delimiter parameter if several exports need different ones — is a small, localized change; don't switch it silently based on the machine's locale, since that makes the exporter's behavior harder to predict.

Extra blank line at the end of the file. Appending a trailing record separator after the last row leaves a trailing newline. RFC 4180 explicitly allows the final record to end with a line break, and most parsers, including Excel, handle it without issue. Only trim it with csvContent.TrimEnd('\r', '\n') before writing if a specific downstream system you've confirmed actually misreads a trailing newline as an extra empty row.

Special characters look garbled in Excel. This is almost always an encoding issue, not an escaping issue. Writing with a UTF-8 byte-order mark, as shown in Step 4, resolves this in most versions of Excel; if you're targeting a system that specifically expects a different code page, you'll need to match that encoding explicitly and test against that consumer.

Numeric or date columns exported as text with unexpected formatting. Converting a DataRow value to a string uses the current thread's culture by default unless you specify one explicitly. If you need consistent formatting regardless of the machine's locale — which matters most for dates, since 09/18/2026 and 18/09/2026 are both valid renderings of the same date depending on culture — format explicitly with CultureInfo.InvariantCulture, for example row["Price"] is decimal price ? price.ToString("F2", CultureInfo.InvariantCulture) : string.Empty, before passing the string into WriteCsvField. 

Culture-invariant conversion gives you deterministic, locale-independent formatting, but it can't know the wire format your export contract actually needs — a DateTime could reasonably become 2026-09-18, 2026-09-18T14:30:00, or something else entirely, so define an explicit format string for any column, especially dates, times, and currency values, where the exact representation matters downstream.

CSV vs. XLSX: Which to Use

RequirementCSVXLSX
Single flat tableYesYes
Multiple worksheetsNoYes
FormulasNoYes
Cell formatting, charts, merged cellsNoYes
Maximum interoperability with other systemsYesDepends on the reading system
Simple, low-memory streaming exportYesDepends on the library

If the export needs to stay a simple, portable table, CSV is usually the simpler and cheaper choice. If it needs any Excel-specific feature — multiple sheets, formulas, formatting, or charts — CSV can't represent that, and you'll need a workbook format instead.

Best Practices

For small to medium exports, the StringBuilder-based approach in Steps 1–4 is simple and easy to reason about. For larger or more memory-sensitive exports, use the streaming TextWriter-based approach shown above instead, so the CSV text itself doesn't need to exist twice in memory; if the source data is also too large for a DataTable, stream the query results directly instead. 

If CSV generation is a recurring need across a codebase rather than a one-off export, a mature library such as CsvHelper is worth adopting: it covers the escaping rules discussed here, plus culture-aware formatting, configurable delimiters, and mapping between CSV columns and typed objects, and it's actively maintained — reach for it once you're doing this often enough, or across enough edge cases, that maintaining the manual version stops paying for itself. 

Whichever approach you use, keep the escaping logic as a single shared function rather than duplicating string-concatenation logic at every call site — it's the part of CSV export most likely to have a subtle bug, and you only want to have to fix it once.

Frequently Asked Questions

How do I export a DataTable to CSV in C# without a library?

Write a small helper that quotes any field containing a comma, double quote, or line break, then join the escaped fields with commas for the header and each row. The WriteCsvField and BuildCsv methods in Steps 2–3 above are a complete, dependency-free implementation.

How do I export a DataTable to CSV in VB.NET?

The same escaping rules apply. The VB.NET versions of WriteCsvField and BuildCsv above check for commas, quotes, and line breaks independently, then build the CSV text with a StringBuilder the same way the C# version does.

How do I escape commas and quotes in a CSV field?

Wrap the field in double quotes if it contains a comma, a double quote, or a line break, and double any embedded double quotes before wrapping. A field with no special characters can be left unquoted.

Can a CSV field contain a line break?

Yes. A quoted field can contain a literal newline, and the record is still considered one row — the closing quote, not the newline, marks the end of the field. See the embedded-newline example in Step 2 above. A parser that only splits on line breaks instead of respecting quotes will misread this.

Should a CSV file use UTF-8 with a byte-order mark (BOM)?

If the file is going into Excel and may contain non-ASCII characters, a UTF-8 BOM makes Excel more likely to detect the encoding correctly. It's not part of CSV's syntax, so if you're targeting a stricter, non-Excel consumer, test whether it expects a BOM or rejects one.

How do I export a large DataTable to CSV without running out of memory?

Write directly to a TextWriter — a file StreamWriter or an HTTP response stream via HttpResponse.Body — row by row, instead of building the whole CSV as one string first. See the WriteCsv(DataTable, TextWriter) method in the streaming section above. 

Keep in mind this avoids a duplicate in-memory CSV string, but doesn't shrink a DataTable that's already holding the full dataset — for very large source data, stream the query results (for example with a DbDataReader) instead of loading everything into a DataTable first.

What is CSV injection, and is escaping enough to prevent it?

CSV injection (spreadsheet formula injection) is when a field value starting with =, +, -, or @ gets interpreted as a formula by a spreadsheet application. Standard CSV escaping does not prevent this, because a quoted field can still begin with one of those characters. 

Even common mitigations like an apostrophe prefix aren't fully reliable, since Excel can strip quotes and escape characters when a file is saved and reopened; a tab-character prefix is a more Excel-resistant option, but any of these changes the stored data and should be applied deliberately to untrusted fields rather than universally, and shouldn't be treated as a complete, one-size-fits-all detector.

Should I use CsvHelper instead of writing my own CSV exporter?

A manual exporter like the one in this article is fine for a one-off export or a small codebase. If CSV generation is a recurring need with varied formatting, delimiter, or mapping requirements, CsvHelper covers those cases and is actively maintained, at the cost of an external dependency.

Related Technologies

For further reading on the APIs and specifications this article relies on:

Conclusion

Exporting a DataTable to CSV in C# or VB.NET doesn't require a third-party library, but it does require getting a few things right: quote any field containing a comma, a double quote, or a newline, and double up internal quotes when you do; be deliberate about DBNull, culture, encoding, and line endings rather than relying on defaults; and, if the data includes anything a user typed, treat spreadsheet formula injection as a separate, imperfectly-solved concern rather than something CSV escaping already handles. 

The WriteCsvField and BuildCsv methods above apply the escaping rules consistently across the header and every row, in both languages, and write the result using an explicit encoding and record separator, with UTF-8 plus a BOM as an Excel-oriented option when that's what your consumer needs. 

If your exports are large enough that memory usage becomes a real concern, use the streaming WriteCsv(DataTable, TextWriter) version instead of building the entire CSV string in memory first — and if the source dataset itself is too large for a DataTable, stream the underlying query as well. 

Evaluate CsvHelper if CSV generation is something your project does often enough to justify the dependency.

Codingvila provides articles and blogs on web and software development for beginners as well as free Academic projects for final year students in Asp.Net, MVC, C#, Vb.Net, SQL Server, Angular Js, Android, PHP, Java, Python, Desktop Software Application and etc.

avatar

Good example. Thank you! It helped me with a project I am working on. There were some errors with the VB code for the input function. I fixed them in the code below.

Public Shared Function WriteCSV(ByVal input As String) As String
Try
If (input Is Nothing) Then
Return String.Empty
End If
Dim containsQuote As Boolean = False
Dim containsComma As Boolean = False
Dim len As Integer = input.Length
Dim i As Integer = 0
Do While ((i < len) _
AndAlso ((containsComma = False) _
OrElse (containsQuote = False)))
Dim ch As Char = input(i)
If (ch = Microsoft.VisualBasic.ChrW(34)) Then
containsQuote = True
ElseIf (ch = Microsoft.VisualBasic.ChrW(44)) Then
containsComma = True
End If

i = (i + 1)
Loop
If (containsQuote AndAlso containsComma) Then
input = input.Replace("""", """""")
End If
If (containsComma) Then
Return """" & input & """"
Else
Return input
End If
Catch ex As Exception
Throw
End Try
End Function

delete April 16, 2019 at 1:11:00 AM GMT+5:30
avatar

Thank you for your valuable feedback. I will review your code and update the code in the article that you have fixed.

delete April 16, 2019 at 5:46:00 PM GMT+5:30
avatar

Please continue this great work and I look forward to more of your awesome blog posts. view

delete March 12, 2021 at 4:49:00 AM GMT+5:30

If you have any questions, contact us on info.codingvila@gmail.com