4

I noticed today when trying to convert an inline lambda function to a closure so I could use the same lambda in multiple places. This will compile to the correct extension method:

appBuilder.Use((ctx, next) => {
    Console.WriteLine("Test");
    return next();
});

That Use is an extension defined by:

public static IAppBuilder Use(this IAppBuilder app, Func<IOwinContext, Func<Task>, Task> handler);

Now if I do the same thing, but move the inline to a variable:

Func<IOwinContext, Func<Task>, Task> handler = (ctx, next) => {
        Console.WriteLine("Test");
        return next();
    };
appBuilder.Use(handler);

The compiler resolves to this method (not the extension):

IAppBuilder Use(object middleware, params object[] args);

What am I doing here to cause that method to change signatures?

Thanks in advance.

Rand Random
  • 7,300
  • 10
  • 40
  • 88
daniefer
  • 118
  • 1
  • 8

4 Answers4

7

What am I doing here to cause that method to change signatures?

A lambda expression doesn't have a type, and is only convertible to compatible delegate and expression-tree types.

Therefore the regular IAppBuilder method with (object, params object[]) parameters is not applicable for your call with the lambda expression argument. At that point, the compiler will look for extension methods.

Compare that with the version with the handler variable - at that point, you've got an argument which is convertible to object, and it's fine for a parameter array to have no values... so the regular method is applicable.

Importantly, if the compiler finds any applicable non-extension methods, it performs overload resolution using those. Extension methods are only used when no non-extension methods are applicable.

If you had either two extension methods or two regular methods, overload resolution would determine that the one with the more specific parameter is better for this invocation than the (object, params object[]) one... but that's not the case; the two are never compared.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
6

Lambdas are very unusual in C# in that they're one of very few types of expressions that don't actually have a type. They rely on their context in order to know what their type is. (Another example being null.)

You can take the exact same lambda, and change the code surrounding it, and it will change the type that that lambda resolves to. If you put it somewhere expecting an Expression<T>, it'll resolve to an expression, if you put it somewhere expecting a delegate, it'll end up being a delegate (with the two being radically different from each other), and the type of delegate it's bound to is based on the type of delegate expected, so the same lambda can resolve to entirely different types of delegates based on what's expected.

Almost nothing else acts like this; most expressions have exactly one type that they resolve to, and you can determine what that type is without looking at the context at all.

So in your first example, when you have:

(ctx, next) => {
    Console.WriteLine("Test");
    return next();
}

You can't determine the type alone. You need to look at where that expression is used to try to find something that that lambda can be converted to. That lambda can't be converted to an object. The compiler would have no idea if it's supposed to be an expression or a delegate, or which delegate it's supposed to be. But it can convert it to an Func<IOwinContext, Func<Task>, Task>, because it's a delegate of an appropriate type. So that's the overload used.

When you pass in handler, that's not a lambda, it's just an instance of a delegate, and that, like any other object, can be converted to object, so the other overload is valid, and being an instance method it "wins" when both are valid.

Servy
  • 202,030
  • 26
  • 332
  • 449
5

When possible, instance methods (non-extension methods) win.

With the inline version, it cannot match, because that lambda by itself has no type; it wouldn't know what delegate type to create, so it cannot cast it to object. Thus the only available match is the extension method that takes a Func<IOwinContext, Func<Task>, Task>, since the lambda can be cast to that concrete matching delegate type.

As soon as you assign the lambda so a local variable: you have given it a type - a type that is convertible to object. So now the instance method is a viable candidate, and: wins.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
0

The other answers explain why your code is behaving the way it is.

Here's a way to solve your original problem, being able to reuse the lambda: Convert it to a local function:

Task Middleware(IOwinContextcontext, Func<Task> next)
{
    Console.WriteLine("test");
    return next();
}

app.Use(Middleware);
Kirill Rakhman
  • 42,195
  • 18
  • 124
  • 148
  • The problem I was having with this approach is that when the compiler chose the `IAppBuilder Use(object middleware, params object[] args);` method, the object it is expecting should have a signature close to that of the OwinMiddleware (not too sure though). Because I was getting exceptions about unexpected type mismatch (or something like that). I resorted to creating a OwinMiddleware subclass. – daniefer Jan 30 '18 at 19:06