简体   繁体   中英

Simple Injector constructor parameter

I'm using Simple Injector as DI Container in a project.

The problem is that I have a SqliteStorage -class, which needs the path to the db. There are multiple dbs, so I need a way to inject the path to the SqliteStorage -class at creation.

My code looks as follows (simplified without interfaces):

public class SqliteStorageOptions
{
    public string Path {get; set;}
}

public class SqliteStorage
{
    private readonly string _path;

    public SqliteStorage(SqliteStorageOptions options)
    {
        _path = options.Path;
    }
}

public class Db1
{
    private readonly SqliteStorage _sqlite;

    public Db1(SqliteStorage sqlite)
    {
        _sqlite = sqlite;
    }
}

public class Db2
{
    private readonly SqliteStorage _sqlite;

    public Db1(SqliteStorage sqlite)
    {
        _sqlite = sqlite;
    }
}


// without di
var db1 = new Db1(new SqliteStorage(new SqliteStorageOptions { Path = "db1.db" });
var db2 = new Db2(new SqliteStorage(new SqliteStorageOptions { Path = "db2.db" });

Possible Solutions:

  1. Include SqliteStorageOptions as parameter at every method in SqliteStorage .
  2. Provide a init -method in SqliteStorage
  3. Create a SqliteStorageFactory with a public SqliteStorage Create(SqliteStorageOptions options) -method.

So are there any built-in solution to my problem in simple-injector or can someone provide another (better) solution?

Thanks

Edit 1: I added some code. Db1 and Db2 both connect to sqlite-dbs (different dbs, different schema), so I wanted to extract all the sqlite-stuff to its own class SqliteStorage . So, the SqliteStorage needs to know the db path.

Which solution is best depends a bit on whether you require Auto-Wiring (automatic constructor injection) or not. Using conditional registrations (using RegisterConditional ) is a good pick, but you have be aware that it is limited to determining the injection based on only its direct parent. This means that you can't make SqliteStorageOptions conditional based on its parent parent (either Db1 or Db2 ).

If the Db1 and Db2 classes solely depend on a SqliteStorage and don't require any other dependencies, Auto-Wiring is not a real issue and your registrations can be as simple as the following:

container.Register<Db1>(
    () => new Db1(new SqliteStorage(new SqliteStorageOptions { Path = "db1.db" }));
container.Register<Db2>(
    () => new Db2(new SqliteStorage(new SqliteStorageOptions { Path = "db2.db" });

In case Auto-Wiring is required inside Db1 and Db2 , RegisterConditional gives a good alternative, because it enables Auto-Wiring:

container.Register<Db1>();
container.Register<Db2>();

container.RegisterConditional<SqliteStorage>(
    Lifestyle.CreateRegistration(
        () => new SqliteStorage(new SqliteStorageOptions { Path = "db1.db" }),
        container),
    c => c.Consumer.ImplementationType == typeof(Db1));

container.RegisterConditional<SqliteStorage>(
    Lifestyle.CreateRegistration(
        () => new SqliteStorage(new SqliteStorageOptions { Path = "db2.db" }),
        container),
    c => c.Consumer.ImplementationType == typeof(Db2)); 

In this code snippet, both Db1 and Db2 are registered 'normally', while the SqliteStorage registrations are conditionally injected based on thei consumer.

This registration is more complex, because RegisterConditonal need to be supplied with a Registration instance: there is no RegisterConditional overload that directly accepts a Func<T> factory delegate.

You can have 2 singletons one per each database connection. Let's consider an example, firstly we'll need to create an interface for your StorageService:

public interface IStorage
{
    void UsePath();
}

Now let's create couple of implementations of this storage service:

public class RedisStorage: IStorage
{
    private readonly string _path;

    public RedisStorage(string path)
    {
        _path = path;
    }

    public void UsePath()
    {
        Console.WriteLine($"Here's path: {_path}");
    }
}

public class SqlStorage: IStorage
{
    private readonly string _path;

    public SqlStorage(string path)
    {
        _path = path;
    }

    public void UsePath()
    {
        Console.WriteLine($"Here's path: {_path}");
    }
}

Enum to differentiate between implementations of IStorage:

public class StorageSource
{
    public enum StorageTypes
    {
        Redis=1,
        Sql=2
    }
}

Once we are done with that, let's create a wrapper for a storage source:

public interface IStorageWrapper
{
    void DoStuff();
}

Now comes a tricky part, instantiate a storage wrapper service decorator:

public class StorageServiceWrapper: IStorageWrapper
{
    private readonly Func<string, IStorage> _storage;

    public StorageServiceWrapper(Func<string, IStorage> storage)
    {
        _storage = storage;
    }

    public void UsePath()
    {
        _storage(StorageSource.StorageTypes.Redis.ToString()).DoStuff();
        //uncomment for sql
        //_storage(StorageSource.StorageTypes.Sql.ToString()).DoStuff();
    }
}

To achieve this, you will need to register your classes in Startup.cs as follows:

services.AddScoped<IStorageWrapper, StorageServiceWrapper>();  
  
services.AddSingleton<RedisStorage>();  
services.AddSingleton<SqlStorage>();  
  
services.AddTransient<Func<string, IStorage>>(serviceProvider => key =>  
{  
    switch (key)  
    {  
        case "Redis":  
            return serviceProvider.GetService<RedisStorage>();  
        default:  
            return serviceProvider.GetService<SqlStorage>();  
    }  
}); 

This wouldn't be as beautiful as calling _storage.DoStuff(); , but I believe would help you with the solution of your problem. If you still want to keep it handy, consider managing your settings file and injecting proper IOptions<> instance with a conn string you need and registering a factory method.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM