1

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;
    }
}
Amit Joshi
  • 15,448
  • 21
  • 77
  • 141
chuckd
  • 13,460
  • 29
  • 152
  • 331
  • *I have User->PracticedStyles->YogaStyles* - why does your PracticedStyle entity have no YogaStyles collection then? Do you mean that PracticedStyle breaks down an M:M relationship between User and YogaStyle? So you have User<-PracticedStyles->YogaStyle? Looks like your entity relationship mapping is incomplete- map both ends of all relationships (user has PracticedStyles, YogaStyle has PracticedStyles, PracticedStyle has User and YogaStyle) to make life easier – Caius Jard Sep 20 '20 at 05:17
  • 3
    *I'm using a generic specification pattern with a generic repository pattern* - to some extent I wonder why people bother doing this, when the result often ends up harder to understand, less readable and less flexible than just using LINQ on the context directly, in the code, as and when needed – Caius Jard Sep 20 '20 at 05:26
  • 1
    I may be wrong, but nested properties should be included with `ThenInclude` like `Include(u => u.PracticedStyles).ThenInclude(s => s.YogaStyle)` – E. Shcherbo Sep 20 '20 at 05:31
  • well, that's a good question. I've followed a Udemy course for a project. But to answer your question, I don't think adding a collection of 'YogaStyles' on the PracticedStyle table will work as it will create a reference to YogaStyle, which is not what I want. 'YogaStyle' is a table that only contains an id and name (style) – chuckd Sep 20 '20 at 05:32
  • E. Scherbo, I think you're right but where do I include it generic spec that I've implemented? – chuckd Sep 20 '20 at 05:35
  • Mohammad, that will not work as 'YogaStyle' only contains two columns (id, yogaStyleName) adding a reference back to some other table i.e. "PracticedStyle' would defeat the purpose of the table. Think of the table like an enum, it just holds a bunch of different yoga style names with an id (ex. 1, Power - 2,Bikram, etc) – chuckd Sep 20 '20 at 05:36
  • Oh, I see that you want a many-to-many relationship here. You're right. – Mohammad Dehghan Sep 20 '20 at 05:42
  • 1
    E. Shcherbo is correct with the 'theninclude' but I don't know how to add that part onto the generic specification code I've used from the project I've followed. This line here in the spec evaluator query = spec.Includes.Aggregate(query, (current, include) => current.Include(include)); – chuckd Sep 20 '20 at 05:46
  • Are you on EF Core or EF 6? For EF 6, you can use `AddInclude(x => x.PracticedStyles.Select(ps => ps.YogaStyle))` – Mohammad Dehghan Sep 20 '20 at 06:09
  • Can't do that as AddInclude takes this type of argument AddInclude(Expression> includeExpression) – chuckd Sep 20 '20 at 06:15
  • It doesn't have a .select available. – chuckd Sep 20 '20 at 06:15
  • 1
    @CaiusJard makes an important point. You're likely making your life harder and your code worse in a number of ways by using this pattern. Also, even if the pattern is appropriate which it can be, you are making a critical mistake by abstracting something before understanding how it works. – Aluan Haddad Sep 20 '20 at 06:18
  • 1
    The whole point of the Specification pattern is abstracting your data access library (EF) behind another layer. Why? Because you may someday decide to change your ORM. And I assure you, this will ***never*** happen. – Mohammad Dehghan Sep 20 '20 at 06:20
  • I most likely agree, but this was from a project I followed along from Udemy. – chuckd Sep 20 '20 at 06:22
  • 1
    I can solve your problem by implementing some classes and interfaces for managing you `Include`s and `ThenInclude`s. But that's lots of code, for no added benefit. – Mohammad Dehghan Sep 20 '20 at 06:23
  • Mohammad - I thought the repo pattern was to change out the ORM, not the spec pattern – chuckd Sep 20 '20 at 06:24
  • @MohammadDehghan not only is that very true, but there are also middle grounds. Like you can define an interface that simply exposes the readonly query functionality as an `IQueryable` backed by an implementation using `DbSet` but even that can well be excessive. – Aluan Haddad Sep 20 '20 at 06:27
  • @user1186050 When using the Repository pattern, to really abstract away your ORM, you have to use the Specification pattern to tell your Repository how to fetch your objects. – Mohammad Dehghan Sep 20 '20 at 06:38

2 Answers2

1

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.

chuckd
  • 13,460
  • 29
  • 152
  • 331
0

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 :)

Mohammad Dehghan
  • 17,853
  • 3
  • 55
  • 72
  • 1
    @user1186050 This solution exposes EF's `IIncludableQueryable` directly to the users of your code, which ruins the whole point of using the Sepcification pattern! Why not just expose the `IQueryable` to the outside?! – Mohammad Dehghan Sep 20 '20 at 07:11
  • IDK - I'm ready to scrap it all and just use a regular repo. I'm kinda new to generics and this is pretty advanced with all the expression func stuff. There should be a way I can create a function called something like AddIncludeWithThen(x => x.PracticedStyles, pS => pS.YogaStyle); and implement it. – chuckd Sep 20 '20 at 07:25
  • 1
    @user1186050 Yes. It's advanced. I know that you are learning how to use different patterns, but here is my advice: Never use a design pattern unless you exactly know what problems it solves in your specific scenario. I've seen LOTS of really bad code, that blindly and excessively use different design patterns. – Mohammad Dehghan Sep 20 '20 at 07:30
  • This is exactly what the project I'm following implements. Repo, Unit of work, spec, etc. I think I hadn't implemented the addincludewithstrings() like this AddInclude($"{nameof(User.Recipes)}.{nameof(Recipe.Ingredients)}"); – chuckd Sep 20 '20 at 07:47
  • 1
    @user1186050 `DbSet` is a _Repo_. `DbContext` is a _Unit of work_, `IQueryable` is a _specification_ builder :) – Aluan Haddad Sep 20 '20 at 08:20