Throughout this series, we've been looking at the various pieces that go into turning a minimal API endpoint handler into a RequestDelegate
that ASP.NET Core can invoke. We looked at the various classes involved, extracting metadata from a minimal API handler, and how binding works in minimal APIs. Next we looked at how RequestDelegateFactory
builds up the Expression
parts that are combined into a RequestDelegate
, both for the argument expressions (parts 1 and 2) and for the response.
In this post, we put all these components together, to see how the RequestDelegate
is built. In previous posts I've glossed over and simplified some of the complexities, but in this post I try to stick as close to the reality as possible. We're going to look at the RequestDelegateFactory.Create()
method, see the order the various Expression
s that go into a RequestDelegate
are built, and see how they're combined. I avoid covering too many of the functions that I've covered in previous posts, so this post is best read after reading the previous posts.
In this post I'm still going to skip over filters and filter factories. We'll come back to these in the next post, to see how they modify things compared to the "simple" case shown in this post.
- Building a
RequestDelegate
:RequestDelegateFactory.Create()
- Creating the
targetExpression
- Creating the
RequestDelegateFactoryContext
andtargetFactory
- Entering
CreateTargetableRequestDelegate
and creating the argument expressions - Creating the method call
Expression
- Building up the request delegate in
CreateParamCheckingResponseWritingMethodCall
- Compiling the
Expression
to aFunc<object?, HttpCotnext, Task>
- Building the final RequestDelegate
Building a RequestDelegate
: RequestDelegateFactory.Create()
For the purposes of this post, I'm going to consider a simple minimal API handler, that takes a single parameter and returns a string:
app.MapGet("/{name}", (string name) => $"Hello {name}!");
The focus of this post is the RequestDelegateFactory.Create()
method. There are several overloads, but they ultimately call into an overload that takes:
Delegate handler
—the minimal API endpoint handler(string name) => $"Hello {name}"
RequestDelegateFactoryOptions? options
—holds various context values and settings about the endpoint, as discussed previously.RequestDelegateMetadataResult? metadataResult
—holds the result of callingRequestDelegateFactory.InferMetadata()
, including the inferred metadata, and the cachedRequestDelegateFactoryContext
.
The latter two parameters are optional and mostly for optimisation reasons; if they're not provided, Create()
re-builds them. For the purposes of this post, I'll assume these values aren't provided, as it makes following the sequence in Create()
a little easier.
In practice, in minimal APIs, these values are always provided, as they're created in
RequestDelegateFactory.InferMetadata()
, which is called beforeCreate()
.
Throughout this post I'll track the content of important variables as we proceed through the Create()
method, so we can see how everything combines.
Creating the targetExpression
Let's start looking at the RequestDelegateFactory.Create()
function, taking things one statement at a time.
public static RequestDelegateResult Create(
Delegate handler,
RequestDelegateFactoryOptions? options = null,
RequestDelegateMetadataResult? metadataResult = null)
{
if (handler is null)
{
throw new ArgumentNullException(nameof(handler));
}
UnaryExpression targetExpression = handler.Target switch
{
object => Expression.Convert(TargetExpr, handler.Target.GetType()),
null => null,
};
// ...
}
The function starts by checking handler
for null
and then creates an Expression
called targetExpression
which has one of two values:
- If
handler.Target
isnull
, then null - Otherwise, an expression that converts a "
target
parameter" to the required type
If you were using a static handler method, handler.Target
would be null
, otherwise, it returns the instance of the type that the handler was invoked on. In our case, as we're using a lambda method, the instance is the closure class (nested inside the Program
class) in which we defined the lambda. This closure class typically has an "invalid" C# name, such as <>c
, so we'll use that from now on.
You may find it surprising that the lambda method is an "instance" instead of static type. The reason is that the compiler creates a closure class to capture the context of the lambda (even if you define the lambda using the
static
keyword). You can see this in action using sharplab.io's "C#" option to see the effective lowered result of theFunc<>
.
At this point we have two variables of interest, handler
and targetExpression
:
Lets move on to the next few lines of Create()
.
Creating the RequestDelegateFactoryContext
and targetFactory
The next couple of lines are relatively simple:
// ...
RequestDelegateFactoryContext factoryContext =
CreateFactoryContext(options, metadataResult, handler);
Expression<Func<HttpContext, object?>> targetFactory = (httpContext) => handler.Target;
// ...
The first line creates a RequestDelegateFactoryContext
type. This type tracks all the important state about creating the RequestDelegate
, and is mostly a bag of properties. You can see the full class definition here, but it holds things such as:
AllowEmptyRequestBody
—are empty request bodies allowed, or should the handler throw.UsingTempSourceString
—does the argument parsing code use atempSourceString
variable.ExtraLocals
—expressions defining extra variables that are needed during argument binding.ParamCheckExpressions
—expressions defining the "argument validation" that occurs after binding.
There are many more properties on the context, some of which are copied straight from the options
object. We already looked at the CreateFactoryContext()
method in a previous post, so we'll leave it at that for now, but we're going to keep track of some of the important properties of RequestDelegateFactoryContext
in subsequent steps.
The next line of Create()
creates an Expression
defining a factory-function that creates the necessary method target instance given an ambient HttpContext
variable: httpContext => handler.Target
. This could also have been defined as _ => handler.Target
, as the parameter is unused; as far as I can tell, the HttpContext
parameter is there for easier composability later (when you're using filters). I'll add the extra variables to our tracking table:
We now come to the CreateTargetableRequestDelegate()
function we looked into in the previous post.
Entering CreateTargetableRequestDelegate
and creating the argument expressions
The next line of RequestDelegateFactory.Create()
is:
// ...
Func<object?, HttpContext, Task>? targetableRequestDelegate =
CreateTargetableRequestDelegate(
handler.Method, targetExpression, factoryContext, targetFactory);
// ...
We looked into CreateTargetableRequestDelegate
in the previous post, where we looked at how the response Expression
is created. In this post we'll walk through this method to see how and where the argument and response expressions are built up. Each Expression
is stored on the factoryContext
variable, so we'll continue to track them in our table of variables.
As you can see from the method call above, we pass pretty much the entire current context into the method:
private static Func<object?, HttpContext, Task>? CreateTargetableRequestDelegate(
MethodInfo methodInfo,
Expression? targetExpression,
RequestDelegateFactoryContext factoryContext,
Expression<Func<HttpContext, object?>>? targetFactory = null)
{
factoryContext.ArgumentExpressions ??=
CreateArgumentsAndInferMetadata(methodInfo, factoryContext);
// ...
}
The first line of this method is CreateArgumentsAndInferMetadata()
. We've already looked extensively at this method and the CreateArgument()
method it relies on to build the argument binding expressions, so for this post we're going to just consider the output Expression
s of this method, of which there are several!
For our example handler method
(string name) => $"Hello {name}!"`
CreateArgumentsAndInferMetadata()
creates three main expressions:
- The
Expression
that is passed to the handler argument—an expression consisting of the variable:name_local
. This is added tofactoryContext.ArgumentExpressions
. - The
Expression
defining extra local variables that are required for the binding—In this case, it's similarly thename_local
variable. This is added tofactoryContext.ExtraLocals
. - The parameter check expression to check that the
string
argument has bound correctly—This is added tofactoryContext.ParamCheckExpressions
. In a previous post you saw that it generates an expression similar to the following:
name_local = httpContext.Request.RouteValues["name"]
if (name_local == null)
{
wasParamCheckFailure = true;
Log.RequiredParameterNotProvided(httpContext, "string", "name", "route");
}
The
Log
function call refers to a "real" method in theRequestDelegateFactory
, which is invoked in theExpression
usingExpression.Call()
.
CreateArgumentsAndInferMetadata()
sets various other helper values and metadata, but these are the main values created in the simple string
-binding-to-routevalue case. For other binding types, you would set additional values on factoryContext
, as described in my previous posts. For example:
factoryContext.UsingTempSourceString
—set totrue
when parsing aTryParse()
type, such as binding anint
parameter.factoryContext.ParameterBinders
—expressions for bindingBindAsync()
parameters.factoryContext.JsonRequestBodyParameter
—Reference to theParameterInfo
(if any) that requires binding to the request body as JSON.factoryContext.FirstFormRequestBodyParameter
—Reference to the firstParameterInfo
(if any) that requires binding to the request body as a form. Note that multiple form request body parameters result in an error.
Putting that all together, after the call to CreateArgumentsAndInferMetadata()
, we have the following variables and cached values:
Lets keep working our way through CreateTargetableRequestDelegate()
.
Creating the method call Expression
The next step in CreateTargetableRequestDelegate
is CreateMethodCall()
, which creates the Expression
for actually invoking the minimal API handler:
private static Func<object?, HttpContext, Task>? CreateTargetableRequestDelegate(
MethodInfo methodInfo,
Expression? targetExpression,
RequestDelegateFactoryContext factoryContext,
Expression<Func<HttpContext, object?>>? targetFactory = null)
{
// ...
factoryContext.MethodCall =
CreateMethodCall(methodInfo, targetExpression, factoryContext.ArgumentExpressions);
// ...
}
CreateMethodCall()
is a small method that takes the handler
method, the targetExpression
, and the argument expressions, and combines them into a single expression:
private static Expression CreateMethodCall(
MethodInfo methodInfo, Expression? target, Expression[] arguments) =>
target is null ?
Expression.Call(methodInfo, arguments) :
Expression.Call(target, methodInfo, arguments);
The Expression
returned is stored in factoryContext.MethodCall
. If you evaluate this, you'll find the resulting code looks a bit like this; it's simply invoking the minimal API handler and passing in the name_local
argument:
((Program.<>c) target).Handler.Invoke(name_local)
This isn't exactly what it looks like, because of all the delegate caching and lowering of lambdas, but it looks sort of like this. Fundamentally, it's invoking your minimal API handler, and passing in the argument expressions.
Alternatively, if you think of your handler lambda as being a Func<string, string>
defined on Program
:
public class Program
{
Func<string, string> handler = (string name) => $"Hello {name}!";
}
Then factoryContext.MethodCall
looks simply like:
target.handler(name_local)
For simplicity, I'll use this notation for the rest of the post. Lets add that to our variables. We aren't going to use the ArgumentExpressions
again, so I've removed them from the table:
The next part of CreateTargetableRequestDelegate
deals with filters, so we're going to skip over that bit for now. and move onto CreateParamCheckingResponseWritingMethodCall
Building up the request delegate in CreateParamCheckingResponseWritingMethodCall
In the next section of CreateTargetableRequestDelegate()
, things really start to come together quickly:
private static Func<object?, HttpContext, Task>? CreateTargetableRequestDelegate(
MethodInfo methodInfo,
Expression? targetExpression,
RequestDelegateFactoryContext factoryContext,
Expression<Func<HttpContext, object?>>? targetFactory = null)
{
// ...
Type returnType = methodInfo.ReturnType;
// ...Filter magic
Expression responseWritingMethodCall = factoryContext.ParamCheckExpressions.Count > 0
? CreateParamCheckingResponseWritingMethodCall(returnType, factoryContext)
: AddResponseWritingToMethodCall(factoryContext.MethodCall, returnType);
// ...
}
In this section we call one of two methods, depending on whether there are any ParamCheckExpressions
:
CreateParamCheckingResponseWritingMethodCall
—called to add anyParamCheckExpression
s to theRequestDelegate
, then callsAddResponseWritingToMethodCall
AddResponseWritingToMethodCall
—Handles serialization of the handler return value, and writing the response.
I covered both of those methods in detail in the previous post, so you can refer to that for details of how the expressions are built. For our example, we do have a ParamCheckExpression
, so we invoke CreateParamCheckingResponseWritingMethodCall
and store the result in the responseWritingMethodCall
variable. For our example, this looks something like:
string name_local; // factoryContext.ExtraLocals
bool wasParamCheckFailure;
name_local = httpContext.Request.RouteValues["name"] //
if (name_local == null) //
{ //
wasParamCheckFailure = true; // factoryContext.ParamCheckExpressions
Log.RequiredParameterNotProvided( //
httpContext, "string", "name", "route"); //
} //
if(wasParamCheckFailure)
{
httpContext.Response.StatusCode = 400;
return Task.CompletedTask;
}
ExecuteWriteStringResponseAsync(
httpContext,
target.handler(name_local)); // factoryContext.MethodCall
The above Expression
is composed both of the Expression
values we created previously, and some new code added by CreateParamCheckingResponseWritingMethodCall
.
The final call in the Expression
invokes the handler, and passes the result to the function ExecuteWriteStringResponseAsync
, along with the httpContext
parameter (which isn't defined yet). ExecuteWriteStringResponseAsync
is a private function in RequestDelegateFactory
, shown below:
private static Task ExecuteWriteStringResponseAsync(
HttpContext httpContext, string text)
{
SetPlaintextContentType(httpContext);
return httpContext.Response.WriteAsync(text);
}
private static void SetPlaintextContentType(HttpContext httpContext)
=> httpContext.Response.ContentType ??= "text/plain; charset=utf-8";
Now we're really getting somewhere. The expression above is stored in responseWritingMethodCall
, so we can stop worrying about tracking the other variables in our table; this is the only Expression
that matters now. The end is in sight!
Compiling the Expression
to a Func<object?, HttpCotnext, Task>
The final lines in CreateTargetableRequestDelegate
are as follows:
private static Func<object?, HttpContext, Task>? CreateTargetableRequestDelegate(
MethodInfo methodInfo,
Expression? targetExpression,
RequestDelegateFactoryContext factoryContext,
Expression<Func<HttpContext, object?>>? targetFactory = null)
{
// ...
if (factoryContext.UsingTempSourceString)
{
responseWritingMethodCall = Expression.Block(
new[] { TempSourceStringExpr }, responseWritingMethodCall);
}
return HandleRequestBodyAndCompileRequestDelegate(
responseWritingMethodCall, factoryContext);
}
The if
conditionally adds string tempSourceString
to the top of the RequestDelegate
if it's required for parsing purposes. It's not needed in our case, but if we had an int
parameter, for example, we would need to add it.
The final method call is HandleRequestBodyAndCompileRequestDelegate()
. The name hints at this method's responsibilities; it's responsible for three things:
- Adding the necessary
Expression
for reading from the request body/form if it's required. - Adding the necessary
Expression
for anyBindAsync
arguments. - Compiling the
Expression
into aFunc
.
Our example API isn't reading from the request body (it's a GET request), so I'll gloss over that part.
See a previous post for more details on the
Expression
generated when binding to the request body.
Our example API also doesn't use any BindAsync
parameters, but for completeness I've included the code generation in the HandleRequestBodyAndCompileRequestDelegate
shown below:
private static Func<object?, HttpContext, Task> HandleRequestBodyAndCompileRequestDelegate(
Expression responseWritingMethodCall,
RequestDelegateFactoryContext factoryContext)
{
// Do we need to bind to the body/form?
if (factoryContext.JsonRequestBodyParameter is null && !factoryContext.ReadForm)
{
// No - Do we have any BindAsync() parameters?
if (factoryContext.ParameterBinders.Count > 0)
{
// Yes, so we need to generate the code for reading from the custom binders calling into the delegate
var continuation = Expression.Lambda<Func<object?, HttpContext, object?[], Task>>(
responseWritingMethodCall, TargetExpr, HttpContextExpr, BoundValuesArrayExpr).Compile();
var binders = factoryContext.ParameterBinders.ToArray();
var count = binders.Length;
return async (target, httpContext) =>
{
var boundValues = new object?[count];
for (var i = 0; i < count; i++)
{
boundValues[i] = await binders[i](httpContext);
}
await continuation(target, httpContext, boundValues);
};
}
// No - we don't have any BindAsync methods
return Expression.Lambda<Func<object?, HttpContext, Task>>(
responseWritingMethodCall, TargetExpr, HttpContextExpr).Compile();
}
// Bind to the form/body
return factoryContext.ReadForm
? HandleRequestBodyAndCompileRequestDelegateForForm(responseWritingMethodCall, factoryContext)
: HandleRequestBodyAndCompileRequestDelegateForJson(responseWritingMethodCall, factoryContext);
}
If you follow the branches through you'll see that for our example, this method doesn't add anything to the existing Expression. This method simply compiles the Expression
we have into a Func<object?, HttpContext, Task>
that looks something like the following:
Task TargetableRequestDelegate(object? target, HttpContext httpContext)
{
string name_local;
bool wasParamCheckFailure;
name_local = httpContext.Request.RouteValues["name"]
if (name_local == null)
{
wasParamCheckFailure = true;
Log.RequiredParameterNotProvided(httpContext, "string", "name", "route");
}
if(wasParamCheckFailure)
{
httpContext.Response.StatusCode = 400;
return Task.CompletedTask;
}
return ExecuteWriteStringResponseAsync(
httpContext,
handler(name_local));
}
Note that this is now a Func<>
, which can be actually invoked, but it's not a RequestDelegate
, as we have the extra target
parameter in the method signature. To create the final RequestDelegate
we need to follow the stack all the way back up to the Create()
method where we left it to call CreateTargetableRequestDelegate()
.
Building the final RequestDelegate
We're almost at the finish line now. The remaining parts of Create()
mostly deal with tidying up and handling edge cases we're not focusing on. The important line is where we convert the Func<>
stored in targetableRequestDelegate
into a real RequestDelegate:
public static RequestDelegateResult Create(
Delegate handler,
RequestDelegateFactoryOptions? options = null,
RequestDelegateMetadataResult? metadataResult = null)
{
RequestDelegate finalRequestDelegate = targetableRequestDelegate switch
{
// if targetableRequestDelegate is null, we're in an edge case that we're not focusing on now!
null => (RequestDelegate)handler,
// Otherwise, build the RequestDelegate
_ => httpContext => targetableRequestDelegate(handler.Target, httpContext),
};
// save the finalRequestDelegate in the RequestDelegateResult
return CreateRequestDelegateResult(finalRequestDelegate, factoryContext.EndpointBuilder);
}
So for our example, the finalRequestDelegate
, which is a RequestDelegate
instance (Func<HttpContext, Task>
), looks something like this:
(HttpContext httpContext) => TargetableRequestDelegate(Program.<>c, httpContext)
Where
Program.<>c
is the cached lambda instanceTargetableRequestDelegate
is the compiledExpression
we saw in the previous section (aFunc<HttpContext, object?, Task>
).
This is the RequestDelegate
that is invoked when your minimal API endpoint handles a request.
That (finally) brings us to the end of this post on how the RequestDelegate
is built up. It's a convoluted and complex process, but the end result is a simple Func<HttpContext, Task>
that can be executed in response to a request, which is pretty elegant in the end!
Of course, we're not done yet. I keep promising we're going to look at filters, so in the next post we will. Hopefully with all this background, understanding how they fit in and their impact on the RequestDelegate
will be a bit easier!
Summary
In this post we walked through the RequestDelegateFactory.Create()
method, to understand how all the Expression
s we've looked at in this series are combined into the final RequestDelegate
.
We started by looking at the targetExpression
and targetFactory
, and established that even static lambda methods have a "target" instance. Next we moved onto CreateArgumentsAndInferMetadata()
and the CreateArgument()
helper in which we define the bulk of the various Expression
components. All the Expression
s related to binding the arguments to the HttpContext
are defined here.
After defining the argument Expression
s we could create the expression for invoking the lambda handler. This was a little hard to show, as it's an Expression
invoking the cached lambda instance by passing in the argument Expression
s.
In the next method, CreateParamCheckingResponseWritingMethodCall
, we combine all the Expression
s together: the local variable definitions, the argument binding, validation, invocation of the handler, and generating of the response. Things really come together at this point, but we're still working with an Expression
.
In HandleRequestBodyAndCompileRequestDelegate
we finally call Compile()
and turn the Expression
into a Func<object, HttpContext, Task>
. This isn't quite a RequestDelegate
, but RequestDelegateFactory.Create()
"wraps" this to create the required signature by passing in the target instance, giving the final, required, Func<HttpContext, Task>
signature of a RequestDelegate
.