Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to write Repository method for .ThenInclude in EF Core 2

Tags:

I'm trying to write a repository method for Entity Framework Core 2.0 that can handle returning child collections of properties using .ThenInclude, but I'm having trouble with the second expression. Here is a working method for .Include, which will return child properties (you supply a list of lambdas) of your entity.

public T GetSingle(Expression<Func<T, bool>> predicate, params Expression<Func<T, object>>[] includeProperties) {     IQueryable<T> query = _context.Set<T>();     foreach (var includeProperty in includeProperties)     {         query = query.Include(includeProperty);     }       return query.Where(predicate).FirstOrDefault(); } 

Now here is my attempt at writing a method that will take a Tuple of two Expressions and feed those into a .Include(a => a.someChild).ThenInclude(b => b.aChildOfSomeChild) chain. This isn't a perfect solution because it only handles one child of a child, but it's a start.

public T GetSingle(Expression<Func<T, bool>> predicate, params Tuple<Expression<Func<T, object>>, Expression<Func<T, object>>>[] includeProperties) {     IQueryable<T> query = _context.Set<T>();     foreach (var includeProperty in includeProperties)     {          query = query.Include(includeProperty.Item1).ThenInclude(includeProperty.Item2);                   }      return query.Where(predicate).FirstOrDefault(); } 

Intellisense returns an error saying "The type cannot be inferred from the usage, try specifying the type explicitly". I have a feeling it's because the expression in Item2 needs to be classified as somehow related to Item1, because it needs to know about the child relationship it has.

Any ideas or better techniques for writing a method like this?

like image 494
redwards510 Avatar asked Sep 22 '17 22:09

redwards510


People also ask

What is Repository pattern in EF core?

Repository Pattern is an abstraction of the Data Access Layer. It hides the details of how exactly the data is saved or retrieved from the underlying data source. The details of how the data is stored and retrieved is in the respective repository.

What is ThenInclude?

The ThenInclude method moves the chaining level to the property included. It allows us to include related objects from the next level. ThenInclude is a syntactic sugar method to make it easier and clearer to include multiple related objects.


2 Answers

I found this repository method online and it does exactly what I wanted. Yared's answer was good, but not all the way there.

/// <summary> /// Gets the first or default entity based on a predicate, orderby delegate and include delegate. This method default no-tracking query. /// </summary> /// <param name="selector">The selector for projection.</param> /// <param name="predicate">A function to test each element for a condition.</param> /// <param name="orderBy">A function to order elements.</param> /// <param name="include">A function to include navigation properties</param> /// <param name="disableTracking"><c>True</c> to disable changing tracking; otherwise, <c>false</c>. Default to <c>true</c>.</param> /// <returns>An <see cref="IPagedList{TEntity}"/> that contains elements that satisfy the condition specified by <paramref name="predicate"/>.</returns> /// <remarks>This method default no-tracking query.</remarks> public TResult GetFirstOrDefault<TResult>(Expression<Func<TEntity, TResult>> selector,                                           Expression<Func<TEntity, bool>> predicate = null,                                           Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,                                           Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> include = null,                                           bool disableTracking = true) {     IQueryable<TEntity> query = _dbSet;     if (disableTracking)     {         query = query.AsNoTracking();     }      if (include != null)     {         query = include(query);     }      if (predicate != null)     {         query = query.Where(predicate);     }      if (orderBy != null)     {         return orderBy(query).Select(selector).FirstOrDefault();     }     else     {         return query.Select(selector).FirstOrDefault();     } } 

Usage:

var affiliate = await affiliateRepository.GetFirstOrDefaultAsync(     predicate: b => b.Id == id,     include: source => source         .Include(a => a.Branches)         .ThenInclude(a => a.Emails)         .Include(a => a.Branches)         .ThenInclude(a => a.Phones)); 
like image 113
redwards510 Avatar answered Oct 17 '22 15:10

redwards510


I had the same issue since EF Core doesn't support lazy loading but i tried to get workaround in the following way:

First create an attribute class to mark our desired navigation properties from other properties of a given class.

[AttributeUsage(AttributeTargets.Property, Inherited = false)] public class NavigationPropertyAttribute : Attribute {     public NavigationPropertyAttribute()     {     } } 

Extension methods to filter out navigation properties and apply Include/ThenInclude using string based Eager loading.

public static class DbContextHelper {      public static Func<IQueryable<T>, IQueryable<T>> GetNavigations<T>() where T : BaseEntity     {         var type = typeof(T);         var navigationProperties = new List<string>();          //get navigation properties         GetNavigationProperties(type, type, string.Empty, navigationProperties);          Func<IQueryable<T>, IQueryable<T>> includes = ( query => {                     return  navigationProperties.Aggregate(query, (current, inc) => current.Include(inc));                });          return includes;     }      private static void GetNavigationProperties(Type baseType, Type type, string parentPropertyName, IList<string> accumulator)     {         //get navigation properties         var properties = type.GetProperties();         var navigationPropertyInfoList = properties.Where(prop => prop.IsDefined(typeof(NavigationPropertyAttribute)));          foreach (PropertyInfo prop in navigationPropertyInfoList)         {             var propertyType = prop.PropertyType;             var elementType = propertyType.GetTypeInfo().IsGenericType ? propertyType.GetGenericArguments()[0] : propertyType;              //Prepare navigation property in {parentPropertyName}.{propertyName} format and push into accumulator             var properyName = string.Format("{0}{1}{2}", parentPropertyName, string.IsNullOrEmpty(parentPropertyName) ? string.Empty : ".", prop.Name);             accumulator.Add(properyName);              //Skip recursion of propert has JsonIgnore attribute or current property type is the same as baseType             var isJsonIgnored = prop.IsDefined(typeof(JsonIgnoreAttribute));             if(!isJsonIgnored && elementType != baseType){                 GetNavigationProperties(baseType, elementType, properyName, accumulator);             }         }     } } 

Sample POCO classes implementing NavigationPropertyAttribute

public class A : BaseEntity{   public string Prop{ get; set; } }  public class B : BaseEntity{    [NavigationProperty]    public virtual A A{ get; set; } }  public class C : BaseEntity{    [NavigationProperty]    public virtual B B{ get; set; } } 

Usage in Repository

public async Task<T> GetAsync(Expression<Func<T, bool>> predicate) {     Func<IQueryable<T>, IQueryable<T>> includes = DbContextHelper.GetNavigations<T>();     IQueryable<T> query = _context.Set<T>();     if (includes != null)     {         query = includes(query);     }      var entity = await query.FirstOrDefaultAsync(predicate);     return entity; } 

Json result for sample class C would be:

{   "B" : {         "A" : {               "Prop" : "SOME_VALUE"             }       } }  
like image 23
Yared Avatar answered Oct 17 '22 17:10

Yared