20190709のC#に関する記事は6件です。

ブラウザ上で動作する.NETデコンパイラ

少し前にブラウザ上で動作する.NETデコンパイラ Kani を作成したのでその紹介です。
ブラウザさえあれば簡単に試すことができます。

Kani.gif

PWAに対応しているのでメニューからインストールすることでローカルで動作させることも可能です。

ソース:yaegaki/Kani

解説

KaniではBlazordnSpyを内部で使用しています。
これらを使用することで簡単にブラウザ上で動作する.NETデコンパイラを作成することができました。

Blazorについて

Blazorはブラウザ上で.NETアプリを動作するためのフレームワークです。
以下の記事がqiitaでも人気になっていたので知っている人も多いのではないでしょうか。

C# で Single Page Web Application が書ける Blazor が凄かった件

Blazorの完成度は結構高く特に意識することなくC#で書いたコードが動作しました。

dnSpyについて

dnSpyはWindows向けの.NETデコンパイラでとにかく完成度が高いです。

凄すぎて大草原不可避な.NET デコンパイラdnSpyを使ってみる

dnSpy自体はWindows向けのアプリケーションですがすべてがC#で書かれています。
なのでGUIなどのWindowsに依存する部分を除けばBlazorで動かすことができるようになります。
幸いなことにdnSpyは機能ごとにプロジェクトが分かれており簡単に再利用できました。

苦労したポイント

2019/7/9時点での情報です

Blazorアプリのビルドに失敗する

Windows上でBlazorアプリをビルドすると特におかしいところがなくてもエラーになる場合がありました。
エラーログは以下のようなものでした。

  Unhandled Exception: Mono.Cecil.AssemblyResolutionException: Failed to resolve assembly: 'C:\Users\xx\.nuget\packaes\system.threading.tasks.parallel\4.3.0\lib\netstandard1.3\System.Threading.Tasks.Parallel.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' ---> Mono.Cecil.AssemblyResolutionException: Failed to resolve assembly: 'C:\Users\xx\.nuget\packaes\system.threading.tasks.parallel\4.3.0\lib\netstandard1.3\System.Threading.Tasks.Parallel.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null'
     at Mono.Linker.DirectoryAssemblyResolver.Resolve(AssemblyNameReference name, ReaderParameters parameters)
     at Mono.Linker.AssemblyResolver.Resolve(AssemblyNameReference name, ReaderParameters parameters)
     at Mono.Linker.LinkContext.Resolve(IMetadataScope scope)
     at Mono.Linker.LinkContext.Resolve(IMetadataScope scope)
     at Mono.Linker.LinkContext.Resolve(String name)
     at Mono.Linker.Steps.ResolveFromAssemblyStep.Process()
     at Mono.Linker.Steps.BaseStep.Process(LinkContext context)
     at Mono.Linker.Pipeline.ProcessStep(LinkContext context, IStep step)
     at Mono.Linker.Pipeline.Process(LinkContext context)
     at Mono.Linker.Driver.Run(ILogger customLogger)
     at Mono.Linker.Driver.Execute(String[] args, ILogger customLogger)
     at Mono.Linker.Driver.Main(String[] args)

内容的にはアセンブリの解決ができなかったというものですがパスをよく見たら何かおかしいです。
本来ならばnugetのパッケージのパスはC:\Users\xx\.nuget\packages\...というものですがC:\Users\xx\.nuget\packaes\...となっています。
(packagespackaesになっている)
詳細はわかりませんがおそらく引数が長くなりすぎて限界を超えてしまったため、一部の情報が欠落してしまったのだと思います。

コマンド プロンプト (Cmd.exe) へ 8192 文字以上の引数を渡した場合に発生する現象

根本的にはどうやって解決するべきかわからなかったため参照するアセンブリの数を減らすという対策をとりました。
dnSpyは多言語に対応しておりリソースファイルが多かったのでその数を減らしました。

https://github.com/yaegaki/dnSpy/commit/d2fcbcdf96cfad23f8e093caffe63560490a7535

ファイルのドラッグドロップについて

Kaniは.NETアセンブリをドラッグドロップすることでデコンパイルを行います。
このドラッグドロップを実装するために少し苦労しました。
ブラウザ上にファイルをドラッグドロップしてそのバイナリを得るためにはdragoverdropで発生するイベントについてpreventDefaultを呼び出す必要があります。
Blazorのバインド機能でイベントを設定した場合、どうやってもpreventDefaultを呼ぶことができずページ遷移が発生してしまいました。
これを回避するためにはjsで処理を書く必要があり、C#だけで完結できずに少し残念でした。

また、jsからUint8ArrayをC#に渡す方法がよくわからず最終的にはblobにしてblobURLを発行しC#側でHTTP.GETを行うという回りくどい方法をとりました。
これについてはもう少しまともな方法があるかもしれません。

最後に

BlazordnSpyが凄すぎて感動しました。:sparkles:
特にBlazorは色々可能性を感じました。:zap:
適当なC#アプリをブラウザに移植してみるのも楽しいと思いますのでぜひやってみてください。:thumbsup:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[C#]正規表現で、ルールに一致した文字列かどうか調べる/一致した文字列を抜き出す

やりたいこと

特定のルールに合致した文字列であるかどうかを確認したい。
また、ルールに合致した文字列を抜き出したい。

やり方

Regexクラスを使う。

サンプルコード

using System;
using System.Text.RegularExpressions;

namespace ConsoleApp5
{
    class Program
    {
        static void Main(string[] args)
        {
            string org = "1dg4-fasd-kf5g-fsaa";

            // ①4桁の英数字を-でつなげたものかどうかを判定
            if (Regex.IsMatch(org, @"^[\da-zA-Z]{4}-[\da-zA-Z]{4}-[\da-zA-Z]{4}-[\da-zA-Z]{4}\z"))
            {
                Console.WriteLine("マッチしました!");
            }

            // ②4桁英数字にマッチしたものを探す
            var match = Regex.Match(org, @"[\da-zA-Z]{4}");
            Console.WriteLine("1個だけ探す:" + match.Value);

            // ③4桁英数字にマッチしたものを全部探す
            var matches = Regex.Matches(org, @"[\da-zA-Z]{4}");
            foreach (Match m in matches)
            {
                Console.WriteLine("全部探す:" + m.Value);
            }
        }
    }
}

こうなる
image.png

参照

.NET の正規表現
https://docs.microsoft.com/ja-jp/dotnet/standard/base-types/regular-expressions

【C#入門】正規表現の使い方総まとめ(Match/Matches/Replace)
https://www.sejuku.net/blog/54508

【C#】正規表現の使い方
https://qiita.com/github129/items/5a2cd4d36abdf4ebe870

サルにもわかる正規表現
https://www.mnet.ne.jp/~nakama/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[C#]StopWatchで処理にかかる時間を測る

やりたいこと

時間がかかってそうな処理があるときに、その処理にどれくらいの時間がかかっているかを調べたい。

やり方

Stopwatchクラスを使う。

サンプルコード

測りたい処理を挟んで、Stopwatchをスタート~ストップして、時間を測る。

using System.Diagnostics;
using System.Threading;

namespace ConsoleApp5
{
    class Program
    {
        static void Main(string[] args)
        {
            // ストップウォッチを作成
            Stopwatch sw = new Stopwatch();

            // ストップウォッチをスタート
            sw.Restart();

            // 測りたい処理
            Thread.Sleep(5000);

            // ストップウォッチを止める
            sw.Stop();

            // 経過時間をsw.Elapsed や sw.ElapsedMilliseconds で取得
            Debug.WriteLine(" 経過時間:" + sw.ElapsedMilliseconds + " ms");
        }
    }
}

メモ

個人的に、スタートするときはRestart()を使う。
Start() → Stop() → Start() → Stop()とすると、一回目の時間をそのまま引き継いでしまうので、それに気づかないと間違った計測結果を採ってしまうので。

参考

Stopwatch Class
https://docs.microsoft.com/ja-jp/dotnet/api/system.diagnostics.stopwatch?view=netframework-4.8

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ASP .NET MVC でモデルの自動バリデーションを無効化する

コントローラに入る前にバリデーションしていただくのは非常に有用で嬉しいけれども。
APIを作るにあたって、レスポンスを揃えられないな、と悩んだ。

結論

Startup.csの以下を編集する

ここを

services.AddMvc()
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

こう

services.AddMvc()
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
        .ConfigureApiBehaviorOptions(options => { options.SuppressModelStateInvalidFilter = true; });

参考

https://docs.microsoft.com/ja-jp/aspnet/core/web-api/index?view=aspnetcore-2.2#automatic-http-400-responses

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

64bitDLLと32bitDLLの共存をWCFサービスで何とかする

64bitDLLと32bitDLLを共存させたい

提供されたDLLが方やx64、方やx86指定でないと使えないものを1つのアプリケーションで使う必要にかられました。

今まで自分ではアーキテクチャ依存のコードはほとんど書いたことがないため、意外とこういった状況は経験がありませんでした。

とりあえず一つのプロセスではx64とx86は共存できません。

さてどうしたものか。

プロセス間通信の実現方法

プロセス間通信の手段として思いつくのは、

  • 昔ながらのメモリ共有
  • コンソールアプリを別プロセスで起動して標準出力で通信
  • ローカルに対してソケット通信

この辺は割とすぐに思いつくものの、お互いがお互いを知らないといけないと言うなるべく避けたい構成。

.Netリモーティングなるものもあるらしい
C# プロセス間通信(IPC)

割と新しい記事ではWCFを勧められている記事が多め?
そもそも記事数が少ないのでイバラの道の予感しかありませんが。

色々と大変だったので備忘録。

とりあえず作ってみたら失敗

WCFサービスライブラリでx86DLLをラップしたサービスライブラリを作ってみる、
x64でサービス参照を追加。
WCFサービスをx86指定でビルドして通信!と思ったらデバッグ開始時点で落ちます。
どうやらWCFサービスのホストが64bitマシンでは標準でx64が選択されるようでそこで落ちてしまう様です。

指定するのにVSコマンドプロンプトでコマンド打って・・・みたいなのもありましたが、アプリ配布時に面倒な環境設定が必要になるかも?な予感。
x86DLLを本体側でx64DLLをWCFと言う手段もなくはないですが、アプリ内での使用頻度と遅延を思うと、x64DLLを本体側に配置したい。

ということで勉強がてらホスト部分を自作してみることにしました。

WCFサービスで通信するための概要

主に以下のプロジェクトを用意しました。

  • x86DLLの模擬DLL
  • WCFサービスのコントラクト(インターフェースだけを持つ)ライブラリ
  • WCFサービスをセルフホストしてx86DLLをラップするアプリ
  • x64DLLを使うWPFアプリ

WPFアプリとWCFサービスアプリでお互いにインターフェースを参照しておくことでデータの型などが制限できます。

注意点として

  • 返り値を持つメソッドはref やoutが使えない場合がある様です。
  • 同じ名前のメソッドは複数定義できません。(引数が変わっててもだめ)

x86DLLの模擬プロジェクト

無くてもいいですが目的の形に近づけるためにx86DLLを作りました。

Class1.cs
namespace ClassLibrary1
{
    public class Class1
    {
        public int IncrementMethod(int value)
        {
            return value + 1;
        }
    }
}

WCFサービスのコントラクト

テンプレートは「空のプロジェクト」を選びました。
出力タイプはクラスライブラリに設定。

System.ServiceModelを参照に追加。
今回は適当な文字列を返すメソッドのを定義してみます。

Interface1.cs
using System.ServiceModel;

namespace HelloWorldServiceContract
{
    [ServiceContract(Namespace = "http://HelloWorldServiceContract")]
    public interface IHelloWorldService
    {
        [OperationContract]
        string SayHello(string name);
    }
}


WCFサービスをセルフホストするコンソールアプリ

テンプレートは「コンソールアプリ」を選択。
System.ServiceModelを参照に追加。
WCFサービスのコントラクトプロジェクトを参照に追加。

実験のためにx86指定のDLLを参照。
プロジェクト自体もx86指定に。

サービス実装クラスを記述。

HelloWorldService.cs
using System;
using HelloWorldServiceContract;

namespace ConsoleApp1
{
    public class HelloWorldService : IHelloWorldService
    {
        public string SayHello(string name)
        {
            var test = new ClassLibrary1.Class1();
            var nowsecond = DateTime.Now.Second;
            return string.Format("Hello, {0} {1} {2}", name, nowsecond, test.IncrementMethod(nowsecond));
        }
    }
}


コンソールアプリのProgram.Mainでサービスをホストする。

Program.cs
using System;
using System.ServiceModel;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {

            using (ServiceHost host = new ServiceHost(typeof(HelloWorldService)))//, baseAddress))
            {
                // Enable metadata publishing.
                //MEMO:App.configに記載

                host.Open();

                Console.WriteLine("The service is ready at {0}");//, baseAddress);
                Console.WriteLine("Press <Enter> to stop the service.");
                Console.ReadLine();

                // Close the ServiceHost.
                host.Close();
            }
        }
    }
}

App.configでサービスの設定を追記
使いたいDLLは古-------いものだったので、地味にuseLegacyV2RuntimeActivationPolicy="trur"が無いと動かなったです。
多分今時X86DLLを使わなければならない様なものは同じ躓きがあるかも知れません。

App.config
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <services>
      <!-- サービスとして動作させるクラスをname属性にする -->
      <service name="ConsoleApp1.HelloWorldService"
               behaviorConfiguration ="metadataAndDebugEnabled">
        <host>
          <baseAddresses>
            <!-- 接続先ベースアドレス -->
            <add baseAddress="net.tcp://localhost:8808/service"/>
          </baseAddresses>
        </host>

        <!-- 接続先および接続方法の設定 -->
        <!-- addressに何もいれていないのでベースアドレスと同一となる -->
        <endpoint address="" binding="netTcpBinding" contract="HelloWorldServiceContract.IHelloWorldService" />

      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="metadataAndDebugEnabled">
          <serviceDebug
            includeExceptionDetailInFaults="true" 
          />
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>

  <startup  useLegacyV2RuntimeActivationPolicy="true">
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
  </startup>
</configuration>

サービス起動時に管理者権限が必要でした・・・。
新しい項目の追加でapp.manifestを追加しコメントに従って変更します。

Httpバインディングでは管理者権限が必要ですが、Net.TCPバインディングなら管理者権限は不要でした。

WPFアプリ

とりあえず空のWPFを作りました。
本題ではないのでMainWindow.xaml.csにWCFサービスのプロセス管理とサービスへのアクセスを記述しました。

Loadedイベントで呼び出している部分でサービスのメソッドを呼んでます。

MainWindow.xaml.cs
using HelloWorldServiceContract;

namespace WCFHostWPFAppTest
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            var serviceProcess = Process.Start("ConsoleApp1.exe");

            var address = new EndpointAddress("net.tcp://localhost:8808/service");
            var binding = new NetTcpBinding();
            var factory = new ChannelFactory<IHelloWorldService>(binding, address);
            var channel = factory.CreateChannel();

            this.Loaded += (_, __) =>
            {
                // サービス呼び出し
                var IHelloWorldService = channel.SayHello("test");
                Console.WriteLine("SayHello() = {0}", IHelloWorldService);

            };

            this.Closing += (_, __) =>
            {

                factory.Close();
                serviceProcess.Kill();
            };
        }
    }
}

起動したらコンソールに無事文字が出ました。

出力
SayHello() = Hello, test 14 15

これで何とかなりました。

WCFHostWPFAppTest

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MagicOnion のストリーミング接続でデバッグ出力する

はじめに

MagicOnion についてはこちらをご覧ください。
非ストリーミング通信での共通処理の挟み方は以前の記事で紹介しました。
ストリーミング通信でも同じく gRPC Interceptor を用いて処理を挟むことができるのですが、
MagicOnion 特有のフォーマットで送受信されているためそれを理解する必要があります。

実装

方針

Grpc.Core.Interceptors.Interceptor クラスを継承し、必要なメソッドを override します。
今回必要になるのは AsyncDuplexStreamingCall のみです。

StreamingLoggingInterceptor
public class StreamingLoggingInterceptor : Interceptor
{
    public override AsyncDuplexStreamingCall<TRequest, TResponse> AsyncDuplexStreamingCall<TRequest, TResponse>(
        ClientInterceptorContext<TRequest, TResponse> context,
        AsyncDuplexStreamingCallContinuation<TRequest, TResponse> continuation
    )
    {
        // とりあえずそのまま返す
        return continuation(context);
    }
}

AsyncDuplexStreamingCall<TRequest, TResponse> の定義は以下のとおりです。

public sealed class AsyncDuplexStreamingCall<TRequest, TResponse> : IDisposable
{
    public IAsyncStreamReader<TResponse> ResponseStream { get; }
    public IClientStreamWriter<TRequest> RequestStream { get; }
    public Task<Metadata> ResponseHeadersAsync { get; }
    public void Dispose();
    public Status GetStatus();
    public Metadata GetTrailers();
}

入出力はそれぞれ RequestStream, ResponseStream を通して行われているようです。
そのためこいつらを自前実装したクラスに差し替えてやれば入出力に対して処理を挟むことができるようになります。

RequestStream, ResponseStream の実装

それぞれ、生データ(byte[])に対する処理と元のStreamを渡す構成にしておきます。

MyRequestStream
class MyRequestStream<T> : IClientStreamWriter<T>
{
    private Action<byte[]> _interceptor;
    private IClientStreamWriter<T> _baseStream;

    public MyRequestStream(Action<byte[]> interceptor, IClientStreamWriter<T> baseStream)
        => (_interceptor, _baseStream) = (interceptor, baseStream);

    // キモの部分
    public Task WriteAsync(T message)
    {
        // 書き込む前にintercept
        _interceptor(message as byte[]);
        return _baseStream.WriteAsync(message);
    }

    // 以下、インターフェースを満たすための実装
    public Task CompleteAsync() => _baseStream.CompleteAsync();
    public WriteOptions WriteOptions
    {
        get => _baseStream.WriteOptions;
        set => _baseStream.WriteOptions = value;
    }
}
MyResponseStream
class MyResponseStream<T> : IAsyncStreamReader<T>
{
    private Action<byte[]> _interceptor;
    private IAsyncStreamReader<T> _baseStream;

    public MyResponseStream(Action<byte[]> interceptor, IAsyncStreamReader<T> baseStream)
        => (_interceptor, _baseStream) = (interceptor, baseStream);

    // キモの部分
    public T Current
    {
        get
        {
            // 読み取る前にintercept
            _interceptor(_baseStream.Current as byte[]);
            return _baseStream.Current;
        }
    }

    // 以下、インターフェースを満たすための実装
    public Task<bool> MoveNext(CancellationToken cancellationToken) => _baseStream.MoveNext(cancellationToken);
    public void Dispose() => _baseStream.Dispose();
}

これを適用してみましょう。

StreamingLoggingInterceptor
public class StreamingLoggingInterceptor : Interceptor
{
    public override AsyncDuplexStreamingCall<TRequest, TResponse> AsyncDuplexStreamingCall<TRequest, TResponse>(
        ClientInterceptorContext<TRequest, TResponse> context,
        AsyncDuplexStreamingCallContinuation<TRequest, TResponse> continuation
    )
    {
        // 差し込む処理の定義
        void intercept(byte[] data)
        {
            // 雑にJSON化
            Debug.Log(MessagePackSerializer.ToJson(data));
        }

        var call = continuation(context);

        // 自作Streamに差し替えたcallを返す
        return new AsyncDuplexStreamingCall<TRequest, TResponse>(
            new MyRequestStream<TRequest>(intercept, call.RequestStream),
            new MyResponseStream<TResponse>(intercept, call.ResponseStream),
            call.ResponseHeadersAsync,
            call.GetStatus,
            call.GetTrailers,
            call.Dispose
        );
    }
}

MagicOnion での送受信フォーマット

これで一応リクエスト/レスポンスデータを見ることはできるのですが、なんだかよくわからないフォーマットになっています。
例えば SendStampAsync(3) というリクエストに対するデータは [2,1766806594,3] となりました。

これがどういう構造なのかは StreamingHubClientBase.csStreamingHubClientBuilder.cs を頑張って読めばわかるのですが、[メッセージID, メソッドID, 引数のデータ本体] という3要素の配列になっています。
メッセージIDは自身が送ったメッセージ1個ごとに固有のIDが振られます。
メソッドIDはメソッド名のFNV1A32ハッシュ値です。(※MethodIdAttribute を付けることで任意のメソッドIDを付けられるようですが、ここでは付けてないものとします。)

レスポンスは3種類のフォーマットがあります。どれが送られてきたかは配列の要素数で判別できるようです。
2要素: Receiverメソッドの呼び出し [メソッドID, 引数のデータ本体]
3要素: リクエストに対するレスポンス [メッセージID, メソッドID, 戻り値]
4要素: エラーレスポンス [メッセージID, ステータスコード, エラー詳細, エラー文言]

フォーマットに従った出力

メソッドIDじゃ分かりにくいのでメソッド名で出力してくれるように改造します。
そのため、初めにメソッドIDからメソッド名への変換テーブルを用意します。

StreamingLoggingInterceptor
public class StreamingLoggingInterceptor<THub, TReceiver> : Interceptor
{
    private Dictionary<int, string> _methodNameDic = new Dictionary<int, string>();

    // メソッドIDからメソッド名への変換テーブルを用意
    public StreamingLoggingInterceptor()
    {
        foreach (var method in typeof(THub).GetMethods())
        {
            _methodNameDic[FNV1A32.GetHashCode(method.Name)] = method.Name;
        }
        foreach (var method in typeof(TReceiver).GetMethods())
        {
            _methodNameDic[FNV1A32.GetHashCode(method.Name)] = method.Name;
        }
    }

    // ログ出力処理本体
    private void PrintLog(string type, byte[] bytes)
    {
        // 要素数を取得
        var readSize = 0;
        var length = MessagePackBinary.ReadArrayHeader(bytes, 0, out readSize);
        var offset = readSize;

        // 要素数4はエラー出力なので無視(!)
        if (length == 4) return;
        // 要素数3ならはじめはメッセージIDなので捨てる
        if (length == 3) offset += MessagePackBinary.ReadNext(bytes, offset);

        // メソッドID取得
        var methodId = MessagePackBinary.ReadInt32(bytes, offset, out readSize);
        offset += readSize;
        // 残りのデータをコピー(ToJsonにoffset渡したい…)
        var newBytes = BufferPool.Default.Rent();
        Array.Copy(bytes, offset, newBytes, 0, bytes.Length - offset);
        // 出力
        Debug.Log($"[{type}]{_methodNameDic[methodId]}: {MessagePackSerializer.ToJson(newBytes)}");
        BufferPool.Default.Return(newBytes);
    }

    public override AsyncDuplexStreamingCall<TRequest, TResponse> AsyncDuplexStreamingCall<TRequest, TResponse>(
        ClientInterceptorContext<TRequest, TResponse> context,
        AsyncDuplexStreamingCallContinuation<TRequest, TResponse> continuation
    )
    {
        var call = continuation(context);
        return new AsyncDuplexStreamingCall<TRequest, TResponse>(
            new MyRequestStream<TRequest>(x => PrintLog("SEND", x), call.RequestStream),
            new MyResponseStream<TResponse>(x => PrintLog("RECEIVE", x), call.ResponseStream),
            call.ResponseHeadersAsync,
            call.GetStatus,
            call.GetTrailers,
            call.Dispose
        );
    }
}

使う側はこんな感じです。

var channel = new Channel("host:port", ChannelCredentials.Insecure);
var invoker = channel.Intercept(new StreamingLoggingInterceptor<IhogeHub, IHogeHubReceiver>());
var client = StreamingHubClient.Connect<IhogeHub, IHogeHubReceiver>(invoker, receiver);

これで以下のようなログが出力されるようになりました。

[SEND]SendStampAsync: 3
[RECEIVE]OnReceiveStamp: [1010826359,3]
[RECEIVE]SendStampAsync: null

おわりに

gRPC 側の仕組みに頼らないフィルタの開発が予定されており、
そちらが実装されるとこんな面倒なことはする必要がなくなるかと思われます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む