はじめに
Azure Functio v2のDIサポートが少しづつ進んでいて、
コンストラクタインジェクションが出来るようになっています。
- azure-functions-host issue #3736 Dependency Injection support for Functions
- しばやん雑記 Azure Functions v2 でインスタンスメソッドも Function として利用可能に
UnitTestが書きやすくなるので、重宝しています。
まだドキュメントが整備されておらず、取っ付き難さがあるので、
よく使いそうなものを サンプルとしてまとめてみました。
おかしいところがあったら突っ込みをぜひお願いします。
ソース全体はこちら GitHub
HttpClient(Factory + Polly)
NuGet
Microsoft.Azure.Functions.Extensions 1.0.0
Microsoft.NET.Sdk.Functions >= 1.0.27
Microsoft.Extensions.Http.Polly 2.2.0
Startup
[assembly: FunctionsStartup(typeof(AzFuncDISample.HttpClientStartup))]
namespace AzFuncDISample
{
public class HttpClientStartup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
//HttpClient
builder.Services.AddHttpClient("httpstat", (provider,httpClient) =>
{
var configuration = provider.GetRequiredService<IConfiguration>();
var hostName = configuration.GetValue<string>("Test:HostName");
httpClient.BaseAddress = new Uri($"https://{hostName}/");
httpClient.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
})
.ConfigurePrimaryHttpMessageHandler(() => GetHttpMessageHandler());
//HttpClient with Polly
builder.Services.AddHttpClient("httpstatWithPolly", (provider, httpClient) =>
{
var configuration = provider.GetRequiredService<IConfiguration>();
var hostName = configuration.GetValue<string>("Test:HostName");
httpClient.BaseAddress = new Uri($"https://{hostName}/");
httpClient.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
})
.SetHandlerLifetime(System.Threading.Timeout.InfiniteTimeSpan)
.AddPolicyHandler(HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(msg => msg.StatusCode == HttpStatusCode.NotFound || (int)msg.StatusCode == 429)
.Or<TimeoutRejectedException>()
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))
))
.AddPolicyHandler(Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(10)))
.ConfigurePrimaryHttpMessageHandler(() => GetHttpMessageHandler());
}
}
}
Function
public class HttpClientFunction
{
private readonly IHttpClientFactory httpClientFactory;
private readonly HttpClient httpClient1;
private readonly HttpClient httpClient2;
public HttpClientFunction(IHttpClientFactory httpClientFactory)
{
this.httpClientFactory = httpClientFactory;
httpClient1 = this.httpClientFactory.CreateClient("httpstat");
httpClient2 = this.httpClientFactory.CreateClient("httpstatWithPolly");
}
[FunctionName(nameof(HttpClientFunction))]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function)] HttpRequest request,
ILogger log)
{
string responseText1 = await httpClient1.GetStringAsync("/200");
string responseText2 = null;
try
{
responseText2 = await httpClient2.GetStringAsync("/429");
}
catch (Exception ex)
{
responseText2 = ex.ToString();
}
return new OkObjectResult(new
{
responseText1,
responseText2,
});
}
}
IConfiguration
Startup
不要
Function
public class IConfigurationFunction
{
private readonly IConfiguration configuration;
public IConfigurationFunction(IConfiguration configuration)
{
this.configuration = configuration;
}
[FunctionName(nameof(IConfigurationFunction))]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function)] HttpRequest request,
ILogger log)
{
var configValue = configuration.GetValue<string>("key");
return new OkObjectResult(new
{
configValue,
});
}
}
Cosmos DB(DocumentDB Client)
NuGet
Microsoft.Azure.Functions.Extensions 1.0.0
Microsoft.NET.Sdk.Functions >= 1.0.27
Microsoft.Azure.DocumentDB.Core 2.3.0
Startup
[assembly: FunctionsStartup(typeof(AzFuncDISample.DocumentClientStartup))]
namespace AzFuncDISample
{
public class DocumentClientStartup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddSingleton<IDocumentClient>(provider => {
var configuration = provider.GetRequiredService<IConfiguration>();
var accountEndpoint = new Uri(configuration.GetValue<string>("Cosmos:AccountEndpoint"));
var accountKey = configuration.GetValue<string>("Cosmos:AccountKey");
var connectionPolicy = new ConnectionPolicy
{
ConnectionMode = ConnectionMode.Direct,
ConnectionProtocol = Protocol.Tcp,
};
connectionPolicy.RetryOptions.MaxRetryAttemptsOnThrottledRequests = 5;
connectionPolicy.RetryOptions.MaxRetryWaitTimeInSeconds = 60;
return new DocumentClient(accountEndpoint, accountKey, connectionPolicy);
});
}
}
}
Function
public class DocumentClientFunction
{
private readonly IDocumentClient documentClient;
public DocumentClientFunction(IDocumentClient documentClient)
{
this.documentClient = documentClient;
}
[FunctionName(nameof(DocumentClientFunction))]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function)] HttpRequest request,
ILogger log)
{
//適当
var databaseAccount = await documentClient.GetDatabaseAccountAsync();
return new OkObjectResult(new
{
databaseId = databaseAccount.Id,
});
}
}
EFCore(SQLServer)(DBContext)
NuGet
Microsoft.Azure.Functions.Extensions 1.0.0
Microsoft.NET.Sdk.Functions >= 1.0.27
Microsoft.EntityFrameworkCore.SqlServer 2.2.4
Startup
[assembly: FunctionsStartup(typeof(AzFuncDISample.DbContextStartup))]
namespace AzFuncDISample
{
public class DbContextStartup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddDbContext<SampleContext>((provider,options) =>
{
var configuration = provider.GetRequiredService<IConfiguration>();
options.UseSqlServer(configuration.GetConnectionString("SampleConnection"));
});
}
}
//適当
public class SampleContext : DbContext
{
public SampleContext(DbContextOptions<SampleContext> options)
: base(options)
{ }
public DbSet<Sample> Samples { get; set; }
}
public class Sample
{
public int Id { get; set; }
public string Value { get; set; }
}
}
Function
public class DbContextFunction
{
private readonly SampleContext dbContext;
public DbContextFunction(SampleContext dbContext)
{
this.dbContext = dbContext;
}
[FunctionName(nameof(DbContextFunction))]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function)] HttpRequest request,
ILogger log)
{
//適当
dbContext.Database.OpenConnection();
using (var connection = dbContext.Database.GetDbConnection())
using (var command = connection.CreateCommand())
{
command.CommandText = "SELECT COUNT(*) FROM sys.tables";
var count = (int)await command.ExecuteScalarAsync();
return new OkObjectResult(count);
}
}
}
気になってるところ
IWebJobsStartup内でIConfigurationの取り出し方、本当に↓でよいか…?(とりあえず動くけど、はしご外されそうで怖い)
var configuration = builder.Services
.Where(s => s.ServiceType == typeof(IConfiguration)).First()
.ImplementationInstance as IConfiguration;
- 2019/07/11 追記
ServiceCollection追加時にFactoryメソッド内で、
IServiceProvider.GetRequierdService()を使用するのが正解だった
builder.Services.AddDbContext<SampleContext>((provider,options) =>
{
var configuration = provider.GetRequiredService<IConfiguration>();
options.UseSqlServer(configuration.GetConnectionString("SampleConnection"));
});
終わりに
DI使ったFunction量産してたんですが、3月中旬のランタイムv2.0.12353のバグ↓にやられました。
azure-functions-host issue [#4203 Depdency Injection and Logging start failing on 2.0.12353] コメント
やっぱり、バージョン固定がいいんですかねぇ…?
2019/5/9 追記
Buildと同時に正式サポートとなったようです
公式ドキュメントはこちら
Use dependency injection in .NET Azure Functions | Microsoft Docs
正式な作法としては、
NuGetPackage Microsoft.Azure.Functions.Extensions 1.0.0の参照を追加して、
IWebJobsStartup
ではなく、FunctionsStartup
を使用するようになったようです。
上記サンプルと GitHub も併せて修正しています。
内部的にはIWebJobsStartup
の薄いラッパーでした。
GitHub Azure/azure-functions-dotnet-extensions
Microsoft.Extensions.Configuration.IConfigurationがFunctionHost側で登録されるのは、
ドキュメント化されたので正式なサポートのようです。