ASP.NET Core Minimal API で EntityFramework を使った PostgreSQL Restful API

.NET6 から導入された Minimal APIの資料が少なかったため、海外サイトで調べたものをまとめてみる。

## 更新履歴
2022/8/1
クロスサイトスクリプティング対応(CORS対応)

## 各種インストール
.NET6、PostgreSQLのインストール方法は省略


## テーブル
小さなテーブルをテスト用に作成
```sql
# create table member (id serial, name text, primary key (id));
# \d member
                             Table "member"
 Column |  Type   | Collation | Nullable |             Default              
--------+---------+-----------+----------+----------------------------------
 id     | integer |           | not null | nextval('member_id_seq'::regclass)
 name   | text    |           |          | 
Indexes:
    "member_pkey" PRIMARY KEY, btree (id)
```


## プロジェクト作成〜パッケージ追加
```web```オプションで作成するとSwaggerなし、```webapi -minimal```オプションで作成するとSwaggerとWeatherForecastありのテンプレートからプロジェクトが作成される。以下 Web画面からRESTのテストをするため、Swaggerありをベースに WeatherForecastを削除して作成。

```PowerShell
Swaggerあり
dotnet new webapi -minimal -o webapiminimal1
```
```PowerShell
Swaggerなし
dotnet new web -o webapiminimal1
```

続けてプロジェクトフォルダへ移動して、パッケージを追加
```PowerShell
cd webapiminimal1
dotnet add package Microsoft.EntityFrameworkCore --version 6.0.7
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL --version 6.0.5
```


## プロジェクトファイル(確認のみ)
パッケージ追加による必要な記述は自動的に行われるため確認のみ。
最終行の Swashbuckle は Swaggerパッケージ。

```webapiminimal1.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.1" />
    <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.2" />
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
  </ItemGroup>

</Project>
```


## アプリケーション設定
デバッグとリリース、それぞれの設定ファイルに PostgreSQL接続文字列を追加。
以下先頭に記述したパターン。

```appsettings.json
{
  "ConnectionStrings": {
    "DefaultConnection": "Host=IPもしくはサーバ名;Port=ポート番号;Database=データベース名;User Id=ユーザ名;Password=パスワード"
  },
  "//":"以下省略"
}
```

```appsettings.Development.json
{
  "ConnectionStrings": {
    "DefaultConnection": "Host=IPもしくはサーバ名;Port=ポート番号;Database=データベース名;User Id=ユーザ名;Password=パスワード"
  },
  "//":"以下省略"
}
```


## モデル作成
Modelフォルダを作ってからファイル作成してもいいし、プロジェクトルートでも良い。
モデルを作成しつつ、DbContextも同一ファイルで定義して問題ない。

ファイル名はPostgreSQLでのテーブル名称(大文字小文字含め)に合わせると管理がラク。

recordでスキーマを定義する場合、DbSetで定義するオブジェクトとテーブル名称の一致が必要(コメントアウト箇所)。DataAnnotationsでスキーマ属性を使用すれば、制約解除できる。

```member.cs
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

public class tblMember : DbContext
{
    public tblMember(DbContextOptions<tblMember> options) : base(options) { }
    // public DbSet<Member> member => Set<Member>();
    public DbSet<Member> members => Set<Member>();
}

// public record Member(int id)
// {
//     public string? name { get; set; }
// }

[Table("member", Schema = "スキーマ")]
public class Member
{
    [Key]
    [Column("id")]
    public int id{ get; set; }

    [Column("name")]
    public string? name{ get; set; }
}
```


## プログラムメイン
Minimal API では、Startup.csを使わず、全て Program.cs に記述する。

```Program.cs
using Microsoft.EntityFrameworkCore;

// Webアプリケーションビルダー
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// CORS(Cross-Origin Resource Sharing)対応
var AllowSpecificOrigins = "_allowSpecificOrigins";
builder.Services.AddCors(options =>
{
    options.AddPolicy(name: AllowSpecificOrigins, policy =>
    {
        policy.WithOrigins("https://アクセス元IPもしくはサーバ名:ポート").AllowAnyHeader().AllowAnyMethod();
    });
});

// サービス追加(Npgsql, 接続文字列は appsettings.json から読み取り)
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<tblMember>(options => options.UseNpgsql(connectionString));

// Webアプリをビルド
var app = builder.Build();

// 開発用のRESTインターフェース
if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); }

// CORS有効化
app.UseCors(AllowSpecificOrigins);

// ルーティング
// GET(1) ※CORS対応
app.MapGet("/member", async (tblMember tbl) => await tbl.members.ToListAsync()).RequireCors(AllowSpecificOrigins);

// GET(2) ※CORS対応
app.MapGet("/member/{id:int}", async (int id, tblMember tbl) =>
{
    return await tbl.members.FindAsync(id) is Member r ? Results.Ok(r) : Results.NotFound();
}).RequireCors(AllowSpecificOrigins);

// GET(3) ※ページング処理用(WhereとSkipとTakeは実際の要件で実装合わせ込みが必要)
app.MapGet("/memberList", async (tblMember tbl) =>
{
    return await tbl.members
        .Where(a => a.id < 6)
        .OrderByDescending(a => a.id)
        .Skip(2)  // SQLでのOFFSET
        .Take(2)  // SQLでのLIMIT
        .ToListAsync();
}).RequireCors(AllowSpecificOrigins);

// PUT ※CORS非対応(必要ならRequireCorsする)
app.MapPut("/member/{id:int}", async (int id, Member r, tblMember tbl) =>
{
    if (r.id != id) return Results.BadRequest();
    var record = await tbl.members.FindAsync(id);
    if (record is null) return Results.NotFound();
    record.name = r.name;
    await tbl.SaveChangesAsync();
    return Results.Ok(record);
});

// POST ※CORS非対応(必要ならRequireCorsする)
app.MapPost("/member/", async (Member r, tblMember tbl) =>
{
    tbl.members.Add(r);
    await tbl.SaveChangesAsync();
    return Results.Created($"/member/{r.id}", r);
});

// DELETE ※CORS非対応(必要ならRequireCorsする)
app.MapDelete("/member/{id:int}", async (int id, tblMember tbl) =>
{

    var record = await tbl.members.FindAsync(id);
    if (record is not null)
    {
        tbl.members.Remove(record);
        await tbl.SaveChangesAsync();
    }
    return Results.NoContent();
});

// GET(3) Webサイトルート ※CORS対応不要
app.MapGet("/", () => "ASP.NET Core Minimal API");

// 起動
app.Run();
```

## デバッグ実行
```dotnet watch run``` で、ホットリロードありにして、https://IPアドレス:動的ポート/Swagger/ で RESTの動作確認をしながら進めるとラク。

Swagger上部はルーティングで定義したREST別の一覧、下部はモデル定義したスキーマ一覧。
![swagger1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/622331/57a6b4a1-bc94-5e5f-1c2e-93aac261a866.png)

POSTメソッドであれば、 ```Try it out``` を押下してで内容を展開する。
![swagger2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/622331/2dd70515-bbc8-ec2f-2e2a-a111af8a11cf.png)

モデルに従ってJSON形式で内容を記述して、```Excute``` を押下。
![swagger4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/622331/0962d370-c79d-3cda-f349-44173c1a17d1.png)

バックグラウンドでの curl 実行内容やサーバレスポンスが確認できる。
![swagger5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/622331/a91ae97a-8785-c9fa-2f0a-42e5261206ee.png)

## 関連ページ
[jQuery AJAXでJSON取得](https://qiita.com/n4a/items/cd1c0b845d5fd6110d3e)