URLを物理パスにマップしてインジェクション

ネタが無いのでASP.NETとSpring.NETでよくやる事を書いてみる。

例えばサーバーサイドで「App_Data」フォルダなんかに置いてある設定ファイルを読み書きする場合、そのファイルのURLを「Server.MapPath」メソッドを使って実際の物理パスに変換するなんて事をよくやると思う。

Default.aspx.cs
public partial class _Default : System.Web.UI.Page {
    protected void Page_Load(object sender, EventArgs e) {
        var path = Server.MapPath("~/App_Data/data/hoge.xml");
        // ファイルを読み書きする。
    }
}

で、こういったURLをベタ書きするなんて事は普通しないので、アプリケーション構成ファイルなんかを使って外部から読み込むようにすると思う。

Spring.NETならプロパティを使ってインジェクションするよね。

Default.aspx.cs
public partial class _Default : System.Web.UI.Page {
    public string Path {
        get;
        set;
    }
    protected void Page_Load(object sender, EventArgs e) {
        var path = Server.MapPath(this.Path);
        // ファイルを読み書きする。
    }
}

以下がSpring.NETのオブジェクト定義ファイル

objects.xml
<?xml version="1.0" encoding="utf-8"?>
<objects xmlns="http://www.springframework.net">
    <object type="Default.aspx">
        <property name="Path" value="~/App_Data/data/hoge.xml" />
    </object>
</objects>

別にこれでも問題ないんだけど、この「Path」プロパティにインジェクションされる値がいつもURLとは限らないわけで、普通に物理パスで指定したい事もあるわけだけど、Server.MapPathは物理パスを渡すと例外を履いてしまう。

じゃあ、URLの場合だけServer.MapPathするようにすればいいんだけど、結構面倒くさい(こういうプロパティが他にもあったら、そこでも同じ処理を書いたりしなくちゃいけない)。

こんな時はインジェクションする時についでにServer.MapPathをしてやればいい。

それを実現するための機構として、Spring.NETには「IFactoryObject」というのが用意されている。

IFactoryObjectインターフェースには以下のメンバが定義されている。

  • GetObject
  • IsSingleton
  • ObjectType

GetObject」メソッドでは実際にインジェクションするオブジェクトを返す。
IsSingleton」プロパティはオブジェクトがSingletonオブジェクトかどうかを返す。これは必ずtrueを返す必要がある。
ObjectType」プロパティではGetObjectメソッドで返すオブジェクトの型を返す。

これらを実装したのが以下

ServerMapPathFactoryObject.cs
/// <summary>
/// 仮想ディレクトリのパスをServer.MapPathを使って物理パスに変換するファクトリクラス
/// </summary>
public sealed class ServerMapPathFactoryObject : IFactoryObject, IInitializingObject {
    /// <summary>
    /// 仮想パスを取得、設定します。
    /// </summary>
    public string VirtualPath {
        get;
        set;
    }

    public ServerMapPathFactoryObject() { 

    object IFactoryObject.GetObject() {
        return HttpContext.Current.Server.MapPath(VirtualPath);
    }
    bool IFactoryObject.IsSingleton {
        get { return true; }
    }
    Type IFactoryObject.ObjectType {
        get { return typeof(string); }
    }

    void IInitializingObject.AfterPropertiesSet() {
        if(string.IsNullOrEmpty(this.VirtualPath)) {
            throw new ArgumentNullException("VirtualPathプロパティが設定されていません");
        }
    }
}

VirtualPath」プロパティで渡された値をGetObjectメソッドの中でServer.MapPathを使って物理パスに変換しているだけ。

ちなみに「IInitializingObject」インターフェースはオブジェクトのプロパティへのインジェクションが完了した時に呼び出される「AfterPropertiesSet」メソッドを実装する為のインターフェースで、これを使うとプロパティにちゃんと値が設定されているかどうかを調べる事ができる(別に必須というわけではない)。

これを使って書き直したのが以下

Default.aspx.cs
public partial class _Default : System.Web.UI.Page {
    public string Path {
        get;
        set;
    }
    protected void Page_Load(object sender, EventArgs e) {
        // ファイルを読み書きする。
    }
}

オブジェクト定義ファイル

objects.xml
<?xml version="1.0" encoding="utf-8"?>
<objects xmlns="http://www.springframework.net">
    <object type="Default.aspx">
        <property name="Path">
            <object type="Hoge.ServerMapPathFactoryObject, Hoge">
                <property name="VirtualPath" value="~/App_Data/data/hoge.xml" />
            </object>
        </property>
    </object>
</objects>

こんな感じでインジェクションする値に合わせてオブジェクト定義ファイルを変えてやれば、プログラム側は渡されてくる値がURLなのか物理パスなのか意識しないで済む。

まぁ、逆に面倒くさくなってる気がしないでもないけど、数が増えてくるとこういうのがじわじわ効いてくるんですよ。

実際にはさらにこのパス情報をオブジェクト定義ファイルからアプリケーション構成ファイルに移さないといけないけど、面倒くさいので割愛する。