0

I have an EF core that I am trying to "Include" some relations by key, and found an answer I can do it like this:

//Get entity by key first, then load relations for it like this:

await context.Entry(entity).Collection(expressions).LoadAsync();

This collection method takes:

Expression<Func<TEntity, IEnumerable<TProperty>>> expressions

All examples I have seen, is a way to pass a single property to it.

Here is the full code of a wrapper that I want:

    public static async Task<TEntity> FindInContextAsync<TEntity, TProperty>(object keyValue, Expression<Func<TEntity, IEnumerable<TProperty>>> expressions)
        where TEntity : class where TProperty : class
    {
        using var scope = _scopeFactory.CreateScope();

        var context = scope.ServiceProvider.GetService<AccountsDbContext>();

        var dbSet = context.Set<TEntity>();

        var Entit = await dbSet.FindAsync(keyValue);
        if (Entit == null)
        {
            throw new NotFoundException(nameof(TEntity), keyValue);
        }

        await context.Entry(Entit).Collection(expressions).LoadAsync();

        return Entit;
    }

I would like to call it as cleanly and as little code as possible, something like this:

await FindInAccountsContextAsync<MyEntity, dynamic>(id, x => x.RelationClass1, x.RelationClass2...);

Seems like above would be more of a params case:

params Expression<Func<TEntity, TProperty>[] expressions

But then this would not get accepted by Collection()

In the end, I want to be able to retrieve any object from DbContext by key, with eager loading specified

Any solution is welcomed, as I can't get anything to work!

Edgaras
  • 449
  • 3
  • 14

2 Answers2

0

DbEntityEntry.Collection takes:

Expression<Func<TEntity, ICollection<TElement>>> navigationProperty

This is to select a single property of type ICollection<TElement>, not multiple properties.

Because ICollection is invariant, and TElement is likely going to be different for each navigation property, you aren't going to be able to pass a collection of these to FindInContextAsync.

The simplest workaround would be to remove the delegate and load the navigations after calling FindInContextAsync, but if you must pass delegates you could do this:

public static async Task<TEntity> FindInContextAsync<TEntity>(
    object keyValue,
    params Func<TEntity, Task>[] afterFind)
    where TEntity : class
{
    // ...

    var entity = await dbSet.FindAsync(keyValue);
    
    // ...

    await Task.WhenAll(afterFind.Select(func => func(entity)));

    return entity;
}

And call like this:

var entity = await FindInAccountsContextAsync(
    id,
    (MyEntity x) => context.Entry(x).Collection(y => y.RelationClass1).LoadAsync(),
    (MyEntity x) => context.Entry(x).Collection(y => y.RelationClass2).LoadAsync());

The above could be simplified slightly by adding a helper function:

Task LoadCollectionAsync<TEntity, TProperty>(
    TEntity entity,
    Expression<Func<TEntity, ICollection<TElement>>> navigationProperty)
{
    return context.Entry(entity).Collection(navigationProperty).LoadAsync();
}

Which would allow you to do this:

var entity = await FindInAccountsContextAsync(
    id,
    (MyEntity x) => LoadCollectionAsync(x, y => y.RelationClass1),
    (MyEntity x) => LoadCollectionAsync(x, y => y.RelationClass2));
Johnathan Barclay
  • 18,599
  • 1
  • 22
  • 35
  • Do you think I could do a loop to call Collection().LoadAsync() multiple times, where I pass a params of select functions? Except that don't know how to write them as > That should shorten the FindInAccountsContextAsync() – Edgaras Jun 30 '21 at 10:03
  • If `DbEntityEntry.Collection` used `IEnumerable` rather than `ICollection` it would be possible, but `ICollection` is invariant and `TProperty` would need to be different each time. – Johnathan Barclay Jun 30 '21 at 10:06
  • Do the lambda parameters to `FindInAccountsContextAsync` really need to have their parameters explicitly typed? Couldn't you just use `FindInAccountsContextAsync(`...? – NetMage Jul 06 '21 at 20:45
0

My mistake for thinking I need to use Collection(). I was able to achieve below using Reference() instead, which takes Expression<Func<TEntity, TProperty>> instead of IEnumerable which is what was causing me issues. Thanks to @NetMage, end result:

var entity = await FindInAccountsContextAsync<AccountCreditDebit>(
id, x => x.CreditDebitType, x => x.CreditDebitSource;

This allows me to pass any DbSet in my Account DB Context (in this case AccountCreditDebit DbSet) and return the entity from DbSet with relations specified (CreditDebitType, CreditDebitSource).

Full Code:

       public static async Task<TEntity> FindInAccountsContextAsync<TEntity>(object keyValue, params Expression<Func<TEntity, object>>[] expressions)
        where TEntity : class where TProperty : class
    {
        using var scope = _scopeFactory.CreateScope();

        var context = scope.ServiceProvider.GetService<AccountsDbContext>();

        var dbSet = context.Set<TEntity>();

        var entity = await dbSet.FindAsync(keyValue);
        if (entity == null)
        {
            throw new NotFoundException(nameof(TEntity), keyValue);
        }

        foreach (var expression in expressions)
        {
            await context.Entry(entity).Reference(expression).LoadAsync();
        }
        

        return entity;
    }

The limitation I can think of, is I cannot pass a function if ICollection in the mix with single property such as:

var entity = await FindInAccountsContextAsync<AccountCreditDebit>(
id, 
x => x.CreditDebitType, /*<= Single property */ 
x => x.ManyCreditDebitSources; //<= A Collection

Old Comment answered in comments:

The only thing I would like to improve, is to remove this dynamic if possible

await FindInAccountsContextAsync<AccountCreditDebit, dynamic>(...)
Edgaras
  • 449
  • 3
  • 14
  • 1
    You can replace `dynamic` with `object` which is probably the net effect in this case, but in that case you don't need a generic type parameter, just use `params Expression>[] expressions`. I am thinking a better approach would be to use a single `Expression> expression` that expects the form `x => new { x.CreditDebitType, x.CreditDebitSource, x.ManyCreditDebitSources }` – NetMage Jun 30 '21 at 18:23
  • [This answer](https://stackoverflow.com/a/18074711/2557128) seems like it is relevant to what you are trying to do and when you should use `Collection`/`Reference`/`Property` and implies you could just have `params string PropertyNames` – NetMage Jun 30 '21 at 18:26
  • @NetMage Thanks for the object tip! I didn't know Reference would accept expression! However, I believe to do x => new{ x.1, x.2...) I would have to introduce the 2nd generic type parameter, which I want to avoid. – Edgaras Jul 02 '21 at 14:48
  • Yes, You would have a second type parameter, but you would not have to set either one - the compiler should be able to infer the types from the parameters. Also, why do you need the first type parameter? Doesn't it always have to be `AccoutnCreditDebit`? – NetMage Jul 02 '21 at 18:37
  • @NetMage It doesn't. It can be anything - It's a DbSet of a DbContext. As DbContext (accounts) have multiple DbSets (tables), this allows me to choose the tabes. – Edgaras Jul 05 '21 at 12:02