In software architecture, dependencies arise when one class requires collaboration from another to function. This relationship often leads to coupling. Consider a standard three-tier structure consisting of a User Interface, Business Logic, and Data Access layer. Without proper abstraction, the business logic layer directly instantiates data access objects, creating a tight coupling where changes in data retrieval logic necessitate changes in business logic.
Interface-Driven Design
To mitigate tight coupling, systems should depend on abstractions rather then concrete implementations. This is achieved by defining interfaces for data access layers. For instance, instead of relying on a specific UserDataStore class, the business logic depends on an IUserDataStore interface.
This approach offers two primary benefits: reduced coupling and clear separation of responsibilities. Development teams can work in parallel; the business logic team defines the interface requirements, while the data team implements the underlying storage mechanisms. If the storage implementation chenges—for example, moving from static data to a SQL database—only the concrete implementation class needs modification, provided it adheres to the interface contract.
Understanding IoC and DI
Inversion of Control (IoC) is a design principle where the flow of control is inverted. Instead of the caller creating dependencies, the control is handed over to a framework or container. Dependency Injection (DI) is a specific pattern used to achieve IoC.
In a DI setup, external components (dependencies) are provided to a class rather than the class creating them internally. This is typically managed by a container that holds registrations of services and their implementations. When a service is requested, the container resolves the dependencies and injects them, often via constructors.
While IoC is a broad concept, DI is the practical mechanism. Confusing the two is common, but technically, DI is a subset of IoC strategies.
AutoFac Container Overview
AutoFac is a popular Inversion of Control container for .NET. It manages the lifecycle of objects and resolves dependencies automatically. It supports various registration modes, including type mapping, instance sharing, and assembly scanning.
Console Application Implementation
The following example demonstrates setting up dependency injection in a .NET Framework console application using AutoFac. The goal is to retrieve a user profile name without manually instantiating the data access layer within the business logic.
Project Structure
The solution is divided into distinct layers:
- Model: Contains data structures.
- Repositroy: Handles data access logic.
- Service: Contains business logic.
- Infrastructure: Manages the DI container.
- ConsoleApp: The entry point.
Model Layer
Defines the data structure used across layers.
namespace Demo.DependencyInjection.Models
{
public class UserProfile
{
public long UserId { get; set; }
public string DisplayName { get; set; }
public int Score { get; set; }
}
}
Repository Layer
Defines the interface and concrete implementation for data access.
using Demo.DependencyInjection.Models;
namespace Demo.DependencyInjection.Repository.Contracts
{
public interface IUserDataStore
{
string FetchUserName(long userId);
}
}
namespace Demo.DependencyInjection.Repository.Implementation
{
public class UserDataStore : IUserDataStore
{
public string FetchUserName(long userId)
{
// Simulating data retrieval
return "Demo User";
}
}
}
Service Layer
Implements business logic depending on the repository interface via constructor injection.
using Demo.DependencyInjection.Repository.Contracts;
namespace Demo.DependencyInjection.Service.Contracts
{
public interface IAccountManager
{
string GetProfileName(long userId);
}
}
namespace Demo.DependencyInjection.Service.Implementation
{
public class AccountManager : IAccountManager
{
private readonly IUserDataStore _dataAccess;
public AccountManager(IUserDataStore dataAccess)
{
_dataAccess = dataAccess;
}
public string GetProfileName(long userId)
{
return _dataAccess.FetchUserName(userId);
}
}
}
Infrastructure Layer
Configures the AutoFac container and registers services.
using Autofac;
using Demo.DependencyInjection.Repository.Contracts;
using Demo.DependencyInjection.Repository.Implementation;
using Demo.DependencyInjection.Service.Contracts;
using Demo.DependencyInjection.Service.Implementation;
namespace Demo.DependencyInjection.Infrastructure
{
public static class DependencyRegistry
{
public static IContainer Container { get; private set; }
public static void Bootstrap()
{
var builder = new ContainerBuilder();
RegisterServices(builder);
Container = builder.Build();
}
private static void RegisterServices(ContainerBuilder builder)
{
builder.RegisterType<UserDataStore>()
.As<IUserDataStore>()
.InstancePerDependency();
builder.RegisterType<AccountManager>()
.As<IAccountManager>()
.InstancePerDependency();
}
}
}
Entry Point
The console application initializes the container and resolves services.
using System;
using Demo.DependencyInjection.Infrastructure;
using Demo.DependencyInjection.Service.Contracts;
namespace Demo.DependencyInjection.ConsoleApp
{
class Startup
{
static void Main(string[] args)
{
DependencyRegistry.Bootstrap();
DisplayUserInfo(1001);
Console.ReadKey();
}
private static void DisplayUserInfo(long id)
{
var manager = DependencyRegistry.Container.Resolve<IAccountManager>();
var name = manager.GetProfileName(id);
Console.WriteLine($"User: {name}");
}
}
}
Execution Flow
- Initialization:
Bootstrapcreates the container and registers mappings between interfaces and concrete classes. - Resolution: When
Resolve<IAccountManager>is called, AutoFac identifiesAccountManageras the implementation. - Injection: During
AccountManagerinstantiation, AutoFac detects theIUserDataStoreconstructor parameter. It recursively resolvesUserDataStoreand injects it. - Execution: The business logic executes using the injected dependency.
Practical Considerations
While console applications demonstrate the core mechanics, production environments like ASP.NET MVC or WebAPI utilize different patterns to avoid manual resolution.
Batch Registration
Manually registering every type is inefficient. In larger systems, assembly scanning is preferred. AutoFac can scan assemblies for types matching specific naming conventions (e.g., ending in "Service" or "Repository") and register them automatically against their interfaces.
Constructor Injection in Controllers
In web frameworks, manual resolution in methods is unnecessary. Frameworks integrate with DI containers to inject dependencies directly into controller constructors. This keeps controllers clean and testable.
public class UserController : Controller
{
private readonly IAccountManager _manager;
public UserController(IAccountManager manager)
{
_manager = manager;
}
public string GetInfo(long id)
{
return _manager.GetProfileName(id);
}
}
Design Complexity
Dependency Injection is a pattern, not a silver bullet. Its utility depends on the project scale. For small scripts, direct instantiation may suffice. However, for maintainable enterprise applications, especially within the .NET Core ecosystem where DI is built-in, understanding these patterns is essential for decoupling components and facilitating unit testing.