Skip to content

Commit 4481147

Browse files
authored
Merge pull request #315 from json-api-dotnet/develop
#312 Deserializer not linking included relationships
2 parents 9013643 + 5791eb3 commit 4481147

20 files changed

+407
-69
lines changed

Diff for: src/JsonApiDotNetCore/Builders/DocumentBuilder.cs

+8-5
Original file line numberDiff line numberDiff line change
@@ -261,11 +261,14 @@ private ResourceIdentifierObject GetRelationship(object entity)
261261
var objType = entity.GetType();
262262
var contextEntity = _jsonApiContext.ContextGraph.GetContextEntity(objType);
263263

264-
return new ResourceIdentifierObject
265-
{
266-
Type = contextEntity.EntityName,
267-
Id = ((IIdentifiable)entity).StringId
268-
};
264+
if(entity is IIdentifiable identifiableEntity)
265+
return new ResourceIdentifierObject
266+
{
267+
Type = contextEntity.EntityName,
268+
Id = identifiableEntity.StringId
269+
};
270+
271+
return null;
269272
}
270273

271274
private ResourceIdentifierObject GetIndependentRelationshipIdentifier(HasOneAttribute hasOne, IIdentifiable entity)

Diff for: src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs

+29-5
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,19 @@ public virtual async Task<TEntity> GetAndIncludeAsync(TId id, string relationshi
8484

8585
public virtual async Task<TEntity> CreateAsync(TEntity entity)
8686
{
87-
AttachHasManyPointers();
87+
AttachRelationships();
8888
_dbSet.Add(entity);
8989

9090
await _context.SaveChangesAsync();
9191
return entity;
9292
}
9393

94+
protected virtual void AttachRelationships()
95+
{
96+
AttachHasManyPointers();
97+
AttachHasOnePointers();
98+
}
99+
94100
/// <summary>
95101
/// This is used to allow creation of HasMany relationships when the
96102
/// dependent side of the relationship already exists.
@@ -107,6 +113,18 @@ private void AttachHasManyPointers()
107113
}
108114
}
109115

116+
/// <summary>
117+
/// This is used to allow creation of HasOne relationships when the
118+
/// independent side of the relationship already exists.
119+
/// </summary>
120+
private void AttachHasOnePointers()
121+
{
122+
var relationships = _jsonApiContext.HasOneRelationshipPointers.Get();
123+
foreach (var relationship in relationships)
124+
if (_context.Entry(relationship.Value).State == EntityState.Detached && _context.EntityIsTracked(relationship.Value) == false)
125+
_context.Entry(relationship.Value).State = EntityState.Unchanged;
126+
}
127+
110128
public virtual async Task<TEntity> UpdateAsync(TId id, TEntity entity)
111129
{
112130
var oldEntity = await GetAsync(id);
@@ -185,17 +203,23 @@ public virtual async Task<IEnumerable<TEntity>> PageAsync(IQueryable<TEntity> en
185203

186204
public async Task<int> CountAsync(IQueryable<TEntity> entities)
187205
{
188-
return await entities.CountAsync();
206+
return (entities is IAsyncEnumerable<TEntity>)
207+
? await entities.CountAsync()
208+
: entities.Count();
189209
}
190210

191-
public Task<TEntity> FirstOrDefaultAsync(IQueryable<TEntity> entities)
211+
public async Task<TEntity> FirstOrDefaultAsync(IQueryable<TEntity> entities)
192212
{
193-
return entities.FirstOrDefaultAsync();
213+
return (entities is IAsyncEnumerable<TEntity>)
214+
? await entities.FirstOrDefaultAsync()
215+
: entities.FirstOrDefault();
194216
}
195217

196218
public async Task<IReadOnlyList<TEntity>> ToListAsync(IQueryable<TEntity> entities)
197219
{
198-
return await entities.ToListAsync();
220+
return (entities is IAsyncEnumerable<TEntity>)
221+
? await entities.ToListAsync()
222+
: entities.ToList();
199223
}
200224
}
201225
}

Diff for: src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs

+21
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
using System;
2+
using System.Linq;
3+
using JsonApiDotNetCore.Internal;
4+
using JsonApiDotNetCore.Models;
25
using Microsoft.EntityFrameworkCore;
36

47
namespace JsonApiDotNetCore.Extensions
@@ -8,5 +11,23 @@ public static class DbContextExtensions
811
[Obsolete("This is no longer required since the introduction of context.Set<T>", error: false)]
912
public static DbSet<T> GetDbSet<T>(this DbContext context) where T : class
1013
=> context.Set<T>();
14+
15+
/// <summary>
16+
/// Determines whether or not EF is already tracking an entity of the same Type and Id
17+
/// </summary>
18+
public static bool EntityIsTracked(this DbContext context, IIdentifiable entity)
19+
{
20+
if (entity == null)
21+
throw new ArgumentNullException(nameof(entity));
22+
23+
var trackedEntries = context.ChangeTracker
24+
.Entries()
25+
.FirstOrDefault(entry =>
26+
entry.Entity.GetType() == entity.GetType()
27+
&& ((IIdentifiable)entry.Entity).StringId == entity.StringId
28+
);
29+
30+
return trackedEntries != null;
31+
}
1132
}
1233
}

Diff for: src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs

-1
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,6 @@ public static void AddJsonApiInternals(
136136
services.AddScoped<IJsonApiReader, JsonApiReader>();
137137
services.AddScoped<IGenericProcessorFactory, GenericProcessorFactory>();
138138
services.AddScoped(typeof(GenericProcessor<>));
139-
services.AddScoped(typeof(GenericProcessor<,>));
140139
services.AddScoped<IQueryAccessor, QueryAccessor>();
141140
services.AddScoped<IQueryParser, QueryParser>();
142141
services.AddScoped<IControllerContext, Services.ControllerContext>();

Diff for: src/JsonApiDotNetCore/Extensions/TypeExtensions.cs

+15-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System;
1+
using JsonApiDotNetCore.Internal;
2+
using System;
23
using System.Collections;
34
using System.Collections.Generic;
45
using System.Linq;
@@ -51,8 +52,20 @@ public static TInterface New<TInterface>(this Type t)
5152
{
5253
if (t == null) throw new ArgumentNullException(nameof(t));
5354

54-
var instance = (TInterface)Activator.CreateInstance(t);
55+
var instance = (TInterface)CreateNewInstance(t);
5556
return instance;
5657
}
58+
59+
private static object CreateNewInstance(Type type)
60+
{
61+
try
62+
{
63+
return Activator.CreateInstance(type);
64+
}
65+
catch (Exception e)
66+
{
67+
throw new JsonApiException(500, $"Type '{type}' cannot be instantiated using the default constructor.", e);
68+
}
69+
}
5770
}
5871
}

Diff for: src/JsonApiDotNetCore/Internal/Generics/GenericProcessor.cs

+3-8
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,7 @@ public interface IGenericProcessor
1414
void SetRelationships(object parent, RelationshipAttribute relationship, IEnumerable<string> relationshipIds);
1515
}
1616

17-
public class GenericProcessor<T> : GenericProcessor<T, int> where T : class, IIdentifiable<int>
18-
{
19-
public GenericProcessor(IDbContextResolver contextResolver) : base(contextResolver) { }
20-
}
21-
22-
public class GenericProcessor<T, TId> : IGenericProcessor where T : class, IIdentifiable<TId>
17+
public class GenericProcessor<T> : IGenericProcessor where T : class, IIdentifiable
2318
{
2419
private readonly DbContext _context;
2520
public GenericProcessor(IDbContextResolver contextResolver)
@@ -38,12 +33,12 @@ public void SetRelationships(object parent, RelationshipAttribute relationship,
3833
{
3934
if (relationship.IsHasMany)
4035
{
41-
var entities = _context.GetDbSet<T>().Where(x => relationshipIds.Contains(x.StringId)).ToList();
36+
var entities = _context.Set<T>().Where(x => relationshipIds.Contains(x.StringId)).ToList();
4237
relationship.SetValue(parent, entities);
4338
}
4439
else
4540
{
46-
var entity = _context.GetDbSet<T>().SingleOrDefault(x => relationshipIds.First() == x.StringId);
41+
var entity = _context.Set<T>().SingleOrDefault(x => relationshipIds.First() == x.StringId);
4742
relationship.SetValue(parent, entity);
4843
}
4944
}

Diff for: src/JsonApiDotNetCore/JsonApiDotNetCore.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<VersionPrefix>2.3.1</VersionPrefix>
3+
<VersionPrefix>2.3.2</VersionPrefix>
44
<TargetFrameworks>$(NetStandardVersion)</TargetFrameworks>
55
<AssemblyName>JsonApiDotNetCore</AssemblyName>
66
<PackageId>JsonApiDotNetCore</PackageId>

Diff for: src/JsonApiDotNetCore/Models/Identifiable.cs

+8-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public class Identifiable<T> : IIdentifiable<T>
1515
public string StringId
1616
{
1717
get => GetStringId(Id);
18-
set => Id = (T)GetConcreteId(value);
18+
set => Id = GetTypedId(value);
1919
}
2020

2121
protected virtual string GetStringId(object value)
@@ -34,6 +34,13 @@ protected virtual string GetStringId(object value)
3434
: stringValue;
3535
}
3636

37+
protected virtual T GetTypedId(string value)
38+
{
39+
var convertedValue = TypeHelper.ConvertType(value, typeof(T));
40+
return convertedValue == null ? default : (T)convertedValue;
41+
}
42+
43+
[Obsolete("Use GetTypedId instead")]
3744
protected virtual object GetConcreteId(string value)
3845
{
3946
return TypeHelper.ConvertType(value, typeof(T));

Diff for: src/JsonApiDotNetCore/Models/RelationshipAttribute.cs

+11
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,17 @@ protected RelationshipAttribute(string publicName, Link documentLinks, bool canI
1313

1414
public string PublicRelationshipName { get; }
1515
public string InternalRelationshipName { get; internal set; }
16+
17+
/// <summary>
18+
/// The related entity type. This does not necessarily match the navigation property type.
19+
/// In the case of a HasMany relationship, this value will be the generic argument type.
20+
/// </summary>
21+
///
22+
/// <example>
23+
/// <code>
24+
/// public List&lt;Articles&gt; Articles { get; set; } // Type => Article
25+
/// </code>
26+
/// </example>
1627
public Type Type { get; internal set; }
1728
public bool IsHasMany => GetType() == typeof(HasManyAttribute);
1829
public bool IsHasOne => GetType() == typeof(HasOneAttribute);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using JsonApiDotNetCore.Models;
2+
using System;
3+
using System.Collections.Generic;
4+
5+
namespace JsonApiDotNetCore.Request
6+
{
7+
/// <summary>
8+
/// Stores information to set relationships for the request resource.
9+
/// These relationships must already exist and should not be re-created.
10+
///
11+
/// The expected use case is POST-ing or PATCH-ing
12+
/// an entity with HasOne relationships:
13+
/// <code>
14+
/// {
15+
/// "data": {
16+
/// "type": "photos",
17+
/// "attributes": {
18+
/// "title": "Ember Hamster",
19+
/// "src": "http://example.com/images/productivity.png"
20+
/// },
21+
/// "relationships": {
22+
/// "photographer": {
23+
/// "data": { "type": "people", "id": "2" }
24+
/// }
25+
/// }
26+
/// }
27+
/// }
28+
/// </code>
29+
/// </summary>
30+
public class HasOneRelationshipPointers
31+
{
32+
private Dictionary<Type, IIdentifiable> _hasOneRelationships = new Dictionary<Type, IIdentifiable>();
33+
34+
/// <summary>
35+
/// Add the relationship to the list of relationships that should be
36+
/// set in the repository layer.
37+
/// </summary>
38+
public void Add(Type dependentType, IIdentifiable entity)
39+
=> _hasOneRelationships[dependentType] = entity;
40+
41+
/// <summary>
42+
/// Get all the models that should be associated
43+
/// </summary>
44+
public Dictionary<Type, IIdentifiable> Get() => _hasOneRelationships;
45+
}
46+
}

Diff for: src/JsonApiDotNetCore/Serialization/IJsonApiDeSerializer.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@ public interface IJsonApiDeSerializer
99
TEntity Deserialize<TEntity>(string requestBody);
1010
object DeserializeRelationship(string requestBody);
1111
List<TEntity> DeserializeList<TEntity>(string requestBody);
12-
object DocumentToObject(DocumentData data);
12+
object DocumentToObject(DocumentData data, List<DocumentData> included = null);
1313
}
14-
}
14+
}

0 commit comments

Comments
 (0)