インターフェイスに対する透過プロキシ

前回の DLR で名前付き引数を使うでは、メソッドに引数として「キーと値のペア」を渡す方法を考え、
動的言語ランタイム (DLR) を利用する方法を紹介しました。
しかし、同じ処理を複数の場所で呼び出す必要があるなど、再利用性を重視する場合は、
なるべく静的にシグネチャを解決する方法がよいでしょう。

そこで今回は、呼び出すサービスをインターフェイスとして定義し、透過プロキシ (transparent proxy) を利用してみます。
先にコードを示します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Proxies;
namespace TransparentHttpConsole
{
public static class HttpProxy
{
public static IService CreateProxy<IService>()
{
return (IService)new HttpProxy<IService>().GetTransparentProxy();
}
}
public class HttpProxy<IService> : RealProxy
{
public string BaseUri { get; }
public HttpProxy() : base(typeof(IService))
{
var serviceType = typeof(IService);
if (!serviceType.IsInterface) throw new InvalidOperationException("IService must be an interface.");
var baseUriAttribute = serviceType.GetCustomAttribute<BaseUriAttribute>(true);
if (baseUriAttribute == null) throw new InvalidOperationException("IService must have a BaseUriAttribute.");
BaseUri = baseUriAttribute.BaseUri;
}
public override IMessage Invoke(IMessage msg)
{
var methodCall = (IMethodCallMessage)msg;
if (methodCall.MethodBase.DeclaringType == typeof(object))
{
var result =
methodCall.MethodName == "GetType" ? typeof(IService) :
methodCall.MethodBase.Invoke(this, methodCall.InArgs);
return CreateReturnMessage(result, methodCall);
}
var query = Enumerable.Range(0, methodCall.InArgCount)
.ToDictionary(methodCall.GetInArgName, methodCall.GetInArg);
switch (methodCall.MethodName)
{
case "Get":
return CreateReturnMessage(HttpHelper.Get(BaseUri, query), methodCall);
default:
throw new InvalidOperationException($"The method \"{methodCall.MethodName}\" is not available.");
}
}
static IMethodReturnMessage CreateReturnMessage(object returnValue, IMethodCallMessage methodCall)
{
return new ReturnMessage(returnValue, new object[0], 0, methodCall.LogicalCallContext, methodCall);
}
#region Methods of Object class
// GetType method can not be overridden.
public override int GetHashCode() => base.GetHashCode();
public override bool Equals(object obj) => ReferenceEquals(GetTransparentProxy(), obj);
public override string ToString() => typeof(IService).ToString();
#endregion
}
[AttributeUsage(AttributeTargets.Interface, Inherited = false, AllowMultiple = false)]
public sealed class BaseUriAttribute : Attribute
{
public string BaseUri { get; }
public BaseUriAttribute(string baseUri)
{
BaseUri = baseUri;
}
}
}
view raw HttpProxy.cs hosted with ❤ by GitHub
using System;
using System.Collections.Generic;
using System.Linq;
namespace TransparentHttpConsole
{
class Program
{
static void Main(string[] args)
{
var cgis = HttpProxy.CreateProxy<ICgisService>();
Console.WriteLine(cgis.Get("6048301"));
Console.WriteLine(cgis.Get("501", 1));
}
}
// http://zip.cgis.biz/
[BaseUri("http://zip.cgis.biz/xml/zip.php")]
public interface ICgisService
{
string Get(string zn);
string Get(string zn, int ver);
}
}
view raw Program.cs hosted with ❤ by GitHub

このコードでは、RealProxy クラスを継承した HttpProxy<IService> クラスを作成しています。
RealProxy はプロキシの実体となるものであり、
その外層である透過プロキシを RealProxy.GetTransparentProxy メソッドで取得できます。

一方、利用する側の Program.cs では、ICgisService インターフェイスを定義しておきます。
その透過プロキシを生成すれば、ICgisService インターフェイスを実装するクラスが存在しなくてもメソッドを呼び出すことができ、
実体は HttpProxy<IService> クラスの Invoke メソッドとなります。

また、BaseUriAttribute クラスを定義しており、
呼び出される Web API のベースとなる URI を属性で指定できるようにしています。

WCF における契約プログラミングでは、クライアント側とサーバー側で同一のインターフェイスを利用し、
クライアント側からのアクセスを上記のように透過プロキシで実装する方法があります。
方法 : ChannelFactory を使用する にある通り、ChannelFactory.CreateChannel メソッドで透過プロキシを生成します。

前回:DLR で名前付き引数を使う
次回:透過プロキシでアスペクト指向プログラミング (1)

作成したサンプル
TransparentHttpConsole (GitHub)

バージョン情報
C# 7.0
.NET Framework 4.5

参照
RealProxy クラス
方法 : ChannelFactory を使用する

DLR で名前付き引数を使う

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;
}
}
}
view raw HttpHelper.cs hosted with ❤ by GitHub
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 }));
}
}
}
view raw Program.cs hosted with ❤ by GitHub

なお、ここでは題材として 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);
}
}
view raw DynamicHttpProxy.cs hosted with ❤ by GitHub
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));
}
}
}
view raw Program.cs hosted with ❤ by GitHub

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

参照
動的言語ランタイムの概要
名前付き引数と省略可能な引数
タプル – C# によるプログラミング入門

スティック PC をリモートで操作できるようにする

マウスコンピューターのスティック PC「MS-NH1-W10」を購入直後にセットアップしたときのメモです。
他の PC からリモートで接続できるように設定するところまでです。

スティックPC MS-NH1-W10
スティックPC MS-NH1-W10

搭載されている OS は Windows 10 Home です。
初回は USB 接続のキーボードまたはマウスが必要になります。ここではマウスのみで操作してみます。

 

OS 初期設定まで

初めて電源をオンにすると、OS の初期設定が始まります。

  • スティック PC 本体を電源、マウス、ディスプレイと接続する
  • 電源をオンにする
  • ネットワーク設定はスキップでよい
  • ユーザー名を入力する
    • 以降はパスワードの入力なしで OS を起動させるため、パスワードは設定しない
    • 入力にはスクリーン キーボードを利用

 

OS 起動後

OS が起動したら、まずは次のようなスクリーン キーボードを使えるようにします。

スクリーン キーボード

スクリーン キーボードを起動するには、[設定] → [簡単操作] → [キーボード] の画面で設定するか、
スタート メニューの [Windows 簡単操作] から選択します。

1-Keyboard-Settings

1-Keyboard-Start

 

スクリーン キーボードを使えるようになったら、コンピューター名 (PC 名) を変更して再起動します。

2-ComputerName

 

リモート接続

Windows 10 Home では、OS 標準のリモート デスクトップ接続を利用できません。
そこで、TeamViewer などを利用してリモート接続できるようにします。

  • ネットワークに接続 (ワイヤレス LAN)
  • TeamViewer を検索してダウンロード
  • TeamViewer をインストールしてリモート接続設定

以上の設定ができれば、このスティック PC に他の PC からリモート接続できるようになり、
これ以降、USB 接続のマウスは不要となります。

Leap Motion v1 が自動でアップグレードされないようにする

Leap Motion Controller のソフトウェアは、v2 よりも v1 のほうがトラッキングの精度自体は高いため、
v1 を利用したいケースも依然としてあります。

Leap Motion v1 の実行環境をセットアップするには、
Leap Motion のセットアップで書いた通りインストーラーを実行したあと、
コントロール パネルを開いて [最新版を自動的にインストール] のチェックをオフに設定します。

Leap-v1-ControlPanel

この設定変更をしないと、しばらく経つと自動的に v2 のインストーラーをダウンロードしてアップグレードしてしまいます。

問題はここからです。
v2 にアップグレードしてしまったとしても「v2 アンインストール& v1 再インストール」でやり直せると思いきや、
なんと v1 を再インストールした直後に間髪入れずに v2 にアップグレードされてしまいます。

これはどうやら、最初に v2 にアップグレードしたときに使用したインストーラーが
    C:\Windows\Temp\leap
に残っていることが原因のようです。

Leap 2.3.1 Installer

このインストーラーを削除してから v1 のインストーラーを再実行すれば解決します。

// 他にも、ユーザー フォルダーの「AppData\Local\Temp\~nsu.tmp」フォルダーに .exe ファイルが残ってたりしますが、
// こちらは直接影響しないようです。

 

2018.02.03 追記:
Windows 10 Fall Creators Update が適用されてから、Leap Motion Controller を USB で接続しても認識されなくなりました。
既定のドライバーでは動作しないようです。
Leap Motion のインストール フォルダーにある次のドライバーを追加でインストールすることで解決しました (64 ビットの場合)。
    C:\Program Files (x86)\Leap Motion\Core Services\Drivers\dpinst64.exe

 

参照
Leap Motion のセットアップ
Leap Motion driver broken on Windows Fall Creators Update

カテゴリー: 周辺機器. タグ: , . 1 Comment »