1
votes

I have 3 classes:

  1. class SqlQueryService : IQueryService
  2. class FileQueryService : IQueryService
  3. class NCRFileQueryService : FileQueryService

I have create an interface factory:

public interface IQueryServiceFactory
{
    IQueryService Create(string connection);
}

In the application module:

Bind(typeof(IQueryService)).To(typeof(SqlQueryService)).Named("Data Source");         
Bind(typeof(IQueryService)).To(typeof(NCRFileQueryService)).Named("NCR File Source");
Bind(typeof(IQueryService)).To(typeof(FileQueryService)).Named("File Source");

Bind<IQueryServiceFactory>().ToFactory();

In my app I want to create an instance of one of three classes based on a parameter like the following:

IQueryService queryService =
    _queryServiceFactory.Create(_configuration.SelectedTPV.Connection);
  • If the string parameter starts with "Data Source" create a SqlQueryService
  • If the string parameter starts with "File Source" create a FileQueryService
  • If the string parameter starts with "NCR File Source" create a NCRFileQueryService

It's possible to do that?

*Note: My app is a winforms app with .NET Framework 3.5 because is for and OLD windows

The Ninject version that I use is 3.2.2.0 and the version of the Ninject Extensions Factory is 3.2.1.0

3
Is your configuration value _configuration.SelectedTPV.Connection a fixed value that doesn't change at runtime, or does that value actually change while the application is running? In either case however, you don't need the factory. Removing IQueryServiceFactory will improve your design. - Steven
The configuration value is setup on a website and can be more than one. Then the user download the app login with his credentials and then the app will connect to an api to get the configuration values. Then the user can choose from a dropdown. If I remove IQueryServiceFactory how can I create the concrete class based on configuration value? - jordi

3 Answers

2
votes

You can do this by creating a custom instance provider and then binding your factory like:

this.Bind<IQueryServiceFactory>()
    .ToFactory(() => new UseFirstArgumentAsNameInstanceProvider());

See Treating the first factory method parameter as a name specifier in the documentation.

2
votes

Since the value you supply to the factory is not a user supplied value, it is not needed to complicate the consuming code with:

  • That configuration value
  • The factory abstraction

Instead of depending on IQueryServiceFactory, the consumer should simply depend on IQueryService. How to supply the consumer with the correct implementation, depends on your application's needs, but there are two options.

Option 1: The configuration value is known at start-up time (before the DI Container is configured)

When the configuration value is known at start-up, before the DI Container is configured, this simply means you only have to register one implementation in the container based on that value.

For instance:

Bind(typeof(IQueryService),
    value == "Data Source" ? typeof(SqlQueryService) :
    value == "NCR File Source ? typeof(NCRFileQueryService) :
    value == "File Source" ? typeof(FileQueryService) :
    throw new InvalidOperationException(value));

Option 2: The configuration value is known after, or can change during the lifetime of the application

Even in case the configuration value isn't fixed or known at startup, there is still no reason to use a factory abstraction and let the consumer depend on that configuration value. This can all be hidden behind the IQueryService abstraction by creating a Proxy:

public class ConfigurationSelectorQueryServiceProxy : IQueryService
{
    private readonly IQueryService a;
    private readonly IQueryService b;
    private readonly IQueryService c;
    public ConfigurationSelectorQueryServiceProxy(
        SqlQueryService a, NCRFileQueryService b, FileQueryService c) {
        this.a = a;
        this.b = b;
        this.c = c;
    }

    // IQueryService methods. Forward the call to one of the wrapped services
    public object SomeMethod(object args) => GetService().SomeMethod(args);

    // Helper methods
    private IQueryService GetService() =>
        // Read configuration value
        GetService(_configuration.SelectedTPV.Connection);

    private IQueryService GetService(string value) =>
        value == "Data Source" ? (this.a :
        value == "NCR File Source ? this.b :
        value == "File Source" ? this.c :
        throw new InvalidOperationException(value);        
}

This ConfigurationSelectorQueryServiceProxy Proxy implementation can be registered as IQueryService and injected into consumers. This way consumers don't have to know about the complexity of selecting the right implementation. They can simply use the IQueryService abstraction.

1
votes

thanks Owen!

I read the wiki but I didn't realize of that customisation.

Finally I resolved adding a class like:

public class QueryServiceInstanceProvider : StandardInstanceProvider
{
    protected override string GetName(System.Reflection.MethodInfo methodInfo, object[] arguments)
    {
        string connection = arguments[0].ToString();

        return connection.Split('=')[0];
    }

    protected override Ninject.Parameters.IConstructorArgument[] GetConstructorArguments(System.Reflection.MethodInfo methodInfo, object[] arguments)
    {
        return base.GetConstructorArguments(methodInfo, arguments).Skip(1).ToArray();
    }
}

and in the application module:

Bind(typeof(IQueryService)).To(typeof(SqlQueryService)).Named("Data Source");         
Bind(typeof(IQueryService)).To(typeof(NCRFileQueryService)).Named("NCR File Source");
Bind(typeof(IQueryService)).To(typeof(FileQueryService)).Named("File Source");

Bind<IQueryServiceFactory>().ToFactory(() => new QueryServiceInstanceProvider());