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

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

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

# どんな内容の話？
↓こんなメソッドがあって
```csharp
void ExecuteOnProperty<U>(Expression<Func<T, U>> expression);
```
↓こんな感じでメソッドを呼んでいる。
```csharp
Util<Person>.ExecuteOnProperty(x => x.Age);
```
でも動的に型指定したい、どうしよう（上の例でいうと`Person`クラスの指定を動的にしたい）。
ってお話です。

# 目的

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

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

https://qiita.com/Takowasa_Naan/items/050ec91a8f8527a85ad4

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

# コードの改善

## ベースとなるコードの提示
```csharp
// 取得したいプロパティをもつクラス
public class Person
{
    public string Name { get; set; }

    public int? Age { get; set; }
}
```
```csharp
// 「プロパティを呼ぶラムダ式」（式ツリー）を引数に指定するメソッドをもつクラス
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を用いた処理（例：プロパティに付与された属性の取得）*/

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

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

## 改善１：対象クラスを動的に指定する
ベースでは、型パラメータに静的に`Person`クラスを指定していたが、動的に指定できるようにした。

```csharp
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]);
    }
}
```

# 付録
## コード１：引数の型が`Expression<Func<T, object>>`だった場合

先述のコードそのままでは、エラーが出てしまう。
- メソッドに型パラメータがないためエラーが出る
- プロパティの戻り値の型から`object`型へ変換できないというエラーが出る

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

```csharp
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を用いた処理（例：プロパティに付与された属性の取得）*/

    }
}
```
```csharp
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`でも変換はできるよう。

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

# 関連

https://qiita.com/Takowasa_Naan/items/050ec91a8f8527a85ad4