Through the minimalist simulation framework, you can understand the design and implementation of ASP.NET Core MVC framework [Part 2]: request response

<200 lines of code, 7 objects -- let you understand the essence of ASP.NET Core framework >Let a lot of readers have a real understanding of ASP.NET Core pipeline. In the past a long time, there have been many private letters to me: can you analyze the design and implementation principle of MVC framework in the same way? I hope this article can meet your needs. We are in the " [part I]: route integration >Simplify the Action method defined in the Controller type to a method that only returns Task or Void, and let the method itself complete all the request processing tasks including the corresponding request, but the real MVC framework is not so. In the real MVC framework, there is an important structure called IActionResult. As the name implies, the IActionResult object is generally the return value of the Action method, and the response Task for the request is basically implemented by this object.

Source code download:

Execution of IActionResult
Type conversion of IActionResult

1, IActionResult

The IActionResult interface, which is the result of the Action method execution and aims to make the final response to the request, also has a very simple definition. As shown in the following code snippet of main, the IActionResult object implements the response to the request. In its unique ExecuteResultAsync method, the ActionContext context for the Action to be executed is its unique input parameter.

public interface IActionResult
{
    Task ExecuteResultAsync(ActionContext context);
}

According to different request response requirements, MVC framework defines a series of IActionResult implementation types for us, and application programs can also define their own IActionResult types as required. As a demonstration, we define the ContentResult type as follows, which takes the specified string as the content of the response body, and the specific content type (media content or MIME type) can be flexibly specified.

public class ContentResult : IActionResult
{
    private readonly string _content;
    private readonly string _contentType;

    public ContentResult(string content, string contentType)
    {
        _content = content;
        _contentType = contentType;
    }

    public Task ExecuteResultAsync(ActionContext context)
    {
        var response = context.HttpContext.Response;
        response.ContentType = _contentType;
        return response.WriteAsync(_content);
    }
}

Because the Action method may not have a return value, in order to make the Action execution process (execute Action method = > convert the return value to IActionResult object = > execute IActionResult object) clear and clear, we define the following nullaactionresult type of "do nothing". It uses the static read-only property Instance to return a single nullaactionresult object.

public sealed class NullActionResult : IActionResult
{
    private NullActionResult() { }
    public static NullActionResult Instance { get; } = new NullActionResult();
    public Task ExecuteResultAsync(ActionContext context) => Task.CompletedTask;
}

2, Execute IActionResult object

Next, we will relax the constraint on the return type of Action method. In addition to Task and Void, the return types of Action method can also be IActionResult, Task < IActionResult > and valuetask < IActionResult >. Based on this new convention, we need to modify the InvokeAsync method of ControllerActionInvoker defined earlier as follows. As shown in the code snippet, after executing the target Action method, we call the ToActionResultAsync method to convert the returned object into a Task < IActionResult > object, and finally the response to the request only needs to execute the IActionResult object directly.

public class ControllerActionInvoker : IActionInvoker
{
    public ActionContext ActionContext { get; }

    public ControllerActionInvoker(ActionContext actionContext) => ActionContext = actionContext;

    public async Task InvokeAsync()
    {
        var actionDescriptor = (ControllerActionDescriptor)ActionContext.ActionDescriptor;
        var controllerType = actionDescriptor.ControllerType;
        var requestServies = ActionContext.HttpContext.RequestServices;
        var controllerInstance = ActivatorUtilities.CreateInstance(requestServies, controllerType);
        if (controllerInstance is Controller controller)
        {
            controller.ActionContext = ActionContext;
        }
        var actionMethod = actionDescriptor.Method;
        var result = actionMethod.Invoke(controllerInstance, new object[0]);
        var actionResult = await ToActionResultAsync(result);
        await actionResult.ExecuteResultAsync(ActionContext);
    }

    private async Task<IActionResult> ToActionResultAsync(object result)
    {
        if (result == null)
        {
            return NullActionResult.Instance;
        }

        if (result is Task<IActionResult> taskOfActionResult)
        {
            return await taskOfActionResult;
        }

        if (result is ValueTask<IActionResult> valueTaskOfActionResult)
        {
            return await valueTaskOfActionResult;
        }
        if (result is IActionResult actionResult)
        {
            return actionResult;
        }

        if (result is Task task)
        {
            await task;
            return NullActionResult.Instance;
        }

        throw new InvalidOperationException("Action method's return value is invalid.");
    }
}

Next, we introduce the content result defined earlier into the FoobarController of the demo instance. As shown in the following code snippet, we replace the return types of the Action methods FooAsync and Bar with task < IActionResult > and IActionResult respectively. Specifically, we return a ContentResult object. Both ContentResult objects take the same HTML fragment as the main content of the response, but the FooAsync method sets the content type to text/html, and the Bar method sets it to text/plain.

public class FoobarController : Controller
{
    private static readonly string _html =
@"<html>
<head>
    <title>Hello</title>
</head>
<body>
    <p>Hello World!</p>
</body>
</html>";

    [HttpGet("/{foo}")]
    public Task<IActionResult> FooAsync()
    {
        return Task.FromResult<IActionResult>(new ContentResult(_html, "text/html"));
    }

    public IActionResult Bar() => new ContentResult(_html, "text/plain");
}

After the demo program is started, if the two Action methods defined in the FoobarController with the same URL access as before are used, we will get the output results on the browser as shown in the figure below. Because the FooAsync method sets the content type to "text/html", the browser will parse the returned content as an HTML document, but the Bar method sets the content type to "text/plain", so the returned content will be output to the browser intact. Source code from Here Download.

3, IActionResult type conversion

The previous content has made a series of constraints on the return type of the Task method, but we know that in the real MVC framework, the Action method defined in the Controller can take any type. To solve this problem, we can consider converting the data object returned by the Action method to an IActionResult object. We define the type conversion rule as a service represented by the IActionResultTypeMapper interface. The type conversion for IActionResult is embodied in the Convert method. It is worth mentioning that the value parameter of the object to be converted represented by the Convert method is not necessarily the return value of the Action method, but the specific data object. If the return value of the Action method is a Task < tresult > or valuetask < tresult > object, the parameter returned by their Result property is the data object to be converted.

public interface IActionResultTypeMapper
{
    IActionResult Convert(object value, Type returnType);
}

For simplicity, we define the ActionResultTypeMapper type as the default implementation of the simulation framework for the IActionResultTypeMapper interface. As shown in the code snippet, the Convert method will return ContentResult objects of content type "text/plain", and the original object string description (return value of ToString method) will be the content of the response subject.

public class ActionResultTypeMapper : IActionResultTypeMapper
{
    public IActionResult Convert(object value, Type returnType) => new ContentResult(value.ToString(), "text/plain");
}

After we remove the limitation on the return type of Action method, our ControllerActionInvoker naturally needs to be further modified. The Action method may return a task < TResult > or a valuetask < TResult > object (the generic parameter TResult can be any type), so we define the following two static methods in the ControllerActionInvoker type (convertfromtaskasync < tvalue > and convertfromvaluetaskasync < tvalue >) to convert them into task < IActionResult > objects, if the returned object is not an IACT Ionresult object, the IActionResultTypeMapper object as a parameter, will be type converted in the future. We define two static read-only fields ('taskConvertMethod' and 'valueTaskConvertMethod') to hold MethodInfo objects that describe these two generic methods.

public class ControllerActionInvoker : IActionInvoker
{
    private static readonly MethodInfo _taskConvertMethod;
    private static readonly MethodInfo _valueTaskConvertMethod;

    static ControllerActionInvoker()
    {
        var bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static;
        _taskConvertMethod = typeof(ControllerActionInvoker).GetMethod(nameof(ConvertFromTaskAsync), bindingFlags);
        _valueTaskConvertMethod = typeof(ControllerActionInvoker).GetMethod(nameof(ConvertFromValueTaskAsync), bindingFlags);
    }

    private static async Task<IActionResult> ConvertFromTaskAsync<TValue>(Task<TValue> returnValue, IActionResultTypeMapper mapper)
    {
        var result = await returnValue;
        return result is IActionResult actionResult
            ? actionResult
            : mapper.Convert(result, typeof(TValue));
    }

    private static async Task<IActionResult> ConvertFromValueTaskAsync<TValue>( ValueTask<TValue> returnValue, IActionResultTypeMapper mapper)
    {
        var result = await returnValue;
        return result is IActionResult actionResult
            ? actionResult
            : mapper.Convert(result, typeof(TValue));
    }
    ...
}

The following is the execution of InvokeAsync method for Action. After executing the target Action method and getting the original return value, we call the ToActionResultAsync method to convert the return value into task < IActionResult >, and finally complete all the request processing tasks by executing the IActionResult object. If the return type is task < tresult > or valuetask < tresult >, we will directly call the convertfromtaskasync < tvalue > or convertfromvaluetaskasync < tvalue > method by reflection (the better way is to execute the type conversion method by expression tree to get better performance).

public class ControllerActionInvoker : IActionInvoker
{
    public async Task InvokeAsync()
    {
        var actionDescriptor = (ControllerActionDescriptor)ActionContext.ActionDescriptor;
        var controllerType = actionDescriptor.ControllerType;
        var requestServies = ActionContext.HttpContext.RequestServices;
        var controllerInstance = ActivatorUtilities.CreateInstance(requestServies, controllerType);
        if (controllerInstance is Controller controller)
        {
            controller.ActionContext = ActionContext;
        }
        var actionMethod = actionDescriptor.Method;
        var returnValue = actionMethod.Invoke(controllerInstance, new object[0]);
        var mapper = requestServies.GetRequiredService<IActionResultTypeMapper>();
        var actionResult = await ToActionResultAsync(
            returnValue, actionMethod.ReturnType, mapper);
        await actionResult.ExecuteResultAsync(ActionContext);
    }

    private Task<IActionResult> ToActionResultAsync(object returnValue, Type returnType, IActionResultTypeMapper mapper)
    {
        //Null
        if (returnValue == null || returnType == typeof(Task) || returnType == typeof(ValueTask))
        {
            return Task.FromResult<IActionResult>(NullActionResult.Instance);
        }

        //IActionResult
        if (returnValue is IActionResult actionResult)
        {
            return Task.FromResult(actionResult);
        }

        //Task<TResult>
        if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>))
        {
            var declaredType = returnType.GenericTypeArguments.Single();
            var taskOfResult = _taskConvertMethod.MakeGenericMethod(declaredType).Invoke(null, new object[] { returnValue, mapper });
            return (Task<IActionResult>)taskOfResult;
        }

        //ValueTask<TResult>
        if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(ValueTask<>))
        {
            var declaredType = returnType.GenericTypeArguments.Single();
            var valueTaskOfResult = _valueTaskConvertMethod.MakeGenericMethod(declaredType).Invoke(null, new object[] { returnValue, mapper });
            return (Task<IActionResult>)valueTaskOfResult;
        }

        return Task.FromResult(mapper.Convert(returnValue, returnType));
    }
}

From the above code snippet, we can see that the iaconresulttypemapper object used in the type conversion process for iaconresult is extracted from the dependency injection container for the current request, so we need to make targeted service annotation before the application starts. We will add the service registration for IActionResultTypeMapper to the AddMvcControllers extension method defined earlier.

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddMvcControllers(this IServiceCollection services)
    {
        return services
            .AddSingleton<IActionDescriptorCollectionProvider, DefaultActionDescriptorCollectionProvider>()
            .AddSingleton<IActionInvokerFactory, ActionInvokerFactory>()
            .AddSingleton <IActionDescriptorProvider, ControllerActionDescriptorProvider>()
            .AddSingleton<ControllerActionEndpointDataSource, ControllerActionEndpointDataSource>()
            .AddSingleton<IActionResultTypeMapper, ActionResultTypeMapper>();
    }
}

In order to verify the simulation framework's support for any return type of Action method, we made the following modifications to the FoobarController defined in the previous example. As shown in the code snippet, we define four Action methods in the type of FoobarController, and the return types are task < ContentResult >, valuetask < ContentResult >, task < string >, valuetask < string >, the content of ContentResult object and the character string directly returned are the same HTML.

public class FoobarController : Controller
{
    private static readonly string _html =
@"<html>
<head>
    <title>Hello</title>
</head>
<body>
    <p>Hello World!</p>
</body>
</html>";

    [HttpGet("/foo")]
    public Task<ContentResult> FooAsync()
    => Task.FromResult(new ContentResult(_html, "text/html"));

    [HttpGet("/bar")]
    public ValueTask<ContentResult> BarAsync()
    => new ValueTask<ContentResult>(new ContentResult(_html, "text/html"));

    [HttpGet("/baz")]
    public Task<string> BazAsync() => Task.FromResult(_html);

    [HttpGet("/qux")]
    public ValueTask<string> QuxAsync() => new ValueTask<string>(_html);
}

In the above four Action methods, we set the route template to "/ foo", "bar", "Baz" and "/ Qux" respectively by labeling the HttpGetAttribute attribute attribute, so we can use the corresponding URL to access the four Action methods. The following figure shows the rendering of the response content of this Action on the browser. Since the Action methods Baz and Qux return a string, according to the conversion rules provided by the ActionResultTypeMapper type, the final returned ContentResult object will take this string as the response content and the content type is "text/plain". Source code from Here Download.

Let you understand the design and implementation of ASP.NET Core MVC framework through minimalist simulation framework [Part 1]: route integration
Through minimalist simulation framework, you can understand the design and implementation of ASP.NET Core MVC framework [2]: request response
Through the minimalist simulation framework, you can understand the design and implementation of ASP.NET Core MVC framework [2]: parameter binding

Source: https://www.cnblogs.com/artech/p/inside-asp-net-core-mvc-02.html

Tags: Attribute Fragment

Posted on Thu, 16 Apr 2020 03:57:40 -0700 by pentinat