Skip to content

Commit

Permalink
Eager loading (#701)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
nicolestandifer3 committed Mar 31, 2020
1 parent 24999c4 commit 3773cf9
Show file tree
Hide file tree
Showing 16 changed files with 406 additions and 44 deletions.
6 changes: 3 additions & 3 deletions benchmarks/Benchmarks.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.12.0" />
<PackageReference Include="moq" Version="$(MoqVersion)" />
<ProjectReference Include="..\src\JsonApiDotNetCore\JsonApiDotNetCore.csproj" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\src\JsonApiDotNetCore\JsonApiDotNetCore.csproj" />
<PackageReference Include="BenchmarkDotNet" Version="0.12.0" />
<PackageReference Include="Moq" Version="$(MoqVersion)" />
</ItemGroup>
</Project>
8 changes: 8 additions & 0 deletions src/Examples/JsonApiDotNetCoreExample/Models/Country.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace JsonApiDotNetCoreExample.Models
{
public class Country
{
public int Id { get; set; }
public string Name { get; set; }
}
}
39 changes: 38 additions & 1 deletion src/Examples/JsonApiDotNetCoreExample/Models/Passport.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,50 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using JsonApiDotNetCore.Models;

namespace JsonApiDotNetCoreExample.Models
{
public sealed class Passport : Identifiable
public class Passport : Identifiable
{
[Attr]
public int? SocialSecurityNumber { get; set; }

[Attr]
public bool IsLocked { get; set; }

[HasOne]
public Person Person { get; set; }

[Attr]
[NotMapped]
public string BirthCountryName
{
get => BirthCountry.Name;
set
{
if (BirthCountry == null)
{
BirthCountry = new Country();
}

BirthCountry.Name = value;
}
}

[EagerLoad]
public Country BirthCountry { get; set; }

[Attr(isImmutable: true)]
[NotMapped]
public string GrantedVisaCountries
{
get => GrantedVisas == null ? null : string.Join(", ", GrantedVisas.Select(v => v.TargetCountry.Name));
// The setter is required only for deserialization in unit tests.
set { }
}

[EagerLoad]
public ICollection<Visa> GrantedVisas { get; set; }
}
}
15 changes: 15 additions & 0 deletions src/Examples/JsonApiDotNetCoreExample/Models/Visa.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;
using JsonApiDotNetCore.Models;

namespace JsonApiDotNetCoreExample.Models
{
public class Visa
{
public int Id { get; set; }

public DateTime ExpiresAt { get; set; }

[EagerLoad]
public Country TargetCountry { get; set; }
}
}
1 change: 0 additions & 1 deletion src/Examples/ReportsExample/ReportsExample.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Dapper" Version="2.0.30" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="$(NpgsqlPostgreSQLVersion)" />
</ItemGroup>
</Project>
35 changes: 34 additions & 1 deletion src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,10 @@ public IResourceGraphBuilder AddResource(Type resourceType, Type idType = null,
IdentityType = idType,
Attributes = GetAttributes(entityType),
Relationships = GetRelationships(entityType),
EagerLoads = GetEagerLoads(entityType),
ResourceDefinitionType = GetResourceDefinitionType(entityType)
};


protected virtual List<AttrAttribute> GetAttributes(Type entityType)
{
var attributes = new List<AttrAttribute>();
Expand Down Expand Up @@ -179,6 +179,39 @@ protected virtual List<RelationshipAttribute> GetRelationships(Type entityType)
protected virtual Type GetRelationshipType(RelationshipAttribute relation, PropertyInfo prop) =>
relation.IsHasMany ? prop.PropertyType.GetGenericArguments()[0] : prop.PropertyType;

private List<EagerLoadAttribute> GetEagerLoads(Type entityType, int recursionDepth = 0)
{
if (recursionDepth >= 500)
{
throw new InvalidOperationException("Infinite recursion detected in eager-load chain.");
}

var attributes = new List<EagerLoadAttribute>();
var properties = entityType.GetProperties();

foreach (var property in properties)
{
var attribute = (EagerLoadAttribute) property.GetCustomAttribute(typeof(EagerLoadAttribute));
if (attribute == null) continue;

Type innerType = TypeOrElementType(property.PropertyType);
attribute.Children = GetEagerLoads(innerType, recursionDepth + 1);
attribute.Property = property;

attributes.Add(attribute);
}

return attributes;
}

private static Type TypeOrElementType(Type type)
{
var interfaces = type.GetInterfaces()
.Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>)).ToArray();

return interfaces.Length == 1 ? interfaces.Single().GenericTypeArguments[0] : type;
}

private Type GetResourceDefinitionType(Type entityType) => typeof(ResourceDefinition<>).MakeGenericType(entityType);

private void AssertEntityIsNotAlreadyDefined(Type entityType)
Expand Down
29 changes: 26 additions & 3 deletions src/JsonApiDotNetCore/Data/DefaultResourceRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,14 @@ public class DefaultResourceRepository<TResource, TId> : IResourceRepository<TRe
}

/// <inheritdoc />
public virtual IQueryable<TResource> Get() => _dbSet;
public virtual IQueryable<TResource> Get()
{
var resourceContext = _resourceGraph.GetResourceContext<TResource>();
return EagerLoad(_dbSet, resourceContext.EagerLoads);
}

/// <inheritdoc />
public virtual IQueryable<TResource> Get(TId id) => _dbSet.Where(e => e.Id.Equals(id));
public virtual IQueryable<TResource> Get(TId id) => Get().Where(e => e.Id.Equals(id));

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

private IQueryable<TResource> EagerLoad(IQueryable<TResource> entities, IEnumerable<EagerLoadAttribute> attributes, string chainPrefix = null)
{
foreach (var attribute in attributes)
{
string path = chainPrefix != null ? chainPrefix + "." + attribute.Property.Name : attribute.Property.Name;
entities = entities.Include(path);

entities = EagerLoad(entities, attribute.Children, path);
}

return entities;
}

public virtual IQueryable<TResource> Include(IQueryable<TResource> entities, IEnumerable<RelationshipAttribute> inclusionChain = null)
{
if (inclusionChain == null || !inclusionChain.Any())
Expand All @@ -288,10 +306,15 @@ public virtual IQueryable<TResource> Include(IQueryable<TResource> entities, IEn

string internalRelationshipPath = null;
foreach (var relationship in inclusionChain)
internalRelationshipPath = (internalRelationshipPath == null)
{
internalRelationshipPath = internalRelationshipPath == null
? relationship.RelationshipPath
: $"{internalRelationshipPath}.{relationship.RelationshipPath}";

var resourceContext = _resourceGraph.GetResourceContext(relationship.RightType);
entities = EagerLoad(entities, resourceContext.EagerLoads, internalRelationshipPath);
}

return entities.Include(internalRelationshipPath);
}

Expand Down
5 changes: 5 additions & 0 deletions src/JsonApiDotNetCore/Internal/ResourceContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ public class ResourceContext
/// </summary>
public List<RelationshipAttribute> Relationships { get; set; }

/// <summary>
/// Related entities that are not exposed as resource relationships.
/// </summary>
public List<EagerLoadAttribute> EagerLoads { get; set; }

private List<IResourceField> _fields;
public List<IResourceField> Fields { get { return _fields ??= Attributes.Cast<IResourceField>().Concat(Relationships).ToList(); } }

Expand Down
41 changes: 41 additions & 0 deletions src/JsonApiDotNetCore/Models/Annotation/EagerLoadAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Reflection;

namespace JsonApiDotNetCore.Models
{
/// <summary>
/// Used to unconditionally load a related entity that is not exposed as a json:api relationship.
/// </summary>
/// <remarks>
/// This is intended for calculated properties that are exposed as json:api attributes, which depend on a related entity to always be loaded.
/// <example><![CDATA[
/// public class User : Identifiable
/// {
/// [Attr(isImmutable: true)]
/// [NotMapped]
/// public string DisplayName => Name.First + " " + Name.Last;
///
/// public Name Name { get; set; }
/// }
///
/// public class Name // not exposed as resource, only database table
/// {
/// public string First { get; set; }
/// public string Last { get; set; }
/// }
///
/// public class Blog : Identifiable
/// {
/// [HasOne]
/// public User Author { get; set; }
/// }
/// ]]></example>
/// </remarks>
public sealed class EagerLoadAttribute : Attribute
{
public PropertyInfo Property { get; internal set; }

public IList<EagerLoadAttribute> Children { get; internal set; }
}
}
4 changes: 2 additions & 2 deletions test/IntegrationTests/IntegrationTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="$(EFCoreVersion)" />
<PackageReference Include="Moq" Version="$(MoqVersion)" />
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
<PackageReference Include="xunit.runner.visualstudio" Version="$(XUnitVersion)" />
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
<PackageReference Include="xunit.runner.visualstudio" Version="$(XUnitVersion)" />
</ItemGroup>
</Project>
Loading

0 comments on commit 3773cf9

Please sign in to comment.