C# course
Lecture 16
IoC in .NET
What is Inversion of Control
Inversion of Control - is a common principle for writting low coupled code.
Imversion control could be implemented via:
- factory pattern
- service locator pattern
- depenency injection
- strategy pattern
- using events\delegates
- using interfaces
IoC vs DI vs DIP
- IoC (Inversion of Control) - most general term indicating idea of invoking client code from a framework
- DI (Dependency Injection) - set of patterns to pass dependencies to a class
- DIP - (Dependency Inversion Principle) - tells that class should depend on abstractions from the same or higher level
More info about the terms (ru)
Reasons to use IoC in your project
- reduce coupling
- remove direct dependencies between classes
- force use abstractions instead of implementations
- manage dependencies in external configuration
- minimize effort on injecting other implementation
- increaze testability of code
Constructor injection
Pass the object of the defined class into the constructor of the dependent class for its entire lifetime
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
|
class Watcher {
INofificationAction action = null;
public Watcher(INofificationAction concreteAction) {
this.action = concreteAction;
}
public void Notify(string message) {
action.ActOnNotification(message);
}
}
|
1:
2:
3:
|
var writer = new EventLogWriter();
var watcher = new Watcher(writer);
watcher.Notify("Sample message to log");
|
Method injection
To work in a method with different concrete class we have to pass the dependency in the method only
1:
2:
3:
4:
5:
6:
7:
|
class Watcher
{
public void Notify(INofificationAction concreteAction, string message)
{
action.ActOnNotification(message);
}
}
|
1:
2:
3:
|
EventLogWriter writer = new EventLogWriter();
var watcher = new Watcher();
watcher.Notify(writer, "Sample message to log");
|
Property injection
If the responsibility of selection of concrete class and invocation of method are in separate places we need property injection
1:
2:
3:
4:
5:
6:
7:
|
class Watcher {
public INofificationAction Action { get; set ; }
public void Notify(string message) {
action.ActOnNotification(message);
}
}
|
1:
2:
3:
4:
5:
6:
|
var writer = new EventLogWriter();
var watcher = new Watcher();
// This can be done in some class
watcher.Action = writer;
// This can be done in some other class
watcher.Notify("Sample message to log");
|
IoC containers
IoC containers are used to:
- automatically inject dependencies
- manage lifecycle of dependencies
- manage dependencies relationship
- split creation dependencies and configuration relationships
DI cons
- high learning curve
- constructors may look complicated
- code may seem "magic" for those who don't know DI
- overkill for small projects
- makes classes hard to use outside IoC container
Autofac - overview
- open source project
- automates constructor, method and property injection
- ligh-weight and fast enought
- has lower learning curve comparing to other containers
- could be configured either via code or xml configuration
- available for all .NET technologies (WPF, ASP MVC, Web API, WinPhone 8, Win RT etc)
- supports modules and automated type loading from an assembly
- supports interceptors
Autofac homepage
Sample container - manual resolution
1:
2:
3:
4:
5:
6:
7:
|
static void Main(string[] args)
{
var consoleOutput = new ConsoleOutput();
var writer = new TodayWriter(consoleOutput);
writer.WriteDate();
}
|
Sample container - resulution with Autofac
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
|
static void Main(string[] args)
{
// register types for DI
var builder = new ContainerBuilder();
// dependency
builder.RegisterType<ConsoleOutput>().As<IOutput>();
// class to inject dependency
builder.RegisterType<TodayWriter>().As<IDateWriter>();
var container = builder.Build();
// resolve types and use instances with injected objects
using (var scope = Container.BeginLifetimeScope())
{
var writer = scope.Resolve<IDateWriter>();
writer.WriteDate();
}
}
|
Glossary
- Container - manager of application Components
- Component - class that declares a Service and dependencies it uses
- Service - is a contract (interface) between Dependencies
- Dependency - Service required by a Component
- Registration - adding Component to Container
- Scope - is context where Instance of a component will be shared by other Components
Registering and resolving components
Register by Type
Components generated by reflection are registered by type:
1:
2:
|
builder.RegisterType<ConsoleLogger>();
builder.RegisterType(typeof(ConfigReader));
|
Autofac automatically uses the matching constructor
1:
2:
3:
4:
5:
|
public class MyComponent : IService {
public MyComponent() { /* ... */ }
public MyComponent(ILogger logger) { /* ... */ }
public MyComponent(ILogger logger, IConfigReader reader) { /* ... */ }
}
|
1:
2:
3:
4:
5:
|
builder.RegisterType<MyComponent>();
builder.RegisterType<ConsoleLogger>().As<ILogger>();
var container = builder.Build();
//...
var component = container.Resolve<MyComponent>();
|
Register by type specifying constructor
You can manually choose a particular constructor to use and override the automatic choice:
1:
2:
|
builder.RegisterType<MyComponent>()
.UsingConstructor(typeof(ILogger), typeof(IConfigReader));
|
Instance components
You can add pre-generate an instance of an object and add it to the container:
1:
2:
|
var output = new StringWriter();
builder.RegisterInstance(output).As<TextWriter>();
|
To avoid Autofac dispose the instance use:
1:
2:
3:
|
var output = new StringWriter();
builder.RegisterInstance(output).As<TextWriter>()
.ExternallyOwned();
|
Lambda expression components
Autofac can create a component using lambda expression:
1:
2:
|
builder.Register(c => new A(c.Resolve<B>()));
// parameter c is an component context of type IComponentContext
|
It is important to use component context rather than a closure to access the container
Lambda expression components - cases
Pass constant value to constructor:
1:
|
builder.Register(c => new UserSession(DateTime.Now.AddMinutes(25)));
|
Property Injection:
1:
2:
|
builder.Register(c => new A(){ MyB = c.ResolveOptional<B>() });
// ResolveOptional will try to resolve dependency but won't throw exception
|
Conditional creation:
1:
2:
3:
4:
5:
|
builder.Register<CreditCard>((c, p) => {
var accountId = p.Named<string>("accountId");
var result = accountId.StartsWith("9") ? new GoldCard(accountId) : new StandardCard(accountId);
return result;
});
|
1:
|
var card = container.Resolve<CreditCard>(new NamedParameter("accountId", "12345"));
|
Services vs. Components
When registering components, Autofac should be specified with services that component exposes
By default, registration exposes itself as the type registered:
1:
2:
|
// This exposes the service CallLogger
builder.RegisterType<CallLogger>();
|
Components can only be resolved by the services they expose
1:
2:
3:
4:
5:
|
// This will work because the component
// exposes the type by default:
scope.Resolve<CallLogger>();
// This will NOT work
scope.Resolve<ILogger>();
|
Exposing multiple services
Component can expose multiple services:
1:
2:
3:
|
builder.RegisterType<CallLogger>()
.As<ILogger>()
.As<ICallInterceptor>();
|
Component can expose even itself as a service along with others:
1:
2:
3:
4:
|
builder.RegisterType<CallLogger>()
.As<ILogger>()
.As<ICallInterceptor>()
.AsSelf();
|
Default registration
Autofac uses the last registered component as the default provider of that service:
1:
2:
3:
4:
|
builder.Register<ConsoleLogger>().As<ILogger>();
builder.Register<FileLogger>().As<ILogger>();
//...
scope.Resolve<ILogger>(); // FileLogger will be returned
|
To override this behavior, use the PreserveExistingDefaults() modifier:
1:
2:
3:
4:
|
builder.Register<ConsoleLogger>().As<ILogger>();
builder.Register<FileLogger>().As<ILogger>().PreserveExistingDefaults();
//...
scope.Resolve<ILogger>(); // ConsoleLogger will be returned
|
Scopes
The scope of a service is the area where that service can be shared with other components that consume it
Scopes in Autofac:
- are nestable and they control how components are share
- track disposable objects and dispose of them when the lifetime scope is disposed
It is important to always resolve services from a lifetime scope and not the root container
Lifetime options
Once registered, components can be configured with their lifetime
- Instance Per Dependency - create new instance on each service request
- Single Instance - aka Singleton
- Instance Per LifeTime Scope - same instance in single scope
- Instance Per Matching LifeTime Scope - singleton within the named scope
More about lifetime scopes
Modules
A module is a class that can be used to bundle up a set of related components behind a 'facade' to simplify configuration and deployment.
Modules:
- Decreased Configuration Complexity
- Configuration Parameters are Explicit
- Abstraction from the Internal Application Architecture
- Better Type Safety
- Dynamic Configuration
Module example
Create module
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
|
public class CarTransportModule : Module {
public bool ObeySpeedLimit { get; set; }
protected override void Load(ContainerBuilder builder) {
builder.Register(c => new Car(c.Resolve<IDriver>())).As<IVehicle>();
if (ObeySpeedLimit)
builder.Register(c => new SaneDriver()).As<IDriver>();
else
builder.Register(c => new CrazyDriver()).As<IDriver>();
}
}
|
Register module
1:
2:
3:
|
builder.RegisterModule(new CarTransportModule() {
ObeySpeedLimit = true
});
|
Autofac Integration
Autofac is integrated with following technologies:
- ASP.NET
- OWIN
- MVC
- Web API
- SignalR
- Web Forms
- WCF
- Managed Extensibility Framework
- NHibernate
- Moq
Full list