Configure a decorator with DI as simple as this:
Imagine you need to wrap some service with a decorator to add some functionality over it, and you need to do this by configuring your DI container. While many popular DI/IoC containers provide this functionality out of the box, this is not true for .Net Core’s DI (
IServiceCollection). Let’s consider particular example.
Here is our service:
And here is the main implementation:
Now I want to decorate this service with retry logic:
Essentially, what you need to do is to tell the DI container that when
IEmailMessageSender service is requested, create decorator
EmailMessageSenderWithRetryDecorator instance, create the original service implementation
SmtpEmailMessageSender, pass the latter to the former, and return the decorator instance. Logically this looks like this:
Basically, decorator creation involves resolving the original service implementation to pass it as a dependency (among others) to the decorator. This is beautiful: both the decorator and the “decoratee” (I’ll use this word throughout this text) creation is the job of the DI container. But there is no way to tell the container when the service interface must be resolved to the original implementation or to the decorator. Usually, the last configured mapping wins. The answer is: “hide” the decoratee from the list of service implementers and explicitly control its creation.
Let’s first look at how this could be configured and run:
AddDecorator is an extension method we will create for
IServiceCollection. Basically, it configures decorator implementation injection and its inner service implementation injection. Then, we ask the service provider for
IEmailMessageSender service in the usual way, and get the decorator instance with a decoratee instance injected to it.
Now it’s left to see how that
Essentially, we intercept decoratee services configuration and tweak the decoratee service descriptor to not announce it implements the service interface. After that the decoratee can only tell that it “implements itself”. We can create its instance via
IServiceProvider. This is an important point, because service provider not only creates instances with respect to configured lifetime scope, but also calls
Dispose when the lifetime ends (if
IDisposable is implemented, of course). Also, service provider allows other familiar dependency resolution approaches, such as providing a factory delegate or existing object.
You can also configure multiple levels of decorators like this:
Try it yourself and let me know what you think!