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.