Liberent-Dev’s blog

株式会社リベル・エンタテインメントのテックブログです。

ASP.NET Coreを初めて触りつつMemory Packを試してみた

あけましておめでとうございます。
システム開発部のK.Mです。

今回の経緯

前回のアイ★チュウ最適化の話の中で、MemoryPackという新しいシリアライザーがリリースされていたのを知ったため、これを試してみようと思ったのがキッカケとなります。

しかしながら、MemoryPackを使うとなると、サーバをC#で実装する必要があります。

  • C#を本格的に触ったことがない
  • CやC++でVisualStudioを触っていた時期もあったが、VisualStudio2000とかもっと前の物だった
  • .NETを使ってのサーバ構築をやったことが無かった

上記の理由で少し不安に感じていたのですが、いざ触ってみたらあっさりとローカル環境が動いたので、同じようにMemoryPackが気になるけど、C#を触ったことのないサーバ・バックエンドのエンジニアに向けて入門編としてお届け出来ればと考えております。

開発環境

Visual Studioのインストール

MacであればこちらからVisual Studio for Macをダウンロード・インストールしてください。
Windowsは、上記リンクの先にあるVisual Studio Communityをダウンロード・インストールしてください。

この後記載してある、操作周りの説明はMacでの操作になりますので、Windowsでは若干異なる可能性があります。

API用のプロジェクト作成

ASP.NET Core の概要
チュートリアル: ASP.NET Core で Web API を作成する

上記のマイクロソフトの公式ドキュメントを確認しつつ、まずはプロジェクト作成してきます。

Visual Studioを起動すると下記のような画面が表示されるので、新規と記載されているところを選択します。

選択後に各種テンプレートを選ぶ画面が表示されるので左側にある「Webとコンソール」→「アプリ」を選択します。
真ん中の部分に更に細かい分類が表示されるので、「ASP.NET Core」の「API」を選択します。
0から作るのであれば「空」が良いのですが、最低限の実装サンプルが入っているので「API」を選択しています。右側にあるプルダウンに関してはC#となります。
選択したら、右下にある「続行」ボタンを選択します。

次に下記のような画面が表示されて、各種設定が可能になります。
フレームワークは「.NET 7.0」として詳細設定のHTTPS用の構成はOFFにしておきます。
ONにして実行すると証明書周りで色々と対応が必要になりそうだったため、ローカルで試す程度のものであればHTTPSは不要かと考えております。

他の3点はONにしております。右下にある「続行」ボタンを選択します。

次に表示されるのが、プロジェクト名を決める画面が表示されるので名前を入力して、右下にある「作成」ボタンを選択します。

これまでの設定した内容でサンプルのプロジェクトが作成されてVisual Studioが起動されてきます。

実行

先述したようにプロジェクト作成後にエディターが起動するので、左上にある再生ボタンから実行します。

起動するブラウザに関して、再生ボタン横にある部分にて変更が可能になっています。

ビルドが行われて、ビルド完了後にブラウザが表示されます。

表示されたブラウザにてSwaggerのページが表示されてサンプルで用意されているAPIが叩けるようになっており.NETでのAPIやWeb周りの開発が進化しているのを個人的に感じて感動しました。
(先程の設定の「OpenAPIサポートを有効にする」の効果。)

NuGetでのMemoryPackインストール

MemoryPackのReadMeを見るにWindowsであれば、パッケージマネージャーのコンソール画面を表示して

Install-Package MemoryPack

と入力すればインストールされるようですが、Macだとやり方が自分では分からず、NuGetパッケージ管理で追加しました。下記のように操作して各種MemoryPack関連をインストールしています。

メニューの「ツール」→「NuGetパッケージの管理...」を選択。

パッケージの管理画面が表示されるので、右上にある検索枠に「memorypack」と入力する。

検索結果でMemoryPack関連のパッケージが表示されたので一通りチェックを入れた後、右下にある「パッケージを追加」ボタンを選択。

  • MemoryPack
  • MemoryPack.Core
  • MemoryPack.Generator
  • MemoryPack.Streaming
  • MemoryPack.AspNetCoreMvcFormatter

をチェックしてインストールしました。

MemoryPack向けに修正

サンプルで生成されたAPIの処理にてMemoryPackが動くように、2ファイルに修正を入れます。
まずはメイン処理の部分でIN/OUTにMemoryPackを使えるように設定を入れます。

using MemoryPack.AspNetCoreMvcFormatter; // ← 1行目に追加

// builder.Services.AddControllers();
// ↓ 下記のように変更
builder.Services.AddControllers(options =>
        {
            options.InputFormatters.Insert(0, new MemoryPackInputFormatter());
            options.OutputFormatters.Insert(0, new MemoryPackOutputFormatter(true));
        });

次にレスポンス定義しているファイルに、MemoryPackで使えるようにする設定を追加しています。

using MemoryPack; // ← 1行目に追加

namespace TestMemoryPack;

[MemoryPackable] // ← クラス宣言の前に追加(これをすることでジャネレータでフォーマッターが自動生成されている)
public partial class WeatherForecast // ← partialを追加
{
    public DateOnly Date { get; set; }

    public int TemperatureC { get; set; }

    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);

    public string? Summary { get; set; }
}

修正したものを実行

上記の修正を行うだけで、サンプルのAPIにてMemoryPack形式でレスポンスが返ってくるようになります。実行して確認してみましょう。
Swaggerのページが起動するので、API名の部分を選択します。

選択すると各種設定や結果とかが表示される部分が開きます。

Media typeのところに「application/x-memorypack」が追加されています。
選択すると、下記画面のように他のtypeが選択出来るようになっております。

SwaggerからAPIにアクセスするには、「Try it out」のボタンを押して「Execute」ボタンを表示させる必要があります。

「Execute」ボタンが表示されたら、選択することでAPIにアクセスします。(単純なGETのためリクエストは不要となります。)

「Execute」ボタンを選択してAPIへアクセスすると、リクエスト情報とレスポンス情報が表示されてきます。

まずは、リクエスト時の情報。
CURLでMemoryPack指定でAPIにアクセスしたよという情報が表示されています。

そして、レスポンス情報。
ヘッダー情報にMemoryPack形式ですよという内容と共にレスポンスされたデータが表示されています。
MemoryPackはバイナリ形式なので、正直この状態で成功しているとは言い難いのですが、json形式では無いものが返ってきているのと後述しているjson形式と同じような文字列が返ってきているようなので、一旦動いているという認識で問題なさそうです。

先ほどの説明したMediaTypeを「application/json」に変えると見やすいjson形式でレスポンスが返ってきます。
MemoryPackで返却されてくる内容と見比べると文字列データに関しては同じようなものが返ってきているので、MemoryPack側が正常に動いていると判断がつきます。

Webアプリのプロジェクト作成

上記のAPIの結果だけではなく、クライアント側でデシリアライズして表示されることを確認しない限りには上手く行ったとは考えられないので、引き続きC#で作成したAPIをMemoryPack形式で叩いてレスポンスをデシリアライズして表示するものを作成していきます。

こちらもVisual Studioのサンプルをベースに作成していきます。
API側のVisual Studioを開いた状態のままであれば一度終了して、再度Visual Studioを立ち上げます。

新規を選択した後に表示されるプロジェクトのテンプレートにてWebアプリケーションを選択し「続行」ボタンを選択します。

各種設定を選択する画面が表示されるので、下記のような設定として「続行」ボタンを選択します。

プロジェクト名を入力する画面が表示されるので、名前を入力して「作成」ボタンを選択します。

ひとまず起動

APIでやった時と同様にエディターが起動するので実行ボタンを選択します。
下記のようにブラウザが表示されて、Webページが表示されます。

API通信とレスポンス表示用の修正

この辺りの公式ドキュメントに記載されている内容をベースにして修正していきます。

APIと通信する処理が記載されているので、Index.cshtml.csのOnGet()を下記のように修正します。

    public async void OnGet()
    {
        // 画面表示時に動く処理なのでAPI叩く処理いれる
        HttpClient client = new();
        client.DefaultRequestHeaders.Accept.Clear();
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github.v3+json"));
        client.DefaultRequestHeaders.Add("User-Agent", ".NET Foundation Repository Reporter");

        await ProcessRepositoriesAsync(client);
    }

    static async Task ProcessRepositoriesAsync(HttpClient client)
    {
        var json = await client.GetStringAsync("https://api.github.com/orgs/dotnet/repos");
        Console.WriteLine(json);
    }

一先ず、この状態で実行してAPIのレスポンス結果のJSONが出力画面に表示されていることを確認します。
Visual Studioの下のほうにある「アプリケーション出力」という場所を選択すると起動時などのログ出力された画面が表示されます。)

API通信が出来てレスポンスが返ってきているのは確認出来たので、ブラウザ画面に表示する対応を行います。
まずは、テンプレートになっているIndex.cshtmlに@Model.ScreenMessageを追加します。

@page
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    @Model.ScreenMessage
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

API通信する部分が先程のマイクロソフトの公式ドキュメントでは、async/awitを使って非同期処理になっているため、このまま修正すると画面表示した後にAPIの結果が返ってきて画面側には反映されないため、ここは簡単対応でAPI通信は同期する形で修正します。
OnGet()の修正とProcessRepositoriesAsync()を削除します。

    public void OnGet()
    {
        // 画面表示時に動く処理なのでAPI叩く処理いれる
        HttpClient client = new();
        client.DefaultRequestHeaders.Accept.Clear();
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github.v3+json"));
        client.DefaultRequestHeaders.Add("User-Agent", ".NET Foundation Repository Reporter");

        // 非同期から同期処理にする
        ScreenMessage = client.GetStringAsync("https://api.github.com/orgs/dotnet/repos").Result;
    }

そして、IndexModelClass内にpublic string ScreenMessage { get; private set; } = "";を追加します。Index.cshtml.cs全体が下記のようになります。

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Net.Http.Headers;

namespace TestMemoryPackWeb.Pages;

public class IndexModel : PageModel
{
    private readonly ILogger<IndexModel> _logger;
    public string ScreenMessage { get; private set; }  = "";

    public IndexModel(ILogger<IndexModel> logger)
    {
        _logger = logger;
    }

    public void OnGet()
    {
        // 画面表示時に動く処理なのでAPI叩く処理いれる
        HttpClient client = new();
        client.DefaultRequestHeaders.Accept.Clear();
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github.v3+json"));
        client.DefaultRequestHeaders.Add("User-Agent", ".NET Foundation Repository Reporter");

        ScreenMessage = client.GetStringAsync("https://api.github.com/orgs/dotnet/repos").Result;
    }
}

この状態で実行すると、起動したブラウザ画面にjsonの結果が表示されます。

ここまでで、C#でのAPI通信との接続と画面上のデータ表示が出来たので、ここからは今回作成したAPIとMemoryPackの対応を入れていきます。

今回作成分のAPIをMemoryPackへの対応

まずはNuGetでMemoryPackを導入します。
(詳細はAPI側のものと同じ操作になりますので割愛します。結構な頻度で更新されていっているのでバージョンは出来る限り合わせておきましょう。)

APIの接続先をローカルのものに変更したうえでヘッダー内容も修正します。

    public void OnGet()
    {
        // 画面表示時に動く処理なのでAPI叩く処理いれる
        HttpClient client = new();
        client.DefaultRequestHeaders.Accept.Clear();
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

        ScreenMessage = client.GetStringAsync("http://localhost:5265/WeatherForecast").Result;
    }

上記修正したのちにAPI側のプロジェクトを別のターミナルから起動させます。
API側のプロジェクトがある場所まで移動してからProgram.csがあるところでdotnet runを実行することでAPI側のサーバが起動します。

[XXXX@MacBook-Pro TestMemoryPack]$ dotnet run       
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5265
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /Users/maeda/Projects/TestMemoryPack/TestMemoryPack

http://localhost:5265/swagger/index.html にブラウザでアクセスしてSwaggerが起動していることを確認し、http://localhost:5265/WeatherForecast へアクセスしてjson形式のレスポンスが表示されていること確認します。

その後、Web側のプロジェクトにて実行を行いブラウザを起動させることで、下記のような状態になっていればWebからローカルのAPIへアクセスしていることが確認出来ます。

ここからMemoryPackの対応となります。

API側のレスポンス定義になっているソースコードの、WeatherForecast.csをWeb側のProgram.csと同じディレクトリにコピーします。
OnGet()内にあるヘッダー定義部分のapplication/jsonapplication/x-memorypackに変更して、不要なUser-Agentの設定部分は削除しておきます。

    public void OnGet()
    {
        // 画面表示時に動く処理なのでAPI叩く処理いれる
        HttpClient client = new();
        client.DefaultRequestHeaders.Accept.Clear();
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/x-memorypack"));

        ScreenMessage = client.GetStringAsync("http://localhost:5265/WeatherForecast").Result;
    }

この状態でWeb側を実行してみると下記のような表示になってMemoryPack形式のバイナリが表示されますが、あくまでバイナリなので文字化けした状態となります。

ここからMemoryPackのデシリアライズ処理を入れていきます。

    public void OnGet()
    {
        // 画面表示時に動く処理なのでAPI叩く処理いれる
        HttpClient client = new();
        client.DefaultRequestHeaders.Accept.Clear();
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/x-memorypack"));

        ReadOnlySpan<byte> resString = client.GetByteArrayAsync("http://localhost:5265/WeatherForecast").Result;
        var resVal = MemoryPackSerializer.Deserialize<WeatherForecast[]>(resString);

        foreach (WeatherForecast w in resVal)
        {
            ScreenMessage += w.Date.ToString() + ":" + w.TemperatureC + ":" + w.TemperatureF + ":" + w.Summary + "  ";
        }
    }

Deserializeの部分が要注意で1件ではなく、複数件レスポンスが来る場合は[]と配列で受け取れるようにしておく必要があります。
(個人的にここで手間取りました。)

実行すれば下記のようにブラウザで表示されます。

ここまででAPIとWeb間でMemoryPackが正常にやり取りを行えることが確認出来ました。
APIとUnity間でも恐らく簡単に対応出来そうなため、今後導入するアプリが増えてきそうかと感じました。

気になった点

今回の修正時にレスポンスのクラス定義部分のソースをAPI側とWeb側でどう合わせるべきかという部分が少し気になりました。

今回に関しては確認程度のものだったので定義部分のソースをそのままコピペしましたが、案件ベースであれば定義だけの実装をプロジェクトかソリューションで分けておいて共通化する方法がベストかと感じました。

最後に

リベル・エンタテインメントでは、このような最新技術などの取り組みに興味のある方を募集しています。
もしご興味を持たれましたら下記サイトにアクセスしてみてください。
https://liberent.co.jp/recruit/