Dependency Injection Using Azure Webjobs Sdk

Dependency injection using Azure WebJobs SDK?

Azure WebJobs SDK now supports instance methods. Combining this with a custom IJobActivator allows you to use DI.

First, create the custom IJobActivator that can resolve a job type using your favourite DI container:

public class MyActivator : IJobActivator
{
private readonly IUnityContainer _container;

public MyActivator(IUnityContainer container)
{
_container = container;
}

public T CreateInstance<T>()
{
return _container.Resolve<T>();
}
}

You need to register this class using a custom JobHostConfiguration:

var config = new JobHostConfiguration
{
JobActivator = new MyActivator(myContainer)
};
var host = new JobHost(config);

Then, you can use a simple class with instance methods for your jobs (here I'm using Unity's constructor injection feature):

public class MyFunctions
{
private readonly ISomeDependency _dependency;

public MyFunctions(ISomeDependency dependency)
{
_dependency = dependency;
}

public Task DoStuffAsync([QueueTrigger("queue")] string message)
{
Console.WriteLine("Injected dependency: {0}", _dependency);

return Task.FromResult(true);
}
}

ASP.Net core Azure Webjob (SDK 3.0.10) DI not resolving

With your current design the host is unaware of the service registrations that were done in main.

Here is a simplification of the current setup based on what was provided in the original example:

class Program {
static void Main(string[] args) {
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");

var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{environment}.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables()
.Build();

var azureQueueOptions = configuration.GetSection("AzureQueueOptions").Get<AzureQueueOptions>();
azureQueueOptions.StorageConnectionString = configuration.GetConnectionString("Storage");

var builder = new HostBuilder()
.ConfigureWebJobs(webJobConfiguration => {
webJobConfiguration.AddAzureStorageCoreServices();
webJobConfiguration.AddAzureStorage(c => c.BatchSize = azureQueueOptions.BatchSize);
})
.ConfigureServices(services => {
services.AddTransient<Functions>();
services.AddTransient<ICloudStorageService, AzureBlobStorageService>();
})
.ConfigureLogging((context, config) => {
config.AddConsole();
});

var host = builder.Build();

host.Run();
}
}

That or create an actual Startup and have the builder use it:

var builder = new HostBuilder()
.UseStartup<Startup>()
//...omitted for brevity

Dependency Injection in Azure WebJobs SDK when using NoAutomaticTrigger?

The WebJobs SDK now supports instance methods to be triggered instead of only static methods. The NoAutomaticTrigger method don't need to be static anymore. Update the SDK, use the custom IJobActivator approach you found and remember to register you Functions class and of course your other dependencies.

Example Program class:

class Program
{

static void Main()
{
var container = new UnityContainer();

//the instance to be injected
var systemClient = new JobSystemClient
{
UserName = "admin",
PassWord = "admin1234"
};

container.RegisterInstance<ISystemClient>(systemClient);

//Registration of the Functions class
container.RegisterType<Functions>();

var activator = new UnityJobActivator(container);

var config = new JobHostConfiguration();
config.JobActivator = activator;

var host = new JobHost(config);
// The following code will invoke a function called ManualTrigger and
// pass in data (value in this case) to the function
host.Call(typeof(Functions).GetMethod("ManualTrigger"), new { value = 20 });

host.RunAndBlock();

}
}

sample Function class:

public class Functions
{
private readonly ISystemClient _systemClient;
public Functions(ISystemClient systemClient)
{
_systemClient = systemClient;
}

//Not static anymore
[NoAutomaticTrigger]
public void ManualTrigger(TextWriter log, int value, [Queue("queue")] out string message)
{
log.WriteLine("Function is invoked with value={0}", value);
message = value.ToString();
log.WriteLine("username:{0} and password:{1}", _systemClient.UserName, _systemClient.PassWord);
}
}

and here is the output:

Found the following functions:
TestWebJob.Functions.ManualTrigger
Executing 'Functions.ManualTrigger' (Reason='This function was programmatically called via the host APIs.', Id=bf9aedc0-89d1-4ba0-a33e-9b23e0d7b8a2)
Function is invoked with value=20
Following message will be written on the Queue=20
username:admin and password:admin1234
Executed 'Functions.ManualTrigger' (Succeeded, Id=bf9aedc0-89d1-4ba0-a33e-9b23e0d7b8a2)
Job host started

Webjobs Method Indexing - Dependency Injection Exception IConfiguration

Instead of using static functions and parameter injection, you can use a full class and constructor injection.


services.AddScoped<Functions>()
services.AddSingleton(context.Configuration);

...

public class Functions
{
private readonly IConfiguration _Configuration;

public Functions(IConfiguration configuration)
{
_Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
}

public void ProcessQueueMessage([QueueTrigger("queuename")] CloudQueueMessage queueMessage,
TextWriter log)
{
//
}
}

Azure Triggered Webjobs Scope for Dependency Injection

I've opened an request Add IDependencyScope to handle scoping to the Azure Webjob team.

I've create a small library to gather classes around Azure Webjobs and SimpleInjector :

  • Nuget download
  • GitHub project

For QueueTrigger and ServiceBustrigger, I've come accross these solutions :

  • ServiceBusTrigger (from this answer: https://stackoverflow.com/a/33759649/4167200):

    public sealed class ScopedMessagingProvider : MessagingProvider
    {
    private readonly ServiceBusConfiguration _config;
    private readonly Container _container;

    public ScopedMessagingProvider(ServiceBusConfiguration config, Container container)
    : base(config)
    {
    _config = config;
    _container = container;
    }

    public override MessageProcessor CreateMessageProcessor(string entityPath)
    {
    return new ScopedMessageProcessor(_config.MessageOptions, _container);
    }

    private class ScopedMessageProcessor : MessageProcessor
    {
    private readonly Container _container;

    public ScopedMessageProcessor(OnMessageOptions messageOptions, Container container)
    : base(messageOptions)
    {
    _container = container;
    }

    public override Task<bool> BeginProcessingMessageAsync(BrokeredMessage message, CancellationToken cancellationToken)
    {
    _container.BeginExecutionContextScope();
    return base.BeginProcessingMessageAsync(message, cancellationToken);
    }

    public override Task CompleteProcessingMessageAsync(BrokeredMessage message, FunctionResult result, CancellationToken cancellationToken)
    {
    _container.GetCurrentExecutionContextScope()?.Dispose();
    return base.CompleteProcessingMessageAsync(message, result, cancellationToken);
    }
    }
    }

    You can use your custom MessagingProvider in your JobHostConfiguration like

    var serviceBusConfig = new ServiceBusConfiguration
    {
    ConnectionString = config.ServiceBusConnectionString
    };
    serviceBusConfig.MessagingProvider = new ScopedMessagingProvider(serviceBusConfig, container);
    jobHostConfig.UseServiceBus(serviceBusConfig);
  • QueueTrigger:

    public sealed class ScopedQueueProcessorFactory : IQueueProcessorFactory
    {
    private readonly Container _container;

    public ScopedQueueProcessorFactory(Container container)
    {
    _container = container;
    }

    public QueueProcessor Create(QueueProcessorFactoryContext context)
    {
    return new ScopedQueueProcessor(context, _container);
    }

    private class ScopedQueueProcessor : QueueProcessor
    {
    private readonly Container _container;

    public ScopedQueueProcessor(QueueProcessorFactoryContext context, Container container)
    : base(context)
    {
    _container = container;
    }

    public override Task<bool> BeginProcessingMessageAsync(CloudQueueMessage message, CancellationToken cancellationToken)
    {
    _container.BeginExecutionContextScope();
    return base.BeginProcessingMessageAsync(message, cancellationToken);
    }

    public override Task CompleteProcessingMessageAsync(CloudQueueMessage message, FunctionResult result,
    CancellationToken cancellationToken)
    {
    _container.GetCurrentExecutionContextScope()?.Dispose();
    return base.CompleteProcessingMessageAsync(message, result, cancellationToken);
    }
    }
    }

    You can use your custom IQueueProcessorFactory in your JobHostConfiguration like this:

     var config = new JobHostConfiguration();
    config.Queues.QueueProcessorFactory = new ScopedQueueProcessorFactory(container);

Azure WebJob and wiring up IServiceCollecton from Microsoft.Extensions.DependencyInjection

Still wasn't able to find much after searching around last night.

But after a bit of fiddling, I managed to get something working with the following:

EDIT: I've added a more complete solution with Entity Framework.
I should note that my ASP.Net Core webapp is built upon 4.6.2 instead of pure core.

using System;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Host;
using Microsoft.Azure.WebJobs.ServiceBus;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;

namespace Settlements.WebJob
{
public class ServiceJobActivator : IJobActivator
{
IServiceProvider _serviceProvider;

public ServiceJobActivator(IServiceCollection serviceCollection) : base()
{
_serviceProvider = serviceCollection.BuildServiceProvider();
}

public T CreateInstance<T>()
{
return _serviceProvider.GetRequiredService<T>();
}
}

class Program
{
static void Main()
{
var config = new JobHostConfiguration();

var dbConnectionString = Properties.Settings.Default.DefaultConnection;

var serviceCollection = new ServiceCollection();

// wire up your services
serviceCollection.AddTransient<IThing, Thing>();

// important! wire up your actual jobs, too
serviceCollection.AddTransient<ServiceBusJobListener>();

// added example to connect EF
serviceCollection.AddDbContext<DbContext>(options =>
options.UseSqlServer(dbConnectionString ));

// add it to a JobHostConfiguration
config.JobActivator = new ServiceJobActivator(serviceCollection);

var host = new JobHost(config);

host.RunAndBlock();
}
}

}



Related Topics



Leave a reply



Submit