[5min +] beautify API and package the returned results of AspNetCore

Series introduction

[five minute dotnet] is a blog series that uses your fragmented time to learn and enrich. net knowledge. It contains all aspects that may be involved in the. net system, such as the small details of C, AspnetCore, and. net knowledge in microservices.

In this article, you will Get:

  • Automatically wrap the data returned by the API into the required format
  • Understand a series of processing procedures of Action return results in AspNetCore

For the demonstration code of this article, please click: Github Link

The duration is about 10 minutes and the content is rich. It is recommended to put in the coin first and then get in the car to watch 😜

text

When we write a controller with AspNet Core, we often define the return result type of an Action as IActionResult, similar to the following code:

[HttpGet]
public IActionResult GetSomeResult()
{
    return OK("My String");
}

When we run, when we call the API through tools such as POSTMan, we will return the result of My String.

But sometimes, you will find that suddenly I forget to declare the return type as IActionResult, instead, I define Action like a normal method, just like the following code:

[HttpGet]
public string GetSomeResult()
{
    return "My String";
}

Run again, and the result is still the same.

So what kind of return type should we use? There are OK(), NotFound(), Redirect() and other methods in the Controller. What are the functions of these methods? These questions will be answered in the following sections.

Reasonable definition of API return format

Let's go back to the topic of this article and talk about the data return format. If you're using the web API, this issue may be more important to you. Because the API we developed is often client-oriented, and the client is usually developed by another developer using the front-end framework (such as Vue, Angular, React).

Therefore, when developing, the staff at both ends should follow some rules, otherwise the game may not be able to play. API data return format is one of them.

In fact, the web API template of the default AspNet Core does not have a specific return format, because these business nature things must be defined and completed by the developers themselves.

Let's experience a case scenario that does not use a unified format:

Xiaoming (developer): I developed this API, and he will return the user's name:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Server: Kestrel

{"name":"Zhang San"}

Xiaoding (front-end staff): Oh, I see. When I return to 200, I will display my name, right? Then I serialize it into a JSON object and read the name attribute to present to the user.

Xiaoming (developer): OK.

Five minutes later......

Xiaoding (front-end staff): what is this? It's agreed to return the object with name?

HTTP/1.1 500 Internal Server Error
Content-Type: application/json; charset=utf-8
Server: Kestrel

at MiCakeDemoApplication.Controllers.DataWrapperController.NormalExceptionResult() in ........................(1000 characters in this province)

Xiaoming (developer): This is the internal error report of the program. You can see that the results are all 500.

Xiaoding (front-end staff): OK, then I will not perform the operation for 500, and then remind the user in the interface of "server returns error".

Five minutes later

Xiaoding (front-end staff): what's the situation now? The returned object is 200, but I have no way to deal with this object, causing the interface to display strange things.

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Server: Kestrel

"Operation failed, the person was not detected"

Xiaoming (developer): This is because I didn't detect this person, so I can only return this result...

Xiaoding (front-end staff): * & & &#¥%…… &(omit N words).

The above scenario may have been encountered by many developers, because a general return model was not built in the early stage, leading to the front-end staff not knowing how to serialize and present the interface according to the returned results. The back-end developers can return the results in the api at will for the convenience of the diagram. It's OK only to be responsible for the business to be able to communicate, but there is no specification.

The front end staff must have ten thousand grass mud horses in the heart, and silently make complaints about it.

What kind of API does this old man write!

The above content is: authentic Sichuan dialect

Therefore, we need to contract a complete model in the early stage of API development. In the later stage of interaction in the front end, we can avoid such problems by following this specification. For example, the following structure:

{
  "statusCode": 200,
  "isError": false,
  "errorCode": null,
  "message": "Request successful.",
  "result": "{"name":"Zhang San"}"
}

{
  "statusCode": 200,
  "isError": true,
  "errorCode": null,
  "message": "This person was not found",
  "result": ""
}

When the business is executed successfully, it will be returned in this format. The front-end personnel can convert the json, and "result" represents the result of the business success. When "isError" is true, it represents the error in the operation business. The error information will be displayed in "message".

In this way, when everyone follows the display specification, it will not cause the front-end staff not to know how to reverse the sequence results, resulting in various undefined or null errors. At the same time, it also avoids all kinds of unnecessary communication costs.

But the back-end staff are not happy at this time. I need to return to the corresponding model every time, like this:

[HttpGet]
public IActionResult GetSomeResult()
{
    return new DataModel(noError,result,noErrorCode);
}

So is there any way to avoid this? Of course, automatically package the results!!!

Result processing flow in AspNet Core

Before we can solve this problem, we need to know what process AspNetCore has gone through after the Action returns the result, so that we can apply the medicine to the case.

For general actions, such as the following action with the return type of string:

[HttpGet]
public string GetSomeResult()
{
    return "My String";
}

After the action, the return result is wrapped as ObjectResult. ObjectResult is a common return type base class for general results in AspNetCore. It inherits from the IActionResult interface:

 public class ObjectResult : ActionResult, IStatusCodeActionResult
{
}

For example, returning basic objects, such as string, int, list, custom model, etc., will be wrapped as ObjectResult.

The following code is from the AspnetCore source code:

//Get the action execution result, for example, return "My String"
var returnValue = await executor.ExecuteAsync(controller, arguments);
//Wrap the result as ActionResult
var actionResult = ConvertToActionResult(mapper, returnValue, executor.AsyncResultType);
return actionResult;

//Conversion process
private IActionResult ConvertToActionResult(IActionResultTypeMapper mapper, object returnValue, Type declaredType)
{
    //If it is already IActionResult, it will be returned; if not, it will be converted.
    //In our example, string is returned, which will be transformed obviously
    var result = (returnValue as IActionResult) ?? mapper.Convert(returnValue, declaredType);
    if (result == null)
    {
        throw new InvalidOperationException(Resources.FormatActionResult_ActionReturnValueCannotBeNull(declaredType));
    }

    return result;
}

//Actual conversion process
public IActionResult Convert(object value, Type returnType)
{
    if (returnType == null)
    {
        throw new ArgumentNullException(nameof(returnType));
    }

    if (value is IConvertToActionResult converter)
    {
        return converter.Convert();
    }

    //At this point, string is wrapped as ObjectResult
    return new ObjectResult(value)
    {
        DeclaredType = returnType,
    };
}

Let's talk about the OK(xx) method we often use when we first learn AspNetCore. What's its internal appearance?

public virtual OkResult Ok(object value)
            => new OkObjectResult(value);

public class OkObjectResult : ObjectResult
{
}

So when OK() is used, ObjectResult is returned in essence. That's why when we use IActionResult as the return type of Action and general type (such as string) as the return type, we will get the same result.

In fact, the two writing methods are the same in most scenes. So we can write API according to our hobbies.

Of course, not all cases return ObjectResult, just like the following cases:

  • When we explicitly return an IActionResult
  • When the return type of Action is Void and Task does not return a result

Remember: the action results of AspnetCore will be wrapped as IActionResult, but ObjectResult is only one of the implementations of IActionResult.

I have listed a diagram here, hoping to give you a reference:

As we can see from the figure, when processing a file, we usually return FileResult instead of ObjectResult. There are other cases where there is no return value or authentication.

However, in most cases, we are the base objects returned, so they will be wrapped as ObjectResult.

So what happens when the returned result becomes IActionResult? How to process the returned result of Http?

IActionResult has a method called ExecuteResultAsync that writes the object content to HttpResponse of HttpContext so that it can be returned to the client.

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

For each specific IActionResult type, there is an internal iactionresultexecutor < T >. The executor implements a specific writing scheme. In the case of ObjectResult, its internal Executor is as follows:

public override Task ExecuteResultAsync(ActionContext context)
{
    var executor = context.HttpContext.RequestServices.GetRequiredService<IActionResultExecutor<ObjectResult>>();
    return executor.ExecuteAsync(context, this);
}

AspNetCore has many built-in executors:

services.TryAddSingleton<IActionResultExecutor<ObjectResult>, ObjectResultExecutor>();
services.TryAddSingleton<IActionResultExecutor<PhysicalFileResult>, PhysicalFileResultExecutor>();
services.TryAddSingleton<IActionResultExecutor<VirtualFileResult>, VirtualFileResultExecutor>();
services.TryAddSingleton<IActionResultExecutor<FileStreamResult>, FileStreamResultExecutor>();
and more.....

So we can see that the specific implementation is completed by IActionResultExecutor. Let's take the above FileStream resultexecutor, which is a little simpler, to write the returned Stream to the body of HttpReponse:

public virtual async Task ExecuteAsync(ActionContext context, FileStreamResult result)
{
    if (context == null)
    {
        throw new ArgumentNullException(nameof(context));
    }

    if (result == null)
    {
        throw new ArgumentNullException(nameof(result));
    }

    using (result.FileStream)
    {
        Logger.ExecutingFileResult(result);

        long? fileLength = null;
        if (result.FileStream.CanSeek)
        {
            fileLength = result.FileStream.Length;
        }

        var (range, rangeLength, serveBody) = SetHeadersAndLog(
            context,
            result,
            fileLength,
            result.EnableRangeProcessing,
            result.LastModified,
            result.EntityTag);

        if (!serveBody)
        {
            return;
        }

        await WriteFileAsync(context, result, range, rangeLength);
    }
}

So from now on, we have a general process in mind:

  1. Action return result
  2. The result is wrapped as IActionResult
  3. IActionResult uses the ExecuteResultAsync method to call its IActionResultExecutor
  4. IActionResultExecutor executes the ExecuteAsync method to write the result to the returned result of Http.

In this way, we return the result from an Action to the result we see from POSTMan.

Return result package

With the above knowledge base, we can consider how to implement the automatic packaging of returned results.

With the knowledge of AspNetCore's pipeline, we can clearly draw such a process:

The Write Data procedure in the figure corresponds to the above IActionResult writing procedure

So to package the results of Action, we have three ideas:

  1. Through middleware: after the completion of MVC middleware, you can get the result of Reponse, read the content, and then package it.
  2. Through Filter: after the Action is executed, it will pass through the following Filter and write the data to Reponse, so you can use the custom Filter to wrap it.
  3. AOP: directly intercept the Action and return the wrapped result.

The three methods are operated from the beginning, the middle and the end. There may be other operations, but they are not mentioned here.

Let's analyze the advantages and disadvantages of these three ways:

  1. In the way of middleware, because of the processing after MVC middleware, the data obtained at this time is often the result written by MVC layer, which may be XML or JSON. So it's hard to control what format the results should be sequenced. Sometimes it is necessary to deserialize the data already serialized by MVC again, which has unnecessary overhead.
  2. The Filter method can take advantage of the formatting advantages of MVC, but there is a very small chance that the results may be conflicted by other filters.
  3. AOP method: although it is more straightforward to do so, the agent will bring some costs, although it is relatively small.

So in the end, I prefer the second way and the third way, but since AspNetCore provides us with such a good Filter, I use the advantages of Filter to complete the result packaging.

From the above we know that there are many implementation classes for IActionResult, so what results should we wrap? All? a part?

After consideration, I intend to just wrap the ObjectResult type, because for other types, we expect it to return results directly, such as file flow, redirection results, etc. Do you want the file stream to be wrapped as a model? 😂)

So you'll soon have the following code:

internal class DataWrapperFilter : IAsyncResultFilter
{
    public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
    {
        if (context.Result is ObjectResult objectResult)
        {
            var statusCode = context.HttpContext.Response.StatusCode;

            var wrappContext = new DataWrapperContext(context.Result,
                                                        context.HttpContext,
                                                        _options,
                                                        context.ActionDescriptor);
            //_The wrapperExecutor is responsible for wrapping the incoming content
            var wrappedData = _wrapperExecutor.WrapSuccesfullysResult(objectResult.Value, wrappContext);
            //Replace the Value of ObjectResult with the wrapped model class
            objectResult.Value = wrappedData;
            }
        }

        await next();
    }
}


//_The method of wrapperExecutor
public virtual object WrapSuccesfullysResult(object orignalData, DataWrapperContext wrapperContext, bool isSoftException = false)
{
    //other code

    //ApiResponse is the format type defined for us
    return new ApiResponse(ResponseMessage.Success, orignalData) { StatusCode = statuCode };
}

Then register the Filter in MVC, and the accessed result will be wrapped in the format we need.

Some students may ask how the result is serialized into json or xml. In fact, during the execution of the IActionResultExecutor of ObjectResult, there is an attribute of type OutputFormatterSelector, which selects the most appropriate one from the registered formatters of MVC to write the result to Reponse. MVC has built in string and json formatter, so the default return is json. If you want to use xml, you need to add a support package for xml at registration time. About the content of this implementation, you can write an article to talk about it separately if you have time later.

There are always some holes

It's really easy to add auto wrapped filters, which I thought at the beginning, especially when I wrote the first version of the implementation and returned wrapped int results through debugging. However, a simple solution may have many details ignored:

Forever statusCode = 200

Soon I found that the httpcode in the wrapped results was 200. I quickly navigate to the code that assigns the code to this sentence:

var statusCode = context.HttpContext.Response.StatusCode;

The reason is that when the IAsyncResultFilter is executed, the specific return content of context.HttpContext.Response has not been written, so there will only be a value of 200, and the real return value is still on the ObjectResult. So I changed the code to:

var statusCode = objectResult.StatusCode ?? context.HttpContext.Response.StatusCode;

Special result ProblemDetail

The Value property of ObjectResult holds the result data returned by Action, such as "123",new MyObject, etc. But there is a special type in AspNetCore: ProblemDetail.

/// <summary>
/// A machine-readable format for specifying errors in HTTP API responses based on https://tools.ietf.org/html/rfc7807.
/// </summary>
public class ProblemDetails
{
    //****
}

This type is a canonical format, so AspNetCore introduced this type. So in many places, there are codes for special processing of this type, such as when formatting ObjectResult:

public virtual void OnFormatting(ActionContext context)
{
    if (context == null)
    {
        throw new ArgumentNullException(nameof(context));
    }

    if (StatusCode.HasValue)
    {
        context.HttpContext.Response.StatusCode = StatusCode.Value;

        if (Value is ProblemDetails details && !details.Status.HasValue)
        {
            details.Status = StatusCode.Value;
        }
    }
}

So I opened a configuration at the time of packaging, WrapProblemDetails to prompt the user whether to process the ProblemDetails.

DeclaredType of ObjectResult

At first, I focused on the Value property of ObjectResult, because when I return a result of type int, it does successfully wrap the result I want. But when I return a type in string format, it throws an exception.

Because the result of type string will finally be handed over to StringOutputFormatter formatter for processing, but it will internally verify whether the format of ObjectResult.Value is expected, otherwise conversion error will occur.

This is because when replacing the result of ObjectResult, we should also replace its DeclaredType with the Type of the corresponding model:

objectResult.Value = wrappedData;
//This line
objectResult.DeclaredType = wrappedData.GetType();

summary

This time, we introduce the process of Action from return result to write Reponse in AspNetCore. Based on this knowledge point, we can easily expand a function of automatically packing return data.

In the following Github link, we provide you with a demonstration project of data packaging.

Github Code: click here to jump

In addition to the basic packaging function, the project also provides user-defined model functions, such as:

 CustomWrapperModel result = new CustomWrapperModel("MiCakeCustomModel");

result.AddProperty("company", s => "MiCake");
result.AddProperty("statusCode", s => (s.ResultData as ObjectResult)?.StatusCode ?? s.HttpContext.Response.StatusCode);
result.AddProperty("result", s => (s.ResultData as ObjectResult)?.Value);
result.AddProperty("exceptionInfo", s => s.SoftlyException?.Message);

You will get the following data format:

{
  "company": "MiCake",
  "statusCode": 200,
  "result": "There result will be wrapped by micake.",
  "exceptionInfo": null
}

At last, I secretly say: it's not easy to create, please give me a recommendation

Tags: JSON xml github Attribute

Posted on Sat, 16 May 2020 03:05:41 -0400 by gavinandresen