ADO.NETヘルパーライブラリを公開しました

## はじめに

[データリポジトリ](https://qiita.com/mxProject/items/2de1ca32db89995179fa) のために開発していたデータベース処理ライブラリに以下の機能を追加しました。

* 非同期メソッド
* インターフェースでプログラミングしやすくするための拡張メソッド
* コネクションやコマンドのメソッドに任意の処理を割り込ませるためのフィルタ

## 公開先

https://github.com/mxProject/AdoNetHelper

https://www.nuget.org/packages/mxProject.Data.AdoNet

## 非同期メソッド

IDbConnection などのインターフェースに対してデータベース処理を非同期化する拡張メソッドを追加しました。.NET Standard 2.1 でサポートされた非同期メソッドと同等の機能を .NET Standard 2.0 でも利用できるようになります。

```csharp
IDbConnection connection = SampleDatabase.CreateConnection();

try
{
    await connection.OpenAsync(cancellationToken);

    using var transaction = await connection.BeginTransactionAsync(cancellationToken: cancellationToken);

    using var command = connection.CreateCommand();

    command.CommandText = "insert into SAMPLE_TABLE values (1, 'abc')";
    command.Transaction = transaction;

    await command.ExecuteNonQueryAsync(cancellationToken);

    await transaction.CommitAsync(cancellationToken);
}
finally
{
    await connection.DisposeAsync();
}
```

追加されるメソッドは以下の通りです。

**IDbConnection インターフェース**

* DisposeAsync
* OpenAsync
* CloseAsync
* ChangeDatabaseAsync
* BeginTransactionAsync

**IDbTransaction**

* DisposeAsync
* CommitAsync
* RollbackAsync

**IDbCommand**

* DisposeAsync
* PrepareAsync
* ExecuteReaderAsync
* ExecuteScalarAsync
* ExecuteNonQueryAsync

**IDataReader**

* DisposeAsync
* CloseAsync
* GetSchemaTableAsync
* ReadAsync
* NextResultAsync

**IDataRecord**

* IsDBNullAsync
* GetValueAsync
* GetValuesAsync


## 拡張メソッド

各インターフェースに対して頻繁に実装する処理を拡張メソッドとして追加しました。

### IDbConnection インターフェース

#### OpenIfClosed メソッド

コネクションが閉じている場合は開きます。開いている場合は何もしません。

```csharp
void Execute(IDbConnection connection)
{
    // コネクションが閉じている場合は開く
    bool opened = connection.OpenIfClosed();

    try
    {
        var command = connection.CreateCommand();
        command.CommandText = "update SAMPLE_TABLE set ...";
        command.ExecuteNonQuery();
    }
    finally
    {
    // コネクションを開いた場合は閉じる
    if (opened) { connection.Close(); }
    }
}
```

#### Cast&lt;TConnection&gt; メソッド

インターフェースの型を用いてアプリケーションロジックを実装しているとき、データベースプロバイダが提供する具象型のプロパティに値を設定したいことがあります。Cast メソッドは自身を具象型にキャストし、指定された処理を実行します。

* コネクションの型が IDbConnectionWrapper インターフェースを実装する場合、ラップしているコネクションをキャストします。ラッパーの仕様によっては、ラップしているコネクションを直接操作することが望ましくない場合があります。そのような場合はこのメソッドを使用しないようにしてください。

```csharp
IDbConnection connection = CreateConnection();

connection.Cast<SqlConnection>(x => {
    // SqlConnection 固有のプロパティに値を設定する
    x.StatisticsEnabled = true;
});
```

---
### IDbTransaction インターフェース

#### Cast&lt;TTransaction&gt; メソッド

インターフェースの型を用いてアプリケーションロジックを実装しているとき、データベースプロバイダが提供する具象型のプロパティに値を設定したいことがあります。Cast メソッドは自身を具象型にキャストし、指定された処理を実行します。

* コネクションの型が IDbTransactionWrapper インターフェースを実装する場合、ラップしているトランザクションをキャストします。ラッパーの仕様によっては、ラップしているトランザクションを直接操作することが望ましくない場合があります。そのような場合はこのメソッドを使用しないようにしてください。

```csharp
IDbTransaction transaction = connection.BeginTransaction();

transaction.Cast<OracleTransaction>(x => {
    // OracleTransaction 固有のメソッドを実行する
    x.Save();
});
```

---
### IDbCommand インターフェース

#### Add{値型}Parameter メソッド

コマンドにパラメーターを追加します。

* 追加されたパラメーターのインデックスとパラメーターのタプルを戻り値として返します。

* パラメーターの値を Object 型で指定する AddParameter メソッドの他、各値型で指定するメソッドを追加しています。なお、SByte, UInt16, UInt32, UInt64, Guid はデータベースプロバイダによっては例外がスローされます。それぞれのデータベースプロバイダのリファレンスを参照してください。
  * AddParameter
  * AddBooleanParameter
  * AddByteParameter
  * AddInt16Parameter
  * AddInt32Parameter
  * AddInt64Parameter
  * AddSByteParameter
  * AddUInt16Parameter
  * AddUInt32Parameter
  * AddUInt64Parameter
  * AddSingleParameter
  * AddDoubleParameter
  * AddDecimalParameter
  * AddDateTimeParameter
  * AddDateTimeOffsetParameter
  * AddTimeParameter
  * AddStringParameter
  * AddCharParameter
  * AddGuidParameter
  * AddBinaryParameter

```csharp
IDbCommand command = CreateCommand();

var (index1, parameter2) = command.AddBooleanParameter("param1", true);

var (index1, parameter2) = command.AddStringParameter("param2");
parameter2.Value = "abc";
```

#### GetParameter メソッド

指定されたインデックスまたはパラメーター名に対応するパラメーターを取得します。

* IDataParameterCollection インターフェースのインデクサで取得できるパラメーターの型は Object 型ですが、このメソッドの戻り値の型は IDbDataParameter 型です。

```csharp
IDbCommand command = CreateCommand();

command.AddBooleanParameter("param1", true);
command.AddStringParameter("param2", "abc");

IDbDataParameter param1 = command.GetParameter(0);
IDbDataParameter param2 = command.GetParameter("param2");
```

#### Set{値型}ParameterValue メソッド

指定されたパラメーターに値を設定します。

* パラメーターの値を Object 型で指定する SetParameterValue メソッドの他、各値型で指定するメソッドを追加しています。なお、SByte, UInt16, UInt32, UInt64, Guid はデータベースプロバイダによっては例外がスローされます。それぞれのデータベースプロバイダのリファレンスを参照してください。
  * SetParameterValue
  * SetBooleanParameterValue
  * SetByteParameterValue
  * SetInt16ParameterValue
  * SetInt32ParameterValue
  * SetInt64ParameterValue
  * SetSByteParameterValue
  * SetUInt16ParameterValue
  * SetUInt32ParameterValue
  * SetUInt64ParameterValue
  * SetSingleParameterValue
  * SetDoubleParameterValue
  * SetDecimalParameterValue
  * SetDateTimeParameterValue
  * SetDateTimeOffsetParameterValue
  * SetTimeParameterValue
  * SetStringParameterValue
  * SetCharParameterValue
  * SetGuidParameterValue
  * SetBinaryParameterValue

```csharp
IDbCommand command = CreateCommand();

command.AddBooleanParameter("param1");
command.AddStringParameter("param2");

command.SetBooleanParameterValue(0, true);
command.SetStringParameterValue("param2", "abc");
```

#### Cast&lt;TCommand&gt; メソッド

インターフェースの型を用いてアプリケーションロジックを実装しているとき、データベースプロバイダが提供する具象型のプロパティに値を設定したいことがあります。Cast メソッドは自身を具象型にキャストし、指定された処理を実行します。

* コマンドの型が IDbCommandWrapper インターフェースを実装する場合、ラップしているコマンドをキャストします。ラッパーの仕様によっては、ラップしているコマンドを直接操作することが望ましくない場合があります。そのような場合はこのメソッドを使用しないようにしてください。

```csharp
IDbCommand command = connection.CreateCommand();

command.Cast<OracleCommand>(x => {
    // OracleCommand 固有のプロパティに値を設定する
    x.BindByName = true;
});
```

---
### IDataParameterCollection インターフェース

#### Cast&lt;TParameters&gt; メソッド

IDataParameterCollection インターフェースに対しても Cast メソッドを追加していますが、使う場面はほとんどないと思います。

* パラメーターコレクションの型が IDataParameterCollectionWrapper インターフェースを実装する場合、ラップしているパラメーターコレクションをキャストします。ラッパーの仕様によっては、ラップしているパラメーターコレクションを直接操作することが望ましくない場合があります。そのような場合はこのメソッドを使用しないようにしてください。

```csharp
using var connection = SampleDatabase.CreateConnection();

var command = connection.CreateCommand();

command.CommandText = "select * from SAMPLE_TABLE where ID = @id";

command.Parameters.Cast<SqlParameterCollection>(x =>
{
    // SqlParameterCollection 固有のメソッドを実行する
    x.AddWithValue("id", 1);
});
```

---
### IDbDataParameter インターフェース

#### Set{値型}Value メソッド

指定されたパラメーターに値を設定します。

* パラメーターの値を Object 型で指定する SetValue メソッドの他、各値型で指定するメソッドを追加しています。なお、SByte, UInt16, UInt32, UInt64, Guid はデータベースプロバイダによっては例外がスローされます。それぞれのデータベースプロバイダのリファレンスを参照してください。
  * SetValue
  * SetBooleanValue
  * SetByteValue
  * SetInt16Value
  * SetInt32Value
  * SetInt64Value
  * SetSByteValue
  * SetUInt16Value
  * SetUInt32Value
  * SetUInt64Value
  * SetSingleValue
  * SetDoubleValue
  * SetDecimalValue
  * SetDateTimeValue
  * SetDateTimeOffsetValue
  * SetTimeValue
  * SetStringValue
  * SetCharValue
  * SetGuidValue
  * SetBinaryValue

```csharp
IDbCommand command = CreateCommand();

var (_, param1) = command.AddBooleanParameter("param1");
var (_, param2) = command.AddStringParameter("param2");

param1.SetBooleanValue(true);
param2.SetStringValue("abc");
```

#### Cast&lt;TParameter&gt; メソッド

インターフェースの型を用いてアプリケーションロジックを実装しているとき、データベースプロバイダが提供する具象型のプロパティに値を設定したいことがあります。Cast メソッドは自身を具象型にキャストし、指定された処理を実行します。

* パラメーターの型が IDbDataParameterWrapper インターフェースを実装する場合、ラップしているパラメーターをキャストします。ラッパーの仕様によっては、ラップしているパラメーターを直接操作することが望ましくない場合があります。そのような場合はこのメソッドを使用しないようにしてください。

```csharp
IDbCommand command = CreateCommand();

var (_, param1) = command.AddBooleanParameter("param1");

param1.Cast<SqlParameter>(x => {
    // SqlParameter 固有のプロパティに値を設定する
    x.ForceColumnEncryption = true;
});
```

---
### IDataReader インターフェース

#### Cast&lt;TCommand&gt; メソッド

インターフェースの型を用いてアプリケーションロジックを実装しているとき、データベースプロバイダが提供する具象型のプロパティに値を設定したいことがあります。Cast メソッドは自身を具象型にキャストし、指定された処理を実行します。

* データリーダーの型が IDataReaderWrapper インターフェースを実装する場合、ラップしているデータリーダーをキャストします。ラッパーの仕様によっては、ラップしているデータリーダーを直接操作することが望ましくない場合があります。そのような場合はこのメソッドを使用しないようにしてください。

```csharp
using var connection = SampleDatabase.CreateConnection();

connection.Open();

using (var command = connection.CreateCommand())
{
    command.CommandText = "select 1 as ID, 'abc' as NAME";

    using var reader = command.ExecuteReader();

    reader.Cast<SqlDataReader>(x =>
    {
        // SqlDataReader 固有のメソッドを実行する
        var idType = x.GetProviderSpecificFieldType(0);
    });
}
```

---
### IDataRecord インターフェース

#### IsDBNull(string name) メソッド

フィールド名を引数とする IsDBNull メソッドです。

* 非同期メソッドも追加しています。

```csharp
IDataReader reader = GetData();

var isnull = reader.IsDBNull("id");
```

#### Get{値型}(string name) メソッド

フィールド名を引数とする GetValue メソッドです。

* 非同期メソッドも追加しています。

```csharp
IDataReader reader = GetData();

var field1 = reader.GetValue("field1");
var field2 = reader.GetBoolean("field2");
var field3 = reader.GetString("field3");
```

#### Get{値型}OrNull メソッド

指定されたフィールドの値が DBNull である場合、null を返します。

* 戻り値の型はNULL許容型です。GetStringOrNull メソッドの戻り値の型は String 型ですが、null である可能性を示す String? として定義しています。

* 非同期メソッドも追加しています。

```csharp
IDataReader reader = GetData();

bool? field1 = reader.GetBooleanOrNull("field1");
string? field2 = reader.GetStringOrNull("field2");
```

#### Get{値型}OrDefault メソッド

指定されたフィールドの値が DBNull である場合、値型の既定値を返します。

* 非同期メソッドも追加しています。

```csharp
IDataReader reader = GetData();

bool field1 = reader.GetBooleanOrDefault("field1");
```

#### GetStringOrEmpty メソッド

指定されたフィールドの値が DBNull である場合、String.Empty を返します。

* 非同期メソッドも追加しています。

```csharp
IDataReader reader = GetData();

string field2 = reader.GetStringOrEmpty("field2");
```

## フィルタ

次のインターフェースのメソッドに対してフィルタを適用することができます。フィルタは任意の処理を割り込ませるための仕組みです。

* IDbConnection
* IDbTransaction
* IDbCommand
* IDataParameterCollection
* IDataReader

```csharp
// フィルタを生成する
var connectionFilters = new IDbConnectionFilter[]{ new SampleConnectionFilter() };
var commandFilters = new IDbCommandFilter[]{ new SampleCommandFilter() };

// コネクションにフィルタを適用する
using var connection = CreateConnection().WithFilter(
    connectionFilters: connectionFilters,
    commandFilters: commandFilters
    );
```

SampleConnectionFilter クラスは生成されたコマンドに既定値を設定するフィルタです。このフィルタが適用されたコネクションの CreateCommand メソッドは、CommandTimeout プロパティに 60 が設定されたコマンドを返します。

```csharp
internal class SampleConnectionFilter : DbConnectionFilterBase
{
    internal SampleConnectionFilter()
    {
    }

    public override DbConnectionFilterTargets TargetMethods =>
        DbConnectionFilterTargets.CreateCommand;

    public override IDbCommand CreateCommand(IDbConnection connection, Func<IDbConnection, IDbCommand> continuation)
    {
        var command = continuation(connection);
        command.CommandTimeout = 60;
        return command;
    }
}
```

SampleCommandFilter クラスはコマンドを実行するときにログを出力するフィルタです。このフィルタが適用されたコマンドを実行すると、ログが出力されます。

```csharp
internal class SampleCommandFilter : DbCommandFilterBase
{
    internal SampleCommandFilter()
    {
    }

    public override DbCommandFilterTargets TargetMethods =>
        DbCommandFilterTargets.ExecuteNonQuery | DbCommandFilterTargets.ExecuteReader | DbCommandFilterTargets.ExecuteScalar;

    public override int ExecuteNonQuery(IDbCommand command, Func<IDbCommand, int> continuation)
    {
        WriteExecutingLog(command);
        try
        {
            var result = continuation(command);
            WriteExecutedLog(command);
            return result;
        }
        catch (Exception ex)
        {
            WriteExceptionLog(command, ex);
            throw;
        }
    }

    public override IDataReader ExecuteReader(IDbCommand command, CommandBehavior behavior, Func<IDbCommand, CommandBehavior, IDataReader> continuation)
    {
        WriteExecutingLog(command);
        try
        {
            var result = continuation(command, behavior);
            WriteExecutedLog(command);
            return result;
        }
        catch (Exception ex)
        {
            WriteExceptionLog(command, ex);
            throw;
        }
    }

    public override object ExecuteScalar(IDbCommand command, Func<IDbCommand, object> continuation)
    {
        WriteExecutingLog(command);
        try
        {
            var result = continuation(command);
            WriteExecutedLog(command);
            return result;
        }
        catch (Exception ex)
        {
            WriteExceptionLog(command, ex);
            throw;
        }
    }

    private void WriteExecutingLog(IDbCommand command)
    {
        // ログを出力する
    }

    private void WriteExecutedLog(IDbCommand command)
    {
        // ログを出力する
    }

    private void WriteExceptionLog(IDbCommand command, Exception exception)
    {
        // ログを出力する
    }
}
```

### DbConnectionFactory クラス

DbConnectionFactory クラスはコネクションを生成するためのファクトリクラスです。フィルタを適用するコネクションを複数回生成する場合、このファクトリクラスを利用してください。フィルタの初期処理で発生するオーバーヘッドを回避することができます。

```csharp
// フィルタを生成する
var connectionFilters = new IDbConnectionFilter[]{ new SampleConnectionFilter() };
var commandFilters = new IDbCommandFilter[]{ new SampleCommandFilter() };

// フィルタを指定してファクトリを生成する
var factory = new DbConnectionFactory(
    connectionFilters: connectionFilters,
    commandFilters: commandFilters
);

// コネクションを生成する
using var connection = factory.CreateConnection();
```

## おわりに

この手の機能を提供するライブラリは既に存在すると思いますが、デファクトスタンダードと言えるものを見つけることができませんでしたので開発しました。

[データリポジトリ](https://qiita.com/mxProject/items/2de1ca32db89995179fa)にも非同期リポジトリを追加しようと考えています。