From fa26049c1c6e88ee931c84523e8419907de2f436 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Sat, 7 Apr 2018 18:33:55 -0500 Subject: [PATCH 01/12] fix(Deserializer): remove dependency on GenericProcessorFactory Rather than fetching data from the database during deserialization, we can set the relationships with instances that just carry the id. It will then be the responsibility of the repository to handle those relationships --- .../Extensions/TypeExtensions.cs | 23 ++++++++++ .../Serialization/JsonApiDeSerializer.cs | 29 ++++++++---- .../Extensions/TypeExtensions_Tests.cs | 44 +++++++++++++++++++ 3 files changed, 88 insertions(+), 8 deletions(-) create mode 100644 test/UnitTests/Extensions/TypeExtensions_Tests.cs diff --git a/src/JsonApiDotNetCore/Extensions/TypeExtensions.cs b/src/JsonApiDotNetCore/Extensions/TypeExtensions.cs index ccc4619966..a78f545e81 100644 --- a/src/JsonApiDotNetCore/Extensions/TypeExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/TypeExtensions.cs @@ -31,5 +31,28 @@ public static Type GetElementType(this IEnumerable enumerable) return elementType; } + + /// + /// Creates a List{TInterface} where TInterface is the generic for type specified by t + /// + public static List GetEmptyCollection(this Type t) + { + if (t == null) throw new ArgumentNullException(nameof(t)); + + var listType = typeof(List<>).MakeGenericType(t); + var list = (List)Activator.CreateInstance(listType); + return list; + } + + /// + /// Creates a new instance of type t, casting it to the specified TInterface + /// + public static TInterface New(this Type t) + { + if (t == null) throw new ArgumentNullException(nameof(t)); + + var instance = (TInterface)Activator.CreateInstance(t); + return instance; + } } } diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index 649d6435ff..37e0314da6 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -9,20 +9,27 @@ using JsonApiDotNetCore.Services; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using JsonApiDotNetCore.Extensions; namespace JsonApiDotNetCore.Serialization { public class JsonApiDeSerializer : IJsonApiDeSerializer { private readonly IJsonApiContext _jsonApiContext; - private readonly IGenericProcessorFactory _genericProcessorFactory; + [Obsolete( + "The deserializer no longer depends on the IGenericProcessorFactory", + error: false)] public JsonApiDeSerializer( IJsonApiContext jsonApiContext, IGenericProcessorFactory genericProcessorFactory) { _jsonApiContext = jsonApiContext; - _genericProcessorFactory = genericProcessorFactory; + } + + public JsonApiDeSerializer(IJsonApiContext jsonApiContext) + { + _jsonApiContext = jsonApiContext; } public object Deserialize(string requestBody) @@ -225,10 +232,11 @@ private object SetHasManyRelationship(object entity, ContextEntity contextEntity, Dictionary relationships) { - var entityProperty = entityProperties.FirstOrDefault(p => p.Name == attr.InternalRelationshipName); + // TODO: is this necessary? if not, remove + // var entityProperty = entityProperties.FirstOrDefault(p => p.Name == attr.InternalRelationshipName); - if (entityProperty == null) - throw new JsonApiException(400, $"{contextEntity.EntityType.Name} does not contain an relationsip named {attr.InternalRelationshipName}"); + // if (entityProperty == null) + // throw new JsonApiException(400, $"{contextEntity.EntityType.Name} does not contain a relationsip named '{attr.InternalRelationshipName}'"); var relationshipName = attr.PublicRelationshipName; @@ -238,11 +246,16 @@ private object SetHasManyRelationship(object entity, if (data == null) return entity; - var genericProcessor = _genericProcessorFactory.GetProcessor(typeof(GenericProcessor<>), attr.Type); + var resourceRelationships = attr.Type.GetEmptyCollection(); - var ids = relationshipData.ManyData.Select(r => r.Id); + var relationshipShells = relationshipData.ManyData.Select(r => + { + var instance = attr.Type.New(); + instance.StringId = r.Id; + return instance; + }); - genericProcessor.SetRelationships(entity, attr, ids); + attr.SetValue(entity, relationshipShells); } return entity; diff --git a/test/UnitTests/Extensions/TypeExtensions_Tests.cs b/test/UnitTests/Extensions/TypeExtensions_Tests.cs new file mode 100644 index 0000000000..92534eef5d --- /dev/null +++ b/test/UnitTests/Extensions/TypeExtensions_Tests.cs @@ -0,0 +1,44 @@ +using JsonApiDotNetCore.Models; +using Xunit; +using JsonApiDotNetCore.Extensions; +using System.Collections.Generic; + +namespace UnitTests.Extensions +{ + public class TypeExtensions_Tests + { + [Fact] + public void GetCollection_Creates_List_If_T_Implements_Interface() + { + // arrange + var type = typeof(Model); + + // act + var collection = type.GetEmptyCollection(); + + // assert + Assert.NotNull(collection); + Assert.Empty(collection); + Assert.IsType>(collection); + } + + [Fact] + public void New_Creates_An_Instance_If_T_Implements_Interface() + { + // arrange + var type = typeof(Model); + + // act + var instance = type.New(); + + // assert + Assert.NotNull(instance); + Assert.IsType(instance); + } + + private class Model : IIdentifiable + { + public string StringId { get; set; } + } + } +} From 91b5d4bb0b0812da3341efa3fbdd087ea7ca2776 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Sat, 7 Apr 2018 19:08:18 -0500 Subject: [PATCH 02/12] fix(typeExtensions): cast to IEnumerable using covariance --- src/JsonApiDotNetCore/Extensions/TypeExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/JsonApiDotNetCore/Extensions/TypeExtensions.cs b/src/JsonApiDotNetCore/Extensions/TypeExtensions.cs index a78f545e81..efe29620f8 100644 --- a/src/JsonApiDotNetCore/Extensions/TypeExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/TypeExtensions.cs @@ -35,12 +35,12 @@ public static Type GetElementType(this IEnumerable enumerable) /// /// Creates a List{TInterface} where TInterface is the generic for type specified by t /// - public static List GetEmptyCollection(this Type t) + public static IEnumerable GetEmptyCollection(this Type t) { if (t == null) throw new ArgumentNullException(nameof(t)); var listType = typeof(List<>).MakeGenericType(t); - var list = (List)Activator.CreateInstance(listType); + var list = (IEnumerable)Activator.CreateInstance(listType); return list; } From 7288de2ab069d47bcfbf38f97b3e924874d2cea6 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Sat, 7 Apr 2018 20:57:26 -0500 Subject: [PATCH 03/12] fix(Deserializer): properly convert collection type when setting it on the model --- src/JsonApiDotNetCore/Extensions/TypeExtensions.cs | 4 ++-- src/JsonApiDotNetCore/Internal/TypeHelper.cs | 10 ++++++++++ src/JsonApiDotNetCore/Models/HasManyAttribute.cs | 2 +- .../Serialization/JsonApiDeSerializer.cs | 10 ++++++---- test/UnitTests/Extensions/TypeExtensions_Tests.cs | 6 +++--- 5 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/JsonApiDotNetCore/Extensions/TypeExtensions.cs b/src/JsonApiDotNetCore/Extensions/TypeExtensions.cs index efe29620f8..8cc7c0dffe 100644 --- a/src/JsonApiDotNetCore/Extensions/TypeExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/TypeExtensions.cs @@ -35,12 +35,12 @@ public static Type GetElementType(this IEnumerable enumerable) /// /// Creates a List{TInterface} where TInterface is the generic for type specified by t /// - public static IEnumerable GetEmptyCollection(this Type t) + public static IEnumerable GetEmptyCollection(this Type t) { if (t == null) throw new ArgumentNullException(nameof(t)); var listType = typeof(List<>).MakeGenericType(t); - var list = (IEnumerable)Activator.CreateInstance(listType); + var list = (IEnumerable)Activator.CreateInstance(listType); return list; } diff --git a/src/JsonApiDotNetCore/Internal/TypeHelper.cs b/src/JsonApiDotNetCore/Internal/TypeHelper.cs index cc64b398dd..2493bfab4a 100644 --- a/src/JsonApiDotNetCore/Internal/TypeHelper.cs +++ b/src/JsonApiDotNetCore/Internal/TypeHelper.cs @@ -1,10 +1,20 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.Reflection; namespace JsonApiDotNetCore.Internal { public static class TypeHelper { + public static IList ConvertCollection(IEnumerable collection, Type targetType) + { + var list = Activator.CreateInstance(typeof(List<>).MakeGenericType(targetType)) as IList; + foreach(var item in collection) + list.Add(ConvertType(item, targetType)); + return list; + } + public static object ConvertType(object value, Type type) { if (value == null) diff --git a/src/JsonApiDotNetCore/Models/HasManyAttribute.cs b/src/JsonApiDotNetCore/Models/HasManyAttribute.cs index b4fd1b42ec..7d2fa87ec4 100644 --- a/src/JsonApiDotNetCore/Models/HasManyAttribute.cs +++ b/src/JsonApiDotNetCore/Models/HasManyAttribute.cs @@ -12,7 +12,7 @@ public override void SetValue(object entity, object newValue) .GetType() .GetProperty(InternalRelationshipName); - propertyInfo.SetValue(entity, newValue); + propertyInfo.SetValue(entity, newValue); } } } diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index 37e0314da6..70246cb351 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Generics; using JsonApiDotNetCore.Models; @@ -9,7 +10,6 @@ using JsonApiDotNetCore.Services; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using JsonApiDotNetCore.Extensions; namespace JsonApiDotNetCore.Serialization { @@ -246,8 +246,6 @@ private object SetHasManyRelationship(object entity, if (data == null) return entity; - var resourceRelationships = attr.Type.GetEmptyCollection(); - var relationshipShells = relationshipData.ManyData.Select(r => { var instance = attr.Type.New(); @@ -255,7 +253,11 @@ private object SetHasManyRelationship(object entity, return instance; }); - attr.SetValue(entity, relationshipShells); + var convertedCollection = TypeHelper.ConvertCollection(relationshipShells, attr.Type); + + // var convertedCollection = TypeHelper.ConvertCollection(relationshipShells, attr.Type); + + attr.SetValue(entity, convertedCollection); } return entity; diff --git a/test/UnitTests/Extensions/TypeExtensions_Tests.cs b/test/UnitTests/Extensions/TypeExtensions_Tests.cs index 92534eef5d..f59fa37be0 100644 --- a/test/UnitTests/Extensions/TypeExtensions_Tests.cs +++ b/test/UnitTests/Extensions/TypeExtensions_Tests.cs @@ -1,7 +1,7 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Models; using Xunit; -using JsonApiDotNetCore.Extensions; -using System.Collections.Generic; namespace UnitTests.Extensions { @@ -14,7 +14,7 @@ public void GetCollection_Creates_List_If_T_Implements_Interface() var type = typeof(Model); // act - var collection = type.GetEmptyCollection(); + var collection = type.GetEmptyCollection(); // assert Assert.NotNull(collection); From 1062ea938ea3589d7480fe0d2d19ef7995c5c695 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Sun, 8 Apr 2018 07:10:26 -0500 Subject: [PATCH 04/12] set new HasManyRelationships with EntityState.Unchanged --- .../Data/DefaultEntityRepository.cs | 19 +++++++ .../Data/IEntityRepository.cs | 4 -- .../Extensions/DbContextExtensions.cs | 16 ++---- .../Request/HasManyRelationshipPointers.cs | 49 +++++++++++++++++++ .../Serialization/JsonApiDeSerializer.cs | 10 +--- .../Services/IJsonApiContext.cs | 2 + .../Services/JsonApiContext.cs | 3 +- 7 files changed, 78 insertions(+), 25 deletions(-) create mode 100644 src/JsonApiDotNetCore/Request/HasManyRelationshipPointers.cs diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index e551bb9491..c4ebf2f738 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -85,10 +85,29 @@ public virtual async Task GetAndIncludeAsync(TId id, string relationshi public virtual async Task CreateAsync(TEntity entity) { _dbSet.Add(entity); + + DetachHasManyPointers(); + await _context.SaveChangesAsync(); return entity; } + /// + /// This is used to allow creation of HasMany relationships when the + /// dependent side of the relationship already exists. + /// + private void DetachHasManyPointers() + { + var relationships = _jsonApiContext.HasManyRelationshipPointers.Get(); + foreach(var relationship in relationships) + { + foreach(var pointer in relationship.Value) + { + _context.Entry(pointer).State = EntityState.Unchanged; + } + } + } + public virtual async Task UpdateAsync(TId id, TEntity entity) { var oldEntity = await GetAsync(id); diff --git a/src/JsonApiDotNetCore/Data/IEntityRepository.cs b/src/JsonApiDotNetCore/Data/IEntityRepository.cs index 4c35d6ea3f..e8bb68ef90 100644 --- a/src/JsonApiDotNetCore/Data/IEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/IEntityRepository.cs @@ -1,7 +1,3 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using JsonApiDotNetCore.Internal.Query; using JsonApiDotNetCore.Models; namespace JsonApiDotNetCore.Data diff --git a/src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs b/src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs index 2606342e29..3cb5ccc359 100644 --- a/src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs @@ -1,20 +1,12 @@ -using Microsoft.EntityFrameworkCore; using System; +using Microsoft.EntityFrameworkCore; namespace JsonApiDotNetCore.Extensions { public static class DbContextExtensions { - public static DbSet GetDbSet(this DbContext context) where T : class - { - var contextProperties = context.GetType().GetProperties(); - foreach(var property in contextProperties) - { - if (property.PropertyType == typeof(DbSet)) - return (DbSet)property.GetValue(context); - } - - throw new ArgumentException($"DbSet of type {typeof(T).FullName} not found on the DbContext", nameof(T)); - } + [Obsolete("This is no longer required since the introduction of context.Set", error: false)] + public static DbSet GetDbSet(this DbContext context) where T : class + => context.Set(); } } diff --git a/src/JsonApiDotNetCore/Request/HasManyRelationshipPointers.cs b/src/JsonApiDotNetCore/Request/HasManyRelationshipPointers.cs new file mode 100644 index 0000000000..721274e3d6 --- /dev/null +++ b/src/JsonApiDotNetCore/Request/HasManyRelationshipPointers.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace JsonApiDotNetCore.Request +{ + /// + /// Stores information to set relationships for the request resource. + /// These relationships must already exist and should not be re-created. + /// + /// The expected use case is POST-ing or PATCH-ing + /// an entity with HasMany relaitonships: + /// + /// { + /// "data": { + /// "type": "photos", + /// "attributes": { + /// "title": "Ember Hamster", + /// "src": "http://example.com/images/productivity.png" + /// }, + /// "relationships": { + /// "tags": { + /// "data": [ + /// { "type": "tags", "id": "2" }, + /// { "type": "tags", "id": "3" } + /// ] + /// } + /// } + /// } + /// } + /// + /// + public class HasManyRelationshipPointers + { + private Dictionary _hasManyRelationships = new Dictionary(); + + /// + /// Add the relationship to the list of relationships that should be + /// set in the repository layer. + /// + public void Add(Type dependentType, IList entities) + => _hasManyRelationships[dependentType] = entities; + + /// + /// Get all the models that should be associated + /// + public Dictionary Get() => _hasManyRelationships; + } +} diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index 70246cb351..9723f79dac 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -232,12 +232,6 @@ private object SetHasManyRelationship(object entity, ContextEntity contextEntity, Dictionary relationships) { - // TODO: is this necessary? if not, remove - // var entityProperty = entityProperties.FirstOrDefault(p => p.Name == attr.InternalRelationshipName); - - // if (entityProperty == null) - // throw new JsonApiException(400, $"{contextEntity.EntityType.Name} does not contain a relationsip named '{attr.InternalRelationshipName}'"); - var relationshipName = attr.PublicRelationshipName; if (relationships.TryGetValue(relationshipName, out RelationshipData relationshipData)) @@ -255,9 +249,9 @@ private object SetHasManyRelationship(object entity, var convertedCollection = TypeHelper.ConvertCollection(relationshipShells, attr.Type); - // var convertedCollection = TypeHelper.ConvertCollection(relationshipShells, attr.Type); - attr.SetValue(entity, convertedCollection); + + _jsonApiContext.HasManyRelationshipPointers.Add(attr.Type, convertedCollection); } return entity; diff --git a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs index a73f0eb53a..132630446d 100644 --- a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs @@ -6,6 +6,7 @@ using JsonApiDotNetCore.Internal.Generics; using JsonApiDotNetCore.Internal.Query; using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Request; namespace JsonApiDotNetCore.Services { @@ -28,6 +29,7 @@ public interface IJsonApiContext Type ControllerType { get; set; } Dictionary DocumentMeta { get; set; } bool IsBulkOperationRequest { get; set; } + HasManyRelationshipPointers HasManyRelationshipPointers { get; } TAttribute GetControllerAttribute() where TAttribute : Attribute; } diff --git a/src/JsonApiDotNetCore/Services/JsonApiContext.cs b/src/JsonApiDotNetCore/Services/JsonApiContext.cs index 1ebf5aeea1..a8bd9fe5de 100644 --- a/src/JsonApiDotNetCore/Services/JsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/JsonApiContext.cs @@ -1,12 +1,12 @@ using System; using System.Collections.Generic; -using System.Linq; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Generics; using JsonApiDotNetCore.Internal.Query; using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Request; using Microsoft.AspNetCore.Http; namespace JsonApiDotNetCore.Services @@ -52,6 +52,7 @@ public JsonApiContext( public Type ControllerType { get; set; } public Dictionary DocumentMeta { get; set; } public bool IsBulkOperationRequest { get; set; } + public HasManyRelationshipPointers HasManyRelationshipPointers { get; } = new HasManyRelationshipPointers(); public IJsonApiContext ApplyContext(object controller) { From ed223c171a5c2af4b7967a01b0379f4eed9adc25 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Sun, 8 Apr 2018 07:26:15 -0500 Subject: [PATCH 05/12] ensure pointers are attached prior to adding the entity --- src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index c4ebf2f738..3c85bdae80 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -84,10 +84,9 @@ public virtual async Task GetAndIncludeAsync(TId id, string relationshi public virtual async Task CreateAsync(TEntity entity) { + AttachHasManyPointers(); _dbSet.Add(entity); - DetachHasManyPointers(); - await _context.SaveChangesAsync(); return entity; } @@ -96,7 +95,7 @@ public virtual async Task CreateAsync(TEntity entity) /// This is used to allow creation of HasMany relationships when the /// dependent side of the relationship already exists. /// - private void DetachHasManyPointers() + private void AttachHasManyPointers() { var relationships = _jsonApiContext.HasManyRelationshipPointers.Get(); foreach(var relationship in relationships) From 5ea225264e08f7144115d60e84542fe2d8306943 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Sun, 8 Apr 2018 15:33:58 -0500 Subject: [PATCH 06/12] fix tests --- .../Unit => UnitTests}/Builders/MetaBuilderTests.cs | 6 +++--- .../Extensions/IServiceCollectionExtensionsTests.cs | 7 +++---- .../Unit => UnitTests}/Models/AttributesEqualsTests.cs | 2 +- test/UnitTests/UnitTests.csproj | 1 + 4 files changed, 8 insertions(+), 8 deletions(-) rename test/{JsonApiDotNetCoreExampleTests/Unit => UnitTests}/Builders/MetaBuilderTests.cs (97%) rename test/{JsonApiDotNetCoreExampleTests/Unit => UnitTests}/Extensions/IServiceCollectionExtensionsTests.cs (92%) rename test/{JsonApiDotNetCoreExampleTests/Unit => UnitTests}/Models/AttributesEqualsTests.cs (97%) diff --git a/test/JsonApiDotNetCoreExampleTests/Unit/Builders/MetaBuilderTests.cs b/test/UnitTests/Builders/MetaBuilderTests.cs similarity index 97% rename from test/JsonApiDotNetCoreExampleTests/Unit/Builders/MetaBuilderTests.cs rename to test/UnitTests/Builders/MetaBuilderTests.cs index 5cd0b765de..0b784ef5b7 100644 --- a/test/JsonApiDotNetCoreExampleTests/Unit/Builders/MetaBuilderTests.cs +++ b/test/UnitTests/Builders/MetaBuilderTests.cs @@ -1,8 +1,8 @@ -using Xunit; -using JsonApiDotNetCore.Builders; using System.Collections.Generic; +using JsonApiDotNetCore.Builders; +using Xunit; -namespace JsonApiDotNetCoreExampleTests.Unit.Builders +namespace UnitTests.Builders { public class MetaBuilderTests { diff --git a/test/JsonApiDotNetCoreExampleTests/Unit/Extensions/IServiceCollectionExtensionsTests.cs b/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs similarity index 92% rename from test/JsonApiDotNetCoreExampleTests/Unit/Extensions/IServiceCollectionExtensionsTests.cs rename to test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs index f6772fa22b..4fe2f09ff1 100644 --- a/test/JsonApiDotNetCoreExampleTests/Unit/Extensions/IServiceCollectionExtensionsTests.cs +++ b/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs @@ -10,12 +10,11 @@ using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; -using UnitTests; using Xunit; +using Microsoft.EntityFrameworkCore; -namespace JsonApiDotNetCoreExampleTests.Unit.Extensions +namespace UnitTests.Extensions { public class IServiceCollectionExtensionsTests { @@ -28,7 +27,7 @@ public void AddJsonApiInternals_Adds_All_Required_Services() services.AddDbContext(options => { - options.UseMemoryCache(new MemoryCache(new MemoryCacheOptions())); + options.UseInMemoryDatabase(); }, ServiceLifetime.Transient); // act diff --git a/test/JsonApiDotNetCoreExampleTests/Unit/Models/AttributesEqualsTests.cs b/test/UnitTests/Models/AttributesEqualsTests.cs similarity index 97% rename from test/JsonApiDotNetCoreExampleTests/Unit/Models/AttributesEqualsTests.cs rename to test/UnitTests/Models/AttributesEqualsTests.cs index 107dd1d593..0b989169ef 100644 --- a/test/JsonApiDotNetCoreExampleTests/Unit/Models/AttributesEqualsTests.cs +++ b/test/UnitTests/Models/AttributesEqualsTests.cs @@ -1,7 +1,7 @@ using JsonApiDotNetCore.Models; using Xunit; -namespace JsonApiDotNetCoreExampleTests.Unit.Models +namespace UnitTests.Models { public class AttributesEqualsTests { diff --git a/test/UnitTests/UnitTests.csproj b/test/UnitTests/UnitTests.csproj index 14a0d30e33..a6ed346e7d 100644 --- a/test/UnitTests/UnitTests.csproj +++ b/test/UnitTests/UnitTests.csproj @@ -12,5 +12,6 @@ + From 1bd8d05f163bd34ad512ef27d06a54b8631de353 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 18 Jun 2018 12:11:14 -0500 Subject: [PATCH 07/12] fix(IResourceService): should inherit from shorthand interfaces --- src/JsonApiDotNetCore/Services/Contract/IResourceService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/Services/Contract/IResourceService.cs b/src/JsonApiDotNetCore/Services/Contract/IResourceService.cs index 82f3505c78..ec534a5f1b 100644 --- a/src/JsonApiDotNetCore/Services/Contract/IResourceService.cs +++ b/src/JsonApiDotNetCore/Services/Contract/IResourceService.cs @@ -3,7 +3,7 @@ namespace JsonApiDotNetCore.Services { public interface IResourceService - : IResourceService + : IResourceCmdService, IResourceQueryService, IResourceService where T : class, IIdentifiable { } From ca33488172e81a44c1a19b0c94ac7d4b01de1283 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 18 Jun 2018 12:13:14 -0500 Subject: [PATCH 08/12] fix(IResourceCmdService): inherit shorthand interfaces --- .../Services/Contract/IResourceCmdService.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/Services/Contract/IResourceCmdService.cs b/src/JsonApiDotNetCore/Services/Contract/IResourceCmdService.cs index 2633fd589b..0f6c5e64b7 100644 --- a/src/JsonApiDotNetCore/Services/Contract/IResourceCmdService.cs +++ b/src/JsonApiDotNetCore/Services/Contract/IResourceCmdService.cs @@ -2,7 +2,12 @@ namespace JsonApiDotNetCore.Services { - public interface IResourceCmdService : IResourceCmdService + public interface IResourceCmdService : + ICreateService, + IUpdateService, + IUpdateRelationshipService, + IDeleteService, + IResourceCmdService where T : class, IIdentifiable { } From 8f04c8218b5d3964bb396b78e4768652452b4690 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 18 Jun 2018 12:14:05 -0500 Subject: [PATCH 09/12] fix(IResourceQueryService): inherit shorthand interfaces --- .../Services/Contract/IResourceQueryService.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/Services/Contract/IResourceQueryService.cs b/src/JsonApiDotNetCore/Services/Contract/IResourceQueryService.cs index 8a9e247a3f..1cd4a94cf3 100644 --- a/src/JsonApiDotNetCore/Services/Contract/IResourceQueryService.cs +++ b/src/JsonApiDotNetCore/Services/Contract/IResourceQueryService.cs @@ -2,7 +2,12 @@ namespace JsonApiDotNetCore.Services { - public interface IResourceQueryService : IResourceQueryService + public interface IResourceQueryService : + IGetAllService, + IGetByIdService, + IGetRelationshipsService, + IGetRelationshipService, + IResourceQueryService where T : class, IIdentifiable { } From 48ecdc0014ccf76b6bb8216f370d89ffb218a705 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 18 Jun 2018 12:17:35 -0500 Subject: [PATCH 10/12] bump version to 2.3.2 --- src/JsonApiDotNetCore/JsonApiDotNetCore.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index 7019a8e6cc..75fc955402 100755 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -1,6 +1,6 @@  - 2.3.1 + 2.3.2 $(NetStandardVersion) JsonApiDotNetCore JsonApiDotNetCore From 9dd7719193d7c8140ce98a0fa0297899a192f4a3 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Wed, 27 Jun 2018 10:23:12 -0700 Subject: [PATCH 11/12] fix(#312): deserializer not handling included relationships --- .../Builders/DocumentBuilder.cs | 13 +- .../Data/DefaultEntityRepository.cs | 14 +- .../IServiceCollectionExtensions.cs | 1 - .../Extensions/TypeExtensions.cs | 17 ++- .../Internal/Generics/GenericProcessor.cs | 11 +- src/JsonApiDotNetCore/Models/Identifiable.cs | 9 +- .../Models/RelationshipAttribute.cs | 11 ++ .../Serialization/IJsonApiDeSerializer.cs | 4 +- .../Serialization/JsonApiDeSerializer.cs | 123 ++++++++++++----- .../Serialization/JsonApiDeSerializerTests.cs | 127 +++++++++++++++++- .../Processors/CreateOpProcessorTests.cs | 4 +- 11 files changed, 272 insertions(+), 62 deletions(-) diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index 0beb0516c1..8142b2ea26 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -262,11 +262,14 @@ private ResourceIdentifierObject GetRelationship(object entity) var objType = entity.GetType(); var contextEntity = _jsonApiContext.ContextGraph.GetContextEntity(objType); - return new ResourceIdentifierObject - { - Type = contextEntity.EntityName, - Id = ((IIdentifiable)entity).StringId - }; + if(entity is IIdentifiable identifiableEntity) + return new ResourceIdentifierObject + { + Type = contextEntity.EntityName, + Id = identifiableEntity.StringId + }; + + return null; } private ResourceIdentifierObject GetIndependentRelationshipIdentifier(HasOneAttribute hasOne, IIdentifiable entity) diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index 032fef13c4..e6d534172f 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -185,17 +185,23 @@ public virtual async Task> PageAsync(IQueryable en public async Task CountAsync(IQueryable entities) { - return await entities.CountAsync(); + return (entities is IAsyncEnumerable) + ? await entities.CountAsync() + : entities.Count(); } - public Task FirstOrDefaultAsync(IQueryable entities) + public async Task FirstOrDefaultAsync(IQueryable entities) { - return entities.FirstOrDefaultAsync(); + return (entities is IAsyncEnumerable) + ? await entities.FirstOrDefaultAsync() + : entities.FirstOrDefault(); } public async Task> ToListAsync(IQueryable entities) { - return await entities.ToListAsync(); + return (entities is IAsyncEnumerable) + ? await entities.ToListAsync() + : entities.ToList(); } } } diff --git a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs index 5e8eeefdd1..ea15036201 100644 --- a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs @@ -136,7 +136,6 @@ public static void AddJsonApiInternals( services.AddScoped(); services.AddScoped(); services.AddScoped(typeof(GenericProcessor<>)); - services.AddScoped(typeof(GenericProcessor<,>)); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/JsonApiDotNetCore/Extensions/TypeExtensions.cs b/src/JsonApiDotNetCore/Extensions/TypeExtensions.cs index 8cc7c0dffe..74c390e8d5 100644 --- a/src/JsonApiDotNetCore/Extensions/TypeExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/TypeExtensions.cs @@ -1,4 +1,5 @@ -using System; +using JsonApiDotNetCore.Internal; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -51,8 +52,20 @@ public static TInterface New(this Type t) { if (t == null) throw new ArgumentNullException(nameof(t)); - var instance = (TInterface)Activator.CreateInstance(t); + var instance = (TInterface)CreateNewInstance(t); return instance; } + + private static object CreateNewInstance(Type type) + { + try + { + return Activator.CreateInstance(type); + } + catch (Exception e) + { + throw new JsonApiException(500, $"Type '{type}' cannot be instantiated using the default constructor.", e); + } + } } } diff --git a/src/JsonApiDotNetCore/Internal/Generics/GenericProcessor.cs b/src/JsonApiDotNetCore/Internal/Generics/GenericProcessor.cs index 8e5d17e56b..1aa6610790 100644 --- a/src/JsonApiDotNetCore/Internal/Generics/GenericProcessor.cs +++ b/src/JsonApiDotNetCore/Internal/Generics/GenericProcessor.cs @@ -14,12 +14,7 @@ public interface IGenericProcessor void SetRelationships(object parent, RelationshipAttribute relationship, IEnumerable relationshipIds); } - public class GenericProcessor : GenericProcessor where T : class, IIdentifiable - { - public GenericProcessor(IDbContextResolver contextResolver) : base(contextResolver) { } - } - - public class GenericProcessor : IGenericProcessor where T : class, IIdentifiable + public class GenericProcessor : IGenericProcessor where T : class, IIdentifiable { private readonly DbContext _context; public GenericProcessor(IDbContextResolver contextResolver) @@ -38,12 +33,12 @@ public void SetRelationships(object parent, RelationshipAttribute relationship, { if (relationship.IsHasMany) { - var entities = _context.GetDbSet().Where(x => relationshipIds.Contains(x.StringId)).ToList(); + var entities = _context.Set().Where(x => relationshipIds.Contains(x.StringId)).ToList(); relationship.SetValue(parent, entities); } else { - var entity = _context.GetDbSet().SingleOrDefault(x => relationshipIds.First() == x.StringId); + var entity = _context.Set().SingleOrDefault(x => relationshipIds.First() == x.StringId); relationship.SetValue(parent, entity); } } diff --git a/src/JsonApiDotNetCore/Models/Identifiable.cs b/src/JsonApiDotNetCore/Models/Identifiable.cs index 0adb073f2f..703cc2f051 100644 --- a/src/JsonApiDotNetCore/Models/Identifiable.cs +++ b/src/JsonApiDotNetCore/Models/Identifiable.cs @@ -15,7 +15,7 @@ public class Identifiable : IIdentifiable public string StringId { get => GetStringId(Id); - set => Id = (T)GetConcreteId(value); + set => Id = GetTypedId(value); } protected virtual string GetStringId(object value) @@ -34,6 +34,13 @@ protected virtual string GetStringId(object value) : stringValue; } + protected virtual T GetTypedId(string value) + { + var convertedValue = TypeHelper.ConvertType(value, typeof(T)); + return convertedValue == null ? default : (T)convertedValue; + } + + [Obsolete("Use GetTypedId instead")] protected virtual object GetConcreteId(string value) { return TypeHelper.ConvertType(value, typeof(T)); diff --git a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs b/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs index 3e66bdc8aa..65a170281e 100644 --- a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs +++ b/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs @@ -13,6 +13,17 @@ protected RelationshipAttribute(string publicName, Link documentLinks, bool canI public string PublicRelationshipName { get; } public string InternalRelationshipName { get; internal set; } + + /// + /// The related entity type. This does not necessarily match the navigation property type. + /// In the case of a HasMany relationship, this value will be the generic argument type. + /// + /// + /// + /// + /// public List<Articles> Articles { get; set; } // Type => Article + /// + /// public Type Type { get; internal set; } public bool IsHasMany => GetType() == typeof(HasManyAttribute); public bool IsHasOne => GetType() == typeof(HasOneAttribute); diff --git a/src/JsonApiDotNetCore/Serialization/IJsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/IJsonApiDeSerializer.cs index 0355c962ed..57b28c6087 100644 --- a/src/JsonApiDotNetCore/Serialization/IJsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/IJsonApiDeSerializer.cs @@ -9,6 +9,6 @@ public interface IJsonApiDeSerializer TEntity Deserialize(string requestBody); object DeserializeRelationship(string requestBody); List DeserializeList(string requestBody); - object DocumentToObject(DocumentData data); + object DocumentToObject(DocumentData data, List included = null); } -} \ No newline at end of file +} diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index 45d77c0f77..f7a54ffbd3 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -54,7 +54,7 @@ public object Deserialize(string requestBody) var document = bodyJToken.ToObject(); _jsonApiContext.DocumentMeta = document.Meta; - var entity = DocumentToObject(document.Data); + var entity = DocumentToObject(document.Data, document.Included); return entity; } catch (Exception e) @@ -95,8 +95,8 @@ public List DeserializeList(string requestBody) var deserializedList = new List(); foreach (var data in documents.Data) { - var entity = DocumentToObject(data); - deserializedList.Add((TEntity)entity); + var entity = (TEntity)DocumentToObject(data, documents.Included); + deserializedList.Add(entity); } return deserializedList; @@ -107,7 +107,7 @@ public List DeserializeList(string requestBody) } } - public object DocumentToObject(DocumentData data) + public object DocumentToObject(DocumentData data, List included = null) { if (data == null) throw new JsonApiException(422, "Failed to deserialize document as json:api."); @@ -117,7 +117,7 @@ public object DocumentToObject(DocumentData data) var entity = Activator.CreateInstance(contextEntity.EntityType); entity = SetEntityAttributes(entity, contextEntity, data.Attributes); - entity = SetRelationships(entity, contextEntity, data.Relationships); + entity = SetRelationships(entity, contextEntity, data.Relationships, included); var identifiableEntity = (IIdentifiable)entity; @@ -172,7 +172,8 @@ private object DeserializeComplexType(JContainer obj, Type targetType) private object SetRelationships( object entity, ContextEntity contextEntity, - Dictionary relationships) + Dictionary relationships, + List included = null) { if (relationships == null || relationships.Count == 0) return entity; @@ -182,8 +183,8 @@ private object SetRelationships( foreach (var attr in contextEntity.Relationships) { entity = attr.IsHasOne - ? SetHasOneRelationship(entity, entityProperties, (HasOneAttribute)attr, contextEntity, relationships) - : SetHasManyRelationship(entity, entityProperties, attr, contextEntity, relationships); + ? SetHasOneRelationship(entity, entityProperties, (HasOneAttribute)attr, contextEntity, relationships, included) + : SetHasManyRelationship(entity, entityProperties, attr, contextEntity, relationships, included); } return entity; @@ -193,39 +194,53 @@ private object SetHasOneRelationship(object entity, PropertyInfo[] entityProperties, HasOneAttribute attr, ContextEntity contextEntity, - Dictionary relationships) + Dictionary relationships, + List included = null) { var relationshipName = attr.PublicRelationshipName; - if (relationships.TryGetValue(relationshipName, out RelationshipData relationshipData)) - { - var relationshipAttr = _jsonApiContext.RequestEntity.Relationships - .SingleOrDefault(r => r.PublicRelationshipName == relationshipName); + if (relationships.TryGetValue(relationshipName, out RelationshipData relationshipData) == false) + return entity; + + var relationshipAttr = _jsonApiContext.RequestEntity.Relationships + .SingleOrDefault(r => r.PublicRelationshipName == relationshipName); - if (relationshipAttr == null) - throw new JsonApiException(400, $"{_jsonApiContext.RequestEntity.EntityName} does not contain a relationship '{relationshipName}'"); + if (relationshipAttr == null) + throw new JsonApiException(400, $"{_jsonApiContext.RequestEntity.EntityName} does not contain a relationship '{relationshipName}'"); - var rio = (ResourceIdentifierObject)relationshipData.ExposedData; + var rio = (ResourceIdentifierObject)relationshipData.ExposedData; - var foreignKey = attr.IdentifiablePropertyName; - var entityProperty = entityProperties.FirstOrDefault(p => p.Name == foreignKey); - if (entityProperty == null && rio != null) - throw new JsonApiException(400, $"{contextEntity.EntityType.Name} does not contain a foreign key property '{foreignKey}' for has one relationship '{attr.InternalRelationshipName}'"); + var foreignKey = attr.IdentifiablePropertyName; + var foreignKeyProperty = entityProperties.FirstOrDefault(p => p.Name == foreignKey); - if (entityProperty != null) - { - // e.g. PATCH /articles - // {... { "relationships":{ "Owner": { "data" :null } } } } - if (rio == null && Nullable.GetUnderlyingType(entityProperty.PropertyType) == null) - throw new JsonApiException(400, $"Cannot set required relationship identifier '{attr.IdentifiablePropertyName}' to null."); + if (foreignKeyProperty == null && rio == null) + return entity; - var newValue = rio?.Id ?? null; - var convertedValue = TypeHelper.ConvertType(newValue, entityProperty.PropertyType); + if (foreignKeyProperty == null && rio != null) + throw new JsonApiException(400, $"{contextEntity.EntityType.Name} does not contain a foreign key property '{foreignKey}' for has one relationship '{attr.InternalRelationshipName}'"); - _jsonApiContext.RelationshipsToUpdate[relationshipAttr] = convertedValue; + // e.g. PATCH /articles + // {... { "relationships":{ "Owner": { "data": null } } } } + if (rio == null && Nullable.GetUnderlyingType(foreignKeyProperty.PropertyType) == null) + throw new JsonApiException(400, $"Cannot set required relationship identifier '{attr.IdentifiablePropertyName}' to null because it is a non-nullable type."); - entityProperty.SetValue(entity, convertedValue); - } + var newValue = rio?.Id ?? null; + var convertedValue = TypeHelper.ConvertType(newValue, foreignKeyProperty.PropertyType); + + _jsonApiContext.RelationshipsToUpdate[relationshipAttr] = convertedValue; + + foreignKeyProperty.SetValue(entity, convertedValue); + + + if(rio != null + // if the resource identifier is null, there should be no reason to instantiate an instance + && rio.Id != null) + { + // we have now set the FK property on the resource, now we need to check to see if the + // related entity was included in the payload and update its attributes + var includedRelationshipObject = GetIncludedRelationship(rio, included, relationshipAttr); + if (includedRelationshipObject != null) + relationshipAttr.SetValue(entity, includedRelationshipObject); } return entity; @@ -235,7 +250,8 @@ private object SetHasManyRelationship(object entity, PropertyInfo[] entityProperties, RelationshipAttribute attr, ContextEntity contextEntity, - Dictionary relationships) + Dictionary relationships, + List included = null) { var relationshipName = attr.PublicRelationshipName; @@ -245,14 +261,13 @@ private object SetHasManyRelationship(object entity, if (data == null) return entity; - var relationshipShells = relationshipData.ManyData.Select(r => + var relatedResources = relationshipData.ManyData.Select(r => { - var instance = attr.Type.New(); - instance.StringId = r.Id; + var instance = GetIncludedRelationship(r, included, attr); return instance; }); - var convertedCollection = TypeHelper.ConvertCollection(relationshipShells, attr.Type); + var convertedCollection = TypeHelper.ConvertCollection(relatedResources, attr.Type); attr.SetValue(entity, convertedCollection); @@ -261,5 +276,41 @@ private object SetHasManyRelationship(object entity, return entity; } + + private IIdentifiable GetIncludedRelationship(ResourceIdentifierObject relatedResourceIdentifier, List includedResources, RelationshipAttribute relationshipAttr) + { + // at this point we can be sure the relationshipAttr.Type is IIdentifiable because we were able to successfully build the ContextGraph + var relatedInstance = relationshipAttr.Type.New(); + relatedInstance.StringId = relatedResourceIdentifier.Id; + + // can't provide any more data other than the rio since it is not contained in the included section + if (includedResources == null || includedResources.Count == 0) + return relatedInstance; + + var includedResource = GetLinkedResource(relatedResourceIdentifier, includedResources); + if (includedResource == null) + return relatedInstance; + + var contextEntity = _jsonApiContext.ContextGraph.GetContextEntity(relationshipAttr.Type); + if (contextEntity == null) + throw new JsonApiException(400, $"Included type '{relationshipAttr.Type}' is not a registered json:api resource."); + + SetEntityAttributes(relatedInstance, contextEntity, includedResource.Attributes); + + return relatedInstance; + } + + private DocumentData GetLinkedResource(ResourceIdentifierObject relatedResourceIdentifier, List includedResources) + { + try + { + return includedResources.SingleOrDefault(r => r.Type == relatedResourceIdentifier.Type && r.Id == relatedResourceIdentifier.Id); + } + catch (InvalidOperationException e) + { + throw new JsonApiException(400, $"A compound document MUST NOT include more than one resource object for each type and id pair." + + $"The duplicate pair was '{relatedResourceIdentifier.Type}, {relatedResourceIdentifier.Id}'", e); + } + } } } diff --git a/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs b/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs index be16d1d9d7..58955bc4e2 100644 --- a/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs +++ b/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs @@ -392,20 +392,145 @@ public void Can_Deserialize_Object_With_HasManyRelationship() Assert.Equal(1, result.Id); Assert.NotNull(result.Dependents); Assert.NotEmpty(result.Dependents); - Assert.Equal(1, result.Dependents.Count); + Assert.Single(result.Dependents); var dependent = result.Dependents[0]; Assert.Equal(2, dependent.Id); } + [Fact] + public void Sets_Attribute_Values_On_Included_HasMany_Relationships() + { + // arrange + var contextGraphBuilder = new ContextGraphBuilder(); + contextGraphBuilder.AddResource("independents"); + contextGraphBuilder.AddResource("dependents"); + var contextGraph = contextGraphBuilder.Build(); + + var jsonApiContextMock = new Mock(); + jsonApiContextMock.SetupAllProperties(); + jsonApiContextMock.Setup(m => m.ContextGraph).Returns(contextGraph); + jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary()); + jsonApiContextMock.Setup(m => m.HasManyRelationshipPointers).Returns(new HasManyRelationshipPointers()); + + var jsonApiOptions = new JsonApiOptions(); + jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); + + var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object); + + var expectedName = "John Doe"; + var contentString = + @"{ + ""data"": { + ""type"": ""independents"", + ""id"": ""1"", + ""attributes"": { }, + ""relationships"": { + ""dependents"": { + ""data"": [ + { + ""type"": ""dependents"", + ""id"": ""2"" + } + ] + } + } + }, + ""included"": [ + { + ""type"": ""dependents"", + ""id"": ""2"", + ""attributes"": { + ""name"": """ + expectedName + @""" + } + } + ] + }"; + + // act + var result = deserializer.Deserialize(contentString); + + // assert + Assert.NotNull(result); + Assert.Equal(1, result.Id); + Assert.NotNull(result.Dependents); + Assert.NotEmpty(result.Dependents); + Assert.Single(result.Dependents); + + var dependent = result.Dependents[0]; + Assert.Equal(2, dependent.Id); + Assert.Equal(expectedName, dependent.Name); + } + + [Fact] + public void Sets_Attribute_Values_On_Included_HasOne_Relationships() + { + // arrange + var contextGraphBuilder = new ContextGraphBuilder(); + contextGraphBuilder.AddResource("independents"); + contextGraphBuilder.AddResource("dependents"); + var contextGraph = contextGraphBuilder.Build(); + + var jsonApiContextMock = new Mock(); + jsonApiContextMock.SetupAllProperties(); + jsonApiContextMock.Setup(m => m.ContextGraph).Returns(contextGraph); + jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary()); + jsonApiContextMock.Setup(m => m.RelationshipsToUpdate).Returns(new Dictionary()); + jsonApiContextMock.Setup(m => m.HasManyRelationshipPointers).Returns(new HasManyRelationshipPointers()); + + var jsonApiOptions = new JsonApiOptions(); + jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); + + var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object); + + var expectedName = "John Doe"; + var contentString = + @"{ + ""data"": { + ""type"": ""dependents"", + ""id"": ""1"", + ""attributes"": { }, + ""relationships"": { + ""independent"": { + ""data"": { + ""type"": ""independents"", + ""id"": ""2"" + } + } + } + }, + ""included"": [ + { + ""type"": ""independents"", + ""id"": ""2"", + ""attributes"": { + ""name"": """ + expectedName + @""" + } + } + ] + }"; + + // act + var result = deserializer.Deserialize(contentString); + + // assert + Assert.NotNull(result); + Assert.Equal(1, result.Id); + Assert.NotNull(result.Independent); + Assert.Equal(2, result.Independent.Id); + Assert.Equal(expectedName, result.Independent.Name); + } + private class OneToManyDependent : Identifiable { + [Attr("name")] public string Name { get; set; } [HasOne("independent")] public OneToManyIndependent Independent { get; set; } public int IndependentId { get; set; } } private class OneToManyIndependent : Identifiable { + [Attr("name")] public string Name { get; set; } [HasMany("dependents")] public List Dependents { get; set; } } } diff --git a/test/UnitTests/Services/Operations/Processors/CreateOpProcessorTests.cs b/test/UnitTests/Services/Operations/Processors/CreateOpProcessorTests.cs index 9e33af63c2..aa76f2dc17 100644 --- a/test/UnitTests/Services/Operations/Processors/CreateOpProcessorTests.cs +++ b/test/UnitTests/Services/Operations/Processors/CreateOpProcessorTests.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Internal; @@ -48,7 +48,7 @@ public async Task ProcessAsync_Deserializes_And_Creates() .AddResource("test-resources") .Build(); - _deserializerMock.Setup(m => m.DocumentToObject(It.IsAny())) + _deserializerMock.Setup(m => m.DocumentToObject(It.IsAny(), It.IsAny>())) .Returns(testResource); var opProcessor = new CreateOpProcessor( From 32522d0340a23333ff4c6ade6cda77dfd61e6835 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Wed, 27 Jun 2018 15:09:45 -0700 Subject: [PATCH 12/12] handle attaching HasOne relationships --- .../Data/DefaultEntityRepository.cs | 20 +++++++- .../Extensions/DbContextExtensions.cs | 21 +++++++++ .../Request/HasOneRelationshipPointers.cs | 46 +++++++++++++++++++ .../Serialization/JsonApiDeSerializer.cs | 5 ++ .../Services/IJsonApiContext.cs | 25 ++++++++++ .../Services/JsonApiContext.cs | 1 + .../Acceptance/TodoItemsControllerTests.cs | 5 +- .../Serialization/JsonApiDeSerializerTests.cs | 1 + 8 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 src/JsonApiDotNetCore/Request/HasOneRelationshipPointers.cs diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index e6d534172f..d1b82e30bc 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -84,13 +84,19 @@ public virtual async Task GetAndIncludeAsync(TId id, string relationshi public virtual async Task CreateAsync(TEntity entity) { - AttachHasManyPointers(); + AttachRelationships(); _dbSet.Add(entity); await _context.SaveChangesAsync(); return entity; } + protected virtual void AttachRelationships() + { + AttachHasManyPointers(); + AttachHasOnePointers(); + } + /// /// This is used to allow creation of HasMany relationships when the /// dependent side of the relationship already exists. @@ -107,6 +113,18 @@ private void AttachHasManyPointers() } } + /// + /// This is used to allow creation of HasOne relationships when the + /// independent side of the relationship already exists. + /// + private void AttachHasOnePointers() + { + var relationships = _jsonApiContext.HasOneRelationshipPointers.Get(); + foreach (var relationship in relationships) + if (_context.Entry(relationship.Value).State == EntityState.Detached && _context.EntityIsTracked(relationship.Value) == false) + _context.Entry(relationship.Value).State = EntityState.Unchanged; + } + public virtual async Task UpdateAsync(TId id, TEntity entity) { var oldEntity = await GetAsync(id); diff --git a/src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs b/src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs index 3cb5ccc359..2756524dce 100644 --- a/src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs @@ -1,4 +1,7 @@ using System; +using System.Linq; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Models; using Microsoft.EntityFrameworkCore; namespace JsonApiDotNetCore.Extensions @@ -8,5 +11,23 @@ public static class DbContextExtensions [Obsolete("This is no longer required since the introduction of context.Set", error: false)] public static DbSet GetDbSet(this DbContext context) where T : class => context.Set(); + + /// + /// Determines whether or not EF is already tracking an entity of the same Type and Id + /// + public static bool EntityIsTracked(this DbContext context, IIdentifiable entity) + { + if (entity == null) + throw new ArgumentNullException(nameof(entity)); + + var trackedEntries = context.ChangeTracker + .Entries() + .FirstOrDefault(entry => + entry.Entity.GetType() == entity.GetType() + && ((IIdentifiable)entry.Entity).StringId == entity.StringId + ); + + return trackedEntries != null; + } } } diff --git a/src/JsonApiDotNetCore/Request/HasOneRelationshipPointers.cs b/src/JsonApiDotNetCore/Request/HasOneRelationshipPointers.cs new file mode 100644 index 0000000000..9e0bdd0e15 --- /dev/null +++ b/src/JsonApiDotNetCore/Request/HasOneRelationshipPointers.cs @@ -0,0 +1,46 @@ +using JsonApiDotNetCore.Models; +using System; +using System.Collections.Generic; + +namespace JsonApiDotNetCore.Request +{ + /// + /// Stores information to set relationships for the request resource. + /// These relationships must already exist and should not be re-created. + /// + /// The expected use case is POST-ing or PATCH-ing + /// an entity with HasOne relationships: + /// + /// { + /// "data": { + /// "type": "photos", + /// "attributes": { + /// "title": "Ember Hamster", + /// "src": "http://example.com/images/productivity.png" + /// }, + /// "relationships": { + /// "photographer": { + /// "data": { "type": "people", "id": "2" } + /// } + /// } + /// } + /// } + /// + /// + public class HasOneRelationshipPointers + { + private Dictionary _hasOneRelationships = new Dictionary(); + + /// + /// Add the relationship to the list of relationships that should be + /// set in the repository layer. + /// + public void Add(Type dependentType, IIdentifiable entity) + => _hasOneRelationships[dependentType] = entity; + + /// + /// Get all the models that should be associated + /// + public Dictionary Get() => _hasOneRelationships; + } +} diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index f7a54ffbd3..a1e3c76214 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -241,6 +241,11 @@ private object SetHasOneRelationship(object entity, var includedRelationshipObject = GetIncludedRelationship(rio, included, relationshipAttr); if (includedRelationshipObject != null) relationshipAttr.SetValue(entity, includedRelationshipObject); + + // we need to store the fact that this relationship was included in the payload + // for EF, the repository will use these pointers to make ensure we don't try to + // create resources if they already exist, we just need to create the relationship + _jsonApiContext.HasOneRelationshipPointers.Add(attr.Type, includedRelationshipObject); } return entity; diff --git a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs index 52036f21af..9cbe2a53ca 100644 --- a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs @@ -81,6 +81,31 @@ public interface IJsonApiRequest : IJsonApiApplication, IUpdateRequest, IQueryRe /// HasManyRelationshipPointers HasManyRelationshipPointers { get; } + /// + /// Stores information to set relationships for the request resource. + /// These relationships must already exist and should not be re-created. + /// + /// The expected use case is POST-ing or PATCH-ing + /// an entity with HasOne relationships: + /// + /// { + /// "data": { + /// "type": "photos", + /// "attributes": { + /// "title": "Ember Hamster", + /// "src": "http://example.com/images/productivity.png" + /// }, + /// "relationships": { + /// "photographer": { + /// "data": { "type": "people", "id": "2" } + /// } + /// } + /// } + /// } + /// + /// + HasOneRelationshipPointers HasOneRelationshipPointers { get; } + /// /// If the request is a bulk json:api v1.1 operations request. /// This is determined by the ` diff --git a/src/JsonApiDotNetCore/Services/JsonApiContext.cs b/src/JsonApiDotNetCore/Services/JsonApiContext.cs index 0643d494d6..f579d8c9a2 100644 --- a/src/JsonApiDotNetCore/Services/JsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/JsonApiContext.cs @@ -53,6 +53,7 @@ public JsonApiContext( public Dictionary DocumentMeta { get; set; } public bool IsBulkOperationRequest { get; set; } public HasManyRelationshipPointers HasManyRelationshipPointers { get; } = new HasManyRelationshipPointers(); + public HasOneRelationshipPointers HasOneRelationshipPointers { get; } = new HasOneRelationshipPointers(); public IJsonApiContext ApplyContext(object controller) { diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs index acd37a535a..672492df16 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs @@ -294,11 +294,12 @@ public async Task Can_Post_TodoItem() // Act var response = await _fixture.Client.SendAsync(request); - var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = (TodoItem)_fixture.GetService().Deserialize(body); // Assert Assert.Equal(HttpStatusCode.Created, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + var deserializedBody = (TodoItem)_fixture.GetService().Deserialize(body); + Assert.Equal(HttpStatusCode.Created, response.StatusCode); Assert.Equal(todoItem.Description, deserializedBody.Description); Assert.Equal(todoItem.CreatedDate.ToString("G"), deserializedBody.CreatedDate.ToString("G")); Assert.Null(deserializedBody.AchievedDate); diff --git a/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs b/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs index 58955bc4e2..2da434c765 100644 --- a/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs +++ b/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs @@ -477,6 +477,7 @@ public void Sets_Attribute_Values_On_Included_HasOne_Relationships() jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary()); jsonApiContextMock.Setup(m => m.RelationshipsToUpdate).Returns(new Dictionary()); jsonApiContextMock.Setup(m => m.HasManyRelationshipPointers).Returns(new HasManyRelationshipPointers()); + jsonApiContextMock.Setup(m => m.HasOneRelationshipPointers).Returns(new HasOneRelationshipPointers()); var jsonApiOptions = new JsonApiOptions(); jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions);