simpleinjector / SimpleInjector

An easy, flexible, and fast Dependency Injection library that promotes best practice to steer developers towards the pit of success.
https://simpleinjector.org
MIT License
1.21k stars 154 forks source link

Hybrid Async Scoped MVC 5 + Async Method bug #983

Closed leonardolb closed 4 months ago

leonardolb commented 11 months ago

Hi! i'm facing this error and I really can't go through it.

image

My code:

it start at this action:

image

goes to this block

image

and exception is thrown at the highlighted part of code.

Can anyone help me?

this is my lifestyle

image

It's an ASP.NET MVC5 app that runs some background tasks, WebAPI controllers and SignalR.

thanks!!

dotnetjunkie commented 11 months ago

Hi @leonardolb,

I've got trouble reading the images on my mobile phone. Would you mind replacing them with actual text, as was requested in the bug report template you used while filling in your question?

Thanks in advance

dotnetjunkie commented 11 months ago

And don't forget to post a full stack trace

leonardolb commented 11 months ago

Sorry about that!!

Im facing this exception:

PersonDataConfigBO is registered using the 'Hybrid Async Scoped / Web Request' lifestyle, but the instance is requested outside the context of an active (Hybrid Async Scoped / Web Request) scope. Please see https://simpleinjector.org/scoped for more information about how apply lifestyles and manage scopes.

My stacktrace is:

SimpleInjector.ActivationException: PersonDataConfigBO is registered using the 'Hybrid Async Scoped / Web Request' lifestyle, but the instance is requested outside the context of an active (Hybrid Async Scoped / Web Request) scope. Please see https://simpleinjector.org/scoped for more information about how apply lifestyles and manage scopes.
at SimpleInjector.Scope.GetScopelessInstance(ScopedRegistration registration)
at SimpleInjector.Scope.GetInstance[TImplementation](ScopedRegistration registration, Scope scope)
at SimpleInjector.Advanced.Internal.LazyScopedRegistration1.GetInstance(Scope scope)
at lambda_method(Closure )
at SimpleInjector.InstanceProducer.GetInstance()
at SimpleInjector.Container.GetInstance[TService]()
at AdSolutions.AdData.Context.Pessoa.PessoaContext.<ConsultarCNPJ>d__3.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter1.GetResult()
at AdSolutions.Site.AdData.Controllers.PersonController.<Detail>d__3.MoveNext() in C:\Users\leonardo\Desktop\svn\AdTraffic\master\AdSolutions.Site.Host\Areas\AdData\Controllers\PersonController.cs:line 100
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Web.Mvc.Async.TaskAsyncActionDescriptor.EndExecute(IAsyncResult asyncResult)
at System.Web.Mvc.Async.AsyncControllerActionInvoker.<>c__DisplayClass8_0.<BeginInvokeAsynchronousActionMethod>b__1(IAsyncResult asyncResult)
at System.Web.Mvc.Async.AsyncControllerActionInvoker.EndInvokeActionMethod(IAsyncResult asyncResult)
at System.Web.Mvc.Async.AsyncControllerActionInvoker.AsyncInvocationWithFilters.<>c__DisplayClass11_0.<InvokeActionMethodFilterAsynchronouslyRecursive>b__0()
at System.Web.Mvc.Async.AsyncControllerActionInvoker.AsyncInvocationWithFilters.<>c__DisplayClass11_2.<InvokeActionMethodFilterAsynchronouslyRecursive>b__2()
at System.Web.Mvc.Async.AsyncControllerActionInvoker.AsyncInvocationWithFilters.<>c__DisplayClass11_2.<InvokeActionMethodFilterAsynchronouslyRecursive>b__2()
at System.Web.Mvc.Async.AsyncControllerActionInvoker.AsyncInvocationWithFilters.<>c__DisplayClass11_2.<InvokeActionMethodFilterAsynchronouslyRecursive>b__2()
at System.Web.Mvc.Async.AsyncControllerActionInvoker.EndInvokeActionMethodWithFilters(IAsyncResult asyncResult)
at System.Web.Mvc.Async.AsyncControllerActionInvoker.<>c__DisplayClass3_6.<BeginInvokeAction>b__4()
at System.Web.Mvc.Async.AsyncControllerActionInvoker.<>c__DisplayClass3_1.<BeginInvokeAction>b__1(IAsyncResult asyncResult)`

Everything start at my PersonController "Detail" Action. It async calls "ConsultarCNPJ" in my context class

public async Task<JsonResult> Detail(PessoaModel model, PessoaDadoPessoaConfigModel documento)
{
    if (model == null)
        model = new PessoaModel();

    var service = base.CreateContext<IPessoaContext>();

    if (documento != null)
    {
        var consultarCnpj = false;

        if (consultarCnpj.False())
            model = service.ObterNovo(documento);
        else
            **_model = await service.ConsultarCNPJ(documento);_**
    }
    else if (model.State.In(ModelStateType.Normal))
        model = service.ObterDetalhes(model);

    return AjaxViewResult(new PessoaViewModel(model));
}

"ConsultarCNPJ" is a simple method that triggers a mediator (ThirdPartyCommandMediator) and awaits all available commands complete (just one expected for this ConsultarCNPJThirdPartyCommand).

When finished, base.CreateBO().List is called to continue some procedures.

base.CreateBO().List is not async and it goes to directly to database (oracle).

CreateBO method is just a wrapper to simpleinjector Container.Resolve.

public async Task<PessoaModel> ConsultarCNPJ(PessoaDadoPessoaConfigModel documento)
{
    var command = new ConsultarCNPJThirdPartyCommand
    {
        CNPJ = documento.dsValor.RemoveSpecialCharacters().RemoveDiacritics()
    };

    var results = await **ThirdPartyCommandMediator**.Handle(command).ConfigureAwait(false);

    var queryData = results[0];

    if (queryData.Success)
    {
        var pessoa = queryData.Command.ReturnValue.Pessoa;

        var documentosPessoa = **base.CreateBO<IPersonDataConfigBO>().List**(
            new PaisModel { sqPais = PaisModel.Brasil },
            TipoPessoa.Juridica,
            Situacao.Ativo).Select(sel => new PessoaDadoPessoaConfigModel
            {
                DadoPessoaConfig = sel,
                State = ModelStateType.New
            }).ToList();
}

I'm using "Lifestyle = Lifestyle.CreateHybrid(new SimpleInjector.Lifestyles.AsyncScopedLifestyle(), new WebRequestLifestyle())" as my lifestyle.

Finally, this is the Handler triggered when ThirdPartyCommandMediator.Handle() is invoked

public override async Task<ThirdPartyCommandHandlerContext<ConsultarCNPJThirdPartyCommand>>
    Handle(ThirdPartyCommandHandlerContext<ConsultarCNPJThirdPartyCommand> commandContext)
{

    var targetApp = new SERPROPartyApp(commandContext.Context);
    var command = commandContext.Command;

    if (targetApp.BaseUrl.IsNullOrWhiteSpace())
        return commandContext;
    else
    {
        var client = new SERPRORestClient(targetApp.Username, targetApp.Password, targetApp.BaseUrl);

        var dados = await client.Consultar(command.CNPJ).ConfigureAwait(false);
    }
}

public async Task<CNPJ> Consultar(string cnpj)
{
    var request = new RestRequest("/consulta-cnpj-df/v2/basica/" + cnpj, Method.Get);

    var response = await this._client.ExecuteAsync<CNPJ>(request).ConfigureAwait(false);

    return response?.Data;
}
dotnetjunkie commented 11 months ago

Can you debug through your application and determine exactly after which line of code the scope is gone? e.g. is that after:

leonardolb commented 11 months ago

Yes! Exception is thrown at this line when resoling IPersonDataConfigBO

var documentosPessoa = base.CreateBO().List in ConsultarCNPJ method.

dotnetjunkie commented 11 months ago

Sorry, that's not what I meant. It's clear to me where the exception is thrown, but there is a point in the application that Simple Injector's Scope is gone. This is what eventually leads to the exception. What I want to know is: what is the first ocurrance in your code where that Scope is gone. This will likely be long before the exception happens.

In order to test this, you might need to make some temporary adjustments to your code; the easiest way to test is by calling into the container to resolve a scoped dependency, for instance just after the lines of code I mentioned above.

leonardolb commented 11 months ago

Sorry my misunderstanding!

I've debuged my code, scope is gone after

var response = await this._client.ExecuteAsync<CNPJ>(request).ConfigureAwait(false);

leonardolb commented 11 months ago

Unfortunately I can't debug further into ExecuteAsync as its is a method from RestSharp.RestClient class.

dotnetjunkie commented 11 months ago

That likely means that there is something happening within RestSharp.RestClient that causes the asynchronous context not to flow. The most likely case is that it is using an async foreach like construct over IAsyncEnumerable. This can cause the asynchronous context to be cleared and Microsoft hasn't provided a fix for this. Although you could try posting a bug report with RestSharp.RestClient, chances are slim that the makers will act on this.

What you can do instead is make sure that all your dependencies for that scope are resolved before this point. This means stepping away from lazy loading your dependencies, as you are doing right now.

leonardolb commented 11 months ago

Thanks! I've removed .ConfigureAwait(false) from it just to test and scope stopped being lost. However, I don't feel I should do it as it may cause deadlock at some background tasks that consumes this same method.

I'll step away from lazyloading so!

thanks!

dotnetjunkie commented 11 months ago

I've removed .ConfigureAwait(false)

I didn't expect this.

However, I don't feel I should do it as it may cause deadlock at some background tasks that consumes this same method.

You don't have to worry about that. This only holds when building client applications such as WPF and Win Forms. This doesn't hold for ASP.NET. AFAIK, It will never deadlock on this.

leonardolb commented 11 months ago

Maybe i need to remove other ConfigureAwait I have so, maybe they are causing some trouble too i guess