Advanced Model Binding Concepts in ASP.NET Core

Advanced Model Binding Concepts in ASP.NET Core

In my previous tutorial on Model Binding technique in ASP.NET Core I covered some of the basics of this technique. Now in this tutorial I will cover some of the advanced and powerful features of this technique.

Model Binding to Arrays

You can do Model Binding for array type parameters. To demonstrate this feature, create an action method called Places in the Home controller as shown by the code given below:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using ModelBindingValidation.Models;

namespace ModelBindingValidation.Controllers
{
    public class HomeController : Controller
    {
        private IRepository repository;
        public HomeController(IRepository repo)
        {
            repository = repo;
        }
        
        // other actions

        public IActionResult Places(string[] places) => View(places);
    }
}

Notice this action method has a parameter of string array type and is named as places. The model binding feature will look for the item called placed (in form data values, routing variables and query strings) and create an array that contains those values.

Create the Places view inside the Views ➤ Home folder and add the following code to it:

@model string[]
@{
    Layout = "_Layout";
    ViewData["Title"] = "Places";
}
 
<h2>Places</h2>
 
@if (Model.Length == 0)
{
    <form asp-action="Places" method="post">
        @for (int i = 1; i <= 3; i++)
        {
            <div class="form-group">
                <label>Place @i:</label>
                <input id="places" name="places" class="form-control" />
            </div>
        }
        <button type="submit" class="btn btn-primary">Submit</button>
    </form>
}
else
{
    <table class="table table-sm table-bordered table-striped">
        @foreach (string place in Model)
        {
            <tr><th>Place:</th><td>@place</td></tr>
        }
    </table>
    <a asp-action="Places" class="btn btn-primary">Back</a>
}

The view contain and if-else block to check if the model is empty or not. It used the code – Model.Length == 0 to perform this check. If the model is not empty then it display all the places in the model object by looping through each of them (see lines 25 to 28). Else in case if the model is empty then a form is displayed to add 3 places in 3 identical input elements (see lines to 12 to 18).

The 3 identical input elements have their names same like the parameter name of the action method which is places. So when the form is submitted then the Model Binding feature will extract the values of all elements that have name called places and create an array containing these values. The array is then passed on to the parameter of the action method.

Run the project and go to the url to initiate the Places action which is – /Home/Places. Now check the html of the 3 input controls in the page source which is given below.

<input id="places" name="places" class="form-control" />
<input id="places" name="places" class="form-control" />
<input id="places" name="places" class="form-control" />

You can see all these 3 inputs have the name as places and this is same as the name of the parameter of the action method. So Model Binding to Arrays come to the picture here.

Now fill the 3 names for the places in the form and submit it. On submitting the 3 values are displayed on the browser. See the image given below.

model binding array

Model Binding to Collection

Model binding also supports Collection. I update the previous example from string array to strongly typed List.

First change the parameter of the Places action from string[] to List.

public IActionResult Places(List<string> places) => View(places);

Then do a 2 slight changes on the Places view –

  • 1. Change model type to List<string>.
  • 2. In the if block use Model.Count.

I have highlighted these 2 thing in the below updated code.

@model List<string>
@{
    Layout = "_Layout";
    ViewData["Title"] = "Places";
}

<h2>Places</h2>

@if (Model.Count == 0)
{
    <form asp-action="Places" method="post">
        @for (int i = 1; i <= 3; i++)
        {
            <div class="form-group">
                <label>Place @i:</label>
                <input id="places" name="places" class="form-control" />
            </div>
        }
        <button type="submit" class="btn btn-primary">Submit</button>
    </form>
}
else
{
    <table class="table table-sm table-bordered table-striped">
        @foreach (string place in Model)
        {
            <tr><th>Place:</th><td>@place</td></tr>
        }
    </table>
    <a asp-action="Places" class="btn btn-primary">Back</a>
}

Now the Model Binding will work for Collection for the Places action method.

Model Binding for Collections of Complex Types

Now I will show how to do Model Binding for Collections of Complex Types. Here multiple objects of PersonAddress class will be involved in Model Binding in a single request. The PersonAddress class is fairly simple and is given below.

public class PersonAddress
{
    public string City { get; set; }
    public string Country { get; set; }
}

Go to the Home Controller and add a new action method named Address to it. The code of this action method is given below:

public IActionResult Address(List<PersonAddress> address) => View(address);

Next create the view called Address inside the Views ➤ Home folder with the code as shown below:

@model List<PersonAddress>
@{
    Layout = "_Layout";
    ViewData["Title"] = "Address";
}
 
<h2>Address</h2>
 
@if (Model.Count() == 0)
{
    <form asp-action="Address" method="post">
        @for (int i = 0; i < 3; i++)
        {
            <fieldset class="form-group">
                <legend>Address @(i + 1)</legend>
                <div class="form-group">
                    <label>City:</label>
                    <input name="[@i].City" class="form-control" />
                </div>
                <div class="form-group">
                    <label>Country:</label>
                    <input name="[@i].Country" class="form-control" />
                </div>
            </fieldset>
        }
        <button type="submit" class="btn btn-primary">Submit</button>
    </form>
}
else
{
    <table class="table table-sm table-bordered table-striped">
        <tr><th>City</th><th>Country</th></tr>
        @foreach (var address in Model)
        {
            <tr><td>@address.City</td><td>@address.Country</td></tr>
        }
    </table>
    <a asp-action="Address" class="btn btn-primary">Back</a>
}

The view renders a form if there are no items in the model. The form contains 3 pairs of City and Country elements whose names are prefixed with an array index as shown in the code below:

<input name="[0].City" class="form-control" />
<input name="[0].Country" class="form-control" />
 
<input name="[1].City" class="form-control" />
<input name="[1].Country" class="form-control" />
 
<input name="[2].City" class="form-control" />
<input name="[2].Country" class="form-control" />

On the submission of the form, the model binding process uses the array index prefixes in the name attributes to obtain values for the action’s argument which is List<personaddress> address.

The names prefixed with [0] are used for the first PersonAddress object, those prefixed with [1] are used for the second PersonAddress object, and finally those prefixed with [2] are used for the third PersonAddress object. You can continue this pattern if you have more objects. It is a classic example for Model Binding for Collections of Complex Types.

Now test it by running your project and going to the URL – /Home/Address. Fill and form and press the submit button, you will see your added values getting displayed on your browser, as shown by the image given below:

model binding collections of complex types

Specifying a Model Binding Source for selection

As stated earlier, the Model Binding process searches for data in 3 places by default:

  • 1. Form data
  • 2. Routing system
  • 3. Query string value

You can override this search behavior and force model binding to use only a specified source for binding data. This can be done by using the attributes that are described in the table given below:

Name Description
FromQuery This attribute is used to select the query string as the only source of binding data.
FromHeader This attribute is used to select a request header as the only source of binding data.
FromBody This attribute is used to specify that the request body should be used as the only source of binding data.
FromForm This attribute is used to select form data as the only source of binding data.
FromRoute This attribute is used to select routing system as the only source of binding data.

In the above attributes the name of the parameter is used to locate a value by default, but this can be changed using the Name property, which allows a different name to be specified.

You can understand the default source selection by Model Binding by reading my tutorial called What is Model Binding

‘FromQuery’ Attribute

The Index Action of the Home Controller code is given below:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using ModelBindingValidation.Models;
 
namespace ModelBindingValidation.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index(int? id)
        {
            return View(repository[Convert.ToInt32(id)]);
        }

        // other methods
    }
}

Now when you go to the URL – /Home/Index/1?id=2. You will see the record of Employee id 1 because the Model Binding feature will search for the id value in the routing data before than query string values. So the above code works the same as when putting [FromRoute] attribute.

public IActionResult Index([FromRoute] int? id)
{
    return View(repository[Convert.ToInt32(id)]);
}

In the routing data it finds the 3rd segment of the URL – /Home(1st segment)/Index(2nd segment)/1(3rd segment) to contain the value of id, which is 1. So you will see the Employee id 1 record.

Now I can easily override this search behavior by using the FromQuery attribute. It will force Model Binding to look only in query string values for the value of Id.

So update the Index Action method to:

public IActionResult Index([FromQuery]int? id)
{
    return View(repository[Convert.ToInt32(id)]);
}

Rerun the application and go to the same URL – /Home/Index/1?id=2. This time you will see the 2nd employee record on the browser. This means the model binding feature searched the value of id in only query string values. See the below image:

[FromQuery] attribute

‘FromHeader’ Attribute

With the FormHeader attribute I can direct the Model Binding feature to select a request header as the only source of binding data.

Add an new action method called Header to the Home Controller as shown in the code below:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using ModelBindingValidation.Models;
 
namespace ModelBindingValidation.Controllers
{
    public class HomeController : Controller
    {
        public string Header([FromHeader]string accept) => $"Header: {accept}";                           
         
        // other methods
    }
}

This action has an parameter called accept the value of it will be bind from the Accept header in the current request and then will be returned by the action method.

Run your application and go to URL – /Home/Header. You will see the Accept header value in the browser as shown below:

Header: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9

Now let us check the Request Header values in the browser’s developer tools. Follow the given steps:

  • 1. Open the Chrome Developer Tools by pressing the F12 key or use shortcut Ctrl+Shift+I.
  • 2. Click the Network tab. Press F5 key it asks for reload to capture values.
  • 3. On the left side you will see the Header text. Click on it, see image below:
  • request response in browser
  • 4. You will get 4 tabs – ‘Headers’, ‘Preview’, ‘Response’, ‘Timing’. Click on the Headers tab and scroll down to find the Request Headers section. See the below image:
  • request headers in browser

The Request Headers have any properties like – Accept, Accept-Encoding, Accept-Language, Cache-Control, Connection, Host, Upgrade-Insecure-Requests and User-Agent.

Now let us bind the User-Agent value. For this configure the FromHeader attribute to use the Name property by specifying the name of the header, which in my case is User-Agent.

The updated code of the Header action method is:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using ModelBindingValidation.Models;

namespace ModelBindingValidation.Controllers
{
    public class HomeController : Controller
    {
        public string Header([FromHeader(Name = "User-Agent")]string accept) => $"Header: {accept}";
    
        // other methods
    
    }
}

Now reload your application and view the same URL – /Home/Header. This time you will see the User-Agent value being displayed on the browser as shown below:

Header: Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36

Binding all the Request Headers in a class

You can also bind all the Request Header values by applying the FromHeader attribute to the properties of a model class. Let us create this feature.

Create a class inside the Models folder and name it FullHeader.cs.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace ModelBindingValidation.Models
{
    public class FullHeader
    {
        [FromHeader]
        public string Accept { get; set; }

        [FromHeader(Name = "Accept-Encoding")]
        public string AcceptEncoding { get; set; }

        [FromHeader(Name = "Accept-Language")]
        public string AcceptLanguage { get; set; }

        [FromHeader(Name = "Cache-Control")]
        public string CacheControl { get; set; }

        [FromHeader(Name = "Connection")]
        public string Connection { get; set; }

        [FromHeader(Name = "Host")]
        public string Host { get; set; }

        [FromHeader(Name = "Upgrade-Insecure-Requests")]
        public string UpgradeInsecureRequests { get; set; }

        [FromHeader(Name = "User-Agent")]
        public string UserAgent { get; set; }
    }
}

Next create an action method by the name of FullHeader in the Home Controller:

public IActionResult FullHeader(FullHeader model) => View(model);

Finally create the FullHeader view inside the Views ➤ Home folder:

@model FullHeader
@{
    Layout = "_Layout";
    ViewData["Title"] = "Full Header";
}
 
<h2>Header</h2>
 
<table class="table table-sm table-bordered table-striped">
    <tr><th>Accept:</th><td>@Model.Accept</td></tr>
    <tr><th>Accept-Encoding:</th><td>@Model.AcceptEncoding</td></tr>
    <tr><th>Accept-Language:</th><td>@Model.AcceptLanguage</td></tr>
    <tr><th>Cache-Control:</th><td>@Model.CacheControl</td></tr>
    <tr><th>Connection:</th><td>@Model.Connection</td></tr>
    <tr><th>Host:</th><td>@Model.Host</td></tr>
    <tr><th>Upgrade-Insecure-Requests:</th><td>@Model.UpgradeInsecureRequests</td></tr>
    <tr><th>UserAgent:</th><td>@Model.UserAgent</td></tr>
</table>

Now check it by running your application and go to the URL – /Home/FullHeader where you will see all the Request Headers values as shown in the image below:

binding all request headers in class

‘FromBody’ Attribute

The FromBody attribute is used to specify that the Model Binding should only use the Request Body as the source.

Related tutorial – I have used the [FromBody] attribute when implementing Web APIs. See this article – Create a Reservation Record by Calling the Web API

For understanding how this attribute works, add new actions called Body to the Home Controller. See the code below:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using ModelBindingValidation.Models;
 
namespace ModelBindingValidation.Controllers
{
    public class HomeController : Controller
    {
        // Removed for clarity
 
        public IActionResult Body() => View();
 
        [HttpPost]
        public Employee Body([FromBody]Employee model) => model;
    }
}

I have decorated the parameter for the Post version of the Body action method with the FromBody attribute. It means that only the request body content will be used for model binding. Also note that the return type of the action method is an Employee class.

Now create Body view inside the Views ➤ Home folder with contents as shown below:

@{
    Layout = "_Layout";
    ViewData["Title"] = "Body";
}
 
<h2>Body</h2>
 
@section scripts {
    <script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.2.1.min.js"></script>
    <script>
        $(document).ready(function () {
            $("button").click(function (e) {
                $.ajax("/Home/Body", {
                    method: "post",
                    contentType: "application/json",
                    data: JSON.stringify({
                        id: 5,
                        name: "Donald Trump"
                    }),
                    success: function (data) {
                        $("#empId").text(data.id);
                        $("#empName").text(data.name);
                    }
                });
            });
        });
    </script>
}

<table class="table table-sm table-bordered table-striped">
    <tr><th>Id:</th><td id="empId"></td></tr>
    <tr><th>Name:</th><td id="empName"></td></tr>
</table>
<button class="btn btn-primary">Submit</button>

In this view, when the button is clicked, then it sends a Http Post request containing JSON data to the URL – /Home/Body.

Notice how the values for id & name are converted to JSON string by using JSON.stringify method of JavaScript and they are then added to the data parameter of the jQuery AJAX method.

data: JSON.stringify({
    id: 5,
    name: "Donald Trump"
})

The Body action method receives this data in it’s Employee type argument, and then returns it back to the view in JSON format. Finally in the Success Callback method of the $.ajax() method I an receiving this data and showing it inside the td elements of the HTML table.

You can check it’s working by going to the URL – /Home/Body and clicking the button.

The image given below shows the working:

[frombody] attribute

‘FromForm’ Attribute

The [FromForm] attribute is used when you want the model binding should get the values from posted form fields.

Create action methods called FromFormExample in the Home Controller.

public IActionResult FromFormExample() => View();

[HttpPost]
public Employee FromFormExample([FromForm]Employee model) => model;

Now create FromFormExample view inside the Views ➤ Home folder.

@{
    Layout = "_Layout";
    ViewData["Title"] = "FromFormExample";
}

<h2>Body</h2>

@section scripts {
    <script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.2.1.min.js"></script>
    <script>
        $(document).ready(function () {
            $("button").click(function (e) {

                data = new FormData();
                data.append("id", 5);
                data.append("name", "Donald Trump");

                $.ajax("/Home/FromFormExample", {
                    method: "post",
                    processData: false,
                    contentType: false,
                    data: data,
                    success: function (data) {
                        $("#empId").text(data.id);
                        $("#empName").text(data.name);
                    }
                });
            });
        });
    </script>
}

<table class="table table-sm table-bordered table-striped">
    <tr><th>Id:</th><td id="empId"></td></tr>
    <tr><th>Name:</th><td id="empName"></td></tr>
</table>
<button class="btn btn-primary">Submit</button>

The view code has ajax() method which calls the FromRouteExample action. Since this action’s argument require the the values from form fields (as it has [FromForm] attribute). Therefore I have to add the values of the fields in the FormData, see below:

data = new FormData();
data.append("Id", 5);
data.append("Name", "Donald Trump");

Then finally add the form data values to the data parameter of ajax method. You can check it’s working by going to the URL – /Home/FromFormExample and clicking the button. The image given below shows the working:

[frombody] attribute
I have used [FromForm] attribute when creating Web APIs, so you can see it’s example at this article – Update a Reservation Records through the Web API

‘FromRoute’ Attribute

This [FromRoute] is used to select routing system as the only source of Model binding data. Create a new action method called FromRouteExample in the Home Controller as shown below. Notice the argument has the [FromRoute] attribute so it will be bind with routes data.

public IActionResult FromRouteExample() => View();

[HttpPost]
public string FromRouteExample([FromRoute]string id) => id;

Next create the view called FromRouteExample inside Views ➤ Home folder as shown below.

@{
    Layout = "_Layout";
    ViewData["Title"] = "FromRouteExample";
}

<h2>Body</h2>

@section scripts {
    <script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.2.1.min.js"></script>
    <script>
        $(document).ready(function () {
            $("button").click(function (e) {
                $.ajax("/Home/FromRouteExample/5",
                    {
                        method: "post",
                        success: function (data) {
                            $("#id").text(data);
                        }
                    });
            });
        });
    </script>
}

<table class="table table-sm table-bordered table-striped">
    <tr><th>Id:</th><td id="id"></td></tr>
</table>
<button class="btn btn-primary">Submit</button>

I am sending a value of 5 when making the ajax call to the action method. This value will be bind to the action’s argument because of the presence of [FromRoute] attribute. Run the project and go to the url – /Home/FromRouteExample click the submit button and you will see the value of 5 will be displayed on the html table. I have shown this all thing in the below video.

[FromRoute] video

You can download the source code using the below link:

Download

Conclusion

This completes the Advanced Model Binding topic of ASP.NET Core. Now you can use this technique and create very powerful data driven applications.

Share this article -

yogihosting

ABOUT THE AUTHOR

This article has been written by the Technical Staff of YogiHosting. Check out other articles on "ASP.NET Core, jQuery, EF Core, SEO, jQuery, HTML" and more.