モダナイゼーションの進め方
ゼロベースで設計する理想のアーキテクチャ
現行596本のプログラム(VB6+COBOL, 約50万行)を「もしゼロから作り直すなら」どう設計すべきか。 技術選定の根拠とともに、具体的な実装パターンを示す。
1. 現行システムの本質を捉え直す
1.1 596本のプログラムは何をしているのか
596本は5つの処理パターン(A: EDI変換200本、B: DB間変換150本、C: バッチ50本、D: 帳票60本、E: マスタ画面~30本)に集約される。詳細は final-report.md セクション1-1を参照。
596本が必要な理由は「連携先 × データ種別」の掛け算。 低圧需要家だけで10+フォーマット、多数の連携先で40+フォーマット。 各フォーマットに個別プログラムを書いているのが複雑さの根源。
1.2 理想はどうなるべきか
現状
連携先ごとに個別プログラム
連携先A用.exe + 連携先B用.exe + ... = 200本
理想
汎用エンジン + フォーマット定義
汎用エンジン(1本) + フォーマット定義ファイル(40個) = 41ファイル
処理パターンが5つなら、エンジンも5つあれば十分。 596本 → 5エンジン + 設定ファイル群 に圧縮できる。
1.3 実データから判明した共通パターン
実際のVB6ソース(VKNAJ0B, VKNAR0B, VKNCA0B)を分析した結果、 EDI受信プログラムは全て同じフレームワーク構造を持つことが判明。
図を読み込み中…
9ステップ中8ステップが共通。連携先固有なのはステップ6だけ。 そしてステップ6も「固定長レコードをパースしてDBに書く」という構造は同じで、 異なるのは「レコード長」「フィールド位置」「書込先テーブル」だけ。
2. 理想のアーキテクチャ: 3つのパターン比較
パターン A: Configuration-Driven(スキーマ駆動)
最推奨。フォーマット定義をJSONファイルに外出しし、汎用エンジンで処理。
図を読み込み中…
フォーマット定義(JSON/YAML) — formats/
- mdms-lp30min.json(低圧30分値, 250byte)
- mdms-hpfix.json(高圧確定検針, 138byte)
- switching-swreq.json(小売スイッチング, 124byte)
- takuso-tsedi.json(託送事業者, 可変)
- ... (40+ファイル)
各ファイルに含まれる情報:
- レコード長、エンコーディング
- フィールド定義(名前、位置、長さ、型)
- ヘッダ行の解析ルール
- 書込先テーブル、カラムマッピング
- バリデーションルール
- 前回/今回区分のCRUD分岐ルール
汎用パーサーエンジン(C#, 1本) の処理:
- FixedLengthReader: レコード読込(encoding対応)
- FieldExtractor: フィールド切り出し(position+length)
- TypeConverter: 型変換(string→decimal, date等)
- Validator: バリデーション(事業者チェック等)
- DbMapper: テーブル・カラムマッピング
- UpsertExecutor: INSERT/UPDATE/DELETE実行
フォーマット定義ファイルの具体例:
{
"formatId": "MDMSHPFX",
"description": "高圧確定検針データ",
"encoding": "shift_jis",
"recordLength": 138,
"headerRules": [
{
"lineNumber": 3,
"fields": [
{ "name": "shoriY", "position": 126, "length": 4, "type": "string" },
{ "name": "shoriM", "position": 131, "length": 2, "type": "string" },
{ "name": "shoriD", "position": 134, "length": 2, "type": "string" }
]
},
{
"lineNumber": 5,
"fields": [
{ "name": "jigyosha", "position": 8, "length": 4, "type": "string" },
{ "name": "kyoten", "position": 23, "length": 2, "type": "string" }
],
"validations": [
{
"field": "jigyosha",
"rule": "equals",
"value": "2903",
"errorMessage": "事業者コードが2903ではない"
}
]
}
],
"dataStartLine": 11,
"fields": [
{
"name": "tenno1",
"position": 0,
"length": 5,
"type": "string",
"inheritFromPrevious": true
},
{
"name": "tenno2",
"position": 5,
"length": 5,
"type": "string",
"inheritFromPrevious": true
},
{ "name": "tariff", "position": 12, "length": 2, "type": "string" },
{ "name": "kakou", "position": 21, "length": 1, "type": "string" },
{ "name": "ctlno", "position": 30, "length": 7, "type": "string" },
{ "name": "motik", "position": 39, "length": 5, "type": "string" },
{ "name": "factr", "position": 46, "length": 2, "type": "string" },
{ "name": "ukeir", "position": 55, "length": 2, "type": "string" },
{ "name": "kubun", "position": 60, "length": 4, "type": "string" },
{ "name": "efun2", "position": 73, "length": 5, "type": "string" },
{ "name": "efpid", "position": 82, "length": 1, "type": "string" },
{ "name": "prcid", "position": 88, "length": 1, "type": "string" },
{ "name": "price", "position": 91, "length": 12, "type": "decimal" },
{ "name": "efuno", "position": 103, "length": 5, "type": "string" },
{ "name": "efstry", "position": 114, "length": 4, "type": "string" },
{ "name": "efstrm", "position": 118, "length": 2, "type": "string" },
{ "name": "efstrd", "position": 120, "length": 2, "type": "string" },
{ "name": "efendy", "position": 125, "length": 4, "type": "string" },
{ "name": "efendm", "position": 129, "length": 2, "type": "string" },
{ "name": "efendd", "position": 131, "length": 2, "type": "string" }
],
"computedFields": [
{
"name": "tenno",
"expression": "{tenno1}-{tenno2}{tariff:ifNotBlank('-')}{tariff}"
},
{
"name": "efstr",
"expression": "{efstry}{efstrm}{efstrd}",
"type": "long"
}
],
"targetTable": "HN050",
"keyColumns": ["cstno", "tenno", "ctlno", "efstr"],
"staticValues": {
"cstno": "01 ",
"chota": "1",
"usrid": "VKNDD0B "
},
"branchField": "kubun",
"branchRules": [
{ "value": "前回", "action": "DELETE" },
{ "value": "今回", "action": "UPSERT" }
]
}
メリット:
- 200本のEDI変換プログラム → 1エンジン + 40定義ファイル
- 新フォーマット追加 = JSONファイル追加(コード変更不要)
- テスト = フォーマットごとのサンプルデータ + 期待結果のデータ駆動テスト
- 定義ファイルはバージョン管理でき、差分も見やすい
デメリット:
- 複雑な業務ロジック(VKNAR0Bの帳票3種生成など)はJSONだけでは表現しきれない
- 定義ファイルのスキーマ設計自体が難しい(網羅性 vs 簡潔さのバランス)
- → 対策: 定義ファイルで80%をカバーし、残り20%はC#のカスタムハンドラで対応
限界の誠実な評価: 全200本がJSON定義だけで完結するわけではない。実際のプログラムを分析すると:
- パターン1(RN920中継型): ~60% → JSON定義のみで完結
- パターン2(直接DB書込型): ~30% → JSON定義 + カスタムロジック
- パターン3(複雑業務ロジック型): ~10% → C#カスタムハンドラ必須
パターン B: Plugin Architecture(プラグイン方式)
各連携先/フォーマットをC#のクラスとして実装するが、共通インターフェースで統一。
// 共通インターフェース
public interface IEdiProcessor
{
string FormatId { get; } // "MDMSHPFX"
Encoding FileEncoding { get; } // Shift_JIS
Task<ProcessResult> ProcessAsync(Stream file, CancellationToken ct);
}
// 高圧確定検針単価用の実装
public class MdmsHpfxProcessor : EdiProcessorBase, IEdiProcessor
{
public string FormatId => "MDMSHPFX";
public Encoding FileEncoding => Encoding.GetEncoding("shift_jis");
protected override int RecordLength => 138;
protected override int DataStartLine => 11;
protected override void ParseHeader(int lineNo, string line)
{
if (lineNo == 3) ParseShori(line);
if (lineNo == 5) ParseJigyosha(line);
}
protected override ParsedRecord ParseDataLine(string line)
{
return new ParsedRecord
{
["tenno1"] = line.Substring(0, 5).Trim(),
["tenno2"] = line.Substring(5, 5).Trim(),
["tariff"] = line.Substring(12, 2).Trim(),
// ...
};
}
protected override string GetTargetTable() => "HN050";
protected override string[] GetKeyColumns()
=> new[] { "cstno", "tenno", "ctlno", "efstr" };
}
// DI(Dependency Injection)で自動登録
services.AddEdiProcessors(typeof(MdmsHpfxProcessor).Assembly);
// 実行時にFormatIdでルーティング
var processor = processorFactory.GetProcessor("MDMSHPFX");
await processor.ProcessAsync(fileStream, ct);
メリット:
- C#の型安全性・IDE支援・リファクタリングが使える
- 複雑な業務ロジックもそのまま書ける
- テストが書きやすい(xUnit + InMemory DB)
デメリット:
- 40+クラスが必要(JSONよりはましだが、まだ多い)
- フォーマット追加ごとにコンパイル・デプロイが必要
パターン C: Hybrid(推奨構成)
Configuration-Driven(パターンA)を基本とし、 複雑なケースはPlugin(パターンB)で対応する。
単純なもの(60%)
JSON定義ファイルのみ
汎用エンジンが処理
やや複雑(30%)
JSON定義 + カスタムバリデータ/トランスフォーマ
JSON定義でパース、C#クラスで業務ロジック
複雑(10%)
C#プラグインクラス
IEdiProcessor実装
フォーマットを複雑度で3層に振り分ける。
// JSON定義を読み込む汎用エンジン
public class ConfigDrivenEdiEngine
{
public async Task<ProcessResult> ProcessAsync(
string formatId, Stream file, CancellationToken ct)
{
var schema = _schemaStore.Load(formatId);
// 1. 汎用パース
var records = _parser.Parse(file, schema);
// 2. カスタムハンドラがあれば実行(オプション)
if (_customHandlers.TryGet(formatId, out var handler))
{
records = await handler.TransformAsync(records);
}
// 3. DB書込
await _dbWriter.WriteAsync(schema.TargetTable, records, schema, ct);
return result;
}
}
// カスタムハンドラ(必要なフォーマットのみ実装)
public class MdmsHpfxCustomHandler : ICustomHandler
{
public string FormatId => "MDMSHPFX";
public Task<List<ParsedRecord>> TransformAsync(List<ParsedRecord> records)
{
// 供給地点番号の空白引き継ぎ、グループ化、KWH2設定などの業務ロジック
return Task.FromResult(ApplyBusinessRules(records));
}
}
3. 全体アーキテクチャ設計
3.1 アーキテクチャスタイル: Modular Monolith
マイクロサービスは3-5人チームにはオーバーヘッドが大きすぎる。 Modular Monolith(モジュラーモノリス)で開始し、将来必要になれば分離可能にする。
power-app/
src/
Power.Core/ # 共通ドメインモデル・インターフェース
Domain/ # Entity, ValueObject
Abstractions/ # IEdiProcessor, IJobQueue 等
Power.EdiIntegration/ # モジュール1: EDI受信・変換
Engine/ # 汎用パーサーエンジン
Schemas/ # JSON定義ファイル(40+)
CustomHandlers/ # 連携先固有ロジック
Receivers/ # スマメ/EDI受信アダプタ
Power.ContractManagement/ # モジュール2: 契約管理
Features/
ReceiveMeter/ # 契約取込
MergeContract/ # 契約統合(CRY819X相当)
MonthlyReset/ # 月初リセット(CRY413相当)
Data/ # KY110, KY130, KY400
Power.GridExpansion/ # モジュール3: 系統展開
Features/
GridExpansion/ # 系統展開(CGR911/912相当)
LoadAggregation/ # 需要集約計算(CJK120-140相当)
PlanGeneration/ # 計画生成(CJK211-217相当)
Data/ # CH010, CH5D0
Power.WorkManagement/ # モジュール4: 工事管理
Features/
DemandResponseCalc/ # 需給調整計算(CSBK系相当)
WorkOrder/ # 工事処理(CSB系相当)
Data/ # SB030, SB120
Power.Reporting/ # モジュール5: 帳票
Reports/ # QuestPDFテンプレート
Power.MasterMaintenance/ # モジュール6: マスタメンテ
Pages/ # Blazor Server画面
Power.Jobs/ # バッチジョブオーケストレーション
Chains/ # BATファイル相当のジョブチェーン定義
Power.Infrastructure/ # インフラ層
Data/ # EF Core DbContext, Migrations
Odbc/ # レガシーDB接続(移行期間用)
FileSystem/ # ファイル入出力
tests/
Power.EdiIntegration.Tests/ # サンプルデータ駆動テスト
Power.ContractManagement.Tests/
...
formats/ # EDIフォーマット定義(JSON)
mdms-lp30min.json
mdms-hpfix.json
...
3.2 モジュール間の依存関係
図を読み込み中…
モジュール間の通信:
- 同期: 直接メソッド呼び出し(同一プロセス内)
- 非同期: イベント発行(将来のマイクロサービス分離に備える)
- ジョブチェーン: Hangfireで順次実行(現行BATの置き換え)
3.3 RN920の置き換え: ジョブキュー
現行で最も依存度が高いテーブル(68プログラムからアクセス)。 EDI受信のジョブキュー/ステータス管理として機能している。
現行
RN920テーブル
NXSTS: 0→1→2→0 でジョブ状態管理
理想
Hangfire + 専用ジョブステータスDB
// 現行BATの置き換え: 低圧30分値データ処理チェーン
public class LowVoltMeterJobChain
{
public string Setup(IBackgroundJobClient jobs)
{
var step1 = jobs.Enqueue<MeterSortJob>(j => j.Execute("lv-meter30"));
var step2 = jobs.ContinueJobWith<MeterImportJob>(step1,
j => j.Execute("lv-meter30", "MDMSLP30"));
var step3 = jobs.ContinueJobWith<ContractMergeJob>(step2,
j => j.Execute());
var step4 = jobs.ContinueJobWith<MasterCheckJob>(step3,
j => j.Execute());
var step5 = jobs.ContinueJobWith<MonthlyResetJob>(step4,
j => j.Execute());
return step1; // ジョブチェーンのルートID
}
}
4. 技術選定
4.1 推奨技術スタック
| レイヤー | 技術 | 選定理由 |
|---|---|---|
| 言語 | C# / .NET 8 | 既定方針。VB6からの移行先として最適 |
| ORM | Entity Framework Core | VB6の手書きSQL→型安全なLINQ |
| 固定長パーサー | 自作 Configuration-Driven Engine | 日本独自フォーマットは既存ライブラリではカバー不可 |
| バッチ処理 | Hangfire Pro | .NETネイティブ、Web UI付き、ジョブチェーン対応。現行BATの自然な置き換え |
| 帳票生成 | QuestPDF + CsvHelper | コードファーストでソース管理可能。COBOL帳票→PDF品質向上 |
| GUI | Blazor Server + Radzen Components | C#一本で完結、VB6のイベント駆動モデルに近い |
| DB | SQL Server(統一) | DB2+Oracle混在→単一DBMSに統合。EF Coreとの相性最良 |
| ログ | Serilog + Seq | 構造化ログ。VZY110.bas/EXLOG.CBLの統合置き換え |
| テスト | xUnit + Verify | Verifyでゴールデンマスターテスト(スナップショットテスト) |
| DI | Microsoft.Extensions.DependencyInjection | 標準。ExeOdbc.basの104コピー問題を根本解決 |
4.2 不採用とした技術と理由
| 技術 | 不採用理由 |
|---|---|
| MuleSoft / Dell Boomi | 日本独自EDIフォーマット未対応。年間数百万円のライセンス |
| Apache Camel + Smooks | Java/JVM。C#方針と不整合 |
| DFDL (Apache Daffodil) | 学習コスト高。40フォーマット程度ではオーバースペック |
| React + ASP.NET API | フロント/バック分離は3-5人チームには負担。C#一本のBlazorの方が効率的 |
| マイクロサービス | 運用負荷が高すぎる。Modular Monolithで開始すべき |
| MediatR (CQRS) | 2025年に商用化。自前Pipelineで十分 |
| Clean Architecture | ファイル爆発が3-5人チームには負担。Vertical Slice Architectureの方が実践的 |
| Airflow / Dagster | Python必須で技術スタックが分裂。Hangfireの方が.NETネイティブ |
| MAUI | デスクトップアプリ。Web化の恩恵(リモートアクセス、デプロイ簡素化)がない |
4.3 ライセンスコスト
| 技術 | コスト |
|---|---|
| .NET 8 | 無料(MIT) |
| Entity Framework Core | 無料(MIT) |
| Hangfire Pro | ~$500/年 |
| QuestPDF | 無料(Community版)or $699/年(Professional版) |
| Radzen Blazor Studio | $699-$1,299/年(初期フェーズのみ。CRUD画面自動生成に使用) |
| CsvHelper | 無料(MS-PL/Apache 2.0) |
| Serilog | 無料(Apache 2.0) |
| 合計 | $1,200〜$2,500/年 |
5. 具体的な実装例
5.1 汎用固定長パーサーエンジン
// フォーマット定義をJSON/YAMLから読み込み、汎用的にパースするエンジン
public class FixedLengthParserEngine
{
private readonly ISchemaStore _schemas;
public async IAsyncEnumerable<ParsedRecord> ParseAsync(
string formatId, Stream stream,
[EnumeratorCancellation] CancellationToken ct = default)
{
var schema = _schemas.Load(formatId);
var encoding = Encoding.GetEncoding(schema.Encoding);
using var reader = new BinaryReader(stream, encoding, leaveOpen: true);
int lineNo = 0;
var headerData = new Dictionary<string, object>();
string? prevItmno1 = null, prevItmno2 = null;
while (stream.Position < stream.Length)
{
ct.ThrowIfCancellationRequested();
var bytes = reader.ReadBytes(schema.RecordLength);
if (bytes.Length < schema.RecordLength) yield break;
lineNo++;
var line = encoding.GetString(bytes);
// ヘッダ行の処理
var headerRule = schema.HeaderRules
.FirstOrDefault(h => h.LineNumber == lineNo);
if (headerRule != null)
{
foreach (var field in headerRule.Fields)
headerData[field.Name] = ExtractField(line, field);
ValidateHeader(headerRule, headerData);
continue;
}
// データ開始行より前はスキップ
if (lineNo < schema.DataStartLine) continue;
// データ行のパース
var record = new ParsedRecord();
foreach (var field in schema.Fields)
{
var value = ExtractField(line, field);
// 空白時の前レコード引き継ぎ
if (field.InheritFromPrevious
&& string.IsNullOrWhiteSpace(value as string))
{
value = record.GetPrevious(field.Name);
}
record[field.Name] = value;
}
// ヘッダ情報をレコードにマージ
foreach (var (key, val) in headerData)
record[key] = val;
// 計算フィールド
foreach (var computed in schema.ComputedFields)
record[computed.Name] = EvaluateExpression(computed, record);
yield return record;
}
}
private object ExtractField(string line, FieldDefinition field)
{
if (field.Position >= line.Length) return field.DefaultValue;
var length = Math.Min(field.Length, line.Length - field.Position);
var raw = line.Substring(field.Position, length);
return field.Type switch
{
"string" => raw.Trim(),
"decimal" => decimal.TryParse(raw.Trim(), out var d) ? d : 0m,
"long" => long.TryParse(raw.Trim(), out var l) ? l : 0L,
"date" => ParseDate(raw, field.Format),
_ => raw.Trim()
};
}
}
5.2 バッチジョブチェーン(BATファイルの置き換え)
// 現行: CHP03TPB.BAT(10本のプログラムを順次実行)
// 理想: Hangfireのジョブチェーンで型安全に定義
public static class LowVoltMeterChain
{
// BATファイル1本 → C#メソッド1本
public static string Register(IBackgroundJobClient jobs, string yearMonth)
{
// CRY125S → CRY125 → VRY811B → CRY819X → CRY301X
// → CHN04A → CHN041 → CRY135 → CRY411X → CRY413
var j1 = jobs.Enqueue<MeterSortJob>(
j => j.Execute(yearMonth));
var j2 = jobs.ContinueJobWith<MeterImportJob>(j1,
j => j.Execute(yearMonth));
var j3 = jobs.ContinueJobWith<MeterFileJob>(j2,
j => j.Execute());
var j4 = jobs.ContinueJobWith<ContractMergeJob>(j3,
j => j.Execute());
var j5 = jobs.ContinueJobWith<UnregisteredMasterCheckJob>(j4,
j => j.Execute());
var j6 = jobs.ContinueJobWith<ControlNumberWorkJob>(j5,
j => j.Execute());
var j7 = jobs.ContinueJobWith<ControlNumberRegisterJob>(j6,
j => j.Execute());
var j8 = jobs.ContinueJobWith<MeterDataListJob>(j7,
j => j.Execute());
var j9 = jobs.ContinueJobWith<MeterFileOutJob>(j8,
j => j.Execute());
var j10 = jobs.ContinueJobWith<MonthlyResetJob>(j9,
j => j.Execute());
return j1;
}
}
5.3 系統展開(Low-Level Code法)
// 現行: CGR911 + CGR912 + CJK120 + CJK130 + CJK140(5本のCOBOLプログラム)
// 理想: 1つのサービスクラス
public class GridLoadService
{
private readonly AppDbContext _db;
/// <summary>
/// Low-Level Code法による系統展開。
/// レベル0(完成品)から順に需要量を集約し、
/// 同一設備が複数の親に使われる場合の二重計算を防ぐ。
/// </summary>
public async Task<ExpansionResult> ExpandAsync(
string yearMonth, CancellationToken ct)
{
// 1. 系統ツリーからLow-Level Codeを事前計算
// (CGR911:ゼロクリア + CGR912:自動設定 に相当)
var items = await _db.Sb020
.OrderBy(b => b.LowLevelCode)
.ToListAsync(ct);
// 2. 契約データ(設備管理)からグロス需要量を算出
// (CJK120:アクティビティチェーン に相当)
var grossRequirements = await CalculateGrossRequirements(yearMonth, ct);
// 3. レベル順に展開
// (CJK130:需要LOAD + CJK140:需要集約 に相当)
var netRequirements = new Dictionary<string, decimal>();
foreach (var level in items.GroupBy(i => i.LowLevelCode)
.OrderBy(g => g.Key))
{
foreach (var item in level)
{
var gross = grossRequirements.GetValueOrDefault(item.PointNo, 0m);
var onHand = item.OnHandQty;
var net = Math.Max(0, gross - onHand);
netRequirements[item.PointNo] = net;
// 下位設備に需要量を集約
var children = await _db.Sb020
.Where(b => b.ParentPointNo == item.PointNo)
.ToListAsync(ct);
foreach (var child in children)
{
var childGross = net * child.RequiredQty;
grossRequirements[child.PointNo] =
grossRequirements.GetValueOrDefault(child.PointNo, 0m)
+ childGross;
}
}
}
return new ExpansionResult(netRequirements);
}
}
5.4 マスタメンテ画面(Blazor Server + Radzen)
// 現行: VB6 Form × 30本
// 理想: Blazor Server + 汎用CRUDコンポーネント
// 177テーブル全てに専用画面を作るのではなく:
// ・主要20テーブル → カスタム画面
// ・残り157テーブル → 汎用DataGrid画面
@page "/master/sl050"
@inject AppDbContext Db
<PageTitle>HN050 料金単価マスタ</PageTitle>
<RadzenDataGrid @ref="grid" Data="@records" TItem="Hn050"
AllowFiltering="true" AllowSorting="true" AllowPaging="true"
PageSize="20" EditMode="DataGridEditMode.Single"
RowCreate="OnCreate" RowUpdate="OnUpdate">
<Columns>
<RadzenDataGridColumn Property="Cstno" Title="連携先" Width="80px" />
<RadzenDataGridColumn Property="Itmno" Title="供給地点番号" Width="160px" />
<RadzenDataGridColumn Property="Ctlno" Title="管理番号" Width="120px" />
<RadzenDataGridColumn Property="Efstr" Title="適用開始" Width="100px"
FormatString="{0:yyyyMMdd}" />
<RadzenDataGridColumn Property="Kwh1" Title="価格1" Width="100px"
FormatString="{0:N2}" TextAlign="TextAlign.Right" />
<RadzenDataGridColumn Property="Kwh2" Title="価格2" Width="100px"
FormatString="{0:N2}" TextAlign="TextAlign.Right" />
</Columns>
</RadzenDataGrid>
6. 新しい連携先・フォーマットが増えた場合
6.1 Configuration-Drivenの場合(推奨)
formats/newcustomer-format01.jsonを作成(30分〜2時間)- サンプルデータでテスト(1時間)
- デプロイ(設定ファイルの追加のみ、コード変更なし)
合計: 半日〜1日(現行は新プログラム開発で1-2週間)
6.2 将来のJSON/XML/API対応
フォーマット定義にパーサータイプを追加するだけ:
{
"formatId": "NEW_API_FORMAT",
"parserType": "json",
"jsonPath": "$.records[*]",
"fieldMappings": [
{ "source": "partNumber", "target": "tenno" },
{ "source": "quantity", "target": "qty" }
],
"targetTable": "KY110"
}
汎用エンジンに JsonParser, XmlParser, CsvParser を追加するだけで、 固定長以外のフォーマットにも対応可能。
7. 移行の現実的なロードマップ
ゼロから作り直すのではなく、Strangler Fig パターンで段階的に移行する。
Phase 0: 基盤構築(0-3ヶ月)
- Modular Monolithのプロジェクト構造を作成
- EF Core + SQL Server の基盤
- 汎用固定長パーサーエンジンの実装
- ExeOdbc.bas → EF Core への置き換え層
Phase 1: EDI受信の統合(3-9ヶ月)
- 40+フォーマットのJSON定義ファイル作成
- 単純なフォーマット(RN920中継型、~60%)から先に移行
- ゴールデンマスターテストで新旧の出力を比較
Phase 2: バッチ処理の移行(6-15ヶ月)
- BATファイル → Hangfireジョブチェーン
- 系統展開の再実装(5本のCOBOL → 1つのC#サービス)
- 帳票生成の移行(QuestPDF)
Phase 3: GUI移行(12-18ヶ月)
- マスタメンテ画面のBlazer Server化
- 主要20テーブル:カスタム画面
- その他:汎用DataGrid
8. まとめ: 596本 → どうなるか
| 現状 | 理想 | 削減率 |
|---|---|---|
| EDI変換 ~200本 | 1エンジン + 40 JSON定義 + ~20カスタムハンドラ | ~70%削減 |
| DB間変換 ~150本 | ~30サービスクラス(共通パイプライン上) | ~80%削減 |
| 一括バッチ ~50本 | ~15ジョブクラス | ~70%削減 |
| 帳票 ~60本 | ~20テンプレート(QuestPDF) | ~67%削減 |
| マスタ画面 ~30本 | ~20 Blazor画面 + 汎用DataGrid | ~33%削減 |
| 共通モジュール ~100コピー | DI + NuGet(コピー0) | 100%削減 |
| 合計 約596本(ユニーク) | ~125ファイル | ~80%削減 |
コード行数の見込み:
- 現行: 約50万行(VB6 + COBOL)
- 理想: ~50,000行(C#)+ ~40 JSON定義ファイル
- 削減率: ~85%
行数の大幅削減は「言語の表現力の差」「フレームワークの活用」「共通化」の3要因による。
関連ドキュメント
| ドキュメント | 内容 |
|---|---|
| システム概要 | プロジェクト全体像・業務概要 |
| 用語集 | 用語集 |
| 実行プレイブック | モダナイゼーション実行計画(Strangler Fig パターン) |
| AI駆動モダナイゼーション戦略 | AI駆動モダナイゼーション戦略 |