1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C# - 「プロパティを呼ぶラムダ式」(式ツリー)を引数に指定するメソッドに対し、対象クラスを動的に指定したい

Last updated at Posted at 2024-11-10

初めに

この記事で使用している.NETのバージョンは8です。

C#初心者が調べたことをまとめているため、不備・不足あるかもしれませんがご了承ください。

どんな内容の話?

↓こんなメソッドがあって

void ExecuteOnProperty<U>(Expression<Func<T, U>> expression);

↓こんな感じでメソッドを呼んでいる。

Util<Person>.ExecuteOnProperty(x => x.Age);

でも動的に型指定したい、どうしよう(上の例でいうとPersonクラスの指定を動的にしたい)。
ってお話です。

目的

「プロパティを呼ぶラムダ式」(式ツリー)を引数に指定するメソッドに対し、対象クラス指定を動的にすること。

以前、プロパティ情報を取得するために、プロパティ名を文字列で指定したくないという目的で、メソッドの引数に「プロパティを呼ぶラムダ式」を指定することを一つの案として挙げたことがあります。
Expression<Func<T, U>>を引数の型にするという内容ですが、次の記事でその話をしていました。

さて、この案を採用したとして、対象のクラスを動的に変えたいときどうすれば?と思った次第です。

コードの改善

ベースとなるコードの提示

// 取得したいプロパティをもつクラス
public class Person
{
    public string Name { get; set; }

    public int? Age { get; set; }
}
// 「プロパティを呼ぶラムダ式」(式ツリー)を引数に指定するメソッドをもつクラス
public static class Util<T>
{
    public static void ExecuteOnProperty<U>(Expression<Func<T, U>> expression)
    {
        ArgumentNullException.ThrowIfNull(expression);

        MemberExpression memberExpression = expression?.Body as MemberExpression;
        PropertyInfo property = memberExpression?.Member as PropertyInfo;

        /* 以下、propertyを用いた処理(例:プロパティに付与された属性の取得)*/

    }
}
// 今回、以下に変更を加えていく
public class Program
{
    public static void Main(string[] args)
    {
        Method();
    }

    private static void Method()
    {
        // ↓静的にPerson型を指定しているところを動的に型を指定したい。
        Util<Person>.ExecuteOnProperty(x => x.Age);
    }
}

改善1:対象クラスを動的に指定する

ベースでは、型パラメータに静的にPersonクラスを指定していたが、動的に指定できるようにした。

public class Program
{
    public static void Main(string[] args)
    {
        // ここでは例として、Personクラスを指定している。
        Method(typeof(Person));
    }

    private static void Method(Type type)
    {
        // expression作成:ここから-------------------------------------------------------
        PropertyInfo property = type.GetProperty("Age");
        MemberExpression memExp = Expression.Property(Expression.New(type), property);
        ParameterExpression paramExp = Expression.Parameter(type);

        Type functionType = typeof(Func<,>).MakeGenericType(type, property.PropertyType);
        // ↓ExpressionのいくつかあるLambdaメソッドからほしいものを取得している
        MethodInfo lambdaMethod = typeof(Expression).GetMethods()
            .Where(m => m.Name == nameof(Expression.Lambda))
            .Where(m => m.IsGenericMethod)
            .Where(m => m.GetParameters().Length == 2)
            .Where(m => m.GetParameters()[0].ParameterType == typeof(Expression))
            .Where(m => m.GetParameters()[1].ParameterType == typeof(ParameterExpression[]))
            .First()
            .MakeGenericMethod(functionType);

        object expression = lambdaMethod.Invoke(null, [memExp, new ParameterExpression[] { paramExp }]);
        // expression作成:ここまで-------------------------------------------------------

        Type utilType = typeof(Util<>).MakeGenericType(type);
        MethodInfo executeOnPropertyMethod = utilType.GetMethod(nameof(Util<object>.ExecuteOnProperty))
            .MakeGenericMethod(property.PropertyType);

        executeOnPropertyMethod.Invoke(null, [expression]);
    }
}

付録

コード1:引数の型がExpression<Func<T, object>>だった場合

先述のコードそのままでは、エラーが出てしまう。

  • メソッドに型パラメータがないためエラーが出る
  • プロパティの戻り値の型からobject型へ変換できないというエラーが出る

そのため、コードを変えてあげる必要がある。

public static class Util<T>
{
    public static void ExecuteOnProperty(Expression<Func<T, object>> expression)
    {
        ArgumentNullException.ThrowIfNull(expression);

        // 変換したためにUnaryExpressionでラップされているので、取得処理を追加している。
        UnaryExpression unaryExpression = expression?.Body as UnaryExpression;
        MemberExpression memberExpression = unaryExpression?.Operand as MemberExpression;
        PropertyInfo property = memberExpression?.Member as PropertyInfo ?? throw new Exception();

        /* 以下、propertyを用いた処理(例:プロパティに付与された属性の取得)*/

    }
}
public class Program
{
    public static void Main(string[] args)
    {
        // ここでは例として、Personクラスを指定している。
        Method(typeof(Person));
    }

    private static void Method(Type type)
    {
        // expression作成:ここから-------------------------------------------------------
        PropertyInfo property = type.GetProperty("Age");
        MemberExpression memExp = Expression.Property(Expression.New(type), property);
        // ↓プロパティの戻り値の型からobjectへ変換
        UnaryExpression unaryExpression = Expression.Convert(memExp, typeof(object));
        ParameterExpression paramExp = Expression.Parameter(type);

        Type functionType = typeof(Func<,>).MakeGenericType(type, typeof(object));
        // ↓ExpressionのいくつかあるLambdaメソッドからほしいものを取得している
        MethodInfo lambdaMethod = typeof(Expression).GetMethods()
            .Where(m => m.Name == nameof(Expression.Lambda))
            .Where(m => m.IsGenericMethod)
            .Where(m => m.GetParameters().Length == 2)
            .Where(m => m.GetParameters()[0].ParameterType == typeof(Expression))
            .Where(m => m.GetParameters()[1].ParameterType == typeof(ParameterExpression[]))
            .First()
            .MakeGenericMethod(functionType);

        object expression = lambdaMethod.Invoke(null, [unaryExpression, new ParameterExpression[] { paramExp }]);
        // expression作成:ここまで-------------------------------------------------------

        Type utilType = typeof(Util<>).MakeGenericType(type);
        MethodInfo executeOnPropertyMethod = utilType.GetMethod(nameof(Util<object>.ExecuteOnProperty));

        executeOnPropertyMethod.Invoke(null, [expression]);
    }
}

プロパティの戻り値の型からobject型へ変換の際、MemberExpressionからUnaryExpressionへ変換している。
そのため、Util<T>クラスでもその追従のため改修している。

Expression.Convertではなく、Expression.TypeAsでも変換はできるよう。

最後に

実際に使う分にはもう少し最適化したほうがいいと思います。

関連

1
0
2

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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?