銀の光と碧い空

クラウドなインフラとC#なアプリ開発の狭間にいるエンジニアの日々

.NET Standard の HttpClientで送信時のBodyをgzip圧縮したい

HttpClientでPOSTやPUTのような送信を行う場合に、Bodyで送信するコンテンツをGZip圧縮したいと思ったのですが、意外とすぐに使えるサンプルコードが見当たりませんでした。

docs.microsoft.com

とりあえず動くコードは書けたので、メモしておきたいと思います。Bodyで送信するコンテンツは、抽象クラスであるHttpContentで扱うのですが、単純な文字列の場合はStringContent、ストリームの場合はStreamContentなど用意されている具象クラスを利用できることがあります。

docs.microsoft.com

docs.microsoft.com

が、GZip圧縮できるクラスはないようなので、自分でHttpContentを継承する必要がありそうです。主に必要なのは、SerializeToStreamAsyncで引数のStreamにGZip圧縮したコンテンツを書き込む処理です。また、実装する際には送信するコンテンツをコンストラクタなどで受け取る必要があります。今回送信するコンテンツは文字列結合で生成しているので、ZStringのUtf8ValueStringBuilderで渡してストリームに書き込むことにしました。

github.com

というわけでこのように実装しました。

using Cysharp.Text;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;

public class GZipZStringContent : HttpContent
{
    private readonly Utf8ValueStringBuilder sb;

    public GZipZStringContent(Utf8ValueStringBuilder sb)
    {
        Headers.TryAddWithoutValidation("Content-Type", "application/gzip");
        Headers.ContentEncoding.Add("gzip");
        this.sb = sb;
    }

    protected override Task SerializeToStreamAsync(Stream stream, TransportContext context)
    {
        var gzipStream = new GZipStream(stream, CompressionMode.Compress, leaveOpen: true);
        return sb.WriteToAsync(gzipStream).ContinueWith(_ =>
        {
            gzipStream?.Dispose();
        });
    }

    protected override bool TryComputeLength(out long length)
    {
        length = -1L;
        return false;
    }
}

SerializeToStreamAsyncメソッドでは、Utf8ValueStringBuilder のWriteToAsyncメソッドを使って生成したGZipSteamにコンテンツを書き込んでいます。また、完了したあとに、GZipStreamのDisposeが呼び出されるようにしています。ZStringではない別のもので受け渡したい場合は、コンストラクタで受け取ったものをGZipStreamに書き込むように処理を変えれば問題ないはずです。 また、コンストラクタではContent-TypeContent-Encodingヘッダーを設定しています。TryComputeLengthはコンテンツのサイズが低コストでわかる場合のみ正しい数値を返すので、今回は計算しないように実装しています。

docs.microsoft.com

利用するときはこのようになります。

// サンプルなので都度生成しているが、HttpClientのインスタンス生成は適切に。
var client = new HttpClient();
var endpoint = "https://example.com/log/v1";
using var sb = ZString.CreateUtf8StringBuilder();
sb.Append("[{\"logs\": [");
// さらにsbに追記
sb.Append("]}]");
var res = await client.PostAsync(Endpoint, new GZipZStringContent(sb));
res.EnsureSuccessStatusCode();

なお、処理全体としては最近公開したライブラリのこのクラスに記述しています。

github.com