Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Autofac unable to resolve between two constructors when one is clearly more specific

Tags:

c#

autofac

If we register a delegate as a component, the AutowiringParam take as much priority as a NamedParameter when resolving!

Here's a boiled down example:

public class AParam { }
public class BParam : IParam { }

public interface IParam { }

public interface IAThing { }
public class AThing : IAThing
{
    public AThing(AParam aParam) { }
    public AThing(BParam anotherParam) { }
}

static void Main(string[] args)
{
    IContainer c = (new ContainerBuilder()).Build();

    var anotherBuilder = new ContainerBuilder();
    anotherBuilder.RegisterType<AThing>().As<IAThing>().InstancePerDependency();
    anotherBuilder.Register((context, parm) => new BParam()).As<BParam>().InstancePerDependency();
    anotherBuilder.Update(c);

    object aParam = new AParam();

    //Throws exception, it's unable to decide which constructor to use....
    var instance = c.Resolve(typeof(IAThing), new[] {new NamedParameter("aParam", aParam) });
}

In this case, I'm specifying that I want exactly the NamedParameter "aParam", but the AutowiritingParameter can fill in for BParam, so it doesn't know which constructor to pick (as they are both of equal parameter length).

How do I do it so that Autofac prioritizes the specific constructor with the named parameter I mentioned? There's no reason why I would want my parameter ignored, which would be the case if it uses the AutowiringParameter.

I could use "UsingConstructor", but as soon as BParam derives from AParam we get back to the same ambiguity. Here I am clearly asking for the constructor with the named parameter. Any ideas?

Edit:

Using a default parameter in your constructor can make Autofac just ignore the user-specified-named-parameter entirely, with no error and you wont notice!

public class AThing : IAThing
{
    public AThing(AParam aParam) { }
    public AThing(BParam anotherParam, bool def = true) { }
}
like image 494
GettnDer Avatar asked Sep 05 '25 16:09

GettnDer


1 Answers

I'm not sure you can really win in the current situation. What's "clearly more specific" to you isn't necessarily 100% more specific - once you've provided the parameter, Autofac takes that provided parameter along with the autowired parameters and tries to determine, given all the information available, which constructors it can fulfill. All parameters being equal - whether they came from registration time (builder.RegisterType<T>().WithParameter(...)), resolve time (scope.Resolve<T>(...)), or autowired - which constructor should it invoke?

If there was a precedence, like parameter X is more important than parameter Y, it could actually start getting pretty complicated and create some hard to troubleshoot behavior. Does a resolve-time parameter take precedence over a registration-time parameter? Can you override that behavior? Why or why not? It gets messy.

Anyway, that's why it's behaving like this and will probably continue to be that way into the future.

However, you have a couple of options:

Option 1 -UsingConstructor: You can just specify that the constructor to use in tests is the one that takes BParam if that's always how it is.

builder.RegisterType<AThing>()
  .As<IAThing>()
  .UsingConstructor(typeof(BParam));

I was going to suggest a potential ability to create a custom IConstructorSelector but you don't get the inbound parameters there, just a list of constructors that could be fulfilled based on the set of parameters already provided. Not enough info to go on. See the end of this answer for more on that.

Option 2 - Lambda registrations: This is probably what I would do if I was you. It's based on the example in the docs here. It's not as one-liner to implement but it gets you the effect you want:

// This is the "default" behavior registration for when
// no parameters are provided. Note it's named, though, so
// the actual default registration for IAThing will be the
// lambda.
builder.RegisterType<AThing>().Named<IAThing>("default-thing");

// This is what will run when you Resolve<IAThing>()
builder.Register((ctx, p) => {
  var aorb = p
    .OfType<NamedParameter>()
    .Where(n => n.Name == "aParam")
    .FirstOrDefault();
  if (aorb != null)
  {
    // You passed the parameter so use it.
    return new AThing((BParam)aorb.Value);
  }
  else
  {
    // Use the default reflection-based registration, above.
    return ctx.ResolveNamed<IAThing>("default-thing", p);
  }
}).As<IAThing>();

That isn't pretty, but it gets the job done. If you have a lot of these, you could wrap some of that up into an extension method that generates the default registration ID, handles the if/else logic, and so on.


I think it could be valuable to enable a more functional constructor selector so that UsingConstructor can do more. To that end, I opened up this issue for you to see if that would be an interesting enhancement.

like image 110
Travis Illig Avatar answered Sep 07 '25 06:09

Travis Illig