今回は、前回のジェネリクスの続きなのですが、自分がある師匠が書いてくれたプログラムを理解するためのポストです。具体的には C# のリフレクションを使ったアプリを作ります。
今回のお題は、リモート(本当は Azure KeyVault) にあるシークレットを取ってきて特定のクラスに格納するというものです。ただし、格納する先のクラスは、どんな属性があるかはまちまちです。こういう時は、クラスのメソッドの一覧をとってきたり、文字列から、メソッドを実行したり、クラスのインスタンスを生成できたりするリフレクションが有効です。
最初のクラス
リモートから、アクセスする先のデータは、今は Dictionary で表現しておきます。そして、それを Foo クラスに詰めるための、コードはこんな感じ。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ReflectionSample
{
class Foo
{
public string Id { get; set; }
public string Name { get; set; }
}
class Bar
{
public string ApplicationId { get; set; }
public string ApplicationSecret { get; set; }
}
class SomeHelper
{
private Dictionary<string, string> dict;
public SomeHelper()
{
dict = new Dictionary<string, string>();
dict["Id"] = "ABCDE";
dict["Name"] = "Yamada";
dict["ApplicationId"] = "ABC-DEF-GHI";
dict["ApplicationSecret"] = "P@ssw0rd";
}
public T GetSecret<T>() where T: new()
{
var properties = typeof(T).GetProperties();
var result = new T();
foreach (var p in properties)
{
p.SetValue(result, dict[p.Name]);
}
return result;
}
}
public class Program
{
static void Main(string[] args)
{
var helper = new SomeHelper();
var foo = helper.GetSecret<Foo>(); // 型情報から、データを取得して、値を詰めて返す。
Console.WriteLine($"Foo: Id:{foo.Id}, Name:{foo.Name}");
Console.ReadLine();
}
}
}
これは師匠の考えてくれたインターフェイスです。
var foo = helper.GetSecret<Foo>();
こんな感じで型を指定したら、型の情報(メソッド名)を元に、それに該当する値を詰めてくれるというメソッドです。凄くカッコいいしシンプルですね。
ポイントは、
public T GetSecret<T>() where T: new()
{
var properties = typeof(T).GetProperties();
var result = new T();
foreach (var p in properties)
{
p.SetValue(result, dict[p.Name]);
}
return result;
}
where T: new()
最初に、前回やった、ジェネリクスで、型を指定できるようにしています。where で、Tに指定できる型は、引数なしのコンストラクタを持ったものに決定されています。
typeof(T).GetProperties();
まず、T の型情報を取ってきて、そのGetProperties を取得して、プロパティの一覧をとってきています。インテリセンスに聞いてみると、メソッドやコンストラクタなどクラスのメタ情報がいろいろ取れます。
new T();
通常、ジェネリクスの型はインスタンス化したり、メソッドを読んだりできませんが、where を書いておけば一部可能です。ここでは、引数なしのコンストラクタがあることを、T の条件にしています。
SetValue メソッド
ハイライトですが、SetValue メソッドで、オブジェクトに値をセットしていきます。値を代入したい先のオブジェクトのメソッドではなく、System.Reflection.Propertyinfo のメソッドであることがポイントです。該当のメソッドのメタ情報を表すインスタンスのSetValue で、値をセットしたい先のインスタンス, 設定する値 をセットしています。
Foo: Id:ABCDE, Name:Yamada
実行結果は予想通りうまくいっています。
問題の発覚
ただ、これだと、問題があります。例えば、Foo に、本当はデータの移送に関係ない型があったらどうなるでしょう? こんな感じで違う型のメソッドを書くと、コンパイル時はOKですが実行時にエラーになります。
class Foo
{
public string Id { get; set; }
public string Name { get; set; }
public int Something { get; set; }
}
実行結果
エラーになります。このコードのは、ディクショナリにそんなものはないからです。たとえあっても、型が違えばエラーになります。そんな時は、何らかのロジックで、Property を書くときに、制限してあげればいいことになります。
Propertyをフィルタする
師匠が書いてくれたフィルタのコードです。プロパティを、インスタンスタイプで、Public のもの。読み書きができるもので、string の型に限定しています。
var properties = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(x => x.CanRead && x.CanWrite)
.Where(x => x.PropertyType == typeof(string));
こう変えれば、先ほどのメソッドは、int なのでフィルタにかかり、不要なメソッドが実行されませんので、無事うまく実行されます。
Foo: Id: ABCDE Name: Yamada Something: 0
ともかく、師匠の書いたコードは理解できるようになったので、本番コードにこれから挑んでみます。実際のものは、Async の呼び出しがあるので、Async のコードなので、おそらく、直列で await してしまう問題が出ると思うのでそのあたり対処してがんばってみます。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Reflection;
namespace ReflectionSample
{
class Foo
{
public string Id { get; set; }
public string Name { get; set; }
public int Something { get; set; }
public string ToString()
{
return $"Foo: Id: {Id} Name: {Name} Something: {Something}";
}
}
class Bar
{
public string ApplicationId { get; set; }
public string ApplicationSecret { get; set; }
}
class SomeHelper
{
private Dictionary<string, string> dict;
public SomeHelper()
{
dict = new Dictionary<string, string>();
dict["Id"] = "ABCDE";
dict["Name"] = "Yamada";
dict["ApplicationId"] = "ABC-DEF-GHI";
dict["ApplicationSecret"] = "P@ssw0rd";
}
public T GetSecret<T>() where T: new()
{
var properties = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(x => x.CanRead && x.CanWrite)
.Where(x => x.PropertyType == typeof(string));
var result = new T();
foreach (var p in properties)
{
p.SetValue(result, dict[p.Name]);
}
return result;
}
}
public class Program
{
static void Main(string[] args)
{
var helper = new SomeHelper();
var foo = helper.GetSecret<Foo>();
Console.WriteLine(foo.ToString());
Console.ReadLine();
}
}
}