Appearance
Decorators, Interceptors, and Activators
TIP
If you need some kind of interception policy that doesn't fit into the admittedly simpler examples shown here, just ask for help in the Lamar Discord channel.
Lamar v5 finally added the full complement of interceptors that users have grown to expect from .Net IoC containers. To differentiate between the three terms, Lamar now supports:
- Decorators - the classic Gang of Four Decorator pattern where at runtime Lamar will "wrap" a concrete decorator object around the registered service
- Interceptors - in a user supplied Lambda, get a chance to work with the newly built object or even return a different object with a possibly dynamic proxy
- Activators - a user supplied Lambda that is called right after an object is created by Lamar, but before it is returned to the caller
Decorators
TIP
When you use a decorator in Lamar, it effectively replaces the original registration with a new registration for the concrete decorator type, then moves the original registration to be an inline dependency of the decorated type. Be aware that the WhatDoIHave()
display will show the concrete decorator type instead of the original registration because of this.
If you look in the Lamar codebase, you'll find dozens of tests that use a fake type named IWidget
that does something:
cs
public interface IWidget
{
void DoSomething();
}
Let's say that we have service registrations in our system for that IWidget
interface, but we want each of them to be decorated by another form of IWidget
like this to perform some kind of cross cutting concern before and/or after the call to the specific widget:
cs
public class WidgetDecorator : IWidget
{
public WidgetDecorator(IThing thing, IWidget inner)
{
Inner = inner;
}
public IWidget Inner { get; }
public void DoSomething()
{
// do something before
Inner.DoSomething();
// do something after
}
}
We can configure Lamar to add a decorator around all other IWidget
registrations with this syntax:
cs
var container = new Container(_ =>
{
// This usage adds the WidgetHolder as a decorator
// on all IWidget registrations
_.For<IWidget>().DecorateAllWith<WidgetDecorator>();
// The AWidget type will be decorated w/
// WidgetHolder when you resolve it from the container
_.For<IWidget>().Use<AWidget>();
_.For<IThing>().Use<Thing>();
});
// Just proving that it actually works;)
container.GetInstance<IWidget>()
.ShouldBeOfType<WidgetDecorator>()
.Inner.ShouldBeOfType<AWidget>();
Activators
TIP
Activators have to be synchronous at this point, so there might have to be some GetAwaiter().GetResult()
action happening to force asynchronous methods to run inline. #sadtrombone.
An activator gives you the chance to make some kind of call against an object that has just been built by Lamar, but before that object is passed to the original service requester.
As an example, let's say that you have some sort of service in your system that is responsible for polling an external web service for more information. If you're like me, you really don't like to perform any kind of work besides putting together necessary state in a constructor function, so you likely have some kind of Start()
method on the polling service to actually start things up like this class:
cs
public class Poller : IPoller
{
public void Dispose()
{
// stop polling
}
public void Start()
{
// start the actual polling
}
}
When we register the class above with Lamar, we can supply a Lambda function to start up Poller
like this:
cs
var container = new Container(x =>
{
x.For<IPoller>().Use<Poller>()
// This registers an activator on just this
// one registration
.OnCreation(poller => poller.Start());
});
In the sample above, we registered an activator on one and only one service registration. Lamar will also let you apply an activator across service registrations that implement or inherit from a common type. Back to the polling example, what if we introduce a new marker interface to denote objects that need to be started or activated like this:
cs
public interface IStartable : IDisposable
{
void Start();
}
cs
public interface IStartable
{
bool WasStarted { get; }
void Start();
}
cs
public interface IStartable
{
bool WasStarted { get; }
void Start();
}
Now, let's create a new StartablePoller
:
cs
public class StartablePoller : IPoller, IStartable
{
public void Dispose()
{
// shut things down
}
public void Start()
{
// start up the polling
}
}
And going back to the IPoller
registration, we could now do this:
cs
var container = new Container(services =>
{
// Remember that Lamar natively understands .Net
// DI registrations w/o any adapter
services.AddSingleton<IPoller, StartablePoller>();
// Other registrations that might include other
// IStartable types
services.For<IStartable>()
.OnCreationForAll(x => x.Start());
});
The call to For<T>().OnCreationForAll(Action<T>)
will apply to any registrations of type T
or any registrations where the implementation type can be cast to T
. So in th case above, the activator will be applied to the StartablePoller
type.
As a last example, you also have an overload to use the constructing container in an activator if you need access to other services. Let's say that for some kind of diagnostic in our system, we want to track what IStartable
objects have been created and floating around in our system at any time with this simple class:
cs
public class StartableTracker
{
public List<IStartable> Startables { get; } = new();
}
Now, we'd like our IStartable
objects to both Start()
and be tracked by the class above, so we'll use an activator like this:
cs
var container = new Container(services =>
{
services.AddSingleton<IPoller, StartablePoller>();
// Other registrations that might include other
// IStartable types
services.AddSingleton<StartableTracker>();
services.For<IStartable>()
.OnCreationForAll((context, startable) =>
{
var tracker = context.GetInstance<StartableTracker>();
tracker.Startables.Add(startable);
startable.Start();
});
});
Interceptors
TIP
The main usage for this feature is most likely for dynamic proxies and Aspect Oriented Programming
Interceptors are similar to activators, but with two differences:
- Interceptors allow you to return a different object than the one initially created by Lamar
- Because of the potential to return a new object, interceptors are applied at the service type level
Here's a sample from the unit tests:
cs
[Fact]
public void intercept_a_single_instance()
{
var container = new Container(x =>
{
x.For<IWidget>().Add<ActivatedWidget>()
.Named("yes")
.OnCreation(w => new DecoratedWidget(w));
x.For<IWidget>().Add<ActivatedWidget>().Named("no");
});
container.GetInstance<IWidget>("yes")
.ShouldBeOfType<DecoratedWidget>()
.Inner.ShouldBeOfType<ActivatedWidget>();
container.GetInstance<IWidget>("no")
.ShouldBeOfType<ActivatedWidget>();
}
Just like activators, there is the option to only use the original object or the ability to use the original object and the constructing container.