ASP.NET Coreで設定ファイルによるDIの設定を行い環境によって使用されるコンポーネントを変更する

# やること

AutofacとAutofac.Configurationを使用してASP.NET Coreで設定ファイルを使用したDIの設定を行います。

設定ファイルを使用することで`Production`、`Development`といった環境毎に使用するコンポーネントの変更が容易となります。

また、設定ファイルでコンポーネントのコンストラクタ引数やプロパティの値を指定する際に、標準ではサポートされない型についての対応方法も記述します。

# 対象読者

* ASP.NET Coreで実行環境に応じて使用されるコンポーネントを差し替えたい人

# 環境

* Visual Strudio 2017
* Autofac 4.6.0
* Autofac.Configuration 4.0.1
* Autofac.Extensions.DependencyInjection 4.1.0

# サンプル

* [AutofacExample](https://github.com/usausa/Example-Net-AspNetCore/tree/master/AutofacExample)

このサンプルでは環境変数`ASPNETCORE_ENVIRONMENT`に応じて`components.json`または`components.Development.json`からコンポーネントの構築を行っています。

`Development`環境では`IService`実装として`DevelopmentService`が使用され、それ以外では`ProductionService`が使用される設定となっており、どちらの実装が使用されているかはホーム画面で確認できるサンプルとなっています。

# 設定ファイルによるIServiceProviderの構築

## JSONファイルを使ったAutofacの設定

`Autofac.Configuration`を使用すると以下のようなコードでJSONファイルを使用したコンテナへのコンポーネント登録が行えます。

```csharp
using Autofac;
using Autofac.Configuration;
using Microsoft.Extensions.Configuration;


var config = new ConfigurationBuilder()
    .AddJsonFile("components.json");
var module = new ConfigurationModule(config.Build());

var builder = new ContainerBuilder();
builder.RegisterModule(module);
var container = builder.Build();
```

設定ファイルの例は以下の様になります。

```javascript:component.json
{
  "defaultAssembly": "AutofacExample",
  "components": [
    {
      "type": "AutofacExample.Services.HogeService",
      "services": [ { "type": "AutofacExample.Services.IService" } ]
    }
  ]
}
```

これはコードで書くと以下の内容と同じです。

```csharp
builder
    .RegisterType<HogeService>()
    .As<IService>();
```

設定ファイルを使用したAutofacの設定方法詳細については[JSON/XML Configuration](http://docs.autofac.org/en/latest/configuration/xml.html)を参照してください。

## IServiceProviderの置換

ASP.NET Coreでの`IServiceProvider`の置換方法については[以前の記事](http://qiita.com/yamaokunousausa/items/bc81d8498ecf62da0208)で解説しています。

`Autofac.Extensions.DependencyInjection`には`IServiceProvider`としてAutofacを使用するための拡張が用意されており、それを使用したコードは以下の様になります。

```csharp:Startup.cs
using Autofac.Extensions.DependencyInjection;


public IServiceProvider ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddMvc();

    // Autofac
    var builder = new ContainerBuilder();
    builder.Populate(services);
    var container = builder.Build();

    return new AutofacServiceProvider(container);
}
```

## 環境に応じた設定ファイルの使用

ASP.NET Coreアプリケーションのテンプレートは、設定ファイル`appsettings.json`を環境に応じて変更する構成になっているかと思います。

これと同様のことを行えばAutofacの設定ファイルについて環境に応じた物が使用できます。

環境に応じた設定ファイルでのAutofacの構築と`IServiceProvider`の置換を組み合わせたコードは以下のようになります。

```csharp:Startup.cs
using System;

using Autofac;
using Autofac.Configuration;
using Autofac.Extensions.DependencyInjection;

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

public class Startup
{
    public IConfigurationRoot Configuration { get; }

    private readonly ConfigurationModule module;

    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();

        // 環境に応じてAutofacの設定ファイルを切り替える
        var config = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("components.json", optional: true)
            .AddJsonFile($"components.{env.EnvironmentName}.json", optional: true);
        module = new ConfigurationModule(config.Build());
    }

    public IServiceProvider ConfigureServices(IServiceCollection services)
    {
        // Add framework services.
        services.AddMvc();

        // Autofac
        var builder = new ContainerBuilder();
        builder.Populate(services);     // IServiceCollectionからのコンポーネントの構築
        builder.RegisterModule(module); // 設定ファイルによるコンポーネントの構築
        var container = builder.Build();

        return new AutofacServiceProvider(container);
    }
...
}
```

# その他

## TypeConverterによる値の変換

設定ファイルを使用してコンポーネントの定義ではコンストラクタ引数やプロパティの値を指定することが可能です。

ただし、`Autofac.Configuration`が標準ではサポートしていない型の項目への設定は例外となります。

例えば、以下の様にbyte配列をコンストラクタ引数とするクラスの定義を行いたい場合、設定ファイルで「`"token": [1, 2, 3]`」の用に記述しても例外となります。

```csharp:ProductionService.cs
public class ProductionService : IService
{
    private readonly byte[] token;

    public ProductionService(byte[] token)
    {
        this.token = token;
    }
}
```

`Autofac.Configuration`がサポートしていない型の設定を行いたい場合、`TypeConverter`を用意することで型変換を行っての設定が可能となります。

例えばbyte配列を16進文字列で設定するようにしたければ、以下の様な`TypeConverter`を実装して適用先で宣言します。

```csharp:ProductionService.cs
using System;
using System.ComponentModel;
using System.Globalization;

public class HexStringConverter : TypeConverter
{
    public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
    {
        return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
    }

    public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
    {
        var s = value as string;
        if (s != null)
        {
            return Decode(s, culture);
        }

        return base.ConvertFrom(context, culture, value);
    }

    private static byte[] Decode(string code, CultureInfo culture)
    {
        var bytes = new byte[code.Length / 2];

        for (var index = 0; index < bytes.Length; index++)
        {
            bytes[index] = byte.Parse(code.Substring(index * 2, 2), NumberStyles.HexNumber, culture);
        }

        return bytes;
    }
}
```

```csharp:ProductionService.cs
using System.ComponentModel;

public class ProductionService : IService
{
    private readonly byte[] token;

    public ProductionService([TypeConverter(typeof(HexStringConverter))] byte[] token)
    {
        this.token = token;
    }
}
```

また、この時の設定ファイルの記述は以下のようになります。

```javascript:component.json
{
  "defaultAssembly": "AutofacExample",
  "components": [
    {
      "type": "AutofacExample.Services.ProductionService",
      "services": [ { "type": "AutofacExample.Services.IService" } ],
      "parameters": {
        "token": "0123456789ABCDEF"
      }
    }
  ]
}
```

# うさコメ

.NETでのDIというとGuice型のもので、コードを使ってコンテナの構築をするケースが多かったりするんじゃないかと思います(・ω・)

ただ、設定ファイルを使って環境によって使用するコンポーネントを切り替えたいみたいなニーズも当然あるわけで、AutofacとAutofac.Configurationを使うと簡単にできるよ、っというのが今回のお話でした。

そもそも、コードベースでのコンポーネントの構築も、規約ベースでのコンポーネントスキャンによる自動登録も、設定ファイルによるコンポーネントの登録も、用途に応じて組み合わせて使い分けるべきもので、どれかがあれば他はいらないとかそういう種のものじゃないと思いますのだ(´・ω・`)