From d7d16bda33427993c7835904b07d9cfd6c99c0df Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Thu, 9 Aug 2018 21:57:31 -0700 Subject: [PATCH 1/4] fix(#343): reload relationships from database if included during POST --- .../Data/DefaultEntityRepository.cs | 85 +++++++++--- .../Data/IEntityReadRepository.cs | 6 +- .../Services/EntityResourceService.cs | 27 +++- .../Acceptance/Spec/CreatingDataTests.cs | 121 ++++++++++++++++++ 4 files changed, 218 insertions(+), 21 deletions(-) diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index 7de24d7360..3cbcc828ff 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,6 +27,10 @@ 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 where TEntity : class, IIdentifiable @@ -48,7 +53,7 @@ public DefaultEntityRepository( _genericProcessorFactory = _jsonApiContext.GenericProcessorFactory; } - /// + /// public virtual IQueryable Get() { if (_jsonApiContext.QuerySet?.Fields != null && _jsonApiContext.QuerySet.Fields.Count > 0) @@ -57,41 +62,43 @@ public virtual IQueryable Get() return _dbSet; } - /// + /// public virtual IQueryable Filter(IQueryable entities, FilterQuery filterQuery) { return entities.Filter(_jsonApiContext, filterQuery); } - /// + /// public virtual IQueryable Sort(IQueryable entities, List sortQueries) { return entities.Sort(sortQueries); } - /// + /// public virtual async Task GetAsync(TId id) { return await Get().SingleOrDefaultAsync(e => e.Id.Equals(id)); } - /// + /// 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 = await IncludeAsync(Get(), relationshipName); + var result = await includedSet.SingleOrDefaultAsync(e => e.Id.Equals(id)); return result; } - /// + /// public virtual async Task CreateAsync(TEntity entity) { AttachRelationships(); _dbSet.Add(entity); await _context.SaveChangesAsync(); + return entity; } @@ -129,7 +136,7 @@ private void AttachHasOnePointers() _context.Entry(relationship.Value).State = EntityState.Unchanged; } - /// + /// public virtual async Task UpdateAsync(TId id, TEntity entity) { var oldEntity = await GetAsync(id); @@ -148,14 +155,14 @@ public virtual async Task UpdateAsync(TId id, TEntity entity) return oldEntity; } - /// + /// public async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable relationshipIds) { var genericProcessor = _genericProcessorFactory.GetProcessor(typeof(GenericProcessor<>), relationship.Type); await genericProcessor.UpdateRelationshipsAsync(parent, relationship, relationshipIds); } - /// + /// public virtual async Task DeleteAsync(TId id) { var entity = await GetAsync(id); @@ -170,7 +177,8 @@ public virtual async Task DeleteAsync(TId id) return true; } - /// + /// + [Obsolete("Use IncludeAsync")] public virtual IQueryable Include(IQueryable entities, string relationshipName) { var entity = _jsonApiContext.RequestEntity; @@ -185,10 +193,57 @@ 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); + } + + /// + public virtual async Task> IncludeAsync(IQueryable entities, string relationshipName) + { + var entity = _jsonApiContext.RequestEntity; + var relationship = entity.Relationships.FirstOrDefault(r => r.PublicRelationshipName == relationshipName); + if (relationship == null) + { + throw new JsonApiException(400, $"Invalid relationship {relationshipName} on {entity.EntityName}", + $"{entity.EntityName} does not have a relationship named {relationshipName}"); + } + + if (!relationship.CanInclude) + { + throw new JsonApiException(400, $"Including the relationship {relationshipName} on {entity.EntityName} is not allowed"); + } + + await ReloadPointerAsync(relationship); + return entities.Include(relationship.InternalRelationshipName); } - /// + /// + /// Ensure relationships on the provided entity have been 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 + /// + private async Task ReloadPointerAsync(RelationshipAttribute relationshipAttr) + { + if (relationshipAttr.IsHasOne && _jsonApiContext.HasOneRelationshipPointers.Get().TryGetValue(relationshipAttr, out var pointer)) + { + await _context.Entry(pointer).ReloadAsync(); + } + + if (relationshipAttr.IsHasMany && _jsonApiContext.HasManyRelationshipPointers.Get().TryGetValue(relationshipAttr, out var pointers)) + { + foreach (var hasManyPointer in pointers) + { + await _context.Entry(hasManyPointer).ReloadAsync(); + } + } + } + + /// public virtual async Task> PageAsync(IQueryable entities, int pageSize, int pageNumber) { if (pageNumber >= 0) @@ -209,7 +264,7 @@ public virtual async Task> PageAsync(IQueryable en .ToListAsync(); } - /// + /// public async Task CountAsync(IQueryable entities) { return (entities is IAsyncEnumerable) @@ -217,7 +272,7 @@ public async Task CountAsync(IQueryable entities) : entities.Count(); } - /// + /// public async Task FirstOrDefaultAsync(IQueryable entities) { return (entities is IAsyncEnumerable) @@ -225,7 +280,7 @@ public async Task FirstOrDefaultAsync(IQueryable entities) : entities.FirstOrDefault(); } - /// + /// public async Task> ToListAsync(IQueryable entities) { return (entities is IAsyncEnumerable) diff --git a/src/JsonApiDotNetCore/Data/IEntityReadRepository.cs b/src/JsonApiDotNetCore/Data/IEntityReadRepository.cs index f861ce10e2..f8110dd4f8 100644 --- a/src/JsonApiDotNetCore/Data/IEntityReadRepository.cs +++ b/src/JsonApiDotNetCore/Data/IEntityReadRepository.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -20,6 +21,9 @@ public interface IEntityReadRepository /// IQueryable Get(); + [Obsolete("Use IncludeAsync")] + IQueryable Include(IQueryable entities, string relationshipName); + /// /// Include a relationship in the query /// @@ -28,7 +32,7 @@ public interface IEntityReadRepository /// _todoItemsRepository.GetAndIncludeAsync(1, "achieved-date"); /// /// - IQueryable Include(IQueryable entities, string relationshipName); + Task> IncludeAsync(IQueryable entities, string relationshipName); /// /// Apply a filter to the provided queryable diff --git a/src/JsonApiDotNetCore/Services/EntityResourceService.cs b/src/JsonApiDotNetCore/Services/EntityResourceService.cs index 300c858c43..b0ee28e8f0 100644 --- a/src/JsonApiDotNetCore/Services/EntityResourceService.cs +++ b/src/JsonApiDotNetCore/Services/EntityResourceService.cs @@ -77,6 +77,12 @@ 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()) + return await GetWithRelationshipsAsync(entity.Id); + return MapOut(entity); } @@ -92,7 +98,7 @@ public virtual async Task> GetAsync() entities = ApplySortAndFilterQuery(entities); if (ShouldIncludeRelationships()) - entities = IncludeRelationships(entities, _jsonApiContext.QuerySet.IncludedRelationships); + entities = await IncludeRelationshipsAsync(entities, _jsonApiContext.QuerySet.IncludedRelationships); if (_jsonApiContext.Options.IncludeTotalRecordCount) _jsonApiContext.PageManager.TotalRecords = await _entities.CountAsync(entities); @@ -218,7 +224,8 @@ protected virtual IQueryable ApplySortAndFilterQuery(IQueryable IncludeRelationships(IQueryable entities, List relationships) + [Obsolete("Use IncludeRelationshipsAsync")] + protected IQueryable IncludeRelationships(IQueryable entities, List relationships) { _jsonApiContext.IncludedRelationships = relationships; @@ -228,14 +235,24 @@ protected virtual IQueryable IncludeRelationships(IQueryable e return entities; } + protected virtual async Task> IncludeRelationshipsAsync(IQueryable entities, List relationships) + { + _jsonApiContext.IncludedRelationships = relationships; + + foreach (var r in relationships) + entities = await _entities.IncludeAsync(entities, r); + + return entities; + } + private async Task GetWithRelationshipsAsync(TId id) { var query = _entities.Get().Where(e => e.Id.Equals(id)); - _jsonApiContext.QuerySet.IncludedRelationships.ForEach(r => + foreach (var r in _jsonApiContext.QuerySet.IncludedRelationships) { - query = _entities.Include(query, r); - }); + query = await _entities.IncludeAsync(query, r); + } var value = await _entities.FirstOrDefaultAsync(query); 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() { From 59bf942aaf092b82181560de531dc0dddd98ee43 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Thu, 9 Aug 2018 22:43:27 -0700 Subject: [PATCH 2/4] don't reload individual relationships, just detach and then fetch --- .../Data/DefaultEntityRepository.cs | 71 ++++++------------- .../Data/IEntityReadRepository.cs | 5 +- .../Data/IEntityRepository.cs | 2 +- .../Data/IEntityWriteRepository.cs | 14 ++++ .../Services/EntityResourceService.cs | 18 ++--- 5 files changed, 44 insertions(+), 66 deletions(-) diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index 3cbcc828ff..bc9bc90544 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -85,7 +85,7 @@ public virtual async Task GetAndIncludeAsync(TId id, string relationshi { _logger.LogDebug($"[JADN] GetAndIncludeAsync({id}, {relationshipName})"); - var includedSet = await IncludeAsync(Get(), relationshipName); + var includedSet = Include(Get(), relationshipName); var result = await includedSet.SingleOrDefaultAsync(e => e.Id.Equals(id)); return result; @@ -108,6 +108,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. @@ -178,7 +200,6 @@ public virtual async Task DeleteAsync(TId id) } /// - [Obsolete("Use IncludeAsync")] public virtual IQueryable Include(IQueryable entities, string relationshipName) { var entity = _jsonApiContext.RequestEntity; @@ -197,52 +218,6 @@ public virtual IQueryable Include(IQueryable entities, string return entities.Include(relationship.InternalRelationshipName); } - /// - public virtual async Task> IncludeAsync(IQueryable entities, string relationshipName) - { - var entity = _jsonApiContext.RequestEntity; - var relationship = entity.Relationships.FirstOrDefault(r => r.PublicRelationshipName == relationshipName); - if (relationship == null) - { - throw new JsonApiException(400, $"Invalid relationship {relationshipName} on {entity.EntityName}", - $"{entity.EntityName} does not have a relationship named {relationshipName}"); - } - - if (!relationship.CanInclude) - { - throw new JsonApiException(400, $"Including the relationship {relationshipName} on {entity.EntityName} is not allowed"); - } - - await ReloadPointerAsync(relationship); - - return entities.Include(relationship.InternalRelationshipName); - } - - /// - /// Ensure relationships on the provided entity have been 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 - /// - private async Task ReloadPointerAsync(RelationshipAttribute relationshipAttr) - { - if (relationshipAttr.IsHasOne && _jsonApiContext.HasOneRelationshipPointers.Get().TryGetValue(relationshipAttr, out var pointer)) - { - await _context.Entry(pointer).ReloadAsync(); - } - - if (relationshipAttr.IsHasMany && _jsonApiContext.HasManyRelationshipPointers.Get().TryGetValue(relationshipAttr, out var pointers)) - { - foreach (var hasManyPointer in pointers) - { - await _context.Entry(hasManyPointer).ReloadAsync(); - } - } - } - /// public virtual async Task> PageAsync(IQueryable entities, int pageSize, int pageNumber) { diff --git a/src/JsonApiDotNetCore/Data/IEntityReadRepository.cs b/src/JsonApiDotNetCore/Data/IEntityReadRepository.cs index f8110dd4f8..ea9450d726 100644 --- a/src/JsonApiDotNetCore/Data/IEntityReadRepository.cs +++ b/src/JsonApiDotNetCore/Data/IEntityReadRepository.cs @@ -21,9 +21,6 @@ public interface IEntityReadRepository /// IQueryable Get(); - [Obsolete("Use IncludeAsync")] - IQueryable Include(IQueryable entities, string relationshipName); - /// /// Include a relationship in the query /// @@ -32,7 +29,7 @@ public interface IEntityReadRepository /// _todoItemsRepository.GetAndIncludeAsync(1, "achieved-date"); /// /// - Task> IncludeAsync(IQueryable entities, string relationshipName); + IQueryable Include(IQueryable entities, string relationshipName); /// /// Apply a filter to the provided queryable diff --git a/src/JsonApiDotNetCore/Data/IEntityRepository.cs b/src/JsonApiDotNetCore/Data/IEntityRepository.cs index e8bb68ef90..290f860566 100644 --- a/src/JsonApiDotNetCore/Data/IEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/IEntityRepository.cs @@ -8,7 +8,7 @@ public interface IEntityRepository { } public interface IEntityRepository - : IEntityReadRepository, + : IEntityReadRepository, IEntityWriteRepository where TEntity : class, IIdentifiable { } diff --git a/src/JsonApiDotNetCore/Data/IEntityWriteRepository.cs b/src/JsonApiDotNetCore/Data/IEntityWriteRepository.cs index 37c60b8521..38198fa676 100644 --- a/src/JsonApiDotNetCore/Data/IEntityWriteRepository.cs +++ b/src/JsonApiDotNetCore/Data/IEntityWriteRepository.cs @@ -19,5 +19,19 @@ public interface IEntityWriteRepository Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable relationshipIds); Task DeleteAsync(TId id); + + /// + /// 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/Services/EntityResourceService.cs b/src/JsonApiDotNetCore/Services/EntityResourceService.cs index b0ee28e8f0..0f8257011c 100644 --- a/src/JsonApiDotNetCore/Services/EntityResourceService.cs +++ b/src/JsonApiDotNetCore/Services/EntityResourceService.cs @@ -81,7 +81,10 @@ public virtual async Task CreateAsync(TResource resource) // been requested // https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/343 if (ShouldIncludeRelationships()) + { + _entities.DetachRelationshipPointers(entity); return await GetWithRelationshipsAsync(entity.Id); + } return MapOut(entity); } @@ -98,7 +101,7 @@ public virtual async Task> GetAsync() entities = ApplySortAndFilterQuery(entities); if (ShouldIncludeRelationships()) - entities = await IncludeRelationshipsAsync(entities, _jsonApiContext.QuerySet.IncludedRelationships); + entities = IncludeRelationships(entities, _jsonApiContext.QuerySet.IncludedRelationships); if (_jsonApiContext.Options.IncludeTotalRecordCount) _jsonApiContext.PageManager.TotalRecords = await _entities.CountAsync(entities); @@ -224,7 +227,6 @@ protected virtual IQueryable ApplySortAndFilterQuery(IQueryable IncludeRelationships(IQueryable entities, List relationships) { _jsonApiContext.IncludedRelationships = relationships; @@ -235,23 +237,13 @@ protected IQueryable IncludeRelationships(IQueryable entities, return entities; } - protected virtual async Task> IncludeRelationshipsAsync(IQueryable entities, List relationships) - { - _jsonApiContext.IncludedRelationships = relationships; - - foreach (var r in relationships) - entities = await _entities.IncludeAsync(entities, r); - - return entities; - } - private async Task GetWithRelationshipsAsync(TId id) { var query = _entities.Get().Where(e => e.Id.Equals(id)); foreach (var r in _jsonApiContext.QuerySet.IncludedRelationships) { - query = await _entities.IncludeAsync(query, r); + query = _entities.Include(query, r); } var value = await _entities.FirstOrDefaultAsync(query); From 66ea1b3cdc25d6a2dcbf31ffd7af0da0311391e5 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Thu, 9 Aug 2018 22:46:09 -0700 Subject: [PATCH 3/4] cleanup --- src/JsonApiDotNetCore/Data/IEntityReadRepository.cs | 1 - src/JsonApiDotNetCore/Services/EntityResourceService.cs | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/JsonApiDotNetCore/Data/IEntityReadRepository.cs b/src/JsonApiDotNetCore/Data/IEntityReadRepository.cs index ea9450d726..f861ce10e2 100644 --- a/src/JsonApiDotNetCore/Data/IEntityReadRepository.cs +++ b/src/JsonApiDotNetCore/Data/IEntityReadRepository.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; diff --git a/src/JsonApiDotNetCore/Services/EntityResourceService.cs b/src/JsonApiDotNetCore/Services/EntityResourceService.cs index 0f8257011c..996fb29610 100644 --- a/src/JsonApiDotNetCore/Services/EntityResourceService.cs +++ b/src/JsonApiDotNetCore/Services/EntityResourceService.cs @@ -227,7 +227,7 @@ protected virtual IQueryable ApplySortAndFilterQuery(IQueryable IncludeRelationships(IQueryable entities, List relationships) + protected virtual IQueryable IncludeRelationships(IQueryable entities, List relationships) { _jsonApiContext.IncludedRelationships = relationships; @@ -241,10 +241,10 @@ private async Task GetWithRelationshipsAsync(TId id) { var query = _entities.Get().Where(e => e.Id.Equals(id)); - foreach (var r in _jsonApiContext.QuerySet.IncludedRelationships) + _jsonApiContext.QuerySet.IncludedRelationships.ForEach(r => { query = _entities.Include(query, r); - } + }); var value = await _entities.FirstOrDefaultAsync(query); From 09f38460905b009d26da9c5d00c6c2938d1bf189 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Thu, 9 Aug 2018 22:58:12 -0700 Subject: [PATCH 4/4] introduce IEntityFrameworkRepository and bump version allows changes to be rolled out in a non-breaking way --- .../Data/DefaultEntityRepository.cs | 3 ++- .../Data/IEntityRepository.cs | 21 +++++++++++++++++++ .../Data/IEntityWriteRepository.cs | 14 ------------- .../JsonApiDotNetCore.csproj | 2 +- .../Services/EntityResourceService.cs | 4 +++- 5 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index bc9bc90544..862079aeee 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -32,7 +32,8 @@ public DefaultEntityRepository( /// abstracting any EF Core APIs away from the service layer. /// public class DefaultEntityRepository - : IEntityRepository + : IEntityRepository, + IEntityFrameworkRepository where TEntity : class, IIdentifiable { private readonly DbContext _context; diff --git a/src/JsonApiDotNetCore/Data/IEntityRepository.cs b/src/JsonApiDotNetCore/Data/IEntityRepository.cs index 290f860566..ac69f4fdac 100644 --- a/src/JsonApiDotNetCore/Data/IEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/IEntityRepository.cs @@ -12,4 +12,25 @@ public interface IEntityRepository 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/Data/IEntityWriteRepository.cs b/src/JsonApiDotNetCore/Data/IEntityWriteRepository.cs index 38198fa676..37c60b8521 100644 --- a/src/JsonApiDotNetCore/Data/IEntityWriteRepository.cs +++ b/src/JsonApiDotNetCore/Data/IEntityWriteRepository.cs @@ -19,19 +19,5 @@ public interface IEntityWriteRepository Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable relationshipIds); Task DeleteAsync(TId id); - - /// - /// 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 996fb29610..1e8dc249a2 100644 --- a/src/JsonApiDotNetCore/Services/EntityResourceService.cs +++ b/src/JsonApiDotNetCore/Services/EntityResourceService.cs @@ -82,7 +82,9 @@ public virtual async Task CreateAsync(TResource resource) // https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/343 if (ShouldIncludeRelationships()) { - _entities.DetachRelationshipPointers(entity); + if(_entities is IEntityFrameworkRepository efRepository) + efRepository.DetachRelationshipPointers(entity); + return await GetWithRelationshipsAsync(entity.Id); }