Microsoft Graph API で Excel テーブルへ行を追加するとき、「列数が可変なので for で値を積み上げたい」「UntypedArray/UntypedNode の入れ子が分かりにくい」という悩みをよく耳にします。本記事は、C# から List → UntypedArray 変換を軸に、可変列の行をスマートに動的生成する実装パターンを、実践コードと併せて徹底解説します。現場ですぐ使えるテンプレート、落とし穴、パフォーマンスまで一気に整理します。
C# × Graph API × Excel:最短で正解に辿り着く考え方
Excel テーブル(WorkbookTable)の行追加 API は、「行の配列(外側)」「列の配列(内側)」という二重配列の JSON を要求します。Graph SDK(C#)では、この配列を UntypedArray と、その要素である UntypedNode で表現します。つまり、
- 外側の
UntypedArray= 複数行 - 内側の
UntypedArray= 1 行の複数列 - 各セル =
UntypedNode(文字列、数値、真偽、null…)
しかし UntypedArray 自体は “配列” なので、後から Add で伸ばす用途に向きません。ここがハマりどころ。コツは簡単で、いったん C# の可変コレクション(List<T>)で組み立て、最後に UntypedArray に変換します。これで「列数可変」や「QA セクションが 1 問4列 × N 問」のような構造を柔軟に表現できます。
結論:List で作って最後に UntypedArray へ変換する
以下は、固定列に続いて可変長 QA セクション(1 問につき 4 列)をループで追加し、最終的に 1 回の API で行を挿入する最小実装です。外側=行配列、内側=列配列の形を、C# らしい書き方で崩さずに組み上げます。
// ---- ① 行(List<UntypedNode>)を作成 ----
var row = new List<UntypedNode>
{
new UntypedString(data.startTime),
new UntypedString(data.endTime),
new UntypedString(data.userType),
new UntypedString(data.device),
new UntypedString(data.module),
new UntypedString(data.mode),
new UntypedString(data.moduleStepCount),
new UntypedString(data.errors),
new UntypedString(data.assistance),
new UntypedInteger(data.questionCount),
new UntypedInteger(data.incorrectCount),
new UntypedInteger(data.correctCount)
};
// ---- ② 可変長部分をループで追加(1 問につき 4 列)----
for (int i = 0; i < data.questionAnswers.Count; i++)
{
var q = data.questionAnswers[i];
row.Add(new UntypedString(q.questionStatus));
row.Add(new UntypedString(q.question));
row.Add(new UntypedString(q.correctAnswers));
row.Add(new UntypedString(q.userAnswers));
}
// ---- ③ List → UntypedArray に変換し、行コレクションに格納 ----
var rows = new List<UntypedArray> { new UntypedArray(row) };
// ---- ④ WorkbookTableRow 要求ボディを生成 ----
var requestBody = new WorkbookTableRow
{
Values = new UntypedArray(rows) // rows は「行の配列」、row は「列の配列」
};
// ---- ⑤ Graph API で行を追加 ----
await GraphClient
.Drives[driveId]
.Items[documentId]
.Workbook
.Tables[tableId]
.Rows
.PostAsync(requestBody);
なぜこの書き方が効くのか(Untyped 系の腹落ち解説)
| 要素 | 役割 | ポイント |
|---|---|---|
| UntypedArray(外側) | 行の配列(rows) | 1 回の呼び出しで複数行を追加可能。パフォーマンス向上に効く。 |
| UntypedArray(内側) | 1 行の列配列(columns) | セル値は UntypedNode の並び。後から追加しづらいので List で組み立てる。 |
| UntypedNode | セル(値) | UntypedString / UntypedInteger / UntypedDouble / UntypedBoolean / UntypedNull など型に応じて選ぶ。 |
まずはデータモデルを定義する(例)
実運用では、API レスポンスやアプリ内 DTO から Excel 行を構成する場面がほとんどです。型を決め、Excel の列順に並べる責務を 1 箇所に集約しましょう。
public sealed class SessionRow
{
public string startTime { get; init; } = "";
public string endTime { get; init; } = "";
public string userType { get; init; } = "";
public string device { get; init; } = "";
public string module { get; init; } = "";
public string mode { get; init; } = "";
public string moduleStepCount { get; init; } = "";
public string errors { get; init; } = "";
public string assistance { get; init; } = "";
public int questionCount { get; init; }
public int incorrectCount { get; init; }
public int correctCount { get; init; }
public List<QuestionAnswer> questionAnswers { get; init; } = new();
}
public sealed class QuestionAnswer
{
public string questionStatus { get; init; } = "";
public string question { get; init; } = "";
public string correctAnswers { get; init; } = "";
public string userAnswers { get; init; } = "";
}
汎用ヘルパー:C# の値 → UntypedNode を安全に変換
列が増えるとキャスト記述が煩雑になりがちです。型ごとの分岐を 1 箇所に封じ込めると、可読性と保守性が大幅に上がります。
public static UntypedNode ToUntyped(object? v) => v switch
{
null => new UntypedNull(),
string s => new UntypedString(s),
int i => new UntypedInteger(i),
long l => new UntypedInteger((int)l),
double d => new UntypedDouble(d),
float f => new UntypedDouble(f),
bool b => new UntypedBoolean(b),
DateTime dt => new UntypedString(dt.ToString("o")), // ISO 8601 として文字列で送る例
DateTimeOffset dto => new UntypedString(dto.ToString("o")),
_ => new UntypedString(v.ToString() ?? string.Empty)
};
行ビルダー:列リストを動的に構築して UntypedArray 化
このヘルパーに「Excel の列順」を閉じ込めます。固定列+可変列の合成も 1 箇所で完了します。
public static UntypedArray BuildRow(SessionRow data)
{
var row = new List<UntypedNode>
{
ToUntyped(data.startTime),
ToUntyped(data.endTime),
ToUntyped(data.userType),
ToUntyped(data.device),
ToUntyped(data.module),
ToUntyped(data.mode),
ToUntyped(data.moduleStepCount),
ToUntyped(data.errors),
ToUntyped(data.assistance),
ToUntyped(data.questionCount),
ToUntyped(data.incorrectCount),
ToUntyped(data.correctCount)
};
// 可変長 QA セクション(1 問 4 列を繰り返し)
foreach (var q in data.questionAnswers)
{
row.Add(ToUntyped(q.questionStatus));
row.Add(ToUntyped(q.question));
row.Add(ToUntyped(q.correctAnswers));
row.Add(ToUntyped(q.userAnswers));
}
return new UntypedArray(row);
}
public static UntypedArray BuildRows(IEnumerable<SessionRow> items)
{
var rows = new List<UntypedArray>();
foreach (var item in items)
{
rows.Add(BuildRow(item));
}
return new UntypedArray(rows);
}
Graph クライアントの初期化(概念)
本質ではないため詳細は割愛しますが、アプリやユーザー委任のトークンを用意し、GraphServiceClient を作成します。以降は graphClient を使ってドライブ・アイテム(Excel)・テーブルを辿るだけです。
// IAuthenticationProvider を用意している前提
var graphClient = new GraphServiceClient(authProvider);
1 行追加:最小コード
先ほどの BuildRow を使えば、行追加は非常にシンプルになります。
var rowArray = BuildRow(data); // 1 行 = 列の配列
var rowsArray = new UntypedArray(new[] { rowArray }); // 行の配列(1 行だけ)
var requestBody = new WorkbookTableRow
{
Values = rowsArray
};
await graphClient
.Drives[driveId]
.Items[documentId]
.Workbook
.Tables[tableId]
.Rows
.PostAsync(requestBody);
複数行の一括追加:パフォーマンスを稼ぐ
大量データは まとめて送るのが基本です。1 回で複数行を押し込めば、往復回数が減り、目に見えて速くなります。
var rowsArray = BuildRows(listOfData); // IEnumerable<SessionRow> → 行配列
var requestBody = new WorkbookTableRow { Values = rowsArray };
await graphClient
.Drives[driveId]
.Items[documentId]
.Workbook
.Tables[tableId]
.Rows
.PostAsync(requestBody);
上限サイズと分割送信の目安
- JSON 本文はサイズに上限があります。セル文字列が長くなるとすぐ肥大化するため、数千セルを超える単位になったら分割を検討。
- 実務目安:行数を 100~500 行程度でチャンクし、1 行あたりの列数・文字量に応じて調整。
列数不一致エラーを未然に防ぐチェック
テーブルの列数と送信する列数が合わないと失敗の原因になります。実運用では「テーブルの列数」と「可変部の展開後列数」を整合させるバリデーションを入れておくのが鉄則です。
public static void EnsureColumnCount(SessionRow data, int expectedColumns)
{
var fixedCols = 12;
var variableCols = (data.questionAnswers?.Count ?? 0) * 4;
var total = fixedCols + variableCols;
if (total != expectedColumns)
{
throw new InvalidOperationException(
$"列数が一致しません。送信={total}, テーブル={expectedColumns}");
}
}
数値・日付・null の取り扱いベストプラクティス
| 値の種類 | 推奨 UntypedNode | 注意点 |
|---|---|---|
| 整数・件数 | UntypedInteger | Excel 上で集計・並べ替えを正しく行える。 |
| 小数・割合 | UntypedDouble | 文字列で送らない。数値として扱わせる。 |
| 真偽 | UntypedBoolean | 条件付き書式やフィルタと相性が良い。 |
| 日付・時刻 | UntypedStringで ISO 8601 文字列 | Excel の表示はシート側の数式/表示形式で整える。 |
| null | UntypedNull | 空セルとして投入。未入力の区別が明確。 |
よくある落とし穴と回避策
| 症状 | 原因 | 対策 |
|---|---|---|
| 値を for で増やしたいのに書けない | UntypedArray を直接初期化している | List で組み立て → 最後に new UntypedArray(list) に切り替える。 |
| 数値が文字列として扱われる | すべて UntypedString で送っている | 型に応じて UntypedInteger/Double/Boolean を使い分ける。 |
| 列数不一致エラー | 可変部の展開後列数がテーブル列数と違う | 送信前に列数チェックを入れる。可変部の構成は 1 箇所に集約。 |
| 遅い・タイムアウト | 1 行ずつ逐次呼び出し | 複数行をまとめて送る。サイズ超過回避のためチャンク化も。 |
既存行の更新・削除・挿入(位置指定)
追加だけでなく、行の更新や削除も Untyped の考え方は同じです。「更新=同じ二重配列でセル値を渡す」「削除=インデックス指定」と覚えておくと整理しやすいです。
// 例:行の更新(index は 0 始まりの行番号)
var updateBody = new WorkbookTableRow { Values = BuildRows(new[] { data }) };
await graphClient
.Drives[driveId]
.Items[documentId]
.Workbook
.Tables[tableId]
.Rows[index]
.PatchAsync(updateBody);
// 例:行の削除
await graphClient
.Drives[driveId]
.Items[documentId]
.Workbook
.Tables[tableId]
.Rows[index]
.DeleteAsync();
// 例:先頭に挿入(Rows.Add のパターンが使える場合)
var body = new WorkbookTableRow { Values = BuildRows(new[] { data }) };
await graphClient
.Drives[driveId]
.Items[documentId]
.Workbook
.Tables[tableId]
.Rows
.PostAsync(body); // SDK/環境によっては Add 関数呼び出しで index 指定も可能
堅牢化テクニック:例外処理・リトライ・スロットリング
大規模投入や定期バッチでは、ネットワークや一時的なスロットリングで失敗することがあります。指数バックオフのリトライと、エラー詳細のロギングは必須です。
try
{
await graphClient
.Drives[driveId]
.Items[documentId]
.Workbook
.Tables[tableId]
.Rows
.PostAsync(new WorkbookTableRow { Values = BuildRows(batch) });
}
catch (ApiException ex)
{
// 例:429/503 を判定して Retry-After を尊重してリトライする
// ex.ResponseStatusCode, ex.Message, ex.StackTrace をログへ
throw;
}
品質を担保するためのユニットテスト観点
- 列順序の保証:固定列 → 可変列(4N 列)が期待どおりに連結されること。
- 型マッピング:整数・小数・真偽・null・日時の各ケースが正しく
UntypedNode化されること。 - 列数検証:可変部の N に応じて期待列数が一致すること。
- 大規模データ:1,000 行規模を想定し、バッチ分割の境界条件を確認。
[Fact]
public void BuildRow_QaTwoItems_ShouldExpandBy8Columns()
{
var row = new SessionRow
{
startTime = "2025-01-01T09:00:00Z",
endTime = "2025-01-01T10:00:00Z",
userType = "Student",
device = "PC",
module = "Algebra",
mode = "Practice",
moduleStepCount = "10",
errors = "2",
assistance = "1",
questionCount = 10,
incorrectCount = 2,
correctCount = 8,
questionAnswers =
{
new() { questionStatus="OK", question="Q1", correctAnswers="A", userAnswers="A" },
new() { questionStatus="NG", question="Q2", correctAnswers="B", userAnswers="C" }
}
};
var rowArray = BuildRow(row);
// 12 固定 + 4*2
var expected = 20;
Assert.Equal(expected, rowArray.GetCount()); // GetCount は概念。実際は列数を検査する補助を用意
}
設計パターン:列スキーマが変わる未来に備える
要件変更で「QA セクションの列が 5 列になった」「別の可変ブロックが増えた」などは日常茶飯事。影響を 1 ファイルに閉じ込められるように、列スキーマは RowBuilder に一元化しましょう。
public interface IRowSection
{
IEnumerable<UntypedNode> ToColumns();
}
public sealed class FixedSection : IRowSection
{
private readonly SessionRow _d;
public FixedSection(SessionRow d) => _d = d;
public IEnumerable<UntypedNode> ToColumns()
{
yield return ToUntyped(_d.startTime);
yield return ToUntyped(_d.endTime);
// ... 固定 12 列分
}
}
public sealed class QaSection : IRowSection
{
private readonly IEnumerable<QuestionAnswer> _qa;
public QaSection(IEnumerable<QuestionAnswer> qa) => _qa = qa;
public IEnumerable<UntypedNode> ToColumns()
{
foreach (var q in _qa)
{
yield return ToUntyped(q.questionStatus);
yield return ToUntyped(q.question);
yield return ToUntyped(q.correctAnswers);
yield return ToUntyped(q.userAnswers);
}
}
}
public static UntypedArray BuildRowV2(SessionRow data)
{
var sections = new IRowSection[]
{
new FixedSection(data),
new QaSection(data.questionAnswers)
};
var list = new List<UntypedNode>();
foreach (var s in sections)
list.AddRange(s.ToColumns());
return new UntypedArray(list);
}
運用のコツ:テーブル設計・数式・表示形式
- 列名は固定:ダッシュボードやピボットを組む想定なら、列の意味(位置)を変えない。
- 数式列は Excel 側で定義:投入は原始データに限定し、計算はテーブル列数式に任せると責務分離が明確。
- 表示形式はシートで設定:日付・数値の書式はセル側で定め、API 側は正しい型で渡すことに集中。
セキュリティ・権限と監査
- 最小権限:ファイル単位の書き込みが必要。アプリ/委任どちらでも、原則「読むだけ権限」を混ぜない。
- 監査ログ:誰が・いつ・どの行を投入したか、呼び出し側アプリに記録を残す。
- 入力検証:Excel に渡す前に、長大文字列・制御文字・列数超過をチェック。
実務テンプレート:そのまま差し替えて使えるメソッド
データ列の定義だけ差し替えれば、そのまま現場で使える汎用テンプレートです。
public static class ExcelTableWriter
{
private readonly static int ChunkSize = 300; // 例:サイズ目安に応じて調整
public static async Task AddRowsAsync(GraphServiceClient graphClient,
string driveId, string itemId, string tableId, IEnumerable<SessionRow> rows,
CancellationToken ct = default)
{
foreach (var chunk in rows.Chunk(ChunkSize))
{
var values = BuildRows(chunk);
var body = new WorkbookTableRow { Values = values };
await graphClient
.Drives[driveId]
.Items[itemId]
.Workbook
.Tables[tableId]
.Rows
.PostAsync(body, cancellationToken: ct);
}
}
}
ミニ FAQ
Q. 列の途中(例:D~H)だけを更新したい
A. 行追加 API は 1 行全体の値を期待します。部分更新が必要なら、既存行を読み出し、更新したい列だけ差し替えてから二重配列で送り返すのが確実です。
Q. 文字列の前後のゼロや記号が消える
A. 数値に見えるが文字列で扱いたい値は、UntypedString で渡し、シート側の列表示形式を「文字列」に設定しておくと安全です。
Q. 1 回の投入で数万行扱いたい
A. 実際には数千行ごとに分割し、API 制限とサイズ上限を避けつつリトライ制御を入れる運用が安定します。
この記事の要点(おさらい)
- UntypedArray は “固定長”。動的構築は List で作ってから変換。
- 外側=行、内側=列、セル=UntypedNode のレイヤを崩さない。
- 型に応じた UntypedNode を使い分け、Excel での集計・並べ替えを正しく効かせる。
- バルク投入+チャンクで高速・安定化。列数検証で事故を防ぐ。
- スキーマは RowBuilder に一元化し、要件変更に強い構成に。
完全版サンプル:固定+可変列を綺麗に積み上げて投入
最後に、クリーンアーキテクチャ寄りの整理を施した、読みやすい完全版を示します。
// ---------- Domain ----------
public sealed record SessionRow(
string StartTime,
string EndTime,
string UserType,
string Device,
string Module,
string Mode,
string ModuleStepCount,
string Errors,
string Assistance,
int QuestionCount,
int IncorrectCount,
int CorrectCount,
IReadOnlyList<QuestionAnswer> QA);
public sealed record QuestionAnswer(
string QuestionStatus, string Question, string CorrectAnswers, string UserAnswers);
// ---------- Infra: Untyped Helpers ----------
public static class Untyped
{
public static UntypedNode Of(object? v) => v switch
{
null => new UntypedNull(),
string s => new UntypedString(s),
int i => new UntypedInteger(i),
long l => new UntypedInteger((int)l),
double d => new UntypedDouble(d),
float f => new UntypedDouble(f),
bool b => new UntypedBoolean(b),
DateTime dt => new UntypedString(dt.ToString("o")),
DateTimeOffset dto => new UntypedString(dto.ToString("o")),
_ => new UntypedString(v.ToString() ?? string.Empty)
};
}
// ---------- Infra: Row Builder ----------
public static class RowBuilder
{
public static UntypedArray Build(SessionRow d)
{
var list = new List<UntypedNode>(capacity: 12 + (d.QA?.Count ?? 0) * 4)
{
Untyped.Of(d.StartTime),
Untyped.Of(d.EndTime),
Untyped.Of(d.UserType),
Untyped.Of(d.Device),
Untyped.Of(d.Module),
Untyped.Of(d.Mode),
Untyped.Of(d.ModuleStepCount),
Untyped.Of(d.Errors),
Untyped.Of(d.Assistance),
Untyped.Of(d.QuestionCount),
Untyped.Of(d.IncorrectCount),
Untyped.Of(d.CorrectCount)
};
if (d.QA is not null)
{
foreach (var q in d.QA)
{
list.Add(Untyped.Of(q.QuestionStatus));
list.Add(Untyped.Of(q.Question));
list.Add(Untyped.Of(q.CorrectAnswers));
list.Add(Untyped.Of(q.UserAnswers));
}
}
return new UntypedArray(list);
}
public static UntypedArray BuildMany(IEnumerable<SessionRow> rows)
=> new(new List<UntypedArray>(rows.Select(Build)));
}
// ---------- Application Service ----------
public sealed class ExcelSessionWriter
{
private readonly GraphServiceClient _graph;
private readonly string _driveId;
private readonly string _itemId;
private readonly string _tableId;
public ExcelSessionWriter(GraphServiceClient graph, string driveId, string itemId, string tableId)
=> (_graph, _driveId, _itemId, _tableId) = (graph, driveId, itemId, tableId);
public async Task AppendAsync(IEnumerable<SessionRow> rows, CancellationToken ct = default)
{
foreach (var batch in rows.Chunk(400)) // 実データ量に応じて調整
{
var values = RowBuilder.BuildMany(batch);
var body = new WorkbookTableRow { Values = values };
await _graph
.Drives[_driveId]
.Items[_itemId]
.Workbook
.Tables[_tableId]
.Rows
.PostAsync(body, cancellationToken: ct);
}
}
}
まとめ
「List で作って最後に UntypedArray」——たったこれだけで、C# から Graph API へ Excel の二重配列を安全・簡潔・高性能に渡せます。列数・行数が可変でもコードが膨らまず、型も崩れません。RowBuilder にスキーマを閉じ込め、数式や表示は Excel に委ねる。これが、データ投入の品質と開発速度を最大化する王道パターンです。今日から現場の投入コードをこの形に刷新し、テストとバッチ分割で運用まで一気通貫の “強い” 実装に仕上げましょう。

コメント