Blazorでファイルをデータベースに保存する完全ガイド|WASM/Server対応・SQL Server/EF Core/ADO.NETストリーミング

Blazor WebAssembly/Blazor Server のどちらでも、ユーザーのローカルから選択されたファイルを安全に受け取り、SQL Server などのデータベースへ保存・配信するには「UI・検証・ストレージ・配信」の4点を設計する必要があります。本記事では小〜中サイズは EF Core、数十MB〜GB 級は ADO.NET ストリーミングで処理する実装を、サンプルコード・スキーマ・設計判断の材料と共に網羅的に解説します。

目次

Blazor でファイルをデータベースに保存する全体像

最初に、この記事で解決する課題をまとめます。

手順内容
1. アップロード UIBlazor の <InputFile> コンポーネントを使用。OnChange で IBrowserFile を受け取り、検証・送信を開始。
2. ファイル読み取りfile.OpenReadStream(maxAllowedSize) で Stream を取得。小容量は MemoryStream にコピー、⼤容量はストリームのまま送信・保存。
3. データベース設計本体は varbinary(max)。MIME や拡張子・サイズ・作成日時は nvarchar/bigint/ datetime2 等でメタデータ列を持つ。
4. 保存処理小〜中サイズは EF Core の byte[] マッピングで INSERT。数十MB以上は ADO.NET による UPDATE ... .WRITE() でチャンク書き込みし、サーバーのメモリを消費しない。
5. 取得/表示配信用エンドポイントからストリームで返却。Results.Stream / FileStreamResult と Range 対応でレジューム・シークを有効化。
6. 大容量/大量ファイルの代案DB 膨張・バックアップ時間を避けるなら、ファイル実体は外部ストレージ(ファイルシステム・Blob)に保存し、DB には URI とハッシュ等のメタデータのみ保存。

以降は、WASM と Server 双方で動く UI、SQL Server を例にした DB スキーマ、EF Core/ADO.NET の保存実装、配信・セキュリティ・運用ノウハウ の順に具体化していきます。

前提と方針(WASM と Server の違いを押さえる)

  • Blazor WebAssembly:ブラウザ内で実行。<InputFile> から得た Stream を HTTP API へ送ってサーバー側で DB 保存する。
  • Blazor Server:イベント処理はサーバー。IBrowserFile.OpenReadStream() でクライアント→サーバーへストリーミング転送されるため、そのまま DB へ流し込み可能。
  • データベースは SQL Server を例に記述(varbinary(max))。PostgreSQL なら bytea、MySQL なら LONGBLOB などで読み替えればOK。

DB スキーマ(SQL Server)

CREATE TABLE dbo.Files (
    Id          uniqueidentifier NOT NULL CONSTRAINT PK_Files PRIMARY KEY,
    FileName    nvarchar(260)    NOT NULL,
    ContentType nvarchar(127)    NOT NULL,
    Length      bigint           NOT NULL,
    Blob        varbinary(max)       NULL,
    Sha256      char(64)             NULL, -- 検知・重複排除用(任意)
    CreatedAt   datetime2(7)     NOT NULL CONSTRAINT DF_Files_CreatedAt DEFAULT (sysutcdatetime()),
    RowVersion  rowversion            NOT NULL
);
GO
CREATE INDEX IX_Files_CreatedAt ON dbo.Files (CreatedAt DESC);
CREATE INDEX IX_Files_Sha256     ON dbo.Files (Sha256);

設計のポイント

  • ファイル名は Windows 互換の 260 文字を上限に例示。必要に応じて拡張。
  • ハッシュ(Sha256)は重複検出や改ざん検出の運用に有効。
  • RowVersion は同時更新保護(楽観的同時実行制御)に利用。

小〜中サイズ(〜数MB)向け:EF Core で byte[] 保存

Entity と DbContext

public sealed class DbFile
{
    public Guid Id { get; set; }
    public string FileName { get; set; } = default!;
    public string ContentType { get; set; } = default!;
    public long Length { get; set; }
    public byte[] Blob { get; set; } = default!;
    public DateTime CreatedAt { get; set; }
    public byte[] RowVersion { get; set; } = default!;
}

public sealed class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions options) : base(options) { }
public DbSet Files => Set();


protected override void OnModelCreating(ModelBuilder b)
{
    b.Entity<DbFile>(e =>
    {
        e.Property(x => x.Blob).HasColumnType("varbinary(max)").IsRequired();
        e.Property(x => x.FileName).HasMaxLength(260).IsRequired();
        e.Property(x => x.ContentType).HasMaxLength(127).IsRequired();
        e.Property(x => x.CreatedAt).HasDefaultValueSql("sysutcdatetime()");
        e.Property(x => x.RowVersion).IsRowVersion();
    });
}


} 

保存サービス(EF Core)

小〜中サイズは MemoryStream に取り込み byte[] で保存するのが最もシンプルです。

public interface IFileStorage
{
    Task<Guid> SaveAsync(Stream input, string fileName, string contentType, long length, CancellationToken ct);
    Task<(string FileName, string ContentType, long Length)> GetMetaAsync(Guid id, CancellationToken ct);
    Task CopyToAsync(Guid id, Stream output, CancellationToken ct);
}

public sealed class EfCoreFileStorage : IFileStorage
{
private readonly AppDbContext _db;
public EfCoreFileStorage(AppDbContext db) => _db = db;


public async Task<Guid> SaveAsync(Stream input, string fileName, string contentType, long length, CancellationToken ct)
{
    using var ms = new MemoryStream(capacity: (int)Math.Min(length, 8 * 1024 * 1024)); // 予測容量
    await input.CopyToAsync(ms, ct);
    var entity = new DbFile
    {
        Id = Guid.NewGuid(),
        FileName = fileName,
        ContentType = string.IsNullOrWhiteSpace(contentType) ? "application/octet-stream" : contentType,
        Length = length,
        Blob = ms.ToArray()
    };
    _db.Add(entity);
    await _db.SaveChangesAsync(ct);
    return entity.Id;
}

public async Task<(string FileName, string ContentType, long Length)> GetMetaAsync(Guid id, CancellationToken ct)
    => await _db.Files
        .Where(f => f.Id == id)
        .Select(f => new ValueTuple<string, string, long>(f.FileName, f.ContentType, f.Length))
        .SingleAsync(ct);

public async Task CopyToAsync(Guid id, Stream output, CancellationToken ct)
{
    var blob = await _db.Files
        .Where(f => f.Id == id)
        .Select(f => f.Blob)
        .SingleAsync(ct);
    using var input = new MemoryStream(blob, writable: false);
    await input.CopyToAsync(output, 81920, ct);
}


} 

備考:EF Core は byte[] を全読み込みするため、巨大ファイルではメモリ圧迫が発生します。数十 MB を超える可能性があるなら、次章の ADO.NET ストリーミングへ移行してください。

数十MB〜GB 向け:ADO.NET(UPDATE ... .WRITE())でチャンク保存

SQL Server の varbinary(max) 列は、チャンク(断片)を連結する .WRITE() 構文を備えます。これを使えばアプリのメモリに乗せずに DB へ追加書き込みが可能です。

using System.Data;
using Microsoft.Data.SqlClient;

public sealed class SqlChunkedFileStorage : IFileStorage
{
    private readonly string _conStr;
    public SqlChunkedFileStorage(IConfiguration cfg) =&gt; _conStr = cfg.GetConnectionString("Default")!;

    public async Task&lt;Guid&gt; SaveAsync(Stream input, string fileName, string contentType, long length, CancellationToken ct)
    {
        var id = Guid.NewGuid();
        await using var con = new SqlConnection(_conStr);
        await con.OpenAsync(ct);
        await using var tx = await con.BeginTransactionAsync(ct);

        // まずメタデータだけを INSERT(本体は空の 0x)
        await using (var insert = con.CreateCommand())
        {
            insert.Transaction = (SqlTransaction)tx;
            insert.CommandText = @"INSERT INTO dbo.Files (Id, FileName, ContentType, Length, Blob)
                                   VALUES (@Id, @FileName, @ContentType, @Length, 0x)";
            insert.Parameters.Add(new SqlParameter("@Id", SqlDbType.UniqueIdentifier) { Value = id });
            insert.Parameters.Add(new SqlParameter("@FileName", SqlDbType.NVarChar, 260) { Value = fileName });
            insert.Parameters.Add(new SqlParameter("@ContentType", SqlDbType.NVarChar, 127) { Value = string.IsNullOrWhiteSpace(contentType) ? "application/octet-stream" : contentType });
            insert.Parameters.Add(new SqlParameter("@Length", SqlDbType.BigInt) { Value = length });
            await insert.ExecuteNonQueryAsync(ct);
        }

        var buffer = new byte[1024 * 1024]; // 1MB チャンク(運用で調整)
        int read;
        while ((read = await input.ReadAsync(buffer, 0, buffer.Length, ct)) &gt; 0)
        {
            await using var update = con.CreateCommand();
            update.Transaction = (SqlTransaction)tx;
            update.CommandText = @"UPDATE dbo.Files SET Blob .WRITE(@chunk, NULL, 0) WHERE Id = @id";
            // 直近のチャンクだけを送る
            var chunk = new byte[read];
            Buffer.BlockCopy(buffer, 0, chunk, 0, read);
            update.Parameters.Add(new SqlParameter("@chunk", SqlDbType.VarBinary, read) { Value = chunk });
            update.Parameters.Add(new SqlParameter("@id", SqlDbType.UniqueIdentifier) { Value = id });
            await update.ExecuteNonQueryAsync(ct);
        }

        await tx.CommitAsync(ct);
        return id;
    }

    public async Task&lt;(string FileName, string ContentType, long Length)&gt; GetMetaAsync(Guid id, CancellationToken ct)
    {
        await using var con = new SqlConnection(_conStr);
        await con.OpenAsync(ct);
        await using var cmd = con.CreateCommand();
        cmd.CommandText = "SELECT FileName, ContentType, Length FROM dbo.Files WHERE Id = @Id";
        cmd.Parameters.Add(new SqlParameter("@Id", SqlDbType.UniqueIdentifier) { Value = id });
        await using var rd = await cmd.ExecuteReaderAsync(ct);
        if (!await rd.ReadAsync(ct)) throw new FileNotFoundException();
        return (rd.GetString(0), rd.GetString(1), rd.GetInt64(2));
    }

    // DB からレスポンスへ直接ストリーミング(SequentialAccess)
    public async Task CopyToAsync(Guid id, Stream output, CancellationToken ct)
    {
        await using var con = new SqlConnection(_conStr);
        await con.OpenAsync(ct);
        await using var cmd = con.CreateCommand();
        cmd.CommandText = "SELECT Blob FROM dbo.Files WHERE Id = @Id";
        cmd.Parameters.Add(new SqlParameter("@Id", SqlDbType.UniqueIdentifier) { Value = id });

        await using var rd = await cmd.ExecuteReaderAsync(CommandBehavior.SequentialAccess, ct);
        if (!await rd.ReadAsync(ct)) throw new FileNotFoundException();

        await using var dbStream = rd.GetStream(0);
        await dbStream.CopyToAsync(output, 81920, ct);
    }
}

運用ヒント

  • ログ肥大を避けるにはチャンクサイズを適切化(1〜4MB など)。
  • 1 件の巨大アップロードはトランザクション時間が長くなるため、タイムアウトに注意。
  • 読み出しは SequentialAccess + GetStream でメモリ使用を最小化。

API(Minimal API)でアップロード/ダウンロードを実装

Program.cs(共通設定)

var builder = WebApplication.CreateBuilder(args);

// 大容量に備えリクエスト制限を調整(必要な範囲で)
builder.Services.Configure(o =>
{
o.MultipartBodyLengthLimit = 1L * 1024 * 1024 * 1024; // 1GB
});
builder.WebHost.ConfigureKestrel(o =>
{
o.Limits.MaxRequestBodySize = 1L * 1024 * 1024 * 1024; // リバースプロキシ/IIS使用時はそちら側も調整
});

// DI
builder.Services.AddDbContext(opt =>
opt.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddScoped(); // もしくは EfCoreFileStorage

var app = builder.Build();
app.UseRouting(); 

POST /api/files(アップロード)

app.MapPost("/api/files", async (HttpRequest req, IFileStorage storage, CancellationToken ct) =>
{
    var form = await req.ReadFormAsync(ct);
    var file = form.Files["file"];
    if (file is null) return Results.BadRequest("file is required.");
    if (file.Length == 0) return Results.BadRequest("empty file.");


// サーバー側 MIME/署名検証(後述のヘルパーを利用してもよい)
await using var stream = file.OpenReadStream(); // 必要に応じ max を指定
var id = await storage.SaveAsync(stream, file.FileName, file.ContentType ?? "application/octet-stream", file.Length, ct);
return Results.Json(new { id });


}); 

GET /api/files/{id}(ダウンロード/配信)

app.MapGet("/api/files/{id:guid}", async (Guid id, IFileStorage storage, CancellationToken ct) =>
{
    (string fileName, string contentType, long length) = await storage.GetMetaAsync(id, ct);


return Results.Stream(
    async (body, _) => await storage.CopyToAsync(id, body, ct),
    contentType,
    fileDownloadName: fileName,
    enableRangeProcessing: true  // Range対応でレジューム/シークが可能
);


}); 

フロントエンド(Blazor)実装:<InputFile> とアップロード

共通の Razor コンポーネント例

@page "/upload"
@inject HttpClient Http



@if (progressPercent > 0)
{
進捗: @progressPercent%
}

@code {
private int progressPercent;


private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
    foreach (var file in e.GetMultipleFiles())
    {
        // サイズ上限(クライアント側の早期リジェクト)
        const long maxAllowedSize = 1L * 1024 * 1024 * 1024; // 1GB
        if (file.Size > maxAllowedSize)
        {
            // ユーザーに通知
            continue;
        }

        // IBrowserFile -> Stream
        await using var src = file.OpenReadStream(maxAllowedSize);

        // 進捗付きで multipart/form-data に載せる
        var content = new MultipartFormDataContent();
        var progress = new Progress<long>(sent =>
        {
            progressPercent = (int)(sent * 100 / file.Size);
            InvokeAsync(StateHasChanged);
        });
        var streamContent = new ProgressStreamContent(src, file.ContentType ?? "application/octet-stream", file.Size, progress);
        content.Add(streamContent, "file", file.Name);

        var res = await Http.PostAsync("api/files", content);
        res.EnsureSuccessStatusCode();
        progressPercent = 0;
    }
}

// HttpContent のシリアライズ過程で Read -> Write 毎に進捗を出すラッパー
private sealed class ProgressStreamContent : HttpContent
{
    private readonly Stream _source;
    private readonly IProgress<long>? _progress;
    private readonly long _length;
    private readonly int _bufferSize;

    public ProgressStreamContent(Stream source, string contentType, long length, IProgress<long>? progress = null, int bufferSize = 81920)
    {
        _source = source;
        _progress = progress;
        _length = length;
        _bufferSize = bufferSize;
        Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType);
    }

    protected override async Task SerializeToStreamAsync(Stream target, TransportContext? context)
    {
        var buffer = new byte[_bufferSize];
        int read;
        long total = 0;
        while ((read = await _source.ReadAsync(buffer, 0, buffer.Length)) > 0)
        {
            await target.WriteAsync(buffer.AsMemory(0, read));
            total += read;
            _progress?.Report(total);
        }
    }

    protected override bool TryComputeLength(out long length) { length = _length; return true; }
}


} 

Blazor Server の補足:非常に大きいファイルを扱う場合は、サーバー側の受信上限(SignalR の MaximumReceiveMessageSize など)を必要範囲で拡張してください。WASM ではバックエンド API 側の MultipartBodyLengthLimit 等を調整します。

MIME/拡張子検証・署名(マジックナンバー)チェック

クライアント申告の Content-Type や拡張子に依存せず、サーバーで先頭バイトを検査しましょう。

形式先頭シグネチャ
PNG89 50 4E 47 0D 0A 1A 0A
JPEGFF D8 FF
PDF25 50 44 46 2D
ZIP/Office50 4B 03 04
GIF47 49 46 38
public static class MimeSniffer
{
    public static bool LooksLikePdf(ReadOnlySpan&lt;byte&gt; head)
        =&gt; head.Length &gt;= 5 &amp;&amp; head[0] == 0x25 &amp;&amp; head[1] == 0x50 &amp;&amp; head[2] == 0x44 &amp;&amp; head[3] == 0x46 &amp;&amp; head[4] == 0x2D;

    public static async Task&lt;(string Mime, bool Safe)&gt; DetectAsync(Stream s, CancellationToken ct)
    {
        var buf = new byte[16];
        var read = await s.ReadAsync(buf.AsMemory(0, buf.Length), ct);
        s.Position = 0; // 後続処理のため巻き戻し
        if (read &gt;= 8 &amp;&amp; buf[0] == 0x89 &amp;&amp; buf[1] == 0x50) return ("image/png", true);
        if (read &gt;= 3 &amp;&amp; buf[0] == 0xFF &amp;&amp; buf[1] == 0xD8) return ("image/jpeg", true);
        if (LooksLikePdf(buf)) return ("application/pdf", true);
        // 未知のものは octet-stream とする
        return ("application/octet-stream", false);
    }
}

運用 TIP:検知結果が Safe=false の場合は保留や追加審査(サンドボックスで開く、ウイルススキャンを強化する等)を行うと現場で事故が減ります。

ウイルス対策・セキュリティ・コンプライアンス

  • ウイルススキャン:アップロード直後にスキャン。スキャンは非同期キュー(メッセージング)で行い、結果 NG はダウンロード不可に。
  • Content-Disposition:配信時は attachment でダウンロードさせ、XSS の踏み台化を防止。
  • X-Content-Type-Options:nosniff を付与。
  • サーバー側バリデーション:拡張子・署名・サイズ・禁止拡張子(.exe 等)をチェック。
  • 暗号化:DB 暗号化(TDE)やアプリ層 AES 暗号化(鍵は KMS)で保護。
  • 監査・追跡:アップロード者・時刻・ハッシュ・元 IP を監査ログへ。

大容量・大量ファイルの代案(外部ストレージ + DB メタデータ)

DB に BLOB を入れるとバックアップ時間やスケールに影響します。以下の折衷構成が実務で安定します。

保存方式長所短所向き/不向き
DB に実体(本記事の方法)一貫したトランザクション・整合性。バックアップ一体化。DB 増大・バックアップ肥大・I/O コスト。台数少・件数少、中規模まで。
外部ストレージに実体、DB に URI/ハッシュのみスケール容易・CDN 併用・安価。二相整合性や署名 URL の実装が必要。大容量・大量配信、マイクロサービス。

Blazor Server 特有の設定(必要に応じて)

// Program.cs
builder.Services.AddServerSideBlazor()
    .AddHubOptions(o =&gt;
    {
        // 受信メッセージ上限(必要に応じてだけ拡張)
        o.MaximumReceiveMessageSize = 64L * 1024 * 1024; // 例: 64MB
    });

ただし、単純に上限を上げるだけでなく、巨大ファイルは API へ直接送る(WASM と同様の経路に寄せる)設計も検討してください。

ダウンロードの最適化:Range/キャッシュ/ETag

  • Range 対応:Results.Stream(..., enableRangeProcessing: true) でメディアのシークが快適に。
  • キャッシュ制御:公開可否に応じて Cache-Control: private, max-age=... or no-store。
  • ETag:ハッシュを ETag にすれば、整合性検証と無駄な再送防止が両立。

アップロード前チェックリスト

  • 拡張子と MIME の両方をチェックし、許可リスト方式で絞る。
  • サイズ上限(クライアントとサーバーの両方)を明示。
  • サーバー側でのタイムアウト・同時実行数を調整。
  • 監査ログ・通知(失敗/成功)・リトライ戦略を用意。
  • ウイルススキャンのバイパス経路が無いことを確認。

トラブルシューティング

症状原因の典型対処
すぐに 413 / Request too largeサーバーの MaxRequestBodySize / MultipartBodyLengthLimit が小さいProgram.cs で上限調整。リバースプロキシ/IIS 側設定も合わせて。
メモリが膨らむEF Core で巨大ファイルを byte[] 化ADO.NET のチャンク書き込みに切替。
ダウンロードが重い全バイトを一度に読み出しSequentialAccess と GetStream() でストリーミング。
ファイル種別が偽装拡張子しか見ていない署名チェック + サーバー側 MIME 判定を追加。

実装を丸ごとつなぐ:最小構成の全体例

以下は「Blazor(WASM/Server 共通 UI)」+「Minimal API」+「SQL Server(チャンク保存)」を組み合わせた最小構成の抜粋です。

Razor(アップロード UI)

&lt;InputFile OnChange="OnInputFileChange" multiple /&gt;
@code {
  private async Task OnInputFileChange(InputFileChangeEventArgs e)
  {
      foreach (var file in e.GetMultipleFiles())
      {
          await using var src = file.OpenReadStream(1L * 1024 * 1024 * 1024);
          var content = new MultipartFormDataContent {
              { new StreamContent(src), "file", file.Name }
          };
          var res = await Http.PostAsync("api/files", content);
          res.EnsureSuccessStatusCode();
      }
  }
}

Minimal API(保存/配信)

app.MapPost("/api/files", async (HttpRequest req, IFileStorage storage, CancellationToken ct) =>
{
    var form = await req.ReadFormAsync(ct);
    var f = form.Files["file"]; if (f is null) return Results.BadRequest();
    await using var s = f.OpenReadStream();
    var id = await storage.SaveAsync(s, f.FileName, f.ContentType ?? "application/octet-stream", f.Length, ct);
    return Results.Json(new { id });
});

app.MapGet("/api/files/{id:guid}", async (Guid id, IFileStorage storage, CancellationToken ct) =>
{
var meta = await storage.GetMetaAsync(id, ct);
return Results.Stream(async output => await storage.CopyToAsync(id, output, ct),
meta.ContentType, meta.FileName, enableRangeProcessing: true);
}); 

ADO.NET ストレージ(チャンク書込)

// 先述の SqlChunkedFileStorage をそのまま使用

パフォーマンス・メモリ・運用の指針

  • スループット:サーバー NIC・DB I/O がボトルネックになりやすい。CPU は低め。
  • メモリ:EF Core はサイズ分のメモリを確保。ADO.NET チャンクなら一定量で頭打ち。
  • バックアップ:BLOB が大きいとバックアップ/復元に時間がかかる。フル/差分の計画を。
  • ガバナンス:保持期間(Retention)を決め、定期的に古いファイルをアーカイブまたは削除。
観点EF Core(byte[])ADO.NET(チャンク)
実装の簡単さ◎○
メモリ効率△(全読込)◎(一定量)
スループット○○〜◎(調整次第)
巨大ファイル対応×◎

よくある設計質問への回答

フロント側のアップロード方法は?

<InputFile> を使い、IBrowserFile.OpenReadStream(maxAllowedSize) で Stream を取得。WASM は HTTP API へ、Server は直接サーバーへストリーミングするのが基本です。

DB 列の型や設計は?

  • 本体:varbinary(max)
  • MIME:nvarchar(127)
  • ファイル名:nvarchar(260)
  • サイズ:bigint
  • 任意:RowVersion、Sha256、作成/更新日時、所有者、分類タグ

大容量ファイルへの対応策は?

  • ADO.NET の UPDATE ... .WRITE() でチャンク書き込み。
  • API・Kestrel・IIS のリクエスト上限を必要最小限で調整。
  • 配信は Range 対応でレジューム・シークを許可。
  • 運用上は外部ストレージ+DB メタデータ方式も候補に。

セキュリティと品質のベストプラクティス(チェックボックス)

  • [ ] サイズ制限:UI とサーバー両方に上限を設定。
  • [ ] 拡張子・MIME・署名の整合:許可リスト方式。
  • [ ] ウイルススキャン:非同期キューで確実に実施。
  • [ ] タイムアウト/リトライ:特に巨大ファイルでの安定性を担保。
  • [ ] 監査ログ:だれが何をいつアップロード/ダウンロードしたか。
  • [ ] 暗号化:転送は TLS、保管は TDE/アプリ層暗号。

まとめ

Blazor アプリで DB へ安全かつ効率的にファイルを保存するには、UI(InputFile)、API(Minimal API)、保存方式(EF Core / ADO.NET チャンク)、配信(Range 対応)、そして セキュリティ を一連の流れで設計することが重要です。小〜中サイズは EF Core、⼤容量は ADO.NET ストリーミングに切り替える二段構えにしておけば、負荷や運用の変化に強い堅牢なシステムを構築できます。

付録:最小限の SQL スクリプトと appsettings.json 例

{
  "ConnectionStrings": {
    "Default": "Server=.;Database=BlazorFiles;Integrated Security=true;TrustServerCertificate=true"
  }
}
-- テーブルは記事先頭の CREATE TABLE を使用
-- サンプルデータの存在確認
SELECT TOP 10 Id, FileName, ContentType, Length, CreatedAt
FROM dbo.Files
ORDER BY CreatedAt DESC;

付録:サンプルの検証用ユニットテスト(抜粋)

[Fact]
public async Task SaveAsync_Then_ReadBack_StreamMatches()
{
    // Arrange
    var bytes = Enumerable.Range(0, 256).Select(i => (byte)i).ToArray();
    await using var src = new MemoryStream(bytes);
    var storage = new SqlChunkedFileStorage(TestConfig.ConnectionString);


// Act
var id = await storage.SaveAsync(src, "test.bin", "application/octet-stream", bytes.Length, CancellationToken.None);
await using var dst = new MemoryStream();
await storage.CopyToAsync(id, dst, CancellationToken.None);

// Assert
dst.ToArray().Should().Equal(bytes);


} 

この記事の要点

  • UI は <InputFile>+OpenReadStream でシンプルに。
  • DB は varbinary(max) とメタデータ列の二層で設計。
  • 小〜中サイズは EF Core、巨大ファイルは ADO.NET のチャンク書込。
  • 配信はストリーミング+Range 対応、セキュリティは多層防御。

この記事を書いた人

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

コメント

コメントする

目次