(18/30)一緒にBlazorを学ぼう:Add()メソッドの修正

(18/30)一緒にBlazorを学ぼう:Add()メソッドの修正

今日、次のような状況を想定します。1つのログがあり、2つ目を追加する前に最初のログを削除しようとすると、何が起こるでしょうか?

最終更新 2021/12/20 23:04
StrayaWorker
読了目安 3 分
カテゴリ
Blazor
テーマ
一緒にBlazorを学ぶシリーズ
タグ
.NET C# ASP.NET Core Blazor

本日、次のような状況を想定してみましょう。ログが1件あり、2件目を追加したがまだコミットしていない状態で、1件目を削除しようとすると、何が起こるでしょうか?

何とエラーが発生しました!削除したいPost.Idをバックエンドに送信しただけなのに、なぜこのようなエラーメッセージが出るのでしょうか?

これはC#の特性に関係しています。C#はオブジェクト指向プログラミング言語です。つまり、データやメソッドを含むあらゆるものをオブジェクトにできます。BlogPostはそれぞれオブジェクトです。このような参照型だけでなく、単純なintboolなどの基本型もあります。

(注:stringは分類上は参照型ですが、構文上は基本型のように扱われます。これは無数のstringによってメモリが溢れるのを防ぐためです。)

基本型の意味は、2つの基本型間の変更が互いに影響を与えないということです。変数int a = 0;を定義し、次にint b = a;を定義すると、bは0になります。ここでb = 3;と代入しても、abは等しくならず、互いに影響しません。下図はLINQPadで示したものです。Dump()はその変数を下のResultsブロックに表示することを意味します。途中でbの値を変更しても、aは影響を受けないことがわかります。

参照型は次のようになります。BオブジェクトがAオブジェクトから派生した場合、どちらのオブジェクトを変更しても、もう一方もそれに応じて変更されます。下図では12行目でBオブジェクトのTitle"BB"に変更したところ、AオブジェクトのTitleも変わっています。

では、これらとBlogにどんな関係があるのでしょうか?バックエンドのBlogRepository.csGetBlog()を見てみましょう。ここでblogが返されており、フロントエンドのBlogBase.razor.csでそれを受け取り、Add()がトリガーされるとBlog.Postsに新しいPostModelが追加されます。

フロントエンドでDeleteボタンをクリックすると、バックエンドのPostRepository.csDeletePost()SaveChanges()をトリガーします。このときのBlog.Postsには、BlogTitleContentを持たないPostModelが含まれています。このデータはまだSubmitボタンをクリックしてバックエンド経由でデータベースに保存されておらず、フロントエンドにしか存在しないデータです。しかし、SaveChanges()がトリガーされると、このデータをデータベースに保存しようとします。TitleContentはnullを許可していないため、当然エラーが発生します。

また、データベースからPostsだけを取得しても、そのデータは見えません。なぜなら、それはBlogに付随するPostModelだからです。

この問題を解決するにはいくつかの方法があります。1つ目は、BlogPostを完全に分離し、それぞれに独自のフロントエンドページを持たせることです。ただし、実際のプロジェクトでこのような落とし穴(そうです、これは筆者が自分で掘った落とし穴です…)に遭遇した場合、このようなリファクタリングを行う時間は通常ありません。

2つ目の方法は、バックエンドのPostRepository.csTitleを持たないPostModelを受け取ったときに、エラーメッセージを返すことです。

フロントエンドのPostBase.razor.csを、deleted.IsSuccessで判定するように変更します。削除が成功した場合はPost!.IdBlogに渡して、そのPostをページから削除します。失敗した場合は失敗の理由を表示します。

エンジニアの観点から見れば、これでエラーは回避できますが、UX(ユーザーエクスペリエンス)の観点から見ると全く意味がわかりません。なぜログを削除する際に、空のログがあってはいけないという制限があるのでしょうか?そこで、3つ目の方法が必要になります。

3つ目は、ViewModelを作成する方法です。ページのCRUDはViewModelに対して行い、その後でModelにマッピングします。

いわゆるViewModelとは、データベースには存在しないがページに表示したいフィールドのことです。例えば、EmployeeというテーブルにFirstNameLastNameの2つのフィールドがあるとします。データベースに保存するときは別々に保存しますが、表示するときに加工したい(例えば結合して大文字にするなど)場合、両方のフィールドをフロントエンドに送ってユーザーのブラウザで処理するか、バックエンドで処理してからViewModelで受け取ってフロントエンドに送ります。

別の例として、クレジットカードがあります。CreditCardテーブルには、ユーザーのクレジットカード番号、3桁の認証コード、生年月日が格納されています。オンラインショッピングではよく、カードの下4桁だけを表示します。このような機密データを16桁すべてフロントエンドに送るわけにはいきません。その場合は、バックエンドで処理してからViewModelを介してフロントエンドに送る必要があります。

まず、BlogViewModelPostViewModelを作成します。これらはViewModelなので、データベース関連の[Key]属性は必要ありません。Modelを使用している箇所はすべてViewModelに変更します。

次に、バックエンドのBlogRepository.csを変更します。ページ表示はViewModelにし、データの読み書きはModelをそのまま使用します。36行目から56行目で手動でマッピングを行っているのがわかります。

PostRepository.csCreatePost()も同様に変更し、DeletePost()では元のelseブロック内のBlog.Postsに対する判定を削除します。

BlogBase.razor.csPostBase.razor.csでは、使用していたModelViewModelに変更します。

ここで新しいデータを作成します。しかし、2件目を作成した直後に2件目を削除しようとすると、Postが見つからないという問題が発生します。なぜでしょうか?

2件目は確かにデータベースに入りましたが、データを再取得していません。そのため、ページ上のBlog.Postsの2件目のPost.Idはまだ0のままです。

Blog.Postsにデータベースを再取得させるためには、PostBase.razor.csEventCallbackを追加して、BlogBase.razor.csLoadData()をもう一度実行するよう通知する必要があります。通知するだけなので、<TValue>は渡しません。

そして、2件目を追加した直後に削除すれば、正常に動作します。2件目を追加した後、3件目を追加し、2件目を削除しても正常です。

(注:下図のようなエラーメッセージが表示される場合は、Visual Studioの問題の可能性があります。まずはVisual Studioを再起動してみてください。)

参照:

  1. .NET Stack and Heap
  2. In C#, why is String a reference type that behaves like a value type?
  3. What is ViewModel in MVC?
  4. Understanding ViewModel in ASP.NET MVC

注:本記事のコードは.NET 6 + Visual Studio 2022でリファクタリングされています。原文のリンクからリファクタリング後のコードと比較して学習できます。お読みいただきありがとうございます。原作者をサポートしてください。

さらに探索

関連読書

その他の記事
同じカテゴリ / 同じタグ 2021/12/25

(29/30)みんなで学ぶBlazor:Blazor単体テスト

システム開発において最も退屈なプロセスは、おそらくバグ修正です。特に、null オブジェクトにアクセスしようとするエラー(`Object reference not set to an instance of an object.`)は、多くの初心者が最初に直面する問題です。退屈なバグ修正から解放されるために、この記事では「単体テスト」を紹介します。

続きを読む
同じカテゴリ / 同じタグ 2021/12/25

(28/30)みんなで学ぶBlazor:ポリシーベースの認可

以前に「ASP.NET Core Identity」は「Claim」ベースの検証を使用すると述べましたが、実は「ASP.NET Core Identity」には異なる種類の認可方法があります。最も簡単な「ログイン認可」「ロール認可」「Claim認可」ですが、これらはすべて同じ方法で実現されています:原則認可(ポリシーベースの認可)です。

続きを読む