Working with JSON in Razor Pages

UI generation and processing within web applications is moving increasingly to the client. Data processing and storage is undertaken on the server, with JSON being the preferred format for exchanging data between the server and the client. The recommended approach to providing data services that work with JSON in ASP.NET Core is to use the Web API framework.

Web API

Web API is a framework for building HTTP-based services. Typically, that means making data available as a service via the HTTP protocol. Services built using Web API conform to the REpresentational State Transfer (REST) architectural pattern. An important element of this is that the service should respond appropriately based on the HTTP verb that was used to make the request (GET, POST, PUT, DELETE etc.). Different verbs are used by the client making the request to express the intent behind the operation. They map to CRUD operations as follows:

HTTP Verb CRUD Operation
POST Create
GET Read
PUT Update
DELETE Delete

Web API controllers are available to a Razor Pages application without any additional configuration, and the default data format that they work with is JSON.

An example application

The following walkthrough illustrates how to integrate Web API into a Razor Pages application to provide CRUD services. The application itself is a single page CRUD application. The subject matter is cars. The application uses jQuery on the client for managing the UI, and an in-memory cache to store the cars. Both choices were made with simplicity in mind for demonstration purposes.

  1. Create a new Razor Pages application using the CLI commands (dotnet new razor) or using the Visual Studio New Project dialog. Name the project RazorAPI if you plan to copy and paste code from here and don't want issues with conflicting namespaces occurring.

  2. Add a folder named Models and add a new C# Class file to it named Car.cs. Replace any existing content with the following:

    namespace RazorAPI.Models
    {
        public class Car
        {
            public int Id { get; set; }
            public string Make { get; set; }
            public string Model { get; set; }
            public int Year { get; set; }
            public int Doors { get; set; }
            public string Colour { get; set; }
            public decimal Price { get; set; }
        }
    }
    
  3. Add a new folder named Services and add a new C# class file to it named CarService.cs. Replace any existing content with the following:

    using Microsoft.Extensions.Caching.Memory;
    using RazorAPI.Models;
    using System.Collections.Generic;
    using System.Linq;
    
    namespace RazorAPI.Services
    {
        public interface ICarService
        {
            List<Car> ReadAll();
            void Create(Car car);
            Car Read(int id);
            void Update(Car modofiedCar);
            void Delete(int id);
        }
    
        public class CarService : ICarService
        {
            private readonly IMemoryCache _cache;
    
            public CarService(IMemoryCache cache)
            {
                _cache = cache;
            }
    
            public List<Car> ReadAll()
            {
                if (_cache.Get("CarList") == null)
                {
                    List<Car> cars = new List<Car>{
                    new Car{Id = 1, Make="Audi",Model="R8",Year=2014,Doors=2,Colour="Red",Price=79995},
                    new Car{Id = 2, Make="Aston Martin",Model="Rapide",Year=2010,Doors=2,Colour="Black",Price=54995},
                    new Car{Id = 3, Make="Porsche",Model=" 911 991",Year=2016,Doors=2,Colour="White",Price=155000},
                    new Car{Id = 4, Make="Mercedes-Benz",Model="GLE 63S",Year=2017,Doors=5,Colour="Blue",Price=83995},
                    new Car{Id = 5, Make="BMW",Model="X6 M",Year=2016,Doors=5,Colour="Silver",Price=62995},
                };
                    _cache.Set("CarList", cars);
                }
                return _cache.Get<List<Car>>("CarList");
            }
    
            public void Create(Car car)
            {
                var cars = ReadAll();
                car.Id = cars.Max(c => c.Id) + 1;
                cars.Add(car);
                _cache.Set("CarList", cars);
            }
    
            public Car Read(int id)
            {
                return ReadAll().Single(c => c.Id == id);
            }
    
            public void Update(Car modifiedCar)
            {
                var cars = ReadAll();
                var car = cars.Single(c => c.Id == modifiedCar.Id);
                car.Make = modifiedCar.Make;
                car.Model = modifiedCar.Model;
                car.Doors = modifiedCar.Doors;
                car.Colour = modifiedCar.Colour;
                car.Year = modifiedCar.Year;
                _cache.Set("CarList", cars);
            }
    
            public void Delete(int id)
            {
                var cars = ReadAll();
                var deletedCar = cars.Single(c => c.Id == id);
                cars.Remove(deletedCar);
                _cache.Set("CarList", cars);
            }
        }
    }
    

    This represents a simple CRUD service for the Car entity defined in the Model. All it does is to store a list of Car entities in memory, and provide some methods for accessing one or all of the entries, and for adding, editing and deleting Car entities. It also exposes an interface that needs to be registered with the dependency injection system.

  4. Open Startup.cs and amend the ConfigureServices method so that it looks like the following:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
        services.AddMemoryCache();
        services.AddTransient<ICarService, CarService>();
    }
    

    You will also need to add using RazorAPI.Services to the using directives at the top of the file to bring the CarService into scope.

    The CarService is registered with the DI system along with the default memory cache component.

  5. Change the existing Pages.Index.cshtml.cs file content to the following:

    using Microsoft.AspNetCore.Mvc.RazorPages;
    
    namespace RazorAPI.Pages
    {
        public class IndexModel : PageModel
        {
            public int Id { get; set; }
            public string Make { get; set; }
            public string Model { get; set; }
            public int Year { get; set; }
            public string Colour { get; set; }
            public int Doors { get; set; }
            public int Price { get; set; }
        }
    }
    

    These properties have been added so that tag helpers can be used for form fields in the UI. Usually, you would decorate properties that are used for form fields with the [BindProperty] attribute, but the form in question will not be posted normally and Razor Pages model binding is not required.

  6. Adjust the _Layout.cshtml file to remove the navigation:

    <nav class="navbar navbar-inverse navbar-fixed-top">
        <div class="container">
            <div class="navbar-header">
                <a asp-page="/Index" class="navbar-brand">RazorAPI</a>
            </div>
        </div>
    </nav>
    <div class="container body-content">
        @RenderBody()
        <hr />
        <footer>
            <p>&copy; 2017 - RazorAPI</p>
        </footer>
    </div>
    

    You can also take this opportunity to delete the Contact and About pages and their PageModel class files.

  7. Add the following to the content of the Index.cshtml file:

    <div class="row" id="content">
        <div class="col-lg-6">
            <h3>All Cars</h3>
            <ul id="car-list"></ul>
            <button class="btn btn-sm btn-default" id="new">Add New</button>
        </div>
        <div class="col-lg-6">
            <div id="details"></div>
            <div id="form">
                <form class="form-horizontal">
                    <input type="hidden" asp-for="Id" />
                    <div class="form-group">
                        <label for="Model">Make</label>
                        <input type="text" asp-for="Make" class="form-control input-sm" />
                    </div>
                    <div class="form-group">
                        <label for="Model">Model</label>
                        <input type="text" asp-for="Model" class="form-control input-sm" />
                    </div>
    
                    <div class="form-group">
                        <label for="Colour">Colour</label>
                        <input type="text" asp-for="Colour" class="form-control input-sm" />
                    </div>
                    <div class="form-group">
                        <label for="Year">Year</label>
                        <input type="number" asp-for="Year" class="form-control input-sm" />
                    </div>
                    <div class="form-group">
                        <label for="Doors">Doors</label>
                        <input type="number" asp-for="Doors" class="form-control input-sm" />
                    </div>
                    <div class="form-group">
                        <label for="Price">Price</label>
                        <input type="number" asp-for="Price" class="form-control input-sm" />
                    </div>
                    <div class="form-group">
                        <button class="btn btn-primary btn-sm" id="save">Submit</button>
                    </div>
                </form>
            </div>
        </div>
    </div>
    

    The markup includes an area where a list of cars will be displayed, another where details of a selected car are shown, and a form for adding and editing cars. The method (POST, GET, PUT etc) of the form is not specified. This will be determined at runtime by client side code.

  8. Add the following declarations to the site.css file:

    input{
        max-width:280px;
    }
    
    .entry {
        min-width: 250px;
        display: inline-block;
    }
    
    .details, .edit, .delete {
        cursor: pointer;
        color: #337ab7;
        text-decoration: underline;
    }
    
    ul{
        padding-left:0;
        list-style-type: none;
    }
    
    #content{
        min-height:500px;
    }
    
  9. Add a new folder called Controllers and then use the Add context menu option in Visual Studio to add a Web API controller with Read/Write actions and name it CarController.cs:

    Image

    This will auto-generate a class with the following attributes:

    [Produces("application/json")]
    [Route("api/Car")]
    public class CarController : Controller
    {
        ...
    

    The Produces attribute specifies the content type of the data that the controller actions generate. The default is JSON. The Route attribute specifies the route for the controller. Individual actions are selected using a combination of the HTTP verb used for the request and any parameters.

    The next step is to replace the default actions that return strings to ones that call methods in the CarService.

  10. Replace the existing controller code with the following:

    using Microsoft.AspNetCore.Mvc;
    using RazorAPI.Models;
    using RazorAPI.Services;
    using System.Collections.Generic;
    
    namespace RazorAPI.Controllers
    {
        [Produces("application/json")]
        [Route("api/Car")]
        public class CarController : Controller
        {
            private readonly ICarService _carService;
    
            public CarController(ICarService carService)
            {
                _carService = carService;
            }
    
            // GET: api/Car
            [HttpGet]
            public IEnumerable<Car> Get()
            {
                return _carService.ReadAll();
            }
    
            // GET: api/Car/5
            [HttpGet("{id}", Name = "Get")]
            public Car Get(int id)
            {
                return _carService.Read(id);
            }
    
            // POST: api/Car
            [HttpPost]
            public void Post([FromBody]Car car)
            {
                _carService.Create(car);
            }
    
            // PUT: api/Car/5
            [HttpPut("{id}")]
            public void Put(int id, [FromBody]Car car)
            {
                _carService.Update(car);
            }
    
            // DELETE: api/ApiWithActions/5
            [HttpDelete("{id}")]
            public void Delete(int id)
            {
                _carService.Delete(id);
            }
        }
    }
    

    The controller exposes 5 methods: Get with no parameters, returning all cars; Get with an integer as a parameter, returning the car with an Id value matching the one passed as a parameter; Post taking values from the body of the request to construct a new Car entity; Put which is used to modify the car identified by the id parameter with the values passed in the request body; and Delete which deletes the car having the Id passed as a parameter.

    The CarService that actually manages these operations is passed in to the controller via constructor injection.

  11. Finally, the client side code. Add the following to the end of the Index.cshtml file:

    @section scripts{
    <script type="text/javascript">
        $(function () {
            var loadCars = function () {
                $('#car-list').empty();
                $.get('/api/car').done(function (cars) {
                    $.each(cars, function (i, car) {
                        var item = `<li>
                                <span class="entry">
                                    <strong>${car.make} ${car.model}</strong>
                                    (£${car.price})
                                    </span>
                                    <span class ="details" data-id="${car.id}">Details</span> |
                                    <span class ="edit"  data-id="${car.id}">Edit</span> |
                                    <span class ="delete"  data-id="${car.id}">Delete</span>
                                </li>`;
                        $('#car-list').append(item);
                    });
                });
            }
    
            var showForm = function () {
                $(':input').val('');
                $('#details').empty();
                $('#form').show();
            }
    
            var clearForm = function () {
                $('#details').empty();
                $(':input').val('');
                $('#form').hide();
            }
    
            $('#new').on('click', showForm);
    
            clearForm();
            loadCars();
    
            $('#car-list').on('click', '.edit, .details', function () {
                var cmd = $(this);
                $.get(`/api/car/${cmd.data('id')}`).done(function (car) {
                    if (cmd.hasClass('details')) {
                        clearForm()
                        $('#details').empty().append(
                            `<h3>Details</h3>
                        <strong>${car.make} ${car.model}</strong><br>
                        £${car.price}<br>
                        Doors: ${car.doors}<br>
                        Year: ${car.year}<br>
                        Colour: ${car.colour}`
                        );
                    } else {
                        showForm();
                        $('#Id').val(car.id);
                        $('#Make').val(car.make);
                        $('#Model').val(car.model);
                        $('#Colour').val(car.colour);
                        $('#Year').val(car.year);
                        $('#Doors').val(car.doors);
                        $('#Price').val(car.price);
                    }
                });
            });
    
            $('#save').on('click', function (e) {
                e.preventDefault();
    
                var url = '/api/car/';
                var method = 'post';
                if ($('#Id').val() !== '') {
                    url += $('#Id').val();
                    method = 'put';
                }
                var car = {};
                $.each($(this).closest('form').serializeArray(), function () {
                    if (this.name !== 'Id' || (this.name === 'Id' && this.value !== '')) {
                        car[this.name] = this.value || '';
                    }
                });
    
                $.ajax({
                    type: method,
                    url: url,
                    data: JSON.stringify(car),
                    contentType: 'application/json'
                }).done(function () {
                    clearForm();
                    loadCars();
                });
            });
    
            $('#car-list').on('click', '.delete', function () {
                $.ajax({
                    type: 'delete',
                    url: '/api/car/' + $(this).data('id'),
                }).done(function () {
                    clearForm();
                    loadCars();
                });
            });
        });
    </script>
    }
    

    The first section of the code declares a function called loadCars. This function clears the ul that has an id of car-list of any content and then calls the API using the GET verb with no parameters which maps to the method that returns a list of all cars. The HTML for the list items is built up using the jQuery each function to iterate the collection of cars returned by the API.

    Notice that the JSON version of the property names on the car object use camel casing (start with lower case). This is the default behaviour when serialising to JSON in .NET Core. In previous versions of ASP.NET, the default was Pascal casing. You can restore this option through configuration:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc()
            .AddJsonOptions(options => options.SerializerSettings.ContractResolver = new DefaultContractResolver());
    }
    

    The resulting HTML consists of a series of list items that include links to show details of the entry, edit it or delete it.

    Next, two convenience functions are defined that show and hide the form so that it is ready for use and doesn't retain information from previous operations. The Add New button is wired up to show the form when it is clicked. Then the functions to clear the form and hide it, and to populate the list are called, resulting in the following content in the page:

    Image

    The Edit and Details links have click handlers attached to them that fire a GET request appended with the Id of the car that they apply to which returns the specified car entity. If the link that was clicked is a details link (determined by the CSS class applied to it), the details of the car are displayed, otherwise they are presented in the form for editing.

    When the form is submitted, the hidden Id field is inspected. If it was populated as a result of the Edit link being clicked, the HTTP method is set to PUT and the Id of the car to be modified is appended to the URL, mapping this request to the [HttpPut("{id}")] Web API route. If it is empty, this is a new car and the method is set to POST.

    In both cases, the content of the form fields is serialised into a Javascript object:

    var car = {};
    $.each($(this).closest('form').serializeArray(), function () {
        if (this.name !== 'Id' || (this.name === 'Id' && this.value !== '')) {
            car[this.name] = this.value || '';
        }
    });
    

    The Id property is only included in the serialised object if it has a value.

    The content type is specified for the request as application/json. Failure to do this will result in an HTTP 415 - Unsupported Media Type error code because the content type of the request will default to application/x-www-form-urlencoded:

    Image

    If you actually want to support application/x-www-form-urlencoded, you need to change the Web API method to use the [FromForm] attribute instead of the [FromBody] attribute. When the FromBody option is specified, the framework uses a JSON Input Formatter which expects to find a JSON string in the body of the request:

    Image

    The final block of code takes care of deleting car objects. It adds the Id of the car to be deleted to the URL and then uses the DELETE method:

    Image

Last updated: 10/5/2023 8:33:33 AM

© 2018 - 2024 - Mike Brind.
All rights reserved.
Contact me at Mike dot Brind at Outlook.com