SlideShare a Scribd company logo
C#, .NET 6, Blazor WebAssembly,
ASP.NET Web API, Azure による
アプリ開発 – その4
鈴⽊ 章太郎
Elastic テクニカルプロダクトマーケティングマネージャー/エバンジェリスト
デジタル庁 省庁業務グループ ソリューションアーキテクト
Elastic
Technical Product Marketing
Manager/Evangelist
デジタル庁
省庁業務グループ
ソリューションアーキテクト
元 Microsoft Technical Evangelist
Twitter : @shosuz
Shotaro Suzuki
l 前回までの復習
l Blazor 概要
l 今回作成する Web アプリケーションの概要
l Blazor WebAssembly プロジェクト作成
l Web API コントローラー追加、モデル追加
l Entity Framework による Code First データベース作成
l 商品サービス、商品リスト、カテゴリーサービス等必要なサービス、
CRUD 処理等の実装
l 検索サービスの追加と検索コンポーネントの実装
l UI/UX の変更、カートサービス
l 認証・ユーザー登録機能、その他の実装 (p.151-p.219)
アジェンダ
今回の範囲
l 2⽉、3⽉、4⽉の復習
l 認証・ユーザー登録機能の実装、その他 (p.151-p.219)
セッションでご紹介した EC アプリ .NET 5版ですが、参考にさせて戴きました。
https://github.com/patrickgod/PreviewYT
Blazor 概要
Modern Web UI with .NET & Blazor
Server WebAssembly Hybrid
HTML、CSS、.NET、C#... JavaScript の代わりに Open Web 標準でアプリ開発
どこにでもホストできる
MVC
Razor
Pages
Blazor
HTTP
APIs
SignalR
Part of the ASP.NET Core family
Web UI Services
Worker gRPC
SPA
Blazor – .NET 5 まで
Blazor Server Blazor WebAssembly
DOM
Blazor
WebAssembly
.NET
Razor Components
Blazor
.NET
Razor Components
DOM
SignalR
ü DB アクセス含むサーバー機能へのフルアクセス
ü ⾼速なスタートアップ
ü コードがサーバーから離れない
ü 古いブラウザとシンクライアントをサポート
ü 永続的な接続が必要
ü UI の遅延が⾼い
ü完全にクライアント側で実⾏
ü必要なサーバー コンポーネントなし
ü静的サイトとしてホスト
üオフラインで実⾏可能
ü⼤きなダウンロードサイズ
üランタイムパフォーマンスの低下
Blazor Server (.NET 5) Blazor WebAssembly (.NET 5)
Blazor – .NET 6 による強化
Blazor Server Blazor WebAssembly
DOM
Blazor
WebAssembly
.NET
Razor Components
Blazor
.NET
Razor Components
DOM
SignalR
Blazor WebAssembly の事前 (AOT) コンパイル対応
Blazor WebAssembly アプリのダウンロードサイズの縮⼩
Error Boundaries
Razor コンポーネント型の推論とジェネリック型の制約
動的コンポーネント
プリレンダリング中の Blazor コンポーネント状態の永続性
Hot Reload, Native File Reference, 他多数
.NET 6
Blazor Server と Blazor WebAssembly の
開発モデルの違い
Blazor Server Blazor WebAssembly
DOM
Blazor
WebAssembly
.NET
Razor Components
Blazor
.NET
Razor Components
DOM
SignalR
Blazor Server
• 開発モデルは C/S 型に近い
• DOM(ブラウザ UI)と Blazor ランタイム(仮想 DOM)
がやりとりし UI 描画(差分更新)
• 画⾯の⼊出⼒部分のみをリモートデスクトップのようにブラウザ
側に持ってきているとみなせる
• SignalR(Web ソケット通信)
• DB に直接アクセス可能
• Web アプリケーションを Client - Server 型に近いモデルで
開発可能
• Web サーバとの常時接続が必要
• サーバ側でリソース効率の⾼いアプリの作り⽅が必要
• Hot Reload
Blazor WebAssembly
• サンドボックス制限
• DB アクセス不可 → Native File Reference による
ローカル DBアクセス
• Web API を介して DB アクセス
• 静的な Web サーバにホスト
• アプリ全体がダウンロード(⼤きくなりがち)
• DOM(ブラウザ UI)と Blazor ランタイム(仮想
DOM)がやりとりしUI 描画(差分更新)、ランタイム
が Blazor アプリ(UI ロジック)とやりとりする
• Hot Reload (デバッグなしで実⾏)
Web Assembly(WASM) とは
• Web ブラウザ上でバイナリコードを直接実⾏できる
• 2019 年 12 ⽉ W3C 勧告、正式なウェブ標準に認定
• 様々な⾔語のバイナリコードを主要なブラウザのサンドボックス内で動作可能
• Web Assembly バイナリコードへのコンパイラなどのツールセットが必要
Edge
Chrome
Safari
Firefox
Web Assembly
バイナリコード
(W3C 標準技術)
C++ WASM
コンパイラ
Rust WASM
コンパイラ
C WASM
コンパイラ
SQLite ソースコード(C)
Rust ソースコード
C++ ソースコード
.NET 6 における
Blazor WebAssembly 新機能
• 事前 (AOT) 実⾏コンパイル
• カスタム要素
• ⼩規模なアプリサイズ
• Native File Reference
• Hot Reload
• Component, .NET, HTML, CSS…
…その他数⼗個の更新あり
Blazor WebAssembly ⼩規模なアプリサイズ
.NET 5
• Publish size: 1.7 MB
.NET 6
• Publish size: 1.0 MB
• ~40% size reduction
Blazor WebAssembly のホスティング
ASP.NET
Blazor
WebAssem
bly
APIs
Globally
distributed
hosting
Blazor
WebAssem
bly
Serverless
functions
APIs
App Services Azure Static Web Apps
ASP.NET
Globally
distributed
hosting Microservices
Blazor
WebAssembly
APIs
Blazor
WebAssembly
APIs
Get started with Blazor
• Go to https://blazor.net
• Install the .NET SDK
• .NET Conf 2021 https://www.dotnetconf.net/
• .NET Conf 2021 – videos/slides/demos
https://github.com/dotnet-presentations/dotNETConf/tree/master/2021/MainEvent/Technical
Visual Studio Visual Studio for Mac Visual Studio Code
+ C# extension
今回作成する Web アプリケーションの概要
ASP.NET Core Blazor プロジェクトの構造
https://docs.microsoft.com/ja-jp/aspnet/core/blazor/project-structure?view=aspnetcore-6.0
Blazor WebAssembly アプリの初期ファイルとディレクトリ構造
[Client]
• Connected Service
• Dependencies
• Pages
• Properties
• Shared
• wwwrooot
• _imports.razor
• App.razor
• Program.cs
[Server]
• Connected Service
• Dependencies
• Controllers
• Pages
• Properties
• appsettings.json
• Program.cs
[Shared]
• Connected Service
• Dependencies
• WeatherForecast.cs
ASP.NET Core Blazor のホスティング モデル
https://docs.microsoft.com/ja-jp/aspnet/core/blazor/hosting-models?view=aspnetcore-6.0
• Blazor WebAssembly hosting model を使⽤すると、次のようになります。
• Blazor アプリ、その依存関係、.NET ランタイムが並⾏してブラウザーにダウンロードされます。
• アプリがブラウザー UI スレッド上で直接実⾏されます。
• 次の展開戦略がサポートされています。
• ASP.NET Core でのホストされた展開
• Blazor アプリは、ASP.NET Core アプリによって提供されます。
• "ホストされたデプロイ" により、 WebAssembly アプリが、Web サーバー上で実⾏されている ASP.NET Core アプリからブラウザーに提供されます。
• クライアント Blazor WebAssembly アプリは、サーバー アプリの他の静的な Web アセットと共に、サーバーアプリの /bin/Release/{TARGET
FRAMEWORK}/publish/wwwroot フォルダーに発⾏されます。
• 2 つのアプリが⼀緒に展開されます。 ASP.NET Core アプリをホストできる Web サーバーが必要です。 ホストされている展開の場合、Visual
Studio には WebAssembly アプリ プロジェクト テンプレートが含まれており (dotnet new コマンドを使⽤する場合は blazorwasm テンプレー
ト)、 Hosted オプションが選択されています (dotnet new コマンドを使⽤する場合は -ho|--hosted)。
• スタンドアロン展開
• Blazor アプリは、Blazor アプリの提供に .NET が使⽤されていない静的ホスティング Web サーバーまたはサービス上に配置されます。
• "スタンドアロン デプロイ" により、 WebAssembly アプリが、クライアントによって直接要求される静的ファイルのセットとして提供されます。 任意の静
的ファイル サーバーで Blazor アプリを提供できます。
• スタンドアロンのデプロイアセットは、/bin/Release/{TARGET FRAMEWORK}/publish/wwwroot フォルダーに発⾏されます。
• Azure App Service
• Blazor WebAssembly アプリは、Blazor 上でアプリをホストするために使⽤される Windows 上の Azure App Service にデプロイできます。
• スタンドアロンの Blazor WebAssembly アプリを Azure App Service for Linux にデプロイすることは、現在サポートされていません。 現時点で
は、アプリをホストする Linux サーバー イメージは使⽤できません。 このシナリオを可能にするための取り組みが進⾏中です。
• Azure Static Web Apps
• 詳細については、「Tutorial: Building a static web app with Blazor in Azure Static Web Apps」を参照してください。
• IIS
EC デモアプリの画⾯遷移例
トップ
検索
Movies Books Video Games
選択
カート
決済・ログイン
ユーザー登録
EC Demo アプリの構成 1
Azure
SQL Database
Elastic Cloud
東⽇本リージョン
マスターノード x 1
データノード x 2
ML ノード x 1
https://f79...c67.japaneast
.azure.elastic-
cloud.com:9243/
全⽂検索クエリ
検索・更新 UI
Azure サブスクリプション
Azure
App Service
Elastic APM
Endpoint に送信
Blazor
Server
APM .NET Agent
Blazor
WebAssembly
CRUD
Visual
Studio
2022 for
Mac Azure Data Studio
EC Demo アプリの構成 2
Azure
SQL Database
Elastic Cloud
東⽇本リージョン
マスターノード x 1
データノード x 2
ML ノード x 1
https://f79...c67.japaneast
.azure.elastic-
cloud.com:9243/
CRUD
Azure サブスクリプション
Visual
Studio
2022 for
Mac
Azure
App Service
Elastic APM
Endpoint に送信
Azure Data Studio
ASP.NET 6 Web API
Azure
Static Web Apps
Blazor
WebAssembly
検索・更新 UI
APM .NET Agent
Blazor
WebAssembly
全⽂検索クエリ
ASP.NET Core Blazor
のホスティング モデル
https://docs.microsoft.com/ja-jp/aspnet/core/blazor/hosting-models?view=aspnetcore-6.0#blazor-webassembly
ホスティング モデルの選択
Blazor サーバー Blazor WebAssembly
完全な .NET Core API の互換性 ✔ ❌
サーバー ソースへの直接アクセス ✔ ❌
⼩さいペイロード サイズと
⾼速な初期読み込み時間
✔ ❌
サーバー上でのアプリ コードの
セキュリティ保護と⾮公開
✔ ❌†
ダウンロードしたアプリを
オフラインで実⾏
❌ ✔
静的サイトのホスティング ❌ ✔
クライアントへの処理のオフロード ❌ ✔
Blazor WebAssembly プロジェクト作成
Blazor WebAssembly プロジェクト⽣成
チェックを⼊れる︕
Product Model の追加
Product Model の追加
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BlazorECommerceApp.Shared
{
public class Product
{
public int Id { get; set; }
public string Title { get; set; };
public string Description { get; set; };
public string ImageUrl { get; set; };
public decimal Price { get; set; }
}
}
---
@using BlazorECommerceApp.Shared
---
ProductList.Razor の追加
ProductList.Razor の追加 1
<h3>ProductList</h3>
---
@code {
public static List<Product> Products = new List<Product>
{
new Product {
Id = "1",
Title = "The Hitchhiker's Guide to the Galaxy",
ImageUrl = "https://upload.wikimedia.org/wikipedia/en/b/bd/H2G2_UK_front_cover.jpg",
Description = "銀河ヒッチハイク・ガイド[注 1](HG2G、[1] HHGTTG、[2] H2G2、[3] tHGttGと表記することもある)は、ダグラス・アダムスが⽣み出したコメディ
SFフランチャイズである。1978年にBBC Radio 4で放送されたラジオコメディが原作で、その後、舞台、⼩説、コミック、1981年のテレビシリーズ、1984年のテキストベー
スのコンピュータゲーム、2005年の⻑編映画など、様々な形式で翻案されている。",
Price. = 9.99m
}
new Product {
Id = "2",
Title = "Ready Player One",
ImageUrl = "https://upload.wikimedia.org/wikipedia/en/a/a4/Ready_Player_One_cover.jpg",
Description = “「レディ・プレイヤー・ワン」は2011年に発表されたSF⼩説で、アメリカ⼈作家アーネスト・クラインのデビュー作である。2045年のディストピアを舞台
に、主⼈公のウェイド・ワッツが世界規模のバーチャルリアリティゲームのイースターエッグを探し、その発⾒によってゲーム製作者の財産を相続することになるというス
トーリーである。クラインは2010年6⽉、⼊札競争の末に本作の出版権をクラウン・パブリッシング・グループ(ランダムハウスの⼀部⾨)に売却した[1]。 本作は2011年8
⽉16⽇に出版された[2]。同⽇にはオーディオブックも発売されており、ナレーションは、章のひとつで少し触れているウィル・ウィートンである[3][4]。 20 2012年には
アメリカ図書館協会のヤングアダルト図書館サービス部⾨からアレックス賞を受賞し[5] 、2011年にはプロメテウス賞を 受賞した[6]。”,
Price. = 7.99m
}
new Product {
Id = "3",
Title = "Nineteen Eighty-Four”,
ImageUrl = "https://upload.wikimedia.org/wikipedia/commons/c/c3/1984first.jpg",
Description = “ Nineteen Eighty-Four(1984)は、イギリスの作家ジョージ・オーウェルが書いたディストピア社会SF⼩説であり、教訓的な物語である。1949年6⽉8
⽇にセッカー&ウォーバーグ社から出版され、オーウェルが⽣前に完成させた9冊⽬にして最後の著作となった。⺠主社会主義者であるオーウェルは、スターリン主義のロシ
アとナチス・ドイツをモデルに、⼩説の中の全体主義政府を描いた[2][3][4]。 より広く、この⼩説では政治における真実と事実の役割と、それらが操られる⽅法を検証し
ている。" ,
Price = 6.99m }
}
ProductList.Razor の追加 2
<h3>ProductList</h3>
<ul class="list-unstyled">
@foreach (var product in ProductService.Products)
{
<li class="media my-3">
<div class="media-img-wrapper mr-2">
<a href="/product/@product.Id">
<img class="media-img" src="@product.ImageUrl" alt="@product.Title" />
</a>
</div>
<div class="media-body">
<a href="/product/@product.Id">
<h4 class="mb-0">@product.Title</h4>
</a>
<p>@product.Description</p>
<h5 class="price">
@GetPriceText(product)
</h5>
</div>
</li>
}
</ul>
---
Index.Razor の変更
@page "/"
<ProductList />
https://localhost:7226/#
Web API コントローラー追加、モデル追加
API コントローラーの追加
ProductController.cs の追加 1
[Route("api/[controller]")]
[ApiController]
public class ProductController : ControllerBase
{
private static List <Product> Products = new List <Product> {
new Product {
Id = "1",
Title = "The Hitchhiker's Guide to the Galaxy",
ImageUrl = "https://upload.wikimedia.org/wikipedia/en/b/bd/H2G2_UK_front_cover.jpg",
Description = "銀河ヒッチハイク・ガイド[注 1](HG2G、[1] HHGTTG、[2] H2G2、[3] tHGttGと表記することもある)は、ダグラス・アダムスが
⽣み出したコメディSFフランチャイズである。1978年にBBC Radio 4で放送されたラジオコメディが原作で、その後、舞台、⼩説、コミック、1981年の
テレビシリーズ、1984年のテキストベースのコンピュータゲーム、2005年の⻑編映画など、様々な形式で翻案されている。",
Price. = 9.99m
}
new Product {
Id = "2",
Title = "Ready Player One",
ImageUrl = "https://upload.wikimedia.org/wikipedia/en/a/a4/Ready_Player_One_cover.jpg",
Description = “「レディ・プレイヤー・ワン」は2011年に発表されたSF⼩説で、アメリカ⼈作家アーネスト・クラインのデビュー作である。2045年の
ディストピアを舞台に、主⼈公のウェイド・ワッツが世界規模のバーチャルリアリティゲームのイースターエッグを探し、その発⾒によってゲーム製作
者の財産を相続することになるというストーリーである。クラインは2010年6⽉、⼊札競争の末に本作の出版権をクラウン・パブリッシング・グループ
(ランダムハウスの⼀部⾨)に売却した[1]。 本作は2011年8⽉16⽇に出版された[2]。同⽇にはオーディオブックも発売されており、ナレーションは、
章のひとつで少し触れているウィル・ウィートンである[3][4]。2012年にはアメリカ図書館協会のヤングアダルト図書館サービス部⾨からアレックス賞
を受賞し[5] 、2011年にはプロメテウス賞を受賞した[6]。”,
Price. = 7.99m
}
new Product {
Id = "3",
Title = "Nineteen Eighty-Four”,
ImageUrl = "https://upload.wikimedia.org/wikipedia/commons/c/c3/1984first.jpg",
Description = “ Nineteen Eighty-Four(1984)は、イギリスの作家ジョージ・オーウェルが書いたディストピア社会SF⼩説であり、教訓的な物語
である。1949年6⽉8⽇にセッカー&ウォーバーグ社から出版され、オーウェルが⽣前に完成させた9冊⽬にして最後の著作となった。⺠主社会主義者で
あるオーウェルは、スターリン主義のロシアとナチス・ドイツをモデルに、⼩説の中の全体主義政府を描いた[2][3][4]。より広く、この⼩説では政治
における真実と事実の役割と、それらが操られる⽅法を検証している。" ,
Price = 6.99m }
}
---
ProductController.cs の追加 2
---
[HttpGet]
public async Task<ActionResult<<List<Product>>> GetProducts()
{
rerurn Ok(Product)
var result = await _productService. GetProductsAsync();
return Ok(result);
}
https://localhost:7226/#
ProductList.Razor の変更(クライアントからの呼び出し)
---
@inject HttpClient Http
<ul class="list-unstyled">
@foreach (var product in ProductService.Products)
{
<li class="media my-3">
<div class="media-img-wrapper mr-2">
<a href="/product/@product.Id">
<img class="media-img" src="@product.ImageUrl" alt="@product.Title" />
</a>
</div>
<div class="media-body">
<a href="/product/@product.Id">
<h4 class="mb-0">@product.Title</h4>
</a>
<p>@product.Description</p>
<h5 class="price">
@GetPriceText(product)
</h5>
</div>
</li>
}
</ul>
---
code@ {
private static List<Product> Products {get; set;} = new List<Product>();
protected override async TaskOnInitializedAsync()
{
Products = await Http.GetFromJsonAsync<List<Product>> ("api/product");
}
}
Entity Framework による Code First
データベース作成
Blazor アプリのデバッグその他の TIPS
dotnet watch run
public class xxx
prop → snippets が出て予測してくれる
swagger インストールその他
• https://localhost:(ポート番
号)/swagger/index.html
---
// AddRazorPages の後
builer.Services.AddEndpointApiExploler();
builer.Services.AddSwaggerGen();
//var ap = buildder.Build();の後
app.UserSwaggerUI();
// app.UseHttpsRedirection();の前
app.UseSwagger();
// Swagger UI で Products の shema が表⽰されない場合
// Public Async Task を書き換え
Task<Action> GetProduct()
→
Task<ActionResult<List<Product>>> GetProduct()
.NET Core Entity Framework 6.0 インストール
• Microsoft.EntityFrameworkCore
• Microsoft.EntityFrameworkCore.
Design
• Microsoft.EntityFrameworkCore.
SqlServer
• Mac の場合は、唯⼀の選択肢︕
Windows の場合は、SQL Server
Express Edition をインストールして使う
⼿もあり
appsettings.json で
”ConnectionString”
とうつと⾃動的に出てくる
この⽂字列をコピペして修正すればOK
※ 注意点
EF で Code First で Database を⾃動⽣成した場合、巨⼤なインスタンスになっ
ている(3⽇くらいで数千円レベル)。
instance のサイズだけはすぐに修正して⼩さいものBasic2TB等にする。
これなら⽉額数百円。
Azure SQL Database 接続⽂字列追加
{
"ConnectionStrings": {
"DefaultConnection":
"Server=tcp:xxx.database.windows.net,1433;Initial Catalog=BlazorECommerceApp;Persist
Security Info=False;User ID=(UserID);Password=(Password);
MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
dotnet ef migration add CreateInitial // Migrations フォルダーと Migration クラス作成
dotnet ef Update Database // Azure SQL データベースとテーブル作成
Product Model の追加
• BlazorECommerceApp.Shared フォルダに、
Product クラスを作成
• Book.cs に右のコードを記載
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BlazorECommerceApp.Shared
{
public class Product
{
public int Id { get; set; }
[Required]
public string Title { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string ImageUrl { get; set; } = string.Empty;
public Category? Category { get; set; }
public int CategoryId { get; set; }
public bool Featured { get; set; } = false;
public List<ProductVariant> Variants { get; set; } =
new List<ProductVariant>();
public bool Visible { get; set; } = true;
public bool Deleted { get; set; } = false;
[NotMapped]
public bool Editing { get; set; } = false;
[NotMapped]
public bool IsNew { get; set; } = false;
}
}
DataContext 作成
• Class を追加
• DataContext.class
• Serverプロジェクト側の Program.cs
修正
• global using
Microsoft.EntityFramework.Core を⼊
れておくと楽
namespace BlazorECommerceApp.Server.Data
{
public class DataContext : DbContext
{
// DataContext を作るのに ctor とタイプするとできる
// 全体的に IntelliCode が補完
public DataContext(DbContextOptions<DataContext>
options) : base(options)
{
}
}
}
• Server プロジェクト側の Program.cs
• DataContext.cs
global using Microsoft.EntityFrameworkCore;
Entity Framework を使った最初の DB Migration
//最初に名前を決めておく
dotnet ef migrations add CreateInitial
//成功したら Migration フォルダを開いて内容を確認
//データベース作成
dotnet ef database update
データのシード(2回⽬のマイグレーション)
---
Protected override void OnModelCreating(ModelBuilder
modelBuilder)
{
modelBuilder.Entity<Product>().HasData(
---<ここに new Product 3エントリをコピペ>---
);
}
dotnet ef migrations add ProductSeeding
dotnet ef database
update
(参考)旧 ProductController.cs
[Route("api/[controller]")]
[ApiController]
public class ProductController : ControllerBase
{
private static List <Product> Products = new List <Product> {
new Product {
Id = "1",
Title = "The Hitchhiker's Guide to the Galaxy",
ImageUrl = "https://upload.wikimedia.org/wikipedia/en/b/bd/H2G2_UK_front_cover.jpg",
Description = "銀河ヒッチハイク・ガイド[注 1](HG2G、[1] HHGTTG、[2] H2G2、[3] tHGttGと表記することもある)は、ダグラス・アダムスが
⽣み出したコメディSFフランチャイズである。1978年にBBC Radio 4で放送されたラジオコメディが原作で、その後、舞台、⼩説、コミック、1981年の
テレビシリーズ、1984年のテキストベースのコンピュータゲーム、2005年の⻑編映画など、様々な形式で翻案されている。",
Price. = 9.99m
}
new Product {
Id = "2",
Title = "Ready Player One",
ImageUrl = "https://upload.wikimedia.org/wikipedia/en/a/a4/Ready_Player_One_cover.jpg",
Description = “「レディ・プレイヤー・ワン」は2011年に発表されたSF⼩説で、アメリカ⼈作家アーネスト・クラインのデビュー作である。2045年の
ディストピアを舞台に、主⼈公のウェイド・ワッツが世界規模のバーチャルリアリティゲームのイースターエッグを探し、その発⾒によってゲーム製作
者の財産を相続することになるというストーリーである。クラインは2010年6⽉、⼊札競争の末に本作の出版権をクラウン・パブリッシング・グループ
(ランダムハウスの⼀部⾨)に売却した[1]。 本作は2011年8⽉16⽇に出版された[2]。同⽇にはオーディオブックも発売されており、ナレーションは、
章のひとつで少し触れているウィル・ウィートンである[3][4]。2012年にはアメリカ図書館協会のヤングアダルト図書館サービス部⾨からアレックス賞
を受賞し[5] 、2011年にはプロメテウス賞を受賞した[6]。”,
Price. = 7.99m
}
new Product {
Id = "3",
Title = "Nineteen Eighty-Four”,
ImageUrl = "https://upload.wikimedia.org/wikipedia/commons/c/c3/1984first.jpg",
Description = “ Nineteen Eighty-Four(1984)は、イギリスの作家ジョージ・オーウェルが書いたディストピア社会SF⼩説であり、教訓的な物語
である。1949年6⽉8⽇にセッカー&ウォーバーグ社から出版され、オーウェルが⽣前に完成させた9冊⽬にして最後の著作となった。⺠主社会主義者で
あるオーウェルは、スターリン主義のロシアとナチス・ドイツをモデルに、⼩説の中の全体主義政府を描いた[2][3][4]。より広く、この⼩説では政治
における真実と事実の役割と、それらが操られる⽅法を検証している。" ,
Price = 6.99m }
}
---
ProductController.cs 内のデータを削除
• Server Program.cs を開き global
using Blazorxxx.Server.Data; を追加
• private readonly DataContext
context;
⽣成されるので、これを修正
• しかしこれを⾃動的に実施したい
//context → _context に変更
public ProductController(DataContext context)
{
_cotext = context;
}
• Server プロジェクト側の Program.cs
global using Blazorxxx.Server.Data;
ツール → オプションから
テキストエディタ → C# → CodeStyle → Naming → Manage Naming Style
Naming Style Title : _fieldName
Capitalizatin : camel Case Name
これを追加したらprivate or internal Style に追加
_fieldName、Suggestion を選択
エディタに戻って create field context を選択する
[HttpGet] GetProduct() 変更
• ProductList
• ProductController
• DataContext
• [HttpGet] GetProduct() 変更
var products = await
_cotext.Products.ToListAsync();
return Ok(products)
商品サービス、商品リスト、カテゴリーサービス
等必要なサービス、CRUD 処理等の実装
Blazor WebAssembly の追加・改修等
• ProductDetail.razor.css 追加
• ProductDetail.razor 編集
@page "/product/{id:int}"
@inject IProductService ProductService
@inject ICartService CartService
Product 単品をクライアントで取得する
Category を実装する
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BlazorECommerceApp.Shared
{
public class Category
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Url { get; set; } = string.Empty;
public bool Visible { get; set; } = true;
public bool Deleted { get; set; } = false;
[NotMapped]
public bool Editing { get; set; } = false;
[NotMapped]
public bool IsNew { get; set; } = false;
}
}
Category の Seeding と Migration(3回⽬)
•
•
---
modelBuilder.Entity<Category>().HasData(
new Category
{
Id = 1,
Name = "Books",
Url = "books"
},
new Category
{
Id = 2,
Name = "Movies",
Url = "movies"
},
new Category
{
Id = 3,
Name = "Video Games",
Url = "video-games"
}
);
---
• DataContext.cs
Category サービスの Client 側 への実装 - 1
•
•
namespace Blazorxxxxxxxx.Client.Services.CategoryService
{
public class CategoryService : ICategoryService
{
private readonly HttpClient _http;
public CategoryService(HttpClient http)
{
_http = http;
}
---
• CategoryServices.cs
Category サービスの Client 側 への実装 - 2
•
CategoryService
• global using で⼀番上に追加
//builder.Services.AddScoped<IProductService,ProductSe
rvice>();の下に追加
builder.Services.AddScoped<ICategoryService,
CategoryService>();
//⼀番上に追加
global using
Blazorxxxxxxxx.Client.Services.CategoryService;
• Program.cs
Category サービスの Client 側 への実装 - 3
•
@using Blazorxxxxxxxx.Client.Services.ProductService
• _Imports.razor
iCategoryServices の実装
•
namespace
Blazorxxxxxxx.Client.Services.CategoryService
{
public interface ICategoryService
{
event Action OnChange;
List<Category> Categories { get; set; }
List<Category> AdminCategories { get; set; }
Task GetCategories();
Task GetAdminCategories();
Task AddCategory(Category category);
Task UpdateCategory(Category category);
Task DeleteCategory(int categoryId);
Category CreateNewCategory();
}
}
• iCategoryServices.cs
NavMenu への Category の表⽰
• NavMenu.razor の編集
• @inject ICategoryService
CategoryService を冒頭に追加
• @code の後半部分に追加
• NavMenuCssClass の追加
protected override async Task OnInitializedAsync()
{
await CategoryService.GetCategories();
}
• NavMenu.razor
• NavMenuCssClass
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
Home
</NavLink>
</div>
@foreach (var category in CategoryService.Categories)
{
<div class="nav-item px-3">
<NavLink class="nav-link" href="@category.Url">
@category.Name
</NavLink>
</div>
}
</nav>
</div>
Server の Category サービスから Product を取得 - 1
•
•
Task<ServiceResponse<Product>> GetProduct(int productId);
• iProductService.cs
public async Task GetProducts(string? categoryUrl = null)
{
var result = categoryUrl == null ?
await
_http.GetFromJsonAsync<ServiceResponse
<List<Product>>>("api/product/featured") :
await _http.GetFromJsonAsync<ServiceResponse
<List<Product>>>($"api/product/category/{categoryUrl}");
if (result != null && result.Data != null)
Products = result.Data;
CurrentPage = 1;
PageCount = 0;
if (Products.Count == 0)
Message = "商品がみつかりません。";
ProductsChanged.Invoke();
}
• ProductService.cs
Server の Category サービスから Product を取得 - 2
•
•
• https://locahost:(ポート番
号)/swagger/index.html
---
[HttpGet("category/{categoryUrl}")]
public async Task<ActionResult<ServiceResponse
<List<Product>>>>
GetProductsByCategory(string categoryUrl)
{
var result =
await _productService.
GetProductsByCategory(categoryUrl);
return Ok(result);
}
---
• ProductController.cs
Client の Category サービスから Product を取得 - 1
•
• Task GetProducts を実装追加
• event ProductChanged を追加
•
public async Task GetProducts(string? categoryUrl = null)
{
var result = categoryUrl == null ?
//この2⾏がポイント
await _http.GetFromJsonAsync<ServiceResponse
<List<Product>>>("api/product/featured") :
await _http.GetFromJsonAsync<ServiceResponse
<List<Product>>>($"api/product/category/{categoryUrl}");
if (result != null && result.Data != null)
Products = result.Data;
CurrentPage = 1;
PageCount = 0;
if (Products.Count == 0)
Message = "商品がみつかりません。";
//ここもポイント
ProductsChanged.Invoke();
}
• iProductService.cs
• iProductService.cs
Task GetProducts(string? categoryUrl = null);
event Action ProductsChanged;
• iProductService.cs
Client の Category サービスから Product を取得 - 2
•
@page "/"
@page "/search/{searchText}/{page:int}"
@page "/{categoryUrl}"
@inject IProductService ProductService
<PageTitle>マイショップ</PageTitle>
@if (SearchText == null && CategoryUrl == null)
{
<FeaturedProducts />
}
else
{
<ProductList />
}
@code {
[Parameter]
public string? CategoryUrl { get; set; } = null;
[Parameter]
public string? SearchText { get; set; } = null;
[Parameter]
public int Page { get; set; } = 1;
protected override async Task OnParametersSetAsync()
{
if (SearchText != null)
{
await ProductService.SearchProducts(SearchText, Page);
}
else
{
await ProductService.GetProducts(CategoryUrl);
}
}
}
• Index.razor
Client の Category サービスから Product を取得 - 3
•
•
•
•
---
@code {
protected override void OnInitialized()
{
ProductService.ProductsChanged += StateHasChanged;
}
• ProductList.razor
---
public void Dispose()
{
ProductService.ProductsChanged -= StateHasChanged;
}
Shared に ProductVariant.cs を追加
•
•
•
• ProductVariant.cs
---
namespace BlazorECommerceApp.Shared
{
public class ProductVariant
{
[JsonIgnore]
public Product? Product { get; set; }
public int ProductId { get; set; }
public ProductType? ProductType { get; set; }
public int ProductTypeId { get; set; }
[Column(TypeName = "decimal(18,2)")]
public decimal Price { get; set; }
[Column(TypeName = "decimal(18,2)")]
public decimal OriginalPrice { get; set; }
public bool Visible { get; set; } = true;
public bool Deleted { get; set; } = false;
[NotMapped]
public bool Editing { get; set; } = false;
[NotMapped]
public bool IsNew { get; set; } = false;
}
}
Composite Primary Key の追加と Seeding の実施(4回⽬)
•
•
•
•
• ProductTypes
• ProductVariants
---
modelBuilder.Entity<ProductVariant>().HasData(
new ProductVariant
{
ProductId = 1,
ProductTypeId = 2,
Price = 9.99m,
OriginalPrice = 19.99m
},
new ProductVariant
{
ProductId = 1,
ProductTypeId = 3,
Price = 7.99m
},
new ProductVariant
{
ProductId = 1,
ProductTypeId = 4,
Price = 19.99m,
OriginalPrice = 29.99m
},
new ProductVariant
{
ProductId = 2,
ProductTypeId = 2,
Price = 7.99m,
OriginalPrice = 14.99m
},
new ProductVariant
{
ProductId = 3,
ProductTypeId = 2,
Price = 6.99m
},
new ProductVariant
{
ProductId = 4,
ProductTypeId = 5,
Price = 3.99m
},
new ProductVariant
{
ProductId = 4,
ProductTypeId = 6,
Price = 9.99m
},
new ProductVariant
{
ProductId = 4,
ProductTypeId = 7,
Price = 19.99m
},
new ProductVariant
{
ProductId = 5,
ProductTypeId = 5,
Price = 3.99m,
},
new ProductVariant
{
ProductId = 6,
ProductTypeId = 5,
Price = 2.99m
},
new ProductVariant
{
ProductId = 7,
ProductTypeId = 8,
Price = 19.99m,
OriginalPrice = 29.99m
},
new ProductVariant
{
ProductId = 7,
ProductTypeId = 9,
Price = 69.99m
},
new ProductVariant
{
ProductId = 7,
ProductTypeId = 10,
Price = 49.99m,
OriginalPrice = 59.99m
},
new ProductVariant
{
ProductId = 8,
ProductTypeId = 8,
Price = 9.99m,
OriginalPrice = 24.99m,
},
new ProductVariant
{
ProductId = 9,
ProductTypeId = 8,
Price = 14.99m
},
new ProductVariant
{
ProductId = 10,
ProductTypeId = 1,
Price = 159.99m,
OriginalPrice = 299m
},
new ProductVariant
{
ProductId = 11,
ProductTypeId = 1,
Price = 79.99m,
OriginalPrice = 399m
}
);
}
---
Product Variants と Types を Product Service に含める - 1
•
•
• タブは Network
• フィルターは Fetch/XHR で実⾏
---
public async Task<ServiceResponse<Product>> GetProductAsync(int productId)
{
var response = new ServiceResponse<Product>();
Product product = null;
if (_httpContextAccessor.HttpContext.User.IsInRole("Admin"))
{
product = await _context.Products
.Include(p => p.Variants.Where(v => !v.Deleted))
.ThenInclude(v => v.ProductType)
.FirstOrDefaultAsync(p => p.Id == productId && !p.Deleted);
}
else
{
---
public async Task<ServiceResponse<List<Product>>> GetProductsAsync()
{
var response = new ServiceResponse<List<Product>>
{
Data = await _context.Products
.Where(p => p.Visible && !p.Deleted)
.Include(p => p.Variants.Where(v => v.Visible && !v.Deleted))
.ToListAsync()
---
public async Task<ServiceResponse
<List<Product>>> GetProductsByCategory(string categoryUrl)
{
var response = new ServiceResponse<List<Product>>
{
Data = await _context.Products
.Where(p => p.Category.Url.ToLower().Equals(categoryUrl.ToLower()) &&
p.Visible && !p.Deleted)
.Include(p => p.Variants.Where(v => v.Visible && !v.Deleted))
.ToListAsync()
---
Product Variants と Types を Product Service に含める - 2
•
• Product は取れている
• movies のところの下で Productを クリック
し、variants の中に Product が列挙される
ように出⼒されていることが確認できる
• id 指定してないと ProductType が⼊ってい
ないが、1と指定してリロードすると id に対応
した ProductType がちゃんと⼊っているのが
⾒える
検索サービスの追加と検索コンポーネントの実装
Product Search 機能の追加と実装 - 1
Server Service ProductService IProductService.cs
---
//追加
Task SearchProducts(string searchText, int page);
---
Product Search 機能の追加と実装 – 2
Server → Services → ProductService → ProductService.cs
---
public async Task SearchProducts(string searchText, int page)
{
LastSearchText = searchText;
var result = await _http
.GetFromJsonAsync<ServiceResponse<ProductSearchResult>>($"api/product/search/{searchText}/{page
}");
if (result != null && result.Data != null)
{
Products = result.Data.Products;
CurrentPage = result.Data.CurrentPage;
PageCount = result.Data.Pages;
}
if (Products.Count == 0) Message = "商品がみつかりません。";
ProductsChanged?.Invoke();
}
---
Product Search 機能の追加と実装 – 3
Server → Services → ProductService → ProductService.cs
---
//ついで
public async Task<List<string>> GetProductSearchSuggestions(string searchText)
{
var result = await _http
.GetFromJsonAsync<ServiceResponse<List<string>>>
($"api/product/searchsuggestions/{searchText}");
return result.Data;
}
//上記の通り実装
Product Search 機能の追加と実装 – 4
Server → Services → ProductService → ProductService.cs
---
public async Task<ServiceResponse<ProductSearchResult>> SearchProducts(string searchText, int page)
{
var pageResults = 2f;
var pageCount = Math.Ceiling((await FindProductsBySearchText(searchText)).Count / pageResults);
var products = await _context.Products
.Where(p => p.Title.ToLower().Contains(searchText.ToLower()) ||
p.Description.ToLower().Contains(searchText.ToLower()) &&
p.Visible && !p.Deleted)
.Include(p => p.Variants)
.Skip((page - 1) * (int)pageResults)
.Take((int)pageResults)
.ToListAsync();
var response = new ServiceResponse<ProductSearchResult>
{
Data = new ProductSearchResult
{
Products = products,
CurrentPage = page,
Pages = (int)pageCount
}
};
return response;
}
//上記の通り実装
---
Product Search 機能の追加と実装 – 5
• デバッグ実⾏
• https://localhost:(port 番号)/swagger/index.html
• ⼩説、等で実⾏。Response Body に1件ずつ全項⽬が表⽰される
Search Suggestions の実装 - 1
Server → Services → ProductService → ProductService.cs
---
public async Task<ServiceResponse<List<string>>> GetProductSearchSuggestions(string searchText)
{
var products = await FindProductsBySearchText(searchText);
List<string> result = new List<string>();
foreach (var product in products)
{
if (product.Title.Contains(searchText, StringComparison.OrdinalIgnoreCase))
{
result.Add(product.Title);
}
if (product.Description != null)
{
var punctuation = product.Description.Where(char.IsPunctuation)
.Distinct().ToArray();
var words = product.Description.Split()
.Select(s => s.Trim(punctuation));
foreach (var word in words)
{
if (word.Contains(searchText, StringComparison.OrdinalIgnoreCase)
&& !result.Contains(word))
{
result.Add(word);
}
}
}
}
return new ServiceResponse<List<string>> { Data = result };
}
---
Search Suggestions の実装 - 2
Server → Controllers → ProductController.cs
---
[HttpGet("searchsuggestions/{searchText}")]
public async Task<ActionResult<ServiceResponse
<List<Product>>>> GetProductSearchSuggestions(string searchText)
{
var result = await _productService.GetProductSearchSuggestions(searchText);
return Ok(result);
}
---
Search Suggestions の実装 – 3
Server → Services → ProductService → ProductService.cs
---
if (product.Title.Contains(searchText, StringComparison.OrdinalIgnoreCase))
{
result.Add(product.Title);
}
if (product.Description != null)
{
var punctuation = product.Description.Where(char.IsPunctuation)
.Distinct().ToArray();
var words = product.Description.Split()
.Select(s => s.Trim(punctuation));
foreach (var word in words)
{
if (word.Contains(searchText, StringComparison.OrdinalIgnoreCase)
&& !result.Contains(word))
{
result.Add(word);
}
}
}
---
• まず、句読点を取得し、句読点の助けを借りて、説明⽂のすべての単語を取得
• その後、単純に任意の単語が検索テキストを含むかどうかをチェックし、もしそうなら、結果に追加する
Search Suggestions の実装 - 4
• デバッグ実⾏
• https://localhost:(port 番号)/swagger/index.html
• ⼩説、等で実⾏。Response Body に出てくるものは Search ボックス内でサジェストされる(ここでは5件)
Search Suggestions の実装 – Client 側 1
Client → Services → ProductService → IProductService.cs
---
namespace BlazorECommerceApp.Client.Services.ProductService
{
public interface IProductService
{
---
Task SearchProducts(string searchText, int page);
Task<List<string>> GetProductSearchSuggestions(string searchText);
---
}
}
---
//を追加
• 「商品が⾒つかりませんでした」というようなメッセージを表⽰する
• その後ユーザーにいくつかの情報を与えるために、サービスが開始される
• リストの⽂字列を送信すると、商品検索候補を取得
Search Suggestions の実装 – Client 側 2
Client → Services → ProductService → ProductService.cs
---
namespace BlazorECommerceApp.Client.Services.ProductService
{
public interface IProductService
{
---
Task SearchProducts(string searchText, int page);
Task<List<string>> GetProductSearchSuggestions(string searchText);
---
}
}
---
//を追加
• 「商品が⾒つかりませんでした」というようなメッセージを表⽰する
• その後ユーザーにいくつかの情報を与えるために、サービスが開始される
• リストの⽂字列を送信すると、商品検索候補を取得
Search Suggestions の実装 – Client 側 3-a
Client → Services → ProductService → ProductService.cs
---
namespace BlazorECommerceApp.Client.Services.ProductService
{
public class ProductService : IProductService
//IProductService.cs インターフェイスをインプリする
{
private readonly HttpClient _http;
public ProductService(HttpClient http)
{
_http = http;
}
---
Search Suggestions の実装 – Client 側 3-b
Client → Services → ProductService → ProductService.cs
---
public List<Product> Products { get; set; } = new List<Product>();
public string Message { get; set; } = "商品をロードしています...";
//メッセージを追加
public int CurrentPage { get; set; } = 1;
public int PageCount { get; set; } = 0;
public string LastSearchText { get; set; } = string.Empty;
public List<Product> AdminProducts { get; set; }
public event Action ProductsChanged;
public async Task<Product> CreateProduct(Product product)
{
var result = await _http.PostAsJsonAsync("api/product", product);
var newProduct = (await result.Content
.ReadFromJsonAsync<ServiceResponse<Product>>()).Data;
return newProduct;
}
---
Search Suggestions の実装 – Client 側 3-c
Client → Services → ProductService → ProductService.cs
---
public async Task DeleteProduct(Product product)
{
var result = await _http.DeleteAsync($"api/product/{product.Id}");
}
public async Task GetAdminProducts()
{
var result = await _http
.GetFromJsonAsync<ServiceResponse<List<Product>>>("api/product/admin");
AdminProducts = result.Data;
CurrentPage = 1;
PageCount = 0;
if (AdminProducts.Count == 0)
Message = "商品がみつかりません。";
}---
Search Suggestions の実装 – Client 側 3-e
Client → Services → ProductService → ProductService.cs
---
public async Task<ServiceResponse<Product>> GetProduct(int productId)
{
var result = await
_http.GetFromJsonAsync<ServiceResponse<Product>>($"api/product/{productId}");
return result;
}
public async Task GetProducts(string? categoryUrl = null)
{
var result = categoryUrl == null ?
await
_http.GetFromJsonAsync<ServiceResponse<List<Product>>>
("api/product/featured") :
await
_http.GetFromJsonAsync<ServiceResponse<List<Product>>>
($"api/product/category/{categoryUrl}");
if (result != null && result.Data != null)
Products = result.Data;
CurrentPage = 1;
PageCount = 0;
if (Products.Count == 0)
Message = "商品がみつかりません。";
ProductsChanged.Invoke();
}
---
Search Suggestions の実装 – Client 側 3-e
Client → Services → ProductService → ProductService.cs
---
public async Task<List<string>> GetProductSearchSuggestions(string searchText)
{
var result = await _http
.GetFromJsonAsync<ServiceResponse<List<string>>>($"api/product/searchsuggestions/{searchText}");
return result.Data;
}
public async Task SearchProducts(string searchText, int page)
{
LastSearchText = searchText;
var result = await _http
.GetFromJsonAsync<ServiceResponse<ProductSearchResult>>($"api/product/search/{searchText}/{page}");
if (result != null && result.Data != null)
{
Products = result.Data.Products;
CurrentPage = result.Data.CurrentPage;
PageCount = result.Data.Pages;
}
if (Products.Count == 0) Message = "商品がみつかりません。";
ProductsChanged?.Invoke();
}
public async Task<Product> UpdateProduct(Product product)
{
var result = await _http.PutAsJsonAsync($"api/product", product);
var content = await result.Content.ReadFromJsonAsync<ServiceResponse<Product>>();
return content.Data;
}
}
}
---
URL を介した検索の実装
Client → Pages → index.razor
---
@page "/"
@page "/search/{searchText}/{page:int}"
@page "/{categoryUrl}"
---
@code {
[Parameter]
public string? CategoryUrl { get; set; } = null;
[Parameter]
public string? SearchText { get; set; } = null;
[Parameter]
public int Page { get; set; } = 1;
protected override async Task OnParametersSetAsync()
{
if (SearchText != null)
{
await ProductService.SearchProducts(SearchText, Page);
}
else
{
await ProductService.GetProducts(CategoryUrl);
}
}
}
---
//を追加
• 単純にフォワードスラッシュ
• 開始ページまたは検索
• 検索テキストまたはカテゴリ URL
• 必要なのはこの新しいパラメータ、検索テキスト
• コードブロック全体の実装
Seach コンポーネント作成 - 1
Client → Shared → Search.razor
---
//先に@code部分作成
@Inject NavigationManager NavigationManager
@inject IProductService ProductService
---
@code {
private string searchText = string.Empty;
private List<string> suggestions = new List<string>();
protected ElementReference searchInput;
protected override async Task OnAfterRenderAsync
(bool firstRender)
{
if (firstRender)
{
await searchInput.FocusAsync();
}
}
public void SearchProducts()
{
NavigationManager.NavigateTo($"search/{searchText}/1");
}
---
• すでにあるものを注⼊する
• Product Service
• いくつかの呼び出し
• Navigation Manager
• ユーザーを特定のページに誘導したい
• NavigateTo メソッドを使⽤
Seach コンポーネント作成 - 2
Client → Shared → Search.razor
---
//先に@code部分作成
@Inject NavigationManager NavigationManager
@inject IProductService ProductService
---
public async Task HandleSearch(KeyboardEventArgs args)
{
if (args.Key == null || args.Key.Equals("Enter"))
{
SearchProducts();
}
else if (searchText.Length > 1)
{
suggestions = await
ProductService.
GetProductSearchSuggestions(searchText);
}
}
• すでにあるものを注⼊する
• Product Service
• いくつかの呼び出し
• Navigation Manager
• ユーザーを特定のページに誘導したい
• NavigateTo メソッドを使⽤
Seach コンポーネント作成 - 3
Client → Shared → Search.razor
---
//最後に HTML 部分
@Inject NavigationManager NavigationManager
@inject IProductService ProductService
---
<div class="input-group">
<input @bind-value="searchText"
@bind-value:event="oninput"
type="search"
list="products"
@onkeyup="HandleSearch"
class="form-control"
placeholder="検索..."
@ref="searchInput" />
<datalist id="products">
@foreach (var suggestion in suggestions)
{
<option>@suggestion</option>
}
</datalist>
<div class="input-group-append">
<button class="btn btn-primary" @onclick="SearchProducts">
<span class="oi oi-magnifying-glass"></span>
</button>
</div>
</div>
• すでにあるものを注⼊する
• Product Service
• いくつかの呼び出し
• Navigation Manager
• ユーザーを特定のページに誘導したい
• NavigateTo メソッドを使⽤
Seach コンポーネント作成 - 4
Client → Shared → MainLayout.razor
---
MainLayout.razor
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<Search />
//ここに Search コンポーネントを⼊れる
</div>
<article class="content px-4">
@Body
</article>
</main>
</div>
---
• すでにあるものを注⼊する
• Product Service
• いくつかの呼び出し
• Navigation Manager
• ユーザーを特定のページに誘導したい
• NavigateTo メソッドを使⽤
UI/UX の変更
レイアウトの変更 - 1
Client → Shared → MainLayout.razor
---
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData"
DefaultLayout="@typeof(ShopLayout)">
<NotAuthorized>
<h3>Whoops! You're not allowed to see this page.</h3>
<h5>Please
<a href="login">login</a> or
<a href="register">register
</a> for a new account.</h5>
</NotAuthorized>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(ShopLayout)">
<p role="alert">
Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
---
• MainLayout
• → App.razor で定義
• MainLayout.razor をコピーすると css
もコピーされる
• ShopLayout.razor と
• ShopLayout.razor.css を作成
• App.razor を編集
• ShopLayout をパラメーターに設定
レイアウトの変更 - 2 Client → Shared → NavMenu.razor
---
@inject ICategoryService CategoryService
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">BlazorECommerceApp</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">
Home
</NavLink>
</div>
@foreach (var category in CategoryService.Categories)
{
<div class="nav-item px-3">
<NavLink class="nav-link" href="@category.Url">
@category.Name
</NavLink>
</div>
}
</nav>
</div>
• ShopNavMenu.razor
• ShopNavMenu.razor.css
• 共に編集し最終形にする
• これによって上のナビゲーションメニュー
ボタンができ、左のメニューが消える
• css は⾯倒だがこの機会にある程度
詳しくなると、他のプラットフォームでも
使いこなせる
• css に慣れるためにも Hot Reload
を活⽤してください︕楽しくなります
レイアウトの変更 - 3
Client → Shared → MainLayout.razor
---
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<Search />
</div>
<article class="content px-4">
@Body
</article>
</main>
</div>
---
• ShopNavMenu.razor
• ShopNavMenu.razor.css
• 共に編集し最終形にする
• これによって上のナビゲーションメニュー
ボタンができ、左のメニューが消える
• css は⾯倒だがこの機会にある程度
詳しくなると、他のプラットフォームでも
使いこなせる
• css に慣れるためにも Hot Reload
を活⽤してください︕楽しくなります
レイアウトの変更 - 4 Client → Shared → ShopNavMenu.razor
---
@inject ICategoryService CategoryService
@implements Idisposable
<div class="top-row ps-3 navbar navbar-dark navbar-toggler-wrapper">
<div class="container-fluid">
<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-nav">
<div class="nav-item px-2">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
Home
</NavLink>
</div>
@foreach (var category in CategoryService.Categories)
{
<div class="nav-item px-2">
<NavLink class="nav-link" href="@category.Url">
@category.Name
</NavLink>
</div>
}
</nav>
</div>
---
• MainLayout
• → App.razor で定義
• MainLayout.razor をコピーすると css
もコピーされる
• ShopLayout.razor と
• ShopLayout.razor.css を作成
• App.razor を編集
• ShopLayout をパラメーターに設定
レイアウトの変更 - 5 Client → Shared → ShopNavMenu.razor
---
@inject ICategoryService CategoryService
@implements IDisposable
---
@code {
private bool collapseNavMenu = true;
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
private void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
protected override async Task OnInitializedAsync()
{
await CategoryService.GetCategories();
CategoryService.OnChange += StateHasChanged;
}
public void Dispose()
{
CategoryService.OnChange -= StateHasChanged;
}
}
---
• MainLayout
• → App.razor で定義
• MainLayout.razor をコピーすると css
もコピーされる
• ShopLayout.razor と
• ShopLayout.razor.css を作成
• App.razor を編集
• ShopLayout をパラメーターに設定
HomeButton.razor - 1
Client → Shared → HomuButton.razor
---
@inject NavigationManager NavigationManager
<button @onclick="GoToHome"
class="btn btn-outline-primary home-button">
マイショップ
</button>
@code {
private void GoToHome()
{
NavigationManager.NavigateTo("");
}
}
---
• Home Button の配置
• razor の作成
HomeButton.razor - 2
Client → Shared → HomuButton.razor.css
---
HomeButton.razor.css
.home-button {
white-space: nowrap;
margin-right: 10px;
transform: rotate(-5deg);
}
---
• Home Button の配置
• css の追加
HomeButton.razor - 3
Client → Shared → ShopLayout.razor
---
@inherits LayoutComponentBase
<div class="page">
<main>
<div class="top-row px-2">
//これを追加
<HomeButton />
<Search />
</div>
<div class="nav-menu">
<ShopNavMenu />
</div>
<article class="content px-2">
@Body
</article>
</main>
</div>
---
• Home Button の配置
• ShopLayout.razor の修正
注⽬の商品 - 1
Shared → Product.cs
---
//新しいプロパティを⼊れる
---
namespace BlazorECommerceApp.Shared
{
public class Product
{
public int Id { get; set; }
[Required]
public string Title { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string ImageUrl { get; set; } = string.Empty;
public Category? Category { get; set; }
public int CategoryId { get; set; }
//これを⼊れる
public bool Featured { get; set; } = false;
public List<ProductVariant> Variants { get; set; } =
new List<ProductVariant>();
---
}
}
---
• Featured Products として3つを top
ページにリコメンドして表⽰する
注⽬の商品 - 2 DataContext.cs
---
//新しいプロパティを追加する(3つのみ)
---
Seed を変更する(5回⽬の Migration)
---
new Product
{
Id = 5,
CategoryId = 2,
Title = "Back to the Future",
Description = "「バック・トゥ・ザ・フューチャー」は、ロバート・ゼメキス監督による1985年のアメ
リカのSF映画である。ゼメキスとボブ・ゲイルの脚本で、マイケル・J・フォックス、クリスト
ファー・ロイド、リア・トンプソン、クリスピン・グローバー、トーマス・F・ウィルソンらが出演し
ています。1985年を舞台に、マーティ・マクフライ(フォックス)は、友⼈の⾵変わりな科
学者、エメット博⼠(ロイド)が作ったタイムトラベル可能なデロリアンに乗って、偶然に
も1955年に戻されることになります。ブラウン(ロイド)。過去に閉じ込められたマーティは、
うっかり未来の両親の出会いを邪魔してしまい、⾃分の存在意義を脅かされてしまう。",
ImageUrl = "https://upload.wikimedia.org/wikipedia/en/d/d2/
Back_to_the_Future.jpg",
//ここを追加。3つのみ
Featured = true
},
---
• Featured Products として3つを top
ページにリコメンドして表⽰する
注⽬の商品 - 3
• Package Manager Console で
Migration 実施
cd ./BlazorECommerceApp
cd ./Server
dot net ef Migrations add FeaturedProducts
注⽬の商品 - 4
• Migrations フォルダ →
FeaturedProducts を開いて内容
を確認
• カラムが追加されることを確認
• フラグが⽴つプロダクトの Id を確認
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BlazorEcommerce.Server.Migrations
{
public partial class FeaturedProducts : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "Featured",
table: "Products",
type: "bit",
nullable: false,
defaultValue: false);
migrationBuilder.UpdateData(
table: "Products",
keyColumn: "Id",
keyValue: 1,
column: "Featured",
value: true);
migrationBuilder.UpdateData(
table: "Products",
keyColumn: "Id",
keyValue: 5,
column: "Featured",
value: true);
migrationBuilder.UpdateData(
table: "Products",
keyColumn: "Id",
keyValue: 9,
column: "Featured",
value: true);
}
---
注⽬の商品 - 5
• Package Manager Console で
DB 作成
• Azure Data Studio で確認
• dbo.Products
• Featured 列が増えている
• フラグが⽴っている
dotnet ef database update
注⽬の商品のローディング - 1
Server → Services → ProductService → iProductService.cs
---
namespace BlazorEcommerceApp.Server.Services.ProductService
{
public interface IProductService
{
---
Task<ServiceResponse<ProductSearchResult>> SearchProducts
(string searchText, int page);
Task<ServiceResponse<List<string>>> GetProductSearchSuggestions(string searchText);
//これを追加する
Task<ServiceResponse<List<Product>>> GetFeaturedProducts();
---
}
}
---
注⽬の商品のローディング - 2
Server → Services → ProductService → ProductService.cs
---
//インターフェイスを実装
public async Task<ServiceResponse<List<Product>>> GetFeaturedProducts()
{
var response = new ServiceResponse<List<Product>>
{
Data = await _context.Products
.Where(p => p.Featured)
.Include(p => p.Variants)
.ToListAsync()
};
return response;
}
---
注⽬の商品のローディング – 3
Server → Controllers → ProductController.cs
//下記を追加
---
[HttpGet("featured")]
public async Task<ActionResult<ServiceResponse<List<Product>>>> GetFeaturedProducts()
{
var result = await _productService.GetFeaturedProducts();
return Ok(result);
}
---
注⽬の商品のローディング – 4
Client → Shared → FeaturedProducts.razor.cs
---
@inject IProductService ProductService
@implements Idisposable
---
@code {
protected override void OnInitialized()
{
ProductService.ProductsChanged += StateHasChanged;
}
public void Dispose()
{
ProductService.ProductsChanged -= StateHasChanged;
}
}
---
• 新しいコンポーネントを作ってフィーチャード
プロダクトを表⽰する
• 先に @code 部分から
注⽬の商品のローディング – 5
Client → Shared → FeaturedProducts.razor.cs
---
@inject IProductService ProductService
@implements Idisposable
---
<center><h2>今⽇の⼈気商品</h2></center>
@if (ProductService.Products == null || ProductService.Products.Count == 0)
{
<span>@ProductService.Message</span>
}
else
{
<div class="container">
@foreach (var product in ProductService.Products)
{
@if (product.Featured)
{
<div class="featured-product">
<div>
<a href="product/@product.Id">
<img src="@product.ImageUrl">
</a>
</div>
<h4><a href="product/@product.Id">@product.Title</a></h4>
@if (product.Variants != null && product.Variants.Count > 0)
{
<h5 class="price">
$@product.Variants[0].Price
</h5>
}
</div>
}
}
</div>
}---
• 新しいコンポーネントを作ってフィーチャード
プロダクトを表⽰する
• @code に続いて View 部分
注⽬の商品のローディング – 6
Client → Shared → FeaturedProducts.razor.cs → FeaturedProducts.razor.css
---
.container {
display: flex;
flex-direction: row;
overflow-x: auto;
justify-content: center;
}
img {
max-width: 200px;
max-height: 200px;
border-radius: 6px;
transition: transform .2s;
margin-bottom: 10px;
}
img:hover {
transform: scale(1.1) rotate(5deg);
}
//ここを追加
.featured-product {
margin: 10px;
text-align: center;
padding: 10px;
border: 1px solid lightgray;
border-radius: 10px;
max-width: 200px;
}
@media (max-width: 1023.98px) {
.container {
justify-content: flex-start;
}
}
• FeaturedProducts.razor.css 作成
• Chrome Dev Tool のモバイルビューなど
にも切り替えながら検証する
• Hot Reload は css にこそ有効
注⽬の商品のローディング – 7
Client → Pages → Index.razor
---
@page "/"
@page "/search/{searchText}/{page:int}"
@page "/{categoryUrl}"
@inject IProductService ProductService
---
<PageTitle>マイショップ</PageTitle>
@if (SearchText == null && CategoryUrl == null)
{
<FeaturedProducts /> //ここを追加
}
else
{
<ProductList />
}
@code {
[Parameter]
public string? CategoryUrl { get; set; } = null;
[Parameter]
public string? SearchText { get; set; } = null;
[Parameter]
public int Page { get; set; } = 1;
protected override async Task OnParametersSetAsync()
{
if (SearchText != null)
{
await ProductService.SearchProducts(SearchText, Page);
}
else
{
await ProductService.GetProducts(CategoryUrl); //ここで Go to Implementation
}
}
• Index.razor を修正
注⽬の商品のローディング – 8
Client → Services → ProductServices → ProductService.cs -
GetProducts
---
---
public async Task GetProducts(string? categoryUrl = null)
{
var result = categoryUrl == null ?
await
_http.GetFromJsonAsync<ServiceResponse
<List<Product>>>
("api/product/featured") :
await
_http.GetFromJsonAsync<ServiceResponse
<List<Product>>>
($"api/product/category/{categoryUrl}");
if (result != null && result.Data != null)
Products = result.Data;
CurrentPage = 1;
PageCount = 0;
if (Products.Count == 0)
Message = "商品がみつかりません。";
ProductsChanged.Invoke();
}
---
}
• Client.Services.ProductServices
ProductService.cs の GetProducts
を修正
検索結果の
ページネーション - 1
Shared → Products → ProductSerchResult.cs
---
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BlazorEcommerceApp.Shared
{
public class ProductSearchResult
{
public List<Product> Products { get; set; } = new
List<Product>();
public int Pages { get; set; }
public int CurrentPage { get; set; }
}
}
---
• データベースに多くの製品が登録されている
場合、分割して表⽰させたい
• 1ページに2つの商品を表⽰し、2ページ⽬、
3ページ⽬......と進む
• タイトルと説明⽂だけ表⽰させる
• 商品からデータ転送オブジェクトを作成し、
DTO は商品タイトルと説明⽂だけを返す
• ProductSerchResult.cs という DTO
オブジェクト
• この DTO で、製品のリストを取得し、ペー
ジ数を取得し、情報として現在のページを
取得
検索結果の
ページネーション - 2
Server → Services → ProductServices → IProductService.cs
---
Server Services ProductServices IProductService.cs
namespace BlazorEcommerceApp.Server.Services.ProductService
{
public interface IProductService
{
---
//ここを追加
Task<ServiceResponse<ProductSearchResult>>
SearchProducts(string searchText, int page);
---
}
}---
• ProductSerchResult.cs という DTO
オブジェクト
• この DTO で、製品のリストを取得し、ペー
ジ数を取得し、情報として現在のページを
取得
検索結果の
ページネーション - 3
Server → Services → ProductServices → ProductService.cs
---
public async Task<ServiceResponse<ProductSearchResult>> SearchProducts(string
searchText, int page)
//リターン値を ProductSearchResult にしてパラメーターに page も追加
{
//下記の両者を定義しておく
var pageResults = 2f;
var pageCount = Math.Ceiling((await
FindProductsBySearchText(searchText))
.Count / pageResults);
var products = await _context.Products
.Where(p =>
p.Title.ToLower().Contains(searchText.ToLower())
||p.Description.ToLower().Contains(searchText.ToLower()) &&
p.Visible && !p.Deleted)
.Include(p => p.Variants)
.Skip((page - 1) * (int)pageResults)
.Take((int)pageResults)
.ToListAsync();
//ここもProductSearchResultに変更
var response = new ServiceResponse<ProductSearchResult>
{
Data = new ProductSearchResult
{
Products = products,
CurrentPage = page,
Pages = (int)pageCount
}
};
return response;
}
---
• Server 上のページネーション
検索結果の
ページネーション - 4
Server → Controllers → ProductContoroller.cs
---
//page 追加
[HttpGet("search/{searchText}/{page}")]
public async
Task<ActionResult<ServiceResponse<ProductSearchResult>>>
//page 追加、デフォルト値=1
SearchProducts(string searchText, int page = 1)
{
var result = await
_productService.SearchProducts(searchText, page);
return Ok(result);
}
------
• コントローラにも変更を加える必要あり
• Product コントローラの Search メソッドに、
もうひとつパラメータを追加(Page)
• デフォルトで1に設定
• アプリケーションを起動
• Swagger ページを開く
• 検索テキストを⼊⼒してテスト
検索結果の
ページネーション - 5
Client → Services → ProductService → IProductService.cs
---
namespace BlazorEcommerceApp.Client.Services.ProductService
{
public interface IProductService
{
--
Task<ServiceResponse<Product>>
GetProduct(int productId);
Task SearchProducts(string searchText, int page);
---
---
• クライアントの変更を実装
Client → Services → ProductService → IProductService.cs
---
namespace BlazorEcommerceApp.Client.Services.ProductService
{
---
public List<Product> Products { get; set; } = new
List<Product>();
//下記3⾏を追加
public string Message { get; set; } =
"商品をロードしています...";
public int CurrentPage { get; set; } = 1;
public int PageCount { get; set; } = 0;
---
検索結果の
ページネーション - 6
Client → Services → ProductService → ProductService.cs
---
public async Task GetProducts(string? categoryUrl = null)
{
---
CurrentPage = 1;
PageCount = 0;
if (Products.Count == 0)
Message = "商品がみつかりません。";
ProductsChanged.Invoke();}
---
• Client 側 GetProducts を修正
• Client 側 SearchProducts を修正
Client → Services → ProductService → ProductService.cs
---
public async Task SearchProducts(string searchText, int page)
//page パラメータを追加
{
LastSearchText = searchText;
var result = await _http
.GetFromJsonAsync<ServiceResponse<ProductSearchResult>>
//List<Products> をProductSearchResult に変更
($"api/product/search/{searchText}/{page}");
//page 部分を追加
if (result != null && result.Data != null)
{
Products = result.Data.Products;
CurrentPage = result.Data.CurrentPage;
PageCount = result.Data.Pages;
}
if (Products.Count == 0) Message = "商品がみつかりません。";
ProductsChanged?.Invoke();
}
---
検索結果の
ページネーション - 7
Client → Shared → Search.razor
---
public void SearchProducts()
{
---
//1 をデフォルト値として追加
NavigationManager.NavigateTo($"search/{searchText}/1");
---
• ページネーションのコンポーネントへの追加
• Search.razor、 Index.razor を修正
Client → Pages → Index.razor
---
//int を追加
@page "/search/{searchText}/{page:int}"
---
// パラメータ追加
[Parameter]
public int Page { get; set; } = 1;
---
//修正
---
protected override async Task OnParametersSetAsync()
{
if (SearchText != null)
{
//page 追加
await ProductService.SearchProducts(SearchText, Page);
}
---
---
検索結果の
ページネーション - 8
Client → Shared → Product.razor
---
//下記を追加
for (var i = 1; i <= ProductService.PageCount; i++)
{
<a class="btn
@(i == ProductService.CurrentPage ?
"btn-info" : "btn-outline-info")
page-selection"
href="/search/@ProductService.LastSearchText/@i">@i</a>
}
---
• ボタンの追加
• これでページネーションは完成 Client → Shared → Product.razor.css
---
//追加
---
.page-selection {
margin-right: 15px;
margin-bottom: 30px;
}
//追加
---
.page-selection {
margin-right: 15px;
margin-bottom: 30px;
}
---
カートサービスの実装
ショッピングカート - 1
Client → Program.cs
---
//下記を追加
---
using Blazored.LocalStorage;
---
builder.Services.AddBlazoredLocalStorage();
---
• ローカルストレージを使う
• Client プロジェクトに NuGet パッケージ
追加
• Blazer Local Storage
Client → Imports.razor
---
//追加
---
@using Blazored.LocalStorage
---
---
ショッピングカート - 2
Client → Shared → CartCounter.razor
---
@inject ICartService CartService
@inject ISyncLocalStorageService LocalStorage
@implements IDisposable
<a href="cart" class="btn btn-info">
<i class="oi oi-cart"></i>
<span class="badge">@GetCartItemsCount()</span>
</a>
@code {
private int GetCartItemsCount()
{
var count = LocalStorage.GetItem<int>("cartItemsCount");
return count;
}
protected override void OnInitialized()
{
CartService.OnChange += StateHasChanged;
}
public void Dispose()
{
CartService.OnChange -= StateHasChanged;
}
}
---
• カートを追加
• デバッグ実⾏して画⾯のカート部分を確認
ショッピングカート - 3
Client → CartItem.cs
---
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BlazorEcommerceApp.Shared
{
public class CartItem
{
public int ProductId { get; set; }
public int ProductTypeId { get; set; }
}
}---
• カートを追加
• デバッグ実⾏して画⾯のカート部分を確認
クライアント側の
CartService 実装 - 1
Client → Program.cs
---
builder.Services.AddScoped<ICategoryService,
CategoryService>();
---
• Client → Service →
ICartService.cs を追加
• Client → Service →
CartService.cs を追加
Client → _Imports.razor
---
@using BlazorEcommerceApp.Client.Services.CartService
---
クライアント側の
CartService 実装 - 2
Client → Client → Service → ICartService.cs
---
namespace
BlazorEcommerceApp.Client.Services.CartService
{
public interface ICartService
{
event Action OnChange;
Task AddToCart(CartItem cartItem);
Task<List<CartProductResponse>>
GetCartProducts();
Task RemoveProductFromCart(int productId,
int productTypeId);
Task UpdateQuantity(CartProductResponse
product);
Task StoreCartItems(bool emptyLocalCart);
Task GetCartItemsCount();
}
}
---
• Client → Service →
ICartService.cs を追加
クライアント側の
CartService 実装 - 3
Client → Client → Service → CartService.cs
---
using Blazored.LocalStorage;
namespace BlazorEcommerceApp.Client.Services.CartService
{
public class CartService : ICartService
{
private readonly ILocalStorageService _localStorage;
private readonly HttpClient _http;
private readonly IAuthService _authService;
public CartService(ILocalStorageService localStorage, HttpClient http,
IAuthService authService)
{
_localStorage = localStorage;
_http = http;
_authService = authService;
}
public event Action OnChange;
public async Task AddToCart(CartItem cartItem)
{
if (await _authService.IsUserAuthenticated())
{
await _http.PostAsJsonAsync("api/cart/add", cartItem);
}
else
{
• Client → Service → CartService.cs
を追加
var cart = await _localStorage.GetItemAsync<List<CartItem>>("cart");
if (cart == null)
{
cart = new List<CartItem>();
}
var sameItem = cart.Find(x => x.ProductId ==
cartItem.ProductId &&
x.ProductTypeId == cartItem.ProductTypeId);
if (sameItem == null)
{
cart.Add(cartItem);
}
else
{
sameItem.Quantity += cartItem.Quantity;
}
await _localStorage.SetItemAsync("cart", cart);
}
await GetCartItemsCount();
}
public async Task GetCartItemsCount()
{
if (await _authService.IsUserAuthenticated())
{
var result = await
_http.GetFromJsonAsync<ServiceResponse<int>>("api/cart/count");
var count = result.Data;
await _localStorage.SetItemAsync<int>
("cartItemsCount", count);
}
else
{
var cart = await
_localStorage.GetItemAsync<List<CartItem>>("cart");
await _localStorage.SetItemAsync<int>
("cartItemsCount", cart != null ? cart.Count : 0);
}
OnChange.Invoke();
}
---
クライアント側の
CartService 実装 - 4
• ProductDetail に Add to Cart ボタン
を追加
Client Pages ProductDetails.razor
// 先に View 側に追加
---
@inject ICartService CartService
---
---
<button class="btn btn-primary" @onclick="AddToCart">
<i class="oi oi-cart"></i>&nbsp;&nbsp;&nbsp;Add
to Cart
</button>
---
---
// 次いで@code側に追加
---
private async Task AddToCart()
{
var productVariant = GetSelectedVariant();
var cartItem = new CartItem
{
ProductId = productVariant.ProductId,
ProductTypeId = productVariant.ProductTypeId
};
await CartService.AddToCart(cartItem);
}
---
クライアント側の
CartService 実装 - 5
• デバッグ実⾏
• Chrome Developer Tools 起動
• アイテムを⼀つ選択
• アプリケーションタブに切り替え
• ローカルストレージ表⽰
• 当該アイテムを Add to Cart で追加
• 値を確認する
クライアント側の
CartService 実装 - 6
• CartCounter 数字のインクリメント
• @Code 部分を先に実装
Client → Shared → CartCounter.razor
// 先に @code 部分を実装
---
@inject ICartService CartService
@inject ISyncLocalStorageService LocalStorage
@implements IDisposable
---
@code {
private int GetCartItemsCount()
{
var count =
LocalStorage.GetItem<int>("cartItemsCount");
return count;
}
protected override void OnInitialized()
{
CartService.OnChange += StateHasChanged;
}
public void Dispose()
{
CartService.OnChange -= StateHasChanged;
}
}
---
クライアント側の
CartService 実装 - 7
• CartCounter 数字のインクリメント
• 続いて view 部分を実装
• CartService.cs →
OnChange.Invoke() を追加
Client → Shared → CartCounter.razor
// View 部分を実装
---
<a href="cart" class="btn btn-info">
<i class="oi oi-cart"></i>
<span class="badge">@GetCartItemsCount()</span>
</a>
---
Client → Service → CartService → CartService.cs
---
OnChange.Invoke();
---
CartItem のサーバー側
Products への送付- 1
• Shared の CartProductResponse.cs
Shared → CartProductResponse.cs
---
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BlazorEcommerceApp.Shared
{
public class CartProductResponse
{
public int ProductId { get; set; }
public string Title { get; set; } = string.Empty;
public int ProductTypeId { get; set; }
public string ProductType { get; set; } =
string.Empty;
public string ImageUrl { get; set; } =
string.Empty;
public decimal Price { get; set; }
public int Quantity { get; set; }
}
}
---
CartItem のサーバー側
Products への送付- 2
• Server → Services → CartService
フォルダ作成
• Server → Services → ICartService
• Server → Services → CartService
を追加
Server → Program.cs
//下記を追加
---
builder.Services.AddScoped<ICartService, CartService>();
---
global using
BlazorEcommerceApp.Server.Services.CartService;
---
CartItem のサーバー側
Products への送付- 3
• Server → Services → CartServiceProgram.cs 実装
Server → Services → CartServiceProgram.cs
---
using System.Security.Claims;
namespace BlazorEcommerceApp.Server.Services.CartService
{
public class CartService : ICartService
{
private readonly DataContext _context;
public CartService(DataContext context, IAuthService authService)
{
_context = context;
}
public async Task<ServiceResponse<List<CartProductResponse>>>
GetCartProducts(List<CartItem> cartItems)
{
var result = new ServiceResponse<List<CartProductResponse>>
{
Data = new List<CartProductResponse>()
};
foreach (var item in cartItems)
{
var product = await _context.Products
.Where(p => p.Id == item.ProductId)
.FirstOrDefaultAsync();
if (product == null)
{
continue;
}
var productVariant = await _context.ProductVariants
.Where(v => v.ProductId == item.ProductId
&& v.ProductTypeId == item.ProductTypeId)
.Include(v => v.ProductType)
.FirstOrDefaultAsync();
if (productVariant == null)
{
continue;
}
var cartProduct = new CartProductResponse
{
ProductId = product.Id,
Title = product.Title,
ImageUrl = product.ImageUrl,
Price = productVariant.Price,
ProductType = productVariant.ProductType.Name,
ProductTypeId = productVariant.ProductTypeId,
Quantity = item.Quantity
};
result.Data.Add(cartProduct);
}
return result;
}
---
CartItem のサーバー側
Products への送付- 4
• Server → Controller→
CartController.cs 実装
Server → Controller → CartController.cs
//
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
namespace BlazorEcommerceApp.Server.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class CartController : ControllerBase
{
private readonly ICartService _cartService;
public CartController(ICartService cartService)
{
_cartService = cartService;
}
[HttpPost("products")]
public async
Task<ActionResult<ServiceResponse<List<CartProductResponse>>>>
GetCartProducts(List<CartItem> cartItems)
{
var result = await _cartService.GetCartProducts(cartItems);
return Ok(result);
}
---
---
クライアント側の
CartProduct 取得
• Client → Services →
ICartService.cs 実装
• Client → Services →
CartService.cs 実装
Client → Services → ICartService.cs
---
namespace BlazorEcommerceApp.Client.Services.CartService
{
public interface ICartService
{
event Action OnChange;
Task AddToCart(CartItem cartItem);
//ここを追加
Task<List<CartProductResponse>> GetCartProducts();
Task RemoveProductFromCart(int productId, int productTypeId);
}
}
---
Client → Services → CartService.cs
---
// このメソッドで取得する
public async Task<List<CartProductResponse>> GetCartProducts()
{
if (await _authService.IsUserAuthenticated())
{
var response = await
_http.GetFromJsonAsync
<ServiceResponse<List<CartProductResponse>>>
("api/cart");
return response.Data;
}
else
{
var cartItems = await
_localStorage.GetItemAsync<List<CartItem>>("cart");
if (cartItems == null)
return new List<CartProductResponse>();
var response = await
_http.PostAsJsonAsync
("api/cart/products", cartItems);
var cartProducts =
await
response.Content.ReadFromJsonAsync
<ServiceResponse<List<CartProductResponse>>>();
return cartProducts.Data;
}
}
---
Cart ページの実装 - 1
• Client → Pages → Cart.razor
• 先に @code 部分を実装する
Client → Pages → Cart.razor
---
// 先に@codeを実装する
@page "/cart"
@inject ICartService CartService
@inject IOrderService OrderService
@inject IAuthService AuthService
@inject NavigationManager NavigationManager
---
@code {
List<CartProductResponse> cartProducts = null;
string message = "Loading cart...";
bool isAuthenticated = false;
protected override async Task OnInitializedAsync()
{
isAuthenticated = await AuthService.IsUserAuthenticated();
await LoadCart();
}
private async Task RemoveProductFromCart(int productId, int productTypeId)
{
await CartService.RemoveProductFromCart(productId, productTypeId);
await LoadCart();
}
private async Task LoadCart()
{
await CartService.GetCartItemsCount();
cartProducts = await CartService.GetCartProducts();
if (cartProducts == null || cartProducts.Count == 0)
{
message = "Your cart is empty.";
}
}
private async Task UpdateQuantity(ChangeEventArgs e, CartProductResponse
product)
{
product.Quantity = int.Parse(e.Value.ToString());
if (product.Quantity < 1)
product.Quantity = 1;
await CartService.UpdateQuantity(product);
}
private async Task PlaceOrder()
{
string url = await OrderService.PlaceOrder();
NavigationManager.NavigateTo(url);
}
}
---
Cart ページの実装 - 2
• Client → Pages → Cart.razor
• 続いて View 部分を実装する
Client → Pages → Cart.razor
---
<PageTitle>Shopping Cart</PageTitle>
<h3>ショッピングカート</h3>
@if (cartProducts == null || cartProducts.Count == 0)
{
<span>@message</span>
}
else
{
<div>
@foreach (var product in cartProducts)
{
<div class="container">
<div class="image-wrapper">
<img src="@product.ImageUrl" class="image" />
</div>
<div class="name">
<h5><a
href="/product/@product.ProductId">@product.Title</a></h5>
<span>@product.ProductType</span><br />
<input type="number" value="@product.Quantity"
@onchange="@((ChangeEventArgs e) =>
UpdateQuantity(e, product))"
class="form-control input-quantity"
min="1" />
<button class="btn-delete" @onclick="@(() =>
RemoveProductFromCart
(product.ProductId, product.ProductTypeId))">
Delete
</button>
</div>
<div class="cart-product-price">
$@(product.Price * product.Quantity)</div>
</div>
}
<div class="cart-product-price">
Total (@cartProducts.Count):
$@cartProducts.Sum
(product => @product.Price * product.Quantity)
</div>
</div>
@if (isAuthenticated)
{
<div>
<h5>Delivery Address</h5>
<AddressForm />
</div>
}
<button @onclick="PlaceOrder" class="btn alert-success
float-end mt-1">Checkout</button>
}
---
Cart ページの実装 - 3
• Client → Pages → Cart.razor.css
実装
Client → Pages → Cart.razor.css
---
.container {
display: flex;
padding: 6px;
}
.image-wrapper {
width: 150px;
text-align: center;
}
.image {
max-height: 150px;
max-width: 150px;
padding: 6px;
}
.name {
flex-grow: 1;
padding: 6px;
}
.cart-product-price {
font-weight: 600;
text-align: right;
}
.btn-delete {
background: none;
border: none;
padding: 0px;
color: red;
font-size: 12px;
}
.btn-delete:hover {
text-decoration: underline;
}
.input-quantity {
width: 70px;
}
---
Cart から Item を削除 - 1
• Client → Services → CartService
ICartService.cs 追加
• Client → Services → CartService
ICartService.cs 修正
• RemoveProductFromCart 追加
Client → Services → CartService → ICartService.cs
---
//追加
Task RemoveProductFromCart(int productId, int productTypeId);
---
Client → Services → CartService → CartService.cs
---
//修正 RemoveProductFromCart 追加
public async Task RemoveProductFromCart(int productId, int productTypeId)
{
if (await _authService.IsUserAuthenticated())
{
await _http.DeleteAsync($"api/cart/{productId}/{productTypeId}");
}
else
{
var cart = await
_localStorage.GetItemAsync<List<CartItem>>("cart");
if (cart == null)
{
return;
}
var cartItem = cart.Find(x => x.ProductId == productId
&& x.ProductTypeId == productTypeId);
if (cartItem != null)
{
cart.Remove(cartItem);
await _localStorage.SetItemAsync("cart", cart);
}
}
}
---
Cart から Item を削除 - 2
• Client → Pages → Cart.razor
• View 部分修正
• @Code 部分修正
Client → Pages → Cart.razor
---
//View 部分修正
<button class="btn-delete" @onclick="@(() =>
RemoveProductFromCart(product.ProductId,
product.ProductTypeId))">
Delete
</button>---
Client → Pages → Cart.razor
---
// @Code 部分修正(追加)
private async Task RemoveProductFromCart
(int productId, int productTypeId)
{
await CartService.RemoveProductFromCart(productId, productTypeId);
await LoadCart();
}
---
private async Task LoadCart()
{
await CartService.GetCartItemsCount();
cartProducts = await CartService.GetCartProducts();
if (cartProducts == null || cartProducts.Count == 0)
{
message = "Your cart is empty.";
}
}
---
Cart から Item を削除 - 3
• Client → Pages → Cart.razor.css
への追加
Client → Pages → Cart.razor.css
---
// 追加
---
.btn-delete {
background: none;
border: none;
padding: 0px;
color: red;
font-size: 12px;
}
--- ---
Cart から Item を削除 - 4
• デバッグ実⾏
• カートを表⽰
• Chrome Dev Tools アプリケーションタブ
に移動
• ローカルストレージを表⽰
• delete ボタン押下してテスト
Cart モデルに数量を追加
- 1
• Shared →
CartProductResponse.cs を修正
Shared → CartProductResponse.cs
---
// 追加
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BlazorEcommerceApp.Shared
{
public class CartProductResponse
{
public int ProductId { get; set; }
public string Title { get; set; } = string.Empty;
public int ProductTypeId { get; set; }
public string ProductType { get; set; } =
string.Empty;
public string ImageUrl { get; set; } =
string.Empty;
public decimal Price { get; set; }
public int Quantity { get; set; }
}
}
---
Cart モデルに数量を追加
- 2a
• Server → Services → CartService
→ CartService.cs を修正
Server → Services → CartService → CartService.cs
---
// 追加
---
var cartProduct = new CartProductResponse
{
ProductId = product.Id,
Title = product.Title,
ImageUrl = product.ImageUrl,
Price = productVariant.Price,
ProductType = productVariant.ProductType.Name,
ProductTypeId = productVariant.ProductTypeId,
Quantity = item.Quantity
};
---
Cart モデルに数量を追加
- 2b
• Server → Services → CartService
→ CartService.cs を修正
Server → Services → CartService → CartService.cs
---
public async Task<ServiceResponse<bool>>
AddToCart(CartItem cartItem)
{
cartItem.UserId = _authService.GetUserId();
var sameItem = await _context.CartItems
.FirstOrDefaultAsync(ci => ci.ProductId ==
cartItem.ProductId &&
ci.ProductTypeId ==
cartItem.ProductTypeId &&
ci.UserId == cartItem.UserId);
if (sameItem == null)
{
_context.CartItems.Add(cartItem);
}
else
{
//この箇所を追加する
sameItem.Quantity += cartItem.Quantity;
}
await _context.SaveChangesAsync();
return new ServiceResponse<bool> { Data = true };
}
---
Cart モデルに数量を追加
- 3a
• Client → Services → CartService
→ ICartService.cs を修正
Client → Services → CartService → ICartService.cs
---
namespace BlazorEcommerceApp.Client.Services.CartService
{
public interface ICartService
{
---
Task UpdateQuantity(CartProductResponse product);
Task StoreCartItems(bool emptyLocalCart);
//ここを追加
Task GetCartItemsCount();
}
} ---
Cart モデルに数量を追加
- 3b
• Client → Services → CartService
→ CartService.cs を修正
Client → Services → CartService → CartService.cs
---
public async Task UpdateQuantity(CartProductResponse product)
{
if (await _authService.IsUserAuthenticated())
{
var request = new CartItem
{
ProductId = product.ProductId,
Quantity = product.Quantity,
ProductTypeId = product.ProductTypeId
};
await _http.PutAsJsonAsync("api/cart/update-quantity", request);
}
else
{
var cart = await _localStorage.GetItemAsync<List<CartItem>>("cart");
if (cart == null)
{
return;
}
var cartItem = cart.Find(x => x.ProductId == product.ProductId
&& x.ProductTypeId == product.ProductTypeId);
if (cartItem != null)
{
cartItem.Quantity = product.Quantity;
await _localStorage.SetItemAsync("cart", cart);
}
}
}
---
Cart モデルに数量を追加
- 4a
• 数値⼊⼒フィールドで数量を更新する
Client → Pages → Cart.razor
//追加
---
<div class="name">
<h5><a
href="/product/@product.ProductId">@product.Title</a>
</h5>
<span>@product.ProductType</span><br />
<input type="number" value="@product.Quantity"
@onchange="@((ChangeEventArgs e) => UpdateQuantity(e, product))"
class="form-control input-quantity"
min="1"
/>
---
Cart モデルに数量を追加
- 4b
• UpdateQuantity 追加する
Client → Pages → Cart.razor
//追加
---
private async Task UpdateQuantity
(ChangeEventArgs e, CartProductResponse product)
{
product.Quantity = int.Parse(e.Value.ToString());
if (product.Quantity < 1)
product.Quantity = 1;
await CartService.UpdateQuantity(product);
}
---
Cart モデルに数量を追加
- 4c
• UpdateQuantity 追加する
Client → Pages → Cart.razor
//追加
---
private async Task UpdateQuantity
(ChangeEventArgs e, CartProductResponse product)
{
product.Quantity = int.Parse(e.Value.ToString());
if (product.Quantity < 1)
product.Quantity = 1;
await CartService.UpdateQuantity(product);
}
---
Cart モデルに数量を追加
- 4c
• デバッグ実⾏
• カートを表⽰
• Chrome Dev Tools アプリケーションタブ
に移動
• ローカルストレージを表⽰
• update ボタン押下してテスト
認証・ユーザー登録、その他の機能の実装
認証 全体の流れ
• 新しいページの追加
• 最初のユーザーを登録
• パスワードのハッシュを作成し、パスワードを解決
• JSON Web Token
• Authorized View の利⽤
UserRegister Model の作成
• ユーザー登録には
新しいモデルが必要
• モデルの名前は
UserRegister
• Shared Project
を右クリックし、ここに
新 Class を追加
• Public Class
shared → UserRegister
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BlazorEcommerceApp.Shared
{
public class UserRegister
{
[Required, EmailAddress]
public string Email { get; set; } = string.Empty;
[Required, StringLength(100, MinimumLength = 6)]
public string Password { get; set; } = string.Empty;
[Compare("Password", ErrorMessage = "The passwords do not match.")]
public string ConfirmPassword { get; set; } = string.Empty;
}
}
ユーザー登録ページの作成
• Client に戻り、 いくつかの
ページフォルダに、新しい
Razor コンポーネントを追加
して、これを呼び出す
• テスト
• https://localhost:(ポート
番号 )/register
Client → Pages → Register.razor
@page "/register”
<PageTitle>Register</PageTitle>
<h3>登録</h3>
@code {
}
ユーザーメニューボタンの実装 - 1
• dropdown クラス
• UserMenuCssClass
• dropdown-item :
Register
• showUserMenu
• UserMenuClass
• ToggleUserMenu
• HideUserMenu
Shared → UserButton.Razor
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject NavigationManager NavigationManager
<div class="dropdown">
<button @onclick="ToggleUserMenu"
@onfocusout="HideUserMenu"
class="btn btn-secondary dropdown-toggle user-button">
<i class="oi oi-person"></i>
</button>
<div class="dropdown-menu dropdown-menu-right @UserMenuCssClass">
<a href="register" class="dropdown-item">Register</a>
</div>
@code {
private bool showUserMenu = false;
private string UserMenuCssClass => showUserMenu ? "show-menu" : null;
private void ToggleUserMenu()
{
showUserMenu = !showUserMenu;
}
private async Task HideUserMenu()
{
await Task.Delay(200);
showUserMenu = false;
}
}
ユーザーメニューボタンの実装 - 2
• .show-menu
• .user-button
• .top-row a
• .dropdown-
item:hover
Shared → UserButton.Razor.css
---
.show-menu {
display: block;
}
.user-button {
margin-left: .5em;
}
.top-row a {
margin-left: 0;
}
.dropdown-item:hover {
background-color: white;
}
---
検証のためのデータアノテーションを追加する
• データアノテーション
または属性をモデル
に追加
• Required 属性を
追加
• Compare 属性を
追加
Shared → UserRegister.cs
===
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BlazorEcommerceApp.Shared
{
public class UserRegister
{
[Required, EmailAddress]
public string Email { get; set; } = string.Empty;
[Required, StringLength(100, MinimumLength = 6)]
public string Password { get; set; } = string.Empty;
[Compare("Password", ErrorMessage = "The passwords do not match.")]
public string ConfirmPassword { get; set; } = string.Empty;
}
}
登録フォームにバリデーションを追加
• DataAnnotations
Validator
コンポーネントを使⽤
• バリデーションのサマリー
• DEMO
• 有効な email
アドレス
Shared → Register.razor
---
<EditForm Model="user" OnValidSubmit="HandleRegistration">
<DataAnnotationsValidator />
<div class="mb-3">
<label for="email">Email</label>
<InputText id="email" @bind-Value="user.Email" class="form-control" />
</div>
<div class="mb-3">
<label for="password">Password</label>
<InputText id="password" @bind-Value="user.Password" class="form-control"
type="password" />
</div>
<div class="mb-3">
<label for="confirmPassword">Confirm Password</label>
<InputText id="confirmPassword" @bind-Value="user.ConfirmPassword" class="form-
control" type="password" />
</div>
<button type="submit" class="btn btn-primary">Register</button>
<div class="@messageCssClass">
<span>@message</span>
</div>
<ValidationSummary />
</EditForm>
---
バリデーションサマリーの代わりにバリデーションメッセージを使う
• DataAnnotations
Validator コン
ポーネントを使⽤
• テキストフィールド後に、
バリデーションメッセージ
を追加
• DEMO
• 有効な email
アドレス
Shared → Register.razor
---
<EditForm Model="user" OnValidSubmit="HandleRegistration">
<DataAnnotationsValidator />
<div class="mb-3">
<label for="email">Email</label>
<InputText id="email" @bind-Value="user.Email" class="form-control" />
<ValidationMessage For="@(() => user.Email)" />
</div>
<div class="mb-3">
<label for="password">Password</label>
<InputText id="password" @bind-Value="user.Password" class="form-control"
type="password" />
<ValidationMessage For="@(() => user.Password)" />
</div>
<div class="mb-3">
<label for="confirmPassword">Confirm Password</label>
<InputText id="confirmPassword" @bind-Value="user.ConfirmPassword" class="form-
control" type="password" />
<ValidationMessage For="@(() => user.ConfirmPassword)" />
</div>
<button type="submit" class="btn btn-primary">Register</button>
<div class="@messageCssClass">
<span>@message</span>
</div>
</EditForm>
---
データベースのユーザーモデルを追加する - 1
• 共有フォルダを右クリック
して、新しいクラス追加
• ハッシュ値とパスワード
ソルトをデータベースに
保存するので、平⽂で
はできない
• DEMO
• 有効な email
アドレス
Shared → User.cs
---
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BlazorEcommerceApp.Shared
{
public class User
{
public int Id { get; set; }
public string Email { get; set; } = string.Empty;
public byte[] PasswordHash { get; set; }
public byte[] PasswordSalt { get; set; }
public DateTime DateCreated { get; set; } = DateTime.Now;
public Address Address { get; set; }
public string Role { get; set; } = "Customer";
}
}
---
データベースのユーザーモデルを追加する - 2
• データベース・テーブルが
必要
• データ・コンテキストに.
移動して別のテーブル
を追加
• Migrations フォルダ
• 2022xxxxx_
Users.cs
• id=Key, email、
password,
hash
password,
hash password
salt, で作成
• テーブル作成
• データベースを Azure
Server → Data → DataContext.cs
---
public DbSet<User> Users { get; set; }
---
terminal
---
cd ..Server
dotnet ef migrations add Users
---
===
dotnet ef database update
===
サーバーに認証サービスを追加する
• Server → Services
→ AuthService
フォルダ作成
• IAuthService.cs
作成
• Program.cs サービス
パイプラインに登録
• IAuthService 実装
Server → Program.cs
---
builder.Services.AddScoped<IAuthService, AuthService>();
global using BlazorEcommerceApp.Server.Services.AuthService;
---
Server → Services → AuthService → IAuthService.cs
---
namespace BlazorEcommerceApp.Server.Services.AuthService
{
public interface IAuthService
{
Task<ServiceResponse<int>> Register (User user, string password);
Task<bool> UserExists(string email);
Task<ServiceResponse<string>> Login(string email, string password);
Task<ServiceResponse<bool>> ChangePassword(int userId, string
newPassword);
int GetUserId();
string GetUserEmail();
Task<User> GetUserByEmail(string email);
}
}
---
ユーザーが既に存在するか確認する
• ユーザーが既に存在する
か確認
Server → Services → AuthService → IuthService.cs
---
public async Task<bool> UserExists(string email)
{
if (await _context.Users.AnyAsync(user =>
user.Email.ToLower()
.Equals(email.ToLower())))
{
return true;
}
return false;
}
---
サーバーへのユーザー登録の実施 - 1
• サーバーへのユーザー
登録
Server → Services → AuthService → AuthService.cs
---
public async Task<ServiceResponse<int>> Register(User user, string password)
{
if (await UserExists(user.Email))
{
return new ServiceResponse<int>
{
Success = false,
Message = "このユーザーは既に存在しています。"
};
}
CreatePasswordHash(password, out byte[] passwordHash, out byte[]
passwordSalt);
user.PasswordHash = passwordHash;
user.PasswordSalt = passwordSalt;
_context.Users.Add(user);
await _context.SaveChangesAsync();
return new ServiceResponse<int>
{ Data = user.Id, Message = "登録が成功しました!!" };
}
---
サーバーへのユーザー登録の実施 - 2
• 変更をテーブルに保存
• テストするには、もちろん
Auth Controller が
必要
Server → Services → AuthService → AuthService.cs
---
private void CreatePasswordHash(string password, out byte[] passwordHash, out byte[]
passwordSalt)
{
using (var hmac = new HMACSHA512())
{
passwordSalt = hmac.Key;
passwordHash = hmac
.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password));
}
}
---
AuthControllerを追加する - 1
• Server →
Controllers →
AuthController →
API コントローラー新規
作成
• control フォルダに別の
空の API コントローラを
作成
Server → Controllers → AuthController.cs
---
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
namespace BlazorEcommerceApp.Server.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AuthController : ControllerBase
{
private readonly IAuthService _authService;
public AuthController(IAuthService authService)
{
_authService = authService;
}
---
---
AuthControllerを追加する - 2
• アプリを起動して、
swagger にアクセス
• localhost://ポート番号
/swagger/index.html
• user already exists
• Azure Data
Explorer で確認
クライアント側で AuthService を作成する
• IAuthService
インターフェイスと
AuthService
実装クラスを追加
Client → Services → AuthService → AuthService.cs
---
namespace BlazorEcommerceApp.Client.Services.AuthService
{
public class AuthService : IAuthService
{
private readonly HttpClient _http;
private readonly AuthenticationStateProvider _authStateProvider;
public AuthService(HttpClient http, AuthenticationStateProvider
authStateProvider)
{
_http = http;
_authStateProvider = authStateProvider;
}
---
}
}
---
global using BlazorEcommerceApp.Client.Services.AuthService;
===
client → wwwroot → imports.razor
===
@using BlazorEcommerceApp.Client.Services.AuthService
===
クライアント側での登録の実装 - 1
• 登録⽤のメソッドを1つ
追加、再び返す
• サービスは UserId で
応答
• register メソッド
リクエストとして登録
• インターフェイスを実装
し、HTTP クライアント
追加
• フィールドを作成
• JSON async await
Client → Services → AuthService → AuthService.cs
---
namespace BlazorEcommerceApp.Client.Services.AuthService
{
public class AuthService : IAuthService
{
private readonly HttpClient _http;
public AuthService(HttpClient http)
{
_http = http;
}
public async Task<ServiceResponse<int>> Register (UserRegister request)
{
var result = await _http.PostAsJsonAsync("api/auth/register",
request);
return await
result.Content.ReadFromJsonAsync<ServiceResponse<int>>();
}
}
}
クライアント側での登録の実装 - 2
• 登録⽤のメソッドを1つ
追加、再び返す
• サービスは UserId で
応答
• register メソッド
リクエストとして登録
• インターフェイスを実装し、
HTTP クライアント追加
• フィールドを作成
• JSON async await
Client → Services → AuthService → IAuthService.cs
namespace BlazorEcommerceApp.Client.Services.AuthService
{
public interface IAuthService
{
Task<ServiceResponse<int>> Register(UserRegister request);
Task<ServiceResponse<string>> Login(UserLogin request);
Task<ServiceResponse<bool>> ChangePassword(UserChangePassword request);
Task<bool> IsUserAuthenticated();
}
}
登録ページで AuthService を利⽤する
• テスト
• chrome 開発ツール
• Register ボタンを押す
ときに Fetch/XHR を
観察
• success
Client → Register.razor
---
@page "/register"
@inject IAuthService AuthService
---
<PageTitle>Register</PageTitle>
---
<button type="submit"
class="btn btn
primary">Register</button>
<div class="@messageCssClass">
<span>@message</span>
</div>
</EditForm>
@code {
UserRegister user =
new UserRegister();
string errormessage =
string.Empty;
---
Client → Register.razor
---
async Task HandleRegistration()
{
var result = await
AuthService.Register(user);
message = result.Message;
if (result.Success)
errormessage =
Result.message;
else
errormessage =
String.Empty;
}
}
---
登録後に成功のメッセージを表⽰する - 1
• テスト
• chrome 開発ツール
• Register ボタンを押す
ときに Fetch/XHR を
観察
• success
Server → Services → AuthService → AuthService.cs
---
public async Task<ServiceResponse<int>> Register(User user, string password)
{
if (await UserExists(user.Email))
{
return new ServiceResponse<int>
{
Success = false,
Message = "このユーザーは既に存在しています。”
};
}
CreatePasswordHash(password, out byte[] passwordHash, out byte[]
passwordSalt);
user.PasswordHash = passwordHash;
user.PasswordSalt = passwordSalt;
_context.Users.Add(user);
await _context.SaveChangesAsync();
return new ServiceResponse<int>
{ Data = user.Id, Message = "登録が成功しました!!" };
}
---
登録後に成功のメッセージを表⽰する - 2
• テスト
• chrome 開発ツール
• Register ボタンを押す
ときに Fetch/XHR を
観察
• success
Client → Register.razor
---
async Task HandleRegistration()
{
var result = await AuthService.Register(user);
message = result.Message;
if (result.Success)
errormessage = Result.message;
else
errormessage = String.Empty;
}
}
---
@code {
UserRegister user = new UserRegister();
string message = string.Empty;
string messageCssClass = string.Empty;
---
登録後に成功のメッセージを表⽰する - 3
• テスト
• chrome dev tool
• Register ボタンを押す
ときに Fetch/XHR を
⾒る
• 存在する email アドレ
ス → "このユーザーは
既に存在しています。”
• 存在しない email アド
レス → "登録が成功
しました!!"
Client → Register.razor
---
<div class="@messageCssClass">
<span>@message</span>
</div>
---
@code {
UserRegister user = new UserRegister();
string message = string.Empty;
string messageCssClass = string.Empty;
async Task HandleRegistration()
{
var result = await AuthService.Register(user);
message = result.Message;
if (result.Success)
messageCssClass = "text-success";
else
messageCssClass = "text-danger";
}
------
UserLogin モデルの追加
• Shared →
UserLogin.cs 作成
Shared → UserLogin.cs
---
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BlazorEcommerceApp.Shared
{
public class UserLogin
{
[Required]
public string Email { get; set; } = string.Empty;
[Required]
public string Password { get; set; } = string.Empty;
}
}
---
ログインページの追加 - 1
• Client → Pages →
Login.razor 作成
Client → Pages → Login.razor
---
@page "/login"
<PageTitle>Login</PageTitle>
<h3>ログイン</h3>
@code {
private UserLogin User = new UserLogin();
private async Task HandleLogin()
{
Console.WriteLine("ログインさせてください! :)" );
}
}
---
ログインページの追加 - 2
• Register.razor から
上の部分をコピペ
• DEMO
chrome dev tool
• Register ボタンを押す
ときに Console 観察
• 既存の email アドレス
→
Console.WriteLine(
"ログインさせてくださ
い! :)"
Client → Pages → Login.razor
---
<EditForm Model="user" OnValidSubmit="HandleLogin">
//ここだけHandleLoginに変更する
<DataAnnotationsValidator />
<div class="mb-3">
<label for="e-mail アドレス">Email</label>
<InputText id="email" @bind-Value="user.Email" class="form-control" />
<ValidationMessage For="@(() => user.Email)" />
</div>
<div class="mb-3">
<label for="パスワード">Password</label>
<InputText id="password" @bind-Value="user.Password" class="form-
control" type="password" />
<ValidationMessage For="@(() => user.Password)" />
</div>
// confirm password は削除
<button type="submit" class="btn btn-primary">Login</button> //Login に変更
</EditForm>
// message class は削除
<div class="text-danger">
<span>@errorMessage</span>
</div>
---
サーバーにログインする準備をする - 1
• Server →
appsettings.json
編集
Client → Pages → Login.razor
---
{
"ConnectionStrings": {
"DefaultConnection":---
---
},
//このセクションを追加
"AppSettings": {
"Token": "my top secret key"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
---
サーバーにログインする準備をする - 2
• Server → Services
→ AuthService
Server → Services → AuthService → IAuthService
---
namespace BlazorEcommerceApp.Server.Services.AuthService
{
public interface IAuthService
{
Task<ServiceResponse<int>> Register(User user, string
password);
Task<bool> UserExists(string email);
//ここを追加
Task<ServiceResponse<string>> Login(string email, string
password);
}
}
---
サーバーにログインする準備をする - 3
• Server → Services
→ AuthService
Server → Services → AuthService → AuthService.cs
---
//⾃動的にインプリされる
===
---
public async Task<ServiceResponse<string>>
Login(UserLogin request)
{
var response = new ServiceResponse<string> {
Data = "token";
};
return response;
---
---
サーバーにログインする準備をする - 4
• Server →
Controllers →
AuthContorller
Server → Controllers → AuthContorller.cs
---
//追加
[HttpPost("login")]
public async Task<ActionResult<ServiceResponse<string>>>
Login(UserLogin request)
{
var response = await
_authService.Login(request.Email, request.Password);
if (!response.Success)
{
return BadRequest(response);
}
return Ok(response);
}
---
ユーザーのパスワードを検証する - 1
• Server →
Controllers →
AuthContorller
Server → Services → AuthService → AuthService.cs
---
public async Task<ServiceResponse<string>> Login(string email, string password)
{
var response = new ServiceResponse<string>();
var user = await _context.Users
.FirstOrDefaultAsync(x => x.Email.ToLower().Equals(email.ToLower()));
if (user == null)
{
response.Success = false;
response.Message = "ユーザーが⾒つかりません!";
}
else if (!VerifyPasswordHash(password, user.PasswordHash, user.PasswordSalt))
{
response.Success = false;
response.Message = "パスワードが間違っています。";
}
else
{
response.Data = CreateToken(user);
}
return response;
}
---
ユーザーのパスワードを検証する - 2
• Server →
Controllers →
AuthContorller
Server → Services → AuthService → AuthService.cs
---
private bool VerifyPasswordHash(string password, byte[] passwordHash,
byte[] passwordSalt)
{
using (var hmac = new HMACSHA512(passwordSalt))
{
var computedHash =
hmac.ComputeHash
(System.Text.Encoding.UTF8.GetBytes(password));
return computedHash.SequenceEqual(passwordHash);
}
}
---
JSON Web Token の作成 - 1
• JSON Web Token
Server → Services → AuthService → AuthService.cs
---
//Login メソッド
public async Task<ServiceResponse<string>> Login(string email, string password)
{
var response = new ServiceResponse<string>();
var user = await _context.Users
.FirstOrDefaultAsync(x => x.Email.ToLower().Equals(email.ToLower()));
if (user == null)
{
response.Success = false;
response.Message = "ユーザーが⾒つかりません!";
}
else if (!VerifyPasswordHash(password, user.PasswordHash, user.PasswordSalt))
{
response.Success = false;
response.Message = "パスワードが間違っています。";
}
else
{
response.Data = CreateToken(user); //ここは appsettings.json を参照
}
return response;
}---
JSON Web Token の作成 - 2
• JSON Web Token
Server → Services → AuthService → AuthService.cs
---
public class AuthService : IAuthService
{
private readonly DataContext _context;
public AuthService(DataContext context,
IConfiguration configuration,
IHttpContextAccessor httpContextAccessor)
{
_context = context;
_configuration = configuration;
_httpContextAccessor = httpContextAccessor;
}
---
---
JSON Web Token の作成 - 3
• DEMO
• localhost:(port
number)/Swagger/
index.html
• Auth
• POST Register
• POST Login
Server → Services → AuthService → AuthService.cs
---
private string CreateToken(User user)
{
List<Claim> claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.Email),
};
var key = new SymmetricSecurityKey(System.Text.Encoding.UTF8
.GetBytes(_configuration.GetSection("AppSettings:Token").Value));
// appsettings.json 参照
var creds = new SigningCredentials(key,
SecurityAlgorithms.HmacSha512Signature);
var token = new JwtSecurityToken(
claims: claims,
expires: DateTime.Now.AddDays(1),
signingCredentials: creds);
var jwt = new JwtSecurityTokenHandler().WriteToken(token);
return jwt;
}
---
1. user 名違い
2. password 違い
実⾏してメッセージを確認
その上で正しい内容を⼊れて
Token を Response Bodyで確認
コピーして jwt.io でペースト
decode された内容が確認できる
クライアントでログインを実装する - 1
• Client → Services
→ AuthService →
IAuthService.cs
Client → Services → AuthService → IAuthService.cs
---
namespace BlazorEcommerceApp.Client.Services.AuthService
{
public interface IAuthService
{
---
Task<ServiceResponse<string>> Login(UserLogin request);
---
}
}
---
クライアントでログインを実装する - 2
• Client → Services
→ AuthService →
IAuthService.cs
Client → Services → AuthService → IAuthService.cs
---
namespace BlazorEcommerceApp.Client.Services.AuthService
{
public interface IAuthService
{
---
Task<ServiceResponse<string>> Login(UserLogin request);
---
}
}
---
クライアントでログインを実装する - 3
• Client → Services
→ AuthService →
AuthService.cs
Client → Services → AuthService → AuthService.cs
---
public class AuthService : IAuthService
//右クリックして Implement Interface で Login メソッドを⽣成
---
//Register メソッドの中⾝をコピーしてペーストして Login に、型を string に
修正
---
public async Task<ServiceResponse<string>> Login(UserLogin request)
{
var result = await
_http.PostAsJsonAsync("api/auth/login", request);
return await
result.Content.ReadFromJsonAsync
<ServiceResponse<string>>();
---
---
クライアントでログインを実装する - 4
• Login.razor に移動
Client → pages → Login.Razor
---
@page "/login"
@inject IAuthService AuthService
@inject ILocalStorageService LocalStorage
@inject NavigationManager NavigationManager
//上記を追加
---
//error message を追加
---
<div class="text-danger">
<span>@errorMessage</span>
</div>
---
@code {
---
---
クライアントでログインを実装する - 5
• Login.razor に移動
Client → pages → Login.Razor
//HandleLogin を書き換え
---
private async Task HandleLogin()
{
var result = await AuthService.Login(user);
if (result.Success)
{
errorMessage = string.Empty;
await LocalStorage.SetItemAsync("authToken", result.Data);
NavigationManager.NavigateTo();
}
else
{
errorMessage = result.Message;
}
}---
---
クライアントでログインを実装する - 5
• Login.razor に移動
Client → pages → Login.Razor
//HandleLogin を書き換え
---
private async Task HandleLogin()
{
var result = await AuthService.Login(user);
if (result.Success)
{
errorMessage = string.Empty;
await LocalStorage.SetItemAsync("authToken", result.Data);
NavigationManager.NavigateTo();
}
else
{
errorMessage = result.Message;
}
}
---
クライアントでログインを実装する - 6
• テスト 実⾏
• Chrome Dev Tool
• Fetch/XHR
• login ボタンで正式な
ユーザーでログインする
• header Payload で
パスワードを確認する
• Preview タブで token
を確認する
• localstorage に変更
して中⾝を確認する
• このトークンを再びコピー
して
• JWT.IO にペーストする
• 中⾝が確認できる
Shared → UserButton.razor
---
//この辺の中⾝を確認
---
<NotAuthorized>
<a
href="login?returnUrl=@NavigationManager.ToBaseRelativePath(NavigationM
anager.Uri)"
class="dropdown-item">Login</a>
<a href="register" class="dropdown-item">Register</a>
</NotAuthorized>
---
カスタム AuthenticationStateProvider の実装 - 1
• AuthenticationStateProvider とは︖
• 認証状態を取得するためにカスケード接続された認証状態コンポーネントによって使⽤される基礎的な
サービス
• ユーザーの認証の現在の状態を提供
• この情報を使って動作するコンポーネントのひとつに Authorized View がある
• 使うには︖
• Nuget Package Manager からインストールする必要あり
• パッケージ名︓ Microsoft.AspNetCore.Components.Authorization
カスタム AuthenticationStateProvider の実装 – 2
• Cient → wwwroot
→ _imports.razor
Client → wwwroot → _Imports.razor
---
@using Microsoft.AspNetCore.Components.Authorization;
---
カスタム AuthenticationStateProvider の実装 – 3
• Cient →
CustomAuthState
Provider.cs
Cient → CustomAuthStateProvider.cs
---
public class CustomAuthStateProvider :
AuthenticationStateProvider
{
public override async Task<AuthenticationState>
GetAuthenticationStateAsync()
{
string authToken = await
_localStorageService.
GetItemAsStringAsync("authToken");
---
using Microsoft.AspNetCore.Components.Authorization;
---
カスタム AuthenticationStateProvider の実装 – 4.1
• Cient →
CustomAuthState
Provider.cs
Cient → CustomAuthStateProvider.cs
---
public class CustomAuthStateProvider : AuthenticationStateProvider
{
private readonly ILocalStorageService _localStorageService;
private readonly HttpClient _http;
public CustomAuthStateProvider(ILocalStorageService localStorageService,
HttpClient http)
{
_localStorageService = localStorageService;
_http = http;
}
public override async Task<AuthenticationState>
GetAuthenticationStateAsync()
{
string authToken = await
_localStorageService.GetItemAsStringAsync("authToken");
var identity = new ClaimsIdentity();
_http.DefaultRequestHeaders.Authorization = null;
---
カスタム AuthenticationStateProvider の実装 – 4.2
• Cient →
CustomAuthState
Provider.cs
Cient → CustomAuthStateProvider.cs
---
---
if (!string.IsNullOrEmpty(authToken))
{
try
{
identity = new ClaimsIdentity(ParseClaimsFromJwt(authToken),
"jwt");
_http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer",
authToken.Replace("¥"", ""));
}
catch
{
await _localStorageService.RemoveItemAsync("authToken");
identity = new ClaimsIdentity();
}
}
var user = new ClaimsPrincipal(identity);
var state = new AuthenticationState(user);
NotifyAuthenticationStateChanged(Task.FromResult(state));
return state;
}
---
カスタム AuthenticationStateProvider の実装 – 5
• Cient →
CustomAuthState
Provider.cs
Cient → CustomAuthStateProvider.cs
---
private byte[] ParseBase64WithoutPadding(string base64)
{
switch (base64.Length % 4)
{
case 2: base64 += "=="; break;
case 3: base64 += "="; break;
}
return Convert.FromBase64String(base64);
}
===
private IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
{
var payload = jwt.Split('.')[1];
var jsonBytes = ParseBase64WithoutPadding(payload);
var keyValuePairs = JsonSerializer
.Deserialize<Dictionary<string, object>>(jsonBytes);
var claims = keyValuePairs.Select(kvp => new Claim(kvp.Key,
kvp.Value.ToString()));
return claims;
}
---
認証状態の公開 - 1
• Client →
Program.cs
Client → Program.cs
---
global using Microsoft.AspNetCore.Components.Authorization;
---
builder.Services.AddOptions();
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AuthenticationStateProvider,
CustomAuthStateProvider>();
//を追加
---
認証状態の公開 - 2
• Client →
Program.cs
Client → App.razor
---
<CascadingAuthenticationState>
---
</CascadingAuthenticationState>
//これで囲む
---
認証状態の公開 - 3
• Client →
App.razor
Client → App.razor
---
//これで囲む
<CascadingAuthenticationState>
---
</CascadingAuthenticationState>
---
//Authorized Viewに置き換える
<AuthorizeRouteView RouteData="@routeData"
DefaultLayout="@typeof(ShopLayout)">
<NotAuthorized>
<h3>Whoops! You're not allowed to see this page.</h3>
<h5>Please <a href="login">login</a> or <a
href="register">register</a> for a new account.</h5>
</NotAuthorized>
</AuthorizeRouteView>
---
AuthorizedView コンポーネントでログアウトオプションを構築 - 1
• Client → Pages
→ Login.Razor
Client → Pages → Login.Razor
---
private async Task HandleLogin()
{
---
await
AuthenticationStateProvider.GetAuthenticationStateAsync();
---
@inject AuthenticationStateProvider
AuthenticationStateProvider
---
AuthorizedView コンポーネントでログアウトオプションを構築 - 2
• Client → Shared →
UserButton.razor
Client → Shared → UserButton.razor
---
@inject ILocalStorageService LocalStorage
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject NavigationManager NavigationManager
//を追加
---
private async Task Logout()
{
await LocalStorage.RemoveItemAsync("authToken");
await
AuthenticationStateProvider.GetAuthenticationStateAsync();
NavigationManager.NavigateTo("");
}
---
AuthorizedView コンポーネントでログアウトオプションを構築 - 2
• Client → Shared →
UserButton.razor
• テスト
chrome Dev Tools
Application タブ
LocalStorage ペイン
Client → Shared → UserButton.razor
---
<div class="dropdown-menu dropdown-menu-right @UserMenuCssClass">
<AuthorizeView>
<Authorized>
<a href="profile" class="dropdown-item">Profile</a>
<a href="orders" class="dropdown-item">Orders</a>
<hr />
<AdminMenu />
<button class="dropdown-item"
@onclick="Logout">Logout</button>
</Authorized>
<NotAuthorized>
<a href="login" class="dropdown-item">Login</a>
<a href="register" class="dropdown-item">Register</a>
</NotAuthorized>
</AuthorizeView>
</div>
---
ログインに戻り先 URL を追加する - 1
• Microsoft.AspNet
Core.WebUtilities
Client → Shared → UserButton.razor
---
<div class="dropdown-menu dropdown-menu-right @UserMenuCssClass">
<AuthorizeView>
<Authorized>
<a href="profile" class="dropdown-item">Profile</a>
<a href="orders" class="dropdown-item">Orders</a>
<hr />
<AdminMenu />
<button class="dropdown-item"
@onclick="Logout">Logout</button>
</Authorized>
<NotAuthorized>
<a
href="login?returnUrl=@NavigationManager.
ToBaseRelativePath(NavigationManager.Uri)"
class="dropdown-item">Login</a>
<a href="register" class="dropdown-item">Register</a>
</NotAuthorized>
</AuthorizeView>
</div>
---
ログインに戻り先 URL を追加する - 2
• Client → Pages →
Login.razor
Client → Pages → Login.razor
---
@code {
private UserLogin user = new UserLogin();
private string errorMessage = string.Empty;
//追加
private string returnUrl = string.Empty;
//追加
protected override void OnInitialized()
{
var uri = NavigationManager.
ToAbsoluteUri(NavigationManager.Uri);
if (QueryHelpers.ParseQuery(uri.Query).
TryGetValue("returnUrl", out var url))
{
returnUrl = url;
}
}
---
ログインに戻り先 URL を追加する - 3
• Client → Pages →
Login.razor
Client → Pages → Login.razor
---
private async Task HandleLogin()
{
var result = await AuthService.Login(user);
if (result.Success)
{
errorMessage = string.Empty;
await LocalStorage.SetItemAsync("authToken", result.Data);
await AuthenticationStateProvider.
GetAuthenticationStateAsync();
//returnURL を追加
NavigationManager.NavigateTo(returnUrl);
}
else
{
errorMessage = result.Message;
}
}
---
ユーザープロファイルページを作成する - 1
• Client → Pages →
Profile.razor 作成
Client → Pages → Profile.razor
---
@page "/profile"
<AuthorizeView>
<h3>こんにちは!あなたは <i>@context.User.Identity.Name</i>.</h3>
</AuthorizeView>
@code {
}
---
ユーザープロファイルページを作成する - 2
• Client → Shared →
UserButton.razor
Client → Shared → UserButton.razor
---
<div class="dropdown">
<button @onclick="ToggleUserMenu"
@onfocusout="HideUserMenu"
class="btn btn-secondary dropdown-toggle user-button">
<i class="oi oi-person"></i>
</button>
<div class="dropdown-menu dropdown-menu-right @UserMenuCssClass">
<AuthorizeView>
<Authorized>
//Profileに変更
<a href="profile" class="dropdown-item">Profile</a>
<a href="orders" class="dropdown-item">Orders</a>
<hr />
<AdminMenu />
<button class="dropdown-item" @onclick="Logout">Logout</button>
</Authorized>
<NotAuthorized>
<a
href="login?returnUrl=@NavigationManager.
ToBaseRelativePath(NavigationManager.Uri)"
class="dropdown-item">Login</a>
<a href="register" class="dropdown-item">Register</a>
</NotAuthorized>
</AuthorizeView>
</div>
</div>
---
クライアントの [Authorize] 属性の活⽤
• Client → wwwroot
→ _Imports.razor
• テスト
Chrome Dev Tools
Application タブ
LocalStorage ペイン
Client → Shared → UserButton.razor
---
//追加
---
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Authorization
---
===
Client → Pages → Profile.razor
===
//追加
@attribute [Authorize]
===
Client → wwwroot → App.razor
===
//追加
---
<AuthorizeRouteView RouteData="@routeData"
DefaultLayout="@typeof(ShopLayout)">
<NotAuthorized>
<h3>Whoops! You're not allowed to see this page.</h3>
<h5>Please <a href="login">login</a> or <a
href="register">register</a> for a new account.</h5>
</NotAuthorized>
</AuthorizeRouteView>
---
---
UserChangePassword モデルを追加する
• Shared →
UserChange
Password.cs
Shared → UserChangePassword.cs
---
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BlazorEcommerceApp.Shared
{
public class UserChangePassword
{
[Required, StringLength(100, MinimumLength = 6)]
public string Password { get; set; } = string.Empty;
[Compare("Password", ErrorMessage = "The passwords do not
match.")]
public string ConfirmPassword { get; set; } = string.Empty;
}
}
---
サーバーのパスワードを変更する - 1
• Server →
Services →
AuthService →
IAuthService.cs
Server → Services → AuthService → IAuthService.cs
---
namespace BlazorEcommerceApp.Client.Services.AuthService
{
public interface IAuthService
{
//追加
---
Task<ServiceResponse<string>> Login(UserLogin request);
Task<ServiceResponse<bool>> ChangePassword(UserChangePassword
request);
---
}
}---
サーバーのパスワードを変更する - 2
• Server →
Services →
AuthService →
AuthService.cs
Server → Services → AuthService → AuthService.cs
//インターフェイスから ChangePassword メソッドを⾃動⽣成
---
public async Task<ServiceResponse<bool>> ChangePassword(int userId, string newPassword)
{
var user = await _context.Users.FindAsync(userId);
if (user == null)
{
return new ServiceResponse<bool>
{
Success = false,
Message = "User not found."
};
}
CreatePasswordHash(newPassword, out byte[] passwordHash, out byte[]
passwordSalt);
user.PasswordHash = passwordHash;
user.PasswordSalt = passwordSalt;
await _context.SaveChangesAsync();
return new ServiceResponse<bool>
{ Data = true, Message = "Password has been changed." };
}
---
認証⽤ミドルウェアの追加
• Server →
Program.cs
• サービスパイプラインに
追加
Server → Program.cs
---
//追加
---
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey =
new SymmetricSecurityKey(System.Text.Encoding.UTF8
.GetBytes(builder.Configuration.GetSection("AppSettings:Token").Value)),
ValidateIssuer = false,
ValidateAudience = false
};
});
---
app.UseAuthentication();
app.UseAuthorization();
---
AuthController でパスワード変更を実装する
• Server →
Controllers →
AuthController →
AuthController.cs
Server → Program.cs
---
//追加
[HttpPost("change-password"), Authorize]
public async Task<ActionResult<ServiceResponse<bool>>>
ChangePassword([FromBody] string newPassword)
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var response = await _authService.ChangePassword(int.Parse(userId),
newPassword);
if (!response.Success)
{
return BadRequest(response);
}
return Ok(response);
}
---
クライアント側でパスワード変更を実装する
• Client → Services
→ AuthService →
IAuthService.cs
• Client → Services
→ AuthService →
AuthService.cs
Client → Services → AuthService → IAuthService.cs
---
//追加
namespace BlazorEcommerceApp.Client.Services.AuthService
{
public interface IAuthService
{
---
Task<ServiceResponse<bool>> ChangePassword(UserChangePassword request);
---
}
}
===
Client → Services → AuthService → AuthService.cs
===
//Interface から ChangePassword メソッドを⾃動⽣成
---
public async Task<ServiceResponse<bool>> ChangePassword(UserChangePassword
request)
{
var result = await _http.PostAsJsonAsync("api/auth/change-password",
request.Password);
return await result.Content.ReadFromJsonAsync<ServiceResponse<bool>>();
}
---
プロファイルページでユーザーのパスワードを変更する - 1
• Client → Pages →
Profile.razor
Client → Pages → Profile.razor
---
@page "/profile"
//追加
@inject IAuthService AuthService
@attribute [Authorize]
---
@code {
//追加
UserChangePassword request = new UserChangePassword();
string message = string.Empty;
private async Task ChangePassword()
{
var result = await AuthService.ChangePassword(request);
message = result.Message;
}
}
---
プロファイルページでユーザーのパスワードを変更する - 2
• Client → Pages →
Profile.razor
• テスト
• chrome Dev Tools
Console タブを開く
• /profile ページ
• すでにログイン済み
• change password
で実際にパスワード変更
• ⼀度ログアウトして再度
ログイン
Client → Pages → Profile.razor
---
//追加
<AuthorizeView>
<h3>こんにちは!あなたは <i>@context.User.Identity.Name</i>.</h3>
</AuthorizeView>
<h5>送付先住所</h5>
<AddressForm />
<p></p>
<h5>パスワード変更</h5>
<EditForm Model="request" OnValidSubmit="ChangePassword">
<DataAnnotationsValidator></DataAnnotationsValidator>
<div class="mb-3">
<label for="password">New Password</label>
<InputText id="password" @bind-Value="request.Password" class="form-control" type="password" />
<ValidationMessage For="@(() => request.Password)" />
</div>
<div class="mb-3">
<label for="confirmPassword">Confirm New Password</label>
<InputText id="confirmPassword" @bind-Value="request.ConfirmPassword" class="form-control"
type="password" />
<ValidationMessage For="@(() => request.ConfirmPassword)" />
</div>
<button type="submit" class="btn btn-primary">Apply</button>
</EditForm>
@message
---
まとめ
まとめ
l 前回までの復習
l Blazor 概要
l 今回作成する Web アプリケーションの概要
l Blazor WebAssembly プロジェクト作成
l Web API コントローラー追加、モデル追加
l Entity Framework による Code First データベース作成
l 商品サービス、商品リスト、カテゴリーサービス等必要なサービス、CRUD 処理等の実装
l 検索サービスの追加と検索コンポーネントの実装、カートサービス、UI/UX の変更
l 認証・ユーザー登録、その他の機能の実装
リソース
l セッションでご紹介した EC アプリ .NET 5版ですが、参考にさせて戴きました。
l https://github.com/patrickgod/BlazorEcommercePreviewYT
Elastic x mabl 共同セミナー (7/29 15:00~16:00)
https://www.elastic.co/jp/virtual-events/elastic-mabl-webinar
デジタルカスタマーエクスペリエンスの向上
〜 Elastic と mabl で実現する、ユーザー視点の アプリケーション Observability 〜
https://devrel.tokyo/japan-2022/
Thank you for your attention!
Elastic APM によるアプリケーションの監視
今回のデモアプリのイメージ
Azure
SQL Database
Elastic Cloud
東⽇本リージョン
マスターノード x 1
データノード x 2
ML ノード x 1
https://f79...c67.japaneast
.azure.elastic-
cloud.com:9243/
全⽂検索クエリ
CRUD
検索・更新 UI
APM .NET Agent
Blazor
WebAssembly
Azure サブスクリプション
Visual
Studio
2022
Azure
App Service
Elastic APM
Endpoint に送信
Azure
Data Explorer
ASP.NET 6
Web API
AntDesign
// .NETアプリへの Nuget パッケージインストール
dotnet add Elastic.Apm.NetCoreAll
Install-Package -ProjectName BlazorApp.Server -Id Elastic.Apm.NetCoreAll
Elastic APM for ASP
.NET Core
https://www.elastic.co/jp/apm/
Configuration on .NET Core
https://www.elastic.co/guide/en/apm/agent/dotnet/current/configuration-on-asp-net-core.html
ASP
.NET Core Quick Start
https://www.elastic.co/guide/en/apm/agent/dotnet/current/setup-asp-net-core.html
// Program.cs へ
の
追加
---
using Elastic.Apm.NetCoreAll;
//Elastic APM 追加
app.UseAllElasticApm(builder.Configuration);
app.UseHttpsRedirection();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
---
// appsettings.json の
更新
---
{
"Logging": {
"LogLevel": {
//"Default": "Information",
//"Microsoft": "Warning",
//"Microsoft.Hosting.Lifetime": "Information"
"Default": "Warning",
"Elastic.Apm": "Debug"
}
} ,
"AllowedHosts": " * " ,
//Elastic ポ
ー
タ
ル
か
ら APM エ
ン
ド
ポ
イ
ン
ト
とSecret を
コ
ピ
ー
&
ペー
ス
ト
"ElasticApm": {
"ServerUrl":
"https://
7d39255475bg8e8e0j99fm870kj48v88.apm.
japaneast.azure.elastic-cloud.com",
"SecretToken": ”f6p81KJytBcGMK2JKS4",
"TransactionSampleRate": 1.0
}
}
Elastic Cloud → Kibana で APM モニタリング
https://cloud.elastic.co/home
その他の機能の実装と UI/UX の変更
AntDesign
• ⼈気 No.1 on
Awesome Blazor
• 企業向け製品のための
デザインシステム
• 効率的で楽しいワーク
エクスペリエンスを実現
https://antblazor.com/en-US/
Install-Package -ProjectName BlazorWASMApp.Client -Id AntDesign
AntDesign
• Components
• Image の使⽤⽅法を
参照
• Source Code 利⽤
可能
https://antblazor.com/en-US/components/image
まとめ
まとめ
l .NET 6 における Blazor Update
l ASP.NET Core Web API を構築
l Blazor WebAssembly でフロントエンドアプリを構築
l Elastic APM によるアプリケーションの監視
.NET MAUI Blazor App - モバイル、デスクトップ、
Web ハイブリッドアプリを開発
https://qiita.com/shosuz/items/4218af93343e5cc999ec
ASP.NET Core Blazor WebAssembly と Web API と Entity Framework
Core で SQL Server のデータを取得したり追加したり更新したり削除したりする
[.NET 6 版]
https://qiita.com/tamtamyarn/items/876a5cd4b9ec9cdc1044
Elastic リソース
• 公式ドキュメント
https://www.elastic.co/guide/index.html
• クラウドネイティブ アプリでの Elasticsearch
https://docs.microsoft.com/ja-jp/dotnet/architecture/cloud-
native/elastic-search-in-azure
• Azure での検索データ ストアの選択
https://docs.microsoft.com/ja-jp/azure/architecture/data-
guide/technology-choices/search-options
• Elastic APM Agent
https://www.elastic.co/guide/en/apm/agent/index.html
• APM
https://www.elastic.co/jp/apm/
• Configuration on .NET Core
https://www.elastic.co/guide/en/apm/agent/dotnet/current/co
nfiguration-on-asp-net-core.html
• ASP.NET Core Quick Start
https://www.elastic.co/guide/en/apm/agent/dotnet/current/set
up-asp-net-core.html
Thank you for your attention!

More Related Content

What's hot (20)

Ingressの概要とLoadBalancerとの比較
Ingressの概要とLoadBalancerとの比較Ingressの概要とLoadBalancerとの比較
Ingressの概要とLoadBalancerとの比較
Mei Nakamura
 
Dockerイメージ管理の内部構造
Dockerイメージ管理の内部構造Dockerイメージ管理の内部構造
Dockerイメージ管理の内部構造
Etsuji Nakai
 
「自分のとこでは動くけど…」を無くす devcontainer
「自分のとこでは動くけど…」を無くす devcontainer「自分のとこでは動くけど…」を無くす devcontainer
「自分のとこでは動くけど…」を無くす devcontainer
Yuta Matsumura
 
実環境にTerraform導入したら驚いた
実環境にTerraform導入したら驚いた実環境にTerraform導入したら驚いた
実環境にTerraform導入したら驚いた
Akihiro Kuwano
 
アジャイルにモデリングは必要か
アジャイルにモデリングは必要かアジャイルにモデリングは必要か
アジャイルにモデリングは必要か
Hiromasa Oka
 
マルチテナント化で知っておきたいデータベースのこと
マルチテナント化で知っておきたいデータベースのことマルチテナント化で知っておきたいデータベースのこと
マルチテナント化で知っておきたいデータベースのこと
Amazon Web Services Japan
 
CloudNativeな決済サービスの開発と2年間の歩み #sf_A4
CloudNativeな決済サービスの開発と2年間の歩み #sf_A4CloudNativeな決済サービスの開発と2年間の歩み #sf_A4
CloudNativeな決済サービスの開発と2年間の歩み #sf_A4
Junya Suzuki
 
ビッグデータ処理データベースの全体像と使い分け
ビッグデータ処理データベースの全体像と使い分けビッグデータ処理データベースの全体像と使い分け
ビッグデータ処理データベースの全体像と使い分け
Recruit Technologies
 
Rdraモデリングをしよう
RdraモデリングをしようRdraモデリングをしよう
Rdraモデリングをしよう
Zenji Kanzaki
 
Aws Dev Day2021 「ドメイン駆動設計のマイクロサービスへの活用とデベロッパーに求められるスキル」参考資料(松岡パート)
Aws Dev Day2021 「ドメイン駆動設計のマイクロサービスへの活用とデベロッパーに求められるスキル」参考資料(松岡パート)Aws Dev Day2021 「ドメイン駆動設計のマイクロサービスへの活用とデベロッパーに求められるスキル」参考資料(松岡パート)
Aws Dev Day2021 「ドメイン駆動設計のマイクロサービスへの活用とデベロッパーに求められるスキル」参考資料(松岡パート)
Koichiro Matsuoka
 
人生がときめくAPIテスト自動化 with Karate
人生がときめくAPIテスト自動化 with Karate人生がときめくAPIテスト自動化 with Karate
人生がときめくAPIテスト自動化 with Karate
Takanori Suzuki
 
91APP: 從 "零" 開始的 DevOps
91APP: 從 "零" 開始的 DevOps91APP: 從 "零" 開始的 DevOps
91APP: 從 "零" 開始的 DevOps
Andrew Wu
 
Azureを頑張る理由と頑張り方(Cloud Skills Challenge 2022 winter 発表資料)
Azureを頑張る理由と頑張り方(Cloud Skills Challenge 2022 winter 発表資料)Azureを頑張る理由と頑張り方(Cloud Skills Challenge 2022 winter 発表資料)
Azureを頑張る理由と頑張り方(Cloud Skills Challenge 2022 winter 発表資料)
NTT DATA Technology & Innovation
 
AWS Black Belt Online Seminar 2016 AWS CloudFormation
AWS Black Belt Online Seminar 2016 AWS CloudFormationAWS Black Belt Online Seminar 2016 AWS CloudFormation
AWS Black Belt Online Seminar 2016 AWS CloudFormation
Amazon Web Services Japan
 
Dockerセキュリティ: 今すぐ役に立つテクニックから,次世代技術まで
 Dockerセキュリティ: 今すぐ役に立つテクニックから,次世代技術まで Dockerセキュリティ: 今すぐ役に立つテクニックから,次世代技術まで
Dockerセキュリティ: 今すぐ役に立つテクニックから,次世代技術まで
Akihiro Suda
 
Amazon Aurora Deep Dive (db tech showcase 2016)
Amazon Aurora Deep Dive (db tech showcase 2016)Amazon Aurora Deep Dive (db tech showcase 2016)
Amazon Aurora Deep Dive (db tech showcase 2016)
Amazon Web Services Japan
 
Redis勉強会資料(2015/06 update)
Redis勉強会資料(2015/06 update)Redis勉強会資料(2015/06 update)
Redis勉強会資料(2015/06 update)
Yuji Otani
 
データ分析基盤、どう作る?システム設計のポイント、教えます - Developers.IO 2019 (20191101)
データ分析基盤、どう作る?システム設計のポイント、教えます - Developers.IO 2019 (20191101)データ分析基盤、どう作る?システム設計のポイント、教えます - Developers.IO 2019 (20191101)
データ分析基盤、どう作る?システム設計のポイント、教えます - Developers.IO 2019 (20191101)
Yosuke Katsuki
 
DynamoDBを導入した話
DynamoDBを導入した話DynamoDBを導入した話
DynamoDBを導入した話
dcubeio
 
Docker入門-基礎編 いまから始めるDocker管理【2nd Edition】
Docker入門-基礎編 いまから始めるDocker管理【2nd Edition】Docker入門-基礎編 いまから始めるDocker管理【2nd Edition】
Docker入門-基礎編 いまから始めるDocker管理【2nd Edition】
Masahito Zembutsu
 
Ingressの概要とLoadBalancerとの比較
Ingressの概要とLoadBalancerとの比較Ingressの概要とLoadBalancerとの比較
Ingressの概要とLoadBalancerとの比較
Mei Nakamura
 
Dockerイメージ管理の内部構造
Dockerイメージ管理の内部構造Dockerイメージ管理の内部構造
Dockerイメージ管理の内部構造
Etsuji Nakai
 
「自分のとこでは動くけど…」を無くす devcontainer
「自分のとこでは動くけど…」を無くす devcontainer「自分のとこでは動くけど…」を無くす devcontainer
「自分のとこでは動くけど…」を無くす devcontainer
Yuta Matsumura
 
実環境にTerraform導入したら驚いた
実環境にTerraform導入したら驚いた実環境にTerraform導入したら驚いた
実環境にTerraform導入したら驚いた
Akihiro Kuwano
 
アジャイルにモデリングは必要か
アジャイルにモデリングは必要かアジャイルにモデリングは必要か
アジャイルにモデリングは必要か
Hiromasa Oka
 
マルチテナント化で知っておきたいデータベースのこと
マルチテナント化で知っておきたいデータベースのことマルチテナント化で知っておきたいデータベースのこと
マルチテナント化で知っておきたいデータベースのこと
Amazon Web Services Japan
 
CloudNativeな決済サービスの開発と2年間の歩み #sf_A4
CloudNativeな決済サービスの開発と2年間の歩み #sf_A4CloudNativeな決済サービスの開発と2年間の歩み #sf_A4
CloudNativeな決済サービスの開発と2年間の歩み #sf_A4
Junya Suzuki
 
ビッグデータ処理データベースの全体像と使い分け
ビッグデータ処理データベースの全体像と使い分けビッグデータ処理データベースの全体像と使い分け
ビッグデータ処理データベースの全体像と使い分け
Recruit Technologies
 
Rdraモデリングをしよう
RdraモデリングをしようRdraモデリングをしよう
Rdraモデリングをしよう
Zenji Kanzaki
 
Aws Dev Day2021 「ドメイン駆動設計のマイクロサービスへの活用とデベロッパーに求められるスキル」参考資料(松岡パート)
Aws Dev Day2021 「ドメイン駆動設計のマイクロサービスへの活用とデベロッパーに求められるスキル」参考資料(松岡パート)Aws Dev Day2021 「ドメイン駆動設計のマイクロサービスへの活用とデベロッパーに求められるスキル」参考資料(松岡パート)
Aws Dev Day2021 「ドメイン駆動設計のマイクロサービスへの活用とデベロッパーに求められるスキル」参考資料(松岡パート)
Koichiro Matsuoka
 
人生がときめくAPIテスト自動化 with Karate
人生がときめくAPIテスト自動化 with Karate人生がときめくAPIテスト自動化 with Karate
人生がときめくAPIテスト自動化 with Karate
Takanori Suzuki
 
91APP: 從 "零" 開始的 DevOps
91APP: 從 "零" 開始的 DevOps91APP: 從 "零" 開始的 DevOps
91APP: 從 "零" 開始的 DevOps
Andrew Wu
 
Azureを頑張る理由と頑張り方(Cloud Skills Challenge 2022 winter 発表資料)
Azureを頑張る理由と頑張り方(Cloud Skills Challenge 2022 winter 発表資料)Azureを頑張る理由と頑張り方(Cloud Skills Challenge 2022 winter 発表資料)
Azureを頑張る理由と頑張り方(Cloud Skills Challenge 2022 winter 発表資料)
NTT DATA Technology & Innovation
 
AWS Black Belt Online Seminar 2016 AWS CloudFormation
AWS Black Belt Online Seminar 2016 AWS CloudFormationAWS Black Belt Online Seminar 2016 AWS CloudFormation
AWS Black Belt Online Seminar 2016 AWS CloudFormation
Amazon Web Services Japan
 
Dockerセキュリティ: 今すぐ役に立つテクニックから,次世代技術まで
 Dockerセキュリティ: 今すぐ役に立つテクニックから,次世代技術まで Dockerセキュリティ: 今すぐ役に立つテクニックから,次世代技術まで
Dockerセキュリティ: 今すぐ役に立つテクニックから,次世代技術まで
Akihiro Suda
 
Amazon Aurora Deep Dive (db tech showcase 2016)
Amazon Aurora Deep Dive (db tech showcase 2016)Amazon Aurora Deep Dive (db tech showcase 2016)
Amazon Aurora Deep Dive (db tech showcase 2016)
Amazon Web Services Japan
 
Redis勉強会資料(2015/06 update)
Redis勉強会資料(2015/06 update)Redis勉強会資料(2015/06 update)
Redis勉強会資料(2015/06 update)
Yuji Otani
 
データ分析基盤、どう作る?システム設計のポイント、教えます - Developers.IO 2019 (20191101)
データ分析基盤、どう作る?システム設計のポイント、教えます - Developers.IO 2019 (20191101)データ分析基盤、どう作る?システム設計のポイント、教えます - Developers.IO 2019 (20191101)
データ分析基盤、どう作る?システム設計のポイント、教えます - Developers.IO 2019 (20191101)
Yosuke Katsuki
 
DynamoDBを導入した話
DynamoDBを導入した話DynamoDBを導入した話
DynamoDBを導入した話
dcubeio
 
Docker入門-基礎編 いまから始めるDocker管理【2nd Edition】
Docker入門-基礎編 いまから始めるDocker管理【2nd Edition】Docker入門-基礎編 いまから始めるDocker管理【2nd Edition】
Docker入門-基礎編 いまから始めるDocker管理【2nd Edition】
Masahito Zembutsu
 

Similar to Application development with c#, .net 6, blazor web assembly, asp.net web api, azure, part 4.pdf (20)

Application development with c#, .net 6, blazor web assembly, asp.net web api...
Application development with c#, .net 6, blazor web assembly, asp.net web api...Application development with c#, .net 6, blazor web assembly, asp.net web api...
Application development with c#, .net 6, blazor web assembly, asp.net web api...
Shotaro Suzuki
 
Application development with c#, .net 6, blazor web assembly, asp.net web api...
Application development with c#, .net 6, blazor web assembly, asp.net web api...Application development with c#, .net 6, blazor web assembly, asp.net web api...
Application development with c#, .net 6, blazor web assembly, asp.net web api...
Shotaro Suzuki
 
Application development with c#, .net 6, blazor web assembly, asp.net web api...
Application development with c#, .net 6, blazor web assembly, asp.net web api...Application development with c#, .net 6, blazor web assembly, asp.net web api...
Application development with c#, .net 6, blazor web assembly, asp.net web api...
Shotaro Suzuki
 
Building simple-app-using-.net 6 asp.net core web api-blazor web assembly-ela...
Building simple-app-using-.net 6 asp.net core web api-blazor web assembly-ela...Building simple-app-using-.net 6 asp.net core web api-blazor web assembly-ela...
Building simple-app-using-.net 6 asp.net core web api-blazor web assembly-ela...
Shotaro Suzuki
 
New Features of DotNet 6 Blazor WASM
New Features of DotNet 6 Blazor WASMNew Features of DotNet 6 Blazor WASM
New Features of DotNet 6 Blazor WASM
Shotaro Suzuki
 
.NET 6 と Blazor で作るクロスプラットフォームアプリ概要
.NET 6 と Blazor で作るクロスプラットフォームアプリ概要.NET 6 と Blazor で作るクロスプラットフォームアプリ概要
.NET 6 と Blazor で作るクロスプラットフォームアプリ概要
Akira Inoue
 
Static Web AppsとBlazor WebAssemblyのすすめ
Static Web AppsとBlazor  WebAssemblyのすすめStatic Web AppsとBlazor  WebAssemblyのすすめ
Static Web AppsとBlazor WebAssemblyのすすめ
TomomitsuKusaba
 
C# で SPA を作る BLAZOR WEBASSEMBLY の進化 - そしてその先へ
C# で SPA を作る BLAZOR WEBASSEMBLY の進化 - そしてその先へC# で SPA を作る BLAZOR WEBASSEMBLY の進化 - そしてその先へ
C# で SPA を作る BLAZOR WEBASSEMBLY の進化 - そしてその先へ
Jun-ichi Sakamoto
 
.NET Core 3.0 で Blazor を使用した​フルスタック C# Web アプリ​の構築
.NET Core 3.0 で Blazor を使用した​フルスタック C# Web アプリ​の構築.NET Core 3.0 で Blazor を使用した​フルスタック C# Web アプリ​の構築
.NET Core 3.0 で Blazor を使用した​フルスタック C# Web アプリ​の構築
Joni
 
モノづくりBlazor勉強会1回目資料_Blazorについて.pptx
モノづくりBlazor勉強会1回目資料_Blazorについて.pptxモノづくりBlazor勉強会1回目資料_Blazorについて.pptx
モノづくりBlazor勉強会1回目資料_Blazorについて.pptx
tkeproject
 
Blazor でアプリを作ろう! ~テンプレートインストールから最初のデバッグ実行まで~
Blazor でアプリを作ろう! ~テンプレートインストールから最初のデバッグ実行まで~Blazor でアプリを作ろう! ~テンプレートインストールから最初のデバッグ実行まで~
Blazor でアプリを作ろう! ~テンプレートインストールから最初のデバッグ実行まで~
m ishizaki
 
Interoperability of webassembly with javascript
Interoperability of webassembly with javascriptInteroperability of webassembly with javascript
Interoperability of webassembly with javascript
Takao Tetsuro
 
Developing .NET 6 Blazor WebAssemby apps with Radzen Blazor component library...
Developing .NET 6 Blazor WebAssemby apps with Radzen Blazor component library...Developing .NET 6 Blazor WebAssemby apps with Radzen Blazor component library...
Developing .NET 6 Blazor WebAssemby apps with Radzen Blazor component library...
Shotaro Suzuki
 
7 つの Blazor
7 つの Blazor7 つの Blazor
7 つの Blazor
m ishizaki
 
Blazor Web Assembly (C#) を触ってみた
Blazor Web Assembly (C#) を触ってみたBlazor Web Assembly (C#) を触ってみた
Blazor Web Assembly (C#) を触ってみた
Naito Oshima
 
Let's build a simple app with .net 6 asp.net core web api, react, and elasti...
Let's build a simple app with  .net 6 asp.net core web api, react, and elasti...Let's build a simple app with  .net 6 asp.net core web api, react, and elasti...
Let's build a simple app with .net 6 asp.net core web api, react, and elasti...
Shotaro Suzuki
 
デモで楽しむ Visual Studio 2022 & .NET 6 最新アップデート
デモで楽しむ Visual Studio 2022 & .NET 6 最新アップデートデモで楽しむ Visual Studio 2022 & .NET 6 最新アップデート
デモで楽しむ Visual Studio 2022 & .NET 6 最新アップデート
Akira Inoue
 
Blazor WebAssembly と Windows Forms でのロジック共有例
Blazor WebAssembly と Windows Forms でのロジック共有例Blazor WebAssembly と Windows Forms でのロジック共有例
Blazor WebAssembly と Windows Forms でのロジック共有例
Koichi Ota
 
C# で Single Page Web アプリを 開発できる Blazor ― その魅力
C# で Single Page Web アプリを開発できる Blazor ― その魅力C# で Single Page Web アプリを開発できる Blazor ― その魅力
C# で Single Page Web アプリを 開発できる Blazor ― その魅力
Jun-ichi Sakamoto
 
【BS2】.NET 6 最新アップデート
【BS2】.NET 6 最新アップデート【BS2】.NET 6 最新アップデート
【BS2】.NET 6 最新アップデート
日本マイクロソフト株式会社
 
Application development with c#, .net 6, blazor web assembly, asp.net web api...
Application development with c#, .net 6, blazor web assembly, asp.net web api...Application development with c#, .net 6, blazor web assembly, asp.net web api...
Application development with c#, .net 6, blazor web assembly, asp.net web api...
Shotaro Suzuki
 
Application development with c#, .net 6, blazor web assembly, asp.net web api...
Application development with c#, .net 6, blazor web assembly, asp.net web api...Application development with c#, .net 6, blazor web assembly, asp.net web api...
Application development with c#, .net 6, blazor web assembly, asp.net web api...
Shotaro Suzuki
 
Application development with c#, .net 6, blazor web assembly, asp.net web api...
Application development with c#, .net 6, blazor web assembly, asp.net web api...Application development with c#, .net 6, blazor web assembly, asp.net web api...
Application development with c#, .net 6, blazor web assembly, asp.net web api...
Shotaro Suzuki
 
Building simple-app-using-.net 6 asp.net core web api-blazor web assembly-ela...
Building simple-app-using-.net 6 asp.net core web api-blazor web assembly-ela...Building simple-app-using-.net 6 asp.net core web api-blazor web assembly-ela...
Building simple-app-using-.net 6 asp.net core web api-blazor web assembly-ela...
Shotaro Suzuki
 
New Features of DotNet 6 Blazor WASM
New Features of DotNet 6 Blazor WASMNew Features of DotNet 6 Blazor WASM
New Features of DotNet 6 Blazor WASM
Shotaro Suzuki
 
.NET 6 と Blazor で作るクロスプラットフォームアプリ概要
.NET 6 と Blazor で作るクロスプラットフォームアプリ概要.NET 6 と Blazor で作るクロスプラットフォームアプリ概要
.NET 6 と Blazor で作るクロスプラットフォームアプリ概要
Akira Inoue
 
Static Web AppsとBlazor WebAssemblyのすすめ
Static Web AppsとBlazor  WebAssemblyのすすめStatic Web AppsとBlazor  WebAssemblyのすすめ
Static Web AppsとBlazor WebAssemblyのすすめ
TomomitsuKusaba
 
C# で SPA を作る BLAZOR WEBASSEMBLY の進化 - そしてその先へ
C# で SPA を作る BLAZOR WEBASSEMBLY の進化 - そしてその先へC# で SPA を作る BLAZOR WEBASSEMBLY の進化 - そしてその先へ
C# で SPA を作る BLAZOR WEBASSEMBLY の進化 - そしてその先へ
Jun-ichi Sakamoto
 
.NET Core 3.0 で Blazor を使用した​フルスタック C# Web アプリ​の構築
.NET Core 3.0 で Blazor を使用した​フルスタック C# Web アプリ​の構築.NET Core 3.0 で Blazor を使用した​フルスタック C# Web アプリ​の構築
.NET Core 3.0 で Blazor を使用した​フルスタック C# Web アプリ​の構築
Joni
 
モノづくりBlazor勉強会1回目資料_Blazorについて.pptx
モノづくりBlazor勉強会1回目資料_Blazorについて.pptxモノづくりBlazor勉強会1回目資料_Blazorについて.pptx
モノづくりBlazor勉強会1回目資料_Blazorについて.pptx
tkeproject
 
Blazor でアプリを作ろう! ~テンプレートインストールから最初のデバッグ実行まで~
Blazor でアプリを作ろう! ~テンプレートインストールから最初のデバッグ実行まで~Blazor でアプリを作ろう! ~テンプレートインストールから最初のデバッグ実行まで~
Blazor でアプリを作ろう! ~テンプレートインストールから最初のデバッグ実行まで~
m ishizaki
 
Interoperability of webassembly with javascript
Interoperability of webassembly with javascriptInteroperability of webassembly with javascript
Interoperability of webassembly with javascript
Takao Tetsuro
 
Developing .NET 6 Blazor WebAssemby apps with Radzen Blazor component library...
Developing .NET 6 Blazor WebAssemby apps with Radzen Blazor component library...Developing .NET 6 Blazor WebAssemby apps with Radzen Blazor component library...
Developing .NET 6 Blazor WebAssemby apps with Radzen Blazor component library...
Shotaro Suzuki
 
7 つの Blazor
7 つの Blazor7 つの Blazor
7 つの Blazor
m ishizaki
 
Blazor Web Assembly (C#) を触ってみた
Blazor Web Assembly (C#) を触ってみたBlazor Web Assembly (C#) を触ってみた
Blazor Web Assembly (C#) を触ってみた
Naito Oshima
 
Let's build a simple app with .net 6 asp.net core web api, react, and elasti...
Let's build a simple app with  .net 6 asp.net core web api, react, and elasti...Let's build a simple app with  .net 6 asp.net core web api, react, and elasti...
Let's build a simple app with .net 6 asp.net core web api, react, and elasti...
Shotaro Suzuki
 
デモで楽しむ Visual Studio 2022 & .NET 6 最新アップデート
デモで楽しむ Visual Studio 2022 & .NET 6 最新アップデートデモで楽しむ Visual Studio 2022 & .NET 6 最新アップデート
デモで楽しむ Visual Studio 2022 & .NET 6 最新アップデート
Akira Inoue
 
Blazor WebAssembly と Windows Forms でのロジック共有例
Blazor WebAssembly と Windows Forms でのロジック共有例Blazor WebAssembly と Windows Forms でのロジック共有例
Blazor WebAssembly と Windows Forms でのロジック共有例
Koichi Ota
 
C# で Single Page Web アプリを 開発できる Blazor ― その魅力
C# で Single Page Web アプリを開発できる Blazor ― その魅力C# で Single Page Web アプリを開発できる Blazor ― その魅力
C# で Single Page Web アプリを 開発できる Blazor ― その魅力
Jun-ichi Sakamoto
 
Ad

More from Shotaro Suzuki (20)

This is how our first offline technical event in three years was able to succ...
This is how our first offline technical event in three years was able to succ...This is how our first offline technical event in three years was able to succ...
This is how our first offline technical event in three years was able to succ...
Shotaro Suzuki
 
Introducing the new features of the Elastic 8.6 release.pdf
Introducing the new features of the Elastic 8.6 release.pdfIntroducing the new features of the Elastic 8.6 release.pdf
Introducing the new features of the Elastic 8.6 release.pdf
Shotaro Suzuki
 
NET MAUI for .NET 7 for iOS, Android app development
 NET MAUI for .NET 7 for iOS, Android app development  NET MAUI for .NET 7 for iOS, Android app development
NET MAUI for .NET 7 for iOS, Android app development
Shotaro Suzuki
 
What's New in the Elastic 8.5 Release
What's New in the Elastic 8.5 ReleaseWhat's New in the Elastic 8.5 Release
What's New in the Elastic 8.5 Release
Shotaro Suzuki
 
Centralized Observability for the Azure Ecosystem
Centralized Observability for the Azure EcosystemCentralized Observability for the Azure Ecosystem
Centralized Observability for the Azure Ecosystem
Shotaro Suzuki
 
What's New in the Elastic 8.4 Release
What's New in the Elastic 8.4 ReleaseWhat's New in the Elastic 8.4 Release
What's New in the Elastic 8.4 Release
Shotaro Suzuki
 
Power Apps x .NET ~ Transforming Business Applications with Fusion Development
Power Apps x .NET ~ Transforming Business Applications with Fusion DevelopmentPower Apps x .NET ~ Transforming Business Applications with Fusion Development
Power Apps x .NET ~ Transforming Business Applications with Fusion Development
Shotaro Suzuki
 
devreljapan2022evaadvoc-final.pdf
devreljapan2022evaadvoc-final.pdfdevreljapan2022evaadvoc-final.pdf
devreljapan2022evaadvoc-final.pdf
Shotaro Suzuki
 
elastic-mabl-co-webinar-20220729
elastic-mabl-co-webinar-20220729elastic-mabl-co-webinar-20220729
elastic-mabl-co-webinar-20220729
Shotaro Suzuki
 
Discover what's new in the Elastic 8.3 release - Find, monitor, and protect e...
Discover what's new in the Elastic 8.3 release - Find, monitor, and protect e...Discover what's new in the Elastic 8.3 release - Find, monitor, and protect e...
Discover what's new in the Elastic 8.3 release - Find, monitor, and protect e...
Shotaro Suzuki
 
Building a search experience with Elastic – Introducing Elastic's latest samp...
Building a search experience with Elastic – Introducing Elastic's latest samp...Building a search experience with Elastic – Introducing Elastic's latest samp...
Building a search experience with Elastic – Introducing Elastic's latest samp...
Shotaro Suzuki
 
Elastic x Microsoft Azure Integration Evolution - Integrated Monitoring for S...
Elastic x Microsoft Azure Integration Evolution - Integrated Monitoring for S...Elastic x Microsoft Azure Integration Evolution - Integrated Monitoring for S...
Elastic x Microsoft Azure Integration Evolution - Integrated Monitoring for S...
Shotaro Suzuki
 
Building 3D mobile apps using Power Apps Mixed Reality controls, Azure SQL Da...
Building 3D mobile apps using Power Apps Mixed Reality controls, Azure SQL Da...Building 3D mobile apps using Power Apps Mixed Reality controls, Azure SQL Da...
Building 3D mobile apps using Power Apps Mixed Reality controls, Azure SQL Da...
Shotaro Suzuki
 
What's New in the Elastic 8.2 Release - Seamless User Experience with Search -
What's New in the Elastic 8.2 Release - Seamless User Experience with Search -What's New in the Elastic 8.2 Release - Seamless User Experience with Search -
What's New in the Elastic 8.2 Release - Seamless User Experience with Search -
Shotaro Suzuki
 
Building Software Reliability through Distributed Tracing.pdf
Building Software Reliability through Distributed Tracing.pdfBuilding Software Reliability through Distributed Tracing.pdf
Building Software Reliability through Distributed Tracing.pdf
Shotaro Suzuki
 
Building a Flutter Development Environment with VSCode and Useful Extensions
Building a Flutter Development Environment with VSCode and Useful ExtensionsBuilding a Flutter Development Environment with VSCode and Useful Extensions
Building a Flutter Development Environment with VSCode and Useful Extensions
Shotaro Suzuki
 
Introducing Elastic 8.1 Release - More Integration, Faster Indexing Speed, Lo...
Introducing Elastic 8.1 Release - More Integration, Faster Indexing Speed, Lo...Introducing Elastic 8.1 Release - More Integration, Faster Indexing Speed, Lo...
Introducing Elastic 8.1 Release - More Integration, Faster Indexing Speed, Lo...
Shotaro Suzuki
 
Introducing the elastic 8.0 release a new era of speed, scale, relevance, and...
Introducing the elastic 8.0 release a new era of speed, scale, relevance, and...Introducing the elastic 8.0 release a new era of speed, scale, relevance, and...
Introducing the elastic 8.0 release a new era of speed, scale, relevance, and...
Shotaro Suzuki
 
Developers-Summit-2022_Improving-Digital-Customer-Experience-with-Enterprise_...
Developers-Summit-2022_Improving-Digital-Customer-Experience-with-Enterprise_...Developers-Summit-2022_Improving-Digital-Customer-Experience-with-Enterprise_...
Developers-Summit-2022_Improving-Digital-Customer-Experience-with-Enterprise_...
Shotaro Suzuki
 
Firebase, Firestore Extension for Elastic App Search Integration-20220216
Firebase, Firestore Extension for Elastic App Search Integration-20220216Firebase, Firestore Extension for Elastic App Search Integration-20220216
Firebase, Firestore Extension for Elastic App Search Integration-20220216
Shotaro Suzuki
 
This is how our first offline technical event in three years was able to succ...
This is how our first offline technical event in three years was able to succ...This is how our first offline technical event in three years was able to succ...
This is how our first offline technical event in three years was able to succ...
Shotaro Suzuki
 
Introducing the new features of the Elastic 8.6 release.pdf
Introducing the new features of the Elastic 8.6 release.pdfIntroducing the new features of the Elastic 8.6 release.pdf
Introducing the new features of the Elastic 8.6 release.pdf
Shotaro Suzuki
 
NET MAUI for .NET 7 for iOS, Android app development
 NET MAUI for .NET 7 for iOS, Android app development  NET MAUI for .NET 7 for iOS, Android app development
NET MAUI for .NET 7 for iOS, Android app development
Shotaro Suzuki
 
What's New in the Elastic 8.5 Release
What's New in the Elastic 8.5 ReleaseWhat's New in the Elastic 8.5 Release
What's New in the Elastic 8.5 Release
Shotaro Suzuki
 
Centralized Observability for the Azure Ecosystem
Centralized Observability for the Azure EcosystemCentralized Observability for the Azure Ecosystem
Centralized Observability for the Azure Ecosystem
Shotaro Suzuki
 
What's New in the Elastic 8.4 Release
What's New in the Elastic 8.4 ReleaseWhat's New in the Elastic 8.4 Release
What's New in the Elastic 8.4 Release
Shotaro Suzuki
 
Power Apps x .NET ~ Transforming Business Applications with Fusion Development
Power Apps x .NET ~ Transforming Business Applications with Fusion DevelopmentPower Apps x .NET ~ Transforming Business Applications with Fusion Development
Power Apps x .NET ~ Transforming Business Applications with Fusion Development
Shotaro Suzuki
 
devreljapan2022evaadvoc-final.pdf
devreljapan2022evaadvoc-final.pdfdevreljapan2022evaadvoc-final.pdf
devreljapan2022evaadvoc-final.pdf
Shotaro Suzuki
 
elastic-mabl-co-webinar-20220729
elastic-mabl-co-webinar-20220729elastic-mabl-co-webinar-20220729
elastic-mabl-co-webinar-20220729
Shotaro Suzuki
 
Discover what's new in the Elastic 8.3 release - Find, monitor, and protect e...
Discover what's new in the Elastic 8.3 release - Find, monitor, and protect e...Discover what's new in the Elastic 8.3 release - Find, monitor, and protect e...
Discover what's new in the Elastic 8.3 release - Find, monitor, and protect e...
Shotaro Suzuki
 
Building a search experience with Elastic – Introducing Elastic's latest samp...
Building a search experience with Elastic – Introducing Elastic's latest samp...Building a search experience with Elastic – Introducing Elastic's latest samp...
Building a search experience with Elastic – Introducing Elastic's latest samp...
Shotaro Suzuki
 
Elastic x Microsoft Azure Integration Evolution - Integrated Monitoring for S...
Elastic x Microsoft Azure Integration Evolution - Integrated Monitoring for S...Elastic x Microsoft Azure Integration Evolution - Integrated Monitoring for S...
Elastic x Microsoft Azure Integration Evolution - Integrated Monitoring for S...
Shotaro Suzuki
 
Building 3D mobile apps using Power Apps Mixed Reality controls, Azure SQL Da...
Building 3D mobile apps using Power Apps Mixed Reality controls, Azure SQL Da...Building 3D mobile apps using Power Apps Mixed Reality controls, Azure SQL Da...
Building 3D mobile apps using Power Apps Mixed Reality controls, Azure SQL Da...
Shotaro Suzuki
 
What's New in the Elastic 8.2 Release - Seamless User Experience with Search -
What's New in the Elastic 8.2 Release - Seamless User Experience with Search -What's New in the Elastic 8.2 Release - Seamless User Experience with Search -
What's New in the Elastic 8.2 Release - Seamless User Experience with Search -
Shotaro Suzuki
 
Building Software Reliability through Distributed Tracing.pdf
Building Software Reliability through Distributed Tracing.pdfBuilding Software Reliability through Distributed Tracing.pdf
Building Software Reliability through Distributed Tracing.pdf
Shotaro Suzuki
 
Building a Flutter Development Environment with VSCode and Useful Extensions
Building a Flutter Development Environment with VSCode and Useful ExtensionsBuilding a Flutter Development Environment with VSCode and Useful Extensions
Building a Flutter Development Environment with VSCode and Useful Extensions
Shotaro Suzuki
 
Introducing Elastic 8.1 Release - More Integration, Faster Indexing Speed, Lo...
Introducing Elastic 8.1 Release - More Integration, Faster Indexing Speed, Lo...Introducing Elastic 8.1 Release - More Integration, Faster Indexing Speed, Lo...
Introducing Elastic 8.1 Release - More Integration, Faster Indexing Speed, Lo...
Shotaro Suzuki
 
Introducing the elastic 8.0 release a new era of speed, scale, relevance, and...
Introducing the elastic 8.0 release a new era of speed, scale, relevance, and...Introducing the elastic 8.0 release a new era of speed, scale, relevance, and...
Introducing the elastic 8.0 release a new era of speed, scale, relevance, and...
Shotaro Suzuki
 
Developers-Summit-2022_Improving-Digital-Customer-Experience-with-Enterprise_...
Developers-Summit-2022_Improving-Digital-Customer-Experience-with-Enterprise_...Developers-Summit-2022_Improving-Digital-Customer-Experience-with-Enterprise_...
Developers-Summit-2022_Improving-Digital-Customer-Experience-with-Enterprise_...
Shotaro Suzuki
 
Firebase, Firestore Extension for Elastic App Search Integration-20220216
Firebase, Firestore Extension for Elastic App Search Integration-20220216Firebase, Firestore Extension for Elastic App Search Integration-20220216
Firebase, Firestore Extension for Elastic App Search Integration-20220216
Shotaro Suzuki
 
Ad

Application development with c#, .net 6, blazor web assembly, asp.net web api, azure, part 4.pdf

  • 1. C#, .NET 6, Blazor WebAssembly, ASP.NET Web API, Azure による アプリ開発 – その4 鈴⽊ 章太郎 Elastic テクニカルプロダクトマーケティングマネージャー/エバンジェリスト デジタル庁 省庁業務グループ ソリューションアーキテクト
  • 3. l 前回までの復習 l Blazor 概要 l 今回作成する Web アプリケーションの概要 l Blazor WebAssembly プロジェクト作成 l Web API コントローラー追加、モデル追加 l Entity Framework による Code First データベース作成 l 商品サービス、商品リスト、カテゴリーサービス等必要なサービス、 CRUD 処理等の実装 l 検索サービスの追加と検索コンポーネントの実装 l UI/UX の変更、カートサービス l 認証・ユーザー登録機能、その他の実装 (p.151-p.219) アジェンダ
  • 4. 今回の範囲 l 2⽉、3⽉、4⽉の復習 l 認証・ユーザー登録機能の実装、その他 (p.151-p.219) セッションでご紹介した EC アプリ .NET 5版ですが、参考にさせて戴きました。 https://github.com/patrickgod/PreviewYT
  • 6. Modern Web UI with .NET & Blazor Server WebAssembly Hybrid HTML、CSS、.NET、C#... JavaScript の代わりに Open Web 標準でアプリ開発 どこにでもホストできる
  • 7. MVC Razor Pages Blazor HTTP APIs SignalR Part of the ASP.NET Core family Web UI Services Worker gRPC SPA
  • 8. Blazor – .NET 5 まで Blazor Server Blazor WebAssembly DOM Blazor WebAssembly .NET Razor Components Blazor .NET Razor Components DOM SignalR ü DB アクセス含むサーバー機能へのフルアクセス ü ⾼速なスタートアップ ü コードがサーバーから離れない ü 古いブラウザとシンクライアントをサポート ü 永続的な接続が必要 ü UI の遅延が⾼い ü完全にクライアント側で実⾏ ü必要なサーバー コンポーネントなし ü静的サイトとしてホスト üオフラインで実⾏可能 ü⼤きなダウンロードサイズ üランタイムパフォーマンスの低下 Blazor Server (.NET 5) Blazor WebAssembly (.NET 5)
  • 9. Blazor – .NET 6 による強化 Blazor Server Blazor WebAssembly DOM Blazor WebAssembly .NET Razor Components Blazor .NET Razor Components DOM SignalR Blazor WebAssembly の事前 (AOT) コンパイル対応 Blazor WebAssembly アプリのダウンロードサイズの縮⼩ Error Boundaries Razor コンポーネント型の推論とジェネリック型の制約 動的コンポーネント プリレンダリング中の Blazor コンポーネント状態の永続性 Hot Reload, Native File Reference, 他多数 .NET 6
  • 10. Blazor Server と Blazor WebAssembly の 開発モデルの違い Blazor Server Blazor WebAssembly DOM Blazor WebAssembly .NET Razor Components Blazor .NET Razor Components DOM SignalR Blazor Server • 開発モデルは C/S 型に近い • DOM(ブラウザ UI)と Blazor ランタイム(仮想 DOM) がやりとりし UI 描画(差分更新) • 画⾯の⼊出⼒部分のみをリモートデスクトップのようにブラウザ 側に持ってきているとみなせる • SignalR(Web ソケット通信) • DB に直接アクセス可能 • Web アプリケーションを Client - Server 型に近いモデルで 開発可能 • Web サーバとの常時接続が必要 • サーバ側でリソース効率の⾼いアプリの作り⽅が必要 • Hot Reload Blazor WebAssembly • サンドボックス制限 • DB アクセス不可 → Native File Reference による ローカル DBアクセス • Web API を介して DB アクセス • 静的な Web サーバにホスト • アプリ全体がダウンロード(⼤きくなりがち) • DOM(ブラウザ UI)と Blazor ランタイム(仮想 DOM)がやりとりしUI 描画(差分更新)、ランタイム が Blazor アプリ(UI ロジック)とやりとりする • Hot Reload (デバッグなしで実⾏)
  • 11. Web Assembly(WASM) とは • Web ブラウザ上でバイナリコードを直接実⾏できる • 2019 年 12 ⽉ W3C 勧告、正式なウェブ標準に認定 • 様々な⾔語のバイナリコードを主要なブラウザのサンドボックス内で動作可能 • Web Assembly バイナリコードへのコンパイラなどのツールセットが必要 Edge Chrome Safari Firefox Web Assembly バイナリコード (W3C 標準技術) C++ WASM コンパイラ Rust WASM コンパイラ C WASM コンパイラ SQLite ソースコード(C) Rust ソースコード C++ ソースコード
  • 12. .NET 6 における Blazor WebAssembly 新機能 • 事前 (AOT) 実⾏コンパイル • カスタム要素 • ⼩規模なアプリサイズ • Native File Reference • Hot Reload • Component, .NET, HTML, CSS… …その他数⼗個の更新あり
  • 13. Blazor WebAssembly ⼩規模なアプリサイズ .NET 5 • Publish size: 1.7 MB .NET 6 • Publish size: 1.0 MB • ~40% size reduction
  • 14. Blazor WebAssembly のホスティング ASP.NET Blazor WebAssem bly APIs Globally distributed hosting Blazor WebAssem bly Serverless functions APIs App Services Azure Static Web Apps ASP.NET Globally distributed hosting Microservices Blazor WebAssembly APIs Blazor WebAssembly APIs
  • 15. Get started with Blazor • Go to https://blazor.net • Install the .NET SDK • .NET Conf 2021 https://www.dotnetconf.net/ • .NET Conf 2021 – videos/slides/demos https://github.com/dotnet-presentations/dotNETConf/tree/master/2021/MainEvent/Technical Visual Studio Visual Studio for Mac Visual Studio Code + C# extension
  • 17. ASP.NET Core Blazor プロジェクトの構造 https://docs.microsoft.com/ja-jp/aspnet/core/blazor/project-structure?view=aspnetcore-6.0 Blazor WebAssembly アプリの初期ファイルとディレクトリ構造 [Client] • Connected Service • Dependencies • Pages • Properties • Shared • wwwrooot • _imports.razor • App.razor • Program.cs [Server] • Connected Service • Dependencies • Controllers • Pages • Properties • appsettings.json • Program.cs [Shared] • Connected Service • Dependencies • WeatherForecast.cs
  • 18. ASP.NET Core Blazor のホスティング モデル https://docs.microsoft.com/ja-jp/aspnet/core/blazor/hosting-models?view=aspnetcore-6.0 • Blazor WebAssembly hosting model を使⽤すると、次のようになります。 • Blazor アプリ、その依存関係、.NET ランタイムが並⾏してブラウザーにダウンロードされます。 • アプリがブラウザー UI スレッド上で直接実⾏されます。 • 次の展開戦略がサポートされています。 • ASP.NET Core でのホストされた展開 • Blazor アプリは、ASP.NET Core アプリによって提供されます。 • "ホストされたデプロイ" により、 WebAssembly アプリが、Web サーバー上で実⾏されている ASP.NET Core アプリからブラウザーに提供されます。 • クライアント Blazor WebAssembly アプリは、サーバー アプリの他の静的な Web アセットと共に、サーバーアプリの /bin/Release/{TARGET FRAMEWORK}/publish/wwwroot フォルダーに発⾏されます。 • 2 つのアプリが⼀緒に展開されます。 ASP.NET Core アプリをホストできる Web サーバーが必要です。 ホストされている展開の場合、Visual Studio には WebAssembly アプリ プロジェクト テンプレートが含まれており (dotnet new コマンドを使⽤する場合は blazorwasm テンプレー ト)、 Hosted オプションが選択されています (dotnet new コマンドを使⽤する場合は -ho|--hosted)。 • スタンドアロン展開 • Blazor アプリは、Blazor アプリの提供に .NET が使⽤されていない静的ホスティング Web サーバーまたはサービス上に配置されます。 • "スタンドアロン デプロイ" により、 WebAssembly アプリが、クライアントによって直接要求される静的ファイルのセットとして提供されます。 任意の静 的ファイル サーバーで Blazor アプリを提供できます。 • スタンドアロンのデプロイアセットは、/bin/Release/{TARGET FRAMEWORK}/publish/wwwroot フォルダーに発⾏されます。 • Azure App Service • Blazor WebAssembly アプリは、Blazor 上でアプリをホストするために使⽤される Windows 上の Azure App Service にデプロイできます。 • スタンドアロンの Blazor WebAssembly アプリを Azure App Service for Linux にデプロイすることは、現在サポートされていません。 現時点で は、アプリをホストする Linux サーバー イメージは使⽤できません。 このシナリオを可能にするための取り組みが進⾏中です。 • Azure Static Web Apps • 詳細については、「Tutorial: Building a static web app with Blazor in Azure Static Web Apps」を参照してください。 • IIS
  • 19. EC デモアプリの画⾯遷移例 トップ 検索 Movies Books Video Games 選択 カート 決済・ログイン ユーザー登録
  • 20. EC Demo アプリの構成 1 Azure SQL Database Elastic Cloud 東⽇本リージョン マスターノード x 1 データノード x 2 ML ノード x 1 https://f79...c67.japaneast .azure.elastic- cloud.com:9243/ 全⽂検索クエリ 検索・更新 UI Azure サブスクリプション Azure App Service Elastic APM Endpoint に送信 Blazor Server APM .NET Agent Blazor WebAssembly CRUD Visual Studio 2022 for Mac Azure Data Studio
  • 21. EC Demo アプリの構成 2 Azure SQL Database Elastic Cloud 東⽇本リージョン マスターノード x 1 データノード x 2 ML ノード x 1 https://f79...c67.japaneast .azure.elastic- cloud.com:9243/ CRUD Azure サブスクリプション Visual Studio 2022 for Mac Azure App Service Elastic APM Endpoint に送信 Azure Data Studio ASP.NET 6 Web API Azure Static Web Apps Blazor WebAssembly 検索・更新 UI APM .NET Agent Blazor WebAssembly 全⽂検索クエリ
  • 22. ASP.NET Core Blazor のホスティング モデル https://docs.microsoft.com/ja-jp/aspnet/core/blazor/hosting-models?view=aspnetcore-6.0#blazor-webassembly ホスティング モデルの選択 Blazor サーバー Blazor WebAssembly 完全な .NET Core API の互換性 ✔ ❌ サーバー ソースへの直接アクセス ✔ ❌ ⼩さいペイロード サイズと ⾼速な初期読み込み時間 ✔ ❌ サーバー上でのアプリ コードの セキュリティ保護と⾮公開 ✔ ❌† ダウンロードしたアプリを オフラインで実⾏ ❌ ✔ 静的サイトのホスティング ❌ ✔ クライアントへの処理のオフロード ❌ ✔
  • 26. Product Model の追加 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace BlazorECommerceApp.Shared { public class Product { public int Id { get; set; } public string Title { get; set; }; public string Description { get; set; }; public string ImageUrl { get; set; }; public decimal Price { get; set; } } } --- @using BlazorECommerceApp.Shared ---
  • 28. ProductList.Razor の追加 1 <h3>ProductList</h3> --- @code { public static List<Product> Products = new List<Product> { new Product { Id = "1", Title = "The Hitchhiker's Guide to the Galaxy", ImageUrl = "https://upload.wikimedia.org/wikipedia/en/b/bd/H2G2_UK_front_cover.jpg", Description = "銀河ヒッチハイク・ガイド[注 1](HG2G、[1] HHGTTG、[2] H2G2、[3] tHGttGと表記することもある)は、ダグラス・アダムスが⽣み出したコメディ SFフランチャイズである。1978年にBBC Radio 4で放送されたラジオコメディが原作で、その後、舞台、⼩説、コミック、1981年のテレビシリーズ、1984年のテキストベー スのコンピュータゲーム、2005年の⻑編映画など、様々な形式で翻案されている。", Price. = 9.99m } new Product { Id = "2", Title = "Ready Player One", ImageUrl = "https://upload.wikimedia.org/wikipedia/en/a/a4/Ready_Player_One_cover.jpg", Description = “「レディ・プレイヤー・ワン」は2011年に発表されたSF⼩説で、アメリカ⼈作家アーネスト・クラインのデビュー作である。2045年のディストピアを舞台 に、主⼈公のウェイド・ワッツが世界規模のバーチャルリアリティゲームのイースターエッグを探し、その発⾒によってゲーム製作者の財産を相続することになるというス トーリーである。クラインは2010年6⽉、⼊札競争の末に本作の出版権をクラウン・パブリッシング・グループ(ランダムハウスの⼀部⾨)に売却した[1]。 本作は2011年8 ⽉16⽇に出版された[2]。同⽇にはオーディオブックも発売されており、ナレーションは、章のひとつで少し触れているウィル・ウィートンである[3][4]。 20 2012年には アメリカ図書館協会のヤングアダルト図書館サービス部⾨からアレックス賞を受賞し[5] 、2011年にはプロメテウス賞を 受賞した[6]。”, Price. = 7.99m } new Product { Id = "3", Title = "Nineteen Eighty-Four”, ImageUrl = "https://upload.wikimedia.org/wikipedia/commons/c/c3/1984first.jpg", Description = “ Nineteen Eighty-Four(1984)は、イギリスの作家ジョージ・オーウェルが書いたディストピア社会SF⼩説であり、教訓的な物語である。1949年6⽉8 ⽇にセッカー&ウォーバーグ社から出版され、オーウェルが⽣前に完成させた9冊⽬にして最後の著作となった。⺠主社会主義者であるオーウェルは、スターリン主義のロシ アとナチス・ドイツをモデルに、⼩説の中の全体主義政府を描いた[2][3][4]。 より広く、この⼩説では政治における真実と事実の役割と、それらが操られる⽅法を検証し ている。" , Price = 6.99m } }
  • 29. ProductList.Razor の追加 2 <h3>ProductList</h3> <ul class="list-unstyled"> @foreach (var product in ProductService.Products) { <li class="media my-3"> <div class="media-img-wrapper mr-2"> <a href="/product/@product.Id"> <img class="media-img" src="@product.ImageUrl" alt="@product.Title" /> </a> </div> <div class="media-body"> <a href="/product/@product.Id"> <h4 class="mb-0">@product.Title</h4> </a> <p>@product.Description</p> <h5 class="price"> @GetPriceText(product) </h5> </div> </li> } </ul> ---
  • 30. Index.Razor の変更 @page "/" <ProductList /> https://localhost:7226/#
  • 33. ProductController.cs の追加 1 [Route("api/[controller]")] [ApiController] public class ProductController : ControllerBase { private static List <Product> Products = new List <Product> { new Product { Id = "1", Title = "The Hitchhiker's Guide to the Galaxy", ImageUrl = "https://upload.wikimedia.org/wikipedia/en/b/bd/H2G2_UK_front_cover.jpg", Description = "銀河ヒッチハイク・ガイド[注 1](HG2G、[1] HHGTTG、[2] H2G2、[3] tHGttGと表記することもある)は、ダグラス・アダムスが ⽣み出したコメディSFフランチャイズである。1978年にBBC Radio 4で放送されたラジオコメディが原作で、その後、舞台、⼩説、コミック、1981年の テレビシリーズ、1984年のテキストベースのコンピュータゲーム、2005年の⻑編映画など、様々な形式で翻案されている。", Price. = 9.99m } new Product { Id = "2", Title = "Ready Player One", ImageUrl = "https://upload.wikimedia.org/wikipedia/en/a/a4/Ready_Player_One_cover.jpg", Description = “「レディ・プレイヤー・ワン」は2011年に発表されたSF⼩説で、アメリカ⼈作家アーネスト・クラインのデビュー作である。2045年の ディストピアを舞台に、主⼈公のウェイド・ワッツが世界規模のバーチャルリアリティゲームのイースターエッグを探し、その発⾒によってゲーム製作 者の財産を相続することになるというストーリーである。クラインは2010年6⽉、⼊札競争の末に本作の出版権をクラウン・パブリッシング・グループ (ランダムハウスの⼀部⾨)に売却した[1]。 本作は2011年8⽉16⽇に出版された[2]。同⽇にはオーディオブックも発売されており、ナレーションは、 章のひとつで少し触れているウィル・ウィートンである[3][4]。2012年にはアメリカ図書館協会のヤングアダルト図書館サービス部⾨からアレックス賞 を受賞し[5] 、2011年にはプロメテウス賞を受賞した[6]。”, Price. = 7.99m } new Product { Id = "3", Title = "Nineteen Eighty-Four”, ImageUrl = "https://upload.wikimedia.org/wikipedia/commons/c/c3/1984first.jpg", Description = “ Nineteen Eighty-Four(1984)は、イギリスの作家ジョージ・オーウェルが書いたディストピア社会SF⼩説であり、教訓的な物語 である。1949年6⽉8⽇にセッカー&ウォーバーグ社から出版され、オーウェルが⽣前に完成させた9冊⽬にして最後の著作となった。⺠主社会主義者で あるオーウェルは、スターリン主義のロシアとナチス・ドイツをモデルに、⼩説の中の全体主義政府を描いた[2][3][4]。より広く、この⼩説では政治 における真実と事実の役割と、それらが操られる⽅法を検証している。" , Price = 6.99m } } ---
  • 34. ProductController.cs の追加 2 --- [HttpGet] public async Task<ActionResult<<List<Product>>> GetProducts() { rerurn Ok(Product) var result = await _productService. GetProductsAsync(); return Ok(result); } https://localhost:7226/#
  • 35. ProductList.Razor の変更(クライアントからの呼び出し) --- @inject HttpClient Http <ul class="list-unstyled"> @foreach (var product in ProductService.Products) { <li class="media my-3"> <div class="media-img-wrapper mr-2"> <a href="/product/@product.Id"> <img class="media-img" src="@product.ImageUrl" alt="@product.Title" /> </a> </div> <div class="media-body"> <a href="/product/@product.Id"> <h4 class="mb-0">@product.Title</h4> </a> <p>@product.Description</p> <h5 class="price"> @GetPriceText(product) </h5> </div> </li> } </ul> --- code@ { private static List<Product> Products {get; set;} = new List<Product>(); protected override async TaskOnInitializedAsync() { Products = await Http.GetFromJsonAsync<List<Product>> ("api/product"); } }
  • 36. Entity Framework による Code First データベース作成
  • 37. Blazor アプリのデバッグその他の TIPS dotnet watch run public class xxx prop → snippets が出て予測してくれる
  • 38. swagger インストールその他 • https://localhost:(ポート番 号)/swagger/index.html --- // AddRazorPages の後 builer.Services.AddEndpointApiExploler(); builer.Services.AddSwaggerGen(); //var ap = buildder.Build();の後 app.UserSwaggerUI(); // app.UseHttpsRedirection();の前 app.UseSwagger(); // Swagger UI で Products の shema が表⽰されない場合 // Public Async Task を書き換え Task<Action> GetProduct() → Task<ActionResult<List<Product>>> GetProduct()
  • 39. .NET Core Entity Framework 6.0 インストール • Microsoft.EntityFrameworkCore • Microsoft.EntityFrameworkCore. Design • Microsoft.EntityFrameworkCore. SqlServer • Mac の場合は、唯⼀の選択肢︕ Windows の場合は、SQL Server Express Edition をインストールして使う ⼿もあり appsettings.json で ”ConnectionString” とうつと⾃動的に出てくる この⽂字列をコピペして修正すればOK ※ 注意点 EF で Code First で Database を⾃動⽣成した場合、巨⼤なインスタンスになっ ている(3⽇くらいで数千円レベル)。 instance のサイズだけはすぐに修正して⼩さいものBasic2TB等にする。 これなら⽉額数百円。
  • 40. Azure SQL Database 接続⽂字列追加 { "ConnectionStrings": { "DefaultConnection": "Server=tcp:xxx.database.windows.net,1433;Initial Catalog=BlazorECommerceApp;Persist Security Info=False;User ID=(UserID);Password=(Password); MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;" }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*" } dotnet ef migration add CreateInitial // Migrations フォルダーと Migration クラス作成 dotnet ef Update Database // Azure SQL データベースとテーブル作成
  • 41. Product Model の追加 • BlazorECommerceApp.Shared フォルダに、 Product クラスを作成 • Book.cs に右のコードを記載 using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using System.Text; using System.Threading.Tasks; namespace BlazorECommerceApp.Shared { public class Product { public int Id { get; set; } [Required] public string Title { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; public string ImageUrl { get; set; } = string.Empty; public Category? Category { get; set; } public int CategoryId { get; set; } public bool Featured { get; set; } = false; public List<ProductVariant> Variants { get; set; } = new List<ProductVariant>(); public bool Visible { get; set; } = true; public bool Deleted { get; set; } = false; [NotMapped] public bool Editing { get; set; } = false; [NotMapped] public bool IsNew { get; set; } = false; } }
  • 42. DataContext 作成 • Class を追加 • DataContext.class • Serverプロジェクト側の Program.cs 修正 • global using Microsoft.EntityFramework.Core を⼊ れておくと楽 namespace BlazorECommerceApp.Server.Data { public class DataContext : DbContext { // DataContext を作るのに ctor とタイプするとできる // 全体的に IntelliCode が補完 public DataContext(DbContextOptions<DataContext> options) : base(options) { } } } • Server プロジェクト側の Program.cs • DataContext.cs global using Microsoft.EntityFrameworkCore;
  • 43. Entity Framework を使った最初の DB Migration //最初に名前を決めておく dotnet ef migrations add CreateInitial //成功したら Migration フォルダを開いて内容を確認 //データベース作成 dotnet ef database update
  • 44. データのシード(2回⽬のマイグレーション) --- Protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Product>().HasData( ---<ここに new Product 3エントリをコピペ>--- ); } dotnet ef migrations add ProductSeeding dotnet ef database update
  • 45. (参考)旧 ProductController.cs [Route("api/[controller]")] [ApiController] public class ProductController : ControllerBase { private static List <Product> Products = new List <Product> { new Product { Id = "1", Title = "The Hitchhiker's Guide to the Galaxy", ImageUrl = "https://upload.wikimedia.org/wikipedia/en/b/bd/H2G2_UK_front_cover.jpg", Description = "銀河ヒッチハイク・ガイド[注 1](HG2G、[1] HHGTTG、[2] H2G2、[3] tHGttGと表記することもある)は、ダグラス・アダムスが ⽣み出したコメディSFフランチャイズである。1978年にBBC Radio 4で放送されたラジオコメディが原作で、その後、舞台、⼩説、コミック、1981年の テレビシリーズ、1984年のテキストベースのコンピュータゲーム、2005年の⻑編映画など、様々な形式で翻案されている。", Price. = 9.99m } new Product { Id = "2", Title = "Ready Player One", ImageUrl = "https://upload.wikimedia.org/wikipedia/en/a/a4/Ready_Player_One_cover.jpg", Description = “「レディ・プレイヤー・ワン」は2011年に発表されたSF⼩説で、アメリカ⼈作家アーネスト・クラインのデビュー作である。2045年の ディストピアを舞台に、主⼈公のウェイド・ワッツが世界規模のバーチャルリアリティゲームのイースターエッグを探し、その発⾒によってゲーム製作 者の財産を相続することになるというストーリーである。クラインは2010年6⽉、⼊札競争の末に本作の出版権をクラウン・パブリッシング・グループ (ランダムハウスの⼀部⾨)に売却した[1]。 本作は2011年8⽉16⽇に出版された[2]。同⽇にはオーディオブックも発売されており、ナレーションは、 章のひとつで少し触れているウィル・ウィートンである[3][4]。2012年にはアメリカ図書館協会のヤングアダルト図書館サービス部⾨からアレックス賞 を受賞し[5] 、2011年にはプロメテウス賞を受賞した[6]。”, Price. = 7.99m } new Product { Id = "3", Title = "Nineteen Eighty-Four”, ImageUrl = "https://upload.wikimedia.org/wikipedia/commons/c/c3/1984first.jpg", Description = “ Nineteen Eighty-Four(1984)は、イギリスの作家ジョージ・オーウェルが書いたディストピア社会SF⼩説であり、教訓的な物語 である。1949年6⽉8⽇にセッカー&ウォーバーグ社から出版され、オーウェルが⽣前に完成させた9冊⽬にして最後の著作となった。⺠主社会主義者で あるオーウェルは、スターリン主義のロシアとナチス・ドイツをモデルに、⼩説の中の全体主義政府を描いた[2][3][4]。より広く、この⼩説では政治 における真実と事実の役割と、それらが操られる⽅法を検証している。" , Price = 6.99m } } ---
  • 46. ProductController.cs 内のデータを削除 • Server Program.cs を開き global using Blazorxxx.Server.Data; を追加 • private readonly DataContext context; ⽣成されるので、これを修正 • しかしこれを⾃動的に実施したい //context → _context に変更 public ProductController(DataContext context) { _cotext = context; } • Server プロジェクト側の Program.cs global using Blazorxxx.Server.Data; ツール → オプションから テキストエディタ → C# → CodeStyle → Naming → Manage Naming Style Naming Style Title : _fieldName Capitalizatin : camel Case Name これを追加したらprivate or internal Style に追加 _fieldName、Suggestion を選択 エディタに戻って create field context を選択する
  • 47. [HttpGet] GetProduct() 変更 • ProductList • ProductController • DataContext • [HttpGet] GetProduct() 変更 var products = await _cotext.Products.ToListAsync(); return Ok(products)
  • 49. Blazor WebAssembly の追加・改修等 • ProductDetail.razor.css 追加 • ProductDetail.razor 編集 @page "/product/{id:int}" @inject IProductService ProductService @inject ICartService CartService
  • 51. Category を実装する using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using System.Text; using System.Threading.Tasks; namespace BlazorECommerceApp.Shared { public class Category { public int Id { get; set; } public string Name { get; set; } = string.Empty; public string Url { get; set; } = string.Empty; public bool Visible { get; set; } = true; public bool Deleted { get; set; } = false; [NotMapped] public bool Editing { get; set; } = false; [NotMapped] public bool IsNew { get; set; } = false; } }
  • 52. Category の Seeding と Migration(3回⽬) • • --- modelBuilder.Entity<Category>().HasData( new Category { Id = 1, Name = "Books", Url = "books" }, new Category { Id = 2, Name = "Movies", Url = "movies" }, new Category { Id = 3, Name = "Video Games", Url = "video-games" } ); --- • DataContext.cs
  • 53. Category サービスの Client 側 への実装 - 1 • • namespace Blazorxxxxxxxx.Client.Services.CategoryService { public class CategoryService : ICategoryService { private readonly HttpClient _http; public CategoryService(HttpClient http) { _http = http; } --- • CategoryServices.cs
  • 54. Category サービスの Client 側 への実装 - 2 • CategoryService • global using で⼀番上に追加 //builder.Services.AddScoped<IProductService,ProductSe rvice>();の下に追加 builder.Services.AddScoped<ICategoryService, CategoryService>(); //⼀番上に追加 global using Blazorxxxxxxxx.Client.Services.CategoryService; • Program.cs
  • 55. Category サービスの Client 側 への実装 - 3 • @using Blazorxxxxxxxx.Client.Services.ProductService • _Imports.razor
  • 56. iCategoryServices の実装 • namespace Blazorxxxxxxx.Client.Services.CategoryService { public interface ICategoryService { event Action OnChange; List<Category> Categories { get; set; } List<Category> AdminCategories { get; set; } Task GetCategories(); Task GetAdminCategories(); Task AddCategory(Category category); Task UpdateCategory(Category category); Task DeleteCategory(int categoryId); Category CreateNewCategory(); } } • iCategoryServices.cs
  • 57. NavMenu への Category の表⽰ • NavMenu.razor の編集 • @inject ICategoryService CategoryService を冒頭に追加 • @code の後半部分に追加 • NavMenuCssClass の追加 protected override async Task OnInitializedAsync() { await CategoryService.GetCategories(); } • NavMenu.razor • NavMenuCssClass <div class="@NavMenuCssClass" @onclick="ToggleNavMenu"> <nav class="flex-column"> <div class="nav-item px-3"> <NavLink class="nav-link" href="" Match="NavLinkMatch.All"> Home </NavLink> </div> @foreach (var category in CategoryService.Categories) { <div class="nav-item px-3"> <NavLink class="nav-link" href="@category.Url"> @category.Name </NavLink> </div> } </nav> </div>
  • 58. Server の Category サービスから Product を取得 - 1 • • Task<ServiceResponse<Product>> GetProduct(int productId); • iProductService.cs public async Task GetProducts(string? categoryUrl = null) { var result = categoryUrl == null ? await _http.GetFromJsonAsync<ServiceResponse <List<Product>>>("api/product/featured") : await _http.GetFromJsonAsync<ServiceResponse <List<Product>>>($"api/product/category/{categoryUrl}"); if (result != null && result.Data != null) Products = result.Data; CurrentPage = 1; PageCount = 0; if (Products.Count == 0) Message = "商品がみつかりません。"; ProductsChanged.Invoke(); } • ProductService.cs
  • 59. Server の Category サービスから Product を取得 - 2 • • • https://locahost:(ポート番 号)/swagger/index.html --- [HttpGet("category/{categoryUrl}")] public async Task<ActionResult<ServiceResponse <List<Product>>>> GetProductsByCategory(string categoryUrl) { var result = await _productService. GetProductsByCategory(categoryUrl); return Ok(result); } --- • ProductController.cs
  • 60. Client の Category サービスから Product を取得 - 1 • • Task GetProducts を実装追加 • event ProductChanged を追加 • public async Task GetProducts(string? categoryUrl = null) { var result = categoryUrl == null ? //この2⾏がポイント await _http.GetFromJsonAsync<ServiceResponse <List<Product>>>("api/product/featured") : await _http.GetFromJsonAsync<ServiceResponse <List<Product>>>($"api/product/category/{categoryUrl}"); if (result != null && result.Data != null) Products = result.Data; CurrentPage = 1; PageCount = 0; if (Products.Count == 0) Message = "商品がみつかりません。"; //ここもポイント ProductsChanged.Invoke(); } • iProductService.cs • iProductService.cs Task GetProducts(string? categoryUrl = null); event Action ProductsChanged; • iProductService.cs
  • 61. Client の Category サービスから Product を取得 - 2 • @page "/" @page "/search/{searchText}/{page:int}" @page "/{categoryUrl}" @inject IProductService ProductService <PageTitle>マイショップ</PageTitle> @if (SearchText == null && CategoryUrl == null) { <FeaturedProducts /> } else { <ProductList /> } @code { [Parameter] public string? CategoryUrl { get; set; } = null; [Parameter] public string? SearchText { get; set; } = null; [Parameter] public int Page { get; set; } = 1; protected override async Task OnParametersSetAsync() { if (SearchText != null) { await ProductService.SearchProducts(SearchText, Page); } else { await ProductService.GetProducts(CategoryUrl); } } } • Index.razor
  • 62. Client の Category サービスから Product を取得 - 3 • • • • --- @code { protected override void OnInitialized() { ProductService.ProductsChanged += StateHasChanged; } • ProductList.razor --- public void Dispose() { ProductService.ProductsChanged -= StateHasChanged; }
  • 63. Shared に ProductVariant.cs を追加 • • • • ProductVariant.cs --- namespace BlazorECommerceApp.Shared { public class ProductVariant { [JsonIgnore] public Product? Product { get; set; } public int ProductId { get; set; } public ProductType? ProductType { get; set; } public int ProductTypeId { get; set; } [Column(TypeName = "decimal(18,2)")] public decimal Price { get; set; } [Column(TypeName = "decimal(18,2)")] public decimal OriginalPrice { get; set; } public bool Visible { get; set; } = true; public bool Deleted { get; set; } = false; [NotMapped] public bool Editing { get; set; } = false; [NotMapped] public bool IsNew { get; set; } = false; } }
  • 64. Composite Primary Key の追加と Seeding の実施(4回⽬) • • • • • ProductTypes • ProductVariants --- modelBuilder.Entity<ProductVariant>().HasData( new ProductVariant { ProductId = 1, ProductTypeId = 2, Price = 9.99m, OriginalPrice = 19.99m }, new ProductVariant { ProductId = 1, ProductTypeId = 3, Price = 7.99m }, new ProductVariant { ProductId = 1, ProductTypeId = 4, Price = 19.99m, OriginalPrice = 29.99m }, new ProductVariant { ProductId = 2, ProductTypeId = 2, Price = 7.99m, OriginalPrice = 14.99m }, new ProductVariant { ProductId = 3, ProductTypeId = 2, Price = 6.99m }, new ProductVariant { ProductId = 4, ProductTypeId = 5, Price = 3.99m }, new ProductVariant { ProductId = 4, ProductTypeId = 6, Price = 9.99m }, new ProductVariant { ProductId = 4, ProductTypeId = 7, Price = 19.99m }, new ProductVariant { ProductId = 5, ProductTypeId = 5, Price = 3.99m, }, new ProductVariant { ProductId = 6, ProductTypeId = 5, Price = 2.99m }, new ProductVariant { ProductId = 7, ProductTypeId = 8, Price = 19.99m, OriginalPrice = 29.99m }, new ProductVariant { ProductId = 7, ProductTypeId = 9, Price = 69.99m }, new ProductVariant { ProductId = 7, ProductTypeId = 10, Price = 49.99m, OriginalPrice = 59.99m }, new ProductVariant { ProductId = 8, ProductTypeId = 8, Price = 9.99m, OriginalPrice = 24.99m, }, new ProductVariant { ProductId = 9, ProductTypeId = 8, Price = 14.99m }, new ProductVariant { ProductId = 10, ProductTypeId = 1, Price = 159.99m, OriginalPrice = 299m }, new ProductVariant { ProductId = 11, ProductTypeId = 1, Price = 79.99m, OriginalPrice = 399m } ); } ---
  • 65. Product Variants と Types を Product Service に含める - 1 • • • タブは Network • フィルターは Fetch/XHR で実⾏ --- public async Task<ServiceResponse<Product>> GetProductAsync(int productId) { var response = new ServiceResponse<Product>(); Product product = null; if (_httpContextAccessor.HttpContext.User.IsInRole("Admin")) { product = await _context.Products .Include(p => p.Variants.Where(v => !v.Deleted)) .ThenInclude(v => v.ProductType) .FirstOrDefaultAsync(p => p.Id == productId && !p.Deleted); } else { --- public async Task<ServiceResponse<List<Product>>> GetProductsAsync() { var response = new ServiceResponse<List<Product>> { Data = await _context.Products .Where(p => p.Visible && !p.Deleted) .Include(p => p.Variants.Where(v => v.Visible && !v.Deleted)) .ToListAsync() --- public async Task<ServiceResponse <List<Product>>> GetProductsByCategory(string categoryUrl) { var response = new ServiceResponse<List<Product>> { Data = await _context.Products .Where(p => p.Category.Url.ToLower().Equals(categoryUrl.ToLower()) && p.Visible && !p.Deleted) .Include(p => p.Variants.Where(v => v.Visible && !v.Deleted)) .ToListAsync() ---
  • 66. Product Variants と Types を Product Service に含める - 2 • • Product は取れている • movies のところの下で Productを クリック し、variants の中に Product が列挙される ように出⼒されていることが確認できる • id 指定してないと ProductType が⼊ってい ないが、1と指定してリロードすると id に対応 した ProductType がちゃんと⼊っているのが ⾒える
  • 68. Product Search 機能の追加と実装 - 1 Server Service ProductService IProductService.cs --- //追加 Task SearchProducts(string searchText, int page); ---
  • 69. Product Search 機能の追加と実装 – 2 Server → Services → ProductService → ProductService.cs --- public async Task SearchProducts(string searchText, int page) { LastSearchText = searchText; var result = await _http .GetFromJsonAsync<ServiceResponse<ProductSearchResult>>($"api/product/search/{searchText}/{page }"); if (result != null && result.Data != null) { Products = result.Data.Products; CurrentPage = result.Data.CurrentPage; PageCount = result.Data.Pages; } if (Products.Count == 0) Message = "商品がみつかりません。"; ProductsChanged?.Invoke(); } ---
  • 70. Product Search 機能の追加と実装 – 3 Server → Services → ProductService → ProductService.cs --- //ついで public async Task<List<string>> GetProductSearchSuggestions(string searchText) { var result = await _http .GetFromJsonAsync<ServiceResponse<List<string>>> ($"api/product/searchsuggestions/{searchText}"); return result.Data; } //上記の通り実装
  • 71. Product Search 機能の追加と実装 – 4 Server → Services → ProductService → ProductService.cs --- public async Task<ServiceResponse<ProductSearchResult>> SearchProducts(string searchText, int page) { var pageResults = 2f; var pageCount = Math.Ceiling((await FindProductsBySearchText(searchText)).Count / pageResults); var products = await _context.Products .Where(p => p.Title.ToLower().Contains(searchText.ToLower()) || p.Description.ToLower().Contains(searchText.ToLower()) && p.Visible && !p.Deleted) .Include(p => p.Variants) .Skip((page - 1) * (int)pageResults) .Take((int)pageResults) .ToListAsync(); var response = new ServiceResponse<ProductSearchResult> { Data = new ProductSearchResult { Products = products, CurrentPage = page, Pages = (int)pageCount } }; return response; } //上記の通り実装 ---
  • 72. Product Search 機能の追加と実装 – 5 • デバッグ実⾏ • https://localhost:(port 番号)/swagger/index.html • ⼩説、等で実⾏。Response Body に1件ずつ全項⽬が表⽰される
  • 73. Search Suggestions の実装 - 1 Server → Services → ProductService → ProductService.cs --- public async Task<ServiceResponse<List<string>>> GetProductSearchSuggestions(string searchText) { var products = await FindProductsBySearchText(searchText); List<string> result = new List<string>(); foreach (var product in products) { if (product.Title.Contains(searchText, StringComparison.OrdinalIgnoreCase)) { result.Add(product.Title); } if (product.Description != null) { var punctuation = product.Description.Where(char.IsPunctuation) .Distinct().ToArray(); var words = product.Description.Split() .Select(s => s.Trim(punctuation)); foreach (var word in words) { if (word.Contains(searchText, StringComparison.OrdinalIgnoreCase) && !result.Contains(word)) { result.Add(word); } } } } return new ServiceResponse<List<string>> { Data = result }; } ---
  • 74. Search Suggestions の実装 - 2 Server → Controllers → ProductController.cs --- [HttpGet("searchsuggestions/{searchText}")] public async Task<ActionResult<ServiceResponse <List<Product>>>> GetProductSearchSuggestions(string searchText) { var result = await _productService.GetProductSearchSuggestions(searchText); return Ok(result); } ---
  • 75. Search Suggestions の実装 – 3 Server → Services → ProductService → ProductService.cs --- if (product.Title.Contains(searchText, StringComparison.OrdinalIgnoreCase)) { result.Add(product.Title); } if (product.Description != null) { var punctuation = product.Description.Where(char.IsPunctuation) .Distinct().ToArray(); var words = product.Description.Split() .Select(s => s.Trim(punctuation)); foreach (var word in words) { if (word.Contains(searchText, StringComparison.OrdinalIgnoreCase) && !result.Contains(word)) { result.Add(word); } } } --- • まず、句読点を取得し、句読点の助けを借りて、説明⽂のすべての単語を取得 • その後、単純に任意の単語が検索テキストを含むかどうかをチェックし、もしそうなら、結果に追加する
  • 76. Search Suggestions の実装 - 4 • デバッグ実⾏ • https://localhost:(port 番号)/swagger/index.html • ⼩説、等で実⾏。Response Body に出てくるものは Search ボックス内でサジェストされる(ここでは5件)
  • 77. Search Suggestions の実装 – Client 側 1 Client → Services → ProductService → IProductService.cs --- namespace BlazorECommerceApp.Client.Services.ProductService { public interface IProductService { --- Task SearchProducts(string searchText, int page); Task<List<string>> GetProductSearchSuggestions(string searchText); --- } } --- //を追加 • 「商品が⾒つかりませんでした」というようなメッセージを表⽰する • その後ユーザーにいくつかの情報を与えるために、サービスが開始される • リストの⽂字列を送信すると、商品検索候補を取得
  • 78. Search Suggestions の実装 – Client 側 2 Client → Services → ProductService → ProductService.cs --- namespace BlazorECommerceApp.Client.Services.ProductService { public interface IProductService { --- Task SearchProducts(string searchText, int page); Task<List<string>> GetProductSearchSuggestions(string searchText); --- } } --- //を追加 • 「商品が⾒つかりませんでした」というようなメッセージを表⽰する • その後ユーザーにいくつかの情報を与えるために、サービスが開始される • リストの⽂字列を送信すると、商品検索候補を取得
  • 79. Search Suggestions の実装 – Client 側 3-a Client → Services → ProductService → ProductService.cs --- namespace BlazorECommerceApp.Client.Services.ProductService { public class ProductService : IProductService //IProductService.cs インターフェイスをインプリする { private readonly HttpClient _http; public ProductService(HttpClient http) { _http = http; } ---
  • 80. Search Suggestions の実装 – Client 側 3-b Client → Services → ProductService → ProductService.cs --- public List<Product> Products { get; set; } = new List<Product>(); public string Message { get; set; } = "商品をロードしています..."; //メッセージを追加 public int CurrentPage { get; set; } = 1; public int PageCount { get; set; } = 0; public string LastSearchText { get; set; } = string.Empty; public List<Product> AdminProducts { get; set; } public event Action ProductsChanged; public async Task<Product> CreateProduct(Product product) { var result = await _http.PostAsJsonAsync("api/product", product); var newProduct = (await result.Content .ReadFromJsonAsync<ServiceResponse<Product>>()).Data; return newProduct; } ---
  • 81. Search Suggestions の実装 – Client 側 3-c Client → Services → ProductService → ProductService.cs --- public async Task DeleteProduct(Product product) { var result = await _http.DeleteAsync($"api/product/{product.Id}"); } public async Task GetAdminProducts() { var result = await _http .GetFromJsonAsync<ServiceResponse<List<Product>>>("api/product/admin"); AdminProducts = result.Data; CurrentPage = 1; PageCount = 0; if (AdminProducts.Count == 0) Message = "商品がみつかりません。"; }---
  • 82. Search Suggestions の実装 – Client 側 3-e Client → Services → ProductService → ProductService.cs --- public async Task<ServiceResponse<Product>> GetProduct(int productId) { var result = await _http.GetFromJsonAsync<ServiceResponse<Product>>($"api/product/{productId}"); return result; } public async Task GetProducts(string? categoryUrl = null) { var result = categoryUrl == null ? await _http.GetFromJsonAsync<ServiceResponse<List<Product>>> ("api/product/featured") : await _http.GetFromJsonAsync<ServiceResponse<List<Product>>> ($"api/product/category/{categoryUrl}"); if (result != null && result.Data != null) Products = result.Data; CurrentPage = 1; PageCount = 0; if (Products.Count == 0) Message = "商品がみつかりません。"; ProductsChanged.Invoke(); } ---
  • 83. Search Suggestions の実装 – Client 側 3-e Client → Services → ProductService → ProductService.cs --- public async Task<List<string>> GetProductSearchSuggestions(string searchText) { var result = await _http .GetFromJsonAsync<ServiceResponse<List<string>>>($"api/product/searchsuggestions/{searchText}"); return result.Data; } public async Task SearchProducts(string searchText, int page) { LastSearchText = searchText; var result = await _http .GetFromJsonAsync<ServiceResponse<ProductSearchResult>>($"api/product/search/{searchText}/{page}"); if (result != null && result.Data != null) { Products = result.Data.Products; CurrentPage = result.Data.CurrentPage; PageCount = result.Data.Pages; } if (Products.Count == 0) Message = "商品がみつかりません。"; ProductsChanged?.Invoke(); } public async Task<Product> UpdateProduct(Product product) { var result = await _http.PutAsJsonAsync($"api/product", product); var content = await result.Content.ReadFromJsonAsync<ServiceResponse<Product>>(); return content.Data; } } } ---
  • 84. URL を介した検索の実装 Client → Pages → index.razor --- @page "/" @page "/search/{searchText}/{page:int}" @page "/{categoryUrl}" --- @code { [Parameter] public string? CategoryUrl { get; set; } = null; [Parameter] public string? SearchText { get; set; } = null; [Parameter] public int Page { get; set; } = 1; protected override async Task OnParametersSetAsync() { if (SearchText != null) { await ProductService.SearchProducts(SearchText, Page); } else { await ProductService.GetProducts(CategoryUrl); } } } --- //を追加 • 単純にフォワードスラッシュ • 開始ページまたは検索 • 検索テキストまたはカテゴリ URL • 必要なのはこの新しいパラメータ、検索テキスト • コードブロック全体の実装
  • 85. Seach コンポーネント作成 - 1 Client → Shared → Search.razor --- //先に@code部分作成 @Inject NavigationManager NavigationManager @inject IProductService ProductService --- @code { private string searchText = string.Empty; private List<string> suggestions = new List<string>(); protected ElementReference searchInput; protected override async Task OnAfterRenderAsync (bool firstRender) { if (firstRender) { await searchInput.FocusAsync(); } } public void SearchProducts() { NavigationManager.NavigateTo($"search/{searchText}/1"); } --- • すでにあるものを注⼊する • Product Service • いくつかの呼び出し • Navigation Manager • ユーザーを特定のページに誘導したい • NavigateTo メソッドを使⽤
  • 86. Seach コンポーネント作成 - 2 Client → Shared → Search.razor --- //先に@code部分作成 @Inject NavigationManager NavigationManager @inject IProductService ProductService --- public async Task HandleSearch(KeyboardEventArgs args) { if (args.Key == null || args.Key.Equals("Enter")) { SearchProducts(); } else if (searchText.Length > 1) { suggestions = await ProductService. GetProductSearchSuggestions(searchText); } } • すでにあるものを注⼊する • Product Service • いくつかの呼び出し • Navigation Manager • ユーザーを特定のページに誘導したい • NavigateTo メソッドを使⽤
  • 87. Seach コンポーネント作成 - 3 Client → Shared → Search.razor --- //最後に HTML 部分 @Inject NavigationManager NavigationManager @inject IProductService ProductService --- <div class="input-group"> <input @bind-value="searchText" @bind-value:event="oninput" type="search" list="products" @onkeyup="HandleSearch" class="form-control" placeholder="検索..." @ref="searchInput" /> <datalist id="products"> @foreach (var suggestion in suggestions) { <option>@suggestion</option> } </datalist> <div class="input-group-append"> <button class="btn btn-primary" @onclick="SearchProducts"> <span class="oi oi-magnifying-glass"></span> </button> </div> </div> • すでにあるものを注⼊する • Product Service • いくつかの呼び出し • Navigation Manager • ユーザーを特定のページに誘導したい • NavigateTo メソッドを使⽤
  • 88. Seach コンポーネント作成 - 4 Client → Shared → MainLayout.razor --- MainLayout.razor @inherits LayoutComponentBase <div class="page"> <div class="sidebar"> <NavMenu /> </div> <main> <div class="top-row px-4"> <Search /> //ここに Search コンポーネントを⼊れる </div> <article class="content px-4"> @Body </article> </main> </div> --- • すでにあるものを注⼊する • Product Service • いくつかの呼び出し • Navigation Manager • ユーザーを特定のページに誘導したい • NavigateTo メソッドを使⽤
  • 90. レイアウトの変更 - 1 Client → Shared → MainLayout.razor --- <CascadingAuthenticationState> <Router AppAssembly="@typeof(App).Assembly"> <Found Context="routeData"> <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(ShopLayout)"> <NotAuthorized> <h3>Whoops! You're not allowed to see this page.</h3> <h5>Please <a href="login">login</a> or <a href="register">register </a> for a new account.</h5> </NotAuthorized> </AuthorizeRouteView> <FocusOnNavigate RouteData="@routeData" Selector="h1" /> </Found> <NotFound> <PageTitle>Not found</PageTitle> <LayoutView Layout="@typeof(ShopLayout)"> <p role="alert"> Sorry, there's nothing at this address.</p> </LayoutView> </NotFound> </Router> </CascadingAuthenticationState> --- • MainLayout • → App.razor で定義 • MainLayout.razor をコピーすると css もコピーされる • ShopLayout.razor と • ShopLayout.razor.css を作成 • App.razor を編集 • ShopLayout をパラメーターに設定
  • 91. レイアウトの変更 - 2 Client → Shared → NavMenu.razor --- @inject ICategoryService CategoryService <div class="top-row ps-3 navbar navbar-dark"> <div class="container-fluid"> <a class="navbar-brand" href="">BlazorECommerceApp</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"> Home </NavLink> </div> @foreach (var category in CategoryService.Categories) { <div class="nav-item px-3"> <NavLink class="nav-link" href="@category.Url"> @category.Name </NavLink> </div> } </nav> </div> • ShopNavMenu.razor • ShopNavMenu.razor.css • 共に編集し最終形にする • これによって上のナビゲーションメニュー ボタンができ、左のメニューが消える • css は⾯倒だがこの機会にある程度 詳しくなると、他のプラットフォームでも 使いこなせる • css に慣れるためにも Hot Reload を活⽤してください︕楽しくなります
  • 92. レイアウトの変更 - 3 Client → Shared → MainLayout.razor --- @inherits LayoutComponentBase <div class="page"> <div class="sidebar"> <NavMenu /> </div> <main> <div class="top-row px-4"> <Search /> </div> <article class="content px-4"> @Body </article> </main> </div> --- • ShopNavMenu.razor • ShopNavMenu.razor.css • 共に編集し最終形にする • これによって上のナビゲーションメニュー ボタンができ、左のメニューが消える • css は⾯倒だがこの機会にある程度 詳しくなると、他のプラットフォームでも 使いこなせる • css に慣れるためにも Hot Reload を活⽤してください︕楽しくなります
  • 93. レイアウトの変更 - 4 Client → Shared → ShopNavMenu.razor --- @inject ICategoryService CategoryService @implements Idisposable <div class="top-row ps-3 navbar navbar-dark navbar-toggler-wrapper"> <div class="container-fluid"> <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-nav"> <div class="nav-item px-2"> <NavLink class="nav-link" href="" Match="NavLinkMatch.All"> Home </NavLink> </div> @foreach (var category in CategoryService.Categories) { <div class="nav-item px-2"> <NavLink class="nav-link" href="@category.Url"> @category.Name </NavLink> </div> } </nav> </div> --- • MainLayout • → App.razor で定義 • MainLayout.razor をコピーすると css もコピーされる • ShopLayout.razor と • ShopLayout.razor.css を作成 • App.razor を編集 • ShopLayout をパラメーターに設定
  • 94. レイアウトの変更 - 5 Client → Shared → ShopNavMenu.razor --- @inject ICategoryService CategoryService @implements IDisposable --- @code { private bool collapseNavMenu = true; private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null; private void ToggleNavMenu() { collapseNavMenu = !collapseNavMenu; } protected override async Task OnInitializedAsync() { await CategoryService.GetCategories(); CategoryService.OnChange += StateHasChanged; } public void Dispose() { CategoryService.OnChange -= StateHasChanged; } } --- • MainLayout • → App.razor で定義 • MainLayout.razor をコピーすると css もコピーされる • ShopLayout.razor と • ShopLayout.razor.css を作成 • App.razor を編集 • ShopLayout をパラメーターに設定
  • 95. HomeButton.razor - 1 Client → Shared → HomuButton.razor --- @inject NavigationManager NavigationManager <button @onclick="GoToHome" class="btn btn-outline-primary home-button"> マイショップ </button> @code { private void GoToHome() { NavigationManager.NavigateTo(""); } } --- • Home Button の配置 • razor の作成
  • 96. HomeButton.razor - 2 Client → Shared → HomuButton.razor.css --- HomeButton.razor.css .home-button { white-space: nowrap; margin-right: 10px; transform: rotate(-5deg); } --- • Home Button の配置 • css の追加
  • 97. HomeButton.razor - 3 Client → Shared → ShopLayout.razor --- @inherits LayoutComponentBase <div class="page"> <main> <div class="top-row px-2"> //これを追加 <HomeButton /> <Search /> </div> <div class="nav-menu"> <ShopNavMenu /> </div> <article class="content px-2"> @Body </article> </main> </div> --- • Home Button の配置 • ShopLayout.razor の修正
  • 98. 注⽬の商品 - 1 Shared → Product.cs --- //新しいプロパティを⼊れる --- namespace BlazorECommerceApp.Shared { public class Product { public int Id { get; set; } [Required] public string Title { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; public string ImageUrl { get; set; } = string.Empty; public Category? Category { get; set; } public int CategoryId { get; set; } //これを⼊れる public bool Featured { get; set; } = false; public List<ProductVariant> Variants { get; set; } = new List<ProductVariant>(); --- } } --- • Featured Products として3つを top ページにリコメンドして表⽰する
  • 99. 注⽬の商品 - 2 DataContext.cs --- //新しいプロパティを追加する(3つのみ) --- Seed を変更する(5回⽬の Migration) --- new Product { Id = 5, CategoryId = 2, Title = "Back to the Future", Description = "「バック・トゥ・ザ・フューチャー」は、ロバート・ゼメキス監督による1985年のアメ リカのSF映画である。ゼメキスとボブ・ゲイルの脚本で、マイケル・J・フォックス、クリスト ファー・ロイド、リア・トンプソン、クリスピン・グローバー、トーマス・F・ウィルソンらが出演し ています。1985年を舞台に、マーティ・マクフライ(フォックス)は、友⼈の⾵変わりな科 学者、エメット博⼠(ロイド)が作ったタイムトラベル可能なデロリアンに乗って、偶然に も1955年に戻されることになります。ブラウン(ロイド)。過去に閉じ込められたマーティは、 うっかり未来の両親の出会いを邪魔してしまい、⾃分の存在意義を脅かされてしまう。", ImageUrl = "https://upload.wikimedia.org/wikipedia/en/d/d2/ Back_to_the_Future.jpg", //ここを追加。3つのみ Featured = true }, --- • Featured Products として3つを top ページにリコメンドして表⽰する
  • 100. 注⽬の商品 - 3 • Package Manager Console で Migration 実施 cd ./BlazorECommerceApp cd ./Server dot net ef Migrations add FeaturedProducts
  • 101. 注⽬の商品 - 4 • Migrations フォルダ → FeaturedProducts を開いて内容 を確認 • カラムが追加されることを確認 • フラグが⽴つプロダクトの Id を確認 using Microsoft.EntityFrameworkCore.Migrations; #nullable disable namespace BlazorEcommerce.Server.Migrations { public partial class FeaturedProducts : Migration { protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.AddColumn<bool>( name: "Featured", table: "Products", type: "bit", nullable: false, defaultValue: false); migrationBuilder.UpdateData( table: "Products", keyColumn: "Id", keyValue: 1, column: "Featured", value: true); migrationBuilder.UpdateData( table: "Products", keyColumn: "Id", keyValue: 5, column: "Featured", value: true); migrationBuilder.UpdateData( table: "Products", keyColumn: "Id", keyValue: 9, column: "Featured", value: true); } ---
  • 102. 注⽬の商品 - 5 • Package Manager Console で DB 作成 • Azure Data Studio で確認 • dbo.Products • Featured 列が増えている • フラグが⽴っている dotnet ef database update
  • 103. 注⽬の商品のローディング - 1 Server → Services → ProductService → iProductService.cs --- namespace BlazorEcommerceApp.Server.Services.ProductService { public interface IProductService { --- Task<ServiceResponse<ProductSearchResult>> SearchProducts (string searchText, int page); Task<ServiceResponse<List<string>>> GetProductSearchSuggestions(string searchText); //これを追加する Task<ServiceResponse<List<Product>>> GetFeaturedProducts(); --- } } ---
  • 104. 注⽬の商品のローディング - 2 Server → Services → ProductService → ProductService.cs --- //インターフェイスを実装 public async Task<ServiceResponse<List<Product>>> GetFeaturedProducts() { var response = new ServiceResponse<List<Product>> { Data = await _context.Products .Where(p => p.Featured) .Include(p => p.Variants) .ToListAsync() }; return response; } ---
  • 105. 注⽬の商品のローディング – 3 Server → Controllers → ProductController.cs //下記を追加 --- [HttpGet("featured")] public async Task<ActionResult<ServiceResponse<List<Product>>>> GetFeaturedProducts() { var result = await _productService.GetFeaturedProducts(); return Ok(result); } ---
  • 106. 注⽬の商品のローディング – 4 Client → Shared → FeaturedProducts.razor.cs --- @inject IProductService ProductService @implements Idisposable --- @code { protected override void OnInitialized() { ProductService.ProductsChanged += StateHasChanged; } public void Dispose() { ProductService.ProductsChanged -= StateHasChanged; } } --- • 新しいコンポーネントを作ってフィーチャード プロダクトを表⽰する • 先に @code 部分から
  • 107. 注⽬の商品のローディング – 5 Client → Shared → FeaturedProducts.razor.cs --- @inject IProductService ProductService @implements Idisposable --- <center><h2>今⽇の⼈気商品</h2></center> @if (ProductService.Products == null || ProductService.Products.Count == 0) { <span>@ProductService.Message</span> } else { <div class="container"> @foreach (var product in ProductService.Products) { @if (product.Featured) { <div class="featured-product"> <div> <a href="product/@product.Id"> <img src="@product.ImageUrl"> </a> </div> <h4><a href="product/@product.Id">@product.Title</a></h4> @if (product.Variants != null && product.Variants.Count > 0) { <h5 class="price"> [email protected][0].Price </h5> } </div> } } </div> }--- • 新しいコンポーネントを作ってフィーチャード プロダクトを表⽰する • @code に続いて View 部分
  • 108. 注⽬の商品のローディング – 6 Client → Shared → FeaturedProducts.razor.cs → FeaturedProducts.razor.css --- .container { display: flex; flex-direction: row; overflow-x: auto; justify-content: center; } img { max-width: 200px; max-height: 200px; border-radius: 6px; transition: transform .2s; margin-bottom: 10px; } img:hover { transform: scale(1.1) rotate(5deg); } //ここを追加 .featured-product { margin: 10px; text-align: center; padding: 10px; border: 1px solid lightgray; border-radius: 10px; max-width: 200px; } @media (max-width: 1023.98px) { .container { justify-content: flex-start; } } • FeaturedProducts.razor.css 作成 • Chrome Dev Tool のモバイルビューなど にも切り替えながら検証する • Hot Reload は css にこそ有効
  • 109. 注⽬の商品のローディング – 7 Client → Pages → Index.razor --- @page "/" @page "/search/{searchText}/{page:int}" @page "/{categoryUrl}" @inject IProductService ProductService --- <PageTitle>マイショップ</PageTitle> @if (SearchText == null && CategoryUrl == null) { <FeaturedProducts /> //ここを追加 } else { <ProductList /> } @code { [Parameter] public string? CategoryUrl { get; set; } = null; [Parameter] public string? SearchText { get; set; } = null; [Parameter] public int Page { get; set; } = 1; protected override async Task OnParametersSetAsync() { if (SearchText != null) { await ProductService.SearchProducts(SearchText, Page); } else { await ProductService.GetProducts(CategoryUrl); //ここで Go to Implementation } } • Index.razor を修正
  • 110. 注⽬の商品のローディング – 8 Client → Services → ProductServices → ProductService.cs - GetProducts --- --- public async Task GetProducts(string? categoryUrl = null) { var result = categoryUrl == null ? await _http.GetFromJsonAsync<ServiceResponse <List<Product>>> ("api/product/featured") : await _http.GetFromJsonAsync<ServiceResponse <List<Product>>> ($"api/product/category/{categoryUrl}"); if (result != null && result.Data != null) Products = result.Data; CurrentPage = 1; PageCount = 0; if (Products.Count == 0) Message = "商品がみつかりません。"; ProductsChanged.Invoke(); } --- } • Client.Services.ProductServices ProductService.cs の GetProducts を修正
  • 111. 検索結果の ページネーション - 1 Shared → Products → ProductSerchResult.cs --- using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace BlazorEcommerceApp.Shared { public class ProductSearchResult { public List<Product> Products { get; set; } = new List<Product>(); public int Pages { get; set; } public int CurrentPage { get; set; } } } --- • データベースに多くの製品が登録されている 場合、分割して表⽰させたい • 1ページに2つの商品を表⽰し、2ページ⽬、 3ページ⽬......と進む • タイトルと説明⽂だけ表⽰させる • 商品からデータ転送オブジェクトを作成し、 DTO は商品タイトルと説明⽂だけを返す • ProductSerchResult.cs という DTO オブジェクト • この DTO で、製品のリストを取得し、ペー ジ数を取得し、情報として現在のページを 取得
  • 112. 検索結果の ページネーション - 2 Server → Services → ProductServices → IProductService.cs --- Server Services ProductServices IProductService.cs namespace BlazorEcommerceApp.Server.Services.ProductService { public interface IProductService { --- //ここを追加 Task<ServiceResponse<ProductSearchResult>> SearchProducts(string searchText, int page); --- } }--- • ProductSerchResult.cs という DTO オブジェクト • この DTO で、製品のリストを取得し、ペー ジ数を取得し、情報として現在のページを 取得
  • 113. 検索結果の ページネーション - 3 Server → Services → ProductServices → ProductService.cs --- public async Task<ServiceResponse<ProductSearchResult>> SearchProducts(string searchText, int page) //リターン値を ProductSearchResult にしてパラメーターに page も追加 { //下記の両者を定義しておく var pageResults = 2f; var pageCount = Math.Ceiling((await FindProductsBySearchText(searchText)) .Count / pageResults); var products = await _context.Products .Where(p => p.Title.ToLower().Contains(searchText.ToLower()) ||p.Description.ToLower().Contains(searchText.ToLower()) && p.Visible && !p.Deleted) .Include(p => p.Variants) .Skip((page - 1) * (int)pageResults) .Take((int)pageResults) .ToListAsync(); //ここもProductSearchResultに変更 var response = new ServiceResponse<ProductSearchResult> { Data = new ProductSearchResult { Products = products, CurrentPage = page, Pages = (int)pageCount } }; return response; } --- • Server 上のページネーション
  • 114. 検索結果の ページネーション - 4 Server → Controllers → ProductContoroller.cs --- //page 追加 [HttpGet("search/{searchText}/{page}")] public async Task<ActionResult<ServiceResponse<ProductSearchResult>>> //page 追加、デフォルト値=1 SearchProducts(string searchText, int page = 1) { var result = await _productService.SearchProducts(searchText, page); return Ok(result); } ------ • コントローラにも変更を加える必要あり • Product コントローラの Search メソッドに、 もうひとつパラメータを追加(Page) • デフォルトで1に設定 • アプリケーションを起動 • Swagger ページを開く • 検索テキストを⼊⼒してテスト
  • 115. 検索結果の ページネーション - 5 Client → Services → ProductService → IProductService.cs --- namespace BlazorEcommerceApp.Client.Services.ProductService { public interface IProductService { -- Task<ServiceResponse<Product>> GetProduct(int productId); Task SearchProducts(string searchText, int page); --- --- • クライアントの変更を実装 Client → Services → ProductService → IProductService.cs --- namespace BlazorEcommerceApp.Client.Services.ProductService { --- public List<Product> Products { get; set; } = new List<Product>(); //下記3⾏を追加 public string Message { get; set; } = "商品をロードしています..."; public int CurrentPage { get; set; } = 1; public int PageCount { get; set; } = 0; ---
  • 116. 検索結果の ページネーション - 6 Client → Services → ProductService → ProductService.cs --- public async Task GetProducts(string? categoryUrl = null) { --- CurrentPage = 1; PageCount = 0; if (Products.Count == 0) Message = "商品がみつかりません。"; ProductsChanged.Invoke();} --- • Client 側 GetProducts を修正 • Client 側 SearchProducts を修正 Client → Services → ProductService → ProductService.cs --- public async Task SearchProducts(string searchText, int page) //page パラメータを追加 { LastSearchText = searchText; var result = await _http .GetFromJsonAsync<ServiceResponse<ProductSearchResult>> //List<Products> をProductSearchResult に変更 ($"api/product/search/{searchText}/{page}"); //page 部分を追加 if (result != null && result.Data != null) { Products = result.Data.Products; CurrentPage = result.Data.CurrentPage; PageCount = result.Data.Pages; } if (Products.Count == 0) Message = "商品がみつかりません。"; ProductsChanged?.Invoke(); } ---
  • 117. 検索結果の ページネーション - 7 Client → Shared → Search.razor --- public void SearchProducts() { --- //1 をデフォルト値として追加 NavigationManager.NavigateTo($"search/{searchText}/1"); --- • ページネーションのコンポーネントへの追加 • Search.razor、 Index.razor を修正 Client → Pages → Index.razor --- //int を追加 @page "/search/{searchText}/{page:int}" --- // パラメータ追加 [Parameter] public int Page { get; set; } = 1; --- //修正 --- protected override async Task OnParametersSetAsync() { if (SearchText != null) { //page 追加 await ProductService.SearchProducts(SearchText, Page); } --- ---
  • 118. 検索結果の ページネーション - 8 Client → Shared → Product.razor --- //下記を追加 for (var i = 1; i <= ProductService.PageCount; i++) { <a class="btn @(i == ProductService.CurrentPage ? "btn-info" : "btn-outline-info") page-selection" href="/search/@ProductService.LastSearchText/@i">@i</a> } --- • ボタンの追加 • これでページネーションは完成 Client → Shared → Product.razor.css --- //追加 --- .page-selection { margin-right: 15px; margin-bottom: 30px; } //追加 --- .page-selection { margin-right: 15px; margin-bottom: 30px; } ---
  • 120. ショッピングカート - 1 Client → Program.cs --- //下記を追加 --- using Blazored.LocalStorage; --- builder.Services.AddBlazoredLocalStorage(); --- • ローカルストレージを使う • Client プロジェクトに NuGet パッケージ 追加 • Blazer Local Storage Client → Imports.razor --- //追加 --- @using Blazored.LocalStorage --- ---
  • 121. ショッピングカート - 2 Client → Shared → CartCounter.razor --- @inject ICartService CartService @inject ISyncLocalStorageService LocalStorage @implements IDisposable <a href="cart" class="btn btn-info"> <i class="oi oi-cart"></i> <span class="badge">@GetCartItemsCount()</span> </a> @code { private int GetCartItemsCount() { var count = LocalStorage.GetItem<int>("cartItemsCount"); return count; } protected override void OnInitialized() { CartService.OnChange += StateHasChanged; } public void Dispose() { CartService.OnChange -= StateHasChanged; } } --- • カートを追加 • デバッグ実⾏して画⾯のカート部分を確認
  • 122. ショッピングカート - 3 Client → CartItem.cs --- using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace BlazorEcommerceApp.Shared { public class CartItem { public int ProductId { get; set; } public int ProductTypeId { get; set; } } }--- • カートを追加 • デバッグ実⾏して画⾯のカート部分を確認
  • 123. クライアント側の CartService 実装 - 1 Client → Program.cs --- builder.Services.AddScoped<ICategoryService, CategoryService>(); --- • Client → Service → ICartService.cs を追加 • Client → Service → CartService.cs を追加 Client → _Imports.razor --- @using BlazorEcommerceApp.Client.Services.CartService ---
  • 124. クライアント側の CartService 実装 - 2 Client → Client → Service → ICartService.cs --- namespace BlazorEcommerceApp.Client.Services.CartService { public interface ICartService { event Action OnChange; Task AddToCart(CartItem cartItem); Task<List<CartProductResponse>> GetCartProducts(); Task RemoveProductFromCart(int productId, int productTypeId); Task UpdateQuantity(CartProductResponse product); Task StoreCartItems(bool emptyLocalCart); Task GetCartItemsCount(); } } --- • Client → Service → ICartService.cs を追加
  • 125. クライアント側の CartService 実装 - 3 Client → Client → Service → CartService.cs --- using Blazored.LocalStorage; namespace BlazorEcommerceApp.Client.Services.CartService { public class CartService : ICartService { private readonly ILocalStorageService _localStorage; private readonly HttpClient _http; private readonly IAuthService _authService; public CartService(ILocalStorageService localStorage, HttpClient http, IAuthService authService) { _localStorage = localStorage; _http = http; _authService = authService; } public event Action OnChange; public async Task AddToCart(CartItem cartItem) { if (await _authService.IsUserAuthenticated()) { await _http.PostAsJsonAsync("api/cart/add", cartItem); } else { • Client → Service → CartService.cs を追加 var cart = await _localStorage.GetItemAsync<List<CartItem>>("cart"); if (cart == null) { cart = new List<CartItem>(); } var sameItem = cart.Find(x => x.ProductId == cartItem.ProductId && x.ProductTypeId == cartItem.ProductTypeId); if (sameItem == null) { cart.Add(cartItem); } else { sameItem.Quantity += cartItem.Quantity; } await _localStorage.SetItemAsync("cart", cart); } await GetCartItemsCount(); } public async Task GetCartItemsCount() { if (await _authService.IsUserAuthenticated()) { var result = await _http.GetFromJsonAsync<ServiceResponse<int>>("api/cart/count"); var count = result.Data; await _localStorage.SetItemAsync<int> ("cartItemsCount", count); } else { var cart = await _localStorage.GetItemAsync<List<CartItem>>("cart"); await _localStorage.SetItemAsync<int> ("cartItemsCount", cart != null ? cart.Count : 0); } OnChange.Invoke(); } ---
  • 126. クライアント側の CartService 実装 - 4 • ProductDetail に Add to Cart ボタン を追加 Client Pages ProductDetails.razor // 先に View 側に追加 --- @inject ICartService CartService --- --- <button class="btn btn-primary" @onclick="AddToCart"> <i class="oi oi-cart"></i>&nbsp;&nbsp;&nbsp;Add to Cart </button> --- --- // 次いで@code側に追加 --- private async Task AddToCart() { var productVariant = GetSelectedVariant(); var cartItem = new CartItem { ProductId = productVariant.ProductId, ProductTypeId = productVariant.ProductTypeId }; await CartService.AddToCart(cartItem); } ---
  • 127. クライアント側の CartService 実装 - 5 • デバッグ実⾏ • Chrome Developer Tools 起動 • アイテムを⼀つ選択 • アプリケーションタブに切り替え • ローカルストレージ表⽰ • 当該アイテムを Add to Cart で追加 • 値を確認する
  • 128. クライアント側の CartService 実装 - 6 • CartCounter 数字のインクリメント • @Code 部分を先に実装 Client → Shared → CartCounter.razor // 先に @code 部分を実装 --- @inject ICartService CartService @inject ISyncLocalStorageService LocalStorage @implements IDisposable --- @code { private int GetCartItemsCount() { var count = LocalStorage.GetItem<int>("cartItemsCount"); return count; } protected override void OnInitialized() { CartService.OnChange += StateHasChanged; } public void Dispose() { CartService.OnChange -= StateHasChanged; } } ---
  • 129. クライアント側の CartService 実装 - 7 • CartCounter 数字のインクリメント • 続いて view 部分を実装 • CartService.cs → OnChange.Invoke() を追加 Client → Shared → CartCounter.razor // View 部分を実装 --- <a href="cart" class="btn btn-info"> <i class="oi oi-cart"></i> <span class="badge">@GetCartItemsCount()</span> </a> --- Client → Service → CartService → CartService.cs --- OnChange.Invoke(); ---
  • 130. CartItem のサーバー側 Products への送付- 1 • Shared の CartProductResponse.cs Shared → CartProductResponse.cs --- using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace BlazorEcommerceApp.Shared { public class CartProductResponse { public int ProductId { get; set; } public string Title { get; set; } = string.Empty; public int ProductTypeId { get; set; } public string ProductType { get; set; } = string.Empty; public string ImageUrl { get; set; } = string.Empty; public decimal Price { get; set; } public int Quantity { get; set; } } } ---
  • 131. CartItem のサーバー側 Products への送付- 2 • Server → Services → CartService フォルダ作成 • Server → Services → ICartService • Server → Services → CartService を追加 Server → Program.cs //下記を追加 --- builder.Services.AddScoped<ICartService, CartService>(); --- global using BlazorEcommerceApp.Server.Services.CartService; ---
  • 132. CartItem のサーバー側 Products への送付- 3 • Server → Services → CartServiceProgram.cs 実装 Server → Services → CartServiceProgram.cs --- using System.Security.Claims; namespace BlazorEcommerceApp.Server.Services.CartService { public class CartService : ICartService { private readonly DataContext _context; public CartService(DataContext context, IAuthService authService) { _context = context; } public async Task<ServiceResponse<List<CartProductResponse>>> GetCartProducts(List<CartItem> cartItems) { var result = new ServiceResponse<List<CartProductResponse>> { Data = new List<CartProductResponse>() }; foreach (var item in cartItems) { var product = await _context.Products .Where(p => p.Id == item.ProductId) .FirstOrDefaultAsync(); if (product == null) { continue; } var productVariant = await _context.ProductVariants .Where(v => v.ProductId == item.ProductId && v.ProductTypeId == item.ProductTypeId) .Include(v => v.ProductType) .FirstOrDefaultAsync(); if (productVariant == null) { continue; } var cartProduct = new CartProductResponse { ProductId = product.Id, Title = product.Title, ImageUrl = product.ImageUrl, Price = productVariant.Price, ProductType = productVariant.ProductType.Name, ProductTypeId = productVariant.ProductTypeId, Quantity = item.Quantity }; result.Data.Add(cartProduct); } return result; } ---
  • 133. CartItem のサーバー側 Products への送付- 4 • Server → Controller→ CartController.cs 実装 Server → Controller → CartController.cs // using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using System.Security.Claims; namespace BlazorEcommerceApp.Server.Controllers { [Route("api/[controller]")] [ApiController] public class CartController : ControllerBase { private readonly ICartService _cartService; public CartController(ICartService cartService) { _cartService = cartService; } [HttpPost("products")] public async Task<ActionResult<ServiceResponse<List<CartProductResponse>>>> GetCartProducts(List<CartItem> cartItems) { var result = await _cartService.GetCartProducts(cartItems); return Ok(result); } --- ---
  • 134. クライアント側の CartProduct 取得 • Client → Services → ICartService.cs 実装 • Client → Services → CartService.cs 実装 Client → Services → ICartService.cs --- namespace BlazorEcommerceApp.Client.Services.CartService { public interface ICartService { event Action OnChange; Task AddToCart(CartItem cartItem); //ここを追加 Task<List<CartProductResponse>> GetCartProducts(); Task RemoveProductFromCart(int productId, int productTypeId); } } --- Client → Services → CartService.cs --- // このメソッドで取得する public async Task<List<CartProductResponse>> GetCartProducts() { if (await _authService.IsUserAuthenticated()) { var response = await _http.GetFromJsonAsync <ServiceResponse<List<CartProductResponse>>> ("api/cart"); return response.Data; } else { var cartItems = await _localStorage.GetItemAsync<List<CartItem>>("cart"); if (cartItems == null) return new List<CartProductResponse>(); var response = await _http.PostAsJsonAsync ("api/cart/products", cartItems); var cartProducts = await response.Content.ReadFromJsonAsync <ServiceResponse<List<CartProductResponse>>>(); return cartProducts.Data; } } ---
  • 135. Cart ページの実装 - 1 • Client → Pages → Cart.razor • 先に @code 部分を実装する Client → Pages → Cart.razor --- // 先に@codeを実装する @page "/cart" @inject ICartService CartService @inject IOrderService OrderService @inject IAuthService AuthService @inject NavigationManager NavigationManager --- @code { List<CartProductResponse> cartProducts = null; string message = "Loading cart..."; bool isAuthenticated = false; protected override async Task OnInitializedAsync() { isAuthenticated = await AuthService.IsUserAuthenticated(); await LoadCart(); } private async Task RemoveProductFromCart(int productId, int productTypeId) { await CartService.RemoveProductFromCart(productId, productTypeId); await LoadCart(); } private async Task LoadCart() { await CartService.GetCartItemsCount(); cartProducts = await CartService.GetCartProducts(); if (cartProducts == null || cartProducts.Count == 0) { message = "Your cart is empty."; } } private async Task UpdateQuantity(ChangeEventArgs e, CartProductResponse product) { product.Quantity = int.Parse(e.Value.ToString()); if (product.Quantity < 1) product.Quantity = 1; await CartService.UpdateQuantity(product); } private async Task PlaceOrder() { string url = await OrderService.PlaceOrder(); NavigationManager.NavigateTo(url); } } ---
  • 136. Cart ページの実装 - 2 • Client → Pages → Cart.razor • 続いて View 部分を実装する Client → Pages → Cart.razor --- <PageTitle>Shopping Cart</PageTitle> <h3>ショッピングカート</h3> @if (cartProducts == null || cartProducts.Count == 0) { <span>@message</span> } else { <div> @foreach (var product in cartProducts) { <div class="container"> <div class="image-wrapper"> <img src="@product.ImageUrl" class="image" /> </div> <div class="name"> <h5><a href="/product/@product.ProductId">@product.Title</a></h5> <span>@product.ProductType</span><br /> <input type="number" value="@product.Quantity" @onchange="@((ChangeEventArgs e) => UpdateQuantity(e, product))" class="form-control input-quantity" min="1" /> <button class="btn-delete" @onclick="@(() => RemoveProductFromCart (product.ProductId, product.ProductTypeId))"> Delete </button> </div> <div class="cart-product-price"> $@(product.Price * product.Quantity)</div> </div> } <div class="cart-product-price"> Total (@cartProducts.Count): [email protected] (product => @product.Price * product.Quantity) </div> </div> @if (isAuthenticated) { <div> <h5>Delivery Address</h5> <AddressForm /> </div> } <button @onclick="PlaceOrder" class="btn alert-success float-end mt-1">Checkout</button> } ---
  • 137. Cart ページの実装 - 3 • Client → Pages → Cart.razor.css 実装 Client → Pages → Cart.razor.css --- .container { display: flex; padding: 6px; } .image-wrapper { width: 150px; text-align: center; } .image { max-height: 150px; max-width: 150px; padding: 6px; } .name { flex-grow: 1; padding: 6px; } .cart-product-price { font-weight: 600; text-align: right; } .btn-delete { background: none; border: none; padding: 0px; color: red; font-size: 12px; } .btn-delete:hover { text-decoration: underline; } .input-quantity { width: 70px; } ---
  • 138. Cart から Item を削除 - 1 • Client → Services → CartService ICartService.cs 追加 • Client → Services → CartService ICartService.cs 修正 • RemoveProductFromCart 追加 Client → Services → CartService → ICartService.cs --- //追加 Task RemoveProductFromCart(int productId, int productTypeId); --- Client → Services → CartService → CartService.cs --- //修正 RemoveProductFromCart 追加 public async Task RemoveProductFromCart(int productId, int productTypeId) { if (await _authService.IsUserAuthenticated()) { await _http.DeleteAsync($"api/cart/{productId}/{productTypeId}"); } else { var cart = await _localStorage.GetItemAsync<List<CartItem>>("cart"); if (cart == null) { return; } var cartItem = cart.Find(x => x.ProductId == productId && x.ProductTypeId == productTypeId); if (cartItem != null) { cart.Remove(cartItem); await _localStorage.SetItemAsync("cart", cart); } } } ---
  • 139. Cart から Item を削除 - 2 • Client → Pages → Cart.razor • View 部分修正 • @Code 部分修正 Client → Pages → Cart.razor --- //View 部分修正 <button class="btn-delete" @onclick="@(() => RemoveProductFromCart(product.ProductId, product.ProductTypeId))"> Delete </button>--- Client → Pages → Cart.razor --- // @Code 部分修正(追加) private async Task RemoveProductFromCart (int productId, int productTypeId) { await CartService.RemoveProductFromCart(productId, productTypeId); await LoadCart(); } --- private async Task LoadCart() { await CartService.GetCartItemsCount(); cartProducts = await CartService.GetCartProducts(); if (cartProducts == null || cartProducts.Count == 0) { message = "Your cart is empty."; } } ---
  • 140. Cart から Item を削除 - 3 • Client → Pages → Cart.razor.css への追加 Client → Pages → Cart.razor.css --- // 追加 --- .btn-delete { background: none; border: none; padding: 0px; color: red; font-size: 12px; } --- ---
  • 141. Cart から Item を削除 - 4 • デバッグ実⾏ • カートを表⽰ • Chrome Dev Tools アプリケーションタブ に移動 • ローカルストレージを表⽰ • delete ボタン押下してテスト
  • 142. Cart モデルに数量を追加 - 1 • Shared → CartProductResponse.cs を修正 Shared → CartProductResponse.cs --- // 追加 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace BlazorEcommerceApp.Shared { public class CartProductResponse { public int ProductId { get; set; } public string Title { get; set; } = string.Empty; public int ProductTypeId { get; set; } public string ProductType { get; set; } = string.Empty; public string ImageUrl { get; set; } = string.Empty; public decimal Price { get; set; } public int Quantity { get; set; } } } ---
  • 143. Cart モデルに数量を追加 - 2a • Server → Services → CartService → CartService.cs を修正 Server → Services → CartService → CartService.cs --- // 追加 --- var cartProduct = new CartProductResponse { ProductId = product.Id, Title = product.Title, ImageUrl = product.ImageUrl, Price = productVariant.Price, ProductType = productVariant.ProductType.Name, ProductTypeId = productVariant.ProductTypeId, Quantity = item.Quantity }; ---
  • 144. Cart モデルに数量を追加 - 2b • Server → Services → CartService → CartService.cs を修正 Server → Services → CartService → CartService.cs --- public async Task<ServiceResponse<bool>> AddToCart(CartItem cartItem) { cartItem.UserId = _authService.GetUserId(); var sameItem = await _context.CartItems .FirstOrDefaultAsync(ci => ci.ProductId == cartItem.ProductId && ci.ProductTypeId == cartItem.ProductTypeId && ci.UserId == cartItem.UserId); if (sameItem == null) { _context.CartItems.Add(cartItem); } else { //この箇所を追加する sameItem.Quantity += cartItem.Quantity; } await _context.SaveChangesAsync(); return new ServiceResponse<bool> { Data = true }; } ---
  • 145. Cart モデルに数量を追加 - 3a • Client → Services → CartService → ICartService.cs を修正 Client → Services → CartService → ICartService.cs --- namespace BlazorEcommerceApp.Client.Services.CartService { public interface ICartService { --- Task UpdateQuantity(CartProductResponse product); Task StoreCartItems(bool emptyLocalCart); //ここを追加 Task GetCartItemsCount(); } } ---
  • 146. Cart モデルに数量を追加 - 3b • Client → Services → CartService → CartService.cs を修正 Client → Services → CartService → CartService.cs --- public async Task UpdateQuantity(CartProductResponse product) { if (await _authService.IsUserAuthenticated()) { var request = new CartItem { ProductId = product.ProductId, Quantity = product.Quantity, ProductTypeId = product.ProductTypeId }; await _http.PutAsJsonAsync("api/cart/update-quantity", request); } else { var cart = await _localStorage.GetItemAsync<List<CartItem>>("cart"); if (cart == null) { return; } var cartItem = cart.Find(x => x.ProductId == product.ProductId && x.ProductTypeId == product.ProductTypeId); if (cartItem != null) { cartItem.Quantity = product.Quantity; await _localStorage.SetItemAsync("cart", cart); } } } ---
  • 147. Cart モデルに数量を追加 - 4a • 数値⼊⼒フィールドで数量を更新する Client → Pages → Cart.razor //追加 --- <div class="name"> <h5><a href="/product/@product.ProductId">@product.Title</a> </h5> <span>@product.ProductType</span><br /> <input type="number" value="@product.Quantity" @onchange="@((ChangeEventArgs e) => UpdateQuantity(e, product))" class="form-control input-quantity" min="1" /> ---
  • 148. Cart モデルに数量を追加 - 4b • UpdateQuantity 追加する Client → Pages → Cart.razor //追加 --- private async Task UpdateQuantity (ChangeEventArgs e, CartProductResponse product) { product.Quantity = int.Parse(e.Value.ToString()); if (product.Quantity < 1) product.Quantity = 1; await CartService.UpdateQuantity(product); } ---
  • 149. Cart モデルに数量を追加 - 4c • UpdateQuantity 追加する Client → Pages → Cart.razor //追加 --- private async Task UpdateQuantity (ChangeEventArgs e, CartProductResponse product) { product.Quantity = int.Parse(e.Value.ToString()); if (product.Quantity < 1) product.Quantity = 1; await CartService.UpdateQuantity(product); } ---
  • 150. Cart モデルに数量を追加 - 4c • デバッグ実⾏ • カートを表⽰ • Chrome Dev Tools アプリケーションタブ に移動 • ローカルストレージを表⽰ • update ボタン押下してテスト
  • 152. 認証 全体の流れ • 新しいページの追加 • 最初のユーザーを登録 • パスワードのハッシュを作成し、パスワードを解決 • JSON Web Token • Authorized View の利⽤
  • 153. UserRegister Model の作成 • ユーザー登録には 新しいモデルが必要 • モデルの名前は UserRegister • Shared Project を右クリックし、ここに 新 Class を追加 • Public Class shared → UserRegister using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text; using System.Threading.Tasks; namespace BlazorEcommerceApp.Shared { public class UserRegister { [Required, EmailAddress] public string Email { get; set; } = string.Empty; [Required, StringLength(100, MinimumLength = 6)] public string Password { get; set; } = string.Empty; [Compare("Password", ErrorMessage = "The passwords do not match.")] public string ConfirmPassword { get; set; } = string.Empty; } }
  • 154. ユーザー登録ページの作成 • Client に戻り、 いくつかの ページフォルダに、新しい Razor コンポーネントを追加 して、これを呼び出す • テスト • https://localhost:(ポート 番号 )/register Client → Pages → Register.razor @page "/register” <PageTitle>Register</PageTitle> <h3>登録</h3> @code { }
  • 155. ユーザーメニューボタンの実装 - 1 • dropdown クラス • UserMenuCssClass • dropdown-item : Register • showUserMenu • UserMenuClass • ToggleUserMenu • HideUserMenu Shared → UserButton.Razor @inject AuthenticationStateProvider AuthenticationStateProvider @inject NavigationManager NavigationManager <div class="dropdown"> <button @onclick="ToggleUserMenu" @onfocusout="HideUserMenu" class="btn btn-secondary dropdown-toggle user-button"> <i class="oi oi-person"></i> </button> <div class="dropdown-menu dropdown-menu-right @UserMenuCssClass"> <a href="register" class="dropdown-item">Register</a> </div> @code { private bool showUserMenu = false; private string UserMenuCssClass => showUserMenu ? "show-menu" : null; private void ToggleUserMenu() { showUserMenu = !showUserMenu; } private async Task HideUserMenu() { await Task.Delay(200); showUserMenu = false; } }
  • 156. ユーザーメニューボタンの実装 - 2 • .show-menu • .user-button • .top-row a • .dropdown- item:hover Shared → UserButton.Razor.css --- .show-menu { display: block; } .user-button { margin-left: .5em; } .top-row a { margin-left: 0; } .dropdown-item:hover { background-color: white; } ---
  • 157. 検証のためのデータアノテーションを追加する • データアノテーション または属性をモデル に追加 • Required 属性を 追加 • Compare 属性を 追加 Shared → UserRegister.cs === using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text; using System.Threading.Tasks; namespace BlazorEcommerceApp.Shared { public class UserRegister { [Required, EmailAddress] public string Email { get; set; } = string.Empty; [Required, StringLength(100, MinimumLength = 6)] public string Password { get; set; } = string.Empty; [Compare("Password", ErrorMessage = "The passwords do not match.")] public string ConfirmPassword { get; set; } = string.Empty; } }
  • 158. 登録フォームにバリデーションを追加 • DataAnnotations Validator コンポーネントを使⽤ • バリデーションのサマリー • DEMO • 有効な email アドレス Shared → Register.razor --- <EditForm Model="user" OnValidSubmit="HandleRegistration"> <DataAnnotationsValidator /> <div class="mb-3"> <label for="email">Email</label> <InputText id="email" @bind-Value="user.Email" class="form-control" /> </div> <div class="mb-3"> <label for="password">Password</label> <InputText id="password" @bind-Value="user.Password" class="form-control" type="password" /> </div> <div class="mb-3"> <label for="confirmPassword">Confirm Password</label> <InputText id="confirmPassword" @bind-Value="user.ConfirmPassword" class="form- control" type="password" /> </div> <button type="submit" class="btn btn-primary">Register</button> <div class="@messageCssClass"> <span>@message</span> </div> <ValidationSummary /> </EditForm> ---
  • 159. バリデーションサマリーの代わりにバリデーションメッセージを使う • DataAnnotations Validator コン ポーネントを使⽤ • テキストフィールド後に、 バリデーションメッセージ を追加 • DEMO • 有効な email アドレス Shared → Register.razor --- <EditForm Model="user" OnValidSubmit="HandleRegistration"> <DataAnnotationsValidator /> <div class="mb-3"> <label for="email">Email</label> <InputText id="email" @bind-Value="user.Email" class="form-control" /> <ValidationMessage For="@(() => user.Email)" /> </div> <div class="mb-3"> <label for="password">Password</label> <InputText id="password" @bind-Value="user.Password" class="form-control" type="password" /> <ValidationMessage For="@(() => user.Password)" /> </div> <div class="mb-3"> <label for="confirmPassword">Confirm Password</label> <InputText id="confirmPassword" @bind-Value="user.ConfirmPassword" class="form- control" type="password" /> <ValidationMessage For="@(() => user.ConfirmPassword)" /> </div> <button type="submit" class="btn btn-primary">Register</button> <div class="@messageCssClass"> <span>@message</span> </div> </EditForm> ---
  • 160. データベースのユーザーモデルを追加する - 1 • 共有フォルダを右クリック して、新しいクラス追加 • ハッシュ値とパスワード ソルトをデータベースに 保存するので、平⽂で はできない • DEMO • 有効な email アドレス Shared → User.cs --- using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace BlazorEcommerceApp.Shared { public class User { public int Id { get; set; } public string Email { get; set; } = string.Empty; public byte[] PasswordHash { get; set; } public byte[] PasswordSalt { get; set; } public DateTime DateCreated { get; set; } = DateTime.Now; public Address Address { get; set; } public string Role { get; set; } = "Customer"; } } ---
  • 161. データベースのユーザーモデルを追加する - 2 • データベース・テーブルが 必要 • データ・コンテキストに. 移動して別のテーブル を追加 • Migrations フォルダ • 2022xxxxx_ Users.cs • id=Key, email、 password, hash password, hash password salt, で作成 • テーブル作成 • データベースを Azure Server → Data → DataContext.cs --- public DbSet<User> Users { get; set; } --- terminal --- cd ..Server dotnet ef migrations add Users --- === dotnet ef database update ===
  • 162. サーバーに認証サービスを追加する • Server → Services → AuthService フォルダ作成 • IAuthService.cs 作成 • Program.cs サービス パイプラインに登録 • IAuthService 実装 Server → Program.cs --- builder.Services.AddScoped<IAuthService, AuthService>(); global using BlazorEcommerceApp.Server.Services.AuthService; --- Server → Services → AuthService → IAuthService.cs --- namespace BlazorEcommerceApp.Server.Services.AuthService { public interface IAuthService { Task<ServiceResponse<int>> Register (User user, string password); Task<bool> UserExists(string email); Task<ServiceResponse<string>> Login(string email, string password); Task<ServiceResponse<bool>> ChangePassword(int userId, string newPassword); int GetUserId(); string GetUserEmail(); Task<User> GetUserByEmail(string email); } } ---
  • 163. ユーザーが既に存在するか確認する • ユーザーが既に存在する か確認 Server → Services → AuthService → IuthService.cs --- public async Task<bool> UserExists(string email) { if (await _context.Users.AnyAsync(user => user.Email.ToLower() .Equals(email.ToLower()))) { return true; } return false; } ---
  • 164. サーバーへのユーザー登録の実施 - 1 • サーバーへのユーザー 登録 Server → Services → AuthService → AuthService.cs --- public async Task<ServiceResponse<int>> Register(User user, string password) { if (await UserExists(user.Email)) { return new ServiceResponse<int> { Success = false, Message = "このユーザーは既に存在しています。" }; } CreatePasswordHash(password, out byte[] passwordHash, out byte[] passwordSalt); user.PasswordHash = passwordHash; user.PasswordSalt = passwordSalt; _context.Users.Add(user); await _context.SaveChangesAsync(); return new ServiceResponse<int> { Data = user.Id, Message = "登録が成功しました!!" }; } ---
  • 165. サーバーへのユーザー登録の実施 - 2 • 変更をテーブルに保存 • テストするには、もちろん Auth Controller が 必要 Server → Services → AuthService → AuthService.cs --- private void CreatePasswordHash(string password, out byte[] passwordHash, out byte[] passwordSalt) { using (var hmac = new HMACSHA512()) { passwordSalt = hmac.Key; passwordHash = hmac .ComputeHash(System.Text.Encoding.UTF8.GetBytes(password)); } } ---
  • 166. AuthControllerを追加する - 1 • Server → Controllers → AuthController → API コントローラー新規 作成 • control フォルダに別の 空の API コントローラを 作成 Server → Controllers → AuthController.cs --- using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using System.Security.Claims; namespace BlazorEcommerceApp.Server.Controllers { [Route("api/[controller]")] [ApiController] public class AuthController : ControllerBase { private readonly IAuthService _authService; public AuthController(IAuthService authService) { _authService = authService; } --- ---
  • 167. AuthControllerを追加する - 2 • アプリを起動して、 swagger にアクセス • localhost://ポート番号 /swagger/index.html • user already exists • Azure Data Explorer で確認
  • 168. クライアント側で AuthService を作成する • IAuthService インターフェイスと AuthService 実装クラスを追加 Client → Services → AuthService → AuthService.cs --- namespace BlazorEcommerceApp.Client.Services.AuthService { public class AuthService : IAuthService { private readonly HttpClient _http; private readonly AuthenticationStateProvider _authStateProvider; public AuthService(HttpClient http, AuthenticationStateProvider authStateProvider) { _http = http; _authStateProvider = authStateProvider; } --- } } --- global using BlazorEcommerceApp.Client.Services.AuthService; === client → wwwroot → imports.razor === @using BlazorEcommerceApp.Client.Services.AuthService ===
  • 169. クライアント側での登録の実装 - 1 • 登録⽤のメソッドを1つ 追加、再び返す • サービスは UserId で 応答 • register メソッド リクエストとして登録 • インターフェイスを実装 し、HTTP クライアント 追加 • フィールドを作成 • JSON async await Client → Services → AuthService → AuthService.cs --- namespace BlazorEcommerceApp.Client.Services.AuthService { public class AuthService : IAuthService { private readonly HttpClient _http; public AuthService(HttpClient http) { _http = http; } public async Task<ServiceResponse<int>> Register (UserRegister request) { var result = await _http.PostAsJsonAsync("api/auth/register", request); return await result.Content.ReadFromJsonAsync<ServiceResponse<int>>(); } } }
  • 170. クライアント側での登録の実装 - 2 • 登録⽤のメソッドを1つ 追加、再び返す • サービスは UserId で 応答 • register メソッド リクエストとして登録 • インターフェイスを実装し、 HTTP クライアント追加 • フィールドを作成 • JSON async await Client → Services → AuthService → IAuthService.cs namespace BlazorEcommerceApp.Client.Services.AuthService { public interface IAuthService { Task<ServiceResponse<int>> Register(UserRegister request); Task<ServiceResponse<string>> Login(UserLogin request); Task<ServiceResponse<bool>> ChangePassword(UserChangePassword request); Task<bool> IsUserAuthenticated(); } }
  • 171. 登録ページで AuthService を利⽤する • テスト • chrome 開発ツール • Register ボタンを押す ときに Fetch/XHR を 観察 • success Client → Register.razor --- @page "/register" @inject IAuthService AuthService --- <PageTitle>Register</PageTitle> --- <button type="submit" class="btn btn primary">Register</button> <div class="@messageCssClass"> <span>@message</span> </div> </EditForm> @code { UserRegister user = new UserRegister(); string errormessage = string.Empty; --- Client → Register.razor --- async Task HandleRegistration() { var result = await AuthService.Register(user); message = result.Message; if (result.Success) errormessage = Result.message; else errormessage = String.Empty; } } ---
  • 172. 登録後に成功のメッセージを表⽰する - 1 • テスト • chrome 開発ツール • Register ボタンを押す ときに Fetch/XHR を 観察 • success Server → Services → AuthService → AuthService.cs --- public async Task<ServiceResponse<int>> Register(User user, string password) { if (await UserExists(user.Email)) { return new ServiceResponse<int> { Success = false, Message = "このユーザーは既に存在しています。” }; } CreatePasswordHash(password, out byte[] passwordHash, out byte[] passwordSalt); user.PasswordHash = passwordHash; user.PasswordSalt = passwordSalt; _context.Users.Add(user); await _context.SaveChangesAsync(); return new ServiceResponse<int> { Data = user.Id, Message = "登録が成功しました!!" }; } ---
  • 173. 登録後に成功のメッセージを表⽰する - 2 • テスト • chrome 開発ツール • Register ボタンを押す ときに Fetch/XHR を 観察 • success Client → Register.razor --- async Task HandleRegistration() { var result = await AuthService.Register(user); message = result.Message; if (result.Success) errormessage = Result.message; else errormessage = String.Empty; } } --- @code { UserRegister user = new UserRegister(); string message = string.Empty; string messageCssClass = string.Empty; ---
  • 174. 登録後に成功のメッセージを表⽰する - 3 • テスト • chrome dev tool • Register ボタンを押す ときに Fetch/XHR を ⾒る • 存在する email アドレ ス → "このユーザーは 既に存在しています。” • 存在しない email アド レス → "登録が成功 しました!!" Client → Register.razor --- <div class="@messageCssClass"> <span>@message</span> </div> --- @code { UserRegister user = new UserRegister(); string message = string.Empty; string messageCssClass = string.Empty; async Task HandleRegistration() { var result = await AuthService.Register(user); message = result.Message; if (result.Success) messageCssClass = "text-success"; else messageCssClass = "text-danger"; } ------
  • 175. UserLogin モデルの追加 • Shared → UserLogin.cs 作成 Shared → UserLogin.cs --- using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text; using System.Threading.Tasks; namespace BlazorEcommerceApp.Shared { public class UserLogin { [Required] public string Email { get; set; } = string.Empty; [Required] public string Password { get; set; } = string.Empty; } } ---
  • 176. ログインページの追加 - 1 • Client → Pages → Login.razor 作成 Client → Pages → Login.razor --- @page "/login" <PageTitle>Login</PageTitle> <h3>ログイン</h3> @code { private UserLogin User = new UserLogin(); private async Task HandleLogin() { Console.WriteLine("ログインさせてください! :)" ); } } ---
  • 177. ログインページの追加 - 2 • Register.razor から 上の部分をコピペ • DEMO chrome dev tool • Register ボタンを押す ときに Console 観察 • 既存の email アドレス → Console.WriteLine( "ログインさせてくださ い! :)" Client → Pages → Login.razor --- <EditForm Model="user" OnValidSubmit="HandleLogin"> //ここだけHandleLoginに変更する <DataAnnotationsValidator /> <div class="mb-3"> <label for="e-mail アドレス">Email</label> <InputText id="email" @bind-Value="user.Email" class="form-control" /> <ValidationMessage For="@(() => user.Email)" /> </div> <div class="mb-3"> <label for="パスワード">Password</label> <InputText id="password" @bind-Value="user.Password" class="form- control" type="password" /> <ValidationMessage For="@(() => user.Password)" /> </div> // confirm password は削除 <button type="submit" class="btn btn-primary">Login</button> //Login に変更 </EditForm> // message class は削除 <div class="text-danger"> <span>@errorMessage</span> </div> ---
  • 178. サーバーにログインする準備をする - 1 • Server → appsettings.json 編集 Client → Pages → Login.razor --- { "ConnectionStrings": { "DefaultConnection":--- --- }, //このセクションを追加 "AppSettings": { "Token": "my top secret key" }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*" } ---
  • 179. サーバーにログインする準備をする - 2 • Server → Services → AuthService Server → Services → AuthService → IAuthService --- namespace BlazorEcommerceApp.Server.Services.AuthService { public interface IAuthService { Task<ServiceResponse<int>> Register(User user, string password); Task<bool> UserExists(string email); //ここを追加 Task<ServiceResponse<string>> Login(string email, string password); } } ---
  • 180. サーバーにログインする準備をする - 3 • Server → Services → AuthService Server → Services → AuthService → AuthService.cs --- //⾃動的にインプリされる === --- public async Task<ServiceResponse<string>> Login(UserLogin request) { var response = new ServiceResponse<string> { Data = "token"; }; return response; --- ---
  • 181. サーバーにログインする準備をする - 4 • Server → Controllers → AuthContorller Server → Controllers → AuthContorller.cs --- //追加 [HttpPost("login")] public async Task<ActionResult<ServiceResponse<string>>> Login(UserLogin request) { var response = await _authService.Login(request.Email, request.Password); if (!response.Success) { return BadRequest(response); } return Ok(response); } ---
  • 182. ユーザーのパスワードを検証する - 1 • Server → Controllers → AuthContorller Server → Services → AuthService → AuthService.cs --- public async Task<ServiceResponse<string>> Login(string email, string password) { var response = new ServiceResponse<string>(); var user = await _context.Users .FirstOrDefaultAsync(x => x.Email.ToLower().Equals(email.ToLower())); if (user == null) { response.Success = false; response.Message = "ユーザーが⾒つかりません!"; } else if (!VerifyPasswordHash(password, user.PasswordHash, user.PasswordSalt)) { response.Success = false; response.Message = "パスワードが間違っています。"; } else { response.Data = CreateToken(user); } return response; } ---
  • 183. ユーザーのパスワードを検証する - 2 • Server → Controllers → AuthContorller Server → Services → AuthService → AuthService.cs --- private bool VerifyPasswordHash(string password, byte[] passwordHash, byte[] passwordSalt) { using (var hmac = new HMACSHA512(passwordSalt)) { var computedHash = hmac.ComputeHash (System.Text.Encoding.UTF8.GetBytes(password)); return computedHash.SequenceEqual(passwordHash); } } ---
  • 184. JSON Web Token の作成 - 1 • JSON Web Token Server → Services → AuthService → AuthService.cs --- //Login メソッド public async Task<ServiceResponse<string>> Login(string email, string password) { var response = new ServiceResponse<string>(); var user = await _context.Users .FirstOrDefaultAsync(x => x.Email.ToLower().Equals(email.ToLower())); if (user == null) { response.Success = false; response.Message = "ユーザーが⾒つかりません!"; } else if (!VerifyPasswordHash(password, user.PasswordHash, user.PasswordSalt)) { response.Success = false; response.Message = "パスワードが間違っています。"; } else { response.Data = CreateToken(user); //ここは appsettings.json を参照 } return response; }---
  • 185. JSON Web Token の作成 - 2 • JSON Web Token Server → Services → AuthService → AuthService.cs --- public class AuthService : IAuthService { private readonly DataContext _context; public AuthService(DataContext context, IConfiguration configuration, IHttpContextAccessor httpContextAccessor) { _context = context; _configuration = configuration; _httpContextAccessor = httpContextAccessor; } --- ---
  • 186. JSON Web Token の作成 - 3 • DEMO • localhost:(port number)/Swagger/ index.html • Auth • POST Register • POST Login Server → Services → AuthService → AuthService.cs --- private string CreateToken(User user) { List<Claim> claims = new List<Claim> { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim(ClaimTypes.Name, user.Email), }; var key = new SymmetricSecurityKey(System.Text.Encoding.UTF8 .GetBytes(_configuration.GetSection("AppSettings:Token").Value)); // appsettings.json 参照 var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature); var token = new JwtSecurityToken( claims: claims, expires: DateTime.Now.AddDays(1), signingCredentials: creds); var jwt = new JwtSecurityTokenHandler().WriteToken(token); return jwt; } --- 1. user 名違い 2. password 違い 実⾏してメッセージを確認 その上で正しい内容を⼊れて Token を Response Bodyで確認 コピーして jwt.io でペースト decode された内容が確認できる
  • 187. クライアントでログインを実装する - 1 • Client → Services → AuthService → IAuthService.cs Client → Services → AuthService → IAuthService.cs --- namespace BlazorEcommerceApp.Client.Services.AuthService { public interface IAuthService { --- Task<ServiceResponse<string>> Login(UserLogin request); --- } } ---
  • 188. クライアントでログインを実装する - 2 • Client → Services → AuthService → IAuthService.cs Client → Services → AuthService → IAuthService.cs --- namespace BlazorEcommerceApp.Client.Services.AuthService { public interface IAuthService { --- Task<ServiceResponse<string>> Login(UserLogin request); --- } } ---
  • 189. クライアントでログインを実装する - 3 • Client → Services → AuthService → AuthService.cs Client → Services → AuthService → AuthService.cs --- public class AuthService : IAuthService //右クリックして Implement Interface で Login メソッドを⽣成 --- //Register メソッドの中⾝をコピーしてペーストして Login に、型を string に 修正 --- public async Task<ServiceResponse<string>> Login(UserLogin request) { var result = await _http.PostAsJsonAsync("api/auth/login", request); return await result.Content.ReadFromJsonAsync <ServiceResponse<string>>(); --- ---
  • 190. クライアントでログインを実装する - 4 • Login.razor に移動 Client → pages → Login.Razor --- @page "/login" @inject IAuthService AuthService @inject ILocalStorageService LocalStorage @inject NavigationManager NavigationManager //上記を追加 --- //error message を追加 --- <div class="text-danger"> <span>@errorMessage</span> </div> --- @code { --- ---
  • 191. クライアントでログインを実装する - 5 • Login.razor に移動 Client → pages → Login.Razor //HandleLogin を書き換え --- private async Task HandleLogin() { var result = await AuthService.Login(user); if (result.Success) { errorMessage = string.Empty; await LocalStorage.SetItemAsync("authToken", result.Data); NavigationManager.NavigateTo(); } else { errorMessage = result.Message; } }--- ---
  • 192. クライアントでログインを実装する - 5 • Login.razor に移動 Client → pages → Login.Razor //HandleLogin を書き換え --- private async Task HandleLogin() { var result = await AuthService.Login(user); if (result.Success) { errorMessage = string.Empty; await LocalStorage.SetItemAsync("authToken", result.Data); NavigationManager.NavigateTo(); } else { errorMessage = result.Message; } } ---
  • 193. クライアントでログインを実装する - 6 • テスト 実⾏ • Chrome Dev Tool • Fetch/XHR • login ボタンで正式な ユーザーでログインする • header Payload で パスワードを確認する • Preview タブで token を確認する • localstorage に変更 して中⾝を確認する • このトークンを再びコピー して • JWT.IO にペーストする • 中⾝が確認できる Shared → UserButton.razor --- //この辺の中⾝を確認 --- <NotAuthorized> <a href="[email protected](NavigationM anager.Uri)" class="dropdown-item">Login</a> <a href="register" class="dropdown-item">Register</a> </NotAuthorized> ---
  • 194. カスタム AuthenticationStateProvider の実装 - 1 • AuthenticationStateProvider とは︖ • 認証状態を取得するためにカスケード接続された認証状態コンポーネントによって使⽤される基礎的な サービス • ユーザーの認証の現在の状態を提供 • この情報を使って動作するコンポーネントのひとつに Authorized View がある • 使うには︖ • Nuget Package Manager からインストールする必要あり • パッケージ名︓ Microsoft.AspNetCore.Components.Authorization
  • 195. カスタム AuthenticationStateProvider の実装 – 2 • Cient → wwwroot → _imports.razor Client → wwwroot → _Imports.razor --- @using Microsoft.AspNetCore.Components.Authorization; ---
  • 196. カスタム AuthenticationStateProvider の実装 – 3 • Cient → CustomAuthState Provider.cs Cient → CustomAuthStateProvider.cs --- public class CustomAuthStateProvider : AuthenticationStateProvider { public override async Task<AuthenticationState> GetAuthenticationStateAsync() { string authToken = await _localStorageService. GetItemAsStringAsync("authToken"); --- using Microsoft.AspNetCore.Components.Authorization; ---
  • 197. カスタム AuthenticationStateProvider の実装 – 4.1 • Cient → CustomAuthState Provider.cs Cient → CustomAuthStateProvider.cs --- public class CustomAuthStateProvider : AuthenticationStateProvider { private readonly ILocalStorageService _localStorageService; private readonly HttpClient _http; public CustomAuthStateProvider(ILocalStorageService localStorageService, HttpClient http) { _localStorageService = localStorageService; _http = http; } public override async Task<AuthenticationState> GetAuthenticationStateAsync() { string authToken = await _localStorageService.GetItemAsStringAsync("authToken"); var identity = new ClaimsIdentity(); _http.DefaultRequestHeaders.Authorization = null; ---
  • 198. カスタム AuthenticationStateProvider の実装 – 4.2 • Cient → CustomAuthState Provider.cs Cient → CustomAuthStateProvider.cs --- --- if (!string.IsNullOrEmpty(authToken)) { try { identity = new ClaimsIdentity(ParseClaimsFromJwt(authToken), "jwt"); _http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authToken.Replace("¥"", "")); } catch { await _localStorageService.RemoveItemAsync("authToken"); identity = new ClaimsIdentity(); } } var user = new ClaimsPrincipal(identity); var state = new AuthenticationState(user); NotifyAuthenticationStateChanged(Task.FromResult(state)); return state; } ---
  • 199. カスタム AuthenticationStateProvider の実装 – 5 • Cient → CustomAuthState Provider.cs Cient → CustomAuthStateProvider.cs --- private byte[] ParseBase64WithoutPadding(string base64) { switch (base64.Length % 4) { case 2: base64 += "=="; break; case 3: base64 += "="; break; } return Convert.FromBase64String(base64); } === private IEnumerable<Claim> ParseClaimsFromJwt(string jwt) { var payload = jwt.Split('.')[1]; var jsonBytes = ParseBase64WithoutPadding(payload); var keyValuePairs = JsonSerializer .Deserialize<Dictionary<string, object>>(jsonBytes); var claims = keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString())); return claims; } ---
  • 200. 認証状態の公開 - 1 • Client → Program.cs Client → Program.cs --- global using Microsoft.AspNetCore.Components.Authorization; --- builder.Services.AddOptions(); builder.Services.AddAuthorizationCore(); builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>(); //を追加 ---
  • 201. 認証状態の公開 - 2 • Client → Program.cs Client → App.razor --- <CascadingAuthenticationState> --- </CascadingAuthenticationState> //これで囲む ---
  • 202. 認証状態の公開 - 3 • Client → App.razor Client → App.razor --- //これで囲む <CascadingAuthenticationState> --- </CascadingAuthenticationState> --- //Authorized Viewに置き換える <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(ShopLayout)"> <NotAuthorized> <h3>Whoops! You're not allowed to see this page.</h3> <h5>Please <a href="login">login</a> or <a href="register">register</a> for a new account.</h5> </NotAuthorized> </AuthorizeRouteView> ---
  • 203. AuthorizedView コンポーネントでログアウトオプションを構築 - 1 • Client → Pages → Login.Razor Client → Pages → Login.Razor --- private async Task HandleLogin() { --- await AuthenticationStateProvider.GetAuthenticationStateAsync(); --- @inject AuthenticationStateProvider AuthenticationStateProvider ---
  • 204. AuthorizedView コンポーネントでログアウトオプションを構築 - 2 • Client → Shared → UserButton.razor Client → Shared → UserButton.razor --- @inject ILocalStorageService LocalStorage @inject AuthenticationStateProvider AuthenticationStateProvider @inject NavigationManager NavigationManager //を追加 --- private async Task Logout() { await LocalStorage.RemoveItemAsync("authToken"); await AuthenticationStateProvider.GetAuthenticationStateAsync(); NavigationManager.NavigateTo(""); } ---
  • 205. AuthorizedView コンポーネントでログアウトオプションを構築 - 2 • Client → Shared → UserButton.razor • テスト chrome Dev Tools Application タブ LocalStorage ペイン Client → Shared → UserButton.razor --- <div class="dropdown-menu dropdown-menu-right @UserMenuCssClass"> <AuthorizeView> <Authorized> <a href="profile" class="dropdown-item">Profile</a> <a href="orders" class="dropdown-item">Orders</a> <hr /> <AdminMenu /> <button class="dropdown-item" @onclick="Logout">Logout</button> </Authorized> <NotAuthorized> <a href="login" class="dropdown-item">Login</a> <a href="register" class="dropdown-item">Register</a> </NotAuthorized> </AuthorizeView> </div> ---
  • 206. ログインに戻り先 URL を追加する - 1 • Microsoft.AspNet Core.WebUtilities Client → Shared → UserButton.razor --- <div class="dropdown-menu dropdown-menu-right @UserMenuCssClass"> <AuthorizeView> <Authorized> <a href="profile" class="dropdown-item">Profile</a> <a href="orders" class="dropdown-item">Orders</a> <hr /> <AdminMenu /> <button class="dropdown-item" @onclick="Logout">Logout</button> </Authorized> <NotAuthorized> <a href="login?returnUrl=@NavigationManager. ToBaseRelativePath(NavigationManager.Uri)" class="dropdown-item">Login</a> <a href="register" class="dropdown-item">Register</a> </NotAuthorized> </AuthorizeView> </div> ---
  • 207. ログインに戻り先 URL を追加する - 2 • Client → Pages → Login.razor Client → Pages → Login.razor --- @code { private UserLogin user = new UserLogin(); private string errorMessage = string.Empty; //追加 private string returnUrl = string.Empty; //追加 protected override void OnInitialized() { var uri = NavigationManager. ToAbsoluteUri(NavigationManager.Uri); if (QueryHelpers.ParseQuery(uri.Query). TryGetValue("returnUrl", out var url)) { returnUrl = url; } } ---
  • 208. ログインに戻り先 URL を追加する - 3 • Client → Pages → Login.razor Client → Pages → Login.razor --- private async Task HandleLogin() { var result = await AuthService.Login(user); if (result.Success) { errorMessage = string.Empty; await LocalStorage.SetItemAsync("authToken", result.Data); await AuthenticationStateProvider. GetAuthenticationStateAsync(); //returnURL を追加 NavigationManager.NavigateTo(returnUrl); } else { errorMessage = result.Message; } } ---
  • 209. ユーザープロファイルページを作成する - 1 • Client → Pages → Profile.razor 作成 Client → Pages → Profile.razor --- @page "/profile" <AuthorizeView> <h3>こんにちは!あなたは <i>@context.User.Identity.Name</i>.</h3> </AuthorizeView> @code { } ---
  • 210. ユーザープロファイルページを作成する - 2 • Client → Shared → UserButton.razor Client → Shared → UserButton.razor --- <div class="dropdown"> <button @onclick="ToggleUserMenu" @onfocusout="HideUserMenu" class="btn btn-secondary dropdown-toggle user-button"> <i class="oi oi-person"></i> </button> <div class="dropdown-menu dropdown-menu-right @UserMenuCssClass"> <AuthorizeView> <Authorized> //Profileに変更 <a href="profile" class="dropdown-item">Profile</a> <a href="orders" class="dropdown-item">Orders</a> <hr /> <AdminMenu /> <button class="dropdown-item" @onclick="Logout">Logout</button> </Authorized> <NotAuthorized> <a href="login?returnUrl=@NavigationManager. ToBaseRelativePath(NavigationManager.Uri)" class="dropdown-item">Login</a> <a href="register" class="dropdown-item">Register</a> </NotAuthorized> </AuthorizeView> </div> </div> ---
  • 211. クライアントの [Authorize] 属性の活⽤ • Client → wwwroot → _Imports.razor • テスト Chrome Dev Tools Application タブ LocalStorage ペイン Client → Shared → UserButton.razor --- //追加 --- @using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Authorization --- === Client → Pages → Profile.razor === //追加 @attribute [Authorize] === Client → wwwroot → App.razor === //追加 --- <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(ShopLayout)"> <NotAuthorized> <h3>Whoops! You're not allowed to see this page.</h3> <h5>Please <a href="login">login</a> or <a href="register">register</a> for a new account.</h5> </NotAuthorized> </AuthorizeRouteView> --- ---
  • 212. UserChangePassword モデルを追加する • Shared → UserChange Password.cs Shared → UserChangePassword.cs --- using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text; using System.Threading.Tasks; namespace BlazorEcommerceApp.Shared { public class UserChangePassword { [Required, StringLength(100, MinimumLength = 6)] public string Password { get; set; } = string.Empty; [Compare("Password", ErrorMessage = "The passwords do not match.")] public string ConfirmPassword { get; set; } = string.Empty; } } ---
  • 213. サーバーのパスワードを変更する - 1 • Server → Services → AuthService → IAuthService.cs Server → Services → AuthService → IAuthService.cs --- namespace BlazorEcommerceApp.Client.Services.AuthService { public interface IAuthService { //追加 --- Task<ServiceResponse<string>> Login(UserLogin request); Task<ServiceResponse<bool>> ChangePassword(UserChangePassword request); --- } }---
  • 214. サーバーのパスワードを変更する - 2 • Server → Services → AuthService → AuthService.cs Server → Services → AuthService → AuthService.cs //インターフェイスから ChangePassword メソッドを⾃動⽣成 --- public async Task<ServiceResponse<bool>> ChangePassword(int userId, string newPassword) { var user = await _context.Users.FindAsync(userId); if (user == null) { return new ServiceResponse<bool> { Success = false, Message = "User not found." }; } CreatePasswordHash(newPassword, out byte[] passwordHash, out byte[] passwordSalt); user.PasswordHash = passwordHash; user.PasswordSalt = passwordSalt; await _context.SaveChangesAsync(); return new ServiceResponse<bool> { Data = true, Message = "Password has been changed." }; } ---
  • 215. 認証⽤ミドルウェアの追加 • Server → Program.cs • サービスパイプラインに 追加 Server → Program.cs --- //追加 --- builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8 .GetBytes(builder.Configuration.GetSection("AppSettings:Token").Value)), ValidateIssuer = false, ValidateAudience = false }; }); --- app.UseAuthentication(); app.UseAuthorization(); ---
  • 216. AuthController でパスワード変更を実装する • Server → Controllers → AuthController → AuthController.cs Server → Program.cs --- //追加 [HttpPost("change-password"), Authorize] public async Task<ActionResult<ServiceResponse<bool>>> ChangePassword([FromBody] string newPassword) { var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); var response = await _authService.ChangePassword(int.Parse(userId), newPassword); if (!response.Success) { return BadRequest(response); } return Ok(response); } ---
  • 217. クライアント側でパスワード変更を実装する • Client → Services → AuthService → IAuthService.cs • Client → Services → AuthService → AuthService.cs Client → Services → AuthService → IAuthService.cs --- //追加 namespace BlazorEcommerceApp.Client.Services.AuthService { public interface IAuthService { --- Task<ServiceResponse<bool>> ChangePassword(UserChangePassword request); --- } } === Client → Services → AuthService → AuthService.cs === //Interface から ChangePassword メソッドを⾃動⽣成 --- public async Task<ServiceResponse<bool>> ChangePassword(UserChangePassword request) { var result = await _http.PostAsJsonAsync("api/auth/change-password", request.Password); return await result.Content.ReadFromJsonAsync<ServiceResponse<bool>>(); } ---
  • 218. プロファイルページでユーザーのパスワードを変更する - 1 • Client → Pages → Profile.razor Client → Pages → Profile.razor --- @page "/profile" //追加 @inject IAuthService AuthService @attribute [Authorize] --- @code { //追加 UserChangePassword request = new UserChangePassword(); string message = string.Empty; private async Task ChangePassword() { var result = await AuthService.ChangePassword(request); message = result.Message; } } ---
  • 219. プロファイルページでユーザーのパスワードを変更する - 2 • Client → Pages → Profile.razor • テスト • chrome Dev Tools Console タブを開く • /profile ページ • すでにログイン済み • change password で実際にパスワード変更 • ⼀度ログアウトして再度 ログイン Client → Pages → Profile.razor --- //追加 <AuthorizeView> <h3>こんにちは!あなたは <i>@context.User.Identity.Name</i>.</h3> </AuthorizeView> <h5>送付先住所</h5> <AddressForm /> <p></p> <h5>パスワード変更</h5> <EditForm Model="request" OnValidSubmit="ChangePassword"> <DataAnnotationsValidator></DataAnnotationsValidator> <div class="mb-3"> <label for="password">New Password</label> <InputText id="password" @bind-Value="request.Password" class="form-control" type="password" /> <ValidationMessage For="@(() => request.Password)" /> </div> <div class="mb-3"> <label for="confirmPassword">Confirm New Password</label> <InputText id="confirmPassword" @bind-Value="request.ConfirmPassword" class="form-control" type="password" /> <ValidationMessage For="@(() => request.ConfirmPassword)" /> </div> <button type="submit" class="btn btn-primary">Apply</button> </EditForm> @message ---
  • 221. まとめ l 前回までの復習 l Blazor 概要 l 今回作成する Web アプリケーションの概要 l Blazor WebAssembly プロジェクト作成 l Web API コントローラー追加、モデル追加 l Entity Framework による Code First データベース作成 l 商品サービス、商品リスト、カテゴリーサービス等必要なサービス、CRUD 処理等の実装 l 検索サービスの追加と検索コンポーネントの実装、カートサービス、UI/UX の変更 l 認証・ユーザー登録、その他の機能の実装
  • 222. リソース l セッションでご紹介した EC アプリ .NET 5版ですが、参考にさせて戴きました。 l https://github.com/patrickgod/BlazorEcommercePreviewYT
  • 223. Elastic x mabl 共同セミナー (7/29 15:00~16:00) https://www.elastic.co/jp/virtual-events/elastic-mabl-webinar デジタルカスタマーエクスペリエンスの向上 〜 Elastic と mabl で実現する、ユーザー視点の アプリケーション Observability 〜
  • 225. Thank you for your attention!
  • 227. 今回のデモアプリのイメージ Azure SQL Database Elastic Cloud 東⽇本リージョン マスターノード x 1 データノード x 2 ML ノード x 1 https://f79...c67.japaneast .azure.elastic- cloud.com:9243/ 全⽂検索クエリ CRUD 検索・更新 UI APM .NET Agent Blazor WebAssembly Azure サブスクリプション Visual Studio 2022 Azure App Service Elastic APM Endpoint に送信 Azure Data Explorer ASP.NET 6 Web API AntDesign
  • 228. // .NETアプリへの Nuget パッケージインストール dotnet add Elastic.Apm.NetCoreAll Install-Package -ProjectName BlazorApp.Server -Id Elastic.Apm.NetCoreAll Elastic APM for ASP .NET Core https://www.elastic.co/jp/apm/ Configuration on .NET Core https://www.elastic.co/guide/en/apm/agent/dotnet/current/configuration-on-asp-net-core.html ASP .NET Core Quick Start https://www.elastic.co/guide/en/apm/agent/dotnet/current/setup-asp-net-core.html // Program.cs へ の 追加 --- using Elastic.Apm.NetCoreAll; //Elastic APM 追加 app.UseAllElasticApm(builder.Configuration); app.UseHttpsRedirection(); app.UseBlazorFrameworkFiles(); app.UseStaticFiles(); --- // appsettings.json の 更新 --- { "Logging": { "LogLevel": { //"Default": "Information", //"Microsoft": "Warning", //"Microsoft.Hosting.Lifetime": "Information" "Default": "Warning", "Elastic.Apm": "Debug" } } , "AllowedHosts": " * " , //Elastic ポ ー タ ル か ら APM エ ン ド ポ イ ン ト とSecret を コ ピ ー & ペー ス ト "ElasticApm": { "ServerUrl": "https:// 7d39255475bg8e8e0j99fm870kj48v88.apm. japaneast.azure.elastic-cloud.com", "SecretToken": ”f6p81KJytBcGMK2JKS4", "TransactionSampleRate": 1.0 } }
  • 229. Elastic Cloud → Kibana で APM モニタリング https://cloud.elastic.co/home
  • 231. AntDesign • ⼈気 No.1 on Awesome Blazor • 企業向け製品のための デザインシステム • 効率的で楽しいワーク エクスペリエンスを実現 https://antblazor.com/en-US/ Install-Package -ProjectName BlazorWASMApp.Client -Id AntDesign
  • 232. AntDesign • Components • Image の使⽤⽅法を 参照 • Source Code 利⽤ 可能 https://antblazor.com/en-US/components/image
  • 234. まとめ l .NET 6 における Blazor Update l ASP.NET Core Web API を構築 l Blazor WebAssembly でフロントエンドアプリを構築 l Elastic APM によるアプリケーションの監視
  • 235. .NET MAUI Blazor App - モバイル、デスクトップ、 Web ハイブリッドアプリを開発 https://qiita.com/shosuz/items/4218af93343e5cc999ec
  • 236. ASP.NET Core Blazor WebAssembly と Web API と Entity Framework Core で SQL Server のデータを取得したり追加したり更新したり削除したりする [.NET 6 版] https://qiita.com/tamtamyarn/items/876a5cd4b9ec9cdc1044
  • 237. Elastic リソース • 公式ドキュメント https://www.elastic.co/guide/index.html • クラウドネイティブ アプリでの Elasticsearch https://docs.microsoft.com/ja-jp/dotnet/architecture/cloud- native/elastic-search-in-azure • Azure での検索データ ストアの選択 https://docs.microsoft.com/ja-jp/azure/architecture/data- guide/technology-choices/search-options • Elastic APM Agent https://www.elastic.co/guide/en/apm/agent/index.html • APM https://www.elastic.co/jp/apm/ • Configuration on .NET Core https://www.elastic.co/guide/en/apm/agent/dotnet/current/co nfiguration-on-asp-net-core.html • ASP.NET Core Quick Start https://www.elastic.co/guide/en/apm/agent/dotnet/current/set up-asp-net-core.html
  • 238. Thank you for your attention!