Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Problems with scoped lifetime services (AddScoped) #5098

Closed
heikkilamarko opened this issue Oct 17, 2019 · 109 comments · Fixed by #6721
Closed

Problems with scoped lifetime services (AddScoped) #5098

heikkilamarko opened this issue Oct 17, 2019 · 109 comments · Fixed by #6721
Assignees
Labels

Comments

@heikkilamarko
Copy link

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.

@heikkilamarko
Copy link
Author

I find it hard to believe this is by design. If it is so, documentation should state that very clearly.

@heikkilamarko
Copy link
Author

The problem is not durable function specific. Same happens with normal TimerTrigger functions also.

@heikkilamarko
Copy link
Author

I made more experiments and it seem that the DI problem occurs only when I have used services.AddHttpClient() to register HttpClients. If I comment out services.AddHttpClient() calls, AddScoped works as expected.

@dhanapalschandran
Copy link

dhanapalschandran commented Oct 19, 2019

Hi @heikkilamarko I am also facing the same issue, when this issue happens last week I have pinned to older runtime version, then it started working, now again breaking. I agree it's not an issue with durable function. Even I am using the same DI pattern for HttpClients, Do we have any workaround for httpclient register or waiting for team to provide global fix

@unt1tled
Copy link

unt1tled commented Oct 20, 2019

@dhanapalschandran Same thing on my side. Could it be breaking again due to #5096?

@fabiocav fabiocav self-assigned this Oct 21, 2019
@fabiocav
Copy link
Member

fabiocav commented Nov 1, 2019

@heikkilamarko can you please share repro code? Is this happening with factory registrations only? Some more details/repro will ensure we're looking exactly at the issue you're running into.

@unt1tled / @dhanapalschandran if you can share the code you're having issues with, we can validate with your scenarios as well.

Thanks!

@heikkilamarko
Copy link
Author

Hi. See below for a repro code (see the comment block in the function code):

using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Net.Http;

[assembly: FunctionsStartup(typeof(Demo.App.Startup))]

namespace Demo.App
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            var services = builder.Services;

            services.AddHttpClient<MyHttpClient>();

            services
                .AddScoped<IMyService, MyService>()
                .AddScoped<IMyServiceSetter, MyServiceSetter>()
                .AddScoped<IMyServiceGetter, MyServiceGetter>();
        }
    }

    public class MyHttpClient
    {
        private readonly HttpClient httpClient;

        public MyHttpClient(HttpClient httpClient)
        {
            this.httpClient = httpClient;
        }
    }

    public interface IMyService
    {
        void Set(int value);

        int Get();
    }

    public class MyService : IMyService
    {
        private int value = 0;

        public int Get()
        {
            return value;
        }

        public void Set(int value)
        {
            this.value = value;
        }
    }

    public interface IMyServiceSetter
    {
        void SetValue(int value);
    }

    public class MyServiceSetter : IMyServiceSetter
    {
        private readonly IMyService myService;
        private readonly IMyServiceGetter myServiceGetter;

        public MyServiceSetter(IMyService myService, IMyServiceGetter myServiceGetter)
        {
            this.myService = myService;
            this.myServiceGetter = myServiceGetter;
        }

        public void SetValue(int value)
        {
            myService.Set(value);
        }
    }

    public interface IMyServiceGetter
    {
        int GetValue();
    }

    public class MyServiceGetter : IMyServiceGetter
    {
        private readonly IMyService myService;
        private readonly MyHttpClient myHttpClient;

        public MyServiceGetter(IMyService myService, MyHttpClient myHttpClient)
        {
            this.myService = myService;
            this.myHttpClient = myHttpClient;
        }

        public int GetValue()
        {
            return myService.Get();
        }
    }

    public class DemoFunction
    {
        private readonly IMyServiceSetter myServiceSetter;
        private readonly IMyServiceGetter myServiceGetter;

        public DemoFunction(
            IMyServiceSetter myServiceSetter,
            IMyServiceGetter myServiceGetter)
        {
            this.myServiceSetter = myServiceSetter;
            this.myServiceGetter = myServiceGetter;
        }

        [FunctionName("Demo")]
        public void Demo([TimerTrigger("0 */5 * * * *", RunOnStartup = true)]TimerInfo timerInfo, ILogger log)
        {
            int value1 = 123;
            myServiceSetter.SetValue(value1);
            int value2 = myServiceGetter.GetValue();

            // Logs '123 - 0'
            // It means that MyServiceGetter and MyServiceSetter got different IMyservice instances.
            // Should be the same since we are using AddScoped to register services.
            // ---
            // If you comment out MyHttpClient injection from MyServiceGetter, it logs '123 - 123'.
            log.LogInformation($"{value1} - {value2}");
        }
    }
}

@t-l-k
Copy link

t-l-k commented Nov 9, 2019

I believe this is the same as my issue #4914 - clearly something wrong with scoped registrations.

@fabiocav can we get an update on these tickets? Are they being investigated? Thx

@APIWT
Copy link

APIWT commented Nov 12, 2019

@fabiocav Hope you are having a nice day :) We are also hopeful that this issue will be fixed soon. We have spent a lot of time writing code to wire up our services and we really love the way scoped services work in ASP.NET Core. Let me know if there is anything I can do to help get this moving!

@buyckkdelaware
Copy link

buyckkdelaware commented Nov 15, 2019

We have experienced the same problem, but I also noticed this:
It's only the first time after the function started/triggered, that the scoped services are 'wrong'. The second time, it seems to be fine. Has anyone else noticed this? (I was able to reproduce it with the code posted here by @heikkilamarko.)

This is a pretty big problem for us as well, any progress on this issue?

Some additional information:
Azure Functions Core Tools - 2.7.1846
Function Runtime Version - 2.0.12858.0

@t-l-k
Copy link

t-l-k commented Nov 25, 2019

@buyckkdelaware during load testing I have found multiple instances of same service type within the same request scope, it occurs frequently for me, at least 1 in 100 requests. I don't know if the weight of the type is relevant, but my DbContext subclasses are definitely affected - I get missed saves because entities get written to an incorrect DbContext, injected into either repositories, or other infrastructure code. I haven't yet managed to run more than 500 requests due to the DryIOC container scope exploding (which appears to be due to DefaultHttpClientFactory, also likely implicated in the HttpClient errors you've witnessed).

Detecting these duplicates is quite easy, using an AsyncLocal static collection in the affected class's constructor to track when more than 1 distinct hashcode has been found in. DbContexts are not thread safe ofc so can't be used by multiple threads anyway.

I haven't yet "handled" it, but I'll probably just throw an exception and fail the request thread using hashcodes as the detection basis. Without handling it, my code sometimes doesn't know that it has failed, it just misses data.

@darrenhull
Copy link

Are there any workarounds for this issue?

@fabiocav
Copy link
Member

fabiocav commented Jan 6, 2020

@darrenhull the workarounds will require some code refactoring at the moment. This is being assigned to a sprint for investigation, so we hope to have an update on this issue soon.

@fabiocav fabiocav added this to the Triaged milestone Jan 6, 2020
@darrenhull
Copy link

darrenhull commented Jan 23, 2020

Hi @fabiocav,

Do you know if there is an update on this issue yet?

Sorry to keep asking but It’s a pretty big blocking issue for my company’s migration which we are pushing further and further away without being able to suggest when we can resume it.

Thanks

@darrenhull
Copy link

Hi @fabiocav,

I see this has been triaged and had the p2 tag. What exactly does the p2 mean, guessing priority2?

Can you give us an update please?

Thanks

@fabiocav
Copy link
Member

@darrenhull yes, this is a priority label. The issue has been triaged, but not yet assigned. We'll have the milestone updated once this is assigned to a sprint.

@daniel-white
Copy link

@fabiocav: this really sounds like a .net core bug/issue. can you confirm?

i'm running into something similar using a vanilla asp.net core service.

@bartdk-be
Copy link

@fabiocav : Not to blame anyone, but how can a basic feature like Scoped or not being labeled as a p2 ? This seems like a rather basic functionality ?
Can you point us to workarounds to resolve this issue ?

@daniel-white
Copy link

This appears to be a problem on at least .NET Core 2.1, but is fixed in 3.x. I've raised it here: dotnet/runtime#34586

@swettstein
Copy link

@fabiocav any update please?

@Schuer84
Copy link

Schuer84 commented Aug 11, 2021

Any updates on this one? We registered our DbContext as scoped, but we noticed, if we start multiple http requests, the context gets shared between the different requests, which leads to threading issues:

{
type: 'NotSupportedException',
message: "A second operation started on this context before a previous asynchronous operation completed. Use 'await' to ensure that any asynchronous operations have completed before calling another method on this context. Any instance members are not guaranteed to be thread safe."
}

Any feedback on how to fix this would be greatly appreciated.
PS: We enabled the flag "AzureWebJobsFeatureFlags": "EnableEnhancedScopes",

@fabiocav
Copy link
Member

@Schuer84 can you please open a separate issue with those details so we can take a closer look at your scenario? We have no evidence that with the changes made behind the flag still have issues with scoped services, so it would be good to rule other things out.

Thanks!

@Gevil
Copy link

Gevil commented Aug 25, 2021

Alright some new info on this in our functions we are also using HttpClients with DI with AddHttpClient.

After enabling EnableEnhancedScopes we are seeing fewer errors related to object disposed exceptions from DryIoc related to our DbContexts when locally debugging.

As soon as we deploy our Function to Azure and run tests on it everything falls apart and almost every function trigger fails.

{ "Error": 39, "StackTrace": " at DryIoc.Throw.It(Int32 error, Object arg0, Object arg1, Object arg2, Object arg3) in D:\\a\\1\\s\\src\\WebJobs.Script.WebHost\\DependencyInjection\\DryIoc\\Container.cs:line 9000\r\n at DryIoc.Container.ThrowIfContainerDisposed() in D:\\a\\1\\s\\src\\WebJobs.Script.WebHost\\DependencyInjection\\DryIoc\\Container.cs:line 412\r\n at DryIoc.Container.ResolveAndCacheDefaultFactoryDelegate(Type serviceType, IfUnresolved ifUnresolved) in D:\\a\\1\\s\\src\\WebJobs.Script.WebHost\\DependencyInjection\\DryIoc\\Container.cs:line 201\r\n at DryIoc.Container.DryIoc.IResolver.Resolve(Type serviceType, IfUnresolved ifUnresolved) in D:\\a\\1\\s\\src\\WebJobs.Script.WebHost\\DependencyInjection\\DryIoc\\Container.cs:line 196\r\n at Microsoft.Azure.WebJobs.Script.WebHost.DependencyInjection.ScopedServiceProvider.GetService(Type serviceType) in D:\\a\\1\\s\\src\\WebJobs.Script.WebHost\\DependencyInjection\\ScopedServiceProvider.cs:line 25\r\n at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetService[T](IServiceProvider provider)\r\n at Microsoft.EntityFrameworkCore.Internal.ScopedLoggerFactory.Create(IServiceProvider internalServiceProvider, IDbContextOptions contextOptions)\r\n at Microsoft.EntityFrameworkCore.Infrastructure.EntityFrameworkServicesBuilder.<>c.<TryAddCoreServices>b__7_0(IServiceProvider p)\r\n at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitFactory(FactoryCallSite factoryCallSite, RuntimeResolverContext context)\r\n at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)\r\n at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)\r\n at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite singletonCallSite, RuntimeResolverContext context)\r\n at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor2.VisitCallSite(ServiceCallSite callSite, TArgument argument)\r\n at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)\r\n at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)\r\n at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)\r\n at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite singletonCallSite, RuntimeResolverContext context)\r\n at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor2.VisitCallSite(ServiceCallSite callSite, TArgument argument)\r\n at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)\r\n at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)\r\n at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)\r\n at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite singletonCallSite, RuntimeResolverContext context)\r\n at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor2.VisitCallSite(ServiceCallSite callSite, TArgument argument)\r\n at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)\r\n at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)\r\n at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)\r\n at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite singletonCallSite, RuntimeResolverContext context)\r\n at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor2.VisitCallSite(ServiceCallSite callSite, TArgument argument)\r\n at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope)\r\n at Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine.<>c__DisplayClass1_0.<RealizeService>b__0(ServiceProviderEngineScope scope)\r\n at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope)\r\n at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType)\r\n at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)\r\n at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)\r\n at Microsoft.EntityFrameworkCore.DbContext.get_DbContextDependencies()\r\n at Microsoft.EntityFrameworkCore.DbContext.get_InternalServiceProvider()\r\n at Microsoft.EntityFrameworkCore.DbContext.get_DbContextDependencies()\r\n at Microsoft.EntityFrameworkCore.DbContext.get_Model()\r\n at Microsoft.EntityFrameworkCore.Internal.InternalDbSet1.get_EntityType()\r\n at Microsoft.EntityFrameworkCore.Internal.InternalDbSet1.get_EntityQueryable()\r\n at Microsoft.EntityFrameworkCore.Internal.InternalDbSet1.System.Linq.IQueryable.get_Provider()\r\n at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.Include[TEntity,TProperty](IQueryable1 source, Expression1 navigationPropertyPath)\r\n at Gsop.Cpo.ChargeLocationService.Business.Services.OpeningTimesService.GetRegularOpeningHoursPeriodsAsync(String chargeStationId, DateTime utcNow) in D:\a\1\s\Gsop.Cpo.ChargeLocationService.Business\Services\OpeningTimesService.cs:line 348\r\n at Gsop.Cpo.ChargeLocationService.Business.Services.OpeningTimesService.GetAllPeriodsForAllChargeStations(String chargeStationId, DateTime utcNow) in D:\a\1\s\Gsop.Cpo.ChargeLocationService.Business\Services\OpeningTimesService.cs:line 182\r\n at Gsop.Cpo.ChargeLocationService.Business.Services.OpeningTimesService.ProcessOpeningHoursPeriods(String chargeStationId, Nullable1 utcNowOverride) in D:\\a\\1\\s\\Gsop.Cpo.ChargeLocationService.Business\\Services\\OpeningTimesService.cs:line 35\r\n at Gsop.Cpo.ChargeLocationService.Functions.FunctionsTimerTriggers.CalculateOpeningTimes(TimerInfo myTimer) in D:\\a\\1\\s\\Gsop.Cpo.ChargeLocationService.Functions\\Functions.Timer.cs:line 36", "Message": "Container is disposed and should not be used: Container is disposed.\r\nYou may include Dispose stack-trace into the message via:\r\ncontainer.With(rules => rules.WithCaptureContainerDisposeStackTrace())", "Data": {}, "Source": "Microsoft.Azure.WebJobs.Script.WebHost", "HResult": -2146233079 }

The function Version is V3 we're running .net core 3.1 and ef core 3.1 version.

This is ridiculous that we can't use proper DI and HttpClient injection with functions runtime and packages that are in production.

You need to provide at least a workaround otherwise there's no point in using azure functions whatsoever.

Without the Feature flag we can at least get our functions running and only encountering the issue rarely.

@thomasvdb
Copy link

@Gevil The workaround we found when using HttpClients is by adding an extra abstraction layer and don't inject an HttpClient (or HttpClientFactory) directly. This is now running for about 1 year in production without any issues.

Example:

public class FunctionWorkaround
{
        private readonly IHttpClientFactory httpClientFactory;

        public FunctionWorkAround(IHttpClientFactory httpClientFactory)
        {
            this.httpClientFactory = httpClientFactory;
        }
        
        public HttpClient CreateClient() => httpClientFactory.CreateClient(...);
}

@eluchsinger
Copy link

eluchsinger commented Sep 20, 2021

@thomasvdb so what do you inject into the Function then? In the example you are injecting an IHttpClientFactory. Do you inject the FunctionWorkaround?

@eluchsinger
Copy link

@fabiocav @brettsam This issue looks like related to my new issue reported. I don't think that it's the same issue though so I wouldn't merge them.

Azure/azure-functions-dotnet-worker#628

@VaclavK
Copy link

VaclavK commented Sep 23, 2021

Our strategy so far dealing with this

in our case (given this is your vanilla ef.core n-tier app with db store, this may be applicable for other folks) was that due to separate db context (and its associated changed entities), data was not being saved as save changes was invoked on the primary db context ,not the secondary

we have moved all functions that talk to Http (fortunately that is few integration points in our apps) to a separate fn app... these are woken by commands and do the http dance and persist whatever they retrieve etc into db store, for many of the data services we have added an optional param to send a repository object to interact with DB - so as to skip the one injected by DI (as that is tied to a separate db context due to this bug)

this way we gain additional runtime isolation - not to frown upon - all tied to the same fn app plan so the overhead is not huge - and the code change associated is easy to dissolve once this is resolved (and we could have done feature flag)

@thomasvdb
Copy link

@eluchsinger Indeed, I inject the FunctionWorkaround (it has an interface as well in our case, left it out in the example).

@swettstein
Copy link

@swettstein are you referring to the change in defaults? If so, the change was not made because of concerns with a few other items landing right before //build and some announcements.

My goal is to have the defaults change with the following release.

@fabiocav any update on making this feature flag default? i haven't seen/heard anything. our team has been tracking this issue for a long time and we have to add this feature flag to each app we make so would be nice that it gets moved to default.

@swettstein
Copy link

@fabiocav any update on making this feature flag default? i haven't seen/heard anything. our team has been tracking this issue for a long time and we have to add this feature flag to each app we make so would be nice that it gets moved to default.

in case anyone else is looking for the answer, i found it here: #7669

seems that it will only be removed in v4, not v3

@soloham
Copy link

soloham commented Mar 5, 2022

I've just experienced this issue when using middlewares (.NET 6, isolated), I have got an auditing scope defined in the middleware before the function call where I resolve a (scoped) EF DB context here and would like to call SaveChanges on this DB context at the end of the request pipeline, but it seems this DB context is different to the one that is resolved in the function, so doing save changes at the end does nothing as the changes never got tracked on this DB context.

Please let me know if this is a bug or expected behavior for the host and if expected how can I achieve the above goal. Thanks!

@fabiocav @brettsam FYI

@brettsam
Copy link
Member

brettsam commented Mar 7, 2022

@soloham -- can you open an issue in the https://github.com/Azure/azure-functions-dotnet-worker/issues repo? Please include some example code to show us how things are set up in your scenario.

This issue is related to the in-proc DI setup, which is different from that in the isolated worker.

@soloham
Copy link

soloham commented Mar 7, 2022

It appears my issue is already answered (#811) in the appropriate repo. Thanks @brettsam!

@jonmeyerson
Copy link

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
Copy link

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
Copy link

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
Copy link

campelo commented Sep 30, 2022

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
Copy link
Member

@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
Copy link
Member

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
Copy link

Assassinbeast commented Oct 19, 2022

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
Azure/azure-functions-dotnet-worker#1133

@kshyju
Copy link
Member

kshyju commented Oct 19, 2022

@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
Copy link
Member

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!

@Azure Azure locked as resolved and limited conversation to collaborators Jun 14, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.