ClosedXMLは、Excelをインストールしていない環境でもC#やVB.NETから.xlsxファイルを作成・読み込み・編集できる.NETライブラリです。ライセンスはMITで、商用のWebシステムやバッチに組み込んでも利用料はかかりません。
この記事では、2026年7月25日公開の最新版0.105.1を前提に、NuGetでの導入から読み書き・シート操作・書式・数式・ASP.NET Coreでのダウンロードのサンプルコード、xls非対応やグラフ作成不可といった「できないこと」、EPPlus・NPOI・MiniExcelとの違いまでを整理します。掲載コードはすべて .NET 10 SDK(10.0.401)とClosedXML 0.105.1でビルド・実行し、コメントに書いた出力を確認済みです。
まとめ:ClosedXMLの要点と採用判断
- ClosedXMLはOpen XML SDKをラップしたライブラリで、扱える拡張子は .xlsx・.xlsm・.xltx・.xltm の4つです。
- 最新版は0.105.1(2026年7月25日)。対象は .NET Standard 2.0/2.1 なので、.NET 10 でも .NET Framework 4.6.2 でも同じパッケージで動きます。
- MITライセンスのため商用利用は無償です。EPPlusはバージョン5以降、非商用向けと有償の商用向けのデュアルライセンスで、NPOIは2.8.0以降のNuGet版に保守料の契約が付いています。
- .xls(Excel 97-2003形式)の読み書き、グラフの新規作成、PDF出力、暗号化ファイルの読み込みはできません。
- 5列×10万行の書き出しは本記事の計測で約2.6秒・最大メモリ約200MiBでした。書式の無いデータを数十万行流すだけなら、ストリーミング型のMiniExcel(同0.44秒・約55MiB)も候補です。
- 公開APIはまだ安定版扱いではなく、版ごとに例外の型などが変わります。バージョンは固定し、更新前にリリースノートと移行ガイドを読んでください。
「xlsを扱わない」「グラフはテンプレートに置いておけばよい」という要件なら、ClosedXMLを第一候補として勧めます。以下、導入手順から順に説明します。
ClosedXMLの位置づけと0.105.1の動作環境
Open XML SDKのラッパーとしての役割
公式READMEはClosedXMLを「Excel 2007+ (.xlsx, .xlsm) ファイルを読み、操作し、書き込むための.NETライブラリ」と説明し、MicrosoftのOpen XML SDKの上に直感的なAPIを被せることを目的に掲げています。Open XML SDKはワークシートや行・セルといったXML要素を直接組み立てる低水準のAPIで、書式を付けるにはスタイルシートの要素も自分で管理します。ClosedXMLではセルへの書き込みが ws.Cell("A1").Value = "商品"; の1行で済みます。
COM経由でExcel本体を操作する方式(Interop)と違い、サーバーにExcelを入れる必要がありません。READMEが典型例に挙げているのも「WebサーバーでのExcelレポート作成」です。一方、公式ドキュメントはUnityのスクリプトエンジンでは動作しないと明記しています。
対象フレームワークと依存パッケージ
NuGetのパッケージ定義(nuspec)から読み取れる0.105.1の構成は次のとおりです。
| 項目 | 0.105.1の値 |
|---|---|
| 公開日(NuGet) | 2026-07-25 |
| ライセンス | MIT |
| 対象フレームワーク | .NET Standard 2.0 / 2.1 |
| DocumentFormat.OpenXml | 3.1.1以上 4.0.0未満 |
| SixLabors.Fonts | 1.0.0以上 3.0.0未満 |
| ClosedXML.Parser | 2.0.0以上 |
| その他の主な依存 | ExcelNumberFormat 1.1.0以上 / RBush.Signed 4.0.0以上 |
0.105.1の変更点は、SixLabors.Fontsの許容範囲を3.0.0未満に制限した1件だけです。機能の追加は前版の0.105.0(2025年5月14日)で入っています。関数名の自動補正や、並べ替え時に数式の参照を追従させる変更がこの版の目玉です。.NET 10自体のサポート期間は.NET 10(LTS)のサポート期限と新機能で整理しています。
バージョン更新で壊れやすい箇所
READMEには「公開APIはまだ安定しておらず、更新のたびにリリースノートと移行ガイドを読むことを強く勧める」とあります。実際、開発ブランチには次期0.106向けの移行ガイドがすでにあります。そこでは、存在しないシート名を Worksheets.Worksheet(name) に渡したときの例外が ArgumentException から KeyNotFoundException に変わると予告されています(0.105.1では ArgumentException が出ることを確認済み)。例外の型で分岐しているコードは、次の版で素通りになります。
対策は2つです。PackageReference でバージョンを固定すること、そして取得系は例外に頼らず TryGetWorksheet などの Try 系メソッドで書くことです。なお、GitHubのREADME冒頭にあるReleaseバッジは0.95.4のまま更新されていません。最新版はNuGetかGitHubのReleasesで確認してください。
NuGetでのインストール手順
.NET CLIならプロジェクトのフォルダで次を実行します。バージョンを明示しておくと、チームや本番環境で同じ版がそろいます。
dotnet add package ClosedXML --version 0.105.1
Visual Studioのパッケージマネージャーコンソールを使う場合は次のとおりです。
PM> Install-Package ClosedXML -Version 0.105.1
コードの先頭には using ClosedXML.Excel; を書きます。主要な型はこの名前空間にあります。VB.NETでも同じパッケージを追加し、Imports ClosedXML.Excel と書けば同じ型を使えます。古い packages.config 形式のプロジェクトでは依存関係の解決に失敗することがあり、公式ドキュメントは PackageReference 形式への移行を第一の解決策に挙げています。
ClosedXMLの基本的な使い方とサンプルコード
ブックの新規作成と保存(XLWorkbook・SaveAs)
new XLWorkbook() で空のブックを作り、AddWorksheet でシートを足し、SaveAs で保存します。XLWorkbook は IDisposable なので using で囲みます。
using ClosedXML.Excel;
using var wb = new XLWorkbook();
var ws = wb.AddWorksheet("売上");
ws.Cell("A1").Value = "商品";
ws.Cell("B1").Value = "数量";
ws.Cell("C1").Value = "単価";
ws.Cell("D1").Value = "金額";
ws.Cell("A2").Value = "りんご";
ws.Cell("B2").Value = 3;
ws.Cell("C2").Value = 120;
ws.Cell("D2").FormulaA1 = "B2*C2";
ws.Cell("A3").Value = "みかん";
ws.Cell("B3").Value = 5;
ws.Cell("C3").Value = 80;
ws.Cell("D3").FormulaA1 = "B3*C3";
wb.SaveAs("sales.xlsx");
// Web APIなどでファイルに書かずに返す場合
using var ms = new MemoryStream();
wb.SaveAs(ms);
byte[] bytes = ms.ToArray();
Console.WriteLine(bytes.Length > 0);
SaveAs には Stream も渡せるので、ファイルに書かずにバイト列として取り出せます。Webアプリから返す実装は、この章の最後の「ASP.NET CoreのExcelダウンロード実装」で扱います。
シートの追加・コピー・削除と並び順、SaveとSaveAsの違い
シートは AddWorksheet で足し、第2引数でタブの位置(1始まり)を指定できます。コピーは CopyTo、移動は Position への代入、削除は Worksheets.Delete です。
using ClosedXML.Excel;
using (var wb = new XLWorkbook())
{
wb.AddWorksheet("4月");
wb.AddWorksheet("6月");
wb.AddWorksheet("5月", 2); // 2番目のタブに挿入(位置は1始まり)
wb.Worksheet("4月").CopyTo("7月"); // 末尾にコピー
wb.Worksheet("7月").Position = 1; // 先頭へ移動
wb.Worksheets.Delete("6月");
Console.WriteLine(string.Join(",", wb.Worksheets.Select(s => s.Name)));
// 4月,5月,7月(追加した順)
Console.WriteLine(string.Join(",", wb.Worksheets.OrderBy(s => s.Position).Select(s => s.Name)));
// 7月,4月,5月(タブの並び)
Console.WriteLine(wb.Worksheet(1).Name); // 7月(番号はタブ位置)
Console.WriteLine(wb.TryGetWorksheet("8月", out _)); // False
try { wb.Save(); }
catch (InvalidOperationException e) { Console.WriteLine(e.Message); }
wb.SaveAs("book.xlsx");
}
using (var wb = new XLWorkbook("book.xlsx"))
{
wb.Worksheet("5月").Cell("A1").Value = "更新";
wb.Save(); // 開いたファイルへ上書き
}
実行すると、コメントに書いた並びと This is a new file. Please use one of the 'SaveAs' methods. が出力されます。つまずきやすい点が2つあります。
1つ目は、メモリ上の wb.Worksheets を列挙すると、タブの並びではなく追加した順に返ることです。Worksheet(1) のような番号指定はタブ位置で引くので、両者が食い違います。保存して開き直すとタブ順で返りますが、編集中にタブ順で処理したいなら OrderBy(s => s.Position) で並べ直してください。存在しないシート名を Worksheet(name) に渡すと0.105.1では ArgumentException になるので、取得は TryGetWorksheet で書きます。
2つ目は Save と SaveAs の役割です。Save() は、読み込み元か直前に SaveAs した保存先へ上書きします。new XLWorkbook() で作ってまだ一度も SaveAs していないブックに呼ぶと、保存先が決まっていないため InvalidOperationException になります(いったん SaveAs した後なら Save() で上書きできることも確認しました)。新規作成のブックは、最初の1回を SaveAs で保存します。
既存ファイルの読み込みと値の取得
ファイルパスを渡して XLWorkbook を開き、RangeUsed() で値の入った範囲を取ります。数式セルの Value は式ではなく計算結果を返し、式そのものは FormulaA1 で読めます。
using ClosedXML.Excel;
using var wb = new XLWorkbook("sales.xlsx");
if (!wb.TryGetWorksheet("売上", out var ws))
{
Console.WriteLine("シート「売上」がありません");
return;
}
foreach (var row in ws.RangeUsed()!.RowsUsed().Skip(1))
{
string name = row.Cell(1).GetString();
int qty = row.Cell(2).GetValue<int>();
XLCellValue amount = row.Cell(4).Value; // 数式セルは計算結果が返る
Console.WriteLine($"{name} {qty} {amount}");
}
// 型が合わないと例外になるセルは TryGetValue で受ける
if (!ws.Cell("A2").TryGetValue<int>(out var n))
{
Console.WriteLine("A2 は数値ではありません");
}
このコードを上の sales.xlsx に対して実行すると「りんご 3 360」「みかん 5 400」が出力されます。GetValue<int>() は文字列のセルでも「123」のように整数へ変換できれば値を返し、「りんご」のように変換できないときに InvalidCastException を投げます。GetValue<int?>() が null を返すのは空セル(Blank)と空文字列のセルだけで、変換できない文字列ではやはり例外になります。表記ゆれが混じる入力ファイルでは TryGetValue で受けてください。読み込んだ行をLINQで集計する書き方はC#・.NETの統合言語クエリ(LINQ)の基礎が参考になります。
リストを表に一括出力するInsertTableとInsertData
1セルずつ Value を代入する代わりに、List<T> や DataTable をまとめて流し込めます。InsertTable は見出し行つきのExcelテーブル(既定のスタイルは TableStyleMedium2)を作ります。InsertData は見出しを付けずに値だけを書き込みます。
using ClosedXML.Attributes;
using ClosedXML.Excel;
var items = new List<Item>
{
new("りんご", 3, 120m),
new("みかん", 5, 80m),
new("ぶどう", 2, 450m),
};
using var wb = new XLWorkbook();
var ws = wb.AddWorksheet("一覧");
// 見出し行つきのExcelテーブルとして出力
ws.Cell("A1").InsertTable(items, "Items", true);
ws.Column(3).Style.NumberFormat.Format = "#,##0";
ws.SheetView.FreezeRows(1);
ws.Columns().AdjustToContents();
// 見出し無しで値だけ流し込む場合
var ws2 = wb.AddWorksheet("値のみ");
ws2.Cell("A1").InsertData(items);
wb.SaveAs("items.xlsx");
Console.WriteLine($"{ws.Cell("A1").GetString()} / {ws2.Cell("A1").GetString()} / {ws.Cell("B1").GetString()}");
record Item(
[property: XLColumn(Header = "商品名", Order = 1)] string Name,
[property: XLColumn(Header = "数量", Order = 2)] int Qty,
[property: XLColumn(Header = "単価", Order = 3)] decimal Price);
見出しの文字列は XLColumn 属性の Header で指定し、Order で列順、Ignore で除外を指定します。この属性は ClosedXML.Attributes 名前空間にあります。公式ドキュメントのサンプルは using ClosedXML.Excel; しか書いていないので、そのまま写すと「’XLColumn’ は属性クラスではありません(CS0616)」でビルドが通りません。
書式・罫線・背景色とセル結合、行の挿入
書式は Style プロパティから設定します。範囲(IXLRange)にまとめて当てられるので、見出し行や合計行は範囲単位で書くと短く済みます。
using ClosedXML.Excel;
using var wb = new XLWorkbook("sales.xlsx");
var ws = wb.Worksheet("売上");
// 見出し行:太字・背景色・罫線
var header = ws.Range("A1:D1");
header.Style.Font.Bold = true;
header.Style.Fill.BackgroundColor = XLColor.FromHtml("#DDEBF7");
header.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
header.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
// 合計行:A5:C5を結合して中央寄せ
var label = ws.Range("A5:C5").Merge();
label.Value = "合計";
label.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
ws.Cell("D5").FormulaA1 = "SUM(D2:D3)";
// 2行目の下に1行挿入し、2行目の内容を複写
ws.Row(2).InsertRowsBelow(1);
ws.Range("A2:D2").CopyTo(ws.Cell("A3"));
wb.SaveAs("sales_styled.xlsx");
Console.WriteLine($"{ws.Cell("A3").GetString()} {ws.Cell("D3").FormulaA1} {ws.Cell("A6").GetString()} {ws.Cell("D6").FormulaA1} merged={ws.Cell("B6").IsMerged()}");
このコードでは、2行目の下に1行挿入したあと複写先の数式が B3*C3 に、1行下へずれた合計の数式が SUM(D2:D4) に自動で直ることを確認しています。結合セルの値は左上のセル(ここではA6)にだけ入り、他のセルは空になります。結合範囲を読み取るときは左上のセルを参照してください。
日付の書き込みと表示形式、条件付き書式
日付は DateTime のまま代入します。表示形式は Style.DateFormat(日付)と Style.NumberFormat(数値)、条件付き書式は範囲の AddConditionalFormat() から設定します。
using ClosedXML.Excel;
using var wb = new XLWorkbook();
var ws = wb.AddWorksheet("出荷");
ws.Cell("A1").Value = "出荷日";
ws.Cell("B1").Value = "数量";
ws.Cell("A2").Value = new DateTime(2026, 10, 14);
ws.Cell("A3").Value = "2026/10/15"; // 文字列のまま入る
ws.Cell("B2").Value = 120;
ws.Cell("B3").Value = 35;
ws.Cell("B4").Value = 80;
Console.WriteLine($"{ws.Cell("A2").DataType} {ws.Cell("A3").DataType}"); // DateTime Text
Console.WriteLine(ws.Cell("A2").Value.GetUnifiedNumber()); // 46309(シリアル値)
ws.Column("A").Style.DateFormat.Format = "yyyy/mm/dd";
Console.WriteLine(ws.Cell("A2").GetFormattedString()); // 2026/10/14
Console.WriteLine(ws.Cell("A3").TryGetValue<DateTime>(out _)); // False
ws.Range("B2:B4").Style.NumberFormat.Format = "#,##0\"個\"";
Console.WriteLine(ws.Cell("B2").GetFormattedString()); // 120個
// 50未満を赤系の背景、最大値を太字に
ws.Range("B2:B4").AddConditionalFormat().WhenLessThan(50)
.Fill.SetBackgroundColor(XLColor.FromHtml("#F8CBAD"));
ws.Range("B2:B4").AddConditionalFormat().WhenIsTop(1)
.Font.SetBold();
wb.SaveAs("dates.xlsx");
ClosedXML 0.105.1のソース(XLCellValue.cs)では、DateTime を代入すると型を DateTime にし、値をExcelのシリアル値として持ちます。2026年10月14日は46309です。一方、"2026/10/15" のような文字列は日付に変換されず Text 型で入ります。この場合 GetDateTime() は InvalidCastException を投げ、TryGetValue<DateTime> も False を返しました。受け取った日付文字列は DateTime.ParseExact で変換してから代入してください。
条件付き書式は、セルに色を塗るのではなくExcelのルールとして保存されます。保存したシートXMLには、conditionalFormatting 要素に「cellIs/lessThan」と「top10」の2規則が入っていました。条件は WhenBetween・WhenGreaterThan・WhenContains などから選べ、使用例は公式Wikiの条件付き書式のページにまとまっています。
数式の設定と再計算の仕組み
数式は FormulaA1(A1形式)か FormulaR1C1 に、先頭の「=」なしで入れます。ClosedXMLは独自の計算エンジンを持っており、Excelを使わずに値を計算できます。
using ClosedXML.Excel;
using var wb = new XLWorkbook();
var ws = wb.AddWorksheet("計算");
ws.Cell("A1").Value = 10;
ws.Cell("A2").Value = 20;
ws.Cell("A3").FormulaA1 = "SUM(A1:A2)";
ws.Cell("B1").FormulaA1 = "CONCAT(\"合計\", A3)";
Console.WriteLine(ws.Cell("A3").Value); // 30
Console.WriteLine(ws.Cell("B1").FormulaA1); // _xlfn.CONCAT("合計", A3)
ws.Cell("A1").Value = 100;
Console.WriteLine(ws.Cell("A3").CachedValue); // 30(再計算前の値)
Console.WriteLine(ws.Cell("A3").Value); // 120(読んだ時点で再計算)
ws.Cell("C1").FormulaA1 = "XLOOKUP(20,A1:A2,A1:A2)";
Console.WriteLine(ws.Cell("C1").Value); // #NAME?(ClosedXML側で未実装)
wb.SaveAs("calc.xlsx", new SaveOptions { EvaluateFormulasBeforeSaving = true });
押さえておくべき挙動は3つあります。1つ目に、Value は依存セルが変わっていれば読んだ時点で再計算し、CachedValue は前回の値をそのまま返します。公式ドキュメントは、速度が必要な場面では CachedValue を使い、保存前に SaveOptions.EvaluateFormulasBeforeSaving で再計算する使い分けを勧めています。
2つ目に、0.105.0から、CONCATのような2013年以降に追加された関数は保存形式の _xlfn. 付きの名前へ自動で補正されます。それ以前の版では、この接頭辞を自分で付けないとExcelで開いたときに #NAME? になっていました。
3つ目に、式として保存できることと、ClosedXMLが計算できることは別です。検証ではTEXTJOINは計算されましたが、XLOOKUPとFILTERは式こそ正しく保存されるものの、ClosedXML上の値は #NAME? でした。C#側でその値を使う処理では、VLOOKUPやINDEX/MATCHで書くか、C#で計算した結果を値として書き込んでください。
列幅の自動調整AdjustToContentsとフォント
AdjustToContents() は文字列の描画幅をフォントで計測して列幅を決めます。この計測に使うのがSixLabors.Fontsベースの「グラフィックエンジン」です。WindowsではMicrosoft Sans Serifを代替フォントに使います。0.102.0(2023年6月)以降は、フォントの無い環境向けにCalibri互換の最小限のフォントがライブラリに埋め込まれました。それより前の版では、Linuxや AWS Lambda で「Unable to find font」例外が出ていました。
ただし、埋め込みフォントは欧文の字幅情報だけを持つCarlito由来のものです。DefaultGraphicEngine に渡すのは、セルのフォント(既定はCalibri)が見つからないときに使う代替フォントの名前です。日本語の列幅をExcelの見た目に近づけたい場合は、コンテナに日本語フォントを入れ、それを代替フォントに指定します。
using ClosedXML.Excel;
using ClosedXML.Graphics;
// 以降に作るブックすべてで、セルのフォントが見つからないときの代替フォントを指定
LoadOptions.DefaultGraphicEngine = new DefaultGraphicEngine("Noto Sans CJK JP");
using var wb = new XLWorkbook();
var ws = wb.AddWorksheet("幅調整");
ws.Cell("A1").Value = "長い日本語の見出しを入れた列";
ws.Columns().AdjustToContents();
Console.WriteLine(ws.Column(1).Width);
Linuxでは /usr/share/fonts/ などが探索先になります。指定したフォント名が見つからなくても例外にはならず、代替フォントで計測されることを確認しました。列幅が思った値にならないときは、まずコンテナ内で fc-list を実行してフォントが入っているかを確かめてください。AdjustToContents() は呼んだ時点のセル内容で幅を決めるので、データを書き込み終えてから呼ぶ必要があります。
ASP.NET CoreのExcelダウンロード実装(Results.File)
Web画面の「Excel出力」は、メモリ上のブックをバイト列にして Results.File で返します。dotnet new web で作ったMinimal APIの例です。
using ClosedXML.Excel;
var app = WebApplication.Create(args);
app.MapGet("/report", () =>
{
using var wb = new XLWorkbook();
var ws = wb.AddWorksheet("売上");
ws.Cell("A1").InsertTable(new[]
{
new { 商品 = "りんご", 数量 = 3 },
new { 商品 = "みかん", 数量 = 5 },
});
using var ms = new MemoryStream();
wb.SaveAs(ms);
return Results.File(ms.ToArray(),
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"売上_2026-10.xlsx");
});
app.Run();
.NET 10で起動して /report を取得すると、ステータス200で、Content-Typeが上のMIMEタイプ、Content-Dispositionが attachment; filename=___2026-10.xlsx; filename*=UTF-8''%E5%A3%B2%E4%B8%8A_2026-10.xlsx になりました。日本語のファイル名は filename* の形式へ自動でエンコードされます。匿名型のプロパティ名(商品・数量)は、そのまま見出し行になります。
ToArray() を挟むのは、MemoryStream をそのまま Results.File(ms, …) に渡すと、応答を書き出す時点で using によりストリームが破棄済みになるためです。試すとステータス500になり、ObjectDisposedException: Cannot access a closed Stream. がログに出ました。旧ASP.NET向けの拡張 ClosedXML.Extensions.WebApi(0.5.0・2024年10月)もありますが、ASP.NET Coreでは不要です。
ClosedXMLでできないこと(0.105.1で確認した制約)
採用後に発覚すると手戻りが大きいものから並べます。
| やりたいこと | 0.105.1の結果 | 代替策 |
|---|---|---|
| .xls の読み書き | ArgumentException | NPOI/事前に.xlsxへ変換 |
| .csv を開く | ArgumentException | CSVは別ライブラリで読む |
| 暗号化ファイルを開く | FileFormatException | 復号してから渡す |
| グラフの新規作成 | API無し | グラフ入りテンプレート |
| PDF出力 | 機能無し | 別の変換ツール |
| 保護されたセルの書き換え防止 | 防げない | アプリ側で制御 |
| 複数スレッドから同時操作 | 保証なし | 逐次処理/別プロセス化 |
ファイル形式と暗号化の制約
.xlsファイルを開くと Extension 'xls' is not supported. Supported extensions are '.xlsx', '.xlsm', '.xltx' and '.xltm'. という ArgumentException になります。.csvも同じ例外です。保存時も同様で、拡張子を.xlsにしたファイル名では保存できません。
「開くときのパスワード」で暗号化したファイルは中身がZIP形式ではないため、System.IO.FileFormatException: File contains corrupted data. で読み込みに失敗しました。取引先から暗号化済みのファイルが届く業務では、復号の工程を別に用意する必要があります。
グラフ・PDF・マクロの扱い
グラフの対応を求めるIssue #50「Charting support」は2016年9月に起票され、2026年9月時点でも未解決です。0.105.1には IXLCharts というインターフェースこそ残っていますが、IXLWorksheet にグラフを扱うプロパティが無く、コードからグラフを追加・編集することはできません(ws.Charts と書くとビルドエラーになります)。一方、グラフを含むファイルを開いてセルだけ書き換えて保存したところ、グラフの定義(xl/charts/chart1.xml)はそのまま残りました。グラフが必要な帳票は、グラフ入りのテンプレートを用意してデータ範囲だけClosedXMLで埋める設計にすると、この制約を回避できます。
SaveAs が受け付ける拡張子は上記の4つだけで、PDFとして保存する機能はありません。公式ドキュメントの機能一覧にもPDF出力は載っていません。.xlsmとしての保存はできますが、VBAマクロを編集・実行するAPIはありません。
シート保護とスレッド安全性
Protect でシートやブックに保護をかけることはできます。ただし公式ドキュメントは、この保護を「誤操作を防ぐためだけのもの」と位置付け、「ClosedXMLはロックされた要素を尊重しない」と明記しています。
using ClosedXML.Excel;
using var wb = new XLWorkbook();
var ws = wb.AddWorksheet("入力");
ws.Range("B2:B10").Style.Protection.Locked = false; // 入力欄だけロック解除
ws.Protect("sheet-pass", XLProtectionAlgorithm.Algorithm.SHA512);
ws.Cell("A1").Value = "書き換え"; // ClosedXMLからは保護を無視して書ける
Console.WriteLine($"{ws.IsProtected} {ws.Cell("A1").GetString()}");
wb.SaveAs("protected.xlsx");
上のコードは保護をかけた直後のA1に書き込めてしまい、True 書き換え と出力されます。ファイルに保存されるのはパスワードそのものではなく、ソルトとハッシュです。xlsxを展開すればXMLを直接書き換えられるので、改ざん防止やアクセス制御の手段として扱ってはいけません。
また、READMEは「ClosedXMLはスレッドセーフではなく、並列操作が動く保証はない。基盤のOpenXMLライブラリもスレッドセーフではない」としています。Parallel.ForEach で1つのブックに書き込む実装は避けてください。ブックをスレッドごとに分けた並列処理も公式には保証されていないため、確実さを優先するなら逐次処理にするか、プロセスを分けて実行します。
大量データでの処理時間とメモリの目安
公式READMEのPerformanceに掲載されているベンチマーク(開発版0.103-beta、Ryzen 5 5500U、.NET 7)は次のとおりです。
| データ | 書き込み+保存 | 保存込みの総メモリ | 読み込み |
|---|---|---|---|
| 25万行×15列(文字列・数値) | 7.962秒 | 477MiB | 15.648秒 |
| 100万行×10列(文字列のみ) | 23.436秒 | 1,880MiB | 49.046秒 |
100万行の書き込みで1,880MiB(約1.8GiB)を使う点が、設計上いちばん効いてくる数字です。ClosedXMLはブック全体をメモリに展開します。メモリ上限のあるコンテナやサーバーレス関数で数十万行を出力するなら、この表は桁の目安にとどめ、文字列の重複率や書式、同時実行数をそろえた実データで測ってから割り当てを決めてください。
本記事の検証環境(Core i9-9880H、macOS、.NET 10のReleaseビルド)でも、整数・文字列・小数・日付・真偽値の5列×10万行を、ストリーミング型のMiniExcel 1.46.0と比べました。時間は各プロセスでウォームアップ実行後の2回目を測り、この計測を3回繰り返した中央値、メモリは別プロセスで1回だけ実行したときの最大常駐メモリ(/usr/bin/time -l のRSS。空の.NETプロセスで約21MiB)です。データは record の列を IEnumerable で遅延生成し(文字列は1,000種の繰り返し)、両ライブラリに同じものを渡しました。
| 処理(5列×10万行) | 時間 | 最大メモリ |
|---|---|---|
| ClosedXML:1セルずつ代入+保存 | 2.70秒 | 約181MiB |
| ClosedXML:InsertData+保存 | 2.55秒 | 約201MiB |
| MiniExcel:SaveAs | 0.44秒 | 約55MiB |
| ClosedXML:開いてRowsUsedを数える | 3.72秒 | 約158MiB |
| MiniExcel:Queryで数える | 1.15秒 | 約50MiB |
InsertData と1セルずつの代入に速度の差はほとんどありませんでした。公式ドキュメントも、InsertData の利点は数式の再計算対象(dirty)を一括で付けられる点だとしています。
MiniExcelは書き出しが約6倍速く、メモリは約4分の1でした(ファイルは4.8MBで、ClosedXMLの2.7MBより大きい)。MiniExcelのREADMEは、全データをメモリに載せずストリームから処理する設計だと説明しています。この効果は IEnumerable を ToList() せずに渡したときに出ます。DataTable を渡すと全件を読み込むため、READMEも勧めていません。.xlsと暗号化ファイルには非対応です。
罫線・結合・数式・条件付き書式を組み立てる帳票はClosedXMLで作ります。書式の無い数十万行を、メモリ上限のあるコンテナやサーバーレス関数で入出力するなら、MiniExcelを先に検討してください。
ClosedXMLのライセンスと商用利用:EPPlus・NPOIとの比較
ClosedXMLのMITライセンスと商用利用条件
ClosedXML本体とテンプレート拡張のClosedXML.Reportは、どちらもMITライセンスです。社内システム・受託開発・SaaSのいずれに組み込んでも利用料は発生しません。義務は、著作権表示とライセンス文を配布物に含めることだけです。ライセンス以外の観点も含めて比べると次のようになります。
| ライブラリ | 最新版(NuGet) | ライセンス | 商用利用 | .xls |
|---|---|---|---|---|
| ClosedXML | 0.105.1(2026-07) | MIT | 無償 | 不可 |
| EPPlus | 8.7.1(2026-09) | Polyform Noncommercial | 有償ライセンス | 不可 |
| NPOI | 2.8.1(2026-09) | Apache 2.0+保守料EULA | 条件により保守料(免除あり) | 可 |
EPPlus 8の商用・非商用ライセンスと設定要件
EPPlusは4系の最終版4.5.3.3(2020年1月)までLGPL-3.0でした。2020年3月公開の5系最初の正式版5.0.3から、Polyform Noncommercial 1.0.0に移っています。EPPlus 8のパッケージ同梱readmeは「個人または非商用なら無償、商用のビジネスで使うには商用ライセンスが必要」と明記しています(EPPlus公式のライセンス概要)。コードの実行前に ExcelPackage.License.SetCommercial("ライセンスキー") か、非商用なら SetNonCommercialPersonal/SetNonCommercialOrganization でライセンスを指定しなければなりません。古い記事の「EPPlusは無料」は4系までの話です。2026年9月22日には9.0.0のプレビュー版がNuGetに公開されています。
NPOIは2.8.0からNuGet版に保守料の契約
NPOIのソースコードは現在もApache License 2.0です。ただし、2026年4月6日公開の2.8.0以降、NuGetで配布されるバイナリには「Open Source Maintenance Fee」のEULA(OSMFEULA.txt)が付きました。収益を生む用途で使い、かつ年間総収入が1万米ドル以上の利用者は月額の保守料を払う契約です。保守担当者に別途サポート料・保守料を払っている利用者は、二重課金を避けるため免除されます。EULA本文は、ソースから自分でビルドしたバイナリにはこの契約が及ばないとしています。2.7.6までのパッケージのライセンス表記はApache-2.0だけなので、NPOIを更新するときはこの差を法務と確認してください。
用途別のライブラリの選び方
「C#のExcelライブラリ」として名前が挙がる7つを、NuGetの最新版(2026年10月14日時点)で並べると次のとおりです。
| ライブラリ | 最新版 | ライセンス | 書き込み | .xls | 向く用途 |
|---|---|---|---|---|---|
| ClosedXML | 0.105.1 | MIT | 可 | 不可 | 書式付きの帳票 |
| EPPlus | 8.7.1 | 商用は有償 | 可 | 不可 | グラフを含む出力 |
| NPOI | 2.8.1 | Apache 2.0+保守料EULA | 可 | 読み書き可 | .xlsの書き出し |
| MiniExcel | 1.46.0 | Apache 2.0 | 可 | 不可 | 大量データの入出力 |
| ExcelDataReader | 3.9.0 | MIT | 不可 | 読み込み可 | 取り込み専用 |
| Open XML SDK | 3.5.1 | MIT | 可 | 不可 | XMLレベルの制御 |
| Excel Interop | (Office本体) | Officeのライセンス | 可 | 読み書き可 | デスクトップ限定 |
判断はファイル形式と、書き込みが要るかどうかから始めます。Excel本体を入れずに.xlsを書き出すなら、表の中ではNPOIだけが残ります。2.8系なら保守料の対象かを確認してください。.xlsを読むだけなら、ExcelDataReader(MIT)も使えます。READMEの対応表には、.xlsx(OpenXml)に加えて.xls(BIFF8ほか)とCSVが載っています。
.xlsxだけで足り、商用で無償に収めたいならClosedXMLです。書式の無いデータを大量に流すだけならMiniExcel、グラフをコードから動的に作る必要があり商用ライセンス費を払えるならEPPlusを検討します。サーバーやバッチでExcel本体をCOM経由で動かすInteropは、Microsoftが無人・非対話のクライアントからのOfficeの自動操作を推奨もサポートもしないと明記しています。新規の採用候補からは外してください。Go言語でExcelを扱う場合の同種ライブラリはExcelizeの使い方とv2.11.0の変更点で解説しています。
ClosedXML.Reportでテンプレートから帳票を作る方法
罫線や書式が細かい帳票は、コードで1セルずつ装飾するより、Excelで作ったテンプレートにデータを流し込むほうが保守しやすくなります。ClosedXML.Report(最新版0.2.12・2025年5月22日)は、セルに書いた {{item.Name}} のような式を展開するテンプレートエンジンです。本体とは別パッケージなので、dotnet add package ClosedXML.Report --version 0.2.12 で追加します。
using ClosedXML.Report;
// template.xlsx:A2に {{item.Name}}、B2に {{item.Qty}} を書き、
// A2:B3 に名前「Items」を付けておく(3行目は空の制御行)
var template = new XLTemplate("template.xlsx");
template.AddVariable(new
{
Items = new[] { new Item("りんご", 3), new Item("みかん", 5) }
});
template.Generate();
template.SaveAs("report.xlsx");
Console.WriteLine("ok");
record Item(string Name, int Qty);
名前付き範囲の最終行は空けておく「制御行」で、データ行はその上に件数分だけ展開されます。注意点として、ClosedXML.Report 0.2.12の依存条件は「ClosedXML 0.105.0以上」です。NuGetは条件を満たす最小の版を選ぶため、ClosedXML.Reportだけを追加すると、復元されるのは0.105.1ではなく0.105.0でした。本体の最新版を使いたい場合は、ClosedXMLのパッケージ参照も明示的に追加してください。
Excel帳票を業務システムに組み込む場合、テンプレートは画面や集計の仕様と一緒に決めると手戻りが減ります。既存のExcel運用を引き継ぐ基幹システム開発のご相談も承っています。
よくある質問
ClosedXMLは商用利用できますか?
できます。MITライセンスなので、著作権表示とライセンス文を配布物に含めれば、商用のシステムやサービスに無償で組み込めます。EPPlus 8とは異なり、ライセンスキーやライセンス種別をコードで設定する必要もありません。
ClosedXMLで.xlsファイルは扱えますか?
扱えません。対応する拡張子は .xlsx・.xlsm・.xltx・.xltm の4つで、.xlsを開くと ArgumentException になります。.xlsが必要ならNPOIを使うか、事前に.xlsxへ変換してから読み込んでください。
ClosedXMLでPDF出力はできますか?
できません。SaveAs で保存できるのは .xlsx・.xlsm・.xltx・.xltm だけです。PDFが必要なら、作った.xlsxを別の変換ツールに渡します。
ClosedXMLとOpen XML SDKの違いは何ですか?
Open XML SDK(DocumentFormat.OpenXml、最新版3.5.1)はxlsxのXML要素を直接組み立てる低水準のAPIで、ClosedXMLはその上のラッパーです。通常はClosedXMLを使い、ClosedXMLが対応しない要素をXMLのレベルで書くときだけOpen XML SDKを直接使います。
AdjustToContentsで列幅が合わないのはなぜですか?
多くの場合、計測に使うフォントが実行環境に無いことが原因です。LinuxのコンテナにはCalibriや日本語フォントが入っていないことが多く、埋め込みの代替フォントで計測されるため、Excelで開いたときの見た目とずれます。日本語フォントをインストールして LoadOptions.DefaultGraphicEngine の代替フォントに指定し、データを書き終えてから AdjustToContents() を呼んでください。