From acb7bc1439e57c868ad59aaad8463ba1bc85743d Mon Sep 17 00:00:00 2001 From: jaredcnance <jaredcnance@gmail.com> Date: Sat, 7 Apr 2018 18:33:55 -0500 Subject: [PATCH 01/13] 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; } + + /// <summary> + /// Creates a List{TInterface} where TInterface is the generic for type specified by t + /// </summary> + public static List<TInterface> GetEmptyCollection<TInterface>(this Type t) + { + if (t == null) throw new ArgumentNullException(nameof(t)); + + var listType = typeof(List<>).MakeGenericType(t); + var list = (List<TInterface>)Activator.CreateInstance(listType); + return list; + } + + /// <summary> + /// Creates a new instance of type t, casting it to the specified TInterface + /// </summary> + public static TInterface New<TInterface>(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 6fb7af55e1..d55cb48a2e 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<string, RelationshipData> 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<IGenericProcessor>(typeof(GenericProcessor<>), attr.Type); + var resourceRelationships = attr.Type.GetEmptyCollection<IIdentifiable>(); - var ids = relationshipData.ManyData.Select(r => r.Id); + var relationshipShells = relationshipData.ManyData.Select(r => + { + var instance = attr.Type.New<IIdentifiable>(); + 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<IIdentifiable>(); + + // assert + Assert.NotNull(collection); + Assert.Empty(collection); + Assert.IsType<List<Model>>(collection); + } + + [Fact] + public void New_Creates_An_Instance_If_T_Implements_Interface() + { + // arrange + var type = typeof(Model); + + // act + var instance = type.New<IIdentifiable>(); + + // assert + Assert.NotNull(instance); + Assert.IsType<Model>(instance); + } + + private class Model : IIdentifiable + { + public string StringId { get; set; } + } + } +} From 5f4640f632aded7ffc9049c5cd542309bc9d2184 Mon Sep 17 00:00:00 2001 From: jaredcnance <jaredcnance@gmail.com> Date: Sat, 7 Apr 2018 19:08:18 -0500 Subject: [PATCH 02/13] 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) /// <summary> /// Creates a List{TInterface} where TInterface is the generic for type specified by t /// </summary> - public static List<TInterface> GetEmptyCollection<TInterface>(this Type t) + public static IEnumerable<TInterface> GetEmptyCollection<TInterface>(this Type t) { if (t == null) throw new ArgumentNullException(nameof(t)); var listType = typeof(List<>).MakeGenericType(t); - var list = (List<TInterface>)Activator.CreateInstance(listType); + var list = (IEnumerable<TInterface>)Activator.CreateInstance(listType); return list; } From 31a4b76dfa07cca313f69c3aa97cc601498869a5 Mon Sep 17 00:00:00 2001 From: jaredcnance <jaredcnance@gmail.com> Date: Sat, 7 Apr 2018 20:57:26 -0500 Subject: [PATCH 03/13] fix(Deserializer): properly convert collection type when setting it on the model --- src/JsonApiDotNetCore/Extensions/TypeExtensions.cs | 4 ++-- src/JsonApiDotNetCore/Internal/TypeHelper.cs | 8 ++++++++ src/JsonApiDotNetCore/Models/HasManyAttribute.cs | 2 +- .../Serialization/JsonApiDeSerializer.cs | 10 ++++++---- test/UnitTests/Extensions/TypeExtensions_Tests.cs | 6 +++--- 5 files changed, 20 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) /// <summary> /// Creates a List{TInterface} where TInterface is the generic for type specified by t /// </summary> - public static IEnumerable<TInterface> GetEmptyCollection<TInterface>(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<TInterface>)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 5135473cdb..0a3e01d0d1 100644 --- a/src/JsonApiDotNetCore/Internal/TypeHelper.cs +++ b/src/JsonApiDotNetCore/Internal/TypeHelper.cs @@ -7,6 +7,14 @@ namespace JsonApiDotNetCore.Internal { public static class TypeHelper { + public static IList ConvertCollection(IEnumerable<object> 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 4519dc8cb6..c2d7594400 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 d55cb48a2e..723e831e1e 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<IIdentifiable>(); - var relationshipShells = relationshipData.ManyData.Select(r => { var instance = attr.Type.New<IIdentifiable>(); @@ -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<IIdentifiable>(); + var collection = type.GetEmptyCollection(); // assert Assert.NotNull(collection); From 341eb8061dcd866ceca6c2565481e81eb4c6152b Mon Sep 17 00:00:00 2001 From: jaredcnance <jaredcnance@gmail.com> Date: Sun, 8 Apr 2018 07:10:26 -0500 Subject: [PATCH 04/13] 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 | 2 + 7 files changed, 78 insertions(+), 24 deletions(-) create mode 100644 src/JsonApiDotNetCore/Request/HasManyRelationshipPointers.cs diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index b6bcda29b3..fe85c84049 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -85,10 +85,29 @@ public virtual async Task<TEntity> GetAndIncludeAsync(TId id, string relationshi public virtual async Task<TEntity> CreateAsync(TEntity entity) { _dbSet.Add(entity); + + DetachHasManyPointers(); + await _context.SaveChangesAsync(); return entity; } + /// <summary> + /// This is used to allow creation of HasMany relationships when the + /// dependent side of the relationship already exists. + /// </summary> + 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<TEntity> 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<T> GetDbSet<T>(this DbContext context) where T : class - { - var contextProperties = context.GetType().GetProperties(); - foreach(var property in contextProperties) - { - if (property.PropertyType == typeof(DbSet<T>)) - return (DbSet<T>)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<T>", error: false)] + public static DbSet<T> GetDbSet<T>(this DbContext context) where T : class + => context.Set<T>(); } } 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 +{ + /// <summary> + /// 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: + /// <code> + /// { + /// "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" } + /// ] + /// } + /// } + /// } + /// } + /// </code> + /// </summary> + public class HasManyRelationshipPointers + { + private Dictionary<Type, IList> _hasManyRelationships = new Dictionary<Type, IList>(); + + /// <summary> + /// Add the relationship to the list of relationships that should be + /// set in the repository layer. + /// </summary> + public void Add(Type dependentType, IList entities) + => _hasManyRelationships[dependentType] = entities; + + /// <summary> + /// Get all the models that should be associated + /// </summary> + public Dictionary<Type, IList> Get() => _hasManyRelationships; + } +} diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index 723e831e1e..5ef13609d6 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<string, RelationshipData> 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<string, object> DocumentMeta { get; set; } bool IsBulkOperationRequest { get; set; } + HasManyRelationshipPointers HasManyRelationshipPointers { get; } TAttribute GetControllerAttribute<TAttribute>() where TAttribute : Attribute; } diff --git a/src/JsonApiDotNetCore/Services/JsonApiContext.cs b/src/JsonApiDotNetCore/Services/JsonApiContext.cs index 2665217fef..0643d494d6 100644 --- a/src/JsonApiDotNetCore/Services/JsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/JsonApiContext.cs @@ -6,6 +6,7 @@ using JsonApiDotNetCore.Internal.Generics; using JsonApiDotNetCore.Internal.Query; using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Request; using Microsoft.AspNetCore.Http; namespace JsonApiDotNetCore.Services @@ -51,6 +52,7 @@ public JsonApiContext( public Type ControllerType { get; set; } public Dictionary<string, object> DocumentMeta { get; set; } public bool IsBulkOperationRequest { get; set; } + public HasManyRelationshipPointers HasManyRelationshipPointers { get; } = new HasManyRelationshipPointers(); public IJsonApiContext ApplyContext<T>(object controller) { From c453f319dd10cf4de2296fc64d668397e3d8d39c Mon Sep 17 00:00:00 2001 From: jaredcnance <jaredcnance@gmail.com> Date: Sun, 8 Apr 2018 07:26:15 -0500 Subject: [PATCH 05/13] 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 fe85c84049..032fef13c4 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -84,10 +84,9 @@ public virtual async Task<TEntity> GetAndIncludeAsync(TId id, string relationshi public virtual async Task<TEntity> CreateAsync(TEntity entity) { + AttachHasManyPointers(); _dbSet.Add(entity); - DetachHasManyPointers(); - await _context.SaveChangesAsync(); return entity; } @@ -96,7 +95,7 @@ public virtual async Task<TEntity> CreateAsync(TEntity entity) /// This is used to allow creation of HasMany relationships when the /// dependent side of the relationship already exists. /// </summary> - private void DetachHasManyPointers() + private void AttachHasManyPointers() { var relationships = _jsonApiContext.HasManyRelationshipPointers.Get(); foreach(var relationship in relationships) From 67bb3d514f5b6dcafe66f1afdd2b5edc29327ca7 Mon Sep 17 00:00:00 2001 From: jaredcnance <jaredcnance@gmail.com> Date: Sun, 8 Apr 2018 15:33:58 -0500 Subject: [PATCH 06/13] 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<AppDbContext>(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 @@ <PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" /> <PackageReference Include="xunit" Version="$(XUnitVersion)" /> <PackageReference Include="Moq" Version="$(MoqVersion)" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="2.0.2" /> </ItemGroup> </Project> From ef60b90c1c02843582e309e7a91da029844b076f Mon Sep 17 00:00:00 2001 From: jaredcnance <jaredcnance@gmail.com> Date: Thu, 10 May 2018 14:18:38 -0500 Subject: [PATCH 07/13] add serialization tests --- .../Serialization/JsonApiDeSerializerTests.cs | 261 +++++++++++------- 1 file changed, 163 insertions(+), 98 deletions(-) diff --git a/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs b/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs index 1e20c0359e..0b80d3a25a 100644 --- a/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs +++ b/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs @@ -2,8 +2,8 @@ using System.Collections.Generic; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Internal.Generics; using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Request; using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Services; using Moq; @@ -11,10 +11,13 @@ using Newtonsoft.Json.Serialization; using Xunit; -namespace UnitTests.Serialization { - public class JsonApiDeSerializerTests { +namespace UnitTests.Serialization +{ + public class JsonApiDeSerializerTests + { [Fact] - public void Can_Deserialize_Complex_Types() { + public void Can_Deserialize_Complex_Types() + { // arrange var contextGraphBuilder = new ContextGraphBuilder(); contextGraphBuilder.AddResource<TestResource>("test-resource"); @@ -29,20 +32,18 @@ public void Can_Deserialize_Complex_Types() { jsonApiOptions.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); - var genericProcessorFactoryMock = new Mock<IGenericProcessorFactory>(); - - var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object, genericProcessorFactoryMock.Object); + var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object); - var content = new Document { - Data = new DocumentData { - Type = "test-resource", - Id = "1", - Attributes = new Dictionary<string, object> { + var content = new Document + { + Data = new DocumentData { - "complex-member", - new { compoundName = "testName" } - } - } + Type = "test-resource", + Id = "1", + Attributes = new Dictionary<string, object> + { + { "complex-member", new { compoundName = "testName" } } + } } }; @@ -55,7 +56,8 @@ public void Can_Deserialize_Complex_Types() { } [Fact] - public void Can_Deserialize_Complex_List_Types() { + public void Can_Deserialize_Complex_List_Types() + { // arrange var contextGraphBuilder = new ContextGraphBuilder(); contextGraphBuilder.AddResource<TestResourceWithList>("test-resource"); @@ -69,22 +71,18 @@ public void Can_Deserialize_Complex_List_Types() { jsonApiOptions.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); - var genericProcessorFactoryMock = new Mock<IGenericProcessorFactory>(); + var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object); - var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object, genericProcessorFactoryMock.Object); - - var content = new Document { - Data = new DocumentData { - Type = "test-resource", - Id = "1", - Attributes = new Dictionary<string, object> { + var content = new Document + { + Data = new DocumentData { - "complex-members", - new [] { - new { compoundName = "testName" } - } - } - } + Type = "test-resource", + Id = "1", + Attributes = new Dictionary<string, object> + { + { "complex-members", new [] { new { compoundName = "testName" } } } + } } }; @@ -98,7 +96,8 @@ public void Can_Deserialize_Complex_List_Types() { } [Fact] - public void Can_Deserialize_Complex_Types_With_Dasherized_Attrs() { + public void Can_Deserialize_Complex_Types_With_Dasherized_Attrs() + { // arrange var contextGraphBuilder = new ContextGraphBuilder(); contextGraphBuilder.AddResource<TestResource>("test-resource"); @@ -113,20 +112,20 @@ public void Can_Deserialize_Complex_Types_With_Dasherized_Attrs() { jsonApiOptions.SerializerSettings.ContractResolver = new DasherizedResolver(); // <-- jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); - var genericProcessorFactoryMock = new Mock<IGenericProcessorFactory>(); + var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object); - var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object, genericProcessorFactoryMock.Object); - - var content = new Document { - Data = new DocumentData { - Type = "test-resource", - Id = "1", - Attributes = new Dictionary<string, object> { + var content = new Document + { + Data = new DocumentData { - "complex-member", - new Dictionary<string, string> { { "compound-name", "testName" } } - } - } + Type = "test-resource", + Id = "1", + Attributes = new Dictionary<string, object> + { + { + "complex-member", new Dictionary<string, string> { { "compound-name", "testName" } } + } + } } }; @@ -139,7 +138,8 @@ public void Can_Deserialize_Complex_Types_With_Dasherized_Attrs() { } [Fact] - public void Immutable_Attrs_Are_Not_Included_In_AttributesToUpdate() { + public void Immutable_Attrs_Are_Not_Included_In_AttributesToUpdate() + { // arrange var contextGraphBuilder = new ContextGraphBuilder(); contextGraphBuilder.AddResource<TestResource>("test-resource"); @@ -156,22 +156,21 @@ public void Immutable_Attrs_Are_Not_Included_In_AttributesToUpdate() { jsonApiOptions.SerializerSettings.ContractResolver = new DasherizedResolver(); jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); - var genericProcessorFactoryMock = new Mock<IGenericProcessorFactory>(); + var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object); - var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object, genericProcessorFactoryMock.Object); - - var content = new Document { - Data = new DocumentData { - Type = "test-resource", - Id = "1", - Attributes = new Dictionary<string, object> { + var content = new Document + { + Data = new DocumentData { - "complex-member", - new Dictionary<string, string> { { "compound-name", "testName" } - } - }, - { "immutable", "value" } - } + Type = "test-resource", + Id = "1", + Attributes = new Dictionary<string, object> + { + { + "complex-member", new Dictionary<string, string> { { "compound-name", "testName" } } + }, + { "immutable", "value" } + } } }; @@ -189,7 +188,8 @@ public void Immutable_Attrs_Are_Not_Included_In_AttributesToUpdate() { } [Fact] - public void Can_Deserialize_Independent_Side_Of_One_To_One_Relationship() { + public void Can_Deserialize_Independent_Side_Of_One_To_One_Relationship() + { // arrange var contextGraphBuilder = new ContextGraphBuilder(); contextGraphBuilder.AddResource<Independent>("independents"); @@ -204,17 +204,16 @@ public void Can_Deserialize_Independent_Side_Of_One_To_One_Relationship() { var jsonApiOptions = new JsonApiOptions(); jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); - var genericProcessorFactoryMock = new Mock<IGenericProcessorFactory>(); - - var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object, genericProcessorFactoryMock.Object); + var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object); var property = Guid.NewGuid().ToString(); - var content = new Document { - Data = new DocumentData { - Type = "independents", - Id = "1", - Attributes = new Dictionary<string, object> { { "property", property } - } + var content = new Document + { + Data = new DocumentData + { + Type = "independents", + Id = "1", + Attributes = new Dictionary<string, object> { { "property", property } } } }; @@ -229,7 +228,8 @@ public void Can_Deserialize_Independent_Side_Of_One_To_One_Relationship() { } [Fact] - public void Can_Deserialize_Independent_Side_Of_One_To_One_Relationship_With_Relationship_Body() { + public void Can_Deserialize_Independent_Side_Of_One_To_One_Relationship_With_Relationship_Body() + { // arrange var contextGraphBuilder = new ContextGraphBuilder(); contextGraphBuilder.AddResource<Independent>("independents"); @@ -244,20 +244,18 @@ public void Can_Deserialize_Independent_Side_Of_One_To_One_Relationship_With_Rel var jsonApiOptions = new JsonApiOptions(); jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); - var genericProcessorFactoryMock = new Mock<IGenericProcessorFactory>(); - - var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object, genericProcessorFactoryMock.Object); + var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object); var property = Guid.NewGuid().ToString(); - var content = new Document { - Data = new DocumentData { - Type = "independents", - Id = "1", - Attributes = new Dictionary<string, object> { { "property", property } - }, - // a common case for this is deserialization in unit tests - Relationships = new Dictionary<string, RelationshipData> { { "dependent", new RelationshipData { } } - } + var content = new Document + { + Data = new DocumentData + { + Type = "independents", + Id = "1", + Attributes = new Dictionary<string, object> { { "property", property } }, + // a common case for this is deserialization in unit tests + Relationships = new Dictionary<string, RelationshipData> { { "dependent", new RelationshipData { } } } } }; @@ -288,24 +286,21 @@ public void Sets_The_DocumentMeta_Property_In_JsonApiContext() var jsonApiOptions = new JsonApiOptions(); jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); - var genericProcessorFactoryMock = new Mock<IGenericProcessorFactory>(); - var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object, genericProcessorFactoryMock.Object); + var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object); var property = Guid.NewGuid().ToString(); - + var content = new Document - { - Meta = new Dictionary<string, object>() { {"foo", "bar"}}, + { + Meta = new Dictionary<string, object>() { { "foo", "bar" } }, Data = new DocumentData { Type = "independents", Id = "1", - Attributes = new Dictionary<string, object> { { "property", property } - }, + Attributes = new Dictionary<string, object> { { "property", property } }, // a common case for this is deserialization in unit tests - Relationships = new Dictionary<string, RelationshipData> { { "dependent", new RelationshipData { } } - } + Relationships = new Dictionary<string, RelationshipData> { { "dependent", new RelationshipData { } } } } }; @@ -318,32 +313,102 @@ public void Sets_The_DocumentMeta_Property_In_JsonApiContext() jsonApiContextMock.VerifySet(mock => mock.DocumentMeta = content.Meta); } - - private class TestResource : Identifiable { + private class TestResource : Identifiable + { [Attr("complex-member")] public ComplexType ComplexMember { get; set; } - [Attr("immutable", isImmutable : true)] + [Attr("immutable", isImmutable: true)] public string Immutable { get; set; } } - private class TestResourceWithList : Identifiable { + private class TestResourceWithList : Identifiable + { [Attr("complex-members")] public List<ComplexType> ComplexMembers { get; set; } } - private class ComplexType { + private class ComplexType + { public string CompoundName { get; set; } } - private class Independent : Identifiable { + private class Independent : Identifiable + { [Attr("property")] public string Property { get; set; } [HasOne("dependent")] public Dependent Dependent { get; set; } } - private class Dependent : Identifiable { + private class Dependent : Identifiable + { [HasOne("independent")] public Independent Independent { get; set; } public int IndependentId { get; set; } } + + + + [Fact] + public void Can_Deserialize_Object_With_HasManyRelationship() + { + // arrange + var contextGraphBuilder = new ContextGraphBuilder(); + contextGraphBuilder.AddResource<OneToManyIndependent>("independents"); + contextGraphBuilder.AddResource<OneToManyDependent>("dependents"); + var contextGraph = contextGraphBuilder.Build(); + + var jsonApiContextMock = new Mock<IJsonApiContext>(); + jsonApiContextMock.SetupAllProperties(); + jsonApiContextMock.Setup(m => m.ContextGraph).Returns(contextGraph); + jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary<AttrAttribute, object>()); + 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 contentString = + @"{ + ""data"": { + ""type"": ""independents"", + ""id"": ""1"", + ""attributes"": { }, + ""relationships"": { + ""dependents"": { + ""data"": [ + { + ""type"": ""dependents"", + ""id"": ""2"" + } + ] + } + } + } + }"; + + // act + var result = deserializer.Deserialize<OneToManyIndependent>(contentString); + + // assert + Assert.NotNull(result); + Assert.Equal(1, result.Id); + Assert.NotNull(result.Dependents); + Assert.NotEmpty(result.Dependents); + Assert.Equal(1, result.Dependents.Count); + + var dependent = result.Dependents[0]; + Assert.Equal(2, dependent.Id); + } + + private class OneToManyDependent : Identifiable + { + [HasOne("independent")] public OneToManyIndependent Independent { get; set; } + public int IndependentId { get; set; } + } + + private class OneToManyIndependent : Identifiable + { + [HasMany("dependents")] public List<OneToManyDependent> Dependents { get; set; } + } } } From b94b252cffa3055d06bce313fec270b115d02687 Mon Sep 17 00:00:00 2001 From: jaredcnance <jaredcnance@gmail.com> Date: Thu, 10 May 2018 17:00:12 -0500 Subject: [PATCH 08/13] refactor(JsonApiContext): begin interface separation remove redundant IsRelationship property --- .../Builders/DocumentBuilder.cs | 2 +- .../Services/EntityResourceService.cs | 5 +- .../Services/IJsonApiContext.cs | 108 ++++++++++++++++-- 3 files changed, 99 insertions(+), 16 deletions(-) diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index 088a2bf092..0beb0516c1 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -107,7 +107,7 @@ public DocumentData GetData(ContextEntity contextEntity, IIdentifiable entity) Id = entity.StringId }; - if (_jsonApiContext.IsRelationshipData) + if (_jsonApiContext.IsRelationshipPath) return data; data.Attributes = new Dictionary<string, object>(); diff --git a/src/JsonApiDotNetCore/Services/EntityResourceService.cs b/src/JsonApiDotNetCore/Services/EntityResourceService.cs index bc7a2adb52..ba9d6b987e 100644 --- a/src/JsonApiDotNetCore/Services/EntityResourceService.cs +++ b/src/JsonApiDotNetCore/Services/EntityResourceService.cs @@ -80,10 +80,7 @@ private async Task<T> GetWithRelationshipsAsync(TId id) } public virtual async Task<object> GetRelationshipsAsync(TId id, string relationshipName) - { - _jsonApiContext.IsRelationshipData = true; - return await GetRelationshipAsync(id, relationshipName); - } + => await GetRelationshipAsync(id, relationshipName); public virtual async Task<object> GetRelationshipAsync(TId id, string relationshipName) { diff --git a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs index 132630446d..6a358f4732 100644 --- a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs @@ -10,27 +10,113 @@ namespace JsonApiDotNetCore.Services { - public interface IJsonApiContext + public interface IJsonApiApplication { JsonApiOptions Options { get; set; } - IJsonApiContext ApplyContext<T>(object controller); IContextGraph ContextGraph { get; set; } - ContextEntity RequestEntity { get; set; } - string BasePath { get; set; } - QuerySet QuerySet { get; set; } - bool IsRelationshipData { get; set; } + } + + public interface IQueryRequest + { List<string> IncludedRelationships { get; set; } - bool IsRelationshipPath { get; } + QuerySet QuerySet { get; set; } PageManager PageManager { get; set; } - IMetaBuilder MetaBuilder { get; set; } - IGenericProcessorFactory GenericProcessorFactory { get; set; } + } + + public interface IUpdateRequest + { Dictionary<AttrAttribute, object> AttributesToUpdate { get; set; } Dictionary<RelationshipAttribute, object> RelationshipsToUpdate { get; set; } + } + + public interface IJsonApiRequest : IJsonApiApplication, IUpdateRequest, IQueryRequest + { + /// <summary> + /// The request namespace. This may be an absolute or relative path + /// depending upon the configuration. + /// </summary> + /// <example> + /// Absolute: https://example.com/api/v1 + /// + /// Relative: /api/v1 + /// </example> + string BasePath { get; set; } + + /// <summary> + /// Stores information to set relationships for the request resource. + /// These relationships must already exist and should not be re-created. + /// By default, it is the responsibility of the repository to use the + /// relationship pointers to persist the relationship. + /// + /// The expected use case is POST-ing or PATCH-ing an entity with HasMany + /// relaitonships: + /// <code> + /// { + /// "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" } + /// ] + /// } + /// } + /// } + /// } + /// </code> + /// </summary> + HasManyRelationshipPointers HasManyRelationshipPointers { get; } + + /// <summary> + /// If the request is a bulk json:api v1.1 operations request. + /// This is determined by the ` + /// <see cref="JsonApiDotNetCore.Serialization.JsonApiDeSerializer" />` class. + /// + /// See [json-api/1254](https://github.com/json-api/json-api/pull/1254) for details. + /// </summary> + bool IsBulkOperationRequest { get; set; } + + /// <summary> + /// The `<see cref="ContextEntity" />`for the target route. + /// </summary> + /// + /// <example> + /// For a `GET /articles` request, `RequestEntity` will be set + /// to the `Article` resource representation on the `JsonApiContext`. + /// </example> + ContextEntity RequestEntity { get; set; } + + /// <summary> + /// The concrete type of the controller that was activated by the MVC routing middleware + /// </summary> Type ControllerType { get; set; } + + /// <summary> + /// The json:api meta data at the document level + /// </summary> Dictionary<string, object> DocumentMeta { get; set; } - bool IsBulkOperationRequest { get; set; } - HasManyRelationshipPointers HasManyRelationshipPointers { get; } + /// <summary> + /// If the request is on the `{id}/relationships/{relationshipName}` route + /// </summary> + bool IsRelationshipPath { get; } + + [Obsolete("Use `IsRelationshipPath` instead.")] + bool IsRelationshipData { get; set; } + } + + public interface IJsonApiContext : IJsonApiRequest + { + IJsonApiContext ApplyContext<T>(object controller); + IMetaBuilder MetaBuilder { get; set; } + IGenericProcessorFactory GenericProcessorFactory { get; set; } + + [Obsolete("Use the proxied method IControllerContext.GetControllerAttribute instead.")] TAttribute GetControllerAttribute<TAttribute>() where TAttribute : Attribute; } } From fa6ee6b822efd7e45eff2c3f82c9c1826083a257 Mon Sep 17 00:00:00 2001 From: jaredcnance <jaredcnance@gmail.com> Date: Tue, 5 Jun 2018 07:01:18 -0500 Subject: [PATCH 09/13] document(IUpdateRequest) --- src/JsonApiDotNetCore/Services/IJsonApiContext.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs index 6a358f4732..52036f21af 100644 --- a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs @@ -25,7 +25,16 @@ public interface IQueryRequest public interface IUpdateRequest { + /// <summary> + /// The attributes that were included in a PATCH request. + /// Only the attributes in this dictionary should be updated. + /// </summary> Dictionary<AttrAttribute, object> AttributesToUpdate { get; set; } + + /// <summary> + /// Any relationships that were included in a PATCH request. + /// Only the relationships in this dictionary should be updated. + /// </summary> Dictionary<RelationshipAttribute, object> RelationshipsToUpdate { get; set; } } From 6741efd9e8f435e05614d557dd2c46a4f3215f91 Mon Sep 17 00:00:00 2001 From: jaredcnance <jaredcnance@gmail.com> Date: Fri, 8 Jun 2018 23:01:42 -0500 Subject: [PATCH 10/13] fix(JsonApiDeserializer): if hasOne is nullable allow it to be set null --- .../Models/HasOneAttribute.cs | 7 +- .../Models/RelationshipAttribute.cs | 22 + .../Serialization/JsonApiDeSerializer.cs | 10 +- .../Services/EntityResourceService.cs | 2 +- .../Spec/UpdatingRelationshipsTests.cs | 95 ++++ .../Builders/DocumentBuilder_Tests.cs | 529 +++++++++--------- .../IServiceCollectionExtensionsTests.cs | 5 +- 7 files changed, 395 insertions(+), 275 deletions(-) diff --git a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs b/src/JsonApiDotNetCore/Models/HasOneAttribute.cs index 03fdb200fc..77422027a7 100644 --- a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs +++ b/src/JsonApiDotNetCore/Models/HasOneAttribute.cs @@ -39,9 +39,14 @@ public HasOneAttribute(string publicName, Link documentLinks = Link.All, bool ca ? $"{InternalRelationshipName}Id" : _explicitIdentifiablePropertyName; + /// <summary> + /// Sets the value of the property identified by this attribute + /// </summary> + /// <param name="entity">The target object</param> + /// <param name="newValue">The new property value</param> public override void SetValue(object entity, object newValue) { - var propertyName = (newValue.GetType() == Type) + var propertyName = (newValue?.GetType() == Type) ? InternalRelationshipName : IdentifiablePropertyName; diff --git a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs b/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs index 2781ecfb53..3e66bdc8aa 100644 --- a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs +++ b/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs @@ -19,6 +19,28 @@ protected RelationshipAttribute(string publicName, Link documentLinks, bool canI public Link DocumentLinks { get; } = Link.All; public bool CanInclude { get; } + public bool TryGetHasOne(out HasOneAttribute result) + { + if (IsHasOne) + { + result = (HasOneAttribute)this; + return true; + } + result = null; + return false; + } + + public bool TryGetHasMany(out HasManyAttribute result) + { + if (IsHasMany) + { + result = (HasManyAttribute)this; + return true; + } + result = null; + return false; + } + public abstract void SetValue(object entity, object newValue); public override string ToString() diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index 5ef13609d6..da206e4930 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -207,15 +207,17 @@ private object SetHasOneRelationship(object entity, var rio = (ResourceIdentifierObject)relationshipData.ExposedData; - if (rio == null) return entity; - - var newValue = rio.Id; - var foreignKey = attr.IdentifiablePropertyName; var entityProperty = entityProperties.FirstOrDefault(p => p.Name == foreignKey); if (entityProperty == null) throw new JsonApiException(400, $"{contextEntity.EntityType.Name} does not contain a foreign key property '{foreignKey}' for has one relationship '{attr.InternalRelationshipName}'"); + // 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."); + + var newValue = rio?.Id ?? null; var convertedValue = TypeHelper.ConvertType(newValue, entityProperty.PropertyType); _jsonApiContext.RelationshipsToUpdate[relationshipAttr] = convertedValue; diff --git a/src/JsonApiDotNetCore/Services/EntityResourceService.cs b/src/JsonApiDotNetCore/Services/EntityResourceService.cs index ba9d6b987e..642ee00a57 100644 --- a/src/JsonApiDotNetCore/Services/EntityResourceService.cs +++ b/src/JsonApiDotNetCore/Services/EntityResourceService.cs @@ -133,7 +133,7 @@ public virtual async Task UpdateRelationshipsAsync(TId id, string relationshipNa .Relationships .FirstOrDefault(r => r.InternalRelationshipName == relationshipName); - var relationshipIds = relationships.Select(r => r.Id?.ToString()); + var relationshipIds = relationships.Select(r => r?.Id?.ToString()); await _entities.UpdateRelationshipsAsync(entity, relationship, relationshipIds); } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingRelationshipsTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingRelationshipsTests.cs index 1dffa6ce87..067483a1b3 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingRelationshipsTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingRelationshipsTests.cs @@ -127,5 +127,100 @@ public async Task Can_Update_ToOne_Relationship_ThroughLink() Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(todoItemsOwner); } + + [Fact] + public async Task Can_Delete_Relationship_By_Patching_Resource() + { + // arrange + var person = _personFaker.Generate(); + var todoItem = _todoItemFaker.Generate(); + todoItem.Owner = person; + + _context.People.Add(person); + _context.TodoItems.Add(todoItem); + _context.SaveChanges(); + + var builder = new WebHostBuilder() + .UseStartup<Startup>(); + + var server = new TestServer(builder); + var client = server.CreateClient(); + + var content = new + { + data = new + { + type = "todo-items", + relationships = new + { + owner = new + { + data = (object)null + } + } + } + }; + + var httpMethod = new HttpMethod("PATCH"); + var route = $"/api/v1/todo-items/{todoItem.Id}"; + var request = new HttpRequestMessage(httpMethod, route); + request.Content = new StringContent(JsonConvert.SerializeObject(content)); + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); + + // Act + var response = await client.SendAsync(request); + + // Assert + var todoItemResult = _context.TodoItems + .AsNoTracking() + .Include(t => t.Owner) + .Single(t => t.Id == todoItem.Id); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Null(todoItemResult.Owner); + } + + [Fact] + public async Task Can_Delete_Relationship_By_Patching_Relationship() + { + // arrange + var person = _personFaker.Generate(); + var todoItem = _todoItemFaker.Generate(); + todoItem.Owner = person; + + _context.People.Add(person); + _context.TodoItems.Add(todoItem); + _context.SaveChanges(); + + var builder = new WebHostBuilder() + .UseStartup<Startup>(); + + var server = new TestServer(builder); + var client = server.CreateClient(); + + var content = new + { + data = (object)null + }; + + var httpMethod = new HttpMethod("PATCH"); + var route = $"/api/v1/todo-items/{todoItem.Id}/relationships/owner"; + var request = new HttpRequestMessage(httpMethod, route); + + request.Content = new StringContent(JsonConvert.SerializeObject(content)); + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); + + // Act + var response = await client.SendAsync(request); + + // Assert + var todoItemResult = _context.TodoItems + .AsNoTracking() + .Include(t => t.Owner) + .Single(t => t.Id == todoItem.Id); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Null(todoItemResult.Owner); + } } } diff --git a/test/UnitTests/Builders/DocumentBuilder_Tests.cs b/test/UnitTests/Builders/DocumentBuilder_Tests.cs index dbca1bebb4..fc816765eb 100644 --- a/test/UnitTests/Builders/DocumentBuilder_Tests.cs +++ b/test/UnitTests/Builders/DocumentBuilder_Tests.cs @@ -1,268 +1,267 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using JsonApiDotNetCore.Builders; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; -using Moq; -using Xunit; - -namespace UnitTests -{ - public class DocumentBuilder_Tests - { - private readonly Mock<IJsonApiContext> _jsonApiContextMock; - private readonly PageManager _pageManager; - private readonly JsonApiOptions _options; - private readonly Mock<IRequestMeta> _requestMetaMock; - - public DocumentBuilder_Tests() - { - _jsonApiContextMock = new Mock<IJsonApiContext>(); - _requestMetaMock = new Mock<IRequestMeta>(); - - _options = new JsonApiOptions(); - - _options.BuildContextGraph(builder => - { - builder.AddResource<Model>("models"); - builder.AddResource<RelatedModel>("related-models"); - }); - - _jsonApiContextMock - .Setup(m => m.Options) - .Returns(_options); - - _jsonApiContextMock - .Setup(m => m.ContextGraph) - .Returns(_options.ContextGraph); - - _jsonApiContextMock - .Setup(m => m.MetaBuilder) - .Returns(new MetaBuilder()); - - _pageManager = new PageManager(); - _jsonApiContextMock - .Setup(m => m.PageManager) - .Returns(_pageManager); - - _jsonApiContextMock - .Setup(m => m.BasePath) - .Returns("localhost"); - - _jsonApiContextMock - .Setup(m => m.RequestEntity) - .Returns(_options.ContextGraph.GetContextEntity(typeof(Model))); - } - - [Fact] - public void Includes_Paging_Links_By_Default() - { - // arrange - _pageManager.PageSize = 1; - _pageManager.TotalRecords = 1; - _pageManager.CurrentPage = 1; - - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); - var entity = new Model(); - - // act - var document = documentBuilder.Build(entity); - - // assert - Assert.NotNull(document.Links); - Assert.NotNull(document.Links.Last); - } - - [Fact] - public void Page_Links_Can_Be_Disabled_Globally() - { - // arrange - _pageManager.PageSize = 1; - _pageManager.TotalRecords = 1; - _pageManager.CurrentPage = 1; - - _options.BuildContextGraph(builder => builder.DocumentLinks = Link.None); - - _jsonApiContextMock - .Setup(m => m.ContextGraph) - .Returns(_options.ContextGraph); - - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); - var entity = new Model(); - - // act - var document = documentBuilder.Build(entity); - - // assert - Assert.Null(document.Links); - } - - [Fact] - public void Related_Links_Can_Be_Disabled() - { - // arrange - _pageManager.PageSize = 1; - _pageManager.TotalRecords = 1; - _pageManager.CurrentPage = 1; - - _jsonApiContextMock - .Setup(m => m.ContextGraph) - .Returns(_options.ContextGraph); - - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); - var entity = new Model(); - - // act - var document = documentBuilder.Build(entity); - - // assert - Assert.Null(document.Data.Relationships["related-model"].Links); - } - - [Fact] - public void Related_Data_Included_In_Relationships_By_Default() - { - // arrange - const string relatedTypeName = "related-models"; - const string relationshipName = "related-model"; - const int relatedId = 1; - _jsonApiContextMock - .Setup(m => m.ContextGraph) - .Returns(_options.ContextGraph); - - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); - var entity = new Model - { - RelatedModel = new RelatedModel - { - Id = relatedId - } - }; - - // act - var document = documentBuilder.Build(entity); - - // assert - var relationshipData = document.Data.Relationships[relationshipName]; - Assert.NotNull(relationshipData); - Assert.NotNull(relationshipData.SingleData); - Assert.NotNull(relationshipData.SingleData); - Assert.Equal(relatedId.ToString(), relationshipData.SingleData.Id); - Assert.Equal(relatedTypeName, relationshipData.SingleData.Type); +using System.Collections; +using System.Collections.Generic; +using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; +using Moq; +using Xunit; + +namespace UnitTests +{ + public class DocumentBuilder_Tests + { + private readonly Mock<IJsonApiContext> _jsonApiContextMock; + private readonly PageManager _pageManager; + private readonly JsonApiOptions _options; + private readonly Mock<IRequestMeta> _requestMetaMock; + + public DocumentBuilder_Tests() + { + _jsonApiContextMock = new Mock<IJsonApiContext>(); + _requestMetaMock = new Mock<IRequestMeta>(); + + _options = new JsonApiOptions(); + + _options.BuildContextGraph(builder => + { + builder.AddResource<Model>("models"); + builder.AddResource<RelatedModel>("related-models"); + }); + + _jsonApiContextMock + .Setup(m => m.Options) + .Returns(_options); + + _jsonApiContextMock + .Setup(m => m.ContextGraph) + .Returns(_options.ContextGraph); + + _jsonApiContextMock + .Setup(m => m.MetaBuilder) + .Returns(new MetaBuilder()); + + _pageManager = new PageManager(); + _jsonApiContextMock + .Setup(m => m.PageManager) + .Returns(_pageManager); + + _jsonApiContextMock + .Setup(m => m.BasePath) + .Returns("localhost"); + + _jsonApiContextMock + .Setup(m => m.RequestEntity) + .Returns(_options.ContextGraph.GetContextEntity(typeof(Model))); + } + + [Fact] + public void Includes_Paging_Links_By_Default() + { + // arrange + _pageManager.PageSize = 1; + _pageManager.TotalRecords = 1; + _pageManager.CurrentPage = 1; + + var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); + var entity = new Model(); + + // act + var document = documentBuilder.Build(entity); + + // assert + Assert.NotNull(document.Links); + Assert.NotNull(document.Links.Last); } - [Fact] - public void IndependentIdentifier__Included_In_HasOne_Relationships_By_Default() - { - // arrange - const string relatedTypeName = "related-models"; - const string relationshipName = "related-model"; - const int relatedId = 1; - _jsonApiContextMock - .Setup(m => m.ContextGraph) - .Returns(_options.ContextGraph); - - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); - var entity = new Model + [Fact] + public void Page_Links_Can_Be_Disabled_Globally() + { + // arrange + _pageManager.PageSize = 1; + _pageManager.TotalRecords = 1; + _pageManager.CurrentPage = 1; + + _options.BuildContextGraph(builder => builder.DocumentLinks = Link.None); + + _jsonApiContextMock + .Setup(m => m.ContextGraph) + .Returns(_options.ContextGraph); + + var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); + var entity = new Model(); + + // act + var document = documentBuilder.Build(entity); + + // assert + Assert.Null(document.Links); + } + + [Fact] + public void Related_Links_Can_Be_Disabled() + { + // arrange + _pageManager.PageSize = 1; + _pageManager.TotalRecords = 1; + _pageManager.CurrentPage = 1; + + _jsonApiContextMock + .Setup(m => m.ContextGraph) + .Returns(_options.ContextGraph); + + var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); + var entity = new Model(); + + // act + var document = documentBuilder.Build(entity); + + // assert + Assert.Null(document.Data.Relationships["related-model"].Links); + } + + [Fact] + public void Related_Data_Included_In_Relationships_By_Default() + { + // arrange + const string relatedTypeName = "related-models"; + const string relationshipName = "related-model"; + const int relatedId = 1; + _jsonApiContextMock + .Setup(m => m.ContextGraph) + .Returns(_options.ContextGraph); + + var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); + var entity = new Model { - RelatedModelId = relatedId - }; - - // act - var document = documentBuilder.Build(entity); - - // assert - var relationshipData = document.Data.Relationships[relationshipName]; - Assert.NotNull(relationshipData); - Assert.NotNull(relationshipData.SingleData); - Assert.NotNull(relationshipData.SingleData); - Assert.Equal(relatedId.ToString(), relationshipData.SingleData.Id); - Assert.Equal(relatedTypeName, relationshipData.SingleData.Type); - } - - [Fact] - public void Build_Can_Build_Arrays() - { - var entities = new[] { new Model() }; - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); - - var documents = documentBuilder.Build(entities); - - Assert.Equal(1, documents.Data.Count); - } - - [Fact] - public void Build_Can_Build_CustomIEnumerables() - { - var entities = new Models(new[] { new Model() }); - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); - - var documents = documentBuilder.Build(entities); - - Assert.Equal(1, documents.Data.Count); - } - - - [Theory] - [InlineData(null, null, true)] - [InlineData(false, null, true)] - [InlineData(true, null, false)] - [InlineData(null, "foo", true)] - [InlineData(false, "foo", true)] - [InlineData(true, "foo", true)] - public void DocumentBuilderOptions(bool? omitNullValuedAttributes, - string attributeValue, - bool resultContainsAttribute) - { - var documentBuilderBehaviourMock = new Mock<IDocumentBuilderOptionsProvider>(); - if (omitNullValuedAttributes.HasValue) - { - documentBuilderBehaviourMock.Setup(m => m.GetDocumentBuilderOptions()) - .Returns(new DocumentBuilderOptions(omitNullValuedAttributes.Value)); - } - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object, null, omitNullValuedAttributes.HasValue ? documentBuilderBehaviourMock.Object : null); - var document = documentBuilder.Build(new Model() { StringProperty = attributeValue }); - - Assert.Equal(resultContainsAttribute, document.Data.Attributes.ContainsKey("StringProperty")); - } - - private class Model : Identifiable - { - [HasOne("related-model", Link.None)] - public RelatedModel RelatedModel { get; set; } - public int RelatedModelId { get; set; } - [Attr("StringProperty")] - public string StringProperty { get; set; } - - } - - private class RelatedModel : Identifiable - { - [HasMany("models")] - public List<Model> Models { get; set; } - } - - private class Models : IEnumerable<Model> - { - private readonly IEnumerable<Model> models; - - public Models(IEnumerable<Model> models) - { - this.models = models; - } - - public IEnumerator<Model> GetEnumerator() - { - return models.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return models.GetEnumerator(); - } - } - } -} + RelatedModel = new RelatedModel + { + Id = relatedId + } + }; + + // act + var document = documentBuilder.Build(entity); + + // assert + var relationshipData = document.Data.Relationships[relationshipName]; + Assert.NotNull(relationshipData); + Assert.NotNull(relationshipData.SingleData); + Assert.NotNull(relationshipData.SingleData); + Assert.Equal(relatedId.ToString(), relationshipData.SingleData.Id); + Assert.Equal(relatedTypeName, relationshipData.SingleData.Type); + } + + [Fact] + public void IndependentIdentifier__Included_In_HasOne_Relationships_By_Default() + { + // arrange + const string relatedTypeName = "related-models"; + const string relationshipName = "related-model"; + const int relatedId = 1; + _jsonApiContextMock + .Setup(m => m.ContextGraph) + .Returns(_options.ContextGraph); + + var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); + var entity = new Model + { + RelatedModelId = relatedId + }; + + // act + var document = documentBuilder.Build(entity); + + // assert + var relationshipData = document.Data.Relationships[relationshipName]; + Assert.NotNull(relationshipData); + Assert.NotNull(relationshipData.SingleData); + Assert.NotNull(relationshipData.SingleData); + Assert.Equal(relatedId.ToString(), relationshipData.SingleData.Id); + Assert.Equal(relatedTypeName, relationshipData.SingleData.Type); + } + + [Fact] + public void Build_Can_Build_Arrays() + { + var entities = new[] { new Model() }; + var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); + + var documents = documentBuilder.Build(entities); + + Assert.Equal(1, documents.Data.Count); + } + + [Fact] + public void Build_Can_Build_CustomIEnumerables() + { + var entities = new Models(new[] { new Model() }); + var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); + + var documents = documentBuilder.Build(entities); + + Assert.Equal(1, documents.Data.Count); + } + + + [Theory] + [InlineData(null, null, true)] + [InlineData(false, null, true)] + [InlineData(true, null, false)] + [InlineData(null, "foo", true)] + [InlineData(false, "foo", true)] + [InlineData(true, "foo", true)] + public void DocumentBuilderOptions(bool? omitNullValuedAttributes, + string attributeValue, + bool resultContainsAttribute) + { + var documentBuilderBehaviourMock = new Mock<IDocumentBuilderOptionsProvider>(); + if (omitNullValuedAttributes.HasValue) + { + documentBuilderBehaviourMock.Setup(m => m.GetDocumentBuilderOptions()) + .Returns(new DocumentBuilderOptions(omitNullValuedAttributes.Value)); + } + var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object, null, omitNullValuedAttributes.HasValue ? documentBuilderBehaviourMock.Object : null); + var document = documentBuilder.Build(new Model() { StringProperty = attributeValue }); + + Assert.Equal(resultContainsAttribute, document.Data.Attributes.ContainsKey("StringProperty")); + } + + private class Model : Identifiable + { + [HasOne("related-model", Link.None)] + public RelatedModel RelatedModel { get; set; } + public int RelatedModelId { get; set; } + [Attr("StringProperty")] + public string StringProperty { get; set; } + + } + + private class RelatedModel : Identifiable + { + [HasMany("models")] + public List<Model> Models { get; set; } + } + + private class Models : IEnumerable<Model> + { + private readonly IEnumerable<Model> models; + + public Models(IEnumerable<Model> models) + { + this.models = models; + } + + public IEnumerator<Model> GetEnumerator() + { + return models.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return models.GetEnumerator(); + } + } + } +} diff --git a/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs b/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs index 4fe2f09ff1..1b00c5aaa1 100644 --- a/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs +++ b/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs @@ -25,10 +25,7 @@ public void AddJsonApiInternals_Adds_All_Required_Services() var services = new ServiceCollection(); var jsonApiOptions = new JsonApiOptions(); - services.AddDbContext<AppDbContext>(options => - { - options.UseInMemoryDatabase(); - }, ServiceLifetime.Transient); + services.AddDbContext<AppDbContext>(options => options.UseInMemoryDatabase("UnitTestDb"), ServiceLifetime.Transient); // act services.AddJsonApiInternals<AppDbContext>(jsonApiOptions); From dae04a182643e011e2151e694eacad5cac826083 Mon Sep 17 00:00:00 2001 From: jaredcnance <jaredcnance@gmail.com> Date: Fri, 8 Jun 2018 23:14:35 -0500 Subject: [PATCH 11/13] fix(JsonApiDeSerializer): null refs --- .../Serialization/JsonApiDeSerializer.cs | 21 +++++++++++-------- .../Serialization/JsonApiDeSerializerTests.cs | 2 -- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index da206e4930..45d77c0f77 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -209,20 +209,23 @@ private object SetHasOneRelationship(object entity, var foreignKey = attr.IdentifiablePropertyName; var entityProperty = entityProperties.FirstOrDefault(p => p.Name == foreignKey); - if (entityProperty == null) + 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}'"); - // 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 (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."); - var newValue = rio?.Id ?? null; - var convertedValue = TypeHelper.ConvertType(newValue, entityProperty.PropertyType); + var newValue = rio?.Id ?? null; + var convertedValue = TypeHelper.ConvertType(newValue, entityProperty.PropertyType); - _jsonApiContext.RelationshipsToUpdate[relationshipAttr] = convertedValue; + _jsonApiContext.RelationshipsToUpdate[relationshipAttr] = convertedValue; - entityProperty.SetValue(entity, convertedValue); + entityProperty.SetValue(entity, convertedValue); + } } return entity; diff --git a/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs b/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs index 0b80d3a25a..ec34d87d24 100644 --- a/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs +++ b/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs @@ -345,8 +345,6 @@ private class Dependent : Identifiable public int IndependentId { get; set; } } - - [Fact] public void Can_Deserialize_Object_With_HasManyRelationship() { From 903fb7171005ac54efad4b45b9e7617557572a42 Mon Sep 17 00:00:00 2001 From: jaredcnance <jaredcnance@gmail.com> Date: Fri, 8 Jun 2018 23:16:16 -0500 Subject: [PATCH 12/13] chore(csproj): bump package version --- 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 eb649ed5dc..f296ef318f 100755 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -1,6 +1,6 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <VersionPrefix>2.2.4</VersionPrefix> + <VersionPrefix>2.2.5/VersionPrefix> <TargetFrameworks>$(NetStandardVersion)</TargetFrameworks> <AssemblyName>JsonApiDotNetCore</AssemblyName> <PackageId>JsonApiDotNetCore</PackageId> From 01181ddb3109850c29d5d09690386d9b4fa6e9b9 Mon Sep 17 00:00:00 2001 From: jaredcnance <jaredcnance@gmail.com> Date: Fri, 8 Jun 2018 23:21:02 -0500 Subject: [PATCH 13/13] fix(csproj): syntax error --- 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 f296ef318f..4f52a23002 100755 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -1,6 +1,6 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <VersionPrefix>2.2.5/VersionPrefix> + <VersionPrefix>2.2.5</VersionPrefix> <TargetFrameworks>$(NetStandardVersion)</TargetFrameworks> <AssemblyName>JsonApiDotNetCore</AssemblyName> <PackageId>JsonApiDotNetCore</PackageId>