In the previous two posts, I looked in detail at how the RequestDelegateFactory.CreateArgument()
method creates the Expression
used to bind different types of parameters in your endpoint handler. The exact Expression
needed is different depending on the parameter Type
you're binding, and the part of the HttpRequest
you're binding it to.
In the same way, depending on the return type of your handler, you need to generate a different Expression
to handle the return type of the handler and write the HTTP response. In this post, we'll start looking at how RequestDelegateFactory.Create()
creates the final RequestDelegate
, focusing on the different expressions that are generated depending on your handler's signature.
- Generating a response with
CreateTargetableRequestDelegate
- Adding parameter validation checks to the
Expression
- Generating the response with
AddResponseWritingToMethodCall()
Generating a response with CreateTargetableRequestDelegate
We're not going to look at the whole RequestDelegateFactory.Create()
method in this post. Instead we're going to look at one of the private methods it calls: CreateTargetableRequestDelegate()
. This method takes the handler endpoint, uses the argument Expression
s created by CreateArguments()
and generates the Expression
that writes the response.
We're only going to look at the response writing part of the method in this post, so I've glossed over some of the other aspects of the method. The important points shown below are:
- Build the argument binding expressions, if not already done.
- Create the
MethodCall
Expression, which is the Expression that actually invokes the handler method using the argument expressions. - If the argument binding expressions include validation,
CreateParamCheckingResponseWritingMethodCall
is called which generates the400 Bad Request
in case of a failure. - If no validation is required,
AddResponseWritingToMethodCall
is called. - If the argument expressions use a
tempSourceString
variable, adds it to the start of the expression. - Adds the
Expression
to read from the request body as shown in the previous post and compiles theExpression
to aFunc<>
that's used to build the finalRequestDelegate
.
private static Func<object?, HttpContext, Task>? CreateTargetableRequestDelegate(
MethodInfo methodInfo, // the lambda handler function
Expression? targetExpression,
RequestDelegateFactoryContext factoryContext,
Expression<Func<HttpContext, object?>>? targetFactory = null)
{
// Build the argument expressions if they weren't previously built for some reason
factoryContext.ArgumentExpressions ??= CreateArgumentsAndInferMetadata(methodInfo, factoryContext);
// Build the Expression that actually invokes the handler, passing in the arguments
factoryContext.MethodCall = CreateMethodCall(
methodInfo, targetExpression, factoryContext.ArgumentExpressions);
var returnType = methodInfo.ReturnType;
// If any of the parameters have validation (e.g. checking for null) then
// generate the validation Expression. Generate the final "write response"
// Expression in both cases and assign to variable.
var responseWritingMethodCall = factoryContext.ParamCheckExpressions.Count > 0 ?
CreateParamCheckingResponseWritingMethodCall(returnType, factoryContext) :
AddResponseWritingToMethodCall(factoryContext.MethodCall, returnType);
// If any of the parameters use the tempSourceString variable for parsing
// then add the variable definition at the start
if (factoryContext.UsingTempSourceString)
{
responseWritingMethodCall = Expression.Block(
new[] { TempSourceStringExpr },
responseWritingMethodCall);
}
// Add the reading of the request body and compile the Expression to a Func<>
return HandleRequestBodyAndCompileRequestDelegate(responseWritingMethodCall, factoryContext);
}
Most of the method is about building the response-writing Expression
, but it also adds the tempSourceString
definition if necessary. Essentially it adds the following to the start of the Expression
:
string tempSourceString;
using the following definition:
internal static readonly ParameterExpression TempSourceStringExpr =
Expression.Variable(typeof(string), "tempSourceString");
The CreateMethodCall()
function takes the reference to the handler method, the "target" for the method call, and the argument expressions. The "target" is the object (if any) that the handler method is invoked on. The target is null
for minimal APIs that use static methods, but if you use an instance method or lambda, this contains a reference to the object:
private static Expression CreateMethodCall(
MethodInfo methodInfo, Expression? target, Expression[] arguments) =>
target is null ?
Expression.Call(methodInfo, arguments) :
Expression.Call(target, methodInfo, arguments);
Once compiled this translates to either calling handler(arg1, arg2)
or target.handler(arg1, arg2)
depending on whether target
is null.
In the next section, we'll look at the CreateParamCheckingResponseWritingMethodCall()
method which is called above when binding parameters to requests could possible fail.
Adding parameter validation checks to the Expression
It's easiest to understand the behaviour of CreateParamCheckingResponseWritingMethodCall()
by thinking about the Expression
s that CreateArgument()
adds. For example, if we take the following simple minimal API endpoint:
app.MapGet("/". (string q) => {});
Then in a previous post you saw that the start of the RequestDelegate
looks something like the following:
Task Invoke(HttpContext httpContext)
{
bool wasParamCheckFailure = false;
string q_local;
q_local = httpContext.Request.Query["q"];
if (q_local == null)
{
wasParamCheckFailure = true;
Log.RequiredParameterNotProvided(httpContext, "string", "q", "query");
}
if(wasParamCheckFailure)
{
httpContext.Response.StatusCode = 400;
return Task.CompletedTask;
}
handler.Invoke(q_local);
}
Not all of the above method is created by CreateArgument
. CreateArgument()
creates three parts of the RequestDelegate
as Expression
s, and stores each Expression
in a different collection in RequestDelegateFactoryContext factoryContext
:
- The definition of any local variables (
q_local
) is stored infactoryContext.ExtraLocals
. - The reference to the argument passed to the handler function (
q_local
) is ultimately stored infactoryContext.ArgumentTypes
. - The "parameter check expression" is stored in
factoryContext.ParamCheckExpressions
.
The "parameter check expression" in this case consists of the assignment of q_local
, the null
check, and recording the failure:
q_local = httpContext.Request.Query["q"];
if (q_local == null)
{
wasParamCheckFailure = true;
Log.RequiredParameterNotProvided(httpContext, "string", "q", "query");
}
With that in mind, let's see how CreateParamCheckingResponseWritingMethodCall()
uses the collections of Expression
to build up the RequestDelegate
. I've added comments to the method below to make it clearer.
private static Expression CreateParamCheckingResponseWritingMethodCall(
Type returnType, // the return type of our endpoint handler, which is void in this example
RequestDelegateFactoryContext factoryContext)
{
// Create an array to hold any local variables we need (e.g. q_local)
// and copy the variable Expressions. +1 variable is added for
// the extra wasParamCheckFailure variable we need
var localVariables = new ParameterExpression[factoryContext.ExtraLocals.Count + 1];
for (var i = 0; i < factoryContext.ExtraLocals.Count; i++)
{
localVariables[i] = factoryContext.ExtraLocals[i];
}
// Add the `bool wasParamCheckFailure` variable
localVariables[factoryContext.ExtraLocals.Count] = WasParamCheckFailureExpr;
// Create an array to hold the parameter check expressions
// and copy the check Expressions. +1 is added to the length
// of the array to store the block that checks if the expression was successful
var checkParamAndCallMethod = new Expression[factoryContext.ParamCheckExpressions.Count + 1];
for (var i = 0; i < factoryContext.ParamCheckExpressions.Count; i++)
{
checkParamAndCallMethod[i] = factoryContext.ParamCheckExpressions[i];
}
// If filters are registered, we need to let them run
// even when there's a binding failure. More on that in a later post
if (factoryContext.EndpointBuilder.FilterFactories.Count > 0)
{
// ... ignoring filter factories for now
}
else
{
// Builds an expression that looks a bit like the following
// if(wasParamCheckFailure)
// {
// httpContext.Response.StatusCode = 400;
// return Task.CompletedTask;
// }
// else
// return handler(q_local); // Added by AddResponseWritingToMethodCall()
var checkWasParamCheckFailure = Expression.Condition(
WasParamCheckFailureExpr,
Expression.Block(
Expression.Assign(StatusCodeExpr, Expression.Constant(400)),
CompletedTaskExpr),
AddResponseWritingToMethodCall(factoryContext.MethodCall!, returnType));
// Add the above Expression to the collection
checkParamAndCallMethod[factoryContext.ParamCheckExpressions.Count] = checkWasParamCheckFailure;
}
// Combine the variable expressions with the parameter check and response
// writing expressions, invoking each in turn, to build a single Expression
return Expression.Block(localVariables, checkParamAndCallMethod);
}
In the final Expression
generated above we call AddResponseWritingToMethodCall()
. This is also called directly in CreateTargetableRequestDelegate()
when we don't have any parameter check expressions.
Generating the response with AddResponseWritingToMethodCall()
The final method we're looking at in this post is AddResponseWritingToMethodCall()
. This method is responsible for two things:
- Invoking the handler method, as an
Expression
, created previously byCreateMethodCall
. - Handling the return value of the handler method by writing the response to the
HttpRequest
, if required.
The body of AddResponseWritingToMethodCall()
is essentially a giant if...else
statement switching on the return type of the handler method, and generating the appropriate Expression
. Rather than show the 15-ish if
clauses, I've summarised how each return type is handled by showing the approximate effective C# code generated for each case, with an example of the handler method with the return type
Return type: void
For the simplest handler methods, which don't return anything, the return type is void
:
app.MapGet("/", (string q) => {})
There's nothing to do other than call the handler here, so the invocation code looks something like this
handler.Invoke(q_local); // invoke the handler
return Task.CompletedTask;
Return type: string
The next simplest handler is a string
response:
app.MapGet("/", () => "Hello world!");
In this case the delegate sets the response Content-Type
to text/plain
and serializes the return value of the handler to the output stream
string text = handler.Invoke(); // this variable is actually inlined, but included for clarity
httpContext.Response.ContentType ??= "text/plain; charset=utf-8";
return httpContext.Response.WriteAsync(text);
Return type: IResult
string
is one of the special-cased return-types in minimal APIs, and IResult
is another. The IResult
object is responsible for writing the response to the response stream and setting any necessary Content-Type
or status codes.
The static
Results
and TypedResults
helpers are the common way to return an IResult
:
app.Map("/", () => Results.NoContent());
For the delegate, that means code that looks a little like this:
var result = handler.Invoke(); // this variable is actually inlined, but included for clarity
return ExecuteResultWriteResponse(result, httpContext); // call the helper to run ExecuteAsync
private static async Task ExecuteResultWriteResponse(
IResult? result, HttpContext httpContext)
{
if (result is null)
{
throw new InvalidOperationException("The IResult returned by the Delegate must not be null.");
}
await result.ExecuteAsync(httpContext);
}
The "interesting" bit here is that the "main" RequestDelegate
doesn't call await
directly. Instead it returns the result of the async
method ExecuteResultWriteResponse
. There's a lot of this "indirection" in the Expression
building as it makes some composability easier for the Expressions.
Another point of detail to bear in mind if you're writing custom IResult
objects is that if your handler returns a value type (struct
) IResult
, it will be boxed before it's passed to the method call. That probably removes most potential benefits of creating struct
IResult
implementations.
Return type: JSON
If you return any other (non-Task
) type, it is serialized to the response body as JSON. For example
app.Map("/", () => new { Message = "Hello world!"});
The RequestDelegate
uses the HttpResponseJsonExtensions.WriteAsJsonAsync()
method to serialize the type to the HttpResponse
. You'll see that it uses the non-generic version of the WriteAsJsonAsync()
method instead of the generic version which avoids issues with source generator polymorphism.
var result = handler.Invoke();
return WriteJsonResponse(result, httpContext); // call the helper to run ExecuteAsync
private static Task WriteJsonResponse(HttpResponse response, object? value)
{
// Call WriteAsJsonAsync() with the runtime type to serialize the runtime
// type rather than the declared type which avoids source generators issues.
return HttpResponseJsonExtensions.WriteAsJsonAsync(
response,
value,
value is null ? typeof(object) : value.GetType(),
default);
}
That covers all the "built-in" types: string
, IResult
, and "other DTO".
Return type: object
When returning object
, the delegate has to check whether the return object is one of the supported serialization types such as IResult
or a string
and handle it accordingly. If it's not either of these, it serializes the result to JSON. The result is pretty much what you would expect: it calls one of the three methods shown already
So for an API that looks like this:
app.Map("/", () => (object) "Hello world!");
you get the following response handling:
// invoke the handler and pass the result to ExecuteAwaitedReturn
return ExecuteAwaitedReturn(handler.Invoke(), httpContext);
private static Task ExecuteAwaitedReturn(object obj, HttpContext httpContext)
{
// Terminal built ins
if (obj is IResult result)
{
return ExecuteResultWriteResponse(result, httpContext);
}
else if (obj is string stringValue)
{
httpContext.Response.ContentType ??= "text/plain; charset=utf-8";
return httpContext.Response.WriteAsync(stringValue);
}
else
{
// Otherwise, we JSON serialize when we reach the terminal state
return WriteJsonResponse(httpContext.Response, obj);
}
}
That covers all of the basic types that you can return for a handler, finally we'll cover the various Task
types.
Return type: Task
or ValueTask
The simplest cases are when you return a Task
or ValueTask
. For example:
app.Map("/", () =>
{
return Task.CompletedTask;
});
This one is simple, we literally just return the handler:
return handler.Invoke();
The ValueTask
version, using ValueTask.CompletedTask
or new ValueTask()
for example, is a little more complex, as it needs to convert the ValueTask
to a Task
:
return ExecuteValueTask(handler.Invoke());
static Task ExecuteValueTask(ValueTask task)
{
// If the ValueTask has already completed (the common sync case)
// We can extract the value and return
if (task.IsCompletedSuccessfully)
{
task.GetAwaiter().GetResult();
return Task.CompletedTask;
}
// otherwise we need to await it (async case)
// Doing the await in a seperate method means we don't pay the cost
// of the async state machine for the common sync case
return ExecuteAwaited(task);
static async Task ExecuteAwaited(ValueTask task)
{
await task;
}
}
That covers the non-generic cases Task
and Task<T>
, now we'll look at the generic cases.
Return type: Task<T>
or ValueTask<T>
Returning Task<T>
or ValueTask<T>
are probably some of the most common return types for a minimal API: you receive a request, you funnel it off to do something asynchronous like read/write from a database, and then return a DTO with the results:
app.MapGet("/", async (IWidgetService service) =>
{
var widgets = await service.GetAll();
return widgets.Select(widget => new { widget.Id, widget.Name });
});
As you might expect, the handling code for Task<T>
and ValueTask<T>
calls await
on the result of the handler, then calls the same serialization code you've already seen in this post, depending on whether the returned object is a string
, an IResult
, or something else.
I'm not going to go through all of these combinations, so lets look at the example for the Task<T>
above:
var result = handler.Invoke(
httpContext.RequestServices.GetRequiredService<IWidgetService>());
// RequestDelegateFactory reads the generic type parameters
// from the Task<T> in result, and calls the generic ExecuteTaskOfT
return ExecuteTaskOfT<IEnumerable<WidgetDto>>(result, httpContext);
private static Task ExecuteTaskOfT<T>(Task<T> task, HttpContext httpContext)
{
// Don't return null as a Task, ever!
if (task is null)
{
throw new InvalidOperationException("The Task returned by the Delegate must not be null.");
}
// If the task has already completed, get the result
// and call WriteJsonResponse immediately
if (task.IsCompletedSuccessfully)
{
return WriteJsonResponse(httpContext.Response, task.GetAwaiter().GetResult());
}
// If it hasn't completed yet, await the call
return ExecuteAwaited(task, httpContext);
static async Task ExecuteAwaited(Task<T> task, HttpContext httpContext)
{
// await the task, and then await the call to WriteJsonResponse
await WriteJsonResponse(httpContext.Response, await task);
}
}
// This WriteJsonResponse method is the same one used when you
// return an object T directly
private static Task WriteJsonResponse(HttpResponse response, object? value)
{
return HttpResponseJsonExtensions.WriteAsJsonAsync(
response, value,value is null ? typeof(object) : value.GetType(), default);
}
There's more code here, but there's nothing too complicated going on:
- Check the result of the handler, has the
Task<T>
already completed?- If yes, write the result using
WriteJsonResponse()
. - If no,
await
theTask<T>
, and then write the result usingWriteJsonResponse()
.
- If yes, write the result using
The WriteJsonResponse
method is the same one called when you return a T
(no Task<>
). The await
call is wrapped in a local function just like the non-generic Task
handler to avoid allocating the async state machine when the Task
has completed synchronously (I assume).
For completeness, I'll show one more - returning a ValueTask<string>
, something like:
app.MapGet("/", () => new ValueTask("Hello world!"));
No surprises here either:
var result = handler.Invoke();
// RequestDelegateFactory reads the generic type parameters
// from the Task<T> in result, and calls ExecuteValueTaskOfString
return ExecuteValueTaskOfString(result, httpContext);
private static Task ExecuteValueTaskOfString(ValueTask<string?> task, HttpContext httpContext)
{
httpContext.Response.ContentType ??= "text/plain; charset=utf-8";
// If the ValueTask has already completed (the common sync case)
// get the result and write it directly to the response
if (task.IsCompletedSuccessfully)
{
return httpContext.Response.WriteAsync(task.GetAwaiter().GetResult()!);
}
// If it hasn't completed yet, need to await the call
return ExecuteAwaited(task, httpContext);
static async Task ExecuteAwaited(ValueTask<string> task, HttpContext httpContext)
{
// await the ValueTask, and then await writing the response
await httpContext.Response.WriteAsync(await task);
}
}
And with that, we're done looking at responses. The actual code generated here is pretty simple and intuitive, taking into account the four supported return types (void
, string,
IResult
, and T
), as well as their Task
/Task<T>
/ValueTask<T>
equivalents. Aside from that, it's mostly defensive programming and performance optimisations.
We've covered a lot in this series, so in the next post we put it all together to look at how the RequestDelegateFactory.Create()
method creates the final RequestDelegate
for an endpoint.
Summary
In this post, we looked at the CreateTargetableRequestDelegate
and focused on the code it produces that wraps the handler method invocation. We looked at two method calls in particular:
CreateParamCheckingResponseWritingMethodCall()
handles the case where there was a problem binding the request parameters, and generates the400 Bad Request
response.AddResponseWritingToMethodCall()
handles generating the final response by serializing the handler's return value to the response (and callingawait
on aTask
response, for example).
These were two of the missing pieces in generating the whole RequestDelegate
for an endpoint, so in the next post we put the whole thing together by looking at RequestDelegateFactory.Create()
.