Skip to content

Commit 57a7971

Browse files
committed
fix: merge
2 parents 1705bef + 5941f19 commit 57a7971

File tree

7 files changed

+108
-24
lines changed

7 files changed

+108
-24
lines changed

src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,14 @@ public virtual async Task<TEntity> CreateAsync(TEntity entity)
171171
/// <summary>
172172
/// Loads the inverse relationships to prevent foreign key constraints from being violated
173173
/// to support implicit removes, see https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/502.
174+
/// <remark>
175+
/// Consider the following example:
176+
/// person.todoItems = [t1,t2] is updated to [t3, t4]. If t3, and/or t4 was
177+
/// already related to a other person, and these persons are NOT loaded in to the
178+
/// db context, then the query may cause a foreign key constraint. Loading
179+
/// these "inverse relationships" into the DB context ensures EF core to take
180+
/// this into account.
181+
/// </remark>
174182
/// </summary>
175183
private void LoadInverseRelationships(object trackedRelationshipValue, RelationshipAttribute relationshipAttr)
176184
{
@@ -181,14 +189,15 @@ private void LoadInverseRelationships(object trackedRelationshipValue, Relations
181189
if (IsHasOneRelationship(hasOneAttr.InverseNavigation, trackedRelationshipValue.GetType()))
182190
{
183191
relationEntry.Reference(hasOneAttr.InverseNavigation).Load();
184-
} else
192+
}
193+
else
185194
{
186195
relationEntry.Collection(hasOneAttr.InverseNavigation).Load();
187196
}
188197
}
189198
else if (relationshipAttr is HasManyAttribute hasManyAttr && !(relationshipAttr is HasManyThroughAttribute))
190199
{
191-
foreach (IIdentifiable relationshipValue in (IList)trackedRelationshipValue)
200+
foreach (IIdentifiable relationshipValue in (IList)trackedRelationshipValue)
192201
{
193202
_context.Entry(relationshipValue).Reference(hasManyAttr.InverseNavigation).Load();
194203
}
@@ -199,13 +208,24 @@ private void LoadInverseRelationships(object trackedRelationshipValue, Relations
199208
private bool IsHasOneRelationship(string internalRelationshipName, Type type)
200209
{
201210
var relationshipAttr = _jsonApiContext.ResourceGraph.GetContextEntity(type).Relationships.SingleOrDefault(r => r.InternalRelationshipName == internalRelationshipName);
211+
<<<<<<< HEAD
202212
if(relationshipAttr != null)
203213
{
204214
if (relationshipAttr is HasOneAttribute) return true;
205215
return false;
206216
} else
207217
{
208218
// relationshipAttr is null when we don't put a [RelationshipAttribute] on the inverse navigation property.
219+
=======
220+
if (relationshipAttr != null)
221+
{
222+
if (relationshipAttr is HasOneAttribute) return true;
223+
return false;
224+
}
225+
else
226+
{
227+
// relationshipAttr is null when there is not put a [RelationshipAttribute] on the inverse navigation property.
228+
>>>>>>> master
209229
// In this case we use relfection to figure out what kind of relationship is pointing back.
210230
return !(type.GetProperty(internalRelationshipName).PropertyType.Inherits(typeof(IEnumerable)));
211231
}
@@ -255,31 +275,39 @@ public virtual async Task<TEntity> UpdateAsync(TId id, TEntity updatedEntity)
255275
/// <inheritdoc />
256276
public virtual async Task<TEntity> UpdateAsync(TEntity updatedEntity)
257277
{
258-
var oldEntity = await GetAsync(updatedEntity.Id);
259-
if (oldEntity == null)
278+
var databaseEntity = await GetAsync(updatedEntity.Id);
279+
if (databaseEntity == null)
260280
return null;
261281

262282
foreach (var attr in _jsonApiContext.AttributesToUpdate.Keys)
263-
attr.SetValue(oldEntity, attr.GetValue(updatedEntity));
283+
attr.SetValue(databaseEntity, attr.GetValue(updatedEntity));
264284

265285
foreach (var relationshipAttr in _jsonApiContext.RelationshipsToUpdate?.Keys)
266286
{
267-
LoadCurrentRelationships(oldEntity, relationshipAttr);
268-
var trackedRelationshipValue = GetTrackedRelationshipValue(relationshipAttr, updatedEntity, out bool wasAlreadyTracked);
287+
/// loads databasePerson.todoItems
288+
LoadCurrentRelationships(databaseEntity, relationshipAttr);
289+
/// trackedRelationshipValue is either equal to updatedPerson.todoItems
290+
/// or replaced with the same set of todoItems from the EF Core change tracker,
291+
/// if they were already tracked
292+
object trackedRelationshipValue = GetTrackedRelationshipValue(relationshipAttr, updatedEntity, out bool wasAlreadyTracked);
293+
/// loads into the db context any persons currently related
294+
/// to the todoItems in trackedRelationshipValue
269295
LoadInverseRelationships(trackedRelationshipValue, relationshipAttr);
270-
AssignRelationshipValue(oldEntity, trackedRelationshipValue, relationshipAttr);
296+
/// assigns the updated relationship to the database entity
297+
AssignRelationshipValue(databaseEntity, trackedRelationshipValue, relationshipAttr);
271298
}
272299

273300
await _context.SaveChangesAsync();
274-
return oldEntity;
301+
return databaseEntity;
275302
}
276303

277304

278305
/// <summary>
279306
/// Responsible for getting the relationship value for a given relationship
280307
/// attribute of a given entity. It ensures that the relationship value
281308
/// that it returns is attached to the database without reattaching duplicates instances
282-
/// to the change tracker.
309+
/// to the change tracker. It does so by checking if there already are
310+
/// instances of the to-be-attached entities in the change tracker.
283311
/// </summary>
284312
private object GetTrackedRelationshipValue(RelationshipAttribute relationshipAttr, TEntity entity, out bool wasAlreadyAttached)
285313
{
@@ -445,9 +473,16 @@ public async Task<IReadOnlyList<TEntity>> ToListAsync(IQueryable<TEntity> entiti
445473

446474
/// <summary>
447475
/// Before assigning new relationship values (UpdateAsync), we need to
448-
/// attach the current relationship state to the dbcontext, else
476+
/// attach the current database values of the relationship to the dbcontext, else
449477
/// it will not perform a complete-replace which is required for
450478
/// one-to-many and many-to-many.
479+
/// <para />
480+
/// For example: a person `p1` has 2 todoitems: `t1` and `t2`.
481+
/// If we want to update this todoitem set to `t3` and `t4`, simply assigning
482+
/// `p1.todoItems = [t3, t4]` will result in EF Core adding them to the set,
483+
/// resulting in `[t1 ... t4]`. Instead, we should first include `[t1, t2]`,
484+
/// after which the reassignment `p1.todoItems = [t3, t4]` will actually
485+
/// make EF Core perform a complete replace. This method does the loading of `[t1, t2]`.
451486
/// </summary>
452487
protected void LoadCurrentRelationships(TEntity oldEntity, RelationshipAttribute relationshipAttribute)
453488
{
@@ -463,21 +498,18 @@ protected void LoadCurrentRelationships(TEntity oldEntity, RelationshipAttribute
463498
}
464499

465500
/// <summary>
466-
/// assigns relationships that were set in the request to the target entity of the request
467-
/// todo: partially remove dependency on IJsonApiContext here: it is fine to
468-
/// retrieve from the context WHICH relationships to update, but the actual
469-
/// values should not come from the context.
501+
/// Assigns the <paramref name="relationshipValue"/> to <paramref name="targetEntity"/>
470502
/// </summary>
471-
private void AssignRelationshipValue(TEntity oldEntity, object relationshipValue, RelationshipAttribute relationshipAttribute)
503+
private void AssignRelationshipValue(TEntity targetEntity, object relationshipValue, RelationshipAttribute relationshipAttribute)
472504
{
473505
if (relationshipAttribute is HasManyThroughAttribute throughAttribute)
474506
{
475507
// todo: this logic should be put in the HasManyThrough attribute
476-
AssignHasManyThrough(oldEntity, throughAttribute, (IList)relationshipValue);
508+
AssignHasManyThrough(targetEntity, throughAttribute, (IList)relationshipValue);
477509
}
478510
else
479511
{
480-
relationshipAttribute.SetValue(oldEntity, relationshipValue);
512+
relationshipAttribute.SetValue(targetEntity, relationshipValue);
481513
}
482514
}
483515

src/JsonApiDotNetCore/Extensions/TypeExtensions.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ namespace JsonApiDotNetCore.Extensions
88
{
99
internal static class TypeExtensions
1010
{
11+
12+
/// <summary>
13+
/// Extension to use the LINQ AddRange method on an IList
14+
/// </summary>
1115
public static void AddRange<T>(this IList list, IEnumerable<T> items)
1216
{
1317
if (list == null) throw new ArgumentNullException(nameof(list));
@@ -25,7 +29,14 @@ public static void AddRange<T>(this IList list, IEnumerable<T> items)
2529
}
2630
}
2731
}
28-
32+
33+
/// <summary>
34+
/// Extension to use the LINQ cast method in a non-generic way:
35+
/// <code>
36+
/// Type targetType = typeof(TEntity)
37+
/// ((IList)myList).Cast(targetType).
38+
/// </code>
39+
/// </summary>
2940
public static IEnumerable Cast(this IEnumerable source, Type type)
3041
{
3142
if (source == null) throw new ArgumentNullException(nameof(source));

src/JsonApiDotNetCore/Formatters/JsonApiReader.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ public Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
8383
}
8484
}
8585

86+
/// <summary> Checks if the deserialized payload has an ID included </summary
8687
private bool CheckForId(object model)
8788
{
8889
if (model == null) return false;
@@ -97,6 +98,7 @@ private bool CheckForId(object model)
9798
return false;
9899
}
99100

101+
/// <summary> Checks if the elements in the deserialized payload have an ID included </summary
100102
private bool CheckForId(IList modelList)
101103
{
102104
foreach (var model in modelList)

src/JsonApiDotNetCore/Internal/InverseRelationships.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,14 @@ namespace JsonApiDotNetCore.Internal
1818
/// </summary>
1919
public interface IInverseRelationships
2020
{
21+
/// <summary>
22+
/// This method is called upon startup by JsonApiDotNetCore. It should
23+
/// deal with resolving the inverse relationships.
24+
/// </summary>
2125
void Resolve();
2226
}
2327

28+
/// <inheritdoc />
2429
public class InverseRelationships : IInverseRelationships
2530
{
2631
private readonly ResourceGraph _graph;
@@ -32,6 +37,7 @@ public InverseRelationships(IResourceGraph graph, IDbContextResolver resolver =
3237
_resolver = resolver;
3338
}
3439

40+
/// <inheritdoc />
3541
public void Resolve()
3642
{
3743
if (EntityFrameworkCoreIsEnabled())

src/JsonApiDotNetCore/Internal/TypeHelper.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ public static object ConvertType(object value, Type type)
4040
if (type == typeof(DateTimeOffset))
4141
return DateTimeOffset.Parse(stringValue);
4242

43+
if(type == typeof(TimeSpan))
44+
return TimeSpan.Parse(stringValue);
45+
4346
if (type.GetTypeInfo().IsEnum)
4447
return Enum.Parse(type, stringValue);
4548

src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ private object SetEntityAttributes(
159159
/// through the decoupling IJsonApiContext), we now no longer need to
160160
/// store the updated relationship values in this property. For now
161161
/// just assigning null as value, will remove this property later as a whole.
162+
/// see #512
162163
_jsonApiContext.AttributesToUpdate[attr] = null;
163164
}
164165
}
@@ -224,14 +225,16 @@ private object SetHasOneRelationship(object entity,
224225
SetHasOneNavigationPropertyValue(entity, attr, rio, included);
225226

226227
// recursive call ...
227-
if(included != null)
228+
if (included != null)
228229
{
229230
var navigationPropertyValue = attr.GetValue(entity);
231+
230232
var resourceGraphEntity = _jsonApiContext.ResourceGraph.GetContextEntity(attr.DependentType);
231233
if(navigationPropertyValue != null && resourceGraphEntity != null)
234+
232235
{
233236
var includedResource = included.SingleOrDefault(r => r.Type == rio.Type && r.Id == rio.Id);
234-
if(includedResource != null)
237+
if (includedResource != null)
235238
SetRelationships(navigationPropertyValue, resourceGraphEntity, includedResource.Relationships, included);
236239
}
237240
}
@@ -257,7 +260,8 @@ private void SetHasOneForeignKeyValue(object entity, HasOneAttribute hasOneAttr,
257260
/// through the decoupling IJsonApiContext), we now no longer need to
258261
/// store the updated relationship values in this property. For now
259262
/// just assigning null as value, will remove this property later as a whole.
260-
if (convertedValue == null ) _jsonApiContext.HasOneRelationshipPointers.Add(hasOneAttr, null);
263+
/// see #512
264+
if (convertedValue == null) _jsonApiContext.HasOneRelationshipPointers.Add(hasOneAttr, null);
261265
}
262266
}
263267

@@ -281,6 +285,7 @@ private void SetHasOneNavigationPropertyValue(object entity, HasOneAttribute has
281285
/// through the decoupling IJsonApiContext), we now no longer need to
282286
/// store the updated relationship values in this property. For now
283287
/// just assigning null as value, will remove this property later as a whole.
288+
/// see #512
284289
_jsonApiContext.HasOneRelationshipPointers.Add(hasOneAttr, null);
285290
}
286291
}
@@ -296,7 +301,7 @@ private object SetHasManyRelationship(object entity,
296301

297302
if (relationships.TryGetValue(relationshipName, out RelationshipData relationshipData))
298303
{
299-
if(relationshipData.IsHasMany == false || relationshipData.ManyData == null)
304+
if (relationshipData.IsHasMany == false || relationshipData.ManyData == null)
300305
return entity;
301306

302307
var relatedResources = relationshipData.ManyData.Select(r =>
@@ -312,7 +317,8 @@ private object SetHasManyRelationship(object entity,
312317
/// through the decoupling IJsonApiContext), we now no longer need to
313318
/// store the updated relationship values in this property. For now
314319
/// just assigning null as value, will remove this property later as a whole.
315-
_jsonApiContext.HasManyRelationshipPointers.Add(attr, null);
320+
/// see #512
321+
_jsonApiContext.HasManyRelationshipPointers.Add(attr, null);
316322
}
317323

318324
return entity;

test/UnitTests/Internal/TypeHelper_Tests.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,30 @@ public void ConvertType_Returns_Default_Value_For_Empty_Strings()
106106
Assert.Equal(t.Value, result);
107107
}
108108
}
109+
110+
[Fact]
111+
public void Can_Convert_TimeSpans()
112+
{
113+
//arrange
114+
TimeSpan timeSpan = TimeSpan.FromMinutes(45);
115+
string stringSpan = timeSpan.ToString();
116+
117+
//act
118+
var result = TypeHelper.ConvertType(stringSpan, typeof(TimeSpan));
119+
120+
//assert
121+
Assert.Equal(timeSpan, result);
122+
}
123+
124+
[Fact]
125+
public void Bad_TimeSpanString_Throws()
126+
{
127+
// arrange
128+
var formattedString = "this_is_not_a_valid_timespan";
129+
130+
// act/assert
131+
Assert.Throws<FormatException>(() => TypeHelper.ConvertType(formattedString, typeof(TimeSpan)));
132+
}
109133

110134
private enum TestEnum
111135
{

0 commit comments

Comments
 (0)