WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

null 許容許容参照型と EF Code First

by WebSurfer 12. May 2022 15:15

Visual Studio 2022 のテンプレートを使って .NET 6.0 アプリのプロジェクトを作ると、デフォルトで「Null 許容」オプションが有効にされています。その状態での EF Code First による SQL Server データベース生成に新発見 (自分が知らなかっただけですが) があったので備忘録として書いておきます。

「Null 許容」オプション

EF Code First でデータベースを生成すると、元となるコードのクラス定義の中のプロパティの型と付与する属性によって生成されるデータベースの列の型と NULL 可/不可が決まってきます。新発見というのは「Null 許容」オプションの有効化によって、生成される列の NULL 可/不可が以前と違ってくるということです。

値型の場合は「Null 許容」オプションの有効/無効は関係なく結果は以前と同じになります。すなわちデフォルトでデータベースの当該列は NULL 不可になります。NULL 可にしたい場合は Nullable<T> 型(例えば int? とか DateTime? など)をプロパティの型に使います。

違うのは参照型の場合です。「Null 許容」オプションが有効にされていると、例えばプロパティの型を string とすると当該データベースの列は NULL 不可に、string? とすると NULL 可になります。

以前 (null 許容参照型が使えない時代または「Null 許容」オプションが無効) は string 型は NULL 可になりました。NULL 不可にしたい場合は当該プロパティに RequiredAttribute 属性を付与していました。

実際にアプリを作って試してみましたので具体例を以下に書きます。

Visual Studio 2022 のテンプレートでフレームワークを .NET 6.0 としてコンソールアプリを作成します。その状態で上の画像のように「Null 許容」オプションが有効化されています。

NuGet パッケージ Microsoft.EntityFrameworkCore.SqlServer と Microsoft.EntityFrameworkCore.Tool をインストールします。前者は SQL Server 用の EF Core 本体、後者は Migration 操作を行うためのツールです。

NuGet パッケージ

Microsoft のドキュメント「新しいデータベースの Code First」と同様なコンテキストクラスとエンティティクラスを実装します。コードは以下の通りです。(null 許容参照型対応のため = null! を追加するなどしていますが基本は同じ)

public class Blog
{
    public int BlogId { get; set; }
    public string Name { get; set; } = null!;

    public virtual List<Post>? Posts { get; set; }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; } = null!;

    public string? Content { get; set; }

    public int BlogId { get; set; }
    public virtual Blog? Blog { get; set; }
}

public class BloggingContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; } = null!;
    public DbSet<Post> Posts { get; set; } = null!;

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        if (!optionsBuilder.IsConfigured)
        {
            optionsBuilder.UseSqlServer("接続文字列");
        }
    }
}

上のコードの Blog クラスの Name プロパティ、Post クラスの Title, Content プロパティの型を string / string? と使い分けている点に注目してください。これをベースに Migration 操作によって SQL Server にデータベースを生成した結果が以下の画像です。

SQL Server にデータベース生成

プロパティが string 型になっている Name, Title プロパティに対応する SQL Server データベースの Name 列と Title 列は NULL 不可に、string? 型になっている Content プロパティに対応するデータベースの Content 列は NULL 可になっています。

以前 (null 許容参照型が使えない時代または「Null 許容」オプションが無効) は、上にも書きましたが、プロパティの型が参照型の場合はデータベースの当該列は NULL 可になります。NULL 不可にする場合は RequiredAttribute 属性を付与します。試しに、以下のように #nullable disable を付与したクラス定義を追加し、Migration 操作で SQL Server に Products テーブルを生成してみました。

#nullable disable
public class Product
{
    public int ProductId { get; set; }

    [Required]
    public string ProductName { get; set; }

    public string Decription { get; set; }

    [Column(TypeName = "decimal(18,4)")]
    public decimal UnitPrice { get; set; }
}

結果は以下の通りです。プロパティの型が string の ProductName, Description に該当する列の NULL 可/不可を見てください。プロパティに RequiredAttribute 属性を付与しないと NULL 可になります。

#nullable disable で生成

既存のデータベースからリバースエンジニアリングで生成したコンテキストクラス、エンティティクラスも「Null 許容」オプションが有効化されている場合は上の Blog, Post クラスと同様になります。

試しに Microsoft のサンプルデータベース Northwind の Categories テーブルからリバースエンジニアリングでコンテキストクラス、エンティティクラスを生成してみました。Categories テーブルは以下の内容になっています。CategoryName 列が NULL 不可、Description 列が NULL 可になっているところに注目してください。

Northwind の Categories テーブル

上の Categories テーブルからリバースエンジニアリングを使ってデータアノテーション属性を含めてエンティティクラスを生成すると以下の通りとなります。データベースのテーブルの各列の NULL 可/不可と、生成されたクラス定義の各プロパティの型を見てください。データベースの列が NULL 可の場合はプロパティの型は null 許容(? を付与されている)となっています。

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

namespace MvcCore6App2.Models
{
    [Index("CategoryName", Name = "CategoryName")]
    public partial class Category
    {
        public Category()
        {
            Products = new HashSet<Product>();
        }

        [Key]
        [Column("CategoryID")]
        public int CategoryId { get; set; }
        [StringLength(15)]
        public string CategoryName { get; set; } = null!;
        [Column(TypeName = "ntext")]
        public string? Description { get; set; }
        [Column(TypeName = "image")]
        public byte[]? Picture { get; set; }

        [InverseProperty("Category")]
        public virtual ICollection<Product> Products { get; set; }
    }
}

この記事の本題の話は以上ですが、このクラス/プロパティ定義をそのまま ASP.NET MVC の Model として使った場合、ユーザー入力の検証がどうなるのかが気になります。それも調べましたので以下に書いておきます。

以前は string 型のプロパティに該当するテキストボックスへのユーザー入力を必須とする場合、その項目の当該プロパティに RequiredAttribute 属性を付与していました。それにより未入力の場合は検証 NG となってエラーメッセージが表示されます。

「Null 許容」オプションが有効化されている場合、string 型の項目は必須入力になるはずですが、上のコードではプロパティには RequiredAttribute 属性は付与されていません。そこはどうなるのかが疑問でした。

実際にアプリを動かして試してみると、RequiredAttribute 属性は付与されてなくても、未入力の場合は検証 NG となってエラーメッセージが表示されました。

View から生成される html ソースを調べてみると、当該 input 要素には data-val-required="The xxx field is required." という検証属性が付与され、未入力の場合は検証機能が働いてエラーメッセージが表示されるようになっていました。

エラーメッセージを任意のものに変えたい場合は、プロパティに RequiredAttribute を付与して ErrorMessage にメッセージを設定します。そうすると data-val-required 属性に設定される文字列が ErrorMessage に設定したものに置き換わります。

このあたりは先の記事「int 型プロパティの検証、エラーメッセージ」に書いた int 型の場合と同じになっているよう���す。

最後にもう一つ、こんなことをする人はいないかもしれませんが、string? 型のプロパティ(null 可)に RequiredAttribute 属性を付与(null 不可)するとどうなるかを書いておきます。

そのような設定をすると、EF Core を使って SQL Server からデータを取得する際当該列のデータに NULL が含まれていると、

System.Data.SqlTypes.SqlNullValueException: Data is Null. This method or property cannot be called on Null values.

・・・という例外がスローされます。下の画像を見てください。

SqlNullValueException

Microsoft のドキュメントによると、SqlNullValueException は「System.Data.SqlTypes 構造体の Value プロパティが null に設定されている場合にスローされる例外」ということだそうです。

メカニズムは不明ですが、string? 型のプロパティに RequiredAttribute 属性を付与し EF Core で SQL Server からデータを取得してくるときに、データに NULL があると「SqlTypes 構造体の Value プロパティが null に設定」ということになるようです。

SqlNullValueException の説明と RequiredAttribute 属性を設定したことが結びつかなくて、解決に悩んで無駄な時間を費やすことになるかもしれませんので注意してください。(実は自分がそうでした)

Tags: , , ,

ADO.NET

Session 情報のストアに SQL Server を利用 (CORE)

by WebSurfer 13. March 2022 15:52

ASP.NET Core アプリで Session 状態を利用する際に SQL Server をストアとして利用する方法を書きます。下の画像の赤線部分は Session 情報を Controller で ViewBag に設定しそれを表示したものです。

Session 状態の表示

ASP.NET Core アプリでの Session の使い方の詳しい話は Microsoft のドキュメント「ASP.NET Core でのセッションと状態の管理」にあります。

それを読めば大体分かると思いますが、.NET Framework 版の ASP.NET アプリの Session との大きな違いで自分が気が付いた点を以下にまとめておきます。

  1. Program.cs (.NET 6.0) または Startup.cs (.NET 5.0 以前) でサービスとミドルウェアの登録が必要。
  2. ASP.NET Core の分散キャッシュをデータのストアに利用する。 (なので、メモリ内キャッシュを使用する場合であってもセッションデータはシリアル化可能なでなければならない)
  3. String 型、Int32 型、Byte[] 型以外のデータ型は格納できない。(String 型、Int32 型、Byte[] 型以外のデータを格納する場合は JSON 文字列にシリアライズするなどの方法で格納する)
  4. 同時アクセスでもロックされず後書き優先になる。 (先の記事「SessionStateModule によるロック」で書いたロック機能は提供されてない)
  5. StateServer モードに相当する機能は「分散 Redis キャッシュ」を使って実現するらしい。(未検証・未確認です)

上の 2 に書いたように Session 情報のストアには分散キャッシュを利用するのですが、それには以下があるそうです。詳しくは Microsoft のドキュメント「ASP.NET Core の分散キャッシュ」を見てください。

  • 分散メモリ キャッシュ - AddDistributedMemoryCache
  • 分散 SQL Server キャッシュ - AddDistributedSqlServerCache
  • 分散 Redis キャッシュ - AddStackExchangeRedisCache
  • 分散 NCache キャッシュ - AddNCacheDistributedCache

上の「分散メモリキャッシュ」というのは、サーバーで実行されている Web アプリのメモリを使って ASP.NET Core の分散キャッシュを実現するためのもので、実際に「分散」されているわけではないそうです。

「分散メモリキャッシュ」を利用しての Session 状態の構成方法や使い方は Microsoft のドキュメント「ASP.NET Core でのセッションと状態の管理」に詳しく書いてありますのでそれを読めば容易に実装できると思います。

この記事では「分散 SQL Server キャッシュ」を利用しての Session 状態の構成方法を書きます。以下の説明と合わせて、Microsoft のドキュメントの「分散 SQL Server キャッシュ」のセクションも見てください。

(1) SQL Server のテーブルを作成

sql-cache create コマンドを実行して SQL Server にテーブルを作成します。以下の点に注意してください。

  • dotnet-sql-cache ツールをインストールしておく必要があります。dotnet tool install --global dotnet-sql-cache コマンドでインストールできます。
  • コマンドの接続文字列の Initial Catalog に設定するデータベースは事前に作成しておく必要があります。その際、sql-cache create コマンドを実行するユーザーのアカウントが SQL Server のログインに設定されていて、そのデータベースに対してテーブルを作成する権限を持っている必要があります。

この記事では開発マシンの SQL Server 2012 Express の既存のデータベース TestDatabase に Windows 認証で接続し、TestCache という名前でテーブルを作成しました。その結果は以下の画像の通りです。

SQL Server のテーブル

Microsoft ドキュメントの説明とは Id の Data Type が nvarchar(900) ではなくて nvarchar(499) と異なりますが、その他は同じ内容で TeestCache テーブルが生成されています。

(2) NuGet パッケージのインストール

NuGet パッケージ Microsoft.Extensions.Caching.SqlServer をインストールします。下の (3) 項でのサービスの登録で AddDistributedSqlServerCache を使うのに必要になります。

Microsoft.Extensions.Caching.SqlServer

(3) サービスとミドルウェアの登録

Program.cs (.NET 6.0) または Stratup.cs (.NET 5.0 以前) でサービスとミドルウェアを登録します。以下の例は Visual Studio 2022 で作成した .NET 6.0 の Program.cs での例です。

// ・・・前略・・・

// SQL Server 分散キャッシュを使用
// "DistCacheConnectionString" は appsettings.json に設定する
// SQL Server への接続文字列
builder.Services.AddDistributedSqlServerCache(options =>
{
    options.ConnectionString = 
        builder.Configuration.GetConnectionString(
                               "DistCacheConnectionString");
    options.SchemaName = "dbo";
    options.TableName = "TestCache";
});

// AddSession の呼び出し
builder.Services.AddSession(options =>
{
    // デフォルトは 20 分。検証用に 300 秒に設定
    options.IdleTimeout = TimeSpan.FromSeconds(300);

    options.Cookie.HttpOnly = true;  // デフォルトで true

    // デフォルトは false。true に設定しないとセッション状態は
    // 機能しない可能性あり
    options.Cookie.IsEssential = true;  
});

// ・・・中略・・・

// UseSession の呼び出し
// 順序が重要で、UseRouting の後かつ MapRazorPages と 
// MapControllerRoute の前に呼び出します
app.UseSession();

// ・・・後略・・・

(4) appsettings.json に接続文字列を設定

上の (3) 項の AddDistributedSqlServerCache メソッドで指定した "DistCacheConnectionString" という名前で構成ファイルから接続文字列を取得できるよう、appsettings.json ファイルに以下の設定を追加します。

"ConnectionStrings": {
  "DistCacheConnectionString": 
    "Data Source=(local)\\sqlexpress;Initial Catalog=TestDatabase;Integrated Security=True;"
}

上の接続文字列は SQL Server Express の sqlexpress という名前付きインスタンスにアタッチされている TestDatabase というデータベースに Windows 認証でローカルに接続するものです。そのあたりは個人の環境に合わせて変更してください。


以上で Session 状態は使えるようになります。

例えば HomeController で以下のように Session を設定して実行し、Home/Privacy に遷移した後で Home/Index に戻るとこの記事の一番上の画像の赤線で示したように Session データが表示されます。

public IActionResult Index()
{
    // Session は home/privacy で設定
    ViewBag.Name = HttpContext.Session.GetString("name");
    ViewBag.Test = HttpContext.Session.GetString("test");
    HttpContext.Session.Clear();

    return View();
}

public IActionResult Privacy()
{
    // Session の設定
    HttpContext.Session.SetString("name", "session value");
    HttpContext.Session.SetString("test", "test value");

   return View();
}

SQL Server に格納された Session データは以下のようになります。Chrome と Edge を立ち上げて両方からアクセスした結果で、それぞれ Session Id (デフォルトで .AspNetCore.Session という名前の cookie に格納されている) が異なるので、TestCache テーブルには Id が異なるレコードが 2 つ存在するという結果になっています。

SQL Server に格納された Session データ

上の画像のレコードはクライアントがブラウザを閉じてもそのまま残ります。と言っても、それが再利用されることはなく、再度ブラウザを立ち上げてアクセスして Session を使うと別の Id でレコードが追加されます。

上の Home/Index のコードにある HttpContext.Session.Clear() でも SQL Server のレコードは削除されません。Session Cookie も削除されません。Value の中のデータが削除され、ExpiresAtTime がその時点から AddSession メソッドの options.IdleTimeout で設定された時間まで延長されるのみです。

サーバー側で Session を使っているユーザーがオンラインか否かを判定する術はなく、サーバーではレコード削除の可否を判断できないのでそうせざるを得ないのではないかと思います。

ということはレコードは削除されずどんどん増えていく一方になるのではと思いましたが、翌日にまた調べてみたら、SQL Server には昨日のレコードは残っていたが、アプリを動かしてみると昨日のレコードは消えました。どうやら、アプリを起動する際(多分 Program.cs のコードが実行される時)に ExpiresAtTime が古いレコードは消去されるらしいです。

IIS を使ってのインプロセスホスティングモデルであればワーカープロセスがリサイクルされる時に Program.cs のコードが実行される(古いレコードは削除される)と思いますが、それ以外のホスティングモデルで Kestrel が使われる時はどうなるかは分かりません。そこは今後の検討課題ということにしたいと思います。

Tags: , ,

CORE

ASP.NET Core で SQL キャッシュ依存関係

by WebSurfer 6. January 2022 13:07

先の記事「SQL キャッシュ依存関係」と同等のキャシュ機能を .NET 6.0 の ASP.NET Core MVC アプリに実装してみました。その実装方法やサンプルコードを備忘録として残しておきます。

SQL キャッシュ依存関係

SQL キャッシュ依存関係とは、ASP.NET のキャッシュと SQL Server のテーブルやレコードとの間に依存関係を持たせ、当該テーブル/レコードが変更されたら ASP.NET のキャッシュを削除し、次のリクエストでは新しいデータを DB から取得してユーザーに提供するとともに、新しいデータをキャッシュに保存できるようにする機能です。

先の記事では .NET Framework の ASP.NET Web Forms アプリに SqlCacheDependency クラスを利用して SQL キャッシュ依存関係を構築していましたが、ASP.NET Core アプリでは SqlCacheDependency クラスは使えないしキャッシュの仕組みも異なるので、そこをどのようにするかという話になります。

ASP.NET Core は「応答キャッシュ」、「メモリ内キャッシュ」、「分散キャッシュ」などいくつかの異なるキャッシュをサポートしているそうですが、この記事で使うのは「メモリ内キャッシュ」です。その説明やサンプルコードは Microsoft のドキュメント「ASP.NET Core のメモリ内キャッシュ」にありますので見てください。

SQL Server から取得したデータをキャッシュするだけなら、上に紹介した Microsoft のドキュメントを参考にして容易に実装できます。問題は、どのように SQL Server との間に依存関係を持たせるか、即ち SQL Server 側で当該テーブル/レコードが変更されてキャッシュされたデータが使えなくなったらそのデータをキャッシュから削除するための機能をどのように組み込むかというところです。

そのためには SQL Server から当該テーブル/レコードが変更されたという通知をもらう必要があります。そこは、先の記事「ASP.NET Core で SqlDependency」に書きましたように、SqlDependency クラスを使って SQL Server のデータが更新されたときのクエリ通知を受け取ることができますので、クエリ通知を受けたらキャッシュを削除するようにすればよさそうです。

そのサンプルを作ってみました。以下に作成手順とサンプルコードを書きます。

(1) サンプルデータベースとテーブルの作成

先の記事「SignalR と SqlDependency」と同じデータベースとテーブルを使います。作成手順はその記事を見てください。

クエリ通知はサービスブローカを使用するため、データベースに対して以下の要件がありますので注意してください。

  1. 通知クエリが実行されるデータベースでサービスブローカが有効になっている必要があります。
  2. クエリ通知を受け取るユーザーには、クエリ通知にサブスクライブするための権限が必要です。

その他クエリ通知に関する詳しいことはMicrosoft のドキュメント「ADO.NET 2.0 のクエリ通知」や「クエリ通知を使用するときの特別な注意事項 (ADO.NET)」などに書いてありますので見てください。

(2) ASP.NET プロジェクトの作成

Visual Studio 2022 のテンプレートを使って、ターゲットフレームワークを .NET 6.0 として ASP.NET Core Web アプリのプロジェクトを作成します。この記事では アプリは MVC を選んで認証は「なし」としておきました。

(3) NuGet パッケージのインストール

NuGet から必要なパッケージをインストールします。

NuGet パッケージ

赤枠で囲ったものが ASP.NET Core アプリでメモリ内キャッシュを利用できるようにするものです。これにより、ASP.NET Core アプリに組み込みの機能を使って、IMemoryCache のインスタンスへの参照を DI できるようになります。

青枠で囲ったものは、SqlConnection, SqlCommand, SqlDataReader クラスを使って SQL Server からデータを取得するのと同時に、SqlDependency クラスを用いてクエリ通知のサブスクリプションの設定に必要です。

その他は Entity Framework Core を使って上のステップ (1) で作成した Products テーブルの CRUD 操作を行うために必要です。リバースエンジニアリングやスキャフォールディングによるコードの自動生成にも必要です。そこはこの記事の主題の「ASP.NET Core で SQL キャッシュ依存関係」には直接関係ありませんが、検証用に Controller / View を作るのでそのために追加しておきます。

(4) コンテキストクラスとエンティティクラスの作成

上のステップ (1) で作成した Products テーブルを Entity Framework Core を使って CRUD 操作を行うためのベースとなるコンテキストクラスとエンティティクラスをリバース エンジニアリングによって作成します。

パッケージマネージャーコンソールからコマンドを実行するのですが、その手順は先の記事「スキャフォールディング機能 (CORE)」の「(1) リバースエンジニアリング」のセクションを見てください。

下にこの記事で使ったコマンドを載せておきます。

Scaffold-DbContext -Connection "Data Source=lpc:(local)\sqlexpress;Initial Catalog=SqlDependency;Integrated Security=True" -Provider Microsoft.EntityFrameworkCore.SqlServer -ContextDir Contexts -OutputDir Models -Tables Products -DataAnnotations

このコマンドで Contexts フォルダに SqlDependencyContext という名前のコンテキストクラスが、Models フォルダに Product という名前のエンティティクラスが生成されます。

SqlDependencyContext クラスに自動生成されているコードの中から、引数を取らないコンストラクタ SqlDependencyContext() をコメントアウトし、OnConfiguring メソッドの中身をコメントアウトしてください。

Product クラスは自動生成されたコードをそのまま使います。以下にコマンドで生成されたコードを載せておきます。

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

namespace MemoryCacheSample.Models
{
    public partial class Product
    {
        [Key]
        [Column("ProductID")]
        public int ProductId { get; set; }
        [StringLength(100)]
        public string Name { get; set; } = null!;
        [Column(TypeName = "decimal(18, 2)")]
        public decimal UnitPrice { get; set; }
        public int Quantity { get; set; }
    }
}

下のステップ (6) の ProductsCache クラスを使って、SQL Server の Products テーブルの全レコードを取得して List<Product> 型のオブジェクトを作成し、それをキャッシュに保持します。

(5) 接続文字列の保存

appsettings.json に接続文字列を保存します。この記事では上のステップ (4) で使った接続文字列と同じにしました。JSON 文字列なので \ はエスケープして \\ にする必要があることに注意してください。

(6) ProductsCache クラスの作成

このクラスを使って SQL Server の Products テーブルからレコードを取得して List<Product> 型のオブジェクトを作ると共に、それをキャッシュに保持します。さらに、SQL Server の Products テーブルに変更があったらクエリ通知を受け取ってキャッシュを削除します。

Controller で List<Product> 型のオブジェクトを View に Model として渡して Products テーブルのレコード一覧を表示しますが、その際 Controller に List<Product> 型のオブジェクトを渡すのがこのクラスです。

コードは以下の通りです。説明はコメントに書きましたのでそちらを見てください。

using Microsoft.Extensions.Caching.Memory;
using MemoryCacheSample.Models;
using System.Data;
using System.Data.SqlClient;

namespace MemoryCacheSample.CacheControllers
{
    // データ List<Product> のキャッシュを管理するクラス。 
    // Program.cs で AddSingleton<ProductsCache> メソッドを使って
    // サービスに登録し、シングルトンインスタンスとしてコントロー
    // ラーに DI して利用する
    public class ProductsCache : IDisposable
    {
        private readonly IMemoryCache _cache;
        private readonly IConfiguration _configuration;
        private readonly string _connString;
        private readonly string _sqlQuery;

        public ProductsCache(IMemoryCache cache, 
                             IConfiguration configuration)
        {
            // IMemoryCache を DI により取得して設定
            _cache = cache;

            // IConfiguration を DI により取得して設定            
            _configuration = configuration;

            // appsettings.json の接続文字列を取得
            _connString = _configuration
                          .GetConnectionString("ProductConnection");

            // SELECT クエリ。テーブル名は dbo.Products とすること。
            // SqlDependency.dbo.Products でも Products でもダメで、
            // 通知のサブスクリプションに失敗する
            _sqlQuery = "SELECT ProductID,Name,UnitPrice,Quantity" +
                        " FROM dbo.Products";

            // クエリ通知のリスナの開始
            // 最初、Start はキャッシュに取る時点に、Stop はキャッ
            // シュから Remove する時点に設定したが、Start / Stop
            // を繰り返すのは不都合があるようで何十秒か固まってし
            // まう。なので、コンストラクタで Start し、Dispose
            // パターンの中で Stop することにした
            SqlDependency.Start(_connString);
        }

       
        // キャッシュからデータを取得して返す。キャッシュに無けれ
        // ば新たに SQL Server に SELECT クエリを投げてデータを取
        // 得し、それをキャッシュに格納してからデータを返す
        public async Task<List<Product>> CacheTryGetValueSet()
        {
            List<Product> cacheEntry;

            // キャッシュにデータがあれば cacheEntry に渡される。キー
            // CacheKeys.Entry は別のクラスファイルに定義した文字列
            if (!_cache.TryGetValue(CacheKeys.Entry, out cacheEntry))
            {
                // キャッシュに無ければ SQL Server からデータを取得
                cacheEntry = await GetProductsSetSqlDependency();

                // キャッシュオプションの設定
                var cacheEntryOptions = new MemoryCacheEntryOptions()
                    // キャッシュする時間を設定。その時間内にアクセス
                    // があればその時点で再度設定される
                    .SetSlidingExpiration(TimeSpan.FromSeconds(60));

                // キャッシュに取得したデータを保持
                _cache.Set(CacheKeys.Entry, cacheEntry, cacheEntryOptions);
            }

            return cacheEntry;
        }

        // SQL Server の Products テーブルからデータを取得して
        // List<Product> 型のオブジェクトとして返す。同時にクエリ
        // 通知のサブスクリプションを設定するヘルパーメソッド
        private async Task<List<Product>> GetProductsSetSqlDependency()
        {
            var products = new List<Product>();
            using (var connection = new SqlConnection(_connString))
            using (var command = new SqlCommand(_sqlQuery, connection))
            {
                var sqlDependency = new SqlDependency(command);

                // イベントハンドラの設定
                sqlDependency.OnChange += OnSqlDependencyChange;

                if (connection.State == ConnectionState.Closed)
                {
                    connection.Open();
                }

                // ExecuteReader でクエリ通知のサブスクリプションが設定
                // される。同時に SqlDataReader でデータを取得できる
                using (var reader = await command.ExecuteReaderAsync())
                {
                    while (reader.Read())
                    {
                        var product = new Product
                        {
                            ProductId = reader.GetInt32(0),
                            Name = reader.GetString(1),
                            UnitPrice = reader.GetDecimal(2),
                            Quantity = reader.GetInt32(3)
                        };
                        products.Add(product);
                    }
                }
            }
            return products;
        }

        // Products テーブルが更新されるとこのイベントハンドラに制御
        // が飛んでくるのでエントリをキャッシュから削除する
        private void OnSqlDependencyChange(object sender,
                                           SqlNotificationEventArgs e)
        {            
            // キャッシュから削除
            _cache.Remove(CacheKeys.Entry);    
        }

        private bool disposedValue;

        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                    // クエリ通知のリスナの停止
                    SqlDependency.Stop(_connString);
                }

                disposedValue = true;
            }
        }

        public void Dispose()
        {
            Dispose(disposing: true);
            GC.SuppressFinalize(this);
        }
    }
}

(7) Program.cs にコードの追加

プロジェクトを作った時に Program.cs というファイル(.NET 5.0 以前の場合は Startup.cs)が自動生成されているはずですので、それに以下の「// 追加」とコメントしたコードを追加します。SqlDependencyContext クラス、ProductsCache クラスのインスタンスへの参照を Controller のコンストラクタの引数経由で DI できるよう、サービスに登録するものです。

// 追加
using MemoryCacheSample.Contexts;
using Microsoft.EntityFrameworkCore;
using MemoryCacheSample.CacheControllers;

var builder = WebApplication.CreateBuilder(args);

// 追加・・・SqlDependencyContext を DI するため
var productConnString = builder.Configuration
    .GetConnectionString("ProductConnection");
builder.Services.AddDbContext<SqlDependencyContext>(options =>
    options.UseSqlServer(productConnString));

// 追加・・・ProductsCache クラスのシングルトン
// インスタンスを DI するため
builder.Services.AddSingleton<ProductsCache>();

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

// ・・・後略・・・

(8) Controller / View の作成

Visual Studio 2020 のスキャフォールディング機能を利用して SQL Server の Products テーブルを CRUD できる ProductsController とその各アクションメソッドに対応する View 一式を自動生成します。

手順は先の記事「スキャフォールディング機能 (CORE)」の「(3) スキャフォールディング」のセクションを見てください。

自分で一行もコードを書かなくても完全なコードが自動生成されるはずです。レコード一覧の表示、追加、削除、更新ができることを確認してください。

(9) Index アクションメソッドの変更

ProductsController の Index アクションメソッドに手を加えて、上のステップ (6) で作成した ProductsCache クラスを使って List<Product> 型のオブジェクトを取得するようにします。

以下のように、ProductsCache クラスのシングルトンインスタンスをコンストラクタの引数経由で DI できるようコードを追加するとともに、Index アクションメソッドの中身を書き換えます。

#nullable disable
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MemoryCacheSample.Contexts;
using MemoryCacheSample.Models;
using MemoryCacheSample.CacheControllers;

namespace MemoryCacheSample.Controllers
{
    public class ProductsController : Controller
    {
        private readonly SqlDependencyContext _context;

        // 追加
        private readonly ProductsCache _productsCache;

        public ProductsController(SqlDependencyContext context,

                                  // 追加
                                  ProductsCache productsCache)
        {
            _context = context;

            // 追加
            _productsCache = productsCache;
        }

        // GET: Products
        public async Task<IActionResult> Index()
        {
            // 書き換え
            List<Product> cacheEntry =
                await _productsCache.CacheTryGetValueSet();
            return View(cacheEntry);

            // 自動生成された元のコード
            //return View(await _context.Products.ToListAsync());
        }

        // ・・・中略・・・
    }
}

Visual Studio から実行して上の Products/Index を呼び出すとこの記事の一番上の画像のようになるはずです。

画面上のリンク Create New, Delete, Edit をクリックすると別画面に遷移して追加、削除、更新ができます。デバッガを使って、その時の ProductsCache クラスのキャッシュコントロールを見て期待通り動いていることを確認してください。

Tags: , , ,

Cache

About this blog

2010年5月にこのブログを立ち上げました。その後ブログ2を追加し、ここはプログラミング関係、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  July 2022  >>
MoTuWeThFrSaSu
27282930123
45678910
11121314151617
18192021222324
25262728293031
1234567

View posts in large calendar