How to make an optional dependency in AutoFac?
Asked Answered
B

4

12

I've got an interface, implementation, and target:

public interface IPerson { public string Name { get; } }
public class Person: IPerson { public string Name { get { return "John"; } } }
public class Target { public Target(IPerson person) {} }

I'm using Autofac to tie things together:

builder.RegisterType<Person>().As<IPerson>().SingleInstance();

The problem is that IPerson lives in a shared assembly, Person lives in a plugin (which may or may not be there), and Target lives in the main application that loads plugins. If there are no plugins loaded that implement IPerson, Autofac goes ballistic about not being able to resolve Target's dependencies. And I cannot really blame it for that.

However I know that Target is able to handle the lack of an IPerson and would be more than happy to get a null instead. In fact, I'm pretty sure that all the components which rely on an IPerson are prepared to take a null it its stead. So how can I tell Autofac - "It's OK sweety, don't worry, just give me back a null, alright?"

One way I found is to add a default parameter to Target:

public class Target { public Target(IPerson person = null) {} }

That works, but then I need to do this for all the components that require an IPerson. Can I also do it the other way round? Somehow tell Autofac "If all else fails for resolving IPerson, return null"?

Bierce answered 9/2, 2015 at 17:5 Comment(3)
See also ResolveOptional().Gamma
If Person lives in plugin, how can you register it? Where does this call goes? builder.RegisterType<Person>().As<IPerson>().SingleInstance();Kannry
@SriramSakthivel - each plugin has a Initialize(ContainerBuilder) method which is called by the main application. Plugins register their components there.Bierce
K
8

You can use this syntax :

  builder.RegisterType<Target>().WithParameter(TypedParameter.From<IPerson>(null));

Unfortunately

  builder.Register(c => (IPerson)null).As<IPerson>();
  // will throw : Autofac.Core.DependencyResolutionException: A delegate registered to create instances of 'ConsoleApplication17.Program+IPerson' returned null.

and

  builder.RegisterInstance<IPerson>(null).As<IPerson>();
  // will throw : Unhandled Exception: System.ArgumentNullException: Value cannot be null.

If you don't want to add a WithParameter for each registration, you can add a module that will do it for you

public class OptionalAutowiringModule : Autofac.Module
{
    public OptionalAutowiringModule(IEnumerable<Type> optionalTypes)
    {
        this._optionalTypes = optionalTypes;
    }
    public OptionalAutowiringModule(params Type[] optionalTypes)
    {
        this._optionalTypes = optionalTypes;
    }


    private readonly IEnumerable<Type> _optionalTypes;


    protected override void AttachToComponentRegistration(IComponentRegistry componentRegistry, IComponentRegistration registration)
    {
        base.AttachToComponentRegistration(componentRegistry, registration);

        registration.Preparing += (sender, e) =>
        {
            e.Parameters = e.Parameters.Concat(new Parameter[] { new OptionalAutowiringParameter(this._optionalTypes) });
        };
    }
}
public class OptionalAutowiringParameter : Parameter
{
    public OptionalAutowiringParameter(IEnumerable<Type> optionalTypes)
    {
        this._optionalTypes = optionalTypes.ToList();
    }


    private readonly List<Type> _optionalTypes;


    public override Boolean CanSupplyValue(ParameterInfo pi, IComponentContext context, out Func<Object> valueProvider)
    {
        if (this._optionalTypes.Contains(pi.ParameterType) && !context.IsRegistered(pi.ParameterType))
        {
            valueProvider = () => null;
            return true;
        }
        else
        {
            valueProvider = null;
            return false;
        }
    }
}

Then, all you have to do is to register your module with your optional dependencies

builder.RegisterModule(new OptionalAutowiringModule(typeof(IPerson)));

But instead of injecting a null reference which may cause a nullReferenceException. An another solution would be to create NullPerson implementation.

  builder.RegisterType<NullPerson>().As<IPerson>();
  builder.RegisterType<Target>();

When you have your real implementation only registered it again, it will overwrite the original implementation.

Ketonuria answered 9/2, 2015 at 17:28 Comment(3)
Well, the first option is not much better than just specifying = null in the parameter declaration. In fact, it's even longer. A NullPerson is kind of a solution, but pretty awkward.Bierce
I updated my sample to avoid registering a parameter for each registration using a moduleKetonuria
OK, so a custom registration source. Well, I guess that's the best possible way then. Thank you! :)Bierce
S
11

Just use optional parameters, see the following sample:

public class SomeClass
{
     public SomeClass(ISomeDependency someDependency = null)
     {
           // someDependency will be null in case you've not registered that before, and will be filled whenever you register that.
     }
}
Spandex answered 5/10, 2016 at 10:12 Comment(2)
This doesn't work. (Tried with the approach like builder.Register(c => (IPerson)null).As<IPerson>(); which I need to use in my project.) -1Andrea
@AlKepp, a missing dependency is one that has simply not been registered. What you're doing is trying to force Autofac to return a null value as a dependency, which it will not do. That form of registration will always throw an exception. That isn't Yaser's fault.Saboteur
K
8

You can use this syntax :

  builder.RegisterType<Target>().WithParameter(TypedParameter.From<IPerson>(null));

Unfortunately

  builder.Register(c => (IPerson)null).As<IPerson>();
  // will throw : Autofac.Core.DependencyResolutionException: A delegate registered to create instances of 'ConsoleApplication17.Program+IPerson' returned null.

and

  builder.RegisterInstance<IPerson>(null).As<IPerson>();
  // will throw : Unhandled Exception: System.ArgumentNullException: Value cannot be null.

If you don't want to add a WithParameter for each registration, you can add a module that will do it for you

public class OptionalAutowiringModule : Autofac.Module
{
    public OptionalAutowiringModule(IEnumerable<Type> optionalTypes)
    {
        this._optionalTypes = optionalTypes;
    }
    public OptionalAutowiringModule(params Type[] optionalTypes)
    {
        this._optionalTypes = optionalTypes;
    }


    private readonly IEnumerable<Type> _optionalTypes;


    protected override void AttachToComponentRegistration(IComponentRegistry componentRegistry, IComponentRegistration registration)
    {
        base.AttachToComponentRegistration(componentRegistry, registration);

        registration.Preparing += (sender, e) =>
        {
            e.Parameters = e.Parameters.Concat(new Parameter[] { new OptionalAutowiringParameter(this._optionalTypes) });
        };
    }
}
public class OptionalAutowiringParameter : Parameter
{
    public OptionalAutowiringParameter(IEnumerable<Type> optionalTypes)
    {
        this._optionalTypes = optionalTypes.ToList();
    }


    private readonly List<Type> _optionalTypes;


    public override Boolean CanSupplyValue(ParameterInfo pi, IComponentContext context, out Func<Object> valueProvider)
    {
        if (this._optionalTypes.Contains(pi.ParameterType) && !context.IsRegistered(pi.ParameterType))
        {
            valueProvider = () => null;
            return true;
        }
        else
        {
            valueProvider = null;
            return false;
        }
    }
}

Then, all you have to do is to register your module with your optional dependencies

builder.RegisterModule(new OptionalAutowiringModule(typeof(IPerson)));

But instead of injecting a null reference which may cause a nullReferenceException. An another solution would be to create NullPerson implementation.

  builder.RegisterType<NullPerson>().As<IPerson>();
  builder.RegisterType<Target>();

When you have your real implementation only registered it again, it will overwrite the original implementation.

Ketonuria answered 9/2, 2015 at 17:28 Comment(3)
Well, the first option is not much better than just specifying = null in the parameter declaration. In fact, it's even longer. A NullPerson is kind of a solution, but pretty awkward.Bierce
I updated my sample to avoid registering a parameter for each registration using a moduleKetonuria
OK, so a custom registration source. Well, I guess that's the best possible way then. Thank you! :)Bierce
S
2

You could just take your dependency on IPerson person = null in your constructor, which is an implicit declaration of an optional IPerson dependency (c.f. @YaserMoradi). However, that puts you in the position of having to solidify this, now and forever after:

"... I'm pretty sure that all the components which rely on an IPerson are prepared to take a null it its stead."

Better that this doesn't have to be a question at all.

The "best practice" pattern (which @CyrilDurand gives as a suffix on his answer) for this is to use a default implementation (link upshot: Autofac will use the last registered component as the default provider of that service). If you have no other implementation coming from your plugin (registered after the default) this default will be used.

In your case, the default component should be some kind of no-op or base implementation of the IPerson service, where any method called will have whatever constitutes the default behavior for your application. This gives a better reuse story as well, since you can define the default behavior once and for all.

Saboteur answered 20/11, 2017 at 21:35 Comment(2)
I tried that = null approach but it doesn't work. It still throws the same exception, regardless of that = null being present or not.Andrea
If it was indeed Autofac that threw the exception at resolution time, something else is going on. But even so, the better solution is using a "special case" default implementation, as noted in the second half of the answer.Saboteur
H
0

As Marc mentioned, a default implementation should be considered in the first place. But there are situations where this might not be appropriate for some reason. If IService service = null is not enough in some situation, Adam Storr describes an alternative way of annotating the constructor parameter as being optional (using a feature introduced with Autofac 5.x).

The result could look like this:

public class Target { public Target([Optional] IPerson person) {} }

The OptionalAttribute can dynamically decide what default value is returned.

Note that this attribute is required for every affected constructor (which is actually something the original poster would like to avoid).

Hobo answered 18/3, 2022 at 7:31 Comment(2)
Interesting! How does an [Optional] IService service dependency differ from the prior "optional" dependency pattern IService service = null?Saboteur
This is indeed a good question. I guess the power of the ParameterFilterAttribute based way is that the attribute can dynamically decide what value it returns. But I agree that a separate attribute seems overkill when the attribute always returns null when TryResolve could not resolve the service.Hobo

© 2022 - 2024 — McMap. All rights reserved.