The Coding Dojo

Things Just Got Better:
Microsoft have finally released a greatly improved DI mechanism for functions. Check out their Sample Code and Docs

We know Azure Functions, we know Dependency Injection. Let's stick 'em together and create an environment we can really work with. You can of course have a go at building this out yourself, though I'm going to be using a pretty sweet NuGet package to get things going a little faster. The source code is available on GitHub too.

Go ahead and add an Azure Function V2 to your project. V2 supports .Net Core, but we're going to need to make a wee tweak to our project file before we continue. We need to change the target framework from netcoreapp2.0 to netstandard2.0.

Update The Target Version.

In your project file, switch out

<TargetFramework>netcoreapp2.0</TargetFramework>

with

<TargetFramework>netstandard2.0</TargetFramework>

Add The DI Package.

Install the DI NuGet package by Boris Wilhelms. This package has quite a following and it's user base is certainly growing.

Important: Your function app (at the time of writing) can't reference any core packages higher than 2.1.x (e.g. 2.2.x). This is currently a known issue, which I'm sure will be resolved. This is certainly not a show stopper for me, not in Azure Function land anyway. This issue can be tracked here.

Azure Deployment Fix.

A slight tweak is required to ensure your extension.json file is created properly when deploying to Azure. Create a new file called Directory.Build.targets and copy the following config to it

<Project>
  <PropertyGroup>
    <_IsFunctionsSdkBuild Condition="$(_FunctionsTaskFramework) != ''">true</_IsFunctionsSdkBuild>
    <_FunctionsExtensionsDir>$(TargetDir)</_FunctionsExtensionsDir>
    <_FunctionsExtensionsDir Condition="$(_IsFunctionsSdkBuild) == 'true'">$(_FunctionsExtensionsDir)bin</_FunctionsExtensionsDir>
  </PropertyGroup>

  <Target Name="CopyExtensionsJson" AfterTargets="_GenerateFunctionsAndCopyContentFiles">
    <Message Importance="High" Text="Overwritting extensions.json file with one from build." />

    <Copy Condition="$(_IsFunctionsSdkBuild) == 'true' AND Exists('$(_FunctionsExtensionsDir)\extensions.json')"
          SourceFiles="$(_FunctionsExtensionsDir)\extensions.json"
          DestinationFiles="$(PublishDir)bin\extensions.json"
          OverwriteReadOnlyFiles="true"
          ContinueOnError="true"/>
  </Target>
</Project>

You can find out more about this here.

That's the ground work and setup done. Now let's get onto the fun cody stuff.

Crating A Startup Class.

Create yourself a Startup.cs class, and paste in the following code.

[assembly: WebJobsStartup(typeof(Startup))]
namespace Function.MySpecialService
{
    internal class Startup : IWebJobsStartup
    {
        public void Configure(IWebJobsBuilder builder) =>
            builder.AddDependencyInjection<ServiceProviderBuilder>();
    }

    internal class ServiceProviderBuilder : IServiceProviderBuilder
    {
        private readonly ILoggerFactory _loggerFactory;
        private readonly IConfiguration _configuration;

        public ServiceProviderBuilder(IConfiguration configuration, ILoggerFactory loggerFactory)
        {
            _configuration = configuration;
            _loggerFactory = loggerFactory;
        }

        public IServiceProvider Build()
        {
            var connectionString = _configuration.GetConnectionString("SqlConnectionString");
            var services = new ServiceCollection();

            // -- Add Your DI Stuff Here -- //

            services.AddDbContext<IMyDataBaseContext, MyDataBaseContext>(options => options.UseSqlServer(connectionString));
            services.AddSingleton(_configuration);
            services.Configure<AppConfiguration>(options =>
            {
                options.ApiKey = _configuration.GetValue<string>("ApiKey");
                options.AnotherSetting = true;
            });
            services.AddSingleton<ILogger>(_ => _loggerFactory.CreateLogger(LogCategories.CreateFunctionUserCategory("Common")));

            // --------------------------- //
            
            return services.BuildServiceProvider();
        }
    }
}

In the example above, I have am pulling IConfiguration and ILoggerFactory into the constructor so I have access to this functionality whilst building out my own DI content. You can see that, for the best part, you build out your DI like you would in any other core or standard app, adding DBContext, interfaces and their implementations as Singleton, Transient or Scoped services.

Accessing Your DI Collection.

To access services in your DI collection from your function, simply add a parameter to your function using the [Inject] attribute. Let's say I wanted to pull out MyDataBaseContext, and use it to load some data from my data store, my function may look something like this

namespace Function.MySpecialService
{
    public static class MySpecialService
    {
        [FunctionName("GetStuff")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", Route = "GetStuff/{key}")] HttpRequest req,
            [Inject] IMyDataBaseContext myDataBaseContext,
            string key,
            ILogger log)
        {            
            if (string.IsNullOrWhiteSpace(key))
                return new BadRequestObjectResult("parameter 'Key' cannot be null or empty.");

            var dbResult = await myDataBaseContext.DbStuff.FirstOrDefaultAsync(stuff => stuff.Key == key);

            if (dbResult == null)
                return new NotFoundResult();

            return new OkObjectResult(dbResult);
        }
    }
}

There you have it, DI in your Azure Function. This may seem a little over kill at times, of course, there is a time and a place. I really enjoy this pattern, and consistency is key for me, so there's a good chance I'll be following this pattern whenever possible.