WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

.NET Core での Dependency Injection

by WebSurfer 1. January 2021 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

タスク並列ライブラリ (TPL)

by WebSurfer 27. December 2020 14:53

タスク並列ライブラリ (TPL) は Windows Forms のような GUI アプリでは使い難い点があるということを今更ながらですが学びましたので、備忘録として書いておきます。

タスク並列ライブラリの検証

最近の PC の CPU はマルチコアが当たり前になっているようですが、PC のマルチコアを有効に使うプログラミングをサポートするため、.NET Framework 4.0 で導入されたタスク並列ライブラリ (TPL・・・Parallel.For とか Parallel.Invoke とか) を利用するという話を巷でよく耳にします。

マルチスレッドの基本的な話ですが、@IT の記事「第1回 マルチスレッドはこんなときに使う (1/2)」の「マルチスレッドの動作原理」セクションを見てください。リンク切れになると困るので画像だけ借用して以下に表示しておきます。

マルチスレッドの比較

② のようになると OS がスレッドを切り替えて処理を行うのでそのオーバーヘッドの分逆に遅くなるということになります。なので、マルチスレッドアプリで処置時間の短縮を図るなら ③ のようにマルチコアを利用できる環境が必要で、さらにマルチコアを有効に利用するプログラミングを行うという話になると思います。

その際に自分がよく聞くのがタスク並列ライブラリ (TPL) や Parallel LINQ (PLINQ) を使うという話です。Microsoft のドキュメント「.NET での並列プログラミング」や、ググるとヒットする記事例えば @IT の記事「ループをParallelクラスで並列処理にするには?」を読むと TPL, PLINQ は並列処理には万能のような気がしてました。

Microsoft のドキュメント「タスク並列ライブラリ (TPL)」には、

"TPL は、使用可能なすべてのプロセッサを最も効率的に使用するように、コンカレンシーの程度を動的に拡大します。The TPL scales the degree of concurrency dynamically to most efficiently use all the processors that are available."

・・・と言う記述がありますし、PC のマルチコアを有効に使うという局面に限れば最強のように思えます。

しかし、Windows Forms のような GUI アプリではそうでもなさそうな感じです。自分が気が付いた限りですが、以下の 2 点が問題だと思いました。(自分が回避策を知らないだけという可能性は否定できませんが)

  1. 並列に実行される複数のメソッドがすべて完了するまで UI がブロックされる。
  2. 並列実行するメソッドには非同期版は使えない。

以下のサンプルコードの ParallelInvoke_Click メソッドは Parallel.Invoke を使って 5 つの同期版メソッド Work を並列に実行するものですが、5 つのメソッドがすべて完了するまで UI がブロックされるので、アプリはフリーズしたようになります。その他の TPL, PLINK を使った xxxxx_Click メソッドも同様で、完了するまで UI がブロックされます。それを回避する手段は、自分が探した限りですが、なさそうです。

さらに、非同期版メソッド WorkAsync は使えません。詳しくはサンプルコードのコメントに書きましたので見てください。なので、ライブラリなどで非同期版メソッドしか提供されてない場合は何ともならないと思われます。

というわけで、下のサンプルコードの WhenAll1_Click, WhenAll2_Click メソッドのように、Parallel.Invoke など使わないで、await Task.WhenAll(...) で待機するようにし、並列化については OS に任せるのが良さそうと思いました。(実際にサンプルを動かしてみると並列化してくれているような感じはしました。② のようになっている可能性は否定しきれませんが)

非同期版メソッド WorkAsync も使えます。下のサンプルコードの WhenAll2_Click メソッドを見てください。上の画像がその実行結果です。(ThreadID が 1 で UI スレッドと同じなのは、WorkAsync メソッドの中で ManagedThreadId を取得するのが UI スレッドだからです)

using System;
using System.Windows.Forms;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;

namespace WinFormsApp1
{
    public partial class TaskParallelLibrary : Form
    {
        public TaskParallelLibrary()
        {
            InitializeComponent();
        }

        // 同期版メソッド
        private string Work(int n)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            string retunVlaue =
                $"n = {n}, ThreadID = {id}, start:{DateTime.Now:ss.fff}, ";

            Thread.Sleep(3000);

            retunVlaue += $"end:{DateTime.Now:ss.fff}";

            return retunVlaue;
        }

        // 非同期版メソッド
        private async Task<string> WorkAsync(int n)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            string retunVlaue =
                $"n = {n}, ThreadID = {id}, start: {DateTime.Now:ss.fff}, ";

            await Task.Delay(3000);

            retunVlaue += $"end: {DateTime.Now:ss.fff}";

            return retunVlaue;
        }

        // 非同期版メソッド 5 つを for ループで逐次実行
        private async void InOrder_Click(object sender, EventArgs e)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            this.label1.Text = $"UI Thread ID = {id}\r\n";

            for (int n = 0; n < 5; n++)
            {
                this.label1.Text += await WorkAsync(n) + "\r\n";
            }

            this.label1.Text += "完了";
        }

        // 非同期版メソッド 5 つを Parallel.Invoke で実行
        // 終了まで UI はブロックされる
        private void ParallelInvoke_Click(object sender, EventArgs e)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            this.label1.Text = $"UI Thread ID = {id}\r\n";

            string[] results = new string[5];

            Parallel.Invoke(
                () => results[0] = Work(0),
                () => results[1] = Work(1),
                () => results[2] = Work(2),
                () => results[3] = Work(3),
                () => results[4] = Work(4));

            foreach (string result in results)
            {
                this.label1.Text += result + "\r\n";
            }

            this.label1.Text += "完了";

            // 非同期版メソッド WorkAsync は使えない

            // 以下のようにすると Task.Result でデッドロックになる
            //Task<string> result1 = null, result2 = null;
            //Parallel.Invoke(
            //    () => result1 = WorkAsync(0),
            //    () => result2 = WorkAsync(1));
            //this.label1.Text += result1.Result + "\r\n";
            //this.label1.Text += result2.Result + "\r\n";

            // 以下のようにすると await で待つことなく終わってしまう
            //string result1 = "", result2 = "";
            //Parallel.Invoke(
            //    async () => result1 = await WorkAsync(0),
            //    async () => result2 = await WorkAsync(1));
            //this.label1.Text += result1 + "\r\n";
            //this.label1.Text += result2 + "\r\n";

        }

        // 非同期版メソッド 5 つを Parallel.For で実行
        // 終了まで UI はブロックされる
        private void ParallelFor_Click(object sender, EventArgs e)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            this.label1.Text = $"UI Thread ID = {id}\r\n";

            string[] results = new string[5];

            Parallel.For(0, 5, n => results[n] = Work(n));

            foreach (string result in results)
            {
                this.label1.Text += result + "\r\n";
            }

            this.label1.Text += "完了";
        }

        // 非同期版メソッド 5 つを Parallel.ForEach で実行
        // 終了まで UI はブロックされる
        private void ParallelForEach_Click(object sender, EventArgs e)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            this.label1.Text = $"UI Thread ID = {id}\r\n";

            string[] results = new string[5];

            Parallel.ForEach(Enumerable.Range(0, 5), 
                             n => results[n] = Work(n));

            foreach (string result in results)
            {
                this.label1.Text += result + "\r\n";
            }

            this.label1.Text += "完了";
        }

        // 非同期版メソッド 5 つを PLINK で実行
        // 終了まで UI はブロックされる
        private void PLINK_Click(object sender, EventArgs e)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            this.label1.Text = $"UI Thread ID = {id}\r\n";

            var results = Enumerable.Range(0, 5).AsParallel().
                          Select(n => Work(n));

            foreach (string result in results)
            {
                this.label1.Text += result + "\r\n";
            }

            this.label1.Text += "完了";
        }

        // ***** 以下 TPL, PLINK に代えて Task.WhenAll を使用 *****

        // 同期版メソッド 5 つを Task.Run で実行、
        // await Task.WhenAll で待機        
        private async void WhenAll1_Click(object sender, EventArgs e)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            this.label1.Text = $"UI Thread ID = {id}\r\n";

            string[] results = new string[5];
            var taskList = new List<Task>();
            for (int n = 0; n < 5; n++)
            {
                int i = n;
                taskList.Add(Task.Run(() => results[i] = Work(i)));
            }

            // WaitAll は await で待機できないので注意
            await Task.WhenAll(taskList.ToArray());

            foreach (string result in results)
            {
                this.label1.Text += result + "\r\n";
            }

            this.label1.Text += "完了";
        }

        // 非同期版メソッド 5 つを実行、
        // await Task.WhenAll で待機
        private async void WhenAll2_Click(object sender, EventArgs e)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            this.label1.Text = $"UI Thread ID = {id}\r\n";

            Task<string>[] results = new Task<string>[5];
            var taskList = new List<Task>();
            for (int n = 0; n < 5; n++)
            {
                int i = n;
                taskList.Add(results[i] = WorkAsync(i));
            }

            await Task.WhenAll(taskList.ToArray());

            foreach (Task<string> result in results)
            {
                this.label1.Text += result.Result + "\r\n";
            }

            this.label1.Text += "完了";
        }

    }
}

Tags: , , , , ,

.NET Framework

Linq の GroupBy

by WebSurfer 4. December 2020 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

About this blog

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

Calendar

<<  March 2021  >>
MoTuWeThFrSaSu
22232425262728
1234567
891011121314
15161718192021
22232425262728
2930311234

View posts in large calendar