Article summary
To use Entity Framework is to be frustrated with Entity Framework. If you’ve ever used .NET Core’s Entity Framework (EF) you’ll likely resonate with that statement. One area that can really trip developers up when starting with EF is the idea of LINQ queries. At first glance, LINQ seems quite simple, as it’s basically a library of operations that can be executed on data structures that implement the appropriate interfaces, such as IEnumerable
.
However, once you take this relatively easy-to-understand concept and apply it to entities in EF, things get dicey very quickly. Today we’re going to look at a simplified real-world case. I ran across something like this recently where the way this all behaves really threw a wrench in the works and prevented me from expressing my ideas as code in a succinct way.
The Problem
I’ve changed the context of things slightly here for brevity and the client’s anonymity. One key piece of context here is that we must consider all these contrived entity types “Third Party.” That is to say, they are coming from a NuGet package and therefore cannot be easily modified to suit our needs. That being said let’s set up a simple code refactoring problem. I have a class responsible for reading from a database that stores blog posts:
class PostDataService {
public List<BlogPost> GetBlogPostsUsingFilters(Filters filters) {
// Do some set up stuff...
var query = dbContext.BlogPosts.AsQueryable()
// Do the complex translation of a filter to a LINQ query
var fullQuery = BuildQueryFromFiltersForBlogPost(query, filters);
// run the query
return fullQuery.ToList();
}
private BuildQueryFromFiltersForBlogPost(IQueryable<BlogPost> query, Filters filters) {
// This function iterates over the filters and maps them to LINQ query Expressions
}
}
Now let’s say after several months the clients want to integrate another feature into the application, news articles! This new type has a lot of similar business logic needed but very different field names. How are we going to accomplish this? Well, let’s start with the simplest possible solution. We’ll use a favorite tool of time-constrained developers all over globe, copying code and modifying it just a little bit:
class PostDataService {
public List<BlogPost> GetBlogPostsUsingFilters(Filters filters) {
// Do some set up stuff...
var query = dbContext.BlogPosts.AsQueryable()
// Do the complex translation of a filter to a LINQ query
var fullQuery = BuildQueryFromFiltersForBlogPost(query, filters);
// run the query
return fullQuery.ToList();
}
// This function iterates over the filters and maps them to LINQ query Expressions
private BuildQueryFromFiltersForBlogPost(IQueryable<NewsArticle> query, Filters filters) {
foreach(var filter in filters) {
// Do some fancy stuff, ultimately adding a LINQ Expression to a list of expressions that get rolled up into something we can add to our IQueryable query.
var expressionToAdd = (entity) => entity.BlogPostId != 0; // Contrived example of data access
}
}
public List<NewsArticle> GetNewsArticlesUsingFilters(Filters filters) {
// Do some set up stuff...
var query = dbContext.NewsArticles.AsQueryable()
// Do the complex translation of a filter to a LINQ query
var fullQuery = BuildQueryFromFiltersForNewsArticle(query, filters);
// run the query
return fullQuery.ToList();
}
private BuildQueryFromFiltersForNewsArticle(IQueryable<NewsArticle> query, Filters filters) {
// This does the same thing as the function for BlogPosts, but uses different field names to access the necessary data for each filter
var expressionToAdd = (entity) => entity.NewsArticleId != 0; // Contrived example of data access
}
}
My First Solution
So obviously we have a problem here, both BuildQueryFromFiltersForBlogPost
and BuildQueryFromFiltersForNewsArticle
are nearly identical functions that express the same complex logic with the only subtle difference being they use different field names from these different entities. My first pass at this was to combine the complex logic into a generic method and use helper methods and pattern matching to inspect the type being used for the generic and pass back the needed data from the entity. Here’s how that looked:
class PostDataService {
public List<BlogPost> GetBlogPostsUsingFilters(Filters filters) {
// Do some set up stuff...
var query = dbContext.BlogPosts.AsQueryable()
// Do the complex translation of a filter to a LINQ query
var fullQuery = BuildQueryFromFilters<BlogPost>(query, filters);
// run the query
return fullQuery.ToList();
}
public List<NewsArticle> GetNewsArticlesUsingFilters(Filters filters) {
// Do some set up stuff...
var query = dbContext.NewsArticles.AsQueryable()
// Do the complex translation of a filter to a LINQ query
var fullQuery = BuildQueryFromFilters<NewsArticle>(query, filters);
// run the query
return fullQuery.ToList();
}
// This function iterates over the filters and maps them to LINQ query Expressions
private BuildQueryFromFilters<T>(IQueryable<T> query, Filters filters) {
foreach(var filter in filters) {
// Do some fancy stuff, ultimately adding a LINQ Expression to a list of expressions that get rolled up into something we can add to our IQueryable query.
var expressionToAdd = (entity) => GetId(entity) != 0;
}
}
private long GetId<T>(T entity) {
return entity switch
{
BlogPost blogPost => blogPost.BlogPostId,
NewsArticle newsArticle => newsArticle.NewsArticleId,
_ => throw new NotSupportedException("Unsupported entity for GetId")
};
}
}
Much to my dismay, this doesn’t work! My unit tests all said the same thing: This expression cannot be translated to SQL. This is because entity framework is very picky about what can and cannot be done in a LINQ expression, while LINQ is not. This means you can end up with nasty run-time errors that will work for any LINQ expression under normal circumstances but will fail at run time if used on EF entities. In my specific case, I had broken a rule without realizing it by using pattern matching within my GetId
helper method, which is something that EF does not support.
The .NET Way?
Here’s the solution I came up with. I ended up doing something significantly more verbose that doesn’t actually solve the problem of repeated complex business logic. But, at least it keeps the code in separate modules using the same interface. I think the key change here is using a generic interface instead of a generic function.
// This interface can now be used in our original data service via dependency injection
interface IQueryBuilder<out T> {
IQueryable<T> GetUsingFilters(Filters filters);
}
class BlogPostQueryBuilder : IQueryBuilder<BlogPost> {
public IQueryable<BlogPost> GetUsingFilters(Filters filters) {
// Do some set up stuff...
var query = dbContext.BlogPosts.AsQueryable()
// Do the complex translation of a filter to a LINQ query
var fullQuery = BuildQueryFromFilters<BlogPost>(query, filters);
// run the query
return fullQuery.ToList();
}
// This function iterates over the filters and maps them to LINQ query Expressions
private IQueryable<NewsArticle> BuildQueryFromFilters(IQueryable<NewsArticle> query, Filters filters) {
foreach(var filter in filters) {
// Do some fancy stuff, ultimately adding a LINQ Expression to a list of expressions that get rolled up into something we can add to our IQueryable query.
var expressionToAdd = (blogPost) => blogPost.BlogPostId != 0;
}
}
}
class NewsArticleQueryBuilder : IQueryBuilder<NewsArticle> {
public IQueryable<NewsArticle> GetUsingFilters(Filters filters) {
// Do some set up stuff...
var query = dbContext.NewsArticles.AsQueryable()
// Do the complex translation of a filter to a LINQ query
var fullQuery = BuildQueryFromFilters<NewsArticle>(query, filters);
// run the query
return fullQuery.ToList();
}
// This function iterates over the filters and maps them to LINQ query Expressions
private IQueryable<NewsArticle> BuildQueryFromFilters(IQueryable<NewsArticle> query, Filters filters) {
foreach(var filter in filters) {
// Do some fancy stuff, ultimately adding a LINQ Expression to a list of expressions that get rolled up into something we can add to our IQueryable query.
var expressionToAdd = (newsArticle) => newsArticle.NewsArticleId != 0;
}
}
}
A Workaround
As you can probably tell, this solution isn’t 100% of what I wanted. I feel the limitations of entity framework should be handled explicitly in the language used to express queries, rather than relying on cryptic runtime errors. I’d like to hear if any readers can think of a more graceful solution to this problem than mine, particularly if it doesn’t involve modifying the existing BlogPost
and NewsArticle
entities. Regardless of the frustrations provided by Entity framework’s interaction with LINQ, I had fun coming up with a way around this problem.