Azure Function v2のDIサンプル

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

* azure-functions-host issue [#3736 Dependency Injection support for Functions](https://github.com/Azure/azure-functions-host/issues/3736)
* しばやん雑記 [Azure Functions v2 でインスタンスメソッドも Function として利用可能に](https://blog.shibayan.jp/entry/20190120/1547967300)

UnitTestが書きやすくなるので、重宝しています。
まだドキュメントが整備されておらず、取っ付き難さがあるので、
よく使いそうなものを サンプルとしてまとめてみました。

おかしいところがあったら突っ込みをぜひお願いします。
ソース全体はこちら [GitHub](https://github.com/HiroyukiSakoh/QiitaAttachments/tree/master/2019/04/AzFuncDISample)

# HttpClient(Factory + Polly)
## NuGet
[Microsoft.Azure.Functions.Extensions 1.0.0](https://www.nuget.org/packages/Microsoft.Azure.Functions.Extensions/1.0.0)
[Microsoft.NET.Sdk.Functions >= 1.0.27](https://www.nuget.org/packages/Microsoft.NET.Sdk.Functions/1.0.27)
[Microsoft.Extensions.Http.Polly 2.2.0](https://www.nuget.org/packages/Microsoft.Extensions.Http.Polly/2.2.0)

## Startup
```cs: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
```cs: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
```cs: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](https://www.nuget.org/packages/Microsoft.Azure.Functions.Extensions/1.0.0)
[Microsoft.NET.Sdk.Functions >= 1.0.27](https://www.nuget.org/packages/Microsoft.NET.Sdk.Functions/1.0.27)
[Microsoft.Azure.DocumentDB.Core 2.3.0](https://www.nuget.org/packages/Microsoft.Azure.DocumentDB.Core/2.3.0)

## Startup
```cs: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
```cs: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](https://www.nuget.org/packages/Microsoft.Azure.Functions.Extensions/1.0.0)
[Microsoft.NET.Sdk.Functions >= 1.0.27](https://www.nuget.org/packages/Microsoft.NET.Sdk.Functions/1.0.27)
[Microsoft.EntityFrameworkCore.SqlServer 2.2.4](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.SqlServer/2.2.4)

## Startup
```cs: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
```cs: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の取り出し方、本当に↓でよいか…?(とりあえず動くけど、はしご外されそうで怖い）~~

```cs
var configuration = builder.Services
 .Where(s => s.ServiceType == typeof(IConfiguration)).First()
 .ImplementationInstance as IConfiguration;
```

* 2019/07/11 追記

ServiceCollection追加時にFactoryメソッド内で、
IServiceProvider.GetRequierdService<IConfiguration>()を使用するのが正解だった

```cs
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](https://github.com/Azure/azure-functions-host/issues/4203)]　[コメント](https://github.com/Azure/azure-functions-host/issues/3736#issuecomment-476765572)
やっぱり、バージョン固定がいいんですかねぇ…？


# 2019/5/9 追記
Buildと同時に[正式サポートとなった](https://azure.microsoft.com/en-us/blog/advancing-the-developer-experience-for-serverless-apps-with-azure-functions/)ようです

公式ドキュメントはこちら
[Use dependency injection in .NET Azure Functions | Microsoft Docs](https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-dotnet-dependency-injection)

正式な作法としては、
NuGetPackage [Microsoft.Azure.Functions.Extensions 1.0.0](https://www.nuget.org/packages/Microsoft.Azure.Functions.Extensions/1.0.0)の参照を追加して、
`IWebJobsStartup`ではなく、`FunctionsStartup`を使用するようになったようです。

上記サンプルと [GitHub](https://github.com/HiroyukiSakoh/QiitaAttachments/commit/4f28eb6ab5c08fdaa37e8ecb8365a86f1a7f5bf3) も併せて修正しています。

内部的には`IWebJobsStartup`の薄いラッパーでした。
[GitHub Azure/azure-functions-dotnet-extensions](https://github.com/Azure/azure-functions-dotnet-extensions/tree/master/src/Extensions/DependencyInjection)

Microsoft.Extensions.Configuration.IConfigurationがFunctionHost側で登録されるのは、
[ドキュメント化された](https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-dotnet-dependency-injection#function-app-provided-services)ので正式なサポートのようです。