C#でバッチ処理を作る方法|実行ファイル化・定期実行・サンプルコードまで解説

はじめに

C#でバッチ処理を作りたいと考えたとき、多くの場合は「決まった時間にファイルを処理したい」「CSVを読み込んでDBに登録したい」「夜間に集計処理を実行したい」「手動ではなく自動で処理を回したい」といった目的があります。

C#はWindows環境との相性がよく、コンソールアプリとしてバッチ処理を作成し、exe化してタスクスケジューラから定期実行する構成を取りやすい言語です。また、.NETを使えばLinux環境でも動かせるため、cronによる定期実行にも対応できます。

この記事では、C#でバッチ処理を作る基本から、実行ファイル化、定期実行、ログ出力、例外処理、よくあるトラブル、実用的なサンプルコードまで解説します。C#バッチを初めて作る方でも、実運用を意識した構成が分かる内容です。

1. C#のバッチ処理とは?できることと使われる場面

1-1. バッチ処理の意味とWebアプリ・常駐アプリとの違い

バッチ処理とは、ユーザーが画面上で操作するのではなく、あらかじめ決められた処理をまとめて実行するプログラムのことです。

たとえば、次のような処理がバッチ処理に該当します。

  • 毎日深夜に売上データを集計する

  • 指定フォルダに置かれたCSVファイルを取り込む

  • 古いログファイルを削除する

  • 外部システムからデータを取得してDBに登録する

  • メール送信や帳票出力をまとめて行う

Webアプリはユーザーの操作に応じて処理を返すアプリケーションです。一方、バッチ処理は人の操作を前提にせず、決められたタイミングやコマンド実行によって処理を開始します。

また、常駐アプリやWindowsサービスは起動したまま動き続けるのに対し、バッチ処理は「起動する」「処理する」「終了する」という流れが基本です。そのため、定期的に実行する処理や、完了後に終了してよい処理に向いています。

1-2. C#でバッチ処理を作るメリット

C#でバッチ処理を作るメリットは、実装しやすく保守しやすいことです。

C#は型安全な言語であり、Visual StudioやVisual Studio Codeなどの開発環境も充実しています。ファイル操作、CSV処理、DB接続、HTTP通信、ログ出力など、バッチ処理でよく使う機能を標準ライブラリやNuGetパッケージで実装できます。

また、C#のコンソールアプリはexeとして配布しやすく、Windowsタスクスケジューラに登録して定期実行する構成も一般的です。社内システムや業務システムの運用バッチでは、C#が採用されることも多くあります。

さらに、.NETを使えばクロスプラットフォームで動作できるため、WindowsだけでなくLinuxサーバー上でcron実行することも可能です。

1-3. C#バッチが向いている処理例

C#バッチは、次のような処理に向いています。

データ連携処理では、CSV、TSV、Excel、JSONなどのファイルを読み込み、DBへ登録したり別システムへ送信したりできます。

集計処理では、日次・月次で売上、在庫、アクセスログなどを集計し、結果をファイルやDBに保存できます。

メンテナンス処理では、一定期間を過ぎた一時ファイルやログファイルを削除したり、不要データをアーカイブしたりできます。

通知処理では、条件に合致したデータを検出してメールやチャットへ通知できます。

外部API連携では、定期的にAPIを呼び出してデータを取得し、自社システムへ反映できます。

このように、C#バッチは「画面は不要だが、定期的または一括で確実に実行したい処理」に適しています。

1-4. 「C# バッチ」と「batファイル」の違い

「C# バッチ」と「batファイル」は混同されやすいですが、役割が異なります。

C#バッチは、C#で作成したコンソールアプリや実行ファイルを指すことが多いです。複雑なロジック、DB接続、ファイル解析、例外処理、ログ出力などを本格的に実装できます。

一方、batファイルはWindowsのコマンドをまとめて実行するためのスクリプトです。exeを呼び出したり、ファイルコピーを行ったり、環境変数を設定したりする用途に向いています。

実務では、C#で作ったバッチ処理をexe化し、それをbatファイルから呼び出す構成もよく使われます。

たとえば、次のようなbatファイルでC#バッチを実行できます。

bat
@echo off
cd /d C:\Batch\SampleBatch
SampleBatch.exe input.csv
exit /b %ERRORLEVEL%

複雑な業務ロジックはC#で実装し、実行時の呼び出しや環境設定をbatファイルで補助する、という使い分けが一般的です。

2. C#でバッチ処理を作る基本手順

2-1. 開発環境を準備する

C#でバッチ処理を作るには、まず.NET SDKをインストールします。開発環境としては、Visual Studio、Visual Studio Code、Riderなどを使用できます。

Windowsで開発する場合は、Visual Studioを使うとプロジェクト作成、ビルド、デバッグ、発行までGUIで操作しやすくなります。軽量に開発したい場合は、Visual Studio Codeと.NET CLIの組み合わせでも問題ありません。

コマンドラインで確認する場合は、次のコマンドを実行します。

Bash
dotnet --version

バージョン番号が表示されれば、.NET SDKが利用できる状態です。

2-2. コンソールアプリとしてプロジェクトを作成する

C#のバッチ処理は、基本的にコンソールアプリとして作成します。

.NET CLIを使う場合は、次のコマンドでプロジェクトを作成できます。

Bash
dotnet new console -n SampleBatch
cd SampleBatch

これにより、SampleBatchというフォルダが作成され、その中にC#のコンソールアプリ用プロジェクトが生成されます。

作成直後のProgram.csには、簡単な出力処理が記述されています。

C#
Console.WriteLine("Hello, World!");

この部分を、実際のバッチ処理に置き換えていきます。

2-3. Mainメソッドで処理の流れを組み立てる

C#バッチでは、起動時に実行されるMainメソッドを中心に処理の流れを組み立てます。

基本的な流れは次のようになります。

C#
internal class Program
{
static int Main(string[] args)
{
try
{
Console.WriteLine("バッチ処理を開始します。");

// ここにメイン処理を書く

Console.WriteLine("バッチ処理が正常終了しました。");
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine($"エラーが発生しました: {ex.Message}");
return 1;
}
}
}

正常終了した場合は0、エラー終了した場合は1などの終了コードを返します。これにより、タスクスケジューラやbatファイル側で実行結果を判定できます。

2-4. コマンドライン引数を受け取れるようにする

C#バッチでは、実行時にコマンドライン引数を受け取れるようにしておくと便利です。

たとえば、処理対象のファイルパスを引数で受け取る場合は、次のように記述します。

C#
internal class Program
{
static int Main(string[] args)
{
if (args.Length == 0)
{
Console.Error.WriteLine("入力ファイルを指定してください。");
return 1;
}

string inputFilePath = args[0];

Console.WriteLine($"入力ファイル: {inputFilePath}");

return 0;
}
}

実行するときは、次のように引数を指定します。

Bash
SampleBatch.exe C:\Data\input.csv

コマンドライン引数を使うと、同じexeでも処理対象や実行モードを切り替えられます。

2-5. 終了コードを返して実行結果を判定できるようにする

バッチ処理では、終了コードが重要です。

終了コードは、プログラムが正常に完了したか、エラーで終了したかを外部から判定するために使われます。一般的には、正常終了を0、異常終了を1以上の値で表します。

C#
static int Main(string[] args)
{
try
{
Execute();
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine(ex);
return 1;
}
}

static void Execute()
{
// バッチ処理本体
}

batファイルから呼び出す場合は、%ERRORLEVEL%で終了コードを確認できます。

bat
SampleBatch.exe
if %ERRORLEVEL% neq 0 (
echo バッチが異常終了しました。
exit /b 1
)

echo バッチが正常終了しました。
exit /b 0

運用時には、終了コードとログを組み合わせて、実行結果を確認できるようにしておくことが重要です。

3. C#バッチ処理のサンプルコード

3-1. ファイルを読み込んで処理する基本サンプル

まずは、テキストファイルを読み込んで1行ずつ処理する基本サンプルです。

C#
using System.Text;

internal class Program
{
static int Main(string[] args)
{
try
{
if (args.Length == 0)
{
Console.Error.WriteLine("ファイルパスを指定してください。");
return 1;
}

string filePath = args[0];

if (!File.Exists(filePath))
{
Console.Error.WriteLine($"ファイルが存在しません: {filePath}");
return 1;
}

foreach (string line in File.ReadLines(filePath, Encoding.UTF8))
{
Console.WriteLine($"読み込み行: {line}");
}

return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine(ex);
return 1;
}
}
}

File.ReadLinesを使うと、ファイル全体を一度にメモリへ読み込まず、1行ずつ処理できます。大量データを扱うバッチでは、File.ReadAllLinesよりもFile.ReadLinesの方が適している場合があります。

3-2. CSVを読み込んでデータ変換するサンプル

次に、CSVを読み込んでデータを変換する例です。

以下のサンプルでは、入力CSVを読み込み、名前を大文字に変換して別ファイルへ出力します。

C#
using System.Text;

internal class Program
{
static int Main(string[] args)
{
try
{
string inputPath = args.Length > 0 ? args[0] : "input.csv";
string outputPath = args.Length > 1 ? args[1] : "output.csv";

using var writer = new StreamWriter(outputPath, false, new UTF8Encoding(false));

foreach (string line in File.ReadLines(inputPath, Encoding.UTF8))
{
if (string.IsNullOrWhiteSpace(line))
{
continue;
}

string[] columns = line.Split(',');

if (columns.Length < 2)
{
Console.WriteLine($"列数不足のためスキップ: {line}");
continue;
}

string id = columns[0];
string name = columns[1].ToUpperInvariant();

writer.WriteLine($"{id},{name}");
}

Console.WriteLine("CSV変換が完了しました。");
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine(ex);
return 1;
}
}
}

単純なCSVであればSplit(',')でも処理できますが、実際のCSVではカンマを含む値やダブルクォートを扱うケースがあります。その場合は、CSV用のライブラリを使う方が安全です。

3-3. フォルダ内のファイルを一括処理するサンプル

指定フォルダ内のファイルをまとめて処理する例です。

C#
internal class Program
{
static int Main(string[] args)
{
try
{
string targetDirectory = args.Length > 0 ? args[0] : @"C:\Data\Input";

if (!Directory.Exists(targetDirectory))
{
Console.Error.WriteLine($"フォルダが存在しません: {targetDirectory}");
return 1;
}

string[] files = Directory.GetFiles(targetDirectory, "*.txt");

if (files.Length == 0)
{
Console.WriteLine("処理対象ファイルはありません。");
return 0;
}

foreach (string file in files)
{
Console.WriteLine($"処理開始: {file}");

string content = File.ReadAllText(file);
int length = content.Length;

Console.WriteLine($"文字数: {length}");
Console.WriteLine($"処理終了: {file}");
}

return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine(ex);
return 1;
}
}
}

フォルダ監視ではなく、定期的にフォルダを見に行って対象ファイルを一括処理する場合、このような構成が使えます。

3-4. 例外発生時にエラーを記録するサンプル

バッチ処理では、エラーが起きたときに原因を追跡できるよう、ログに記録することが重要です。

C#
internal class Program
{
static int Main(string[] args)
{
try
{
Execute();
return 0;
}
catch (Exception ex)
{
WriteErrorLog(ex);
return 1;
}
}

static void Execute()
{
throw new InvalidOperationException("サンプルエラーです。");
}

static void WriteErrorLog(Exception ex)
{
string logDirectory = "logs";
Directory.CreateDirectory(logDirectory);

string logPath = Path.Combine(logDirectory, $"error_{DateTime.Now:yyyyMMdd}.log");

string message =
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] ERROR{Environment.NewLine}" +
ex + Environment.NewLine;

File.AppendAllText(logPath, message);
}
}

ex.Messageだけでなく、ex.ToString()を記録すると、スタックトレースも含めて確認できます。障害調査ではスタックトレースが非常に重要です。

3-5. 処理結果をログに出力するサンプル

正常時の開始・終了、処理件数などもログに出力しておくと、後から実行状況を確認しやすくなります。

C#
internal class Program
{
static int Main(string[] args)
{
try
{
WriteLog("バッチ開始");

int count = Execute();

WriteLog($"バッチ正常終了 処理件数={count}");
return 0;
}
catch (Exception ex)
{
WriteLog($"バッチ異常終了 {ex}");
return 1;
}
}

static int Execute()
{
int processedCount = 0;

for (int i = 1; i <= 5; i++)
{
WriteLog($"処理中 ID={i}");
processedCount++;
}

return processedCount;
}

static void WriteLog(string message)
{
Directory.CreateDirectory("logs");

string logPath = Path.Combine("logs", $"batch_{DateTime.Now:yyyyMMdd}.log");
string line = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {message}";

File.AppendAllText(logPath, line + Environment.NewLine);
Console.WriteLine(line);
}
}

ログには、開始時刻、終了時刻、処理件数、エラー内容、対象ファイル名などを出力しておくと運用しやすくなります。

4. C#バッチでよく使う実装ポイント

4-1. 設定値をappsettings.jsonで管理する

バッチ処理では、接続文字列、入力フォルダ、出力フォルダ、ログ出力先、リトライ回数などの設定値をコードに直接書かない方がよいです。

設定値はappsettings.jsonにまとめると、環境ごとの変更がしやすくなります。

JSON
{
"BatchSettings": {
"InputDirectory": "C:\\Data\\Input",
"OutputDirectory": "C:\\Data\\Output",
"LogDirectory": "C:\\Data\\Logs",
"RetryCount": 3
}
}

読み込みには、Microsoft.Extensions.Configuration.Jsonなどを使用します。

C#
using Microsoft.Extensions.Configuration;

var configuration = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false)
.Build();

string inputDirectory = configuration["BatchSettings:InputDirectory"]!;
int retryCount = int.Parse(configuration["BatchSettings:RetryCount"]!);

ポイントは、SetBasePath(AppContext.BaseDirectory)を指定することです。これにより、実行ファイルが配置されているフォルダを基準にappsettings.jsonを読み込めます。

4-2. ログ出力を実装する

C#バッチでは、ログ出力を必ず実装しておくべきです。

コンソールに出力するだけでは、タスクスケジューラやサーバー上で実行したときに内容を確認しづらくなる場合があります。ファイルログや標準出力ログを残す設計にしておくと、障害調査がしやすくなります。

ログに出すべき主な内容は次のとおりです。

  • バッチ開始時刻

  • バッチ終了時刻

  • 処理対象件数

  • 正常処理件数

  • エラー件数

  • 対象ファイル名

  • 例外内容

  • 終了コード

小規模なバッチであれば自前のログ出力でも構いませんが、本格的に運用する場合は、ログライブラリの利用も検討します。

4-3. 例外処理とリトライ処理を入れる

バッチ処理では、ファイルアクセス、DB接続、API通信などで一時的なエラーが発生することがあります。

たとえば、外部APIが一時的に応答しない、ネットワークが不安定、DB接続が一瞬失敗する、といったケースです。このような場合は、即座に異常終了するのではなく、一定回数リトライする設計が有効です。

C#
static void ExecuteWithRetry(Action action, int retryCount)
{
for (int attempt = 1; attempt <= retryCount; attempt++)
{
try
{
action();
return;
}
catch (Exception ex)
{
Console.WriteLine($"試行 {attempt} 回目でエラー: {ex.Message}");

if (attempt == retryCount)
{
throw;
}

Thread.Sleep(3000);
}
}
}

ただし、すべてのエラーをリトライすればよいわけではありません。入力ファイルの形式不正や設定ミスなど、リトライしても成功しないエラーは、すぐに異常終了して原因を修正する方が適切です。

4-4. 二重起動を防止する

バッチ処理では、前回の処理が終わる前に次の処理が起動してしまうと、同じファイルを二重に処理したり、DB更新が競合したりする可能性があります。

二重起動を防ぐ方法として、ミューテックスを使う方法があります。

C#
using System.Threading;

internal class Program
{
static int Main(string[] args)
{
using var mutex = new Mutex(false, "Global\\SampleBatchMutex");

if (!mutex.WaitOne(0, false))
{
Console.WriteLine("既にバッチが実行中のため終了します。");
return 0;
}

try
{
Console.WriteLine("バッチ処理を開始します。");

// メイン処理

Console.WriteLine("バッチ処理を終了します。");
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine(ex);
return 1;
}
finally
{
mutex.ReleaseMutex();
}
}
}

タスクスケジューラ側にも「新しいインスタンスを開始しない」などの設定がありますが、アプリ側でも二重起動対策を入れておくと安全です。

4-5. 処理対象データがない場合の扱い

バッチ処理では、処理対象データがないこと自体はエラーではない場合があります。

たとえば、指定フォルダにCSVファイルが存在しない、送信対象の通知データがない、集計対象データがない、といったケースです。

この場合は、異常終了ではなく正常終了として扱うことが多いです。

C#
if (files.Length == 0)
{
WriteLog("処理対象ファイルがないため正常終了します。");
return 0;
}

ただし、本来存在するはずのファイルがない場合はエラーとして扱うべきです。処理対象がないことを正常とするか異常とするかは、業務要件に合わせて決めます。

4-6. DB接続を使う場合の注意点

C#バッチでDB接続を使う場合は、接続文字列をコードに直接書かず、設定ファイルや環境変数で管理します。

また、DB接続は使い終わったら確実に破棄する必要があります。usingを使うと、処理終了時に自動的に破棄されます。

C#
using var connection = new SqlConnection(connectionString);
connection.Open();

// DB処理

大量データを更新する場合は、トランザクションの範囲にも注意が必要です。すべてを1つのトランザクションにすると、ロック時間が長くなり、他の処理に影響する可能性があります。

一方で、途中で失敗したときにデータ不整合が起きないよう、どの単位でコミットするかを設計しておく必要があります。

5. C#バッチを実行ファイル化する方法

5-1. Visual Studioでビルドする方法

Visual StudioでC#バッチを作成している場合、ビルドはメニューから簡単に実行できます。

通常は、上部メニューの「ビルド」から「ソリューションのビルド」を選択します。ビルドに成功すると、bin\Debugまたはbin\Release配下に実行ファイルが生成されます。

本番環境へ配置する場合は、DebugではなくRelease構成でビルドします。Releaseビルドの方が最適化され、本番運用に適しています。

5-2. dotnet publishでexe化する方法

.NET CLIを使う場合は、dotnet publishコマンドで発行できます。

Bash
dotnet publish -c Release -o ./publish

このコマンドを実行すると、publishフォルダに実行に必要なファイルが出力されます。

Windows向けに発行する場合は、ランタイムを指定できます。

Bash
dotnet publish -c Release -r win-x64 -o ./publish

自己完結型で発行する場合は、次のように指定します。

Bash
dotnet publish -c Release -r win-x64 --self-contained true -o ./publish

単一ファイルとして発行したい場合は、次のようにします。

Bash
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o ./publish

ただし、単一ファイル化しても設定ファイルやログ出力先など、外部ファイルが必要になる場合があります。配置時には必要なファイルを確認しましょう。

5-3. フレームワーク依存と自己完結型の違い

C#バッチの発行方法には、大きく分けてフレームワーク依存と自己完結型があります。

フレームワーク依存は、実行環境に.NETランタイムがインストールされている前提で動作します。出力ファイルのサイズは比較的小さくなりますが、サーバー側に対応するランタイムが必要です。

自己完結型は、実行に必要なランタイムを含めて発行する方法です。実行環境に.NETランタイムがなくても動作しやすい反面、出力ファイルのサイズは大きくなります。

社内サーバーで.NETランタイムを管理できる場合はフレームワーク依存、配布先の環境をできるだけ固定したくない場合は自己完結型を選ぶとよいでしょう。

5-4. 実行ファイルの配置方法

発行したC#バッチは、サーバーや実行端末の任意のフォルダに配置します。

たとえば、次のような配置にします。

C:\Batch\SampleBatch\
SampleBatch.exe
appsettings.json
logs\
input\
output\

実行ファイルだけでなく、appsettings.jsonや必要なDLL、テンプレートファイル、証明書ファイルなども忘れずに配置します。

ログフォルダや入力フォルダを使う場合は、実行ユーザーに読み書き権限があることも確認します。

5-5. コマンドプロンプトから実行する方法

C#バッチは、コマンドプロンプトから直接実行できます。

bat
cd /d C:\Batch\SampleBatch
SampleBatch.exe

引数を渡す場合は、次のように指定します。

bat
SampleBatch.exe C:\Data\input.csv

終了コードを確認する場合は、次のように実行します。

bat
SampleBatch.exe
echo %ERRORLEVEL%

タスクスケジューラに登録する前に、まずコマンドプロンプトから正常に動作することを確認しておきましょう。

5-6. batファイルからC#バッチを呼び出す方法

C#バッチをbatファイルから呼び出すと、作業フォルダの移動、引数指定、終了コード判定などをまとめて管理できます。

bat
@echo off
setlocal

cd /d C:\Batch\SampleBatch

SampleBatch.exe C:\Batch\SampleBatch\input\data.csv

if %ERRORLEVEL% neq 0 (
echo C#バッチが異常終了しました。
exit /b 1
)

echo C#バッチが正常終了しました。
exit /b 0

タスクスケジューラから直接exeを呼び出してもよいですが、batファイルを経由するとトラブル時の調査や設定変更がしやすくなります。

6. C#バッチを定期実行する方法

6-1. Windowsタスクスケジューラで定期実行する

Windows環境でC#バッチを定期実行する場合は、タスクスケジューラを使うのが一般的です。

タスクスケジューラでは、毎日、毎週、毎月、ログオン時、PC起動時など、さまざまな条件でプログラムを実行できます。

基本的な流れは次のとおりです。

  1. タスクスケジューラを起動する

  2. 「基本タスクの作成」または「タスクの作成」を選択する

  3. 実行タイミングを設定する

  4. 実行するプログラムを指定する

  5. 必要に応じて引数と作業フォルダを設定する

  6. 実行ユーザーや権限を設定する

本番運用では、「基本タスクの作成」よりも「タスクの作成」を使った方が細かい設定ができます。

6-2. 実行プログラム・引数・作業フォルダの設定方法

タスクスケジューラで重要なのは、「プログラム」「引数」「開始」フォルダの設定です。

exeを直接実行する場合は、次のように設定します。

プログラム/スクリプト:
C:\Batch\SampleBatch\SampleBatch.exe

引数の追加:
C:\Batch\SampleBatch\input\data.csv

開始:
C:\Batch\SampleBatch

batファイルを実行する場合は、次のように設定します。

プログラム/スクリプト:
C:\Batch\SampleBatch\run.bat

開始:
C:\Batch\SampleBatch

「開始」フォルダを設定していないと、相対パスの基準が想定と異なり、ファイルが見つからない原因になります。

6-3. 毎日・毎時・月次で実行する設定例

毎日深夜1時に実行する場合は、トリガーで「毎日」を選択し、開始時刻を01:00に設定します。

毎時実行する場合は、トリガーの詳細設定で「繰り返し間隔」を1時間に設定します。

月次で実行する場合は、「毎月」を選択し、実行する日付を指定します。たとえば、毎月1日の午前2時に月次集計バッチを実行する、といった設定ができます。

バッチの実行時間が長くなる可能性がある場合は、前回実行が残っているときに次の実行をどう扱うかも設定しておきます。二重実行を避けるため、タスクスケジューラ側とアプリ側の両方で対策すると安全です。

6-4. 実行ユーザーと権限の注意点

タスクスケジューラで実行する場合、手動実行時と実行ユーザーが異なることがあります。

たとえば、自分のユーザーでコマンドプロンプトから実行すると成功するのに、タスクスケジューラでは失敗する場合、実行ユーザーの権限不足が原因かもしれません。

確認すべき権限は次のとおりです。

  • 実行ファイルの読み取り権限

  • 入力フォルダの読み取り権限

  • 出力フォルダの書き込み権限

  • ログフォルダの書き込み権限

  • ネットワーク共有フォルダへのアクセス権限

  • DBへの接続権限

  • 環境変数や証明書へのアクセス権限

本番環境では、管理者ユーザーで実行するのではなく、バッチ実行用の専用ユーザーを作成し、必要最小限の権限を付与するのが望ましいです。

6-5. タスクスケジューラで動かない場合の確認ポイント

タスクスケジューラでC#バッチが動かない場合は、次の点を確認します。

まず、コマンドプロンプトから同じユーザー、同じ引数、同じ作業フォルダで実行できるか確認します。

次に、タスクスケジューラの「履歴」を有効化し、実行結果やエラーを確認します。

また、ログファイルが出力されているか、出力先フォルダに権限があるかも確認します。

よくある原因は、作業フォルダ未設定、相対パスの使用、権限不足、appsettings.jsonの配置漏れ、ネットワークドライブの参照不可などです。

6-6. Linux環境でcron実行する場合の考え方

.NETで作成したC#バッチは、Linux環境で動かすこともできます。

Linuxで定期実行する場合は、cronを使います。たとえば、毎日午前1時に実行する場合は、crontabに次のように記述します。

Bash
0 1 * * * /usr/bin/dotnet /opt/batch/SampleBatch/SampleBatch.dll >> /var/log/samplebatch.log 2>&1

自己完結型で発行している場合は、実行ファイルを直接呼び出せます。

Bash
0 1 * * * /opt/batch/SampleBatch/SampleBatch >> /var/log/samplebatch.log 2>&1

Linuxでは、ファイルパスの区切り文字、実行権限、文字コード、タイムゾーン、改行コードに注意が必要です。Windowsで動作していたC#バッチをLinuxに移す場合は、環境差分を確認しましょう。

7. C#バッチ処理の設計で注意すべきこと

7-1. 処理単位を小さく分ける

C#バッチを設計するときは、処理を小さな単位に分けることが重要です。

すべての処理をProgram.csの中に書いてしまうと、コードが長くなり、修正やテストが難しくなります。

たとえば、次のように役割ごとに分けます。

Program.cs            起動処理
BatchService.cs バッチ全体の制御
CsvReaderService.cs CSV読み込み
DataConverter.cs データ変換
FileWriterService.cs ファイル出力

処理を分けることで、どこで何をしているかが分かりやすくなり、エラー発生時の調査もしやすくなります。

7-2. 再実行できる設計にする

バッチ処理では、途中で失敗した後に再実行するケースがあります。

そのため、同じバッチを再実行してもデータが二重登録されない設計にしておくことが重要です。

たとえば、DB登録時には一意キーを使って重複登録を防ぐ、処理済みファイルを別フォルダに移動する、処理状態をDBに記録する、といった方法があります。

再実行できないバッチは、障害発生時の復旧が難しくなります。本番運用を考えるなら、最初から再実行を前提に設計しましょう。

7-3. エラー時に途中から再開できるようにする

大量データを処理するバッチでは、途中で失敗したときに最初からやり直すと時間がかかる場合があります。

そのため、処理済みの位置や状態を記録し、途中から再開できるようにしておくと便利です。

たとえば、次のような方法があります。

  • 処理済みIDをDBに保存する

  • ファイル単位で処理済みフォルダへ移動する

  • 成功したレコードと失敗したレコードを分けて出力する

  • チェックポイントを保存する

ただし、途中再開の仕組みは設計が複雑になるため、処理件数や業務影響に応じて導入を判断します。

7-4. ログから原因調査できるようにする

バッチ処理は無人で実行されることが多いため、ログがなければ何が起きたか分かりません。

ログには、単に「エラーが発生しました」と出すだけでなく、原因調査に必要な情報を含めます。

たとえば、次の情報があると調査しやすくなります。

  • 実行日時

  • バッチ名

  • 処理対象ファイル

  • 処理件数

  • エラーが発生したデータのID

  • 例外メッセージ

  • スタックトレース

  • 設定値の一部

ただし、パスワード、アクセストークン、個人情報などの機密情報をログに出してはいけません。ログは調査に必要な情報とセキュリティのバランスを考えて設計します。

7-5. 本番環境と開発環境の設定を分ける

C#バッチでは、開発環境、検証環境、本番環境で設定を分けることがよくあります。

たとえば、DB接続先、入力フォルダ、出力フォルダ、API URL、ログ出力先などは環境ごとに異なります。

設定をコードに直接書いてしまうと、環境ごとにビルドし直す必要が出てしまいます。appsettings.jsonや環境変数を使って、設定を外部化しておきましょう。

例として、次のように環境別の設定ファイルを用意できます。

appsettings.Development.json
appsettings.Staging.json
appsettings.Production.json

本番環境の設定を誤って開発環境で使わないよう、設定ファイルの管理には注意が必要です。

7-6. 大量データ処理ではメモリ使用量に注意する

大量のファイルやデータを扱うC#バッチでは、メモリ使用量に注意します。

たとえば、巨大なCSVをFile.ReadAllLinesで一括読み込みすると、メモリを大量に消費します。大量データの場合は、File.ReadLinesStreamReaderを使って1行ずつ処理する方が安全です。

C#
foreach (string line in File.ReadLines(filePath))
{
// 1行ずつ処理
}

DBから大量データを取得する場合も、一度にすべてをリスト化せず、ページングやストリーミングを検討します。

メモリ使用量が増えすぎると、処理速度低下や異常終了の原因になります。データ量が増えても安定して動く設計を意識しましょう。

8. C#バッチが動かないときの原因と対処法

8-1. exeを直接実行すると動くがタスクスケジューラでは動かない

C#バッチでよくあるトラブルが、exeを直接実行すると動くのに、タスクスケジューラから実行すると動かないケースです。

主な原因は、実行ユーザー、作業フォルダ、権限、環境変数の違いです。

対処法としては、まずタスクスケジューラの「開始」フォルダを設定します。次に、実行ユーザーに必要な権限があるか確認します。

また、バッチ側でAppContext.BaseDirectoryDirectory.GetCurrentDirectory()をログに出しておくと、どのフォルダを基準に動いているか確認できます。

C#
Console.WriteLine($"BaseDirectory: {AppContext.BaseDirectory}");
Console.WriteLine($"CurrentDirectory: {Directory.GetCurrentDirectory()}");

8-2. 相対パス指定でファイルが見つからない

相対パスを使っている場合、実行環境によって基準フォルダが変わることがあります。

たとえば、次のようなコードは実行場所によって結果が変わります。

C#
string path = "input.csv";

タスクスケジューラでは、想定外のフォルダがカレントディレクトリになることがあります。

対策として、実行ファイルのフォルダを基準にパスを組み立てます。

C#
string baseDirectory = AppContext.BaseDirectory;
string path = Path.Combine(baseDirectory, "input.csv");

または、appsettings.jsonに絶対パスを設定しておく方法もあります。

8-3. 権限不足でファイルやDBにアクセスできない

タスクスケジューラで実行する場合、手動実行時とは異なるユーザーで動くことがあります。そのため、ファイルやDBへのアクセス権限が不足してエラーになることがあります。

特に注意すべきなのは、ネットワーク共有フォルダです。ログオン中のユーザーではアクセスできても、タスク実行ユーザーではアクセスできない場合があります。

対策として、実行ユーザーに必要な権限を付与します。また、ネットワークドライブの割り当てではなく、UNCパスを使う方が安定します。

\\server\share\input

DB接続でWindows認証を使っている場合も、タスク実行ユーザーがDBにアクセスできるか確認しましょう。

8-4. appsettings.jsonが読み込まれない

appsettings.jsonが読み込まれない原因として多いのは、ファイルが出力フォルダにコピーされていない、または読み込み基準パスが想定と違うケースです。

プロジェクトファイルで、appsettings.jsonを出力先にコピーする設定を入れます。

XML
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

読み込み時には、実行ファイルの配置フォルダを基準にします。

C#
var configuration = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json", optional: false)
.Build();

これにより、タスクスケジューラから実行した場合でも設定ファイルを見つけやすくなります。

8-5. ログが出力されない

ログが出力されない場合は、ログ出力先のパスと権限を確認します。

相対パスでログ出力している場合、想定外のフォルダに出力されている可能性があります。ログの出力先は絶対パスにするか、AppContext.BaseDirectoryを基準に指定します。

C#
string logDirectory = Path.Combine(AppContext.BaseDirectory, "logs");
Directory.CreateDirectory(logDirectory);

また、タスク実行ユーザーにログフォルダへの書き込み権限がない場合も出力に失敗します。ログ出力処理自体で例外が発生していないかも確認しましょう。

8-6. 文字化けや文字コードの問題が起きる

CSVやテキストファイルを扱うC#バッチでは、文字コードの違いによる文字化けが発生することがあります。

UTF-8のファイルをShift-JISとして読んだり、Shift-JISのファイルをUTF-8として読んだりすると、日本語が文字化けします。

UTF-8で読み込む場合は、次のように指定します。

C#
using System.Text;

string text = File.ReadAllText(filePath, Encoding.UTF8);

Shift-JISを扱う場合は、エンコーディングプロバイダーの登録が必要になる場合があります。

C#
using System.Text;

Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
Encoding sjis = Encoding.GetEncoding("shift_jis");

string text = File.ReadAllText(filePath, sjis);

外部システムとの連携では、入力ファイルと出力ファイルの文字コードを事前に確認しておくことが重要です。

9. C#バッチ処理の実装例:実用的な構成

9-1. フォルダ構成の例

実用的なC#バッチでは、処理を役割ごとに分けた構成にすると保守しやすくなります。

SampleBatch/
Program.cs
appsettings.json
Services/
BatchService.cs
FileService.cs
LogService.cs
Models/
BatchSettings.cs
input/
output/
logs/

小規模なバッチであっても、設定、ログ、メイン処理を分けておくと、後から機能追加しやすくなります。

9-2. Program.csの実装例

以下は、設定ファイルを読み込み、ログを出力しながらフォルダ内のテキストファイルを処理する実装例です。

C#
using Microsoft.Extensions.Configuration;

internal class Program
{
static int Main(string[] args)
{
try
{
var configuration = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json", optional: false)
.Build();

string inputDirectory = configuration["BatchSettings:InputDirectory"]!;
string outputDirectory = configuration["BatchSettings:OutputDirectory"]!;
string logDirectory = configuration["BatchSettings:LogDirectory"]!;

var logger = new FileLogger(logDirectory);

logger.Info("バッチ処理を開始します。");

if (!Directory.Exists(inputDirectory))
{
logger.Error($"入力フォルダが存在しません: {inputDirectory}");
return 1;
}

Directory.CreateDirectory(outputDirectory);

string[] files = Directory.GetFiles(inputDirectory, "*.txt");

if (files.Length == 0)
{
logger.Info("処理対象ファイルがありません。正常終了します。");
return 0;
}

int processedCount = 0;

foreach (string file in files)
{
logger.Info($"処理開始: {file}");

string fileName = Path.GetFileName(file);
string outputPath = Path.Combine(outputDirectory, fileName);

string content = File.ReadAllText(file);
string converted = content.ToUpperInvariant();

File.WriteAllText(outputPath, converted);

processedCount++;

logger.Info($"処理終了: {file}");
}

logger.Info($"バッチ処理が正常終了しました。処理件数={processedCount}");
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine(ex);
return 1;
}
}
}

public class FileLogger
{
private readonly string _logDirectory;

public FileLogger(string logDirectory)
{
_logDirectory = logDirectory;
Directory.CreateDirectory(_logDirectory);
}

public void Info(string message)
{
Write("INFO", message);
}

public void Error(string message)
{
Write("ERROR", message);
}

private void Write(string level, string message)
{
string logPath = Path.Combine(_logDirectory, $"batch_{DateTime.Now:yyyyMMdd}.log");
string line = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] [{level}] {message}";

File.AppendAllText(logPath, line + Environment.NewLine);
Console.WriteLine(line);
}
}

この例では、入力フォルダ内の.txtファイルを読み込み、大文字に変換して出力フォルダへ保存しています。ログは日付別のファイルに出力されます。

9-3. 設定ファイルの例

appsettings.jsonは次のようにします。

JSON
{
"BatchSettings": {
"InputDirectory": "C:\\Batch\\SampleBatch\\input",
"OutputDirectory": "C:\\Batch\\SampleBatch\\output",
"LogDirectory": "C:\\Batch\\SampleBatch\\logs"
}
}

本番環境では、開発環境とは異なるパスを設定することが多いです。配置先に合わせて設定値を変更します。

また、設定ファイルは実行ファイルと同じフォルダに配置します。発行時にコピーされるよう、プロジェクト設定も確認しておきましょう。

9-4. ログ出力の例

上記のサンプルを実行すると、次のようなログが出力されます。

[2026-06-18 01:00:00] [INFO] バッチ処理を開始します。
[2026-06-18 01:00:01] [INFO] 処理開始: C:\Batch\SampleBatch\input\data1.txt
[2026-06-18 01:00:01] [INFO] 処理終了: C:\Batch\SampleBatch\input\data1.txt
[2026-06-18 01:00:02] [INFO] バッチ処理が正常終了しました。処理件数=1

エラー時には、例外内容や対象ファイル名を出力するようにしておくと、原因調査がスムーズになります。

実運用では、ログローテーションも考慮します。ログを出し続けるとディスク容量を圧迫するため、一定期間を過ぎたログを削除する仕組みも検討しましょう。

9-5. タスクスケジューラ登録までの流れ

実装したC#バッチを定期実行するまでの流れは、次のとおりです。

まず、Releaseビルドまたはdotnet publishで発行します。

Bash
dotnet publish -c Release -r win-x64 -o C:\Batch\SampleBatch

次に、appsettings.json、入力フォルダ、出力フォルダ、ログフォルダを確認します。

その後、コマンドプロンプトで手動実行します。

bat
cd /d C:\Batch\SampleBatch
SampleBatch.exe

手動実行で正常に動作することを確認したら、タスクスケジューラに登録します。

タスクスケジューラでは、プログラムにexeまたはbatファイルを指定し、開始フォルダにバッチの配置フォルダを指定します。

最後に、タスクスケジューラから手動実行し、ログが出力されることを確認します。ここまで確認してから、日次や月次のスケジュールを有効化すると安全です。

10. C#バッチ処理に関するよくある質問

10-1. C#でバッチ処理を作るならコンソールアプリでよい?

はい。C#でバッチ処理を作る場合、基本的にはコンソールアプリで問題ありません。

コンソールアプリは、起動して処理を実行し、終了コードを返して終了するというバッチ処理の流れに適しています。

画面が不要な処理、定期実行する処理、コマンドライン引数で制御する処理は、コンソールアプリとして作るのが一般的です。

10-2. Windowsサービスとバッチ処理はどちらを選ぶべき?

処理を常に起動して監視し続ける必要がある場合は、Windowsサービスが向いています。

一方、毎日1回、毎時1回、月末だけなど、決まったタイミングで実行して終了すればよい処理は、バッチ処理が向いています。

たとえば、フォルダをリアルタイムに監視し続けたい場合はWindowsサービス、深夜にCSVをまとめて取り込みたい場合はC#バッチが適しています。

運用の簡単さを重視するなら、まずはC#のコンソールアプリとタスクスケジューラの組み合わせを検討するとよいでしょう。

10-3. .NET Frameworkと.NETのどちらで作るべき?

新規でC#バッチを作るなら、基本的には.NETを選ぶのがおすすめです。

.NETは現在のC#開発で主流のプラットフォームであり、WindowsだけでなくLinuxにも対応しやすいです。また、パフォーマンスやライブラリの面でもメリットがあります。

ただし、既存システムが.NET Frameworkで構築されている場合や、利用しているライブラリが.NET Frameworkに依存している場合は、.NET Frameworkを選ぶケースもあります。

新規開発では.NET、既存資産との互換性が重要な場合は.NET Frameworkも検討、という考え方が分かりやすいです。

10-4. C#バッチで非同期処理は使える?

C#バッチでも非同期処理は使えます。

たとえば、HTTP通信、ファイルI/O、DBアクセスなどでasync/awaitを利用できます。

C#
internal class Program
{
static async Task<int> Main(string[] args)
{
try
{
await ExecuteAsync();
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine(ex);
return 1;
}
}

static async Task ExecuteAsync()
{
using var httpClient = new HttpClient();
string result = await httpClient.GetStringAsync("https://example.com");
Console.WriteLine(result);
}
}

非同期処理を使うと、I/O待ちの多い処理で効率的に実装できます。ただし、並列実行しすぎると外部APIやDBに負荷をかけるため、同時実行数の制御も重要です。

10-5. サーバーで実行するときに必要なものは?

C#バッチをサーバーで実行するには、発行した実行ファイル一式、設定ファイル、実行に必要なランタイム、アクセス権限が必要です。

フレームワーク依存で発行した場合は、サーバーに対応する.NETランタイムをインストールします。自己完結型で発行した場合は、ランタイムを含めて配置できます。

また、次の点も確認します。

  • 実行ユーザーの権限

  • 入出力フォルダの存在

  • ログフォルダの書き込み権限

  • DB接続情報

  • ファイアウォールやネットワーク設定

  • タスクスケジューラやcronの設定

  • 文字コードやタイムゾーン

サーバー上では、開発端末と環境が異なるため、必ず本番に近い環境で動作確認を行いましょう。

10-6. C#バッチを安全に本番運用するには?

C#バッチを安全に本番運用するには、エラー時の調査、再実行、監視、権限管理を考慮する必要があります。

最低限、ログ出力、終了コード、例外処理、設定ファイルの外部化、二重起動防止は入れておくべきです。

また、異常終了した場合に担当者が気付けるよう、監視ツールやメール通知、タスクスケジューラの履歴確認などを組み合わせます。

本番運用では、「正常に動くこと」だけでなく、「失敗したときに原因が分かること」「安全に再実行できること」「データ不整合が起きないこと」が重要です。

まとめ

C#でバッチ処理を作る場合、基本はコンソールアプリとして実装し、exe化してタスクスケジューラやcronから定期実行する構成になります。

C#バッチでは、ファイル読み込み、CSV変換、フォルダ一括処理、DB連携、外部API連携、ログ出力など、業務システムで必要になるさまざまな処理を実装できます。

実装時には、コマンドライン引数、終了コード、例外処理、ログ出力、設定ファイル、二重起動防止、再実行設計を意識することが重要です。

また、タスクスケジューラで動かす場合は、作業フォルダ、実行ユーザー、権限、相対パス、appsettings.jsonの配置に注意しましょう。exeを直接実行すると動くのに定期実行では動かない場合、多くは環境差分や権限が原因です。

C#バッチは、シンプルなファイル処理から本格的な業務バッチまで対応できる実用的な方法です。まずはコンソールアプリで小さく作り、ログや設定管理を追加しながら、運用しやすい構成に整えていくとよいでしょう。