Lesson Weekend

In the last lesson, we learned how to seed our database with data. In this lesson, we'll learn how to add search parameters to our get request so that we can request and retrieve filtered data.

Query Strings


Right now, when a GET request is sent to http://localhost:5000/api/animals, all animals in the database are returned in JSON format. Our Get method currently looks like this:

public async Task<ActionResult<IEnumerable<Animal>>> Get()
{
  return await _db.Animals.ToListAsync();
}

What if we wanted the animals endpoint of our API to have the ability to return results that are filtered by certain search criteria? For example, say a user wanted to get all animals that are dinosaurs or all female animals? The API query in those cases would look something like this:

http://localhost:5000/api/animals?species=dinosaur
http://localhost:5000/api/animals?gender=female

We've seen this syntax for API calls before. As a reminder, the ? here represents the beginning of a query string. What follows are key value pairs that represent the search parameter and its value. We'll need to change our logic a bit to handle this request.

Handling Search Parameters


In order for us to return a filtered set of results based on species, let's edit our Get method in our controller:

Controllers/AnimalsController.cs
...
    // GET: api/Animals
    [HttpGet]
    public async Task<ActionResult<IEnumerable<Animal>>> Get(string species)
    {
      var query = _db.Animals.AsQueryable();

      if (species != null)
      {
        query = query.Where(entry => entry.Species == species);
      }

      return await query.ToListAsync();
    }
...
  • We've added a parameter to the method of type string that we've called species. The naming here is important as .NET will automatically bind parameter values based on the query string. A call to http://localhost:5000/api/animals?species=dinosaur will now trigger our Get method and automatically bind the value "dinosaur" to the variable species. The framework does this by utilizing Model Binding which we used in our MVC apps to collect information from the route or from forms to use in our controllers.

  • In the body of the method we create a variable called query and then collect the list of all animals from our database and return it as a queryable LINQ object. We return a queryable object so that we can use LINQ methods to build onto the query before finalizing our selection.

  • Then we do a check to see if there is a species parameter, and if there is, we build onto the query by calling the Where method.

  • The Where method accepts a function that will check whether each element passes the condition and does not get filtered out. In our case, we pass in entry => entry.Species == species to specify that we only want an entry if its species value matches the query parameter from our route.

  • Finally, we call ToListAsync on the final query to turn our new results into a list.

Let's test out our new species search functionality in Postman:

Result of API call with search parameter in Postman

Handling Multiple Parameters


We can now retrieve entries from the database that are of a specific species, but what if we wanted to drill down further and find all the female dinosaurs? In order to do this, we can build on the query we created and add new parameters, like so:

...
    // GET: api/Animals
    [HttpGet]
    public async Task<ActionResult<IEnumerable<Animal>>> Get(string species, string gender)
    {
      var query = _db.Animals.AsQueryable();

      if (species != null)
      {
        query = query.Where(entry => entry.Species == species);
      }

      if (gender != null)
      {
        query = query.Where(entry => entry.Gender == gender);
      }      

      return await query.ToListAsync();
    }
...

Now, we can send a GET request to http://localhost:5000/api/animals?species=dinosaur&gender=female and receive the following result:

[
    {
        "animalId": 2,
        "name": "Rexie",
        "species": "Dinosaur",
        "age": 10,
        "gender": "Female"
    },
    {
        "animalId": 3,
        "name": "Matilda",
        "species": "Dinosaur",
        "age": 2,
        "gender": "Female"
    }
]
  • Notice that we chain new parameters to our query by using the & symbol between each key-value pair.

We've successfully filtered the results with multiple parameters, but we can add as many parameters as we want with this pattern. Further, this method allows for any combination of parameters to be used in the request. Let's also add a name parameter:

...
// GET api/animals
[HttpGet]
public Task<ActionResult<IEnumerable<Animal>>> Get(string species, string gender, string name)
{
  var query = _db.Animals.AsQueryable();

  if (species != null)
  {
    query = query.Where(entry => entry.Species == species);
  }

  if (gender != null)
  {
    query = query.Where(entry => entry.Gender == gender);
  }

  if (name != null)
  {
    query = query.Where(entry => entry.Name == name);
  }

  return await query.ToListAsync();
}
...

Now we are able to search for a female dinosaur named Matilda and our API will successfully return that specific entry:

Requesting http://localhost:5000/api/animals?species=dinosaur&gender=female&name=matilda will yeild:

[
    {
        "animalId": 3,
        "name": "Matilda",
        "species": "Dinosaur",
        "age": 2,
        "gender": "Female"
    }
]

Non-string Parameters

We don't always have to filter content based on whether it matches the value in the search parameter directly. For example, If we wanted to get all dinosaurs that were older than 5 years old, it would be necessary for us to author this API endpoint to allow a request with a parameter that specifies a minimum age.

Because Model Binding in our web API works for any primitive, we can then add another parameter of int type called minimumAge and handle the logic in a similar fashion:

...
// GET api/animals
[HttpGet]
public async Task<List<Animal>> Get(string species, string gender, string name, int minimumAge)
{
  IQueryable<Animal> query = _db.Animals.AsQueryable();

  if (species != null)
  {
    query = query.Where(entry => entry.Species == species);
  }

  if (gender != null)
  {
    query = query.Where(entry => entry.Gender == gender);
  }

  if (name != null)
  {
    query = query.Where(entry => entry.Name == name);
  }

  if (minimumAge > 0)
  {
    query = query.Where(entry => entry.Age >= minimumAge);
  }

  return await query.ToListAsync();
}
...

Because intigers in C# are non-nullable data types the default for an intiger value parameter will be 0 when no minimumAge parameter is entered. So we can check minimumAge > 0 in our if statment.

Now if we request http://localhost:5000/api/animals?minimumAge=5 in postman we should get:

[
    {
        "animalId": 1,
        "name": "Matilda",
        "species": "Woolly Mammoth",
        "age": 7,
        "gender": "Female"
    },
    {
        "animalId": 2,
        "name": "Rexie",
        "species": "Dinosaur",
        "age": 10,
        "gender": "Female"
    },
    {
        "animalId": 5,
        "name": "Bartholomew",
        "species": "Dinosaur",
        "age": 22,
        "gender": "Male"
    }
]

Repository Reference

Follow the link below to view how a sample version of the project should look at this point. Note that this is a link to a specific branch in the repository.

Example GitHub Repo for Cretaceous Park

Lesson 8 of 22
Last updated April 6, 2022