Blazor WebAssemblyで画像アップロード:ASP.NET Core Web API(IFormFile)実装とSwagger ISwaggerProviderエラー解決ガイド

Blazor WebAssembly(Standalone)から ASP.NET Core Web API に画像をアップロードする実装は、「multipart/form-data」「パラメーター名」「API の URL」「CORS」「サイズ制限」が噛み合っていないと簡単に失敗します。さらに Swagger で ISwaggerProvider の例外が出る場合も、Program.cs の登録不足が原因であることがほとんどです。動く一式サンプルと、つまずきポイントの潰し方をまとめます。

目次

Blazor WebAssembly から Web API に画像アップロードできない原因は「実装のピース不足」

Blazor WebAssembly(WASM)から ASP.NET Core Web API にファイルを送るとき、見た目は「POST するだけ」ですが、実際には次の条件が揃って初めて安定して動きます。

  • 送信形式が multipart/form-data になっている(JSON では送れない)
  • フォームフィールド名(キー名)が Web API 側の IFormFile 引数名 と一致している
  • Blazor WASM の HttpClient の BaseAddress ではなく、別ポートで動く API のフル URL に投げている
  • 別オリジン(ポート違い含む)なら、Web API 側で CORS 許可 が入っている
  • クライアント側(OpenReadStream)とサーバー側(FormOptions 等)で サイズ制限 が一致している

よくある「見落とし」と「症状」を、最初に表で整理します。

症状ありがちな原因対処
400 BadRequest(No file uploaded)フォームキー名が違う(例:”image” で送っている)content.Add(..., "file", ...) の “file” を API の引数名に合わせる
415 Unsupported Media Typemultipart/form-data ではなく JSON 等で送っているMultipartFormDataContent を使う
404 Not FoundURL(ポート/ルート)が違うAPI の実際のエンドポイント(例:/api/ImageUpload/upload)に合わせる
CORS error(ブラウザーのコンソールに blocked)別オリジンなのに CORS 未設定Web API 側で WithOrigins などで許可する
413 Payload Too Large / 0.5MB 付近で落ちるクライアント側 OpenReadStream の maxAllowedSize / サーバー側 MultipartBodyLengthLimit が小さい両方の上限を揃える

動く構成(ローカル開発の想定)

以下は「Blazor WASM と Web API を別ポートで動かす」想定です。ローカルで分離して動かす構成は、CORS と URL のミスが起きやすい反面、本番構成に近いのでおすすめです。

要素例役割
フロントエンドBlazor WebAssembly Standalone(例:https://localhost:5001)InputFile で画像選択 → multipart/form-data で API に POST
バックエンドASP.NET Core Web API(例:https://localhost:7133)ImageUploadController.UploadImage(IFormFile file) で受け取り保存
アップロード先API 側プロジェクトの Uploads フォルダー受信したファイルを保存

このあと紹介するコードは、上の構成でそのまま動くように揃えています(ポートはあなたの環境に合わせて置き換えてください)。

Blazor WebAssembly 側:Upload.razor(InputFile から IBrowserFile を受け取る)

Blazor のファイル選択は <InputFile> が基本です。ファイルが選択されたタイミングで InputFileChangeEventArgs が渡され、そこから IBrowserFile を取り出してアップロードします。

@page "/upload"
@using BlazorStandAloneApp.Services
@inject ImageUploadService ImageUploadService

<h3>Upload Image</h3>

<InputFile OnChange="HandleFileSelected" accept="image/*" />
<p>@message</p>

@code {
    private string message = string.Empty;

    private async Task HandleFileSelected(InputFileChangeEventArgs e)
    {
        var file = e.File;
        if (file != null)
        {
            var result = await ImageUploadService.UploadImage(file);
            message = $"Upload result: {result}";
        }
    }
}

ポイント

  • accept="image/*" を入れておくと、ファイル選択ダイアログで画像が選びやすくなります(完全な制限ではないので、サーバー側検証は必須)。
  • e.File は 1 ファイル想定です。複数ファイルにしたい場合は e.GetMultipleFiles() を使います。
  • アップロードが失敗したときは、画面に「失敗」だけ出すのではなく、HTTP ステータスやレスポンス本文(エラー詳細)を表示すると原因特定が速くなります。

複数ファイル対応の例(必要な場合のみ)

private async Task HandleFileSelected(InputFileChangeEventArgs e)
{
    foreach (var file in e.GetMultipleFiles())
    {
        var result = await ImageUploadService.UploadImage(file);
        // 必要なら結果を一覧表示するなど
    }
}

Blazor WebAssembly 側:ImageUploadService(MultipartFormDataContent で送る)

ここが一番重要です。Web API 側の IFormFile に対応させるには、multipart/form-data で送る必要があります。Blazor WASM では次のように MultipartFormDataContent を作って PostAsync します。

using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Forms;

public class ImageUploadService
{
    private readonly HttpClient _http;

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

    public async Task<string> UploadImage(IBrowserFile file)
    {
        var content = new MultipartFormDataContent();

        // 最大 10MB まで許可(クライアント側)
        using var fileStream = file.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024);

        var fileContent = new StreamContent(fileStream);
        fileContent.Headers.ContentType = new MediaTypeHeaderValue(file.ContentType);

        // ★ここが最重要:キー名 "file" は Web API の UploadImage(IFormFile file) の引数名に合わせる
        content.Add(fileContent, "file", file.Name);

        // Web API が別ポートの場合、BaseAddress ではなくフル URL を指定するのが分かりやすい
        var response = await _http.PostAsync(
            "https://localhost:7133/api/ImageUpload/upload",
            content);

        if (response.IsSuccessStatusCode)
        {
            return "Upload successful!";
        }

        // 失敗時はレスポンス本文も返すと原因調査が楽
        var errorText = await response.Content.ReadAsStringAsync();
        return $"Upload failed. Status={(int)response.StatusCode} Body={errorText}";
    }
}

ここで詰まりやすい注意点

注意点なぜ重要か具体策
フォームキー名API が受け取る名前と一致しないと、file が null になるcontent.Add(..., "file", ...) の “file” を IFormFile の引数名と一致させる
OpenReadStream の上限指定しないと小さい上限で例外になることがあるmaxAllowedSize を明示(例:10MB)
API URLWASM の HttpClient BaseAddress は「クライアント自身」になりやすい別ポート API にはフル URL で POST(または設定で API BaseUrl を管理)
Content-TypeMIME が合わないとサーバー側で弾く設計にしやすいfileContent.Headers.ContentType に file.ContentType を設定

より実戦的:API のレスポンス JSON を読み取る

Web API 側が return Ok(new { FilePath = filePath }); のように JSON を返すなら、Blazor 側も JSON を読んで表示した方が確認が簡単です。

using System.Net.Http.Json;

public async Task<string> UploadImage(IBrowserFile file)
{
    var content = new MultipartFormDataContent();

    using var fileStream = file.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024);
    var fileContent = new StreamContent(fileStream);
    fileContent.Headers.ContentType = new MediaTypeHeaderValue(file.ContentType);

    content.Add(fileContent, "file", file.Name);

    var response = await _http.PostAsync("https://localhost:7133/api/ImageUpload/upload", content);

    if (!response.IsSuccessStatusCode)
    {
        var errorText = await response.Content.ReadAsStringAsync();
        return $"NG: {(int)response.StatusCode} {errorText}";
    }

    // 例:{ "filePath": "Uploads/xxxx.png" } のようなレスポンスを想定
    var json = await response.Content.ReadFromJsonAsync<Dictionary<string, string>>();
    if (json != null && json.TryGetValue("filePath", out var path))
    {
        return $"OK: {path}";
    }

    return "OK";
}

Blazor WebAssembly 側:Program.cs(DI 登録がないと @inject で解決できない)

Blazor WASM の @inject ImageUploadService は DI(依存性注入)に登録されていないと失敗します。最低限、HttpClient とサービスを登録しておきます。

using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using BlazorApp;
using BlazorApp.Services;

var builder = WebAssemblyHostBuilder.CreateDefault(args);

builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

// HttpClient を DI に登録(BaseAddress はクライアント自身の URL になりやすい)
builder.Services.AddScoped(sp =>
    new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

// 画像アップロード用サービスを登録
builder.Services.AddScoped<ImageUploadService>();

await builder.Build().RunAsync();

おすすめ:API のベース URL を appsettings.json で管理する

フル URL をコードに直書きすると、ポートが変わるたびに修正が必要になります。ローカルと本番で切り替えるなら、WASM 側の wwwroot/appsettings.json に API の URL を持たせるのが運用しやすいです。

{
  "ApiBaseUrl": "https://localhost:7133"
}

そしてサービス側で組み立てます(例)。

public class ImageUploadService
{
    private readonly HttpClient _http;
    private readonly string _apiBaseUrl;

    public ImageUploadService(HttpClient http, IConfiguration config)
    {
        _http = http;
        _apiBaseUrl = config["ApiBaseUrl"] ?? "";
    }

    public async Task<string> UploadImage(IBrowserFile file)
    {
        var content = new MultipartFormDataContent();

        using var fileStream = file.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024);
        var fileContent = new StreamContent(fileStream);
        fileContent.Headers.ContentType = new MediaTypeHeaderValue(file.ContentType);

        content.Add(fileContent, "file", file.Name);

        var url = $"{_apiBaseUrl.TrimEnd('/')}/api/ImageUpload/upload";
        var response = await _http.PostAsync(url, content);

        return response.IsSuccessStatusCode ? "Upload successful!" : "Upload failed.";
    }
}

この形にしておくと、環境ごとの URL 差し替えが簡単になり、デプロイ後の「URL 間違い」による 404 を減らせます。

Web API 側:ImageUploadController(Uploads に保存する)

Web API 側の最小実装は、IFormFile を受け取って保存するだけです。ただし実運用を意識すると、保存先パスとファイル名は少し工夫した方が安全です。

まずは動く最小例(質問の形に近い)

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.IO;
using System.Threading.Tasks;

[Route("api/[controller]")]
[ApiController]
public class ImageUploadController : ControllerBase
{
[HttpPost("upload")]
public async Task UploadImage(IFormFile file)
{
if (file == null || file.Length == 0)
return BadRequest("No file uploaded.");


    var uploadDir = Path.Combine("Uploads");
    if (!Directory.Exists(uploadDir))
    {
        Directory.CreateDirectory(uploadDir);
    }

    var filePath = Path.Combine(uploadDir, file.FileName);

    using (var stream = new FileStream(filePath, FileMode.Create))
    {
        await file.CopyToAsync(stream);
    }

    return Ok(new { FilePath = filePath });
}


}

この形でも動きますが、次の観点で改善すると事故が減ります。

  • 相対パス問題:Path.Combine("Uploads") は実行時の作業ディレクトリ依存になりやすい
  • ファイル名の安全性:file.FileName をそのまま使うと、衝突や想定外の名前(パス要素など)のリスクがある
  • 拡張子/MIME 検証:画像以外をアップロードされる可能性がある

推奨:ContentRootPath を基準に保存し、ファイル名は Guid 化する

より安全で扱いやすい実装例です。Swagger でもアップロード UI が出やすいように、[FromForm] と [Consumes("multipart/form-data")] も付けています。

using Microsoft.AspNetCore.Mvc;

[Route("api/[controller]")]
[ApiController]
public class ImageUploadController : ControllerBase
{
    private readonly IWebHostEnvironment _env;

    public ImageUploadController(IWebHostEnvironment env)
    {
        _env = env;
    }

    [HttpPost("upload")]
    [Consumes("multipart/form-data")]
    public async Task<IActionResult> UploadImage([FromForm] IFormFile file)
    {
        if (file == null || file.Length == 0)
            return BadRequest("No file uploaded.");

        // 簡易チェック(必要に応じて拡張)
        var allowedExts = new[] { ".jpg", ".jpeg", ".png", ".gif", ".webp" };
        var ext = Path.GetExtension(file.FileName).ToLowerInvariant();
        if (!allowedExts.Contains(ext))
            return BadRequest("Unsupported file type.");

        // 保存先(API プロジェクト直下の Uploads)
        var uploadDir = Path.Combine(_env.ContentRootPath, "Uploads");
        Directory.CreateDirectory(uploadDir);

        // ファイル名衝突を避ける
        var safeFileName = $"{Guid.NewGuid():N}{ext}";
        var filePath = Path.Combine(uploadDir, safeFileName);

        await using (var stream = System.IO.File.Create(filePath))
        {
            await file.CopyToAsync(stream);
        }

        // クライアントに返すのは相対パスや識別子に留めるのが扱いやすい
        var relativePath = $"Uploads/{safeFileName}";
        return Ok(new { filePath = relativePath });
    }
}

Uploads を「公開したい」か「非公開で保持したい」かで設計が変わる

アップロードされた画像をそのまま URL で参照できるようにしたい場合、保存先は wwwroot 配下にし、静的ファイルとして配信する構成がシンプルです。非公開で保持し、認可したユーザーだけに返すなら、wwwroot 外に置き、ダウンロード用 API を作る方が安全です。

目的保存先配信方法向いているケース
公開画像として配信wwwroot/uploads などUseStaticFiles で配信プロフィール画像、ブログ画像など
非公開で保持ContentRootPath/Uploads など認可付きの API 経由で返す契約書、社内資料、限定公開コンテンツ

大きいファイルを扱う場合の設定(クライアント+サーバー両方)

「10MB まで OK」にしたつもりでも、片側だけ設定して片側が小さいままだと失敗します。クライアント側(Blazor)とサーバー側(Web API)で、上限を揃えるのが基本です。

クライアント側(Blazor):OpenReadStream の maxAllowedSize

Blazor WASM はブラウザーのファイルアクセス制限があるため、file.OpenReadStream(maxAllowedSize: ...) を明示しないと、想定より小さい上限で例外になって止まることがあります。アップロードしたい上限に合わせて必ず指定しましょう。

using var stream = file.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024); // 10MB

サーバー側(Web API):MultipartBodyLengthLimit を調整

ASP.NET Core 側は、フォームの最大サイズ制限が噛むことがあります。必要に応じて上限を上げます(例:10MB)。

using Microsoft.AspNetCore.Http.Features;

builder.Services.Configure<FormOptions>(options =>
{
    options.MultipartBodyLengthLimit = 10 * 1024 * 1024; // 10MB
});

実環境で IIS やリバースプロキシ(Nginx など)を使う場合、そちらにもアップロード上限があることが多いので、413 が出るときは「どこで弾かれているか」を切り分けてください。

場所弾かれ方の例対処の方向性
ブラウザー/Blazor例外が発生して送信自体が止まるOpenReadStream の maxAllowedSize を増やす
ASP.NET Core413 や BadRequestFormOptions / Kestrel / エンドポイント単位の制限を見直す
リバースプロキシAPI に到達前に 413Nginx/IIS 側の上限も増やす

CORS 設定:別ポートは「別オリジン」なので必須

Blazor WASM と Web API を別ポートで動かす場合、ブラウザーから見ると別オリジンです。つまり Web API 側で CORS を許可しないと、リクエストはブラウザーに止められます(サーバーまで届かないこともあります)。

開発用(最小):とりあえず動かす

開発中だけ切り分けのために全許可することはありますが、本番では推奨されません。

builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowAll", policy =>
        policy.AllowAnyOrigin()
              .AllowAnyMethod()
              .AllowAnyHeader());
});

推奨:フロントの URL を限定する

本番運用やチーム開発なら、フロントエンドの URL に絞るのが一般的です(例として localhost を記載)。

builder.Services.AddCors(options =>
{
    options.AddPolicy("Frontend", policy =>
        policy.WithOrigins("https://localhost:5001")
              .AllowAnyMethod()
              .AllowAnyHeader());
});

そしてミドルウェアで適用します。

app.UseCors("Frontend");

注意:Cookie 認証などで資格情報(credentials)を使う場合

将来的に Cookie 認証やセッション等で credentials を使う場合、CORS はさらに制約があります。AllowAnyOrigin() と AllowCredentials() は同時に使えません。必要になったら、必ず WithOrigins で明示し、フロント側も資格情報付きの送信に揃えます。

HTTPS と Mixed Content:ローカルでも油断すると詰まる

ローカル開発で意外と多いのが、フロントは https、API は http(またはその逆)になっていてブラウザーにブロックされるケースです。画像アップロードはプリフライト(OPTIONS)が走ることもあり、余計に症状が分かりにくくなります。

  • フロントが https のとき、API もできれば https に揃える
  • 開発証明書(ASP.NET Core の dev-certs)が未信頼なら、ブラウザーが警告/ブロックすることがある
  • Network タブで「リクエストが出ているか」「OPTIONS が失敗していないか」を確認する

Swagger で「Unable to resolve service for type ‘ISwaggerProvider’」が出る理由

Swagger の画面(例:/swagger/index.html)にアクセスしたら、次のような例外が出ることがあります。

InvalidOperationException: Unable to resolve service for type
'Swashbuckle.AspNetCore.Swagger.ISwaggerProvider'
while attempting to invoke middleware 'SwaggerMiddleware'.

このエラーはほぼ確実に、Swagger 用サービスの DI 登録(AddSwaggerGen)が不足していることが原因です。つまり、app.UseSwagger() は書いたが、builder.Services.AddSwaggerGen() を書き忘れた(または条件分岐で実行されていない)状態です。

正しい Program.cs(コントローラー + Swagger + CORS の最小構成)

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

// Swagger 用サービス(★これがないと ISwaggerProvider が解決できずに落ちる)
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// CORS(例:開発用に全許可)
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll", policy =>
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseCors("AllowAll");

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

この形に揃えると、Swagger の SwaggerMiddleware が必要とする ISwaggerProvider が DI に登録され、例外は解消します。

NuGet パッケージの確認(csproj)

Swashbuckle が入っていない、または参照が壊れている場合もあるので、.csproj に以下があるか確認してください。

<ItemGroup>
  <PackageReference Include="Swashbuckle.AspNetCore" Version="7.3.1" />
</ItemGroup>

パッケージを追加・更新したら、ローカルのビルドキャッシュが悪さをすることもあるため、クリーン → リビルド → 再起動の順でやり直すと安定します。

Swagger UI から画像アップロード API を試すときのコツ

Swagger UI でファイルアップロードを試す場合、コントローラー側が multipart/form-data を想定している必要があります。一般に次の条件が揃っていると、Swagger UI にファイル選択 UI が出やすくなります。

  • アクション引数が IFormFile になっている
  • [FromForm] を付けてフォーム入力であることを明示している
  • [Consumes("multipart/form-data")] を付けてコンテンツタイプを明示している

例(再掲):

[HttpPost("upload")]
[Consumes("multipart/form-data")]
public async Task<IActionResult> UploadImage([FromForm] IFormFile file)
{
    ...
}

Swagger UI では、POST /api/ImageUpload/upload を開き、file パラメーターでファイルを選択して Execute します。ここで成功すれば、サーバー側の実装とルーティングはほぼ正しいので、次に疑うべきは Blazor 側(URL・CORS・キー名)です。

実際に詰まったときのデバッグ手順(最短で原因に辿り着く)

「動かない」ときは、闇雲にコードをいじるより、通信がどう失敗しているかを先に固定すると早いです。以下の順で確認すると、原因がほぼ一本道になります。

手順 1:Swagger で API 単体が動くか確認

  • Swagger UI から画像をアップロードして 200 OK になるか
  • Uploads フォルダーに保存されているか

手順 2:Blazor 側の Network タブでステータスコードを見る

ブラウザーの開発者ツール(Network)で、POST /api/ImageUpload/upload の結果を確認します。

ステータス/症状疑う場所見直すポイント
リクエスト自体が出ていないBlazor UI / 例外HandleFileSelected が呼ばれているか、例外が出ていないか
404URLポート、https/http、ルート(api/ImageUpload/upload)が一致しているか
415Content-TypeMultipartFormDataContent を使っているか
400(No file uploaded)フォームキー名content.Add のキー名が “file” になっているか
CORS で blockedWeb API の CORSWithOrigins/AllowAnyOrigin、UseCors の位置、OPTIONS が通るか
413サイズ制限OpenReadStream と FormOptions の上限を揃える

手順 3:失敗時はレスポンス本文を画面に出す

「Upload failed.」だけだと原因が分かりません。Blazor 側で StatusCode と ReadAsStringAsync() を表示しておくと、サーバーが返しているエラーメッセージ(BadRequest の理由など)が一発で見えます。

まとめ:Blazor WebAssembly の画像アップロードと Swagger エラーは“整合性”で解決できる

  • Blazor WASM → Web API の画像アップロードは MultipartFormDataContent が必須
  • content.Add(..., "file", ...) の “file” は IFormFile 引数名と一致させる
  • API が別ポートなら、Blazor の HttpClient BaseAddress ではなく API のフル URL(または設定値)で POST する
  • 別オリジンなら Web API 側に CORS 設定が必要(本番は WithOrigins 推奨)
  • Swagger の ISwaggerProvider 例外はほぼ AddSwaggerGen の登録不足。Program.cs を揃える
  • 実運用を考えるなら、ファイル名は Guid 化、拡張子/MIME 検証、保存先の方針(公開/非公開)を最初に決める

この記事を書いた人

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

コメント

コメントする

目次