Your Init method is trying to reinvent the constructor, but without the benefit of the language supporting it.
- You need to KNOW to call Init() on the database. If you forget, you will get a lot of errors without a clear indication of where they are coming from.
This is precisely what the language provides for constructors. Constructors are a bottleneck where you can force the caller to pass by.
Would you create a transient settings class which you populate in the controller and inject into the DatabaseService and Repository?
This is one of those functional but dirty implementations that is better avoided. This gets really dirty really quickly. To keep things clean, the DI container should be locked down by the time your controller logic executes, so you shouldn't try to still add something to the registration at that point.
However, you could implement a little helper that access the http context and parses out the name, and register that little helper. This is a viable approach. However, this only works on the web-project level, not below; which is why I opted for a different solution further down in the answer, one which can be applied on all levels.
Is there a way to delay/pass the database name directly to DI so it can instantiate with the correct database name?
The above point of your DI container being locked in by the time you could supply the database name remains valid here.
Your approach is partly right, in that you need to go around DI to pass this custom argument. However, the Init() method is not the way to do it, because of the issues you've encountered yourself.
- The repository called within the DatabaseService is not injectable since I need to pass it the database name. If I want to make it injectable I will again need to add an Init() or property that I have to set before using the repository, which in turn will result in the same issue as above
You're trying to fix the Init() problem by repeating the Init() problem a layer down, effectively passing the buck to the next layer. You've already shifted this once before from DatabaseService down to Repository.
There are two ways in which you tend to use a constructor:
- To inject dependencies (= predefined at the start of runtime)
- To pass specific arguments (= provided by your logic/user during the runtime)
The automation of the DI container makes it hard to pass arbitrary values at the same time. I believe this is what led you to develop Init(), to act as the second "constructor", the one which received the specifically chosen argument instead of the dependencies.
A better solution here is the factory pattern. This allows you to create a bottleneck that your code must pass by, which is effectively the solution to forcing how Init() has to be called.
With the factory pattern, you no longer inject your DatabaseService, but instead you inject a DatabaseServiceFactory, and it will generate the right DatabaseService for you.
Because you were also talking about an Init() for Repository, I infer that you're struggling with the same issue there. The same solution applies: RepositoryFactory.
The following is a small overview of the structure. I've omitted a few bells and whistles such as interface implementation for the sake of keeping the example simple and readable. Feel free to add these things back in.
The RepositoryFactory gets all the dependencies injected that the Repository should get, and stores them internally. I've used IRepositoryDependency as an example here.
Then, your code will call a specific CreateRepository(...) method on the factory, which provides the necessary argument, in this case the database name.
public class RepositoryFactory
{
private readonly IRepositoryDependency _repositoryDependency;
public RepositoryFactory(IRepositoryDependency repositoryDependency)
{
_repositoryDependency = repositoryDependency;
}
public Repository CreateRepository(string databaseName)
{
return new Repository(
_repositoryDependency,
databaseName
);
}
}
Notice how the factory pattern allows us to elegantly merge the injected repository dependencies with the custom arguments.
I leave the implementation of the Repository constructor up to you. Also, you will have to register RepositoryFactory in your DI container, and remove the Repository registration.
Now, the same pattern applies for DatabaseServiceFactory, with the added detail that it also has a dependency on the RepositoryFactory.
public class DatabaseServiceFactory
{
private readonly IDatabaseServiceDependency _databaseServiceDependency;
private readonly RepositoryFactory _repositoryFactory;
public DatabaseServiceFactory(IDatabaseServiceDependency databaseServiceDependency, RepositoryFactory repositoryFactory)
{
_databaseServiceDependency = databaseServiceDependency;
_repositoryFactory = repositoryFactory;
}
public DatabaseService CreateDatabaseService(string databaseName)
{
return new DatabaseService(
_databaseServiceDependency,
_repositoryFactory.CreateRepository(databaseName)
);
}
}
Note that I am again using IDatabaseServiceDependency as an example of any additional dependencies your database wants injected.
I again leave the implementation of the DatabaseService constructor up to you. All needed information is supplied there now. Also, you will have to register DatabaseServiceFactory in your DI container, and remove the DatabaseService registration.
And now, you can implement your controller:
public class LoadDatabaseControllerBase : Controller
{
protected DatabaseServiceFactory _databaseServiceFactory;
public LoadDatabaseControllerBase(DatabaseServiceFactory databaseServiceFactory)
{
_databaseServiceFactory = databaseServiceFactory;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
base.OnActionExecuting(context);
var databaseName = Request.Host.Host.Split('.').First();
var databaseService = _databaseServiceFactory.CreateDatabaseService(databaseName);
// Now you can use your databaseService
}
}
By hiding your DatabaseService type so that it can only be accessed through the DatabaseServiceFactory, you effectively force your code to pass by the DatabaseServiceFactory (and thus be forced to supply the database name). It cannot be forgotten, as you simply cannot meaningfully write any DatabaseService-handling logic without first getting the DatabaseService from its factory.
And even if someone ignores the factory and tries to instantiate a DatabaseService directly, its constructor forces them to supply all of the dependencies and the custom argument (such as database name), which again means that you cannot use a DatabaseService object without having supplied a database name.