The Coding Dojo

I wanted to make an Azure Function that could return a result from a database, and as you can imagine, this was a total breeze with lot's of examples available. What I really wanted to do though, was use practices and patterns that I am familiar with and trust. I wanted to provide my DbContext via the DI pipeline. As it turns out, I was in luck again. I found a really nice NuGet package Willezone.Azure.WebJobs.Extensions.DependencyInjection which helped me do just that.

There are a couple of little gotchas to tackle first.

  1. Make sure your function's TargetFramework is set to netstandard2.0.
  2. As of writing this post, any Core package references cannot be any higher than 2.1.x. If you are referencing Core packages 2.2.0 +, you really need to drop them down 2.1.x for the time being. You can follow the issue here.

With those wee bumps out of the way, let's start. I Setup an EF DbContext as I would any other app. Here's what my RazorTemplate data model looks like.

namespace Function.RazorTemplateService
{
    public class RazorTemplate 
    {
        public int Id { get; set; }
        public string TemplateKey { get; set; }
        public DateTimeOffset LastModifiedDate { get; set; }
        public string RazorContent { get; set; }
    }
}

and this is my very simple DbContext

Namespace Function.RazorTemplateService
{
    public class RazorTemplateContext : DbContext, IRazorTemplateContext
    {
        public RazorTemplateContext(DbContextOptions<RazorTemplateContext> options)
            : base(options)
        { }

        public DbSet<RazorTemplate> RazorTemplates { get; set; }
    }
}

I added my DB connection string to the local.settings.json file. Mental Note: Don't forget to copy this setting to Azure when I deploy.

{
 ...
 "ConnectionStrings": {
    "SqlConnectionString": "Server=tcp:myspecialguy.database.windows.net,1433..."
  }
}

Now for the DI bit. You can obviously give this a shot on your own, However, this has already been tackled, and with this NuGet package, it was a breeze. There's two things I really cared about here. 1) Accessing my configuration 2) Adding my config and DbContext to the DI pipe. Having a familiar pattern to setup my Functions really does help the whole process sink in.

[assembly: WebJobsStartup(typeof(Startup))]
namespace Function.RazorTemplateService
{
    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();

            services.AddDbContext<IRazorTemplateContext, RazorTemplateContext>(options => options.UseSqlServer(connectionString));
            services.AddSingleton(_configuration);
            services.AddSingleton<ILogger>(_ => _loggerFactory.CreateLogger(LogCategories.CreateFunctionUserCategory("Common")));

            return services.BuildServiceProvider();
        }
    }
}

The Willezone.Azure.WebJobs.Extensions.DependencyInjection docs tell me I needed to include a Directory.Build.targtes file. Here's what it looks like. This was just a copy and paste job.

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

Finally, here's my function. Pretty simple huh? I've injected my db context, and am querying my data store, looking for a template with the key that has been provided in the query string.

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

            var template = await razorTemplateContext.RazorTemplates.FirstOrDefaultAsync(t => t.TemplateKey == key);

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

            return new OkObjectResult(template);
        }
    }
}

And that's really it. You may think it's a little overkill to use DI for such a simple task, you may not. I personally feel like it's a good pattern, and with patterns comes consistency. For now, I'm definitely going to continue down this path. 😊