Search code examples
entity-frameworkdependency-injectionazure-functionstimer-trigger

TimerTrigger does not inject EF Database Context


I have an Azure Function (v3) using Entity Framework (3.0.11).

I am attempting to run the code on a TimerTrigger however injecting the database within a timer trigger does not seem to work.

Here are some (rapidly anonymized) code samples.

the CSPROJ

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <AzureFunctionsVersion>v3</AzureFunctionsVersion>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="AzureFunctions.Extensions.DependencyInjection" Version="1.1.3" />
    <PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.1.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.10" />
    <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.11" />
  </ItemGroup>
  <ItemGroup>
    <None Update="host.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update="local.settings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <CopyToPublishDirectory>Never</CopyToPublishDirectory>
    </None>
  </ItemGroup>
</Project>

a model and DBContext

namespace DataImport
{
    public class Sample
    {
        public int SampleID { get; set; }
        public string SampleField { get; set; }
    }

    public class MyDbContext : DbContext
    {
        public MyDbContext(DbContextOptions<MyDbContext> options) : base(options) { }
        public virtual DbSet<Sample> MyRecords { get; set; }
    }
}

a startup class

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;

[assembly: FunctionsStartup(typeof(DataImport.Startup))]
namespace DataImport
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            string con = builder.GetContext().Configuration.GetSection("ConnectionStrings:DefaultConnection").Value.ToString();
            builder.Services.AddDbContext<MyDbContext>(config => config.UseSqlServer(con));
        }
    }
}

a Program.cs

using System;
using System.Linq;
using System.Threading.Tasks;
using AzureFunctions.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace DataImport
{


    public class Program
    {
        private readonly MyDbContext db;
        public Program(MyDbContext database)
        {
            db = database;
        }

        [FunctionName("SampleFunction_works")]
        public async Task<IActionResult> HttpRun([HttpTrigger(AuthorizationLevel.Anonymous, "GET")] HttpRequest req, ILogger log, ExecutionContext context)
        {
            log.LogInformation($"C# Timer trigger function executed at: {DateTime.Now}");
            var foo = db.MyRecords.Where(c => c.SampleField == "000").FirstOrDefault();
            await db.MyRecords.AddAsync(new Sample());
            log.LogInformation(foo.SampleField);
            return new OkObjectResult(foo);
        }

        [FunctionName("SampleFunction_no_work")]
        public static void Run([TimerTrigger("%TimerInterval%")] TimerInfo myTimer, ILogger log, ExecutionContext context)
        {
            log.LogInformation($"C# Timer trigger function executed at: {DateTime.Now}");
            // tried dozens of things here, nothing works sofar.
            // injecting IServiceProvider fails,
            // what other ways to solve this?
            // could a timer trigger perhaps make an HTTP call to the HttpRun function above? 
        }
    }
}

when running the SampleFunction_works with a database connection we see the result of the function call as successful. Injection works within the context of an HTTP trigger. On a timertrigger however, this does not work.

I have tried a good 8 hours of different things at this point:

  • unsurprisingly accessing the db without injecting turns up a null property, no magic there.
  • adding MyDbContext to the Run function fails because it can't be injected public static void Run([TimerTrigger("%TimerInterval%")] TimerInfo myTimer, ILogger log, MyDbContext db)
Microsoft.Azure.WebJobs.Host: Error indexing method 'SampleFunction_no_work'. Microsoft.Azure.WebJobs.Host: Cannot bind parameter 'db' to type MyDbContext. Make sure the parameter Type is supported by the binding. If you're using binding MyDbContext(e.g. Azure Storage, ServiceBus, Timers, etc.) make sure you've called the registration method for the extension(s) in your startup code (e.g. builder.AddAzureStorage(), builder.AddServiceBus(), builder.AddTimers(), etc.).
  • doing the same as the previous but by adding IServiceProvider services to the method signature results in a similar error message, adding the line db = services.GetRequiredService<MyDbContext>(); is irrelevant if it can't get injected
  • some variables DO seem to be injectable in this scope ExecutionContext for example, but there doesn't seem to be anything I can use on that object.

Is there a way to:

  1. inject a timer trigger with a database?
  2. use a timer trigger to CALL an HTTPtrigger located within the same function?
  3. any other solution that will allow me to access an EF database within a timertrigger context?

update:

@StevePy's comment below was correct. You can make a timertrigger's RUN method non-static and leverage the power of injection. I'd previously read that this wasn't possible, but it appears that information was out of date.

See this BLOG post for more info: https://marcroussy.com/2019/05/31/azure-functions-built-in-dependency-injection/

Or grab this sample code to run for yourself locally:

        [FunctionName("MY_FANCY_FUCNTION")]
        public async Task Run([TimerTrigger("%TimerInterval%")] TimerInfo myTimer, ILogger ilog, ExecutionContext context)
        {
            ilog.LogInformation($"TIMER EXECUTED IS DB NULL? '{db == null}'");
            // note that the key part of this DOES log out as NOT NULL
            // which is what we want.
            return;
            await Main(ilog, context);
        }

Solution

  • Try using a non-static Run method. Many examples use a static method which can be recommended where you don't have dependencies and the method is pure. (since Functional methods should strive to be pure) See https://marcroussy.com/2019/05/31/azure-functions-built-in-dependency-injection/ for an example of TimerTriggers /w DI.