LoginSignup
1
3

More than 3 years have passed since last update.

【ASP.NET】マルチテナントサービスにおけるデータベースアクセスのDI

Last updated at Posted at 2020-01-07

あけましておめでとうございます。

2019年は個人的にクリーンアーキテクチャの年でした。
Webアプリケーションにクリーンアーキテクチャを適用するため試行錯誤をした結果、ようやっとまとまりだしたので、自分用のメモという意味も含め、Qiitaに初記事を投下しようという試みです。
至らない点を許すことなくお付き合いいただけますと幸いです。

注意

この記事ではタイトルに書いたこと以外は極力説明を省きます。
クリーンアーキテクチャの概要や実装方法については尊敬する先達の記事を参照ください。

実装クリーンアーキテクチャ - @nrslib
https://qiita.com/nrslib/items/a5f902c4defc83bd46b8

ソースコードで理解するクリーンアーキテクチャ - Sansan Builders Box
https://buildersbox.corp-sansan.com/entry/2019/07/10/110000

環境

フレームワーク

.Net Framework 4.6.1

DIコンテナ

Simple Injector

EntityFramework等のORMは使用していません。

なお、とあるBtoBアプリケーションのユーザ情報に関する処理という前提で説明します。

概要

前置きが長くなりましたがここから本題です。

クリーンアーキテクチャの概念を自社のサービスに取り入れるにあたって様々な困難がありました。
その中でも頭を悩ませたのがビジネスロジックとデータの永続化を分離する部分で、大きく以下の2点です。

  1. マルチテナントサービスにおけるデータベースアクセスのDI
  2. 複数のリポジトリにまたがるトランザクション

今回はこのうち、マルチテナントサービスにおけるデータベースアクセスのDIに対する実装例をご紹介します。

直面した課題

はじめ、クリーンアーキテクチャで実装をしようと思ってさくっと調べ、見よう見まねで以下のような構成をとろうとしました。
ユーザのIDでユーザ名を取得するような処理を例とします。

UserRepository.cs
using MySql.Data.MySqlClient;

public interface IUserRepository 
{
    string GetUserNameById(int id);
}

public class UserRepository: IUserRepository 
{
    public string GetUserNameById(int id) 
    {
        var name = "";

        using (var connection = new MySqlConnection(SomeConfig.ConnectionString))
        using (var command = connection.CreateCommand())
        {
            connection.Open();
            command.CommandText = "SELECT name FROM user WHERE id = @id";
            command.Parameters.Add(new MySqlParameter("@id", id));
            name = command.ExecuteScalar().ToString();
        }

        return name;
    }
}
GetUserNameUseCase.cs
public interface IGetUserNameByIdUseCase
{
    string Handle(int id);
}

public class GetUserNameByIdUseCase : IGetUserNameByIdUseCase
{
    private readonly IUserRepository _repository;

    public GetUserNameByIdUseCase(IUserRepository repository)
    {
        this._repository = repository;
    }

    public string Handle(int id)
    {
        var name = this._repository.GetUserNameById(id);
        return name;
    }
}
Global.asax
using System.Web.Mvc;
using SimpleInjector;
using SimpleInjector.Integration.Web.Mvc;

public class UserApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        //アプリケーション設定のいろいろ

        var diContainer = new Container();
        diContainer.Register<IUserRepository, UserRepository>();
        diContainer.Verify();

        DependencyResolver.SetResolver(new SimpleInjectorDependencyResolver(diContainer));
    }
}

今回の問題点として、マルチテナントの構成をとっている場合、UserRepositoryが使用する接続文字列が単一ではなく、リクエスト単位で異なります。そうなるとリクエストに紐づいた接続文字列をUserRepositoryまで渡してやる必要があります。

また、毎回コネクションを開く処理を書くのも冗長です。どげんかせんといかん。

解決方法

Step1. DataAccessContextを作る

まず、データベースへのアクセスはそれ自体が一つの関心ごとになると言えます。
UserRepositoryが複数の関心ごとにとらわれないよう、UserRepositoryから接続の関心ごとを分離しましょう。

以下のような接続に関する処理をまとめたクラスを作成し、処理を委任します。

DataAccessContext.cs
using MySql.Data.MySqlClient;

public class DataAccessContext 
{
    private readonly string _connectionString;

    public DataAccessContext(string connectionString)
    {
        this._connectionString = connectionString;
    }

    public void ExecuteDataAccess(Action<MySqlConnection, MySqlCommand> action)
    {
        using (var connection = new MySqlConnection(this._connectionString))
        using (var command = connection.CreateCommand())
        {
            connection.Open();
            action(connection, command);
        }
    }
} 

Step2. リポジトリのフィールドにDataAccessContextを加える

UserRepository.cs
using MySql.Data.MySqlClient;

public class UserRepository: IUserRepository 
{
    private readonly DataAccessContext _context;

    public UserRepository(DataAccessContext context) 
    {
        this._context = context;
    }

    public string GetUserNameById(int id) 
    {
        var name = "";

        this._context.ExecuteDataAccess((connection, command) => 
        {
            command.CommandText = $"SELECT name FROM user WHERE id = @id";
            command.Parameters.Add(new MySqlParameter("@id", id));
            name = command.ExecuteScalar().ToString();
        });

        return name;
    }
}

すっきりしました。

Step3. Global.asaxで、DataAccessContextをInject

今回はマルチテナントの想定なので、リクエストに基づいた接続文字列をDataAccessContextに渡す必要があります。
ログイン時にセッションへ接続文字列を保存してある想定とすると、現在のセッション情報をもとにオブジェクトを生成することができます。

Global.asax
using System.Web.Mvc;
using SimpleInjector;
using SimpleInjector.Integration.Web.Mvc;

public class UserApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        //アプリケーション設定のいろいろ

        var diContainer = new Container();
        diContainer.Register(typeof(DataAccessContext), () =>
        {
            var connectionString = HttpContext.Current.Session["connectionString"]?.ToString();
            if (connectionString != null)
            {
                return new DataAccessContext(connectionString);
            } 
            else 
            {
                return (DataAccessContext)null;
            }
        });

        diContainer.Register<IUserRepository, UserRepository>();
        diContainer.Verify();

        DependencyResolver.SetResolver(new SimpleInjectorDependencyResolver(diContainer));
    }
}

以上でマルチテナントサービスでもデータベースアクセスをDIすることができました。

振り返り

振り返ってみると解決法自体はシンプルですね。
今回主につまづいたのはStep3に関する部分であり、なぜつまづいたかをまとめてみました。

  1. Simple Injectorのオブジェクト生成タイミングが理解できていなかった
  2. HttpContext.Currentを忘れてた

どちらにも共通して言えることは、使用するフレームワークに対する知識の浅さが要因ということです。
採用した技術に関して理解を深めるというのは技術を使用する立場としての当然の心得ですが、それを強烈に認識できたことはいい経験になったなぁと思います。

あと、今回書いたコードはQiita用に雑にこしらえたものでありそのままコピペとかでは動かない恐れがあります。ごめんなさい。
余裕があればトランザクションも含めたソースコードも公開したいなと…思って…います…。

次回

次回は複数のリポジトリにまたがったトランザクションについて書きます。

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3