はじめに
タイトルの通り、ASP.NET Core Minimal API + Vertical Slice + Transaction Script の実装例についての備忘録です。
改訂履歴
- 2026/08/26 : 初版公開。
本文
1. 環境
- .NET 8.0
- Visual Studio Community 2022
2. 構成
ソリューションの構成は以下の通りです。Vertical Slice らしく、Features 配下を機能単位で分割しています。
MinimalApiExample/
│
├── Entities/
│ ├── OrderEntity.cs
│ ├── OrderItemEntity.cs
│ ├── TableNames.cs
│ └── UserEntity.cs
│
├── Features/
│ ├── Orders/
│ │ ├── CreateOrder.cs
│ │ └── DeleteOrder.cs
│ │
│ ├── Users/
│ │ ├── CreateUser.cs
│ │ ├── DeleteUser.cs
│ │ ├── GetAllUsers.cs
│ │ ├── GetUser.cs
│ │ └── UpdateUser.cs
│ │
│ └── IEndpoint.cs
│
├── Infrastructure/
│ └── DbConnectionFactory.cs
│
├── Utils/
│ └── TypeExtensions.cs
│
└── Program.cs
今回は API 毎に 1 クラスを用意し、各クラスの中で HTTP エンドポイントの定義と、そのエンドポイントで必要となる処理を完結させています。API は以下の通りです。
3. 実装例
全てのソースを記載すると、それなりの分量になるため、大事なところだけをピックアップしていきます。
3-1. IEndpoint
まずは IEndpoint です。このインターフェースでは MapEndpoint を必須としています。static abstract を利用することで、各クラスにインスタンスを生成することなく、エンドポイント登録用のメソッドを統一できます。
namespace MinimalApiExample.Features;
/// <summary>
/// エンドポイントを表します。
/// </summary>
public interface IEndpoint
{
/// <summary>
/// エンドポイントを追加します。
/// </summary>
/// <param name="app">ルートビルダー。</param>
public static abstract void MapEndpoint(IEndpointRouteBuilder app);
}
3-2. CreateUser
次は IEndpoint を実装した CreateUser クラスです。GetNamespaceName() は名前空間の末尾を取得する自作の拡張メソッドです。Swagger のタグとして利用しています。
using System.Data;
using Dapper;
using MinimalApiExample.Entities;
using MinimalApiExample.Infrastructure;
using MinimalApiExample.Utils;
namespace MinimalApiExample.Features.Users;
/// <summary>
/// ユーザー作成を提供します。
/// </summary>
public sealed class CreateUser: IEndpoint
{
private static readonly string s_route = "/users";
/// <summary>
/// ユーザー作成の要求を表します。
/// </summary>
/// <param name="Name">ユーザー名。</param>
/// <param name="Email">ユーザーの E メール。</param>
public sealed record Request(string Name, string Email);
/// <inheritdoc/>
public static void MapEndpoint(IEndpointRouteBuilder app)
=> app
.MapPost(s_route, Execute)
.WithTags(typeof(CreateUser).GetNamespaceName())
.WithSummary("ユーザー作成 API");
private static async Task<IResult> Execute(
Request request,
DbConnectionFactory factory,
CancellationToken ct)
{
var parameter = new UserEntity
{
Id = Guid.NewGuid(),
Name = request.Name,
Email = request.Email,
CreatedAt = DateTime.Now
};
string sql = $"""
INSERT INTO {TableNames.User}
(
{nameof(UserEntity.Id)}
,{nameof(UserEntity.Name)}
,{nameof(UserEntity.Email)}
,{nameof(UserEntity.CreatedAt)}
)
VALUES
(
@{nameof(UserEntity.Id)}
,@{nameof(UserEntity.Name)}
,@{nameof(UserEntity.Email)}
,@{nameof(UserEntity.CreatedAt)}
)
""";
using IDbConnection db = factory.Create();
await db.ExecuteAsync(
new CommandDefinition(
sql,
parameter,
cancellationToken: ct));
return Results.Created($"{s_route}/{parameter.Id}", parameter);
}
}
3-3. Program
最後はエントリポイントの Program クラスです。Swagger のオプション設定、DbConnectionFactory クラスの DI 登録、各エンドポイントの登録を追加しています。
using MinimalApiExample.Features.Orders;
using MinimalApiExample.Features.Users;
using MinimalApiExample.Infrastructure;
namespace MinimalApiExample;
/// <summary>
/// アプリケーションのエントリポイントを定義します。
/// </summary>
public class Program
{
/// <summary>
/// アプリケーションのエントリポイントです。
/// </summary>
public static void Main(string[] args)
{
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization();
builder.Services.AddEndpointsApiExplorer();
- builder.Services.AddSwaggerGen();
+ builder.Services.AddSwaggerGen(options =>
+ {
+ // 名前衝突を回避するため FullName で一意化する。
+ options.CustomSchemaIds(type => type.FullName!.Replace("+", "."));
+ });
+ builder.Services.AddSingleton<DbConnectionFactory>();
WebApplication app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseAuthorization();
+ // 各エンドポイントの登録。
+ CreateOrder.MapEndpoint(app);
+ CreateUser.MapEndpoint(app);
+ DeleteOrder.MapEndpoint(app);
+ DeleteUser.MapEndpoint(app);
+ GetUser.MapEndpoint(app);
+ GetAllUsers.MapEndpoint(app);
+ UpdateUser.MapEndpoint(app);
app.Run();
}
}
4. 参考
おわりに
最初はこれくらいシンプルで良いと思います。記事を書きながら気付きましたが IEndpoint を使えば、各エンドポイントの登録はもっとスマートに実装できそうですね。