ASP.NET Core Blazor WebAssembly と Web API と Entity Framework Core で SQL Server のデータを取得したり追加したり更新したり削除したりする [.NET 6 版]

# はじめに

2022 年 11 月に .NET 6 と Visual Studio 2022 がリリースされました。ということで .NET 6 版の Blazor WebAssembly / ASP.NET Core Web API / Entity Framework を使って  CRUD アプリケーションを作っていきたいと思います。

（以前書いた[これ](https://qiita.com/tamtamyarn/items/835ca4bd262ae35cfe5a)の .NET 6 版です）

https://qiita.com/tamtamyarn/items/835ca4bd262ae35cfe5a

## 完成イメージ

![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/383318/8f1f529d-0491-d8da-f8e3-1ea291792935.gif)

## 環境

- Windows 10 Pro
- Microsoft Visual Studio Community 2022 (64 ビット) 
- .NET 6.0

# プロジェクト作成

Visual Studio を起動して「新しいプロジェクトの作成」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/383318/990dd6bf-4326-d7e1-787b-5dc7c78e3965.png)

「Blazor WebAssembly アプリ」を選択して「次へ」。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/383318/1ee10eac-9ae3-ee39-be95-f78eefebd8ab.png)

プロジェクト名とソリューション名を「BlazorApps」、任意の場所を選択して「次へ」。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/383318/8f44aa29-11cf-feca-388a-50ffec3904a0.png)

フレームワークに .NET 6.0を選択し、「HTTPS 用の構成」と「ASP.NET Core でホストされた」を有効にして「作成」。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/383318/575b26f1-8814-d0bd-aa9e-7fe3d8bd78fa.png)

以下のようにプロジェクトが３つ生成されます。Client がフロントエンド、Server がバックエンドで、Shared には共通で利用するクラスを配置します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/383318/d8d4513d-04a4-c701-7671-6640b900d292.png)

プロジェクトファイルを見てみましょう。

```xml:BlazorApp.Client.csproj
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

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

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.0" />
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.0" PrivateAssets="all" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\Shared\BlazorApp.Shared.csproj" />
  </ItemGroup>

</Project>

```

```xml:BlazorApp.Server.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">

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

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="6.0.0" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\Client\BlazorApp.Client.csproj" />
    <ProjectReference Include="..\Shared\BlazorApp.Shared.csproj" />
  </ItemGroup>


</Project>

```

```xml:BlazorApp.Shared.csproj
<Project Sdk="Microsoft.NET.Sdk">

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

  <ItemGroup>
    <SupportedPlatform Include="browser" />
  </ItemGroup>
</Project>

```

.NET 6.0 では既定で Null 許容参照型と ImplicitUsings が有効になります。

```xml
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
```

# データベース作成（コードファースト）

データベース用のモデルクラスを作成していきます。あとでクライアント側でも同じクラスを使うので、`BlazorApp.Shared` プロジェクト直下に `Models` フォルダーを作成して、その配下に `Book.cs` を作成します。

```csharp:BlazorApp.Shared/Models/Book.cs
namespace BlazorApp.Shared.Models
{
    public class Book
    {
        public int BookId { get; set; }
        public string Title { get; set; }
        public string Author { get; set; }

        public Book(int bookId, string title, string author)
        {
            BookId = bookId;
            Title = title;
            Author = author;
        }
    }
}

```

パッケージマネージャーコンソールから Entity Framework Core のパッケージをインストールします。

```posh
Install-Package -ProjectName BlazorApp.Server -Id Microsoft.EntityFrameworkCore.SqlServer
Install-Package -ProjectName BlazorApp.Server -Id Microsoft.EntityFrameworkCore.Tools
```

次にエンティティクラスとテーブルをマッピングするためのデータベースコンテキストクラスを作成していきます。`BlazorApp.Server` プロジェクト直下に `Data` フォルダーを作成し、`AppDbContext` クラスを作成します。

```csharp:BlazorApp.Server/Data/AppDbContext.cs
using BlazorApp.Shared.Models;
using Microsoft.EntityFrameworkCore;

namespace BlazorApp.Server.Data
{
    public class AppDbContext : DbContext
    {
        public AppDbContext(DbContextOptions options) : base(options)
        {
        }

        public DbSet<Book> Books => Set<Book>();
    }
}

```

`BlazorApp.Server` プロジェクトの `Program.cs` にコンテクストクラスをサービスとして登録します。

```csharp:BlazorApp.Server/Program.cs
using BlazorApp.Server.Data;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); //追加

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseWebAssemblyDebugging();
}
else
{
    app.UseExceptionHandler("/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.UseBlazorFrameworkFiles();
app.UseStaticFiles();

app.UseRouting();


app.MapRazorPages();
app.MapControllers();
app.MapFallbackToFile("index.html");

app.Run();

```

`appsettings.json` に接続文字列を追加します。下記では LocalDb に BlazorAppDb という名前のデータベースを指定しています。

```json:BlazorApp.Server/appsettings.json
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=BlazorAppDb;Trusted_Connection=True;"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

```

パッケージマネージャーコンソールで以下のコマンドを実行します。（Migrations フォルダーと Migration クラスが作成される）

```posh
Add-Migration -Name Initial
```

続けて以下のコマンドを実行。（データベースとテーブルが作成される）

```posh
Update-Database
```

SQL Server Managment Studio か何かで以下のクエリを実行してレコードを追加しておきます。

```sql
INSERT INTO Books (Title, Author) VALUES (N'たったひとつの冴えたやりかた',N'ジェイムズ・ティプトリー・ジュニア');
INSERT INTO Books (Title, Author) VALUES (N'アンドロイドは電気羊の夢を見るか?',N'フィリップ・K・ディック');
INSERT INTO Books (Title, Author) VALUES (N'夏への扉',N'ロバート・A. ハインライン');
INSERT INTO Books (Title, Author) VALUES (N'幼年期の終り',N'アーサー C クラーク');
INSERT INTO Books (Title, Author) VALUES (N'われはロボット',N'アイザック・アシモフ');
```

# Read（全件）

## バックエンド

全件取得用の Web API を準備します。`BlazorApp.Server` プロジェクトの `Controllers` フォルダーに `BooksController` コントローラーを追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/383318/25a472ae-f1f6-f463-b7ad-f22574f47b9a.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/383318/b1c6c39f-5ce9-a84e-63c0-53b30b1e228b.png)


```csharp:BlazoreApp.Server/Controllers/BooksController.cs
using BlazorApp.Server.Data;
using BlazorApp.Shared.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace BlazorApp.Server.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class BooksController : ControllerBase
    {
        private readonly AppDbContext context;

        public BooksController(AppDbContext context)
        {
            this.context = context;
        }

        [HttpGet]
        public async Task<ActionResult<List<Book>>> ListAsync()
        {
            var books = await context.Books.ToListAsync();
            return Ok(books);
        }
    }
}

```

デバッグ実行して `/api/books` にアクセスして、以下の情報が取得できることを確認しておきます。

```json
[
	{
		"bookId": 1,
		"title": "たったひとつの冴えたやりかた",
		"author": "ジェイムズ・ティプトリー・ジュニア"
	},
	{
		"bookId": 2,
		"title": "アンドロイドは電気羊の夢を見るか?",
		"author": "フィリップ・K・ディック"
	},
	{
		"bookId": 3,
		"title": "夏への扉",
		"author": "ロバート・A. ハインライン"
	},
	{
		"bookId": 4,
		"title": "幼年期の終り",
		"author": "アーサー C クラーク"
	},
	{
		"bookId": 5,
		"title": "われはロボット",
		"author": "アイザック・アシモフ"
	}
]
```

## Ant Design Blazor インストール

今回は UI コンポーネントライブラリを使ってフロントエンドを開発していきます。ライブラリは [Awesome Blazor](https://github.com/AdrienTorris/awesome-blazor) によると一番人気がありそうな [Ant Design Blazor](https://antblazor.com/en-US/) を使います。

パッケージマネージャーから以下のコマンドを実行して `AntDesign` をインストールします。

```posh
Install-Package -ProjectName BlazorApp.Client -Id AntDesign
```

`BlazorApp.Client` プロジェクトの `Program.cs` にサービスを登録します。

```csharp:BlazorApp.Client/Program.cs
using BlazorApp.Client;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddAntDesign(); //追加

await builder.Build().RunAsync();

```

`BlazorApp.Client` プロジェクト `wwwroot/index.html` に CSS と JavaScript を追加します。

```html:BlazorApp.Client/wwwroot/index.html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>BlazorApp</title>
    <base href="/" />
    <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
    <link href="css/app.css" rel="stylesheet" />
    <link href="BlazorApp.Client.styles.css" rel="stylesheet" />
    <link href="_content/AntDesign/css/ant-design-blazor.css" rel="stylesheet" /><!--追加-->
</head>

<body>
    <div id="app">Loading...</div>

    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>
    <script src="_framework/blazor.webassembly.js"></script>
    <script src="_content/AntDesign/js/ant-design-blazor.js"></script><!--追加-->
</body>

</html>

```

`BlazorApp.Client` プロジェクト直下の `_imports.razor` に `AntDesign` を追加します。

```:BlazorApp.Client/_imports.razor
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using BlazorApp.Client
@using BlazorApp.Client.Shared
@using AntDesign

```

`BlazorApp.Client` プロジェクト直下の `App.razor` に `<AntContainer />` を追加します。

```xml:BlazorApp.Client/App.razor
<Router AppAssembly="@typeof(App).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
    <NotFound>
        <PageTitle>Not found</PageTitle>
        <LayoutView Layout="@typeof(MainLayout)">
            <p role="alert">Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

<AntContainer /><!--追加-->
```

Ant Design Blazor を使う準備は以上です。

### フロントエンド

`BlazorApp.Client` プロジェクトの `Pages` フォルダー配下に `BookList` Razor コンポーネントを追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/383318/4569b07d-074e-ac10-4a59-4bfef391b19e.png)

`BookList` コンポーネントを以下のように編集します。

```razor:BlazorApp.Client/Pages/BookList.razor
@page "/booklist"
@using BlazorApp.Shared.Models
@inject HttpClient Http

@if (books == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <Table TItem="Book" DataSource="@books">
        <Column @bind-Field="@context.BookId" Title="ID" />
        <Column @bind-Field="@context.Title" />
        <Column @bind-Field="@context.Author" />
    </Table>
}

@code {
    private List<Book>? books;

    protected override async Task OnInitializedAsync()
    {
        books = await Http.GetFromJsonAsync<List<Book>>("api/books");
    }
}

```

コンポーネントを開く際に `OnInitializedAsycn()` メソッドが呼ばれるため、そこで先ほど作った API を呼んで、テーブルに書き出すという流れです。

このままだと動線がないので、`Shared/NavMenu.razor` のリンクに `BookList` へのリンクを追加しておきます。

```razor:BlazorApp.Client/Shared/NavMenu.razor
<div class="top-row ps-3 navbar navbar-dark">
    <div class="container-fluid">
        <a class="navbar-brand" href="">BlazorApp</a>
        <button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
            <span class="navbar-toggler-icon"></span>
        </button>
    </div>
</div>

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <nav class="flex-column">
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="counter">
                <span class="oi oi-plus" aria-hidden="true"></span> Counter
            </NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="fetchdata">
                <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
            </NavLink>
        </div>
        <!--追加-->
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="booklist">
                <span class="oi oi-book" aria-hidden="true"></span> Book list
            </NavLink>
        </div>
        <!--追加ここまで-->
    </nav>
</div>

@code {
    private bool collapseNavMenu = true;

    private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;

    private void ToggleNavMenu()
    {
        collapseNavMenu = !collapseNavMenu;
    }
}

```

ここまで出来たら実行して以下のように表示されれば OK です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/383318/8dcb1f90-3f29-b4c7-1ea4-87282546a933.png)

# Read（1 件）

## バックエンド

1 件取得用の Web API を準備します。`BooksController.cs` に `GetAsync` メソッドを追加します。

```csharp:BlazoreApp.Server/Controllers/BooksController.cs
using BlazorApp.Server.Data;
using BlazorApp.Shared.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace BlazorApp.Server.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class BooksController : ControllerBase
    {
        private readonly AppDbContext context;

        public BooksController(AppDbContext context)
        {
            this.context = context;
        }

        [HttpGet]
        public async Task<ActionResult<List<Book>>> ListAsync()
        {
            var books = await context.Books.ToListAsync();
            return Ok(books);
        }

        //追加
        [HttpGet("{id}")]
        public async Task<ActionResult<Book>> GetAsync(int id)
        {
            var book = await context.Books.SingleOrDefaultAsync(b => b.BookId.Equals(id));

            if (book == null)
            {
                return NotFound();
            }

            return Ok(book);
        }
    }
}

```

## フロントエンド

`BookDetail` コンポーネントを追加します。（Detail と言っても今回は一覧と内容同じだけど）

```razor:BlazorApp.Client/Pages/BookDetail.razor
@using BlazorApp.Shared.Models

@if (Book == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <Descriptions>
        <DescriptionsItem Title="ID" Span="3">@Book.BookId</DescriptionsItem>
        <DescriptionsItem Title="Title" Span="3">@Book.Title</DescriptionsItem>
        <DescriptionsItem Title="Author" Span="3">@Book.Author</DescriptionsItem>
    </Descriptions>
}

@code {
    [Parameter]
    public Book? Book { get; set; }
}

```

ID をクリックすると `BookDetail` コンポーネントをモーダルに表示するように `BookList` コンポーネントを修正します。

```razor:BlazorApp.Client/Pages/BookList.razor
@page "/booklist"
@using BlazorApp.Shared.Models
@inject HttpClient Http

@if (books == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <Table TItem="Book" DataSource="@books">
        <Column @bind-Field="@context.BookId" Title="ID">
            <Button Type="link" OnClick="() => OpenDetailModal(context.BookId)">@context.BookId</Button>
        </Column>
        <Column @bind-Field="@context.Title" />
        <Column @bind-Field="@context.Author" />
    </Table>

    <Modal Title="Detail" Visible="@showDetailModal" OnCancel="@CloseModal" Footer="@null">
        <BookDetail Book="@book"></BookDetail>
    </Modal>
}

@code {
    private List<Book>? books;
    private Book? book;
    private bool showDetailModal = false;

    protected override async Task OnInitializedAsync()
    {
        books = await Http.GetFromJsonAsync<List<Book>>("api/books");
    }

    public async Task OpenDetailModal(int id)
    {
        book = await Http.GetFromJsonAsync<Book>($"api/books/{id}");
        showDetailModal = true;
    }

    public void CloseModal()
    {
        showDetailModal = false;
    }
}

```

こんな感じになります。

![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/383318/6670f74f-1b79-5e0f-9579-5f888c094021.gif)

# Create

## バックエンド

`BooksController` に `Create` メソッドを追加します。

```csharp:BlazoreApp.Server/Controllers/BooksController.cs
using BlazorApp.Server.Data;
using BlazorApp.Shared.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace BlazorApp.Server.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class BooksController : ControllerBase
    {
        private readonly AppDbContext context;

        public BooksController(AppDbContext context)
        {
            this.context = context;
        }

        [HttpGet]
        public async Task<ActionResult<List<Book>>> ListAsync()
        {
            var books = await context.Books.ToListAsync();
            return Ok(books);
        }

        [HttpGet("{id}")]
        public async Task<ActionResult<Book>> GetAsync(int id)
        {
            var book = await context.Books.SingleOrDefaultAsync(b => b.BookId.Equals(id));

            if (book == null)
            {
                return NotFound();
            }

            return Ok(book);
        }

        //追加
        [HttpPost]
        public async Task<ActionResult<Book>> CreateAsync(Book book)
        {
            if (book.BookId != 0)
            {
                return BadRequest();
            }

            context.Books.Add(book);
            await context.SaveChangesAsync();

            return CreatedAtAction("Get", new { id = book.BookId }, book);
        }
    }
}

```

## フロントエンド

`BookCreate` コンポーネントを追加します。

```razor:BlazorApp.Client/Pages/BookCreate.razor
@using BlazorApp.Shared.Models
@using System.Text.Json
@inject HttpClient Http
@inject NotificationService Notice

@if (Book == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <Form Model="@Book" OnFinish="Add" LabelColSpan="4" WrapperColSpan="20">
    <FormItem Label="Title">
        <Input @bind-Value="@context.Title" />
    </FormItem>
    <FormItem Label="Author">
        <Input @bind-Value="@context.Author" />
    </FormItem>
    <FormItem WrapperColSpan="24" Style="text-align: center">
        <Button Type="primary" HtmlType="submit">Add</Button>
    </FormItem>
    </Form>
}

@code {
    [Parameter]
    public Book? Book { get; set; }

    [Parameter]
    public EventCallback Callback { get; set; }

    private async Task Add()
    {
        var response = await Http.PostAsJsonAsync("api/books", Book);

        var createdBookJson = await response.Content.ReadAsStringAsync();

        await Callback.InvokeAsync();
        await Notice.Open(new NotificationConfig()
        {
            Message = "New Book is Created!",
            Description = createdBookJson
        });
    }
}
```

ボタンをクリックすると `BookCreate` コンポーネントをモーダルに表示するように `BookList` コンポーネントを修正します。

```razor:BlazorApp.Client/Pages/BookList.razor
@page "/booklist"
@using BlazorApp.Shared.Models
@inject HttpClient Http

@if (books == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <Button Type="primary" OnClick="OpenCreateModal" Block>New Book</Button>
    <Divider></Divider>

    <Table TItem="Book" DataSource="@books">
        <Column @bind-Field="@context.BookId" Title="ID">
            <Button Type="link" OnClick="() => OpenDetailModal(context.BookId)">@context.BookId</Button>
        </Column>
        <Column @bind-Field="@context.Title" />
        <Column @bind-Field="@context.Author" />
    </Table>

    <Modal Title="Detail" Visible="@showDetailModal" OnCancel="@CloseModal" Footer="@null">
        <BookDetail Book="@book"></BookDetail>
    </Modal>

    <Modal Title="Create" Visible="@showCreateModal" OnCancel="@CloseModal" Footer="@null">
        <BookCreate Book="@book" Callback="@OnCallback"></BookCreate>
    </Modal>
}

@code {
    private List<Book>? books;
    private Book? book;
    private bool showDetailModal = false;
    private bool showCreateModal = false;

    protected override async Task OnInitializedAsync()
    {
        books = await Http.GetFromJsonAsync<List<Book>>("api/books");
    }

    public async Task OpenDetailModal(int id)
    {
        book = await Http.GetFromJsonAsync<Book>($"api/books/{id}");
        showDetailModal = true;
    }

    public void CloseModal()
    {
        showDetailModal = false;
        showCreateModal = false;
    }

    private void OpenCreateModal()
    {
        book = new(0, "", "");
        showCreateModal = true;
    }

    private async Task OnCallback()
    {
        books = await Http.GetFromJsonAsync<List<Book>>("api/books");
        showCreateModal = false;
    }
}

```

こんな感じになります。

![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/383318/473b6ed4-8ce8-3f3a-6c26-769163774870.gif)

# Delete

## バックエンド

`BooksController` に `Delete` メソッドを追加します。

```csharp:BlazoreApp.Server/Controllers/BooksController.cs
using BlazorApp.Server.Data;
using BlazorApp.Shared.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace BlazorApp.Server.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class BooksController : ControllerBase
    {
        private readonly AppDbContext context;

        public BooksController(AppDbContext context)
        {
            this.context = context;
        }

        [HttpGet]
        public async Task<ActionResult<List<Book>>> ListAsync()
        {
            var books = await context.Books.ToListAsync();
            return Ok(books);
        }

        [HttpGet("{id}")]
        public async Task<ActionResult<Book>> GetAsync(int id)
        {
            var book = await context.Books.SingleOrDefaultAsync(b => b.BookId.Equals(id));

            if (book == null)
            {
                return NotFound();
            }

            return Ok(book);
        }

        [HttpPost]
        public async Task<ActionResult<Book>> CreateAsync(Book book)
        {
            if (book.BookId != 0)
            {
                return BadRequest();
            }

            context.Books.Add(book);
            await context.SaveChangesAsync();

            return CreatedAtAction("Get", new { id = book.BookId }, book);
        }

        [HttpDelete("{id}")]
        public async Task<ActionResult> DeleteAsync(int id)
        {
            var book = await context.Books.SingleOrDefaultAsync(b => b.BookId.Equals(id));

            if (book == null)
            {
                return NotFound();
            }

            context.Books.Remove(book);
            await context.SaveChangesAsync();

            return NoContent();
        }
    }
}

```

## フロントエンド

ボタンをクリックするとアイテムを削除するように BookList コンポーネントを修正します。

```razor:BlazorApp.Client/Pages/BookList.razor
@page "/booklist"
@using BlazorApp.Shared.Models
@inject HttpClient Http

@if (books == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <Button Type="primary" OnClick="OpenCreateModal" Block>New Book</Button>
    <Divider></Divider>

    <Table TItem="Book" DataSource="@books">
        <Column @bind-Field="@context.BookId" Title="ID">
            <Button Type="link" OnClick="() => OpenDetailModal(context.BookId)">@context.BookId</Button>
        </Column>
        <Column @bind-Field="@context.Title" />
        <Column @bind-Field="@context.Author" />
        <ActionColumn>
            <Popconfirm Title="本当に削除しますか？" OkText="はい" CancelText="いいえ" OnConfirm="() => Delete(context.BookId)">
                <Button Danger>Delete</Button>
            </Popconfirm>
        </ActionColumn>
    </Table>

    <Modal Title="Detail" Visible="@showDetailModal" OnCancel="@CloseModal" Footer="@null">
        <BookDetail Book="@book"></BookDetail>
    </Modal>

    <Modal Title="Create" Visible="@showCreateModal" OnCancel="@CloseModal" Footer="@null">
        <BookCreate Book="@book" Callback="@OnCallback"></BookCreate>
    </Modal>
}

@code {
    private List<Book>? books;
    private Book? book;
    private bool showDetailModal = false;
    private bool showCreateModal = false;

    protected override async Task OnInitializedAsync()
    {
        books = await Http.GetFromJsonAsync<List<Book>>("api/books");
    }

    public async Task OpenDetailModal(int id)
    {
        book = await Http.GetFromJsonAsync<Book>($"api/books/{id}");
        showDetailModal = true;
    }

    public void CloseModal()
    {
        showDetailModal = false;
        showCreateModal = false;
    }

    private void OpenCreateModal()
    {
        book = new(0, "", "");
        showCreateModal = true;
    }

    private async Task OnCallback()
    {
        books = await Http.GetFromJsonAsync<List<Book>>("api/books");
        showCreateModal = false;
    }

    public async Task Delete(int bookId)
    {
        await Http.DeleteAsync($"api/books/{bookId}");
        books = await Http.GetFromJsonAsync<List<Book>>("api/books");
    }
}

```

こんな感じになります。

![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/383318/f24b1a09-dd36-36ac-3f2d-5fc8fe96625b.gif)

# Update

## バックエンド

`BooksController` に `Update` メソッドを追加します。

```csharp:BlazoreApp.Server/Controllers/BooksController.cs
using BlazorApp.Server.Data;
using BlazorApp.Shared.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace BlazorApp.Server.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class BooksController : ControllerBase
    {
        private readonly AppDbContext context;

        public BooksController(AppDbContext context)
        {
            this.context = context;
        }

        [HttpGet]
        public async Task<ActionResult<List<Book>>> ListAsync()
        {
            var books = await context.Books.ToListAsync();
            return Ok(books);
        }

        [HttpGet("{id}")]
        public async Task<ActionResult<Book>> GetAsync(int id)
        {
            var book = await context.Books.SingleOrDefaultAsync(b => b.BookId.Equals(id));

            if (book == null)
            {
                return NotFound();
            }

            return Ok(book);
        }

        [HttpPost]
        public async Task<ActionResult<Book>> CreateAsync(Book book)
        {
            if (book.BookId != 0)
            {
                return BadRequest();
            }

            context.Books.Add(book);
            await context.SaveChangesAsync();

            return CreatedAtAction("Get", new { id = book.BookId }, book);
        }

        [HttpDelete("{id}")]
        public async Task<ActionResult> DeleteAsync(int id)
        {
            var book = await context.Books.SingleOrDefaultAsync(b => b.BookId.Equals(id));

            if (book == null)
            {
                return NotFound();
            }

            context.Books.Remove(book);
            await context.SaveChangesAsync();

            return NoContent();
        }

        [HttpPut]
        public async Task<ActionResult> Update(Book book)
        {
            if (await context.Books.AsNoTracking().SingleOrDefaultAsync(b => b.BookId == book.BookId) == null)
            {
                return NotFound();
            }

            context.Entry(book).State = EntityState.Modified;
            await context.SaveChangesAsync();
            return NoContent();
        }
    }
}

```

## フロントエンド

`BookEdit` コンポーネントを追加します。

```
@using BlazorApp.Shared.Models
@using System.Text.Json
@inject HttpClient Http
@inject NotificationService Notice

@if (Book == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <Form Model="@Book" OnFinish="Update" LabelColSpan="4" WrapperColSpan="20">
        <FormItem Label="ID">
            @context.BookId
        </FormItem>
        <FormItem Label="Title">
            <Input @bind-Value="@context.Title" />
        </FormItem>
        <FormItem Label="Author">
            <Input @bind-Value="@context.Author" />
        </FormItem>
        <FormItem WrapperColSpan="24" Style="text-align: center">
            <Button Type="primary" HtmlType="submit">Update</Button>
        </FormItem>
    </Form>
}

@code {
    [Parameter]
    public Book? Book { get; set; }

    [Parameter]
    public EventCallback Callback { get; set; }

    private async Task Update()
    {
        await Http.PutAsJsonAsync("api/books", Book);

        await Callback.InvokeAsync();
        await Notice.Open(new NotificationConfig()
        {
            Message = "更新しました！"
        });
    }
}
```

ボタンをクリックすると `BookEdit` コンポーネントをモーダルに表示するように `BookList` コンポーネントを修正します。

```
@page "/booklist"
@using BlazorApp.Shared.Models
@inject HttpClient Http

@if (books == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <Button Type="primary" OnClick="OpenCreateModal" Block>New Book</Button>
    <Divider></Divider>

    <Table TItem="Book" DataSource="@books">
        <Column @bind-Field="@context.BookId" Title="ID">
            <Button Type="link" OnClick="() => OpenDetailModal(context.BookId)">@context.BookId</Button>
        </Column>
        <Column @bind-Field="@context.Title" />
        <Column @bind-Field="@context.Author" />
        <ActionColumn>
            <Button OnClick="() => OpenEditModal(context.BookId)">Edit</Button>
        </ActionColumn>
        <ActionColumn>
            <Popconfirm Title="本当に削除しますか？" OkText="はい" CancelText="いいえ" OnConfirm="() => Delete(context.BookId)">
                <Button Danger>Delete</Button>
            </Popconfirm>
        </ActionColumn>
    </Table>

    <Modal Title="Detail" Visible="@showDetailModal" OnCancel="@CloseModal" Footer="@null">
        <BookDetail Book="@book"></BookDetail>
    </Modal>

    <Modal Title="Create" Visible="@showCreateModal" OnCancel="@CloseModal" Footer="@null">
        <BookCreate Book="@book" Callback="@OnCallback"></BookCreate>
    </Modal>

    <Modal Title="Edit" Visible="@showEditModal" OnCancel="@CloseModal" Footer="@null">
        <BookEdit Book="@book" Callback="@OnCallback"></BookEdit>
    </Modal>
}

@code {
    private List<Book>? books;
    private Book? book;
    private bool showDetailModal = false;
    private bool showCreateModal = false;
    private bool showEditModal = false;

    protected override async Task OnInitializedAsync()
    {
        books = await Http.GetFromJsonAsync<List<Book>>("api/books");
    }

    public async Task OpenDetailModal(int id)
    {
        book = await Http.GetFromJsonAsync<Book>($"api/books/{id}");
        showDetailModal = true;
    }

    public void CloseModal()
    {
        showDetailModal = false;
        showCreateModal = false;
        showEditModal = false;
        book = null;
    }

    private void OpenCreateModal()
    {
        book = new(0, "", "");
        showCreateModal = true;
    }

    private async Task OnCallback()
    {
        books = await Http.GetFromJsonAsync<List<Book>>("api/books");
        this.CloseModal();
    }

    public async Task Delete(int bookId)
    {
        await Http.DeleteAsync($"api/books/{bookId}");
        books = await Http.GetFromJsonAsync<List<Book>>("api/books");
    }

    private async Task OpenEditModal(int id)
    {
        book = await Http.GetFromJsonAsync<Book>($"api/books/{id}");
        showEditModal = true;
    }
}

```

こんな感じになります。

![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/383318/5725fc61-59f0-102b-1d30-86bf3455b3a1.gif)

# ソースコード

https://github.com/tamtamyarn/BlazorApp6.0