モダナイゼーションの進め方

ゼロベースで設計する理想のアーキテクチャ

現行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本) の処理:

  1. FixedLengthReader: レコード読込(encoding対応)
  2. FieldExtractor: フィールド切り出し(position+length)
  3. TypeConverter: 型変換(string→decimal, date等)
  4. Validator: バリデーション(事業者チェック等)
  5. DbMapper: テーブル・カラムマッピング
  6. 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" }
  ]
}
json

メリット:

  • 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);
csharp

メリット:

  • 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));
    }
}
csharp

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
    }
}
csharp

4. 技術選定

4.1 推奨技術スタック

レイヤー技術選定理由
言語C# / .NET 8既定方針。VB6からの移行先として最適
ORMEntity Framework CoreVB6の手書きSQL→型安全なLINQ
固定長パーサー自作 Configuration-Driven Engine日本独自フォーマットは既存ライブラリではカバー不可
バッチ処理Hangfire Pro.NETネイティブ、Web UI付き、ジョブチェーン対応。現行BATの自然な置き換え
帳票生成QuestPDF + CsvHelperコードファーストでソース管理可能。COBOL帳票→PDF品質向上
GUIBlazor Server + Radzen ComponentsC#一本で完結、VB6のイベント駆動モデルに近い
DBSQL Server(統一)DB2+Oracle混在→単一DBMSに統合。EF Coreとの相性最良
ログSerilog + Seq構造化ログ。VZY110.bas/EXLOG.CBLの統合置き換え
テストxUnit + VerifyVerifyでゴールデンマスターテスト(スナップショットテスト)
DIMicrosoft.Extensions.DependencyInjection標準。ExeOdbc.basの104コピー問題を根本解決

4.2 不採用とした技術と理由

技術不採用理由
MuleSoft / Dell Boomi日本独自EDIフォーマット未対応。年間数百万円のライセンス
Apache Camel + SmooksJava/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 / DagsterPython必須で技術スタックが分裂。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()
        };
    }
}
csharp

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;
    }
}
csharp

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);
    }
}
csharp

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>
csharp

6. 新しい連携先・フォーマットが増えた場合

6.1 Configuration-Drivenの場合(推奨)

  1. formats/newcustomer-format01.json を作成(30分〜2時間)
  2. サンプルデータでテスト(1時間)
  3. デプロイ(設定ファイルの追加のみ、コード変更なし)

合計: 半日〜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"
}
json

汎用エンジンに 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駆動モダナイゼーション戦略

このページはお役に立ちましたか?

未来明るく電力

システム資産可視化