📜 ⬆️ ⬇️

We unify the behavior of LINQ to IEnumerable and LINQ to IQueriable in terms of working with null values. ExpressionVisitor Example

It's no secret that although LINQ is positioned as a universal query language for collections of different origins (both collections in memory and various remote data sources, for example, databases), in fact, the results of identical queries are different depending on which collection was the query. In particular, if there is a null in the Property1 property used in the expression in the .Select method (c => c.Property1.Property2), you can get both NullReferenceException and null as a result.

Example:

Suppose you have a DbContext that gives you access to a table of books. In this case, each book may be listed by the author, presented in the table of authors.

Accordingly, the author’s view of the code looks something like this:
')
public class Book { public string Title{get;set;} public string ISBN{get;set;} public Author Author{get;set;} ... } public class Author { public string Name {get;set;} ... } 

Suppose also that in addition to the local database, we also use a web service as a data source, which provides us information from somewhere outside. We wrapped it in an adapter, so it provides us with a collection of exactly the same books with the authors, however, inside it is a List.

To unify, we wrap both data sources in the IBookSource interface:

 public interface IBookSource { T GetBookData<T>(string isbn, Expression<Func<Book, T>> selector) where T:class; } 

Such a Generic implementation allows not always taking the whole Book entity with its navigation properties from the database, but receiving only the necessary information by a query. The implementation of this method for DbContext is trivial:

 public T GetBookData<T>(string isbn, Expression<Func<Book, T>> selector) where T:class { return Set<Book>().Where(b=>b.ISBN == isbn).Select(selector).SingleOrDefault(); } 

What happens if we call this method for a book where the author is not specified as follows:

 var authorName = GetBookData(isbn, b=>b.Author.Name); 

The authorName variable will be null.

Now we will try to implement the same method in the case when the source of data is actually a collection in memory (List booklist, which we received by requesting the service from the data provider):

 public T GetBookData<T>(string isbn, Expression<Func<Book, T>> selector) where T:class { List<Book> books = GetDataFromService(); return books.Where(b=>b.ISBN == isbn).Select(selector.Compile()).SingleOrDefault(); } 

Note that since books are IEnumerable and not IQueriable, we need to compile the expression before passing it to the Select method.

What happens if we again call the method for a book that does not have the author listed as follows:

 var authorName = GetBookData(isbn, b=>b.Author.Name); 

We get a NullReferenceException. The question arises of what to do, because we want to extract data in a uniform way regardless of what is behind the IBookSource interface. So we need to reduce the behavior of IQueriable and IEnumerable in the access issue with the class properties to the same behavior. In this case, I like the IQueriable behavior more, so I will transform the Expression, which runs on the IEnumerable to the same behavior.
To do this, it is necessary for each PropertyGetter in the expression to be checked whether the value on which the PropertyGetter is called is null, and if so, instead of calling the PropertyGetter, null is returned. Here is a method that implements this behavior:

 public static TResult With<TSource, TResult>(TSource source, Func<TSource, TResult> action) where TSource : class { if (source != default(TSource)) return action(source); return default(TResult); } 

However, how to add this behavior to an already finished expression (Expression)?

The ExpressionVisitor class comes to the rescue. This is a special class that allows you to go through all the nodes of the expression tree (ExpressionTree) and modify them as we need.

  public class AddMaybeVisitor : ExpressionVisitor { //      ,   . public Expression<Func<T1, T2>> Modify<T1, T2>(Expression<Func<T1, T2>> expression) { return (Expression<Func<T1, T2>>)Visit(expression); } //      ,          ,   ,    . protected override Expression VisitMember(MemberExpression node) { Visit(node.Expression); var expressionType = node.Expression.Type; var memberType = node.Type; var withMethodinfo = typeof(AddMaybeVisitor) .GetMethod("With") .MakeGenericMethod(expressionType, memberType); var p = Expression.Parameter(expressionType); var l = Expression.Lambda(Expression.MakeMemberAccess(p, node.Member), p); return Expression.Call(withMethodinfo, node.Expression, Expression.Constant(l.Compile(), typeof(Func<,>).MakeGenericType(expressionType, memberType)) ); } public static TResult With<TSource, TResult>(TSource source, Func<TSource, TResult> action) where TSource : class { if (source != default(TSource)) return action(source); return default(TResult); } } 

How to use this ExpressionVisitor? Very simple:

 public T GetBookData<T>(string isbn, Expression<Func<Book, T>> selector) where T:class { List<Book> books = GetDataFromService(); var modifiedSelector = new AddMaybeVisitor().Modify(selector); return books.Where(b=>b.ISBN == isbn).Select(modifiedSelector.Compile()).SingleOrDefault(); } 

Now, if you request the author's name from a book for which the author is not specified, we will get null, and not NullReferenceException, in exactly the same way as when requested in the database via DbContext.

Source: https://habr.com/ru/post/256257/


All Articles