Khalid Abuhakmeh had a post a while back about conditional LINQ clauses. I thought I might take the very cool extensions he created and show how we can use them in a real-world situation, namely, building a complex search engine. There will be ASP.NET Core Razor Pages, and also board games. Prepare yourselves!

This family is playing Monopoly wrong; they are way too happy. Photo by National Cancer Institute / Unsplash

The Sample Project

As always, there's a sample project over on GitHub with the complete C# code for this post. Check it out!

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

What Are Conditional LINQ Clauses?

If you've seen my Modeling Practice series, you know I'm a fan of board games. For this project, let's pretend that we're going to code up a LINQ-based search engine for various types of board games.

Oh COME ON! Now I have to go all the way around! Photo by Dave Henri / Unsplash

If you've ever looked at the site BoardGameGeek, you know that there are a LOT of different types of board games. Some games only allow four players, some allow many more, some are cooperative or competitive, some are easy to learn and play and some come with giant rulebooks that are not for the faint of heart.

Our LINQ-based search engine will need to be able to search on many different properties of board games. However, we want to allow the engine to only search on properties the user selects. For example, the user might want to search by recommended age and game type (e.g. strategy or party games), or by maximum number of players, or by cooperative vs competitive, or by any combination thereof!

As I mentioned at the top, I originally saw the conditional LINQ clauses on Khalid Abuhakmeh's blog; I ported his code over to my sample project. The extension methods he published look like this:

public static class LINQExtensions
{
    public static IQueryable<T> If<T>(
        this IQueryable<T> query,
        bool should,
        params Func<IQueryable<T>, IQueryable<T>>[] transforms)
    {
        return should
            ? transforms.Aggregate(query,
                (current, transform) => transform.Invoke(current))
            : query;
    }

    public static IEnumerable<T> If<T>(
        this IEnumerable<T> query,
        bool should,
        params Func<IEnumerable<T>, IEnumerable<T>>[] transforms)
    {
        return should
            ? transforms.Aggregate(query,
                (current, transform) => transform.Invoke(current))
            : query;
    }
}

These extension methods are meant to conditionally apply LINQ clauses if a boolean is set to true. For example, you might use them like this:

var query = items.Where(x => x.SomeProperty > someValue);

query = query.If(searchByOtherProperty, q => q.Where(x => x.OtherProperty == otherValue);

The query will then be modified so that if searchByOtherProperty is true, the query will return items where OtherProperty equals a certain value.

We are going to use these conditional LINQ clauses to build our board game search engine. But first, we need to do a little setup.

The Setup

Let's start our modeling with a class and a couple of enumerations, like this:

public class BoardGame
{
    public string Name { get; set; }
    public int MaxPlayers { get; set; }
    public GameType GameType { get; set; }
    public int AverageGameTimeMinutes { get; set; }
    public int SuggestedMinimumAge { get; set; }
    public PlayType PlayType { get; set; }

    public BoardGame(string name, int maxPlayers, GameType type, 
                     int minutes, int age, PlayType playtype)
    {
        Name = name;
        MaxPlayers = maxPlayers;
        GameType = type;
        AverageGameTimeMinutes = minutes;
        SuggestedMinimumAge = age;
        PlayType = playtype;
    }
}

public enum GameType
{
    //Players play individiually, competing against other players
    Competitive,

    //Players play in teams, competing against other teams
    CompetitiveTeams,

    //All players work together to acheive a goal.
    Cooperative
}

public enum PlayType
{
    Party,
    Strategy,
    Childrens,
    Abstract
}

We can also instantiate a whole list of board games to act as our data store:

public class BoardGameRepository
{
    public List<BoardGame> GetAll()
    {
        return new List<BoardGame>()
        {
            new BoardGame("Catan", 4, GameType.Competitive, 
                          45, 10, PlayType.Strategy),
            new BoardGame("Candy Land", 4, GameType.Competitive, 
                          30, 3, PlayType.Childrens),
            new BoardGame("Battleship", 2, GameType.Competitive, 
                          30, 8, PlayType.Childrens),
            new BoardGame("Pandemic", 4, GameType.Cooperative, 
                          45, 8, PlayType.Strategy),
            new BoardGame("Ticket to Ride", 4, GameType.Competitive, 
                          30, 8, PlayType.Strategy),
            new BoardGame("Gloomhaven", 4, GameType.Cooperative, 
                          60, 12, PlayType.Strategy),
            new BoardGame("Time's Up! Title Recall", 18, GameType.CompetitiveTeams, 
                          60, 12, PlayType.Party),
            new BoardGame("Cards Against Humanity", 30, GameType.Competitive, 
                          30, 17, PlayType.Party),
            new BoardGame("Azul", 4, GameType.Competitive, 
                          30, 8, PlayType.Abstract),
            new BoardGame("Go", 2, GameType.Competitive, 
                          75, 8, PlayType.Abstract),
            new BoardGame("Codenames", 8, GameType.Competitive, 
                          15, 14, PlayType.Party),
            new BoardGame("Robinson Crusoe", 4, GameType.Cooperative, 
                          90, 14, PlayType.Strategy),
            new BoardGame("Taboo", 10, GameType.CompetitiveTeams, 
                          20, 12, PlayType.Party)
        };
    }
}
Originally I had "Year Published" in here. Guess what it was for Go.

Now we need to build the page to allow the user to search our board game data store.

The Front-End

We have five properties of board games that our users can search by:

  • Max Number of Players
  • Minimum Age
  • Game Type (e.g. Competitive or Cooperative)
  • Average Play Time
  • Play Type (e.g. Strategy, Children's, Abstract, or Party)

We not only need the values of these properties that are being searched for, but also properties that say whether or not the former properties are even being used in the search. All of this results in a rather large page model

public class IndexModel : PageModel
{
    //The Board Games found by the current search
    public List<BoardGame> Results { get; set; } = new List<BoardGame>();

    [BindProperty]
    public GameType SelectedGameType { get; set; }

    [BindProperty]
    [DisplayName("Search by Game Type")]
    public bool SearchByGameType { get; set; }

    [BindProperty]
    public int SelectedMinAge { get; set; }

    [BindProperty]
    [DisplayName("Search by Minimum Age")]
    public bool SearchByMinAge { get; set; }

    [BindProperty]
    public int SelectedPlayTime { get; set; }

    [BindProperty]
    [DisplayName("Search by Play Time")]
    public bool SearchByPlayTime { get; set; }

    [BindProperty]
    public int SelectedMaxPlayers { get; set; }

    [BindProperty]
    [DisplayName("Search by Max Players")]
    public bool SearchByMaxPlayers { get; set; }

    [BindProperty]
    public PlayType SelectedPlayType { get; set; }

    [BindProperty]
    [DisplayName("Search by Play Type")]
    public bool SearchByPlayType { get; set; }

    public IndexModel() {}

    public void OnGet() {}

    public void OnPost() { /*To Be Implemented*/ }
}

We also need the corresponding HTML on the page itself (example is shortened for brevity):

<h1>Board Game Search</h1>

<form method="post">
    <div class="form-row">
        <div class="col-md-4">
            <input type="checkbox" asp-for="SearchByGameType" />
            <label asp-for="SearchByGameType"></label>

            <select class="form-control @(Model.SearchByGameType ? "" : "d-none")" asp-for="SelectedGameType" asp-items="Html.GetEnumSelectList<GameType>()"></select>
        </div>
        <div class="col-md-4">
            <input type="checkbox" asp-for="SearchByMinAge" />
            <label asp-for="SearchByMinAge"></label>

            <select class="form-control @(Model.SearchByMinAge ? "" : "d-none")" asp-for="SelectedMinAge">
                <option value="3">3+</option>
                <option value="8">8+</option>
                <option value="10">10+</option>
                <option value="12">12+</option>
                <option value="17">17+</option>
            </select>
        </div>
        <div class="col-md-4">
            <input type="checkbox" asp-for="SearchByPlayTime" />
            <label asp-for="SearchByPlayTime"></label>

            <select class="form-control @(Model.SearchByPlayTime ? "" : "d-none")" asp-for="SelectedPlayTime">
                <option value="15">15+ minutes</option>
                <option value="30">30+ minutes</option>
                <option value="45">45+ minutes</option>
                <option value="60">60+ minutes</option>
                <option value="90">90+ minutes</option>
            </select>
        </div>
    </div>
    <div class="form-row">
        <div class="col-md-6">
            <input type="checkbox" asp-for="SearchByMaxPlayers" />
            <label asp-for="SearchByMaxPlayers"></label>

            <select class="form-control @(Model.SearchByMaxPlayers ? "" : "d-none")" asp-for="SelectedMaxPlayers">
                <option value="2">2 players</option>
                <option value="4">4 players</option>
                <option value="8">8 players</option>
                <option value="18">18 players</option>
                <option value="30">30 players</option>
            </select>
        </div>
        <div class="col-md-6">
            <input type="checkbox" asp-for="SearchByPlayType" />
            <label asp-for="SearchByPlayType"></label>

            <select class="form-control @(Model.SearchByPlayType ? "" : "d-none")" asp-for="SelectedPlayType" asp-items="Html.GetEnumSelectList<PlayType>()"></select>
        </div>
    </div>
    <div class="form-row">
        <button type="submit" class="btn btn-primary btn-block align-middle">Search</button>
    </div>
</form>
If you don't recognize the asp-for properties, read up on Tag Helpers

Finally, we are using some simple jQuery to ensure that search fields are only shown when they are needed:

@section Scripts
{
    <script type="text/javascript">
        $("#SearchByGameType").change(function () {
            if ($("#SearchByGameType").prop("checked")) {
                $("#SelectedGameType").removeClass("d-none");
            }
            else {
                $("#SelectedGameType").addClass("d-none");
            }
        });

        //Other fields are implemented in the same manner
    </script>
}

But all of this is just to get to the real meat of this post: How do we use conditional LINQ clauses to implement the actual search method?

Since we have properties that represent whether or not a particular search field is included, and the search field's value, we can chain together the conditional LINQ query extensions we defined earlier in the Razor Page's model, like this:

public void OnPost()
{
    BoardGameRepository repo = new BoardGameRepository();
    Results = repo.GetAll();

    Results = Results.If(SearchByGameType, 
                         q => q.Where(x => x.GameType == SelectedGameType))
                     .If(SearchByMinAge, 
                         q => q.Where(x => x.SuggestedMinimumAge 
                                            >= SelectedMinAge))
                     .If(SearchByPlayTime, 
                         q => q.Where(x => x.AverageGameTimeMinutes 
                                            >= SelectedPlayTime))
                     .If(SearchByMaxPlayers, 
                         q => q.Where(x => x.MaxPlayers == SelectedMaxPlayers))
                     .If(SearchByPlayType, 
                         q => q.Where(x => x.PlayType == SelectedPlayType))
                     .ToList();
}

You could just as well implement this using a bunch of if() statements, but this is much cleaner and, in my opinion, easier to read.

GIF Time!

To demonstrate how this works in the browser, here's a GIF:

Summary

Using conditional LINQ clauses, we can generate a complex search feature that allows our users to choose what they want to search by. Said feature is done by having properties in our page model that specify whether or not a specific search field is being used, and by chaining conditional LINQ to implement the actual search.

It was a massacre! Photo by JESHOOTS.COM / Unsplash

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

Did you enjoy this post? I would really appreciate your support! Check out my Buy Me a Coffee page to support Exception Not Found and all my projects!

Matthew Jones
Iā€™m a .NET developer, blogger, speaker, husband and father who just really enjoys the teaching/learning cycle.One of the things I try to do differently with Exception...

Happy Coding!