WebClientのDownloadStringAsync/DownloadFileAsyncをHttpClientへ安全に移行する完全ガイド【C#・VB対応】

既存の .NET アプリで WebClient を使ったダウンロード処理をそのまま残していないでしょうか。近年は HttpClient への移行が強く推奨されていますが、「DownloadStringAsync や DownloadFileAsync をどう書き換えればいいの?」というところで手が止まりがちです。本記事では、実際の置き換えパターンと設計のポイントを、VB / C# の具体例つきで詳しく解説します。

目次

WebClient から HttpClient への移行と基本方針

WebClient は .NET Framework 当時から存在する比較的古いクラスで、シンプルに HTTP 通信を行える反面、拡張性やパフォーマンス、テストのしやすさといった点では現在の要求に合わなくなりつつあります。特に、非同期処理のモデルが現代的ではなく、イベントベースでの完了通知や、例外処理の仕様などが扱いづらい場面が増えました。

一方、HttpClient は

  • タスクベースの非同期 (async/await) に完全対応
  • ヘッダーやメソッド、認証など、HTTP の細かい制御がしやすい
  • DI コンテナと相性がよく、テストしやすい設計にしやすい

といった特徴があり、.NET で HTTP 通信を行う場合の「標準手段」として位置付けられています。

ただし、WebClient にあった

  • DownloadStringAsync
  • DownloadFileAsync

といった「そのまま文字列/ファイルにしてくれる便利メソッド」が、その名のまま HttpClient に存在しないため、どの API を組み合わせれば同等のことができるのかが直感的に分かりづらい、という問題があります。

そこで本記事では、まずは代表的な置き換えパターンを整理し、そのうえで実装のコツや実務でハマりやすいポイントを解説していきます。

WebClient と HttpClient の機能対応表

まずは、WebClient でよく使われるメソッドと、HttpClient 側での代表的な置き換え先を表形式で整理しておきます。

旧 WebClient新 HttpClient(代表例)用途・補足
DownloadStringAsync(uri)GetStringAsync(uri)URL から文字列をそのまま取得する最もシンプルなパターン。
DownloadDataTaskAsync(uri)GetByteArrayAsync(uri) または
GetAsync → ReadAsByteArrayAsync
バイナリデータ(画像や ZIP など)をまとめて取得したい場合。
DownloadFileAsync(uri, path)GetStreamAsync(uri) → CopyToAsync(FileStream)
または GetAsync(...ResponseHeadersRead) → ReadAsStreamAsync
ファイルとして保存する場合。大きなファイルはストリーミング推奨。
UploadStringTaskAsyncPostAsync / PutAsync(StringContent など)文字列を POST / PUT する場合は HttpContent を作成して送信。
Headers プロパティHttpClient.DefaultRequestHeaders や
HttpRequestMessage の Headers
共通ヘッダーは DefaultRequestHeaders、リクエスト単位なら HttpRequestMessage。

以降では、特に質問の多い 文字列取得 と ファイル保存 のパターンに絞って、具体的なコードとともに見ていきます。

文字列を非同期に取得する(DownloadStringAsync の置き換え)

もっともシンプルなのは、DownloadStringAsync を GetStringAsync に置き換えるパターンです。戻り値が文字列である点も変わらず、ほぼ 1 行で書けます。

VB での文字列取得例


Imports System.Net.Http

Public Class Sample
    Private ReadOnly _http As HttpClient = New HttpClient()

    Public Async Function DownloadTextAsync(url As String) As Task(Of String)
        ' ここで例外が出た場合は HttpRequestException として捕捉可能
        Dim text As String = Await _http.GetStringAsync(url)
        Return text
    End Function
End Class

ポイントは次の通りです。

  • HttpClient はフィールドとして一度だけ生成し、使い回している。
  • 戻り値は Task(Of String) として、呼び出し側で Await できるようにしている。

C# での文字列取得例


using System.Net.Http;
using System.Threading.Tasks;

public class Sample
{
    private readonly HttpClient _http = new HttpClient();

    public async Task<string> DownloadTextAsync(string url)
    {
        string text = await _http.GetStringAsync(url);
        return text;
    }
}

コンソールアプリなど、スコープが小さいサンプルであれば次のようにローカル変数で持っても構いませんが、実務の長寿命アプリケーションでは後述の「HttpClient の使い回し」に従って設計してください。


using var http = new HttpClient();
string text = await http.GetStringAsync(url);

レスポンスコードを厳密に見たい場合

GetStringAsync は内部でステータスコードをチェックし、エラーの場合は HttpRequestException を投げます。レスポンスのヘッダーやステータスコードを詳細に扱いたい場合は、次のように GetAsync + ReadAsStringAsync に分解して書くと柔軟です。


using var http = new HttpClient();

using var response = await http.GetAsync(url);
if (!response.IsSuccessStatusCode)
{
    // ログ出力や詳細なエラーハンドリング
    throw new HttpRequestException(
        $"Error: {(int)response.StatusCode} {response.ReasonPhrase}");
}

string text = await response.Content.ReadAsStringAsync();

このパターンは、後述する「大きなファイルのストリーミング」や「進捗表示」と組み合わせる際の基本形にもなります。

小さめのファイルをバイト列として保存する(DownloadFileAsync のシンプル置き換え)

数 MB 程度までの比較的小さなファイルであれば、GetByteArrayAsync で一気にダウンロードし、そのまま File.WriteAllBytesAsync に渡すというシンプルな書き方ができます。

VB の例


Imports System.Net.Http
Imports System.IO
Imports System.Threading.Tasks

Public Class FileDownloader
    Private ReadOnly _http As HttpClient = New HttpClient()

    Public Async Function DownloadFileAsync(url As String, savePath As String) As Task
        Dim bytes As Byte() = Await _http.GetByteArrayAsync(url)
        Await File.WriteAllBytesAsync(savePath, bytes)
    End Function
End Class

C# の例


using System.Net.Http;
using System.IO;
using System.Threading.Tasks;

public class FileDownloader
{
    private readonly HttpClient _http = new HttpClient();

    public async Task DownloadFileAsync(string url, string savePath)
    {
        var bytes = await _http.GetByteArrayAsync(url);
        await File.WriteAllBytesAsync(savePath, bytes);
    }
}

この書き方は旧 DownloadFileAsync に最も近い感覚で書けますが、メモリ上に一度すべて展開するため、数百 MB クラスの大きなファイルには向きません。そういった場合は次の「ストリームで保存するパターン」を使うのが安全です。

大きなファイルをストリームで保存する(推奨パターン)

ISO イメージや動画ファイルなど、大きなサイズが想定されるダウンロードでは、「ヘッダー取得時点で制御を返し、コンテンツはストリーミングで受信」するパターンが推奨されます。HttpClient の GetAsync に HttpCompletionOption.ResponseHeadersRead を指定するのがポイントです。

VB の例(ストリームでファイル保存)


Imports System.Net.Http
Imports System.IO
Imports System.Threading.Tasks

Public Class LargeFileDownloader
    Private ReadOnly _http As HttpClient = New HttpClient()

    Public Async Function DownloadFileAsync(url As String, savePath As String) As Task
        Using response = Await _http.GetAsync(
            url, HttpCompletionOption.ResponseHeadersRead)

            response.EnsureSuccessStatusCode()

            Using input = Await response.Content.ReadAsStreamAsync()
                Using output = File.Create(savePath)
                    Await input.CopyToAsync(output)
                End Using
            End Using
        End Using
    End Function
End Class

C# の例(ストリームでファイル保存)


using System.Net.Http;
using System.IO;
using System.Threading.Tasks;

public class LargeFileDownloader
{
    private readonly HttpClient _http = new HttpClient();

    public async Task DownloadFileAsync(string url, string savePath)
    {
        using var response = await _http.GetAsync(
            url, HttpCompletionOption.ResponseHeadersRead);

        response.EnsureSuccessStatusCode();

        await using var input = await response.Content.ReadAsStreamAsync();
        await using var output = File.Create(savePath);
        await input.CopyToAsync(output);
    }
}

ここでのポイントを整理しておきます。

  • HttpCompletionOption.ResponseHeadersRead を指定することで、本文を最後まで読み切る前に制御が戻る。
  • 以降は ReadAsStreamAsync から得たストリームを、CopyToAsync でファイルストリームに転送するだけでよい。
  • ファイルサイズが大きくても、メモリ消費を抑えながらダウンロードできる。

GetStreamAsync を使った簡易版

レスポンスコードを細かく見ない、あるいは簡易ツールとしてサクッと書きたい場合は、次のように GetStreamAsync でまとめて書くこともできます。


using var http = new HttpClient();

await using var input = await http.GetStreamAsync(url);
await using var output = File.Create(savePath);
await input.CopyToAsync(output);

シンプルさを優先するならこの形でも十分ですが、業務システムではやはり EnsureSuccessStatusCode やログ出力を挟める GetAsync パターンをベースにしておく方が安全です。

進捗表示付きダウンロードの実装アイデア

WebClient では DownloadProgressChanged イベントで進捗を取るのが定番でしたが、HttpClient では「自分でストリームを読みつつ、読み込んだバイト数をカウントする」という形になります。基本の流れは次の通りです。

  1. response.Content.Headers.ContentLength からコンテンツ長(バイト)を取得する。
  2. 一定サイズのバッファ(例:81920 バイト)で ReadAsync を繰り返す。
  3. 読み込んだ総バイト数 / コンテンツ長 × 100 で進捗率を計算する。

C# の進捗付きダウンロード例(コンソール用)


using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;

public class ProgressDownloader
{
    private readonly HttpClient _http = new HttpClient();

    public async Task DownloadWithProgressAsync(string url, string savePath)
    {
        using var response = await _http.GetAsync(
            url, HttpCompletionOption.ResponseHeadersRead);

        response.EnsureSuccessStatusCode();

        var contentLength = response.Content.Headers.ContentLength;
        if (contentLength is null)
        {
            // コンテンツ長が不明な場合は、進捗を割合ではなくバイト数で表示する等の工夫が必要
            Console.WriteLine("Content-Length が取得できませんでした。");
        }

        await using var input = await response.Content.ReadAsStreamAsync();
        await using var output = File.Create(savePath);

        var buffer = new byte[81920];
        long totalRead = 0;
        int read;

        while ((read = await input.ReadAsync(buffer, 0, buffer.Length)) > 0)
        {
            await output.WriteAsync(buffer, 0, read);
            totalRead += read;

            if (contentLength.HasValue)
            {
                double progress = (double)totalRead / contentLength.Value * 100;
                Console.Write($"\r{progress:F2}%");
            }
            else
            {
                Console.Write($"\r{totalRead} bytes");
            }
        }

        Console.WriteLine("\nDownload completed.");
    }
}

WPF や WinForms、MAUI といった UI アプリケーションであれば、IProgress<T> を使って UI スレッドに進捗を投げることで、プログレスバーの更新なども容易に行えます。

HttpClient を正しく使い回すための設計

HttpClient への移行で一番多い「落とし穴」が、毎回 new HttpClient() してすぐ破棄してしまうパターンです。これはソケット枯渇などの問題を引き起こし、長期間動かすとパフォーマンス劣化や接続エラーの原因になります。

なぜ HttpClient の毎回生成がダメなのか

  • HttpClient は内部でソケットプールを持っており、短時間で大量に生成/破棄するとソケットが TIME_WAIT のまま残る。
  • 結果として、一定時間接続できなくなる「ソケット枯渇」の状態を招く可能性がある。
  • DNS 更新の反映タイミングにも影響し、接続先を変更したのに古い IP に接続し続けるといった問題が起きることもある。

シンプルな使い回しパターン

もっとも単純なのは、「アプリケーション全体で 1 つだけ HttpClient インスタンスを持つ」という設計です。


public static class HttpClientHolder
{
    public static readonly HttpClient Instance = new HttpClient();
}

あるいは DI コンテナを使わない場合でも、クラスのフィールドとして一度だけ生成しておけば十分です。


public class ApiClient
{
    private readonly HttpClient _http;

    public ApiClient()
    {
        _http = new HttpClient
        {
            Timeout = TimeSpan.FromSeconds(30)
        };
    }

    // 以降、_http を使い回す
}

IHttpClientFactory を活用する

ASP.NET Core や .NET 6 以降のコンソールアプリでは、IHttpClientFactory を使う設計が推奨されています。DNS 更新やハンドラーのライフサイクルをよしなに管理してくれるため、「長寿命に再利用しつつも、内部のソケットハンドラーは適切に入れ替える」 というバランスの取れた設計になります。


// Program.cs など
builder.Services.AddHttpClient("default", client =&gt;
{
    client.Timeout = TimeSpan.FromSeconds(30);
});

// 利用側
public class MyService
{
    private readonly HttpClient _http;

    public MyService(IHttpClientFactory factory)
    {
        _http = factory.CreateClient("default");
    }
}

既存の WebClient ベースのコードを HttpClient に置き換えるタイミングで、同時にこの設計も取り入れておくと、後々の拡張に強い構成になります。

エラー処理・タイムアウト・キャンセルをどう扱うか

WebClient から移行するときに軽視されがちですが、実務では「失敗したときにどうするか」を先に決めておくことが重要です。HttpClient では、主に次のポイントを押さえておくとよいでしょう。

ステータスコードのチェック

response.EnsureSuccessStatusCode() を使うと、ステータスコードが 2xx 以外のときに HttpRequestException を投げてくれます。


using var response = await _http.GetAsync(url);
response.EnsureSuccessStatusCode();

メッセージを細かく制御したい場合は、自前で IsSuccessStatusCode を見て例外を投げる、あるいは戻り値にラップしてハンドリングするパターンもよく使われます。

タイムアウトの設定

通信がいつまでも返ってこないケースを考慮して、タイムアウトは必ずどこかで設定しておきましょう。

  • HttpClient.Timeout プロパティに共通タイムアウトを設定する。
  • または CancellationTokenSource を使って、呼び出し側で自由にタイムアウトを制御する。

// 共通タイムアウト
_http.Timeout = TimeSpan.FromSeconds(30);

// リクエスト単位のタイムアウト
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
using var response = await _http.GetAsync(url, cts.Token);

キャンセル操作への対応

UI アプリケーションでは、「キャンセル」ボタンを押したらダウンロードを中断したい、という要望がよくあります。この場合も CancellationToken をダウンロード処理全体に渡していく形にすれば、比較的簡単に対応できます。


public async Task DownloadFileAsync(string url, string savePath, CancellationToken token)
{
    using var response = await _http.GetAsync(
        url, HttpCompletionOption.ResponseHeadersRead, token);

    response.EnsureSuccessStatusCode();

    await using var input = await response.Content.ReadAsStreamAsync(token);
    await using var output = File.Create(savePath);

    var buffer = new byte[81920];
    int read;
    while ((read = await input.ReadAsync(buffer, 0, buffer.Length, token)) &gt; 0)
    {
        await output.WriteAsync(buffer, 0, read, token);
    }
}

token.ThrowIfCancellationRequested() を要所要所で呼び出すことで、独自処理を挟みつつもキャンセルに迅速に反応させることができます。

JSON API の呼び出しと HttpClient

近年の Web API は JSON を返すものがほとんどです。WebClient 時代は、JSON.NET でパースするコードがあちこちに散らばっていた、というケースも多いでしょう。HttpClient では System.Net.Http.Json 名前空間に便利メソッドが用意されています。

JSON を取得してオブジェクトにデシリアライズする


using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;

public class WeatherResponse
{
    public string City { get; set; }
    public int Temperature { get; set; }
}

public class WeatherClient
{
    private readonly HttpClient _http;

    public WeatherClient(HttpClient http)
    {
        _http = http;
    }

    public async Task&lt;WeatherResponse&gt; GetWeatherAsync(string url)
    {
        var result = await _http.GetFromJsonAsync&lt;WeatherResponse&gt;(url);
        return result;
    }
}

JSON を POST する


using System.Net.Http.Json;

var payload = new { Name = "Sample", Enabled = true };
using var response = await _http.PostAsJsonAsync("https://example.com/api/items", payload);
response.EnsureSuccessStatusCode();

JSON のシリアライズ/デシリアライズをできるだけ一箇所にまとめておくと、のちの仕様変更(フィールド名の変更など)にも対応しやすくなります。

部分再開(レジュームダウンロード)の基本アイデア

WebClient と同様に、途中で中断したダウンロードを途中から再開したい という要件も、HttpClient では Range ヘッダーを付けることで実現できます。

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

  1. すでに保存済みのファイルサイズ(例:existingLength)を調べる。
  2. HttpRequestMessage に Range ヘッダーを設定する(bytes=existingLength-)。
  3. サーバーが Range リクエストに対応していれば、206 Partial Content が返ってくる。
  4. ファイルの末尾に追記する形で保存する。

using System.Net.Http;
using System.Net.Http.Headers;
using System.IO;
using System.Threading.Tasks;

public async Task ResumeDownloadAsync(string url, string savePath)
{
    long existingLength = 0;
    if (File.Exists(savePath))
    {
        existingLength = new FileInfo(savePath).Length;
    }

    using var request = new HttpRequestMessage(HttpMethod.Get, url);

    if (existingLength &gt; 0)
    {
        request.Headers.Range = new RangeHeaderValue(existingLength, null);
    }

    using var response = await _http.SendAsync(
        request, HttpCompletionOption.ResponseHeadersRead);

    if (response.StatusCode == System.Net.HttpStatusCode.RequestedRangeNotSatisfiable)
    {
        // サーバーが Range に対応していない、あるいは指定範囲が不正
        // 必要に応じてファイルを削除して再ダウンロードする等の対応を行う
        throw new HttpRequestException("Range request not satisfiable.");
    }

    response.EnsureSuccessStatusCode();

    await using var input = await response.Content.ReadAsStreamAsync();
    await using var output = new FileStream(
        savePath, FileMode.Append, FileAccess.Write, FileShare.None);

    await input.CopyToAsync(output);
}

Range 対応はサーバー側の実装にも依存するため、「必ずレジュームできる」という前提で設計せず、非対応だった場合にどう振る舞うか(最初からダウンロードし直す/ユーザーに確認する等)も合わせて決めておくとよいでしょう。

VB と C# のコードスタイルの違いを意識する

WebClient から HttpClient への移行では、「VB プロジェクトのコードを C# の記事を見ながら書き換える」ことも珍しくありません。その際に戸惑いやすいポイントを簡単に整理しておきます。

観点VBC#
非同期メソッドの宣言Public Async Function X() As Task(Of T)public async Task<T> X()
Await の記述Dim x = Await client.GetStringAsync(url)var x = await client.GetStringAsync(url);
Using 構文Using response = Await client.GetAsync(...)using var response = await client.GetAsync(...);
名前空間のインポートImports System.Net.Httpusing System.Net.Http;

移行作業では、まずは C# のサンプルコードで動きをつかみ、VB に書き直すという手順を踏むと理解しやすいケースが多い印象です。

既存 WebClient コードを HttpClient に置き換える手順

最後に、実際の現場での置き換え作業の流れを、ざっくりとしたステップとしてまとめておきます。

  1. WebClient を使っている箇所を洗い出す
    検索で WebClient, DownloadStringAsync, DownloadFileAsync 等を探し、影響範囲を確認します。
  2. 用途別にパターン分けをする
    文字列ダウンロードなのか、ファイル保存なのか、JSON API 呼び出しなのかを分類します。
  3. 簡単な箇所から置き換える
    ログ取得など、リスクの低い部分から GetStringAsync などに置き換えて動作確認します。
  4. 大容量ダウンロードや業務クリティカルな箇所を重点的にテストする
    ストリーミング、進捗表示、キャンセル、レジュームが必要な箇所は個別に時間をかけてテストします。
  5. HttpClient のライフタイムを統一する
    プロジェクトやレイヤー単位で「HttpClient はこう使う」というルールを決め、コンストラクタインジェクションや IHttpClientFactory の導入を検討します。

このようにパターン化して進めることで、「ところどころ HttpClient に変わっているが、設計がバラバラでメンテしづらい」という状態を避けやすくなります。

まとめ

WebClient から HttpClient への移行で一番つまずきやすいポイントは、「DownloadStringAsync / DownloadFileAsync に対応するメソッドがどれなのか」が直感的に分かりづらいことです。しかし本記事で紹介したように、対応づけてしまえばやることはシンプルです。

  • 文字列の取得は GetStringAsync(もしくは GetAsync + ReadAsStringAsync)。
  • 小さなファイルの保存は GetByteArrayAsync + File.WriteAllBytesAsync。
  • 大きなファイルの保存は GetAsync(...ResponseHeadersRead) → ReadAsStreamAsync → CopyToAsync。

あわせて、

  • HttpClient のライフタイムを意識して 使い回す設計 にすること
  • エラー処理・タイムアウト・キャンセル・進捗表示 を最初から組み込んでおくこと
  • JSON やレジュームダウンロードなど、実務的なユースケースにあわせて パターンのテンプレートを用意 しておくこと

といった点を押さえておけば、今後の保守や機能追加がぐっと楽になります。

長く運用してきたアプリほど WebClient ベースのコードが残りがちですが、HttpClient への移行は「いま少し頑張っておくことで、数年先の自分を助ける投資」です。本記事のサンプルをベースに、自身のプロジェクトに合わせたテンプレートを作っておくとよいでしょう。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次