Skip to content

Commit e8cbc4b

Browse files
authored
Merge pull request #284 from json-api-dotnet/fix/#218
Fix/#218
2 parents 723614d + 1a8531f commit e8cbc4b

File tree

5 files changed

+156
-52
lines changed

5 files changed

+156
-52
lines changed

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

+57-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,42 @@ 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+
if (relatedContextEntity == null) // TODO: this should probably be a debug log at minimum
280+
return null;
281+
282+
return new ResourceIdentifierObject
283+
{
284+
Type = relatedContextEntity.EntityName,
285+
Id = independentRelationshipIdentifier.ToString()
286+
};
287+
}
261288
}
262289
}

Diff for: src/JsonApiDotNetCore/JsonApiDotNetCore.csproj

+9-12
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<VersionPrefix>2.2.3</VersionPrefix>
3+
<VersionPrefix>2.2.4</VersionPrefix>
44
<TargetFrameworks>$(NetStandardVersion)</TargetFrameworks>
55
<AssemblyName>JsonApiDotNetCore</AssemblyName>
66
<PackageId>JsonApiDotNetCore</PackageId>
7+
<LangVersion>7.2</LangVersion>
78
</PropertyGroup>
89

910
<PropertyGroup>
@@ -25,20 +26,16 @@
2526
<PackageReference Include="System.ValueTuple" Version="$(TuplesVersion)" />
2627
</ItemGroup>
2728

28-
<!-- XML documentation -->
29-
<PropertyGroup>
29+
<!--
30+
To generate DocFX documentation on Windows platforms: dotnet build -c Release /p:DocFx=true
31+
-->
32+
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
3033
<IsWindows Condition="'$(OS)' == 'Windows_NT'">true</IsWindows>
31-
</PropertyGroup>
32-
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
34+
<GenerateDocumentation Condition="'$(IsWindows)|$(DocFx)' == 'true|true'">true</GenerateDocumentation>
3335
<DocumentationFile>bin\Release\netstandard2.0\JsonApiDotNetCore.xml</DocumentationFile>
3436
</PropertyGroup>
35-
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|netstandard2.0|AnyCPU'">
36-
<LangVersion>7.2</LangVersion>
37-
</PropertyGroup>
38-
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|netstandard2.0|AnyCPU'">
39-
<LangVersion>7.2</LangVersion>
40-
</PropertyGroup>
41-
<ItemGroup Condition="$(IsWindows)=='true'">
37+
38+
<ItemGroup Condition="'$(GenerateDocumentation)' == 'true'">
4239
<PackageReference Include="docfx.console" Version="2.33.0" />
4340
</ItemGroup>
4441

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

+58-7
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,72 @@ 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+
/// <summary>
58+
/// Gets the value of the independent identifier (e.g. Article.AuthorId)
59+
/// </summary>
60+
///
61+
/// <param name="entity">
62+
/// An instance of dependent resource
63+
/// </param>
64+
///
65+
/// <returns>
66+
/// The property value or null if the property does not exist on the model.
67+
/// </returns>
68+
internal object GetIdentifiablePropertyValue(object entity) => entity
69+
.GetType()
70+
.GetProperty(IdentifiablePropertyName)
71+
?.GetValue(entity);
2172
}
2273
}

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)