C# で、メソッドに引数として「キーと値のペア」を渡す方法を考えてみます。
例えば HTTP で GET でアクセスするときに URL でクエリ文字列を指定する場合が挙げられます。
よく使われているのは、メソッドの引数に Dictionary や匿名型オブジェクトを渡す方法かと思います。
以下の HttpHelper クラスのように実装します。
なお、WebClient クラスではクエリ文字列 (QueryString プロパティ) は NameValueCollection 型であるため、
受け取った情報を NameValueCollection 型に変換しています。
| using System; | |
| using System.Collections.Generic; | |
| using System.Collections.Specialized; | |
| using System.ComponentModel; | |
| using System.Linq; | |
| using System.Net; | |
| using System.Text; | |
| namespace DynamicHttpConsole | |
| { | |
| public static class HttpHelper | |
| { | |
| public static string Get(string uri, object query) => | |
| query == null ? Get(uri) : | |
| IsPropertiesObject(query) ? Get(uri, ToDictionary(query)) : | |
| Get(uri, new { query }); | |
| static bool IsPropertiesObject(object o) | |
| { | |
| if (o == null) return false; | |
| var oType = o.GetType(); | |
| return oType.IsClass && oType != typeof(string); | |
| } | |
| static IDictionary<string, object> ToDictionary(object obj) => | |
| TypeDescriptor.GetProperties(obj) | |
| .Cast<PropertyDescriptor>() | |
| .ToDictionary(p => p.Name, p => p.GetValue(obj)); | |
| public static string Get(string uri, IDictionary<string, object> query = null) | |
| { | |
| using (var web = new WebClient { Encoding = Encoding.UTF8 }) | |
| { | |
| if (query != null) | |
| web.QueryString = query.ToNameValueCollection(); | |
| return web.DownloadString(uri); | |
| } | |
| } | |
| public static NameValueCollection ToNameValueCollection(this IDictionary<string, object> dictionary) | |
| { | |
| var collection = new NameValueCollection(); | |
| foreach (var item in dictionary) | |
| collection[item.Key] = item.Value?.ToString(); | |
| return collection; | |
| } | |
| } | |
| } |
| using System; | |
| using System.Collections.Generic; | |
| using System.Linq; | |
| namespace DynamicHttpConsole | |
| { | |
| class Program | |
| { | |
| // http://zip.cgis.biz/ | |
| const string Uri_Cgis_Xml = "http://zip.cgis.biz/xml/zip.php"; | |
| static void Main(string[] args) | |
| { | |
| // No query | |
| Console.WriteLine(HttpHelper.Get(Uri_Cgis_Xml)); | |
| // Dictionary | |
| Console.WriteLine(HttpHelper.Get(Uri_Cgis_Xml, new Dictionary<string, object> { { "zn", "6048301" } })); | |
| Console.WriteLine(HttpHelper.Get(Uri_Cgis_Xml, new Dictionary<string, object> { { "zn", "501" }, { "ver", 1 } })); | |
| // Anonymous type | |
| Console.WriteLine(HttpHelper.Get(Uri_Cgis_Xml, new { zn = "6050073" })); | |
| Console.WriteLine(HttpHelper.Get(Uri_Cgis_Xml, new { zn = "502", ver = 1 })); | |
| } | |
| } | |
| } |
なお、ここでは題材として CGI’s 郵便番号検索 API を利用しています。
さて、動的言語ランタイム (DLR) と名前付き引数を利用して、引数の情報を実行時に解決できないかと考えると、
次のような方法を思いつきます。
dynamic http = new DynamicHttpProxy();
var result = http.Get(Uri_Cgis_Xml, zn: "402", ver: 1);
実際、DynamicObject クラスを継承した DynamicHttpProxy クラスを次のように作れば可能です。
| using System; | |
| using System.Collections.Generic; | |
| using System.Dynamic; | |
| using System.Linq; | |
| namespace DynamicHttpConsole | |
| { | |
| public class DynamicHttpProxy : DynamicObject | |
| { | |
| public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) | |
| { | |
| if (binder.Name == "Get") | |
| { | |
| result = Get(binder.CallInfo.ArgumentNames, args); | |
| return true; | |
| } | |
| return base.TryInvokeMember(binder, args, out result); | |
| } | |
| static string Get(ICollection<string> argNames, object[] args) | |
| { | |
| var query = argNames | |
| .Zip(args.Skip(1), (n, v) => new { n, v }) | |
| .ToDictionary(_ => _.n, _ => _.v); | |
| return HttpHelper.Get((string)args[0], query); | |
| } | |
| public string Get(string uri, object query = null) => HttpHelper.Get(uri, query); | |
| } | |
| } |
| using System; | |
| using System.Collections.Generic; | |
| using System.Linq; | |
| namespace DynamicHttpConsole | |
| { | |
| class Program | |
| { | |
| // http://zip.cgis.biz/ | |
| const string Uri_Cgis_Xml = "http://zip.cgis.biz/xml/zip.php"; | |
| static void Main(string[] args) | |
| { | |
| dynamic http = new DynamicHttpProxy(); | |
| // No query | |
| Console.WriteLine(http.Get(Uri_Cgis_Xml)); | |
| // Named arguments | |
| Console.WriteLine(http.Get(Uri_Cgis_Xml, zn: "1510052")); | |
| Console.WriteLine(http.Get(Uri_Cgis_Xml, zn: "402", ver: 1)); | |
| } | |
| } | |
| } |
TryInvokeMember メソッドの中で、引数の名前は binder.CallInfo.ArgumentNames で取得できます。
ただし、引数の名前を指定せずに渡された分はここに含まれない (コレクションの長さが変わる) ため注意が必要です。
また、C# 7.0 で追加された ValueTuple を利用して、
var result = HttpHelper.Get(Uri_Cgis_Xml, (zn: "6050073"));
とする案もありましたが、
- 要素が 1 つ以下の場合、タプル リテラルを記述できない
- コンパイル後はフィールド名が残らないため、実行時に動的に取得できない
という制約により実現できませんでした。
作成したサンプル
DynamicHttpConsole (GitHub)
バージョン情報
C# 7.0
.NET Framework 4.5
