pakrym / jab

C# Source Generator based dependency injection container implementation.
MIT License
1.03k stars 33 forks source link

Creating lazy type-safe factories for safe runtime service provision #152

Open cn-ml opened 9 months ago

cn-ml commented 9 months ago

Problem

I have the scenario where I want to decide at runtime which service to inject.

public interface IExampleService;
public class ExampleServiceA : IExampleService;
public class ExampleServiceB : IExampleService;

I see different solutions for this right now:

Use injected IServiceProvider

[ServiceProvider]
[Singleton<IExampleService>(Factory = nameof(ChooseExampleService))]
[Singleton<ExampleServiceA>] // No service for type 'releaser.Lib.ExampleServiceA' has been registered.
[Singleton<ExampleServiceB>] // No service for type 'releaser.Lib.ExampleServiceB' has been registered.
public partial class ExampleProvider(bool useA) {
    public IExampleService ChooseExampleService(IServiceProvider provider) => useA
        ? provider.GetRequiredService<ExampleServiceA>()
        : provider.GetRequiredService<ExampleServiceB>();
}

⚠️ This works as expected, unless I do not register the example services using the attributes, which will result in runtime errors.

Use pre-generated instances

[ServiceProvider]
[Singleton<IExampleService>(Factory = nameof(ChooseExampleService))]
[Singleton<ExampleServiceA>]
[Singleton<ExampleServiceB>]
public partial class ExampleProvider(bool useA) {
    public IExampleService ChooseExampleService(ExampleServiceA a, ExampleServiceB b) => useA ? a : b;
}

⚠️ This also works, with the benefit of being type-safe, but eager evaluated. Missing the service registrations

Proposed solution

Using your IServiceProvider<T> interface or other wrappers to lazily create instances of the service. [^1]

Edit: maybe using something like Lazy<T> is better suited for this purpose.

[ServiceProvider]
[Singleton<IExampleService>(Factory = nameof(ChooseExampleService))]
[Singleton<ExampleServiceA>]
[Singleton<ExampleServiceB>]
public partial class ExampleProvider(bool useA) {
    internal IExampleService ChooseExampleService(IServiceProvider<ExampleServiceA> aProvider, IServiceProvider<ExampleServiceB> bProvider) => useA
        ? aProvider.GetService()
        : bProvider.GetService();
}

✅ This allows Jab to gather the necessary information on service dependencies beforehand (i.e. in this case IExampleService depends on ExampleServiceA and ExampleServiceB during runtime), which is lost when using the plain IServiceProvider interface. This could be a minimally invasive approach to solve this problem.

Let me know what you think of this idea. If you think this is out of scope, feel free to close this issue, it shall just be my suggestion. I just thought it might be fitting with the whole "build-time checking" and source generation aspects of this project.

[^1]: Note that the factory currently has to be internal at most, because of the accessibility level of the IServiceProvider<T>.

pakrym commented 9 months ago

This does make sense, I think injecting the Func<ExampleServiceA> is the way most of DI containers solve this.

cn-ml commented 9 months ago

This does make sense, I think injecting the Func is the way most of DI containers solve this.

Ah yes, you are right, somehow I forgot about the existence of Func<T>. That is definitely the better way to solve this regarding declared intent of the factory.