diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index 4c98f8cec8..6e18aebc96 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -105,9 +105,12 @@ public virtual async Task UpdateAsync(TId id, TEntity entity) attr.SetValue(oldEntity, attr.GetValue(entity)); }); + foreach(var relationship in _jsonApiContext.RelationshipsToUpdate) + relationship.Key.SetValue(oldEntity, relationship.Value); + await _context.SaveChangesAsync(); - return oldEntity; + return oldEntity; } public async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable relationshipIds) diff --git a/src/JsonApiDotNetCore/Models/HasManyAttribute.cs b/src/JsonApiDotNetCore/Models/HasManyAttribute.cs index 13e4a9efad..445b82c22a 100644 --- a/src/JsonApiDotNetCore/Models/HasManyAttribute.cs +++ b/src/JsonApiDotNetCore/Models/HasManyAttribute.cs @@ -1,4 +1,4 @@ -using System; +using System.Reflection; namespace JsonApiDotNetCore.Models { @@ -9,5 +9,14 @@ public HasManyAttribute(string publicName) { PublicRelationshipName = publicName; } + + public override void SetValue(object entity, object newValue) + { + var propertyInfo = entity + .GetType() + .GetProperty(InternalRelationshipName); + + propertyInfo.SetValue(entity, newValue); + } } } diff --git a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs b/src/JsonApiDotNetCore/Models/HasOneAttribute.cs index e5670eae29..29661de485 100644 --- a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs +++ b/src/JsonApiDotNetCore/Models/HasOneAttribute.cs @@ -1,3 +1,5 @@ +using System.Reflection; + namespace JsonApiDotNetCore.Models { public class HasOneAttribute : RelationshipAttribute @@ -7,5 +9,18 @@ public HasOneAttribute(string publicName) { PublicRelationshipName = publicName; } + + public override void SetValue(object entity, object newValue) + { + var propertyName = (newValue.GetType() == Type) + ? InternalRelationshipName + : $"{InternalRelationshipName}Id"; + + var propertyInfo = entity + .GetType() + .GetProperty(propertyName); + + propertyInfo.SetValue(entity, newValue); + } } } diff --git a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs b/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs index 45b3565592..5e02eaf1ef 100644 --- a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs +++ b/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs @@ -1,9 +1,8 @@ using System; -using System.Reflection; namespace JsonApiDotNetCore.Models { - public class RelationshipAttribute : Attribute + public abstract class RelationshipAttribute : Attribute { protected RelationshipAttribute(string publicName) { @@ -16,13 +15,6 @@ protected RelationshipAttribute(string publicName) public bool IsHasMany { get { return this.GetType() == typeof(HasManyAttribute); } } public bool IsHasOne { get { return this.GetType() == typeof(HasOneAttribute); } } - public void SetValue(object entity, object newValue) - { - var propertyInfo = entity - .GetType() - .GetProperty(InternalRelationshipName); - - propertyInfo.SetValue(entity, newValue); - } + public abstract void SetValue(object entity, object newValue); } } diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index 63187099ea..367904f8dd 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -141,12 +141,18 @@ private object _setHasOneRelationship(object entity, if (relationships.TryGetValue(relationshipName, out RelationshipData relationshipData)) { + var relationshipAttr = _jsonApiContext.RequestEntity.Relationships + .SingleOrDefault(r => r.PublicRelationshipName == relationshipName); + var data = (Dictionary)relationshipData.ExposedData; if (data == null) return entity; var newValue = data["id"]; var convertedValue = TypeHelper.ConvertType(newValue, entityProperty.PropertyType); + + _jsonApiContext.RelationshipsToUpdate[relationshipAttr] = convertedValue; + entityProperty.SetValue(entity, convertedValue); } diff --git a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs index f7d05dc4b3..2860c3eb74 100644 --- a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs @@ -3,6 +3,7 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Models; namespace JsonApiDotNetCore.Services { @@ -20,5 +21,6 @@ public interface IJsonApiContext PageManager PageManager { get; set; } IMetaBuilder MetaBuilder { get; set; } IGenericProcessorFactory GenericProcessorFactory { get; set; } + Dictionary RelationshipsToUpdate { get; set; } } } diff --git a/src/JsonApiDotNetCore/Services/JsonApiContext.cs b/src/JsonApiDotNetCore/Services/JsonApiContext.cs index f9bd3f0b0f..9cc244677d 100644 --- a/src/JsonApiDotNetCore/Services/JsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/JsonApiContext.cs @@ -4,6 +4,7 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Models; using Microsoft.AspNetCore.Http; namespace JsonApiDotNetCore.Services @@ -23,6 +24,7 @@ public JsonApiContext( Options = options; MetaBuilder = metaBuilder; GenericProcessorFactory = genericProcessorFactory; + RelationshipsToUpdate = new Dictionary(); } public JsonApiOptions Options { get; set; } @@ -36,6 +38,7 @@ public JsonApiContext( public PageManager PageManager { get; set; } public IMetaBuilder MetaBuilder { get; set; } public IGenericProcessorFactory GenericProcessorFactory { get; set; } + public Dictionary RelationshipsToUpdate { get; set; } public IJsonApiContext ApplyContext() { diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs index c87683fa2f..a5ad8f6a7a 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; @@ -7,15 +6,15 @@ using Bogus; using DotNetCoreDocs; using DotNetCoreDocs.Writers; -using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; +using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; using Xunit; +using Person = JsonApiDotNetCoreExample.Models.Person; namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec { @@ -25,14 +24,18 @@ public class UpdatingDataTests private DocsFixture _fixture; private AppDbContext _context; private Faker _todoItemFaker; + private Faker _personFaker; public UpdatingDataTests(DocsFixture fixture) { _fixture = fixture; _context = fixture.GetService(); - _todoItemFaker = new Faker() + _todoItemFaker = new Faker() .RuleFor(t => t.Description, f => f.Lorem.Sentence()) .RuleFor(t => t.Ordinal, f => f.Random.Number()); + _personFaker = new Faker() + .RuleFor(p => p.FirstName, f => f.Name.FirstName()) + .RuleFor(p => p.LastName, f => f.Name.LastName()); } [Fact] @@ -73,5 +76,62 @@ public async Task Respond_404_If_EntityDoesNotExist() // Assert Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } + + [Fact] + public async Task Can_Patch_Entity_And_HasOne_Relationships() + { + // arrange + var todoItem = _todoItemFaker.Generate(); + var person = _personFaker.Generate(); + _context.TodoItems.Add(todoItem); + _context.People.Add(person); + _context.SaveChanges(); + + var builder = new WebHostBuilder() + .UseStartup(); + var server = new TestServer(builder); + var client = server.CreateClient(); + + var content = new + { + data = new + { + type = "todo-items", + attributes = new + { + description = todoItem.Description, + ordinal = todoItem.Ordinal + }, + relationships = new + { + owner = new + { + data = new + { + type = "people", + id = person.Id.ToString() + } + } + } + } + }; + + 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); + var updatedTodoItem = _context.TodoItems.AsNoTracking() + .Include(t => t.Owner) + .SingleOrDefault(t => t.Id == todoItem.Id); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(person.Id, updatedTodoItem.OwnerId); + } } }