10
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Azure Function v2のDIサンプル

Last updated at Posted at 2019-04-20

はじめに

Azure Functio v2のDIサポートが少しづつ進んでいて、
コンストラクタインジェクションが出来るようになっています。

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

HttpClientStartup.cs
[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

HttpClientFunction.cs
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

IConfigurationFunction.cs
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

DocumentClientStartup.cs
[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

DocumentClientFunction.cs
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

DbContextStartup.cs
[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

DocumentClientFunction.cs
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側で登録されるのは、
ドキュメント化されたので正式なサポートのようです。

10
8
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
10
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?