5
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[C#]コンソールアプリでDIコンテナ、appsettings.json、EF Coreを利用する(汎用Hostを使用)

Last updated at Posted at 2023-03-02

この記事でやること

DBにアクセスするコンソールアプリケーションを作成する際に、Entity Framework Core、DIコンテナ、appsettings.json に対応させる手順を実践的に紹介する。

具体的には、以下のようなDIを用いたコードを書いてDBにアクセスできるようにし、煩わしい設定ファイルの読み込みやDBの初期化コードとアプリケーション本体を分離する。

DIを用いたDBアクセス
using System;
using Microsoft.Extensions.Options;
using System.Linq;

// アプリケーション本体
public class MyService
{
    private Entities.Models.MyDbContext _dbContext;
    private MySettings _settings;

    // コンストラクタ
    public MyService(Entities.Models.MyDbContext dbContext, IOptions<MySettings> settings)
    {
        // コンストラクタからDIしたオブジェクトをメンバに格納
        _dbContext = dbContext;
        _settings = settings.Value;
    }

    // アプリケーションの実行
    public void Execute()
    {
        Console.WriteLine(_settings.AppName);

        var r = _dbContext.Mytable.FirstOrDefault(rec => rec.Id == 1) ?? new Entities.Models.Mytable { Id = 1, Name = "test" };
        Console.WriteLine(r.Name);
    }
}

既存のDBからDbContextやエンティティクラスを生成する手順は、コンソールアプリケーションでなくとも参考になるだろう。

また、DIコンテナやappsettings.jsonへの対応は、「汎用ホスト(Generic Host)」という機能を用いることで非常にシンプルに実現している。今や当たり前となりつつあるこれらの機能をコンソールアプリケーションでも利用できることにより、開発効率の大幅な向上を図る。

主に以下のことを順番に行う。

  • 新規ソリューションでDBのスキャッフォルド
    新規ソリューションを作成し、コンソールアプリのプロジェクトを作る。そこに、DB関連のクラスをまとめるEntitiesプロジェクトを別途作り、Entitiesプロジェクト内に、別途存在するデータベースから自動生成したDbContextやエンティティクラス群を格納する。

  • プロジェクトにappsettings.jsonを追加する
    DB接続文字列をappsettings.jsonから読み込むようにしたいので、まずはappsettings.jsonを追加する。

  • 汎用ホストを使ってコンソールアプリをDIコンテナやappsettings.json対応にする
    DIコンテナに慣れてくると、コンソールアプリだってDIコンテナ対応にしたい。
    また、appsettings.json対応にする設定も大変だ。
    その辺をまとめてやってくれるのが「汎用ホスト」だ。

新規ソリューションでDBのスキャッフォルド

この部分は、知ってる方は飛ばして問題ない。DbContextクラスとMytableエンティティクラスをDBから自動生成する。

やりたいこと

DBにアクセスするプロジェクト「MyDbTest.csproj」があるとする。
PostgreSQL上にmydbというデータベースがあり、そこにはmytableというテーブルがある。

id name
1 テスト名1
2 テスト名2
3 テスト名3

そのmytableからDbContextクラスやエンティティクラスを自動生成したい。

自動生成されるModelクラス群は、MyDbTestプロジェクトとは別のプロジェクト、例えば「Entities」という名前のプロジェクトに置きたい。
そこでまず、同じソリューション内にEntities.csprojを作成する(クラスライブラリプロジェクトとして新規作成)。

Scaffold-DbContextコマンド

※コメント欄にて、@juner さんが dotnetコマンドで同じことを行う方法について解説してくださっています。今だとこちらの方が楽とのこと! ご参照ください。@juner さんありがとうございます。

スキャッフォルドを行えるコマンドは、現在はScaffold-DbContextというコマンドのようだ。このコマンドは、Microsoft.EntityFrameworkCore.Toolsをインストールすると使えるようになる。
Microsoft.EntityFrameworkCore.Tools.DotNetをインストールせよという記事も見つかるが、現在(.NET7)ではこれはもう非推奨となっている。

私は今、Visual Studio 2019を使っており、.NET5のプロジェクトしか作れない(無理やりすれば.NET6や7も可能だと思うが公式サポートされていない)。
その為、インストールするパッケージも.NET5対応のバージョンとなる。尚、今回の手順は.NET5向けとなるが、適切なバージョンを選択すれば、.NET6や.NET7でも応用可能だと思われる。

Microsoft.EntityFrameworkCore.Tools は、現時点の最新の7.0.3はnet6.0以降対応となっている。6.0.0ですら、同じくnet6.0以降対応だ。net5.0に対応するのは、.NETStandard2.0以降対応の5.0.17となる。

NuGetパッケージマネージャやパッケージマネージャーコンソールから、Microsoft.EntityFrameworkCore.Tools 5.0.17をインストールする。

Install-Package Microsoft.EntityFrameworkCore.Tools -Version 5.0.17

上記をインストールすると、Scaffold-DbContextというコマンドラインツールが使えるようになる。

早速使ってみよう。
(コマンドを実行する前に、現時点でのソリューションのビルドが通ることを確認しておく。ビルドが通らないと、Build faildと怒られる)

Scaffold-DbContext -Connection "Server=<host>;Database=<dbname>;Username=<user>;Password=<pass>;" -Provider Npgsql.EntityFrameworkCore.PostgreSQL -OutputDir Models -Context MyDbContext -StartupProject MyDbTest -Project Entities

パラメータを一つ一つ説明する。

パラメータ 説明
-Connection "Server=;Database=;Username=;Password=;" DB接続文字列
-Provider Npgsql.EntityFrameworkCore.PostgreSQL 使用するプロバイダのパッケージ名。今回はPostgresを利用するのでこれになる。SQL Serverの場合はMicrosoft.EntityFrameworkCore.SqlServer、Sqliteの場合はMicrosoft.EntityFrameworkCore.Sqliteを指定する。
-OutputDir Models 出力されるModelクラスが格納されるフォルダ名。自動的に生成される。
-Context MyDbContext 出力されるDbContextクラスのクラス名。
-StartupProject MyDbTest スタートアッププロジェクト名。
-Project Entities 出力対象のプロジェクト名。

さて、上記の通りのパラメータでScaffold-DbContextを実行すると、すぐにエラーが出る。

Unable to find provider assembly 'Npgsql.EntityFrameworkCore.PostgreSQL'. Ensure the name is correct and it's referenced by the project.

使用するDBプロバイダのパッケージがプロジェクトから参照されていないとのこと。

Entitiesプロジェクトに、Npgsql.EntityFrameworkCore.PostgreSQLをインストールする。このパッケージも、そのまま入れるとnet5.0に対応していないので、net5.0対応の最新バージョンである5.0.10をインストールする。

(インストール先のプロジェクトとして「Entities」を選択して)

Install-Package Npgsql.EntityFrameworkCore.PostgreSQL -Version 5.0.10 -Project Entities

インストールできたらもう一度Scaffold-DbContextを実行。するとまた怒られる。

Your startup project 'MyDbTest' doesn't reference Microsoft.EntityFrameworkCore.Design. This package is required for the Entity Framework Core Tools to work. Ensure your startup project is correct, install the package, and try again.

スタートアッププロジェクトである「MyDbTest」に、Microsoft.EntityFrameworkCore.Design の参照が設定されていないというエラーだ。EntityFrameworkCore.Toolsの動作にこれが必要とのこと。同様に、net5.0対応の最新バージョンをMyDbTestプロジェクトにインストールする。

Install-Package Microsoft.EntityFrameworkCore.Design -Version 5.0.17 -MyDbTest

インストールできたらもう一度Scaffold-DbContextを実行。するとめでたく「Build succeeded.」。
ソリューションエクスプローラを開くと、Entitiesプロジェクト内に次のようにクラスが生成されている。

  • Entities
    • Models
      • MyDbContext.cs
      • Mytable.cs
MyDbContext.cs
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;

#nullable disable

namespace Entities.Models
{
    public partial class MyDbContext : DbContext
    {
        public MyDbContext()
        {
        }

        public MyDbContext(DbContextOptions<MyDbContext> options)
            : base(options)
        {
        }

        public virtual DbSet<Mytable> Mytables { get; set; }
        

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            if (!optionsBuilder.IsConfigured)
            {
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see http://go.microsoft.com/fwlink/?LinkId=723263.
                optionsBuilder.UseNpgsql("Server=<host>;Database=<dbname>;Username=<user>;Password=<pass>;");
            }
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.HasAnnotation("Relational:Collation", "C");

            modelBuilder.Entity<Mytable>(entity =>
            {
                entity.ToTable("mytable");

                entity.Property(e => e.Id).HasColumnName("id");

                entity.Property(e => e.Name).HasColumnName("name");
            });

            OnModelCreatingPartial(modelBuilder);
        }

        partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
    }
}
Mytable.cs
using System;
using System.Collections.Generic;

#nullable disable

namespace Entities.Models
{
    public partial class Mytable
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
}

DbContextからDB接続文字列を除外する

めでたしめでたし…と思いたいが、コマンドライン上では何やら黄色く警告が出ているのが気になる。

To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see http://go.microsoft.com/fwlink/?LinkId=723263.

生成されたDbContextクラスのソースコードに、コマンドラインで指定したDB接続文字列が埋め込まれているので、セキュリティ的に良くないですよ、と言われている。
実際、生成されたMyDbContextクラスを開いてみると、OnConfiguringメソッドでDB接続文字列が直接記述されており、#warningが付いていてうざいし、実際この部分は最終的には外部から読み込むようにしたいので、削除したい。

これを行ってくれるオプションが、-NoOnConfiguringだ。OnConfiguraingメソッドを生成しないようにしてくれる。
一旦、Entities/Modelsフォルダを丸ごと削除し、再度Scaffold-DbContextコマンドを-NoOnConfiguring付きで実行する。

Scaffold-DbContext -Connection "Server=<host>;Database=<dbname>;Username=<user>;Password=<pass>;" -Provider Npgsql.EntityFrameworkCore.PostgreSQL -OutputDir Models -Context MyDbContext -StartupProject MyDbTest -Project Entities -NoOnConfiguring

今度は何のエラーも警告も表示されず、生成が完了した。MyDbContextクラスにも、OnConfiguringメソッドがない。ありがたい。

MyDbContext.cs
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;

#nullable disable

namespace Entities.Models
{
    public partial class MyDbContext : DbContext
    {
        public MyDbContext()
        {
        }

        public MyDbContext(DbContextOptions<MyDbContext> options)
            : base(options)
        {
        }

        public virtual DbSet<Mytable> Mytables { get; set; }
        

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.HasAnnotation("Relational:Collation", "C");

            modelBuilder.Entity<Mytable>(entity =>
            {
                entity.ToTable("mytable");

                entity.Property(e => e.Id).HasColumnName("id");

                entity.Property(e => e.Name).HasColumnName("name");
            });

            OnModelCreatingPartial(modelBuilder);
        }

        partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
    }
}

これで「DBからのスキャッフォルド」の目的は完了だ。
ちなみに、次回以降、DBの変更があって再度スキャッフォルドしたい場合には、-Forceというオプションを付けると、既存のOutputDirを上書きしてくれる。

次に、それをアプリケーションという形で利用できるようにMyDbTestプロジェクトを設定していこう。

appsettings.jsonのサポート

 DB接続文字列など、ソース中に書きたくないリソースをappsettings.jsonに書いて読み込めると便利である。
 その為の手順は次の通りとなる。

NuGet で Microsoft.Extensions.Configuration.Json をインストール(当環境では7.0.0)

Install-Package Microsoft.Extensions.Configuration.Json -Version 7.0.0

プロジェクトのノードを右クリック→追加→新しい項目

  • 「javaScript JSON 構成ファイル」を選択
  • 「名前」欄に appsettings.json と入力
  • 「追加」ボタンを押下

appsettings.json を UTF-8にで保存しなおす。

  • 「名前を付けて保存」から「上書き保存▼」のドロップダウンメニューの中の「エンコード付きで保存」を選択。
  • エンコードとして「Unicode (UTF-8シグネチャ付き) - コードページ 65001」を選択。
  • 保存。
  • ※これをすることで、日本語の文字列をappsettings.jsonに保存した際にUTF-8で読み出せる。システムをShift-JISで作っているならShift-JISにする。

appsettings.jsonの出力設定を「常にコピーする」に変更

  • ソリューションエクスプローラからappsettings.jsonを選択肢、右クリックから「プロパティ」を選択。
  • 「出力ディレクトリにコピー」の項目を「常にコピーする」にする。
  • ※これをすることで、exeと同じ場所に常にappsettings.jsonが存在するようになる。

以上で、プロジェクトにappsettings.jsonが追加される。

今回appsetting.jsonに保存したい値は、次の2つとなる。

  • ConnectionStrings
  • MySettings

ConnectionStringsには、今のところMyDbの接続文字列しかない。また、MySettingsにも、AppNameのみを持っている。これらは今後増やしていける。

appsettings.json
{
  "ConnectionStrings": {
    "MyDb": "Server=<host>;Database=<dbname>;Username=<user>;Password=<pass>;"
  },

  "MySettings": {
    "AppName": "サンプルアプリケーション1"
  }
}

MySettingsについてはクラスとして定義しておくと、後々便利である。

MySettings.cs
public class MySettings
{
    public string AppName { get; set; }
}

appsetting.jsonから設定値を読み出す方法はいろいろあるが、今回は汎用ホストに任せる方法をとる。

汎用ホストを使ってコンソールアプリをDIコンテナやappsettings.json対応にする

コンソールアプリケーションであっても、気持ちよくプログラミングするために、次のような機能は標準で装備させたい。
(もちろん、どうしてもバイナリサイズを小さくしたいなどの要件がある場合はまた別だが)

  • appsettings.json からアプリケーション構成を読み込む機能
  • DIコンテナ対応
  • Logger対応

これらの機能を標準で備えているのが、「汎用ホスト(Generic Host)」というライブラリである。
https://qiita.com/nozmiz/items/a7c6a7d33c9f67c79336

汎用ホストは、バックグラウンドタスクなどのようにプロセスとして常駐し、外部とのやりとりを行いながら長時間かかるような処理を管理するためのプログラムを作る「基盤」となるものだ。

しかし今回は、このホスト自体を実行(host.Run())することはせず、ホストのDI機能やappsettings.json対応機能のみを利用する。

まず先に、「今回実行したいアプリケーションの本体」を、MyServiceというクラスに定義する。
Executeメソッドを呼び出すと、やりたいことを実行する。これがこのコンソールアプリケーションの本体となる。

MyService.cs
using System;
using Microsoft.Extensions.Options;
using System.Linq;

public class MyService
{
    private Entities.Models.MyDbContext _dbContext;
    private MySettings _settings;

    public MyService(Entities.Models.MyDbContext dbContext, IOptions<MySettings> settings)
    {
        _dbContext = dbContext;
        _settings = settings.Value;
    }

    public void Execute()
    {
        Console.WriteLine(_settings.AppName);

        var r = _dbContext.Mytable.FirstOrDefault(rec => rec.Id == 1) ?? new Entities.Models.Mytable { Id = 1, Name = "test" };
        Console.WriteLine(r.Name);
    }
}

上記のコードではIOptionsを利用している。これはオプションパターンと呼ばれるもので、以下のNuGetパッケージをインストールする必要がある

  • Microsoft.Extensions.Options
  • Microsoft.Extensions.Options.ConfigurationExtensions

内容の説明は大したことをしていないので割愛する。
ここで注目したいのは、MyDbContextと、MySettingsをDependency Injectionしている点である。
これがやりたいからこそ、今回、汎用ホストを利用しているわけだ。

汎用ホストのセットアップ(全体)

上記のMyServiceを実行する為の汎用ホストのセットアップは次の通りとなる。

Program.cs
using System;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

namespace Sample1
{
    class Program
    {
        static void Main(string[] args)
        {
            // 汎用ホストの生成(ビルドパターンを使用)
            var host = Host.CreateDefaultBuilder(args)
                .ConfigureServices(ConfigureServices)
                .Build();

            // 汎用ホストのDIコンテナからMyService(アプリケーション本体)のインスタンスを取得
            var my = host.Services.GetRequiredService<MyService>();
            // アプリケーション本体の実行
            my.Execute();
        }

        // 汎用ホストの構成
        private static void ConfigureServices(HostBuilderContext hostBuilderContext, IServiceCollection services)
        {
            // DbContextの設定と登録
            services.AddDbContext<Entities.Models.MyDbContext>((provider, options) =>
            {
                string connectionString = hostBuilderContext.Configuration.GetConnectionString("MyDb");
                options.UseNpgsql(connectionString);
            });
            
            // MySettingsをappsettings.jsonからDIする為の設定
            services.Configure<MySettings>(
                hostBuilderContext.Configuration.GetSection("MySettings"));

            // MyServiceをDIコンテナに登録
            services.AddTransient<MyService, MyService>();
        }
    }

一つ一つ説明していこう。

汎用ホストの生成(ビルドパターンを使用)

MyDbText.csprojのエントリポイントであるProgram.csのMainメソッドで、Host.CreateDefaultBuilder(args)を呼び出して規定の汎用ホストを生成している。(C#9以降の最上位レベルステートメントを利用して、Mainメソッドを省略で書く事もできるが、ここではMainを利用している)

// 汎用ホストの生成(ビルドパターンを使用)
var host = Host.CreateDefaultBuilder(args)
    .ConfigureServices(ConfigureServices)
    .Build();

汎用ホストの構成(ConfigureServices)

生成された汎用ホストに対して設定をするのがConfigureServicesメソッドである。
このメソッドはAction<HostBuilderContext, IServiceCollection>を引数に取るので、別途、staticなConfigureServicesメソッドを定義し、そのメソッドを渡すようにしている。

この構成内容は、あくまでも「汎用ホストを使ってDIコンテナとかappsettings.jsonの読み込み機能を実装しちゃおう」という目的の為のものであり、汎用ホストでサーバーやバックグラウンド実行処理などを作る場合には、他にも設定が必要となる。

    // 汎用ホストの構成
    private static void ConfigureServices(HostBuilderContext hostBuilderContext, IServiceCollection services)
    {
        // DbContextの設定と登録
        services.AddDbContext<Entities.Models.MyDbContext>((provider, options) =>
        {
            string connectionString = hostBuilderContext.Configuration.GetConnectionString("MyDb");
            options.UseNpgsql(connectionString);
        });
        
        // MySettingsをappsettings.jsonからDIする為の設定
        services.Configure<MySettings>(
            hostBuilderContext.Configuration.GetSection("MySettings"));

        // MyServiceをDIコンテナに登録
        services.AddTransient<MyService, MyService>();
    }

上記の内容の説明をする。
まず、AddDbContext<T>で、MyDbContextを登録している。

        // DbContextの設定と登録
        services.AddDbContext<Entities.Models.MyDbContext>((provider, options) =>
        {
            string connectionString = hostBuilderContext.Configuration.GetConnectionString("MyDb");
            options.UseNpgsql(connectionString);
        });

options.UseNpgsql(connectionString)に渡す文字列は、hostBuilderContext.Configuration経由でappsettings.jsonの値を取得している。

次に、appsettings.jsonのMySettingsを、MySettingsというクラスにバインドして取得するための設定を行う。

        // MySettingsをappsettings.jsonからDIする為の設定
        services.Configure<MySettings>(
            hostBuilderContext.Configuration.GetSection("MySettings"));

これで、MySettingsが必要なクラスからIOptions<MySettings>という型でDIできるようになる。

最後に、アプリケーション本体であるMyServiceをDIコンテナに登録する。

        // MyServiceをDIコンテナに登録
        services.AddTransient<MyService, MyService>();

汎用ホストとそのDIコンテナ機能のセットアップは以上となる。

サービス(アプリケーション本体)の取得と実行

あとは、Mainメソッドの続きに戻る。

    // 汎用ホストのDIコンテナからMyService(アプリケーション本体)のインスタンスを取得
    var my = host.Services.GetRequiredService<MyService>();
    // アプリケーション本体の実行
    my.Execute();

host.ServicesがDIコンテナ本体だ。そこからGetRequiredService<T>で、MyServiceのインスタンスを取得し、my変数に格納している。
このようにしてMyServiceのインスタンスを取得することで、MyServiceのコンストラクタ引数に定義した各オブジェクトがDIコンテナから提供される。

MyServiceクラスのコンストラクタ部分
    public MyService(Entities.Models.MyDbContext dbContext, IOptions<MySettings> settings)
    {
        _dbContext = dbContext;
        _settings = settings.Value;
    }

最後に、取得したMyServiceExecuteを呼び出せば、全て整った状態でアプリケーション本体が実行される。
以上である。

実行結果
サンプルアプリケーション1
テスト名1

アプリケーション本体以外のいろんなことは、汎用ホストが面倒を見てくれる。ありがたい。

まとめ

コンソールアプリケーションに、Entity Framework Coreを使ったDBアクセス、DIコンテナからの依存性注入、appsettings.jsonからの設定情報の読み込み、といった便利な基盤機能を導入する手順を紹介した。

Entity Framework Coreによるスキャッフォルドの話と、汎用ホストを使ってコンソールアプリケーションをDIコンテナ対応させる話は別の記事にしようかとも思ったが、これらは密接に関連した一連の作業であり、一続きになっていた方が相互の理解も深まると考え、1つの記事にした。

実際には、それらは個別に利用できるものなので、必要に応じて必要な箇所のみ読んで頂ければと思う。

5
8
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?