diff --git a/benchmarks/Benchmarks.csproj b/benchmarks/Benchmarks.csproj
index f57e4c7d..49761638 100644
--- a/benchmarks/Benchmarks.csproj
+++ b/benchmarks/Benchmarks.csproj
@@ -5,11 +5,11 @@
-
-
+
-
+
+
diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Country.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Country.cs
new file mode 100644
index 00000000..3e81c1a5
--- /dev/null
+++ b/src/Examples/JsonApiDotNetCoreExample/Models/Country.cs
@@ -0,0 +1,8 @@
+namespace JsonApiDotNetCoreExample.Models
+{
+ public class Country
+ {
+ public int Id { get; set; }
+ public string Name { get; set; }
+ }
+}
diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Passport.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Passport.cs
index e7d9336a..6e6debd8 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Models/Passport.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Models/Passport.cs
@@ -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 GrantedVisas { get; set; }
}
}
diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Visa.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Visa.cs
new file mode 100644
index 00000000..a7b31743
--- /dev/null
+++ b/src/Examples/JsonApiDotNetCoreExample/Models/Visa.cs
@@ -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; }
+ }
+}
diff --git a/src/Examples/ReportsExample/ReportsExample.csproj b/src/Examples/ReportsExample/ReportsExample.csproj
index 0bf022bb..e1b25693 100644
--- a/src/Examples/ReportsExample/ReportsExample.csproj
+++ b/src/Examples/ReportsExample/ReportsExample.csproj
@@ -8,7 +8,6 @@
-
diff --git a/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs
index 2f98f7e6..e012f17c 100644
--- a/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs
+++ b/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs
@@ -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 GetAttributes(Type entityType)
{
var attributes = new List();
@@ -179,6 +179,39 @@ protected virtual List GetRelationships(Type entityType)
protected virtual Type GetRelationshipType(RelationshipAttribute relation, PropertyInfo prop) =>
relation.IsHasMany ? prop.PropertyType.GetGenericArguments()[0] : prop.PropertyType;
+ private List GetEagerLoads(Type entityType, int recursionDepth = 0)
+ {
+ if (recursionDepth >= 500)
+ {
+ throw new InvalidOperationException("Infinite recursion detected in eager-load chain.");
+ }
+
+ var attributes = new List();
+ 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)
diff --git a/src/JsonApiDotNetCore/Data/DefaultResourceRepository.cs b/src/JsonApiDotNetCore/Data/DefaultResourceRepository.cs
index de6fdeeb..2b082596 100644
--- a/src/JsonApiDotNetCore/Data/DefaultResourceRepository.cs
+++ b/src/JsonApiDotNetCore/Data/DefaultResourceRepository.cs
@@ -47,9 +47,14 @@ public DefaultResourceRepository(
}
///
- public virtual IQueryable Get() => _dbSet;
+ public virtual IQueryable Get()
+ {
+ var resourceContext = _resourceGraph.GetResourceContext();
+ return EagerLoad(_dbSet, resourceContext.EagerLoads);
+ }
+
///
- public virtual IQueryable Get(TId id) => _dbSet.Where(e => e.Id.Equals(id));
+ public virtual IQueryable Get(TId id) => Get().Where(e => e.Id.Equals(id));
///
public virtual IQueryable Select(IQueryable entities, IEnumerable fields = null)
@@ -279,6 +284,19 @@ public virtual async Task DeleteAsync(TId id)
return true;
}
+ private IQueryable EagerLoad(IQueryable entities, IEnumerable 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 Include(IQueryable entities, IEnumerable inclusionChain = null)
{
if (inclusionChain == null || !inclusionChain.Any())
@@ -288,10 +306,15 @@ public virtual IQueryable Include(IQueryable 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);
}
diff --git a/src/JsonApiDotNetCore/Internal/ResourceContext.cs b/src/JsonApiDotNetCore/Internal/ResourceContext.cs
index 3aaad31f..2ebf4c1d 100644
--- a/src/JsonApiDotNetCore/Internal/ResourceContext.cs
+++ b/src/JsonApiDotNetCore/Internal/ResourceContext.cs
@@ -42,6 +42,11 @@ public class ResourceContext
///
public List Relationships { get; set; }
+ ///
+ /// Related entities that are not exposed as resource relationships.
+ ///
+ public List EagerLoads { get; set; }
+
private List _fields;
public List Fields { get { return _fields ??= Attributes.Cast().Concat(Relationships).ToList(); } }
diff --git a/src/JsonApiDotNetCore/Models/Annotation/EagerLoadAttribute.cs b/src/JsonApiDotNetCore/Models/Annotation/EagerLoadAttribute.cs
new file mode 100644
index 00000000..50c2d9e7
--- /dev/null
+++ b/src/JsonApiDotNetCore/Models/Annotation/EagerLoadAttribute.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+
+namespace JsonApiDotNetCore.Models
+{
+ ///
+ /// Used to unconditionally load a related entity that is not exposed as a json:api relationship.
+ ///
+ ///
+ /// This is intended for calculated properties that are exposed as json:api attributes, which depend on a related entity to always be loaded.
+ /// 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; }
+ /// }
+ /// ]]>
+ ///
+ public sealed class EagerLoadAttribute : Attribute
+ {
+ public PropertyInfo Property { get; internal set; }
+
+ public IList Children { get; internal set; }
+ }
+}
diff --git a/test/IntegrationTests/IntegrationTests.csproj b/test/IntegrationTests/IntegrationTests.csproj
index 3f68195a..54ad8b5f 100644
--- a/test/IntegrationTests/IntegrationTests.csproj
+++ b/test/IntegrationTests/IntegrationTests.csproj
@@ -11,7 +11,7 @@
-
-
+
+
diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EagerLoadTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EagerLoadTests.cs
new file mode 100644
index 00000000..2b795d6c
--- /dev/null
+++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EagerLoadTests.cs
@@ -0,0 +1,169 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+using Bogus;
+using JsonApiDotNetCoreExample.Models;
+using JsonApiDotNetCoreExampleTests.Helpers.Models;
+using Xunit;
+using Person = JsonApiDotNetCoreExample.Models.Person;
+
+namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec
+{
+ public class EagerLoadTests : FunctionalTestCollection
+ {
+ private readonly Faker _personFaker;
+ private readonly Faker _passportFaker;
+ private readonly Faker _countryFaker;
+ private readonly Faker _todoItemFaker;
+ private readonly Faker _visaFaker;
+
+ public EagerLoadTests(StandardApplicationFactory factory) : base(factory)
+ {
+ _todoItemFaker = new Faker()
+ .RuleFor(t => t.Description, f => f.Lorem.Sentence())
+ .RuleFor(t => t.Ordinal, f => f.Random.Number())
+ .RuleFor(t => t.CreatedDate, f => f.Date.Past());
+ _personFaker = new Faker()
+ .RuleFor(t => t.FirstName, f => f.Name.FirstName())
+ .RuleFor(t => t.LastName, f => f.Name.LastName());
+ _passportFaker = new Faker()
+ .RuleFor(t => t.SocialSecurityNumber, f => f.Random.Number(100, 10_000));
+ _countryFaker = new Faker()
+ .RuleFor(c => c.Name, f => f.Address.Country());
+ _visaFaker = new Faker()
+ .RuleFor(v => v.ExpiresAt, f => f.Date.Future());
+ }
+
+ [Fact]
+ public async Task GetSingleResource_TopLevel_AppliesEagerLoad()
+ {
+ // Arrange
+ var passport = _passportFaker.Generate();
+ passport.BirthCountry = _countryFaker.Generate();
+
+ var visa1 = _visaFaker.Generate();
+ visa1.TargetCountry = _countryFaker.Generate();
+
+ var visa2 = _visaFaker.Generate();
+ visa2.TargetCountry = _countryFaker.Generate();
+
+ passport.GrantedVisas = new List { visa1, visa2 };
+
+ _dbContext.Add(passport);
+ _dbContext.SaveChanges();
+
+ // Act
+ var (body, response) = await Get($"/api/v1/passports/{passport.Id}");
+
+ // Assert
+ AssertEqualStatusCode(HttpStatusCode.OK, response);
+
+ var resultPassport = _deserializer.DeserializeSingle(body).Data;
+ Assert.Equal(passport.Id, resultPassport.Id);
+ Assert.Equal(passport.BirthCountry.Name, resultPassport.BirthCountryName);
+ Assert.Equal(visa1.TargetCountry.Name + ", " + visa2.TargetCountry.Name, resultPassport.GrantedVisaCountries);
+ }
+
+ [Fact]
+ public async Task GetMultiResource_Nested_AppliesEagerLoad()
+ {
+ // Arrange
+ var person = _personFaker.Generate();
+ person.Passport = _passportFaker.Generate();
+ person.Passport.BirthCountry = _countryFaker.Generate();
+
+ _dbContext.People.RemoveRange(_dbContext.People);
+ _dbContext.Add(person);
+ _dbContext.SaveChanges();
+
+ // Act
+ var (body, response) = await Get($"/api/v1/people?include=passport");
+
+ // Assert
+ AssertEqualStatusCode(HttpStatusCode.OK, response);
+
+ var resultPerson = _deserializer.DeserializeList(body).Data.Single();
+ Assert.Equal(person.Id, resultPerson.Id);
+ Assert.Equal(person.Passport.Id, resultPerson.Passport.Id);
+ Assert.Equal(person.Passport.BirthCountryName, resultPerson.Passport.BirthCountry.Name);
+ }
+
+ [Fact]
+ public async Task GetMultiResource_DeeplyNested_AppliesEagerLoad()
+ {
+ // Arrange
+ var todo = _todoItemFaker.Generate();
+ todo.Assignee = _personFaker.Generate();
+ todo.Owner = _personFaker.Generate();;
+ todo.Owner.Passport = _passportFaker.Generate();
+ todo.Owner.Passport.BirthCountry = _countryFaker.Generate();
+
+ _dbContext.Add(todo);
+ _dbContext.SaveChanges();
+
+ // Act
+ var (body, response) = await Get($"/api/v1/people/{todo.Assignee.Id}/assignedTodoItems?include=owner.passport");
+
+ // Assert
+ AssertEqualStatusCode(HttpStatusCode.OK, response);
+
+ var resultTodoItem = _deserializer.DeserializeList(body).Data.Single();
+ Assert.Equal(todo.Owner.Passport.BirthCountryName, resultTodoItem.Owner.Passport.BirthCountry.Name);
+ }
+
+ [Fact]
+ public async Task PostSingleResource_TopLevel_AppliesEagerLoad()
+ {
+ // Arrange
+ var passport = _passportFaker.Generate();
+ passport.BirthCountry = _countryFaker.Generate();
+
+ var serializer = GetSerializer(p => new { p.SocialSecurityNumber, p.BirthCountryName });
+ var content = serializer.Serialize(passport);
+
+ // Act
+ var (body, response) = await Post($"/api/v1/passports", content);
+
+ // Assert
+ AssertEqualStatusCode(HttpStatusCode.Created, response);
+
+ var resultPassport = _deserializer.DeserializeSingle(body).Data;
+ Assert.Equal(passport.SocialSecurityNumber, resultPassport.SocialSecurityNumber);
+ Assert.Equal(passport.BirthCountry.Name, resultPassport.BirthCountryName);
+ Assert.Null(resultPassport.GrantedVisaCountries);
+ }
+
+ [Fact]
+ public async Task PatchResource_TopLevel_AppliesEagerLoad()
+ {
+ // Arrange
+ var passport = _passportFaker.Generate();
+ passport.BirthCountry = _countryFaker.Generate();
+ var visa = _visaFaker.Generate();
+ visa.TargetCountry = _countryFaker.Generate();
+ passport.GrantedVisas = new List { visa };
+
+ _dbContext.Add(passport);
+ _dbContext.SaveChanges();
+
+ passport.SocialSecurityNumber = _passportFaker.Generate().SocialSecurityNumber;
+ passport.BirthCountry.Name = _countryFaker.Generate().Name;
+
+ var serializer = GetSerializer(p => new { p.SocialSecurityNumber, p.BirthCountryName });
+ var content = serializer.Serialize(passport);
+
+ // Act
+ var (body, response) = await Patch($"/api/v1/passports/{passport.Id}", content);
+
+ // Assert
+ AssertEqualStatusCode(HttpStatusCode.OK, response);
+
+ var resultPassport = _deserializer.DeserializeSingle(body).Data;
+ Assert.Equal(passport.Id, resultPassport.Id);
+ Assert.Equal(passport.SocialSecurityNumber, resultPassport.SocialSecurityNumber);
+ Assert.Equal(passport.BirthCountry.Name, resultPassport.BirthCountryName);
+ Assert.Equal(passport.GrantedVisas.First().TargetCountry.Name, resultPassport.GrantedVisaCountries);
+ }
+ }
+}
diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs
index e3cb86dd..99286f3d 100644
--- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs
+++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs
@@ -78,7 +78,7 @@ protected IResponseDeserializer GetDeserializer()
var builder = new ResourceGraphBuilder(formatter);
foreach (var rc in resourcesContexts)
{
- if (rc.ResourceType == typeof(TodoItem) || rc.ResourceType == typeof(TodoItemCollection))
+ if (rc.ResourceType == typeof(TodoItem) || rc.ResourceType == typeof(TodoItemCollection) || rc.ResourceType == typeof(Passport))
{
continue;
}
@@ -86,6 +86,7 @@ protected IResponseDeserializer GetDeserializer()
}
builder.AddResource(formatter.FormatResourceName(typeof(TodoItem)));
builder.AddResource(formatter.FormatResourceName(typeof(TodoItemCollection)));
+ builder.AddResource(formatter.FormatResourceName(typeof(Passport)));
return new ResponseDeserializer(builder.Build());
}
diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/NestedResourceTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/NestedResourceTests.cs
index 222ec684..5fbd8f7e 100644
--- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/NestedResourceTests.cs
+++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/NestedResourceTests.cs
@@ -14,40 +14,42 @@ public sealed class NestedResourceTests : FunctionalTestCollection _todoItemFaker;
private readonly Faker _personFaker;
private readonly Faker _passportFaker;
+ private readonly Faker _countryFaker;
public NestedResourceTests(StandardApplicationFactory factory) : base(factory)
{
_todoItemFaker = new Faker()
- .RuleFor(t => t.Description, f => f.Lorem.Sentence())
- .RuleFor(t => t.Ordinal, f => f.Random.Number())
- .RuleFor(t => t.CreatedDate, f => f.Date.Past());
+ .RuleFor(t => t.Description, f => f.Lorem.Sentence())
+ .RuleFor(t => t.Ordinal, f => f.Random.Number())
+ .RuleFor(t => t.CreatedDate, f => f.Date.Past());
_personFaker = new Faker()
- .RuleFor(t => t.FirstName, f => f.Name.FirstName())
- .RuleFor(t => t.LastName, f => f.Name.LastName());
+ .RuleFor(t => t.FirstName, f => f.Name.FirstName())
+ .RuleFor(t => t.LastName, f => f.Name.LastName());
_passportFaker = new Faker()
- .RuleFor(t => t.SocialSecurityNumber, f => f.Random.Number());
+ .RuleFor(t => t.SocialSecurityNumber, f => f.Random.Number(100, 10_000));
+ _countryFaker = new Faker()
+ .RuleFor(c => c.Name, f => f.Address.Country());
}
[Fact]
public async Task NestedResourceRoute_RequestWithIncludeQueryParam_ReturnsRequestedRelationships()
{
// Arrange
- var assignee = _dbContext.Add(_personFaker.Generate()).Entity;
- var todo = _dbContext.Add(_todoItemFaker.Generate()).Entity;
- var owner = _dbContext.Add(_personFaker.Generate()).Entity;
- var passport = _dbContext.Add(_passportFaker.Generate()).Entity;
- _dbContext.SaveChanges();
- todo.AssigneeId = assignee.Id;
- todo.OwnerId = owner.Id;
- owner.PassportId = passport.Id;
+ var todo = _todoItemFaker.Generate();
+ todo.Assignee = _personFaker.Generate();
+ todo.Owner = _personFaker.Generate();
+ todo.Owner.Passport = _passportFaker.Generate();
+ todo.Owner.Passport.BirthCountry = _countryFaker.Generate();
+
+ _dbContext.Add(todo);
_dbContext.SaveChanges();
// Act
- var (body, response) = await Get($"/api/v1/people/{assignee.Id}/assignedTodoItems?include=owner.passport");
+ var (body, response) = await Get($"/api/v1/people/{todo.Assignee.Id}/assignedTodoItems?include=owner.passport");
// Assert
AssertEqualStatusCode(HttpStatusCode.OK, response);
- var resultTodoItem = _deserializer.DeserializeList(body).Data.SingleOrDefault();
+ var resultTodoItem = _deserializer.DeserializeList(body).Data.Single();
Assert.Equal(todo.Id, resultTodoItem.Id);
Assert.Equal(todo.Owner.Id, resultTodoItem.Owner.Id);
Assert.Equal(todo.Owner.Passport.Id, resultTodoItem.Owner.Passport.Id);
diff --git a/test/JsonApiDotNetCoreExampleTests/Helpers/Models/PassportClient.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Models/PassportClient.cs
new file mode 100644
index 00000000..af739445
--- /dev/null
+++ b/test/JsonApiDotNetCoreExampleTests/Helpers/Models/PassportClient.cs
@@ -0,0 +1,17 @@
+using JsonApiDotNetCore.Models;
+using JsonApiDotNetCoreExample.Models;
+
+namespace JsonApiDotNetCoreExampleTests.Helpers.Models
+{
+ ///
+ /// this "client" version of the is required because the
+ /// base property that is overridden here does not have a setter. For a model
+ /// defined on a json:api client, it would not make sense to have an exposed attribute
+ /// without a setter.
+ ///
+ public class PassportClient : Passport
+ {
+ [Attr]
+ public new string GrantedVisaCountries { get; set; }
+ }
+}
diff --git a/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs b/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs
index a040c725..0b3751e0 100644
--- a/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs
+++ b/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs
@@ -13,6 +13,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using JsonApiDotNetCore.Graph;
using Person = JsonApiDotNetCoreExample.Models.Person;
using JsonApiDotNetCore.Internal.Contracts;
using JsonApiDotNetCore.Serialization;
@@ -186,8 +187,13 @@ public class HooksTestsSetup : HooksDummyData
var dbContext = repoDbContextOptions != null ? new AppDbContext(repoDbContextOptions) : null;
- SetupProcessorFactoryForResourceDefinition(gpfMock, mainResource.Object, mainDiscovery, dbContext);
- SetupProcessorFactoryForResourceDefinition(gpfMock, nestedResource.Object, nestedDiscovery, dbContext);
+ var resourceGraph = new ResourceGraphBuilder()
+ .AddResource()
+ .AddResource()
+ .Build();
+
+ SetupProcessorFactoryForResourceDefinition(gpfMock, mainResource.Object, mainDiscovery, dbContext, resourceGraph);
+ SetupProcessorFactoryForResourceDefinition(gpfMock, nestedResource.Object, nestedDiscovery, dbContext, resourceGraph);
var execHelper = new HookExecutorHelper(gpfMock.Object, options);
var traversalHelper = new TraversalHelper(_resourceGraph, ufMock.Object);
@@ -217,9 +223,15 @@ public class HooksTestsSetup : HooksDummyData
var dbContext = repoDbContextOptions != null ? new AppDbContext(repoDbContextOptions) : null;
- SetupProcessorFactoryForResourceDefinition(gpfMock, mainResource.Object, mainDiscovery, dbContext);
- SetupProcessorFactoryForResourceDefinition(gpfMock, firstNestedResource.Object, firstNestedDiscovery, dbContext);
- SetupProcessorFactoryForResourceDefinition(gpfMock, secondNestedResource.Object, secondNestedDiscovery, dbContext);
+ var resourceGraph = new ResourceGraphBuilder()
+ .AddResource()
+ .AddResource()
+ .AddResource()
+ .Build();
+
+ SetupProcessorFactoryForResourceDefinition(gpfMock, mainResource.Object, mainDiscovery, dbContext, resourceGraph);
+ SetupProcessorFactoryForResourceDefinition(gpfMock, firstNestedResource.Object, firstNestedDiscovery, dbContext, resourceGraph);
+ SetupProcessorFactoryForResourceDefinition(gpfMock, secondNestedResource.Object, secondNestedDiscovery, dbContext, resourceGraph);
var execHelper = new HookExecutorHelper(gpfMock.Object, options);
var traversalHelper = new TraversalHelper(_resourceGraph, ufMock.Object);
@@ -314,7 +326,8 @@ private void SetupProcessorFactoryForResourceDefinition(
Mock processorFactory,
IResourceHookContainer modelResource,
IHooksDiscovery discovery,
- AppDbContext dbContext = null
+ AppDbContext dbContext = null,
+ IResourceGraph resourceGraph = null
)
where TModel : class, IIdentifiable
{
@@ -329,7 +342,7 @@ private void SetupProcessorFactoryForResourceDefinition(
var idType = TypeHelper.GetIdentifierType();
if (idType == typeof(int))
{
- IResourceReadRepository repo = CreateTestRepository(dbContext);
+ IResourceReadRepository repo = CreateTestRepository(dbContext, resourceGraph);
processorFactory.Setup(c => c.Get>(typeof(IResourceReadRepository<,>), typeof(TModel), typeof(int))).Returns(repo);
}
else
@@ -341,11 +354,11 @@ private void SetupProcessorFactoryForResourceDefinition(
}
private IResourceReadRepository CreateTestRepository(
- AppDbContext dbContext
+ AppDbContext dbContext, IResourceGraph resourceGraph
) where TModel : class, IIdentifiable
{
IDbContextResolver resolver = CreateTestDbResolver(dbContext);
- return new DefaultResourceRepository(null, resolver, null, null, NullLoggerFactory.Instance);
+ return new DefaultResourceRepository(null, resolver, resourceGraph, null, NullLoggerFactory.Instance);
}
private IDbContextResolver CreateTestDbResolver(AppDbContext dbContext) where TModel : class, IIdentifiable
@@ -395,4 +408,3 @@ protected List GetIncludedRelationshipsChain(string chain
}
}
}
-
diff --git a/test/UnitTests/UnitTests.csproj b/test/UnitTests/UnitTests.csproj
index 72a6cd98..6a49628d 100644
--- a/test/UnitTests/UnitTests.csproj
+++ b/test/UnitTests/UnitTests.csproj
@@ -3,6 +3,12 @@
$(NetCoreAppVersion)
+
+
+ PreserveNewest
+
+
+
@@ -15,10 +21,4 @@
-
-
-
- PreserveNewest
-
-