DavidAJohn / PhotoPortfolio

Personal photo portfolio .NET web application which implements a Blazor and Tailwind CSS user interface with a MongoDb database, and includes integration with Stripe Checkout and Prodigi Print API
0 stars 0 forks source link

Order Approval background service problem #61

Closed DavidAJohn closed 9 months ago

DavidAJohn commented 9 months ago

In trying to use a message queue to add some resiliency to the order approval process, I've come up against a problem with the scoping of services in .NET.

The message consumer needs to be running as a background/hosted service. But background services are singletons by default.

This is a problem when you need to call a method from a scoped service (in this case, the Order Service) from within the background service, and it results in a runtime error.

Microsoft have a solution, which is to create a separate scoped service which the background service methods can run within. It feels kind of messy and I found it a bit difficult to follow even after reading it several times. There is a better option - see https://github.com/DavidAJohn/PhotoPortfolio/issues/61#issuecomment-1753346259

I'm looking at other solutions. Maybe it's time to try using MediatR, although I'm not sure if that would fix the scoping problem either.

DavidAJohn commented 9 months ago

As I suspected, MediatR can't solve the problem. Here's the error I get when the Message Consumer class tries to process an incoming message. It's effectively the same issue as before:

[18:56:08 ERR] Message Consumer -> Error when deserializing message body: Cannot resolve 'MediatR.IRequestHandler``1[PhotoPortfolio.Server.Messaging.OrderApproved]' from root provider because it requires scoped service 'PhotoPortfolio.Server.Contracts.IOrderService'. System.InvalidOperationException: Cannot resolve 'MediatR.IRequestHandler``1[PhotoPortfolio.Server.Messaging.OrderApproved]' from root provider because it requires scoped service 'PhotoPortfolio.Server.Contracts.IOrderService'. at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteValidator.ValidateResolution(Type serviceType, IServiceScope scope, IServiceScope rootScope) at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope) at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType) at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider) at MediatR.Wrappers.RequestHandlerWrapperImpl``1.<>c__DisplayClass1_0.<<Handle>g__Handler|0>d.MoveNext()

The MediatR request handler can't get around the need to called a scoped service. Which makes sense really.

Using MediatR now kind of feels like overkill. It's not a bad thing to separate the sending of the order to Prodigi from the message consumer class, but that could be done without MediatR by just calling the existing method from the Order Service (which apparently we'll have to do anyway).

DavidAJohn commented 9 months ago

Here is another simpler potential solution - using IServiceScopeFactory:

https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection#scope-scenarios

The key part to note is this:

To achieve scoping services within implementations of IHostedService, such as the BackgroundService, do not inject the service dependencies via constructor injection. Instead, inject IServiceScopeFactory, create a scope, then resolve dependencies from the scope to use the appropriate service lifetime.

DavidAJohn commented 9 months ago

After trying it out, using IServiceScopeFactory seems to solve the problem.