ASP.NET Core MVC(.NET 8)は、旧来の ASP.NET MVC/WebForms とは認証や状態管理の考え方がかなり変わっています。本記事では「認証・状態管理・データアクセス・設計パターン」を実務目線でまとめ、今日からそのまま使える最小構成やコード例まで丁寧に解説します。
ASP.NET Core MVC(.NET 8)でまず押さえるべき4つの軸
.NET 8 の ASP.NET Core MVC アプリを設計するとき、最初に決めるべきポイントは次の4つです。
- 認証/認可:ASP.NET Core Identity を使うか、外部 IdP を使うか
- 状態管理:Cookie/Session/TempData/キャッシュの切り分け
- データアクセス:EF Core / Dapper / ADO.NET の役割分担
- 設計パターン:DI・ミドルウェア・層構造・CQRS などをどこまで導入するか
ひとつずつ、.NET 8 時代の「いまのベストプラクティス」に寄せて整理していきます。
認証・認可:ASP.NET Core Identity を前提に考える
なぜ旧 Membership ではなく ASP.NET Core Identity なのか
ASP.NET Core では、従来の Membership(aspnet_Users テーブルなど)ではなく、ASP.NET Core Identity が標準の認証・ユーザー管理フレームワークになっています。Identity はクレームベースで、ユーザー・ロール・クレーム・トークン・外部ログインなどを一式で提供します。
Identity を使うメリットは次の通りです。
- Cookie 認証・パスワード管理・ロックアウト・2FA などを一括で面倒見てくれる
- 内部ストアは EF Core ベースなので、SQL Server 接続も簡単
- ユーザーテーブルやクレーム構造をカスタマイズしやすい
- Razor Pages で提供される既成 UI をスキャフォールディングして、ログイン画面を即座に用意できる
そのため、新規開発であれば「とりあえず Identity」を前提に設計するのが基本線になります。
.NET 8 の空テンプレートから Identity を組み込む手順
「空の MVC プロジェクト」から最小構成で Identity を組み込む手順を、.NET 8 の Program.cs ベースで整理します。
- EF Core + SQL Server の DbContext を用意
- Identity を登録(AddDefaultIdentity + AddRoles)
- セッション/キャッシュを登録
- ミドルウェアの順序を正しく並べる
DbContext の定義と登録
まずはアプリ用の AppDbContext を作成し、IdentityDbContext を継承させることで Identity のテーブルも同じ DB に持たせます。
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
public class AppDbContext : IdentityDbContext<IdentityUser>
{
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options)
{
}
// 業務ドメインの DbSet をここに追加
public DbSet<Product> Products => Set<Product>();
}
Program.cs では、接続文字列を appsettings.json から読み込み、AddDbContext で DI に登録します。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
// DbContext(SQL Server 接続)
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(
builder.Configuration.GetConnectionString("DefaultConnection")));
Identity 登録:AddDefaultIdentity + AddRoles
次に Identity を登録します。AddDefaultIdentity は、ユーザー管理・Cookie 認証・トークンプロバイダー・既定の UI などをまとめて有効化する拡張メソッドです。
builder.Services
.AddDefaultIdentity<IdentityUser>(options =>
{
// パスワードポリシーなどはここでカスタマイズ
options.SignIn.RequireConfirmedAccount = true;
})
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<AppDbContext>();
ロールを使う場合は AddRoles<IdentityRole>() を忘れずに追加します。これで、AspNetUsers や AspNetRoles などの Identity テーブルが自動的に EF Core モデルとして扱われます。
ミドルウェアの順序(UseSession / UseAuthentication / UseAuthorization)
ASP.NET Core ではミドルウェアの呼び出し順が非常に重要です。Microsoft の公式ドキュメントでも、UseRouting → セッション → 認証 → 認可 → エンドポイント という順番が推奨されています。
var app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
// Session は Routing の後・エンドポイントの前
app.UseSession();
// Identity の Cookie 認証
app.UseAuthentication();
app.UseAuthorization();
// MVC / Razor Pages のエンドポイント
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages(); // Identity UI をスキャフォールディングした場合
app.Run();
UseSession が UseRouting より前にあったり、MapControllerRoute より後ろにあったりすると、HttpContext.Session が利用できなかったり、意図しない動作を引き起こすので注意しましょう。
[Authorize] とポリシーベース認可
認可は基本的に [Authorize] 属性、ロール、ポリシー(クレーム)で行います。ロールベース認可は次のように書けます。
[Authorize(Roles = "Administrator")]
public class AdminController : Controller
{
public IActionResult Index()
=> View();
}
より柔軟な条件(「部署が Sales で、かつ Manager ロール」など)を扱う場合は、ポリシーベースを使います。
// Program.cs
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("SalesManager", policy =>
policy.RequireRole("Manager")
.RequireClaim("Department", "Sales"));
});
[Authorize(Policy = "SalesManager")]
public IActionResult SalesReport()
{
...
}
ポリシーを使うと、「認可ロジックをコードに散らさずに一箇所にまとめる」ことができ、後からの要件変更にも対応しやすくなります。
旧 ASP.NET MVC/Membership からの移行方針
既存の Membership ベースのアプリから移行する場合は、ざっくり次のようなステップになります。
- 新規に ASP.NET Core MVC(.NET 8)アプリを作成し、Identity を構成
- 旧 DB にあるユーザー情報を、Identity スキーマ(AspNetUsers 等)にマイグレーション
- パスワードハッシュ形式が異なる場合は、カスタムパスワードハッシャーを実装し、一度ログイン時に再ハッシュする
- ロール・権限テーブルを Identity Roles/Claims に写経、またはマッピングテーブルを用意
特に「パスワードをどう扱うか」が最大のポイントです。移行時には、旧形式のハッシュを認識するカスタムハッシャーを実装し、ユーザー初回ログイン時に Identity 形式に更新する戦略が現実的です。
状態管理:Cookie/Session/TempData/キャッシュの使い分け
状態管理手段の比較表
ASP.NET Core では ViewState が廃止され、状態管理の選択肢が整理されました。代表的な手段を用途別にまとめると次のようになります。
| 用途 | 手段 | 有効範囲 | 典型シナリオ | 注意点 |
|---|---|---|---|---|
| ログイン状態の保持 | 認証 Cookie(Identity) | ブラウザ単位 | ユーザー識別、クレーム参照 | Cookie サイズを肥大化させない |
| 画面遷移をまたぐウィザード | Session(ISession) | ブラウザ+サーバサイド | 注文カート、入力途中の一時保存 | スケールアウト時は分散キャッシュ必須 |
| リダイレクト後に一度だけ表示 | TempData | 1 回読み取りまで | 成功/エラーメッセージ(フラッシュメッセージ) | 大きなデータは入れない |
| 頻出データの高速化 | IMemoryCache | アプリケーション内(サーバ単位) | マスタデータ、設定値 | サーバをまたぐと共有されない |
| スケールアウト前提のキャッシュ | IDistributedCache | 分散(Redis/SQL 等) | 大規模サイトのキャッシュ、分散セッション | 外部ストアの遅延や障害に注意 |
| 認可・フィルタ用の属性情報 | Claims(クレーム) | 認証 Cookie | 部署、テナント、権限レベル | 入れ過ぎると Cookie が大きくなる |
Cookie 認証と Claims の実務的な設計
ASP.NET Core Identity では、ログインするとサーバ側のユーザー情報から ClaimsIdentity を組み立て、これを暗号化した Cookie にシリアライズしてブラウザに返します。
クレームには「UserId」「ユーザー名」だけでなく、「部署」「テナント」「権限レベル」などを入れておくと、認可判定を高速に行えます。ただし、クレームを入れ過ぎると Cookie が肥大化し、リクエストヘッダーが太りすぎるので、本当に頻繁に参照する情報だけを詰めるようにしましょう。
Session(ISession)のベストプラクティス
Session は IDistributedCache をバックエンドとして実装されており、既定では AddDistributedMemoryCache によるサーバメモリが利用されます。サーバを複数台に増やす予定があるなら、最初から Redis や SQL Server を使った分散キャッシュを前提に設計しましょう。
// Program.cs
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis");
});
builder.Services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(30);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
コントローラからの使用例は次の通りです。
public IActionResult Step1(string name)
{
HttpContext.Session.SetString("Name", name);
return RedirectToAction("Step2");
}
public IActionResult Step2()
{
var name = HttpContext.Session.GetString("Name");
return View(model: name);
}
Session は「ユーザー固有の短期的な状態」を扱うのに向いていますが、オブジェクトを無造作にシリアライズして放り込む」のは避け、必要最小限の情報(ID やフラグ)に留めるのが安全です。
TempData:一度きりのメッセージ用
TempData は、リダイレクトを跨いで 1 回だけデータを保持する仕組みで、典型的には「登録に成功しました」のようなフラッシュメッセージの表示に使います。既定では Cookie ベースですが、Session プロバイダを切り替えることも可能です。
public IActionResult Create()
{
// 登録処理 ...
TempData["Message"] = "商品を登録しました。";
return RedirectToAction("Index");
}
public IActionResult Index()
{
ViewBag.Message = TempData["Message"]?.ToString();
return View();
}
TempData に大きなデータ(リストなど)を入れると Cookie サイズやパフォーマンスに影響が出るため、メッセージ文字列や ID 程度に絞りましょう。
キャッシュ:IMemoryCache / IDistributedCache の使い分け
頻繁に参照されるマスタデータ(都道府県リスト、商品カテゴリなど)は、EF Core で毎回クエリするよりも IMemoryCache や IDistributedCache に載せた方が効率的です。EF Core のパフォーマンスガイドでも、キャッシュの活用が推奨されています。
public class CategoryService
{
private readonly AppDbContext _db;
private readonly IMemoryCache _cache;
public CategoryService(AppDbContext db, IMemoryCache cache)
{
_db = db;
_cache = cache;
}
public async Task<IReadOnlyList<Category>> GetCategoriesAsync()
{
return await _cache.GetOrCreateAsync("categories", async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);
return await _db.Categories.AsNoTracking().ToListAsync();
});
}
}
スケールアウト後もキャッシュを共有したい場合は、上記の IMemoryCache を IDistributedCache ベースに置き換えるか、Redis を使った専用のキャッシュ層を設ける構成がよく使われます。
データアクセス:EF Core / Dapper / ADO.NET の現実的な使い分け
EF Core:新規開発の標準データアクセス
EF Core は .NET 向けの軽量な O/R マッパーで、LINQ ベースのクエリ、トラッキング、マイグレーション、変更履歴の管理など、業務アプリで必要になる機能を一通り備えています。
特徴を整理すると、次のようになります。
- Unit of Work + Repository 相当の機能を
DbContextが内包 - コードファースト/DB ファースト両対応(
Scaffold-DbContextで既存 DB からエンティティ生成) - 変更トラッキングにより更新処理が簡潔(エンティティの状態に応じて UPDATE/INSERT を自動生成)
- LINQ でクエリを書けるため、コンパイル時に型チェック・リファクタリングが効く
新規開発で、CRUD 中心の典型的な業務システムを作るなら、まずは EF Core を標準 ORM として採用するのがよいでしょう。
Dapper:SQL 主導の高速読み取りで活躍
Dapper は「マイクロ ORM」と呼ばれるライブラリで、ADO.NET の DbConnection に薄いマッピング機能を足したものです。SQL は開発者が自分で書きますが、その分、複雑な結合やチューニング済みのストアドプロシージャとの相性が良く、読み取り中心の処理で高い性能を発揮します。
Dapper を使うときの典型的なユースケースは次のようなものです。
- 既存システムがストアドプロシージャ中心で、SQL を手動で維持している
- ダッシュボードや帳票など、読み取りが圧倒的に多い画面で極限まで性能を詰めたい
- JOIN を多用した複雑な集計クエリを、そのまま SQL として管理したい
「書き込みは EF Core、重たい読み取りのみ Dapper」というハイブリッド構成も実務ではよく採用されます。
ADO.NET:必要最小限のローレベル API として残す
ADO.NET の SqlConnection / SqlCommand は、最も低レベルなデータアクセス API です。Dapper も内部では ADO.NET を使っているので、基本的には「Dapper で書けない/書きづらい場合の最終手段」として位置づけるとよいでしょう。
例えば次のようなケースでは、あえて ADO.NET を直接使う選択肢もあります。
- 大量のバルクインサート(
SqlBulkCopyなど) - DB ドライバ固有の機能を叩きたいとき
- 非常に細かいトランザクション制御が必要な場合
三者の整理:どれを標準にするか
| 状況 | 推奨選択 | 理由 |
|---|---|---|
| 新規開発、CRUD 多め、モデル重視 | EF Core | 変更トラッキングとマイグレーションで開発効率が高い |
| 既存 DB / ストアド多数、読み取り性能重視 | Dapper + ADO.NET | SQL 主導で既存資産を活かしやすい |
| 読み取りが極端に重い画面が一部だけある | EF Core + Dapper 混在 | 基本は EF Core、一部ホットパスのみ Dapper に寄せる |
| 特殊な DB API を叩きたい | ADO.NET | 低レベル API として直接制御できる |
設計パターン:ASP.NET Core 標準を土台に必要なものだけ足す
まずは「ASP.NET Core 標準パターン」を使いこなす
ASP.NET Core 自体が、多くの設計パターンを組み込んだフレームワークです。特に次の4つを「アプリの骨格」として意識するだけで、過剰な自作フレームワークを避けられます。
- 依存性注入(DI):サービスをコンストラクタインジェクションで受け取る
- Options パターン:設定値を
IOptions<T>で型安全に扱う - ミドルウェア:HTTP パイプラインを関心ごとごとに分離
- ロギング:
ILogger<T>を通じて構造化ログを出力
これらを土台に、Controller → Application Service(UseCase) → Domain → Infrastructure のような薄い層構造を作ると、テストと保守がかなりしやすくなります。
Repository パターンを「二重に」被せない
EF Core の DbContext はすでに「Unit of Work」「Repository」相当の責務を持っているため、これをさらに汎用的な Repository でラップし過ぎると、抽象化のための抽象化になりがちです。
実務では次のような方針が扱いやすいことが多いです。
- アプリ規模が小〜中:DbContext を直接アプリケーションサービスから使う
- 複雑なドメイン:ドメイン単位で薄い Repository インターフェースを定義(例:
IOrderRepository) - クエリが複雑:Repository に「仕様(Specification)オブジェクト」や Dapper を注入してクエリの組み立てを分離
「全エンティティを同じ汎用リポジトリで扱う」よりも、「重要な集合のみ個別のリポジトリを用意する」ほうが、実装の自由度と可読性のバランスが取りやすくなります。
CQRS / Mediator(MediatR 等)の導入タイミング
コマンド(書き込み)とクエリ(読み取り)を分離する CQRS パターンは、MediatR などの Mediator ライブラリと組み合わせると実装しやすくなります。ただし、小規模なアプリに最初からフルセットで導入すると、クラス数が一気に増え、学習コストが高くなりがちです。
次のような条件が揃った時点で段階的に導入するのがおすすめです。
- ユースケースが増え、Controller が肥大化してきた
- ユースケース単位でロギング・トランザクション・バリデーションを一括で扱いたい
- 将来的にメッセージング(キュー)への移行も視野に入れている
まずは「新規の複雑ユースケースだけ MediatR で書いてみる」「既存 Controller の一部だけをハンドラに抽出する」といったスモールステップで始めると良いでしょう。
サガ(Saga)パターンは「マイクロサービス化してから」
分散トランザクションを扱うサガパターン(Orchestration / Choreography)は、メッセージキューやイベントバスを前提としたマイクロサービスアーキテクチャ向けのパターンです。単一の ASP.NET Core MVC アプリで DB も 1 つであれば、まずはシンプルなローカルトランザクションで十分です。
サガを適用するのは、次のような状況になってからで構いません。
- サービスが複数に分割され、それぞれ独立した DB を持ち始めた
- クラウド上のメッセージング基盤(Azure Service Bus など)が標準で使える
- ビジネス的にも最終的整合性を許容できる
それまでは「モノリス + 分割しやすい層構造」を維持し、将来の分割に備えるのが現実的です。
バリデーション戦略:DataAnnotations + FluentValidation
ASP.NET Core MVC のモデルバインディングは、[Required] や [StringLength] などの DataAnnotations に標準対応しています。簡単な入力チェックであれば、これだけでも十分です。
一方、「DB との整合性を含む複雑なルール」や「画面ごとに異なるバリデーション」を行いたい場合は、FluentValidation を併用して、Validator クラスにルールを切り出すのがおすすめです。
最小構成サンプル:Identity + EF Core + Session をまとめて有効化
ここまでの内容を踏まえ、空の MVC プロジェクトを「実務でそのまま使える最小構成」に仕立てるコード例を示します。
Program.cs の全体像
var builder = WebApplication.CreateBuilder(args);
// MVC
builder.Services.AddControllersWithViews();
// DbContext(SQL Server)
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(
builder.Configuration.GetConnectionString("DefaultConnection")));
// Identity(ユーザー + ロール)
builder.Services
.AddDefaultIdentity<IdentityUser>(options =>
{
options.SignIn.RequireConfirmedAccount = true;
})
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<AppDbContext>();
// キャッシュ & セッション
builder.Services.AddDistributedMemoryCache();
builder.Services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(20);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
// アプリ構築
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseSession();
app.UseAuthentication();
app.UseAuthorization();
// ルート定義
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
// Identity UI を使うなら
app.MapRazorPages();
app.Run();
Controller からの利用イメージ
[Authorize]
public class OrderController : Controller
{
private readonly AppDbContext _db;
public OrderController(AppDbContext db)
{
_db = db;
}
public async Task<IActionResult> Index()
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var orders = await _db.Orders
.AsNoTracking()
.Where(o => o.UserId == userId)
.ToListAsync();
return View(orders);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(OrderInputModel input)
{
if (!ModelState.IsValid)
{
return View(input);
}
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var order = new Order
{
UserId = userId,
CreatedAt = DateTime.UtcNow,
// ...
};
_db.Orders.Add(order);
await _db.SaveChangesAsync();
TempData["Message"] = "注文を登録しました。";
return RedirectToAction(nameof(Index));
}
}
この例だけで、次の要素がすべて機能します。
- Identity によるログイン(
[Authorize]) - EF Core による SQL Server への CRUD
- TempData でのフラッシュメッセージ表示
- 必要に応じて Session/キャッシュを追加できる拡張性
よくある落とし穴と対策まとめ
- UseSession の位置が誤っている
UseRoutingの後、UseAuthentication・UseAuthorizationの前に置くのが基本。順序が違うとHttpContext.Sessionが null になったり、値が保持されなかったりします。 - Repository を作り込みすぎる
EF Core の機能と重複する抽象化は、開発コストだけ増やしてメリットが薄くなりがちです。「最初は DbContext 直利用 → 必要になった部分だけ Repository 導入」という順番が現実的です。 - マイクロサービス前提で過剰設計
いきなりサガやイベント駆動を前提にすると、インフラ構築に時間を取られます。まずはモノリスで素直に実装し、境界づけられたコンテキストを意識した層構造にしておくことで、後からの分割を容易にします。 - Identity Cookie の肥大化
クレームを入れすぎると Cookie が大きくなり、すべてのリクエストにそれが乗るため帯域を圧迫します。頻繁にチェックする項目のみに絞り、それ以外は DB やキャッシュから参照しましょう。 - セッションに「なんでも」突っ込む
大きなオブジェクトをセッションにシリアライズすると、メモリや分散ストアを圧迫します。セッションには ID などのキー情報だけを持たせ、実データは別途取得する設計が安全です。 - NoTracking を使わず大量読み取り
一覧表示など更新しない読み取りには、AsNoTracking()を付けるだけでパフォーマンスが改善されます。必要に応じて Dapper と組み合わせると、さらに高速化できます。
実務指向のスタートプラン
最後に、本記事の内容を踏まえた「小さく始めて大きく育てる」ためのスタートプランを整理します。
- まずは空の MVC プロジェクトに Identity + EF Core + SQL Server を導入
ログイン・ユーザー登録・ロール管理までを Identity で一気に通すことで、認証/認可の土台が固まります。 - 状態管理は Cookie + TempData + Session を役割分担して設計
フラッシュメッセージは TempData、ウィザード形式は Session、長期的なユーザー識別は Cookie に任せます。将来のスケールアウトを見据えて、Session/キャッシュは分散ストアを前提にしておきましょう。 - データアクセスは EF Core を標準にし、一部ホットパスのみ Dapper を併用
まずは EF Core で CRUD を素直に実装し、性能上のボトルネックが見えてきた箇所だけ Dapper に切り替える方針が、トータルのコストを抑えやすいです。 - 設計パターンは「DI・Options・ミドルウェア・層構造」から始める
CQRS や Mediator は、Controller が肥大化してから導入しても遅くありません。まずは ASP.NET Core 標準のパターンを徹底的に活かしましょう。 - 分散トランザクションやサガは、マイクロサービス化してから
今すぐにマイクロサービスを前提にせず、モノリスの中でコンテキスト境界を意識した設計にしておけば、将来的にサービスを分割しやすくなります。
この流れに沿って開発を進めれば、ASP.NET Core MVC(.NET 8)のプロジェクトでも「小さく始めて拡張しやすい」構成を自然に作ることができます。まずは Identity でログイン一式を通し、EF Core で CRUD を実装しつつ、必要なところだけ Dapper や高度なパターンを追加していきましょう。

コメント