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;
}
}
Here is a solution. Make your
AddInclude
methods return something likeISpecificationInclude
: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 ofBaseSpecification
class should be a collection of this interface.In your
SpecificationEvaluator
, process yourIncludes
, and all theThenIncludes
they may have, recursively.I know that it's lots of code, but I'm afraid there is no other way :)