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!
The Sample Project
As always, there's a sample project over on GitHub with the complete C# code for this post. Check it out!
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.
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:
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):
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?
Implementing the Search
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.
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!
Happy Coding!