Skip to content

Feat/#494 #496

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 12 commits into from
Apr 25, 2019
Merged
11 changes: 11 additions & 0 deletions src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)

modelBuilder.Entity<ArticleTag>()
.HasKey(bc => new { bc.ArticleId, bc.TagId });


modelBuilder.Entity<TodoItem>()
.HasOne(t => t.DependentTodoItem);

modelBuilder.Entity<TodoItem>()
.HasMany(t => t.ChildrenTodoItems)
.WithOne(t => t.ParentTodoItem)
.HasForeignKey(t => t.ParentTodoItemId);


}

public DbSet<TodoItem> TodoItems { get; set; }
Expand Down
17 changes: 16 additions & 1 deletion src/Examples/JsonApiDotNetCoreExample/Models/TodoItem.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using JsonApiDotNetCore.Models;

namespace JsonApiDotNetCoreExample.Models
Expand Down Expand Up @@ -30,7 +31,7 @@ public TodoItem()
public DateTime? UpdatedDate { get; set; }



public int? OwnerId { get; set; }
public int? AssigneeId { get; set; }
public Guid? CollectionId { get; set; }
Expand All @@ -43,5 +44,19 @@ public TodoItem()

[HasOne("collection")]
public virtual TodoItemCollection Collection { get; set; }

public virtual int? DependentTodoItemId { get; set; }
[HasOne("dependent-on-todo")]
public virtual TodoItem DependentTodoItem { get; set; }




// cyclical structure
public virtual int? ParentTodoItemId {get; set;}
[HasOne("parent-todo")]
public virtual TodoItem ParentTodoItem { get; set; }
[HasMany("children-todos")]
public virtual List<TodoItem> ChildrenTodoItems { get; set; }
}
}
62 changes: 56 additions & 6 deletions src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -225,13 +225,15 @@ private void AttachHasMany(TEntity entity, HasManyAttribute relationship, IList
var relatedList = (IList)entity.GetType().GetProperty(relationship.EntityPropertyName)?.GetValue(entity);
foreach (var related in relatedList)
{
_context.Entry(related).State = EntityState.Unchanged;
if (_context.EntityIsTracked(related as IIdentifiable) == false)
_context.Entry(related).State = EntityState.Unchanged;
}
}
else
{
foreach (var pointer in pointers)
{
if (_context.EntityIsTracked(pointer as IIdentifiable) == false)
_context.Entry(pointer).State = EntityState.Unchanged;
}
}
Expand Down Expand Up @@ -298,21 +300,69 @@ public virtual async Task<TEntity> UpdateAsync(TId id, TEntity entity)

if (_jsonApiContext.RelationshipsToUpdate.Any())
{
/// For one-to-many and many-to-many, the PATCH must perform a
/// complete replace. When assigning new relationship values,
/// it will only be like this if the to-be-replaced entities are loaded
foreach (var relationship in _jsonApiContext.RelationshipsToUpdate)
{
if (relationship.Key is HasManyThroughAttribute throughAttribute)
{
await _context.Entry(oldEntity).Collection(throughAttribute.InternalThroughName).LoadAsync();
}
}

/// @HACK @TODO: It is inconsistent that for many-to-many, the new relationship value
/// is assigned in AttachRelationships() helper fn below, but not for
/// one-to-many and one-to-one (we need to do that manually as done below).
/// Simultaneously, for a proper working "complete replacement", in the case of many-to-many
/// we need to LoadAsync() BEFORE calling AttachRelationships(), but for one-to-many we
/// need to do it AFTER AttachRelationships or we we'll get entity tracking errors
/// This really needs a refactor.
AttachRelationships(oldEntity);

foreach (var relationship in _jsonApiContext.RelationshipsToUpdate)
{
/// If we are updating to-many relations from PATCH, we need to include the relation first,
/// else it will not peform a complete replacement, as required by the specs.
/// Also, we currently do not support the same for many-to-many
if (relationship.Key is HasManyAttribute && !(relationship.Key is HasManyThroughAttribute))

if ((relationship.Key.TypeId as Type).IsAssignableFrom(typeof(HasOneAttribute)))
{
relationship.Key.SetValue(oldEntity, relationship.Value);
}
if ((relationship.Key.TypeId as Type).IsAssignableFrom(typeof(HasManyAttribute)))
{
await _context.Entry(oldEntity).Collection(relationship.Key.InternalRelationshipName).LoadAsync();
relationship.Key.SetValue(oldEntity, relationship.Value); // article.tags = nieuwe lijst
var value = CheckForSelfReferingUpdate((IEnumerable<object>)relationship.Value, oldEntity);
relationship.Key.SetValue(oldEntity, value);
}
}
}
await _context.SaveChangesAsync();
return oldEntity;
}

object CheckForSelfReferingUpdate(IEnumerable<object> relatedEntities, TEntity oldEntity)
{
var entity = relatedEntities.FirstOrDefault();
var list = new List<TEntity>();
bool refersSelf = false;
if (entity?.GetType() == typeof(TEntity))
{
foreach (TEntity e in relatedEntities)
{
if (oldEntity.StringId == e.StringId)
{
list.Add(oldEntity);
refersSelf = true;
}
else
{
list.Add(e);
}
}
}
return (refersSelf ? list : relatedEntities);

}

/// <inheritdoc />
public async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable<string> relationshipIds)
{
Expand Down
122 changes: 122 additions & 0 deletions test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,128 @@ public async Task Can_Update_Many_To_Many()
Assert.Equal(tag.Id, persistedArticleTag.TagId);
}

[Fact]
public async Task Can_Update_Many_To_Many_With_Complete_Replacement()
{
// arrange
var context = _fixture.GetService<AppDbContext>();
var firstTag = _tagFaker.Generate();
var article = _articleFaker.Generate();
var articleTag = new ArticleTag
{
Article = article,
Tag = firstTag
};
context.ArticleTags.Add(articleTag);
var secondTag = _tagFaker.Generate();
context.Tags.Add(secondTag);
await context.SaveChangesAsync();

var route = $"/api/v1/articles/{article.Id}";
var request = new HttpRequestMessage(new HttpMethod("PATCH"), route);
var content = new
{
data = new
{
type = "articles",
id = article.StringId,
relationships = new Dictionary<string, dynamic>
{
{ "tags", new {
data = new [] { new
{
type = "tags",
id = secondTag.StringId
} }
} }
}
}
};

request.Content = new StringContent(JsonConvert.SerializeObject(content));
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json");

// act
var response = await _fixture.Client.SendAsync(request);

// assert
var body = await response.Content.ReadAsStringAsync();
Assert.True(HttpStatusCode.OK == response.StatusCode, $"{route} returned {response.StatusCode} status code with payload: {body}");

var articleResponse = _fixture.GetService<IJsonApiDeSerializer>().Deserialize<Article>(body);
Assert.NotNull(articleResponse);

_fixture.ReloadDbContext();
var persistedArticle = await _fixture.Context.Articles
.Include("ArticleTags.Tag")
.SingleOrDefaultAsync(a => a.Id == article.Id);
var tag = persistedArticle.ArticleTags.Select(at => at.Tag).Single();
Assert.Equal(secondTag.Id, tag.Id);
}

[Fact]
public async Task Can_Update_Many_To_Many_With_Complete_Replacement_With_Overlap()
{
// arrange
var context = _fixture.GetService<AppDbContext>();
var firstTag = _tagFaker.Generate();
var article = _articleFaker.Generate();
var articleTag = new ArticleTag
{
Article = article,
Tag = firstTag
};
context.ArticleTags.Add(articleTag);
var secondTag = _tagFaker.Generate();
context.Tags.Add(secondTag);
await context.SaveChangesAsync();

var route = $"/api/v1/articles/{article.Id}";
var request = new HttpRequestMessage(new HttpMethod("PATCH"), route);
var content = new
{
data = new
{
type = "articles",
id = article.StringId,
relationships = new Dictionary<string, dynamic>
{
{ "tags", new {
data = new [] { new
{
type = "tags",
id = firstTag.StringId
}, new
{
type = "tags",
id = secondTag.StringId
} }
} }
}
}
};

request.Content = new StringContent(JsonConvert.SerializeObject(content));
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json");

// act
var response = await _fixture.Client.SendAsync(request);

// assert
var body = await response.Content.ReadAsStringAsync();
Assert.True(HttpStatusCode.OK == response.StatusCode, $"{route} returned {response.StatusCode} status code with payload: {body}");

var articleResponse = _fixture.GetService<IJsonApiDeSerializer>().Deserialize<Article>(body);
Assert.NotNull(articleResponse);

_fixture.ReloadDbContext();
var persistedArticle = await _fixture.Context.Articles
.Include(a => a.ArticleTags)
.SingleOrDefaultAsync( a => a.Id == article.Id);
var tags = persistedArticle.ArticleTags.Select(at => at.Tag).ToList();
Assert.Equal(2, tags.Count);
}

[Fact]
public async Task Can_Update_Many_To_Many_Through_Relationship_Link()
{
Expand Down
Loading