From 3773cf9dd31050991562cb0ccf546d569a4a5fdd Mon Sep 17 00:00:00 2001 From: Nicole Standifer Date: Tue, 31 Mar 2020 12:25:19 +0200 Subject: [PATCH] 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 --- benchmarks/Benchmarks.csproj | 6 +- .../Models/Country.cs | 8 + .../Models/Passport.cs | 39 +++- .../JsonApiDotNetCoreExample/Models/Visa.cs | 15 ++ .../ReportsExample/ReportsExample.csproj | 1 - .../Builders/ResourceGraphBuilder.cs | 35 +++- .../Data/DefaultResourceRepository.cs | 29 ++- .../Internal/ResourceContext.cs | 5 + .../Models/Annotation/EagerLoadAttribute.cs | 41 +++++ test/IntegrationTests/IntegrationTests.csproj | 4 +- .../Acceptance/Spec/EagerLoadTests.cs | 169 ++++++++++++++++++ .../Acceptance/Spec/EndToEndTest.cs | 3 +- .../Acceptance/Spec/NestedResourceTests.cs | 34 ++-- .../Helpers/Models/PassportClient.cs | 17 ++ .../ResourceHooks/ResourceHooksTestsSetup.cs | 32 ++-- test/UnitTests/UnitTests.csproj | 12 +- 16 files changed, 406 insertions(+), 44 deletions(-) create mode 100644 src/Examples/JsonApiDotNetCoreExample/Models/Country.cs create mode 100644 src/Examples/JsonApiDotNetCoreExample/Models/Visa.cs create mode 100644 src/JsonApiDotNetCore/Models/Annotation/EagerLoadAttribute.cs create mode 100644 test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EagerLoadTests.cs create mode 100644 test/JsonApiDotNetCoreExampleTests/Helpers/Models/PassportClient.cs 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 class DefaultResourceRepository : IResourceRepository - 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 @@ protected DbContextOptions InitInMemoryDb(Action seeder Mock processorFactory, IResourceHookContainer modelResource, IHooksDiscovery discovery, - AppDbContext dbContext = null + AppDbContext dbContext = null, + IResourceGraph resourceGraph = null ) where TModel : class, IIdentifiable { @@ -329,7 +342,7 @@ protected DbContextOptions InitInMemoryDb(Action seeder 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 @@ protected DbContextOptions InitInMemoryDb(Action seeder } 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 - -