Azure / azure-functions-host

The host/runtime that powers Azure Functions
https://functions.azure.com
MIT License
1.94k stars 441 forks source link

Problems with scoped lifetime services (AddScoped) #5098

Closed heikkilamarko closed 1 year ago

heikkilamarko commented 5 years ago

Hi,

We are using durable functions with dependency injection. In our Startup.cs file, we register our dependencies as follows:

services.AddScoped<IMyService>(serviceProvider => new MyService(...))

In our activity function, we are using normal constructor injection to get IMyService instances.

The problem is that even if we are using AddScoped for registering the service, during the activity function run, each class that asks for the service, gets a different instance of IMyService. This breaks our app logic, because IMyService users won't see each other's changes.

As a workaround to the earlier Azure Functions runtime DI issues, we had the following pinning in place FUNCTIONS_EXTENSION_VERSION = 2.0.12673.0. The pinned version is not supported anymore by Azure, so our function DI is now broken again.

jonmeyerson commented 2 years ago

I ran into a similar issue in 3.1, I had a ScopedServiceProvider and when HttpClientFactory.CreateClient was called inside it, it caused the next time I resolved a interface some of the instances to be lost. After looking at the source code for I did get it to work setting SupressHandlerScope to true. Not sure what side effects this has. its used to control this logic in the HttpClientFactory - DefaultHttpClientFactory

jorge-duarte commented 2 years ago

I confirm that the issue persists even in version 4 of the Azure Functions runtime. Worst of all, I don't use the IHttpClientFactory interface and yet AddScoped always generates new instances on each constructor injection.

evaldas-raisutis commented 2 years ago

Found this thread after banging by head against he wall for 3 days.

We had a transient class A which depended on a scoped dbcontext and another scoped service B with httpclient added through httpclient nuget package and dbcontext

I could reproduce an issue where the first time service A was resolved through a scoped service provider, two instances of dbcontext were created. Service A and Service B receives separate instances of db context.

Service B modified entities through dbcontext and save changes was called on service A. Because there were two instances of dbcontext and each service wrongly had its own instance - nothing was saved to the database.

Subsequent resolutions of service A did not produce the same behavior and both service A and service B received the same instance of dbcontext.

Enabling the feature flag mentioned earlier helped, but I am yet to test it on production.

campelo commented 2 years ago

He everybody.

I see this issue has been opened for almost 3 years. I'm using all the most recent versions and I'm continue facing the same issue. Could we have a permanent solution without workarounds?

brettsam commented 2 years ago

@campelo -- are you on v4? Can you please provide an example of what you're seeing in a new issue and tag me and @fabiocav in it? There are a lot of mixed scenarios in this issue and it's getting hard to follow.

brettsam commented 2 years ago

I confirm that the issue persists even in version 4 of the Azure Functions runtime. Worst of all, I don't use the IHttpClientFactory interface and yet AddScoped always generates new instances on each constructor injection.

@jorge-duarte -- Without seeing your exact repro, I believe this is working as expected. Each function invocation is its own scope, which mean it gets its own instance of scoped services. Similar to an http request in ASP.NET. Could you also open a separate issue and tag me and @fabiocav in it so we can take a look at your exact scenario?

Assassinbeast commented 2 years ago

I also have Scoped lifetime problem with Isolated runtime.

It looks like there are two scoped lifetimes for one http request. One for the "Middlewares" and one for the "Function".

Here is my code:

Startup.cs

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults((IFunctionsWorkerApplicationBuilder configure) =>
    {
        configure.UseMiddleware<MyMiddleware>();
        configure.UseMiddleware<MyMiddleware2>();
    })
    .ConfigureServices(services =>
    {
        services.AddScoped<FooService>();
        services.AddScoped<BarService>();
    })
    .Build();
host.Run();

Function1.cs

namespace FunctionApp1
{
    public class Function1
    {
        private readonly FooService _fooService;
        private readonly BarService _barService;

        public Function1(FooService fooService, BarService barService)
        {
            _fooService = fooService;
            _barService = barService;
        }

        [Function("Function1")]
        public HttpResponseData Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req)
        {
            _fooService.LogFooId();
            _barService.Start();
            return req.CreateResponse(HttpStatusCode.OK);
        }
    }

    public class MyMiddleware : IFunctionsWorkerMiddleware
    {
        private readonly FooService _fooService;

        public MyMiddleware(FooService fooService)
        {
            _fooService = fooService;
        }

        public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
        {
            _fooService.LogFooId();
            await next(context);
        }
    }
    public class MyMiddleware2 : IFunctionsWorkerMiddleware
    {
        private readonly FooService _fooService;

        public MyMiddleware2(FooService fooService)
        {
            _fooService = fooService;
        }

        public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
        {
            _fooService.LogFooId();
            await next(context);
        }
    }

    public class FooService
    {
        public Guid Id { get; set; } = Guid.NewGuid();
        private readonly ILogger _logger;

        public FooService(ILogger<FooService> logger) 
        {
            _logger = logger;
        }

        public void LogFooId()
        {
            _logger.LogInformation($"FooId: {Id}");
        }
    }

    public class BarService
    {
        private readonly FooService _fooService;

        public BarService(FooService fooService)
        {
            _fooService = fooService;
        }

        public void Start()
        {
            _fooService.LogFooId();
        }
    }
}

When i call "http://localhost:7071/api/Function1", then it will log this out:

FooId: 2514bc34-f5d6-4833-b14b-5090020479f5 FooId: 2514bc34-f5d6-4833-b14b-5090020479f5 FooId: 3e1a5a61-c90c-4e67-bfc6-3c3151933c34 FooId: 3e1a5a61-c90c-4e67-bfc6-3c3151933c34

Its expected that there should be 1 scoped lifetime in one http request, where the "Middleware" and "Function" scopes are both shared.

So this is a bug too, correct?

Im using:

Core Tools Version: 4.0.4829 Commit hash: N/A (64-bit) Function Runtime Version: 4.11.2.19273

It happens both with .NET 7 rc 2 and .NET 6 with the latest versions of the nuget packages and also the preview packages.

Workaround

My workout right now, is to instantiate all services in my middleware through the FunctionContext, and then it will work

public class MyMiddleware : IFunctionsWorkerMiddleware
{
    public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
    {
        var fooService = context.InstanceServices.GetRequiredService<FooService>();
        fooService.LogFooId();
        await next(context);
    }
}
public class MyMiddleware2 : IFunctionsWorkerMiddleware
{
    public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
    {
        var fooService = context.InstanceServices.GetRequiredService<FooService>();
        fooService.LogFooId();
        await next(context);
    }
}

Then when i call the api, it will log out:

FooId: 7b36df46-8ec2-41c8-97f7-57a642ac743f
FooId: 7b36df46-8ec2-41c8-97f7-57a642ac743f
FooId: 7b36df46-8ec2-41c8-97f7-57a642ac743f
FooId: 7b36df46-8ec2-41c8-97f7-57a642ac743f

Edit: I have opened this issue in the azure-functions-dotnet-workerproject https://github.com/Azure/azure-functions-dotnet-worker/issues/1133

kshyju commented 2 years ago

@Assassinbeast Your issues seems to be in the isolated model. Could you please open a new issue here: https://github.com/Azure/azure-functions-dotnet-worker/issues

fabiocav commented 1 year ago

Closing this issue as based on the recent reports, the default behavior in 4.0 (or using the EnableEnhancedScopes) seem to address the problem.

If you continue to experience similar problems, please open a separate issue with the details so we can take a closer look.

Thank you!