ASP.NET Core MVCでシンプルなToDoListを作ってみた

# はじめに
C#でシンプルなToDoリストWebアプリを作ってみます。
DBはローカルのSQL Serverを使います。
Entity Framework Coreのスキャフォールディング (リバース エンジニアリング)という仕組みを使ってDBのテーブル定義からC#側のクラスを自動的に作成してみます。

## 使う技術スタック
.NET 6.0
ASP.Net Core MVC
Entity Framework Core

## 使う開発ツール
Visual Studio2022
SQLServer Management Studio

# Visual Studioでプロジェクト作成
Create a new project

↓TemplateはASP.NET Core Web App(MVC)を選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378457/eff06864-9e68-166a-239f-67542e16e251.png)

プロジェクト名は`ToDoList`にして任意の場所に保存。
Frameworlは.Net6.0を選択してCreateボタンを押下。

↓プロジェクトができたら▷ボタンを押下してデバッグしてみる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378457/3b42af65-38fc-c088-968a-a6fdc040fad6.png)

↓ASP.NET Core Web App(MVC)のテンプレが立ち上がったらOK
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378457/07dd5a05-a235-86f1-2cf2-37bf768a84b7.png)

# データベースとテーブルの作成
SSMS(SQLServer Management Studio)を立ち上げる。

新しいデータベースを作成する。
データベース名は`ToDoList`とする。

データベース:ToDoListに以下のDDLを実行してテーブルを作成する。

```sql:Create文
Create Table [dbo].[ToDo] (
[id] UNIQUEIDENTIFIER DEFAULT NEWID() NOT NULL 
,
[ToDo] [nvarchar] (50)
,
[DueDate] [datetime] NOT NULL 
, 
CONSTRAINT [PK_ToDo] PRIMARY KEY (id)
);

```

PKが一つしかないシンプルなテーブル定義にしている。
PKである`id`はUUIDで自動採番されるようにする。

テーブルが作成できたら↓の方法でテストデータを登録しておく。（ただしidはUUIDなので値をいれなくていい）

https://mugendennou.net/sqlserver/sqlserverutil/pastedatafromexcel/

# Nugetパッケージの入手
[ツール] メニューで、[NuGet パッケージ マネージャー]>[パッケージ マネージャー コンソール] (PMC) の順に選択

以下のパッケージをインストールする。
```
Install-Package Microsoft.EntityFrameworkCore
Install-Package Microsoft.EntityFrameworkCore.SqlServer
Install-Package Microsoft.EntityFrameworkCore.Design
Install-Package Microsoft.EntityFrameworkCore.Tools
```

↓`Microsoft.EntityFrameworkCore.Design`を実行する時のイメージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378457/c5ae0507-b1da-51b8-0c37-009a7beefb6c.png)

# Visual StudioとSQLServerの接続

「表示」→「SQL Server オブジェクトエクスプローラー」
↓こんな感じで、SSMSで作成したDB（ToDoList）が表示されるようにする
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378457/22212a41-2237-62e0-0fbf-3a146538586d.png)

SQLServerを右クリックしてプロパティを表示。
General>ConnectionStringから接続文字列をコピー

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378457/13ac0979-59d2-6f31-8781-a154798c8ada.png)

コピーした接続文字列にカタログ名`Initial Catalog=ToDoList`を追加

```
Data Source=(サーバ名)\\SQLEXPRESS;Initial Catalog=ToDoList(カタログ名);Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False
```

ソリューションエクスプローラーからappsettings.jsonを選択
↓のように`ConnectionStrings`を追加する。
`ToDoList`のValueに接続文字列を貼り付ける

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "ToDoList": "Data Source=(サーバ名)\\SQLEXPRESS;Initial Catalog=ToDoList;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False"  }
}
```

# リバーススキャフォールディング(モデルとコンテキスト作成）
DB`ToDoList`のテーブル`ToDo`のテーブル定義を読み取ってC#側にクラスを作っていく。
まずは以下の手順でモデルとコンテキストを作る。

[ツール] メニューで、[NuGet パッケージ マネージャー]>[パッケージ マネージャー コンソール] (PMC) の順に選択
以下のコマンドを入力。

```
Scaffold-DbContext 'Name=ConnectionStrings:ToDoList' Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models -ContextDir Context -Tables ToDo
```

`Name=ConnectionStrings:ToDoList`でappsetting.jsonで指定した接続文字列を取得している。
`-OutputDir Models`は、作成されるモデルクラスの出力先フォルダを指定。
`-ContextDir Context`で、作成されるコンテキストクラスの出力先フォルダを指定。
`-Tables ToDo`で、カタログ`ToDoList`のToDoテーブルを読み取ることを指定。

↓コマンドの結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378457/ab76c342-d25b-38fa-62f2-2827cda23c61.png)

↓こんな感じで、ContextとModelのクラスが生成される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378457/20f45f62-21c1-fc55-1cb5-1bb8f846e752.png)

↓ToDo.csの中身はこんな感じ。
```C#:ToDo.cs
using System;
using System.Collections.Generic;

namespace ToDoList.Models;

public partial class ToDo
{
    public Guid Id { get; set; }

    public string? ToDo1 { get; set; }

    public DateTime DueDate { get; set; }
}
```


↓ToDoListContext.csの中身
```C#:ToDoListContext.cs
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using ToDoList.Models;

namespace ToDoList.Context;

public partial class ToDoListContext : DbContext
{
    public ToDoListContext()
    {
    }

    public ToDoListContext(DbContextOptions<ToDoListContext> options)
        : base(options)
    {
    }

    public virtual DbSet<ToDo> ToDos { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder.UseSqlServer("Name=ConnectionStrings:ToDoList");

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<ToDo>(entity =>
        {
            entity.ToTable("ToDo");

            entity.Property(e => e.Id)
                .HasDefaultValueSql("(newid())")
                .HasColumnName("id");
            entity.Property(e => e.DueDate).HasColumnType("datetime");
            entity.Property(e => e.ToDo1)
                .HasMaxLength(50)
                .HasColumnName("ToDo");
        });

        OnModelCreatingPartial(modelBuilder);
    }

    partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}

```

↓プロジェクト直下の`Program.cs`からDBContextを参照させる。
以下のコードを追加。
```C#:Program.cs
/*ここから*/
using Microsoft.EntityFrameworkCore;
using ToDoList.Context;
/*ここまで */
var builder = WebApplication.CreateBuilder(args);
/*ここから*/
builder.Services.AddDbContext<ToDoListContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("ToDoList") ?? throw new InvalidOperationException("Connection string 'ToDoList' not found.")));
/*ここまで*/
// Add services to the container.
builder.Services.AddControllersWithViews();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

```

# スキャフォールディング(ビューとコントローラー作成)

モデルとコンテキストの次は、ビューとコントローラーを作成する。

↓ソリューション エクスプローラーで、Controllers フォルダーを右クリックし、[追加] > [新規スキャフォールディング アイテム] の順に選択。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378457/952e644c-d996-e044-4bb1-78309c391b88.png)


[スキャフォールディングを追加] ダイアログで、[Entity Framework を使用したビューがある MVC コントローラー] > [追加] の順に選択

↓ModelClassに`ToDo`を、DataContext classに`ToDoListContext`を選択してAddを押下
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378457/d48b7928-5987-86de-93f2-a1ea36e1a02b.png)

↓ビューとコントローラーが追加されている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378457/e198c463-a060-fba8-7c71-2ba171a56593.png)

テーブル`ToDo`にCRUDできるか確認する。
ツールバーの▷ボタンを押下し、デバッグを実行する。

トップページが表示されたら、以下のURLにアクセス
`https://localhost:(ポート番号)/ToDoList`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378457/a0f94844-5afa-c3ad-5498-dbecd4c046db.png)

Create Newを押下してToDoを登録する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378457/b17ff9e7-8366-2635-cb96-12721c59fb91.png)

↓登録したToDoをSSMS側で確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378457/6d02aeb8-33e3-5e6a-d53b-ddafb6fad055.png)

# テンプレートのいらない部分削除
`Viewa>Shared>_Layout.cshtml`を、以下の通り不要部部分を削除

```html:_Lauout.cshtml
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - ToDoList</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
    <link rel="stylesheet" href="~/ToDoList.styles.css" asp-append-version="true" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container-fluid">
                <!--a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">ToDoList</a-->
                <!--↓コントローラーを更新></-->
                <a class="navbar-brand" asp-area="" asp-controller="ToDoList" asp-action="Index">ToDoList</a>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <!--div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
                        </li>
                    </ul>
                </div-->
            </div>
        </nav>
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2023 - ToDoList - <!--a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a-->
        </div>
    </footer>
    <script src="~/lib/jquery/dist/jquery.min.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>
    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>
```

`Program.cs`の`app.MapControllerRoute`のコントローラーを更新

```C#:Program.cs
app.MapControllerRoute(
    name: "default",
    /* ↓このように更新 pattern: "{controller=Home}/{action=Index}/{id?}"); */
    pattern: "{controller=ToDoList}/{action=Index}/{id?}");
```

以下のファイル・フォルダを削除
Controllers>HomeController.cs
Views>Home
Views>Home>Index.cshtml
Views>Home>Privacy.cshtml
Context>MasterContext.cs

完成!

# 参考
https://hirahira.blog/c-sharp-sqlserver-connect/

https://learn.microsoft.com/ja-jp/ef/core/managing-schemas/scaffolding/?tabs=dotnet-core-cli

https://qiita.com/t13801206/items/cafcda5b4a5664fc3d64

https://learn.microsoft.com/ja-jp/aspnet/core/tutorials/first-mvc-app/adding-model?view=aspnetcore-6.0&tabs=visual-studio