Skip to content

Commit 74f32f7

Browse files
author
Bart Koelman
authored
Change tracking for patch updates, to improve json:api spec compliance (#704)
Change tracking for patch updates, to improve json:api spec compliance I have added some calculated properties to the examples, in order to have tests for: - nothing outside the patch request changed (returns empty data) - `KebabCaseFormatterTests.KebabCaseFormatter_Update_IsUpdated` has no side effects in updating `KebabCasedModel` attributes - `ManyToManyTests.Can_Update_Many_To_Many` has no side effects in updating `Article` tags - `ManyToManyTests.Can_Update_Many_To_Many_With_Complete_Replacement` has no side effects in updating `Article` tags - `ManyToManyTests.Can_Update_Many_To_Many_With_Complete_Replacement_With_Overlap` has no side effects in updating `Article` tags - exposed attribute that was not in PATCH request changed (returns attributes) - `UpdatingDataTests.PatchResource_ModelWithEntityFrameworkInheritance_IsPatched` updates `User.Password` property, which updates exposed `LastPasswordChange` attribute - `UpdatingDataTests.Patch_Entity_With_HasMany_Does_Not_Include_Relationships` updates `Person.FirstName` property, which updates exposed `Initials` attribute - `TodoItemsControllerTests.Can_Patch_TodoItemWithNullable` does not update exposed `TodoItem.AlwaysChangingValue` attribute - exposed attribute that was in PATCH request changed (returns attributes) - `TodoItemsControllerTests.Can_Patch_TodoItem` updates `TodoItem.AlwaysChangingValue` attribute Also updated a few places where an empty list was allocated each time with a cached empty array instance.
1 parent c13d950 commit 74f32f7

28 files changed

+348
-118
lines changed

src/Examples/JsonApiDotNetCoreExample/Models/Passport.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Collections.Generic;
23
using System.ComponentModel.DataAnnotations.Schema;
34
using System.Linq;
@@ -7,8 +8,24 @@ namespace JsonApiDotNetCoreExample.Models
78
{
89
public class Passport : Identifiable
910
{
11+
private int? _socialSecurityNumber;
12+
13+
[Attr]
14+
public int? SocialSecurityNumber
15+
{
16+
get => _socialSecurityNumber;
17+
set
18+
{
19+
if (value != _socialSecurityNumber)
20+
{
21+
LastSocialSecurityNumberChange = DateTime.Now;
22+
_socialSecurityNumber = value;
23+
}
24+
}
25+
}
26+
1027
[Attr]
11-
public int? SocialSecurityNumber { get; set; }
28+
public DateTime LastSocialSecurityNumberChange { get; set; }
1229

1330
[Attr]
1431
public bool IsLocked { get; set; }

src/Examples/JsonApiDotNetCoreExample/Models/Person.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Collections.Generic;
2+
using System.Linq;
23
using JsonApiDotNetCore.Models;
34
using JsonApiDotNetCore.Models.Links;
45

@@ -12,10 +13,26 @@ public sealed class PersonRole : Identifiable
1213

1314
public sealed class Person : Identifiable, IIsLockable
1415
{
16+
private string _firstName;
17+
1518
public bool IsLocked { get; set; }
1619

1720
[Attr]
18-
public string FirstName { get; set; }
21+
public string FirstName
22+
{
23+
get => _firstName;
24+
set
25+
{
26+
if (value != _firstName)
27+
{
28+
_firstName = value;
29+
Initials = string.Concat(value.Split(' ').Select(x => char.ToUpperInvariant(x[0])));
30+
}
31+
}
32+
}
33+
34+
[Attr]
35+
public string Initials { get; set; }
1936

2037
[Attr]
2138
public string LastName { get; set; }

src/Examples/JsonApiDotNetCoreExample/Models/TodoItem.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ public TodoItem()
2222
[Attr]
2323
public Guid GuidProperty { get; set; }
2424

25+
[Attr]
26+
public string AlwaysChangingValue
27+
{
28+
get => Guid.NewGuid().ToString();
29+
set { }
30+
}
31+
2532
[Attr]
2633
public DateTime CreatedDate { get; set; }
2734

src/Examples/JsonApiDotNetCoreExample/Models/User.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,29 @@
1+
using System;
12
using JsonApiDotNetCore.Models;
23

34
namespace JsonApiDotNetCoreExample.Models
45
{
56
public class User : Identifiable
67
{
8+
private string _password;
9+
710
[Attr] public string Username { get; set; }
8-
[Attr] public string Password { get; set; }
11+
12+
[Attr]
13+
public string Password
14+
{
15+
get => _password;
16+
set
17+
{
18+
if (value != _password)
19+
{
20+
_password = value;
21+
LastPasswordChange = DateTime.Now;
22+
}
23+
}
24+
}
25+
26+
[Attr] public DateTime LastPasswordChange { get; set; }
927
}
1028

1129
public sealed class SuperUser : User

src/Examples/JsonApiDotNetCoreExample/Services/CustomArticleService.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Microsoft.Extensions.Logging;
99
using System.Collections.Generic;
1010
using System.Threading.Tasks;
11+
using JsonApiDotNetCore.RequestServices;
1112

1213
namespace JsonApiDotNetCoreExample.Services
1314
{
@@ -19,8 +20,9 @@ public CustomArticleService(
1920
ILoggerFactory loggerFactory,
2021
IResourceRepository<Article, int> repository,
2122
IResourceContextProvider provider,
23+
IResourceChangeTracker<Article> resourceChangeTracker,
2224
IResourceHookExecutor hookExecutor = null)
23-
: base(queryParameters, options, loggerFactory, repository, provider, hookExecutor)
25+
: base(queryParameters, options, loggerFactory, repository, provider, resourceChangeTracker, hookExecutor)
2426
{ }
2527

2628
public override async Task<Article> GetAsync(int id)

src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
using JsonApiDotNetCore.Serialization.Server;
2222
using Microsoft.Extensions.DependencyInjection.Extensions;
2323
using JsonApiDotNetCore.QueryParameterServices.Common;
24+
using JsonApiDotNetCore.RequestServices;
2425

2526
namespace JsonApiDotNetCore.Builders
2627
{
@@ -161,6 +162,7 @@ public void ConfigureServices()
161162
_services.AddScoped<ITargetedFields, TargetedFields>();
162163
_services.AddScoped<IResourceDefinitionProvider, ResourceDefinitionProvider>();
163164
_services.AddScoped<IFieldsToSerialize, FieldsToSerialize>();
165+
_services.AddScoped(typeof(IResourceChangeTracker<>), typeof(DefaultResourceChangeTracker<>));
164166
_services.AddScoped<IQueryParameterActionFilter, QueryParameterActionFilter>();
165167

166168
AddServerSerialization();

src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ public virtual async Task<IActionResult> PatchAsync(TId id, [FromBody] T entity)
133133
throw new InvalidModelStateException(ModelState, typeof(T), _jsonApiOptions.IncludeExceptionStackTraceInErrors);
134134

135135
var updatedEntity = await _update.UpdateAsync(id, entity);
136-
return Ok(updatedEntity);
136+
return updatedEntity == null ? Ok(null) : Ok(updatedEntity);
137137
}
138138

139139
public virtual async Task<IActionResult> PatchRelationshipsAsync(TId id, string relationshipName, [FromBody] object relationships)

src/JsonApiDotNetCore/Data/DefaultResourceRepository.cs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -195,16 +195,12 @@ private void DetachRelationships(TResource entity)
195195
}
196196

197197
/// <inheritdoc />
198-
public virtual async Task<TResource> UpdateAsync(TResource updatedEntity)
198+
public virtual async Task UpdateAsync(TResource requestEntity, TResource databaseEntity)
199199
{
200-
_logger.LogTrace($"Entering {nameof(UpdateAsync)}({(updatedEntity == null ? "null" : "object")}).");
201-
202-
var databaseEntity = await Get(updatedEntity.Id).FirstOrDefaultAsync();
203-
if (databaseEntity == null)
204-
return null;
200+
_logger.LogTrace($"Entering {nameof(UpdateAsync)}({(requestEntity == null ? "null" : "object")}, {(databaseEntity == null ? "null" : "object")}).");
205201

206202
foreach (var attribute in _targetedFields.Attributes)
207-
attribute.SetValue(databaseEntity, attribute.GetValue(updatedEntity));
203+
attribute.SetValue(databaseEntity, attribute.GetValue(requestEntity));
208204

209205
foreach (var relationshipAttr in _targetedFields.Relationships)
210206
{
@@ -213,7 +209,7 @@ public virtual async Task<TResource> UpdateAsync(TResource updatedEntity)
213209
// trackedRelationshipValue is either equal to updatedPerson.todoItems,
214210
// or replaced with the same set (same ids) of todoItems from the EF Core change tracker,
215211
// which is the case if they were already tracked
216-
object trackedRelationshipValue = GetTrackedRelationshipValue(relationshipAttr, updatedEntity, out _);
212+
object trackedRelationshipValue = GetTrackedRelationshipValue(relationshipAttr, requestEntity, out _);
217213
// loads into the db context any persons currently related
218214
// to the todoItems in trackedRelationshipValue
219215
LoadInverseRelationships(trackedRelationshipValue, relationshipAttr);
@@ -223,7 +219,6 @@ public virtual async Task<TResource> UpdateAsync(TResource updatedEntity)
223219
}
224220

225221
await _context.SaveChangesAsync();
226-
return databaseEntity;
227222
}
228223

229224
/// <summary>
@@ -303,6 +298,13 @@ public virtual async Task<bool> DeleteAsync(TId id)
303298
return true;
304299
}
305300

301+
public virtual void FlushFromCache(TResource entity)
302+
{
303+
_logger.LogTrace($"Entering {nameof(FlushFromCache)}({nameof(entity)}).");
304+
305+
_context.Entry(entity).State = EntityState.Detached;
306+
}
307+
306308
private IQueryable<TResource> EagerLoad(IQueryable<TResource> entities, IEnumerable<EagerLoadAttribute> attributes, string chainPrefix = null)
307309
{
308310
foreach (var attribute in attributes)

src/JsonApiDotNetCore/Data/IResourceWriteRepository.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ public interface IResourceWriteRepository<TResource, in TId>
1414
{
1515
Task<TResource> CreateAsync(TResource entity);
1616

17-
Task<TResource> UpdateAsync(TResource entity);
17+
Task UpdateAsync(TResource requestEntity, TResource databaseEntity);
1818

1919
Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable<string> relationshipIds);
2020

2121
Task<bool> DeleteAsync(TId id);
22+
23+
void FlushFromCache(TResource entity);
2224
}
2325
}

src/JsonApiDotNetCore/Middleware/DefaultTypeMatchFilter.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using System;
21
using System.Linq;
32
using System.Net.Http;
43
using JsonApiDotNetCore.Exceptions;

0 commit comments

Comments
 (0)