CHAPTER 5
Up until this point in the book, we have focused mainly on local queries. Local queries are those that operate on IEnumerable<T> sequences. Local queries result in the query operators that are defined in the System.Linq.Enumerable class being executed. In this way, executing local queries results in specific code running (from the query operators in the Enumerable class) as defined at compile time.
Interpreted queries, on the other hand, describe the “shape” of the query that instead is interpreted at runtime—hence the name “interpreted.” Interpreted queries operate on IQueryable<T> sequences, and when writing LINQ queries, the query operators resolve to methods defined in the System.Linq.Queryable class rather than the System.Linq.Enumerable class as with local queries.
Another way to think about the difference between local (IEnumerable<T>) queries and interpreted (IQueryable<T>) queries is that the local query operators contain the actual query implementation code that gets executed, whereas the interpreted query operators do not. With interpreted queries, the actual query code that gets executed and returns some result is defined in a query provider. The query provider is passed the description of the query that needs to be executed; it executes the query (for example, executing some SQL against a database) and returns the result.
The query operators that work on IQueryable<T> differ from their equivalent versions for local queries.
Local (IEnumerable<T>) versions of query operators take delegates as parameters (often described using a lambda expression). In contrast, interpreted queries take expression trees as parameters.
Because C# will implicitly convert a lambda expression when used as a query operator parameter to an expression, the same lambda expression can be used regardless of whether the query operator is a local or remote one. The following code shows both the local (IEnumerable<T>) version of the Where query operator and the interpreted (IQueryable<T>) version being used. Notice that the lambda expression is the same in both cases.
IQueryable<int> interpretedQuery = remoteData.Where(x => x > 100); IEnumerable<int> localQuery = localData.Where(x => x > 100); |
The following code shows the method signatures for the Where query operator for both the local and interpreted versions.
// Local version of Where from System.Linq.Enumerable public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate) |
Notice in the preceding code that the local version takes a predicate parameter of type Func<TSource, bool>. Contrast this with the interpreted version where the predicate parameter type is Expression<Func<TSource, bool>>. This is the expression tree that the query provider can turn into some meaningful code execution, such as generating an equivalent SQL statement and executing against a database.
Because IQueryable<T> is a subtype of IEnumerable<T>, the compiler has to choose whether to use the local or remote version of a give query operator. The compiler rules dictate that the more specific version will be chosen. Because IQueryable<T> is more specific than IEnumerable<T>, the compiler will choose the interpreted versions of the query operators (as defined in System.Linq.Queryable) when dealing with interpreted queries, and local versions of query operators (as defined in System.Linq.Enumerable) when dealing with local queries. This fact (and C#’s implicit conversion of lambda expressions to Expression<T>) means that LINQ queries can provide a unified API regardless of whether the query is a local one or a remote interpreted query.
Note: Not all of the standard query operators will be supported by all interpreted query providers.
Microsoft™ has provided pre-built query providers that operate with databases, such as the older LINQ to SQL and the newer Entity Framework.
There are also numerous third party or open source query providers including:
· LINQ to Twitter
· LINQ to flickr
· LINQ to JSON
These examples serve to demonstrate the powerful and flexible nature of LINQ interpreted queries. Remote data stores of different types can be queried using a common set of LINQ query operators.
In addition to LINQ providers built by others, it is also possible to create them ourselves, though the work required may be quite involved.
As an example of using LINQ to query a database using Entity Framework, imagine a SQL Server database that contains the following entities (tables).
public class Recipe { public int ID { get; set; } public string Name { get; set; } public virtual ICollection<Review> Reviews { get; set; } } { public int ID { get; set; } public int RecipeID { get; set; } public string ReviewText { get; set; } public virtual Recipe Recipe { get; set; } } |
In the preceding code, the model represents that a recipe can have multiple reviews.
Tip: To learn more about using Entity Framework, be sure to check out the Entity Framework Code First Succinctly eBook from Syncfusion.
Now that the entities are defined, a data context class can be defined, as the following code demonstrates.
public class RecipeDbContext : DbContext { public RecipeDbContext() : base ("RecipeDbConnectionString") { } public DbSet<Recipe> Recipes { get; set; } public DbSet<Review> Reviews { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Conventions.Remove<PluralizingTableNameConvention>(); } } |
A database initializer is created to always recreate the database and populate it with some initial demo data, as the following code shows.
public class RecipeDbInitializer : { protected override void Seed(RecipeDbContext context) { context.Recipes.Add(new Recipe { Name = "Cherry Pie" }); context.Recipes.Add(new Recipe {Name = "Apple Pie"}); context.SaveChanges(); context.Reviews.Add(new Review {ReviewText = "Quite nice", context.Reviews.Add(new Review {ReviewText = "I hate cherries!", context.Reviews.Add(new Review {ReviewText = "Ok", context.Reviews.Add(new Review {ReviewText = "Not too bad", context.SaveChanges(); } } |
Now that there is an EF model and a database containing some demo data, it can be queried using LINQ via the RecipeDbContext. The following code shows how to use fluent style syntax to get all the recipes sorted by the recipe name.
RecipeDbContext ctx = new RecipeDbContext(); IQueryable<Recipe> allRecipies = ctx.Recipes .OrderBy(x => x.Name); foreach (var recipe in allRecipies) { Console.WriteLine(recipe.Name); } |
This produces the following output:
Apple Pie
Cherry Pie
Just as with local queries, query expression syntax can also be used with interpreted queries.
The following code shows the combination of using query expression syntax against an Entity Framework model and projecting into an X-DOM using functional construction.
RecipeDbContext ctx = new RecipeDbContext(); XElement xml = new XElement("recipes", from rec in ctx.Recipes.Include(x => x.Reviews).ToList() select new XElement("recipe", new XAttribute("name", rec.Name), new XAttribute("id", rec.ID), new XElement("reviews", from rev in rec.Reviews select new XElement("review", rev.ReviewText)) ));
Console.WriteLine(xml); |
This produces the following output:
<recipes>
<recipe name="Cherry Pie" id="1">
<reviews>
<review>Quite nice</review>
<review>I hate cherries!</review>
<review>Ok</review>
</reviews>
</recipe>
<recipe name="Apple Pie" id="2">
<reviews>
<review>Not too bad</review>
</reviews>
</recipe>
</recipes>
In the preceding code, there are a couple of noteworthy items. The first is the Entity Framework Include extension method. This instructs Entity Framework to retrieve all the related reviews for each of the recipes. Without this, Entity Framework will end up submitting multiple select queries to the database, rather than just a single SELECT.
The second item of note is the explicit call to ToList(). This is required to retrieve the results into local entity objects before the X-DOM projection takes place. Without this, an exception will be thrown: “System.NotSupportedException : Only parameterless constructors and initializers are supported in LINQ to Entities.” This is because XElement does not have a default parameterless constructor defined.
Other than these Entity Framework specific items, the LINQ query follows the same pattern and syntax it would if we were working with a local data.