Dependency injection decouples modules by supplying their dependencies from the outside rather than having each class create its own. .NET Core ships a built-in DI container that handles service registration, lifetime management, and resolution — and provides two under-used helpers for constructing non-service objects.
The Problem: Tight Coupling
When classes directly instantiate their own dependencies, every change to a dependency propagates up the call chain:
// Tight coupling — each class directly depends on concrete types
public class ClassOne { }
public class ClassTwo
{
private readonly ClassOne _classOne;
public ClassTwo(ClassOne classOne) => _classOne = classOne;
}
public class ClassThree
{
private readonly ClassOne _classOne;
private readonly ClassTwo _classTwo;
public ClassThree(ClassOne classOne, ClassTwo classTwo)
{
_classOne = classOne;
_classTwo = classTwo;
}
}A. Creating the Service Provider
Register services in ServiceCollection (from the Microsoft.Extensions.DependencyInjection NuGet package), then build a ServiceProvider. The provider resolves all transitive dependencies automatically:
// Services registered with the DI container
public class EmpoyeesService : IEmployeesService
{
private readonly List<Employee> _employees = new List<Employee>
{
new Employee("John", EmployeeGrade.I),
new Employee("Jack", EmployeeGrade.II)
};
public List<Employee> FindEmployees(Expression<Func<Employee, bool>> where)
=> _employees.Where(where.Compile()).ToList();
}
public class CompensationService : ICompensationService
{
public double GetCompensationFor(EmployeeGrade grade) => grade switch
{
EmployeeGrade.I => 100,
EmployeeGrade.II => 1000,
EmployeeGrade.III => double.MaxValue,
_ => 0
};
}
public class PaymentsService : IPaymentsService
{
private readonly IEmployeesService _employeesSvc;
private readonly ICompensationService _compensationService;
public PaymentsService(IEmployeesService empoyeeRepository, ICompensationService compensationService)
{
_employeesSvc = empoyeeRepository;
_compensationService = compensationService;
}
public double CalculatePaymentsForEmployeesWithFirstGrade()
{
var employees = _employeesSvc.FindEmployees(x => x.Grade == EmployeeGrade.I);
var compensation = _compensationService.GetCompensationFor(EmployeeGrade.I);
return employees.Count * compensation;
}
}// Building the ServiceProvider and resolving a service
var collection = new ServiceCollection();
collection.AddScoped<IEmployeesService, EmpoyeesService>();
collection.AddScoped<ICompensationService, CompensationService>();
collection.AddScoped<IPaymentsService, PaymentsService>();
ServiceProvider serviceProvider = collection.BuildServiceProvider();
IPaymentsService paymentsSvc = serviceProvider.GetService<IPaymentsService>();
Console.WriteLine(quot;Financial effort: {paymentsSvc.CalculatePaymentsForEmployeesWithFirstGrade()}");B. ActivatorUtilities
ActivatorUtilities instantiates objects that are not registered in the container. It resolves their registered-service constructor parameters from the ServiceProvider while accepting additional runtime arguments inline. ASP.NET Core uses this internally to resolve controllers:
// ActivatorUtilities — instantiate objects NOT registered in DI
// that depend on a mix of registered services and runtime arguments
public class TheBoss
{
// Constructor only requires registered services — ActivatorUtilities.CreateInstance resolves them
public TheBoss(IPaymentsService paymentsService) { /* ... */ }
}
public class HREmployee
{
// Mix of runtime arg (string) + registered service
public HREmployee(string employeeName, IEmployeesService employeesService) { /* ... */ }
}
// Instantiate TheBoss — all constructor args come from the service provider
TheBoss boss = ActivatorUtilities.CreateInstance<TheBoss>(serviceProvider);
// Instantiate HREmployee — provide the runtime arg explicitly; service is resolved automatically
HREmployee natashaFromHr = ActivatorUtilities.CreateInstance<HREmployee>(serviceProvider, "Natasha");C. ObjectFactory
ActivatorUtilities.CreateFactory compiles a reusable delegate. If you need to create the same type many times, compile the factory once and invoke it repeatedly — avoiding repeated reflection overhead:
// ObjectFactory — compile a delegate for repeated instantiation of the same type
// Avoids repeated reflection overhead when creating many instances
// Declare which arg types will be passed at invocation time (the rest come from DI)
ObjectFactory hrFactory = ActivatorUtilities.CreateFactory(typeof(HREmployee), new[] { typeof(string) });
// Invoke the factory — pass runtime args; DI fills the rest
HREmployee alice = hrFactory.Invoke(serviceProvider, new object[] { "Alice" }) as HREmployee;
HREmployee bob = hrFactory.Invoke(serviceProvider, new object[] { "Bob" }) as HREmployee;