3
\$\begingroup\$

Problem

I have a DI scenario where I have multiple classes that implement a common interface (IModelService).

I'm using a generic framework class (PredictionEnginePool<,>) that has options injected (PredictionEnginePoolOptions<>). This has a property (ModelLoader) that must be set using AddOptions in Startup.ConfigureServices where I set it to an instance of CustomModelLoader obtained from the service provider (from the AddSingleton implementation factory) during options configuration. CustomModelLoader depends on a single IModelService for its lifetime, so it needn't be injected with IEnumerable<IModelService>.

Research

I have researched various patterns to achieve my aim which is to have CustomModelLoader have access to a single registered IModelService object while being assigned to the ModelLoader property of the options class PredictionEnginePoolOptions<,> (which is then injected into PredictionEnginePool<,>). But most of the solutions I've found use a service locator anti-pattern or break SRP and/or OCP; for example, most of the solutions offered on this SO question. And many people suggest that the built-in DI framework of .NET Core is too simple to cater for complex scenarios and to use something like Autofac or StructureMap.

However, I refuse to believe that Microsoft would make such a scenario difficult or impossible (as it's not that complicated) in a new modern framework as .NET Core which builds on almost two decades of experience from .NET Framework. I've been pretty impressed with .NET Core so far in terms of complying with and enabling SOLID and other good coding principles.

Own Attempt So Far

The code below shows my attempt to resolve this based on various related questions on SO. Basically, to provide CustomModelLoader with a single IModelService object, I inject options into it (CustomModelLoaderOptions). This has a IModelService property on it. To set this, I have another call to AddOptions which sets the property (ModelService) by getting the single IModelService object that returns true from CanTrain (a strategy design pattern). I use GetServices<IModelService> to resolve the IModelService objects. CanTrain is implemented in each concrete type (RegressionModelService and TimeSeriesModelService) to state whether it can work with the type specified at startup for each generic version of PredictionEnginePool (RegressionPrediction and TimeSeriesPrediction which are just simple POCOs).

These are the interfaces and classes:

interface IModelService
{
    bool CanTrain(Type type);
}

class RegressionModelService : IModelService
{
    public bool CanTrain(Type type) => type == typeof(RegressionPrediction);
}

class TimeSeriesModelService : IModelService
{
    public bool CanTrain(Type type) => type == typeof(TimeSeriesPrediction);
}

class CustomModelLoaderOptions
{
    public IModelService ModelService { get; set; }
}

class CustomModelLoader : ModelLoader
{
    public CustomModelLoader(IOptionsFactory<CustomModelLoaderOptions> options) {}
}

My only real restriction is PredictionEnginePool<,> and PredictionEnginePoolOptions<,>. These are framework classes that cannot change and must be registered as per below. The following code is in Startup.ConfigureServices:

services.AddScoped<IModelService, RegressionModelService>();
services.AddScoped<IModelService, TimeSeriesModelService>();
services.AddScoped<CustomModelLoader>();

AddPredictionEnginePool<RegressionPrediction>();
AddPredictionEnginePool<TimeSeriesPrediction>();

void AddPredictionEnginePool<T>()
{
    services.AddSingleton<PredictionEnginePool<Input, T>>(sp =>
    {
        services.AddOptions<PredictionEnginePoolOptions<Input, T>>().Configure(options1 =>
        {
            services.AddOptions<CustomModelLoaderOptions>().Configure(options2 =>
            {
                options2.ModelService = sp.GetServices<IModelService>().Single(s => s.CanTrain(typeof(T)));
            });

            options1.ModeLoader = sp.GetService<CustomModelLoader>();
        });
    });
}

Question

I have spent many hours on this and I believe the above solution solves it without breaking SOLID principles (particularly SRP and OCP) and without using any anti-patterns. It's also using the built-in DI framework and avoids named instances.

However, I am not entirely happy with my solution because it still requires me to call GetService on the service provider to get IModelService and then assign it to a property on CustomModelLoaderOptions which is then injected into CustomModelLoader. I don't mind doing this for CustomModelLoader itself and assigning it to a property on PredictionEnginePoolOptions<,> which is then injected into PredictionEnginePool<,> as this is the documented way (per the sample applications) to do it for this class, but I feel I'm just copying this methodology to work around another design problem that could be solved without having to call GetService, and somehow let the DI framework take care of it. Is this possible, perhaps with a different code design pattern?

\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.