xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

Azure Functions (Isolated Worker) で QueueMessage 型にマップする

.NET で利用する Azure Functions には In-Process Model と Isolated Model のふたつがあります。Isolated Model は .NET 6 から利用できるようになった新しいタイプです。それぞれの違いについては公式ドキュメントをご覧ください。

.NET 7 以降では Isolated Model を利用することが強制されるので、.NET 6 以前から Azure Functions を利用している方は移行作業が必要になります。そのときに必要となる作業のひとつとして「QueueTrigger 開発時に QueueMessage 型に直接マップできない」というものがあります。どういうことか、実装を見てみましょう。

QueueMessage へのマップができない件の差分

In-Process Model のとき

関数のエントリーポイントの引数で直接 Azure.Storage.Queues.Models.QueueMessage 型にマップできます。QueueMessage 型には DequeueCountExpiresOn など、たまに使いたくなるようなプロパティが生えていて有用です。

[FunctionName(nameof(QueueTriggerFunction))]
public void EntryPoint(
    [QueueTrigger("sample-queue")] QueueMessage queueMessage)
{ }

Isolated Model のとき

一方で Isolated Model になると byte[] などの比較的プリミティブな型が Binding 可能な型としてサポートされているだけで、QueueMessage 型などの SDK 固有の型へはダイレクトにマップできません。では DequeueCount などへはどうやってアクセスすれば良いのかということになりますが、代替手法として FunctionContext.BindingContext.BindingData が汎用データストアとして用意されており、それを利用することになります。

[FunctionName(nameof(QueueTriggerFunction))]
public void EntryPoint(
    [QueueTrigger("sample-queue")] byte[] payload,
    FunctionContext context)
{
    // BindingData に IReadOnlyDictionary<string, object?> として入っている
    var kvs = context.BindingContext.BindingData;
    var value = (string)kvs["DequeueCount"]!;
    var dequeueCount = long.Parse(value);
}

上のサンプルにコメントとして記載しましたが、IReadOnlyDictionary<string, object?> として提供されるのでだいぶ扱いづらいんですよね。

QueueMessage に変換する

ということで扱いやすい形にするために QueueMessage 型に変換してみましょう。ざっくり以下のような拡張メソッドを作ってみました。

using System;
using System.Collections.Generic;
using Azure.Storage.Queues.Models;
using Microsoft.Azure.Functions.Worker;

public static class BindingContextExtensions
{
    public static QueueMessage ToQueueMessage(this BindingContext context)
    {
        // QueueTrigger かどうかを確認しておく
        var kvs = context.BindingData;
        if (!kvs.TryGetValue("QueueTrigger", out var payload))
            throw new InvalidOperationException("Function is not triggered by QueueTrigger.");

        // 値を取得
        var messageId = getString(kvs, "Id");
        var popReceipt = getString(kvs, "PopReceipt");
        var body = BinaryData.FromString((string)payload!);
        var dequeueCount = getInt64(kvs, "DequeueCount");
        var nextVisibleOn = getNullableDateTimeOffset(kvs, "NextVisibleTime");
        var insertedOn = getNullableDateTimeOffset(kvs, "InsertionTime");
        var expiresOn = getNullableDateTimeOffset(kvs, "ExpirationTime");
        return QueuesModelFactory.QueueMessage(messageId, popReceipt, body, dequeueCount, nextVisibleOn, insertedOn, expiresOn);

        #region ローカル関数
        static string getString(IReadOnlyDictionary<string, object?> kvs, string key)
        {
            var value = kvs[key];
            return value is null
                ? throw new NotSupportedException($"Null value is not supported. | Key : {key}")
                : (string)value;
        }

        static long getInt64(IReadOnlyDictionary<string, object?> kvs, string key)
        {
            var value = getString(kvs, key);
            return long.Parse(value);
        }

        static DateTimeOffset? getNullableDateTimeOffset(IReadOnlyDictionary<string, object?> kvs, string key)
        {
            var value = kvs[key];
            if (value is null)
                return null;

            var span = (value as string).AsSpan();
            span = span.Trim('"');  // 前後に謎の " が含まれるので削除
            return DateTimeOffset.Parse(span);
        }
        #endregion
    }
}

これで以下のように利用できるようになります。QueueMessage のプロパティにアクセスするのがだいぶ楽になるのではないでしょうか。

[FunctionName(nameof(QueueTriggerFunction))]
public void EntryPoint(
    [QueueTrigger("sample-queue")] byte[] payload,
    FunctionContext context)
{
    var queueMessage = context.BindingContext.ToQueueMessage();
    var dequeueCount = queueMessage.DequeueCount;
}