When I fetch a "Users" info for their profile page. I'm fetching multiple child entity collections from the "User" (parent) entity to either get info from it (ex. images) or I'm fetching them just to get a count (ex. Likes)
Problem - When fetching all the "Yogaband" children entities it's killing the time it takes to fetch (~12 secs) because, I assume, each "Yogaband" entity has many child entities and even the children have children, so there is a ton of data or the underlying query is complex (haven't looked) despite having roughly only ~15 "Yogaband" entities in the DB.
What I need - All I need is the count of "Yogabands" from the "User" and a few other counts of child collections but I'm not sure how to modify my specification pattern and generic repo to just fetch a count. The code now only fetches the entire entity (ex. AddInclude()) You can see in the automapper mapping code below I have YogabandCount, which is my attempt at keeping a numerical count of the "Yogabands" but that could end up getting disconnected from the actual count and probably isn't the best way to handle this problem. What I need is a better way to fetch the "Yogaband" count without getting the entire entity.
I'd like to have something like this below to the specification pattern
AddCount()
I'll start with my controller to fetch the "User"
[HttpGet("{username}", Name = "GetMember")]
public async Task<IActionResult> GetMember(string username)
{
var spec = new MembersWithTypesSpecification(username);
var user = await _unitOfWork.Repository<User>().GetEntityWithSpec(spec);
if (user == null) return NotFound(new ApiResponse(404));
var userToReturn = _mapper.Map<MemberForDetailDto>(user);
return Ok(userToReturn);
}
Here is MembersWithTypesSpecification for creating everything I want
public class MembersWithTypesSpecification : BaseSpecification<User>
{
public MembersWithTypesSpecification(string userName)
: base(x => x.UserName == userName)
{
AddInclude(x => x.UserPhotos);
AddInclude(x => x.PracticedStyles);
AddInclude(x => x.PracticedPoses);
AddInclude(x => x.InstructedStyles);
AddInclude(x => x.InstructedPoses);
AddInclude(x => x.InstructorPrograms);
AddInclude(x => x.Yogabands);
AddInclude(x => x.ReceivedReviews);
AddInclude(x => x.Likers);
}
}
In the BaseSpecification file I have this below for AddInclude
public class BaseSpecification<T> : ISpecification<T>
{
public BaseSpecification() {}
public BaseSpecification(Expression<Func<T, bool>> criteria)
{
Criteria = criteria;
}
public Expression<Func<T, bool>> Criteria { get; }
public List<Expression<Func<T, object>>> Includes { get; } = new List<Expression<Func<T, object>>>();
public List<string> IncludeStrings { get; } = new List<string>();
public Expression<Func<T, object>> OrderBy { get; private set; }
public Expression<Func<T, object>> OrderByDescending { get; private set; }
public Expression<Func<T, object>> GroupBy { get; private set; }
public int Take { get; private set; }
public int Skip { get; private set; }
public bool IsPagingEnabled { get; private set; }
protected void AddInclude(Expression<Func<T, object>> includeExpression)
{
Includes.Add(includeExpression);
}
protected void AddInclude(string includeString)
{
IncludeStrings.Add(includeString);
}
protected void AddOrderBy(Expression<Func<T, object>> orderByExpression)
{
OrderBy = orderByExpression;
}
protected void AddOrderByDescending(Expression<Func<T, object>> orderByDescExpression)
{
OrderByDescending = orderByDescExpression;
}
protected void AddGroupBy(Expression<Func<T, object>> groupByExpression)
{
GroupBy = groupByExpression;
}
protected void ApplyPaging(int skip, int take)
{
Skip = skip;
Take = take;
IsPagingEnabled = true;
}
}
Here is GetEntityWithSpec() from my generic repo
public class GenericRepository<T> : IGenericRepository<T> where T : class
{
private readonly DataContext _context;
public GenericRepository(DataContext context)
{
_context = context;
}
public async Task<T> GetByIdAsync(int id)
{
return await _context.Set<T>().FindAsync(id);
}
public async Task<IReadOnlyList<T>> ListAllAsync()
{
return await _context.Set<T>().ToListAsync();
}
public async Task<T> GetEntityWithSpec(ISpecification<T> spec)
{
return await ApplySpecification(spec).FirstOrDefaultAsync();
}
public async Task<IReadOnlyList<T>> ListAsync(ISpecification<T> spec)
{
return await ApplySpecification(spec).ToListAsync();
}
public async Task<int> CountAsync(ISpecification<T> spec)
{
return await ApplySpecification(spec).CountAsync();
}
private IQueryable<T> ApplySpecification(ISpecification<T> spec)
{
return SpecificationEvaluator<T>.GetQuery(_context.Set<T>().AsQueryable(), spec);
}
public void Add(T entity)
{
_context.Set<T>().Add(entity);
}
public void Update(T entity)
{
_context.Set<T>().Attach(entity);
_context.Entry(entity).State = EntityState.Modified;
}
public void Delete(T entity)
{
_context.Set<T>().Remove(entity);
}
public async Task<bool> SaveChangesAsync()
{
return await _context.SaveChangesAsync() > 0;
}
}
And finally here is how I map the data with Automapper after it's been fetched.
CreateMap<User, MemberForDetailDto>()
.ForMember(d => d.YearsPracticing, o => o.MapFrom(s => System.DateTime.Now.Year - s.YearStarted))
.ForMember(d => d.Age, o => o.MapFrom(d => d.DateOfBirth.CalculateAge()))
.ForMember(d => d.Gender, o => o.MapFrom(d => (int)d.Gender))
.ForMember(d => d.Photos, opt => opt.MapFrom(src => src.UserPhotos.OrderByDescending(p => p.IsMain)))
.ForMember(d => d.Yogabands, opt => opt.MapFrom(source => source.Yogabands.Where(p => p.IsActive).Count()))
// .ForMember(d => d.Yogabands, opt => opt.MapFrom(source => source.YogabandsCount))
// .ForMember(d => d.Likers, opt => opt.MapFrom(source => source.Likers.Count()))
.ForMember(d => d.Likers, opt => opt.MapFrom(source => source.LikersCount))
.ForMember(d => d.Reviews, opt => {
opt.PreCondition(source => (source.IsInstructor == true));
opt.MapFrom(source => (source.ReceivedReviews.Count()));
})
// .ForMember(d => d.Reviews, opt => opt.MapFrom(source => source.ReceivedReviews.Count()))
.ForMember(d => d.ExperienceLevel, opt => opt.MapFrom(source => source.ExperienceLevel.GetEnumName()))
.ForMember(d => d.PracticedStyles, opt => opt.MapFrom(source => source.PracticedStyles.Count()))
.ForMember(d => d.PracticedPoses, opt => opt.MapFrom(source => source.PracticedPoses.Count()))
.ForMember(d => d.InstructedStyles, opt => opt.MapFrom(source => source.InstructedStyles.Count()))
.ForMember(d => d.InstructedPoses, opt => opt.MapFrom(source => source.InstructedPoses.Count()))
.ForMember(d => d.InstructorPrograms, opt => opt.MapFrom(source => source.InstructorPrograms.Where(p => p.IsActive == true)));