With ASP.Net Core, using the DI with AddScoped is understood that the lifetime is per request.
With WebJobs 3.0, AddScoped behaves the same way as AddSingleton, which does not play well when using a DbContext.
AddScoped should be per "request", basically per function call.
At least, provide some entry points to start/stop the scope properly. Right now, the JobActivator does not allow to do such a thing because it is created with the IHostedService, which has a lifetime of the app. The GetService() cannot be used with scoping either.
I have searched everywhere how to actually hook up the scoping properly, and either all the executors are sealed or internal. The only way to make this work is to create a scope in each of my function calls which defeat the purpose of dependency injection...
I could create a scope in the CustomJobActivator, but it is not disposed properly.
AddScoped<IMyService, MyService>() should be per request when injected in the Function class.
Or, injecting my IMyService in the method parameters could work as well.
Everything is treated as Singleton scope.
public class ContinuousMethods
{
private readonly IServiceProvider serviceProvider;
private readonly IMyService myService;
public ContinuousMethods(
IServiceProvider serviceProvider
// IMyService myService // I wish it were Scoped
)
{
this.serviceProvider = serviceProvider;
//this.myService = myService;
}
public async Task SendEmail(
[QueueTrigger("%AzureStorage:Queue:SendEmail%")] int emailId,
ILogger logger
// IMyService myService // Why not...? But we don't have a binder for this right now.
)
{
// Ugly workaround that I have to insert in all my functions.
using (var scope = serviceProvider.CreateScope())
using (var myService = scope.ServiceProvider.GetService<IMyService>())
{
await myService.SendEmailAsync(emailId);
}
}
}
WebJobs 3.0
I second this. I would like my QueueTrigger and TimerTrigger methods to be inside a scope, just like Controller methods in AspNetCore. Or you could control it by inheriting the same scope as when the container class is added to DI services: i.e. if you add your Functions (or whatever you call it) as Scoped, then calls to it's timer and queue methods etc are scoped, or if you add it as a singleton, they are also singleton scoped.
My hack for scopes is similar:
public class Functions
{
private readonly IServiceScopeFactory _serviceScopeFactory;
public Functions(IServiceScopeFactory serviceScopeFactory)
{
_serviceScopeFactory = serviceScopeFactory;
}
public async Task ProcessQueueMessage([QueueTrigger("jobsqueue")] JobLite[] jobs, TextWriter logger)
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var jp = scope.ServiceProvider.GetRequiredService<IJobProcessor>();
await jp.ProcessJobsAsync(jobs.ToList());
}
}
public async Task NightlyCleanupTimerAsync(
[TimerTrigger("0 0 8 * * *", RunOnStartup = false)]
TimerInfo timerInfo,
TextWriter log)
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var ttp = scope.ServiceProvider.GetRequiredService<ITimedTaskProcessor>();
await ttp.RunNightlyCleanupAsync();
}
}
Other idea:
We implemented a custom job activator for this:
.AddSingleton<IJobActivator>(provider => new DependencyScopedJobActivator(provider));
public class DependencyScopedJobActivator : IJobActivator
{
private readonly IServiceProvider _serviceProvider;
public DependencyScopedJobActivator(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public T CreateInstance<T>()
{
var scope = _serviceProvider.CreateScope();
var job = scope.ServiceProvider.GetRequiredService<T>();
if (job is DependencyScopedJob baseJob)
{
baseJob.ServiceScope = scope;
}
return job;
}
}
public class DependencyScopedJob : IDisposable
{
public IServiceScope ServiceScope
{ get; set; }
public void Dispose()
{
ServiceScope?.Dispose();
}
}
Only downside is that our webjobs now need to inherit from DependencyScopedJob
This would be immensely helpful as caching in db contexts is shared between all functions in functions class and for the lifetime of the request currently; even splitting functions in different classes nets you the same DbContext instance with the service lifetime of the DbContext is transient and the Service lifetime is transient. This isn't clear as transient is per resolution but it seems it is resolved once per application lifetime and the same one is used each time after that.
It could be a risk to clear the dbcontext cache if another function is also using it or if the webjob is running the same function on 2 different items that trigger the function to run(Like messages).
This issue becomes more complex when exceptions or errors can occur. If an exception occurs it seems like a process of cleaning up items can begin. Which can make a dbcontext being used by another function to become unavailable. The suggested usage of a servicebus trigger is to allow an exception to be thrown out of a function as then the message is put into an abandoned state and moved to the dead letter queue after the max number or retries.
This is now resolved and published to NuGet in 3.0.5
@fabiocav Do you mind to share some doc on how it should work now? Did you guys publish a +.1 patch and bring in this as a feature? That's quite the behavior change? I'm fine with it but would have loved to see the commit/implementation or some documentation!
Thanks for fixing this!
I was going to say the same thing! Thanks - and what is the fix?
@fabiocav Ping? Can we get some info about this... Otherwise we will learn about it in years from the documentation.
@fabiocav any info on the fix?
I'll give it a few more days and if I don't hear back, I'll open a bug to update the documentation. That way it would be actionable.
To anybody interested, I have opened a bug, since people who have answers about this matter don't seem to be following this bug.
I was going to say the same thing! Thanks - and what is the fix?
The fix is "simple" update your NuGet package to >= 3.5.0 from then on, AddScoped will behave as you expect, and scope per function call.
Is being reproduced with
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Storage" Version="3.0.6" />
both locally and in Azure.
Currently, I've replaced in the app AddScoped with AddTransient when creating Ef Core Context.
I would like this issue to be re-opened. Currently, AddScoped is causing services to behave like singletons in some circumstances. I believe this is a bug and there are numerous reports on this issue tracker. Could we please get an official response from Microsoft today? @fabiocav
Any news on that? This issue is still present in 3.0.10 @fabiocav
same issue here
@pzbyszynski / @anna-git could you please open a new issue with the details of how you鈥檙e registering and consuming your services and the behavior you鈥檙e seeing? Scopes services should be working as expected and it would be good to take a closer look at your scenario.
Any further update on this? We have few queue triggers which need the 'scoped' functionality.
I ran into this problem today and discovered that the only time I experienced scoped DI service acting as a transient DI transient when I included a service added as an HTTPClient to the constructor of a scoped service.
Example:
Startup:
builder.Services.AddHttpClient<HttpCustomClient>();
builder.Services.AddScoped<TestHandler>();
builder.ServicesAddScoped<SqlService>();
TestHandler:
public class TestHandler(HttpCustomClient http, SqlService sql) { }
However, if I set this up like this it scoped services will act normally:
Startup:
builder.Services.AddHttpClient();
builder.Services.AddScoped<HttpCustomClient>();
builder.Services.AddScoped<TestHandler>();
builder.ServicesAddScoped<SqlService>();
TestHandler:
public class TestHandler(HttpCustomClient http, SqlService sql) { }
According to the MSFT HttpClientFactory documentation when you add a custom HTTPClient it will act as a transient. https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-3.1
Most helpful comment
Any news on that? This issue is still present in
3.0.10@fabiocav