[C#]MemoryPackを使ってみたい3 - TypeScriptで使う

Cysharp/MemoryPack

ASP.NET CoreでMemoryPackが簡単に使えてレスポンスよくなるかも!というところがわかったところで、クライアント側がTypeScript/JavaScriptなんですが…というケースに対応していきましょう。

なんとMemoryPackは、C#側で書いたコードから、MemoryPackableなオブジェクトをTypeScriptでシリアライズ/デシリアライズするためのコードを生成してくれます。

見ていきましょう。

TypeScriptで使う

TypeScript and ASP.NET Core Formatter にあるとおり、csprojに下記を追加。

<!-- output memorypack TypeScript code to directory -->
<ItemGroup>
    <CompilerVisibleProperty Include="MemoryPackGenerator_TypeScriptOutputDirectory" />
</ItemGroup>
<PropertyGroup>
    <MemoryPackGenerator_TypeScriptOutputDirectory>$(MSBuildProjectDirectory)\wwwroot\js\memorypack</MemoryPackGenerator_TypeScriptOutputDirectory>
</PropertyGroup>

MemoryPackable属性をつけたWeatherForecastにGenerateTypeScript属性を追加。

5

そしたらもう、MemoryPackGenerator_TypeScriptOutputDirectoryに指定したディレクトリに、MemoryPackのシリアライズ/デシリアライズできるTypeScriptコードが生成されるのです、全自動すぎて不安になるぐらい。

かつてなんか定義が変わるたびにそれぞれコードを書き換え、そして変更を忘れ、型を間違え…という見事な独り相撲を演じていたわけですが、なんかその辺全部やってくれて、なおかつコンパクトで速いなんてほぼチートツール。

TypeScriptでデータを取得

さて早速これを使ってみたいと思います。

まずAPIの方に、WeatherForecastを1件だけ返すメソッドを追加します。 ちょっと無理矢理ですがサクッと試すだけなので勘弁してください。

[HttpGet("item/{place}")]
public WeatherForecast Get(string place)
{
    return new WeatherForecast
    {
        Date = DateTime.Now,
        TemperatureC = Random.Shared.Next(-20, 55),
        Summary = place
    };
}

あと、今回はTypeScirptをVSCodeのLive Serverで動かすのでCORSの対応。

Program.cs

builder.Services.AddCors(options =>
{
    options.AddPolicy("MyPolicy", policy =>
    {
        policy
            .WithOrigins("http://127.0.0.1:5500")
            .AllowAnyMethod()
            .AllowAnyHeader()
            .AllowCredentials()
            .Build();
    });
});

~~~
app.UseCors("MyPolicy");

そして簡単なTypeScriptのアプリケーションを用意します。

6

MemoryPackReader.ts, MemoryPackWriter.ts, WeatherForecast.tsは、上記の生成先をここにするか、コピーして持ってきてください。

src/client.ts

import { WeatherForecast } from "./WeatherForecast.js";

export async function getWeatherForecast(place: string): Promise<WeatherForecast | null> {
    let response = await fetch(
        `https://localhost:7104/WeatherForecast/item/${place}`,
        { 
            method: 'GET', 
            headers: { 'accept': 'application/x-memorypack', 'Access-Control-Allow-Origin': '*' }
        });

    let buffer = await response.arrayBuffer();
    let item = WeatherForecast.deserialize(buffer);

    return item;
}

wwwroot/get.html

※実際にはこんなプレーンな作り方ではなくなんらかフレームワークを使うと思うので、出力結果はブラウザのコンソールに出すだけにしときます。

<html>
<head></head>
<body></body>

<script type="module">
  import { getWeatherForecast } from './js/client.js';

  let item = await getWeatherForecast('hoge');
  console.log(item);
</script>

</html>

package.json

{
    "name": "weatherforecast",
    "scripts": {
        "build": "npx tsc",
        "watch": "npx tsc --watch"
    },
    "dependencies": {
        "typescript": "^4.8.4"
    },
    "type": "module"
}

tsconfig.json

{
    "compilerOptions": {
        "target": "ES2020",
        "module": "ES2020", 
        "rootDir": "src",
        "outDir": "wwwroot/js/",
        "esModuleInterop": true,
        "forceConsistentCasingInFileNames": true,
        "strict": true,
        "skipLibCheck": true
    }
}

なんかこんな感じにして、

$ npm install
$ npm run build

動くようにしたら、Visual StudioでAPIの方を実行しておき、Live Serverでget.htmlをブラウザに表示させてみてください。

7

API側がMemoryPackで返したものをちゃんとデシリアライズできています。 もう、C#側で定義したクラス(のjson表現とか)をいちいちTypeScript側にも用意したり、型の違いを気にしたりしながらコードを書く必要はないのです。 一人でどっちもやってるような器用貧乏なエンジニアにとってはもうまさに神。

TypeScriptでリスト取得

まあだいたい業務で使うようなWebアプリの場合、「一覧画面」なんて言うのがだいたい存在していますので、APIからリストを取得するという操作が必要になります。そういうデータを扱うために、serializeArray/deserializeArrayというメソッドが生成されます。

C#側は何もしなくていいです。必要なコードはすでに生成されていますので、TypeScript側に必要なものを追加します。

src/client.ts

export async function listWeatherForecast(): Promise<(WeatherForecast | null)[] | null> {
    let response = await fetch(
        `https://localhost:7104/WeatherForecast/`,
        { 
            method: 'GET', 
            headers: { 'accept': "application/x-memorypack", 'Access-Control-Allow-Origin': '*' }
        });

    let buffer = await response.arrayBuffer();
    let list = WeatherForecast.deserializeArray(buffer);

    return list;
}

wwwroot/list.html

<html>
<head></head>
<body></body>

<script type="module">
  import { listWeatherForecast } from './js/client.js';

let list = await listWeatherForecast();
console.log(list);
</script>

</html>

これで、list.htmlを動かすと…

7

注文通りリストが返ってきています。

まとめ

簡単な割にはなかなかに強力です。既存のコードに追加したり改修したりするのもそれほど難しいことではないでしょう。

開発担当がサーバーとクライアントで違う場合等、生成されたTypeScriptのコードをどうやりとりすればいいのかが課題になりそうに思いますが、このパターンでの開発は一人でしかやったことがないので正直わかりません…

とはいえ、MemoryPackがC#に閉じたものではなく、広くクライアント側に使われているTypeScriptでも安心して扱えるので、単純にパフォーマンスを上げられる手段として検討する価値は十分にあるでしょう。

次回は、用意されている細かい機能を見てみます。