Blazor + Entity Framework + SQL Server で ゼロからCRUD構築手順 .Net 8

## １．新しいプロジェクトの作成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/c2583c77-5715-56ef-17ed-3222952ba64d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/8c898d56-9951-962f-43ee-0de2c4afe2d4.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/1e03e789-96dd-6974-d644-6833e4fc8e7d.png)

## ２．NuGetパッケージの追加
追加導入するパッケージは３つ
- Microsoft.EntityFrameworkCore
- Microsoft.EntityFrameworkCore.Design
- Microsoft.EntityFrameworkCore.SqlServer

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/521dd25e-aec0-1d55-be80-6acb17e16a58.png)


## ３．DB接続文字列をappsettings.jsonに登録

```json
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=.\\SQLEXPRESS;Database=MovieDb;Trusted_Connection=true;TrustServerCertificate=true"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

```
- .\\SQLEXPRESSの「.」はlocalhost
- Trusted_Connection=true
  → Windows認証で接続
- TrustServerCertificate=true
  →サーバー 証明書の検証をスキップ（信頼する）


## ４．テーブル構造（class）の作成
\\Models\Movie.cs
```C#
namespace Blazor_EF_CRUD.Models
{
    public class Movie
    {
        public int Id { get; set; }
        public required string Title { get; set; }
        public int ReleaseYear { get; set; }
    }
}
```
:::note info
名前が Id または [クラス名]Idのプロパティが自動的にプライマリーキーになる。
:::


## ５．DbContext継承クラスの作成
\\Context\DataContext.cs
```c#
using Blazor_EF_CRUD.Models;
using Microsoft.EntityFrameworkCore;

namespace Blazor_EF_CRUD.Context
{
    public class DataContext : DbContext
    {
        public DataContext(DbContextOptions<DataContext> options)
            : base(options)
        {
        }
        public required DbSet<Movie> Movies { get; set; }
    }
}
```
:::note info
DbContext継承クラスに登録されているDbSet（ここではMovieクラス）がマイグレーション時にテーブルとして作成される。
:::

## ６．テーブルアクセスの作成
### ６．１ インターフェース
\\Services\IMovieService.cs
```c#
using Blazor_EF_CRUD.Models;
namespace Blazor_EF_CRUD.Services
{
    public interface IMovieService
    {
        // ------------ READ ------------
        // 全件取得
        Task<List<Movie>> GetAllMovies();
        // IDで1件取得
        Task<Movie?> GetMovieById(int id);


        // ------------ CREATE ------------
        Task<Movie> AddMovie(Movie movie);
        // ------------ UPDATE ------------
        Task<Movie> EditMovie(int id, Movie movie);
        // ------------ DELETE ------------
        Task DeleteMovie(int id);
    }
}
```
### ６．２ サービス
\\Services\MovieService.cs
```c#
using Blazor_EF_CRUD.Context;
using Blazor_EF_CRUD.Models;
using Microsoft.EntityFrameworkCore;

namespace Blazor_EF_CRUD.Services
{
    public class MovieService : IMovieService
    {
        private readonly DataContext _context;

        public MovieService(DataContext context)
        {
            _context = context;
        }

        // ------------ READ ------------
        // 全件取得
        public async Task<List<Movie>> GetAllMovies()
        {
            var movies = await _context.Movies.ToListAsync();
            return movies;
        }

        // IDで1件取得
        public async Task<Movie?> GetMovieById(int id)
        {
            var movie = await _context.Movies.FindAsync(id);
            return movie;
        }

        // ------------ CREATE ------------
        public async Task<Movie> AddMovie(Movie movie)
        {
            _context.Movies.Add(movie);
            await _context.SaveChangesAsync();

            return movie;
        }

        // ------------ UPDATE ------------
        public async Task<Movie> EditMovie(int id, Movie newMovie)
        {
            var dbMovie = await _context.Movies.FindAsync(id);
            if (dbMovie == null)
            {
                throw new Exception($"Movie not found ID={id}");
            }

            dbMovie.Title = newMovie.Title;
            dbMovie.ReleaseYear = newMovie.ReleaseYear;

            _context.Movies.Update(dbMovie);
            await _context.SaveChangesAsync();
            return dbMovie;
        }

        // ------------ DELETE ------------
        public async Task DeleteMovie(int id)
        {
            var dbMovie = await _context.Movies.FindAsync(id);
            if (dbMovie == null)
            {
                throw new Exception($"Movie not found ID={id}");
            }
            _context.Remove(dbMovie);
            await _context.SaveChangesAsync();
        }
    }
}
```

## ７．program.csへ Entity Framework 情報追加
```C#
using Blazor_EF_CRUD.Components;
using Blazor_EF_CRUD.Context;
using Blazor_EF_CRUD.Services;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

// -------------- Entity Framework ---------------
var dbConnection = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<DataContext>(options =>
    options.UseSqlServer(dbConnection));

builder.Services.AddScoped<IMovieService, MovieService>();
// -----------------------------------------------

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    // 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.UseAntiforgery();

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

app.Run();
```

## ８．マイグレーションの開始
### ８．１ パッケージマネージャーコンソールの表示
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/5d28001a-3a70-d332-3fc8-9efc844b0fd4.png)

### ８．２ program.csのあるディレクトリへ移動
```
PM> cd D:\Dev\Blazor_EF_CRUD\Blazor_EF_CRUD
PM> dir


    ディレクトリ: D:\Dev\Blazor_EF_CRUD\Blazor_EF_CRUD


Mode                 LastWriteTime         Length Name                                              
----                 -------------         ------ ----                                              
d-----        2024/12/03      5:07                bin                                               
d-----        2024/12/03      5:07                Components                                        
d-----        2024/12/03      5:32                Context                                           
d-----        2024/12/03      5:30                Models                                            
d-----        2024/12/03      5:13                obj                                               
d-----        2024/12/03      5:07                Properties                                        
d-----        2024/12/03      5:49                Services                                          
d-----        2024/12/03      5:07                wwwroot                                           
-a----        2024/12/03      5:07            127 appsettings.Development.json                      
-a----        2024/12/03      6:00            301 appsettings.json                                  
-a----        2024/12/03      6:09            686 Blazor_EF_CRUD.csproj                             
-a----        2024/12/03      5:07            238 Blazor_EF_CRUD.csproj.user                        
-a----        2024/12/03      6:03           1157 Program.cs                                        


PM> 
```


### ８．３ マイグレーションの作成
```
PM> dotnet ef migrations add Initial
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/90232c41-23f4-c0fb-ef58-154457a2855c.png)

### ８．４ マイグレーションをSQL Serverへ反映
```
PM> dotnet ef database update
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/fb49afc3-43b7-6a68-8c2a-9d99bc50121b.png)

### ８．５ SQL SERVERで作成されたDBとテーブルを確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/03ceeaa6-9a71-d2ff-79a4-68e16316c027.png)




## ９．Blazor側を作成

### ９．１ 一覧画面
\\Components\Pages\Movies.razor

```html
@page "/movies"
@using Blazor_EF_CRUD.Models
@using Blazor_EF_CRUD.Services
@inject IMovieService MovieService
@inject NavigationManager NavigationManager
@attribute [StreamRendering(true)]
@rendermode @(new InteractiveServerRenderMode(prerender: false))

<h3>MovieList</h3>
@if (movies == null)
{
    <p>Loading...</p>
}
else
{
    <div class="mb-3">
        <table class="table">
            <thead>
                <tr>
                    <th>タイトル</th>
                    <th>発表</th>
                    <th></th>
                    <th></th>
                </tr>
            </thead>
            <tbody>
                @foreach (var movie in movies)
                {
                    <tr>
                        <td class="align-middle">@movie.Title</td>
                        <td class="align-middle">@movie.ReleaseYear</td>
                        <td class="align-middle"><button @onclick="() => EditMovie(movie.Id)" class="btn btn-link">編集</button></td>
                        <td class="align-middle"><button @onclick="() => DeleteMovie(movie.Id)" class="btn btn-danger">削除</button></td>
                    </tr>
                }
            </tbody>
        </table>
    </div>
}
<div class="mb-3">
    <button @onclick="CreateMovie" class="btn btn-primary">新しい映画を追加</button>
</div>


@code {
    List<Movie>? movies = null;

    protected override async Task OnInitializedAsync()
    {
        movies = await MovieService.GetAllMovies();
    }

    void CreateMovie(){
        NavigationManager.NavigateTo("/editmovie");
    }

    void EditMovie(int id)
    {
        NavigationManager.NavigateTo($"/editmovie/{id}");
    }

    async Task DeleteMovie(int id)
    {
        await MovieService.DeleteMovie(id);
        NavigationManager.NavigateTo(NavigationManager.Uri, true);
    }
}
```


### ９．２ 登録／修正画面
\\Components\Pages\EditMovie.razor

```html
@page "/editmovie"
@page "/editmovie/{Id:int}"
@using Blazor_EF_CRUD.Models
@using Blazor_EF_CRUD.Services
@inject IMovieService MovieService
@inject NavigationManager NavigationManager
@rendermode InteractiveServer

<h3>Edit Movie</h3>
<EditForm Model="movie" OnSubmit="HandleSubmit">
    <div>
        <div class="mb-3">
            <label class="form-label">タイトル</label>
            <InputText @bind-Value="movie.Title" class="form-control"></InputText>
        </div>
        <div class="mb-3">
            <label class="form-label">発表（年）</label>
            <InputNumber @bind-Value="movie.ReleaseYear" class="form-control"></InputNumber>
        </div>
    </div>
    <button type="submit" class="btn btn-primary">登録</button>
</EditForm>

@code {
    [Parameter]
    public int? Id { get; set; }
    Movie movie { get; set; } = new Movie{Title = string.Empty};

    // Idを受け取った場合はデータ取得
    protected override async Task OnParametersSetAsync()
    {
        if (Id == null) {
            return;
        }
        var getMovie = await MovieService.GetMovieById((int)Id);
        if (getMovie != null){
            movie = getMovie;
        }
    }

    async Task HandleSubmit()
    {
        if(Id == null) {
            await MovieService.AddMovie(movie);
        }else{
            await MovieService.EditMovie((int)Id, movie);
        }

        NavigationManager.NavigateTo("/movies");
    }
}
```

### ９．３ ナビゲーションメニューの修正
\\Components\Layout\NavMenu.razor
```html
<div class="top-row ps-3 navbar navbar-dark">
    <div class="container-fluid">
        <a class="navbar-brand" href="">Blazor_EF_CRUD</a>
    </div>
</div>

<input type="checkbox" title="Navigation menu" class="navbar-toggler" />

<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
    <nav class="flex-column">
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
            </NavLink>
        </div>

        <div class="nav-item px-3">
            <NavLink class="nav-link" href="movies">
                <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-film" viewBox="0 0 16 16">
                    <path d="M0 1a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1zm4 0v6h8V1zm8 8H4v6h8zM1 1v2h2V1zm2 3H1v2h2zM1 7v2h2V7zm2 3H1v2h2zm-2 3v2h2v-2zM15 1h-2v2h2zm-2 3v2h2V4zm2 3h-2v2h2zm-2 3v2h2v-2zm2 3h-2v2h2z" />
                </svg> Movies
            </NavLink>
        </div>
    </nav>
</div>
```

## １０． 動作確認

### １０．１ 初期一覧画面（登録データ0件）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/8ca7fd7a-fa76-2fdd-60d5-4d35f5cef254.png)

### １０．２ 登録画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/66c7b94c-f539-1e02-d2d0-bb1fad28a13f.png)

### １０．３ 数件登録後の一覧画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/25893f6b-acb9-1024-e00c-1445c3b69d58.png)

### １０．４ 修正画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/599c0a77-0d6c-ce54-7e9b-f73ea7bb610b.png)

## １１．全ソース
https://github.com/masayahak/Blazor_EF_CRUD