Skip to content

DefaultEntityRepository cleanup #518

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 30 commits into from
Jun 17, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
5f3e04b
refactor: AttachHasOnePointers
maurei Jun 7, 2019
e8d1096
refactor: AssignHasMany pointers
maurei Jun 7, 2019
1420d6d
refactor assign relationships and applied to updateasync method
maurei Jun 7, 2019
24dd97c
refactor: tests passing again
maurei Jun 7, 2019
0304725
test: add implicit removal test
maurei Jun 7, 2019
f60587f
test: all passing
maurei Jun 7, 2019
4cd9465
chore: addedd notes wrt implicit remove
maurei Jun 7, 2019
d1f39e6
comments: added comment to assign method
maurei Jun 7, 2019
bcbf8c3
Merge branch 'master' into fix/reattachment
maurei Jun 7, 2019
7df5233
fix: support for entity resource split
maurei Jun 7, 2019
0296eb4
fix: minor refactor, comments
maurei Jun 8, 2019
f0d5924
fix: foreignkey set null bug
maurei Jun 8, 2019
9f7550c
feat: decoupled repository from JsonApiContext with respect to updati…
maurei Jun 11, 2019
7aea60c
feat: decoupled IJsonApiContext from repository wrt updating resources
maurei Jun 11, 2019
652d65f
fix: resource separation issue|
maurei Jun 11, 2019
9838627
chore: cherry picked inverse relationships from hooks branch
maurei Jun 11, 2019
fbe69fc
fix: tests
maurei Jun 11, 2019
35a2f54
feat: implicit remove support
maurei Jun 11, 2019
6e6f7fa
fix: test
maurei Jun 11, 2019
c1d472d
fix: bugs with inverse relationship loading
maurei Jun 11, 2019
f45972f
tests: implicit remove through create tests
maurei Jun 11, 2019
d8b4217
feat: mark obsolete UpdateAsync(TId id, TEntity entity) method, add n…
maurei Jun 11, 2019
30765c3
fix: #520
maurei Jun 11, 2019
9139852
fix: separation tests
maurei Jun 11, 2019
457e93d
chore: comments
maurei Jun 12, 2019
65f8a3a
Update DefaultEntityRepository.cs
maurei Jun 12, 2019
3ddb6a2
Update DefaultEntityRepository.cs
maurei Jun 17, 2019
2437077
Update TypeExtensions.cs
maurei Jun 17, 2019
415306e
Update DefaultEntityRepository.cs
maurei Jun 17, 2019
52a452d
Update JsonApiReader.cs
maurei Jun 17, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 52 additions & 22 deletions src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ public DefaultEntityRepository(
_resourceDefinition = resourceDefinition;
}

public DefaultEntityRepository(
public
DefaultEntityRepository(
ILoggerFactory loggerFactory,
IJsonApiContext jsonApiContext,
IDbContextResolver contextResolver,
Expand Down Expand Up @@ -171,6 +172,13 @@ public virtual async Task<TEntity> CreateAsync(TEntity entity)
/// <summary>
/// Loads the inverse relationships to prevent foreign key constraints from being violated
/// to support implicit removes, see https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/502.
///
/// example:
/// person.todoItems = [t1,t2] is updated to [t3, t4]. If t3, and/or t4 was
/// already related to a other person, and these persons are NOT loaded in to the
/// db context, then the query may cause a foreign key constraint. Loading
/// these "inverse relationships" into the DB context ensures EF core to take
/// this into account.
/// </summary>
private void LoadInverseRelationships(object trackedRelationshipValue, RelationshipAttribute relationshipAttr)
{
Expand All @@ -181,14 +189,15 @@ private void LoadInverseRelationships(object trackedRelationshipValue, Relations
if (IsHasOneRelationship(hasOneAttr.InverseNavigation, trackedRelationshipValue.GetType()))
{
relationEntry.Reference(hasOneAttr.InverseNavigation).Load();
} else
}
else
{
relationEntry.Collection(hasOneAttr.InverseNavigation).Load();
}
}
else if (relationshipAttr is HasManyAttribute hasManyAttr && !(relationshipAttr is HasManyThroughAttribute))
{
foreach (IIdentifiable relationshipValue in (IList)trackedRelationshipValue)
foreach (IIdentifiable relationshipValue in (IList)trackedRelationshipValue)
{
_context.Entry(relationshipValue).Reference(hasManyAttr.InverseNavigation).Load();
}
Expand All @@ -198,9 +207,18 @@ private void LoadInverseRelationships(object trackedRelationshipValue, Relations

private bool IsHasOneRelationship(string internalRelationshipName, Type type)
{
var relationshipAttr = _jsonApiContext.ResourceGraph.GetContextEntity(type).Relationships.Single(r => r.InternalRelationshipName == internalRelationshipName);
if (relationshipAttr is HasOneAttribute) return true;
return false;
var relationshipAttr = _jsonApiContext.ResourceGraph.GetContextEntity(type).Relationships.SingleOrDefault(r => r.InternalRelationshipName == internalRelationshipName);
if (relationshipAttr != null)
{
if (relationshipAttr is HasOneAttribute) return true;
return false;
}
else
{
// relationshipAttr is null when there is not put a [RelationshipAttribute] on the inverse navigation property.
// In this case we use relfection to figure out what kind of relationship is pointing back.
return !(type.GetProperty(internalRelationshipName).PropertyType.Inherits(typeof(IEnumerable)));
}
}


Expand Down Expand Up @@ -247,31 +265,39 @@ public virtual async Task<TEntity> UpdateAsync(TId id, TEntity updatedEntity)
/// <inheritdoc />
public virtual async Task<TEntity> UpdateAsync(TEntity updatedEntity)
{
var oldEntity = await GetAsync(updatedEntity.Id);
if (oldEntity == null)
var databaseEntity = await GetAsync(updatedEntity.Id);
if (databaseEntity == null)
return null;

foreach (var attr in _jsonApiContext.AttributesToUpdate.Keys)
attr.SetValue(oldEntity, attr.GetValue(updatedEntity));
attr.SetValue(databaseEntity, attr.GetValue(updatedEntity));

foreach (var relationshipAttr in _jsonApiContext.RelationshipsToUpdate?.Keys)
{
LoadCurrentRelationships(oldEntity, relationshipAttr);
var trackedRelationshipValue = GetTrackedRelationshipValue(relationshipAttr, updatedEntity, out bool wasAlreadyTracked);
/// loads databasePerson.todoItems
LoadCurrentRelationships(databaseEntity, relationshipAttr);
/// trackedRelationshipValue is either equal to updatedPerson.todoItems
/// or replaced with the same set of todoItems from the EF Core change tracker,
/// if they were already tracked
object trackedRelationshipValue = GetTrackedRelationshipValue(relationshipAttr, updatedEntity, out bool wasAlreadyTracked);
/// loads into the db context any persons currently related
/// to the todoItems in trackedRelationshipValue
LoadInverseRelationships(trackedRelationshipValue, relationshipAttr);
AssignRelationshipValue(oldEntity, trackedRelationshipValue, relationshipAttr);
/// assigns the updated relationship to the database entity
AssignRelationshipValue(databaseEntity, trackedRelationshipValue, relationshipAttr);
}

await _context.SaveChangesAsync();
return oldEntity;
return databaseEntity;
}


/// <summary>
/// Responsible for getting the relationship value for a given relationship
/// attribute of a given entity. It ensures that the relationship value
/// that it returns is attached to the database without reattaching duplicates instances
/// to the change tracker.
/// to the change tracker. It does so by checking if there already are
/// instances of the to-be-attached entities in the change tracker.
/// </summary>
private object GetTrackedRelationshipValue(RelationshipAttribute relationshipAttr, TEntity entity, out bool wasAlreadyAttached)
{
Expand Down Expand Up @@ -436,9 +462,16 @@ public async Task<IReadOnlyList<TEntity>> ToListAsync(IQueryable<TEntity> entiti

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

/// <summary>
/// assigns relationships that were set in the request to the target entity of the request
/// todo: partially remove dependency on IJsonApiContext here: it is fine to
/// retrieve from the context WHICH relationships to update, but the actual
/// values should not come from the context.
/// Assigns the <paramref name="relationshipValue"/> to <paramref name="targetEntity"/>
/// </summary>
private void AssignRelationshipValue(TEntity oldEntity, object relationshipValue, RelationshipAttribute relationshipAttribute)
private void AssignRelationshipValue(TEntity targetEntity, object relationshipValue, RelationshipAttribute relationshipAttribute)
{
if (relationshipAttribute is HasManyThroughAttribute throughAttribute)
{
// todo: this logic should be put in the HasManyThrough attribute
AssignHasManyThrough(oldEntity, throughAttribute, (IList)relationshipValue);
AssignHasManyThrough(targetEntity, throughAttribute, (IList)relationshipValue);
}
else
{
relationshipAttribute.SetValue(oldEntity, relationshipValue);
relationshipAttribute.SetValue(targetEntity, relationshipValue);
}
}

Expand Down
26 changes: 26 additions & 0 deletions src/JsonApiDotNetCore/Extensions/TypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,32 @@ namespace JsonApiDotNetCore.Extensions
{
internal static class TypeExtensions
{
public static void AddRange<T>(this IList list, IEnumerable<T> items)
{
if (list == null) throw new ArgumentNullException(nameof(list));
if (items == null) throw new ArgumentNullException(nameof(items));

if (list is List<T>)
{
((List<T>)list).AddRange(items);
}
else
{
foreach (var item in items)
{
list.Add(item);
}
}
}


/// <summary>
/// Extension to use the LINQ cast method in a non-generic way:
/// <code>
/// Type targetType = typeof(TEntity)
/// ((IList)myList).Cast(targetType).
/// </code>
/// </summary>
public static IEnumerable Cast(this IEnumerable source, Type type)
{
if (source == null) throw new ArgumentNullException(nameof(source));
Expand Down
6 changes: 6 additions & 0 deletions src/JsonApiDotNetCore/Internal/InverseRelationships.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,14 @@ namespace JsonApiDotNetCore.Internal
/// </summary>
public interface IInverseRelationships
{
/// <summary>
/// This method is called upon startup by JsonApiDotNetCore. It should
/// deal with resolving the inverse relationships.
/// </summary>
void Resolve();
}

/// <inheritdoc />
public class InverseRelationships : IInverseRelationships
{
private readonly ResourceGraph _graph;
Expand All @@ -32,6 +37,7 @@ public InverseRelationships(IResourceGraph graph, IDbContextResolver resolver =
_resolver = resolver;
}

/// <inheritdoc />
public void Resolve()
{
if (EntityFrameworkCoreIsEnabled())
Expand Down
1 change: 0 additions & 1 deletion src/JsonApiDotNetCore/Models/RelationshipAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ protected RelationshipAttribute(string publicName, Link documentLinks, bool canI

public string PublicRelationshipName { get; internal set; }
public string InternalRelationshipName { get; internal set; }

public string InverseNavigation { get; internal set; }

/// <summary>
Expand Down
16 changes: 10 additions & 6 deletions src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ private object SetEntityAttributes(
/// through the decoupling IJsonApiContext), we now no longer need to
/// store the updated relationship values in this property. For now
/// just assigning null as value, will remove this property later as a whole.
/// see #512
_jsonApiContext.AttributesToUpdate[attr] = null;
}
}
Expand Down Expand Up @@ -224,14 +225,14 @@ private object SetHasOneRelationship(object entity,
SetHasOneNavigationPropertyValue(entity, attr, rio, included);

// recursive call ...
if(included != null)
if (included != null)
{
var navigationPropertyValue = attr.GetValue(entity);
var resourceGraphEntity = _jsonApiContext.ResourceGraph.GetContextEntity(attr.Type);
if(navigationPropertyValue != null && resourceGraphEntity != null)
if (navigationPropertyValue != null && resourceGraphEntity != null)
{
var includedResource = included.SingleOrDefault(r => r.Type == rio.Type && r.Id == rio.Id);
if(includedResource != null)
if (includedResource != null)
SetRelationships(navigationPropertyValue, resourceGraphEntity, includedResource.Relationships, included);
}
}
Expand All @@ -257,7 +258,8 @@ private void SetHasOneForeignKeyValue(object entity, HasOneAttribute hasOneAttr,
/// through the decoupling IJsonApiContext), we now no longer need to
/// store the updated relationship values in this property. For now
/// just assigning null as value, will remove this property later as a whole.
if (convertedValue == null ) _jsonApiContext.HasOneRelationshipPointers.Add(hasOneAttr, null);
/// see #512
if (convertedValue == null) _jsonApiContext.HasOneRelationshipPointers.Add(hasOneAttr, null);
}
}

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

if (relationships.TryGetValue(relationshipName, out RelationshipData relationshipData))
{
if(relationshipData.IsHasMany == false || relationshipData.ManyData == null)
if (relationshipData.IsHasMany == false || relationshipData.ManyData == null)
return entity;

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

return entity;
Expand Down