.net core, C# tip, Web Development

Creating a RESTful Web API template in .NET Core 1.1 – Part #2: Improving search

Previously I’ve written about how to improve the default Web API template in .NET Core 1.1. Presently it’s really basic – we’ve methods for PUT, POST, DELETE and a couple of GET methods – one which returns all objects, and one which returns an object matching the integer ID passed to the method.

You probably know that in a real application, searching for objects through an API is a lot more complex:

  • Presently the default template just returns simple strings – but usually we’re going to return more complex objects, which we probably want to pass back in a structured format.
  • It would be nice if we could provide some level of validation feedback to the user if they do something wrong – for example, we might be able to support the following RESTful call:
/api/Values/123   // valid - searches by unique integer identifier

But we can’t support the following call and should return something useful.

/api/Values/objectname   // invalid - several objects might have the same name
  • Usually we need more fine grained control than “get one thing” or “get all the things”.

So let’s look at some code which addresses each of these to improve our template.

Format the results using JSON.net

I want my default template to be a useful starting point – not just to me, but also to other members on my team.

  • Most of the time we won’t be returning a simple string – so I’ve constructed a simple anonymous object to return.
  • I prefer to use JSON.net from NewtonSoft which is pretty much the industry standard at this point, and not waste time with a bunch of answers on StackOverflow describing other (slower and more complex) ways of doing it.
// GET api/values/5
[HttpGet("{id}")]
public IActionResult Get(int id)
{
    var customObject = new { id = id, name = "name" };
 
    var formattedCustomObject = JsonConvert.SerializeObject(customObject, Formatting.Indented);
 
    return Ok(formattedCustomObject);
}

Easy – and when we run our service and browse to api/values/5, we get the following JSON response.

{
  "id": 5,
  "name": "name"
}

Help the user when they make a common mistake

I’ve frequently made a mistake when manually modifying a GET call – I search by name when I should search by id.

/api/Values/123   // correct - searches by unique integer identifier

/api/Values/objectname   // wrong - this is the mistake I sometimes make

/api/Values?name=objectname   // correct - this is what I should have done

So what result do we presently get when I make this mistake? I’d expect some kind of error message…but actually we get this:

{
  "id": 0,
  "name": "name"
}

OK – our template is somewhat contrived, but somehow the text “objectname” has been converted to a zero. What’s happened?

Our GET action has tried to interpret the value “objectname” as an integer, because there’s only one GET action which accepts one parameter, and that parameter is an integer. So the string is cast to an int, becomes zero, and out comes our incorrect and unhelpful result.

But with .NET Core we can fix this – route templates are a pretty cool and configurable feature, and can do some useful type validation for us using Route Constraints. You can read more about this here, and it allows us to create two different GET actions in our controller – one for when we’re passed an integer parameter, and one for when we’re passed a string.

First of all, let’s handle the correct case – passing an integer. I’ll rename my action to be “GetById” to be more explicit about the action’s purpose.

[HttpGet("{id:int:min(1)}", Name = "GetById")]
public IActionResult GetById([Required]int id)
{
    try
    {
        // Dummy search result - this would normally be replaced by another service call, perhaps to a database
        var customObject = new { id = id, name = "name" };
 
        var formattedCustomObject = JsonConvert.SerializeObject(customObject, Formatting.Indented);
 
        return Ok(formattedCustomObject);
    }
    catch (KeyNotFoundException)
    {
        Response.Headers.Add("x-status-reason"$"No resource was found with the unique identifier '{id}'.");
        return NotFound();
    }
}

In addition to the renamed action, you can see that the route constraint has changed to the code shown below:

[HttpGet("{id:int:min(1)}", Name = "GetById")]

So for the parameter named “id”, we’ve specified that it needs to be an integer, with a minimum value of 1. If the data doesn’t match those criteria…then this action won’t be recognised or called.

Now let’s handle the situation where someone tries to GET a text value – we’ll call this method “GetByName”.

[HttpGet("{name}")]
public IActionResult GetByName([Required]string name)
{
    Response.Headers.Add("x-status-reason"$"The value '{name}' is not recognised as a valid integer to uniquely identify a resource.");
    return BadRequest();
}

There’s a few things worth calling out here:

  • This method only returns a BadRequest() – so when we pass in a string like “objectname”, we get a 400 error.
  • Additionally, I pass an error message through the headers which I hope helpfully describes the mistake and how to correct it.
  • Finally, I’ve included a catch clause for when the id we search for hasn’t been found – in this case I return a helpful message in the response headers, and the HTTP status code 404 using the NotFound() method.

A more traditional method of error handling would be to check for all the errors in one method – but there’s a couple of reasons why I prefer to have two methods:

  • If the parameter to a single GET method is an integer, we will lose the string value passed by the client and have it replaced by a simple zero. I’d like to pass that string value back in an error message.
  • I think using two methods – one for the happy path, one for the exception – is more consistent with the single responsibility principle.

Allow custom search queries

So say we want to do something a bit more sophisticated than just search by Id or return all results – say we want to search our repository of data by a field like “name”.

The first step is to create a custom SearchOptions class.

public class SearchOptions
{
    public string Name { getset; }
}

It’s easy to add custom properties to this class – say you want to search for a people whose ages are between two limits, so you’d add properties like the ones below:

public int? LowerAgeLimit { getset; }
 
public int? UpperAgeLimit { getset; }

How do we use this “SearchOptions” class?

I’d expect a simple RESTful search query to look something like this:

/api/Values?name=objectname&lowerAgeLimit=20

If a GET method takes SearchOptions as a parameter, then because of the magic of MVC and auto-wiring properties, the searchOptions object will be populated with the name and LowerAgeLimit specified in the querystring.

The method below shows what I mean. You can see below that I’ve simply created a list of anonymous objects in the method and pretend they are the search results – we’d obviously replace this method with some kind of service call which would accept searchOptions as a parameter, and use that information to get some real search results.

[HttpGet]
public IActionResult Get([FromQueryRequired]SearchOptions searchOptions)
{
    var searchResults = new[]
                            {
                                new { id = 1, Name = "value 1" },
                                new { id = 2, Name = "value 2" }
                            }.ToList();
 
    var formattedResult = JsonConvert.SerializeObject(searchResults, Formatting.Indented);
 
    Response.Headers.Add("x-total-count", searchResults.Count.ToString());
 
    return Ok(formattedResult);
}

I’ve structured the results as JSON like I’ve shown previously, but one more thing that I’ve done is add another header which contains the total number of results – I’ve done this to present some helpful meta-information to the service consumer.

Wrapping Up

So we’ve covered quite a lot of ground in this post – previously we ended with a very simple controller which returned HTTP status codes, but this time we have something a little more advanced:

  • We return complex objects using JSON;
  • We validate data passed to the GET method, passing back a 400 code and error message if the service is being used incorrectly;
  • We provide a mechanism to allow for more complex searching and pass some useful meta-data.

I’ve included the complete class below.

using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
 
namespace MyWebAPI.Controllers
{
    [Route("api/[controller]")]
    public class ValuesController : Controller
    {
        // GET api/values
        [HttpGet]
        public IActionResult Get([FromQueryRequired]SearchOptions searchOptions)
        {
            // Dummy search results - this would normally be replaced by another service call, perhaps to a database
            var searchResults = new[]{
                                        new{ id=1, Name="value 1" },
                                        new{ id=2, Name="value 2"}
                                     }.ToList();
 
            var formattedResult = JsonConvert.SerializeObject(searchResults, Formatting.Indented);
 
            Response.Headers.Add("x-total-count", searchResults.Count.ToString());
 
            return Ok(formattedResult);
        }
 
        // GET api/values/5
        [HttpGet("{id:int:min(1)}", Name = "GetById")]
        public IActionResult GetById([Required]int id)
        {
            try
            {
                // Dummy search result - this would normally be replaced by another service call, perhaps to a database
                var customObject = new { id = id, name = "name" };
 
                var formattedCustomObject = JsonConvert.SerializeObject(customObject, Formatting.Indented);
 
                return Ok(formattedCustomObject);
            }
            catch (KeyNotFoundException)
            {
                Response.Headers.Add("x-status-reason"$"No resource was found with the unique identifier '{id}'.");
                return NotFound();
            }
        }
 
        [HttpGet("{name}")]
        public IActionResult GetByName([Required]string name)
        {
            Response.Headers.Add("x-status-reason"$"The value '{name}' is not recognised as a valid integer to uniquely identify a resource.");
            return BadRequest();
        }
 
        // POST api/values
        [HttpPost]
        public IActionResult Post([FromBody]string value)
        {
            return Created($"api/Values/{value}", value);
        }
 
        // PUT api/values/5
        [HttpPut("{id}")]
        public IActionResult Put(int id, [FromBody]string value)
        {
            return Accepted(value);
        }
 
        // DELETE api/values/5
        [HttpDelete("{id}")]
        public IActionResult Delete(int id)
        {
            return NoContent();
        }
    }
 
    // This should go into its own separate file - included here for simplicity
    public class SearchOptions
    {
        public string Name { getset; }
    }
}

Obviously this is still a template – I’m aiming to include the absolute minimum amount of code to demonstrate how to do common useful things. Hopefully this is helpful to anyone reading this.