お断り
試した環境は以下の通りで、それ以外の環境については一切試していません。
- Visual Studio Community 2017 (15.6)
- Miscrosoft.AspNetCore.App (2.1.0-preview1-final)
- Miscrosoft.NETCore.App (2.1.0-previwe1-26216-03)
HttpClient についてのアレコレ
System.Net.Http.HttpClient はIDisposableを実装しているのに、データアクセスのたびにusing
を用いることは好ましくないという罠があります。
もちろん私もDbConnectionやStreamを扱うような感じでじゃんじゃんusing
しまくっていた一人です。
では、どうすれば良かったのかというと、HttpClientオブジェクトをシングルトンのようにして扱わないといけないとのことです。
シングルトンを使うとテストを記述するのが一段階難しくなるので出来れば御免被りたいところです。
HttpClientFactory
HttpClientの使い勝手が直感に反する問題を受けてか、HttpClientFactory
がASP.NET Core 2.1にて追加されました*1。
https://github.com/aspnet/HttpClientFactory/blob/dev/src/Microsoft.Extensions.Http/DefaultHttpClientFactory.cs#L110github.com
読んで字のごとく、HttpClient
を返すFactoryなのですが、悩ましかったライフサイクル管理を自分でしなくてもよくなるようです。
var client = _httpClientFactory.CreateClient();
var result = await client.GetStringAsync("http://~");
CreateClient()
が返すものはやっぱりHttpClient
で、依然としてIDisposable
を実装しているので、下記のような使い方はダメです。
(追記:実装を読むと、disposeHandler: falseとなっているのでusingしても問題は無いのかもしれません)
//NG! 色んな人の努力が無に帰す using(var client = _httpClientFactory.CreateClient()){ var result = await client.GetStringAsync("http://~"); }
Dependency Injectionと組み合わせる
さて、HttpClientFactory
のままでもそれなりに使えるのですが、上記Webサイト内でTyped Clients
と称されている使い方とDIを組み合わせてみます。
ソースコード
//[SampleDojo.Repository] Project using System.Threading.Tasks; namespace SampleDojo.Repository { //ITodoRepository.cs public interface ITodoRepository { Task<string> GetTodoAsync(); } //IGorillaRepository.cs public interface IGorillaRepository { Task<string> GetGorillaAsync(); } }
//[SampleDojo.Repository.Http] Project using System; using System.Threading.Tasks; using System.Net.Http; using Microsoft.Extensions.Logging; namespace SampleDojo.Repository.Http { //TodoRepository.cs public class TodoRepository : ITodoRepository { private readonly HttpClient httpClient; private readonly ILogger<TodoRepository> logger; public TodoRepository(HttpClient httpClient, ILogger<TodoRepository> logger) { this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public async Task<string> GetTodoAsync() { var uri = "/api/todo"; return await httpClient.GetStringAsync(uri).ConfigureAwait(false); //return await Task.FromResult(httpClient.BaseAddress.ToString()); } } //GorillaRepository.cs public class GorillaRepository : IGorillaRepository { private readonly HttpClient httpClient; private readonly ILogger<GorillaRepository> logger; public GorillaRepository(HttpClient httpClient, ILogger<GorillaRepository> logger) { this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public async Task<string> GetGorillaAsync() { var uri = "/api/gorilla"; return await httpClient.GetStringAsync(uri).ConfigureAwait(false); //return await Task.FromResult(httpClient.BaseAddress.ToString()); } } }
//[SampleDojo.WebSite] Project //Startup.cs public class Startup { public void ConfigureServices(IServiceCollection services) { var uri = new Uri("http://localhost:5005"); var gorillaUri = new Uri("http://localhost:55555"); services.AddHttpClient<ITodoRepository, Repository.Http.TodoRepository>(c => c.BaseAddress = uri); services.AddHttpClient<IGorillaRepository, Repository.Http.GorillaRepository>(c => c.BaseAddress = gorillaUri);
SampleDojo.WebSite.Startup().ConfigureServices(IServiceCollection )
内、AddHttpClient
がHttpClientFactory
周辺のDIとなります。
これによりRepository.Http.TodoRepository
のコンストラクタには、BaseAddress : http://localhost:5005
と設定されたHttpClient
がInjectされます。
ページでの利用は、例えば下記の通りです。
namespace SampleDojo.WebSite.Pages { public class TodoModel : PageModel { private readonly ITodoRepository todoService; public ContactModel(ITodoRepository todoService) { this.todoService = todoService ?? throw new ArgumentNullException(nameof(todoService)); } public string Message { get; set; } public async Task OnGet() { Message = await todoService.GetTodoAsync().ConfigureAwait(false); } } }
RepositoryのDapper実装例と比較してみる
次に、SampleDojo.Repository
の各インターフェースをDapperを用いた実装例を作成してみます。
//[SampleDojo.Repository.SqlServer] Project using Dapper; using Microsoft.Extensions.Logging; using System; using System.Data; using System.Threading.Tasks; namespace SampleDojo.Repository.SqlServer { //TodoRepository.cs public class TodoRepository : ITodoRepository { private readonly IDbConnection dbConnection; private readonly ILogger<TodoRepository> logger; public TodoRepository(IDbConnection dbConnection, ILogger<TodoRepository> logger) { this.dbConnection = dbConnection ?? throw new ArgumentNullException(nameof(dbConnection)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public async Task<string> GetTodoAsync() { var sql = "SELECT ~"; return await dbConnection.QueryFirstOrDefaultAsync<string>(sql).ConfigureAwait(false); //return await Task.FromResult(dbConnection.ConnectionString); } } //GorillaRepository.cs public class GorillaRepository : IGorillaRepository { private readonly IDbConnection dbConnection; private readonly ILogger<GorillaRepository> logger; public GorillaRepository(IDbConnection dbConnection, ILogger<GorillaRepository> logger) { this.dbConnection = dbConnection ?? throw new ArgumentNullException(nameof(dbConnection)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public async Task<string> GetGorillaAsync() { var sql = "SELECT ~"; return await dbConnection.QueryFirstOrDefaultAsync<string>(sql).ConfigureAwait(false); //return await Task.FromResult(dbConnection.ConnectionString); } } }
//[SampleDojo.WebSite] Project //Startup.cs public class Startup { public void ConfigureServices(IServiceCollection services) { var connectionString = Configuration.GetConnectionString("DefaultConnection"); services.AddScoped<IDbConnection>(p => new SqlConnection(connectionString)); services.AddTransient<ITodoRepository, Repository.SqlServer.TodoRepository>(); services.AddTransient<IGorillaRepository, Repository.SqlServer.GorillaRepository>();
Repositoryの実装部分、SampleDojo.Repository.Http
とSampleDojo.Repository.SqlServer
のコードは、形が非常に似ました。
ここで重要なことは、形が同じであるけれど、SampleDojo.Repository.Http
のHttpClient
はHttpClientFactory
を利用しているおかげでDispose
されることはなくされても問題なく動作し、SampleDojo.Repository.SqlServer
でのIDbConnection
はDIコンテナにより正しくDispose
される*2ということです。
このように、データアクセス方法によらずロジックを似せることが出来ました。これぞDIの妙と言ったところではないでしょうか。
感想
HttpClientFactory
とその周辺を試しました。非常に便利なものになると思います。