[C#]MemoryPackを使ってみたい7 - Nuxtでのサンプル

dalian-spacekey/MemoryPackSampleApp

コードの断片だけじゃなくて、なんかある程度実際に即した動くものを作ってみました。

まずは、ASP.NET Coreで動くAPIサーバーからNuxt 2(TypeScript)でデータをとってきて表示します。この構成は何年か前に「サーバー + Webアプリ + iOS/Android」の3点セットの業務系システムをいくつか作ったパターンの一部です。こういう事例は多分いろんなところに転がってると思います。

当時一人で作業していたこともあってできるだけ脳の負荷を減らすため、サーバーはASP.NET Core、iOS/AndroidはXamarin.FormsとC#で統一したものの、Webだけがどうにもならず色々悩んだあげくNuxtであればTypeScriptとnuxt-property-decoratorなどでなんとなくJavaScriptスメルを緩和することができそうだったので採用した経緯があります。いまではBlazorがあるので新規でやることはないと思いますが、当時色々めんどくさかったこととか、こんな風にしてみたいなと思っていたことがMemoryPackだったら解決できたよねぇ、という回顧というか供養の意味で書いてます。

(最初に手をつけたAngular4もさっき見たら14になってたしNuxtもそろそろメジャーバージョンアップだしReactはあの書き方がどうしてもなじめないしjsのフレームワークは我慢して仲良くしようとしても一瞬で置いて行かれるしパッケージの依存関係とかもややこしいしnode_modulesがディスク無駄遣いして邪魔だしそもそもJavaScriptはなんであんなにごちゃごちゃしてて書きづらいんだとか色々…)

Sharedプロジェクト(src/Shared)

サーバー外とやりとりするために必要なデータ構造はSharedに置いておきます。クライアントとなるC#プロジェクト(XamarinとかMAUIとかBlazorとか)はこのプロジェクトを参照します。Nuxtは直接これを見ませんが、中で定義されているTypeScript向けのクラスに[MemoryPackable][GenerateTypeScript]がついていて、csprojに設定が入っているので、ここからNuxtの所定のディレクトリにコードが生成されるようになっています。

<MemoryPackGenerator_TypeScriptOutputDirectory>$(MSBuildProjectDirectory)\..\Web\src\models\MemoryPack</MemoryPackGenerator_TypeScriptOutputDirectory>

今回のNuxtプロジェクトの場合、importに.jsがつくと都合が悪いので、MemoryPackGenerator_TypeScriptImportExtensionは空にしておきます。

<MemoryPackGenerator_TypeScriptImportExtension></MemoryPackGenerator_TypeScriptImportExtension>

Serverプロジェクト(src/Server)

APIサーバーとデータベース周りが入ってます。ASP.NET CoreでAPIサーバーを作るとこんな感じでしょ?みたいな典型的なパターンにしてあるつもりです。

builder.Services.AddControllers(options =>
{
    options.InputFormatters.Insert(0, new MemoryPackInputFormatter());
    options.OutputFormatters.Insert(0, new MemoryPackOutputFormatter(true));
});

しっかりとフォーマッタを差し込んでおきます。true書いとかないと泣きますよ。

Controllerについては、相手がC#とTypeScript用にわざわざ別にしていますが、この辺はサンプルとしての体を優先してるだけなので、実際のケースで一緒でもいけるんだったら別にする必要は何にもありません(Sharedしかり)。

実行するとswaggerで呼び出し確認できます。application/jsonapplication/x-memorypackを選ぶとちゃんとフォーマットが切り替わっているのがわかります。

※TypeScript側の話ではないんですが、日本語の文章などが入るようなフィールドの場合[Utf16StringFormatter]をつけてやることで、パフォーマンスがさらによくなる上、シリアライズしたときのサイズがさらに小さくなります(短い文字列だと大して変わらない)。application/x-memorypack/web/Messages/maui/Messagesを呼んでみると、文章部分の表示が違って見えます。

テストデータについて

テスト用のデータをセットしていますが、ユーザーデータっぽいものは個人情報テストデータジェネレーターで生成したものを加工しています。文章的なものは、青空文庫の魯山人のテキストを元にマルコフ連鎖でランダム生成していて、そのマルコフ連鎖のコードはshibayanさんのshibayan/MarkovDemoを使わせてもらってます。皆様ありがとうございます。

Nuxtプロジェクト(src/Web)

Nuxtで構築したアプリケーションです。こっちはvscodeで書いていたのでそれを踏襲して、Visual Studioのソリューションには入っていません。

なんか無駄にFluxパターンなんですが、ReactやVueで作るとみんなそんな感じにするように見えたので真似してました。過去のコードは手元になく書き方を忘れていて復元するのに苦労しました。(このパターン、一人で延々とたらい回しするようなコードを書き続けないといけなくて馬鹿らしくてきらい)

とりあえず要点は、models/MemoryPackSharedで定義したクラスとシリアライザが生成されているところです。そこでエクスポートだけしておくと、他の場所で使えるようになります。

apiディレクトリにAPI呼び出ししているコードがあり、比較のためにJSONで取得する場合と並べてあります。

const response = await this.axios.get(`messages`);
return response.data as MessageForTypeScript[];
const response = await this.axios.get(`messages`, { responseType: 'arraybuffer' });
let payload = MessageForTypeScript.deserializeArray(response.data);
return payload === null ? [] : payload!.map(x => x!) ;

arraybufferで受信して、生成されたデシリアライザにかけるだけです。

ちなみにaxiosはplugins/gateway-accessor.tsで定義していて、MemoryPack用はapplication/x-memorypackでやりとりするようにヘッダを仕込んであります。

いろんなところでJSONで呼んだやつと併記してあるので比較したらわかるとおり、もともとJSONだったものをMemoryPackに置き換えても変更量はたいしたことないですね。

一応コンソールに処理時間出してますが、この程度のデータをローカルでやりとりしてても、元々の処理時間がたいしたことないので大して速くはなりません。ただ確実に送ってるデータのサイズは小さくなってますから、効率は必ず上がります。

あと、Messageについては申し訳程度ですが編集機能がついていて、TypeScript側でシリアライズしてサーバーに送るようになっています。当然ですが、MemoryPackにシリアライズされて送信され、無事に更新されます。

まとめ

まずはTypeScriptでもJSONの時と同じようにできることがわかりました。

かつては、サーバー側のスキーマをTypeScript側でいちいち書きおこし、変更や修正があるたびにどちらも書き換えて、もしくは書き換え忘れてバグって調べ、みたいなことをやっていてそれなりにストレスでした。 今回はデータをやりとりする効率を上げるためにMemoryPackを導入しようとしたら、おまけでコーディングが一気に楽になるという現象を確認しました。 Reactなど他のフレームワークはまともにかけないのですが、おそらくそんなに困ることはないと思います。(Reactはやってみようかと思ったんですが挫折しました…)