ASP.NET Core のセッティングは前回のブログで理解できたが、実際にコードを書こうとすると困った。
Controller 以外の場所から設定したコンフィグを使うにはどうしたらいいのだろう?例えば DocumentDB にアクセスするためのラッパークラスを作成するとして、そこに引数として、AppSettings のインスタンス渡すのとかダサすぎる。
以前だと、Configuration Manager
があれば、コードのどこでも環境変数等が取れたので、そういうことをしたいときはどうしたらいいのだろう?この問題はこのブログで半分は解決した。
Dependency Injection
ASP.NET Core で大きく変わったところは、DI(Dependency Injection) がサポートされていること。詳しくはこのあたりのドキュメントやブログが詳しい。
- ASP.NET CoreにおけるDI(Dependency Injection)
- Introduction to Dependency Injection in ASP.NET Core
- ASP.NET - 依存関係の挿入による ASP.NET Core でのクリーンなコードの作成
DI に対する疑問
DIはとても良いが、Configuration Manager で出来たように、どこからでもDIされたインスタンスを取得するためには、Singleton 的なインスタンスにどこからでもアクセスできる必要があるが、Startup.cs
を見る限り、あくまでインスタンスなので、staticとしてはアクセスできない。
現時点で私がわかっているのは、DIが組み込みで入っているのだから、Controller の下でインスタンスをくみ上げてしまえば、そこには、DIされるはずだ。現時点でわかっていないことは、じゃあ、完全に Controller から独立したライブラリを書いた場合、どうやって、AppSettingsを取得するのか?ということだ。ConfigrationBuilderを使って、フレームワークがやっているのと同じコードを書けば確かに達成できるけど、せっかくフレームワークがやっていることを自分がもう一度やるのはダサいなと感じるのでここは何か解法があるのだろうか?
Controller 配下でのクラス作成
このやり方はわかったのでメモしておく。例えば、CosmosDBのラッパを書いてみる。ラッパーからは、当然AppSettingsが必要になる。、依存関係をこういう風に考えてみる。
HomeController -> CosmosDBRepository -> AppSettings
という感じだろうか。ASP.NET Core だと、コンストラクタインジェクションを実施してくれるみたいなのでそれを利用する。
前回のブログと同じだが
AppSettings.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace PrintServerlessWeb.Models
{
public class AppSettings
{
public CosmosDB CosmosDB { get; set; }
}
public class CosmosDB
{
public string EndPointUrl { get; set; }
public string AuthorizationKey { get; set; }
public string DatabaseName { get; set; }
public string CollectionName { get; set; }
}
}
があり、それを使った、CosmosDBのラッパを書く。これには、先に作った、AppSettings がインジェクションされる必要がある。
DocumentDBRepository
using Microsoft.Azure.Documents.Client;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using PrintServerlessWeb.Models;
using Microsoft.Extensions.Options;
namespace PrintServerlessWeb
{
public interface IDocumentDBRepository
{
void Hello();
}
public class DocumentDBRepository: IDocumentDBRepository
{
private static DocumentClient client;
private readonly AppSettings appSettings;
public DocumentDBRepository(IOptions<AppSettings> optionsAccessor)
{
this.appSettings = optionsAccessor.Value;
}
public void Hello()
{
Console.WriteLine($"Hello! ******* apppsettings: {appSettings.CosmosDB.EndPointUrl}");
}
}
}
依存性を注入する
これらのオブジェクトを注入するためには、Startup.cs
クラスを見る。今回は、AddSingleton<インターフェイス, 実装クラス>
のように書いておくと、インジェクションしてくれる様子。Singletonの他にも。毎回インスタンスを作るTansient、特定のスコープで有効になる Scoped というオプションがある。これだけ。あとは簡単だ。実際のインジェクトされるクラスにコンストラクタを書いて引数にターゲットのインターフェイスを書いておくとよい。
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.Configure<AppSettings>(Configuration);
services.AddSingleton<IDocumentDBRepository, DocumentDBRepository>();
}
依存性が注入される元の HomeController にもインジェクトする必要があるので、コンストラクタを追加した。注入するインターフェイスを足したら、単に引数を足せばよい。
HomeController
public HomeController(IOptions<AppSettings> optionsAccessor, IDocumentDBRepository repository)
{
this.appSettings = optionsAccessor.Value;
this.cosmosdb = repository;
}
:
public IActionResult Index()
{
Console.WriteLine("***************** start");
Console.WriteLine($"CosmosDB: EndPointUrl : {this.appSettings.CosmosDB.EndPointUrl}");
Console.WriteLine($"CosmosDB: AuthorizationKey: {this.appSettings.CosmosDB.AuthorizationKey}");
Console.WriteLine("***************** end");
cosmosdb.Hello();
return View();
}
:
これでしっかりインジェクションができました。
未解決事項
残念ながら、Controllerの配下にないクラスはどうするんだろう?といのはまだ溶けてない。
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
これを書いたらいけるのはわかるけど、ダサすぎだよね、、、いい方法あるのかな?