WebSurfer's Home

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

User-Agent Client Hints

by WebSurfer 2023年4月21日 15:27

Windows OS のブラウザから、要求ヘッダに含まれて Web サーバーに送信されてくる従来の User-Agent では、OS のバージョンが Windows 10 なのか Windows 11 なのかの識別ができません。

しかし、Microsoft のドキュメント「User-Agent クライアント ヒントを使用してWindows 11と CPU アーキテクチャを検出する」によると、User-Agent Client Hints を利用すればサーバー側で OS のバージョンが Windows 10 なのか Windows 11 なのかの識別ができるとのことなので検証してみました。

なお、ドキュメントには書いてありませんが、HTTPS 通信に限ると言うところに注意してください。また、現時点では実験的な機能であり、対応ブラウザも Edge, Chrome, Opera に限られている点にも注意してください。(参考 : ユーザーエージェントクライアントヒント API

ブラウザはデフォルトで要求ヘッダに User-Agent Client Hints 関係の情報 sec-ch-ua, sec-ch-ua-mobile, sec-ch-ua-platform を含めます (HTTPS 通信の場合に限ります)。Windows 10 22H2 の Microsoft Edge 112.0.1722.48 の場合以下の通りとなります。

sec-ch-ua: "Chromium";v="112", "Microsoft Edge";v="112", "Not:A-Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"

上の情報からは OS が Windows だというのは分かりますがバージョンは分かりません。バージョン情報もブラウザが送信するようにするには、Web サーバーからの応答ヘッダに、

Accept-CH: Sec-CH-UA-Platform-Version

を含めます。そうすると、次の要求からブラウザが要求ヘッダに、

sec-ch-ua-platform-version: "10.0.0"

という OS のバージョン情報を含めて送信してくれるようになります。 (上の例で OS は Windows 10 22H2)

ただし、ブラウザからの最初の要求にはバージョン情報は含まれず、次の要求からになります。そこが使いにくい点かもしれません。

そこが問題であれば、上に紹介した Microsoft のドキュメントの「検出パフォーマンスの最適化 Critical-CH」セクションに書いてあるように、Accept-CH と一緒に Critical-CH ヘッダーを応答ヘッダに含めてクライアント(ブラウザ)に送るのが良さそうです。

具体的には、例えば、Web サーバーからの応答ヘッダに、

Accept-CH: Sec-CH-UA-Platform-Version
Critical-CH: Sec-CH-UA-Platform-Version 

を含めると、ブラウザは最初の応答を受けた後、直ちに再度要求を出し、その要求ヘッダには Critical-CH ヘッダーに指定された情報(この例では sec-ch-ua-platform-version: "10.0.0")を含めて送信してくれます。

以降は、ユーザーがブラウザの「Cookie およびその他のサイト データ」を消去しない限り、ブラウザは指定された情報を送信し続けます。

また、再度 Accept-CH と一緒に Critical-CH ヘッダーを応答ヘッダに含めて送信しても 2 回要求が出るということはありません。

以下に、Fiddler で要求・応答をキャプチャした画像を貼って説明を加えておきます。

(1) 最初の要求

(1) 最初の要求

Default.aspx.cs の Page_Load に以下の C# コードを実装して要求をかけたものです。

Response.AppendHeader("Accept-CH", "Sec-CH-UA-Platform-Version");
Response.AppendHeader("Critical-CH", "Sec-CH-UA-Platform-Version");

画像の青枠部分がブラウザからの要求ヘッダに含まれる User-Agent Client Hints 情報です。この時点では、バージョン情報 Sec-CH-UA-Platform-Version は含まれていません。

画像の赤枠部分に示した通り、Web サーバーからの応答ヘッダには上の C# コードで指定した Accept-CH と Critical-CH が設定されています。

(2) 自動的に再度要求が出る

(2) 自動的に再度要求が出る

ブラウザは Web サーバーからの応答ヘッダの Accept-CH と Critical-CH を見て自動的に再度 Default.aspx に要求を出します。

画像の青枠の通り要求ヘッダにバージョン情報 sec-ch-ua-platform-version が含まれています。

(3) 他のページを要求

(3) 他のページを要求

Web サーバーからの応答ヘッダに Accept-CH, Critical-CH を含まない他のページ(この例では Contact.aspx)を要求してみます。

画像の青枠の通り要求ヘッダに sec-ch-ua-platform-version が含まれています。

(4) 再度 Default.aspx を要求

(4) 再度 Default.aspx を要求

ブラウザから再度 Default.aspx を要求してみます。画像の赤枠の通り Web サーバーからの応答ヘッダには Accept-CH と Critical-CH が含まれていますが、、上の (2) のように再度要求が出ることはありません。


ASP.NET の C# のコードでブラウザからの要求ヘッダに含まれる Sec-CH-UA-Platform および Sec-CH-UA-Platform-Version の情報を取得するには以下のようにします。

string platform = Request.ServerVariables["HTTP_SEC_CH_UA_PLATFORM"];
string version = Request.ServerVariables["HTTP_SEC_CH_UA_PLATFORM_VERSION"];

上のコードで、platform, version に取得された文字列はダブルクォーテーション " で囲まれるので注意してください。

Tags: ,

ASP.NET

MVC5 での Dependency Injection

by WebSurfer 2023年4月15日 16:11

.NET Framework の ASP.NET MVC5 アプリで Microsoft.Extensions.DependencyInjection 名前空間にあるクラスを利用して Dependency Injection (DI) 機能を実装してみましたので、忘れないように備忘録として残しておきます。

MVC5 で Dependency Injection

ターゲットフレームワークが .NET Framework 4.6.1 以降であれば ASP.NET Core で DI に使われている Microsoft.Extensions.DependencyInjection 名前空間にあるクラス類を利用できるそうなので試してみた次第です。

参考にしたのはググって探して GitHub に見つかった ASP.NET MVC and ServiceCollection sample という記事です。その記事のコードをほぼそのまま Global.asax.cs にコピーすれば必要最低限の DI 機能が実装できます。

ベースとした ASP.NET MVC5 アプリのプロジェクトは Visual Studio 2022 のテンプレートを使ってターゲットフレームワークを .NET Framework 4.8 として作成したものです。

プロジェクトの作成

そのプロジェクトに NuGet から Microsoft.Extensions.DependencyInjection をインストールします。この記事を書いた時点での最新版 7.0.0 をインストールしました。(他に関係パッケージが 4 つ同時に自動的にインストールされます)

NuGet パッケージ

自動生成された Global.asax.cs を開き以下のように書き換えます。参考にした記事 ASP.NET MVC and ServiceCollection sample のコードほぼそのままです。自分なりに調べていろいろ分かったことをコメントとして追記してあります。

using System;
using System.Collections.Generic;
using System.Web;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using Microsoft.Extensions.DependencyInjection;
using Mvc5DependencyInjection.Controllers;
using Mvc5DependencyInjection.Repositories;

// 下の設定により Application_Start より前に InitModule メソッド
// が実行され HTTP モジュール ServiceScopeModule が登録される
[assembly: PreApplicationStartMethod(
    typeof(Mvc5DependencyInjection.MvcApplication), "InitModule")]

namespace Mvc5DependencyInjection
{
    public class MvcApplication : System.Web.HttpApplication
    {
        public static void InitModule()
        {
            // HTTP モジュール ServiceScopeModule を登録
            RegisterModule(typeof(ServiceScopeModule));
        }

        protected void Application_Start()
        {
            // DI コンテナの作成
            var services = new ServiceCollection();

            // DI コンテナにサービスを登録する。
            // ConfigureServices メソッドは下のコード参照
            ConfigureServices(services);

            // サービスプロバイダの作成
            ServiceProvider provider = services.BuildServiceProvider();

            // HTTP モジュール ServiceScopeModule に上の provider を渡し、
            // モジュール内でサービスプロバイダを利用できるようにする
            ServiceScopeModule.SetServiceProvider(provider);

            // 下の 4 行は Global.asax.cs にもともと含まれていたもの
            AreaRegistration.RegisterAllAreas();
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);

            // SetResolver メソッドは Microsoft のドキュメントによると
            // "Provides a registration point for dependency resolvers,
            // using the specified dependency resolver interface." との
            // こと。要するに、これにより MVC5 アプリで DI が実現できる
            // ようになるらしい
            var resolver = new ServiceProviderDependencyResolver();
            DependencyResolver.SetResolver(resolver);
        }

        // DI コンテナにサービスを登録するメソッド。
        // Controller については、ASP.NET Core では AddController,
        // AddMvc, AddControllersWithViews, AddRazorPages などを使うと
        // アセンブリをスキャンして全てを DI コンテナに登録するという
        // ことが行われるようだが、それはこのサンプルでは未実装。
        // すべて以下のメソッド内で登録する。
        private void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<ScopedThing>();
            services.AddTransient<HomeController>();
        }
    }

    // このサンプルではこのオブジェクトを HomeController に Inject
    public class ScopedThing : IDisposable
    {
        public ScopedThing()
        {
        }

        // Home/Index にてメッセージを取得できるように追加
        public string GetMessage()
        {
            return "Message form ScopedThing";
        }


        public void Dispose()
        {
        }
    }

    // HTTP モジュール
    // BeginRequest で IServiceScope を取得して HttpContext.Items
    // に保持する。EndRequest で IServiceScope を Dispose すること
    // により登録されたサービス (AddSingleton で登録されたものは除
    // く) は Dispose される
    internal class ServiceScopeModule : IHttpModule
    {
        private static ServiceProvider _serviceProvider;

        // インターフェイスに定義されているので実装が必要
        public void Dispose() { }

        public void Init(HttpApplication context)
        {
            context.BeginRequest += Context_BeginRequest;
            context.EndRequest += Context_EndRequest;
        }

        // IServiceScope オブジェクトを HttpContext.Items に保持
        private void Context_BeginRequest(object sender, EventArgs e)
        {
            var context = ((HttpApplication)sender).Context;
            context.Items[typeof(IServiceScope)] = 
                _serviceProvider.CreateScope();
        }

        // IServiceScope オブジェクトを Dispose する
        private void Context_EndRequest(object sender, EventArgs e)
        {
            var context = ((HttpApplication)sender).Context;
            if (context.Items[typeof(IServiceScope)] 
                is IServiceScope scope)
            {
                scope.Dispose();
            }
        }

        // Application_Start でサービスプロバイダが渡される
        public static void SetServiceProvider(
            ServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }
    }

    // BeginRequest で IServiceScope が HttpContext.Items に保持され
    // ているので、それを使って DI に登録されたサービスを取得できる
    internal class ServiceProviderDependencyResolver 
                   : IDependencyResolver
    {
        public object GetService(Type serviceType)
        {
            if (HttpContext.Current?.Items[typeof(IServiceScope)] 
                is IServiceScope scope)
            {
                return scope.ServiceProvider.GetService(serviceType);
            }

            throw new InvalidOperationException(
                                    "IServiceScope not provided");
        }

        public IEnumerable<object> GetServices(Type serviceType)
        {
            if (HttpContext.Current?.Items[typeof(IServiceScope)] 
                is IServiceScope scope)
            {
                return scope.ServiceProvider.GetServices(serviceType);
            }

            throw new InvalidOperationException(
                                      "IServiceScope not provided");
        }
    }
}

上の ConfigureServices メソッドに Controller とそれが依存するクラスを登録すれば DI 機能は働くようになります。

この記事では HomeController と ScopedThing を登録しています。自動生成された HomeController のコードに手を加えて、以下のようにコンストラクタ経由で ScopedThing を受け取れるようにすれば、クライアントからの要求を受けて HomeController が呼び出されたときに DI 機能により ScopedThing のインスタンスへの参照がコンストラクタの引数に渡されます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace Mvc5DependencyInjection.Controllers
{
    public class HomeController : Controller
    {
        private readonly ScopedThing _scopedThing;

        public HomeController(ScopedThing scopedThing)
        {
            this._scopedThing = scopedThing;
        }

        public ActionResult Index()
        {
            string msg = _scopedThing.GetMessage();
            ViewBag.Msg = msg;
            return View();
        }        
    }
}

Index アクションメソッドに書いたように、Inject された ScopedThing インスタンスの GetMessage メソッドで "Message form ScopedThing" という文字列を取得できます。

処理が終わると HttpApplication.EndRequest イベントが発生するので、HTTP モジュール ServiceScopeModule により ScopedThing の Dispose メソッドが自動的に呼ばれます。


先の記事「ASP.NET MVC5 で Autofac.Mvc5 使って DI」と同様に、下の画像のようなリポジトリパターンを実装し、「Entity データモデル」を「本番用クラス」に、「本番用クラス」を「コントローラークラス」に Inject することもできます。

リポジトリパターン

具体例を書くと、まず、先の記事と同様に (1) Visual Studio の ADO.NET Entity Data Model ウィザードで Entity Data Model (EDM) を作成し、(2) それをベースにスキャフォールディング機能を使って CRUD 用の Controller を自動生成させ、(3) Controller から SQL Server にアクセスして操作するコードをリポジトリクラスに切り出し、(4) Controller にリポジトリクラスを Inject できるようにコンストラクタを追加します。その手順の詳しい説明とコード例は先の記事にありますので見てください。

その後、作成した EDM のコンテキストクラス、リポジトリクラス、コントローラークラス を上の Global.asax.cs の ConfigureServices メソッドで DI コンテナに登録します。以下のコードで「// 追加」とコメントした下の 3 行がそれです。それだけで DI 機能が働くようになります。

private void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<ScopedThing>();
    services.AddTransient<HomeController>();

    // 追加
    services.AddScoped<NORTHWINDEntities>();
    services.AddScoped<IProductRepository, ProductRepository>();
    services.AddTransient<ProductsController>();
}

上の画像の「コントローラークラス」が ProductsController に、「インターフェイス」が IProductRepository に、「本番用クラス」が ProductRepository に、「Entity データモデル」が NORTHWINDEntities に該当します。

ブラウザから Prodtcuts/Index を要求すると、ProductsController が依存する ProductRepository、ProductRepository が依存する NORTHWINDEntities は DI 機能により自動的に Inject され、SQL Server からデータを取得して結果が表示されます。それがこの記事の一番上の画像です。

Tags: , ,

MVC

.NET Framework での Dependency Injection

by WebSurfer 2023年4月13日 15:12

ASP.NET Core で Dependency Injection (DI) に使われている Microsoft.Extensions.DependencyInjection 名前空間にあるクラス類は .NET Framework でもバージョン 4.6.1 以降であれば利用できるそうですので、.NET Framework 4.8 のコンソールアプリで試してみました。

先の記事「.NET Core での Dependency Injection」でターゲットフレームワーク .NET 5.0 のコンソールアプリに DI 機能を実装してみましたが、その .NET Framework 4.8 版です。

まず、Visual Studio 2022 のテンプレートを使ってターゲットフレームワーク .NET Framework 4.8 でコンソースアプリを作成し、NuGet から Microsoft.Extensions.DependencyInjection をインストールします。この記事を書いた時点での最新版 7.0.0 をインストールしました。

NuGet でインストール

他の Micosoft.Bcl.AsyncInterfaces などのパッケージは Microsoft.Extensions.DependencyInjection をインストールした時に同時に自動的にインストールされたものです。

検証に使ったコードは以下の通りです。先の記事「.NET Core での Dependency Injection」のものと同じです。

using Microsoft.Extensions.DependencyInjection;
using System;

namespace ConsoleAppDependencyInjection
{
    internal class Program
    {
        static void Main(string[] args)
        {
            IServiceCollection services = new ServiceCollection();

            services.AddTransient<IOrderRepository, SqlOrderRepository>();
            services.AddSingleton<ILogger, Logger>();
            services.AddScoped<IEventPublisher, EventPublisher>();
            services.AddTransient<CancelOrderHandler>();

            var provider = services.BuildServiceProvider();
            var handler = provider.GetRequiredService<CancelOrderHandler>();

            var orderId = Guid.NewGuid();
            var command = new Order { OrderId = orderId };
            handler.Handle(command);
        }
    }

    public class CancelOrderHandler
    {
        private readonly IOrderRepository repository;
        private readonly ILogger logger;
        private readonly IEventPublisher publisher;

        // Use constructor injection for the dependencies
        public CancelOrderHandler(IOrderRepository repository,
                                  ILogger logger,
                                  IEventPublisher publisher)
        {
            this.repository = repository;
            this.logger = logger;
            this.publisher = publisher;
        }

        public void Handle(Order command)
        {
            this.logger.Log($"Cancelling order {command.OrderId}");
            var order = this.repository.GetById(command.OrderId);
            order.OrderStatus = "Cancelled";
            this.repository.Save(order);
            this.publisher.Publish(order);
        }
    }

    public interface IOrderRepository
    {
        Order GetById(Guid orderId);

        void Save(Order order);
    }

    public class SqlOrderRepository : IOrderRepository
    {
        private readonly ILogger logger;

        // Use constructor injection for the dependencies
        public SqlOrderRepository(ILogger logger)
        {
            this.logger = logger;
        }

        public Order GetById(Guid orderId)
        {
            this.logger.Log($"Getting Order {orderId}");

            // Retrieve from db.・・・のつもり
            var order = new Order
            {
                OrderId = orderId,
                ProductName = "911-GT3",
                OrderStatus = "Ordered"
            };

            return order;
        }

        public void Save(Order order)
        {
            this.logger.Log($"Saving order {order.OrderId}");
            // Save to db.
        }
    }

    public interface ILogger
    {
        void Log(string log);
    }

    public class Logger : ILogger
    {
        public void Log(string log)
        {
            Console.WriteLine(log);
        }
    }

    public interface IEventPublisher
    {
        void Publish(Order order);
    }

    public class EventPublisher : IEventPublisher
    {
        public void Publish(Order order)
        {
            Console.WriteLine($"Publish order {order.OrderId}, " +
                $"{order.ProductName}, {order.OrderStatus}");
        }
    }

    public class Order
    {
        public Guid OrderId { get; set; }

        public string ProductName { get; set; }

        public string OrderStatus { get; set; }
    }
}

実行結果は以下の画像のようになります。先の記事の .NET 5.0 版と同様に期待した結果になっています。

実行結果

Tags: ,

.NET Framework

About this blog

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

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar