IAsyncDisposableを実装したObjectをASP.NET CoreのDIでライフタイム管理するときの挙動を見てみる

Using IAsyncDisposable

C#8から追加されたIAsyncDisposable、言わずと知れた await using ~ を実現するための存在ですが、ASP.NET Coreで開発している場合、各Objectのライフタイム管理は、DIContainerに任せることが大半だと思います。 DIContainerにライフタイム管理を任せる場合に、IAsyncDisposableを実装した場合どうなるかを見ていきます。

SqlManager

とりあえずインジェクションしたいのでインジェクションするためにObjectを作ります。

本当はIAsyncDisposableの実装だけにしたいんですが、IDisposable の実装が優先される可能性があるかも見たいので今回は両方実装してます。

using Dapper;
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Threading.Tasks;

namespace MicroORMWrapper {
    public class SqlManager : IAsyncDisposable, IDisposable {
        private DbConnection DbConnection { get; set; }

        public bool IsOpenedConnection => DbConnection.State == ConnectionState.Open;

        public SqlManager(DbConnection dbConnection) {
            DbConnection = dbConnection;

            OpenConnection();
        }

        public void OpenConnection() {
            if (IsOpenedConnection) {
                return;
            }

            DbConnection.Open();
        }

        public async ValueTask CloseConnectionAsync() {
            if (!IsOpenedConnection) {
                return;
            }

            DbConnection.CloseAsync();
        }

        public IEnumerable<TResult> Select<TResult>(string query) where TResult : class? {
            return DbConnection.Query<TResult>(query);
        }

        public IEnumerable<TResult> Select<TResult>(string query, object prameters) where TResult : class? {
            return DbConnection.Query<TResult>(query, prameters);
        }

        public IEnumerable<TResult> Select<TResult, TInclude1>(string query, Func<TResult, TInclude1, TResult> includeFunc, object prameters, string splitOn = "Id") where TResult : class? where TInclude1 : class? {
            return DbConnection.Query(query, includeFunc, prameters, null, true, splitOn);
        }

        public IEnumerable<TResult> Select<TResult, TInclude1, TInclude2>(string query, Func<TResult, TInclude1, TInclude2, TResult> includeFunc, object prameters, string splitOn = "Id") where TResult : class? where TInclude1 : class? where TInclude2 : class? {
            return DbConnection.Query(query, includeFunc, prameters, null, true, splitOn);
        }

        public Task<IEnumerable<TResult>> SelectAsync<TResult>(string query) where TResult : class? {
            return DbConnection.QueryAsync<TResult>(query);
        }

        public Task<IEnumerable<TResult>> SelectAsync<TResult>(string query, object prameters) where TResult : class? {
            return DbConnection.QueryAsync<TResult>(query, prameters);
        }

        public Task<IEnumerable<TResult>> SelectAsync<TResult, TInclude1>(string query, Func<TResult, TInclude1, TResult> includeFunc, object prameters, string splitOn = "Id") where TResult : class? where TInclude1 : class? {
            return DbConnection.QueryAsync(query, includeFunc, prameters, null, true, splitOn);
        }

        public Task<IEnumerable<TResult>> SelectAsync<TResult, TInclude1, TInclude2>(string query, Func<TResult, TInclude1, TInclude2, TResult> includeFunc, object prameters, string splitOn = "Id") where TResult : class? where TInclude1 : class? where TInclude2 : class? {
            return DbConnection.QueryAsync(query, includeFunc, prameters, null, true, splitOn);
        }

        public List<TResult> SelectAsList<TResult>(string query) where TResult : class? {
            return DbConnection.Query<TResult>(query).AsList();
        }

        public List<TResult> SelectAsList<TResult>(string query, object prameters) where TResult : class? {
            return DbConnection.Query<TResult>(query, prameters).AsList();
        }

        public List<TResult> SelectAsList<TResult, TInclude1>(string query, Func<TResult, TInclude1, TResult> includeFunc, object prameters, string splitOn = "Id") where TResult : class? where TInclude1 : class? {
            return DbConnection.Query(query, includeFunc, prameters, null, true, splitOn).AsList();
        }

        public List<TResult> SelectAsList<TResult, TInclude1, TInclude2>(string query, Func<TResult, TInclude1, TInclude2, TResult> includeFunc, object prameters, string splitOn = "Id") where TResult : class? where TInclude1 : class? where TInclude2 : class? {
            return DbConnection.Query(query, includeFunc, prameters, null, true, splitOn).AsList();
        }

        public BuiltInType GetValue<BuiltInType>(string query) {
            return DbConnection.ExecuteScalar<BuiltInType>(query);
        }

        public BuiltInType GetValue<BuiltInType>(string query, object prameters) {
            return DbConnection.ExecuteScalar<BuiltInType>(query, prameters);
        }

        public Task<BuiltInType> GetValueAsync<BuiltInType>(string query) {
            return DbConnection.ExecuteScalarAsync<BuiltInType>(query);
        }

        public Task<BuiltInType> GetValueAsync<BuiltInType>(string query, object prameters) {
            return DbConnection.ExecuteScalarAsync<BuiltInType>(query, prameters);
        }

        public int Execute(string query) {
            return DbConnection.Execute(query);
        }

        public int Execute(string query, object prameters) {
            return DbConnection.Execute(query, prameters);
        }

        public Task<int> ExecuteAsync(string query) {
            return DbConnection.ExecuteAsync(query);
        }

        public Task<int> ExecuteAsync(string query, object prameters) {
            return DbConnection.ExecuteAsync(query, prameters);
        }

        public async ValueTask DisposeAsync() {
            await CloseConnectionAsync();
            GC.SuppressFinalize(this);
        }

        public void Dispose() {
            CloseConnection();
            GC.SuppressFinalize(this);
        }

        public void CloseConnection() {
            if (!IsOpenedConnection) {
                return;
            }

            DbConnection.Close();
        }
    }
}

DI Settings

services.AddControllers();
            services
                .AddScoped<DbConnection>((servicProvider) => new SqlConnection( [SettingPoint:ConnectionString] ))
                .AddScoped<SqlManager>();

SqlManagerはDbConnectionをコンストラクターインジェクションで受け取りたいので、それぞれ、PerRequestで追加します。

Execute

f:id:CreatioVitae:20191009170210p:plain 結論から言うと、ASP.NET Core Mvcの場合、IAsyncDisposableを実装している場合は、IDisposableは無視するようで、Disposeメソッドのブレイクには引っ掛かりません。 なので、100%DI管理下での利用だとわかっている場合は、IDisposableの実装はASP.NET Core MVCでは不要ということになります。

Remarks

もちろん普通にawait using、usingでの宣言を行う場合は、IAsyncDisposable, IDisposableがそれぞれ必要になります。

//こっちで書く場合はIAsyncDisposableの実装が必要
await using var sqlManager= new SqlManager(new SqlConnection( [SettingPoint:ConnectionString] ));
//こっちで書く場合はIDisposableの実装が必要
using var sqlManager= new SqlManager(new SqlConnection( [SettingPoint:ConnectionString] ));

どちらでもできるようにするためには、両方のインターフェースの実装が必要になりますが、そもそもコンソールアプリとかだと、async Mainも使えるので、一旦はIAsyncDisposableのみ実装して、必要に迫られたらIDisposableを実装するでいい気がしてます。 というのも、同期版があると、やっぱりまだまだ皆さん同期版を使ってしまう印象なので。