Skip to content

Support implicit remove when creating/updating one-to-one relationship #503

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

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 5 additions & 1 deletion src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
.WithOne(t => t.ParentTodoItem)
.HasForeignKey(t => t.ParentTodoItemId);

modelBuilder.Entity<Person>()
.HasOne(p => p.Passport)
.WithOne(p => p.Person)
.HasForeignKey<Person>(p => p.PassportId);

}

Expand All @@ -61,13 +65,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
public DbSet<Author> Authors { get; set; }
public DbSet<NonJsonApiResource> NonJsonApiResources { get; set; }
public DbSet<User> Users { get; set; }

public DbSet<CourseEntity> Courses { get; set; }
public DbSet<DepartmentEntity> Departments { get; set; }
public DbSet<CourseStudentEntity> Registrations { get; set; }
public DbSet<StudentEntity> Students { get; set; }
public DbSet<PersonRole> PersonRoles { get; set; }
public DbSet<ArticleTag> ArticleTags { get; set; }
public DbSet<Tag> Tags { get; set; }
public DbSet<Passport> Passports { get; set; }
}
}
11 changes: 11 additions & 0 deletions src/Examples/JsonApiDotNetCoreExample/Models/Passport.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using JsonApiDotNetCore.Models;

namespace JsonApiDotNetCoreExample.Models
{
public class Passport : Identifiable
{
public virtual int? SocialSecurityNumber { get; set; }
[HasOne("person")]
public virtual Person Person { get; set; }
}
}
4 changes: 4 additions & 0 deletions src/Examples/JsonApiDotNetCoreExample/Models/Person.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ public class Person : Identifiable, IHasMeta
[HasOne("unincludeable-item", documentLinks: Link.All, canInclude: false)]
public virtual TodoItem UnIncludeableItem { get; set; }

public int? PassportId { get; set; }
[HasOne("passport")]
public virtual Passport Passport { get; set; }

public Dictionary<string, object> GetMeta(IJsonApiContext context)
{
return new Dictionary<string, object> {
Expand Down
11 changes: 11 additions & 0 deletions src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using JsonApiDotNetCore.Extensions;
using JsonApiDotNetCore.Internal;
Expand All @@ -10,6 +11,7 @@
using JsonApiDotNetCore.Models;
using JsonApiDotNetCore.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.Extensions.Logging;

namespace JsonApiDotNetCore.Data
Expand Down Expand Up @@ -338,6 +340,15 @@ public virtual async Task<TEntity> UpdateAsync(TId id, TEntity entity)
{
if ((relationship.Key.TypeId as Type).IsAssignableFrom(typeof(HasOneAttribute)))
{
var attr = relationship.Key;
if (_jsonApiContext.HasOneRelationshipPointers.Get().TryGetValue(attr, out var pointer) )
{
/// we need to attach inverse relations to make sure
/// we're not violating any foreign key constraints
/// when implicitly removing pre-existing relations.
/// See #502 for more info.
_context.LoadInverseNavigation<TEntity>(attr, pointer);
}
relationship.Key.SetValue(oldEntity, relationship.Value);
}
if ((relationship.Key.TypeId as Type).IsAssignableFrom(typeof(HasManyAttribute)))
Expand Down
33 changes: 33 additions & 0 deletions src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,39 @@ public static class DbContextExtensions
public static DbSet<T> GetDbSet<T>(this DbContext context) where T : class
=> context.Set<T>();

/// <summary>
/// Given a child entity and a relationship attribute between a parent
/// entity to that child entity, attaches the entities on the inverse navigation
/// property to the dbContext.
/// </summary>
/// <param name="context">Context.</param>
/// <param name="parentToChildAttribute">Parent to child relationship attribute.</param>
/// <param name="childEntity">Child entity.</param>
/// <typeparam name="TParent">The 1st type parameter.</typeparam>
public static void LoadInverseNavigation<TParent>(
this DbContext context,
RelationshipAttribute parentToChildAttribute,
object childEntity) where TParent : class, IIdentifiable
{
var navigationMeta = context.Model
.FindEntityType(typeof(TParent))
.FindNavigation(parentToChildAttribute.InternalRelationshipName);
var inverseNavigationMeta = navigationMeta.FindInverse();
if (inverseNavigationMeta != null)
{
var inversePropertyType = inverseNavigationMeta.PropertyInfo.PropertyType;
var inversePropertyName = inverseNavigationMeta.Name;
var entityEntry = context.Entry(childEntity);
if (inversePropertyType.IsGenericType )
{ // if generic, means we're dealing with a list
entityEntry.Collection(inversePropertyName).Load();
} else
{
entityEntry.Navigation(inversePropertyName).Load();
}
}
}

/// <summary>
/// Get the DbSet when the model type is unknown until runtime
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -621,5 +621,57 @@ public async Task Can_Delete_Relationship_By_Patching_Relationship()
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Null(todoItemResult.Owner);
}

[Fact]
public async Task Updating_ToOne_Relationship_With_Implicit_Remove()
{
// Arrange
var context = _fixture.GetService<AppDbContext>();
var passport = new Passport();
var person1 = _personFaker.Generate();
person1.Passport = passport;
var person2 = _personFaker.Generate();
context.People.AddRange(new List<Person>() { person1, person2 });
await context.SaveChangesAsync();

var passportId = person1.PassportId;

var content = new
{
data = new
{
type = "people",
id = person2.Id,
relationships = new Dictionary<string, object>
{
{ "passport", new
{
data = new { type = "passports", id = $"{passportId}" }
}
}
}
}
};

var httpMethod = new HttpMethod("PATCH");
var route = $"/api/v1/people/{person2.Id}";
var request = new HttpRequestMessage(httpMethod, route);

string serializedContent = JsonConvert.SerializeObject(content);
request.Content = new StringContent(serializedContent);
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 _person1 = context.People.AsNoTracking().Include(ppl => ppl.Passport).Single(ppl => ppl.Id == person1.Id);
var _person2 = context.People.AsNoTracking().Include(ppl => ppl.Passport).Single(ppl => ppl.Id == person2.Id);
Assert.Null(_person1.Passport);
Assert.Equal(passportId, _person2.PassportId);
}
}
}