Generics, expressions, and extension methods are amazing
features that open the doors to incredible new features and abilities in C#
(and .NET in general).
Quick Summary
Using C# extension methods, we will rewrite commonly used Linq "where" clauses into filter methods that we can re-use in our code. We will turn
"DbSet.Where(v=> v..EffectiveFrom < DateTime.Now && v.EffectiveTo > DateTime.Now)"
into "DbSet.WhereActive()" and better satisfy the Don't Repeat Yourself (DRY) principle.
"DbSet.Where(v=> v..EffectiveFrom < DateTime.Now && v.EffectiveTo > DateTime.Now)"
into "DbSet.WhereActive()" and better satisfy the Don't Repeat Yourself (DRY) principle.
Introduction
Using generics, expressions, and extension methods we can build reusable and testable Linq filters. In this post, we are going to demonstrate how to create a Linq filter using C# extension methods.
We will be using the method syntax as opposed to the query syntax as that makes it easier to visualize the queryable as a pipeline that resembles "from -> where -> orderby -> select".
A simple Linq query consists of those 4 parts; from, where, orderby, and select. The "from" clause specifies the data source, the "where" clause can optionally limit the results, the "orderby" clause optionally sorts the results, and the "select" clause optionally projects the results. Our projection matches the default projection and just outputs the same type of object as DbSet is. Since it matches the default, this clause is optional in this case.
A simple Linq query consists of those 4 parts; from, where, orderby, and select. The "from" clause specifies the data source, the "where" clause can optionally limit the results, the "orderby" clause optionally sorts the results, and the "select" clause optionally projects the results. Our projection matches the default projection and just outputs the same type of object as DbSet is. Since it matches the default, this clause is optional in this case.
Our filters are specifically about replacing "where" clauses with our filter methods. There are some interesting things we can do with "orderby" and "select" but we will leave that as a subject for another day.
In projects, we commonly see certain clauses used
repeatedly. One such clause checks if an
object is active. In several of our
tables, "active" is defined as "the datetime value in the
EffectiveFrom column is less than the current time and the datetime value in
the EffectiveTo column is greater than the current time." Rather than repeating this clause over and
over, we have created an WhereActive() extension method that we can drop into any
query against this object type.
Initial Query
Our example DbSet is a collection of "StatusCode" objects and the simple query we are converting to use our filter looks like this
DbSet .Where(v => v.EffectiveFrom < DateTime.Now && v.EffectiveTo > DateTime.Now) .OrderBy(v => v.Id) .Select(v => v)
About Extension Methods
In order to implement that clause as an extention method, we
first need an extension methods class. (Additional reading on extension methods at Scott Hanselman's blog) Like any extension methods class, this class must be a static
class. It doesn't matter what the class is called but when you want to use that extension method anywhere in your project, that class must be in the same project or in the references and the namespace of your extension methods class must be in a using statement in the file your use the extensions in.
Create an Extension Method to Implement our WhereActive filter
Now we will implement an extension method that extends IQueryable<StatusCode> and returns IQueryable<StatusCode>. This gives us a method that we can put next to any existing query to produce a result that has only active values. For example, where “DbSet” would return all values from a table, “DbSet.WhereActive()” would return only the active values.namespace Project.Extensions { public static class FilterExtensions { public static IQueryable<StatusCode> WhereActive(this IQueryable<StatusCode> query) { return query.Where(v => v.EffectiveFrom < DateTime.Now && v.EffectiveTo > DateTime.Now); } } }
Rewrite our query
At this point, we can re-write the above query using our filter.DbSet .WhereActive() .OrderBy(v => v.Id) .Select(v => v)
This is the same exact query as our original one but it is easier to read and debug and it allows us to put the definition for what it means to be "Active" in a single place in the code, satisfying our need to avoid repeating ourselves.
It also makes it easy to change our definition of what "Active" means. If we later decide that “active” is defined as “has the active flag set,” we only have to rewrite the “WhereActive” filter to implement this change.
Add Another Filter
Now I can add another filter implement another commonly used clause. We have defined "Approved" for this entity as “ApprovalDate column is set to a non-null value.” As you can see, this is not something that a later developer can easily see by looking at the code or the data model. Our filter, however, makes that definition much more obvious.public static IQueryable<StatusCode> WhereApproved(this IQueryable<StatusCode> query) { return query.Where(v => v.ApprovalDate != null); }
Now our code can execute a query consisting of
A later developer looking at this code can more easily understand what our result set should include and why. It should be apparent looking at this code that we want all StatusCode objects that are currently active and approved.
DbSet .WhereActive() .WhereApproved() .OrderBy(v => v.Id) .Select(v => v)
A later developer looking at this code can more easily understand what our result set should include and why. It should be apparent looking at this code that we want all StatusCode objects that are currently active and approved.
Summary
We looked at creating filter methods that turns commonly used where clauses into filter functions that we can re-use within our code. We demonstrated how to build them and showed what the Linq query looks like before and after switching to filter functions.
Next time we will look at how we can write simple unit tests to verify that our filter methods are operating as intended.