こんにちは、サイオステクノロジーの佐藤 陽です。
今回も ASP.NET Core についての記事を書いていきます。
今回は、第四弾として ASP.NET Core の DI コンテナの機能ついて解説していきたいと思います。
とは言いつつ、ASP.NET Core に依存した部分はそんなに大きくないので
ASP.NET Core関係なしに
- DI って何?
- 依存性の注入って何?
- Injection の種類ってどんなのがあるの?
といった方はぜひ最後まで読んでみてください!
また繰り返しになりますが
まだ自分も勉強中の身なので、記事の内容に誤りなどありましたらコメントにて指摘いただけると幸いです。
ではよろしくお願いいたします。
はじめに
今回は ASP.NET Core で提供される依存性注入(Dependency Injection = DI)および DI コンテナの機能について解説していきます。
優れたアーキテクチャを実装するうえでは欠かせない技術なので是非習得したいところです。
DI とは
まずは DI について簡単に説明したいと思います。
DI は Dependency Injection の略で、日本語では依存性の注入と呼ばれます。
よく言われますが、この「依存性の注入」という日本語がなかなかクセが強くて分かりづらいですよね。
出来るだけ分かりやすく解説していきたいと思うので、ここでくじけず最後まで読んでいただけると幸いです。
Dependency
まずは Dependency(依存性)の部分について解説します。
この「依存性」の重要さに関しては別の記事でまとめてありますので、こちらを読んでいたけると嬉しいです。
簡単に述べると、ポイントとしては以下 2 点です。
- 依存先を安定した抽象(Interface)とする=依存関係逆転の原則
- 重要なビジネスルールが関係のない修正の影響を及ぼさないようにする=オープン・クローズドの原則
Injection
次に Injection(注入)の部分について解説します。
実はこれも前回の記事の最後の方に触れています。
IDataAccess dataAccess = new MySQLDataAccess();
ここの部分ですね。 Interface の変数を定義し、そこに実装されたクラスを new しています。
ただ前回の記事でも触れていますが、 このような形で単純に実装してしまうと依存関係が望ましくない形になってしまいます。
そこで 「このインスタンス化する処理をもっと外側に持っていき、依存関係に影響を与えないようにしよう」 というのが Dependency Injection の仕組みになります。
イメージ図ですが、以下のような感じです。
今回は先ほどのソースコードの例で、MySQLDataAccess を BusinessRule から利用したいとします。
この時、MySQLDatabaseAccessへの直接依存を避けるため以下2つの点に注意します。
- BusinessRuleがIDatabaseAccess への依存するよう実装します。
- MySQLDataAccess のインスタンス化も BusinessRule クラス内では行わない。
これを実現するため、どこか外部のところでインスタンス化を行い、それを BusinessRule クラスに渡してあげる(=Injection)のです。
こうすることにより BusinessRule クラス自体は、MySQLDatabaseAccess クラスの存在を全く知らない(=依存しない)状態で、制御を行うことができます。
ではこのインスタンス化をはどこで行うのでしょうか?
その解決方法が DI コンテナであり、次の章で詳しく見ていきたいと思います。
DI コンテナ
DI コンテナとは上記したような DI の仕組みを実現するためのフレームワークです。
このフレームワークを使うことで、抽象(Interface)に対して、それを実装する具体的な型をマッピングすることができます。
ASP.NET Core では DI コンテナが標準搭載されており、今回はそれを使っていきます。 (標準以外の DI コンテナを ASP.NET Core 導入することも可能です)
ASP.NET Core で提供される DI コンテナ
DI コンテナを利用するにあたり、抽象型(Interface)と、具象型(実際に挿入するインスタンス)の依存関係を教えてあげる必要があります。
こうすることで、実際に使われるクラスでの挿入の部分や、インスタンスの作成および破棄の役割を DI コンテナが担ってくれます。
ASP.NET Core には①事前定義済みの抽象型と、②自分で登録できるカスタム定義の抽象型があります。
事前定義済みの依存関係
事前定義として用意されているものは多くあります。
これらは明示的に DI コンテナに依存関係を登録しなくても利用することができます。
よく利用されるのは IWebHostEnvironment などでしょうか?
この IWebHostEnvironment は実行環境の情報を持っており、開発環境・本番環境によって処理を切り分けることが可能となります。
せっかくなのでIWebHostEnvironment を試しに使ってみたいと思います。
今回はこれまでの記事でも出てきたサンプルの mvc プロジェクトをベースとし、IWebHostEnvironment を扱います。
実装の流れとしては
- BusinessRule クラス内で Injection された IWebHostEnvironment を利用して開発環境か本番環境か判定する関数(WebHostEnvironment)を実装する
- HomeController で 1.で実装されたメソッドを実行するような関数(Environment)を追加する
といった形で実装したいと思います。
クラス構成としては以下のような形です。
作成・修正するファイルとしては以下4つです。
IBusinessRule.cs | BusinessRule 用のインターフェイスの定義ファイル (あまりBusinessRuleのクラスっぽい事はしていませんが、そこは見逃していただけると…) |
BusinessRule.cs | IBusinessRule を実装するクラスの定義ファイル |
HomeController.cs | BusinessRule を呼び出すようなアクションを追加する |
Program.cs | DI コンテナに対して、必要なサービスを注入する |
IBusinessRule.cs
namespace aspnetcore_8_mvc
{
public interface IBusinessRule
{
public string WebHostEnvironment();
}
}
BusinessRule.cs
namespace aspnetcore_8_mvc
{
public class BusinessRule : IBusinessRule
{
IWebHostEnvironment _env;
//DIコンテナからIWebHostEnvironmentをInjectionしている
//インスタンスの生成自体はこのクラス内で行わない
public BusinessRule(IWebHostEnvironment env)
{
_env = env;
}
public string WebHostEnvironment()
{
//Injectionされた値を利用して環境を判定
if (_env.IsDevelopment())
{
return "開発環境です";
}
else
{
return "本番環境です";
}
}
}
}
HomeController.cs
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using aspnetcore_8_mvc.Models;
namespace aspnetcore_8_mvc.Controllers;
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly IBusinessRule _businessRule;
public HomeController(ILogger<HomeController> logger, IBusinessRule businessRule)
{
_logger = logger;
_businessRule = businessRule;
}
//BusinessRuleを実行するためのActionを追加
public IActionResult Environment()
{
return Content(_businessRule.WebHostEnvironment());
}
public IActionResult Index()
{
return View();
}
public IActionResult Privacy()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
Program.cs
using aspnetcore_8_mvc;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.Configure<IISServerOptions>(options =>
{
options.AutomaticAuthentication = false;
});
//DIコンテナにBusinessRuleをInjectionする
builder.Services.AddSingleton<IBusinessRule, BusinessRule>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
BusinessRule.cs 内で IWebHostEnvironment を利用して実行環境の判定を行っていますが、そのインスタンスはコンストラクタの引数として与えられています。
ではこの BusinessRule のコンストラクタの引数はどのように与えられているかというと、
アプリケーションのコードで明示的に与えるのではなく、DI コンテナの仕組みとしてインジェクションされているのです。
せっかくなので実行します。
https://localhost:44314/home/environment
ちなみに補足ですが、「IWebHostEnvironment の型の引数をコンストラクタの引数に追加すればどこでも使える!」というわけではありません。
そのコンストラクタが定義されているクラスが DI コンテナに依存関係として登録されている必要があります。
今回であれば BusinessRule クラスですね。
ここは次に説明するカスタム依存関係の登録にも関わる部分なので、次章にて説明します。
カスタム定義の依存関係
事前定義済みの依存関係に関しては、特にアプリケーションコードでの準備は必要なく
コンストラクタに引数を追加すればよいのですが、自分で定義した型を登録するためには明示的に追加してあげる必要があります。
このカスタム定義の依存関係なのですが、実は先ほどのサンプルコードにて IBusinessRule として既に実装済みです。
HomeController から BusinessRule クラスを利用していますが、HomeController クラスにおいては BusinessRule クラスのインスタンスの生成は行っていません。
先ほどの IWebHostEnvironment と同様に、DI コンテナがインスタンス生成を行っています。
ただ、先ほどの事前定義のものとは違い、カスタム定義の依存関係を DI コンテナに事前に教えてあげる必要があります。 それが Program.cs に書かれている以下のコードです。
builder.Services.AddSingleton<IBusinessRule, BusinessRule>();
このようにインスタンス生成を DI コンテナに任せることで、HomeController は BusinessRule クラスに依存していないという状況を実現できます。
これは前回の記事でも散々述べてきたことで、最重要ポイントでもあります。
実装の拡張
例えば、BusinessRule を異なるルールに置き換えたいとします。
あまりいい例ではないかもしれませんが、表示文字列を英語で返すような EnglishBusinessRuleクラスを定義します。
なお、実装する対象のインターフェイスは変わらず IBusinessRule です。
EnglishBusinessRule.cs
namespace aspnetcore_8_mvc
{
public class EnglishBusinessRule : IBusinessRule
{
IWebHostEnvironment _env;
public EnglishBusinessRule(IWebHostEnvironment env)
{
_env = env;
}
public string WebHostEnvironment()
{
if (_env.IsDevelopment())
{
return "This is Development Environment";
}
else
{
return "This is Production Environment";
}
}
}
}
クラスの関係性としては以下のような形です。
では、BusinessRule クラスから EnglishBusinessRule クラスに置き換えをしたい場合、どういった修正が必要になるでしょうか?
今回は、DIコンテナを利用しているため、Program.cs において依存関係を登録している部分のみの修正で実現できます。
//builder.Services.AddSingleton<IBusinessRule, BusinessRule>();
//↓
builder.Services.AddSingleton<IBusinessRule, EnglishBusinessRule>();
実行してみます。
https://localhost:44314/home/environment
メッセージが英語で表示されていることが確認できました。
仮に DI コンテナを利用していない場合、BusinessRule クラスのインスタンスを生成しているすべての箇所のソースコードの修正が必要になります。
その点、DI コンテナを利用することで呼び出し側の修正なしで変更できるので、変更量が少なくて済みます。
ライフタイムの管理
DI コンテナに依存関係を伝える手段として、ASP.NET Core には 3 種類の方法が提供されています。
AddTransient | 呼び出しのたびに指定した型の新しいインスタンスが呼び出し元に返される |
AddSingleton | 指定した型の最初に作成されたインスタンスが呼び出し元に返される 型に関係なく、各アプリケーションが独自のインスタンスを取得する |
AddScoped | AddSingletonと同じだが、スコープは現在のリクエストになる |
(出典:プログラミングASP.NET Core)
どれを使うかはアプリケーションの設計に基づいて決定します。
例えばRESTfulなAPIを実装する場合を想定すると、
前の状態に依存したりしないのでAddSingletonで追加しても動作に影響なく、かつ無駄なインスタンス生成が行われないので良さそうな選択です。。
また書籍に以下のような注意事項が書かれていました。
この時注意することとして、特定のライフタイムで登録されたコンポーネントが、それよりも短いライフタイムで登録された他のコンポーネントに依存できないことです。
確かにSingletonとして登録されたコンポーネントAに対して、Transientとして登録されたコンポーネントBを注入した場合、コンポーネントBが想定されたライフタイムよりも長生きしてしまう可能性があります。
必ずしもバグが発生するとは限りませんが、なんだか良からぬ事が発生してしまいそうです。
この点注意してライフタイムの管理をしていきましょう。
まとめ
今回はASP.NET CoreのDIコンテナについての概要を解説しました。
このDIコンテナを正しく扱うことで、以前書いた「依存性の制御」をより強力に扱うことができるようになります。
DIコンテナもまだまだ奥が深い部分がありますが、このあたりがしっかり理解できると、DDDやClean Architectureといったようなアーキテクチャもうまく扱えるようになると思います!
今回の記事で「何となく分かったなぁ」となった方は、また色々と深堀してみてください!
ではまた!