NPOI 是 Apache POI 的 .NET 移植版,**純 managed、無 Office 相依、跨平台 (Linux/Docker/Windows)、支援 .xls (HSSF) 與 .xlsx (XSSF) 、MIT 授權免費商用**。對比 EPPlus (LGPL/商業)、ClosedXML (僅 .xlsx)、Microsoft Interop (需 Office、Windows only),NPOI 是**企業級通用首選**。
// .xls (BIFF8)
IWorkbook wb = new HSSFWorkbook();
// .xlsx (OOXML)
IWorkbook wb = new XSSFWorkbook();
// 統一介面操作
ISheet sheet = wb.CreateSheet("Sheet1");
IRow row = sheet.CreateRow(0);
ICell cell = row.CreateCell(0);
cell.SetCellValue("Hello NPOI");
[HttpGet("export")]
public IActionResult ExportUsers()
{
var users = _userService.GetAll(); // List
using var wb = new XSSFWorkbook();
var sheet = wb.CreateSheet("用戶清單");
// 樣式定義
var headerStyle = CreateHeaderStyle(wb);
var dataStyle = CreateDataStyle(wb);
var dateStyle = CreateDateStyle(wb);
// 表頭
var headerRow = sheet.CreateRow(0);
var headers = new[] { "編號", "姓名", "Email", "手機", "角色", "建立時間" };
for (int i = 0; i < headers.Length; i++)
{
var cell = headerRow.CreateCell(i);
cell.SetCellValue(headers[i]);
cell.CellStyle = headerStyle;
}
// 資料列
for (int i = 0; i < users.Count; i++)
{
var u = users[i];
var row = sheet.CreateRow(i + 1);
var vals = new object[] { u.Id, u.Name, u.Email, u.Phone, u.Role, u.CreatedAt };
for (int j = 0; j < vals.Length; j++)
{
var cell = row.CreateCell(j);
SetCellValue(cell, vals[j], dataStyle, dateStyle);
}
}
// 自動欄寬
for (int i = 0; i < headers.Length; i++) sheet.AutoSizeColumn(i);
// 輸出串流
using var ms = new MemoryStream();
wb.Write(ms);
ms.Position = 0;
return File(ms.ToArray(), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", $"Users_{DateTime.Now:yyyyMMdd}.xlsx");
}
private ICellStyle CreateHeaderStyle(IWorkbook wb)
{
var style = wb.CreateCellStyle();
var font = wb.CreateFont();
font.Boldweight = (short)FontBoldWeight.Bold;
font.Color = IndexedColors.White.Index;
style.SetFont(font);
style.FillForegroundColor = IndexedColors.DarkBlue.Index;
style.FillPattern = FillPattern.SolidForeground;
style.BorderBottom = BorderStyle.Thin;
style.Alignment = HorizontalAlignment.Center;
return style;
}
private void SetCellValue(ICell cell, object value, ICellStyle defaultStyle, ICellStyle dateStyle)
{
cell.CellStyle = defaultStyle;
switch (value)
{
case null: cell.SetCellValue(""); break;
case string s: cell.SetCellValue(s); break;
case int i: cell.SetCellValue(i); break;
case long l: cell.SetCellValue(l); break;
case double d: cell.SetCellValue(d); break;
case decimal m: cell.SetCellValue((double)m); break;
case DateTime dt: cell.SetCellValue(dt); cell.CellStyle = dateStyle; break;
case bool b: cell.SetCellValue(b); break;
default: cell.SetCellValue(value.ToString()); break;
}
}
XSSF 會將整份工作簿載入記憶體,**百萬筆會 OOM**。使用 SXSSFWorkbook (僅 .xlsx) 實現**滾動視窗寫入磁碟**:
public IActionResult ExportHugeData()
{
// windowSize = 保留在記憶體的行數 (預設 100)
using var wb = new SXSSFWorkbook(windowSize: 100, compressTmpFiles: true);
var sheet = wb.CreateSheet("大數據");
// 寫入邏輯相同...
for (int i = 0; i < 1_000_000; i++)
{
var row = sheet.CreateRow(i);
row.CreateCell(0).SetCellValue(i);
row.CreateCell(1).SetCellValue($"Data_{i}");
}
using var ms = new MemoryStream();
wb.Write(ms); // 此時會將暫存檔合併寫入串流
ms.Position = 0;
return File(ms.ToArray(), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "HugeData.xlsx");
}
public IActionResult ExportWithTemplate()
{
// 1. 讀取模板 (含格式、圖表、命名範圍)
using var templateStream = _hostingEnv.ContentRootFileProvider.GetFileInfo("Templates/ReportTemplate.xlsx").CreateReadStream();
using var wb = new XSSFWorkbook(templateStream);
var sheet = wb.GetSheet("Report");
// 2. 填充資料 (使用命名範圍或固定位置)
var data = _reportService.GetData();
var startRow = 5; // 模板預留表頭 5 行
foreach (var item in data)
{
var row = sheet.CreateRow(startRow++);
row.CreateCell(0).SetCellValue(item.Name);
row.CreateCell(1).SetCellValue(item.Value);
// 樣式自動繼承自模板對應行
}
// 3. 重新計算公式 (模板含 SUM 等)
var helper = new XSSFFormulaEvaluator(wb);
helper.EvaluateAll();
using var ms = new MemoryStream();
wb.Write(ms);
return File(ms.ToArray(), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "Report.xlsx");
}
[HttpPost("import")]
public async Task ImportUsers(IFormFile file)
{
if (file == null || file.Length == 0) return BadRequest("無檔案");
using var stream = file.OpenReadStream();
using var wb = WorkbookFactory.Create(stream); // 自動偵測 .xls/.xlsx
var sheet = wb.GetSheetAt(0);
var results = new List();
var errors = new List();
// 假設第 1 列為表頭,從第 2 列開始
for (int rowIdx = 1; rowIdx <= sheet.LastRowNum; rowIdx++)
{
var row = sheet.GetRow(rowIdx);
if (row == null) continue;
try
{
var dto = new UserImportDto
{
Name = GetCellString(row.GetCell(0)),
Email = GetCellString(row.GetCell(1)),
Phone = GetCellString(row.GetCell(2)),
Role = GetCellString(row.GetCell(3))
};
// 業務驗證
var validationErrors = ValidateUser(dto);
if (validationErrors.Any())
{
errors.Add(new ImportError { Row = rowIdx + 1, Errors = validationErrors });
continue;
}
results.Add(await _userService.CreateAsync(dto));
}
catch (Exception ex)
{
errors.Add(new ImportError { Row = rowIdx + 1, Errors = new[] { ex.Message } });
}
}
return Ok(new { SuccessCount = results.Count, Errors = errors });
}
private string GetCellString(ICell cell)
{
if (cell == null) return null;
return cell.CellType switch
{
CellType.String => cell.StringCellValue.Trim(),
CellType.Numeric => DateUtil.IsCellDateFormatted(cell)
? cell.DateCellValue.ToString("yyyy-MM-dd")
: cell.NumericCellValue.ToString("G"),
CellType.Boolean => cell.BooleanCellValue.ToString(),
CellType.Formula => cell.StringCellValue, // 公式結果文字
_ => ""
};
}
| 場景 | 技巧 / 避坑 |
|---|---|
| 日期顯示錯誤 | 儲存格需設定 CellStyle.DataFormat = wb.CreateDataFormat().GetFormat("yyyy/mm/dd"),且 SetCellValue(DateTime) |
| 長數字變科學記號 | 設為字串型別 cell.SetCellValue("123456789012345678") 或自訂格式 @ (文字) |
| 公式不計算 | 導出前呼叫 new XSSFFormulaEvaluator(wb).EvaluateAll() 或開啟時 WorkbookFactory.Create(stream, true) |
| 合併儲存格 | sheet.AddMergedRegion(new CellRangeAddress(firstRow, lastRow, firstCol, lastCol)) |
| 凍結窗格 | sheet.CreateFreezePane(0, 1) (凍結首行) |
| 資料驗證 (下拉選單) | var dv = sheet.CreateDataValidationHelper().CreateValidation(...) 設定列表來源 |
| 圖片插入 | var pictureIdx = wb.AddPicture(bytes, PictureType.PNG); var anchor = new XSSFClientAnchor(...); patriarch.CreatePicture(anchor, pictureIdx); |
| 多執行緒安全 | NPOI 物件**非執行緒安全**,每請求建立新 Workbook,勿共用靜態實例 |
| Linux 字體缺失導致 PDF/列印異常 | 安裝 fonts-dejavu-core fonts-noto-cjk;NPOI 本身不轉 PDF,但 Aspose/FreeSpire 會受影響 |
| 資料量 | 模式 | 耗時 | 記憶體峰值 |
|---|---|---|---|
| 1 萬列 × 20 欄 | XSSFWorkbook | 1.2 秒 | 180 MB |
| 1 萬列 × 20 欄 | SXSSFWorkbook (window=100) | 1.5 秒 | 15 MB |
| 100 萬列 × 10 欄 | SXSSFWorkbook | 45 秒 | 30 MB |
| 導入 1 萬列 | WorkbookFactory.Create | 0.8 秒 | 120 MB |
[Fact]
public void Export_Should_Generate_Valid_Xlsx()
{
// Arrange
var users = new[] { new UserDto { Id = 1, Name = "Test", Email = "test@test.com" } };
// Act
var result = _controller.ExportUsers() as FileContentResult;
// Assert
Assert.NotNull(result);
Assert.Equal("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", result.ContentType);
using var ms = new MemoryStream(result.FileContents);
using var wb = new XSSFWorkbook(ms);
var sheet = wb.GetSheetAt(0);
Assert.Equal("用戶清單", sheet.SheetName);
Assert.Equal("編號", sheet.GetRow(0).GetCell(0).StringCellValue);
Assert.Equal("Test", sheet.GetRow(1).GetCell(1).StringCellValue);
}