The best way to get value from Json request on ASP.NET Core, like axios

Zack Yang
7 min readMar 4, 2021

--

This article introduces a way to bind JSON request data submitted by Axios or other front-ends in ASP.NET Core to the plain parameters of the Action method, and explains how it works.

1. Reading data from a JSON request is cumbersome

In ASP.NET Core MVC/ ASP.NET Core WebAPI(abbreviated as ASP.NET Core), you can use [FromQuery] to get parameter values from QueryString and [FromForm] to get parameter values from Form (x-www-form-urlencoded) requests.

With the popularity of separating front-end and back-end, more and more front-end request bodies use JSON format. For example, the very popular Axios front-end library post requests in JSON format by default, and the WeChat MiniPrograme requests are int JSON forma by default too. In ASP.NET Core, you can use [FromBody] to bind the Action parameters to the request data. For example, if the the HTTP request is as follows:

{“UserName”:”test”,”Password”:”123”}

You must declare a User class containing UserName and Password, and then declare the Action parameters as follows:

public IActionResult Login([FromBody]User u);

As a result, almost every Action method must declare a class of complex type corresponding to the request. If there are many Actions in the project, there will be too many “parameter classes for actions” .NET Core cannot bind a JSON property to an Action parameter of a simple type like [FromQuery].

So I developed the YouZack.FromJsonBody open source library that allows us to bind simple type parameters in the following way:

Test([FromJsonBody] int i2,

[FromJsonBody(“author.age”)]int aAge,

[FromJsonBody(“author.father.name”)] string dadName)

The Action parameters above can retrieve data directly from the following JSON request

{“i1”:1,”i2":5,”author”:{“name”:”yzk”,”age”:18,”father”:{“name”:”laoyang”,”age”:28}}}

2. Introduction to YouZack.FromJsonBody

The library is developed using.NET Standard, so it supports the.NET Framework and.NET Core, both ASP.NET Core MVC and ASP.NET Core Web API.

GitHub repository:https://github.com/yangzhongke/YouZack.FromJsonBody

Step one:

Install the package in the ASP.NET Core project via NuGet:

Install-Package YouZack.FromJsonBody

Step two:

Add “using YouZack.FromJsonBody” in Startup.cs.

Insert the following code before “UseEndpoints()” in Configure() method:

app.UseFromJsonBody();

Step three:

Add the [FromJsonBody] attribute to the Controller’s Action parameter, which by default gets the value from the JSON request’s property of the same name. If the “propertyName” parameter of FromJsonBody is not null, the value is bound from the propertyName property of the JSON request. PropertyName values also support multilevel property bindings such as [FromJsonBody (“Author.Father. Name”)].

Example one, as for the following json request:

{“phoneNumber”:”119110",”age”:3,”salary”:333.3,”gender”:true,”dir”:”west”,”name”:”zack yang”}

The Javascript code for this request is as follows:

axios.post(‘@Url.Action(“Test”,”Home”)’,

{ phoneNumber: “119110”, age: 3, salary: 333.3, gender: true,dir:”west”,name:”zack yang” })

.then(function (response)

{

alert(response.data);

})

.catch(function (error)

{

alert(‘Send failed’);

});

The following code is the corresponding Action of Controller:

public IActionResult Test([FromJsonBody]string phoneNumber, [FromJsonBody]string test1,

[FromJsonBody][Range(0,100,ErrorMessage =”Age must be between 0 and 100")]int? age,

[FromJsonBody] bool gender,

[FromJsonBody] double salary,[FromJsonBody]DirectionTypes dir,

[FromJsonBody][Required]string name)

{

if(ModelState.IsValid==false)

{

var errors = ModelState.SelectMany(e => e.Value.Errors).Select(e=>e.ErrorMessage);

return Json(“Invalid input!”+string.Join(“\r\n”,errors));

}

return Json($”phoneNumber={phoneNumber},test1={test1},age={age},gender={gender},salary={salary},dir={dir}”);

}

Example two, as for the following json request:

{“i1”:1,”i2":5,”author”:{“name”:”yzk”,”age”:18,”father”:{“name”:”laoyang”,”age”:28}}}

The Javascript code for this request is as follows:

axios.post(‘/api/API’,

{ i1: 1, i2: 5, author: { name: ‘yzk’, age: 18, father: {name:’laoyang’,age:28}} })

.then(function (response)

{

alert(response.data);

})

.catch(function (error)

{

alert(‘Send failed’);

});

The following code is the corresponding Action of Controller:

public async Task<int> Post([FromJsonBody(“i1”)] int i3, [FromJsonBody] int i2,

[FromJsonBody(“author.age”)]int aAge,[FromJsonBody(“author.father.name”)] string dadName)

{

Debug.WriteLine(aAge);

Debug.WriteLine(dadName);

return i3 + i2+aAge;

}

3. How YouZack.FromJsonBody works?

See the following GitHub address for the full code of the project:

https://github.com/yangzhongke/YouZack.FromJsonBody

“FromJsonBodyAttribute” is a custom Attribute for data binding. The main source code is as follows

public class FromJsonBodyAttribute : ModelBinderAttribute

{

public string PropertyName { get; private set; }

public FromJsonBodyAttribute(string propertyName=null) : base(typeof(FromJsonBodyBinder))

{

this.PropertyName = propertyName;

}

}

All data binding attributes should inherit from the ModelBinderAttribute class, and the FromJsonBodyBinder class is called to perform the calculation whenever an attempt is made to evaluate the binding value of a parameter with the ModelBinderAttribute.

Because FromJsonBodyBinder needs to fetch data from the JSON request body, to improve performance, we wrote a custom middleware, named FromJsonBodyMiddleware, to parse the JSON request body string to the parsed JSONDocument object, and then use the parsed JSONDocument object for subsequent FromJsonBodyBinder. The UseFromJsonBody() method we call in Startup applies the FromJsonBodyMiddleware middleware. The source code for the UseFromJsonBody() method is as follows:

public static IApplicationBuilder UseFromJsonBody(this IApplicationBuilder appBuilder)

{

return appBuilder.UseMiddleware<FromJsonBodyMiddleware>();

}

The following code is the main code of the FromJsonBodyMiddleware (see GitHub for the full code):

public sealed class FromJsonBodyMiddleware

{

public const string RequestJsonObject_Key = “RequestJsonObject”;

private readonly RequestDelegate _next;

public FromJsonBodyMiddleware(RequestDelegate next)

{

_next = next;

}

public async Task Invoke(HttpContext context)

{

string method = context.Request.Method;

if (!Helper.ContentTypeIsJson(context, out string charSet)

||”GET”.Equals(method, StringComparison.OrdinalIgnoreCase))

{

await _next(context);

return;

}

Encoding encoding;

if(string.IsNullOrWhiteSpace(charSet))

{

encoding = Encoding.UTF8;

}

else

{

encoding = Encoding.GetEncoding(charSet);

}

context.Request.EnableBuffering();

int contentLen = 255;

if (context.Request.ContentLength != null)

{

contentLen = (int)context.Request.ContentLength;

}

Stream body = context.Request.Body;

string bodyText;

if(contentLen<=0)

{

bodyText = “”;

}

else

{

using (StreamReader reader = new StreamReader(body, encoding, true, contentLen, true))

{

bodyText = await reader.ReadToEndAsync();

}

}

if(string.IsNullOrWhiteSpace(bodyText))

{

await _next(context);

return;

}

if(!(bodyText.StartsWith(“{“)&& bodyText.EndsWith(“}”)))

{

await _next(context);

return;

}

try

{

using (JsonDocument document = JsonDocument.Parse(bodyText))

{

body.Position = 0;

JsonElement jsonRoot = document.RootElement;

context.Items[RequestJsonObject_Key] = jsonRoot;

await _next(context);

}

}

catch(JsonException ex)

{

await _next(context);

return;

}

}

}

The Invoke() method will be called every time an HTTP request arrives at the server. Because GET requests generally do not come with the request body, GET requests are not handled here. If the ContentType of the request is not application/json, the request will not be handled too. This avoids the performance impact of irrelevant requests being processed.

To reduce memory usage, by default, ASP.NET Core can only read the request body once, not repeatedly. However, FromJsonBodyMiddleware needs to read the JSON, and parses the body of the request, but other parts of ASP.NET Core may also need to read the body of the request, so we allow multiple reads of the body of the request with request.EnableBuffering(), which increases the memory usage slightly. In general, however, the request body of a JSON request is not too large, so this is not a serious problem.

Then, we use the new JSON library System.Text.Json to parse JSON requests:

JsonDocument document = JsonDocument.Parse(bodyText)

The parsed JSON object is placed in Context.Items for FromJsonBodyBinder to use:

context.Items[RequestJsonObject_Key] = jsonRoot;

The core code of FromJsonBodyBinder is as follows:

public class FromJsonBodyBinder : IModelBinder

{

public static readonly IDictionary<string, FromJsonBodyAttribute> fromJsonBodyAttrCache = new ConcurrentDictionary<string, FromJsonBodyAttribute>();

public Task BindModelAsync(ModelBindingContext bindingContext)

{

var key = FromJsonBodyMiddleware.RequestJsonObject_Key;

object itemValue = bindingContext.ActionContext.HttpContext.Items[key];

JsonElement jsonObj = (JsonElement)itemValue;

string fieldName = bindingContext.FieldName;

FromJsonBodyAttribute fromJsonBodyAttr = GetFromJsonBodyAttr(bindingContext, fieldName);

if (!string.IsNullOrWhiteSpace(fromJsonBodyAttr.PropertyName))

{

fieldName = fromJsonBodyAttr.PropertyName;

}

object jsonValue;

if (ParseJsonValue(jsonObj, fieldName, out jsonValue))

{

object targetValue = jsonValue.ChangeType(bindingContext.ModelType);

bindingContext.Result = ModelBindingResult.Success(targetValue);

}

else

{

bindingContext.Result = ModelBindingResult.Failed();

}

return Task.CompletedTask;

}

private static bool ParseJsonValue(JsonElement jsonObj, string fieldName, out object jsonValue)

{

int firstDotIndex = fieldName.IndexOf(‘.’);

if (firstDotIndex>=0)

{

string firstPropName = fieldName.Substring(0, firstDotIndex);

string leftPart = fieldName.Substring(firstDotIndex + 1);

if(jsonObj.TryGetProperty(firstPropName, out JsonElement firstElement))

{

return ParseJsonValue(firstElement, leftPart, out jsonValue);

}

else

{

jsonValue = null;

return false;

}

}

else

{

bool b = jsonObj.TryGetProperty(fieldName, out JsonElement jsonProperty);

if (b)

{

jsonValue = jsonProperty.GetValue();

}

else

{

jsonValue = null;

}

return b;

}

}

private static FromJsonBodyAttribute GetFromJsonBodyAttr(ModelBindingContext bindingContext, string fieldName)

{

var actionDesc = bindingContext.ActionContext.ActionDescriptor;

string actionId = actionDesc.Id;

string cacheKey = $”{actionId}:{fieldName}”;

FromJsonBodyAttribute fromJsonBodyAttr;

if (!fromJsonBodyAttrCache.TryGetValue(cacheKey, out fromJsonBodyAttr))

{

var ctrlActionDesc = bindingContext.ActionContext.ActionDescriptor as ControllerActionDescriptor;

var fieldParameter = ctrlActionDesc.MethodInfo.GetParameters().Single(p => p.Name == fieldName);

fromJsonBodyAttr = fieldParameter.GetCustomAttributes(typeof(FromJsonBodyAttribute), false).Single() as FromJsonBodyAttribute;

fromJsonBodyAttrCache[cacheKey] = fromJsonBodyAttr;

}

return fromJsonBodyAttr;

}

}

I will explain the FromJsonBodyBinder class. When a parameter with [FromJsonBody] is bound, BindModelAsync() method will be called, and the result of the binding (that is, the value of the calculated parameters) should be set to bindingContext.Result. If the binding succeeds, the result should be set as follows: “indingContext.Result= ModelBindingResult.Success(theValue)”. If the binding fails, the result should be set as follows: “indingContext.Result= ModelBindingResult. Failed ()”.

In the BindModelAsync() method of FromJsonBodyBinder, the JsonElement object, which is stored in FromJsonBodyMiddleware, will be fetched from HttpContext by “bindingContext.ActionContext.HttpContext.Items[key]”. If there are five parameters in Action, the BindModelAsync() method will be invoked five times; as a result , if the “parsing Json request” processes are performed every times the BindModelAsync() method is invoked, the performance will be super low. Therefore, the Json request is parsed into JsonElement object in advance in FromJsonBodyMiddleware, which improves the performance.

Next, the custom method GetFromJsonBodyAttr() fetches the FromJsonBodyAttribute object of the parameter and check if the propertyName is set on the FromJsonBodyAttribute object. If so, use the propertyName as the name of the JSON property to be bound. If the propertyName is not set, then use the variable name of the binding parameter bindingContext.FieldName as the property name of the JSON to be bound.

Next, the custom method ParseJsonValue() fetches the value of the corresponding property from the JSON object. Since the data type extracted from the JSON object may not match the type of the parameter, we need to call the extension method ChangeType () for the type conversion. The ChangeType() method encapsulates Convert.ChangeType() and handles special types such as Nullables, Enums, and GUIDs. See the source code on GitHub for detail.

The custom ParseJsonValue() method provides support for multiple levels of JSON nesting such as “author.father.name “ through recursion. The firstPropName is the extracted “author”, the leftPart is the remaining “father.name “, and then recursively calls ParseJsonValue() method for further calculation.

The custom GetFromJsonBodyAttr() method uses reflection to get the FromJsonBodyAttribute object of the parameter. To improve performance, the results are cached. The ActionDescriptor object in ASP.NET Core has an Id property, which is used to get a unique identifier for the Action method, which, along with the name of the parameter, forms the Key for the cached item.

4. Summary

Zack.FromJsonBody allows developers to bind the parameters with the simple type of ASP.NET Core MVC and ASP.NET Core WebAPI applications to the JSON request. This open source project has been proven by Youzack.com, an English learning website, for over a year, and you can use it with confidence. I hope this open-source project can help you. Please feel free to give feedback. If you like it, please don’t hesitate to recommend it to your friends.

--

--

Zack Yang
Zack Yang

No responses yet