Programme to run with the injected interface working.
Webjob errors with "Object reference not set to an instance of an object"
Provide any related information
Error
fail: Host.Results[0]
Microsoft.Azure.WebJobs.Host.FunctionInvocationException: Exception while executing function: Functions.ProcessWorkItem_ServiceBus ---> System.NullReferenceException: Object reference not set to an instance of an object.
at lambda_method(Closure , Functions , Object[] )
at Microsoft.Azure.WebJobs.Host.Executors.VoidTaskMethodInvoker 2.InvokeAsync(TReflected instance, Object[] arguments) in azure-webjobs-sdk-3.0.0-rc1\azure-webjobs-sdk-3.0.0-rc1\src\Microsoft.Azure.WebJobs.Host\Executors\VoidTaskMethodInvoker.cs:line 20
at Microsoft.Azure.WebJobs.Host.Executors.FunctionInvoker 2.InvokeAsync(Object instance, Object[] arguments) in azure-webjobs-sdk-3.0.0-rc1\azure-webjobs-sdk-3.0.0-rc1\src\Microsoft.Azure.WebJobs.Host\Executors\FunctionInvoker.cs:line 63
at Microsoft.Azure.WebJobs.Host.Executors.FunctionExecutor.InvokeAsync(IFunctionInvoker invoker, ParameterHelper parameterHelper, CancellationTokenSource timeoutTokenSource, CancellationTokenSource functionCancellationTokenSource, Boolean throwOnTimeout, TimeSpan timerInterval, IFunctionInstance instance) in azure-webjobs-sdk-3.0.0-rc1\azure-webjobs-sdk-3.0.0-rc1\src\Microsoft.Azure.WebJobs.Host\Executors\FunctionExecutor.cs:line 562
at Microsoft.Azure.WebJobs.Host.Executors.FunctionExecutor.ExecuteWithWatchersAsync(IFunctionInstance instance, ParameterHelper parameterHelper, ILogger logger, CancellationTokenSource functionCancellationTokenSource) in azure-webjobs-sdk-3.0.0-rc1\azure-webjobs-sdk-3.0.0-rc1\src\Microsoft.Azure.WebJobs.Host\Executors\FunctionExecutor.cs:line 509
at Microsoft.Azure.WebJobs.Host.Executors.FunctionExecutor.ExecuteWithLoggingAsync(IFunctionInstance instance, ParameterHelper parameterHelper, IFunctionOutputDefinition outputDefinition, ILogger logger, CancellationTokenSource functionCancellationTokenSource) in azure-webjobs-sdk-3.0.0-rc1\azure-webjobs-sdk-3.0.0-rc1\src\Microsoft.Azure.WebJobs.Host\Executors\FunctionExecutor.cs:line 445
at Microsoft.Azure.WebJobs.Host.Executors.FunctionExecutor.ExecuteWithLoggingAsync(IFunctionInstance instance, FunctionStartedMessage message, FunctionInstanceLogEntry instanceLogEntry, ParameterHelper parameterHelper, ILogger logger, CancellationToken cancellationToken) in azure-webjobs-sdk-3.0.0-rc1\azure-webjobs-sdk-3.0.0-rc1\src\Microsoft.Azure.WebJobs.Host\Executors\FunctionExecutor.cs:line 249
--- End of inner exception stack trace ---
at Microsoft.Azure.WebJobs.Host.Executors.FunctionExecutor.ExecuteWithLoggingAsync(IFunctionInstance instance, FunctionStartedMessage message, FunctionInstanceLogEntry instanceLogEntry, ParameterHelper parameterHelper, ILogger logger, CancellationToken cancellationToken) in azure-webjobs-sdk-3.0.0-rc1\azure-webjobs-sdk-3.0.0-rc1\src\Microsoft.Azure.WebJobs.Host\Executors\FunctionExecutor.cs:line 293
at Microsoft.Azure.WebJobs.Host.Executors.FunctionExecutor.TryExecuteAsync(IFunctionInstance functionInstance, CancellationToken cancellationToken) in azure-webjobs-sdk-3.0.0-rc1\azure-webjobs-sdk-3.0.0-rc1\src\Microsoft.Azure.WebJobs.Host\Executors\FunctionExecutor.cs:line 89
Program.cs
public static async Task Main(string[] args)
{
var builder = new HostBuilder()
.UseEnvironment("Development")
.ConfigureWebJobs(b =>
{
b.AddDashboardLogging()
.AddAzureStorageCoreServices()
.AddAzureStorage()
.AddServiceBus()
.AddEventHubs()
.AddExecutionContextBinding();
})
.ConfigureAppConfiguration(b =>
{
// Adding command line as a configuration source
b.AddCommandLine(args);
})
.ConfigureServices((hostContext, services) => {
services.AddSingleton<IJobActivator>(new JobActivator(services.BuildServiceProvider()));
services.AddScoped<Functions, Functions>();
services.AddSingleton<IWriteText, WriteText>(); })
.ConfigureLogging((context, b) =>
{
b.SetMinimumLevel(LogLevel.Debug);
b.AddConsole();
// If this key exists in any config, use it to enable App Insights
string appInsightsKey = context.Configuration["APPINSIGHTS_INSTRUMENTATIONKEY"];
if (!string.IsNullOrEmpty(appInsightsKey))
{
b.AddApplicationInsights(o => o.InstrumentationKey = appInsightsKey);
}
})
.UseConsoleLifetime();
var host = builder.Build();
using (host)
{
await host.RunAsync();
}
}
Functions.cs
public class Functions
{
private readonly IWriteText _writeText;
public Functions(IWriteText writeText)
{
_writeText = writeText;
}
public async Task ProcessWorkItem_ServiceBus(
[ServiceBusTrigger("%queue%", Connection = "AzureWebJobsServiceBus")] string item, ILogger log)
{
log.LogInformation(_writeText.PopPop());
}
}
IWriteText.cs
public interface IWriteText
{
string PopPop();
}
public class WriteText : IWriteText
{
public string PopPop()
{
return "Pop Pop!";
}
}
High level comment: during the .NET Core port, we never really reconciled the IJobActivator interface with the new DI capabilities. I logged https://github.com/Azure/azure-webjobs-sdk/issues/1917 for that.
That said, I tried the pattern you're using here and it works for me. You didn't provide the source code for your JobActivator Type - perhaps your null ref issue is there?
The following code works for me:
Registration:
.ConfigureServices(services =>
{
services.AddSingleton<IJobActivator, MyJobActivator>();
services.AddScoped<Functions, Functions>();
services.AddSingleton<IWriteText, WriteText>();
})
Job Activator:
public class MyJobActivator : IJobActivator
{
private readonly IServiceProvider _serviceProvider;
public MyJobActivator(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public T CreateInstance<T>()
{
object instance = _serviceProvider.GetRequiredService(typeof(T));
return (T)instance;
}
}
Functions class:
public class Functions
{
private readonly IWriteText _writeText;
public Functions(IWriteText writeText)
{
_writeText = writeText;
}
public void ProcessWorkItem(
[QueueTrigger("test")] WorkItem workItem, ILogger logger)
{
_writeText.PopPop();
logger.LogInformation($"Processed work item {workItem.ID}");
}
}
Thanks @mathewc it turns out that the one bit of code I didn't show was indeed at fault. I used your job activator and it worked straight away!
FYI - we'll make this more natural via something like https://github.com/Azure/azure-webjobs-sdk/pull/1919. You shouldn't have to register your own activator just to get DI
@mathewc that's great I was hoping that DI would become more seamless this version
Amy news on first class DI support, and documentation?
Hi @mathewc,
Unfortunately, the the IJobActivator registration and implementation you gave above is flawed and unusable. Let me explain why.
Your MyJobActivator is registered as Singleton. As such, it will get injected with the root IServiceProvider. As the MyJobActivator is used to construct an object graph this will lead to concurrency bugs and memory leaks in all but the simplest of cases:
MyJobActivator resolves an object graph that contains a Scoped dependency, that dependency will be cached in the root container and every concurrent or parallel request will be provided with the same instance. The root container acts as a global, application-wide scope. This will cause concurrency bugs because most Scoped registrations are not thread safe (e.g. DbContext).MyJobActivator resolves an object graph taht contains a Transient dependency that implements IDisposable, that dependency will be tracked by the scope and disposed when the scope ends. As this 'scope' is in this case the root container, the instance will be cached indefinately. This causes memory leaks.Normally, this situation would be easily solvable by changing the registration of IJobActivator to the following:
``` c#
services.AddScoped
Changing `MyJobActivator` to `Scoped` would normally solve the problem as a `Scoped` registration is *not* injected with the root container, but rather an `IServiceProvider` that is specific to the scope from which `MyJobActivator` is resolved.
This, however, does mean that the Azure WebJobs SDK should internally create a new `IServiceScope` and resolve the `IJobActivator` from that scope鈥攁nd never cache the `IJobActivator` as a singleton.
Doing this also solves the problem for other other DI Containers (the so-called 'non-conformers') who which to integrate with the Azure WebJobs SDK, which currently seems problematic with the current design. When WebJobs manages a scope internally (and resolve the `IJobActivator` from that scope), a Simple Injector-specific implementation can be created quite easily as the following code sample shows:
``` c#
public class SimpleInjectorJobActivator : IJobActivator
{
private readonly ScopeDisposable disposer;
private readonly Container container;
public SimpleInjectorJobActivator(ScopeDisposable disposer, Container container)
{
this.disposer = disposer;
this.container = container;
}
public T CreateInstance<T>()
{
// Start a new Simple Injector scope.
var scope = AsyncScopedLifestyle.BeginScope(this.container);
// Setting the scope will ensure it is disposed together with the disposer.
this.disposer.Scope = scope;
return (T)this.container.GetInstance(typeof(T));
}
public sealed class ScopeDisposable : IDisposable
{
public Scope Scope { get; set; }
public void Dispose() => this.Scope?.Dispose();
}
}
This integration can than be wired as follows:
c#
services
.AddSingleton(container)
.AddScoped<SimpleInjectorJobActivator.ScopeDisposable>()
.AddScoped<IJobActivator, SimpleInjectorJobActivator>();
In case it is not feasible for WebJobs to resolve an IJobActivator from within a scope, it means that WebJobs either has to:
IServiceScope or its scoped IServiceProvider to the IJobActivator.CreateInstance method. Without this context, it is impossible to let a container like Simple Injector ensure that its created components are disposed when the request ends, or@fabiocav recently added support for scoped service injection in https://github.com/Azure/azure-webjobs-sdk/commit/77234d6aef493788cfa25c02467fe31e1db265d6. He may be able to comment more on how to accomplish your scenario on the latest bits.
Hi @mathewc, @fabiocav's change does indeed fix the problem. Wonder how I missed that. When implementing an IJobActicatorEx, the supplied IFunctionInstanceEx contains a IServiceProvider property, which is the actual scoped provider. This means that every DI Container should now be able to plugin to this model. Here's the Simple Injector verion:
``` c#
public class SimpleInjectorJobActivator : IJobActivatorEx
{
private readonly Container container;
public SimpleInjectorJobActivator(Container container) => this.container = container;
public T CreateInstance<T>(IFunctionInstanceEx functionInstance)
{
var disposer = functionInstance.InstanceServices.GetRequiredService<ScopeDisposable>();
this.disposer.Scope = AsyncScopedLifestyle.BeginScope(this.container);
// Scopes in Simple Injector are ambient; you always resolve from the container
return (T)this.container.GetInstance(typeof(T));
}
// Ensures a created Simple Injector scope is disposed at the end of the request
public sealed class ScopeDisposable : IDisposable
{
public Scope Scope { get; set; }
public void Dispose() => this.Scope?.Dispose();
}
}
By using the following configuration, jobs can be resolved using Simple Injector:
``` c#
services
.AddScoped<SimpleInjectorJobActivator.ScopeDisposable>()
.AddSingleton<IJobActivator>(new SimpleInjectorJobActivator(container));
Awesome work @fabiocav.
Doesn't seem to work in ServiceBus triggers, see https://github.com/Azure/azure-webjobs-sdk/issues/2262
Does not seem to work if a web job/function is marked with Singleton
Most helpful comment
Amy news on first class DI support, and documentation?