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 Type | multipart/form-data ではなく JSON 等で送っている | MultipartFormDataContent を使う |
| 404 Not Found | URL(ポート/ルート)が違う | 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 URL | WASM の HttpClient BaseAddress は「クライアント自身」になりやすい | 別ポート API にはフル URL で POST(または設定で API BaseUrl を管理) |
| Content-Type | MIME が合わないとサーバー側で弾く設計にしやすい | 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 Core | 413 や BadRequest | FormOptions / Kestrel / エンドポイント単位の制限を見直す |
| リバースプロキシ | API に到達前に 413 | Nginx/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 が呼ばれているか、例外が出ていないか |
| 404 | URL | ポート、https/http、ルート(api/ImageUpload/upload)が一致しているか |
| 415 | Content-Type | MultipartFormDataContent を使っているか |
| 400(No file uploaded) | フォームキー名 | content.Add のキー名が “file” になっているか |
| CORS で blocked | Web API の CORS | WithOrigins/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 検証、保存先の方針(公開/非公開)を最初に決める

コメント