出力パイプラインの構造定義
データベースより取得した集合データをExcel形式に変換する際、一般的な実装フローは「ヘッダーマッピング定義」と「データセットの順次展開」の二段階に分類できます。本項では、これらのステップをカプセル化し、異なるデータソースに対応可能な再利用可能なコンポーネントを実装します。
コアクラスの実装
以下の実装では、ファイルパスの妥当性チェック、ワークブックの初期化、セル样式の統一管理、リフレクションによる動的プロパティ解決、最終的なディスク書き込み処理を一体化させたラッパーを作成しています。元のロジックを整理し、パフォーマンス向上のためプロパティメタデータのキャッシュ、安全なnull判定、モジュール化したスタイル適用処理を追加しています。
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using NPOI.HSSF.UserModel;
using NPOI.SS.UserModel;
/// <summary>
/// NPOIを使用したExcelエクスポート処理のカプセル化クラス
/// </summary>
public class NpoiDataExporter
{
/// <summary>
/// 生成対象のファイル保存先パス
/// </summary>
public string TargetPath { get; set; }
/// <summary>
/// プロパティキーと見出し表示名の対応付け辞書
/// </summary>
public Dictionary<string, string> HeaderMap { get; set; }
private HSSFWorkbook _document;
private ISheet _targetSheet;
public NpoiDataExporter(string targetPath, Dictionary<string, string> headerMap)
{
TargetPath = targetPath;
HeaderMap = headerMap ?? throw new ArgumentNullException(nameof(headerMap));
ValidateFilePath(TargetPath);
InitializeWorkbook();
}
private void ValidateFilePath(string path)
{
var ext = Path.GetExtension(path).ToLowerInvariant();
if (!new[] { ".xls", ".xlsx" }.Contains(ext))
throw new ArgumentException("Unsupported extension. Please use .xls or .xlsx.");
var info = new FileInfo(path);
if (!info.Exists)
info.Create().Dispose();
}
private void InitializeWorkbook()
{
_document = new HSSFWorkbook();
_targetSheet = _document.CreateSheet("ReportData");
}
public void ExportToExcel<T>(IEnumerable<T> records) where T : class
{
RenderHeaders();
RenderDataRows(records);
AutoFitColumns();
PersistToFile();
}
private void RenderHeaders()
{
var style = _document.CreateCellStyle();
ApplyBorderAndAlign(style);
var font = _document.CreateFont();
font.IsBold = true;
style.SetFont(font);
style.FillForegroundColor = IndexedColors.Blue.Index;
style.FillPattern = FillPattern.SolidForeground;
var row = _targetSheet.CreateRow(0);
row.HeightInPoints = 30;
int colIdx = 0;
foreach (var mapping in HeaderMap)
{
var cell = row.CreateCell(colIdx++);
cell.SetCellValue(mapping.Value);
cell.CellStyle = style;
}
}
private void RenderDataRows<T>(IEnumerable<T> records)
{
var textStyle = _document.CreateCellStyle();
textStyle.DataFormat = HSSFDataFormat.GetBuiltinFormat("@");
ApplyBorderAndAlign(textStyle);
var textFont = _document.CreateFont();
textFont.Boldweight = (short)FontBoldWeight.Normal;
textStyle.SetFont(textFont);
// リフレクションコスト削減のため事前に PropertyInfo を取得
var props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
int rowIndex = 1;
foreach (var entity in records)
{
var row = _targetSheet.CreateRow(rowIndex);
row.HeightInPoints = 25;
int cellIdx = 0;
foreach (var mapping in HeaderMap)
{
var prop = props.FirstOrDefault(p => p.Name == mapping.Key);
var cell = row.CreateCell(cellIdx++);
if (prop == null)
{
cell.SetCellValue($"'{mapping.Key}'フィールドが定義されていません");
}
else
{
var val = prop.GetValue(entity);
if (val != null && !Equals(val, DBNull.Value))
cell.SetCellValue(val.ToString());
}
cell.CellStyle = textStyle;
}
rowIndex++;
}
}
private void ApplyBorderAndAlign(ICellStyle style)
{
style.BorderBottom = BorderStyle.Thin;
style.BorderLeft = BorderStyle.Thin;
style.BorderRight = BorderStyle.Thin;
style.BorderTop = BorderStyle.Thin;
style.VerticalAlignment = VerticalAlignment.Center;
style.Alignment = HorizontalAlignment.Center;
}
private void AutoFitColumns()
{
for (int i = 0; i < HeaderMap.Count; i++)
{
_targetSheet.AutoSizeColumn(i);
}
}
private void PersistToFile()
{
using (var fs = new FileStream(TargetPath, FileMode.OpenOrCreate, FileAccess.Write))
{
_document.Write(fs);
fs.Flush();
}
_document.Close();
}
}
適用シナリオ
匿名型コレクションの出力
動的型付けやLINQクエリの結果など、具体的な实体クラスを持たないデータ列の場合でも、リフレクションマッピングを採用しているためシームレスに連携可能です。
Dictionary<string, string> fields = new Dictionary<string, string>
{
{ "ID", "主キー" },
{ "Name", "氏名" },
{ "Age", "年齢" },
{ "Birthday", "生年月日" }
};
var exporter = new NpoiDataExporter(@"./export_dynamic.xls", fields);
List<object> dataset = new List<object>
{
new { ID = 1, Name = "山田太郎", Age = 28, Birthday = DateTime.Today },
new { ID = 2, Name = "佐藤花子", Age = 35, Birthday = DateTime.Today.AddDays(-5) }
};
exporter.ExportToExcel(dataset);
厳密に型指定されたエンティティリストの出力
ORMやカスタムリポジトリパターンから取得した標準ドメインモデルの一覧に対して使用するケースです。ジェネリック制約によりIDEの静的解析支援が効き、コード補完とエラー検知の精度が向上します。
// DBアクセス層より一覧を取得済みの想定
var entities = DatabaseContext.MemberRecords.ToList();
Dictionary<string, string> mapConfig = new Dictionary<string, string>
{
{ "MemberID", "会員番号" },
{ "AccountCode", "ログインID" },
{ "DisplayName", "表示名" },
{ "StatusFlag", "アクティブ状態" },
{ "CreditBalance", "残高ポイント" }
};
var exporter = new NpoiDataExporter(@"./export_strongly_typed.xls", mapConfig);
exporter.ExportToExcel(entities);