Keywords: .NET Core | Dependency Injection | Constructor Parameters | Factory Delegates | ActivatorUtilities
Abstract: This article provides an in-depth exploration of various methods for passing parameters to constructors within the .NET Core dependency injection container. It focuses on factory delegates and the ActivatorUtilities helper class, comparing their applicability and performance characteristics. Through practical code examples, it demonstrates proper handling of service dependencies and runtime parameters, offering comprehensive solutions for parameter injection.
The Challenge of Parameter Passing in Dependency Injection
In modern application development, dependency injection has become the standard pattern for managing object dependencies. However, parameter passing becomes complex when constructors contain both service dependencies and ordinary parameters. Consider the following service definition:
public class Service : IService
{
public Service(IOtherService service1, IAnotherOne service2, string arg)
{
// Constructor implementation
}
}
In this example, the constructor requires two service dependencies and one string parameter. Traditional dependency injection registration cannot handle this scenario directly, requiring specialized techniques.
Factory Delegate Approach
The most straightforward method involves using factory delegates. In .NET Core's dependency injection container, the AddSingleton, AddTransient, and AddScoped methods all support accepting factory functions as parameters.
Basic Factory Delegate Implementation
Here is the correct implementation using factory delegates:
services.AddSingleton<IService>(x =>
new Service(x.GetRequiredService<IOtherService>(),
x.GetRequiredService<IAnotherOne>(),
"parameter value"));
The key points here are:
- The factory delegate parameter
xis anIServiceProviderinstance - Use
GetRequiredService<T>()to resolve service dependencies - Regular parameters are passed directly as concrete values
- Factory delegates are executed lazily, only when service resolution is needed
Incorrect Implementation Example
Avoid this erroneous implementation:
// Wrong example - do not call BuildServiceProvider() inside factory
services.AddSingleton<IService>(x =>
new Service(
services.BuildServiceProvider().GetService<IOtherService>(),
services.BuildServiceProvider().GetService<IAnotherOne>(),
""));
This implementation causes repeated building of the service container, potentially leading to performance issues and unexpected lifecycle behaviors.
ActivatorUtilities Helper Class
For more complex scenarios, .NET Core provides the ActivatorUtilities helper class, which intelligently mixes service resolution with direct parameter injection.
CreateInstance Method
The ActivatorUtilities.CreateInstance<T> method automatically resolves service dependencies while allowing specification of parameters for direct injection:
services.AddSingleton<IService>(x =>
ActivatorUtilities.CreateInstance<Service>(x, "parameter value"));
This method works by:
- Automatically resolving all dependencies available through the service container
- Matching provided parameter arrays to constructor parameters not resolved by services
- Performing parameter matching from left to right
Dependency Replacement Scenario
ActivatorUtilities is particularly useful for scenarios requiring temporary replacement of specific dependencies:
var specialService = ActivatorUtilities.CreateInstance<Service>(serviceProvider,
new OtherServiceB());
In this example, the first parameter of type IOtherService is replaced by the OtherServiceB instance, while other dependencies are still resolved from the service container.
Performance Optimization Solutions
For high-frequency creation scenarios, factory caching can significantly improve performance.
CreateFactory Method
The ActivatorUtilities.CreateFactory method creates reusable factory functions:
var serviceFactory = ActivatorUtilities.CreateFactory(typeof(Service),
new Type[] { typeof(string) });
// Invoke factory when needed
var serviceInstance = serviceFactory(serviceProvider, "runtime parameter");
This approach avoids reflection overhead during each instance creation, making it particularly suitable for high-request scenarios like SignalR.
Practical Application Example
Here is a complete application example demonstrating how to use these techniques in real projects:
public class DemoService
{
private readonly HelloWorldService helloWorldService;
private readonly string firstname;
private readonly string lastname;
public DemoService(HelloWorldService helloWorldService, string firstname, string lastname)
{
this.helloWorldService = helloWorldService;
this.firstname = firstname;
this.lastname = lastname;
}
public string HelloWorld()
{
return this.helloWorldService.Hello(firstname, lastname);
}
}
// Service registration
services.AddTransient<HelloWorldService>();
services.AddTransient<DemoService>(provider =>
ActivatorUtilities.CreateInstance<DemoService>(provider, "Tseng", "Stackoverflow"));
Method Selection Guide
When choosing parameter passing methods, consider the following factors:
- Simple Factory Delegates: Suitable for scenarios with few parameters and simple logic
- ActivatorUtilities.CreateInstance: Ideal for scenarios requiring mixed service resolution and direct parameter injection
- ActivatorUtilities.CreateFactory: Best for high-performance, high-frequency creation scenarios
- Options Pattern: Appropriate for configuration parameters, especially those that vary across environments
Conclusion
The .NET Core dependency injection container offers multiple flexible parameter passing mechanisms. Factory delegates provide straightforward solutions for most scenarios; the ActivatorUtilities helper class offers advanced mixed injection capabilities; and factory caching provides optimization for performance-sensitive situations. Developers should select appropriate methods based on specific requirements to ensure code maintainability and performance.