1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Microsoft AzureAdvent Calendar 2023

Day 18

【Azure Functions】ミドルウェアを使いやすくするミドルウェアを書いてみた【Middleware】

Last updated at Posted at 2023-12-17

Microsoft Azure Advent Calendar 2023 に空きがあったので、Azure Functionsについて投稿させていただきます!

はじめに

Azure Functionsを分離プロセス(isolated)で実行するとエンドポイント固有の処理の前後にミドルウェアという形で処理を挟めるのですが、如何せんこれが使いにくい。

以下 公式ドキュメント から引用。

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults(workerApplication =>
    {
        // Register our custom middlewares with the worker

        workerApplication.UseMiddleware<ExceptionHandlingMiddleware>();

        workerApplication.UseMiddleware<MyCustomMiddleware>();

        workerApplication.UseWhen<StampHttpHeaderMiddleware>((context) =>
        {
            // We want to use this middleware only for http trigger invocations.
            return context.FunctionDefinition.InputBindings.Values
                          .First(a => a.Type.EndsWith("Trigger")).Type == "httpTrigger";
        });
    })
    .Build();

ジェネリックホストの起動設定時に UseMiddleware<T>()UseWhen<T>() でミドルウェアを指定するのですが、

  • 全部のエンドポイントに適用
  • クラス名によってミドルウェアを適用するかどうか変える

くらいの用途しか想定されてないっぽく、かなり辛いです。

こういう風に書きたい!

  • 一箇所に書くのをやめてクラスに個別で書きたい
    • Attributeが良さそう
  • 一気に全体に適用したい&個別で細かく指定したい
    • 親クラスに適用したら子クラスにも適用したい
    • メソッドにも個別で指定したい

つまりこんな感じ。

BaseController.cs
[UseMiddleware(typeof(BaseMiddleware))]
public class BaseController
{
}
DerivedController.cs
[UseMiddleware(typeof(DerivedMiddleware))]
public class DerivedController : BaseController
{
    [UseMiddleware(typeof(FooMiddleware))]
    public Task FooAsync()
    {
        // エンドポイント個別の処理
    }
}

で、こんな感じの順番で実行。

BaseMiddlewareの実行
  DerivedMiddlewareの実行
    FooMiddlewareの実行
      エンドポイント個別の処理

実装

というわけで実装してみました。

UseMiddlewareAttribute.cs
using System.Runtime.CompilerServices;

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
public sealed class UseMiddlewareAttribute : Attribute
{
    public Type Middleware { get; }
    internal int CallerLineNumber { get; }

    public UseMiddlewareAttribute(Type middleware, [CallerLineNumber] int callerLineNumber = 0)
    {
        this.Middleware = middleware;
        this.CallerLineNumber = callerLineNumber;
    }
}
MiddlewaresInvokerMiddleware.cs
using System.Reflection;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Middleware;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;

public sealed class MiddlewaresInvokerMiddleware : IFunctionsWorkerMiddleware
{
    private static readonly Dictionary<string, IReadOnlyList<Type>> FunctionNameToMiddlewareTypesMap = new();

    private static void TryInitialize()
    {
        if (FunctionNameToMiddlewareTypesMap.Any()) return;

        foreach (var (functionName, methodInfo) in GetFunctionNameToMethodInfoMap())
        {
            FunctionNameToMiddlewareTypesMap.Add(
                functionName,
                GetUseMiddlewareAttributes(methodInfo)
                    .Select(a => a.Middleware)
                    .DistinctBy(m => m)
                    .ToArray()
            );
        }

        return;

        static IReadOnlyDictionary<string, MethodInfo> GetFunctionNameToMethodInfoMap()
        {
            var map = new Dictionary<string, MethodInfo>();
            var methodInfos = AppDomain.CurrentDomain
                .GetAssemblies()
                .SelectMany(assembly => assembly.GetTypes())
                .SelectMany(type => type.GetMethods());

            foreach (var methodInfo in methodInfos)
            {
                var attribute = methodInfo.GetCustomAttribute<FunctionAttribute>();
                if (attribute != null) map.Add(attribute.Name, methodInfo);
            }

            return map;
        }

        static IReadOnlyList<UseMiddlewareAttribute> GetUseMiddlewareAttributes(MethodInfo methodInfo)
        {
            var inheritanceTree = GetInheritanceTree(methodInfo.DeclaringType);
            var middlewareAttributes = new List<UseMiddlewareAttribute>();

            foreach (var type in inheritanceTree)
            {
                var attributes = type
                    .GetCustomAttributes(typeof(UseMiddlewareAttribute))
                    .Cast<UseMiddlewareAttribute>()
                    .OrderBy(a => a.CallerLineNumber);

                middlewareAttributes.AddRange(attributes);
            }

            {
                var attributes = methodInfo
                    .GetCustomAttributes(typeof(UseMiddlewareAttribute))
                    .Cast<UseMiddlewareAttribute>()
                    .OrderBy(a => a.CallerLineNumber);

                middlewareAttributes.AddRange(attributes);
            }

            return middlewareAttributes;
        }

        static IReadOnlyList<Type> GetInheritanceTree(Type type)
        {
            var types = new List<Type>();

            while (type != null)
            {
                types.Add(type);
                type = type.BaseType;
            }

            types.Reverse();
            return types;
        }
    }

    public static void AddMiddlewares(IServiceCollection services)
    {
        TryInitialize();

        foreach (var middlewareType in FunctionNameToMiddlewareTypesMap.Values.SelectMany(middleware => middleware))
        {
            services.TryAddSingleton(middlewareType);
        }
    }

    async Task IFunctionsWorkerMiddleware.Invoke(FunctionContext context, FunctionExecutionDelegate next)
    {
        next = new FunctionExecutionDelegate(
            new Func<FunctionContext, FunctionExecutionDelegate, Func<FunctionContext, Task>>(
                    (_, n) => n.Invoke
                )
                .Invoke(context, next)
        );

        if (FunctionNameToMiddlewareTypesMap.TryGetValue(context.FunctionDefinition.Name, out var middlewareTypes))
        {
            foreach (var middlewareType in middlewareTypes.Reverse())
            {
                var middleware = (IFunctionsWorkerMiddleware)context.InstanceServices.GetService(middlewareType);

                next = new FunctionExecutionDelegate(
                    new Func<IFunctionsWorkerMiddleware, FunctionExecutionDelegate, Func<FunctionContext, Task>>(
                            (m, n) => c => m.Invoke(c, n)
                        )
                        .Invoke(middleware, next)
                );
            }
        }

        await next.Invoke(context);
    }
}

public static class IFunctionsWorkerApplicationBuilderExtensions
{
    public static IFunctionsWorkerApplicationBuilder AddMiddlewaresInvokerMiddleware(this IFunctionsWorkerApplicationBuilder builder)
    {
        MiddlewaresInvokerMiddleware.AddMiddlewares(builder.Services);
        return builder.UseMiddleware<MiddlewaresInvokerMiddleware>();
    }
}

使い方

コンフィグで有効化してクラスかメソッドに [UseMiddleware(typeof(ミドルウェア名))] をつけるだけです。

Program.cs
using Microsoft.Extensions.Hosting;

var host = new HostBuilder()
    // 今回実装したミドルウェアを有効化
    .ConfigureFunctionsWorkerDefaults(builder => builder.AddMiddlewaresInvokerMiddleware())
    .Build();

host.Run();

BaseControlelr.cs
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Middleware;

[UseMiddleware(typeof(BaseClassMiddleware))]
public class BaseController
{
}

public sealed class BaseClassMiddleware : IFunctionsWorkerMiddleware
{
    public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
    {
        Console.WriteLine($"[{nameof(BaseClassMiddleware)}] before call next(context)");
        await next(context);
        Console.WriteLine($"[{nameof(BaseClassMiddleware)}] after call next(context)");
    }
}
TestController.cs
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Azure.Functions.Worker.Middleware;

[UseMiddleware(typeof(TestClassMiddleware))]
public class TestController : BaseController
{
    [Function("Test01")]
    public async Task<HttpResponseData> Test01Async(
        [HttpTrigger(AuthorizationLevel.Anonymous, "get")]
        HttpRequestData requestData
    )
    {
        Console.WriteLine($"{nameof(Test01Async)} called");
        return requestData.CreateResponse();
    }

    [Function("Test02")]
    [UseMiddleware(typeof(TestClassMethodMiddleware))]
    public async Task<HttpResponseData> Test02Async(
        [HttpTrigger(AuthorizationLevel.Anonymous, "get")]
        HttpRequestData requestData
    )
    {
        Console.WriteLine($"{nameof(Test02Async)} called");
        return requestData.CreateResponse();
    }
}

public sealed class TestClassMiddleware : IFunctionsWorkerMiddleware
{
    public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
    {
        Console.WriteLine($"[{nameof(TestClassMiddleware)}] before call next(context)");
        await next(context);
        Console.WriteLine($"[{nameof(TestClassMiddleware)}] after call next(context)");
    }
}

public sealed class TestClassMethodMiddleware : IFunctionsWorkerMiddleware
{
    public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
    {
        Console.WriteLine($"[{nameof(TestClassMethodMiddleware)}] before call next(context)");
        await next(context);
        Console.WriteLine($"[{nameof(TestClassMethodMiddleware)}] after call next(context)");
    }
}

http://localhost:7071/api/Test01 にアクセスしたときのログ:

[BaseClassMiddleware] before call next(context)
[TestClassMiddleware] before call next(context)
Test01Async called
[TestClassMiddleware] after call next(context)
[BaseClassMiddleware] after call next(context)
  1. 親クラスに指定したミドルウェア
  2. 子クラスに指定したミドルウェア

の順で実行されます。

 
http://localhost:7071/api/Test02 にアクセスしたときのログ:

[BaseClassMiddleware] before call next(context)
[TestClassMiddleware] before call next(context)
[TestClassMethodMiddleware] before call next(context)
Test02Async called
[TestClassMethodMiddleware] after call next(context)
[TestClassMiddleware] after call next(context)
[BaseClassMiddleware] after call next(context)
  1. 親クラスに指定したミドルウェア
  2. 子クラスに指定したミドルウェア
  3. メソッドに指定したミドルウェア

の順で実行されます。

ちなみに

複数 [UseMiddleware()] を書いたときに GetCustomAttributes() でソースコードに書かれた順番に返ってくる保証はないので、 [CallerLineNumber] で行番号を取得して順番を保証しています。

 
参考: Get properties in order of declaration using reflection

使ってみた感想

  • これが「顧客が本当にほしかったもの」だ!
  • 親クラスに指定した [UseMiddleware(..)] が適用されるのは継承が深くなるとバグの温床になりそう
    • ログ取る/エラーメール飛ばす のような必須処理に使うのはいいけど、多用は禁物
    • 継承の機能は使わずに毎回クラス個別に指定するのが安全かも

終わり

実務でははさらに SkipMiddlewareAttribute を実装してたり Order プロパティで実行順を上書きできるようにしてたりしますが、とりあえず今回紹介した UseMiddlewareAttribute の実装だけでもかなり便利だと思います。

ぜひ使ってみてください。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?