From fa26049c1c6e88ee931c84523e8419907de2f436 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Sat, 7 Apr 2018 18:33:55 -0500 Subject: [PATCH 01/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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 fd1eaee56324f63b7344ff79116d12436f071a82 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Tue, 15 May 2018 11:24:09 -0500 Subject: [PATCH 07/54] chore(csproj): bump package version also add build property for docfx targets closes #283 --- .../JsonApiDotNetCore.csproj | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index 53649bb7e7..eb649ed5dc 100755 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -1,9 +1,10 @@  - 2.2.3 + 2.2.4 $(NetStandardVersion) JsonApiDotNetCore JsonApiDotNetCore + 7.2 @@ -25,20 +26,16 @@ - - + + true - - + true bin\Release\netstandard2.0\JsonApiDotNetCore.xml - - 7.2 - - - 7.2 - - + + From cb93418a5b5b1c533f447ac0a65a4f5e64a66204 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Tue, 15 May 2018 14:09:56 -0500 Subject: [PATCH 08/54] fix(#218): include independent hasOne identifier in all requests for the dependent side of the relationship by default --- .../Builders/DocumentBuilder.cs | 85 ++++++++++++------- .../Models/HasOneAttribute.cs | 54 ++++++++++-- .../Serialization/JsonApiDeSerializer.cs | 6 +- .../Builders/DocumentBuilder_Tests.cs | 29 +++++++ 4 files changed, 134 insertions(+), 40 deletions(-) diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index 3edea50e13..49d40e0f28 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -13,9 +13,9 @@ public class DocumentBuilder : IDocumentBuilder private readonly IJsonApiContext _jsonApiContext; private readonly IContextGraph _contextGraph; private readonly IRequestMeta _requestMeta; - private readonly DocumentBuilderOptions _documentBuilderOptions; + private readonly DocumentBuilderOptions _documentBuilderOptions; - public DocumentBuilder(IJsonApiContext jsonApiContext, IRequestMeta requestMeta=null, IDocumentBuilderOptionsProvider documentBuilderOptionsProvider=null) + public DocumentBuilder(IJsonApiContext jsonApiContext, IRequestMeta requestMeta = null, IDocumentBuilderOptionsProvider documentBuilderOptionsProvider = null) { _jsonApiContext = jsonApiContext; _contextGraph = jsonApiContext.ContextGraph; @@ -143,34 +143,42 @@ private bool OmitNullValuedAttribute(AttrAttribute attr, object attributeValue) private void AddRelationships(DocumentData data, ContextEntity contextEntity, IIdentifiable entity) { data.Relationships = new Dictionary(); + contextEntity.Relationships.ForEach(r => + data.Relationships.Add( + r.PublicRelationshipName, + GetRelationshipData(r, contextEntity, entity) + ) + ); + } + + private RelationshipData GetRelationshipData(RelationshipAttribute attr, ContextEntity contextEntity, IIdentifiable entity) + { var linkBuilder = new LinkBuilder(_jsonApiContext); - contextEntity.Relationships.ForEach(r => + var relationshipData = new RelationshipData(); + + if (attr.DocumentLinks.HasFlag(Link.None) == false) { - var relationshipData = new RelationshipData(); + relationshipData.Links = new Links(); + if (attr.DocumentLinks.HasFlag(Link.Self)) + relationshipData.Links.Self = linkBuilder.GetSelfRelationLink(contextEntity.EntityName, entity.StringId, attr.PublicRelationshipName); - if (r.DocumentLinks.HasFlag(Link.None) == false) - { - relationshipData.Links = new Links(); - if (r.DocumentLinks.HasFlag(Link.Self)) - relationshipData.Links.Self = linkBuilder.GetSelfRelationLink(contextEntity.EntityName, entity.StringId, r.PublicRelationshipName); + if (attr.DocumentLinks.HasFlag(Link.Related)) + relationshipData.Links.Related = linkBuilder.GetRelatedRelationLink(contextEntity.EntityName, entity.StringId, attr.PublicRelationshipName); + } - if (r.DocumentLinks.HasFlag(Link.Related)) - relationshipData.Links.Related = linkBuilder.GetRelatedRelationLink(contextEntity.EntityName, entity.StringId, r.PublicRelationshipName); - } - - var navigationEntity = _jsonApiContext.ContextGraph - .GetRelationship(entity, r.InternalRelationshipName); - - if (navigationEntity == null) - relationshipData.SingleData = null; - else if (navigationEntity is IEnumerable) - relationshipData.ManyData = GetRelationships((IEnumerable)navigationEntity); - else - relationshipData.SingleData = GetRelationship(navigationEntity); - - data.Relationships.Add(r.PublicRelationshipName, relationshipData); - }); + // this only includes the navigation property, we need to actually check the navigation property Id + var navigationEntity = _jsonApiContext.ContextGraph.GetRelationship(entity, attr.InternalRelationshipName); + if (navigationEntity == null) + relationshipData.SingleData = attr.IsHasOne + ? GetIndependentRelationshipIdentifier((HasOneAttribute)attr, entity) + : null; + else if (navigationEntity is IEnumerable) + relationshipData.ManyData = GetRelationships((IEnumerable)navigationEntity); + else + relationshipData.SingleData = GetRelationship(navigationEntity); + + return relationshipData; } private List GetIncludedEntities(List included, ContextEntity contextEntity, IIdentifiable entity) @@ -240,23 +248,40 @@ private List GetRelationships(IEnumerable enti var relationships = new List(); foreach (var entity in entities) { - relationships.Add(new ResourceIdentifierObject { + relationships.Add(new ResourceIdentifierObject + { Type = typeName.EntityName, Id = ((IIdentifiable)entity).StringId }); } return relationships; } + private ResourceIdentifierObject GetRelationship(object entity) { var objType = entity.GetType(); + var contextEntity = _jsonApiContext.ContextGraph.GetContextEntity(objType); - var typeName = _jsonApiContext.ContextGraph.GetContextEntity(objType); - - return new ResourceIdentifierObject { - Type = typeName.EntityName, + return new ResourceIdentifierObject + { + Type = contextEntity.EntityName, Id = ((IIdentifiable)entity).StringId }; } + + private ResourceIdentifierObject GetIndependentRelationshipIdentifier(HasOneAttribute hasOne, IIdentifiable entity) + { + var independentRelationshipIdentifier = hasOne.GetIdentifiablePropertyValue(entity); + if (independentRelationshipIdentifier == null) + return null; + + var relatedContextEntity = _jsonApiContext.ContextGraph.GetContextEntity(hasOne.Type); + + return new ResourceIdentifierObject + { + Type = relatedContextEntity.EntityName, + Id = independentRelationshipIdentifier.ToString() + }; + } } } diff --git a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs b/src/JsonApiDotNetCore/Models/HasOneAttribute.cs index f863c8819b..de7b7822f6 100644 --- a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs +++ b/src/JsonApiDotNetCore/Models/HasOneAttribute.cs @@ -2,21 +2,61 @@ namespace JsonApiDotNetCore.Models { public class HasOneAttribute : RelationshipAttribute { - public HasOneAttribute(string publicName, Link documentLinks = Link.All, bool canInclude = true) + /// + /// Create a HasOne relational link to another entity + /// + /// + /// The relationship name as exposed by the API + /// Which links are available. Defaults to + /// Whether or not this relationship can be included using the ?include=public-name query string + /// The foreign key property name. Defaults to "{RelationshipName}Id" + /// + /// + /// Using an alternative foreign key: + /// + /// + /// public class Article : Identifiable + /// { + /// [HasOne("author", withForiegnKey: nameof(AuthorKey)] + /// public Author Author { get; set; } + /// public int AuthorKey { get; set; } + /// } + /// + /// + /// + public HasOneAttribute(string publicName, Link documentLinks = Link.All, bool canInclude = true, string withForiegnKey = null) : base(publicName, documentLinks, canInclude) - { } + { + _explicitIdentifiablePropertyName = withForiegnKey; + } + + private readonly string _explicitIdentifiablePropertyName; + + /// + /// The independent entity identifier. + /// + public string IdentifiablePropertyName => string.IsNullOrWhiteSpace(_explicitIdentifiablePropertyName) + ? $"{InternalRelationshipName}Id" + : _explicitIdentifiablePropertyName; public override void SetValue(object entity, object newValue) { - var propertyName = (newValue.GetType() == Type) - ? InternalRelationshipName - : $"{InternalRelationshipName}Id"; - + var propertyName = (newValue.GetType() == Type) + ? InternalRelationshipName + : IdentifiablePropertyName; + var propertyInfo = entity .GetType() .GetProperty(propertyName); - + propertyInfo.SetValue(entity, newValue); } + + // HACK: this will likely require boxing + // we should be able to move some of the reflection into the ContextGraphBuilder + internal object GetIdentifiablePropertyValue(object entity) => entity + .GetType() + .GetProperty(IdentifiablePropertyName) + .GetValue(entity); } } diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index 649d6435ff..6fb7af55e1 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -175,7 +175,7 @@ private object SetRelationships( foreach (var attr in contextEntity.Relationships) { entity = attr.IsHasOne - ? SetHasOneRelationship(entity, entityProperties, attr, contextEntity, relationships) + ? SetHasOneRelationship(entity, entityProperties, (HasOneAttribute)attr, contextEntity, relationships) : SetHasManyRelationship(entity, entityProperties, attr, contextEntity, relationships); } @@ -184,7 +184,7 @@ private object SetRelationships( private object SetHasOneRelationship(object entity, PropertyInfo[] entityProperties, - RelationshipAttribute attr, + HasOneAttribute attr, ContextEntity contextEntity, Dictionary relationships) { @@ -204,7 +204,7 @@ private object SetHasOneRelationship(object entity, var newValue = rio.Id; - var foreignKey = attr.InternalRelationshipName + "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}'"); diff --git a/test/UnitTests/Builders/DocumentBuilder_Tests.cs b/test/UnitTests/Builders/DocumentBuilder_Tests.cs index b2131f6ec9..dbca1bebb4 100644 --- a/test/UnitTests/Builders/DocumentBuilder_Tests.cs +++ b/test/UnitTests/Builders/DocumentBuilder_Tests.cs @@ -145,6 +145,35 @@ public void Related_Data_Included_In_Relationships_By_Default() // 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); From 34cc27100bead30ac37821eda59316fda223bd1c Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Tue, 5 Jun 2018 05:45:57 -0500 Subject: [PATCH 09/54] fix(HasOneAttribute): return null if property doesnt exist --- src/JsonApiDotNetCore/Models/HasOneAttribute.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs b/src/JsonApiDotNetCore/Models/HasOneAttribute.cs index de7b7822f6..03fdb200fc 100644 --- a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs +++ b/src/JsonApiDotNetCore/Models/HasOneAttribute.cs @@ -54,9 +54,20 @@ public override void SetValue(object entity, object newValue) // HACK: this will likely require boxing // we should be able to move some of the reflection into the ContextGraphBuilder + /// + /// Gets the value of the independent identifier (e.g. Article.AuthorId) + /// + /// + /// + /// An instance of dependent resource + /// + /// + /// + /// The property value or null if the property does not exist on the model. + /// internal object GetIdentifiablePropertyValue(object entity) => entity .GetType() .GetProperty(IdentifiablePropertyName) - .GetValue(entity); + ?.GetValue(entity); } } From 1a8531fb14204184fc9f0820ed671c107800ee67 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Tue, 5 Jun 2018 06:11:02 -0500 Subject: [PATCH 10/54] fix(DocumentBuilder): handle case when hasOne relationship type is unknown --- src/JsonApiDotNetCore/Builders/DocumentBuilder.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index 49d40e0f28..088a2bf092 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -276,6 +276,8 @@ private ResourceIdentifierObject GetIndependentRelationshipIdentifier(HasOneAttr return null; var relatedContextEntity = _jsonApiContext.ContextGraph.GetContextEntity(hasOne.Type); + if (relatedContextEntity == null) // TODO: this should probably be a debug log at minimum + return null; return new ResourceIdentifierObject { From acb7bc1439e57c868ad59aaad8463ba1bc85743d Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Sat, 7 Apr 2018 18:33:55 -0500 Subject: [PATCH 11/54] 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 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 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 5f4640f632aded7ffc9049c5cd542309bc9d2184 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Sat, 7 Apr 2018 19:08:18 -0500 Subject: [PATCH 12/54] 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 31a4b76dfa07cca313f69c3aa97cc601498869a5 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Sat, 7 Apr 2018 20:57:26 -0500 Subject: [PATCH 13/54] 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) /// /// 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 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 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(); - 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 341eb8061dcd866ceca6c2565481e81eb4c6152b Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Sun, 8 Apr 2018 07:10:26 -0500 Subject: [PATCH 14/54] 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 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 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 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 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 DocumentMeta { get; set; } public bool IsBulkOperationRequest { get; set; } + public HasManyRelationshipPointers HasManyRelationshipPointers { get; } = new HasManyRelationshipPointers(); public IJsonApiContext ApplyContext(object controller) { From c453f319dd10cf4de2296fc64d668397e3d8d39c Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Sun, 8 Apr 2018 07:26:15 -0500 Subject: [PATCH 15/54] 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 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 67bb3d514f5b6dcafe66f1afdd2b5edc29327ca7 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Sun, 8 Apr 2018 15:33:58 -0500 Subject: [PATCH 16/54] 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 ef60b90c1c02843582e309e7a91da029844b076f Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Thu, 10 May 2018 14:18:38 -0500 Subject: [PATCH 17/54] 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("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(); - - 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 { + var content = new Document + { + Data = new DocumentData { - "complex-member", - new { compoundName = "testName" } - } - } + Type = "test-resource", + Id = "1", + Attributes = new Dictionary + { + { "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("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(); + 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 { + var content = new Document + { + Data = new DocumentData { - "complex-members", - new [] { - new { compoundName = "testName" } - } - } - } + Type = "test-resource", + Id = "1", + Attributes = new Dictionary + { + { "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("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(); + 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 { + var content = new Document + { + Data = new DocumentData { - "complex-member", - new Dictionary { { "compound-name", "testName" } } - } - } + Type = "test-resource", + Id = "1", + Attributes = new Dictionary + { + { + "complex-member", new Dictionary { { "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("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(); + 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 { + var content = new Document + { + Data = new DocumentData { - "complex-member", - new Dictionary { { "compound-name", "testName" } - } - }, - { "immutable", "value" } - } + Type = "test-resource", + Id = "1", + Attributes = new Dictionary + { + { + "complex-member", new Dictionary { { "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("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(); - - 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 { { "property", property } - } + var content = new Document + { + Data = new DocumentData + { + Type = "independents", + Id = "1", + Attributes = new Dictionary { { "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("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(); - - 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 { { "property", property } - }, - // a common case for this is deserialization in unit tests - Relationships = new Dictionary { { "dependent", new RelationshipData { } } - } + var content = new Document + { + Data = new DocumentData + { + Type = "independents", + Id = "1", + Attributes = new Dictionary { { "property", property } }, + // a common case for this is deserialization in unit tests + Relationships = new Dictionary { { "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(); - 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() { {"foo", "bar"}}, + { + Meta = new Dictionary() { { "foo", "bar" } }, Data = new DocumentData { Type = "independents", Id = "1", - Attributes = new Dictionary { { "property", property } - }, + Attributes = new Dictionary { { "property", property } }, // a common case for this is deserialization in unit tests - Relationships = new Dictionary { { "dependent", new RelationshipData { } } - } + Relationships = new Dictionary { { "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 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("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 contentString = + @"{ + ""data"": { + ""type"": ""independents"", + ""id"": ""1"", + ""attributes"": { }, + ""relationships"": { + ""dependents"": { + ""data"": [ + { + ""type"": ""dependents"", + ""id"": ""2"" + } + ] + } + } + } + }"; + + // act + var result = deserializer.Deserialize(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 Dependents { get; set; } + } } } From b94b252cffa3055d06bce313fec270b115d02687 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Thu, 10 May 2018 17:00:12 -0500 Subject: [PATCH 18/54] 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(); 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 GetWithRelationshipsAsync(TId id) } public virtual async Task GetRelationshipsAsync(TId id, string relationshipName) - { - _jsonApiContext.IsRelationshipData = true; - return await GetRelationshipAsync(id, relationshipName); - } + => await GetRelationshipAsync(id, relationshipName); public virtual async Task 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(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 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 AttributesToUpdate { get; set; } Dictionary RelationshipsToUpdate { get; set; } + } + + public interface IJsonApiRequest : IJsonApiApplication, IUpdateRequest, IQueryRequest + { + /// + /// The request namespace. This may be an absolute or relative path + /// depending upon the configuration. + /// + /// + /// Absolute: https://example.com/api/v1 + /// + /// Relative: /api/v1 + /// + string BasePath { get; set; } + + /// + /// 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: + /// + /// { + /// "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" } + /// ] + /// } + /// } + /// } + /// } + /// + /// + HasManyRelationshipPointers HasManyRelationshipPointers { get; } + + /// + /// If the request is a bulk json:api v1.1 operations request. + /// This is determined by the ` + /// ` class. + /// + /// See [json-api/1254](https://github.com/json-api/json-api/pull/1254) for details. + /// + bool IsBulkOperationRequest { get; set; } + + /// + /// The ``for the target route. + /// + /// + /// + /// For a `GET /articles` request, `RequestEntity` will be set + /// to the `Article` resource representation on the `JsonApiContext`. + /// + ContextEntity RequestEntity { get; set; } + + /// + /// The concrete type of the controller that was activated by the MVC routing middleware + /// Type ControllerType { get; set; } + + /// + /// The json:api meta data at the document level + /// Dictionary DocumentMeta { get; set; } - bool IsBulkOperationRequest { get; set; } - HasManyRelationshipPointers HasManyRelationshipPointers { get; } + /// + /// If the request is on the `{id}/relationships/{relationshipName}` route + /// + bool IsRelationshipPath { get; } + + [Obsolete("Use `IsRelationshipPath` instead.")] + bool IsRelationshipData { get; set; } + } + + public interface IJsonApiContext : IJsonApiRequest + { + IJsonApiContext ApplyContext(object controller); + IMetaBuilder MetaBuilder { get; set; } + IGenericProcessorFactory GenericProcessorFactory { get; set; } + + [Obsolete("Use the proxied method IControllerContext.GetControllerAttribute instead.")] TAttribute GetControllerAttribute() where TAttribute : Attribute; } } From fa6ee6b822efd7e45eff2c3f82c9c1826083a257 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Tue, 5 Jun 2018 07:01:18 -0500 Subject: [PATCH 19/54] 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 { + /// + /// The attributes that were included in a PATCH request. + /// Only the attributes in this dictionary should be updated. + /// Dictionary AttributesToUpdate { get; set; } + + /// + /// Any relationships that were included in a PATCH request. + /// Only the relationships in this dictionary should be updated. + /// Dictionary RelationshipsToUpdate { get; set; } } From 6741efd9e8f435e05614d557dd2c46a4f3215f91 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Fri, 8 Jun 2018 23:01:42 -0500 Subject: [PATCH 20/54] 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; + /// + /// Sets the value of the property identified by this attribute + /// + /// The target object + /// The new property value 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(); + + 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(); + + 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 _jsonApiContextMock; - private readonly PageManager _pageManager; - private readonly JsonApiOptions _options; - private readonly Mock _requestMetaMock; - - public DocumentBuilder_Tests() - { - _jsonApiContextMock = new Mock(); - _requestMetaMock = new Mock(); - - _options = new JsonApiOptions(); - - _options.BuildContextGraph(builder => - { - builder.AddResource("models"); - builder.AddResource("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 _jsonApiContextMock; + private readonly PageManager _pageManager; + private readonly JsonApiOptions _options; + private readonly Mock _requestMetaMock; + + public DocumentBuilder_Tests() + { + _jsonApiContextMock = new Mock(); + _requestMetaMock = new Mock(); + + _options = new JsonApiOptions(); + + _options.BuildContextGraph(builder => + { + builder.AddResource("models"); + builder.AddResource("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(); - 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 Models { get; set; } - } - - private class Models : IEnumerable - { - private readonly IEnumerable models; - - public Models(IEnumerable models) - { - this.models = models; - } - - public IEnumerator 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(); + 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 Models { get; set; } + } + + private class Models : IEnumerable + { + private readonly IEnumerable models; + + public Models(IEnumerable models) + { + this.models = models; + } + + public IEnumerator 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(options => - { - options.UseInMemoryDatabase(); - }, ServiceLifetime.Transient); + services.AddDbContext(options => options.UseInMemoryDatabase("UnitTestDb"), ServiceLifetime.Transient); // act services.AddJsonApiInternals(jsonApiOptions); From dae04a182643e011e2151e694eacad5cac826083 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Fri, 8 Jun 2018 23:14:35 -0500 Subject: [PATCH 21/54] 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 Date: Fri, 8 Jun 2018 23:16:16 -0500 Subject: [PATCH 22/54] 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 @@  - 2.2.4 + 2.2.5/VersionPrefix> $(NetStandardVersion) JsonApiDotNetCore JsonApiDotNetCore From 01181ddb3109850c29d5d09690386d9b4fa6e9b9 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Fri, 8 Jun 2018 23:21:02 -0500 Subject: [PATCH 23/54] 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 @@  - 2.2.5/VersionPrefix> + 2.2.5 $(NetStandardVersion) JsonApiDotNetCore JsonApiDotNetCore From e7dc01843e3d11ce780baed098c982263bbadf81 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Sun, 10 Jun 2018 12:38:43 -0500 Subject: [PATCH 24/54] test(spec): can create hasOne relationship with resource --- .../Acceptance/Spec/CreatingDataTests.cs | 61 ++++++++++++++++++- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs index 2c71275473..015d79cdbd 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs @@ -106,7 +106,7 @@ public async Task Cannot_Create_Entity_With_Client_Generate_Id() attributes = new { description = todoItem.Description, - ordinal = todoItem.Ordinal, + ordinal = todoItem.Ordinal, createdDate = DateTime.Now } } @@ -174,7 +174,7 @@ public async Task Can_Create_Guid_Identifiable_Entity_With_Client_Defined_Id_If_ var httpMethod = new HttpMethod("POST"); var server = new TestServer(builder); var client = server.CreateClient(); - + var context = _fixture.GetService(); var owner = new JsonApiDotNetCoreExample.Models.Person(); @@ -285,6 +285,63 @@ public async Task Can_Create_And_Set_HasMany_Relationships() Assert.NotEmpty(contextCollection.TodoItems); } + [Fact] + public async Task Can_Create_And_Set_HasOne_Relationships() + { + // arrange + var builder = new WebHostBuilder() + .UseStartup(); + var httpMethod = new HttpMethod("POST"); + var server = new TestServer(builder); + var client = server.CreateClient(); + + var context = _fixture.GetService(); + + var todoItem = new TodoItem(); + var owner = new JsonApiDotNetCoreExample.Models.Person(); + context.People.Add(owner); + await context.SaveChangesAsync(); + + var route = "/api/v1/todo-items"; + var request = new HttpRequestMessage(httpMethod, route); + var content = new + { + data = new + { + type = "todo-items", + relationships = new Dictionary + { + { "owner", new { + data = new + { + type = "people", + id = owner.Id.ToString() + } + } } + } + } + }; + + request.Content = new StringContent(JsonConvert.SerializeObject(content)); + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); + + // act + var response = await client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + + // assert + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + var deserializedBody = (TodoItem)_fixture.GetService().Deserialize(body); + var newId = deserializedBody.Id; + + context = _fixture.GetService(); + var todoItemResult = context.TodoItems + .Include(c => c.Owner) + .SingleOrDefault(c => c.Id == newId); + + Assert.Equal(owner.Id, todoItemResult.OwnerId); + } + [Fact] public async Task ShouldReceiveLocationHeader_InResponse() { From 99ce78ec5a8226e775120ba76d11cd2c5affe42b Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Mon, 11 Jun 2018 21:33:09 -0500 Subject: [PATCH 25/54] docs(*): add xml comments --- .../IDocumentBuilderOptionsProvider.cs | 6 +-- .../NullAttributeResponseBehavior.cs | 25 +++++++-- src/JsonApiDotNetCore/Models/AttrAttribute.cs | 53 ++++++++++++++++++- .../Models/HasManyAttribute.cs | 26 ++++++++- .../Services/QueryComposer.cs | 5 +- 5 files changed, 100 insertions(+), 15 deletions(-) diff --git a/src/JsonApiDotNetCore/Builders/IDocumentBuilderOptionsProvider.cs b/src/JsonApiDotNetCore/Builders/IDocumentBuilderOptionsProvider.cs index d8effd4fe3..fe014bced5 100644 --- a/src/JsonApiDotNetCore/Builders/IDocumentBuilderOptionsProvider.cs +++ b/src/JsonApiDotNetCore/Builders/IDocumentBuilderOptionsProvider.cs @@ -1,11 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Text; - namespace JsonApiDotNetCore.Builders { public interface IDocumentBuilderOptionsProvider { - DocumentBuilderOptions GetDocumentBuilderOptions(); + DocumentBuilderOptions GetDocumentBuilderOptions(); } } diff --git a/src/JsonApiDotNetCore/Configuration/NullAttributeResponseBehavior.cs b/src/JsonApiDotNetCore/Configuration/NullAttributeResponseBehavior.cs index 1b10140f5e..125d38b5fc 100644 --- a/src/JsonApiDotNetCore/Configuration/NullAttributeResponseBehavior.cs +++ b/src/JsonApiDotNetCore/Configuration/NullAttributeResponseBehavior.cs @@ -1,19 +1,34 @@ -using System; -using System.Collections.Generic; -using System.Text; - namespace JsonApiDotNetCore.Configuration { + /// + /// Allows null attributes to be ommitted from the response payload + /// public struct NullAttributeResponseBehavior { + /// Do not serialize null attributes + /// + /// Allow clients to override the serialization behavior through a query parmeter. + /// + /// ``` + /// GET /articles?omitNullValuedAttributes=true + /// ``` + /// + /// public NullAttributeResponseBehavior(bool omitNullValuedAttributes = false, bool allowClientOverride = false) { OmitNullValuedAttributes = omitNullValuedAttributes; AllowClientOverride = allowClientOverride; } + /// + /// Do not include null attributes in the response payload. + /// public bool OmitNullValuedAttributes { get; } + + /// + /// Allows clients to specify a `omitNullValuedAttributes` boolean query param to control + /// serialization behavior. + /// public bool AllowClientOverride { get; } - // ... } } diff --git a/src/JsonApiDotNetCore/Models/AttrAttribute.cs b/src/JsonApiDotNetCore/Models/AttrAttribute.cs index db61cb56ea..e38b47abec 100644 --- a/src/JsonApiDotNetCore/Models/AttrAttribute.cs +++ b/src/JsonApiDotNetCore/Models/AttrAttribute.cs @@ -5,6 +5,26 @@ namespace JsonApiDotNetCore.Models { public class AttrAttribute : Attribute { + /// + /// Defines a public attribute exposed by the API + /// + /// + /// How this attribute is exposed through the API + /// Prevent PATCH requests from updating the value + /// Prevent filters on this attribute + /// Prevent this attribute from being sorted by + /// + /// + /// + /// + /// public class Author : Identifiable + /// { + /// [Attr("name")] + /// public string Name { get; set; } + /// } + /// + /// + /// public AttrAttribute(string publicName, bool isImmutable = false, bool isFilterable = true, bool isSortable = true) { PublicAttributeName = publicName; @@ -20,20 +40,51 @@ internal AttrAttribute(string publicName, string internalName, bool isImmutable IsImmutable = isImmutable; } + /// + /// How this attribute is exposed through the API + /// public string PublicAttributeName { get; } + + /// + /// The internal property name this attribute belongs to. + /// public string InternalAttributeName { get; internal set; } + + /// + /// Prevents PATCH requests from updating the value. + /// public bool IsImmutable { get; } + + /// + /// Whether or not this attribute can be filtered on via a query string filters. + /// Attempts to filter on an attribute with `IsFilterable == false` will return + /// an HTTP 400 response. + /// public bool IsFilterable { get; } + + /// + /// Whether or not this attribute can be sorted on via a query string sort. + /// Attempts to filter on an attribute with `IsSortable == false` will return + /// an HTTP 400 response. + /// public bool IsSortable { get; } + /// + /// Get the value of the attribute for the given object. + /// Returns null if the attribute does not belong to the + /// provided object. + /// public object GetValue(object entity) { return entity .GetType() .GetProperty(InternalAttributeName) - .GetValue(entity); + ?.GetValue(entity); } + /// + /// Sets the value of the attribute on the given object. + /// public void SetValue(object entity, object newValue) { var propertyInfo = entity diff --git a/src/JsonApiDotNetCore/Models/HasManyAttribute.cs b/src/JsonApiDotNetCore/Models/HasManyAttribute.cs index c2d7594400..877df29146 100644 --- a/src/JsonApiDotNetCore/Models/HasManyAttribute.cs +++ b/src/JsonApiDotNetCore/Models/HasManyAttribute.cs @@ -2,16 +2,40 @@ namespace JsonApiDotNetCore.Models { public class HasManyAttribute : RelationshipAttribute { + /// + /// Create a HasMany relational link to another entity + /// + /// + /// The relationship name as exposed by the API + /// Which links are available. Defaults to + /// Whether or not this relationship can be included using the ?include=public-name query string + /// + /// + /// + /// + /// public class Author : Identifiable + /// { + /// [HasMany("articles"] + /// public virtual List
Articles { get; set; } + /// } + /// + /// + /// public HasManyAttribute(string publicName, Link documentLinks = Link.All, bool canInclude = true) : base(publicName, documentLinks, canInclude) { } + /// + /// Sets the value of the property identified by this attribute + /// + /// The target object + /// The new property value public override void SetValue(object entity, object newValue) { var propertyInfo = entity .GetType() .GetProperty(InternalRelationshipName); - + propertyInfo.SetValue(entity, newValue); } } diff --git a/src/JsonApiDotNetCore/Services/QueryComposer.cs b/src/JsonApiDotNetCore/Services/QueryComposer.cs index 8e0819a438..e365811704 100644 --- a/src/JsonApiDotNetCore/Services/QueryComposer.cs +++ b/src/JsonApiDotNetCore/Services/QueryComposer.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using JsonApiDotNetCore.Internal.Query; -using Microsoft.Extensions.Logging; namespace JsonApiDotNetCore.Services { @@ -14,8 +13,8 @@ public class QueryComposer : IQueryComposer public string Compose(IJsonApiContext jsonApiContext) { string result = ""; - if(jsonApiContext != null && jsonApiContext.QuerySet != null) - { + if (jsonApiContext != null && jsonApiContext.QuerySet != null) + { List filterQueries = jsonApiContext.QuerySet.Filters; if (filterQueries.Count > 0) { From ab15d70d10968faf50f28d30f84e842a9dfee0c0 Mon Sep 17 00:00:00 2001 From: Bj Date: Mon, 11 Jun 2018 23:07:19 -0400 Subject: [PATCH 26/54] Make `AddJsonApi` chainable --- .../JsonApiDotNetCoreExample/Startup.cs | 23 ++++++-------- .../IServiceCollectionExtensions.cs | 14 +++++---- .../Helpers/Startups/MetaStartup.cs | 31 +++++++------------ 3 files changed, 30 insertions(+), 38 deletions(-) diff --git a/src/Examples/JsonApiDotNetCoreExample/Startup.cs b/src/Examples/JsonApiDotNetCoreExample/Startup.cs index 1388004a55..378a948a61 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startup.cs @@ -29,19 +29,16 @@ public virtual IServiceProvider ConfigureServices(IServiceCollection services) { var loggerFactory = new LoggerFactory(); loggerFactory.AddConsole(LogLevel.Trace); - services.AddSingleton(loggerFactory); - services.AddDbContext(options => - { - options.UseNpgsql(GetDbConnectionString()); - }, ServiceLifetime.Transient); - - services.AddJsonApi(opt => - { - opt.Namespace = "api/v1"; - opt.DefaultPageSize = 5; - opt.IncludeTotalRecordCount = true; - }); + services + .AddSingleton(loggerFactory) + .AddDbContext(options => + options.UseNpgsql(GetDbConnectionString()), ServiceLifetime.Transient) + .AddJsonApi(options => { + options.Namespace = "api/v1"; + options.DefaultPageSize = 5; + options.IncludeTotalRecordCount = true; + }); var provider = services.BuildServiceProvider(); var appContext = provider.GetRequiredService(); @@ -67,4 +64,4 @@ public virtual void Configure( public string GetDbConnectionString() => Config["Data:DefaultConnection"]; } -} \ No newline at end of file +} diff --git a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs index 807d21c018..217b5f077b 100644 --- a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs @@ -20,21 +20,21 @@ namespace JsonApiDotNetCore.Extensions // ReSharper disable once InconsistentNaming public static class IServiceCollectionExtensions { - public static void AddJsonApi(this IServiceCollection services) + public static IServiceCollection AddJsonApi(this IServiceCollection services) where TContext : DbContext { var mvcBuilder = services.AddMvc(); - AddJsonApi(services, (opt) => { }, mvcBuilder); + return AddJsonApi(services, (opt) => { }, mvcBuilder); } - public static void AddJsonApi(this IServiceCollection services, Action options) + public static IServiceCollection AddJsonApi(this IServiceCollection services, Action options) where TContext : DbContext { var mvcBuilder = services.AddMvc(); - AddJsonApi(services, options, mvcBuilder); + return AddJsonApi(services, options, mvcBuilder); } - public static void AddJsonApi(this IServiceCollection services, + public static IServiceCollection AddJsonApi(this IServiceCollection services, Action options, IMvcBuilder mvcBuilder) where TContext : DbContext { @@ -52,9 +52,10 @@ public static void AddJsonApi(this IServiceCollection services, }); AddJsonApiInternals(services, config); + return services; } - public static void AddJsonApi(this IServiceCollection services, + public static IServiceCollection AddJsonApi(this IServiceCollection services, Action options, IMvcBuilder mvcBuilder) { @@ -70,6 +71,7 @@ public static void AddJsonApi(this IServiceCollection services, }); AddJsonApiInternals(services, config); + return services; } public static void AddJsonApiInternals( diff --git a/test/JsonApiDotNetCoreExampleTests/Helpers/Startups/MetaStartup.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Startups/MetaStartup.cs index 0001878a50..df32a1223a 100644 --- a/test/JsonApiDotNetCoreExampleTests/Helpers/Startups/MetaStartup.cs +++ b/test/JsonApiDotNetCoreExampleTests/Helpers/Startups/MetaStartup.cs @@ -20,25 +20,18 @@ public MetaStartup(IHostingEnvironment env) public override IServiceProvider ConfigureServices(IServiceCollection services) { var loggerFactory = new LoggerFactory(); - - loggerFactory - .AddConsole(LogLevel.Trace); - - services.AddSingleton(loggerFactory); - - services.AddDbContext(options => - { - options.UseNpgsql(GetDbConnectionString()); - }, ServiceLifetime.Transient); - - services.AddJsonApi(opt => - { - opt.Namespace = "api/v1"; - opt.DefaultPageSize = 5; - opt.IncludeTotalRecordCount = true; - }); - - services.AddScoped(); + loggerFactory.AddConsole(LogLevel.Trace); + + services + .AddSingleton(loggerFactory) + .AddDbContext(options => + options.UseNpgsql(GetDbConnectionString()), ServiceLifetime.Transient) + .AddJsonApi(options => { + options.Namespace = "api/v1"; + options.DefaultPageSize = 5; + options.IncludeTotalRecordCount = true; + }) + .AddScoped(); return services.BuildServiceProvider(); } From 443e0392e4e9d1e62a5c444ee494b83c561b3b19 Mon Sep 17 00:00:00 2001 From: Bj Date: Mon, 11 Jun 2018 23:10:35 -0400 Subject: [PATCH 27/54] Use `MvcCore` instead of `Mvc` --- .travis.yml | 4 ++-- .../Controllers/Restricted/ReadOnlyController.cs | 8 ++++---- .../Controllers/TestValuesController.cs | 2 +- .../Controllers/TodoItemsCustomController.cs | 2 +- src/Examples/NoEntityFrameworkExample/Startup.cs | 4 ++-- src/Examples/ReportsExample/ReportsExample.csproj | 2 +- src/Examples/ReportsExample/Startup.cs | 2 +- .../Controllers/JsonApiControllerMixin.cs | 2 +- .../Controllers/JsonApiOperationsController.cs | 2 +- .../Extensions/IServiceCollectionExtensions.cs | 10 +++++----- src/JsonApiDotNetCore/JsonApiDotNetCore.csproj | 3 ++- 11 files changed, 21 insertions(+), 20 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7ac38bba9f..9c5e937589 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,12 @@ language: csharp dist: trusty sudo: required -services: +services: - postgresql before_script: - psql -c 'create database JsonApiDotNetCoreExample;' -U postgres mono: none -dotnet: 2.1.105 # https://www.microsoft.com/net/download/linux +dotnet: 2.1.300 # https://www.microsoft.com/net/download/linux branches: only: - master diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/Restricted/ReadOnlyController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/Restricted/ReadOnlyController.cs index 57fafc8fc6..2515b1ea7a 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/Restricted/ReadOnlyController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/Restricted/ReadOnlyController.cs @@ -5,7 +5,7 @@ namespace JsonApiDotNetCoreExample.Controllers.Restricted { [Route("[controller]")] [HttpReadOnly] - public class ReadOnlyController : Controller + public class ReadOnlyController : ControllerBase { [HttpGet] public IActionResult Get() => Ok(); @@ -22,7 +22,7 @@ public class ReadOnlyController : Controller [Route("[controller]")] [NoHttpPost] - public class NoHttpPostController : Controller + public class NoHttpPostController : ControllerBase { [HttpGet] public IActionResult Get() => Ok(); @@ -39,7 +39,7 @@ public class NoHttpPostController : Controller [Route("[controller]")] [NoHttpPatch] - public class NoHttpPatchController : Controller + public class NoHttpPatchController : ControllerBase { [HttpGet] public IActionResult Get() => Ok(); @@ -56,7 +56,7 @@ public class NoHttpPatchController : Controller [Route("[controller]")] [NoHttpDelete] - public class NoHttpDeleteController : Controller + public class NoHttpDeleteController : ControllerBase { [HttpGet] public IActionResult Get() => Ok(); diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/TestValuesController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/TestValuesController.cs index 3443d34b74..a29295c426 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/TestValuesController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/TestValuesController.cs @@ -3,7 +3,7 @@ namespace JsonApiDotNetCoreExample.Controllers { [Route("[controller]")] - public class TestValuesController : Controller + public class TestValuesController : ControllerBase { [HttpGet] public IActionResult Get() diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsCustomController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsCustomController.cs index de784c129a..fc25f8396e 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsCustomController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsCustomController.cs @@ -33,7 +33,7 @@ public CustomJsonApiController( } public class CustomJsonApiController - : Controller where T : class, IIdentifiable + : ControllerBase where T : class, IIdentifiable { private readonly ILogger _logger; private readonly IResourceService _resourceService; diff --git a/src/Examples/NoEntityFrameworkExample/Startup.cs b/src/Examples/NoEntityFrameworkExample/Startup.cs index dfba27ddd9..86eee500c9 100755 --- a/src/Examples/NoEntityFrameworkExample/Startup.cs +++ b/src/Examples/NoEntityFrameworkExample/Startup.cs @@ -1,4 +1,4 @@ -using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; @@ -31,7 +31,7 @@ public Startup(IHostingEnvironment env) public virtual IServiceProvider ConfigureServices(IServiceCollection services) { // Add framework services. - var mvcBuilder = services.AddMvc(); + var mvcBuilder = services.AddMvcCore(); services.AddJsonApi(options => { options.Namespace = "api/v1"; diff --git a/src/Examples/ReportsExample/ReportsExample.csproj b/src/Examples/ReportsExample/ReportsExample.csproj index bd4b402071..48df3ea2c8 100644 --- a/src/Examples/ReportsExample/ReportsExample.csproj +++ b/src/Examples/ReportsExample/ReportsExample.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/Examples/ReportsExample/Startup.cs b/src/Examples/ReportsExample/Startup.cs index 72710681b9..fe476c0406 100644 --- a/src/Examples/ReportsExample/Startup.cs +++ b/src/Examples/ReportsExample/Startup.cs @@ -25,7 +25,7 @@ public Startup(IHostingEnvironment env) public virtual void ConfigureServices(IServiceCollection services) { - var mvcBuilder = services.AddMvc(); + var mvcBuilder = services.AddMvcCore(); services.AddJsonApi(opt => { opt.BuildContextGraph(builder => diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiControllerMixin.cs b/src/JsonApiDotNetCore/Controllers/JsonApiControllerMixin.cs index 6fff3a22c8..840422eb6c 100644 --- a/src/JsonApiDotNetCore/Controllers/JsonApiControllerMixin.cs +++ b/src/JsonApiDotNetCore/Controllers/JsonApiControllerMixin.cs @@ -5,7 +5,7 @@ namespace JsonApiDotNetCore.Controllers { - public abstract class JsonApiControllerMixin : Controller + public abstract class JsonApiControllerMixin : ControllerBase { protected IActionResult UnprocessableEntity() { diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiOperationsController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiOperationsController.cs index c3b667bc7d..f6db9f0d06 100644 --- a/src/JsonApiDotNetCore/Controllers/JsonApiOperationsController.cs +++ b/src/JsonApiDotNetCore/Controllers/JsonApiOperationsController.cs @@ -8,7 +8,7 @@ namespace JsonApiDotNetCore.Controllers /// /// A controller to be used for bulk operations as defined in the json:api 1.1 specification /// - public class JsonApiOperationsController : Controller + public class JsonApiOperationsController : ControllerBase { private readonly IOperationsProcessor _operationsProcessor; diff --git a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs index 217b5f077b..5e8eeefdd1 100644 --- a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs @@ -23,20 +23,20 @@ public static class IServiceCollectionExtensions public static IServiceCollection AddJsonApi(this IServiceCollection services) where TContext : DbContext { - var mvcBuilder = services.AddMvc(); - return AddJsonApi(services, (opt) => { }, mvcBuilder); + var mvcBuilder = services.AddMvcCore(); + return AddJsonApi(services, opt => { }, mvcBuilder); } public static IServiceCollection AddJsonApi(this IServiceCollection services, Action options) where TContext : DbContext { - var mvcBuilder = services.AddMvc(); + var mvcBuilder = services.AddMvcCore(); return AddJsonApi(services, options, mvcBuilder); } public static IServiceCollection AddJsonApi(this IServiceCollection services, Action options, - IMvcBuilder mvcBuilder) where TContext : DbContext + IMvcCoreBuilder mvcBuilder) where TContext : DbContext { var config = new JsonApiOptions(); @@ -57,7 +57,7 @@ public static IServiceCollection AddJsonApi(this IServiceCollection se public static IServiceCollection AddJsonApi(this IServiceCollection services, Action options, - IMvcBuilder mvcBuilder) + IMvcCoreBuilder mvcBuilder) { var config = new JsonApiOptions(); diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index 4f52a23002..960f0f84ea 100755 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -19,11 +19,12 @@ - + + - 15.3.0-preview-20170427-09 - 1.1.2 - 2.3.0-beta3-build3705 - 15.0.3 - 4.7.99 + 15.7.2 + 2.3.1 + 22.1.2 + 4.8.3 diff --git a/benchmarks/Benchmarks.csproj b/benchmarks/Benchmarks.csproj index 1e8b227f3c..21de8e1aa2 100644 --- a/benchmarks/Benchmarks.csproj +++ b/benchmarks/Benchmarks.csproj @@ -5,7 +5,7 @@ Benchmarks - + diff --git a/src/Examples/JsonApiDotNetCoreExample/Migrations/20180327120810_initial.Designer.cs b/src/Examples/JsonApiDotNetCoreExample/Migrations/20180327120810_initial.Designer.cs index c86425b00c..c9788bf82f 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Migrations/20180327120810_initial.Designer.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Migrations/20180327120810_initial.Designer.cs @@ -1,4 +1,4 @@ -// +// using JsonApiDotNetCoreExample.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -7,6 +7,7 @@ using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage.Internal; using System; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace JsonApiDotNetCoreExample.Migrations { diff --git a/src/Examples/JsonApiDotNetCoreExample/Migrations/20180327120810_initial.cs b/src/Examples/JsonApiDotNetCoreExample/Migrations/20180327120810_initial.cs index ba19b62ef6..cc696f54bf 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Migrations/20180327120810_initial.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Migrations/20180327120810_initial.cs @@ -1,7 +1,8 @@ -using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; using System; using System.Collections.Generic; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace JsonApiDotNetCoreExample.Migrations { diff --git a/src/Examples/JsonApiDotNetCoreExample/Migrations/AppDbContextModelSnapshot.cs b/src/Examples/JsonApiDotNetCoreExample/Migrations/AppDbContextModelSnapshot.cs index c0794103fe..08c284393e 100755 --- a/src/Examples/JsonApiDotNetCoreExample/Migrations/AppDbContextModelSnapshot.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Migrations/AppDbContextModelSnapshot.cs @@ -1,4 +1,4 @@ -// +// using JsonApiDotNetCoreExample.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -7,6 +7,7 @@ using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage.Internal; using System; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace JsonApiDotNetCoreExample.Migrations { diff --git a/src/Examples/NoEntityFrameworkExample/NoEntityFrameworkExample.csproj b/src/Examples/NoEntityFrameworkExample/NoEntityFrameworkExample.csproj index eed5f1b09e..efdaa68e5b 100755 --- a/src/Examples/NoEntityFrameworkExample/NoEntityFrameworkExample.csproj +++ b/src/Examples/NoEntityFrameworkExample/NoEntityFrameworkExample.csproj @@ -18,7 +18,7 @@ - + diff --git a/src/Examples/ReportsExample/ReportsExample.csproj b/src/Examples/ReportsExample/ReportsExample.csproj index 48df3ea2c8..24c01b9a8d 100644 --- a/src/Examples/ReportsExample/ReportsExample.csproj +++ b/src/Examples/ReportsExample/ReportsExample.csproj @@ -17,7 +17,7 @@ - + diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index 960f0f84ea..b8af04e278 100755 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -17,14 +17,14 @@ - + - - + +