今回は C# の Http Trigger の Azure Functions の DI する際の Tips です。
例えば、こんなパス /api/costomers/abc123/order
にアクセスする際、abc123
が顧客の ID でその値を使って DI したいときとかの話です。後述しますがなんでも DI した方がいいってこともなく、Factory パターンを使っていい感じにインスタンスを生成・管理するってケースもあるので設計には注意が必要ですが。
そんなことはさておきやってきましょう。
環境
今回の C# / Azure Functions のざっくりな環境は以下です。
- VS 2022 (v17.4.x)
- Azure Functions のプロジェクト関連:
- Functions worker:
.NET 6.0
- Microsoft.NET.Sdk.Functions (NuGet) :
v4.1.x
- Functions worker:
2023年1月時点では、.NET 7 Isolated はまだまだイマイチだし LTS でもないので、.NET 6.0 を使っています。
Http Request の情報を使って DI をする
さて本題です。
DI の準備
今回は、Visual Studio で DependencyInjectionWithHttpContextSample って名前の Function App を作りました。 で、Azure Functions で DI をするためのコードを追加するんですが、基本的には以下のドキュメントの通りなので詳しい設定方法省きます。
雑に書いておくと、ドキュメントにそって「サービスを登録する」セクションまでやれば OK です。Startup class の Configure
メソッドの中身は空っぽで OK です。今回使ってる NuGet パッケージのバージョン書いておきます。
- Microsoft.Azure.Functions.Extensions:
v1.1.0
- Microsoft.Extensions.DependencyInjection:
v6.0.1
DI を構成する
まずはサンプルとして DI で取得したい class をこんな感じで定義しました。
namespace DependencyInjectionWithHttpContextSample; public class SampleOptions { public string Hobby { get; set; } public string CustomerId { get; set; } public string UserId { get; set; } }
次は本題の Startup.cs です。
Http リクエストからの情報を取得するには、IHttpContextAccessor を使えば実現できます。
15行目で IHttpContextAccessor
のインスタンスを取得することで、Http Request に関する情報が取得できます。今回は代表的なパターンとして以下3つを取得してみました。
- Request header の情報 (18行目)
- リクエストで来たパスの情報 (22-23行目)
- query string の情報 (26行目)
※ 余談ですが、Options 的な class 書いたなら Options pattern 使おうよとかは、無駄にサンプルコードが増えるので今回は無しです。
DI を構成する際の注意点
DI を構成する際の注意点は色々ありますが、1つだけ書いておくとすればインスタンスの lifetime (またはライフサイクル) の設定を適切にしましょうってところでしょうか。
Function App (C#) での lifetime は、 Singleton
or Scoped
or Transient
が構成できます。DB とか外部アクセスのクライアントの DI は lifetime を間違えることで重大なバグの温床になりかねないので、ユースケースや SDK の仕様にあった適切な設定をしましょう。
余談として、Singleton
or Scoped
or Transient
の違いは、公式ドキュメントのこちら だとわかりずらいと感じた場合、以下のブログで具体的な動作の違いを解説したので参考になるかもです。すごーく昔に書いた ASP.NET に関する話ですが、Singleton
or Scoped
or Transient
の概念はこの時から一緒です。
Function のコード
本題に戻りますが、あとは Function のコードでいい感じに呼び出せばいいだけです。今回は Constructor injectionで取得した SampleOptions
class のインスタンスを返すだけのコードです。
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.Extensions.Logging; using System.Threading.Tasks; namespace DependencyInjectionWithHttpContextSample; public class Function1 { private readonly SampleOptions _options; public Function1(SampleOptions options) { _options = options; } [FunctionName("Function1")] public async Task<IActionResult> Run( [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "customers/{customerId}/order")] HttpRequest req, ILogger log) { return new OkObjectResult(_options); } }
リクエストを送信して動作確認
こんな感じで POSTMAN からリクエストを送信してみます。
そうすると、以下のようにレスポンスが返ってくるので想定通りの動作になっていることが確認できます。
(補足) DI よりも Factory pattern が扱いやすいケースもある
参考までに、場合によっては DI を使わず Factory pattern が扱いやすいこともあるでしょう。そんなときは以下のコードでが実現できます。
[FunctionName("Function1")] public async Task<IActionResult> Run( [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "customers/{customerId}/order")] HttpRequest req, ILogger log) { var options = SampleOptionsFactory.Create(req); return new OkObjectResult(options); } internal class SampleOptionsFactory { internal static SampleOptions Create(HttpRequest req) { return new SampleOptions { UserId = req.Headers["X-MS-CLIENT-PRINCIPAL-ID"], CustomerId = req.Path.ToString().Split('/')[3], Hobby = req.Query["hobby"] }; } }
まとめ
用途に合わせていい感じに Function を活用しましょうってお話しでした。
1年以上前に作った自分のタスクを今更消化した...