diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index 3edea50e13..088a2bf092 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -13,9 +13,9 @@ public class DocumentBuilder : IDocumentBuilder private readonly IJsonApiContext _jsonApiContext; private readonly IContextGraph _contextGraph; private readonly IRequestMeta _requestMeta; - private readonly DocumentBuilderOptions _documentBuilderOptions; + private readonly DocumentBuilderOptions _documentBuilderOptions; - public DocumentBuilder(IJsonApiContext jsonApiContext, IRequestMeta requestMeta=null, IDocumentBuilderOptionsProvider documentBuilderOptionsProvider=null) + public DocumentBuilder(IJsonApiContext jsonApiContext, IRequestMeta requestMeta = null, IDocumentBuilderOptionsProvider documentBuilderOptionsProvider = null) { _jsonApiContext = jsonApiContext; _contextGraph = jsonApiContext.ContextGraph; @@ -143,34 +143,42 @@ private bool OmitNullValuedAttribute(AttrAttribute attr, object attributeValue) private void AddRelationships(DocumentData data, ContextEntity contextEntity, IIdentifiable entity) { data.Relationships = new Dictionary(); + contextEntity.Relationships.ForEach(r => + data.Relationships.Add( + r.PublicRelationshipName, + GetRelationshipData(r, contextEntity, entity) + ) + ); + } + + private RelationshipData GetRelationshipData(RelationshipAttribute attr, ContextEntity contextEntity, IIdentifiable entity) + { var linkBuilder = new LinkBuilder(_jsonApiContext); - contextEntity.Relationships.ForEach(r => + var relationshipData = new RelationshipData(); + + if (attr.DocumentLinks.HasFlag(Link.None) == false) { - var relationshipData = new RelationshipData(); + relationshipData.Links = new Links(); + if (attr.DocumentLinks.HasFlag(Link.Self)) + relationshipData.Links.Self = linkBuilder.GetSelfRelationLink(contextEntity.EntityName, entity.StringId, attr.PublicRelationshipName); - if (r.DocumentLinks.HasFlag(Link.None) == false) - { - relationshipData.Links = new Links(); - if (r.DocumentLinks.HasFlag(Link.Self)) - relationshipData.Links.Self = linkBuilder.GetSelfRelationLink(contextEntity.EntityName, entity.StringId, r.PublicRelationshipName); + if (attr.DocumentLinks.HasFlag(Link.Related)) + relationshipData.Links.Related = linkBuilder.GetRelatedRelationLink(contextEntity.EntityName, entity.StringId, attr.PublicRelationshipName); + } - if (r.DocumentLinks.HasFlag(Link.Related)) - relationshipData.Links.Related = linkBuilder.GetRelatedRelationLink(contextEntity.EntityName, entity.StringId, r.PublicRelationshipName); - } - - var navigationEntity = _jsonApiContext.ContextGraph - .GetRelationship(entity, r.InternalRelationshipName); - - if (navigationEntity == null) - relationshipData.SingleData = null; - else if (navigationEntity is IEnumerable) - relationshipData.ManyData = GetRelationships((IEnumerable)navigationEntity); - else - relationshipData.SingleData = GetRelationship(navigationEntity); - - data.Relationships.Add(r.PublicRelationshipName, relationshipData); - }); + // this only includes the navigation property, we need to actually check the navigation property Id + var navigationEntity = _jsonApiContext.ContextGraph.GetRelationship(entity, attr.InternalRelationshipName); + if (navigationEntity == null) + relationshipData.SingleData = attr.IsHasOne + ? GetIndependentRelationshipIdentifier((HasOneAttribute)attr, entity) + : null; + else if (navigationEntity is IEnumerable) + relationshipData.ManyData = GetRelationships((IEnumerable)navigationEntity); + else + relationshipData.SingleData = GetRelationship(navigationEntity); + + return relationshipData; } private List GetIncludedEntities(List included, ContextEntity contextEntity, IIdentifiable entity) @@ -240,23 +248,42 @@ private List GetRelationships(IEnumerable enti var relationships = new List(); foreach (var entity in entities) { - relationships.Add(new ResourceIdentifierObject { + relationships.Add(new ResourceIdentifierObject + { Type = typeName.EntityName, Id = ((IIdentifiable)entity).StringId }); } return relationships; } + private ResourceIdentifierObject GetRelationship(object entity) { var objType = entity.GetType(); + var contextEntity = _jsonApiContext.ContextGraph.GetContextEntity(objType); - var typeName = _jsonApiContext.ContextGraph.GetContextEntity(objType); - - return new ResourceIdentifierObject { - Type = typeName.EntityName, + return new ResourceIdentifierObject + { + Type = contextEntity.EntityName, Id = ((IIdentifiable)entity).StringId }; } + + private ResourceIdentifierObject GetIndependentRelationshipIdentifier(HasOneAttribute hasOne, IIdentifiable entity) + { + var independentRelationshipIdentifier = hasOne.GetIdentifiablePropertyValue(entity); + if (independentRelationshipIdentifier == null) + return null; + + var relatedContextEntity = _jsonApiContext.ContextGraph.GetContextEntity(hasOne.Type); + if (relatedContextEntity == null) // TODO: this should probably be a debug log at minimum + return null; + + return new ResourceIdentifierObject + { + Type = relatedContextEntity.EntityName, + Id = independentRelationshipIdentifier.ToString() + }; + } } } diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index 53649bb7e7..eb649ed5dc 100755 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -1,9 +1,10 @@  - 2.2.3 + 2.2.4 $(NetStandardVersion) JsonApiDotNetCore JsonApiDotNetCore + 7.2 @@ -25,20 +26,16 @@ - - + + true - - + true bin\Release\netstandard2.0\JsonApiDotNetCore.xml - - 7.2 - - - 7.2 - - + + diff --git a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs b/src/JsonApiDotNetCore/Models/HasOneAttribute.cs index f863c8819b..03fdb200fc 100644 --- a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs +++ b/src/JsonApiDotNetCore/Models/HasOneAttribute.cs @@ -2,21 +2,72 @@ namespace JsonApiDotNetCore.Models { public class HasOneAttribute : RelationshipAttribute { - public HasOneAttribute(string publicName, Link documentLinks = Link.All, bool canInclude = true) + /// + /// Create a HasOne relational link to another entity + /// + /// + /// The relationship name as exposed by the API + /// Which links are available. Defaults to + /// Whether or not this relationship can be included using the ?include=public-name query string + /// The foreign key property name. Defaults to "{RelationshipName}Id" + /// + /// + /// Using an alternative foreign key: + /// + /// + /// public class Article : Identifiable + /// { + /// [HasOne("author", withForiegnKey: nameof(AuthorKey)] + /// public Author Author { get; set; } + /// public int AuthorKey { get; set; } + /// } + /// + /// + /// + public HasOneAttribute(string publicName, Link documentLinks = Link.All, bool canInclude = true, string withForiegnKey = null) : base(publicName, documentLinks, canInclude) - { } + { + _explicitIdentifiablePropertyName = withForiegnKey; + } + + private readonly string _explicitIdentifiablePropertyName; + + /// + /// The independent entity identifier. + /// + public string IdentifiablePropertyName => string.IsNullOrWhiteSpace(_explicitIdentifiablePropertyName) + ? $"{InternalRelationshipName}Id" + : _explicitIdentifiablePropertyName; public override void SetValue(object entity, object newValue) { - var propertyName = (newValue.GetType() == Type) - ? InternalRelationshipName - : $"{InternalRelationshipName}Id"; - + var propertyName = (newValue.GetType() == Type) + ? InternalRelationshipName + : IdentifiablePropertyName; + var propertyInfo = entity .GetType() .GetProperty(propertyName); - + propertyInfo.SetValue(entity, newValue); } + + // HACK: this will likely require boxing + // we should be able to move some of the reflection into the ContextGraphBuilder + /// + /// Gets the value of the independent identifier (e.g. Article.AuthorId) + /// + /// + /// + /// An instance of dependent resource + /// + /// + /// + /// The property value or null if the property does not exist on the model. + /// + internal object GetIdentifiablePropertyValue(object entity) => entity + .GetType() + .GetProperty(IdentifiablePropertyName) + ?.GetValue(entity); } } diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index 649d6435ff..6fb7af55e1 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -175,7 +175,7 @@ private object SetRelationships( foreach (var attr in contextEntity.Relationships) { entity = attr.IsHasOne - ? SetHasOneRelationship(entity, entityProperties, attr, contextEntity, relationships) + ? SetHasOneRelationship(entity, entityProperties, (HasOneAttribute)attr, contextEntity, relationships) : SetHasManyRelationship(entity, entityProperties, attr, contextEntity, relationships); } @@ -184,7 +184,7 @@ private object SetRelationships( private object SetHasOneRelationship(object entity, PropertyInfo[] entityProperties, - RelationshipAttribute attr, + HasOneAttribute attr, ContextEntity contextEntity, Dictionary relationships) { @@ -204,7 +204,7 @@ private object SetHasOneRelationship(object entity, var newValue = rio.Id; - var foreignKey = attr.InternalRelationshipName + "Id"; + var foreignKey = attr.IdentifiablePropertyName; var entityProperty = entityProperties.FirstOrDefault(p => p.Name == foreignKey); if (entityProperty == null) throw new JsonApiException(400, $"{contextEntity.EntityType.Name} does not contain a foreign key property '{foreignKey}' for has one relationship '{attr.InternalRelationshipName}'"); diff --git a/test/UnitTests/Builders/DocumentBuilder_Tests.cs b/test/UnitTests/Builders/DocumentBuilder_Tests.cs index b2131f6ec9..dbca1bebb4 100644 --- a/test/UnitTests/Builders/DocumentBuilder_Tests.cs +++ b/test/UnitTests/Builders/DocumentBuilder_Tests.cs @@ -145,6 +145,35 @@ public void Related_Data_Included_In_Relationships_By_Default() // act var document = documentBuilder.Build(entity); + // assert + var relationshipData = document.Data.Relationships[relationshipName]; + Assert.NotNull(relationshipData); + Assert.NotNull(relationshipData.SingleData); + Assert.NotNull(relationshipData.SingleData); + Assert.Equal(relatedId.ToString(), relationshipData.SingleData.Id); + Assert.Equal(relatedTypeName, relationshipData.SingleData.Type); + } + + [Fact] + public void IndependentIdentifier__Included_In_HasOne_Relationships_By_Default() + { + // arrange + const string relatedTypeName = "related-models"; + const string relationshipName = "related-model"; + const int relatedId = 1; + _jsonApiContextMock + .Setup(m => m.ContextGraph) + .Returns(_options.ContextGraph); + + var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); + var entity = new Model + { + RelatedModelId = relatedId + }; + + // act + var document = documentBuilder.Build(entity); + // assert var relationshipData = document.Data.Relationships[relationshipName]; Assert.NotNull(relationshipData);