Skip to content

Commit 1610f28

Browse files
author
Bart Koelman
authored
Eager loading (#701)
* Added eager loading of one-to-one and one-to-many entity relations * Removed dead code * Fixed: Eagerloads can have nested Eagerload properties * Added safeguard against infinite loop. * Cleanup and realign of project files * Removed unused project dependencies * Empty commit to restart TravisCI * Post-merge fixes * Post-merge fixes * More post-merge fixes * Review feedback: make EagerLoad private
1 parent 579a340 commit 1610f28

File tree

16 files changed

+406
-44
lines changed

16 files changed

+406
-44
lines changed

benchmarks/Benchmarks.csproj

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
</PropertyGroup>
66

77
<ItemGroup>
8-
<PackageReference Include="BenchmarkDotNet" Version="0.12.0" />
9-
<PackageReference Include="moq" Version="$(MoqVersion)" />
8+
<ProjectReference Include="..\src\JsonApiDotNetCore\JsonApiDotNetCore.csproj" />
109
</ItemGroup>
1110

1211
<ItemGroup>
13-
<ProjectReference Include="..\src\JsonApiDotNetCore\JsonApiDotNetCore.csproj" />
12+
<PackageReference Include="BenchmarkDotNet" Version="0.12.0" />
13+
<PackageReference Include="Moq" Version="$(MoqVersion)" />
1414
</ItemGroup>
1515
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace JsonApiDotNetCoreExample.Models
2+
{
3+
public class Country
4+
{
5+
public int Id { get; set; }
6+
public string Name { get; set; }
7+
}
8+
}
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,50 @@
1+
using System.Collections.Generic;
2+
using System.ComponentModel.DataAnnotations.Schema;
3+
using System.Linq;
14
using JsonApiDotNetCore.Models;
25

36
namespace JsonApiDotNetCoreExample.Models
47
{
5-
public sealed class Passport : Identifiable
8+
public class Passport : Identifiable
69
{
10+
[Attr]
711
public int? SocialSecurityNumber { get; set; }
12+
13+
[Attr]
814
public bool IsLocked { get; set; }
915

1016
[HasOne]
1117
public Person Person { get; set; }
18+
19+
[Attr]
20+
[NotMapped]
21+
public string BirthCountryName
22+
{
23+
get => BirthCountry.Name;
24+
set
25+
{
26+
if (BirthCountry == null)
27+
{
28+
BirthCountry = new Country();
29+
}
30+
31+
BirthCountry.Name = value;
32+
}
33+
}
34+
35+
[EagerLoad]
36+
public Country BirthCountry { get; set; }
37+
38+
[Attr(isImmutable: true)]
39+
[NotMapped]
40+
public string GrantedVisaCountries
41+
{
42+
get => GrantedVisas == null ? null : string.Join(", ", GrantedVisas.Select(v => v.TargetCountry.Name));
43+
// The setter is required only for deserialization in unit tests.
44+
set { }
45+
}
46+
47+
[EagerLoad]
48+
public ICollection<Visa> GrantedVisas { get; set; }
1249
}
1350
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System;
2+
using JsonApiDotNetCore.Models;
3+
4+
namespace JsonApiDotNetCoreExample.Models
5+
{
6+
public class Visa
7+
{
8+
public int Id { get; set; }
9+
10+
public DateTime ExpiresAt { get; set; }
11+
12+
[EagerLoad]
13+
public Country TargetCountry { get; set; }
14+
}
15+
}

src/Examples/ReportsExample/ReportsExample.csproj

-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
</ItemGroup>
99

1010
<ItemGroup>
11-
<PackageReference Include="Dapper" Version="2.0.30" />
1211
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="$(NpgsqlPostgreSQLVersion)" />
1312
</ItemGroup>
1413
</Project>

src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs

+34-1
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,10 @@ public IResourceGraphBuilder AddResource(Type resourceType, Type idType = null,
7979
IdentityType = idType,
8080
Attributes = GetAttributes(entityType),
8181
Relationships = GetRelationships(entityType),
82+
EagerLoads = GetEagerLoads(entityType),
8283
ResourceDefinitionType = GetResourceDefinitionType(entityType)
8384
};
8485

85-
8686
protected virtual List<AttrAttribute> GetAttributes(Type entityType)
8787
{
8888
var attributes = new List<AttrAttribute>();
@@ -179,6 +179,39 @@ protected virtual List<RelationshipAttribute> GetRelationships(Type entityType)
179179
protected virtual Type GetRelationshipType(RelationshipAttribute relation, PropertyInfo prop) =>
180180
relation.IsHasMany ? prop.PropertyType.GetGenericArguments()[0] : prop.PropertyType;
181181

182+
private List<EagerLoadAttribute> GetEagerLoads(Type entityType, int recursionDepth = 0)
183+
{
184+
if (recursionDepth >= 500)
185+
{
186+
throw new InvalidOperationException("Infinite recursion detected in eager-load chain.");
187+
}
188+
189+
var attributes = new List<EagerLoadAttribute>();
190+
var properties = entityType.GetProperties();
191+
192+
foreach (var property in properties)
193+
{
194+
var attribute = (EagerLoadAttribute) property.GetCustomAttribute(typeof(EagerLoadAttribute));
195+
if (attribute == null) continue;
196+
197+
Type innerType = TypeOrElementType(property.PropertyType);
198+
attribute.Children = GetEagerLoads(innerType, recursionDepth + 1);
199+
attribute.Property = property;
200+
201+
attributes.Add(attribute);
202+
}
203+
204+
return attributes;
205+
}
206+
207+
private static Type TypeOrElementType(Type type)
208+
{
209+
var interfaces = type.GetInterfaces()
210+
.Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>)).ToArray();
211+
212+
return interfaces.Length == 1 ? interfaces.Single().GenericTypeArguments[0] : type;
213+
}
214+
182215
private Type GetResourceDefinitionType(Type entityType) => typeof(ResourceDefinition<>).MakeGenericType(entityType);
183216

184217
private void AssertEntityIsNotAlreadyDefined(Type entityType)

src/JsonApiDotNetCore/Data/DefaultResourceRepository.cs

+26-3
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,14 @@ public DefaultResourceRepository(
4747
}
4848

4949
/// <inheritdoc />
50-
public virtual IQueryable<TResource> Get() => _dbSet;
50+
public virtual IQueryable<TResource> Get()
51+
{
52+
var resourceContext = _resourceGraph.GetResourceContext<TResource>();
53+
return EagerLoad(_dbSet, resourceContext.EagerLoads);
54+
}
55+
5156
/// <inheritdoc />
52-
public virtual IQueryable<TResource> Get(TId id) => _dbSet.Where(e => e.Id.Equals(id));
57+
public virtual IQueryable<TResource> Get(TId id) => Get().Where(e => e.Id.Equals(id));
5358

5459
/// <inheritdoc />
5560
public virtual IQueryable<TResource> Select(IQueryable<TResource> entities, IEnumerable<AttrAttribute> fields = null)
@@ -279,6 +284,19 @@ public virtual async Task<bool> DeleteAsync(TId id)
279284
return true;
280285
}
281286

287+
private IQueryable<TResource> EagerLoad(IQueryable<TResource> entities, IEnumerable<EagerLoadAttribute> attributes, string chainPrefix = null)
288+
{
289+
foreach (var attribute in attributes)
290+
{
291+
string path = chainPrefix != null ? chainPrefix + "." + attribute.Property.Name : attribute.Property.Name;
292+
entities = entities.Include(path);
293+
294+
entities = EagerLoad(entities, attribute.Children, path);
295+
}
296+
297+
return entities;
298+
}
299+
282300
public virtual IQueryable<TResource> Include(IQueryable<TResource> entities, IEnumerable<RelationshipAttribute> inclusionChain = null)
283301
{
284302
if (inclusionChain == null || !inclusionChain.Any())
@@ -288,10 +306,15 @@ public virtual IQueryable<TResource> Include(IQueryable<TResource> entities, IEn
288306

289307
string internalRelationshipPath = null;
290308
foreach (var relationship in inclusionChain)
291-
internalRelationshipPath = (internalRelationshipPath == null)
309+
{
310+
internalRelationshipPath = internalRelationshipPath == null
292311
? relationship.RelationshipPath
293312
: $"{internalRelationshipPath}.{relationship.RelationshipPath}";
294313

314+
var resourceContext = _resourceGraph.GetResourceContext(relationship.RightType);
315+
entities = EagerLoad(entities, resourceContext.EagerLoads, internalRelationshipPath);
316+
}
317+
295318
return entities.Include(internalRelationshipPath);
296319
}
297320

src/JsonApiDotNetCore/Internal/ResourceContext.cs

+5
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ public class ResourceContext
4242
/// </summary>
4343
public List<RelationshipAttribute> Relationships { get; set; }
4444

45+
/// <summary>
46+
/// Related entities that are not exposed as resource relationships.
47+
/// </summary>
48+
public List<EagerLoadAttribute> EagerLoads { get; set; }
49+
4550
private List<IResourceField> _fields;
4651
public List<IResourceField> Fields { get { return _fields ??= Attributes.Cast<IResourceField>().Concat(Relationships).ToList(); } }
4752

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Reflection;
4+
5+
namespace JsonApiDotNetCore.Models
6+
{
7+
/// <summary>
8+
/// Used to unconditionally load a related entity that is not exposed as a json:api relationship.
9+
/// </summary>
10+
/// <remarks>
11+
/// This is intended for calculated properties that are exposed as json:api attributes, which depend on a related entity to always be loaded.
12+
/// <example><![CDATA[
13+
/// public class User : Identifiable
14+
/// {
15+
/// [Attr(isImmutable: true)]
16+
/// [NotMapped]
17+
/// public string DisplayName => Name.First + " " + Name.Last;
18+
///
19+
/// public Name Name { get; set; }
20+
/// }
21+
///
22+
/// public class Name // not exposed as resource, only database table
23+
/// {
24+
/// public string First { get; set; }
25+
/// public string Last { get; set; }
26+
/// }
27+
///
28+
/// public class Blog : Identifiable
29+
/// {
30+
/// [HasOne]
31+
/// public User Author { get; set; }
32+
/// }
33+
/// ]]></example>
34+
/// </remarks>
35+
public sealed class EagerLoadAttribute : Attribute
36+
{
37+
public PropertyInfo Property { get; internal set; }
38+
39+
public IList<EagerLoadAttribute> Children { get; internal set; }
40+
}
41+
}

test/IntegrationTests/IntegrationTests.csproj

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<ItemGroup>
1212
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="$(EFCoreVersion)" />
1313
<PackageReference Include="Moq" Version="$(MoqVersion)" />
14-
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
15-
<PackageReference Include="xunit.runner.visualstudio" Version="$(XUnitVersion)" />
14+
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
15+
<PackageReference Include="xunit.runner.visualstudio" Version="$(XUnitVersion)" />
1616
</ItemGroup>
1717
</Project>

0 commit comments

Comments
 (0)