macOSでASP .NET Coreを使ってAPIサーバをHerokuにデプロイする

macOSで.NET Coreを使ってAPIサーバをHerokuにデプロイするまでの手順をまとめます。


# 前提条件
実行環境は macOS Catalina です。
以下のツールはインストール済みとします。

- VS Code
- Docker


# 何を作るの？
シンプルなTodo管理APIサーバを構築します。データはデータベースに保存します。


# .NET Core macOS版のインストール

[Microsoftのウェブサイト](https://dotnet.microsoft.com/download)からダウンロードしてインストールします。


# 必要な.NETツールをインストール

```shell
dotnet tool install --global dotnet-aspnet-codegenerator
dotnet tool install --global dotnet-ef
```


# プロジェクトの作成
 `webapi`テンプレートを使ってプロジェクトを作成します。
`--no-https`をつけないと、デフォルトで`localhost`の証明書を作る流れになってややこしくなります。

```shell
dotnet new webapi -o TodoApi --no-https
```


# 初期パッケージのインストール
Entity Framework Coreを使って、データベースへの書き込みを実現するので、以下のようにパッケージをインストールします。
ただ、今回はSQLiteだけ使うので、`Microsoft.EntityFrameworkCore.Sqlite` だけインストールしても問題ありません。

```shell
cd TodoApi
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.InMemory
```


# 初回起動
`dotnet run` で起動できます。

```shell
dotnet run

info: Microsoft.Hosting.Lifetime[0]
      Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /Users/XXXXX/TodoApi
```

その後、`curl`を以下のように実行するとテンプレートの初期設定である`WeatherForecast`の情報が取得できます。

```shell
curl http://localhost:5000/WeatherForecast
```

次のようなJSONが返されます。

```json
[
   {
      "date" : "2020-08-01T08:48:53.47458+09:00",
      "summary" : "Mild",
      "temperatureC" : 39,
      "temperatureF" : 102
   },
   {
      "date" : "2020-08-02T08:48:53.474598+09:00",
      "summary" : "Mild",
      "temperatureC" : -20,
      "temperatureF" : -3
   },
   {
      "date" : "2020-08-03T08:48:53.474609+09:00",
      "summary" : "Scorching",
      "temperatureC" : 47,
      "temperatureF" : 116
   },
   {
      "date" : "2020-08-04T08:48:53.474619+09:00",
      "summary" : "Scorching",
      "temperatureC" : 54,
      "temperatureF" : 129
   },
   {
      "date" : "2020-08-05T08:48:53.474629+09:00",
      "summary" : "Sweltering",
      "temperatureC" : 39,
      "temperatureF" : 102
   }
]
```


# モデルクラス TodoItem の追加
以下のように`TodoItem.cs`ファイルを`Models`フォルダの下に作成します。

```c#:TodoItem.cs
public class TodoItem
{
    public long Id { get; set; }
    public string Name { get; set; }
    public bool IsComplete { get; set; }
}
```

# データベースコンテキストの追加
同様に`Models`フォルダの下に`TodoContext.cs`を作成します。

```c#:TodoContext.cs
using Microsoft.EntityFrameworkCore;

namespace TodoApi.Models
{
    public class TodoContext : DbContext
    {
        public TodoContext(DbContextOptions<TodoContext> options)
            : base(options)
        {
        }

        public DbSet<TodoItem> TodoItems { get; set; }
    }
}
```


# Startup.cs を修正
`Startup.cs`を以下のように修正します。ここで先ほど作成したモデルクラスやデータベースコンテキストを登録します。
今回は、SQLiteを使うのでそれ以外をコメントアウトしています。

```c#:Startup.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

// 追加
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

namespace TodoApi
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<TodoContext>(opt =>
                //opt.UseInMemoryDatabase("TodoList"));
                opt.UseSqlite("Filename=db.sqlite3"));  // 今回はこれを使う。 SQLiteのファイル名は db.sqlite3
                //opt.UseNpgsql("Host=localhost;Database=mydb;Port=15432;Username=user;Password=password")); // PosgreSQL
                //opt.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); // SQL Server

            services.AddControllers();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}
```


# コントローラを生成
必要なパッケージを追加します。

```shell
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.Design
```

コントローラ`TodoItemsController`を生成します。

```shell
dotnet aspnet-codegenerator controller -name TodoItemsController -async -api -m TodoItem -dc TodoContext -outDir Controllers
```

この状態で自動的に`TodoItem`のCRUD APIが実装されます。

# データベースマイグレーションとデータベースの更新
`dotnet ef migrations add <どんな変更を加えたのかを表す任意の名称>`という形でマイグレーションを作成します。
`dotnet ef database update`でデータベースを更新します。

```shell
dotnet ef migrations add InitialCreate
dotnet ef database update
```


# APIを試してみる

`dotnet run`でサーバを起動します。

## POSTのテスト
```shell
curl -X POST \
     -H "Content-Type:application/json" \
     -d '{"Name":"Task 01", "IsComplete":false}' \
     http://localhost:5000/api/TodoItems

curl -X POST \
     -H "Content-Type:application/json" \
     -d '{"Name":"Task 02", "IsComplete":false}' \
     http://localhost:5000/api/TodoItems

```

## GETのテスト：一覧取得
```shell
curl http://localhost:5000/api/TodoItems
[{"id":1,"name":"Task 01","isComplete":false},{"id":2,"name":"Task 02","isComplete":false}]
```

## PUTのテスト
```shell
curl -X PUT \
     -H "Content-Type:application/json" \
     -d '{"Id":2, "IsComplete":true}' \
     http://localhost:5000/api/TodoItems/2
```

## GETのテスト：特定レコードの取得
```shell
curl http://localhost:5000/api/TodoItems/2
{"id":2,"name":"Task 02","isComplete":true}
```

## DELETEのテスト
```shell
curl -X DELETE \
     -H "Content-Type:application/json" \
     http://localhost:5000/api/TodoItems/2
{"id":2,"name":"Task 02","isComplete":true}
```

# TodoItemにカラムを追加

以下のように`TodoItem`に`CreatedAt`を追加します。

```c#:TodoItem.cs
using System;  // 追加

public class TodoItem
{
    public long Id { get; set; }
    public string Name { get; set; }
    public bool IsComplete { get; set; }
    public DateTime CreatedAt { get; set; } // 追加
}
```

以下のようにマイグレーションを実行します。

```shell
dotnet ef migrations add AddDateColumn
dotnet ef database update
```

# データベースが更新されたことを確認

```shell
sqlite db.sqlite3
sqlite> PRAGMA table_info('TodoItems');
0|Id|INTEGER|1||1
1|Name|TEXT|0||0
2|IsComplete|INTEGER|1||0
3|CreatedAt|TEXT|1|'0001-01-01 00:00:00'|0
sqlite> 
```

# Herokuへデプロイ
以下のような`Dockerfile`を作ります。

```dockerfile:Dockerfile
FROM mcr.microsoft.com/dotnet/core/aspnet

COPY bin/Release/netcoreapp3.1/publish/ App/
WORKDIR /App

CMD export ASPNETCORE_URLS=http://0.0.0.0:$PORT; dotnet TodoApi.dll
```


デプロイファイルを生成します。
`bin/Release/netcoreapp3.1/` 配下にリリースようファイル群が生成されます。

```shell
dotnet publish -c Release
```

Herokuのインスタンスを作成して、Dockerfileをpush & releaseします。

```shell
heroku create 
heroku container:login
heroku container:push web --app <appName>
heroku container:release web --app <appName>
```


# 参考

チュートリアル: ASP.NET Core で Web API を作成する
https://docs.microsoft.com/ja-jp/aspnet/core/tutorials/first-web-api?view=aspnetcore-3.1&tabs=visual-studio-code#prerequisites

チュートリアル: 移行機能の使用 - ASP.NET MVC と EF Core
https://docs.microsoft.com/ja-jp/aspnet/core/data/ef-mvc/migrations?view=aspnetcore-3.1

チュートリアル: JavaScript を使用して ASP.NET Core Web API を呼び出す
https://docs.microsoft.com/ja-jp/aspnet/core/tutorials/web-api-javascript?view=aspnetcore-3.1

ASP.NET Core を使って Web API を作成する
https://docs.microsoft.com/ja-jp/aspnet/core/web-api/?view=aspnetcore-3.1