WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

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

by WebSurfer 2022年1月6日 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

HTTP ハンドラでキャッシュコントロール

by WebSurfer 2015年5月19日 17:55

このブログでは、アプリケーションのフォルダに保存した画像やスクリプトファイルなどを応答として返すのに HTTP ハンドラを使っています。

F5 キーを押すなどしてページをリロードすると、下の画像のように HTTP 304 Not Modified 応答を返しますが、これを HTTP ハンドラで実現するにはどのようにするかという話をメインに、HTTP ハンドラを利用したキャッシュコントロールについて書きます。

HTTP 304 応答

HTTP ハンドラを使わないで、以下のように直接 img 要素の src 属性に画像ファイルの URL を設定した場合は、IIS が ETag, Last-Modified に設定する値を当該ファイルから取得し、それらを応答ヘッダに含めて返してくれます。

<img alt="" src="/images/sample2.jpg" />

クライアントが F5 キーなどでページをリロードすると、再度ブラウザが src 属性に設定された URL の画像を要求しますが、そのとき If-Modified-Since と If-None-Match を要求ヘッダに含めて送ってくれます。

要求を受けた IIS は要求ヘッダに含まれる If-Modified-Since と If-None-Match を見て、条件を満たせば(即ち、画像が変更されてなければ) HTTP 304 Not Modified 応答を返します(ヘッダのみでコンテンツは送信しない。ブラウザのキャッシュを使えという指示と同じ)。

ちなみに、ブラウザのキャッシュを削除すると、画像を要求する際 If-Modified-Since と If-None-Match は要求ヘッダには含まれなくなます。なので、キャッシュを削除してから F5 キーを押せば、IIS は画像のコンテンツを含めて HTTP 200 応答を返します。

src 属性に HTTP ハンドラを指定する(HTTP ハンドラ経由でファイルをダウンロードする)場合は、IIS が自動的に行ってくれる上記の処置を、自力で HTTP ハンドラにコーディングすることになります。

簡単に書くと、(1) ブラウザ送られてくる要求ヘッダの中の If-None-Match, If-Modified-Since の値を取得し、その値に応じて HTTP 200 応答(コンテンツを含む)または HTTP 304 応答(コンテンツ無し)のどちらかを返す。(2) 要求を受けたら毎回、応答ヘッダの ETag, Last-Modified に設定する値を当該ファイルから取得し、それらを応答ヘッダに含めてブラウザへ返す・・・という処置をコーディングすることになります。

さらに、せっかく HTTP ハンドラを使って細かくキャッシュコントロールのためのコードが書けるのですから、上記 (1), (2) だけでなく(ETag, Last Modified の応答ヘッダへの設定だけでなく)、Cache-Control, Expires 等の応答ヘッダへの設定も行って、プロキシやブラウザのキャッシュをきちんとコントロールした方がよさそうです。

また、サーバー側でのキャッシュもコントロールすることが可能になりますので、サーバーのキャッシュが利用できればそれを使わない手はないですよね。

ということで、例として、画像ファイル用とスクリプトファイル用それぞれの HTTP ハンドラを、Web サイトプロジェクトの ジェネリックハンドラ(.ashx ファイル)の形で書いたコードを以下に紹介します。ベースは BlogEngine 1.6.1.0 のものです。

(1) 画像用の HTTP ハンドラ

<%@ WebHandler Language="C#" Class="_0114_ImageHandler" %>

using System;
using System.Web;
using System.IO;
using System.Net;

public class _0114_ImageHandler : IHttpHandler {
    
  public void ProcessRequest (HttpContext context) 
  {
    HttpResponse response = context.Response;
    HttpRequest request = context.Request;
        
    // ファイル名はクエリ文字列で指定。
    string fileName = request.QueryString["picture"];
        
    // 画像ファイルはアプリケーションルート直下の
    // images という名前のフォルダにあることが前提。
    string folder = "~/images/";

    if (!String.IsNullOrEmpty(fileName))
    {            
      string path = 
          context.Server.MapPath(folder + fileName);
      FileInfo fileInfo = new FileInfo(path);
      if (fileInfo.Exists)
      {            
        // BlogEngine では何故か作成日時 (CreationTimeUtc) が
        // 使われていたが、IIS が設定するのと同様に、更新日時
        // (LastWriteTimeUtc) を使用するように変更。
        DateTime lastWriteTime = fileInfo.LastWriteTimeUtc;

        // ETag は更新日時のタイマ刻み数。(IIS とは異なる)
        string etag = "\"" + lastWriteTime.Ticks + "\"";
        string ifNoneMatch = request.Headers["If-None-Match"];

        DateTime ifModifiedSince = DateTime.MinValue;
        DateTime.TryParse(
            request.Headers["If-Modified-Since"], 
            out ifModifiedSince);

        response.Clear();
        // ブラウザとプロキシにキャッシュを許可
        response.Cache.SetCacheability(HttpCacheability.Public);
        // 有効期限を、要求を受けた日時から 1 年に設定。
        response.Cache.SetExpires(DateTime.Now.AddYears(1));
        // Last-Modified を応答ヘッダに設定。
        response.Cache.SetLastModified(lastWriteTime);
        // ETag を応答ヘッダに設定。
        response.Cache.SetETag(etag);

        // 要求ヘッダの If-None-Match, If-Modified-Since と、
        // 上のコードで取得した etag, lastWriteTime を比較し、
        // どちらかが一致したら HTTP 304 応答を返して終了。
        if (ifNoneMatch == etag || 
            ifModifiedSince == lastWriteTime)
        {
          response.StatusCode = 
              (int)HttpStatusCode.NotModified;
          return;
        }

        int index = fileName.LastIndexOf(".") + 1;
        string extension = 
            fileName.Substring(index).ToLowerInvariant();

        // IE は image/jpg を認識しないことへの対応
        if (extension == "jpg")
        {
          context.Response.ContentType = "image/jpeg";
        }
        else
        {
          context.Response.ContentType = "image/" + extension;
        }
                
        response.TransmitFile(fileInfo.FullName);
      }
    }
  }
 
  public bool IsReusable 
  {
    get 
    {
      return false;
    }
  }
}

ETag, Last Modified, Expires の違いについては以下の stackoverflow の記事が参考になると思います。

(2) スクリプト用の HTTP ハンドラ

<%@ WebHandler Language="C#" Class="_0114_ScriptHandler" %>

using System;
using System.Web;
using System.IO;
using System.Security;
using System.Web.Caching;
using System.Net;

public class _0114_ScriptHandler : IHttpHandler 
{
    
  public void ProcessRequest (HttpContext context) 
  {
    HttpResponse response = context.Response;
    HttpRequest request = context.Request;

    // ファイル  名はクエリ文字列で指定。
    string fileName = request.QueryString["filename"];

    // スクリプトファイルはアプリケーションルート直下の
    // scripts と言う名前のフォルダにあることが前提。
    string folder = "~/scripts/";

    if (!String.IsNullOrEmpty(fileName))
    {
      string script = null;
            
      // まずサーバーのCacheをチェック。なければファイルに
      // アクセスしてスクリプトを取得。
      if (context.Cache[fileName] == null)
      {
        if (!fileName.EndsWith(".js",
            StringComparison.OrdinalIgnoreCase))
        {
          throw new SecurityException("アクセス不可");
        }

        string path =
            context.Server.MapPath(folder + fileName);
        FileInfo fileInfo = new FileInfo(path);

        if (fileInfo.Exists)
        {
          using (StreamReader reader = 
                              new StreamReader(path))
          {
            script = reader.ReadToEnd();
                        
            // Minify は割愛。
                        
            // サーバーのキャッシュに保持。
            context.Cache.Insert(
                        fileName,
                        script,
                        new CacheDependency(path));
          }
        }
      }
      else
      {
        script = (string)context.Cache[fileName];
      }

      if (!string.IsNullOrEmpty(script))
      {
        response.Clear();
        response.ContentType = "text/javascript";

        // Vary: Accept-Encoding をヘッダに設定。
        response.Cache.VaryByHeaders["Accept-Encoding"] = true;

        // 有効期限 (Expires ヘッダ) を 7 日に設定。
        response.Cache.SetExpires(
                DateTime.Now.ToUniversalTime().AddDays(7));
        // Cache-Control ヘッダの max-age を 7 日(604800 秒)
        // に設定。
        response.Cache.SetMaxAge(new TimeSpan(7, 0, 0, 0));
        // Cache-Control ヘッダに must-revalidate を設定。
        response.Cache.SetRevalidation(
                HttpCacheRevalidation.AllCaches);

        int hash = script.GetHashCode();
                
        string etag = "\"" + hash.ToString() + "\"";
        string ifNoneMatch = request.Headers["If-None-Match"];
                
        // ETag を応答ヘッダに設定。
        response.Cache.SetETag(etag);
        // Cache-Control ヘッダに public を設定(ブラウザと
        // プロキシにキャッシュを許可)
        response.Cache.SetCacheability(HttpCacheability.Public);
                
        // 要求ヘッダの If-None-Match と上のコードで取得した
        // etag を比較し一致したら HTTP 304 応答を返して終了。
        // サーバーの Cache を利用する関係で Last Modified
        // は使用しない。
        if (ifNoneMatch == etag)
        {
          response.StatusCode = 
                      (int)HttpStatusCode.NotModified;
          return;
        }

        response.Write(script);
      }            
    }
  }
 
  public bool IsReusable 
  {
    get 
    {
      return false;
    }
  }
}

ファイルから取得したスクリプトの文字列をサーバーのキャッシュに保持するようにし、2 回目以降の要求ではキャッシュから取得するようにしています。

キャッシュから取得する場合はスクリプトファイルの更新日時を取得できないので、Last Modified は使わず ETag のみを応答ヘッダに含めています。

ETag の値は、スクリプトの文字列から String.GetHashCode メソッドでハッシュコードを取得し、それを設定しています。

Tags:

Cache

@OutputCache ディレクティブ

by WebSurfer 2012年4月28日 16:50

@ OutputCache ディレクティブ を使ったキャッシュの設定方法は、MSDN ライブラリなどを検索するといろいろ見つかりますが、結局読んでもよく分かりません。(笑)

という訳で、実際にコードを書いて試して見ました。今回調べたのは Location 属性の設定によってどう変わるかということです。いろいろ発見があったので、忘れないように書いておきます。

まず、@ OutputCache ディレクティブを設定しない場合ですが、その場合はサーバーではキャッシュされません。

応答ヘッダは以下のようになりますので、コンテンツはブラウザのユーザー専用キャッシュにのみキャッシュされます。

Cache-Control: private
Date: Sat, 28 Apr 2012 01:44:22 GMT

ただし、ASP.NET ページには有効期限がないため、キャッシュは常に古いと見なされます。そのため、ASPX リソースをリクエストすると、ページがキャッシュされてない場合と同じように、常にサーバーから新しいリソースを取得することになります。

次に、例として、@ OutputCache ディレクティブを以下のように設定した場合、キャッシュがどのように設定されるかを書きます。

<%@ OutputCache 
    Duration="15" 
    Location="xxxxx" 
    VaryByParam="None" %>

Location 属性の xxxxx のところには、None, Client, Downstream, Server, ServerAndClient, Any(デフォルト)のいずれかを指定します。結果はそれぞれ以下のようになります。

(1) None

ブラウザ、プロキシ、サーバーいずれでもキャッシュされません。

ブラウザおよびプロキシにキャッシュの方法を指示する HTTP 応答ヘッダは以下のようになります。

Cache-Control: no-cache
Date: Sat, 28 Apr 2012 01:45:43 GMT
Expires: -1
Pragma: no-cache

なお、Duration と VaryByParam は必須属性ですが、Location="None" と設定してあると Duration を設定しなくてもエラーにはなりません。逆に、Duration が設定してあっても無視されるようです。

(2) Client

ブラウザのみでキャッシュされます。サーバーではキャッシュされません。

応答ヘッダは以下のようになります。

Cache-Control: private, max-age=15
Date: Sat, 28 Apr 2012 02:49:17 GMT
Expires: Sat, 28 Apr 2012 02:49:32 GMT

(3) Downstream

ブラウザおよびプロキシ(もしあれば)のみでキャッシュされます。サーバーではキャッシュされません。

応答ヘッダは以下のようになります。

Cache-Control: public, max-age=15
Date: Sat, 28 Apr 2012 02:56:09 GMT
Expires: Sat, 28 Apr 2012 02:56:24 GMT

(4) Server

サーバーのみでキャッシュされます。Duration 属性に指定した時間が経過すると、キャッシュは破棄されます。その後に要求を受けると、その応答コンテンツが再びキャッシュされます。

応答ヘッダは以下のようになりますので、ブラウザとプロキシではキャッシュされません。

Cache-Control: no-cache
Date: Sat, 28 Apr 2012 02:28:31 GMT
Expires: -1
Pragma: no-cache

なお、サーバーキャッシュは GET 用と POST 用の 2 種類生成されることに注意してください。下に示した検証用のコードで、Duration を十分長い時間にとって、GET で要求した時と、POST で要求した時とで、ラベルに表示される時間を比較してみてください。違う値になるはずです。

(5) ServerAndClient

サーバーとブラウザでキャッシュされます。

応答ヘッダは以下のようになります(Client の場合と同様)。

Cache-Control: private, max-age=15
Date: Sat, 28 Apr 2012 02:37:17 GMT
Expires: Sat, 28 Apr 2012 02:37:32 GMT

(6) Any

ブラウザ、プロキシ、サーバーのすべてでキャッシュされます。

応答ヘッダは以下のようになります(DownStream の場合と同様)。

Cache-Control: public, max-age=15
Date: Sat, 28 Apr 2012 02:18:21 GMT
Expires: Sat, 28 Apr 2012 02:18:36 GMT

なお、Location 属性はデフォルトで Any ですので、Location="Any" を削除しても結果は同じになります。

上に述べた、検証に使ったコードは以下の通りです。

<%@ Page Language="C#" %>
<%@ OutputCache Duration="3600" 
    Location="Server" 
    VaryByParam="None" %> 

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">

    protected void Page_Load(object sender, EventArgs e)
    {
        Label1.Text = DateTime.Now.ToString();
    }
</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:Button ID="Button1" 
            runat="server" 
            Text="Button" />
        <asp:Label ID="Label1" runat="server">
        </asp:Label>
    </div>
    </form>
</body>
</html>

最後に、今回参考にしたキャッシュ関係の記事のリンクを張っておきます。

Web 2.0 アプリケーションのパフォーマンスを改善する

プロキシキャッシュ対策

ブラウザキャッシュでパフォーマンス向上

Tags:

Cache

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年3月  >>
252627282912
3456789
10111213141516
17181920212223
24252627282930
31123456

View posts in large calendar