WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

WinForms で構成情報とコンテキストの DI (CORE)

by WebSurfer 30. March 2021 15:00

.NET Core 3.1 の Windows Forms アプリで、構成情報を取得する方法、さらに DI 機能を追加して、取得した構成情報と EF Core で利用するコンテキストクラスを DI する方法を書きます。

DataGridView に結果を表示

ASP.NET Core アプリのプロジェクトを Visual Studio 2019 のテンプレートを使って作成すると、appsettings.json などの構成ファイルが自動的に生成されてプロジェクトに含まれます。さらに、構成ファイルから情報を読み込んで IConfiguration オブジェクトが生成され、構成情報を取得できるようになります。(詳しくは Microsoft のドキュメント「ASP.NET Core の構成」参照)

また、作成したプロジェクトには DI 機能も自動的に組み込まれ、生成された IConfiguration オブジェクトを DI コンテナに登録し、必要に応じて Controller や Page のコンストラクタ経由で DI できる機能が実装されます。

DI コンテナには ILogger, UserManager, EF Core で使用するコンテキストクラスなども登録でき、これらも必要に応じて Controller や Page のコンストラクタ経由で DI できます。

Windows Forms やコンソールアプリの場合はそれらの機能は Visual Studio 2019 のテンプレートを使っても実装されず、自力でコードを書いて実装する必要があります。

.NET Core 3.1 の Windows Forms アプリで appsettings.json ファイルから IConfiguration オブジェクトを作って構成情報を取得する方法、DI機能を実装して IConfiguration オブジェクトとコンテキストクラスを DI する方法を以下に書きます。

(1) プロジェクトの作成

Visual Studio 2019 のテンプレートを利用して、ターゲットフレームワーク .NET Core 3.1 で Windows Forms アプリのプロジェクトを作成します。

VS2019 のテンプレート

ターゲットフレームワークを、この記事を書いた時点での最新 .NET 5.0 ではなく .NET Core 3.1 としたのは、3.1 が Long Term Support (LTS) 版であること、5.0 でサポートされた新機能を使用しなくても可能なことを確認するためです。

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

Visual Studio 2019 の[ツール(T)]⇒[NuGet パッケージマネージャ(N)]⇒[ソリューションの NuGet パッケージの管理(N)...] を開いて以下のパッケージをインストールします。

  1. Microsoft.EntityFrameworkCore.SqlServer
  2. Microsoft.EntityFrameworkCore.Tools
  3. Microsoft.Extensions.Configuration.Json
  4. Microsoft.Extensions.DependencyInjection

下の画像がインストールした結果です。各パッケージのバージョンはこの記事を書いた時点での最新です。プロジェクトのターゲットフレームワークは .NET Core 3.1 なのですが、それに合わせる必要はなかったです。

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

ちなみに、上のリストの 1 は EF Core を利用して SQL Server にアクセスして処理を行うための機能、2 は Visual Studio の NuGet Pakage Manager Console で Add-Migration, Scaffold-DbContext などのコマンドを利用できるようにするための機能、3 は appsettings.json などの構成ファイルから情報を取得するための機能、4 は DI 機能の実装のために必要です。

(3) appsettings.json の作成

ソリューションエクスプローラーを操作してプロジェクトに json ファイルを追加します。ファイル名は任意ですが、この記事では ASP.NET アプリに合わせて "appsettings.json" にしました。内容はこの記事では以下の通り接続文字列のみとしましたが、他に任意の情報を含めることができます。

{
  "ConnectionStrings": {
    "NorthwindConnection": "Data Source=(local)\\sqlexpress; ..."
  }
}

作成したらそのプロパティの中の「出力ディレクトリにコピー」を「常にコピーする」または「新しい場合はコピーする」に設定するのを忘れないようにしてください。接続文字列のバックスラッシュ \ は \\ にエスケープする必要があるので注意してください。

appsettings.json のプロパティ設定

(4) 構成情報が取得できることを確認

上記ステップ (1) で作成した Windows Forms アプリの Form1 のコンストラクタに以下のコードを追加して、変数 connString に appsettings.json に設定した接続文字列が取得できることを確認します。

using System.Windows.Forms;
using System.IO;
using WinFormsCore3App1.Contexts;

// NuGet packages:
// Microsoft.Extensions.Configuration.Json
// Microsoft.Extensions.DependencyInjection
// Microsoft.EntityFrameworkCore.SqlServer
// Microsoft.EntityFrameworkCore.Tools

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;

namespace WinFormsCore3App1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            // Configuration の生成
            // appsettings.json に接続文字列が含まれている
            var builder = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json", optional: false);
            IConfiguration config = builder.Build();

            // appsettings.json から取得した接続文字列
            string connString = 
                config.GetConnectionString("NorthwindConnection");
        }
    }
}

(5) DI 機能の実装

DI 機能の実装と IConfiguration オブジェクトの DI コンテナへの登録は以下の 2 行を上のステップ (4) の Form1 のコンストラクタに追加することで可能です。

// DI コンテナ
IServiceCollection services = new ServiceCollection();

// Configuration を DI コンテナに登録
services.AddSingleton(config);

Configuration を DI できるようにするにはさらなるコードの追加が必要ですが、それについては以下にコンテキストクラスの DI 方法と共に書きます。

(6) コンテキストクラスの生成

この記事では Microsoft のサンプルデータベース Northwind の Products, Categories, Suppliers テーブルからリバースエンジニアリングでコンテキストクラスとエンティティクラスを生成して使います。

詳しくは先の記事「スキャフォールディング機能 (CORE)」のステップ (1) を見てください。

上の appsettings.json と違ってバックスラッシュ \ はエスケープする必要はないところに注意してください。エスケープして \\ としたりするとエラーになります。

成功するとコンテキストクラス NorthwindContext.cs と各テーブルのエンティティクラス Product.cs, Category.cs, Supplier.cs が指定したフォルダに生成されます。

NorthwindContext.cs ファイルの NorthwindContext クラスの引数を持たないコンストラクタと OnConfiguring メソッドはコメントアウトしてください。

(7) ProductService クラスの作成

SQL Server から EF Core を利用してデータを取得するクラスを作成します。ソリューションエクスプローラーでクラスファイルを ProductService.cs という名前で追加し、以下のコードを実装します。

コードの説明はコメントに書きましたのでそれを見てください。

using Microsoft.Extensions.Configuration;
using WinFormsCore3App1.Contexts;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;

namespace WinFormsCore3App1
{
    public class ProductService
    {
        private readonly IConfiguration _configuration;
        private readonly NorthwindContext _context;

        // コンストラクタの引数経由で Configuration と
        // NorthwindContext のインスタンスを DI する
        public ProductService(IConfiguration configuration,
                              NorthwindContext context)
        {
            this._configuration = configuration;
            this._context = context;
        }

        // DI された Configuration から接続文字列が取得できる
        // ことを確認するための検証用メソッド
        public string GetConnectionString()
        {
            return _configuration
                   .GetConnectionString("NorthwindConnection");
        }

        // DI されたコンテキスト NorthwindContext を使ってSQL
        // Server の Northwind データベースからデータを取得し
        // List<ProductItem> 型のオブジェクトとして返す。
        // それを DataGridView に表示
        public async Task<List<ProductItem>> GetListAsync()
        {
            var list = from p in _context.Products
                       join s in _context.Suppliers
                       on p.SupplierId equals s.SupplierId
                       join c in _context.Categories
                       on p.CategoryId equals c.CategoryId
                       select new ProductItem 
                       { 
                           ProductId = p.ProductId,
                           ProductName = p.ProductName,
                           Supplier = s.CompanyName,
                           Category = c.CategoryName,
                           UnitPrice = p.UnitPrice.Value
                       };

            return await list.ToListAsync();
        }
    }

    // DataGridView に渡すデータを格納する Data Transfer
    // Object クラスの定義
    public class ProductItem
    {
        public int ProductId { get; set; }

        public string ProductName { get; set; }

        public string Supplier { get; set; }

        public string Category { get; set; }

        public decimal UnitPrice { get; set; }

    }
}

(8) Form1 クラスの完成

Form1 のコンストラクタで DI コンテナに Configuration, NorthwindContext, ProductService を登録し、ServiceProvider から ProductService のインスタンスを生成する際 Configuration と NorthwindContext のインスタンスはコンストラクタ経由 DI されるように設定します。

上記ステップ (4) のコードを含めた完全なコードは以下の通りです。実行した結果がこの記事の一番上にある画像です。

using System.Windows.Forms;
using System.IO;

// NuGet packages:
// Microsoft.Extensions.Configuration.Json
// Microsoft.Extensions.DependencyInjection
// Microsoft.EntityFrameworkCore.SqlServer
// Microsoft.EntityFrameworkCore.Tools

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using WinFormsCore3App1.Contexts;
using Microsoft.EntityFrameworkCore;

namespace WinFormsCore3App1
{
    public partial class Form1 : Form
    {
        // ProductService は SQL Server から EF Core を利用
        // してデータを取得するクラス
        private readonly ProductService productService;

        public Form1()
        {
            InitializeComponent();

            // Configuration の生成
            // appsettings.json に接続文字列が含まれている
            var builder = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json", optional: false);
            IConfiguration config = builder.Build();

            // appsettings.json から取得した接続文字列
            string connString = 
                config.GetConnectionString("NorthwindConnection");

            // DI コンテナ
            IServiceCollection services = new ServiceCollection();

            // Configuration を DI コンテナに登録
            services.AddSingleton(config);

            // NorthwindContext はリバースエンジニアリングで生成
            // したコンテキストクラス。それを DI コンテナに登録
            services.AddDbContext<NorthwindContext>(options =>
                options.UseSqlServer(connString));

            // ProductService を DI コンテナに登録
            services.AddSingleton<ProductService>();

            // ServiceProvider を生成
            var provider = services.BuildServiceProvider();

            // ServiceProvider から ProductService のインスタンス
            // を生成。その際、Configuration と NorthwindContext
            // のインスタンスはコンストラクタ DI される
            productService = 
                provider.GetRequiredService<ProductService>();

            // DataGridView と BindingSource はデザイン画面で
            // ツールボックスから Form にドラッグ&ドロップ
            dataGridView1.DataSource = bindingSource1;
        }

        private async void Form1_Load(object sender, System.EventArgs e)
        {
            // ProductService に DI した Configuration から接続
            // 文字列を取得できることの確認用
            var northwind = productService.GetConnectionString();

            // EF Core を使って Northwind の Products テーブルか
            // ら List<T> 型のデータを取得し DataGridView に表示 
            var list = await productService.GetListAsync();
            bindingSource1.DataSource = list;
        }
    }
}

Tags: , , ,

CORE

Windows Forms で IHttpClientFactory 利用 (CORE)

by WebSurfer 12. March 2021 13:05

Windows Forms アプリから Web API などにアクセスする際、IHttpClientFactory を利用して HTTP 接続プールを作り、それから HttpClient インスタンスを取得してアクセスする方法を書きます。

WinForms から ASP.NET Web API にアクセス

HttpClient のインスタンスを生成すると、そのたびにソケットも生成されます。しかし、HttpClient のインスタンスを Dispose してもソケットはすぐにはクローズされないので(デフォルトで 4 分かかるそう)、短期間で何度も生成 / Dispose を繰り返すとソケットの枯渇につながるという問題があり、それを避けるため、HttpClient のインスタンスはシングルトンにしてアプリで使いまわすということが推奨されています。ただし、長期にわたってシングルトンにした HttpClient のインスタンスを使い続けると、DNS の変更が反映されないという別の問題があるそうです。

先の記事「ASP.NET と HttpClient (CORE)」で書きましたように、Core 2.1 以降の ASP.NET Web アプリでは IHttpClientFactory を利用して、Microsoft のドキュメント「IHttpClientFactory を使用して回復力の高い HTTP 要求を実装する」の Figure 8-4 にあるように HTTP 接続をプールして使うことができるそうです。

上に紹介した Microsoft のドキュメントによると、IHttpClientFactory オブジェクトを利用することにより HTTP 接続プールが生成でき、プールから HttpClient を取得して使うことにより上記の問題(socket の枯渇と DNS の変更)を回避できるということのようです。

そのためには、Core に備わっている DI 機能を使って IHttpClientFactory オブジェクトを注入する必要があるそうです。(自分が知らないだけで DI 以外の方法もあるかもしれませんが)

ASP.NET Core アプリを Visual Studio 2019 のテンプレートを使って生成した場合は DI 機能はプロジェクトに組み込まれるのですが、Windows Forms アプリの場合は自力で実装する必要があります。そのために以下の NuGet パッケージをインストールします。

  • Microsoft.Extensions.DependencyInjection
  • Microsoft.Extensions.Http

前者は DI 機能の実装のために必要です。それだけで、先の記事「.NET Core での Dependency Injection」に書きましたようにコンソールアプリにさえも DI 機能を実装できます。

後者は IServiceCollection(DI コンテナ)に対して AddHttpClient 拡張メソッドを使用し IHttpClientFactory を DI コンテナに登録するために必要だそうです。

ということで Core v5 の Windows Forms アプリで試してみました。比較のために、先の記事「HttpClient で ASP.NET Web API にアクセス」と全く同じ機能を実装し、HttpClient を利用するところだけ DI コンテナから注入された IHttpClientFactory が作る HTTP 接続プールを使うようにしてみました。

そのコードを以下にアップしておきます。上の画像を表示したものです。見やすくするため HttpClient の使い方のコメントは先の記事から削除していますので、必要なら上にリンクを張った先の記事を見てください。

HttpClientWebApi.cs

HttpClientWebApi クラスが HttpClient を使って Web API にアクセスするためのクラスです。初期化する際にコンストラクタで IHttpClientFactory を注入できるように設定します。クラスに実装した GetToken, PostData メソッドでは、IHttpClientFactory が作る HTTP 接続プールから CreateClient メソッドで HttpClient インスタンスを取得して使っています。

JSON のシリアライズ / デシリアライズには先の記事と同じく DataContractJsonSerializer を使っています。Core v3.0 以降であれば System.Text.Json の JsonSerializer クラスを使うべきかもしれませんが、比較しやすくするため DI 機能部分以外は先の記事と同じにするということでそうしています。

using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using System.Net.Http;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;

namespace WinFormsApp1
{
    class HttpClientWebApi
    {
        private readonly IHttpClientFactory _clientFactory;

        // コンストラクタで IHttpClientFactory を注入
        public HttpClientWebApi(IHttpClientFactory clientFactory)
        {
            this._clientFactory = clientFactory;
        }

        // id と password を loginUrl に送信してベアラトークンを
        // 取得し、戻り値として返すメソッド
        public async Task<string> GetToken(string id, 
                                           string password, 
                                           string loginUrl)
        {
            var param = new Dictionary<string, string>
            {
                { "grant_type", "password"},
                { "username", id },
                { "password", password }
            };

            var content = new FormUrlEncodedContent(param);

            // DI コンテナから注入された IHttpClientFactory が作る
            // HTTP 接続プールから HttpClient インスタンスを取得
            HttpClient client = _clientFactory.CreateClient();

            var response = await client.PostAsync(loginUrl, content);

            using (Stream responseStream =
                await response.Content.ReadAsStreamAsync())
            {
                var ser = new DataContractJsonSerializer(typeof(Token));
                Token auth = (Token)ser.ReadObject(responseStream);
                return auth.access_token;
            }
        }

        // id と name(この記事の例では "6" と "ガッチャマンの息子")
        // をベアラトークン token と共に apiUrl に POST 送信し、
        // Api から帰ってきた JSON 文字列をデシリアライズして文字列を
        // 組み立てて戻り値として返すメソッド
        public async Task<string> PostData(string id, 
                                           string name, 
                                           string apiUrl, 
                                           string token)
        {
            var request = new HttpRequestMessage(HttpMethod.Post, apiUrl);
            string postData = "";

            Hero postHero = new Hero
            {
                Id = int.Parse(id),
                Name = name
            };

            using (MemoryStream stream = new MemoryStream())
            {
                var ser = new DataContractJsonSerializer(typeof(Hero));
                ser.WriteObject(stream, postHero);
                stream.Position = 0;
                using (var reader = new StreamReader(stream))
                {
                    postData = reader.ReadToEnd();
                }
            }

            request.Content = new StringContent(postData,
                                            Encoding.UTF8,
                                            "application/json");

            request.Headers.Add("Authorization",
                                "Bearer " + token);


            // DI コンテナから注入された IHttpClientFactory が作る
            // HTTP 接続プールから HttpClient インスタンスを取得
            HttpClient client = _clientFactory.CreateClient();

            var response = await client.SendAsync(request);

            using (Stream responseStream =
                await response.Content.ReadAsStreamAsync())
            {
                var ser = new DataContractJsonSerializer(typeof(List<Hero>));
                List<Hero> heros = (List<Hero>)ser.ReadObject(responseStream);
                string result = "";
                foreach (Hero hero in heros)
                {
                    result += string.Format("{0}: {1}\r\n", hero.Id, hero.Name);
                }
                return result;
            }
        }
    }

    // トークン要求に対し応答として返ってくるデータ
    [DataContract]
    public class Token
    {
        [DataMember]
        public string access_token { get; set; }

        [DataMember]
        public string token_type { get; set; }

        [DataMember]
        public int expires_in { get; set; }

        [DataMember]
        public string userName { get; set; }
    }

    // Web API に POST 送信するデータ
    [DataContract]
    public class Hero
    {
        [DataMember]
        public int Id { get; set; }

        [DataMember]
        public string Name { get; set; }
    }
}

Form1.cs

Form1 のコンストラクタで IServiceCollection オブジェクト(DI コンテナ)を生成し、AddHttpClient メソッドで IHttpClientFactory を DI コンテナに登録しています。

さらに AddSingleton メソッドで上のコードの HttpClientWebApi クラスを登録してから、BuildServiceProvider メソッドで ServiceProvider オブジェクトを取得し、GetRequiredService<HttpClientWebApi> メソッドで IHttpClientFactory が DI 済の HttpClientWebApi インスタンスを取得しています。

using System;
using System.Windows.Forms;
using Microsoft.Extensions.DependencyInjection;

namespace WinFormsApp1
{
    public partial class Form1 : Form
    {
        private string loginUrl = "トークン要求先 URL";
        private string apiUrl = "Web API の URL";
        private string email = "ユーザー ID";
        private string passsword = "パスワード";
        private int id = 6;
        private string name = "ガッチャマンの息子";
        private string token = "";

        private HttpClientWebApi httpClientWebApi;

        public Form1()
        {
            InitializeComponent();

            this.textBox1.Text = email;
            this.textBox2.Text = passsword;
            this.textBox3.Text = id.ToString();
            this.textBox4.Text = name;

            IServiceCollection services = new ServiceCollection();

            // これを忘れないように!
            services.AddHttpClient();

            services.AddSingleton<HttpClientWebApi>();
            var provider = services.BuildServiceProvider();
            httpClientWebApi = provider.GetRequiredService<HttpClientWebApi>();
        }

        // [Get Token] ボタンクリック
        private async void button1_Click(object sender, EventArgs e)
        {
            if (!string.IsNullOrEmpty(this.token)) return;

            this.token = await httpClientWebApi.GetToken(this.textBox1.Text,
                                                         this.textBox2.Text, 
                                                         loginUrl);
        }

        // [Post Data] ボタンクリック
        private async void button2_Click(object sender, EventArgs e)
        {
            if (string.IsNullOrEmpty(this.token)) return;

            this.textBox5.Text = await httpClientWebApi.PostData(this.textBox3.Text,
                                                                 this.textBox4.Text, 
                                                                 apiUrl, 
                                                                 this.token);
        }
    }
}

上の HttpClientWebApi クラスのコードでは、HttpClient を使う直前にその都度 IHttpClientFactory.CreateClient メソッドで HTTP 接続プールから HttpClient インスタンスを取得しています。そうしないと DNS 変更の問題に対処できないと思われるからです。

Microsoft のドキュメント「ASP.NET Core で IHttpClientFactory を使用して HTTP 要求を行う」の「IHttpClientFactory の代替手段」のセクションに "Using IHttpClientFactory in a DI-enabled app avoids: ... Stale DNS problems by cycling HttpMessageHandler instances at regular intervals." と書いてありますように、定期的に HTTP 接続プールをリサイクルしているので、HttpClient を使う直前にプールからインスタンスを取得しないと IHttpClientFactory を使う意味がないということになるはずです。

それから、ADO.NET + SqlClient で使う接続プールのようにプールから取得してきた接続は使い終わったらプールに戻さないとプールの接続の枯渇になるのではと思ったのですが、IHttpClientFactory が作る HTTP 接続プールにはそういう心配はなさそうです。

使い終わった HttpClient を HTTP 接続プールに戻すために Dispose するのかと思いましたが、「HttpClient と有効期間の管理」のセクションに "HttpClient instances can generally be treated as .NET objects not requiring disposal. Disposal cancels outgoing requests and guarantees the given HttpClient instance can't be used after calling Dispose. IHttpClientFactory tracks and disposes resources used by HttpClient instances." と書いてあるように、そんな必要はなさそうです。

しかしながら、そもそも短い期間で一日に数回程度しか HttpClient は使わない Windows Forms アプリであれば、上記のように IHttpClientFactory を DI して使う必要はないかもしれません。

なので、そのような HttpClient の使い方であれば、先の記事「HttpClient で ASP.NET Web API にアクセス」に書いたように、単純に static と宣言して使い回す方が正解だと思います。

Tags: , ,

CORE

About this blog

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

Calendar

<<  April 2021  >>
MoTuWeThFrSaSu
2930311234
567891011
12131415161718
19202122232425
262728293012
3456789

View posts in large calendar