How to add a .ThenInclude for a nested object within a generic specification pattern in C#

1.8k Views Asked by At

I've implemented a generic spec pattern for my generic repo, but I don't know how I can add a .ThenInclude() to the code.
FYI - I have 3 entities (User->PracticedStyles->YogaStyles) and when I go to fetch my User I want to fetch all the YogaStyles he/she practices (ex. bikram, vinyasa, etc). But I can't get the YogaStyle entities, I can fetch all the PracticedStyle entities for the User because it's only one entity deep, but I can't figure out how to fetch/include the YogaStyle entity from each PracticedStyle.

I'm using a generic specification pattern with a generic repository pattern and I've created an intermediate table to hold all the styles, maybe this is wrong or I don't know how to use the generic spec pattern correctly?

public class User : IdentityUser<int>
{
   public ICollection<PracticedStyle> PracticedStyles { get; set; }
}
public class PracticedStyle : BaseEntity
{
    public int UserId { get; set; }
    public User User { get; set; }
    public int YogaStyleId { get; set; }
    public YogaStyle YogaStyle { get; set; }
}
public class YogaStyle : BaseEntity
{
    public string Name { get; set; } // strength, vinyasa, bikram, etc
}

Here is my controller and the methods the controller calls from

[HttpGet("{id}", Name = "GetMember")]
public async Task<IActionResult> GetMember(int id)
{
   var spec = new MembersWithTypesSpecification(id);
   var user = await _membersRepo.GetEntityWithSpec(spec);
   if (user == null) return NotFound(new ApiResponse(404));
   var userToReturn = _mapper.Map<MemberForDetailDto>(user);
   return Ok(userToReturn);
}
public class MembersWithTypesSpecification : BaseSpecification<User>
{
   public MembersWithTypesSpecification(int id) 
        : base(x => x.Id == id) 
    {
        AddInclude(x => x.UserPhotos);
        AddInclude(x => x.Experience);
        AddInclude(x => x.Membership);
        AddInclude(x => x.PracticedStyles);
        // doesn't work - yogastyles is not a collection
        // AddInclude(x => x.PracticedStyles.YogaStyles);
        AddInclude(x => x.InstructedStyles);
    }
}

Here is the 'AddInclude' from BaseSpecification

public class BaseSpecification<T> : ISpecification<T>
{
   public BaseSpecification()
    {
    }

    public BaseSpecification(Expression<Func<T, bool>> criteria)
    {
        Criteria = criteria;
    }
   public List<Expression<Func<T, object>>> Includes { get; } = new List<Expression<Func<T, object>>>();
   protected void AddInclude(Expression<Func<T, object>> includeExpression)
    {
        Includes.Add(includeExpression);
    }
}

Here is the getEntityWithSpec

public async Task<T> GetEntityWithSpec(ISpecification<T> spec)
{
   return await ApplySpecification(spec).FirstOrDefaultAsync();
}
private IQueryable<T> ApplySpecification(ISpecification<T> spec)
{
    return SpecificationEvaluator<T>.GetQuery(_context.Set<T>().AsQueryable(), spec);
}

and spec evaluator

public class SpecificationEvaluator<TEntity> where TEntity : class // BaseEntity // when using BaseEntity, we constrain it to on base entities
{
    public static IQueryable<TEntity> GetQuery(IQueryable<TEntity> inputQuery, ISpecification<TEntity> spec)
    {
        var query = inputQuery;

        if (spec.Criteria != null)
        {
            query = query.Where(spec.Criteria); // e => e.YogaEventTypeId == id
        }

        if (spec.OrderBy != null)
        {
            query = query.OrderBy(spec.OrderBy);
        }

        if (spec.OrderByDescending != null)
        {
            query = query.OrderByDescending(spec.OrderByDescending);
        }

        if (spec.IsPagingEnabled)
        {
            query = query.Skip(spec.Skip).Take(spec.Take);
        }

        query = spec.Includes.Aggregate(query, (current, include) => current.Include(include)); // 'current' represents entity

        return query;
    }
}
2

There are 2 best solutions below

5
On

Here is a solution. Make your AddInclude methods return something like ISpecificationInclude:

public interface ISpecificationInclude<TFrom, TTo>
    where TFrom : IEntity
    where TTo : IEntity
// I know that you do not have a `IEntity` interface, but I advise you to
// add it to your infrastructure and implement it by all your entity classes.
{
    ISpecificationInclude<TTo, TAnother> ThenInclude<TAnother>(Expression<Func<TTo, TAnother>> includeExpression);
    ISpecificationInclude<TTo, TAnother> ThenInclude<TAnother>(Expression<Func<TTo, IEnumerable<TAnother>>> collectionIncludeExpression);
}

Implement this interface appropriately. The implementation should be a wrapper around a single "include" expression. You probably need two implementations: one for wrapping a collection include and one for a simple object include.

The Includes property of BaseSpecification class should be a collection of this interface.

In your SpecificationEvaluator, process your Includes, and all the ThenIncludes they may have, recursively.

I know that it's lots of code, but I'm afraid there is no other way :)

1
On

I figured out what I needed. Following this link I needed to add

AddInclude($"{nameof(User.PracticedStyles)}.{nameof(PracticedStyle.YogaStyle)}");

and

query = specification.IncludeStrings.Aggregate(query,
                            (current, include) => current.Include(include));

and

public List<string> IncludeStrings { get; } = new List<string>();
protected virtual void AddInclude(string includeString)
{
    IncludeStrings.Add(includeString);
}

and that allowed me to use .thenInclude(), but as a series of strings.