新しい.NETのスキャフォールディングを試してみる

## はじめに

これまでCLIで新しいプロジェクトを作成することはできましたが、新しい`dotnet scaffold`コマンドを利用して作成済みのプロジェクトにコントローラーや、パッケージの追加などが行えるようになりました。

https://devblogs.microsoft.com/dotnet/introducing-dotnet-scaffold/

Visual StudioやRiderといったIDEではなく、CLIベースで作業する人にはうれしい新機能ですね。あとは、チュートリアルやQiitaのようにドキュメントベースで説明を行う場合にも、文字ベースで説明できるのでうれしい機能かもしれません。

## インストール

グローバルにインストールしてもよいのですが、お試しなのでローカルインストールで進めていきます。まずはプロジェクトとツールマニフェストを作って、`dotnet-scaffold`サブコマンドをインストールします。

```
❯ dotnet new webapi -o SampleApp1
❯ cd SampleApp1
❯ dotnet new tool-manifest
テンプレート "dotnet ローカル ツール マニフェスト ファイル" が正常に作成されました。

❯ dotnet tool install Microsoft.dotnet-scaffold
次のコマンドを使用してこのディレクトリからツールを呼び出すことができます: 'dotnet tool run dotnet-scaffold' または 'dotnet dotnet-scaffold'。
ツール 'microsoft.dotnet-scaffold' (バージョン '9.0.0') が正常にインストールされました。マニフェスト ファイル C:\localrepo\tmp\.config\dotnet-tools.json にエントリが追加されました。
```

初めて `dotnet scaffold` を利用する場合はテレメトリの収集に関する注意書きが表示されるので、なにかキーを押してしばらく待ちます。テレメトリを収集してほしくない場合は、ツールのインストール前に[DOTNET_SCAFFOLD_TELEMETRY_OPTOUT](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dotnet-scaffold-telemetry?view=aspnetcore-9.0#how-to-opt-out)や[DOTNET_CLI_TELEMETRY_OPTOUT](https://learn.microsoft.com/ja-jp/dotnet/core/tools/dotnet-environment-variables#dotnet_cli_telemetry_optout)環境変数へ`true`, `1`, `yes`のいずれかを設定すればデータ収集は行われなくなるそうです。

```
❯ dotnet scaffold
Telemetry

dotnet-scaffold collects usage data in order to help us improve your experience. The data is collected by Microsoft and shared with the community. You can opt-out of telemetry by setting the
DOTNET_SCAFFOLD_TELEMETRY_OPTOUT environment variable to '1' or 'true' using your favorite shell.

Read more about dotnet-scaffold telemetry: https://aka.ms/dotnet-scaffold/telemetry
Read more about .NET CLI Tools telemetry: https://aka.ms/dotnet-cli-telemetry

Press any key to continue...


                                    Installing Microsoft.dotnet-scaffold-aspnet.
▰▰▱▱▱▱▱ Getting ready               Installing Microsoft.dotnet-scaffold-aspire.
```

## スキャフォールディングを使ったEFCoreのCRUD操作を持つコントローラーの追加

`dotnet scaffold`を引数無しで実行すると、利用可能なスキャフォールドの一覧が表示されます。

```
❯ dotnet scaffold
Steps
───────────────────────
 Scaffolding Category
───────────────────────


Pick a scaffolding category:

> API
  Aspire
  Blazor
  Identity
  MVC
  Razor Pages
  (Show All)

  Navigation
    Exit
```

後は、表示されたウィザードに従いメニューを選択したり、テキストを入力すると対象のファイルが作成されます。今回はEFCoreのCRUD操作を持つコントローラーを追加したいので、まずはモデルを追加します。モデルは純粋な.NETのクラスなのでScaffoldingの対象にはなっていたいため、VSCodeやOSからファイルを追加する必要がありそうです。

``` User.cs
using System.ComponentModel.DataAnnotations;

namespace SampleApp1.Models;

public class User
{
    public int Id { get; set; }
    
    [Required]
    [MaxLength(200)]
    public string Name { get; set; } = null!;
}
```

改めてdotnet scaffoldを実行して、コントローラーやデータコンテキストをスキャフォールドします。データベースにはMySQLを指定したいところですが、現時点では存在しないのでSQLiteを指定しました。

![ControllerScaffolding.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/eec5815d-da06-0612-f4fc-c0e036c39a40.gif)

スキャフォールドを実行するとこのあたりのファイルが作成されたり、更新されたりします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/943c2468-27aa-30d3-4cbd-8657048b2037.png)

`Microsoft.EntityFrameworkCore.Tools`への参照を忘れないのエラい。いつも新しいプロジェクトを作るとマイグレーション時に気づくことが多いので

```diff_xml:SampleApp1.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">

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

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0">
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+      <PrivateAssets>all</PrivateAssets>
+    </PackageReference>
  </ItemGroup>

</Project>

```

接続文字列もDbContextに直書きでなく、appsettings.jsonに外だしされるのはGood。

```diff_json:appsettings.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
-  "AllowedHosts": "*"
+  "AllowedHosts": "*",
+  "ConnectionStrings": {
+    "AppDbContext": "Data Source=AppDbContext.db"
+  }
}
```

Program.csにはDbContextの初期化コードだけ追加されます。

```diff_c_sharp:Program.cs
+using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
+var connectionString = builder.Configuration.GetConnectionString("AppDbContext") ?? throw new InvalidOperationException("Connection string 'AppDbContext' not found.");
+
+builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlite(connectionString));

// Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.UseHttpsRedirection();

var summaries = new[]
{
    "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

app.MapGet("/weatherforecast", () =>
{
    var forecast =  Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
})
.WithName("GetWeatherForecast");

app.Run();

record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
```

## ちょっとだけ修正

残念ながらサービスの依存関係（AddControllers）や、ルーティングの追加（MapControllers）は行ってくれなかったので、追加で修正する必要があります。あと、テスト用に開発時はSwaggerを使いたいのでパッケージを追加します。

```
❯ dotnet add package NSwag.AspNetCore
```

使わなそうなMinimal API部分はがりっと消して、3行追加しました。

```diff_c_sharp:Program.cs
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("AppDbContext") ?? throw new InvalidOperationException("Connection string 'AppDbContext' not found.");

builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlite(connectionString));

// Add services to the container.
+builder.Services.AddControllers();

// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
+    app.UseSwaggerUi(settings => settings.DocumentPath = "openapi/v1.json");
}

app.UseAuthorization();

+app.MapControllers();

app.Run();

```

マイグレーションスクリプトを登録後、データベースに反映して、

```
❯ dotnet tool install dotnet-ef
❯ dotnet ef migrations add first
❯ dotnet ef database update
```

実行します。

```
❯ dotnet run
```

出ました。ローカルのSQLiteに対してCRUD操作ができることを確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/787cfc25-3f61-52de-ec20-023bdd98fbe4.png)

## おわりに

次の部分でいくつか気になるところがありますが、
- プロジェクトのルート以外ではファイルを生成できないわりに、コントローラーがカレントに吐き出される
- UserControllerと入れたのにControllerが小文字になってしまっている
- コントローラーの利用に必要なサービスの依存関係（AddController）やルーティングの設定（MapControllers）が追加されない

これから拡充されていくと思うので期待します～