Skip to content

Commit cb93418

Browse files
committed
fix(#218): include independent hasOne identifier
in all requests for the dependent side of the relationship by default
1 parent fd1eaee commit cb93418

File tree

4 files changed

+134
-40
lines changed

4 files changed

+134
-40
lines changed

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

+55-30
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ public class DocumentBuilder : IDocumentBuilder
1313
private readonly IJsonApiContext _jsonApiContext;
1414
private readonly IContextGraph _contextGraph;
1515
private readonly IRequestMeta _requestMeta;
16-
private readonly DocumentBuilderOptions _documentBuilderOptions;
16+
private readonly DocumentBuilderOptions _documentBuilderOptions;
1717

18-
public DocumentBuilder(IJsonApiContext jsonApiContext, IRequestMeta requestMeta=null, IDocumentBuilderOptionsProvider documentBuilderOptionsProvider=null)
18+
public DocumentBuilder(IJsonApiContext jsonApiContext, IRequestMeta requestMeta = null, IDocumentBuilderOptionsProvider documentBuilderOptionsProvider = null)
1919
{
2020
_jsonApiContext = jsonApiContext;
2121
_contextGraph = jsonApiContext.ContextGraph;
@@ -143,34 +143,42 @@ private bool OmitNullValuedAttribute(AttrAttribute attr, object attributeValue)
143143
private void AddRelationships(DocumentData data, ContextEntity contextEntity, IIdentifiable entity)
144144
{
145145
data.Relationships = new Dictionary<string, RelationshipData>();
146+
contextEntity.Relationships.ForEach(r =>
147+
data.Relationships.Add(
148+
r.PublicRelationshipName,
149+
GetRelationshipData(r, contextEntity, entity)
150+
)
151+
);
152+
}
153+
154+
private RelationshipData GetRelationshipData(RelationshipAttribute attr, ContextEntity contextEntity, IIdentifiable entity)
155+
{
146156
var linkBuilder = new LinkBuilder(_jsonApiContext);
147157

148-
contextEntity.Relationships.ForEach(r =>
158+
var relationshipData = new RelationshipData();
159+
160+
if (attr.DocumentLinks.HasFlag(Link.None) == false)
149161
{
150-
var relationshipData = new RelationshipData();
162+
relationshipData.Links = new Links();
163+
if (attr.DocumentLinks.HasFlag(Link.Self))
164+
relationshipData.Links.Self = linkBuilder.GetSelfRelationLink(contextEntity.EntityName, entity.StringId, attr.PublicRelationshipName);
151165

152-
if (r.DocumentLinks.HasFlag(Link.None) == false)
153-
{
154-
relationshipData.Links = new Links();
155-
if (r.DocumentLinks.HasFlag(Link.Self))
156-
relationshipData.Links.Self = linkBuilder.GetSelfRelationLink(contextEntity.EntityName, entity.StringId, r.PublicRelationshipName);
166+
if (attr.DocumentLinks.HasFlag(Link.Related))
167+
relationshipData.Links.Related = linkBuilder.GetRelatedRelationLink(contextEntity.EntityName, entity.StringId, attr.PublicRelationshipName);
168+
}
157169

158-
if (r.DocumentLinks.HasFlag(Link.Related))
159-
relationshipData.Links.Related = linkBuilder.GetRelatedRelationLink(contextEntity.EntityName, entity.StringId, r.PublicRelationshipName);
160-
}
161-
162-
var navigationEntity = _jsonApiContext.ContextGraph
163-
.GetRelationship(entity, r.InternalRelationshipName);
164-
165-
if (navigationEntity == null)
166-
relationshipData.SingleData = null;
167-
else if (navigationEntity is IEnumerable)
168-
relationshipData.ManyData = GetRelationships((IEnumerable<object>)navigationEntity);
169-
else
170-
relationshipData.SingleData = GetRelationship(navigationEntity);
171-
172-
data.Relationships.Add(r.PublicRelationshipName, relationshipData);
173-
});
170+
// this only includes the navigation property, we need to actually check the navigation property Id
171+
var navigationEntity = _jsonApiContext.ContextGraph.GetRelationship(entity, attr.InternalRelationshipName);
172+
if (navigationEntity == null)
173+
relationshipData.SingleData = attr.IsHasOne
174+
? GetIndependentRelationshipIdentifier((HasOneAttribute)attr, entity)
175+
: null;
176+
else if (navigationEntity is IEnumerable)
177+
relationshipData.ManyData = GetRelationships((IEnumerable<object>)navigationEntity);
178+
else
179+
relationshipData.SingleData = GetRelationship(navigationEntity);
180+
181+
return relationshipData;
174182
}
175183

176184
private List<DocumentData> GetIncludedEntities(List<DocumentData> included, ContextEntity contextEntity, IIdentifiable entity)
@@ -240,23 +248,40 @@ private List<ResourceIdentifierObject> GetRelationships(IEnumerable<object> enti
240248
var relationships = new List<ResourceIdentifierObject>();
241249
foreach (var entity in entities)
242250
{
243-
relationships.Add(new ResourceIdentifierObject {
251+
relationships.Add(new ResourceIdentifierObject
252+
{
244253
Type = typeName.EntityName,
245254
Id = ((IIdentifiable)entity).StringId
246255
});
247256
}
248257
return relationships;
249258
}
259+
250260
private ResourceIdentifierObject GetRelationship(object entity)
251261
{
252262
var objType = entity.GetType();
263+
var contextEntity = _jsonApiContext.ContextGraph.GetContextEntity(objType);
253264

254-
var typeName = _jsonApiContext.ContextGraph.GetContextEntity(objType);
255-
256-
return new ResourceIdentifierObject {
257-
Type = typeName.EntityName,
265+
return new ResourceIdentifierObject
266+
{
267+
Type = contextEntity.EntityName,
258268
Id = ((IIdentifiable)entity).StringId
259269
};
260270
}
271+
272+
private ResourceIdentifierObject GetIndependentRelationshipIdentifier(HasOneAttribute hasOne, IIdentifiable entity)
273+
{
274+
var independentRelationshipIdentifier = hasOne.GetIdentifiablePropertyValue(entity);
275+
if (independentRelationshipIdentifier == null)
276+
return null;
277+
278+
var relatedContextEntity = _jsonApiContext.ContextGraph.GetContextEntity(hasOne.Type);
279+
280+
return new ResourceIdentifierObject
281+
{
282+
Type = relatedContextEntity.EntityName,
283+
Id = independentRelationshipIdentifier.ToString()
284+
};
285+
}
261286
}
262287
}

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

+47-7
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,61 @@ namespace JsonApiDotNetCore.Models
22
{
33
public class HasOneAttribute : RelationshipAttribute
44
{
5-
public HasOneAttribute(string publicName, Link documentLinks = Link.All, bool canInclude = true)
5+
/// <summary>
6+
/// Create a HasOne relational link to another entity
7+
/// </summary>
8+
///
9+
/// <param name="publicName">The relationship name as exposed by the API</param>
10+
/// <param name="documentLinks">Which links are available. Defaults to <see cref="Link.All"/></param>
11+
/// <param name="canInclude">Whether or not this relationship can be included using the <c>?include=public-name</c> query string</param>
12+
/// <param name="withForiegnKey">The foreign key property name. Defaults to <c>"{RelationshipName}Id"</c></param>
13+
///
14+
/// <example>
15+
/// Using an alternative foreign key:
16+
///
17+
/// <code>
18+
/// public class Article : Identifiable
19+
/// {
20+
/// [HasOne("author", withForiegnKey: nameof(AuthorKey)]
21+
/// public Author Author { get; set; }
22+
/// public int AuthorKey { get; set; }
23+
/// }
24+
/// </code>
25+
///
26+
/// </example>
27+
public HasOneAttribute(string publicName, Link documentLinks = Link.All, bool canInclude = true, string withForiegnKey = null)
628
: base(publicName, documentLinks, canInclude)
7-
{ }
29+
{
30+
_explicitIdentifiablePropertyName = withForiegnKey;
31+
}
32+
33+
private readonly string _explicitIdentifiablePropertyName;
34+
35+
/// <summary>
36+
/// The independent entity identifier.
37+
/// </summary>
38+
public string IdentifiablePropertyName => string.IsNullOrWhiteSpace(_explicitIdentifiablePropertyName)
39+
? $"{InternalRelationshipName}Id"
40+
: _explicitIdentifiablePropertyName;
841

942
public override void SetValue(object entity, object newValue)
1043
{
11-
var propertyName = (newValue.GetType() == Type)
12-
? InternalRelationshipName
13-
: $"{InternalRelationshipName}Id";
14-
44+
var propertyName = (newValue.GetType() == Type)
45+
? InternalRelationshipName
46+
: IdentifiablePropertyName;
47+
1548
var propertyInfo = entity
1649
.GetType()
1750
.GetProperty(propertyName);
18-
51+
1952
propertyInfo.SetValue(entity, newValue);
2053
}
54+
55+
// HACK: this will likely require boxing
56+
// we should be able to move some of the reflection into the ContextGraphBuilder
57+
internal object GetIdentifiablePropertyValue(object entity) => entity
58+
.GetType()
59+
.GetProperty(IdentifiablePropertyName)
60+
.GetValue(entity);
2161
}
2262
}

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ private object SetRelationships(
175175
foreach (var attr in contextEntity.Relationships)
176176
{
177177
entity = attr.IsHasOne
178-
? SetHasOneRelationship(entity, entityProperties, attr, contextEntity, relationships)
178+
? SetHasOneRelationship(entity, entityProperties, (HasOneAttribute)attr, contextEntity, relationships)
179179
: SetHasManyRelationship(entity, entityProperties, attr, contextEntity, relationships);
180180
}
181181

@@ -184,7 +184,7 @@ private object SetRelationships(
184184

185185
private object SetHasOneRelationship(object entity,
186186
PropertyInfo[] entityProperties,
187-
RelationshipAttribute attr,
187+
HasOneAttribute attr,
188188
ContextEntity contextEntity,
189189
Dictionary<string, RelationshipData> relationships)
190190
{
@@ -204,7 +204,7 @@ private object SetHasOneRelationship(object entity,
204204

205205
var newValue = rio.Id;
206206

207-
var foreignKey = attr.InternalRelationshipName + "Id";
207+
var foreignKey = attr.IdentifiablePropertyName;
208208
var entityProperty = entityProperties.FirstOrDefault(p => p.Name == foreignKey);
209209
if (entityProperty == null)
210210
throw new JsonApiException(400, $"{contextEntity.EntityType.Name} does not contain a foreign key property '{foreignKey}' for has one relationship '{attr.InternalRelationshipName}'");

Diff for: test/UnitTests/Builders/DocumentBuilder_Tests.cs

+29
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,35 @@ public void Related_Data_Included_In_Relationships_By_Default()
145145
// act
146146
var document = documentBuilder.Build(entity);
147147

148+
// assert
149+
var relationshipData = document.Data.Relationships[relationshipName];
150+
Assert.NotNull(relationshipData);
151+
Assert.NotNull(relationshipData.SingleData);
152+
Assert.NotNull(relationshipData.SingleData);
153+
Assert.Equal(relatedId.ToString(), relationshipData.SingleData.Id);
154+
Assert.Equal(relatedTypeName, relationshipData.SingleData.Type);
155+
}
156+
157+
[Fact]
158+
public void IndependentIdentifier__Included_In_HasOne_Relationships_By_Default()
159+
{
160+
// arrange
161+
const string relatedTypeName = "related-models";
162+
const string relationshipName = "related-model";
163+
const int relatedId = 1;
164+
_jsonApiContextMock
165+
.Setup(m => m.ContextGraph)
166+
.Returns(_options.ContextGraph);
167+
168+
var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object);
169+
var entity = new Model
170+
{
171+
RelatedModelId = relatedId
172+
};
173+
174+
// act
175+
var document = documentBuilder.Build(entity);
176+
148177
// assert
149178
var relationshipData = document.Data.Relationships[relationshipName];
150179
Assert.NotNull(relationshipData);

0 commit comments

Comments
 (0)