SwiftExcel (Free) Free
The free, open-source edition of SwiftExcel. Streams a single-sheet
.xlsx document directly to a file or arbitrary
Stream, with near-zero memory usage. Source code on
GitHub.
Installation
Install via NuGet:
dotnet add package SwiftExcel
Then bring the namespace into scope:
using SwiftExcel; using SwiftExcel.Extensions; // only if you use ExportToExcel
The library targets netstandard2.0 and depends on SharpCompress for ZIP packaging.
Quick start
The minimal "100 rows × 10 columns" example from the official sandbox:
using (var ew = new ExcelWriter("C:/temp/test.xlsx")) { for (var row = 1; row <= 100; row++) { for (var col = 1; col <= 10; col++) { ew.Write($"row:{row}-col:{col}", col, row); } } }
Wrapping the writer in using finalizes the document on
Dispose(). Calling Save() manually is also
supported and equivalent.
Streaming model — what you must know
-
Cells must be written in increasing
(row, col)order. Writing to a row or column you have already passed throwsSwiftExcelException. This is what makes streaming so cheap; it is intentional. -
colandroware 1-based. Passing0or a negative value throws. -
Skipping rows or columns is fine — empty
<row/>and empty cells are emitted automatically. -
One
ExcelWriterper file/stream. Not thread-safe.
API reference
Namespace: SwiftExcel.
ExcelWriter
Implements IDisposable. Two constructor overloads:
ExcelWriter(string filePath, Sheet sheet = null)
filePath— full path to the output file. The directory is created if it does not exist.sheet— optionalSheetwith custom name, column widths, RTL and wrap-text settings.nulluses defaults ("Sheet1", no column widths, LTR, no wrap).
ExcelWriter(Stream stream, Sheet sheet = null)
stream— your own writable stream (FileStream,MemoryStream, an Azure BlobBlockBlobClient.OpenWrite()stream, etc.). Useful for writing directly to cloud storage without a temp file.- For non-seekable streams, Zip64 is automatically disabled internally.
- The library does not close an externally provided stream when disposed; you own its lifetime.
Write()
void Write(string value, int col, int row, DataType dataType = DataType.Text)
value— the cell value as astring.nullis allowed and is escaped safely.col— 1-based column index (column1=A).row— 1-based row index.dataType—DataType.Text(default) orDataType.Number. Numbers are written without thet="str"attribute so Excel parses them as numeric.
Throws SwiftExcelException if col ≤ 0,
row ≤ 0, or you try to write to an already-processed
(row, col).
WriteFormula()
void WriteFormula(FormulaType type, int col, int row, int sourceCol, int sourceRowStart, int sourceRowEnd)
type— one ofFormulaType(Average,Count,Max,Sum).col,row— destination cell of the formula result.sourceCol— column the formula reads from.sourceRowStart,sourceRowEnd— inclusive row range to operate over.
Free SwiftExcel only supports the four built-in formulas. For
arbitrary formulas (IF, VLOOKUP,
HYPERLINK, etc.) you need
SwiftExcel.Pro's
DataType.CustomFormula.
Save() and Dispose()
void Save()
Closes the worksheet, finalizes the ZIP entries and disposes the
internal stream (only the internal one — externally supplied streams
are left open). Calling Save() twice is a no-op because
of an internal Finalized guard.
void Dispose() (inherited)
Calls Save() if it has not been called yet. Always wrap
ExcelWriter in using unless you have a
specific reason to call Save() manually.
DataType (enum)
| Value | Meaning |
|---|---|
DataType.Text | Default. Cell is emitted with t="str"; Excel treats it as a string. Safe for any value, including numeric strings that should not be coerced. |
DataType.Number | Cell is emitted without the t="str" attribute, so Excel parses the value as numeric. Use this for integers, decimals, and dates that have been converted with ToOADate(). |
FormulaType (enum)
| Value | Excel function |
|---|---|
FormulaType.Average | =AVERAGE(range) |
FormulaType.Count | =COUNT(range) |
FormulaType.Max | =MAX(range) |
FormulaType.Sum | =SUM(range) |
Sheet
Optional configuration object passed to the ExcelWriter constructor.
| Property | Type | Description |
|---|---|---|
Name | string | Sheet tab name. Defaults to "Sheet1". Truncated to 31 characters and XML-escaped automatically. |
RightToLeft | bool | Right-to-left column direction. |
WrapText | bool | Apply wrap-text alignment to all cells. Required if your cell values contain \n and you want them displayed as multi-line. |
ColumnsWidth | IList<double> | Per-column widths starting at column 1. Index i sets width for column i+1. Columns past the list use Excel's default. |
Configuration (static)
static bool Configuration.UseEnchancedXmlEscaping = true
When true (default), control characters that are
invalid in XML are encoded using Excel's
_xHHHH_ escape syntax. Set to false only
if you have already sanitized your input and want a small perf
boost.
IList<T>.ExportToExcel() — list export extension
For one-shot export of a collection of objects to a sheet, the free
edition includes a reflection-based extension method in
SwiftExcel.Extensions:
void IList<T>.ExportToExcel(string filePath, string sheetName = null)
- Public properties become columns.
- The first row contains headers.
- Numeric properties (int, decimal, byte, …) are written as
DataType.Number; everything else asDataType.Text. - Decorate properties with
[ExcelExport]to overrideName,Order, and columnWidth.
ExcelExportAttribute
Located in SwiftExcel.Attributes:
public class TestModel { [ExcelExport(Order = 1)] public int FirstProperty { get; set; } [ExcelExport(Name = "Custom SecondProperty Name", Order = 2)] public string SecondProperty { get; set; } [ExcelExport(Name = "Third", Order = 3, Width = 40.00)] public char ThirdProperty { get; set; } [ExcelExport(Width = 60.00)] public DateTime FourthProperty { get; set; } public byte FifthProperty { get; set; } }
Examples
Data & structure — Basic write
The canonical example from the official sandbox:
using (var ew = new ExcelWriter("C:/temp/test.xlsx")) { for (var row = 1; row <= 100; row++) { for (var col = 1; col <= 100; col++) { ew.Write($"row:{row}-col:{col}", col, row); } } }
Data & structure — Sheet name, widths, RTL, wrap text
Configure the sheet via the optional Sheet parameter and use manual Save():
var sheet = new Sheet { Name = "Monthly Report", RightToLeft = true, WrapText = true, ColumnsWidth = new List<double> { 10, 12, 8, 8, 35 } }; var ew = new ExcelWriter("C:/temp/test.xlsx", sheet); for (var row = 1; row <= 100; row++) { for (var col = 1; col <= 10; col++) { ew.Write($"row:{row}-col:{col}", col, row); } } ew.Save();
Data & structure — Direct streaming to Azure Blob Storage
The Stream constructor lets you upload a workbook to cloud
storage while it is being generated — no temporary file, no
full-document buffer. Pattern from the sandbox:
var blobServiceClient = new BlobServiceClient(connectionString); var containerClient = blobServiceClient.GetBlobContainerClient(containerName); var blobClient = containerClient.GetBlockBlobClient("report.xlsx"); using (var stream = blobClient.OpenWrite(true)) using (var ew = new ExcelWriter(stream)) { for (var row = 1; row <= 100; row++) { for (var col = 1; col <= 100; col++) { ew.Write($"row:{row}-col:{col}", col, row); } } }
The Azure Blob stream is non-seekable, so SwiftExcel automatically disables Zip64 internally. The blob client is responsible for closing its own stream.
Formulas — Built-in Average / Count / Max / Sum
From the sandbox: write 20 numeric values, then four aggregate formulas under them.
using (var ew = new ExcelWriter("C:/temp/test.xlsx")) { const int col = 1; var row = 1; for (; row <= 20; row++) { ew.Write(row.ToString(), col, row, DataType.Number); } ew.WriteFormula(FormulaType.Average, col, ++row, col, 1, 20); ew.WriteFormula(FormulaType.Count, col, ++row, col, 1, 20); ew.WriteFormula(FormulaType.Max, col, ++row, col, 1, 20); ew.WriteFormula(FormulaType.Sum, col, ++row, col, 1, 20); }
Data types — Numbers
Pass DataType.Number so Excel parses the value as numeric instead of text:
ew.Write("42", 1, 1, DataType.Number); ew.Write("3.14159", 2, 1, DataType.Number); ew.Write("-12.5", 3, 1, DataType.Number);
Numeric values are written using the invariant culture internally, so
3.14 stays 3.14 regardless of the user's
thread culture (no surprise commas).
Data types — Dates with .ToOADate() (critical pattern)
Excel does not have a native date type — internally a date is just a
double (an OLE Automation date). To write a real date value
you must convert it with
DateTime.ToOADate() and write it as
DataType.Number:
var dateToExport = new DateTime(2023, 8, 21, 10, 51, 18); using (var ew = new ExcelWriter("C:/temp/test.xlsx")) { ew.Write($"{dateToExport.ToOADate()}", 1, 1, DataType.Number); }
Why this matters: if you write the date as a string
(DataType.Text) Excel will not let users sort, filter, or
compute on it. With ToOADate() + DataType.Number
the cell is genuinely a date — but in the Free edition it will display
as a raw double until a user formats the column as a date in Excel.
For automatic date formatting use
SwiftExcel.Pro with
NumberFormat.Custom("dd/mmm/yyyy hh:mm:ss").
Hyperlinks Pro
The free edition does not support hyperlinks. To write
a clickable link, install
SwiftExcel.Pro
and use the HYPERLINK Excel function via
DataType.CustomFormula. See
SwiftExcel.Pro · Hyperlinks.
A workaround in Free is to write the URL as plain text — Excel will auto-link it on cell-edit but it is not a real hyperlink.
Special values — Invalid XML & reserved characters
SwiftExcel automatically escapes < > & ' " and,
when Configuration.UseEnchancedXmlEscaping is true
(default), encodes XML-invalid control characters using Excel's
_xHHHH_ syntax. You can pass any user input safely:
Configuration.UseEnchancedXmlEscaping = true; using (var ew = new ExcelWriter("C:/temp/test.xlsx")) { ew.Write("<", 1, 1); ew.Write(">", 2, 1); ew.Write("&", 3, 1); ew.Write("'", 4, 1); ew.Write("\"", 5, 1); }
Bulk export — IList<T>.ExportToExcel()
One-line export of a collection. Headers are derived from property names or [ExcelExport] attributes.
var testCollection = new List<TestModel> { new TestModel(), new TestModel() }; // default sheet name "Sheet1" testCollection.ExportToExcel("C:/temp/test.xlsx"); // or with a custom sheet name testCollection.ExportToExcel("C:/temp/test.xlsx", sheetName: "Sheet2");
Property order in the workbook follows the
[ExcelExport(Order = N)] attribute (ascending). Properties
without an explicit order go last in the order returned by reflection.
FAQ
Can SwiftExcel read existing .xlsx files?
No. SwiftExcel is intentionally write-only. Skipping the read path is what allows it to stream and stay near-zero memory. For reading you'll need a different library (e.g. ExcelDataReader).
Does it support .xls or .xlsm?
No. SwiftExcel produces only the modern Open XML
.xlsx format. There is no support for legacy binary
.xls or macro-enabled .xlsm files.
Is ExcelWriter thread-safe?
No. A single ExcelWriter must not be
shared across threads. The streaming model writes XML in strict order,
so concurrent access will corrupt the file. Parallelize across
files (one writer per thread) instead of within a single workbook.
Which .NET versions are supported?
The package targets netstandard2.0, so it works on
.NET Framework 4.6.1+, .NET Core 2.0+, and .NET 5, 6, 7, 8 and 9.
The only runtime dependency is SharpCompress.
How big a file can SwiftExcel produce?
Memory consumption is essentially flat regardless of row count, so files with millions of rows are routine. The hard upper bounds are Excel's own: 1,048,576 rows × 16,384 columns per worksheet. Free SwiftExcel writes a single sheet, so that's the absolute ceiling.
How do I output a date?
Convert your DateTime with
DateTime.ToOADate() and write it as
DataType.Number. The Free edition does not support
custom number formats, so the cell will display as a raw double
until a date format is applied in Excel. For automatic date
formatting use SwiftExcel.Pro.
How do I output a hyperlink?
Free SwiftExcel does not support hyperlinks directly. Use
SwiftExcel.Pro's
DataType.CustomFormula with
Excel's HYPERLINK("url","label") function.
Can I cancel an in-progress export?
Just stop calling Write() and call Dispose()
(or let the using block exit). The partial file will be a
valid .xlsx containing whatever you have written so far.
If you want to discard the result, delete the file or close the stream
without flushing.
Why do my cells appear in the wrong column on a new row?
Make sure you have at least version 1.1.10 of the
package — earlier versions had a bug where the column counter was not
reset between rows. Update via
dotnet add package SwiftExcel.
I need styles, multiple sheets, hyperlinks, freeze panes…
Those features live in SwiftExcel.Pro. The streaming engine, performance characteristics and data-flow semantics are the same — only the API surface is richer.