読者です 読者をやめる 読者になる 読者になる

xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

DynamicSignalR

One ASP.NET Advent Calendar 2013、12日目を担当させて頂きます@xin9leです。「1000年に1度のアイドル」と言われて人気を博した橋本環奈ちゃんじゃないですが、「北陸のイケメンMVP」とか嘘八百言われつつの2回目の登場です。今回も前回同様SignalRネタですが、これはSignalRを触り始めた頃についカッとなってやったボツ案です。作る前は「こんなのドヤァ!」と意気込んでいたけれど、いざ作ってみたらイマイチ感どころかイマハチ感が拭い去れなかったというお決まりのパターン...。日の目を見ないで死んで行くのも可哀想 (謎) なので、ここら辺で登場して頂こうではありませんか。

文字列ベースでのメソッド呼び出し

SignalRではサーバー側/クライアント側とのやり取りをするに当たり、RPC (Remote Procedure Call) スタイルを採用しています。つまり、サーバー側/クライアント側それぞれがお互いのメソッド名を指定して直接呼び出すような形を取っているという事です。例えば次のコードはその一例です。

public class MyHub : Hub
{
    public void Send(string message)
    {
        //--- Allプロパティの戻り値はdynamic型
        this.Clients.All.Receive(message);
    }
}
var url          = "http://localhost:12345/";
this.Connection  = new HubConnection(url);
this.MyHub       = this.Connection.CreateHubProxy("MyHub");
await this.Connection.Start();

//--- イベントを文字列で指定
this.MyHub.On<string>("Receive", Console.WriteLine);

//--- サーバー側のメソッドを文字列で指定して呼び出し
await this.MyHub.Invoke("Send", DateTime.Now.ToString());

これをよく見てみると、サーバー側がクライアント側のメソッドを呼び出すときはdynamicを利用した記述をしているにも関わらず、クライアント側はすべて文字列ベースで記述しています。この差が気に入らない。文字列ベースで記述するくらいなら、dynamicで書いたって大差ない。ASP.NET MVCでもViewDataViewBagになったりしたじゃないか。などと考え、クライアント側もdynamicベースにしてみました。

動的解決をするHubProxyのラッパーの作成

HubProxyのラッパーの作成にあたり、Dispose処理を外部からデリゲートで指定するヘルパークラスを作成しておきます。

internal class AnonymousDisposable : IDisposable
{
    private readonly Action onDispose = null;

    public AnonymousDisposable(Action onDispose)
    {
        this.onDispose = onDispose;
    }

    public void Dispose()
    {
        if (this.onDispose != null)
            this.onDispose();
    }
}

次に、動的なメソッド呼び出しとイベント登録ができるようにしたHubProxyをラッパークラスを作ります。SignalRのイベントはUIスレッドではないところで実行されるので、簡単にUIスレッドで動かせるように同期コンテキストを指定できるようにしておきました。

internal class DynamicHubProxy : DynamicObject
{
    private readonly IHubProxy proxy = null;
    private readonly SynchronizationContext context = null;
    private readonly IDictionary<string, IDisposable> eventDisposers = null;

    //--- 操作対象となるIHubProxyと、イベントをUIスレッドで実行させるための同期コンテキストの指定
    public DynamicHubProxy(IHubProxy proxy, SynchronizationContext context = null)
    {
        this.proxy          = proxy;
        this.context        = context;
        this.eventDisposers = new Dictionary<string, IDisposable>();
    }

    #region DynamicObject override methods
    //--- サーバー側のメソッド呼び出し
    public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
    {
        result = this.proxy.Invoke(binder.Name, args);
        return true;
    }

    //--- イベントとして受けるデリゲートの設定
    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        //--- 登録済みのハンドラを解除
        if (this.eventDisposers.ContainsKey(binder.Name))
        {
            this.eventDisposers[binder.Name].Dispose();
            this.eventDisposers.Remove(binder.Name);
        }

        //--- コールバックが指定されていなければ終了
        var callback = value as Delegate;
        if (callback == null)
            return true;
        
        //--- イベントハンドラ
        Action<IList<JToken>> handler = tokens =>
        {
            var args = callback.Method.GetParameters()
                     .Select((x, i) => new { Token = tokens[i], Type = x.ParameterType })
                     .Select(x => x.Token == null ? null : x.Token.ToObject(x.Type))
                     .ToArray();
            if (this.context == null)
            {
                callback.DynamicInvoke(args);
                return;
            }
            this.context.Post(_ =>
            {
                callback.DynamicInvoke(args);
            }, null);
        };

        //--- 購読 & 解除方法の記憶
        var subscription      = this.proxy.Subscribe(binder.Name);
        var disposer          = new AnonymousDisposable(() => subscription.Received -= handler);
        subscription.Received += handler;
        this.eventDisposers.Add(binder.Name, disposer);

        //--- ok
        return true;
    }
    #endregion
}

最後に、このinternalなDynamicHubProxyを生成するための拡張メソッドを定義します。これで準備は整いました。

public static class HubConnectionExtensions
{
    public static dynamic CreateDynamicHubProxy(this HubConnection connection, string hubName, bool capturesSynchronizationContext = false)
    {
        var context = capturesSynchronizationContext ? SynchronizationContext.Current : null;
        return connection.CreateDynamicHubProxy(hubName, context);
    }

    public static dynamic CreateDynamicHubProxy(this HubConnection connection, string hubName, SynchronizationContext context = null)
    {
        var proxy = connection.CreateHubProxy(hubName);
        return new DynamicHubProxy(proxy, context);
    }
}

How to use

早速作成したDynamicHubProxyを利用した形に書き換えてみます。強調表示した行が冒頭の記述から変更されている箇所です。文字列指定していた箇所がすべて消えましたね!よかった×2。

var url         = "http://localhost:12345/";
this.Connection = new HubConnection(url);
this.MyHub      = this.Connection.CreateDynamicHubProxy("MyHub", true);  //--- true : 同期コンテキストをキャプチャする
await this.Connection.Start();

//--- イベントはプロパティにデリゲートを設定する形で対応
this.MyHub.Receive = new Action<string>(Console.WriteLine);

//--- サーバー側のメソッド呼び出し
await this.MyHub.Send(DateTime.Now.ToString());

ここまでやってみて

ここまで苦労してみましたが、変わったところは基本的に文字列によるメソッド名指定が消えただけです。同期コンテキストのキャプチャ機能も軽いオマケ程度の存在にしか見えず、dynamicなので当然インテリセンスも効きません。加えて、DynamicHubProxyはただの機能制限版ラッパーなのでIHubProxyの機能をすべて利用できません。もはや「文字列消せたよね、うん。で?」な感じで全然嬉しくない。...ということで、やっぱり文字列指定でも全然イイんじゃないかと思えてしまい、結局ボツ案と相成りました。

この案はポシャったワケですが、これよりもマシな解法は見つけていて業務ではそちらを採用しました。それについては12月23日の担当になっているC# Advent Calendar 2013でご紹介できればと思っています。とは言え、消すのも勿体ないので今回のソースはGitHubにアップロードしておきました。

DynamicSignalR

ということで、今回はここまで!次回は@cerberus1974さんです。よろしくお願いします!