Using Transient service within a Singleton Service
Asked Answered
T

2

5

I am using ASP.Net Core 3.1 Web API. I have a need to perform an additional activity in the background, when I get a call to a controller action method, without adding too much delay in the processing of current action. Additional activity that I am performing is prepping some data into Cache so its available before user performs next step. So I want to simply trigger that activity, without waiting for it to complete. But at the same time, I don't want task to be disposed without completing, just because the action method completed it processing, returned response back to caller and the transient controller was disposed.

So I plan to use a Singleton service injected into controller to perform the task. But the task itself may involve needing to use a transient service. So instead of directly injecting transient service into Singleton Service, I am thinking injecting transient service into Controller. And then in the action method, I will pass the transient service as parameter to an async method on Singleton service and then within that method, call the required service.

public IActionResult GetSomething(string input1)
{
  var resp = await _transientService1.GetSomethingElse(input1);
  // I am not awaiting this task
  var backgroundTask = _singletonService1.DoSomethingAsync(_transientService2, resp.someattr);
  return Ok(resp);
}

Now within the singleton service, I will get the required data and write it into cache.

public async Task DoSomethingAsync(ITransientService2 myTransientService2, string someParam)
{
   var temp1 = await myTransientService2.GetSomethingNew(someParam);
   var temp2 = await _SingletonService2.WriteCache(temp1);
}

So I wanted to know first of all, if this approach will work. If it works, what are the pitfalls or gotchas, that I need to be aware of.

Currently, this is all conceptual. Else I would have tried it out directly:) Hence the questions.

Trifid answered 24/6, 2021 at 20:0 Comment(2)
I would say this is a good place to use background tasks with hosted services in ASP.NET Core.Adenocarcinoma
You might want to question how you are passing information between controllers and a background process. There is bug smell there. If the singleton really is a background process, asking it to do things from a request thread might introduce some synchronization problems that could be avoided with a request queue.Fielder
H
7

That can work as long as you're happy with passing the dependency as an argument.

If you don't want to pass the transient dependency in as an argument, another option is to inject the IServiceProvider into the singleton service, and instantiate the transient service when it's needed.

class MySingleton
{
    private readonly IServiceProvider _serviceProvider;

    public MySingleton(IServiceProvider serviceProvider)
    {
         _serviceProvider = serviceProvider;
    }

    public async Task ExecuteAsync()
    {
        // The scope informs the service provider when you're
        // done with the transient service so it can be disposed
        using (var scope = _serviceProvider.CreateScope())
        {
            var transientService = scope.ServiceProvider.GetRequiredService<MyTransientService>();
            await transientService.DoSomethingAsync();
        }
    }
}
Hamm answered 24/6, 2021 at 20:13 Comment(1)
It would be better if it was IServiceScopeFactory instead. Registering the service provider itself in your service collection opens up for possible abuse and introducing service resolution anti-patterns. Not only that, the IServiceProvider's scope is not always a singleton, so it can break the IServiceProvider's scope depending on the context.Hauteur
H
3

Will this approach work?

Probably not. Having transients as apart of your singleton (or injecting them within your constructor) will break them as transients. You can directly pass a transient created in the parent to the method of your singleton to work on, but that's the limit at which it will work.

What are the pitfalls or gotchas?

Anti-patterns, spaghetti code and technical debt. By having transients dictating logic outside of a singleton pattern makes me suspicious you're baking in spaghetti code. I can't say that for certain but it's beginning to look like it.

My answer:

Let's look at Transients and why we want them: a transient is an object that can exist in the program as multiple instances. You don't even have to register it in your service collection if it's just a data transfer object or shared model: just var myObj = new MyObject(); and you're good. Example:

public class MyDto
{
  // by convention my dtos are nullable
  public string? Name { get; set; }
  public int? Id { get; set; }
}

//... in code ...
var myTransient = new MyDto();

//... or automapper ....
List<MyDto> objects = DbContext.Customers
  .Where(x => x.Name.Contains("something")
  .ProjectTo<MyDto>(config)
  .ToList();

However, if you want logging, or options to be injected or anything like that, then you want to register it as a transient in your service collection. This example may describe a known use case where a setting from an appsettings configuration file needs to determine when an object has timed out. It's convenient to have that as a calculated property and to inject the settings directly:

public class MyTransient
{
  private readonly IOptions<TransientSettings> _settings;
  public MyTransient(IOptions<TransientSettings> transientSettings)
  {
    _settings = transientSettings;
  }

  public DateTime? MyDate { get; set; }
  public DateTime? Timeout => MyDate is not null ? 
    MyDate.AddSeconds(_settings.Timeout) : null;
}

//... register it
serviceCollection.AddTransient<MyTransient>();

Unfortunately you can't just willy nilly inject your ServiceProvider, well you can, but you introduce what is called an anti-pattern. This is a no-no. I think the author of this page has established a very solid argument against the use of injecting your service provider anywhere in your application.

Nonetheless, what I suggest you do is think of Transients as objects you work on, like a simple data transfer object, or a shared model that's coming from your database layer. Transients should probably, in 90% of your cases, be simple instantiated objects that you don't even need to register. Always pass them in as arguments instead and have the logic for working on them within your infrastructure, NOT YOUR TRANSIENTS.

A mistake, I think anyway, that some developers tend to make is bake in logic directly in their transients, instead of thinking about logic that acts on transients to be outside of the transient. By reversing this you're decoupling business logic from your data transfer objects which reduces spaghetti code, and in my opinion makes it much cleaner.

If you want to inject transients into a singleton

If you really want to go this route, which shouldn't be often, you'll want to circumvent using the service provider. Never use it, never have it as apart of your service collection. Instead, .net has given us the IServiceScopeFactory which is a singleton you can use anywhere:

public void DoSomething(IServiceScopeFactory serviceScope)
{
  using var myScope = serviceScope.CreateScope();
  var myService = myScope.ServiceProvider.GetRequiredService<IMyService>();
  myService.DoSomethingElse();
}

And in this case, in my own work, I even abstract that out one more layer by ensuring that I'm only using it within a singleton environment and only resolving a transient I've explicitly allowed to be resolved with forethought using the ITransientService interface:

public class TransientServiceProvider : ITransientServiceProvider
{
    private readonly IServiceScopeFactory _serviceScopeFactory;

    public TransientServiceProvider(IServiceScopeFactory serviceScopeFactory)
    {
        _serviceScopeFactory = serviceScopeFactory;
    }

    public T GetRequiredTransient<T>() where T : ITransientService
    {
        var scopedServiceProvider = _serviceScopeFactory.CreateScope();
        return scopedServiceProvider.ServiceProvider.GetRequiredService<T>();
    }
}

I prefer these kinds of targeted approaches to resolution so that avoid introducing anti-patterns and to keep code clean, readable, and purposeful.

Hope this helps some folks out there wondering about this.

Hauteur answered 2/3, 2023 at 22:10 Comment(2)
In your last example, you are disposing of the scope used to create the transient you are returning in TransientServiceProvider.GetRequiredTransient. Doesn't this mean your transient would have been disposed with the scope if it is IDisposable?Alaster
@Alaster - You are correct, thank you for catching that. At first I was thinking object scope and automatic garbage collection and then thought about it. Most of the time, when I'm using the using statement in this way, I'm using it on objects that ARE NOT tied to logic that would require the dispose method or I'm specifically fetching a db context object, but in case anyone really cares about using that interface on a transient, this wouldn't work. I've edited my answer to be more ambiguous in this case.Hauteur

© 2022 - 2024 — McMap. All rights reserved.