Skip to content

Eager loading #701

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Mar 31, 2020
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 DefaultResourceRepository(
}

/// <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