diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs
index 7de24d7360..862079aeee 100644
--- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs
+++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs
@@ -13,6 +13,7 @@
namespace JsonApiDotNetCore.Data
{
+ ///
public class DefaultEntityRepository
: DefaultEntityRepository,
IEntityRepository
@@ -26,8 +27,13 @@ public DefaultEntityRepository(
{ }
}
+ ///
+ /// Provides a default repository implementation and is responsible for
+ /// abstracting any EF Core APIs away from the service layer.
+ ///
public class DefaultEntityRepository
- : IEntityRepository
+ : IEntityRepository,
+ IEntityFrameworkRepository
where TEntity : class, IIdentifiable
{
private readonly DbContext _context;
@@ -48,7 +54,7 @@ public DefaultEntityRepository(
_genericProcessorFactory = _jsonApiContext.GenericProcessorFactory;
}
- /// inheritdoc>
+ ///
public virtual IQueryable Get()
{
if (_jsonApiContext.QuerySet?.Fields != null && _jsonApiContext.QuerySet.Fields.Count > 0)
@@ -57,41 +63,43 @@ public virtual IQueryable Get()
return _dbSet;
}
- /// inheritdoc>
+ ///
public virtual IQueryable Filter(IQueryable entities, FilterQuery filterQuery)
{
return entities.Filter(_jsonApiContext, filterQuery);
}
- /// inheritdoc>
+ ///
public virtual IQueryable Sort(IQueryable entities, List sortQueries)
{
return entities.Sort(sortQueries);
}
- /// inheritdoc>
+ ///
public virtual async Task GetAsync(TId id)
{
return await Get().SingleOrDefaultAsync(e => e.Id.Equals(id));
}
- /// inheritdoc>
+ ///
public virtual async Task GetAndIncludeAsync(TId id, string relationshipName)
{
_logger.LogDebug($"[JADN] GetAndIncludeAsync({id}, {relationshipName})");
- var result = await Include(Get(), relationshipName).SingleOrDefaultAsync(e => e.Id.Equals(id));
+ var includedSet = Include(Get(), relationshipName);
+ var result = await includedSet.SingleOrDefaultAsync(e => e.Id.Equals(id));
return result;
}
- /// inheritdoc>
+ ///
public virtual async Task CreateAsync(TEntity entity)
{
AttachRelationships();
_dbSet.Add(entity);
await _context.SaveChangesAsync();
+
return entity;
}
@@ -101,6 +109,28 @@ protected virtual void AttachRelationships()
AttachHasOnePointers();
}
+ ///
+ public void DetachRelationshipPointers(TEntity entity)
+ {
+ foreach (var hasOneRelationship in _jsonApiContext.HasOneRelationshipPointers.Get())
+ {
+ _context.Entry(hasOneRelationship.Value).State = EntityState.Detached;
+ }
+
+ foreach (var hasManyRelationship in _jsonApiContext.HasManyRelationshipPointers.Get())
+ {
+ foreach (var pointer in hasManyRelationship.Value)
+ {
+ _context.Entry(pointer).State = EntityState.Detached;
+ }
+
+ // HACK: detaching has many relationships doesn't appear to be sufficient
+ // the navigation property actually needs to be nulled out, otherwise
+ // EF adds duplicate instances to the collection
+ hasManyRelationship.Key.SetValue(entity, null);
+ }
+ }
+
///
/// This is used to allow creation of HasMany relationships when the
/// dependent side of the relationship already exists.
@@ -129,7 +159,7 @@ private void AttachHasOnePointers()
_context.Entry(relationship.Value).State = EntityState.Unchanged;
}
- /// inheritdoc>
+ ///
public virtual async Task UpdateAsync(TId id, TEntity entity)
{
var oldEntity = await GetAsync(id);
@@ -148,14 +178,14 @@ public virtual async Task UpdateAsync(TId id, TEntity entity)
return oldEntity;
}
- /// inheritdoc>
+ ///
public async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable relationshipIds)
{
var genericProcessor = _genericProcessorFactory.GetProcessor(typeof(GenericProcessor<>), relationship.Type);
await genericProcessor.UpdateRelationshipsAsync(parent, relationship, relationshipIds);
}
- /// inheritdoc>
+ ///
public virtual async Task DeleteAsync(TId id)
{
var entity = await GetAsync(id);
@@ -170,7 +200,7 @@ public virtual async Task DeleteAsync(TId id)
return true;
}
- /// inheritdoc>
+ ///
public virtual IQueryable Include(IQueryable entities, string relationshipName)
{
var entity = _jsonApiContext.RequestEntity;
@@ -185,10 +215,11 @@ public virtual IQueryable Include(IQueryable entities, string
{
throw new JsonApiException(400, $"Including the relationship {relationshipName} on {entity.EntityName} is not allowed");
}
+
return entities.Include(relationship.InternalRelationshipName);
}
- /// inheritdoc>
+ ///
public virtual async Task> PageAsync(IQueryable entities, int pageSize, int pageNumber)
{
if (pageNumber >= 0)
@@ -209,7 +240,7 @@ public virtual async Task> PageAsync(IQueryable en
.ToListAsync();
}
- /// inheritdoc>
+ ///
public async Task CountAsync(IQueryable entities)
{
return (entities is IAsyncEnumerable)
@@ -217,7 +248,7 @@ public async Task CountAsync(IQueryable entities)
: entities.Count();
}
- /// inheritdoc>
+ ///
public async Task FirstOrDefaultAsync(IQueryable entities)
{
return (entities is IAsyncEnumerable)
@@ -225,7 +256,7 @@ public async Task FirstOrDefaultAsync(IQueryable entities)
: entities.FirstOrDefault();
}
- /// inheritdoc>
+ ///
public async Task> ToListAsync(IQueryable entities)
{
return (entities is IAsyncEnumerable)
diff --git a/src/JsonApiDotNetCore/Data/IEntityRepository.cs b/src/JsonApiDotNetCore/Data/IEntityRepository.cs
index e8bb68ef90..ac69f4fdac 100644
--- a/src/JsonApiDotNetCore/Data/IEntityRepository.cs
+++ b/src/JsonApiDotNetCore/Data/IEntityRepository.cs
@@ -8,8 +8,29 @@ public interface IEntityRepository
{ }
public interface IEntityRepository
- : IEntityReadRepository,
+ : IEntityReadRepository,
IEntityWriteRepository
where TEntity : class, IIdentifiable
{ }
+
+ ///
+ /// A staging interface to avoid breaking changes that
+ /// specifically depend on EntityFramework.
+ ///
+ internal interface IEntityFrameworkRepository
+ {
+ ///
+ /// Ensures that any relationship pointers created during a POST or PATCH
+ /// request are detached from the DbContext.
+ /// This allows the relationships to be fully loaded from the database.
+ ///
+ ///
+ ///
+ /// The only known case when this should be called is when a POST request is
+ /// sent with an ?include query.
+ ///
+ /// See https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/343
+ ///
+ void DetachRelationshipPointers(TEntity entity);
+ }
}
diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj
index 4af725919b..c786cd5153 100755
--- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj
+++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj
@@ -1,6 +1,6 @@
- 2.5.0
+ 2.5.1
$(NetStandardVersion)
JsonApiDotNetCore
JsonApiDotNetCore
diff --git a/src/JsonApiDotNetCore/Services/EntityResourceService.cs b/src/JsonApiDotNetCore/Services/EntityResourceService.cs
index 300c858c43..1e8dc249a2 100644
--- a/src/JsonApiDotNetCore/Services/EntityResourceService.cs
+++ b/src/JsonApiDotNetCore/Services/EntityResourceService.cs
@@ -77,6 +77,17 @@ public virtual async Task CreateAsync(TResource resource)
entity = await _entities.CreateAsync(entity);
+ // this ensures relationships get reloaded from the database if they have
+ // been requested
+ // https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/343
+ if (ShouldIncludeRelationships())
+ {
+ if(_entities is IEntityFrameworkRepository efRepository)
+ efRepository.DetachRelationshipPointers(entity);
+
+ return await GetWithRelationshipsAsync(entity.Id);
+ }
+
return MapOut(entity);
}
diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs
index e65278db12..a34312ca2f 100644
--- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs
+++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs
@@ -285,6 +285,71 @@ public async Task Can_Create_And_Set_HasMany_Relationships()
Assert.NotEmpty(contextCollection.TodoItems);
}
+ [Fact]
+ public async Task Can_Create_With_HasMany_Relationship_And_Include_Result()
+ {
+ // arrange
+ var builder = new WebHostBuilder()
+ .UseStartup();
+ var httpMethod = new HttpMethod("POST");
+ var server = new TestServer(builder);
+ var client = server.CreateClient();
+
+ var context = _fixture.GetService();
+
+ var owner = new JsonApiDotNetCoreExample.Models.Person();
+ var todoItem = new TodoItem();
+ todoItem.Owner = owner;
+ todoItem.Description = "Description";
+ context.People.Add(owner);
+ context.TodoItems.Add(todoItem);
+ await context.SaveChangesAsync();
+
+ var route = "/api/v1/todo-collections?include=todo-items";
+ var request = new HttpRequestMessage(httpMethod, route);
+ var content = new
+ {
+ data = new
+ {
+ type = "todo-collections",
+ relationships = new Dictionary
+ {
+ { "owner", new {
+ data = new
+ {
+ type = "people",
+ id = owner.Id.ToString()
+ }
+ } },
+ { "todo-items", new {
+ data = new dynamic[]
+ {
+ new {
+ type = "todo-items",
+ id = todoItem.Id.ToString()
+ }
+ }
+ } }
+ }
+ }
+ };
+
+ request.Content = new StringContent(JsonConvert.SerializeObject(content));
+ request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json");
+
+ // act
+ var response = await client.SendAsync(request);
+
+ // assert
+ Assert.Equal(HttpStatusCode.Created, response.StatusCode);
+ var body = await response.Content.ReadAsStringAsync();
+ var collectionResult = _fixture.GetService().Deserialize(body);
+
+ Assert.NotNull(collectionResult);
+ Assert.NotEmpty(collectionResult.TodoItems);
+ Assert.Equal(todoItem.Description, collectionResult.TodoItems.Single().Description);
+ }
+
[Fact]
public async Task Can_Create_And_Set_HasOne_Relationships()
{
@@ -342,6 +407,62 @@ public async Task Can_Create_And_Set_HasOne_Relationships()
Assert.Equal(owner.Id, todoItemResult.OwnerId);
}
+ [Fact]
+ public async Task Can_Create_With_HasOne_Relationship_And_Include_Result()
+ {
+ // arrange
+ var builder = new WebHostBuilder().UseStartup();
+
+ var httpMethod = new HttpMethod("POST");
+ var server = new TestServer(builder);
+ var client = server.CreateClient();
+
+ var context = _fixture.GetService();
+
+ var todoItem = new TodoItem();
+ var owner = new JsonApiDotNetCoreExample.Models.Person
+ {
+ FirstName = "Alice"
+ };
+ context.People.Add(owner);
+
+ await context.SaveChangesAsync();
+
+ var route = "/api/v1/todo-items?include=owner";
+ var request = new HttpRequestMessage(httpMethod, route);
+ var content = new
+ {
+ data = new
+ {
+ type = "todo-items",
+ relationships = new Dictionary
+ {
+ { "owner", new {
+ data = new
+ {
+ type = "people",
+ id = owner.Id.ToString()
+ }
+ } }
+ }
+ }
+ };
+
+ request.Content = new StringContent(JsonConvert.SerializeObject(content));
+ request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json");
+
+ // act
+ var response = await client.SendAsync(request);
+ var body = await response.Content.ReadAsStringAsync();
+
+ // assert
+ Assert.Equal(HttpStatusCode.Created, response.StatusCode);
+ var todoItemResult = (TodoItem)_fixture.GetService().Deserialize(body);
+ Assert.NotNull(todoItemResult);
+ Assert.NotNull(todoItemResult.Owner);
+ Assert.Equal(owner.FirstName, todoItemResult.Owner.FirstName);
+ }
+
[Fact]
public async Task Can_Create_And_Set_HasOne_Relationships_From_Independent_Side()
{