Azure Functions(Function App)でClosedXML.Reportsを使ってExcelレポートを生成し、Azure Blob Storageへ保存したいのに「Template.xlsxが見つからない」で止まる。さらにテンプレート変数({{HeaderField}})が繰り返されたりUnknown identifierになる――本記事では、実環境で確実に動かすための原因切り分けと実装・テンプレ設計のコツをまとめます。
想定シナリオ:Azure FunctionsでExcelを生成してBlobへ保存する
まず前提として、Excel生成~Blob保存の流れを整理します。テキストファイルは保存できるのにExcelだけ失敗する場合、ほとんどが「テンプレートの読み込み(ファイルパス/配置)」か「テンプレート側の書き方(ClosedXML.Reportsの解釈)」に原因があります。
| 処理ステップ | やること | 失敗しやすいポイント | ログに出すべき情報 |
|---|---|---|---|
| テンプレートのパス決定 | Template.xlsxのフルパスを作る | 基準ディレクトリが想定と違う | 組み立てたフルパス、基準ディレクトリ |
| 存在確認 | File.Exists()で有無を確認 | publishに含まれていない | File.Exists結果、テンプレート配置フォルダ一覧 |
| テンプレート読み込み | XLTemplateで読み込む | パス誤り、読み取り不可 | 例外メッセージ、スタックトレース |
| 変数投入・生成 | AddVariable→Generate | テンプレ側プレースホルダーが不正 | 投入した変数名、対象シート名 |
| ストリーム化 | MemoryStreamへSaveAs | Position未リセット | stream.Length、stream.Position |
| Blobへアップロード | UploadAsyncで保存 | 接続文字列/権限/コンテナ未作成 | コンテナ名、Blob名、レスポンス |
最小構成のサンプル(Excel生成→Blob保存)
まず「動く型」を作っておくと切り分けが一気に楽になります。以下は、テンプレート読み込み・変数置換・Blob保存までを通す基本形です(C#/Functionsのin-process想定)。
using System;
using System.IO;
using System.Threading.Tasks;
using Azure.Storage.Blobs;
using ClosedXML.Report; // ClosedXML.Reports のXLTemplateが入る想定
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
public static class CreateReportFunction
{
[FunctionName("CreateReport")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req,
ILogger log,
ExecutionContext context)
{
// テンプレートのフルパス(後述の「FunctionAppDirectory基準」)
var templatePath = Path.Combine(context.FunctionAppDirectory, "Templates", "Template.xlsx");
log.LogInformation("FunctionAppDirectory: {dir}", context.FunctionAppDirectory);
log.LogInformation("Template path: {path}", templatePath);
log.LogInformation("Template exists: {exists}", File.Exists(templatePath));
if (!File.Exists(templatePath))
{
return new NotFoundObjectResult($"Template not found: {templatePath}");
}
// 変数例
var headerfield = "請求書(サンプル)";
using var template = new XLTemplate(templatePath);
template.AddVariable("HeaderField", headerfield);
// 必要ならオブジェクト/リストも AddVariable する
// template.AddVariable("Items", items);
template.Generate();
using var stream = new MemoryStream();
template.Workbook.SaveAs(stream);
stream.Position = 0;
var connectionString = Environment.GetEnvironmentVariable("AzureWebJobsStorage");
var containerName = "reports";
var blobName = $"report-{DateTime.UtcNow:yyyyMMddHHmmss}.xlsx";
var blobServiceClient = new BlobServiceClient(connectionString);
var containerClient = blobServiceClient.GetBlobContainerClient(containerName);
await containerClient.CreateIfNotExistsAsync();
var blobClient = containerClient.GetBlobClient(blobName);
await blobClient.UploadAsync(stream, overwrite: true);
log.LogInformation("Uploaded blob: {name}", blobName);
return new OkObjectResult(new { container = containerName, blob = blobName });
}
}
ここから先は、この流れのうち「テンプレート読み込みで止まる問題」と「変数埋め込みの解釈で崩れる問題」を、現場で再発しにくい形に落とし込みます。
問題:Azure FunctionsでTemplate.xlsxが見つからず、Blob保存まで進まない
「テキストはBlobに保存できるのに、Excel生成だけ失敗する」場合、まず疑うべきはテンプレートファイルの配置と参照パスです。ローカル(func start)では動くのにAzure上だけ失敗するのも典型的なパターンです。
よくある症状
- テンプレートを
AppContext.BaseDirectory + "Template.xlsx"で探している - ローカルでは見つかるが、Azure上ではFileNotFoundException相当になる
- ログを見ると、想定していないディレクトリ配下を見に行っている
- VSCodeのpublishでパッケージに入れたつもりなのに、実環境に存在しない
なぜAppContext.BaseDirectoryでハマるのか
AppContext.BaseDirectory は「実行時のアプリのベースディレクトリ」を返しますが、Azure Functionsではホスティング形態や実行モデル、パッケージ配置(Run From Packageなど)によって“期待した場所”とズレることがあります。結果として、ローカルの出力フォルダを基準にしたつもりが、Azure上では別のディレクトリを基準にしてしまい、テンプレートが見つからない…という事故が起きます。
| 基準パスの取り方 | 特徴 | Azure Functionsでの落とし穴 | 推奨度 |
|---|---|---|---|
AppContext.BaseDirectory | .NETランタイムのベースを返す | ホスト都合で指す場所が変わりやすい | △ |
Environment.CurrentDirectory | カレントディレクトリ | 実行コンテキストで変わりやすい | △ |
Directory.GetCurrentDirectory() | カレントディレクトリ(同上) | 同上 | △ |
ExecutionContext.FunctionAppDirectory | Function Appのルートを基準にできる | in-process向け(受け取りが必要) | ◎ |
解決:ExecutionContextを受け取り、FunctionAppDirectory基準でテンプレートパスを組み立てる
最も堅いのは、FunctionsのRunメソッドでExecutionContextを受け取り、context.FunctionAppDirectoryを基準にテンプレートの相対パスを組み立てる方法です。
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req,
ILogger log,
ExecutionContext context)
{
var templatePath = Path.Combine(context.FunctionAppDirectory, "Templates", "Template.xlsx");
...
}
これで「デプロイされたFunction Appのルートから見たTemplates/Template.xlsx」を安定して参照できます。Windows/Linuxの差異もPath.Combine()に任せられるため、将来のホスティング変更にも強いです。
パスが正しいかを最短で確認するログの出し方
テンプレート問題は、思い込みで時間を溶かしがちです。次の3点をログに出すだけで、原因の9割が見えます。
- 組み立てたテンプレートのフルパス
File.Exists()の結果- テンプレートを置いたフォルダのファイル一覧(必要に応じて)
log.LogInformation("FunctionAppDirectory: {dir}", context.FunctionAppDirectory);
var templateDir = Path.Combine(context.FunctionAppDirectory, "Templates");
log.LogInformation("TemplateDir: {dir}", templateDir);
log.LogInformation("TemplateDir exists: {exists}", Directory.Exists(templateDir));
var templatePath = Path.Combine(templateDir, "Template.xlsx");
log.LogInformation("Template path: {path}", templatePath);
log.LogInformation("Template exists: {exists}", File.Exists(templatePath));
// デバッグ用(本番常時は推奨しない)
if (Directory.Exists(templateDir))
{
foreach (var f in Directory.GetFiles(templateDir, "*", SearchOption.TopDirectoryOnly))
{
log.LogInformation("TemplateDir file: {file}", f);
}
}
ここでTemplateDir exists: falseなら「デプロイ物にTemplatesフォルダごと入っていない」、TemplateDir exists: trueでもTemplate exists: falseなら「ファイルだけ抜けている」か「ファイル名/拡張子が違う」などに絞れます。
テンプレートをpublishに“確実に含める”設定
ローカル実行で見えているからといって、publishにも入るとは限りません。特にCopyToOutputDirectoryだけ指定している場合、publishの成果物に含まれず、Azure上で見つからないことがあります。テンプレートファイルは「ビルド出力」と「publish出力」の両方に確実に入るように設定しておくのが安全です。
例:Templates/Template.xlsxを含める(.csproj)
<ItemGroup>
<None Include="Templates\Template.xlsx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>Always</CopyToPublishDirectory>
</None>
</ItemGroup>
| 設定 | 反映先 | 用途 | 注意点 |
|---|---|---|---|
CopyToOutputDirectory | bin/Debug, bin/Release など | ローカル実行・デバッグで必要 | publishに入るとは限らない |
CopyToPublishDirectory | publish成果物 | Azureへデプロイする物に必要 | Always/PreserveNewestの選択に注意 |
VSCodeからpublishしている場合でも、最終的には「publish成果物にテンプレートが含まれているか」が勝負です。テンプレートが入っていない限り、Azure上では永遠に見つかりません。
Azure側でテンプレートが本当に配置されているか確認する
ローカルの設定が正しくても、デプロイ手順・パッケージング・スロット切替などで期待通りに配置されていないケースはあります。Azure側では次の観点で確認すると早いです。
- Function Appのコンテンツルート(一般的にsite/wwwroot相当)配下にTemplates/Template.xlsxが存在するか
- ファイル名の大文字小文字、拡張子(.xlsx)に揺れがないか
- 「Run From Package」運用の場合、ポータルからファイルを“後置き”しても反映されない(パッケージに入っていないものは使えない)
実運用では「デプロイ済みの実体を見て確認する」癖をつけると、テンプレート問題の再発を大幅に減らせます。
それでも安定しない場合の代替案(テンプレートをファイルとして持たない)
テンプレートの「ファイル配置」を前提にすると、デプロイ方式や構成管理の影響を受けます。運用要件によっては、テンプレートの置き場所を変えることでさらに堅牢になります。
| 方式 | 概要 | メリット | デメリット | 向いているケース |
|---|---|---|---|---|
| Blobにテンプレートを置く | 起動時/都度ダウンロードして使用 | デプロイとテンプレ更新を分離できる | ダウンロード遅延、権限/キャッシュ設計が必要 | テンプレ更新頻度が高い |
| 埋め込みリソース | Template.xlsxをAssemblyに埋め込む | 「存在しない」が起きない | 更新はビルド/デプロイ必須 | テンプレが固定、配布の一体化が重要 |
| ファイル配置(本記事の主軸) | wwwroot配下に同梱 | 実装がシンプル、読み込みが速い | publish設定ミスで事故りやすい | テンプレが少数で安定 |
まずは本記事の「FunctionAppDirectory基準 + publishに含める設定」で解消するケースが大多数ですが、チームの運用と相性が悪いなら代替案も検討してください。
問題:ClosedXML.Reportsで{{HeaderField}}が繰り返されたり「Unknown identifier」になる
テンプレートファイルを読めるようになった次にハマりやすいのが、「変数を埋め込むと表示が崩れる」「同じ値が行方向に繰り返される」「Unknown identifier ‘HeaderField’ が出る」といったテンプレート解釈の問題です。
まず押さえる:単純な変数置換は“名前付き範囲”が不要
このケースのポイントはここです。
- 単純な値(文字列、日付、数値など)を1か所に入れるだけなら、Excel側で名前付き範囲(Name/Range)を定義する必要はありません
- セルに{{HeaderField}}を“文字として”置けば、
AddVariable("HeaderField", ...)で置換されます
Excelテンプレート側は、対象セルに以下のように入力するだけでOKです。
{{HeaderField}}
コツとして、Excelが勝手に数式や別形式として解釈しそうなときは、セルの表示形式を「文字列」にするか、先頭にアポストロフィ(')を付けて文字として固定すると事故が減ります(例:'{{HeaderField}})。
Excel側の“余計な定義”がUnknown identifierの原因になり得る
Unknown identifier 'HeaderField' は、「テンプレート内にある識別子が、ClosedXML.Reportsの想定する形で見つからない/解釈できない」時に起きます。特に次のような状態だと発生しやすいです。
- Excel側で「HeaderField」という名前付き範囲を定義してしまっている
- 名前付き範囲が意図せず別シートや別セルを指している
- テンプレートに組み込み機能(繰り返し範囲・表・コレクション出力)として認識される位置に置いている
- 変数名のタイプミス(全角/半角、余計なスペース、似た文字)
解決の基本方針はシンプルで、「余計な名前定義や範囲指定を外し、単純なプレースホルダーとして置く」ことです。特にヘッダーのような単発値は、繰り返し出力の範囲(リストの行)とは別領域に置いてください。
「行方向に繰り返される」典型パターンと直し方
同じヘッダーが行方向に繰り返される場合、テンプレート上でそのセルが“繰り返し生成される領域”に含まれていることが多いです。例えば「明細行を増やすテンプレート範囲」の中に{{HeaderField}}が入っていると、明細行数に応じてヘッダーも複製されてしまいます。
| 症状 | 原因の可能性 | 直し方 | 再発防止の設計 |
|---|---|---|---|
| ヘッダーが明細行の数だけ増える | ヘッダーセルが繰り返し範囲に含まれている | ヘッダーを繰り返し範囲の外へ移動 | ヘッダー領域と明細領域を上下で分離 |
| 一部セルが壊れてレイアウトが崩れる | 結合セル/表の構造が繰り返しと衝突 | 繰り返し領域の結合セルを避ける | 明細はシンプルな行列で作る |
| Unknown identifierになる | 名前付き範囲や識別子の衝突、タイプミス | 名前定義を削除し、{{HeaderField}}を文字として配置 | 変数名は短く、テンプレで統一 |
テンプレート変数を安全に運用するためのルール
運用で事故を減らすには、テンプレート設計にルールを持たせるのが効果的です。チームで共有しやすいように、実務で使える形に落とし込みます。
- 単発の変数(ヘッダー、発行日、担当者名など)は「繰り返し領域の外」に配置する
- テンプレート内のプレースホルダー名は、C#側のAddVariable名と完全一致させる(大文字小文字、全角半角、スペースに注意)
- テンプレートに名前付き範囲を作るのは、繰り返し出力など明確な目的がある場合だけに絞る
- ヘッダーセルは可能なら「文字列」形式にして、Excelの自動変換を避ける
- テンプレートの変更はGit管理し、テンプレ改修時は必ずFunctionsの統合テスト(生成→Blob保存まで)を回す
原因切り分け:Unknown identifierのチェックリスト
Unknown identifierが出たら、次の順番で確認すると早いです。
| チェック項目 | 確認内容 | 具体例 |
|---|---|---|
| 変数名の一致 | テンプレ:{{HeaderField}} / コード:AddVariable(“HeaderField”, …) | HeaderFiled になっていないか |
| 余計な名前定義 | Excelの「名前の管理」でHeaderFieldが定義されていないか | 昔の残骸でNameが残る |
| 配置場所 | 繰り返し領域や表の中に置いていないか | 明細行のテンプレ範囲内に混入 |
| セルの内容が文字か | 数式/リンク/別形式として解釈されていないか | 先頭に’を付けて固定 |
| テンプレートの複雑さ | 結合セルや複雑な表が影響していないか | 一度シンプルなテンプレで再現確認 |
実装側のコツ:テンプレート起因かどうかを切り分ける
ClosedXML.Reportsの問題は「コードが悪いのか」「テンプレートが悪いのか」が混ざって見えます。再現性を上げるために、次の手順が有効です。
- まずテンプレートを最小化する(1シート、1セルに{{HeaderField}}だけ置いたTemplate.xlsxを作る)
- 最小テンプレで置換できるなら、問題は「元テンプレの構造(繰り返し範囲や名前定義)」に寄っている
- 最小テンプレでもUnknown identifierなら、変数名やライブラリ参照、パッケージ差異を疑う
テンプレートを最小化してから段階的に戻すと、崩れるポイントをほぼ確実に特定できます。
まとめ:Azure Functions × ClosedXML.Reportsでハマりやすい2点は「パス」と「テンプレ設計」
最後に、本記事の要点を運用で使える形にまとめます。
- Template.xlsxが見つからない問題は、ExecutionContext.FunctionAppDirectoryを基準にし、Path.Combine()で組み立てるのが堅い
- 原因切り分けは、テンプレートのフルパスとFile.Exists()をログに出すだけで一気に進む
- テンプレートがpublishに入らない事故を防ぐには、CopyToPublishDirectoryも明示して「確実に同梱」する
- {{HeaderField}}が繰り返されたりUnknown identifierになる問題は、Excel側の書き方が原因になりやすい。単発変数は名前付き範囲不要で、セルに{{HeaderField}}を文字として置く
- ヘッダー(単発)と明細(繰り返し)は領域を分け、余計な名前定義を避けるとテンプレが壊れにくい
この2点を押さえるだけで、「ローカルでは動くのにAzureで止まる」「テンプレを直したら別の箇所が崩れる」といったトラブルを大幅に減らせます。テンプレートの配置と参照は“仕組み化”、テンプレートの書き方は“ルール化”して、継続運用で強い構成にしていきましょう。

コメント