ASP .NET Core provides a way to create RESTful services via .NET Core web API applications. Web APIs accept HTTP requests and return HTTP responses to the callers. Hence, it becomes very important to understand the core features of how the data is received and processed by Web APIs.
In this article, we are going to have look at how the incoming data from HTTP request is used as parameters for processing the requests.
General Terms
Before we begin, let’s have a quick glance at some basic terms that we should know.
Every .NET Web API has three important components:
- A bootstrapper (In
Program.cs
,Main
Method) - A
Startup
configurations to configure all required services and request processing pipeline - One or many
controllers
Main method is simple, some people may call it boring because it does not change very frequently. It just initializes configurations and runs a host that actually accepts HTTP requests.
Startup class mainly has two methods:
Configure Services
– to configure all the required services like dependency injection, configurations, etc.Configure
– for defining request process pipeline for the web API app
Controllers are public classes which are derived from ControllerBase and they have ApiControllerAttribute. A controller
can have some additional attributes like RouteAttribute – specifying some patterns, these patterns are used to find a best-match for executing incoming request.
I already have covered most of these topics in detail in some of my previous blog posts. If you want to have a look at whole series of articles, refer the readme file from this GitHub repository.
What is an Action?
Every public method defined in a controller is generally called as Action. Every Action
can have additional attributes like RouteAttribute or Http Method Attributes (GET, POST, etc) specifying which Http methods can be served by the Action
.
Like any other methods, every action has some input parameters and every action has some return type. In this article, we will not discuss about return types. This article is focusing on explaining how input data from HTTP requests can be bound to input parameters of Actions.
What is Model Binding ?
Web APIs receives the requests in the form of HTTP messages. Every http message has headers, URLs, query strings parameters, request body, etc.
Model Binding is a system that is responsible for:
- Retrieving data from various sources such as route data, form fields, and query strings.
- Providing this data to input parameters of Actions. For Razor pages, there can be class level properties binding to the incoming request data.
- Converting the string data from incoming request to appropriate target .NET data type
- Populating complex data types used as input parameters
Model binding can either be explicitly specified or it can be inferred. Every model binding gets the data from some “source” (e.g. query string or form data, etc.) and populates the “target” field.
In case of Web APIs, input parameters to actions are the target for any model binding.
e.g. Consider an action as shown below. It would take a parameter ID from route (i.e. URL) and another parameter name from query string.
[HttpGet("{id}")] public ActionResult SomeActionName(int id, string name)
Model Binding Sources
In this section, let’s have a look at various sources which can be used for populating parameters.
From Route
The value for input parameters for an Action
might come from route parameters. Route parameters can be either specified using Http Verb Attributes or Route attribute as shown in code example below.
This type of binding can be inferred. The route template name is matched with parameter name. If parameter with same name is found, then the string value from route is converted to appropriate data type.
This type of binding can also be explicitly specified on the input parameter by using [FromRoute]
attribute.
From Query
The input parameters can also take values from query string.
For a HttpGet Action
, if a parameter is not mapped to any route parameter and there is no other attribute, then framework checks if there is any query string with key matching to the parameter name. If query string is found, then its value is converted to appropriate data type and value is assigned to parameter.
Also, a parameter can be explicitly decorated with [FromQuery]
attribute. Below code example shows how this attribute can be used.
From Body
This attribute is required to be placed explicitly and it cannot be inferred.
When this attribute is placed on an input parameter, the value of that parameter is populated from HTTP request body. Responsibility of handling populating the parameter is delegated to input formatters.
Input formatters generally try matching property names from incoming JSON (or XML) to the class property names. Once the match is found, string values are converted to appropriate target type.
Generally this attribute is used with Complex types (i.e. class). If a class has any other model binding attributes specified on individual properties, then it is ignored by input formatter.
Don’t apply [FromBody]
to more than one parameter per action method. Once the request stream is read by an input formatter, it’s no longer available to be read again for binding other [FromBody]
parameters.
From Headers
An attribute – [FromHeader]
– can be placed on input parameters to bind the input parameter to a request header. Sometimes some important data is passed via headers. Best examples can be API key headers, which identifies if the caller is authorized.
Code Example
Below code example shows examples of all sources mentioned above.
As shown below, combination of attributes can also be used to read data from more than one sources. Also, if the source property name is different from target property name, then these attributes also provide facility to specify source property name.
What if No Source Found ?
Obvious question would be – what if appropriate source is not found in incoming request ? Would it throw errors ? OR it would just continue with default values ?
If the source is not found, then :
- Nullable parameters are set to null
- Non-Nullable parameters are set to their default values (i.e. default(T) )
- Complex types are instantiated using default parameter-less constructor
- Array types are instantiated but they do not have individual items in it (i.e. empty array).
- Only exception is byte[] which is set to null.
If a source is found but can’t be converted into the target type, model state is flagged as invalid. The target parameter or property is set to null or a default value, as noted in the previous section.
Advanced Model Binding
In most of the practical scenarios, the attributes mentioned above should be sufficient. But if by any chance, these are not working for application’s needs, then a custom model binding logic can be used.
I hope you find this information useful. Let me know your thoughts.
Pingback: The Code Blogger - Which Type Should Be Returned From .NET Core Web API Actions ?
Pingback: The Code Blogger - Model Validation Attributes in .NET Core Web APIs