Reputation: 1716
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.
Upvotes: 5
Views: 12302
Reputation: 2993
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.
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.
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 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.
Upvotes: 3
Reputation: 13254
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();
}
}
}
Upvotes: 8