From c0d404dff18bd82cfe4ca642875e187afedd8e58 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 4 Mar 2020 12:20:46 +0100 Subject: [PATCH 01/11] Added eager loading of one-to-one and one-to-many entity relations --- .../Models/Country.cs | 8 + .../Models/Passport.cs | 45 ++++- .../JsonApiDotNetCoreExample/Models/Visa.cs | 11 ++ .../Builders/ResourceGraphBuilder.cs | 19 +- .../Data/DefaultResourceRepository.cs | 28 ++- .../Data/IResourceReadRepository.cs | 4 + .../Internal/ResourceContext.cs | 5 + .../Models/Annotation/EagerLoadAttribute.cs | 38 ++++ .../Acceptance/Spec/EagerLoadTests.cs | 164 ++++++++++++++++++ .../Acceptance/Spec/EndToEndTest.cs | 3 +- .../Acceptance/Spec/NestedResourceTests.cs | 36 ++-- .../Helpers/Models/PassportClient.cs | 17 ++ .../ResourceHooks/ResourceHooksTestsSetup.cs | 31 +++- 13 files changed, 374 insertions(+), 35 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/src/Examples/JsonApiDotNetCoreExample/Models/Country.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Country.cs new file mode 100644 index 0000000000..3e81c1a51e --- /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 8775ecbab5..75c51ebf43 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 class Passport : Identifiable { - public virtual int? SocialSecurityNumber { get; set; } - public virtual bool IsLocked { get; set; } + [Attr] + public int? SocialSecurityNumber { get; set; } + + [Attr] + public bool IsLocked { get; set; } [HasOne] - public virtual Person Person { get; set; } + 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.CountryCode)); + // The setter is required only for deserialization in unit tests. + set { } + } + + [EagerLoad] + public ICollection GrantedVisas { get; set; } } -} \ No newline at end of file +} diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Visa.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Visa.cs new file mode 100644 index 0000000000..ce2d7b19ad --- /dev/null +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Visa.cs @@ -0,0 +1,11 @@ +using System; + +namespace JsonApiDotNetCoreExample.Models +{ + public class Visa + { + public int Id { get; set; } + public string CountryCode { get; set; } + public DateTime ExpiresAt { get; set; } + } +} diff --git a/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs index f9d8c9da13..75922ff1f2 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,23 @@ 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) + { + var attributes = new List(); + var properties = entityType.GetProperties(); + + foreach (var property in properties) + { + var attribute = (EagerLoadAttribute)property.GetCustomAttribute(typeof(EagerLoadAttribute)); + if (attribute == null) continue; + + attribute.Property = property; + attributes.Add(attribute); + } + + return attributes; + } + 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 f03e3e4f24..18abbde6fe 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,18 @@ public virtual async Task DeleteAsync(TId id) return true; } + public virtual IQueryable EagerLoad(IQueryable entities, IEnumerable attributes, string chainPrefix = null) + { + foreach (var attribute in attributes) + { + entities = chainPrefix != null + ? entities.Include(chainPrefix + "." + attribute.Property.Name) + : entities.Include(attribute.Property.Name); + } + + return entities; + } + public virtual IQueryable Include(IQueryable entities, IEnumerable inclusionChain = null) { if (inclusionChain == null || !inclusionChain.Any()) @@ -288,10 +305,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/Data/IResourceReadRepository.cs b/src/JsonApiDotNetCore/Data/IResourceReadRepository.cs index 63867c6bea..d8ba2477e1 100644 --- a/src/JsonApiDotNetCore/Data/IResourceReadRepository.cs +++ b/src/JsonApiDotNetCore/Data/IResourceReadRepository.cs @@ -28,6 +28,10 @@ public interface IResourceReadRepository /// IQueryable Select(IQueryable entities, IEnumerable fields); /// + /// Include related entities that are not exposed as json:api relationships. + /// + IQueryable EagerLoad(IQueryable entities, IEnumerable attributes, string chainPrefix = null); + /// /// Include a relationship in the query /// /// diff --git a/src/JsonApiDotNetCore/Internal/ResourceContext.cs b/src/JsonApiDotNetCore/Internal/ResourceContext.cs index 15ab4b4c7a..2be0a0747d 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 = _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 0000000000..56b8186eba --- /dev/null +++ b/src/JsonApiDotNetCore/Models/Annotation/EagerLoadAttribute.cs @@ -0,0 +1,38 @@ +using System; +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; } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EagerLoadTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EagerLoadTests.cs new file mode 100644 index 0000000000..3295e40be2 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EagerLoadTests.cs @@ -0,0 +1,164 @@ +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()) + .RuleFor(v => v.CountryCode, f => f.Address.CountryCode()); + } + + [Fact] + public async Task GetSingleResource_TopLevel_AppliesEagerLoad() + { + // Arrange + var passport = _passportFaker.Generate(); + passport.BirthCountry = _countryFaker.Generate(); + + var visa1 = _visaFaker.Generate(); + var visa2 = _visaFaker.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.CountryCode + ", " + visa2.CountryCode, 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(); + passport.GrantedVisas = new List { _visaFaker.Generate() }; + + _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().CountryCode, resultPassport.GrantedVisaCountries); + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs index e3cb86dd9d..99286f3d64 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 e023590da5..aef2fc05ea 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/NestedResourceTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/NestedResourceTests.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using System.Net; using System.Threading.Tasks; using Bogus; @@ -14,40 +14,42 @@ public 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 0000000000..af739445d3 --- /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 28f35f2c83..50f2864582 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 @@ 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 @@ 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 @@ void SetupProcessorFactoryForResourceDefinition( } 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); } IDbContextResolver CreateTestDbResolver(AppDbContext dbContext) where TModel : class, IIdentifiable From d33b03cf9fd32d3b24abd9d4f1da550286c37222 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 4 Mar 2020 12:20:56 +0100 Subject: [PATCH 02/11] Removed dead code --- .../AuthorizedTodoItemsRepository.cs | 31 ------------------- .../JsonApiDotNetCoreExampleTests.csproj | 10 ------ 2 files changed, 41 deletions(-) delete mode 100644 test/JsonApiDotNetCoreExampleTests/Helpers/Repositories/AuthorizedTodoItemsRepository.cs diff --git a/test/JsonApiDotNetCoreExampleTests/Helpers/Repositories/AuthorizedTodoItemsRepository.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Repositories/AuthorizedTodoItemsRepository.cs deleted file mode 100644 index 4ce5da35d8..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/Helpers/Repositories/AuthorizedTodoItemsRepository.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Linq; -using JsonApiDotNetCore.Data; -using JsonApiDotNetCore.Services; -using JsonApiDotNetCoreExample.Models; -using JsonApiDotNetCoreExampleTests.Services; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCoreExampleTests.Repositories -{ - public class AuthorizedTodoItemsRepository : DefaultEntityRepository - { - private readonly ILogger _logger; - private readonly IAuthorizationService _authService; - - public AuthorizedTodoItemsRepository( - ILoggerFactory loggerFactory, - IJsonApiContext jsonApiContext, - IDbContextResolver contextResolver, - IAuthorizationService authService) - : base(loggerFactory, jsonApiContext, contextResolver) - { - _logger = loggerFactory.CreateLogger(); - _authService = authService; - } - - public override IQueryable Get() - { - return base.Get().Where(todoItem => todoItem.OwnerId == _authService.CurrentUserId); - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj b/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj index 919ccc48ac..4b4e7a61f4 100644 --- a/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj +++ b/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj @@ -27,14 +27,4 @@ - - - - - - - - - - From 5a0c835774658a633d641933d019a45cfba7d520 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 4 Mar 2020 13:49:25 +0100 Subject: [PATCH 03/11] Fixed: Eagerloads can have nested Eagerload properties --- .../JsonApiDotNetCoreExample/Models/Passport.cs | 2 +- .../JsonApiDotNetCoreExample/Models/Visa.cs | 6 +++++- .../Builders/ResourceGraphBuilder.cs | 11 +++++++++++ .../Data/DefaultResourceRepository.cs | 7 ++++--- .../Models/Annotation/EagerLoadAttribute.cs | 3 +++ .../Acceptance/Spec/EagerLoadTests.cs | 15 ++++++++++----- 6 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Passport.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Passport.cs index 75c51ebf43..6e6debd819 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Passport.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Passport.cs @@ -39,7 +39,7 @@ public string BirthCountryName [NotMapped] public string GrantedVisaCountries { - get => GrantedVisas == null ? null : string.Join(", ", GrantedVisas.Select(v => v.CountryCode)); + get => GrantedVisas == null ? null : string.Join(", ", GrantedVisas.Select(v => v.TargetCountry.Name)); // The setter is required only for deserialization in unit tests. set { } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Visa.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Visa.cs index ce2d7b19ad..a7b31743e2 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Visa.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Visa.cs @@ -1,11 +1,15 @@ using System; +using JsonApiDotNetCore.Models; namespace JsonApiDotNetCoreExample.Models { public class Visa { public int Id { get; set; } - public string CountryCode { get; set; } + public DateTime ExpiresAt { get; set; } + + [EagerLoad] + public Country TargetCountry { get; set; } } } diff --git a/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs index 75922ff1f2..28c4f952c4 100644 --- a/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs @@ -189,13 +189,24 @@ private List GetEagerLoads(Type entityType) var attribute = (EagerLoadAttribute)property.GetCustomAttribute(typeof(EagerLoadAttribute)); if (attribute == null) continue; + Type innerType = TypeOrElementType(property.PropertyType); + attribute.Children = GetEagerLoads(innerType); 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 18abbde6fe..d6326fe33c 100644 --- a/src/JsonApiDotNetCore/Data/DefaultResourceRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultResourceRepository.cs @@ -288,9 +288,10 @@ public virtual IQueryable EagerLoad(IQueryable entities, I { foreach (var attribute in attributes) { - entities = chainPrefix != null - ? entities.Include(chainPrefix + "." + attribute.Property.Name) - : entities.Include(attribute.Property.Name); + string path = chainPrefix != null ? chainPrefix + "." + attribute.Property.Name : attribute.Property.Name; + entities = entities.Include(path); + + entities = EagerLoad(entities, attribute.Children, path); } return entities; diff --git a/src/JsonApiDotNetCore/Models/Annotation/EagerLoadAttribute.cs b/src/JsonApiDotNetCore/Models/Annotation/EagerLoadAttribute.cs index 56b8186eba..50c2d9e7f6 100644 --- a/src/JsonApiDotNetCore/Models/Annotation/EagerLoadAttribute.cs +++ b/src/JsonApiDotNetCore/Models/Annotation/EagerLoadAttribute.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Reflection; namespace JsonApiDotNetCore.Models @@ -34,5 +35,7 @@ namespace JsonApiDotNetCore.Models public sealed class EagerLoadAttribute : Attribute { public PropertyInfo Property { get; internal set; } + + public IList Children { get; internal set; } } } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EagerLoadTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EagerLoadTests.cs index 3295e40be2..2b795d6c54 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EagerLoadTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EagerLoadTests.cs @@ -32,8 +32,7 @@ public EagerLoadTests(StandardApplicationFactory factory) : base(factory) _countryFaker = new Faker() .RuleFor(c => c.Name, f => f.Address.Country()); _visaFaker = new Faker() - .RuleFor(v => v.ExpiresAt, f => f.Date.Future()) - .RuleFor(v => v.CountryCode, f => f.Address.CountryCode()); + .RuleFor(v => v.ExpiresAt, f => f.Date.Future()); } [Fact] @@ -44,7 +43,11 @@ public async Task GetSingleResource_TopLevel_AppliesEagerLoad() 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); @@ -59,7 +62,7 @@ public async Task GetSingleResource_TopLevel_AppliesEagerLoad() var resultPassport = _deserializer.DeserializeSingle(body).Data; Assert.Equal(passport.Id, resultPassport.Id); Assert.Equal(passport.BirthCountry.Name, resultPassport.BirthCountryName); - Assert.Equal(visa1.CountryCode + ", " + visa2.CountryCode, resultPassport.GrantedVisaCountries); + Assert.Equal(visa1.TargetCountry.Name + ", " + visa2.TargetCountry.Name, resultPassport.GrantedVisaCountries); } [Fact] @@ -137,7 +140,9 @@ public async Task PatchResource_TopLevel_AppliesEagerLoad() // Arrange var passport = _passportFaker.Generate(); passport.BirthCountry = _countryFaker.Generate(); - passport.GrantedVisas = new List { _visaFaker.Generate() }; + var visa = _visaFaker.Generate(); + visa.TargetCountry = _countryFaker.Generate(); + passport.GrantedVisas = new List { visa }; _dbContext.Add(passport); _dbContext.SaveChanges(); @@ -158,7 +163,7 @@ public async Task PatchResource_TopLevel_AppliesEagerLoad() Assert.Equal(passport.Id, resultPassport.Id); Assert.Equal(passport.SocialSecurityNumber, resultPassport.SocialSecurityNumber); Assert.Equal(passport.BirthCountry.Name, resultPassport.BirthCountryName); - Assert.Equal(passport.GrantedVisas.First().CountryCode, resultPassport.GrantedVisaCountries); + Assert.Equal(passport.GrantedVisas.First().TargetCountry.Name, resultPassport.GrantedVisaCountries); } } } From aab12106c716b31b0e119696aac0f02dc4ebadf4 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 4 Mar 2020 13:55:04 +0100 Subject: [PATCH 04/11] Added safeguard against infinite loop. --- .../Builders/ResourceGraphBuilder.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs index 28c4f952c4..9ae34c4f83 100644 --- a/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs @@ -179,18 +179,23 @@ 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) + 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)); + var attribute = (EagerLoadAttribute) property.GetCustomAttribute(typeof(EagerLoadAttribute)); if (attribute == null) continue; Type innerType = TypeOrElementType(property.PropertyType); - attribute.Children = GetEagerLoads(innerType); + attribute.Children = GetEagerLoads(innerType, recursionDepth + 1); attribute.Property = property; attributes.Add(attribute); From f9c6f19cdc7e9da7328183337c55e360fa07a91e Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Thu, 5 Mar 2020 09:08:30 +0100 Subject: [PATCH 05/11] Cleanup and realign of project files --- benchmarks/Benchmarks.csproj | 8 +++--- .../GettingStarted/GettingStarted.csproj | 7 +---- .../JsonApiDotNetCoreExample.csproj | 11 +------- .../NoEntityFrameworkExample.csproj | 5 ++-- .../ReportsExample/ReportsExample.csproj | 8 +----- .../JsonApiDotNetCore.csproj | 3 --- test/DiscoveryTests/DiscoveryTests.csproj | 4 +-- test/IntegrationTests/IntegrationTests.csproj | 18 +++++-------- .../JsonApiDotNetCoreExampleTests.csproj | 5 ++-- .../NoEntityFrameworkTests.csproj | 12 +++------ test/UnitTests/UnitTests.csproj | 27 ++++++++----------- 11 files changed, 35 insertions(+), 73 deletions(-) diff --git a/benchmarks/Benchmarks.csproj b/benchmarks/Benchmarks.csproj index 02bfbf378d..55f1008fa2 100644 --- a/benchmarks/Benchmarks.csproj +++ b/benchmarks/Benchmarks.csproj @@ -3,12 +3,14 @@ Exe $(NetCoreAppVersion) + + + + + - - - diff --git a/src/Examples/GettingStarted/GettingStarted.csproj b/src/Examples/GettingStarted/GettingStarted.csproj index 048de21397..a63659bb9d 100644 --- a/src/Examples/GettingStarted/GettingStarted.csproj +++ b/src/Examples/GettingStarted/GettingStarted.csproj @@ -4,12 +4,7 @@ - - - - - - + diff --git a/src/Examples/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj b/src/Examples/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj index d67f773ea7..7da7bd1f31 100644 --- a/src/Examples/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj +++ b/src/Examples/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj @@ -1,15 +1,10 @@ $(NetCoreAppVersion) - true - JsonApiDotNetCoreExample - Exe - JsonApiDotNetCoreExample - InProcess - + @@ -27,8 +22,4 @@ - - - - diff --git a/src/Examples/NoEntityFrameworkExample/NoEntityFrameworkExample.csproj b/src/Examples/NoEntityFrameworkExample/NoEntityFrameworkExample.csproj index b387f93746..8816a9c1e7 100644 --- a/src/Examples/NoEntityFrameworkExample/NoEntityFrameworkExample.csproj +++ b/src/Examples/NoEntityFrameworkExample/NoEntityFrameworkExample.csproj @@ -1,11 +1,12 @@ $(NetCoreAppVersion) - InProcess + - + + diff --git a/src/Examples/ReportsExample/ReportsExample.csproj b/src/Examples/ReportsExample/ReportsExample.csproj index ee832bdf7a..8816a9c1e7 100644 --- a/src/Examples/ReportsExample/ReportsExample.csproj +++ b/src/Examples/ReportsExample/ReportsExample.csproj @@ -1,15 +1,10 @@ $(NetCoreAppVersion) - InProcess - - - - - + @@ -18,5 +13,4 @@ - diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index 3faf239f51..33be2dabaa 100644 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -42,7 +42,4 @@ - - - diff --git a/test/DiscoveryTests/DiscoveryTests.csproj b/test/DiscoveryTests/DiscoveryTests.csproj index 1be010a233..7f975af592 100644 --- a/test/DiscoveryTests/DiscoveryTests.csproj +++ b/test/DiscoveryTests/DiscoveryTests.csproj @@ -1,12 +1,10 @@ - $(NetCoreAppVersion) - false - + diff --git a/test/IntegrationTests/IntegrationTests.csproj b/test/IntegrationTests/IntegrationTests.csproj index ef508b1d82..e41d36c0ee 100644 --- a/test/IntegrationTests/IntegrationTests.csproj +++ b/test/IntegrationTests/IntegrationTests.csproj @@ -1,13 +1,13 @@ - - + $(NetCoreAppVersion) - - false - - JADNC.IntegrationTests + + + + + @@ -18,10 +18,4 @@ - - - - - - diff --git a/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj b/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj index 4b4e7a61f4..b17dc7aba8 100644 --- a/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj +++ b/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj @@ -1,7 +1,6 @@ $(NetCoreAppVersion) - false @@ -11,8 +10,8 @@ - - + + diff --git a/test/NoEntityFrameworkTests/NoEntityFrameworkTests.csproj b/test/NoEntityFrameworkTests/NoEntityFrameworkTests.csproj index 5b652cb098..9ea7c922c6 100644 --- a/test/NoEntityFrameworkTests/NoEntityFrameworkTests.csproj +++ b/test/NoEntityFrameworkTests/NoEntityFrameworkTests.csproj @@ -1,22 +1,19 @@ $(NetCoreAppVersion) - true - NoEntityFrameworkTests - Exe - NoEntityFrameworkTests - true - true + PreserveNewest + - + + @@ -25,5 +22,4 @@ - diff --git a/test/UnitTests/UnitTests.csproj b/test/UnitTests/UnitTests.csproj index f3bd3ca0c0..7292fa1a36 100644 --- a/test/UnitTests/UnitTests.csproj +++ b/test/UnitTests/UnitTests.csproj @@ -1,12 +1,19 @@ - + $(NetCoreAppVersion) - false + - - + + PreserveNewest + + + + + + + @@ -17,16 +24,4 @@ - - - PreserveNewest - - - - - - - - - From 847f83f29190fbd4f2076a89411eb1bcf6b04077 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Thu, 5 Mar 2020 09:22:30 +0100 Subject: [PATCH 06/11] Removed unused project dependencies --- src/Examples/ReportsExample/ReportsExample.csproj | 1 - test/IntegrationTests/IntegrationTests.csproj | 1 - .../JsonApiDotNetCoreExampleTests.csproj | 2 -- test/NoEntityFrameworkTests/NoEntityFrameworkTests.csproj | 2 -- 4 files changed, 6 deletions(-) diff --git a/src/Examples/ReportsExample/ReportsExample.csproj b/src/Examples/ReportsExample/ReportsExample.csproj index 8816a9c1e7..bb7a8a6322 100644 --- a/src/Examples/ReportsExample/ReportsExample.csproj +++ b/src/Examples/ReportsExample/ReportsExample.csproj @@ -11,6 +11,5 @@ - diff --git a/test/IntegrationTests/IntegrationTests.csproj b/test/IntegrationTests/IntegrationTests.csproj index e41d36c0ee..d960165ee2 100644 --- a/test/IntegrationTests/IntegrationTests.csproj +++ b/test/IntegrationTests/IntegrationTests.csproj @@ -14,7 +14,6 @@ - diff --git a/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj b/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj index b17dc7aba8..9a479bde42 100644 --- a/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj +++ b/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj @@ -21,8 +21,6 @@ - - diff --git a/test/NoEntityFrameworkTests/NoEntityFrameworkTests.csproj b/test/NoEntityFrameworkTests/NoEntityFrameworkTests.csproj index 9ea7c922c6..c8d908a76e 100644 --- a/test/NoEntityFrameworkTests/NoEntityFrameworkTests.csproj +++ b/test/NoEntityFrameworkTests/NoEntityFrameworkTests.csproj @@ -17,8 +17,6 @@ - - From 028440a7e3a474b39473cc4297481fd315ee7c7d Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Thu, 5 Mar 2020 10:19:54 +0100 Subject: [PATCH 07/11] Empty commit to restart TravisCI From cf8b79f07455145d8e89c9966b5141661bda747d Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Tue, 24 Mar 2020 17:48:31 +0100 Subject: [PATCH 08/11] Post-merge fixes --- src/Examples/JsonApiDotNetCoreExample/Models/Passport.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Passport.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Passport.cs index f70045778a..6e6debd819 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Passport.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Passport.cs @@ -5,7 +5,7 @@ namespace JsonApiDotNetCoreExample.Models { - public sealed class Passport : Identifiable + public class Passport : Identifiable { [Attr] public int? SocialSecurityNumber { get; set; } @@ -47,4 +47,4 @@ public string GrantedVisaCountries [EagerLoad] public ICollection GrantedVisas { get; set; } } -} \ No newline at end of file +} From ea0a2f2617af5f42b191a4357e59329f82de6520 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 25 Mar 2020 13:56:20 +0100 Subject: [PATCH 09/11] Post-merge fixes --- test/UnitTests/UnitTests.csproj | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/UnitTests/UnitTests.csproj b/test/UnitTests/UnitTests.csproj index 72a6cd98c4..6a49628d6a 100644 --- a/test/UnitTests/UnitTests.csproj +++ b/test/UnitTests/UnitTests.csproj @@ -3,6 +3,12 @@ $(NetCoreAppVersion) + + + PreserveNewest + + + @@ -15,10 +21,4 @@ - - - - PreserveNewest - - From 0d4b380360968592c6f85f15770888161d8ee925 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 25 Mar 2020 13:57:42 +0100 Subject: [PATCH 10/11] More post-merge fixes --- benchmarks/Benchmarks.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/Benchmarks.csproj b/benchmarks/Benchmarks.csproj index b7e72340af..49761638ed 100644 --- a/benchmarks/Benchmarks.csproj +++ b/benchmarks/Benchmarks.csproj @@ -10,6 +10,6 @@ - + From b6675cf06b3c0ce3b96998421962b00ab0a79d0f Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 25 Mar 2020 17:47:49 +0100 Subject: [PATCH 11/11] Review feedback: make EagerLoad private --- src/JsonApiDotNetCore/Data/DefaultResourceRepository.cs | 2 +- src/JsonApiDotNetCore/Data/IResourceReadRepository.cs | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/JsonApiDotNetCore/Data/DefaultResourceRepository.cs b/src/JsonApiDotNetCore/Data/DefaultResourceRepository.cs index 37ec237fd8..2b082596d6 100644 --- a/src/JsonApiDotNetCore/Data/DefaultResourceRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultResourceRepository.cs @@ -284,7 +284,7 @@ public virtual async Task DeleteAsync(TId id) return true; } - public virtual IQueryable EagerLoad(IQueryable entities, IEnumerable attributes, string chainPrefix = null) + private IQueryable EagerLoad(IQueryable entities, IEnumerable attributes, string chainPrefix = null) { foreach (var attribute in attributes) { diff --git a/src/JsonApiDotNetCore/Data/IResourceReadRepository.cs b/src/JsonApiDotNetCore/Data/IResourceReadRepository.cs index d8ba2477e1..63867c6bea 100644 --- a/src/JsonApiDotNetCore/Data/IResourceReadRepository.cs +++ b/src/JsonApiDotNetCore/Data/IResourceReadRepository.cs @@ -28,10 +28,6 @@ public interface IResourceReadRepository /// IQueryable Select(IQueryable entities, IEnumerable fields); /// - /// Include related entities that are not exposed as json:api relationships. - /// - IQueryable EagerLoad(IQueryable entities, IEnumerable attributes, string chainPrefix = null); - /// /// Include a relationship in the query /// ///