I need to be able to pass a connection string into some of my service implementations. I am doing this in the constructor. The connection string is configurable by user will be added the ClaimsPrincipal as a Claim.
All fine so far.
Unfortunately, I also want to be able to use the dependency injection features in ASP.NET Core to the fullest and resolve the service implementation though DI.
I have a POC implmentation:
public interface IRootService { INestedService NestedService { get; set; } void DoSomething(); } public class RootService : IRootService { public INestedService NestedService { get; set; } public RootService(INestedService nestedService) { NestedService = nestedService; } public void DoSomething() { // implement } } public interface INestedService { string ConnectionString { get; set; } void DoSomethingElse(); } public class NestedService : INestedService { public string ConnectionString { get; set; } public NestedService(string connectionString) { ConnectionString = connectionString; } public void DoSomethingElse() { // implement } }
These services have been registered during startup and INestedService
has been added the constructor of a controller.
public HomeController(INestedService nestedService) { NestedService = nestedService; }
As expected, I get the error Unable to resolve service for type 'System.String' while attempting to activate 'Test.Dependency.Services.NestedService'.
What are my options here?
To pass a runtime parameter not known at the start of the application, you have to use the factory pattern. You have two options here:
factory class (similar to how IHttpClientFactory
is implemented)
public class RootService : IRootService { public RootService(INestedService nested, IOtherService other) { // ... } } public class RootServiceFactory : IRootServiceFactory { // in case you need other dependencies, that can be resolved by DI private readonly IServiceProvider services; public RootServiceFactory(IServiceProvider services) { this.services = services; } public IRootService CreateInstance(string connectionString) { // instantiate service that needs runtime parameter var nestedService = new NestedService(connectionString); // note that in this example, RootService also has a dependency on // IOtherService - ActivatorUtilities.CreateInstance will automagically // resolve that dependency, and any others not explicitly provided, from // the specified IServiceProvider return ActivatorUtilities.CreateInstance<RootService>(services, new object[] { nestedService, }); } }
and inject IRootServiceFactory
instead of your IRootService
IRootService rootService = rootServiceFactory.CreateInstance(connectionString);
factory method
services.AddTransient<Func<string,INestedService>>((provider) => { return new Func<string,INestedService>( (connectionString) => new NestedService(connectionString) ); });
and inject the factory method into your service instead of INestedService
public class RootService : IRootService { public INestedService NestedService { get; set; } public RootService(Func<string,INestedService> nestedServiceFactory) { NestedService = nestedServiceFactory("ConnectionStringHere"); } public void DoSomething() { // implement } }
or resolve it per call
public class RootService : IRootService { public Func<string,INestedService> NestedServiceFactory { get; set; } public RootService(Func<string,INestedService> nestedServiceFactory) { NestedServiceFactory = nestedServiceFactory; } public void DoSomething(string connectionString) { var nestedService = nestedServiceFactory(connectionString); // implement } }
public void ConfigureServices(IServiceCollection services) { // Choose Scope, Singleton or Transient method services.AddSingleton<IRootService, RootService>(); services.AddSingleton<INestedService, NestedService>(serviceProvider=> { return new NestedService("someConnectionString"); }); }
If you decide to hide your connection string inside appSettings.json, e.g:
"Data": { "ConnectionString": "someConnectionString" }
Then provided that you've loaded your appSettings.json in the ConfigurationBuilder (usually located in the constructor of the Startup class), then your ConfigureServices would look like this:
public void ConfigureServices(IServiceCollection services) { // Choose Scope, Singleton or Transient method services.AddSingleton<IRootService, RootService>(); services.AddSingleton<INestedService, NestedService>(serviceProvider=> { var connectionString = Configuration["Data:ConnectionString"]; return new NestedService(connectionString); }); }
namespace Microsoft.Extensions.DependencyInjection { public static class RootServiceExtensions //you can pick a better name { //again pick a better name public static IServiceCollection AddRootServices(this IServiceCollection services, string connectionString) { // Choose Scope, Singleton or Transient method services.AddSingleton<IRootService, RootService>(); services.AddSingleton<INestedService, NestedService>(_ => new NestedService(connectionString)); } } }
Then your ConfigureServices method would look like this
public void ConfigureServices(IServiceCollection services) { var connectionString = Configuration["Data:ConnectionString"]; services.AddRootServices(connectionString); }
Should you need more parameters, you can go a step further and create an options class which you pass to RootService's constructor. If it becomes complex, you can use the Builder pattern.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With