In this post I’m going to be taking a look at query strings and specifically how we can work with them in Blazor, because at this point in time, Blazor doesn’t give us any tools out of the box. In fact, Blazor pretty much ignores them.
We’re going to start off by covering what query strings are and why we’d want to use them over route parameters. Then we’ll get into some code and I’ll show you a couple of options which should make working with them in your Blazor applications much easier.
Example Code: A sample project to accompany this blog post can be found on GitHub
What are query strings?
Query strings are essentially an instance or collection of key value pairs encoded into a URL. Below is an example of what a query string looks like.
mysite.com/about?name=Chris&favouritecolor=orange
The start of a query string is separated from the rest of the URL by a ?
. Then comes the key value pairs, each key and value is separated by a =
. If there’s more than one pair a &
is used to separate them.
In the example above, the query string contains two pairs, name
with a value of Chris
and favouritecolour
with a value of orange
.
One of the original use cases for query strings was to hold form data. When a form was submitted the field names and their values were encoded into the URL as query string values.
Why use them over route parameters?
A good question, for me, query strings provide a more flexibility over route parameters when it comes to optional values. Having optional parameters in a route can be a real pain if not impossible, in my experience at least. A good example of this is when using filters on a list page.
Let’s pretend we have a page listing cars (/carsearch
) and we offer the user the ability to filter that list by make, model, and colour. If we wanted to use route parameters we’d have to use multiple route templates.
@page "/carsearch"
@page "/carsearch/{make}"
@page "/carsearch/{make}/{model}"
@page "/carsearch/{make}/{model}/{colour}"
The problem is what happens if the user only selected make and colour? The route would look like this /carsearch/ford/blue
. Now we have a problem, the router is going to find a match with the 3rd template we’ve defined, @page "/carsearch/{make}/{model}"
. So we’d be trying to find all cars with a make of ford
and a model of blue
, oh dear.
Now, we could work round this but using defaults for the various filters but it would be much simplier to use query strings instead.
Query strings don’t care about order of values, or even if a value is present or not, we don’t even need to define them in a route template. Which means we can go back to using a single route template for our car search page.
@page "/carsearch"
And when the user wants to filter we can just add the selected criteria to the URL using query strings.
/carsearch?make=ford&colour=blue
The only problem now is how do we actually get hold of and use the values in our query string?
Introducing WebUtilites
There is a library called Microsoft.AspNetCore.WebUtilities
which contains a fantastic helpers for dealing with query strings. It will chop up a query string for us and allow us to retrieve values in a straightforward way, meaning we don’t have to get into loads of string manipulation.
We’re going to update the Counter page in the default template to look for a query string which sets the initial count for the counter. So given the url /counter?initialCount=10
, we’d expect the counter to start at 10. Here’s how we can achieve that.
@page "/counter"
@inject NavigationManager NavManager
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
int currentCount = 0;
protected override void OnInitialized()
{
var uri = NavManager.ToAbsoluteUri(NavManager.Uri);
if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("initialCount", out var _initialCount))
{
currentCount = Convert.ToInt32(_initialCount);
}
}
void IncrementCount()
{
currentCount++;
}
}
We can inject the NavigationManager
which gives us access to the current URI. Once we have that, we can pass the query string (uri.Query
) to the WebUtilities
helper and ask for the value of initialCount
. The last thing we need to do is convert the value of initialCount
to an int
as all values are returned as string
s by the helper.
That wasn’t actually too bad and if we just needed to do this on one page then we’re now sorted. However, if we needed to use query strings in multiple places in our app this becomes a lot of boilerplate to have kicking around. So let’s make this better.
Moving to an Extension Method
We can encapsulate all this functionality into a extension method on the NavigationManager
class. This should make working with query strings all over our app trivial going forward. Here’s the code.
public static class NavigationManagerExtensions
{
public static bool TryGetQueryString<T>(this NavigationManager navManager, string key, out T value)
{
var uri = navManager.ToAbsoluteUri(navManager.Uri);
if (QueryHelpers.ParseQuery(uri.Query).TryGetValue(key, out var valueFromQueryString))
{
if (typeof(T) == typeof(int) && int.TryParse(valueFromQueryString, out var valueAsInt))
{
value = (T)(object)valueAsInt;
return true;
}
if (typeof(T) == typeof(string))
{
value = (T)(object)valueFromQueryString.ToString();
return true;
}
if (typeof(T) == typeof(decimal) && decimal.TryParse(valueFromQueryString, out var valueAsDecimal))
{
value = (T)(object)valueAsDecimal;
return true;
}
}
value = default;
return false;
}
}
A lot of the code above is the same as we just saw, except I’ve used some generics to allow the caller to specify the type they want the requested value to be converted to. I’ve then added a some checks to covert values to string
int
or decimal
you could add whatever other ones you wish.
If we refactor our counter page to use our new extension method this is what we end up with.
@page "/counter"
@inject NavigationManager NavManager
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
int currentCount = 0;
protected override void OnInitialized()
{
NavManager.TryGetQueryString<int>("initialCount", out currentCount);
}
void IncrementCount()
{
currentCount++;
}
}
That now looks much cleaner and we’ve got all our query string code in one place which is great for maintainability.
Dealing with updates to query string values
One last scenario I want to cover is how to react to updates in query string values. Lets add a few links to our counter page which set the counter to different initial values, say 10, 20 and 30.
The problem we have no is that when we click any of these links Blazor isn’t going to call the OnInitialized
life cycle method again as we are already on the correct componet for the route. So how can we react to the new query string value? It turns out the NavigationManager.LocationChanged
event still fires, so we can setup a handler for that event which will retrieve the new values.
@page "/counter"
@implement IDisposable
@inject NavigationManager NavManager
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
<hr />
<a href="/Counter?initialCount=10">Start counter at 10.</a> |
<a href="/Counter?initialCount=20">Start counter at 20.</a> |
<a href="/Counter?initialCount=30">Start counter at 30.</a>
@code {
int currentCount = 0;
protected override void OnInitialized()
{
GetQueryStringValues();
NavManager.LocationChanged += HandleLocationChanged;
}
void HandleLocationChanged(object sender, LocationChangedEventArgs e)
{
GetQueryStringValues();
StateHasChanged();
}
void GetQueryStringValues()
{
NavManager.TryGetQueryString<int>("initialCount", out currentCount);
}
void IncrementCount()
{
currentCount++;
}
public void Dispose()
{
NavManager.LocationChanged -= HandleLocationChanged;
}
}
I think that wraps things up. We can now easily access query string values from any of our components both on initial load and when the URL is updated.
Summary
In this post I talked about working with query strings in Blazor. I started off by describing what query strings are and why you would choose to use them over route parameters.
I then suggested some options on how to achieve this in Blazor. I started off with a simple solution based on the Microsoft.AspNetCore.WebUtilities
library. I then developed that into an extension method for the NavigationManager
class to avoid code duplication and ease use and maintainability.