I've written before about implementing a pattern known as POST-REDIRECT-GET in my ASP.NET MVC apps, and since my team and I are working on a new ASP.NET Core Razor Pages project I thought it might be useful to see if such a pattern can be implemented there.

Don't stare; you might see things you wish you hadn't. Like F#. Photo by Paweł Czerwiński / Unsplash

Turns out, it can, and it's much simpler to implement PRG in Razor Pages than it was in MVC. Let's see how, and why should want to use it in our apps!

The Sample App

As always with my code-heavy posts, there is a sample application on GitHub that demonstrates the ideas herein. Check it out!

exceptionnotfound/RazorPagesPRGDemo
Contribute to exceptionnotfound/RazorPagesPRGDemo development by creating an account on GitHub.

Background

Our sample application allows us to query for maps of countries, including when they were published. Here's the C# class for a map, and the enum Country:

public class Map
{
    public int ID { get; set; }
    public Country Country { get; set; }
    public DateTime PublicationDate { get; set; }
}

public enum Country
{
    UnitedStates,
    Canada,
    Mexico,
    India,
    UnitedKingdom,
    Germany,
    Russia,
    China,
    Japan,
    Serbia,
    Nigeria,
    Sudan,
    Australia
}

What we want is to be able to search for maps of a particular country that were published in a selected time frame. Our search page might look like this:

We must have either a selected country or a selected range (e.g. both start and end dates) in order to do a search. The actual search code is in a class called MapRepository, which looks like this:

public List<Map> Search(Country? country, DateTime? startRange, DateTime? endRange)
{
    var allMaps = GetAll().AsQueryable();

    if(country.HasValue)
    {
        allMaps = allMaps.Where(x => x.Country == country);
    }
    if(startRange.HasValue && endRange.HasValue)
    {
        allMaps = allMaps.Where(x => x.PublicationDate >= startRange.Value && x.PublicationDate <= endRange.Value);
    }

    return allMaps.ToList();
}

Non-PRG Solution

First, let's see how we might implement this search without using the POST-REDIRECT-GET pattern.

To do this, we will call the repository which searches the database on the POST action instead of the GET, which results in code that looks like this:

public class SearchOldModel : PageModel
{
    private readonly IMapRepository _mapRepo;

    public SearchOldModel(IMapRepository mapRepo)
    {
        _mapRepo = mapRepo;
    }

    [BindProperty]
    public Country? SelectedCountry { get; set; }

    [BindProperty]
    public DateTime? StartRange { get; set; }

    [BindProperty]
    public DateTime? EndRange { get; set; }

    public List<Map> Results { get; set; }
    public bool HasSearch { get; set; }

    public void OnGet() { }

    public void OnPost()
    {
        if(SelectedCountry.HasValue || (StartRange.HasValue && EndRange.HasValue))
        {
            HasSearch = true;
            Results = _mapRepo.Search(SelectedCountry, StartRange, EndRange);
        }
    }
}

If the user performs a valid search, they get results, just like we would expect:

However, when they try to refresh the page, they get this:

This is not a great user experience. For one, it "breaks" the back button; once a search is performed if the user presses the back button that popup will appear. For another thing, you cannot send a URL of search results to someone else; anyone who wants to do the same search you did will have to come to this page and manually perform that search.

We can improve this by implementing the POST-REDIRECT-GET pattern, slightly modified for ASP.NET Core Razor Pages.

The POST-REDIRECT-GET Pattern

In order to implement PRG in our Razor Pages app, we make the search parameters inputs to our OnGet() method, and only do a redirect in OnPost(). The final code looks something like this:

public class SearchPRGModel : PageModel
{
    //Properties and constructor removed for brevity

    public void OnGet(Country? country, DateTime? startRange, DateTime? endRange)
    {
        SelectedCountry = country;
        StartRange = startRange;
        EndRange = endRange;

        if(country.HasValue || (startRange.HasValue && endRange.HasValue))
        {
            HasSearch = true;
            Results = _mapRepo.Search(country, startRange, endRange);
        }
    }

    public IActionResult OnPost()
    {
        return RedirectToPage("/SearchPRG", new { country = SelectedCountry, 
                                                startRange = StartRange, 
                                                endRange = EndRange });
    }
}

As before, doing a search gives us results:

But now, clicking the back button actually takes us back to the page before we did the search. Further, the URL for this results screen now includes a query string, which may look like this

/?country=UnitedStates

This means that we can now send this search to other people as a URL, and when they click it, the search will be performed again with no input from them. Overall, the PRG pattern results in an improved experience for our users.

The key part of the PRG pattern is that the inputs to the search need to be parameters to theOnGet() function, and the OnPost() function must redirect back to the same page using RedirectToPage().

Summary

We can remove the annoying "Confirm Form Resubmission" popups and make our user experience just that much better by using the POST-REDIRECT-GET (PRG) pattern in ASP.NET Core and Razor Pages. Remember the following in order to implement PRG:

  • Make the parameters to the search inputs to the OnGet() function.
  • Ensure the OnPost() function redirects back to the search page using RedirectToPage().

Don't forget to check out the sample project over on GitHub!

Happy Coding!