しばやん雑記

Azure とメイドさんが大好きなフリーランスのプログラマーのブログ

ASP.NET の App Settings と Connection Strings の値を環境変数からオーバーライド可能にする

App Service は Azure Portal から設定した App Settings と Connection Strings の値を、いい感じにオーバーライドしてくれるのが非常に便利だったので、同じように環境変数からオーバーライド出来るようにするライブラリを書きました。主に Docker 向けとして考えてます。

リポジトリは用意しましたが、NuGet とかにはしてないです。今後考えます。

やってることは App Service と同じプリフィックスが付いた環境変数を読み取って、ConfigurationManager の値を書き換えているだけです。App Service がどう実装してるのかは謎です。

Application_Start より早く処理しておきたいので、PreApplicationStartMethod を使いました。

using System;
using System.Collections;
using System.Configuration;
using System.Linq;
using System.Web;

[assembly: PreApplicationStartMethod(typeof(OverrideConfig.OverrideBootstrapper), "Start")]

namespace OverrideConfig
{
    public class OverrideBootstrapper
    {
        public static void Start()
        {
            var environmentVariables = Environment.GetEnvironmentVariables()
                                                  .Cast<DictionaryEntry>()
                                                  .ToDictionary(x => (string)x.Key, x => (string)x.Value);

            foreach (var appSetting in environmentVariables.Where(x => x.Key.StartsWith(AppSettingsPrefix, StringComparison.OrdinalIgnoreCase)))
            {
                ConfigurationManager.AppSettings[appSetting.Key.Substring(AppSettingsPrefix.Length)] = appSetting.Value;
            }

            var connectionStrings = new ConnectionStringSettingsCollectionWrapper(ConfigurationManager.ConnectionStrings);

            foreach (var connectionString in environmentVariables.Where(x => x.Key.StartsWith(SqlServerConnStrPrefix, StringComparison.OrdinalIgnoreCase)))
            {
                connectionStrings.AddOrUpdate(connectionString.Key.Substring(SqlServerConnStrPrefix.Length), connectionString.Value, SqlServerProviderName);
            }
        }

        private const string AppSettingsPrefix = "APPSETTING_";
        private const string SqlServerConnStrPrefix = "SQLSERVERCONNSTR_";

        private const string SqlServerProviderName = "System.Data.SqlClient";
    }
}

ループを 2 つ回してるのがちょっとイケてない感じしますが、Connection Strings は後付けだったので暇なときに何とかする方向にします。

AppSettings は読み書きが自由なので問題なかったのですが、ConnectionStrings は読み取り専用になってしまっていて、そのままだとコレクションの操作が出来なかったです。少し調べてみると、リフレクションを使って内部のフラグを書き換えろとありました。

ちょっと無理やりな気もしますが、それ以外は Web.config を物理的に書き換える方法しか見つからなかったので、リフレクションを使って書き換える方法を採用することにしました。

最近は Reference Source のおかげで、こういった内部のフラグを調べるのが楽になりましたね。

https://referencesource.microsoft.com/#System.Configuration/System/Configuration/ConfigurationElement.cs,58
https://referencesource.microsoft.com/#System.Configuration/System/Configuration/ConfigurationElementCollection.cs,29

AppSettings とインターフェースを出来るだけ合わせるために、Wrapper を用意して内部でリフレクションを使ってフラグを書き換えるようにしました。

この時 Collection と Element の両方で書き換え処理が必要なので注意です。

internal class ConnectionStringSettingsCollectionWrapper
{
    public ConnectionStringSettingsCollectionWrapper(ConnectionStringSettingsCollection connectionStrings)
    {
        _connectionStrings = connectionStrings;

        typeof(ConfigurationElementCollection).GetField("bReadOnly", BindingFlags.Instance | BindingFlags.NonPublic)
                                                .SetValue(_connectionStrings, false);
    }

    private readonly ConnectionStringSettingsCollection _connectionStrings;

    public void AddOrUpdate(string name, string connectionString, string providerName)
    {
        var settings = _connectionStrings[name];

        if (settings == null)
        {
            _connectionStrings.Add(new ConnectionStringSettings
            {
                Name = name,
                ConnectionString = connectionString,
                ProviderName = providerName
            });
        }
        else
        {
            typeof(ConfigurationElement).GetField("_bReadOnly", BindingFlags.Instance | BindingFlags.NonPublic)
                                        .SetValue(settings, false);

            settings.ConnectionString = connectionString;
            settings.ProviderName = providerName;
        }
    }
}

既に同名のキーが存在する場合には上書き、存在しない場合には新規追加という風にしました。

最後に簡単に動作確認だけしておきます。今回は以下のような設定を Web.config に追加しました。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <appSettings>
    <add key="TEST" value="11111"/>
  </appSettings>
  <connectionStrings>
    <add name="DefaultConnection" connectionString="Server=.\sqlexpress;Database=default;Trusted_Connection=True;" providerName="System.Data.SqlClient"/>
  </connectionStrings>
</configuration>

今回、環境変数として登録した値は以下の通りです。プリフィックスは省略しますが TEST / TEST2 / DefaultConnection をオーバーライドするように環境変数を追加してあります。

f:id:shiba-yan:20170506230052p:plain

そして上で作成したクラスライブラリを読み込むようにした ASP.NET アプリケーションを作成して、AppSettings と ConnectionStrings の値を一覧表示したものが以下になります。

f:id:shiba-yan:20170506230116p:plain

正しく環境変数に追加した値が反映されていることが確認できました。面倒だったので載せてはいないですが、Application_Start の時点でもオーバーライドされた値が取れるようになっているので安心です。

これで Windows Server Containers で ASP.NET アプリケーションを動かす時に、環境変数から接続文字列などを渡すことが出来るようになりました。アプリケーション側の改修が要らないのがメリットです。