我々フレームワークにFluentな設定機能を追加する方法

例えばAutoMapperとかね、オブジェクトの変換ルールをこんな風に設定しますが(・∀・)

public class CalendarEvent
{
    public DateTime EventDate { get; set; }
}

public class CalendarEventForm
{
    public DateTime EventDate { get; set; }
    public int EventHour { get; set; }
    public int EventMinute { get; set; }
}

Mapper.CreateMap<CalendarEvent, CalendarEventForm>()
    .ForMember( dest => dest.EventDate, opt => opt.MapFrom( src => src.EventDate.Date ) )
    .ForMember( dest => dest.EventHour, opt => opt.MapFrom( src => src.EventDate.Hour ) )
    .ForMember( dest => dest.EventMinute, opt => opt.MapFrom( src => src.EventDate.Minute ) );

ForMember()で設定とか(・∀・)カコイイよね、っということで、オレオレフレームワークにもFluentな設定機能を追加する方法を考えてみます。

サンプルフレームワーク

っということで、題材としてはFactoryというか簡易コンテナのようなものを例に取ってみて。
まずはFluentな設定機能無しの実装として、以下のような実装を用意してみて(・ω・)

// 生成するTypeに関するメタ情報
public class TypeMeta
{
    // 生成するType
    public Type Type { get; private set; }

    // Singletonにするか
    public bool IsSingleton { get; set; }

    // コンストラクタをFactory任せではなく独自に行うか
    public Func<object> CustomConstructer { get; set; }

    // プロパティメタ情報一覧
    private readonly Dictionary<string, PropertyMeta> propertyMetas = new Dictionary<string, PropertyMeta>();

    // コンストラクタ
    public TypeMeta(Type type)
    {
        Type = type;
        foreach( PropertyInfo pi in type.GetProperties( BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance ) )
        {
            PropertyMeta propertyMeta = new PropertyMeta( pi );
            this.propertyMetas[ pi.Name ] = propertyMeta;
        }
    }

    // プロパティ一覧取得
    public IEnumerable<PropertyMeta> GetProperties()
    {
        return this.propertyMetas.Values;
    }
    
    // プロパティ取得
    public PropertyMeta GetProperty(string name)
    {
        PropertyMeta propertyMeta;
        this.propertyMetas.TryGetValue( name, out propertyMeta );
        return propertyMeta;
    }
}
// Typeのプロパティに関するメタ情報
public class PropertyMeta
{
    // 初期値の設定を独自に行うか
    public Func<object, object> ValueResolver { get; set; }

    // 再帰的な生成対象から除外するか
    public bool Ignore { get; set; }

    // プロパティの型
    public Type PropertyType
    {
        get { return this.propertyInfo.PropertyType; }
    }

    // PropertyInfo
    private readonly PropertyInfo propertyInfo;

    // コンストラクタ
    public PropertyMeta(PropertyInfo pi)
    {
        this.propertyInfo = pi;
    }

    // 値の設定
    public void SetValue(object target, object value)
    {
        this.propertyInfo.SetValue( target, value, null );
    }
}
// 例題用簡易ファクトリー
public static class Factory
{
    // メタ情報一覧
    private static readonly Dictionary<Type, TypeMeta> typeMetas = new Dictionary<Type, TypeMeta>();
    // シングルトン一覧
    private static readonly Dictionary<Type, object> singletons = new Dictionary<Type, object>();

    // メタ情報の追加
    public static void AddMetaData(TypeMeta typeMeta)
    {
        typeMetas[ typeMeta.Type ] = typeMeta;
    }

    // インスタンス取得
    public static T Create<T>()
    {
        return (T)Create( typeof(T) );
    }

    // インスタンス取得
    public static object Create(Type type)
    {
        // メタ情報が無いインスタンスはそのまま生成
        TypeMeta typeMeta;
        if( !typeMetas.TryGetValue( type, out typeMeta ) )
        {
            return CreateInstance( type );
        }

        // カスタムコンストラクタが定義されていればそれで生成
        object target = typeMeta.CustomConstructer != null ?
                        typeMeta.CustomConstructer() :
                        CreateInstance( typeMeta.Type );

        // プロパティについて再帰的に生成
        foreach( PropertyMeta propertyMeta in typeMeta.GetProperties() )
        {
            if( !propertyMeta.Ignore )
            {
                object value = propertyMeta.ValueResolver != null ?
                               propertyMeta.ValueResolver( target ) :
                               Create( propertyMeta.PropertyType );
                propertyMeta.SetValue( target, value );
            }
        }

        return target;
    }

    // インスタンス生成
    private static object CreateInstance(Type type)
    {
        if( type.IsValueType || ( type == typeof(string) ) )
        {
            return null;
        }
        else
        {
            TypeMeta typeMeta;
            if( typeMetas.TryGetValue( type, out typeMeta ) )
            {
                // シングルトン?
                if( typeMeta.IsSingleton )
                {
                    object target;
                    if( !singletons.TryGetValue( type, out target ) )
                    {
                        target = Activator.CreateInstance( type );
                        singletons[ type ] = target;
                    }
                    return target;
                }
            }

            return Activator.CreateInstance( type );
        }
    }
}

例題用ということで手抜きな実装ですが(´д`;)
マルチスレッドの考慮はしてないですし、メンバの処理もPropertyInfoのみを対象にしています。
あと、interfaceと実装の分離なんかも今回は省略します。

仕様検討

っで、ここまでの実装は既にあるという所からが話の本題(`・ω・´)
現状だと、Factoryクラスのメタ情報の設定は以下の様にに行うわけですが。

// 従来の設定方法
TypeMeta typeMeta = new TypeMeta( typeof(DataObject) );
typeMeta.GetProperty( "Id" ).ValueResolver = src => IdGenerator.Next();
typeMeta.GetProperty( "Ignore" ).Ignore = true;
typeMeta.GetProperty( "CreatedAt" ).ValueResolver = src => DateTime.Now;
Factory.AddMetaData( typeMeta );

これに以下のような、Type safeかつFluentな設定ができるような処理を追加してみたいと思います。

// Type safeでFluentな設定方法
Factory.CreateMeta<DataObject>().
    ForMember( c => c.Id, option => option.UseFrom( src => IdGenerator.Next() ) ).
    ForMember( c => c.Ignore, option => option.Ignore() ).
    ForMember( c => c.CreatedAt, option => option.UseValue( DateTime.Now ) );

Fluentな設定機能の追加

手順としては、まずはメタ情報の式表現として以下のようなクラスを用意して。

// TypeMetaの式表現
public class TypeMetaExpression<TTarget>
{
    private readonly TypeMeta typeMeta;

    // コンストラクタ
    public TypeMetaExpression(TypeMeta typeMeta)
    {
        this.typeMeta = typeMeta;
    }

    // シングルトン設定
    public TypeMetaExpression<TTarget> UseSingleton()
    {
        this.typeMeta.IsSingleton = true;
        return this;
    }

    // カスタムコンストラクタ設定
    public TypeMetaExpression<TTarget> CreateUsing(Func<TTarget> ctor)
    {
        this.typeMeta.CustomConstructer = () => ctor();
        return this;
    }

    // メンバPropertyMetaの設定
    public TypeMetaExpression<TTarget> ForMember(Expression<Func<TTarget, object>> expr, Action<PropertyMetaExpression<TTarget>> option)
    {
        string name = ExpressionUtil.PropertyName<TTarget>( expr );
        PropertyMeta propertyMeta = this.typeMeta.GetProperty( name );
        option( new PropertyMetaExpression<TTarget>( propertyMeta ) );
        return this;
    }
}
// PropertyMetaの式表現
public class PropertyMetaExpression<TTarget>
{
    private readonly PropertyMeta propertyMeta;

    // コンストラクタ
    public PropertyMetaExpression(PropertyMeta propertyMeta)
    {
        this.propertyMeta = propertyMeta;
    }

    // 除外設定
    public void Ignore()
    {
        this.propertyMeta.Ignore = true;
    }

    // カスタム値設定(Func)
    public void UseFrom<TMember>(Func<TTarget, TMember> resolver)
    {
        this.propertyMeta.ValueResolver = src => resolver( (TTarget)src );
    }

    // カスタム値設定(固定値)
    public void UseValue<TMember>(TMember value)
    {
        this.propertyMeta.ValueResolver = src => value;
    }
}

TypeMetaExpressionの各メソッドでは、Fluentな処理にするためにthisをreturnしています。
また、ForMemberメソッドではExpressionによりType safeにプロパティ名を取得しています。
Expressionからプロパティ名を取得するユーティリティーはこんな感じで(・ω・)

// Expression関連のユーティリティー
public static class ExpressionUtil
{
    // プロパティ名取得
    public static string PropertyName<TTarget>(Expression<Func<TTarget, object>> expr)
    {
        PropertyInfo pi = GetMemberInfo( expr ) as PropertyInfo;
        if( pi == null )
        {
            throw new ArgumentException( "expr" );
        }
        return pi.Name;
    }

    // ExpressionからMemberInfo取得
    public static MemberInfo GetMemberInfo(Expression expr)
    {
        while( true )
        {
            switch( expr.NodeType )
            {
                case ExpressionType.Convert:
                    expr = ((UnaryExpression)expr).Operand;
                    break;
                case ExpressionType.Lambda:
                    expr = ((LambdaExpression)expr).Body;
                    break;
                case ExpressionType.MemberAccess:
                    MemberExpression memberExpression = (MemberExpression)expr;
                    if( memberExpression.Expression.NodeType != ExpressionType.Parameter &&
                        memberExpression.Expression.NodeType != ExpressionType.Convert )
                    {
                        throw new ArgumentException( "expr" );
                    }
                    MemberInfo member = memberExpression.Member;
                    return member;
                default:
                    return null;
            }
        }
    }
}

っで、最後にFactoryにTypeMetaExpressionを返すメソッドを追加して終了です。

public static class Factory
{
...
    // TypeMetaの式表現を返す
    public static TypeMetaExpression<TTarget> CreateMeta<TTarget>()
    {
        TypeMeta typeMeta = new TypeMeta( typeof(TTarget) );
        AddMetaData( typeMeta );
        return new TypeMetaExpression<TTarget>( typeMeta );
    }
}

テスト

Type safeでFluentな設定ができるようになったので、その動作を確認してみます。

// Singletonテスト用クラス
public class SingletonObject
{
}

// カスタムコンストラクタテスト用クラス
public class CustomObject
{
    public string Data { get; set; }
}

// Ignoreテスト用クラス
public class IgnoreObject
{
}

// テスト用クラス
public class DataObject
{
    public int Id { get; set; }
    public IgnoreObject NonIgnore { get; set; }
    public IgnoreObject Ignore { get; set; }
    public CustomObject Custom { get; set; }
    public SingletonObject Singleton { get; set; }
    public DateTime CreatedAt { get; set; }
    public string Name { get; set; }
    public int Amount { get; set; }
}

// ID生成用ヘルパー
public static class IdGenerator
{
    private static int current = 1;
       
    public static int Next()
    {
        return current++;
    }
}
public void Example()
{
    // Singleton設定
    Factory.CreateMeta<SingletonObject>().
        UseSingleton();
    
    // カスタムコンストラクタ設定
    Factory.CreateMeta<CustomObject>().
        CreateUsing( () => new CustomObject() { Data = "Created" } ).
        ForMember( c => c.Data, option => option.Ignore() );
    
    // IdはIdGeneratorで初期値を設定
    // NonIgnoreは再帰的に生成、Ignoreは生成を除外
    // CreatedAtはDateTime.Nowを初期値で設定
    Factory.CreateMeta<DataObject>().
        ForMember( c => c.Id, option => option.UseFrom( src => IdGenerator.Next() ) ).
        ForMember( c => c.Ignore, option => option.Ignore() ).
        ForMember( c => c.CreatedAt, option => option.UseValue( DateTime.Now ) );
    
    // インスタンス生成
    DataObject obj1 = Factory.Create<DataObject>();
    
    Debug.Assert( obj1.Id == 1 );
    Debug.Assert( obj1.Custom.Data == "Created" );
    Debug.Assert( obj1.NonIgnore != null );
    Debug.Assert( obj1.Ignore == null );
    Debug.Assert( obj1.Name == null );
    Debug.Assert( obj1.Amount == 0 );
    
    // インスタンス生成
    DataObject obj2 = Factory.Create<DataObject>();
    
    Debug.Assert( obj2.Id == 2 );
    Debug.Assert( !ReferenceEquals( obj1, obj2 ) );
    Debug.Assert( ReferenceEquals( obj1.Singleton, obj2.Singleton ) );
    Debug.Assert( !ReferenceEquals( obj1.NonIgnore, obj2.NonIgnore ) );
}

っということで、Type safeでFluentな設定機能の追加ができました(・∀・)


今回のネタ元はAutoMapperのソースで。
フレームワーク的なものを作っていると、そこで遭遇する固有のパターンみたいなものがいくつかありますが。
こういうパターン(式表現クラスを使ってType safeでFluentな設定機能の追加する)にも名称が欲しい所(・ω・)