C#およびNPOIを用いたExcelファイル出力処理の汎用ラッパー設計

出力パイプラインの構造定義

データベースより取得した集合データを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);

タグ: NPOI C# ExcelAutomation HSSFWorkbook Reflection

7月18日 19:06 投稿