この記事でやること
DBにアクセスするコンソールアプリケーションを作成する際に、Entity Framework Core、DIコンテナ、appsettings.json に対応させる手順を実践的に紹介する。
具体的には、以下のようなDIを用いたコードを書いてDBにアクセスできるようにし、煩わしい設定ファイルの読み込みや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
- Models
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);
}
}
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メソッドがない。ありがたい。
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のみを持っている。これらは今後増やしていける。
{
"ConnectionStrings": {
"MyDb": "Server=<host>;Database=<dbname>;Username=<user>;Password=<pass>;"
},
"MySettings": {
"AppName": "サンプルアプリケーション1"
}
}
MySettingsについてはクラスとして定義しておくと、後々便利である。
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メソッドを呼び出すと、やりたいことを実行する。これがこのコンソールアプリケーションの本体となる。
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を実行する為の汎用ホストのセットアップは次の通りとなる。
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コンテナから提供される。
public MyService(Entities.Models.MyDbContext dbContext, IOptions<MySettings> settings)
{
_dbContext = dbContext;
_settings = settings.Value;
}
最後に、取得したMyService
のExecute
を呼び出せば、全て整った状態でアプリケーション本体が実行される。
以上である。
サンプルアプリケーション1
テスト名1
アプリケーション本体以外のいろんなことは、汎用ホストが面倒を見てくれる。ありがたい。
まとめ
コンソールアプリケーションに、Entity Framework Coreを使ったDBアクセス、DIコンテナからの依存性注入、appsettings.jsonからの設定情報の読み込み、といった便利な基盤機能を導入する手順を紹介した。
Entity Framework Coreによるスキャッフォルドの話と、汎用ホストを使ってコンソールアプリケーションをDIコンテナ対応させる話は別の記事にしようかとも思ったが、これらは密接に関連した一連の作業であり、一続きになっていた方が相互の理解も深まると考え、1つの記事にした。
実際には、それらは個別に利用できるものなので、必要に応じて必要な箇所のみ読んで頂ければと思う。