WebSurfer's Home

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

.NET Core での Dependency Injection

by WebSurfer 2021年1月1日 14:35

.NET Core アプリでは Microsoft の Microsoft.Extensions.DependencyInjection 名前空間にあるクラス類を使って Dependency Injection (DI) 機能を実装できるという話を書きます。

NuGet でインストール

Visual Studio 2019 のテンプレートで ASP.NET Core MVC / Razor Page アプリを作成すると DI コンテナを含めて DI に必要な機能がフレームワークに組み込まれ、要求を受けて ASP.NET が Controller を初期化する際、Controller が依存するクラスのインスタンスが自動的に生成され、そのインスタンスへの参照が Controller のコンストラクタの引数に渡されるようになっています。(詳しくは Microsoft のドキュメント「ASP.NET Core での依存関係の挿入」を見てください)

その際に使われているのが Microsoft.Extensions.DependencyInjection 名前空間にあるクラス類だそうです。なので、ASP.NET Core アプリでなくても、例えば .NET Core コンソールアプリでも、上の画像のように NuGet で必要なアセンブリをプロジェクトにインストールすれば DI 機能を実装できます。

知ってました? 実は無知な自分は最近まで知らなかったです。(汗) 試しに .NET 5.0 ベースのコンソールアプリに DI 機能を実装してみましたので、備忘録として書いておくことにした次第です。

サードパーティ製の DI コンテナは巷に多々あるそうで、自分も Simple Injector を使ってみたことがあります。(その話は「SimpleInjector を ASP.NET MVC & Web API で利用」に書きましたので興味があれば見てください)

Simple Injector なら多少の実装経験があるということで、比較のために Simple Injector のドキュメント Quick Start にあるサンプルとほぼ同じコンソールアプリを、Microsoft.Extensions.DependencyInjection 名前空間の ServiceCollection クラスServiceProvider クラスを利用して実装してみました。

そのコードは以下の通りです。サービスに DI するクラスを登録するのに AddTransient, AddScoped, AddSingleton の 3 種類を使ってますが、使い分けているわけではなくて、このようなコンソールアプリではどれを使っても同じで、試しに全種類を使ってみただけですのでご注意ください。違いはこの記事の下の方で説明します。

using System;
using Microsoft.Extensions.DependencyInjection;

namespace ConsoleAppDependencyInjection
{
    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
    {
        public Order GetById(Guid orderId);

        public 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
    {
        public 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; }
    }
}

実行結果は以下の画像のようになります。

実行結果

上のコードでサービスに DI するクラスを登録する AddSingleton, AddScoped, AddTransient メソッドの違いは、Microsoft.Extensions.DependencyInjection Deep Dive という記事の「生成と破棄」のセクションに詳しく書いてあります。

上の記事の説明では違いが自分には分かりませんでしたが、ASP.NET Web アプリで説明すると以下のようになるようです。

  • AddSingleton: 一度 DI されると、アプリケーションが終了するまで、最初の DI で生成されたインスタンスを使いまわす
  • AddScoped: 一つの HTTP 要求から応答を返すまでの間では、最初の DI で生成されたインスタンスをその後の DI でも使いまわす
  • AddTransient: DI が行われるたびに新しいインスタンスを生成する

ASP.NET アプリはクライアントから要求を受けるたびにスレッドプールからスレッドを取得し、処理に必要なアセンブリをメモリにロードし、処理が完了してクライアントに応答を返すとメモリをクリアし、使ったスレッドをプールに戻すマルチスレッドアプリです。(ググって調べた記事 Is Kestrel using a single thread for processing requests like Node.js? などを見た限りですが Kestrel も同じだそうです。)

AddSingleton を選んだ場合、ワーカープロセスが立ち上がった後の最初の DI でインスタンスが生成されると、ワーカープロセスがリサイクルされるまで、すべての要求に同じインスタンスが使い回されるということになります。初期化するのに時間がかかるとかメモリなどリソースを大量に消費するクラスを DI する場合には AddSingleton の利用を考えるのがよさそうです。

AddScoped の使い道、即ち一回の要求から応答を返すまでの間に同じクラスを複数回 DI するケースというのは、View への DI もサポートされていること(詳しくは「ASP.NET Core でのビューへの依存関係の挿入」参照)、サービス、ミドルウェアその他カスタムクラスにも DI 機能を実装することができることを考えるといろいろありそうです。

例えば、Visual Studio で作成したプロジェクトでは、ASP.NET Identity を利用した認証関係の Razor Class Library (RCL) のページモデルと _LoginPartial.cshtml では、SignInManager<IdentityUser>, UserManager<IdentityUser> を DI する設定になっています。そういうクラスは AddScoped で ServiceCollection に登録しておくのが良さそうです。

AddTransient を使うと、Controller、View、サービス、ミドルウェアその他カスタムクラスで DI 操作が行われるたび、新たにインスタンスを生成してそれへの参照を渡すということになります。Account confirmation and password recovery in ASP.NET Core に書いてあった EmailSender クラスの登録などで例を見ました。(実際に AddTransient を使う必要があるのかは分かりませんが)

Tags: , ,

CORE

Linq の GroupBy

by WebSurfer 2020年12月4日 12:33

Linq to Objects で GroupBy メソッドを使って IEnumerable<IGrouping<TKey, TElement>> オブジェクトを取得できるということを備忘録として書いておきます。

SELECT クエリ

SQL 文の GROUP BY 句はグループごとの集計を取るために使われるものと理解しています。例えば上の画像にあるように、Products テーブルから SupplierID 別に商品単価の最高と最低の値を取得するというように。(DB は Microsoft が提供するサンプルデータベース Northwind です)

なので、GROUP BY 句で指定したフィールド(上の例では SupplierID)以外や、最大値と最小値を返す MAX(), MIN()、合計を返す SUM() や平均を返す AVG() など以外を SELECT することはできません。

例えば、上の画像の SELECT クエリで ProductName を SELECT しようとすると "列 'NORTHWIND.dbo.Products.ProductName' は選択リスト内では無効です。この列は集計関数または GROUP BY 句に含まれていません" というエラーになります。

Linq でも同様で、下のような Select メソッドなしで使うものではないと思っていました。

var result = await _context.Products.GroupBy(p => p.SupplierId).
                   Select(g => new 
                   { 
                       Id = g.Key, 
                       Max = g.Max(g => g.UnitPrice), 
                       Min = g.Min(g => g.UnitPrice) 
                   }).
                   ToListAsync();

Select メソッドなしで使うと、例えば以下のようにすると "Client side GroupBy is not supported" というエラーになります。(注: これは Core 3.0 以降の話で、Core 2.x 以前はサポートされていたそうです。.NET Framework の EF でもサポートされています。詳しくは Breaking changes included in EF Core 3.x の最初のセクション LINQ queries are no longer evaluated on the client を見てください)

var result1 = await _context.Products.GroupBy(p => p.SupplierId).
                    ToListAsync();

// または

var result2 = await (from p in _context.Products
                     group p by p.SupplierId into g
                     select g).ToListAsync();

そのエラーの原因は Linq to Entities で上のような GroupBy を使った Linq 式は SQL 文に変換できないということのようです。(ただし、Core 2.x 以前と .NET Framework では EF 側でオブジェクトの生成をサポートしていて、上記のコードでも OK です)

でも、Linq to Objects の場合は話が違ってきます。以下のコードは ASP.NET Core MVC のアクションメソッドですが、IEnumerable<IGrouping<int?, Products>> オブジェクトを model に取得して View に渡すことができています。

public async Task<IActionResult> ListBySupplier2()
{
    // Linq to Objects で GroupBy を使うのは問題ない
    List<Products> productList = await _context.Products.ToListAsync();
    var model = productList.GroupBy(p => p.SupplierId);

    return View(model);
}

Visual Studio 2019 のデバッガで上のコードの model を展開すると以下の画像のようになっています。

デバッガで見た model

これが全くの予想外で驚いたので備忘録として残しておくことにした次第です。(自分が無知なだけということかもしれませんけど)

これが何の役に立つのかと言われると答えに窮しますが、例えば以下の View を使って SupplierID 別にテーブルを作ると言ったことができます。

@model IEnumerable<IGrouping<int?, Products>>

@{
    ViewData["Title"] = "ListBySupplier2";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h1>ListBySupplier2</h1>

@foreach (var item in Model)
{
    <table class="table">
        <thead>
            <tr>
                <th>
                    @Html.DisplayNameFor(m => item.First().ProductId)
                </th>
                <th>
                    @Html.DisplayNameFor(m => item.First().ProductName)
                </th>
                <th>
                    @Html.DisplayNameFor(m => item.First().SupplierId)
                </th>
                <th>
                    @Html.DisplayNameFor(m => item.First().UnitPrice)
                </th>
            </tr>
        </thead>
        <tbody>
            @foreach (var product in item)
            {
                <tr>
                    <td>
                        @Html.DisplayFor(m => product.ProductId)
                    </td>
                    <td>
                        @Html.DisplayFor(m => product.ProductName)
                    </td>
                    <td>
                        @Html.DisplayFor(m => product.SupplierId)
                    </td>
                    <td>
                        @Html.DisplayFor(m => product.UnitPrice)
                    </td>
                </tr>
            }
        </tbody>
    </table>
}

上のコードで foreach (var item in model) の item は IGrouping<int?, Products> に、foreach (var product in item) の product は Products になります。

結果、ブラウザには以下のように SupplierID 別のテーブルが表示されます。

結果

ORDER BY [SupplierID] で並べて一つのテーブルに表示した方がスマートかつ見やすいと思いますが、それはとりあえず置いときます。(笑)

Tags: , ,

ADO.NET

ASP.NET と HttpClient (CORE)

by WebSurfer 2020年11月8日 14:52

ASP.NET Core 3.1 MVC アプリから HttpClient を利用して他のサイトの ASP.NET Web API にアクセスして情報を取得する方法を書きます。

ASP.NET と HttpClient

HttpClient のインスタンスを生成すると、そのたびにソケットも生成されます。しかし、HttpClient のインスタンスを Dispose してもソケットはクローズされないので(下記注参照)、何度も繰り返すとソケットの枯渇につながるという問題があり、それを避けるため、HttpClient のインスタンスはシングルトンにしてアプリで使いまわすということを行うそうです。ただし、そうすると DNS の変更が反映されないという別の問題があるそうですが。

注: Microsoft のドキュメント「ASP.NET Core パフォーマンスのベストプラクティス」によると "Closed HttpClient instances leave sockets open in the TIME_WAIT state for a short period of time" とのことです。別の記事「開発者を苦しめる.NETのHttpClientのバグと紛らわしいドキュメント」にはデフォルトは 4 分と書いてあります。

.NET Framework 版の ASP.NET Web アプリでの対処方法は Microsoft のドキュメント「Improper Instantiation antipattern」の How to fix the problem というセクションに書かれているのを見つけました。

そのドキュメントには、コントローラーに、

private static readonly HttpClient httpClient;

という static フィールドを設けて、コントローラーのコンストラクタで、

httpClient = new HttpClient();

とすると書いてあります。しかし、コントローラーのコンストラクタはクライアントから要求を受けるたびに呼び出されるので、要求を受けるたびに HttpClient のインスタンスを新たに作るということになってしまうと思うのですが・・・ 無知な自分には何故それが問題ないのか理解し難いです。

でも、まぁ、Microsoft のドキュメントですし、検証したようですし、.NET Framework 版の ASP.NET アプリでは他に適当な手はなさそうですし、もし問題が起きたら Microsoft のせいにできるので(笑)、その方法を使ってみるのが良いかもしれません。

しかし、Core 2.1 以降の ASP.NET Web アプリでは話が違ってくるようで、Microsoft の以下のドキュメントに書いてある IHttpClientFactory を利用する手段があるそうです。

詳しい仕組みの理解はちょっと置いといて、要するに上の一番目の記事の IHttpClientFactory の代替手段のセクションに書いてある以下の点を信じればよさそうです。(翻訳がイマイチなので英語版)

Using IHttpClientFactory in a DI-enabled app avoids:

  • Resource exhaustion problems by pooling HttpMessageHandler instances.
  • Stale DNS problems by cycling HttpMessageHandler instances at regular intervals.

上の 2 つの問題の前者は HttpClient のインスタンスの生成・廃棄を繰り返すことによるソケットの枯渇、後者はそれに対処するためシングルトンにして長期に使いまわすと DNS の変更が反映されないことを言っており、Core に備わっている DI 機能を使って IHttpClientFactory オブジェクトを注入する方法でそれらの問題を回避できるということのようです。

というわけで、詳しい仕組みは理解できてませんが、とりあえず上の一番目の記事の「基本的な使用方法」のセクションに従って��装してみました。

Startup.cs

namespace MvcCoreApp
{
    public class Startup
    {        
        // ・・・中略・・・

        public void ConfigureServices(IServiceCollection services)
        {
            // 以下を追加。これにより IHttpClientFactory を DI できる
            services.AddHttpClient();

        // ・・・中略・・・
}

Controller / Action Method

using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using System.Net.Http;
using System.IO;
using System.Text.Json;
using System.Text.Encodings.Web;
using System.Text.Unicode;

namespace MvcCoreApp.Controllers
{
    public class IHttpClientFactoryController : Controller
    {
        private readonly IHttpClientFactory _clientFactory;

        public IHttpClientFactoryController(
                              IHttpClientFactory clientFactory)
        {
            _clientFactory = clientFactory;
        }

        public async Task<IActionResult> Index()
        {
            var request = new HttpRequestMessage(HttpMethod.Get, 
                                    "https://localhost:44365/values");
            HttpClient client = _clientFactory.CreateClient();
            HttpResponseMessage response = await client.SendAsync(request);
            List<Hero> list = null;

            if (response.IsSuccessStatusCode)
            {
                using (Stream responseStream = 
                              await response.Content.ReadAsStreamAsync())
                {
                    list = await System.Text.Json.JsonSerializer.
                           DeserializeAsync<List<Hero>>(responseStream);
                }
            }

            // JSON 文字列のエスケープ回避&インデント設定
            return Json(list, new JsonSerializerOptions
            {
                Encoder = JavaScriptEncoder.Create(UnicodeRanges.All),
                WriteIndented = true,
            });
        }
    }

    public class Hero
    {
        public int id { get; set; }
        public string name { get; set; }
    }
}

上のコントローラーの Index アクションメソッドを呼び出した結果がこの記事の上の方にある画像です。一応動くということを確認しただけで、ソケットの枯渇とか DNS の変更に対応できているかは分かりませんが。 (汗)

最後にもう一つ。ASP.NET Core 3.1 Web API が返す JSON 文字列のキーの最初の文字が小文字になってしまうことに注意してください。Web API でも同様で、デフォルトで camel casing になるということだそうです。なので、上の Hero クラスのプロパティの最初の文字を小文字にしておかないとデシリアライズに失敗します。camel casing になるのを回避する方法はあります。詳しくは別の記事「JsonSerializer の Camel Casing」を見てください。

Tags: , ,

CORE

About this blog

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

Calendar

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

View posts in large calendar