2.5K Views
July 23, 22
スライド概要
C#, .NET 6, Blazor WebAssembly, ASP. NET Web API, Azure によるアプリ開発 – その4
2月~4月の .NET ラボ勉強会でご紹介した Blazor アプリ開発のうち、積み残していた認証・ユーザー登録機能の実装、その他について、解説します。
https://dotnetlab.connpass.com/event/252711/
FPT ジャパン エグゼクティブエバンジェリスト 独立行政法人 国立印刷局 デジタル統括アドバイザー兼最高情報セキュリティアドバイザー Microsoft で13年間、テクニカルエバンジェリストとして .NET、C#、Visual Studio、Windows、iOS、Android、Microsoft Azure 等の開発者向け技術啓発活動 (DevRel) 。Dell、Accenture、Elastic、VMware 等での DevRel 後、2024年11月1日より現職で DevRel 活動を開始。NVIDIA との戦略的協業 AI GPU クラウド、Azure/AWS/GC 上の AI &データ関連サービスのマーケティング、プリセールス、教育、関連新規サービス開発。元内閣官房 IT 総合戦略室 政府 CIO 補佐官(兼務)、元デジタル庁 ソリューションアーキテクト(兼務)。
C#, .NET 6, Blazor WebAssembly, ASP.NET Web API, Azure による アプリ開発 – その4 鈴⽊ 章太郎 Elastic テクニカルプロダクトマーケティングマネージャー/エバンジェリスト デジタル庁 省庁業務グループ ソリューションアーキテクト
Shotaro Suzuki Twitter : @shosuz Elastic Technical Product Marketing Manager/Evangelist デジタル庁 省庁業務グループ ソリューションアーキテクト 元 Microsoft Technical Evangelist
l アジェンダ l l l l l 前回までの復習 l Blazor 概要 l 今回作成する Web アプリケーションの概要 l Blazor WebAssembly プロジェクト作成 l Web API コントローラー追加、モデル追加 Entity Framework による Code First データベース作成 商品サービス、商品リスト、カテゴリーサービス等必要なサービス、 CRUD 処理等の実装 検索サービスの追加と検索コンポーネントの実装 UI/UX の変更、カートサービス 認証・ユーザー登録機能、その他の実装 (p.151-p.219)
今回の範囲 l l 2⽉、3⽉、4⽉の復習 認証・ユーザー登録機能の実装、その他 (p.151-p.219) セッションでご紹介した EC アプリ .NET 5版ですが、参考にさせて戴きました。 https://github.com/patrickgod/PreviewYT
Blazor 概要
Modern Web UI with .NET & Blazor HTML、CSS、.NET、C#... JavaScript の代わりに Open Web 標準でアプリ開発 Server WebAssembly どこにでもホストできる Hybrid
Part of the ASP.NET Core family Web UI Services MVC Razor Pages HTTP APIs SignalR SPA Blazor Worker gRPC
Blazor – .NET 5 まで Blazor Server Blazor WebAssembly Blazor DOM SignalR .NET Razor Components Blazor DOM .NET Razor Components WebAssembly ü DB アクセス含むサーバー機能へのフルアクセス ü ⾼速なスタートアップ ü コードがサーバーから離れない ü 古いブラウザとシンクライアントをサポート ü完全にクライアント側で実⾏ ü必要なサーバー コンポーネントなし ü静的サイトとしてホスト üオフラインで実⾏可能 ü 永続的な接続が必要 ü⼤きなダウンロードサイズ ü UI の遅延が⾼い üランタイムパフォーマンスの低下 Blazor Server (.NET 5) Blazor WebAssembly (.NET 5)
Blazor – .NET 6 による強化 Blazor Server Blazor WebAssembly Blazor DOM SignalR .NET Razor Components Blazor DOM .NET Razor Components WebAssembly .NET 6 Blazor WebAssembly の事前 (AOT) コンパイル対応 Blazor WebAssembly アプリのダウンロードサイズの縮⼩ Error Boundaries Razor コンポーネント型の推論とジェネリック型の制約 動的コンポーネント プリレンダリング中の Blazor コンポーネント状態の永続性 Hot Reload, Native File Reference, 他多数
Blazor Server と Blazor WebAssembly の 開発モデルの違い Blazor WebAssembly Blazor Server Blazor Blazor DOM SignalR .NET Razor Components DOM .NET Razor Components WebAssembly 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 バイナリコードへのコンパイラなどのツールセットが必要 C++ WASM コンパイラ Edge C++ ソースコード Rust ソースコード Chrome Rust WASM コンパイラ C WASM コンパイラ SQLite ソースコード(C) Safari Web Assembly バイナリコード (W3C 標準技術) Firefox
.NET 6 における Blazor WebAssembly 新機能 • 事前 (AOT) 実⾏コンパイル • カスタム要素 • ⼩規模なアプリサイズ • Native File Reference • Hot Reload • Component, .NET, HTML, CSS… …その他数⼗個の更新あり
Blazor WebAssembly ⼩規模なアプリサイズ .NET 5 .NET 6 • Publish size: 1.7 MB • Publish size: 1.0 MB • ~40% size reduction
Blazor WebAssembly のホスティング Blazor Blazor WebAssem WebAssembly bly APIs APIs ASP.NET App Services Blazor Blazor WebAssem WebAssembly bly Globally Globally distributed distributed hosting hosting APIs Serverless Microservices functions Azure Static Web Apps
Get started with Blazor • Go to https://blazor.net • Install the .NET SDK Visual Studio Visual Studio for Mac Visual Studio Code + C# extension • .NET Conf 2021 https://www.dotnetconf.net/ • .NET Conf 2021 – videos/slides/demos https://github.com/dotnet-presentations/dotNETConf/tree/master/2021/MainEvent/Technical
今回作成する 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 App Service Blazor WebAssembly CRUD 全⽂検索クエリ Blazor Server Azure SQL Database Elastic APM Endpoint に送信 検索・更新 UI APM .NET Agent Elastic Cloud Visual Studio 2022 for Mac https://f79...c67.japaneast .azure.elasticcloud.com:9243/ Azure Data Studio 東⽇本リージョン マスターノード x 1 データノード x 2 ML ノード x 1 Azure サブスクリプション
EC Demo アプリの構成 2 Blazor WebAssembly CRUD 全⽂検索クエリ Azure Static Web Apps Azure App Service Blazor WebAssembly ASP.NET 6 Web API 検索・更新 UI Elastic APM Endpoint に送信 APM .NET Agent Elastic Cloud Visual Studio 2022 for Mac https://f79...c67.japaneast .azure.elasticcloud.com:9243/ Azure Data Studio Azure SQL Database 東⽇本リージョン マスターノード x 1 データノード x 2 ML ノード x 1 Azure サブスクリプション
ASP.NET Core Blazor のホスティング モデル ホスティング モデルの選択 Blazor サーバー Blazor WebAssembly 完全な .NET Core API の互換性 ✔ ❌ サーバー ソースへの直接アクセス ✔ ❌ ⼩さいペイロード サイズと ⾼速な初期読み込み時間 ✔ ❌ サーバー上でのアプリ コードの セキュリティ保護と⾮公開 ✔ ❌† ダウンロードしたアプリを オフラインで実⾏ ❌ ✔ 静的サイトのホスティング ❌ ✔ クライアントへの処理のオフロード ❌ ✔ https://docs.microsoft.com/ja-jp/aspnet/core/blazor/hosting-models?view=aspnetcore-6.0#blazor-webassembly
Blazor WebAssembly プロジェクト作成
Blazor WebAssembly プロジェクト⽣成 チェックを⼊れる︕
Product Model の追加
Product Model の追加 using using using using using System; System.Collections.Generic; System.Linq; System.Text; 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 インストールその他 --// AddRazorPages の後 builer.Services.AddEndpointApiExploler(); builer.Services.AddSwaggerGen(); //var ap = buildder.Build();の後 app.UserSwaggerUI(); • https://localhost:(ポート番 号)/swagger/index.html // 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 using using using using using using System; System.Collections.Generic; System.ComponentModel.DataAnnotations; System.ComponentModel.DataAnnotations.Schema; System.Linq; System.Text; 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 作成
• DataContext.cs
•
•
Class を追加
DataContext.class
namespace BlazorECommerceApp.Server.Data
{
public class DataContext : DbContext
{
// DataContext を作るのに ctor とタイプするとできる
// 全体的に IntelliCode が補完
public DataContext(DbContextOptions<DataContext>
options) : base(options)
{
• Serverプロジェクト側の Program.cs
修正
•
global using
Microsoft.EntityFramework.Core を⼊
れておくと楽
}
}
}
• Server プロジェクト側の Program.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 • Server Program.cs を開き global using Blazorxxx.Server.Data; を追加 • private readonly DataContext context; ⽣成されるので、これを修正 • しかしこれを⾃動的に実施したい global using Blazorxxx.Server.Data; //context → _context に変更 public ProductController(DataContext context) { _cotext = context; } ツール → オプションから テキストエディタ → 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() 変更 • [HttpGet] GetProduct() 変更 • ProductList • ProductController • DataContext 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 using using using using using System; System.Collections.Generic; System.ComponentModel.DataAnnotations.Schema; System.Linq; System.Text; 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回⽬)
• DataContext.cs
--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"
}
);
•
•
---
Category サービスの Client 側 への実装 - 1 • CategoryServices.cs • namespace Blazorxxxxxxxx.Client.Services.CategoryService { public class CategoryService : ICategoryService { private readonly HttpClient _http; • public CategoryService(HttpClient http) { _http = http; } ---
Category サービスの Client 側 への実装 - 2 • Program.cs //builder.Services.AddScoped<IProductService,ProductSe rvice>();の下に追加 builder.Services.AddScoped<ICategoryService, CategoryService>(); • CategoryService • global using で⼀番上に追加 //⼀番上に追加 global using Blazorxxxxxxxx.Client.Services.CategoryService;
Category サービスの Client 側 への実装 - 3 • _Imports.razor @using Blazorxxxxxxxx.Client.Services.ProductService •
iCategoryServices の実装 • iCategoryServices.cs • 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(); } }
NavMenu への Category の表⽰
• NavMenu.razor
protected override async Task OnInitializedAsync()
{
await CategoryService.GetCategories();
}
• NavMenu.razor の編集
• @inject ICategoryService
CategoryService を冒頭に追加
• @code の後半部分に追加
• NavMenuCssClass の追加
• 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
• iProductService.cs
Task<ServiceResponse<Product>> GetProduct(int productId);
• ProductService.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();
}
Server の Category サービスから Product を取得 - 2
• ProductController.cs
•
•
•
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);
}
---
Client の Category サービスから Product を取得 - 1
• iProductService.cs
Task GetProducts(string? categoryUrl = null);
event Action ProductsChanged;
• iProductService.cs
•
•
•
•
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();
}
Client の Category サービスから Product を取得 - 2
• 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);
}
}
}
Client の Category サービスから Product を取得 - 3 • ProductList.razor • • • • --@code { protected override void OnInitialized() { ProductService.ProductsChanged += StateHasChanged; } --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
--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()
タブは Network
フィルターは Fetch/XHR で実⾏
---
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 を介した検索の実装
•
•
•
•
•
単純にフォワードスラッシュ
開始ページまたは検索
検索テキストまたはカテゴリ 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);
}
}
}
--//を追加
Seach コンポーネント作成 - 1
• すでにあるものを注⼊する
• Product Service
• いくつかの呼び出し
• Navigation Manager
• ユーザーを特定のページに誘導したい
• NavigateTo メソッドを使⽤
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");
}
---
Seach コンポーネント作成 - 2
• すでにあるものを注⼊する
• Product Service
• いくつかの呼び出し
• Navigation Manager
• ユーザーを特定のページに誘導したい
• NavigateTo メソッドを使⽤
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);
}
}
Seach コンポーネント作成 - 3
• すでにあるものを注⼊する
• Product Service
• いくつかの呼び出し
• Navigation Manager
• ユーザーを特定のページに誘導したい
• NavigateTo メソッドを使⽤
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>
Seach コンポーネント作成 - 4 Client → Shared → MainLayout.razor --MainLayout.razor @inherits LayoutComponentBase • すでにあるものを注⼊する • Product Service • いくつかの呼び出し • Navigation Manager • ユーザーを特定のページに誘導したい • NavigateTo メソッドを使⽤ <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> ---
UI/UX の変更
レイアウトの変更 - 1
• MainLayout
• → App.razor で定義
• MainLayout.razor をコピーすると css
もコピーされる
• ShopLayout.razor と
• ShopLayout.razor.css を作成
• App.razor を編集
• ShopLayout をパラメーターに設定
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>
---
レイアウトの変更 - 2
• ShopNavMenu.razor
• ShopNavMenu.razor.css
• 共に編集し最終形にする
• これによって上のナビゲーションメニュー
ボタンができ、左のメニューが消える
• css は⾯倒だがこの機会にある程度
詳しくなると、他のプラットフォームでも
使いこなせる
• css に慣れるためにも Hot Reload
を活⽤してください︕楽しくなります
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>
レイアウトの変更 - 3 Client → Shared → MainLayout.razor --@inherits LayoutComponentBase • ShopNavMenu.razor • ShopNavMenu.razor.css • 共に編集し最終形にする • これによって上のナビゲーションメニュー ボタンができ、左のメニューが消える • css は⾯倒だがこの機会にある程度 詳しくなると、他のプラットフォームでも 使いこなせる • css に慣れるためにも Hot Reload を活⽤してください︕楽しくなります <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> ---
レイアウトの変更 - 4
• MainLayout
• → App.razor で定義
• MainLayout.razor をコピーすると css
もコピーされる
• ShopLayout.razor と
• ShopLayout.razor.css を作成
• App.razor を編集
• ShopLayout をパラメーターに設定
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>
---
レイアウトの変更 - 5
Client → Shared → ShopNavMenu.razor
--@inject ICategoryService CategoryService
@implements IDisposable
--@code {
private bool collapseNavMenu = true;
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
• MainLayout
• → App.razor で定義
• MainLayout.razor をコピーすると css
もコピーされる
• ShopLayout.razor と
• ShopLayout.razor.css を作成
• App.razor を編集
• ShopLayout をパラメーターに設定
private void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
protected override async Task OnInitializedAsync()
{
await CategoryService.GetCategories();
CategoryService.OnChange += StateHasChanged;
}
public void Dispose()
{
CategoryService.OnChange -= StateHasChanged;
}
}
---
HomeButton.razor - 1
Client → Shared → HomuButton.razor
--@inject NavigationManager NavigationManager
• Home Button の配置
• razor の作成
<button @onclick="GoToHome"
class="btn btn-outline-primary home-button">
マイショップ
</button>
@code {
private void GoToHome()
{
NavigationManager.NavigateTo("");
}
}
---
HomeButton.razor - 2 Client → Shared → HomuButton.razor.css --HomeButton.razor.css • Home Button の配置 • css の追加 .home-button { white-space: nowrap; margin-right: 10px; transform: rotate(-5deg); } ---
HomeButton.razor - 3 Client → Shared → ShopLayout.razor --@inherits LayoutComponentBase • Home Button の配置 • ShopLayout.razor の修正 <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> ---
注⽬の商品 - 1
• Featured Products として3つを top
ページにリコメンドして表⽰する
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>();
--}
}
---
注⽬の商品 - 2
• Featured Products として3つを top
ページにリコメンドして表⽰する
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
},
---
注⽬の商品 - 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 dotnet ef database update • Package Manager Console で DB 作成 • Azure Data Studio で確認 • • • dbo.Products Featured 列が増えている フラグが⽴っている
注⽬の商品のローディング - 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 • 新しいコンポーネントを作ってフィーチャード プロダクトを表⽰する • 先に @code 部分から Client → Shared → FeaturedProducts.razor.cs --@inject IProductService ProductService @implements Idisposable --@code { protected override void OnInitialized() { ProductService.ProductsChanged += StateHasChanged; } public void Dispose() { ProductService.ProductsChanged -= StateHasChanged; } } ---
注⽬の商品のローディング – 5
• 新しいコンポーネントを作ってフィーチャード
プロダクトを表⽰する
• @code に続いて View 部分
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>
}---
注⽬の商品のローディング – 6 • FeaturedProducts.razor.css 作成 • Chrome Dev Tool のモバイルビューなど にも切り替えながら検証する • Hot Reload は css にこそ有効 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; } }
注⽬の商品のローディング – 7
• Index.razor を修正
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
}
}
注⽬の商品のローディング – 8
• Client.Services.ProductServices
ProductService.cs の GetProducts
を修正
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();
}
--}
検索結果の
ページネーション - 1
• データベースに多くの製品が登録されている
場合、分割して表⽰させたい
• 1ページに2つの商品を表⽰し、2ページ⽬、
3ページ⽬......と進む
• タイトルと説明⽂だけ表⽰させる
• 商品からデータ転送オブジェクトを作成し、
DTO は商品タイトルと説明⽂だけを返す
• ProductSerchResult.cs という DTO
オブジェクト
• この DTO で、製品のリストを取得し、ペー
ジ数を取得し、情報として現在のページを
取得
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; }
}
}
---
検索結果の
ページネーション - 2
• ProductSerchResult.cs という DTO
オブジェクト
• この DTO で、製品のリストを取得し、ペー
ジ数を取得し、情報として現在のページを
取得
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);
--}
}---
検索結果の
ページネーション - 3
• Server 上のページネーション
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;
}
---
検索結果の
ページネーション - 4
• コントローラにも変更を加える必要あり
• Product コントローラの Search メソッドに、
もうひとつパラメータを追加(Page)
• デフォルトで1に設定
• アプリケーションを起動
• Swagger ページを開く
• 検索テキストを⼊⼒してテスト
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);
}
------
検索結果の
ページネーション - 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 = "商品がみつかりません。";
• Client 側 GetProducts を修正
ProductsChanged.Invoke();}
---
• 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
• ページネーションのコンポーネントへの追加
• Search.razor、 Index.razor を修正
Client → Shared → Search.razor
--public void SearchProducts()
{
--//1 をデフォルト値として追加
NavigationManager.NavigateTo($"search/{searchText}/1");
--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 プロジェクトに NuGet パッケージ 追加 • Blazer Local Storage Client → Program.cs --//下記を追加 --using Blazored.LocalStorage; --builder.Services.AddBlazoredLocalStorage(); --- 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 → Service → ICartService.cs を追加 • Client → Service → CartService.cs を追加 Client → Program.cs --builder.Services.AddScoped<ICategoryService, CategoryService>(); --Client → _Imports.razor --@using BlazorEcommerceApp.Client.Services.CartService ---
クライアント側の
CartService 実装 - 2
• Client → Service →
ICartService.cs を追加
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();
}
}
---
クライアント側の
CartService 実装 - 3
• Client → Service → CartService.cs
を追加
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
{
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> 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
private async Task RemoveProductFromCart(int productId, int productTypeId)
{
await CartService.RemoveProductFromCart(productId, productTypeId);
await LoadCart();
}
• 先に @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 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):
[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>
}
---
Cart ページの実装 - 3 • Client → Pages → Cart.razor.css 実装 Client → Pages → Cart.razor.css --.container { display: flex; padding: 6px; } .image-wrapper { width: 150px; text-align: center; } .cart-product-price { font-weight: 600; text-align: right; } .btn-delete { background: none; border: none; padding: 0px; color: red; font-size: 12px; } .image { max-height: 150px; max-width: 150px; padding: 6px; } .name { flex-grow: 1; padding: 6px; } .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 コンポーネントを追加
して、これを呼び出す
Client → Pages → Register.razor
@page "/register”
• テスト
• https://localhost:(ポート
番号 )/register
<h3>登録</h3>
<PageTitle>Register</PageTitle>
@code {
}
ユーザーメニューボタンの実装 - 1
Shared → UserButton.Razor
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject NavigationManager NavigationManager
• dropdown クラス
• UserMenuCssClass
• dropdown-item :
Register
• showUserMenu
• UserMenuClass
• ToggleUserMenu
• HideUserMenu
<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 Shared → UserButton.Razor.css --.show-menu { display: block; } • .user-button • .top-row a • .dropdownitem:hover .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="formcontrol" 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="formcontrol" 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 → Program.cs
--builder.Services.AddScoped<IAuthService, AuthService>();
• Server → Services
→ AuthService
フォルダ作成
• IAuthService.cs
作成
• Program.cs サービス
パイプラインに登録
• IAuthService 実装
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 開発ツール Client → Register.razor --async Task HandleRegistration() { var result = await AuthService.Register(user); message = result.Message; • Register ボタンを押す ときに Fetch/XHR を 観察 • success 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 --using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text; using System.Threading.Tasks; • Shared → UserLogin.cs 作成 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
--@page "/login"
<PageTitle>Login</PageTitle>
<h3>ログイン</h3>
• Client → Pages →
Login.razor 作成
@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="formcontrol" 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
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
---
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 参照
1. user 名違い
2. password 違い
実⾏してメッセージを確認
var creds = new SigningCredentials(key,
SecurityAlgorithms.HmacSha512Signature);
その上で正しい内容を⼊れて
Token を Response Bodyで確認
コピーして jwt.io でペースト
decode された内容が確認できる
var token = new JwtSecurityToken(
claims: claims,
expires: DateTime.Now.AddDays(1),
signingCredentials: creds);
var jwt = new JwtSecurityTokenHandler().WriteToken(token);
return jwt;
}
---
クライアントでログインを実装する - 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="[email protected](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 → CustomAuthStateProvider.cs
--public class CustomAuthStateProvider : AuthenticationStateProvider
{
private readonly ILocalStorageService _localStorageService;
private readonly HttpClient _http;
• Cient →
CustomAuthState
Provider.cs
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
--@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;
}
}
• Client → Pages →
Login.razor
---
ログインに戻り先 URL を追加する - 3
Client → Pages → Login.razor
--private async Task HandleLogin()
{
var result = await AuthService.Login(user);
if (result.Success)
{
errorMessage = string.Empty;
• Client → Pages →
Login.razor
await LocalStorage.SetItemAsync("authToken", result.Data);
await AuthenticationStateProvider.
GetAuthenticationStateAsync();
//returnURL を追加
NavigationManager.NavigateTo(returnUrl);
}
else
{
errorMessage = result.Message;
}
}
---
ユーザープロファイルページを作成する - 1
Client → Pages → Profile.razor
--@page "/profile"
• Client → Pages →
Profile.razor 作成
<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 → UserChangePassword.cs
--using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
• Shared →
UserChange
Password.cs
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
--namespace BlazorEcommerceApp.Client.Services.AuthService
{
• Server →
Services →
AuthService →
IAuthService.cs
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 --//追加 <AuthorizeView> <h3>こんにちは!あなたは <i>@context.User.Identity.Name</i>.</h3> </AuthorizeView> • Client → Pages → Profile.razor • テスト • chrome Dev Tools Console タブを開く • /profile ページ • すでにログイン済み • change password で実際にパスワード変更 • ⼀度ログアウトして再度 ログイン <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 l l l 前回までの復習 l Blazor 概要 l 今回作成する Web アプリケーションの概要 l Blazor WebAssembly プロジェクト作成 l Web API コントローラー追加、モデル追加 Entity Framework による Code First データベース作成 商品サービス、商品リスト、カテゴリーサービス等必要なサービス、CRUD 処理等の実装 検索サービスの追加と検索コンポーネントの実装、カートサービス、UI/UX の変更 認証・ユーザー登録、その他の機能の実装
リソース l l セッションでご紹介した EC アプリ .NET 5版ですが、参考にさせて戴きました。 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 App Service ASP.NET 6 Web API Azure SQL Database CRUD Blazor WebAssembly AntDesign 全⽂検索クエリ Elastic APM Endpoint に送信 検索・更新 UI APM .NET Agent Elastic Cloud Visual Studio 2022 Azure Data Explorer https://f79...c67.japaneast .azure.elasticcloud.com:9243/ 東⽇本リージョン マスターノード x 1 データノード x 2 ML ノード x 1 Azure サブスクリプション
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
// .NETアプリへの Nuget パッケージインストール
dotnet add Elastic.Apm.NetCoreAll
Install-Package -ProjectName BlazorApp.Server -Id Elastic.Apm.NetCoreAll
// 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 https://antblazor.com/en-US/ • ⼈気 No.1 on Awesome Blazor • 企業向け製品のための デザインシステム • 効率的で楽しいワーク エクスペリエンスを実現 Install-Package -ProjectName BlazorWASMApp.Client -Id AntDesign
AntDesign https://antblazor.com/en-US/components/image • Components • Image の使⽤⽅法を 参照 • Source Code 利⽤ 可能
まとめ
まとめ l l l l .NET 6 における Blazor Update ASP.NET Core Web API を構築 Blazor WebAssembly でフロントエンドアプリを構築 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 リソース • 公式ドキュメント • APM https://www.elastic.co/guide/index.html https://www.elastic.co/jp/apm/ • クラウドネイティブ アプリでの Elasticsearch • Configuration on .NET Core https://docs.microsoft.com/ja-jp/dotnet/architecture/cloudnative/elastic-search-in-azure https://www.elastic.co/guide/en/apm/agent/dotnet/current/co nfiguration-on-asp-net-core.html • Azure での検索データ ストアの選択 • ASP.NET Core Quick Start https://docs.microsoft.com/ja-jp/azure/architecture/dataguide/technology-choices/search-options • Elastic APM Agent https://www.elastic.co/guide/en/apm/agent/index.html https://www.elastic.co/guide/en/apm/agent/dotnet/current/set up-asp-net-core.html
Thank you for your attention!