Series is introduced

Five Minutes of DotNet is a blog series that uses your fragmented time to learn and enrich your.NET knowledge. It covers all aspects of the.net architecture that might be involved, such as C# details, AspnetCore,.net knowledge in microservices, and so on.

Through this article you will Get:

  • Automatically wrap the data returned by the API into the desired format
  • understandAspNetCoreIn theActionA sequence of processes that returns results

For a demo of this article, click on Github Link

The length is about ten minutes, the content is rich, it is suggested to put a coin in the car to watch 😜

The body of the

When writing controllers using AspNet Core, we often define the return type of an Action as IActionResult, similar to the following code:

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

When we run it, calling the API through tools like POSTMan returns something like My String.

Sometimes, however, you’ll notice that I suddenly forget to declare the return type as IActionResult and instead define the Action as a normal method, something like this:

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

Run it again and return the same result.

So what return type should we use? Controllers have OK(), NotFound(), Redirect(), etc. What do these methods do? These questions will be answered in the following sections.

Define the API return format properly

Back to the subject of this article, let’s talk about data return formats. This issue may be more important to you if you are using a WebAPI. The apis we develop are often client-oriented, and the clients are usually developed by other developers using front-end frameworks (such as Vue, Angular, and React).

So there are rules that people at both ends of the development process need to follow, or the game might not work. The API’s data return format is one of them.

The default WebAPI template for AspNet Core does not have a specific return format, as these are business things that must be defined and completed by the developer.

Get a feel for a case scenario that doesn’t use a uniform format:

Xiao Ming (developer) : I developed this API and it will return the user’s name:

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

{"name":"Zhang"}
Copy the code

Xiaoding (front staff) : Oh, I see, when the return 200 is to show the name? So I’m going to serialize it into a JSON object, and I’m going to read the name property and present it to the user.

Xiaoming (developer) : Ok.

Five minutes later……

Ding: What is this thing? What happened to returning this object with a name?

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

at MiCakeDemoApplication.Controllers.DataWrapperController.NormalExceptionResult() in.......................................... (Here in the province1000Char)Copy the code

Xiaoming (developer) : this is a program internal error, you see the results are 500 ah.

Xiaoding (front end staff) : Ok, I won’t perform the operation at 500, and then remind the user “server return error” on the interface.

Another five minutes……

Xiaoding (front end staff) : So what happens now, 200 is returned, but I have no way to deal with this object, so the interface shows strange things.

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

"Operation failed. No person detected."
Copy the code

Xiao Ming (developer) : This is because the person is not detected, so I can only return this result…

Ding (front end staff) : *&&…… & # $%…… & (omits N words).

The above scenario is probably familiar to many developers, because the front end does not know how to serialize and render the interface based on the returned results without building a common return model. For convenience, the backend developer returns results in the API at will, only responsible for the business can be tuned OK, but there is no specification.

At this time, the front staff must have ten thousand grass mud horses in the pentium, silently teasing:

This old few write what crooked API oh!Copy the code

The above content is: authentic Sichuan dialect

Therefore, we need to agree on a complete model at the beginning of the API development, and later in the interaction with the front end, everyone follows this specification to avoid this kind of problem. Take this structure below:

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

{
  "statusCode": 200."isError": true."errorCode": null."message": "This man has not been found."."result": ""
}
Copy the code

When the business is successfully executed, it is returned in this format. Front-end personnel can convert the JSON. Result indicates the result of a successful service. When isError is true, an error occurs in the operation, and the error information is displayed in Message.

In this way, when everyone follows the display specification, there is no front end personnel do not know how to reverse the sequence results, resulting in various undefined or null errors. It also avoids all kinds of unnecessary communication costs.

But the back end people get annoyed at this point, and I need to return the corresponding model each time, like this:

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

So, is there a way to avoid this? Of course, the results are automatically wrapped!!

Result processing flow in AspNet Core

Before we solve this problem, we need to take a look at the process that AspNetCore goes through after the Action returns the result, so that we can get the right answer.

For general actions, such as the following Action that returns type string:

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

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

 public class ObjectResult : ActionResult, IStatusCodeActionResult
{
}
Copy the code

Return base objects such as Strings, ints, lists, custom Models, and so on, all wrapped as ObjectResult.

The following code comes from AspnetCore source code:

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

// The conversion process
private IActionResult ConvertToActionResult(IActionResultTypeMapper mapper, object returnValue, Type declaredType)
{
    // Return if it is already IActionResult, or convert if it is not.
    // In our example, we return a string, which will obviously be converted
    var result = (returnValue as IActionResult) ?? mapper.Convert(returnValue, declaredType);
    if (result == null)
    {
        throw new InvalidOperationException(Resources.FormatActionResult_ActionReturnValueCannotBeNull(declaredType));
    }

    return result;
}

// The 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();
    }

    // The string is wrapped as ObjectResult
    return new ObjectResult(value)
    {
        DeclaredType = returnType,
    };
}
Copy the code

What does the OK(xx) method look like inside AspNetCore?

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

public class OkObjectResult : ObjectResult{}Copy the code

So when OK() is used, it essentially returns ObjectResult, which is why we get the same result when we use IActionResult as the return type for an Action as when we use a generic type (such as string).

In fact, these two notations are the same in most scenarios. So we can write apis to our liking.

Of course, not all cases return ObjectResult, as in the following cases:

  • When we explicitly return oneIActionResultwhen
  • Action returns Void, Task returns no result, etc

Remember: AspnetCore action results are wrapped as IActionResult, but ObjectResult is just one implementation of IActionResult.

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

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

However, for the most part, we are the underlying object returned, and so will be wrapped as ObjectResult.

So, when the return result becomes zeroIActionResultAfter? How is it handled as an Http return?

IActionResult has a method called ExecuteResultAsync, which is used to write the object content to the HttpResponse of the HttpContext so that it can be returned to the client.

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

For each specific IActionResult type, there is an IActionResultExecutor

that implements the specific write scheme. Take the ObjectResult, whose internal Executor looks like this:

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

AspNetCore has a number of executors built in:

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

As you can see, the implementation is done by IActionResultExecutor. Here is a slightly simpler FileStreamResultExecutor, which writes the returned Stream to the HttpReponse body:

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;
        }

        awaitWriteFileAsync(context, result, range, rangeLength); }}Copy the code

So now we have a general process in our heart:

  1. Action returns result
  2. The result is wrapped asIActionResult
  3. IActionResultuseExecuteResultAsyncMethod calls belong to itIActionResultExecutor
  4. IActionResultExecutorperformExecuteAsyncMethod writes the result to the Http return result.

So we return the result from an Action to the result we see from POSTMan.

Return result wrapper

With this knowledge in hand, we can consider how to implement automatic packaging of returned results.

Combined with AspNetCore’s pipeline knowledge, we can clearly draw a process like this:

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

So to wrap the result of an Action, we have three general ideas:

  1. By middleware: After the MVC middleware is done, the Reponse results can be obtained, the content read, and then wrapped.
  2. Through Filter: After the Action is executed, data is written to the Reponse through the following Filter, so you can use custom Filter to wrap.
  3. AOP: Intercepts the Action directly and returns the wrapped result.

The three modes operate from the start, middle and end time periods respectively. There may be other manipulations, but I won’t mention them here.

So let’s analyze the advantages and disadvantages of these three methods:

  1. As middleware is processed 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 to serialize the results into. Sometimes it is unnecessary to deserialize MVC already serialized data.
  2. The Filter method can take advantage of MVC formatting, but there is a small chance that the results may be conflicted by other filters.
  3. AOP approach: While this is cleaner, proxy incurs some cost overhead, albeit small.

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

From the above we know that IActionResult has many, many implementation classes, so which results should we wrap? All? Part?

After consideration, I’m going to just wrap the ObjectResult type, because for the other types, we’d prefer it to return results directly, such as file streams, redirect results, and so on. Do you want the file stream to be wrapped as a model? 😂)

So you’ll soon have some code like this:

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);
            //_wrapperExecutor is responsible for wrapping incoming content based on incoming content
            var wrappedData = _wrapperExecutor.WrapSuccesfullysResult(objectResult.Value, wrappContext);
            // Replace ObjectResult's Value with the wrapped model classobjectResult.Value = wrappedData; }}awaitnext(); }}/ / _wrapperExecutor method
public virtual object WrapSuccesfullysResult(object orignalData, DataWrapperContext wrapperContext, bool isSoftException = false)
{
    //other code

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

Then register the Filter with MVC, and the result will be wrapped in the format we want.

Some of you might ask, how does this result get serialized into JSON or XML, but actually in the IActionResultExecutor of ObjectResult, there’s a property of type OutputFormatterSelector, This property selects the most appropriate program from the formatters MVC has registered to write the results to Reponse. MVC has a built-in formatter for string and JSON, so the default return is JSON. If you are going to use XML, you need to add a support package for XML at registration time. You can write a separate article about this implementation later if you have time.

There are always some potholes

Adding an auto-wrapped filter was really easy, and I thought so at first, especially after I wrote the first version of the implementation and returned wrapped int results through debugging. However, there are many details that can be overlooked in simple schemes:

Forever statusCode = 200

I soon found that the wrapped results all had httpcode of 200. I quickly located the code that assigned the code:

var statusCode = context.HttpContext.Response.StatusCode;
Copy the code

Reason is IAsyncResultFilter when executed, the context. The HttpContext. The Response of the content has not been written to return, so there will be a value of 200, only the real return values are on ObjectResult now. So I changed the code to:

var statusCode = objectResult.StatusCode ?? context.HttpContext.Response.StatusCode;
Copy the code

Special result ProblemDetail

ObjectResult’s Value property holds the result data returned by the Action, such as “123”,new MyObject, and so on. 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
{
    / / * * * *
}
Copy the code

This type is a canonical format, so AspNetCore introduced it. So there are lots of places where there is code to do something special with this type, such as when ObjectResult is formatted:

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 isProblemDetails details && ! details.Status.HasValue) { details.Status = StatusCode.Value; }}}Copy the code

So when wrapping, I turn on a configuration, WrapProblemDetails, to prompt the user whether to process the ProblemDetails.

The ObjectResult DeclaredType

Initially, I focused on ObjectResult’s Value property, because when I returned a result of type int, it did successfully wrap for the result I wanted. But when I return a string, it throws an exception.

Because a result of type String is ultimately passed to the StringOutputFormatter for processing, but it internally verifies that objectresult. Value is in the expected format, otherwise the conversion will fail.

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();
Copy the code

conclusion

AspNetCore Action from return to write Reponse process, on the basis of this knowledge we can easily extend a function to automatically wrap returned data.

The Github link below provides a demonstration project of data wrapping.

Github Code: Click here to jump to

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

 CustomWrapperModel result = new CustomWrapperModel("MiCakeCustomModel");

result.AddProperty("company", s => "MiCake");
result.AddProperty("statusCode", s => (s.ResultData asObjectResult)? .StatusCode ?? s.HttpContext.Response.StatusCode); result.AddProperty("result", s => (s.ResultData asObjectResult)? .Value); result.AddProperty("exceptionInfo", s => s.SoftlyException? .Message);Copy the code

You get the following data format:

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

Finally, secretly say: creation is not easy, point a recommendation…..