Reputation: 947
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:
SqliteStorageOptions
as parameter at every method in SqliteStorage
.init
-method in SqliteStorage
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.
Upvotes: 0
Views: 1017
Reputation: 172646
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.
Upvotes: 1
Reputation: 985
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.
Upvotes: 0