I have a table representing a class hierarchy using the TPH model.
Some of those sub classes have navigation properties (collections and/or references).
I'm trying to preload them as I know I will need them later in the process.
I'm also trying to not have to manually write the casting and loading of all those properties, which would quickly become unmaintainable.
All that said, I saw two options, code generation and exploring the navigation properties exposed by the EF Core change tracker.
This is my implementation of the second solution:
protected async Task LoadRecursiveAsync<TEntityToLoad>(TEntityToLoad entity, CancellationToken cancellationToken) where TEntityToLoad : class, IEntity
{
// this.Context is of type Microsoft.EntityFrameworkCore.DbContext
EntityEntry<TEntityToLoad> entry = this.Context.Entry(entity);
foreach (NavigationEntry navigationEntry in entry.Navigations.Where(ne => !ne.IsLoaded))
{
cancellationToken.ThrowIfCancellationRequested();
await LoadRecursiveAsyncInternal(navigationEntry);
}
return;
async Task LoadRecursiveAsyncInternal(NavigationEntry navigationEntry)
{
await navigationEntry.LoadAsync(cancellationToken);
object? currentValue = navigationEntry.CurrentValue;
switch (currentValue)
{
case IEntity loadedEntity:
{
foreach (NavigationEntry navigation in this.Context.Entry(loadedEntity).Navigations.Where(ne => !ne.IsLoaded))
{
cancellationToken.ThrowIfCancellationRequested();
await LoadRecursiveAsyncInternal(navigation);
}
break;
}
case null:
break;
default:
{
Type type = currentValue.GetType();
if(!type.IsAssignableTo(typeof(IEnumerable)))
{
return;
}
if (type.GetGenericArguments() is [{ } entityType] && entityType.IsAssignableTo(typeof(IEntity)))
{
foreach (IEntity loadedEntity in (IEnumerable)currentValue)
{
foreach (NavigationEntry navigation in this.Context.Entry(loadedEntity).Navigations.Where(ne => !ne.IsLoaded))
{
cancellationToken.ThrowIfCancellationRequested();
await LoadRecursiveAsyncInternal(navigation);
}
}
}
break;
}
}
}
}
For completeness's sake, the IEntity
interface:
public interface IEntity
{
long Id { get; set; }
}
From my limited testing, it appears to work. I'm not too worried about any stack overflow, my model is more large than deep.
This is declared and implemented in my base generic repository class, by declaring this method as protected, I want developers to have to think before using this, as they will need to create a new method in the relevant Repository class (and maybe even to create a repository) to use this method.
Edit: I've successfully loaded the entirety of my model with this, it was slower than with split queries created by EF Core, but as the objective is only to load fragments, performance is not an issue (yet?)
Edit 2: Comparison of manual includes with the above recursive method, made with BenchmarkDotNet (Runtime: .NET 6.0, IterationCount 100)
Method | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio |
---|---|---|---|---|---|---|---|---|---|---|
LoadExplicitAsync | 9.655 ms | 0.1228 ms | 0.3504 ms | 9.509 ms | 1.00 | 0.00 | 328.1250 | 140.6250 | 2.61 MB | 1.00 |
LoadRecursiveAsync | 56.355 ms | 0.3518 ms | 0.9866 ms | 56.376 ms | 5.85 | 0.25 | 700.0000 | 200.0000 | 5.85 MB | 2.24 |
1 Answer 1
Just focusing on the code structure at hand, the most obvious thing it that the following codeblock is basically repeated three times which violates DRY (Don't Repeat Yourself):
foreach (NavigationEntry navigationEntry in entry.Navigations.Where(ne => !ne.IsLoaded))
{
cancellationToken.ThrowIfCancellationRequested();
await LoadRecursiveAsyncInternal(navigationEntry);
}
Also the code trying to convert the value into a bascially an IEnumerable<IEntity>
seems a bit more complicated than it ought to be. Pretty sure the baked in dotnet tools like as
and ?.
and LINQ's OfType
should achieve the same but with less verbosity.
My suggestion would be something along these lines:
protected async Task LoadRecursiveAsync<TEntityToLoad>(TEntityToLoad entity, CancellationToken cancellationToken) where TEntityToLoad : class, IEntity
{
// this.Context is of type Microsoft.EntityFrameworkCore.DbContext
EntityEntry<TEntityToLoad> entry = this.Context.Entry(entity);
await LoadNavigations(entry.Navigations);
return;
async Task LoadNavigations(IEnumerable<NavigationEntry> navigationEntries)
{
foreach (var navigationEntry in navigationEntries.Where(ne => !ne.IsLoaded))
{
cancellationToken.ThrowIfCancellationRequested();
await navigationEntry.LoadAsync(cancellationToken);
object? currentValue = navigationEntry.CurrentValue;
foreach (var entity in ValueAsEntities(currentValue))
{
await LoadNavigations(this.Context.Entry(entity).Navigations)
}
}
}
IEnumerable<IEntity> ValueAsEntities(object currentValue)
{
var entityVal = currentValue as IEntity;
if (entityVal != null)
{
yield return entityVal;
}
else
{
foreach (entityVal in (currentValue as IEnumerable)?.OfType<IEntity>())
{
yield return entityVal;
}
}
}
}
The ValueAsEntities
implementation still seems a bit clunky - not sure if that can be solved a bit more elegantly.
Saves about 20 LOC's
Completely untested obviously.
-
\$\begingroup\$ Why didn't I think of just trying to cast to IEnumerable -_-, thanks for that, I'll be trying it out. \$\endgroup\$Irwene– Irwene2023年10月19日 07:31:05 +00:00Commented Oct 19, 2023 at 7:31
-
\$\begingroup\$ Oh, and there seem to be an issue in your latest
foreach
compiler complains that there must be an assignation at the left of thein
keyword. So, not able to reuse a previous variable name apparently. \$\endgroup\$Irwene– Irwene2023年10月19日 07:52:29 +00:00Commented Oct 19, 2023 at 7:52 -
\$\begingroup\$ Additionally those suggested modifications resulted in a minimal increase in performance (Ratio went down to 5.65, and RatioSD to 0.24), because of cast usage instead of reflection. \$\endgroup\$Irwene– Irwene2023年10月19日 08:01:22 +00:00Commented Oct 19, 2023 at 8:01
Include
and usingLoadRecursiveAsync
) with a 100-1000 number of executions. it should give you a baseline on where are you standing, and what you need to do next. (you could useBenchmarkDotNet
for that). \$\endgroup\$