-1

I'm having a huge problem with the configuration/dependency injection of an application.

I have a singleton class added through DI with AddSingleton, that has in its constructor a IRequestClient, that is scoped because busConfigurator.AddRequestClient() which among other things, has the same effect as AddScoped.

When I start the app, it says "Cannot consume scoped service 'MassTransit.IRequestClient`1[...]' from singleton '...'.)"

Which absolutely makes sense.

The weirdest thing is that I have another app set up the exact same way, but it just works and I would really like for that class to remain singleton.

My colleague and I spent an entire day trying to find the differences between the two applications, but they are virtually the same in their configurations, so we are having trouble in understanding why one works while the other doesn't.

I'm not entirely sure on what details could be important to better define the problem, so feel free to ask.

We've looked all around the internet trying to find a solution, but it was always "Change singleton to transient", but that's not an option, first because it HAS to be a singleton, otherwise it wouldn't make sense in our app, as that thing is what caches lots of date from our db so we can't just go around keeping on collecting heaps of data, second because the first app works with singleton, not with transient and we'd like to keep it that way

// This method is called in Main()
private static void ConfigureMassTransit(IServiceCollection services)
        {
            services.AddMassTransit(busConfigurators =>
            {
                busConfigurators.AddRequestClient<ICacheRepository>();

                busConfigurators.AddConsumers(typeof(Program).GetTypeInfo().Assembly);

                busConfigurators.UsingRabbitMq((context, cfg) =>
                {
                    cfg.Host(new Uri($"rabbitmq://{Config.Settings.RabbitMq_Host}"), hostConfigurator =>
                    {
                        hostConfigurator.Username(Config.Settings.RabbitMq_User);
                        hostConfigurator.Password(Config.Settings.RabbitMq_Password);
                    });

                    cfg.ReceiveEndpoint("myApp", e =>
                    {
                        e.ConfigureConsumers(context);
                    });
                });

            });

// CacheRepository
public class CacheRepository : ICacheRepository
    {
        private readonly IClient Client;

        public CacheRepository(ICacheRepository client, ILogger<CacheRepository> logger)
        {

            this.client = client;
            this.logger = logger;
        }
     }
  • "I have another app set up the exact same way, but it just works" that doesn't seem right. i'm quite certain there's a difference you've just overlooked. because you simply _can't_ directly consume a scoped service in a context that doesn't have a scope. (using a scoped service in singleton is usually done by utilising a `ServiceScopeFactory`). could you include a minimal reproducible example of this phenomenon in your question? – Franz Gleichmann Nov 11 '22 at 15:53
  • Hi, I added a code snippet as requested. Let me know if you need anything else – nonnodacciaio Nov 14 '22 at 11:40
  • ...your code snippet doesn't show _any_ service being added to the service collection. – Franz Gleichmann Nov 21 '22 at 15:54

2 Answers2

0

When a dependency is scoped, the implication is that a new instance is needed for each scope (which is usually an incoming HTTP request or message.) It implies that the instance should not be re-used for multiple requests.

If you have a singleton that depends on that scoped dependency, that singleton will be created using an instance of that dependency (the request client.) Because that singleton "lives forever," so does the instance of the request client it contains.

The result is that the request client is not supposed to be re-used across different scopes, but now it is. One instance is used forever.

A likely solution is to modify the class that depends on that client so that it doesn't need to be a singleton. You mentioned that it has to be a singleton because it caches data.

How does it cache data? Does it do so by storing data in a private field? If so, perhaps you could make that field static. Now the class instance isn't re-used, but those fields are shared between instances. (Verify that interaction with those fields is thread safe if they may be accessed concurrently.)

Or if there's some other cache mechanism, you could move that into its own dependency and make that a singleton.

Then your class can be scoped. It will depend on the singleton cache, always using the same instance. It will also depend on the scoped request client, using a new instance for each scope.

Scott Hannen
  • 27,588
  • 3
  • 45
  • 62
0

You could inject IServiceProvider instead, and create a scope when the singleton needs to perform a request. That way, you're sticking to the expected use of the request client.

await using var scope = provider.CreateAsyncScope();

var client = scope.ServiceProvider.GetRequiredService<IRequestClient<T>>();

await client.GetResponse(...);
Chris Patterson
  • 28,659
  • 3
  • 47
  • 59