pakrym / jab

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

Support class modules and Import(Type, string Factory) #97

Open ViktorHofer opened 2 years ago

ViktorHofer commented 2 years ago

It would be great if a module could be defined as a class so that it can have a constructor which is useful when passing in Func factory methods to the module. In the provider itself, the module would then be declared as [Import(typeof(X), Factory="ModuleFactory")].

Just in theory, is that possible? What was the reason for choosing an interface over a class for modules?

pakrym commented 2 years ago

Can you show me a more extended example of what you are imagining, please?

What was the reason for choosing an interface over a class for modules?

It avoids many hard questions that I'm not sure are worth solving :)

Imagine all factories were classes, now all of them need to be instantiated. When does that happen and what do we do with their ctor parameters? What if some services from modules are unused (because they are overridden by other modules)? What if someone wants to resolve another service from a module service factory implementation?

ViktorHofer commented 2 years ago

An example from my tooling in which I use a module and two factories.

Current code

namespace Microsoft.DotNet.PackageValidation
{
    [ServiceProvider]
    [Import(typeof(IServiceLocatorModule))]
    [Singleton(typeof(CompatibleFrameworkInPackageValidator))]
    [Singleton(typeof(CompatibleTfmValidator))]
    [Singleton(typeof(BaselinePackageValidator))]
    internal partial class ServiceProvider : IServiceLocatorModule
    {
        private readonly Func<ISuppressionEngine, ICompatibilityLogger> _logFactory;
        private readonly Func<ISuppressionEngine> _suppressionEngineFactory;

        // It's important to use GetService<T> here instead of directly invoking the factory
        // to avoid two suppression engine being created.
        public ICompatibilityLogger LogFactory() => _logFactory(GetService<ISuppressionEngine>());

        public ISuppressionEngine SuppressionEngineFactory() => _suppressionEngineFactory();

        public ServiceProvider(Func<ISuppressionEngine, ICompatibilityLogger> logFactory,
            Func<ISuppressionEngine> suppressionEngineFactory)
        {
            _logFactory = logFactory;
            _suppressionEngineFactory = suppressionEngineFactory;
        }
    }

    [ServiceProviderModule]
    [Singleton(typeof(ISuppressionEngine), Factory = nameof(SuppressionEngineFactory))]
    [Singleton(typeof(ICompatibilityLogger), Factory = nameof(LogFactory))]
    [Singleton(typeof(IApiComparerFactory), typeof(ApiComparerFactory))]
    [Singleton(typeof(IAssemblySymbolLoaderFactory), typeof(AssemblySymbolLoaderFactory))]
    [Singleton(typeof(IMetadataStreamProvider), typeof(MetadataStreamProvider))]
    [Singleton(typeof(IApiCompatRunner), typeof(ApiCompatRunner))]
    internal interface IServiceLocatorModule
    {
        ICompatibilityLogger LogFactory();

        ISuppressionEngine SuppressionEngineFactory();
    }
}

Potential implementation with class module

namespace Microsoft.DotNet.PackageValidation
{
    [ServiceProvider]
    [Import(typeof(CommonServiceLocatorModule), Factory = nameof(CreateCommonServiceLocatorModule))]
    [Singleton(typeof(CompatibleFrameworkInPackageValidator))]
    [Singleton(typeof(CompatibleTfmValidator))]
    [Singleton(typeof(BaselinePackageValidator))]
    internal partial class ServiceProvider
    {
        public readonly Func<CompatibilityServiceProviderModule> CreateCommonServiceLocatorModule;

        public ServiceProvider(Func<ISuppressionEngine, ICompatibilityLogger> logFactory,
            Func<ISuppressionEngine> suppressionEngineFactory)
        {
            CreateCommonServiceLocatorModule = () => new CommonServiceLocatorModule(logFactory, suppressionEngineFactory);
        }
    }

    [ServiceProviderModule]
    [Singleton(typeof(ISuppressionEngine), Factory = nameof(SuppressionEngineFactory))]
    [Singleton(typeof(ICompatibilityLogger), Factory = nameof(LogFactory))]
    [Singleton(typeof(IApiComparerFactory), typeof(ApiComparerFactory))]
    [Singleton(typeof(IAssemblySymbolLoaderFactory), typeof(AssemblySymbolLoaderFactory))]
    [Singleton(typeof(IMetadataStreamProvider), typeof(MetadataStreamProvider))]
    [Singleton(typeof(IApiCompatRunner), typeof(ApiCompatRunner))]
    internal class CommonServiceLocatorModule
    {
        public readonly Func<ICompatibilityLogger> LogFactory;
        public readonly Func<ISuppressionEngine> SuppressionEngineFactory;

        public CommonServiceLocatorModule(Func<ISuppressionEngine, ICompatibilityLogger> logFactory,
            Func<ISuppressionEngine> suppressionEngineFactory)
        {
            // It's important to use GetService<T> here instead of directly invoking the factory
            // to avoid two suppression engine being created.
            LogFactory = () => logFactory(GetService<ISuppressionEngine>());
            SuppressionEngineFactory = suppressionEngineFactory;
        }
    }
}

This is just an example with two factories (as I don't use more yet) and with your recent contribution the code become less verbose when using them. I don't know if it's worth introducing the extra complexity but I hope the above example better demonstrates what I was imagining from an API consumption standpoint.

skarllot commented 2 years ago

The following code works:

[ServiceProviderModule]
[Import(typeof(IOptionsModule))]
[Singleton(typeof(ILoggerFactory), typeof(LoggerFactory))]
[Singleton(typeof(ILogger<>), typeof(Logger<>))]
[Singleton(typeof(IConfigureOptions<LoggerFilterOptions>), Factory = nameof(CreateFilterOptions))]
public interface ILoggingModule
{
    public static IConfigureOptions<LoggerFilterOptions> CreateFilterOptions(LogLevel logLevel) =>
        new ConfigureOptions<LoggerFilterOptions>(options => options.MinLevel = logLevel);
}