0

My BaseRespository is like so:

public abstract class BaseRespository<TEntity, TContext> : IBaseRepository<TEntity> 
    where TEntity : class
    where TContext : DbContext
{
    private readonly TContext _context;

    protected BaseRespository(TContext context)
    {
        _context = context;
    }

    public async Task<TEntity> GetByCondition(Expression<Func<TEntity, bool>> predicate)
    {
        return await _context.Set<TEntity>().Where(predicate).FirstOrDefaultAsync();
    }      
}

And I access the GetByCondition method like so:

public async Task<Tips> GetTipBySlug(string slug)
{
    Expression<Func<Tips, bool>> predicate = (t) => t.Slug == slug &&
                                                    t.Status == (int)LU_Status.active &&
                                                    t.User.Status == (int)LU_Status.active;

    return await _tipRepository.GetByCondition(predicate);
}

I want to use the Include and ThenInclude of EF Core with the predicate(This is just my desire) like so:

public async Task<Tips> GetTipBySlug(string slug)
        {
            Expression<Func<Tips, bool>> whereExpr = (t) => t.Include(t=>t.User).ThenInclude(u=>u.UserImages)
                                                            t.Slug == slug &&
                                                            t.Status == (int)LU_Status.active &&
                                                            t.User.Status == (int)LU_Status.active;

            return await _tipRepository.GetByCondition(whereExpr);
        }

How can I add the desired t.Include(t=>t.User).ThenInclude(u=>u.UserImages) to the predicate using EF CORE 2 and above?

Offir
  • 3,252
  • 3
  • 41
  • 73
  • A predicate is function that returns a boolean. It shouldn't be abused by somehow adding includes to it (separation of concerns). Add `Includes` as arguments to the Get method. That is, if you insist on using the repository pattern. – Gert Arnold May 03 '20 at 18:10
  • @GertArnold I have already read some of your answers when I was searching an answer before, it seems like you don't like this pattern. I read some of the solutions for an old questions, I was just wondering if there was something out of the box for EF Core. – Offir May 03 '20 at 18:26
  • 1
    Don't like... well, it's just that there's often no reason to add this redundant layer to your code. That's a dry fact, not a matter of opinion. If you have good reasons, go for it. But there's no way to combine a predicate and `Include`s. For one, a predicate is the input of a `Where` clause. An `Include` is a different thing altogether. – Gert Arnold May 03 '20 at 18:34
  • So I need a second parameter for the path. – Offir May 03 '20 at 19:05
  • 1
    Yep, that's what all repo builders do. – Gert Arnold May 03 '20 at 19:05
  • The essential part of the post marked as duplicate is the `include` parameter of the [GetFirstOrDefaultAsync](https://github.com/Arch/UnitOfWork/blob/master/src/UnitOfWork/Repository.cs#L379) method in [Microsoft.EntityFrameworkCore.UnitOfWork](https://learn.microsoft.com/en-us/ef/core/extensions/#microsoftentityframeworkcoreunitofwork) pacjkage which is extended variant of your `GetByCondition` method. – Ivan Stoev May 04 '20 at 07:18
  • @IvanStoev thx, I missed that question. – Offir May 04 '20 at 13:31

1 Answers1

1

Even if this worked without the need to split up your logic across multiple repository argumets, would you really prefer to write

        Expression<Func<Tips, bool>> whereExpr = (t) => t.Include(t=>t.User).ThenInclude(u=>u.UserImages)
                                                        t.Slug == slug &&
                                                        t.Status == (int)LU_Status.active &&
                                                        t.User.Status == (int)LU_Status.active;

        return await _tipRepository.GetByCondition(whereExpr);

over the way EF was designed to be used:

    var q = _tipRepository.Set<Tips>() 
                          .Include(t=>t.User)
                          .ThenInclude(u=>u.UserImages)
                          .Where(t => t.Status == (int)LU_Status.active)
                          .Where(t => t.User.Status == (int)LU_Status.active);

    return await q.FirstOrDefaultAsync();

Why would you want to create an Expression<Func<Tips,bool>> instead of just an IQueryabe<T>. It has nothing do do with whether the repository is "generic", and everything to do with how you want to write queries. Queries are written by the consumers of the repository. Not by or in the repository (except to the extent you want to reuse a query across consumers).

The crazy thing about this design is that it allows the repo's consumer to specify the query. It just forces them to do it through a clunky, bespoke API.

You could write it like this:

public async Task<TEntity> GetByCondition<TEntity>(Expression<Func<TEntity, bool>> predicate, Func<DbSet<TEntity>, IQueryable<TEntity>> baseQuery = null) where TEntity : class
{
    IQueryable<TEntity> q = Set<TEntity>();
    if (baseQuery != null)
    {
       q = baseQuery(Set<TEntity>());
    }
    return await q.Where(predicate).FirstOrDefaultAsync();
}

You don't need Expression for that one because the Include is not deferred. It's a function that returns an IIncludableQueryable, so it's a query transformation function.

And then:

public async Task<Tips> GetTipBySlug(string slug)
{
    Expression<Func<Tips, bool>> whereExpr = (t) => t.Slug == slug &&
                                                    t.Status ==1 &&
                                                    t.User.Status == 1;

    return await GetByCondition(whereExpr, s => s.Include(t => t.User).ThenInclude(u => u.UserImages))                                                           ;
}
Offir
  • 3,252
  • 3
  • 41
  • 73
David Browne - Microsoft
  • 80,331
  • 6
  • 39
  • 67
  • I don't want to write the same query multiple times if the only difference between them is the `ThenInclude` part, the predicate is the same so why should I write many queries? By sending 2 arguments to the `generic` repository it suppose to make life easier... The main point in the question is how to send the second argument? I couldn't find a recent answer for that(Including the `ThenInclude` part) – Offir May 03 '20 at 23:04
  • If you're repeating the "active=1" boilerplate, why not use a Global Query Filter? https://learn.microsoft.com/en-us/ef/core/querying/filters – David Browne - Microsoft May 03 '20 at 23:13
  • That's a nice feature, is it possible to send as argument the `ThenInclude` Part to the repository, all the answers I saw had only the `Include` part? – Offir May 03 '20 at 23:25
  • See updated answer, but I don't see how that's going to make anyone's life easier. – David Browne - Microsoft May 03 '20 at 23:35
  • Thank you David, I will try this tomorrow, why not? This way I have 1 method in my generic repository `GetByCondition` and I can send it the predicate and the tables as arguments instead of writing 2 methods that will be different only by the included tables. – Offir May 03 '20 at 23:43
  • Because you don't need either method in your repository. You end up having to alter your repository for each controller or use case. Just let the calling code decide what objects they need, how the graphs are shaped, whether they are change-tracking or not, etc. – David Browne - Microsoft May 04 '20 at 00:12
  • Thx, I tried working without repository, it lead to too many custom functions I had to write, I prefer it this way , Controller -> BLL -> DAL -> BLL ->DTO->ViewModel-> View – Offir May 04 '20 at 12:30
  • Hi David, can you look at this question as well? https://stackoverflow.com/questions/61698796/ef-core-2-2-cannot-project-while-using-iqueryable – Offir May 09 '20 at 16:17