diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index 0beb0516c1..8142b2ea26 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -262,11 +262,14 @@ private ResourceIdentifierObject GetRelationship(object entity) var objType = entity.GetType(); var contextEntity = _jsonApiContext.ContextGraph.GetContextEntity(objType); - return new ResourceIdentifierObject - { - Type = contextEntity.EntityName, - Id = ((IIdentifiable)entity).StringId - }; + if(entity is IIdentifiable identifiableEntity) + return new ResourceIdentifierObject + { + Type = contextEntity.EntityName, + Id = identifiableEntity.StringId + }; + + return null; } private ResourceIdentifierObject GetIndependentRelationshipIdentifier(HasOneAttribute hasOne, IIdentifiable entity) diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index 032fef13c4..d1b82e30bc 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -84,13 +84,19 @@ public virtual async Task GetAndIncludeAsync(TId id, string relationshi public virtual async Task CreateAsync(TEntity entity) { - AttachHasManyPointers(); + AttachRelationships(); _dbSet.Add(entity); await _context.SaveChangesAsync(); return entity; } + protected virtual void AttachRelationships() + { + AttachHasManyPointers(); + AttachHasOnePointers(); + } + /// /// This is used to allow creation of HasMany relationships when the /// dependent side of the relationship already exists. @@ -107,6 +113,18 @@ private void AttachHasManyPointers() } } + /// + /// This is used to allow creation of HasOne relationships when the + /// independent side of the relationship already exists. + /// + private void AttachHasOnePointers() + { + var relationships = _jsonApiContext.HasOneRelationshipPointers.Get(); + foreach (var relationship in relationships) + if (_context.Entry(relationship.Value).State == EntityState.Detached && _context.EntityIsTracked(relationship.Value) == false) + _context.Entry(relationship.Value).State = EntityState.Unchanged; + } + public virtual async Task UpdateAsync(TId id, TEntity entity) { var oldEntity = await GetAsync(id); @@ -185,17 +203,23 @@ public virtual async Task> PageAsync(IQueryable en public async Task CountAsync(IQueryable entities) { - return await entities.CountAsync(); + return (entities is IAsyncEnumerable) + ? await entities.CountAsync() + : entities.Count(); } - public Task FirstOrDefaultAsync(IQueryable entities) + public async Task FirstOrDefaultAsync(IQueryable entities) { - return entities.FirstOrDefaultAsync(); + return (entities is IAsyncEnumerable) + ? await entities.FirstOrDefaultAsync() + : entities.FirstOrDefault(); } public async Task> ToListAsync(IQueryable entities) { - return await entities.ToListAsync(); + return (entities is IAsyncEnumerable) + ? await entities.ToListAsync() + : entities.ToList(); } } } diff --git a/src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs b/src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs index 3cb5ccc359..2756524dce 100644 --- a/src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs @@ -1,4 +1,7 @@ using System; +using System.Linq; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Models; using Microsoft.EntityFrameworkCore; namespace JsonApiDotNetCore.Extensions @@ -8,5 +11,23 @@ public static class DbContextExtensions [Obsolete("This is no longer required since the introduction of context.Set", error: false)] public static DbSet GetDbSet(this DbContext context) where T : class => context.Set(); + + /// + /// Determines whether or not EF is already tracking an entity of the same Type and Id + /// + public static bool EntityIsTracked(this DbContext context, IIdentifiable entity) + { + if (entity == null) + throw new ArgumentNullException(nameof(entity)); + + var trackedEntries = context.ChangeTracker + .Entries() + .FirstOrDefault(entry => + entry.Entity.GetType() == entity.GetType() + && ((IIdentifiable)entry.Entity).StringId == entity.StringId + ); + + return trackedEntries != null; + } } } diff --git a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs index 5e8eeefdd1..ea15036201 100644 --- a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs @@ -136,7 +136,6 @@ public static void AddJsonApiInternals( services.AddScoped(); services.AddScoped(); services.AddScoped(typeof(GenericProcessor<>)); - services.AddScoped(typeof(GenericProcessor<,>)); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/JsonApiDotNetCore/Extensions/TypeExtensions.cs b/src/JsonApiDotNetCore/Extensions/TypeExtensions.cs index 8cc7c0dffe..74c390e8d5 100644 --- a/src/JsonApiDotNetCore/Extensions/TypeExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/TypeExtensions.cs @@ -1,4 +1,5 @@ -using System; +using JsonApiDotNetCore.Internal; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -51,8 +52,20 @@ public static TInterface New(this Type t) { if (t == null) throw new ArgumentNullException(nameof(t)); - var instance = (TInterface)Activator.CreateInstance(t); + var instance = (TInterface)CreateNewInstance(t); return instance; } + + private static object CreateNewInstance(Type type) + { + try + { + return Activator.CreateInstance(type); + } + catch (Exception e) + { + throw new JsonApiException(500, $"Type '{type}' cannot be instantiated using the default constructor.", e); + } + } } } diff --git a/src/JsonApiDotNetCore/Internal/Generics/GenericProcessor.cs b/src/JsonApiDotNetCore/Internal/Generics/GenericProcessor.cs index 8e5d17e56b..1aa6610790 100644 --- a/src/JsonApiDotNetCore/Internal/Generics/GenericProcessor.cs +++ b/src/JsonApiDotNetCore/Internal/Generics/GenericProcessor.cs @@ -14,12 +14,7 @@ public interface IGenericProcessor void SetRelationships(object parent, RelationshipAttribute relationship, IEnumerable relationshipIds); } - public class GenericProcessor : GenericProcessor where T : class, IIdentifiable - { - public GenericProcessor(IDbContextResolver contextResolver) : base(contextResolver) { } - } - - public class GenericProcessor : IGenericProcessor where T : class, IIdentifiable + public class GenericProcessor : IGenericProcessor where T : class, IIdentifiable { private readonly DbContext _context; public GenericProcessor(IDbContextResolver contextResolver) @@ -38,12 +33,12 @@ public void SetRelationships(object parent, RelationshipAttribute relationship, { if (relationship.IsHasMany) { - var entities = _context.GetDbSet().Where(x => relationshipIds.Contains(x.StringId)).ToList(); + var entities = _context.Set().Where(x => relationshipIds.Contains(x.StringId)).ToList(); relationship.SetValue(parent, entities); } else { - var entity = _context.GetDbSet().SingleOrDefault(x => relationshipIds.First() == x.StringId); + var entity = _context.Set().SingleOrDefault(x => relationshipIds.First() == x.StringId); relationship.SetValue(parent, entity); } } diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index 7019a8e6cc..75fc955402 100755 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -1,6 +1,6 @@  - 2.3.1 + 2.3.2 $(NetStandardVersion) JsonApiDotNetCore JsonApiDotNetCore diff --git a/src/JsonApiDotNetCore/Models/Identifiable.cs b/src/JsonApiDotNetCore/Models/Identifiable.cs index 0adb073f2f..703cc2f051 100644 --- a/src/JsonApiDotNetCore/Models/Identifiable.cs +++ b/src/JsonApiDotNetCore/Models/Identifiable.cs @@ -15,7 +15,7 @@ public class Identifiable : IIdentifiable public string StringId { get => GetStringId(Id); - set => Id = (T)GetConcreteId(value); + set => Id = GetTypedId(value); } protected virtual string GetStringId(object value) @@ -34,6 +34,13 @@ protected virtual string GetStringId(object value) : stringValue; } + protected virtual T GetTypedId(string value) + { + var convertedValue = TypeHelper.ConvertType(value, typeof(T)); + return convertedValue == null ? default : (T)convertedValue; + } + + [Obsolete("Use GetTypedId instead")] protected virtual object GetConcreteId(string value) { return TypeHelper.ConvertType(value, typeof(T)); diff --git a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs b/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs index 3e66bdc8aa..65a170281e 100644 --- a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs +++ b/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs @@ -13,6 +13,17 @@ protected RelationshipAttribute(string publicName, Link documentLinks, bool canI public string PublicRelationshipName { get; } public string InternalRelationshipName { get; internal set; } + + /// + /// The related entity type. This does not necessarily match the navigation property type. + /// In the case of a HasMany relationship, this value will be the generic argument type. + /// + /// + /// + /// + /// public List<Articles> Articles { get; set; } // Type => Article + /// + /// public Type Type { get; internal set; } public bool IsHasMany => GetType() == typeof(HasManyAttribute); public bool IsHasOne => GetType() == typeof(HasOneAttribute); diff --git a/src/JsonApiDotNetCore/Request/HasOneRelationshipPointers.cs b/src/JsonApiDotNetCore/Request/HasOneRelationshipPointers.cs new file mode 100644 index 0000000000..9e0bdd0e15 --- /dev/null +++ b/src/JsonApiDotNetCore/Request/HasOneRelationshipPointers.cs @@ -0,0 +1,46 @@ +using JsonApiDotNetCore.Models; +using System; +using System.Collections.Generic; + +namespace JsonApiDotNetCore.Request +{ + /// + /// Stores information to set relationships for the request resource. + /// These relationships must already exist and should not be re-created. + /// + /// The expected use case is POST-ing or PATCH-ing + /// an entity with HasOne relationships: + /// + /// { + /// "data": { + /// "type": "photos", + /// "attributes": { + /// "title": "Ember Hamster", + /// "src": "http://example.com/images/productivity.png" + /// }, + /// "relationships": { + /// "photographer": { + /// "data": { "type": "people", "id": "2" } + /// } + /// } + /// } + /// } + /// + /// + public class HasOneRelationshipPointers + { + private Dictionary _hasOneRelationships = new Dictionary(); + + /// + /// Add the relationship to the list of relationships that should be + /// set in the repository layer. + /// + public void Add(Type dependentType, IIdentifiable entity) + => _hasOneRelationships[dependentType] = entity; + + /// + /// Get all the models that should be associated + /// + public Dictionary Get() => _hasOneRelationships; + } +} diff --git a/src/JsonApiDotNetCore/Serialization/IJsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/IJsonApiDeSerializer.cs index 0355c962ed..57b28c6087 100644 --- a/src/JsonApiDotNetCore/Serialization/IJsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/IJsonApiDeSerializer.cs @@ -9,6 +9,6 @@ public interface IJsonApiDeSerializer TEntity Deserialize(string requestBody); object DeserializeRelationship(string requestBody); List DeserializeList(string requestBody); - object DocumentToObject(DocumentData data); + object DocumentToObject(DocumentData data, List included = null); } -} \ No newline at end of file +} diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index 45d77c0f77..a1e3c76214 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -54,7 +54,7 @@ public object Deserialize(string requestBody) var document = bodyJToken.ToObject(); _jsonApiContext.DocumentMeta = document.Meta; - var entity = DocumentToObject(document.Data); + var entity = DocumentToObject(document.Data, document.Included); return entity; } catch (Exception e) @@ -95,8 +95,8 @@ public List DeserializeList(string requestBody) var deserializedList = new List(); foreach (var data in documents.Data) { - var entity = DocumentToObject(data); - deserializedList.Add((TEntity)entity); + var entity = (TEntity)DocumentToObject(data, documents.Included); + deserializedList.Add(entity); } return deserializedList; @@ -107,7 +107,7 @@ public List DeserializeList(string requestBody) } } - public object DocumentToObject(DocumentData data) + public object DocumentToObject(DocumentData data, List included = null) { if (data == null) throw new JsonApiException(422, "Failed to deserialize document as json:api."); @@ -117,7 +117,7 @@ public object DocumentToObject(DocumentData data) var entity = Activator.CreateInstance(contextEntity.EntityType); entity = SetEntityAttributes(entity, contextEntity, data.Attributes); - entity = SetRelationships(entity, contextEntity, data.Relationships); + entity = SetRelationships(entity, contextEntity, data.Relationships, included); var identifiableEntity = (IIdentifiable)entity; @@ -172,7 +172,8 @@ private object DeserializeComplexType(JContainer obj, Type targetType) private object SetRelationships( object entity, ContextEntity contextEntity, - Dictionary relationships) + Dictionary relationships, + List included = null) { if (relationships == null || relationships.Count == 0) return entity; @@ -182,8 +183,8 @@ private object SetRelationships( foreach (var attr in contextEntity.Relationships) { entity = attr.IsHasOne - ? SetHasOneRelationship(entity, entityProperties, (HasOneAttribute)attr, contextEntity, relationships) - : SetHasManyRelationship(entity, entityProperties, attr, contextEntity, relationships); + ? SetHasOneRelationship(entity, entityProperties, (HasOneAttribute)attr, contextEntity, relationships, included) + : SetHasManyRelationship(entity, entityProperties, attr, contextEntity, relationships, included); } return entity; @@ -193,39 +194,58 @@ private object SetHasOneRelationship(object entity, PropertyInfo[] entityProperties, HasOneAttribute attr, ContextEntity contextEntity, - Dictionary relationships) + Dictionary relationships, + List included = null) { var relationshipName = attr.PublicRelationshipName; - if (relationships.TryGetValue(relationshipName, out RelationshipData relationshipData)) - { - var relationshipAttr = _jsonApiContext.RequestEntity.Relationships - .SingleOrDefault(r => r.PublicRelationshipName == relationshipName); + if (relationships.TryGetValue(relationshipName, out RelationshipData relationshipData) == false) + return entity; + + var relationshipAttr = _jsonApiContext.RequestEntity.Relationships + .SingleOrDefault(r => r.PublicRelationshipName == relationshipName); - if (relationshipAttr == null) - throw new JsonApiException(400, $"{_jsonApiContext.RequestEntity.EntityName} does not contain a relationship '{relationshipName}'"); + if (relationshipAttr == null) + throw new JsonApiException(400, $"{_jsonApiContext.RequestEntity.EntityName} does not contain a relationship '{relationshipName}'"); - var rio = (ResourceIdentifierObject)relationshipData.ExposedData; + var rio = (ResourceIdentifierObject)relationshipData.ExposedData; - var foreignKey = attr.IdentifiablePropertyName; - var entityProperty = entityProperties.FirstOrDefault(p => p.Name == foreignKey); - if (entityProperty == null && rio != null) - throw new JsonApiException(400, $"{contextEntity.EntityType.Name} does not contain a foreign key property '{foreignKey}' for has one relationship '{attr.InternalRelationshipName}'"); + var foreignKey = attr.IdentifiablePropertyName; + var foreignKeyProperty = entityProperties.FirstOrDefault(p => p.Name == foreignKey); - if (entityProperty != null) - { - // e.g. PATCH /articles - // {... { "relationships":{ "Owner": { "data" :null } } } } - if (rio == null && Nullable.GetUnderlyingType(entityProperty.PropertyType) == null) - throw new JsonApiException(400, $"Cannot set required relationship identifier '{attr.IdentifiablePropertyName}' to null."); + if (foreignKeyProperty == null && rio == null) + return entity; - var newValue = rio?.Id ?? null; - var convertedValue = TypeHelper.ConvertType(newValue, entityProperty.PropertyType); + if (foreignKeyProperty == null && rio != null) + throw new JsonApiException(400, $"{contextEntity.EntityType.Name} does not contain a foreign key property '{foreignKey}' for has one relationship '{attr.InternalRelationshipName}'"); - _jsonApiContext.RelationshipsToUpdate[relationshipAttr] = convertedValue; + // e.g. PATCH /articles + // {... { "relationships":{ "Owner": { "data": null } } } } + if (rio == null && Nullable.GetUnderlyingType(foreignKeyProperty.PropertyType) == null) + throw new JsonApiException(400, $"Cannot set required relationship identifier '{attr.IdentifiablePropertyName}' to null because it is a non-nullable type."); - entityProperty.SetValue(entity, convertedValue); - } + var newValue = rio?.Id ?? null; + var convertedValue = TypeHelper.ConvertType(newValue, foreignKeyProperty.PropertyType); + + _jsonApiContext.RelationshipsToUpdate[relationshipAttr] = convertedValue; + + foreignKeyProperty.SetValue(entity, convertedValue); + + + if(rio != null + // if the resource identifier is null, there should be no reason to instantiate an instance + && rio.Id != null) + { + // we have now set the FK property on the resource, now we need to check to see if the + // related entity was included in the payload and update its attributes + var includedRelationshipObject = GetIncludedRelationship(rio, included, relationshipAttr); + if (includedRelationshipObject != null) + relationshipAttr.SetValue(entity, includedRelationshipObject); + + // we need to store the fact that this relationship was included in the payload + // for EF, the repository will use these pointers to make ensure we don't try to + // create resources if they already exist, we just need to create the relationship + _jsonApiContext.HasOneRelationshipPointers.Add(attr.Type, includedRelationshipObject); } return entity; @@ -235,7 +255,8 @@ private object SetHasManyRelationship(object entity, PropertyInfo[] entityProperties, RelationshipAttribute attr, ContextEntity contextEntity, - Dictionary relationships) + Dictionary relationships, + List included = null) { var relationshipName = attr.PublicRelationshipName; @@ -245,14 +266,13 @@ private object SetHasManyRelationship(object entity, if (data == null) return entity; - var relationshipShells = relationshipData.ManyData.Select(r => + var relatedResources = relationshipData.ManyData.Select(r => { - var instance = attr.Type.New(); - instance.StringId = r.Id; + var instance = GetIncludedRelationship(r, included, attr); return instance; }); - var convertedCollection = TypeHelper.ConvertCollection(relationshipShells, attr.Type); + var convertedCollection = TypeHelper.ConvertCollection(relatedResources, attr.Type); attr.SetValue(entity, convertedCollection); @@ -261,5 +281,41 @@ private object SetHasManyRelationship(object entity, return entity; } + + private IIdentifiable GetIncludedRelationship(ResourceIdentifierObject relatedResourceIdentifier, List includedResources, RelationshipAttribute relationshipAttr) + { + // at this point we can be sure the relationshipAttr.Type is IIdentifiable because we were able to successfully build the ContextGraph + var relatedInstance = relationshipAttr.Type.New(); + relatedInstance.StringId = relatedResourceIdentifier.Id; + + // can't provide any more data other than the rio since it is not contained in the included section + if (includedResources == null || includedResources.Count == 0) + return relatedInstance; + + var includedResource = GetLinkedResource(relatedResourceIdentifier, includedResources); + if (includedResource == null) + return relatedInstance; + + var contextEntity = _jsonApiContext.ContextGraph.GetContextEntity(relationshipAttr.Type); + if (contextEntity == null) + throw new JsonApiException(400, $"Included type '{relationshipAttr.Type}' is not a registered json:api resource."); + + SetEntityAttributes(relatedInstance, contextEntity, includedResource.Attributes); + + return relatedInstance; + } + + private DocumentData GetLinkedResource(ResourceIdentifierObject relatedResourceIdentifier, List includedResources) + { + try + { + return includedResources.SingleOrDefault(r => r.Type == relatedResourceIdentifier.Type && r.Id == relatedResourceIdentifier.Id); + } + catch (InvalidOperationException e) + { + throw new JsonApiException(400, $"A compound document MUST NOT include more than one resource object for each type and id pair." + + $"The duplicate pair was '{relatedResourceIdentifier.Type}, {relatedResourceIdentifier.Id}'", e); + } + } } } diff --git a/src/JsonApiDotNetCore/Services/Contract/IResourceCmdService.cs b/src/JsonApiDotNetCore/Services/Contract/IResourceCmdService.cs index 2633fd589b..0f6c5e64b7 100644 --- a/src/JsonApiDotNetCore/Services/Contract/IResourceCmdService.cs +++ b/src/JsonApiDotNetCore/Services/Contract/IResourceCmdService.cs @@ -2,7 +2,12 @@ namespace JsonApiDotNetCore.Services { - public interface IResourceCmdService : IResourceCmdService + public interface IResourceCmdService : + ICreateService, + IUpdateService, + IUpdateRelationshipService, + IDeleteService, + IResourceCmdService where T : class, IIdentifiable { } diff --git a/src/JsonApiDotNetCore/Services/Contract/IResourceQueryService.cs b/src/JsonApiDotNetCore/Services/Contract/IResourceQueryService.cs index 8a9e247a3f..1cd4a94cf3 100644 --- a/src/JsonApiDotNetCore/Services/Contract/IResourceQueryService.cs +++ b/src/JsonApiDotNetCore/Services/Contract/IResourceQueryService.cs @@ -2,7 +2,12 @@ namespace JsonApiDotNetCore.Services { - public interface IResourceQueryService : IResourceQueryService + public interface IResourceQueryService : + IGetAllService, + IGetByIdService, + IGetRelationshipsService, + IGetRelationshipService, + IResourceQueryService where T : class, IIdentifiable { } diff --git a/src/JsonApiDotNetCore/Services/Contract/IResourceService.cs b/src/JsonApiDotNetCore/Services/Contract/IResourceService.cs index 82f3505c78..ec534a5f1b 100644 --- a/src/JsonApiDotNetCore/Services/Contract/IResourceService.cs +++ b/src/JsonApiDotNetCore/Services/Contract/IResourceService.cs @@ -3,7 +3,7 @@ namespace JsonApiDotNetCore.Services { public interface IResourceService - : IResourceService + : IResourceCmdService, IResourceQueryService, IResourceService where T : class, IIdentifiable { } diff --git a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs index 52036f21af..9cbe2a53ca 100644 --- a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs @@ -81,6 +81,31 @@ public interface IJsonApiRequest : IJsonApiApplication, IUpdateRequest, IQueryRe /// HasManyRelationshipPointers HasManyRelationshipPointers { get; } + /// + /// Stores information to set relationships for the request resource. + /// These relationships must already exist and should not be re-created. + /// + /// The expected use case is POST-ing or PATCH-ing + /// an entity with HasOne relationships: + /// + /// { + /// "data": { + /// "type": "photos", + /// "attributes": { + /// "title": "Ember Hamster", + /// "src": "http://example.com/images/productivity.png" + /// }, + /// "relationships": { + /// "photographer": { + /// "data": { "type": "people", "id": "2" } + /// } + /// } + /// } + /// } + /// + /// + HasOneRelationshipPointers HasOneRelationshipPointers { get; } + /// /// If the request is a bulk json:api v1.1 operations request. /// This is determined by the ` diff --git a/src/JsonApiDotNetCore/Services/JsonApiContext.cs b/src/JsonApiDotNetCore/Services/JsonApiContext.cs index 0643d494d6..f579d8c9a2 100644 --- a/src/JsonApiDotNetCore/Services/JsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/JsonApiContext.cs @@ -53,6 +53,7 @@ public JsonApiContext( public Dictionary DocumentMeta { get; set; } public bool IsBulkOperationRequest { get; set; } public HasManyRelationshipPointers HasManyRelationshipPointers { get; } = new HasManyRelationshipPointers(); + public HasOneRelationshipPointers HasOneRelationshipPointers { get; } = new HasOneRelationshipPointers(); public IJsonApiContext ApplyContext(object controller) { diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs index acd37a535a..672492df16 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs @@ -294,11 +294,12 @@ public async Task Can_Post_TodoItem() // Act var response = await _fixture.Client.SendAsync(request); - var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = (TodoItem)_fixture.GetService().Deserialize(body); // Assert Assert.Equal(HttpStatusCode.Created, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + var deserializedBody = (TodoItem)_fixture.GetService().Deserialize(body); + Assert.Equal(HttpStatusCode.Created, response.StatusCode); Assert.Equal(todoItem.Description, deserializedBody.Description); Assert.Equal(todoItem.CreatedDate.ToString("G"), deserializedBody.CreatedDate.ToString("G")); Assert.Null(deserializedBody.AchievedDate); diff --git a/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs b/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs index be16d1d9d7..2da434c765 100644 --- a/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs +++ b/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs @@ -392,20 +392,146 @@ public void Can_Deserialize_Object_With_HasManyRelationship() Assert.Equal(1, result.Id); Assert.NotNull(result.Dependents); Assert.NotEmpty(result.Dependents); - Assert.Equal(1, result.Dependents.Count); + Assert.Single(result.Dependents); var dependent = result.Dependents[0]; Assert.Equal(2, dependent.Id); } + [Fact] + public void Sets_Attribute_Values_On_Included_HasMany_Relationships() + { + // arrange + var contextGraphBuilder = new ContextGraphBuilder(); + contextGraphBuilder.AddResource("independents"); + contextGraphBuilder.AddResource("dependents"); + var contextGraph = contextGraphBuilder.Build(); + + var jsonApiContextMock = new Mock(); + jsonApiContextMock.SetupAllProperties(); + jsonApiContextMock.Setup(m => m.ContextGraph).Returns(contextGraph); + jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary()); + jsonApiContextMock.Setup(m => m.HasManyRelationshipPointers).Returns(new HasManyRelationshipPointers()); + + var jsonApiOptions = new JsonApiOptions(); + jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); + + var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object); + + var expectedName = "John Doe"; + var contentString = + @"{ + ""data"": { + ""type"": ""independents"", + ""id"": ""1"", + ""attributes"": { }, + ""relationships"": { + ""dependents"": { + ""data"": [ + { + ""type"": ""dependents"", + ""id"": ""2"" + } + ] + } + } + }, + ""included"": [ + { + ""type"": ""dependents"", + ""id"": ""2"", + ""attributes"": { + ""name"": """ + expectedName + @""" + } + } + ] + }"; + + // act + var result = deserializer.Deserialize(contentString); + + // assert + Assert.NotNull(result); + Assert.Equal(1, result.Id); + Assert.NotNull(result.Dependents); + Assert.NotEmpty(result.Dependents); + Assert.Single(result.Dependents); + + var dependent = result.Dependents[0]; + Assert.Equal(2, dependent.Id); + Assert.Equal(expectedName, dependent.Name); + } + + [Fact] + public void Sets_Attribute_Values_On_Included_HasOne_Relationships() + { + // arrange + var contextGraphBuilder = new ContextGraphBuilder(); + contextGraphBuilder.AddResource("independents"); + contextGraphBuilder.AddResource("dependents"); + var contextGraph = contextGraphBuilder.Build(); + + var jsonApiContextMock = new Mock(); + jsonApiContextMock.SetupAllProperties(); + jsonApiContextMock.Setup(m => m.ContextGraph).Returns(contextGraph); + jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary()); + jsonApiContextMock.Setup(m => m.RelationshipsToUpdate).Returns(new Dictionary()); + jsonApiContextMock.Setup(m => m.HasManyRelationshipPointers).Returns(new HasManyRelationshipPointers()); + jsonApiContextMock.Setup(m => m.HasOneRelationshipPointers).Returns(new HasOneRelationshipPointers()); + + var jsonApiOptions = new JsonApiOptions(); + jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); + + var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object); + + var expectedName = "John Doe"; + var contentString = + @"{ + ""data"": { + ""type"": ""dependents"", + ""id"": ""1"", + ""attributes"": { }, + ""relationships"": { + ""independent"": { + ""data"": { + ""type"": ""independents"", + ""id"": ""2"" + } + } + } + }, + ""included"": [ + { + ""type"": ""independents"", + ""id"": ""2"", + ""attributes"": { + ""name"": """ + expectedName + @""" + } + } + ] + }"; + + // act + var result = deserializer.Deserialize(contentString); + + // assert + Assert.NotNull(result); + Assert.Equal(1, result.Id); + Assert.NotNull(result.Independent); + Assert.Equal(2, result.Independent.Id); + Assert.Equal(expectedName, result.Independent.Name); + } + private class OneToManyDependent : Identifiable { + [Attr("name")] public string Name { get; set; } [HasOne("independent")] public OneToManyIndependent Independent { get; set; } public int IndependentId { get; set; } } private class OneToManyIndependent : Identifiable { + [Attr("name")] public string Name { get; set; } [HasMany("dependents")] public List Dependents { get; set; } } } diff --git a/test/UnitTests/Services/Operations/Processors/CreateOpProcessorTests.cs b/test/UnitTests/Services/Operations/Processors/CreateOpProcessorTests.cs index 9e33af63c2..aa76f2dc17 100644 --- a/test/UnitTests/Services/Operations/Processors/CreateOpProcessorTests.cs +++ b/test/UnitTests/Services/Operations/Processors/CreateOpProcessorTests.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Internal; @@ -48,7 +48,7 @@ public async Task ProcessAsync_Deserializes_And_Creates() .AddResource("test-resources") .Build(); - _deserializerMock.Setup(m => m.DocumentToObject(It.IsAny())) + _deserializerMock.Setup(m => m.DocumentToObject(It.IsAny(), It.IsAny>())) .Returns(testResource); var opProcessor = new CreateOpProcessor(