5

I have a service that I want to share between other transient services. Right now it's not really a service, but in real life application it will. How would I share my service using dependency injection?

I added some demo code below. The SharedService should be the same object for MyTransientService1 and MyTransientService2 in the "Scope" of MyCreatorService.

The second assert fails, while this is what I'd like to accomplish.

    class Program
    {
        static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        private static IHostBuilder CreateHostBuilder(string[] args)
            => Host.CreateDefaultBuilder(args)
                .ConfigureServices((_, services) =>
                {
                    services.AddScoped<SharedService>();
                    services.AddTransient<MyTransientService1>();
                    services.AddTransient<MyTransientService2>();
                    services.AddTransient<MyCreatorService>();
                    services.AddHostedService<MyHostedService>();
                });
    }

    public class SharedService
    {
        public Guid Id { get; set; }
    }

    public class MyTransientService1
    {
        public SharedService Shared;

        public MyTransientService1(SharedService shared)
        {
            Shared = shared;
        }
    }

    public class MyTransientService2
    {
        public SharedService Shared;

        public MyTransientService2(SharedService shared)
        {
            Shared = shared;
        }
    }

    public class MyCreatorService
    {
        public MyTransientService1 Service1;
        public MyTransientService2 Service2;

        public MyCreatorService(MyTransientService1 s1, MyTransientService2 s2)
        {
            Service1 = s1;
            Service2 = s2;
        }
    }

    public class MyHostedService : BackgroundService
    {
        private readonly IServiceProvider _serviceProvider;

        public MyHostedService(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }
        protected override Task ExecuteAsync(CancellationToken stoppingToken)
        {
            var creator1 = _serviceProvider.GetRequiredService<MyCreatorService>();
            var creator2 = _serviceProvider.GetRequiredService<MyCreatorService>();
            
            Assert.That(creator1.Service1.Shared.Id, Is.EqualTo(creator1.Service2.Shared.Id));
            Assert.That(creator1.Service1.Shared.Id, Is.Not.EqualTo(creator2.Service1.Shared.Id));
            
            return Task.CompletedTask;
        }
    }
Steven
  • 166,672
  • 24
  • 332
  • 435
  • 3
    You ask for a solution, which already seems to be provided in your question. Can you explain what is not working with this setup? – Silvermind Jun 07 '21 at 07:43
  • @Silvermind: The second assert fails, and this is what I'd like to accomplish. I'd like to create services that share another service in some kind of scoped way. For example, MyHostedService creates two MyCreatorServices. MyCreatorService has two transient services which should share a common service. But this common service is unique per MyCreatorService. Hope this makes sence. – Arnoud Vangrunderbeek Jun 07 '21 at 08:15
  • With MS.DI, if you resolve a Scoped dependency from the root scope, that Scoped will effectively be a Singleton. This is what happens in your hosted service. A hosted service is effectively a singleton (even though you might register it as transient, that doesn't matter) and its `IServiceProvider` is the root provider. That's what is causing your problem. The solution is to start a new scope using `serviceProvider.CreateScope()` inside the hosted service's methods. – Steven Jun 07 '21 at 08:39

2 Answers2

1

If you use AddScoped, the instance will be the same within the request (for instance for a HTTP Request & Response). If you need your other services to be created everytime they are resolved, you can indeed use AddTransient, but otherwise you can also use AddScoped.

I would also suggest you bind MyHostedService in this manner (if it has anything to do with your described problem), since it seems to be providing a Singleton binding. If a scope of an outer service (one with injected dependencies) is narrower, it will hold hostage injected dependencies. A singleton service with transient dependencies will therefore make all its dependencies singleton, since the outer service will only be created once and its dependencies only resolved once.

UPDATE

After understanding the problem more clearly this should work for you (no other bindings needed):

services.AddTransient<MyCreatorService>(_ =>
            {
                var shared = new SharedService();
                shared.Id = Guid.NewGuid();
                return new MyCreatorService(
                    new MyTransientService1(shared),
                    new MyTransientService2(shared));
            });

Aage
  • 5,932
  • 2
  • 32
  • 57
  • I need the other services (MyTransientService1 & MyTransientService2) to be created everytime they are resolved, and they need to share the SharedService. Can you give me more details on how to do so ? Thanks a lot in advance. – Arnoud Vangrunderbeek Jun 07 '21 at 08:07
  • Like @silvermind asked in the comments, what is not working in your setup? – Aage Jun 07 '21 at 08:09
  • The second assert fails, and this is what I'd like to accomplish. I'd like to create services that share another service in some kind of scoped way. For example, MyHostedService creates two MyCreatorServices. MyCreatorService has two transient services which should share a common service. But this common service is unique per MyCreatorService. Hope this makes sence. – Arnoud Vangrunderbeek Jun 07 '21 at 08:15
  • Your example should have two *different* instances of `MyCreatorService` using the *same* instance of `SharedService`. Is this not what you want? Your second Assert fails because they *are* the same `Id`. – Aage Jun 07 '21 at 08:25
  • Yes I know, but I want them to be different. See I want MyCreatorService having MyTransientService1 and MyTransientService2 sharing the same MySharedService. So for every "new" MyCreatorService, there should be a "new" MySharedService, which is shared between the newly created MyTransientService1 and MyTransientService2. – Arnoud Vangrunderbeek Jun 07 '21 at 08:28
  • MyCreatorService in my real-life setup can be created 100'ds of times. So it is not possible to create different instances of it. – Arnoud Vangrunderbeek Jun 07 '21 at 08:40
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/233410/discussion-between-aage-and-arnoud-vangrunderbeek). – Aage Jun 07 '21 at 08:44
0
  1. Add a constructor to the SharedService class, otherwise the Id is always 000.

  2. Use the following code to create a different scope, so the SharedService will be initialized twice:

    MyCreatorService creator1;
    MyCreatorService creator2;
    using (var scope1 = _serviceProvider.CreateScope())
    {
      creator1 = scope1.ServiceProvider.GetRequiredService<MyCreatorService>();
    }
    using (var scope2 = _serviceProvider.CreateScope())
    {
      creator2 = scope2.ServiceProvider.GetRequiredService<MyCreatorService>();
    }
    
Jeremy Caney
  • 7,102
  • 69
  • 48
  • 77
PeterT
  • 487
  • 3
  • 9