Skip to content

Commit 9dd7719

Browse files
committed
fix(#312): deserializer not handling included relationships
1 parent 02dbeab commit 9dd7719

File tree

11 files changed

+272
-62
lines changed

11 files changed

+272
-62
lines changed

src/JsonApiDotNetCore/Builders/DocumentBuilder.cs

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

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

272275
private ResourceIdentifierObject GetIndependentRelationshipIdentifier(HasOneAttribute hasOne, IIdentifiable entity)

src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs

+10-4
Original file line numberDiff line numberDiff line change
@@ -185,17 +185,23 @@ public virtual async Task<IEnumerable<TEntity>> PageAsync(IQueryable<TEntity> en
185185

186186
public async Task<int> CountAsync(IQueryable<TEntity> entities)
187187
{
188-
return await entities.CountAsync();
188+
return (entities is IAsyncEnumerable<TEntity>)
189+
? await entities.CountAsync()
190+
: entities.Count();
189191
}
190192

191-
public Task<TEntity> FirstOrDefaultAsync(IQueryable<TEntity> entities)
193+
public async Task<TEntity> FirstOrDefaultAsync(IQueryable<TEntity> entities)
192194
{
193-
return entities.FirstOrDefaultAsync();
195+
return (entities is IAsyncEnumerable<TEntity>)
196+
? await entities.FirstOrDefaultAsync()
197+
: entities.FirstOrDefault();
194198
}
195199

196200
public async Task<IReadOnlyList<TEntity>> ToListAsync(IQueryable<TEntity> entities)
197201
{
198-
return await entities.ToListAsync();
202+
return (entities is IAsyncEnumerable<TEntity>)
203+
? await entities.ToListAsync()
204+
: entities.ToList();
199205
}
200206
}
201207
}

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>();

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
}

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
}

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));

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);

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+
}

src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs

+87-36
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public object Deserialize(string requestBody)
5454
var document = bodyJToken.ToObject<Document>();
5555

5656
_jsonApiContext.DocumentMeta = document.Meta;
57-
var entity = DocumentToObject(document.Data);
57+
var entity = DocumentToObject(document.Data, document.Included);
5858
return entity;
5959
}
6060
catch (Exception e)
@@ -95,8 +95,8 @@ public List<TEntity> DeserializeList<TEntity>(string requestBody)
9595
var deserializedList = new List<TEntity>();
9696
foreach (var data in documents.Data)
9797
{
98-
var entity = DocumentToObject(data);
99-
deserializedList.Add((TEntity)entity);
98+
var entity = (TEntity)DocumentToObject(data, documents.Included);
99+
deserializedList.Add(entity);
100100
}
101101

102102
return deserializedList;
@@ -107,7 +107,7 @@ public List<TEntity> DeserializeList<TEntity>(string requestBody)
107107
}
108108
}
109109

110-
public object DocumentToObject(DocumentData data)
110+
public object DocumentToObject(DocumentData data, List<DocumentData> included = null)
111111
{
112112
if (data == null) throw new JsonApiException(422, "Failed to deserialize document as json:api.");
113113

@@ -117,7 +117,7 @@ public object DocumentToObject(DocumentData data)
117117
var entity = Activator.CreateInstance(contextEntity.EntityType);
118118

119119
entity = SetEntityAttributes(entity, contextEntity, data.Attributes);
120-
entity = SetRelationships(entity, contextEntity, data.Relationships);
120+
entity = SetRelationships(entity, contextEntity, data.Relationships, included);
121121

122122
var identifiableEntity = (IIdentifiable)entity;
123123

@@ -172,7 +172,8 @@ private object DeserializeComplexType(JContainer obj, Type targetType)
172172
private object SetRelationships(
173173
object entity,
174174
ContextEntity contextEntity,
175-
Dictionary<string, RelationshipData> relationships)
175+
Dictionary<string, RelationshipData> relationships,
176+
List<DocumentData> included = null)
176177
{
177178
if (relationships == null || relationships.Count == 0)
178179
return entity;
@@ -182,8 +183,8 @@ private object SetRelationships(
182183
foreach (var attr in contextEntity.Relationships)
183184
{
184185
entity = attr.IsHasOne
185-
? SetHasOneRelationship(entity, entityProperties, (HasOneAttribute)attr, contextEntity, relationships)
186-
: SetHasManyRelationship(entity, entityProperties, attr, contextEntity, relationships);
186+
? SetHasOneRelationship(entity, entityProperties, (HasOneAttribute)attr, contextEntity, relationships, included)
187+
: SetHasManyRelationship(entity, entityProperties, attr, contextEntity, relationships, included);
187188
}
188189

189190
return entity;
@@ -193,39 +194,53 @@ private object SetHasOneRelationship(object entity,
193194
PropertyInfo[] entityProperties,
194195
HasOneAttribute attr,
195196
ContextEntity contextEntity,
196-
Dictionary<string, RelationshipData> relationships)
197+
Dictionary<string, RelationshipData> relationships,
198+
List<DocumentData> included = null)
197199
{
198200
var relationshipName = attr.PublicRelationshipName;
199201

200-
if (relationships.TryGetValue(relationshipName, out RelationshipData relationshipData))
201-
{
202-
var relationshipAttr = _jsonApiContext.RequestEntity.Relationships
203-
.SingleOrDefault(r => r.PublicRelationshipName == relationshipName);
202+
if (relationships.TryGetValue(relationshipName, out RelationshipData relationshipData) == false)
203+
return entity;
204+
205+
var relationshipAttr = _jsonApiContext.RequestEntity.Relationships
206+
.SingleOrDefault(r => r.PublicRelationshipName == relationshipName);
204207

205-
if (relationshipAttr == null)
206-
throw new JsonApiException(400, $"{_jsonApiContext.RequestEntity.EntityName} does not contain a relationship '{relationshipName}'");
208+
if (relationshipAttr == null)
209+
throw new JsonApiException(400, $"{_jsonApiContext.RequestEntity.EntityName} does not contain a relationship '{relationshipName}'");
207210

208-
var rio = (ResourceIdentifierObject)relationshipData.ExposedData;
211+
var rio = (ResourceIdentifierObject)relationshipData.ExposedData;
209212

210-
var foreignKey = attr.IdentifiablePropertyName;
211-
var entityProperty = entityProperties.FirstOrDefault(p => p.Name == foreignKey);
212-
if (entityProperty == null && rio != null)
213-
throw new JsonApiException(400, $"{contextEntity.EntityType.Name} does not contain a foreign key property '{foreignKey}' for has one relationship '{attr.InternalRelationshipName}'");
213+
var foreignKey = attr.IdentifiablePropertyName;
214+
var foreignKeyProperty = entityProperties.FirstOrDefault(p => p.Name == foreignKey);
214215

215-
if (entityProperty != null)
216-
{
217-
// e.g. PATCH /articles
218-
// {... { "relationships":{ "Owner": { "data" :null } } } }
219-
if (rio == null && Nullable.GetUnderlyingType(entityProperty.PropertyType) == null)
220-
throw new JsonApiException(400, $"Cannot set required relationship identifier '{attr.IdentifiablePropertyName}' to null.");
216+
if (foreignKeyProperty == null && rio == null)
217+
return entity;
221218

222-
var newValue = rio?.Id ?? null;
223-
var convertedValue = TypeHelper.ConvertType(newValue, entityProperty.PropertyType);
219+
if (foreignKeyProperty == null && rio != null)
220+
throw new JsonApiException(400, $"{contextEntity.EntityType.Name} does not contain a foreign key property '{foreignKey}' for has one relationship '{attr.InternalRelationshipName}'");
224221

225-
_jsonApiContext.RelationshipsToUpdate[relationshipAttr] = convertedValue;
222+
// e.g. PATCH /articles
223+
// {... { "relationships":{ "Owner": { "data": null } } } }
224+
if (rio == null && Nullable.GetUnderlyingType(foreignKeyProperty.PropertyType) == null)
225+
throw new JsonApiException(400, $"Cannot set required relationship identifier '{attr.IdentifiablePropertyName}' to null because it is a non-nullable type.");
226226

227-
entityProperty.SetValue(entity, convertedValue);
228-
}
227+
var newValue = rio?.Id ?? null;
228+
var convertedValue = TypeHelper.ConvertType(newValue, foreignKeyProperty.PropertyType);
229+
230+
_jsonApiContext.RelationshipsToUpdate[relationshipAttr] = convertedValue;
231+
232+
foreignKeyProperty.SetValue(entity, convertedValue);
233+
234+
235+
if(rio != null
236+
// if the resource identifier is null, there should be no reason to instantiate an instance
237+
&& rio.Id != null)
238+
{
239+
// we have now set the FK property on the resource, now we need to check to see if the
240+
// related entity was included in the payload and update its attributes
241+
var includedRelationshipObject = GetIncludedRelationship(rio, included, relationshipAttr);
242+
if (includedRelationshipObject != null)
243+
relationshipAttr.SetValue(entity, includedRelationshipObject);
229244
}
230245

231246
return entity;
@@ -235,7 +250,8 @@ private object SetHasManyRelationship(object entity,
235250
PropertyInfo[] entityProperties,
236251
RelationshipAttribute attr,
237252
ContextEntity contextEntity,
238-
Dictionary<string, RelationshipData> relationships)
253+
Dictionary<string, RelationshipData> relationships,
254+
List<DocumentData> included = null)
239255
{
240256
var relationshipName = attr.PublicRelationshipName;
241257

@@ -245,14 +261,13 @@ private object SetHasManyRelationship(object entity,
245261

246262
if (data == null) return entity;
247263

248-
var relationshipShells = relationshipData.ManyData.Select(r =>
264+
var relatedResources = relationshipData.ManyData.Select(r =>
249265
{
250-
var instance = attr.Type.New<IIdentifiable>();
251-
instance.StringId = r.Id;
266+
var instance = GetIncludedRelationship(r, included, attr);
252267
return instance;
253268
});
254269

255-
var convertedCollection = TypeHelper.ConvertCollection(relationshipShells, attr.Type);
270+
var convertedCollection = TypeHelper.ConvertCollection(relatedResources, attr.Type);
256271

257272
attr.SetValue(entity, convertedCollection);
258273

@@ -261,5 +276,41 @@ private object SetHasManyRelationship(object entity,
261276

262277
return entity;
263278
}
279+
280+
private IIdentifiable GetIncludedRelationship(ResourceIdentifierObject relatedResourceIdentifier, List<DocumentData> includedResources, RelationshipAttribute relationshipAttr)
281+
{
282+
// at this point we can be sure the relationshipAttr.Type is IIdentifiable because we were able to successfully build the ContextGraph
283+
var relatedInstance = relationshipAttr.Type.New<IIdentifiable>();
284+
relatedInstance.StringId = relatedResourceIdentifier.Id;
285+
286+
// can't provide any more data other than the rio since it is not contained in the included section
287+
if (includedResources == null || includedResources.Count == 0)
288+
return relatedInstance;
289+
290+
var includedResource = GetLinkedResource(relatedResourceIdentifier, includedResources);
291+
if (includedResource == null)
292+
return relatedInstance;
293+
294+
var contextEntity = _jsonApiContext.ContextGraph.GetContextEntity(relationshipAttr.Type);
295+
if (contextEntity == null)
296+
throw new JsonApiException(400, $"Included type '{relationshipAttr.Type}' is not a registered json:api resource.");
297+
298+
SetEntityAttributes(relatedInstance, contextEntity, includedResource.Attributes);
299+
300+
return relatedInstance;
301+
}
302+
303+
private DocumentData GetLinkedResource(ResourceIdentifierObject relatedResourceIdentifier, List<DocumentData> includedResources)
304+
{
305+
try
306+
{
307+
return includedResources.SingleOrDefault(r => r.Type == relatedResourceIdentifier.Type && r.Id == relatedResourceIdentifier.Id);
308+
}
309+
catch (InvalidOperationException e)
310+
{
311+
throw new JsonApiException(400, $"A compound document MUST NOT include more than one resource object for each type and id pair."
312+
+ $"The duplicate pair was '{relatedResourceIdentifier.Type}, {relatedResourceIdentifier.Id}'", e);
313+
}
314+
}
264315
}
265316
}

0 commit comments

Comments
 (0)