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が良さそう
- 一気に全体に適用したい&個別で細かく指定したい
- 親クラスに適用したら子クラスにも適用したい
- メソッドにも個別で指定したい
つまりこんな感じ。
[UseMiddleware(typeof(BaseMiddleware))]
public class BaseController
{
}
[UseMiddleware(typeof(DerivedMiddleware))]
public class DerivedController : BaseController
{
[UseMiddleware(typeof(FooMiddleware))]
public Task FooAsync()
{
// エンドポイント個別の処理
}
}
で、こんな感じの順番で実行。
BaseMiddlewareの実行
DerivedMiddlewareの実行
FooMiddlewareの実行
エンドポイント個別の処理
実装
というわけで実装してみました。
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;
}
}
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(ミドルウェア名))]
をつけるだけです。
using Microsoft.Extensions.Hosting;
var host = new HostBuilder()
// 今回実装したミドルウェアを有効化
.ConfigureFunctionsWorkerDefaults(builder => builder.AddMiddlewaresInvokerMiddleware())
.Build();
host.Run();
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)");
}
}
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)
- 親クラスに指定したミドルウェア
- 子クラスに指定したミドルウェア
の順で実行されます。
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)
- 親クラスに指定したミドルウェア
- 子クラスに指定したミドルウェア
- メソッドに指定したミドルウェア
の順で実行されます。
ちなみに
複数 [UseMiddleware()]
を書いたときに GetCustomAttributes()
でソースコードに書かれた順番に返ってくる保証はないので、 [CallerLineNumber]
で行番号を取得して順番を保証しています。
参考: Get properties in order of declaration using reflection
使ってみた感想
- これが「顧客が本当にほしかったもの」だ!
- 親クラスに指定した
[UseMiddleware(..)]
が適用されるのは継承が深くなるとバグの温床になりそう- ログ取る/エラーメール飛ばす のような必須処理に使うのはいいけど、多用は禁物
- 継承の機能は使わずに毎回クラス個別に指定するのが安全かも
終わり
実務でははさらに SkipMiddlewareAttribute
を実装してたり Order
プロパティで実行順を上書きできるようにしてたりしますが、とりあえず今回紹介した UseMiddlewareAttribute
の実装だけでもかなり便利だと思います。
ぜひ使ってみてください。