初めに
この記事で使用している.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
でも変換はできるよう。
最後に
実際に使う分にはもう少し最適化したほうがいいと思います。
関連