Write better Azure Functions
This project aims at increasing usabiltiy of Azure Functions in real life applications with usage of custom input and output bindings. Azure Functions come with built in support for some triggers, inputs and outputs, mainly for Azure services like Cosmos DB, Azure Storage, Event Grid, Microsoft Graph etc. However, mature applications require more than just that: some sort of dependency injection for testability purposes; use of non-Azure services, like Redis; configurable parameters that are not hardcoded into the function. Custom input and output bindings provided by this project solve these problems in native Azure Functions way.
Binding | Purpose | Sample | Nuget |
---|---|---|---|
[Config("key")] |
Configuration via Application Settings | ConfigurationFunction | |
[Inject] |
Dependency Injection with Autofac | AutofacFunction | |
[Inject] |
Dependency Injection with built-in .NET Core container | InjectionFunction | |
[Inject] |
Dependency Injection with Unity containers | UnityFunction | |
[Redis("key")] |
Redis input and output with POCO support | RedisFunction |
Use [Inject] attribute to inject all your dependencies in Azure Function declaration.
[FunctionName("Example")]
public static IActionResult Run(
[HttpTrigger("GET")] HttpRequest request,
[Inject] IStorageAccess storageAccess)
{
...
}
Microsoft.Extensions.Configuration.IConfiguration instance is pre-registered for your convinience that you can use to read settings from local settings file or application settings depending on whether you running locally or on Azure respectively. In addition, Microsoft.Extensions.Logging.ILogger instance is also pre-registered for you to log to file system and App Insights. Just declare it as dependency in your implementation class anywhere in your dependency tree.
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
public class ValueProvider
{
public ValueProvider(IConfiguration configuration, ILogger logger)
{
_configuration = configuration;
_logger = logger;
}
public string ReadSetting(string settingName)
{
_logger.LogInformation($"Reading value of '{settingName}'");
return _configuration[settingName];
}
...
}
Supported IoC containers:
Create implementation of IDependencyConfig interface (public visibility) in your function's binary:
public class DependencyConfig : IDependencyConfig
{
public void RegisterComponents(ContainerBuilder builder)
{
builder
.RegisterType<StorageAccess>()
.As<IStorageAccess>();
}
}
For further details see working sample or function declarations in tests.
Register all your dependencies in Startup class:
[assembly: WebJobsStartup(typeof(InjectionFunctionSample.Startup))]
namespace InjectionFunctionSample
{
public class Startup : IWebJobsStartup
{
public void Configure(IWebJobsBuilder builder)
{
builder.Services.AddSingleton<ICache, CacheProvider>();
builder.Services.AddTransient<ICacheConfigProvider, CacheConfigProvider>();
builder.Services.AddTransient<IStorageAccess, StorageAccess>();
builder.Services.AddTransient<ITableAccess, CloudTableAccess>();
}
}
}
For further details see working sample or function declarations in tests. For details on how to use ASP.NET Core's ServiceCollection see official guide.
Create implementation of IDependencyConfig interface (public visibility) in your function's binary:
public class DependencyInjectionConfig : IDependencyConfig
{
public void RegisterComponents(UnityContainer container)
{
container.RegisterType<IStorageAccess, StorageAccess>();
}
}
For further details see working sample or function declarations in tests.
-
What if I need multiple containers for my application?
Azure Functions or any Function as a Service is a culmination of decades long effort towards reducing deployment, but more importatnly maintenance complexity by breaking down a monolith into applications to individual functions. So use it right, and separate your other function that needs a different container into a separate binary.
Some applications might have pre-production environments that require different set of parameters (settings) to be fed into your application, e.g. integration tests might have more aggressive timeouts or different integration URL for external service.
[FunctionName("ConfigFunctionExample")]
public static IActionResult Run(
[HttpTrigger("GET")] HttpRequest request,
[Config("StringSetting")] string stringValue,
[Config("IntSetting")] int intValue,
[Config("TimeSpanSetting")] TimeSpan timeSpanValue)
{
...
}
Here is a working sample. The binding supports simple types and string. In addition, it supports structs like DateTime, DateTimeOffset, Guid and TimeSpan. A full list of supported types can be found in integration tests.
[Redis] binding enables reading Redis strings:
[FunctionName("GetCachedString")]
public static IActionResult GetString(
[HttpTrigger("GET", Route = "cache/{key}")] HttpRequest request,
[Redis(Key = "{key}")] string cachedValue)
{
return new OkObjectResult(cachedValue);
}
OR you can deserialize (JSON) string keys into custom objects:
[FunctionName("GetPoco")]
public static IActionResult GetPoco(
[HttpTrigger("GET", Route = "poco/{key}")] HttpRequest request,
[Redis(Key = "{key}")] CustomObject cachedValue)
{
...
}
public class CustomObject
{
public int IntegerProperty { get; set; }
public string StringProperty { get; set; }
}
And of course your can write back to Redis:
[FunctionName("SetPoco")]
public static async Task<IActionResult> SetPoco(
[HttpTrigger("POST", Route = "poco/{key}")] HttpRequest request,
[Redis(Key = "{key}")] IAsyncCollector<CustomObject> collector)
{
string requestBody;
using (var reader = new StreamReader(request.Body))
{
requestBody = reader.ReadToEnd();
var value = JsonConvert.DeserializeObject<CustomObject>(requestBody);
await collector.AddAsync(value);
}
return new OkObjectResult(requestBody);
}
To configure your Redis connection string set it in RedisConfigurationOptions setting. See working sample or integration tests for full range of functionality.
This project is a consequence of building rehttp service using Azure Functions. I quickly came to realization that in order to build a reliable and maintainable service I was missing DI for unit testability, configurability for intergration testing and Redis POCO to keep my test code clean.