.NET Azure Functions supports the dependency injection (DI) software design pattern to achieve Inversion of Control (IoC) between classes and their dependencies. The official article explains how to implement dependency injection in Azure .NET Functions.
2 key points are summarized as below:
1. Create a custom Startup.cs that inherits from FunctionsStartup class from the Microsoft.Azure.Functions.Extensions NuGet package. This startup class is meant for setup and dependency registration only but not for using any of the registered services during startup process. The startup class runs once and only once to build up the ServiceCollection when the function host starts up.
2. Service lifetimes:
This blog will show you a coding pitfall in implementing dependency injection in .NET azure functions to help you better understand the registered service lifetime, which is one of the most important parts in the DI world.
Let us review the code first:
public class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
new ConfigurationBuilder()
.SetBasePath(Environment.CurrentDirectory)
.AddJsonFile("local.settings.json", true, true)
.AddEnvironmentVariables()
.Build();
builder.Services.AddLogging();
AppleDeliveryService appleDeliveryService = new AppleDeliveryService(new HttpClient());
BananaDeliveryService bananaDeliveryService = new BananaDeliveryService(new HttpClient());
builder.Services.AddTransient<IFruitDeliveryCoordinator>(cls => new FruitDeliveryCoordinator(bananaDeliveryService, appleDeliveryService));
}
}
public interface IBananaDeliveryService
{
void DeliverBanana(string address, ILogger log);
}
public interface IAppleDeliveryService
{
void DeliverApple(string address, ILogger log);
}
public interface IFruitDeliveryCoordinator
{
void DeliverFruit(string address, ILogger log);
}
public class BananaDeliveryService : IBananaDeliveryService
{
private readonly HttpClient client;
public string Random { get; }
public BananaDeliveryService(HttpClient client)
{
this.client = client;
Random = Guid.NewGuid().ToString();
}
public void DeliverBanana(string address, ILogger log)
{
log.LogInformation("Deliver banana by guid - {Random}", Random);
this.client.DefaultRequestHeaders.Add("Banana-Delivery-Key", "Banana-Delivery-Value");
// HttpContent httpContent = new StringContent(JsonConvert.SerializeObject(requestBody), Encoding.UTF8, "application/json");
// HttpResponseMessage response = await _client.PostAsync(url, httpContent);
// string responseJson = await response.Content.ReadAsStringAsync();
return;
}
}
public class AppleDeliveryService : IAppleDeliveryService
{
private readonly HttpClient client;
public string Random { get; }
public AppleDeliveryService(HttpClient client)
{
this.client = client;
Random = Guid.NewGuid().ToString();
}
public void DeliverApple(string address, ILogger log)
{
log.LogInformation("Deliver apple by guid - {Random}", Random);
this.client.DefaultRequestHeaders.Add("Apple-Delivery-Key", "Apple-Delivery-Value");
// HttpContent httpContent = new StringContent(JsonConvert.SerializeObject(requestBody), Encoding.UTF8, "application/json");
// HttpResponseMessage response = await _client.PostAsync(url, httpContent);
// string responseJson = await response.Content.ReadAsStringAsync();
return;
}
}
public class FruitDeliveryCoordinator : IFruitDeliveryCoordinator
{
private readonly IBananaDeliveryService bananaDeliveryService;
private readonly IAppleDeliveryService appleDeliveryService;
public string Random { get; }
public FruitDeliveryCoordinator(IBananaDeliveryService bananaDeliveryService,
IAppleDeliveryService appleDeliveryService)
{
this.bananaDeliveryService = bananaDeliveryService;
this.appleDeliveryService = appleDeliveryService;
Random = Guid.NewGuid().ToString();
}
public void DeliverFruit(string address, ILogger log)
{
log.LogInformation("Fruit Coordinator by guid - {Random}", Random);
bananaDeliveryService.DeliverBanana(address, log);
appleDeliveryService.DeliverApple(address, log);
}
}
public class DeliveryFunction
{
private readonly ILogger<DeliveryFunction> log;
private readonly IFruitDeliveryCoordinator fruitDeliveryCoordinator;
public DeliveryFunction(IFruitDeliveryCoordinator fruitDeliveryCoordinator, ILogger<DeliveryFunction> log)
{
this.fruitDeliveryCoordinator = fruitDeliveryCoordinator;
this.log = log;
}
[FunctionName("FruitDeliveryFunction")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req)
{
log.LogInformation("C# HTTP trigger function processed a request.");
fruitDeliveryCoordinator.DeliverFruit("Gru address", log);
string name = req.Query["name"];
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
dynamic data = JsonConvert.DeserializeObject(requestBody);
name = name ?? data?.name;
string responseMessage = string.IsNullOrEmpty(name)
? "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response."
: $"Hello, {name}. This HTTP triggered function executed successfully.";
return new OkObjectResult(responseMessage);
}
}
The Problem:
The fruit delivery service hosted on the Azure App Service is operating well for some time, then it would be broken with consistent 400 errors "The size of the request headers is too long" or equivalent messages. The problem can be mitigated by a function app restart which typically means it is an application layer issue.
Root Cause Analysis:
new
keyword where the FruitDeliveryCoordinator was injected as a transient service. builder.Services.AddLogging();
AppleDeliveryService appleDeliveryService = new AppleDeliveryService(new HttpClient());
BananaDeliveryService bananaDeliveryService = new BananaDeliveryService(new HttpClient());
builder.Services.AddTransient<IFruitDeliveryCoordinator>(cls => new FruitDeliveryCoordinator(bananaDeliveryService, appleDeliveryService));
new
keyword as a part of the AppleDeliveryService instantiation, so a single instance is created when the function host is started up, hence all of the transient AppleDeliveryService are referencing the same instance of httpclient. If one call many times of “client.DefaultRequestHeaders.Add()”, it will have the header get longer and longer as it appends the new value instead of replacing it, eventually hit the maxRequestBytes restricted by IIS and the web server will return 400 Bad Request.
Fixes:
builder.Services.AddTransient<IFruitDeliveryCoordinator>(cls => new FruitDeliveryCoordinator(new BananaDeliveryService(new HttpClient()), new AppleDeliveryService(new HttpClient())));
Or, an equivalent fix is to ingest all of the dependencies as transient services, the request header accumulation won't happen as well since the httpclient instance is no longer to be a single instance. builder.Services.AddTransient<IBananaDeliveryService, BananaDeliveryService>();
builder.Services.AddTransient<IAppleDeliveryService, AppleDeliveryService>();
builder.Services.AddTransient<IFruitDeliveryCoordinator, FruitDeliveryCoordinator>();
However, both of the fixes smell BAD, because it violates the guidelines of developing an azure function that NOT to create a new client with every function invocation, for a function app hosted on the Azure platform, it would hold more connections than necessary and eventually lead to the SNAT port exhaustion issue, more explanations about the SNAT port exhaustion can be found at the SNAT official blog. new
instantiation approach but it is more elegant.builder.Services.AddSingleton<IBananaDeliveryService, BananaDeliveryService>();
builder.Services.AddSingleton<IAppleDeliveryService, AppleDeliveryService>();
builder.Services.AddTransient<IFruitDeliveryCoordinator, FruitDeliveryCoordinator>();
Then add an if clause to check if the same request header already exists in the DefaultRequestHeaders collection of the httpclient.
if (!this.client.DefaultRequestHeaders.Contains("Banana-Delivery-Key"))
{
this.client.DefaultRequestHeaders.Add("Banana-Delivery-Key", "Banana-Delivery-Value");
}
Take-aways:
new
instantiation with the DI approach. DI only works for classes created by DI. If you create classes with new
keyword, there is no DI happening.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.