From e651a3e56dbdc7c2cf9b3f35fa171a2732096cc1 Mon Sep 17 00:00:00 2001 From: Rob Lankey Date: Mon, 12 Nov 2018 19:14:49 -0500 Subject: [PATCH 01/12] add rider to ignore --- .gitignore | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/.gitignore b/.gitignore index b6767ff903..ddb61683d3 100644 --- a/.gitignore +++ b/.gitignore @@ -234,3 +234,71 @@ _Pvt_Extensions # FAKE - F# Make .fake/ + +### Rider ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser \ No newline at end of file From b9923f3200dd39fa089e61a38425548833abda28 Mon Sep 17 00:00:00 2001 From: Rob Lankey Date: Mon, 12 Nov 2018 21:10:33 -0500 Subject: [PATCH 02/12] update git ignore to ignore all rider --- .gitignore | 67 +----------------------------------------------------- 1 file changed, 1 insertion(+), 66 deletions(-) diff --git a/.gitignore b/.gitignore index ddb61683d3..f3282bbc8c 100644 --- a/.gitignore +++ b/.gitignore @@ -236,69 +236,4 @@ _Pvt_Extensions .fake/ ### Rider ### -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf - -# Generated files -.idea/**/contentModel.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/modules.xml -# .idea/*.iml -# .idea/modules - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format -*.iws - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser \ No newline at end of file +.idea/ \ No newline at end of file From b395945fb314fb55c01281a3ec4362624f2a86e4 Mon Sep 17 00:00:00 2001 From: Rob Lankey Date: Mon, 12 Nov 2018 21:11:17 -0500 Subject: [PATCH 03/12] change Type to ResourceType and add EntityType that defaults to ResourceType. updated DefaultEntityRepository to correctly attach relationship when creating a resource --- .../Models/Resources/CourseResource.cs | 3 +- .../Builders/ContextGraphBuilder.cs | 19 +++----- .../Builders/DocumentBuilder.cs | 4 +- .../Data/DefaultEntityRepository.cs | 20 +++++--- .../Extensions/IQueryableExtensions.cs | 2 +- .../Internal/Query/BaseAttrQuery.cs | 4 +- .../Models/HasManyAttribute.cs | 5 +- .../Models/HasOneAttribute.cs | 11 +++-- .../Models/RelationshipAttribute.cs | 15 ++++-- .../Serialization/JsonApiDeSerializer.cs | 10 ++-- .../Services/EntityResourceService.cs | 6 +-- .../Operations/Processors/GetOpProcessor.cs | 2 +- .../Acceptance/AddTests.cs | 48 +++++++++++++++++++ 13 files changed, 103 insertions(+), 46 deletions(-) diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Resources/CourseResource.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Resources/CourseResource.cs index cacd9e11d1..4d5a92e99e 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Resources/CourseResource.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Resources/CourseResource.cs @@ -1,6 +1,7 @@ using JsonApiDotNetCore.Models; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using JsonApiDotNetCoreExample.Models.Entities; namespace JsonApiDotNetCoreExample.Models.Resources { @@ -17,7 +18,7 @@ public class CourseResource : Identifiable [Attr("description")] public string Description { get; set; } - [HasOne("department")] + [HasOne("department", withEntityType: typeof(DepartmentEntity))] public DepartmentResource Department { get; set; } public int? DepartmentId { get; set; } diff --git a/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs index 80ba077866..bc10c64ff7 100644 --- a/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs @@ -165,10 +165,10 @@ protected virtual List GetRelationships(Type entityType) attribute.PublicRelationshipName = attribute.PublicRelationshipName ?? JsonApiOptions.ResourceNameFormatter.FormatPropertyName(prop); attribute.InternalRelationshipName = prop.Name; - attribute.Type = GetRelationshipType(attribute, prop); + attribute.ResourceType = GetRelationshipType(attribute, prop); attributes.Add(attribute); - if(attribute is HasManyThroughAttribute hasManyThroughAttribute) { + if (attribute is HasManyThroughAttribute hasManyThroughAttribute) { var throughProperty = properties.SingleOrDefault(p => p.Name == hasManyThroughAttribute.InternalThroughName); if(throughProperty == null) throw new JsonApiSetupException($"Invalid '{nameof(HasManyThroughAttribute)}' on type '{entityType}'. Type does not contain a property named '{hasManyThroughAttribute.InternalThroughName}'."); @@ -198,26 +198,21 @@ protected virtual List GetRelationships(Type entityType) ?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a relationship id property to type {entityType} with name {leftIdPropertyName}"); // Article → ArticleTag.Tag - hasManyThroughAttribute.RightProperty = throughProperties.SingleOrDefault(x => x.PropertyType == hasManyThroughAttribute.Type) - ?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a navigation property to type {hasManyThroughAttribute.Type}"); + hasManyThroughAttribute.RightProperty = throughProperties.SingleOrDefault(x => x.PropertyType == hasManyThroughAttribute.ResourceType) + ?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a navigation property to type {hasManyThroughAttribute.ResourceType}"); // ArticleTag.TagId var rightIdPropertyName = JsonApiOptions.RelatedIdMapper.GetRelatedIdPropertyName(hasManyThroughAttribute.RightProperty.Name); hasManyThroughAttribute.RightIdProperty = throughProperties.SingleOrDefault(x => x.Name == rightIdPropertyName) - ?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a relationship id property to type {hasManyThroughAttribute.Type} with name {rightIdPropertyName}"); + ?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a relationship id property to type {hasManyThroughAttribute.ResourceType} with name {rightIdPropertyName}"); } } return attributes; } - protected virtual Type GetRelationshipType(RelationshipAttribute relation, PropertyInfo prop) - { - if (relation.IsHasMany) - return prop.PropertyType.GetGenericArguments()[0]; - else - return prop.PropertyType; - } + protected virtual Type GetRelationshipType(RelationshipAttribute relation, PropertyInfo prop) => + relation.IsHasMany ? prop.PropertyType.GetGenericArguments()[0] : prop.PropertyType; private Type GetResourceDefinitionType(Type entityType) => typeof(ResourceDefinition<>).MakeGenericType(entityType); diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index 2d2c978179..1dee426f79 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -240,7 +240,7 @@ private List IncludeSingleResourceRelationships( { if (relationshipChainIndex < relationshipChain.Length) { - var nextContextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(relationship.Type); + var nextContextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(relationship.ResourceType); var resource = (IIdentifiable)navigationEntity; // recursive call if (relationshipChainIndex < relationshipChain.Length - 1) @@ -327,7 +327,7 @@ private ResourceIdentifierObject GetIndependentRelationshipIdentifier(HasOneAttr if (independentRelationshipIdentifier == null) return null; - var relatedContextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(hasOne.Type); + var relatedContextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(hasOne.ResourceType); if (relatedContextEntity == null) // TODO: this should probably be a debug log at minimum return null; diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index b92667af95..8e69029f74 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -155,7 +155,7 @@ public virtual async Task CreateAsync(TEntity entity) protected virtual void AttachRelationships(TEntity entity = null) { AttachHasManyPointers(entity); - AttachHasOnePointers(); + AttachHasOnePointers(entity); } /// @@ -163,7 +163,7 @@ public void DetachRelationshipPointers(TEntity entity) { foreach (var hasOneRelationship in _jsonApiContext.HasOneRelationshipPointers.Get()) { - _context.Entry(hasOneRelationship.Value).State = EntityState.Detached; + _context.Entry(hasOneRelationship.Key.EntityType).State = EntityState.Detached; } foreach (var hasManyRelationship in _jsonApiContext.HasManyRelationshipPointers.Get()) @@ -227,12 +227,18 @@ private void AttachHasManyThrough(TEntity entity, HasManyThroughAttribute hasMan /// This is used to allow creation of HasOne relationships when the /// independent side of the relationship already exists. /// - private void AttachHasOnePointers() + private void AttachHasOnePointers(TEntity entity) { var relationships = _jsonApiContext.HasOneRelationshipPointers.Get(); foreach (var relationship in relationships) - if (_context.Entry(relationship.Value).State == EntityState.Detached && _context.EntityIsTracked(relationship.Value) == false) - _context.Entry(relationship.Value).State = EntityState.Unchanged; + { + var relatedEntityType = relationship.Key.EntityType; // DepartmentEntity + var relatedEntityMember = typeof(TEntity).GetProperties().SingleOrDefault(p => p.PropertyType == relatedEntityType)?.Name; + var relatedEntity = relatedEntityMember == null ? null : entity.GetType().GetProperty(relatedEntityMember)?.GetValue(entity); + + if (relatedEntity != null && _context.Entry(relatedEntity).State == EntityState.Detached && _context.EntityIsTracked((IIdentifiable)relatedEntity) == false) + _context.Entry(relatedEntity).State = EntityState.Unchanged; + } } /// @@ -265,7 +271,7 @@ public async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute // of the property... var typeToUpdate = (relationship is HasManyThroughAttribute hasManyThrough) ? hasManyThrough.ThroughType - : relationship.Type; + : relationship.ResourceType; var genericProcessor = _genericProcessorFactory.GetProcessor(typeof(GenericProcessor<>), typeToUpdate); await genericProcessor.UpdateRelationshipsAsync(parent, relationship, relationshipIds); @@ -317,7 +323,7 @@ public virtual IQueryable Include(IQueryable entities, string : $"{internalRelationshipPath}.{relationship.RelationshipPath}"; if(i < relationshipChain.Length) - entity = _jsonApiContext.ResourceGraph.GetContextEntity(relationship.Type); + entity = _jsonApiContext.ResourceGraph.GetContextEntity(relationship.ResourceType); } return entities.Include(internalRelationshipPath); diff --git a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs index fc0386da57..39e4ee590c 100644 --- a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs @@ -252,7 +252,7 @@ private static IQueryable CallGenericWhereMethod(IQueryable a.Is(attribute)); } } diff --git a/src/JsonApiDotNetCore/Models/HasManyAttribute.cs b/src/JsonApiDotNetCore/Models/HasManyAttribute.cs index 7dc4c9ae65..b23583220b 100644 --- a/src/JsonApiDotNetCore/Models/HasManyAttribute.cs +++ b/src/JsonApiDotNetCore/Models/HasManyAttribute.cs @@ -11,6 +11,7 @@ public class HasManyAttribute : RelationshipAttribute /// 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 + /// If the entity model of this relationship refers to a different type, specify that here /// /// /// @@ -23,8 +24,8 @@ public class HasManyAttribute : RelationshipAttribute /// /// /// - public HasManyAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true) - : base(publicName, documentLinks, canInclude) + public HasManyAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true, Type withEntityType = null) + : base(publicName, documentLinks, canInclude, withEntityType) { } /// diff --git a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs b/src/JsonApiDotNetCore/Models/HasOneAttribute.cs index 7e3cc6fb1a..998563b30b 100644 --- a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs +++ b/src/JsonApiDotNetCore/Models/HasOneAttribute.cs @@ -11,6 +11,7 @@ public class HasOneAttribute : RelationshipAttribute /// /// The relationship name as exposed by the API /// Which links are available. Defaults to + /// If the entity model of this relationship refers to a different type, specify that here /// Whether or not this relationship can be included using the ?include=public-name query string /// The foreign key property name. Defaults to "{RelationshipName}Id" /// @@ -20,21 +21,21 @@ public class HasOneAttribute : RelationshipAttribute /// /// public class Article : Identifiable /// { - /// [HasOne("author", withForiegnKey: nameof(AuthorKey)] + /// [HasOne("author", withForeignKey: nameof(AuthorKey)] /// public Author Author { get; set; } /// public int AuthorKey { get; set; } /// } /// /// /// - public HasOneAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true, string withForeignKey = null) - : base(publicName, documentLinks, canInclude) + public HasOneAttribute(string publicName = null, Link documentLinks = Link.All, Type withEntityType = null, bool canInclude = true, string withForeignKey = null) + : base(publicName, documentLinks, canInclude, withEntityType) { _explicitIdentifiablePropertyName = withForeignKey; } private readonly string _explicitIdentifiablePropertyName; - + /// /// The independent resource identifier. /// @@ -49,7 +50,7 @@ public HasOneAttribute(string publicName = null, Link documentLinks = Link.All, /// The new property value public override void SetValue(object resource, object newValue) { - var propertyName = (newValue?.GetType() == Type) + var propertyName = (newValue?.GetType() == ResourceType) ? InternalRelationshipName : IdentifiablePropertyName; diff --git a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs b/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs index facb7780d9..c6f99ec84e 100644 --- a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs +++ b/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs @@ -6,11 +6,12 @@ namespace JsonApiDotNetCore.Models { public abstract class RelationshipAttribute : Attribute { - protected RelationshipAttribute(string publicName, Link documentLinks, bool canInclude) + protected RelationshipAttribute(string publicName, Link documentLinks, bool canInclude, Type withEntityType) { PublicRelationshipName = publicName; DocumentLinks = documentLinks; CanInclude = canInclude; + _entityType = withEntityType; } public string PublicRelationshipName { get; internal set; } @@ -26,7 +27,11 @@ protected RelationshipAttribute(string publicName, Link documentLinks, bool canI /// public List<Tag> Tags { get; set; } // Type => Tag /// /// - public Type Type { get; internal set; } + public Type ResourceType { get; internal set; } + + private readonly Type _entityType; + public Type EntityType => _entityType == null ? ResourceType : _entityType; + public bool IsHasMany => GetType() == typeof(HasManyAttribute) || GetType().Inherits(typeof(HasManyAttribute)); public bool IsHasOne => GetType() == typeof(HasOneAttribute); public Link DocumentLinks { get; } = Link.All; @@ -55,10 +60,10 @@ public bool TryGetHasMany(out HasManyAttribute result) } public abstract void SetValue(object entity, object newValue); - + public object GetValue(object entity) => entity - ?.GetType() - .GetProperty(InternalRelationshipName) + ?.GetType()? + .GetProperty(InternalRelationshipName)? .GetValue(entity); public override string ToString() diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index 9c46c22500..0f43784d8e 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -218,7 +218,7 @@ private object SetHasOneRelationship(object entity, if(included != null) { var navigationPropertyValue = attr.GetValue(entity); - var resourceGraphEntity = _jsonApiContext.ResourceGraph.GetContextEntity(attr.Type); + var resourceGraphEntity = _jsonApiContext.ResourceGraph.GetContextEntity(attr.ResourceType); if(navigationPropertyValue != null && resourceGraphEntity != null) { var includedResource = included.SingleOrDefault(r => r.Type == rio.Type && r.Id == rio.Id); @@ -292,7 +292,7 @@ private object SetHasManyRelationship(object entity, return instance; }); - var convertedCollection = TypeHelper.ConvertCollection(relatedResources, attr.Type); + var convertedCollection = TypeHelper.ConvertCollection(relatedResources, attr.ResourceType); attr.SetValue(entity, convertedCollection); @@ -305,7 +305,7 @@ private object SetHasManyRelationship(object entity, private IIdentifiable GetIncludedRelationship(ResourceIdentifierObject relatedResourceIdentifier, List includedResources, RelationshipAttribute relationshipAttr) { // at this point we can be sure the relationshipAttr.Type is IIdentifiable because we were able to successfully build the ResourceGraph - var relatedInstance = relationshipAttr.Type.New(); + var relatedInstance = relationshipAttr.ResourceType.New(); relatedInstance.StringId = relatedResourceIdentifier.Id; // can't provide any more data other than the rio since it is not contained in the included section @@ -316,9 +316,9 @@ private IIdentifiable GetIncludedRelationship(ResourceIdentifierObject relatedRe if (includedResource == null) return relatedInstance; - var contextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(relationshipAttr.Type); + var contextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(relationshipAttr.ResourceType); if (contextEntity == null) - throw new JsonApiException(400, $"Included type '{relationshipAttr.Type}' is not a registered json:api resource."); + throw new JsonApiException(400, $"Included type '{relationshipAttr.ResourceType}' is not a registered json:api resource."); SetEntityAttributes(relatedInstance, contextEntity, includedResource.Attributes); diff --git a/src/JsonApiDotNetCore/Services/EntityResourceService.cs b/src/JsonApiDotNetCore/Services/EntityResourceService.cs index 63547eed80..991cb76e8e 100644 --- a/src/JsonApiDotNetCore/Services/EntityResourceService.cs +++ b/src/JsonApiDotNetCore/Services/EntityResourceService.cs @@ -170,7 +170,7 @@ public virtual async Task UpdateRelationshipsAsync(TId id, string relationshipNa .Relationships .FirstOrDefault(r => r.Is(relationshipName)); - var relationshipType = relationship.Type; + var relationshipType = relationship.ResourceType; // update relationship type with internalname var entityProperty = typeof(TEntity).GetProperty(relationship.InternalRelationshipName); @@ -180,7 +180,7 @@ public virtual async Task UpdateRelationshipsAsync(TId id, string relationshipNa $"could not be found on entity."); } - relationship.Type = relationship.IsHasMany + relationship.ResourceType = relationship.IsHasMany ? entityProperty.PropertyType.GetGenericArguments()[0] : entityProperty.PropertyType; @@ -188,7 +188,7 @@ public virtual async Task UpdateRelationshipsAsync(TId id, string relationshipNa await _entities.UpdateRelationshipsAsync(entity, relationship, relationshipIds); - relationship.Type = relationshipType; + relationship.ResourceType = relationshipType; } protected virtual async Task> ApplyPageQueryAsync(IQueryable entities) diff --git a/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs b/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs index 0551b0a3cd..a139199012 100644 --- a/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs +++ b/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs @@ -135,7 +135,7 @@ private async Task GetRelationshipAsync(Operation operation) // TODO: need a better way to get the ContextEntity from a relationship name // when no generic parameter is available var relationshipType = _resourceGraph.GetContextEntity(operation.GetResourceTypeName()) - .Relationships.Single(r => r.Is(operation.Ref.Relationship)).Type; + .Relationships.Single(r => r.Is(operation.Ref.Relationship)).ResourceType; var relatedContextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(relationshipType); diff --git a/test/ResourceEntitySeparationExampleTests/Acceptance/AddTests.cs b/test/ResourceEntitySeparationExampleTests/Acceptance/AddTests.cs index 87e0a9d1ce..da8bfd27aa 100644 --- a/test/ResourceEntitySeparationExampleTests/Acceptance/AddTests.cs +++ b/test/ResourceEntitySeparationExampleTests/Acceptance/AddTests.cs @@ -46,6 +46,54 @@ public async Task Can_Create_Course() Assert.Equal(course.Title, data.Title); Assert.Equal(course.Description, data.Description); } + + [Fact] + public async Task Can_Create_Course_With_Department_Id() + { + // arrange + var route = $"/api/v1/courses/"; + var course = _fixture.CourseFaker.Generate(); + + var department = _fixture.DepartmentFaker.Generate(); + _fixture.Context.Departments.Add(department); + _fixture.Context.SaveChanges(); + + var content = new + { + data = new + { + type = "courses", + attributes = new Dictionary + { + { "number", course.Number }, + { "title", course.Title }, + { "description", course.Description } + }, + relationships = new + { + department = new + { + data = new + { + type = "departments", + id = department.Id + } + } + } + } + }; + + // act + var (response, data) = await _fixture.PostAsync(route, content); + + // assert + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + Assert.NotNull(data); + Assert.Equal(course.Number, data.Number); + Assert.Equal(course.Title, data.Title); + Assert.Equal(course.Description, data.Description); + Assert.Equal(department.Id, data.DepartmentId); + } [Fact] public async Task Can_Create_Department() From 649648f0bd5d7d9d57f8cefef68e6951bc8a8c4e Mon Sep 17 00:00:00 2001 From: Rob Lankey Date: Mon, 12 Nov 2018 21:13:44 -0500 Subject: [PATCH 04/12] bump package version? --- src/JsonApiDotNetCore/JsonApiDotNetCore.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index 183a7c8084..844effdda7 100644 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -1,6 +1,6 @@ - 3.0.0 + 3.0.1 $(NetStandardVersion) JsonApiDotNetCore JsonApiDotNetCore From a7a22bb3d2037c3fd31b3758741533e183d3afa2 Mon Sep 17 00:00:00 2001 From: Rob Lankey Date: Mon, 12 Nov 2018 22:00:16 -0500 Subject: [PATCH 05/12] re-change the typing since it was causing issues when the entity had more than one property with that type --- .../Models/Resources/CourseResource.cs | 2 +- .../Builders/ContextGraphBuilder.cs | 8 ++-- .../Builders/DocumentBuilder.cs | 4 +- .../Data/DefaultEntityRepository.cs | 37 ++++++++++++++----- .../Extensions/IQueryableExtensions.cs | 2 +- .../Internal/Query/BaseAttrQuery.cs | 2 +- .../Models/HasManyAttribute.cs | 5 +-- .../Models/HasOneAttribute.cs | 15 ++++++-- .../Models/RelationshipAttribute.cs | 9 +---- .../Serialization/JsonApiDeSerializer.cs | 10 ++--- .../Services/EntityResourceService.cs | 6 +-- .../Operations/Processors/GetOpProcessor.cs | 2 +- 12 files changed, 61 insertions(+), 41 deletions(-) diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Resources/CourseResource.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Resources/CourseResource.cs index 4d5a92e99e..c9ee7347f5 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Resources/CourseResource.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Resources/CourseResource.cs @@ -18,7 +18,7 @@ public class CourseResource : Identifiable [Attr("description")] public string Description { get; set; } - [HasOne("department", withEntityType: typeof(DepartmentEntity))] + [HasOne("department", withEntity: "Department")] public DepartmentResource Department { get; set; } public int? DepartmentId { get; set; } diff --git a/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs index bc10c64ff7..ca6caad1f8 100644 --- a/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs @@ -165,7 +165,7 @@ protected virtual List GetRelationships(Type entityType) attribute.PublicRelationshipName = attribute.PublicRelationshipName ?? JsonApiOptions.ResourceNameFormatter.FormatPropertyName(prop); attribute.InternalRelationshipName = prop.Name; - attribute.ResourceType = GetRelationshipType(attribute, prop); + attribute.Type = GetRelationshipType(attribute, prop); attributes.Add(attribute); if (attribute is HasManyThroughAttribute hasManyThroughAttribute) { @@ -198,13 +198,13 @@ protected virtual List GetRelationships(Type entityType) ?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a relationship id property to type {entityType} with name {leftIdPropertyName}"); // Article → ArticleTag.Tag - hasManyThroughAttribute.RightProperty = throughProperties.SingleOrDefault(x => x.PropertyType == hasManyThroughAttribute.ResourceType) - ?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a navigation property to type {hasManyThroughAttribute.ResourceType}"); + hasManyThroughAttribute.RightProperty = throughProperties.SingleOrDefault(x => x.PropertyType == hasManyThroughAttribute.Type) + ?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a navigation property to type {hasManyThroughAttribute.Type}"); // ArticleTag.TagId var rightIdPropertyName = JsonApiOptions.RelatedIdMapper.GetRelatedIdPropertyName(hasManyThroughAttribute.RightProperty.Name); hasManyThroughAttribute.RightIdProperty = throughProperties.SingleOrDefault(x => x.Name == rightIdPropertyName) - ?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a relationship id property to type {hasManyThroughAttribute.ResourceType} with name {rightIdPropertyName}"); + ?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a relationship id property to type {hasManyThroughAttribute.Type} with name {rightIdPropertyName}"); } } diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index 1dee426f79..2d2c978179 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -240,7 +240,7 @@ private List IncludeSingleResourceRelationships( { if (relationshipChainIndex < relationshipChain.Length) { - var nextContextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(relationship.ResourceType); + var nextContextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(relationship.Type); var resource = (IIdentifiable)navigationEntity; // recursive call if (relationshipChainIndex < relationshipChain.Length - 1) @@ -327,7 +327,7 @@ private ResourceIdentifierObject GetIndependentRelationshipIdentifier(HasOneAttr if (independentRelationshipIdentifier == null) return null; - var relatedContextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(hasOne.ResourceType); + var relatedContextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(hasOne.Type); if (relatedContextEntity == null) // TODO: this should probably be a debug log at minimum return null; diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index 8e69029f74..690f2f8902 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -163,7 +163,17 @@ public void DetachRelationshipPointers(TEntity entity) { foreach (var hasOneRelationship in _jsonApiContext.HasOneRelationshipPointers.Get()) { - _context.Entry(hasOneRelationship.Key.EntityType).State = EntityState.Detached; + var hasOne = (HasOneAttribute) hasOneRelationship.Key; + if (hasOne.EntityPropertyName != null) + { + var relatedEntity = entity.GetType().GetProperty(hasOne.EntityPropertyName)?.GetValue(entity); + if (relatedEntity != null) + _context.Entry(relatedEntity).State = EntityState.Detached; + } + else + { + _context.Entry(hasOneRelationship.Value).State = EntityState.Detached; + } } foreach (var hasManyRelationship in _jsonApiContext.HasManyRelationshipPointers.Get()) @@ -232,12 +242,21 @@ private void AttachHasOnePointers(TEntity entity) var relationships = _jsonApiContext.HasOneRelationshipPointers.Get(); foreach (var relationship in relationships) { - var relatedEntityType = relationship.Key.EntityType; // DepartmentEntity - var relatedEntityMember = typeof(TEntity).GetProperties().SingleOrDefault(p => p.PropertyType == relatedEntityType)?.Name; - var relatedEntity = relatedEntityMember == null ? null : entity.GetType().GetProperty(relatedEntityMember)?.GetValue(entity); - - if (relatedEntity != null && _context.Entry(relatedEntity).State == EntityState.Detached && _context.EntityIsTracked((IIdentifiable)relatedEntity) == false) - _context.Entry(relatedEntity).State = EntityState.Unchanged; + if (relationship.Key.GetType() != typeof(HasOneAttribute)) + continue; + + var hasOne = (HasOneAttribute) relationship.Key; + if (hasOne.EntityPropertyName != null) + { + var relatedEntity = entity.GetType().GetProperty(hasOne.EntityPropertyName)?.GetValue(entity); + if (relatedEntity != null && _context.Entry(relatedEntity).State == EntityState.Detached && _context.EntityIsTracked((IIdentifiable)relatedEntity) == false) + _context.Entry(relatedEntity).State = EntityState.Unchanged; + } + else + { + if (_context.Entry(relationship.Value).State == EntityState.Detached && _context.EntityIsTracked(relationship.Value) == false) + _context.Entry(relationship.Value).State = EntityState.Unchanged; + } } } @@ -271,7 +290,7 @@ public async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute // of the property... var typeToUpdate = (relationship is HasManyThroughAttribute hasManyThrough) ? hasManyThrough.ThroughType - : relationship.ResourceType; + : relationship.Type; var genericProcessor = _genericProcessorFactory.GetProcessor(typeof(GenericProcessor<>), typeToUpdate); await genericProcessor.UpdateRelationshipsAsync(parent, relationship, relationshipIds); @@ -323,7 +342,7 @@ public virtual IQueryable Include(IQueryable entities, string : $"{internalRelationshipPath}.{relationship.RelationshipPath}"; if(i < relationshipChain.Length) - entity = _jsonApiContext.ResourceGraph.GetContextEntity(relationship.ResourceType); + entity = _jsonApiContext.ResourceGraph.GetContextEntity(relationship.Type); } return entities.Include(internalRelationshipPath); diff --git a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs index 39e4ee590c..fc0386da57 100644 --- a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs @@ -252,7 +252,7 @@ private static IQueryable CallGenericWhereMethod(IQueryable a.Is(attribute)); } diff --git a/src/JsonApiDotNetCore/Models/HasManyAttribute.cs b/src/JsonApiDotNetCore/Models/HasManyAttribute.cs index b23583220b..7dc4c9ae65 100644 --- a/src/JsonApiDotNetCore/Models/HasManyAttribute.cs +++ b/src/JsonApiDotNetCore/Models/HasManyAttribute.cs @@ -11,7 +11,6 @@ public class HasManyAttribute : RelationshipAttribute /// 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 - /// If the entity model of this relationship refers to a different type, specify that here /// /// /// @@ -24,8 +23,8 @@ public class HasManyAttribute : RelationshipAttribute /// /// /// - public HasManyAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true, Type withEntityType = null) - : base(publicName, documentLinks, canInclude, withEntityType) + public HasManyAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true) + : base(publicName, documentLinks, canInclude) { } /// diff --git a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs b/src/JsonApiDotNetCore/Models/HasOneAttribute.cs index 998563b30b..a306fc10cf 100644 --- a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs +++ b/src/JsonApiDotNetCore/Models/HasOneAttribute.cs @@ -11,9 +11,9 @@ public class HasOneAttribute : RelationshipAttribute /// /// The relationship name as exposed by the API /// Which links are available. Defaults to - /// If the entity model of this relationship refers to a different type, specify that here /// Whether or not this relationship can be included using the ?include=public-name query string /// The foreign key property name. Defaults to "{RelationshipName}Id" + /// If the entity model of this relationship refers to a different type, specify that here /// /// /// Using an alternative foreign key: @@ -28,13 +28,15 @@ public class HasOneAttribute : RelationshipAttribute /// /// /// - public HasOneAttribute(string publicName = null, Link documentLinks = Link.All, Type withEntityType = null, bool canInclude = true, string withForeignKey = null) - : base(publicName, documentLinks, canInclude, withEntityType) + public HasOneAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true, string withForeignKey = null, string withEntity = null) + : base(publicName, documentLinks, canInclude) { _explicitIdentifiablePropertyName = withForeignKey; + EntityPropertyName = withEntity; } private readonly string _explicitIdentifiablePropertyName; + private readonly string _relatedEntityPropertyName; /// /// The independent resource identifier. @@ -43,6 +45,11 @@ public HasOneAttribute(string publicName = null, Link documentLinks = Link.All, ? JsonApiOptions.RelatedIdMapper.GetRelatedIdPropertyName(InternalRelationshipName) : _explicitIdentifiablePropertyName; + /// + /// For use in entity / resource separation when the related property is also separated + /// + public string EntityPropertyName { get; } + /// /// Sets the value of the property identified by this attribute /// @@ -50,7 +57,7 @@ public HasOneAttribute(string publicName = null, Link documentLinks = Link.All, /// The new property value public override void SetValue(object resource, object newValue) { - var propertyName = (newValue?.GetType() == ResourceType) + var propertyName = (newValue?.GetType() == Type) ? InternalRelationshipName : IdentifiablePropertyName; diff --git a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs b/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs index c6f99ec84e..9646493210 100644 --- a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs +++ b/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs @@ -6,12 +6,11 @@ namespace JsonApiDotNetCore.Models { public abstract class RelationshipAttribute : Attribute { - protected RelationshipAttribute(string publicName, Link documentLinks, bool canInclude, Type withEntityType) + protected RelationshipAttribute(string publicName, Link documentLinks, bool canInclude) { PublicRelationshipName = publicName; DocumentLinks = documentLinks; CanInclude = canInclude; - _entityType = withEntityType; } public string PublicRelationshipName { get; internal set; } @@ -27,11 +26,7 @@ protected RelationshipAttribute(string publicName, Link documentLinks, bool canI /// public List<Tag> Tags { get; set; } // Type => Tag /// /// - public Type ResourceType { get; internal set; } - - private readonly Type _entityType; - public Type EntityType => _entityType == null ? ResourceType : _entityType; - + public Type Type { get; internal set; } public bool IsHasMany => GetType() == typeof(HasManyAttribute) || GetType().Inherits(typeof(HasManyAttribute)); public bool IsHasOne => GetType() == typeof(HasOneAttribute); public Link DocumentLinks { get; } = Link.All; diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index 0f43784d8e..9c46c22500 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -218,7 +218,7 @@ private object SetHasOneRelationship(object entity, if(included != null) { var navigationPropertyValue = attr.GetValue(entity); - var resourceGraphEntity = _jsonApiContext.ResourceGraph.GetContextEntity(attr.ResourceType); + var resourceGraphEntity = _jsonApiContext.ResourceGraph.GetContextEntity(attr.Type); if(navigationPropertyValue != null && resourceGraphEntity != null) { var includedResource = included.SingleOrDefault(r => r.Type == rio.Type && r.Id == rio.Id); @@ -292,7 +292,7 @@ private object SetHasManyRelationship(object entity, return instance; }); - var convertedCollection = TypeHelper.ConvertCollection(relatedResources, attr.ResourceType); + var convertedCollection = TypeHelper.ConvertCollection(relatedResources, attr.Type); attr.SetValue(entity, convertedCollection); @@ -305,7 +305,7 @@ private object SetHasManyRelationship(object entity, private IIdentifiable GetIncludedRelationship(ResourceIdentifierObject relatedResourceIdentifier, List includedResources, RelationshipAttribute relationshipAttr) { // at this point we can be sure the relationshipAttr.Type is IIdentifiable because we were able to successfully build the ResourceGraph - var relatedInstance = relationshipAttr.ResourceType.New(); + var relatedInstance = relationshipAttr.Type.New(); relatedInstance.StringId = relatedResourceIdentifier.Id; // can't provide any more data other than the rio since it is not contained in the included section @@ -316,9 +316,9 @@ private IIdentifiable GetIncludedRelationship(ResourceIdentifierObject relatedRe if (includedResource == null) return relatedInstance; - var contextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(relationshipAttr.ResourceType); + var contextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(relationshipAttr.Type); if (contextEntity == null) - throw new JsonApiException(400, $"Included type '{relationshipAttr.ResourceType}' is not a registered json:api resource."); + throw new JsonApiException(400, $"Included type '{relationshipAttr.Type}' is not a registered json:api resource."); SetEntityAttributes(relatedInstance, contextEntity, includedResource.Attributes); diff --git a/src/JsonApiDotNetCore/Services/EntityResourceService.cs b/src/JsonApiDotNetCore/Services/EntityResourceService.cs index 991cb76e8e..63547eed80 100644 --- a/src/JsonApiDotNetCore/Services/EntityResourceService.cs +++ b/src/JsonApiDotNetCore/Services/EntityResourceService.cs @@ -170,7 +170,7 @@ public virtual async Task UpdateRelationshipsAsync(TId id, string relationshipNa .Relationships .FirstOrDefault(r => r.Is(relationshipName)); - var relationshipType = relationship.ResourceType; + var relationshipType = relationship.Type; // update relationship type with internalname var entityProperty = typeof(TEntity).GetProperty(relationship.InternalRelationshipName); @@ -180,7 +180,7 @@ public virtual async Task UpdateRelationshipsAsync(TId id, string relationshipNa $"could not be found on entity."); } - relationship.ResourceType = relationship.IsHasMany + relationship.Type = relationship.IsHasMany ? entityProperty.PropertyType.GetGenericArguments()[0] : entityProperty.PropertyType; @@ -188,7 +188,7 @@ public virtual async Task UpdateRelationshipsAsync(TId id, string relationshipNa await _entities.UpdateRelationshipsAsync(entity, relationship, relationshipIds); - relationship.ResourceType = relationshipType; + relationship.Type = relationshipType; } protected virtual async Task> ApplyPageQueryAsync(IQueryable entities) diff --git a/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs b/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs index a139199012..0551b0a3cd 100644 --- a/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs +++ b/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs @@ -135,7 +135,7 @@ private async Task GetRelationshipAsync(Operation operation) // TODO: need a better way to get the ContextEntity from a relationship name // when no generic parameter is available var relationshipType = _resourceGraph.GetContextEntity(operation.GetResourceTypeName()) - .Relationships.Single(r => r.Is(operation.Ref.Relationship)).ResourceType; + .Relationships.Single(r => r.Is(operation.Ref.Relationship)).Type; var relatedContextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(relationshipType); From 0938c33e59d643df2f7173649272f6f47ec5819b Mon Sep 17 00:00:00 2001 From: Rob Lankey Date: Mon, 12 Nov 2018 19:14:49 -0500 Subject: [PATCH 06/12] add rider to ignore --- .gitignore | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/.gitignore b/.gitignore index b6767ff903..ddb61683d3 100644 --- a/.gitignore +++ b/.gitignore @@ -234,3 +234,71 @@ _Pvt_Extensions # FAKE - F# Make .fake/ + +### Rider ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser \ No newline at end of file From d51365824b57082e879160c228dbbfd30f95d3e3 Mon Sep 17 00:00:00 2001 From: Rob Lankey Date: Mon, 12 Nov 2018 21:10:33 -0500 Subject: [PATCH 07/12] update git ignore to ignore all rider --- .gitignore | 67 +----------------------------------------------------- 1 file changed, 1 insertion(+), 66 deletions(-) diff --git a/.gitignore b/.gitignore index ddb61683d3..f3282bbc8c 100644 --- a/.gitignore +++ b/.gitignore @@ -236,69 +236,4 @@ _Pvt_Extensions .fake/ ### Rider ### -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf - -# Generated files -.idea/**/contentModel.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/modules.xml -# .idea/*.iml -# .idea/modules - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format -*.iws - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser \ No newline at end of file +.idea/ \ No newline at end of file From 32338693d715c41be45f309ca338d18f3166e365 Mon Sep 17 00:00:00 2001 From: Rob Lankey Date: Mon, 12 Nov 2018 21:11:17 -0500 Subject: [PATCH 08/12] change Type to ResourceType and add EntityType that defaults to ResourceType. updated DefaultEntityRepository to correctly attach relationship when creating a resource --- .../Models/Resources/CourseResource.cs | 3 +- .../Builders/ContextGraphBuilder.cs | 19 +++----- .../Builders/DocumentBuilder.cs | 4 +- .../Data/DefaultEntityRepository.cs | 20 +++++--- .../Extensions/IQueryableExtensions.cs | 2 +- .../Models/HasManyAttribute.cs | 5 +- .../Models/HasOneAttribute.cs | 11 +++-- .../Models/RelationshipAttribute.cs | 15 ++++-- .../Serialization/JsonApiDeSerializer.cs | 10 ++-- .../Services/EntityResourceService.cs | 6 +-- .../Operations/Processors/GetOpProcessor.cs | 2 +- .../Acceptance/AddTests.cs | 48 +++++++++++++++++++ 12 files changed, 101 insertions(+), 44 deletions(-) diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Resources/CourseResource.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Resources/CourseResource.cs index cacd9e11d1..4d5a92e99e 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Resources/CourseResource.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Resources/CourseResource.cs @@ -1,6 +1,7 @@ using JsonApiDotNetCore.Models; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using JsonApiDotNetCoreExample.Models.Entities; namespace JsonApiDotNetCoreExample.Models.Resources { @@ -17,7 +18,7 @@ public class CourseResource : Identifiable [Attr("description")] public string Description { get; set; } - [HasOne("department")] + [HasOne("department", withEntityType: typeof(DepartmentEntity))] public DepartmentResource Department { get; set; } public int? DepartmentId { get; set; } diff --git a/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs index 80ba077866..bc10c64ff7 100644 --- a/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs @@ -165,10 +165,10 @@ protected virtual List GetRelationships(Type entityType) attribute.PublicRelationshipName = attribute.PublicRelationshipName ?? JsonApiOptions.ResourceNameFormatter.FormatPropertyName(prop); attribute.InternalRelationshipName = prop.Name; - attribute.Type = GetRelationshipType(attribute, prop); + attribute.ResourceType = GetRelationshipType(attribute, prop); attributes.Add(attribute); - if(attribute is HasManyThroughAttribute hasManyThroughAttribute) { + if (attribute is HasManyThroughAttribute hasManyThroughAttribute) { var throughProperty = properties.SingleOrDefault(p => p.Name == hasManyThroughAttribute.InternalThroughName); if(throughProperty == null) throw new JsonApiSetupException($"Invalid '{nameof(HasManyThroughAttribute)}' on type '{entityType}'. Type does not contain a property named '{hasManyThroughAttribute.InternalThroughName}'."); @@ -198,26 +198,21 @@ protected virtual List GetRelationships(Type entityType) ?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a relationship id property to type {entityType} with name {leftIdPropertyName}"); // Article → ArticleTag.Tag - hasManyThroughAttribute.RightProperty = throughProperties.SingleOrDefault(x => x.PropertyType == hasManyThroughAttribute.Type) - ?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a navigation property to type {hasManyThroughAttribute.Type}"); + hasManyThroughAttribute.RightProperty = throughProperties.SingleOrDefault(x => x.PropertyType == hasManyThroughAttribute.ResourceType) + ?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a navigation property to type {hasManyThroughAttribute.ResourceType}"); // ArticleTag.TagId var rightIdPropertyName = JsonApiOptions.RelatedIdMapper.GetRelatedIdPropertyName(hasManyThroughAttribute.RightProperty.Name); hasManyThroughAttribute.RightIdProperty = throughProperties.SingleOrDefault(x => x.Name == rightIdPropertyName) - ?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a relationship id property to type {hasManyThroughAttribute.Type} with name {rightIdPropertyName}"); + ?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a relationship id property to type {hasManyThroughAttribute.ResourceType} with name {rightIdPropertyName}"); } } return attributes; } - protected virtual Type GetRelationshipType(RelationshipAttribute relation, PropertyInfo prop) - { - if (relation.IsHasMany) - return prop.PropertyType.GetGenericArguments()[0]; - else - return prop.PropertyType; - } + protected virtual Type GetRelationshipType(RelationshipAttribute relation, PropertyInfo prop) => + relation.IsHasMany ? prop.PropertyType.GetGenericArguments()[0] : prop.PropertyType; private Type GetResourceDefinitionType(Type entityType) => typeof(ResourceDefinition<>).MakeGenericType(entityType); diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index 1287f35a05..a292f62bbc 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -242,7 +242,7 @@ private List IncludeSingleResourceRelationships( { if (relationshipChainIndex < relationshipChain.Length) { - var nextContextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(relationship.Type); + var nextContextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(relationship.ResourceType); var resource = (IIdentifiable)navigationEntity; // recursive call if (relationshipChainIndex < relationshipChain.Length - 1) @@ -329,7 +329,7 @@ private ResourceIdentifierObject GetIndependentRelationshipIdentifier(HasOneAttr if (independentRelationshipIdentifier == null) return null; - var relatedContextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(hasOne.Type); + var relatedContextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(hasOne.ResourceType); if (relatedContextEntity == null) // TODO: this should probably be a debug log at minimum return null; diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index b92667af95..8e69029f74 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -155,7 +155,7 @@ public virtual async Task CreateAsync(TEntity entity) protected virtual void AttachRelationships(TEntity entity = null) { AttachHasManyPointers(entity); - AttachHasOnePointers(); + AttachHasOnePointers(entity); } /// @@ -163,7 +163,7 @@ public void DetachRelationshipPointers(TEntity entity) { foreach (var hasOneRelationship in _jsonApiContext.HasOneRelationshipPointers.Get()) { - _context.Entry(hasOneRelationship.Value).State = EntityState.Detached; + _context.Entry(hasOneRelationship.Key.EntityType).State = EntityState.Detached; } foreach (var hasManyRelationship in _jsonApiContext.HasManyRelationshipPointers.Get()) @@ -227,12 +227,18 @@ private void AttachHasManyThrough(TEntity entity, HasManyThroughAttribute hasMan /// This is used to allow creation of HasOne relationships when the /// independent side of the relationship already exists. /// - private void AttachHasOnePointers() + private void AttachHasOnePointers(TEntity entity) { var relationships = _jsonApiContext.HasOneRelationshipPointers.Get(); foreach (var relationship in relationships) - if (_context.Entry(relationship.Value).State == EntityState.Detached && _context.EntityIsTracked(relationship.Value) == false) - _context.Entry(relationship.Value).State = EntityState.Unchanged; + { + var relatedEntityType = relationship.Key.EntityType; // DepartmentEntity + var relatedEntityMember = typeof(TEntity).GetProperties().SingleOrDefault(p => p.PropertyType == relatedEntityType)?.Name; + var relatedEntity = relatedEntityMember == null ? null : entity.GetType().GetProperty(relatedEntityMember)?.GetValue(entity); + + if (relatedEntity != null && _context.Entry(relatedEntity).State == EntityState.Detached && _context.EntityIsTracked((IIdentifiable)relatedEntity) == false) + _context.Entry(relatedEntity).State = EntityState.Unchanged; + } } /// @@ -265,7 +271,7 @@ public async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute // of the property... var typeToUpdate = (relationship is HasManyThroughAttribute hasManyThrough) ? hasManyThrough.ThroughType - : relationship.Type; + : relationship.ResourceType; var genericProcessor = _genericProcessorFactory.GetProcessor(typeof(GenericProcessor<>), typeToUpdate); await genericProcessor.UpdateRelationshipsAsync(parent, relationship, relationshipIds); @@ -317,7 +323,7 @@ public virtual IQueryable Include(IQueryable entities, string : $"{internalRelationshipPath}.{relationship.RelationshipPath}"; if(i < relationshipChain.Length) - entity = _jsonApiContext.ResourceGraph.GetContextEntity(relationship.Type); + entity = _jsonApiContext.ResourceGraph.GetContextEntity(relationship.ResourceType); } return entities.Include(internalRelationshipPath); diff --git a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs index af33e883a6..0aee5318d3 100644 --- a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs @@ -254,7 +254,7 @@ private static IQueryable CallGenericWhereMethod(IQueryableThe 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 + /// If the entity model of this relationship refers to a different type, specify that here /// /// /// @@ -23,8 +24,8 @@ public class HasManyAttribute : RelationshipAttribute /// /// /// - public HasManyAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true) - : base(publicName, documentLinks, canInclude) + public HasManyAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true, Type withEntityType = null) + : base(publicName, documentLinks, canInclude, withEntityType) { } /// diff --git a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs b/src/JsonApiDotNetCore/Models/HasOneAttribute.cs index 7e3cc6fb1a..998563b30b 100644 --- a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs +++ b/src/JsonApiDotNetCore/Models/HasOneAttribute.cs @@ -11,6 +11,7 @@ public class HasOneAttribute : RelationshipAttribute /// /// The relationship name as exposed by the API /// Which links are available. Defaults to + /// If the entity model of this relationship refers to a different type, specify that here /// Whether or not this relationship can be included using the ?include=public-name query string /// The foreign key property name. Defaults to "{RelationshipName}Id" /// @@ -20,21 +21,21 @@ public class HasOneAttribute : RelationshipAttribute /// /// public class Article : Identifiable /// { - /// [HasOne("author", withForiegnKey: nameof(AuthorKey)] + /// [HasOne("author", withForeignKey: nameof(AuthorKey)] /// public Author Author { get; set; } /// public int AuthorKey { get; set; } /// } /// /// /// - public HasOneAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true, string withForeignKey = null) - : base(publicName, documentLinks, canInclude) + public HasOneAttribute(string publicName = null, Link documentLinks = Link.All, Type withEntityType = null, bool canInclude = true, string withForeignKey = null) + : base(publicName, documentLinks, canInclude, withEntityType) { _explicitIdentifiablePropertyName = withForeignKey; } private readonly string _explicitIdentifiablePropertyName; - + /// /// The independent resource identifier. /// @@ -49,7 +50,7 @@ public HasOneAttribute(string publicName = null, Link documentLinks = Link.All, /// The new property value public override void SetValue(object resource, object newValue) { - var propertyName = (newValue?.GetType() == Type) + var propertyName = (newValue?.GetType() == ResourceType) ? InternalRelationshipName : IdentifiablePropertyName; diff --git a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs b/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs index facb7780d9..c6f99ec84e 100644 --- a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs +++ b/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs @@ -6,11 +6,12 @@ namespace JsonApiDotNetCore.Models { public abstract class RelationshipAttribute : Attribute { - protected RelationshipAttribute(string publicName, Link documentLinks, bool canInclude) + protected RelationshipAttribute(string publicName, Link documentLinks, bool canInclude, Type withEntityType) { PublicRelationshipName = publicName; DocumentLinks = documentLinks; CanInclude = canInclude; + _entityType = withEntityType; } public string PublicRelationshipName { get; internal set; } @@ -26,7 +27,11 @@ protected RelationshipAttribute(string publicName, Link documentLinks, bool canI /// public List<Tag> Tags { get; set; } // Type => Tag /// /// - public Type Type { get; internal set; } + public Type ResourceType { get; internal set; } + + private readonly Type _entityType; + public Type EntityType => _entityType == null ? ResourceType : _entityType; + public bool IsHasMany => GetType() == typeof(HasManyAttribute) || GetType().Inherits(typeof(HasManyAttribute)); public bool IsHasOne => GetType() == typeof(HasOneAttribute); public Link DocumentLinks { get; } = Link.All; @@ -55,10 +60,10 @@ public bool TryGetHasMany(out HasManyAttribute result) } public abstract void SetValue(object entity, object newValue); - + public object GetValue(object entity) => entity - ?.GetType() - .GetProperty(InternalRelationshipName) + ?.GetType()? + .GetProperty(InternalRelationshipName)? .GetValue(entity); public override string ToString() diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index 9c46c22500..0f43784d8e 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -218,7 +218,7 @@ private object SetHasOneRelationship(object entity, if(included != null) { var navigationPropertyValue = attr.GetValue(entity); - var resourceGraphEntity = _jsonApiContext.ResourceGraph.GetContextEntity(attr.Type); + var resourceGraphEntity = _jsonApiContext.ResourceGraph.GetContextEntity(attr.ResourceType); if(navigationPropertyValue != null && resourceGraphEntity != null) { var includedResource = included.SingleOrDefault(r => r.Type == rio.Type && r.Id == rio.Id); @@ -292,7 +292,7 @@ private object SetHasManyRelationship(object entity, return instance; }); - var convertedCollection = TypeHelper.ConvertCollection(relatedResources, attr.Type); + var convertedCollection = TypeHelper.ConvertCollection(relatedResources, attr.ResourceType); attr.SetValue(entity, convertedCollection); @@ -305,7 +305,7 @@ private object SetHasManyRelationship(object entity, private IIdentifiable GetIncludedRelationship(ResourceIdentifierObject relatedResourceIdentifier, List includedResources, RelationshipAttribute relationshipAttr) { // at this point we can be sure the relationshipAttr.Type is IIdentifiable because we were able to successfully build the ResourceGraph - var relatedInstance = relationshipAttr.Type.New(); + var relatedInstance = relationshipAttr.ResourceType.New(); relatedInstance.StringId = relatedResourceIdentifier.Id; // can't provide any more data other than the rio since it is not contained in the included section @@ -316,9 +316,9 @@ private IIdentifiable GetIncludedRelationship(ResourceIdentifierObject relatedRe if (includedResource == null) return relatedInstance; - var contextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(relationshipAttr.Type); + var contextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(relationshipAttr.ResourceType); if (contextEntity == null) - throw new JsonApiException(400, $"Included type '{relationshipAttr.Type}' is not a registered json:api resource."); + throw new JsonApiException(400, $"Included type '{relationshipAttr.ResourceType}' is not a registered json:api resource."); SetEntityAttributes(relatedInstance, contextEntity, includedResource.Attributes); diff --git a/src/JsonApiDotNetCore/Services/EntityResourceService.cs b/src/JsonApiDotNetCore/Services/EntityResourceService.cs index 53cfc7a7c1..7891beff95 100644 --- a/src/JsonApiDotNetCore/Services/EntityResourceService.cs +++ b/src/JsonApiDotNetCore/Services/EntityResourceService.cs @@ -170,7 +170,7 @@ public virtual async Task UpdateRelationshipsAsync(TId id, string relationshipNa .Relationships .FirstOrDefault(r => r.Is(relationshipName)); - var relationshipType = relationship.Type; + var relationshipType = relationship.ResourceType; // update relationship type with internalname var entityProperty = typeof(TEntity).GetProperty(relationship.InternalRelationshipName); @@ -180,7 +180,7 @@ public virtual async Task UpdateRelationshipsAsync(TId id, string relationshipNa $"could not be found on entity."); } - relationship.Type = relationship.IsHasMany + relationship.ResourceType = relationship.IsHasMany ? entityProperty.PropertyType.GetGenericArguments()[0] : entityProperty.PropertyType; @@ -188,7 +188,7 @@ public virtual async Task UpdateRelationshipsAsync(TId id, string relationshipNa await _entities.UpdateRelationshipsAsync(entity, relationship, relationshipIds); - relationship.Type = relationshipType; + relationship.ResourceType = relationshipType; } protected virtual async Task> ApplyPageQueryAsync(IQueryable entities) diff --git a/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs b/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs index 0551b0a3cd..a139199012 100644 --- a/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs +++ b/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs @@ -135,7 +135,7 @@ private async Task GetRelationshipAsync(Operation operation) // TODO: need a better way to get the ContextEntity from a relationship name // when no generic parameter is available var relationshipType = _resourceGraph.GetContextEntity(operation.GetResourceTypeName()) - .Relationships.Single(r => r.Is(operation.Ref.Relationship)).Type; + .Relationships.Single(r => r.Is(operation.Ref.Relationship)).ResourceType; var relatedContextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(relationshipType); diff --git a/test/ResourceEntitySeparationExampleTests/Acceptance/AddTests.cs b/test/ResourceEntitySeparationExampleTests/Acceptance/AddTests.cs index 87e0a9d1ce..da8bfd27aa 100644 --- a/test/ResourceEntitySeparationExampleTests/Acceptance/AddTests.cs +++ b/test/ResourceEntitySeparationExampleTests/Acceptance/AddTests.cs @@ -46,6 +46,54 @@ public async Task Can_Create_Course() Assert.Equal(course.Title, data.Title); Assert.Equal(course.Description, data.Description); } + + [Fact] + public async Task Can_Create_Course_With_Department_Id() + { + // arrange + var route = $"/api/v1/courses/"; + var course = _fixture.CourseFaker.Generate(); + + var department = _fixture.DepartmentFaker.Generate(); + _fixture.Context.Departments.Add(department); + _fixture.Context.SaveChanges(); + + var content = new + { + data = new + { + type = "courses", + attributes = new Dictionary + { + { "number", course.Number }, + { "title", course.Title }, + { "description", course.Description } + }, + relationships = new + { + department = new + { + data = new + { + type = "departments", + id = department.Id + } + } + } + } + }; + + // act + var (response, data) = await _fixture.PostAsync(route, content); + + // assert + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + Assert.NotNull(data); + Assert.Equal(course.Number, data.Number); + Assert.Equal(course.Title, data.Title); + Assert.Equal(course.Description, data.Description); + Assert.Equal(department.Id, data.DepartmentId); + } [Fact] public async Task Can_Create_Department() From f1a0cf69cf5a8aa44edab5f2a061aa4d03bfc354 Mon Sep 17 00:00:00 2001 From: Rob Lankey Date: Mon, 12 Nov 2018 21:13:44 -0500 Subject: [PATCH 09/12] bump package version? --- src/JsonApiDotNetCore/JsonApiDotNetCore.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index 183a7c8084..844effdda7 100644 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -1,6 +1,6 @@ - 3.0.0 + 3.0.1 $(NetStandardVersion) JsonApiDotNetCore JsonApiDotNetCore From 6bc818722216dbfefc687e94ca72d9eeba85fd81 Mon Sep 17 00:00:00 2001 From: Rob Lankey Date: Mon, 12 Nov 2018 22:00:16 -0500 Subject: [PATCH 10/12] re-change the typing since it was causing issues when the entity had more than one property with that type --- .../Models/Resources/CourseResource.cs | 2 +- .../Builders/ContextGraphBuilder.cs | 8 ++-- .../Builders/DocumentBuilder.cs | 4 +- .../Data/DefaultEntityRepository.cs | 37 ++++++++++++++----- .../Extensions/IQueryableExtensions.cs | 2 +- .../Models/HasManyAttribute.cs | 5 +-- .../Models/HasOneAttribute.cs | 15 ++++++-- .../Models/RelationshipAttribute.cs | 9 +---- .../Serialization/JsonApiDeSerializer.cs | 10 ++--- .../Services/EntityResourceService.cs | 6 +-- .../Operations/Processors/GetOpProcessor.cs | 2 +- 11 files changed, 60 insertions(+), 40 deletions(-) diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Resources/CourseResource.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Resources/CourseResource.cs index 4d5a92e99e..c9ee7347f5 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Resources/CourseResource.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Resources/CourseResource.cs @@ -18,7 +18,7 @@ public class CourseResource : Identifiable [Attr("description")] public string Description { get; set; } - [HasOne("department", withEntityType: typeof(DepartmentEntity))] + [HasOne("department", withEntity: "Department")] public DepartmentResource Department { get; set; } public int? DepartmentId { get; set; } diff --git a/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs index bc10c64ff7..ca6caad1f8 100644 --- a/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs @@ -165,7 +165,7 @@ protected virtual List GetRelationships(Type entityType) attribute.PublicRelationshipName = attribute.PublicRelationshipName ?? JsonApiOptions.ResourceNameFormatter.FormatPropertyName(prop); attribute.InternalRelationshipName = prop.Name; - attribute.ResourceType = GetRelationshipType(attribute, prop); + attribute.Type = GetRelationshipType(attribute, prop); attributes.Add(attribute); if (attribute is HasManyThroughAttribute hasManyThroughAttribute) { @@ -198,13 +198,13 @@ protected virtual List GetRelationships(Type entityType) ?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a relationship id property to type {entityType} with name {leftIdPropertyName}"); // Article → ArticleTag.Tag - hasManyThroughAttribute.RightProperty = throughProperties.SingleOrDefault(x => x.PropertyType == hasManyThroughAttribute.ResourceType) - ?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a navigation property to type {hasManyThroughAttribute.ResourceType}"); + hasManyThroughAttribute.RightProperty = throughProperties.SingleOrDefault(x => x.PropertyType == hasManyThroughAttribute.Type) + ?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a navigation property to type {hasManyThroughAttribute.Type}"); // ArticleTag.TagId var rightIdPropertyName = JsonApiOptions.RelatedIdMapper.GetRelatedIdPropertyName(hasManyThroughAttribute.RightProperty.Name); hasManyThroughAttribute.RightIdProperty = throughProperties.SingleOrDefault(x => x.Name == rightIdPropertyName) - ?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a relationship id property to type {hasManyThroughAttribute.ResourceType} with name {rightIdPropertyName}"); + ?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a relationship id property to type {hasManyThroughAttribute.Type} with name {rightIdPropertyName}"); } } diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index a292f62bbc..1287f35a05 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -242,7 +242,7 @@ private List IncludeSingleResourceRelationships( { if (relationshipChainIndex < relationshipChain.Length) { - var nextContextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(relationship.ResourceType); + var nextContextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(relationship.Type); var resource = (IIdentifiable)navigationEntity; // recursive call if (relationshipChainIndex < relationshipChain.Length - 1) @@ -329,7 +329,7 @@ private ResourceIdentifierObject GetIndependentRelationshipIdentifier(HasOneAttr if (independentRelationshipIdentifier == null) return null; - var relatedContextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(hasOne.ResourceType); + var relatedContextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(hasOne.Type); if (relatedContextEntity == null) // TODO: this should probably be a debug log at minimum return null; diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index 8e69029f74..690f2f8902 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -163,7 +163,17 @@ public void DetachRelationshipPointers(TEntity entity) { foreach (var hasOneRelationship in _jsonApiContext.HasOneRelationshipPointers.Get()) { - _context.Entry(hasOneRelationship.Key.EntityType).State = EntityState.Detached; + var hasOne = (HasOneAttribute) hasOneRelationship.Key; + if (hasOne.EntityPropertyName != null) + { + var relatedEntity = entity.GetType().GetProperty(hasOne.EntityPropertyName)?.GetValue(entity); + if (relatedEntity != null) + _context.Entry(relatedEntity).State = EntityState.Detached; + } + else + { + _context.Entry(hasOneRelationship.Value).State = EntityState.Detached; + } } foreach (var hasManyRelationship in _jsonApiContext.HasManyRelationshipPointers.Get()) @@ -232,12 +242,21 @@ private void AttachHasOnePointers(TEntity entity) var relationships = _jsonApiContext.HasOneRelationshipPointers.Get(); foreach (var relationship in relationships) { - var relatedEntityType = relationship.Key.EntityType; // DepartmentEntity - var relatedEntityMember = typeof(TEntity).GetProperties().SingleOrDefault(p => p.PropertyType == relatedEntityType)?.Name; - var relatedEntity = relatedEntityMember == null ? null : entity.GetType().GetProperty(relatedEntityMember)?.GetValue(entity); - - if (relatedEntity != null && _context.Entry(relatedEntity).State == EntityState.Detached && _context.EntityIsTracked((IIdentifiable)relatedEntity) == false) - _context.Entry(relatedEntity).State = EntityState.Unchanged; + if (relationship.Key.GetType() != typeof(HasOneAttribute)) + continue; + + var hasOne = (HasOneAttribute) relationship.Key; + if (hasOne.EntityPropertyName != null) + { + var relatedEntity = entity.GetType().GetProperty(hasOne.EntityPropertyName)?.GetValue(entity); + if (relatedEntity != null && _context.Entry(relatedEntity).State == EntityState.Detached && _context.EntityIsTracked((IIdentifiable)relatedEntity) == false) + _context.Entry(relatedEntity).State = EntityState.Unchanged; + } + else + { + if (_context.Entry(relationship.Value).State == EntityState.Detached && _context.EntityIsTracked(relationship.Value) == false) + _context.Entry(relationship.Value).State = EntityState.Unchanged; + } } } @@ -271,7 +290,7 @@ public async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute // of the property... var typeToUpdate = (relationship is HasManyThroughAttribute hasManyThrough) ? hasManyThrough.ThroughType - : relationship.ResourceType; + : relationship.Type; var genericProcessor = _genericProcessorFactory.GetProcessor(typeof(GenericProcessor<>), typeToUpdate); await genericProcessor.UpdateRelationshipsAsync(parent, relationship, relationshipIds); @@ -323,7 +342,7 @@ public virtual IQueryable Include(IQueryable entities, string : $"{internalRelationshipPath}.{relationship.RelationshipPath}"; if(i < relationshipChain.Length) - entity = _jsonApiContext.ResourceGraph.GetContextEntity(relationship.ResourceType); + entity = _jsonApiContext.ResourceGraph.GetContextEntity(relationship.Type); } return entities.Include(internalRelationshipPath); diff --git a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs index 0aee5318d3..af33e883a6 100644 --- a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs @@ -254,7 +254,7 @@ private static IQueryable CallGenericWhereMethod(IQueryableThe 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 - /// If the entity model of this relationship refers to a different type, specify that here /// /// /// @@ -24,8 +23,8 @@ public class HasManyAttribute : RelationshipAttribute /// /// /// - public HasManyAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true, Type withEntityType = null) - : base(publicName, documentLinks, canInclude, withEntityType) + public HasManyAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true) + : base(publicName, documentLinks, canInclude) { } /// diff --git a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs b/src/JsonApiDotNetCore/Models/HasOneAttribute.cs index 998563b30b..a306fc10cf 100644 --- a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs +++ b/src/JsonApiDotNetCore/Models/HasOneAttribute.cs @@ -11,9 +11,9 @@ public class HasOneAttribute : RelationshipAttribute /// /// The relationship name as exposed by the API /// Which links are available. Defaults to - /// If the entity model of this relationship refers to a different type, specify that here /// Whether or not this relationship can be included using the ?include=public-name query string /// The foreign key property name. Defaults to "{RelationshipName}Id" + /// If the entity model of this relationship refers to a different type, specify that here /// /// /// Using an alternative foreign key: @@ -28,13 +28,15 @@ public class HasOneAttribute : RelationshipAttribute /// /// /// - public HasOneAttribute(string publicName = null, Link documentLinks = Link.All, Type withEntityType = null, bool canInclude = true, string withForeignKey = null) - : base(publicName, documentLinks, canInclude, withEntityType) + public HasOneAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true, string withForeignKey = null, string withEntity = null) + : base(publicName, documentLinks, canInclude) { _explicitIdentifiablePropertyName = withForeignKey; + EntityPropertyName = withEntity; } private readonly string _explicitIdentifiablePropertyName; + private readonly string _relatedEntityPropertyName; /// /// The independent resource identifier. @@ -43,6 +45,11 @@ public HasOneAttribute(string publicName = null, Link documentLinks = Link.All, ? JsonApiOptions.RelatedIdMapper.GetRelatedIdPropertyName(InternalRelationshipName) : _explicitIdentifiablePropertyName; + /// + /// For use in entity / resource separation when the related property is also separated + /// + public string EntityPropertyName { get; } + /// /// Sets the value of the property identified by this attribute /// @@ -50,7 +57,7 @@ public HasOneAttribute(string publicName = null, Link documentLinks = Link.All, /// The new property value public override void SetValue(object resource, object newValue) { - var propertyName = (newValue?.GetType() == ResourceType) + var propertyName = (newValue?.GetType() == Type) ? InternalRelationshipName : IdentifiablePropertyName; diff --git a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs b/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs index c6f99ec84e..9646493210 100644 --- a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs +++ b/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs @@ -6,12 +6,11 @@ namespace JsonApiDotNetCore.Models { public abstract class RelationshipAttribute : Attribute { - protected RelationshipAttribute(string publicName, Link documentLinks, bool canInclude, Type withEntityType) + protected RelationshipAttribute(string publicName, Link documentLinks, bool canInclude) { PublicRelationshipName = publicName; DocumentLinks = documentLinks; CanInclude = canInclude; - _entityType = withEntityType; } public string PublicRelationshipName { get; internal set; } @@ -27,11 +26,7 @@ protected RelationshipAttribute(string publicName, Link documentLinks, bool canI /// public List<Tag> Tags { get; set; } // Type => Tag /// /// - public Type ResourceType { get; internal set; } - - private readonly Type _entityType; - public Type EntityType => _entityType == null ? ResourceType : _entityType; - + public Type Type { get; internal set; } public bool IsHasMany => GetType() == typeof(HasManyAttribute) || GetType().Inherits(typeof(HasManyAttribute)); public bool IsHasOne => GetType() == typeof(HasOneAttribute); public Link DocumentLinks { get; } = Link.All; diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index 0f43784d8e..9c46c22500 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -218,7 +218,7 @@ private object SetHasOneRelationship(object entity, if(included != null) { var navigationPropertyValue = attr.GetValue(entity); - var resourceGraphEntity = _jsonApiContext.ResourceGraph.GetContextEntity(attr.ResourceType); + var resourceGraphEntity = _jsonApiContext.ResourceGraph.GetContextEntity(attr.Type); if(navigationPropertyValue != null && resourceGraphEntity != null) { var includedResource = included.SingleOrDefault(r => r.Type == rio.Type && r.Id == rio.Id); @@ -292,7 +292,7 @@ private object SetHasManyRelationship(object entity, return instance; }); - var convertedCollection = TypeHelper.ConvertCollection(relatedResources, attr.ResourceType); + var convertedCollection = TypeHelper.ConvertCollection(relatedResources, attr.Type); attr.SetValue(entity, convertedCollection); @@ -305,7 +305,7 @@ private object SetHasManyRelationship(object entity, private IIdentifiable GetIncludedRelationship(ResourceIdentifierObject relatedResourceIdentifier, List includedResources, RelationshipAttribute relationshipAttr) { // at this point we can be sure the relationshipAttr.Type is IIdentifiable because we were able to successfully build the ResourceGraph - var relatedInstance = relationshipAttr.ResourceType.New(); + var relatedInstance = relationshipAttr.Type.New(); relatedInstance.StringId = relatedResourceIdentifier.Id; // can't provide any more data other than the rio since it is not contained in the included section @@ -316,9 +316,9 @@ private IIdentifiable GetIncludedRelationship(ResourceIdentifierObject relatedRe if (includedResource == null) return relatedInstance; - var contextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(relationshipAttr.ResourceType); + var contextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(relationshipAttr.Type); if (contextEntity == null) - throw new JsonApiException(400, $"Included type '{relationshipAttr.ResourceType}' is not a registered json:api resource."); + throw new JsonApiException(400, $"Included type '{relationshipAttr.Type}' is not a registered json:api resource."); SetEntityAttributes(relatedInstance, contextEntity, includedResource.Attributes); diff --git a/src/JsonApiDotNetCore/Services/EntityResourceService.cs b/src/JsonApiDotNetCore/Services/EntityResourceService.cs index 7891beff95..53cfc7a7c1 100644 --- a/src/JsonApiDotNetCore/Services/EntityResourceService.cs +++ b/src/JsonApiDotNetCore/Services/EntityResourceService.cs @@ -170,7 +170,7 @@ public virtual async Task UpdateRelationshipsAsync(TId id, string relationshipNa .Relationships .FirstOrDefault(r => r.Is(relationshipName)); - var relationshipType = relationship.ResourceType; + var relationshipType = relationship.Type; // update relationship type with internalname var entityProperty = typeof(TEntity).GetProperty(relationship.InternalRelationshipName); @@ -180,7 +180,7 @@ public virtual async Task UpdateRelationshipsAsync(TId id, string relationshipNa $"could not be found on entity."); } - relationship.ResourceType = relationship.IsHasMany + relationship.Type = relationship.IsHasMany ? entityProperty.PropertyType.GetGenericArguments()[0] : entityProperty.PropertyType; @@ -188,7 +188,7 @@ public virtual async Task UpdateRelationshipsAsync(TId id, string relationshipNa await _entities.UpdateRelationshipsAsync(entity, relationship, relationshipIds); - relationship.ResourceType = relationshipType; + relationship.Type = relationshipType; } protected virtual async Task> ApplyPageQueryAsync(IQueryable entities) diff --git a/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs b/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs index a139199012..0551b0a3cd 100644 --- a/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs +++ b/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs @@ -135,7 +135,7 @@ private async Task GetRelationshipAsync(Operation operation) // TODO: need a better way to get the ContextEntity from a relationship name // when no generic parameter is available var relationshipType = _resourceGraph.GetContextEntity(operation.GetResourceTypeName()) - .Relationships.Single(r => r.Is(operation.Ref.Relationship)).ResourceType; + .Relationships.Single(r => r.Is(operation.Ref.Relationship)).Type; var relatedContextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(relationshipType); From 5ee3068e8dd0ff05f104e06093194349d02b68db Mon Sep 17 00:00:00 2001 From: Rob Lankey Date: Sun, 18 Nov 2018 03:56:01 -0500 Subject: [PATCH 11/12] add entity check to hasmany --- .../Models/Resources/CourseResource.cs | 2 +- .../Models/Resources/DepartmentResource.cs | 2 +- .../Data/DefaultEntityRepository.cs | 39 ++++++++++--- .../Models/HasManyAttribute.cs | 5 +- .../Models/HasManyThroughAttribute.cs | 13 +++-- .../Models/HasOneAttribute.cs | 13 +---- .../Models/RelationshipAttribute.cs | 6 +- .../Acceptance/AddTests.cs | 58 +++++++++++++++++++ 8 files changed, 110 insertions(+), 28 deletions(-) diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Resources/CourseResource.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Resources/CourseResource.cs index c9ee7347f5..e981c70cc9 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Resources/CourseResource.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Resources/CourseResource.cs @@ -18,7 +18,7 @@ public class CourseResource : Identifiable [Attr("description")] public string Description { get; set; } - [HasOne("department", withEntity: "Department")] + [HasOne("department", mappedBy: "Department")] public DepartmentResource Department { get; set; } public int? DepartmentId { get; set; } diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Resources/DepartmentResource.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Resources/DepartmentResource.cs index d9d6725b71..47648afcdf 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Resources/DepartmentResource.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Resources/DepartmentResource.cs @@ -8,7 +8,7 @@ public class DepartmentResource : Identifiable [Attr("name")] public string Name { get; set; } - [HasMany("courses")] + [HasMany("courses", mappedBy: "Courses")] public List Courses { get; set; } } } diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index 690f2f8902..1ddced3d26 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -178,11 +178,23 @@ public void DetachRelationshipPointers(TEntity entity) foreach (var hasManyRelationship in _jsonApiContext.HasManyRelationshipPointers.Get()) { - foreach (var pointer in hasManyRelationship.Value) + var hasMany = (HasOneAttribute) hasManyRelationship.Key; + if (hasMany.EntityPropertyName != null) { - _context.Entry(pointer).State = EntityState.Detached; + var relatedList = (IList)entity.GetType().GetProperty(hasMany.EntityPropertyName)?.GetValue(entity); + foreach (var related in relatedList) + { + _context.Entry(related).State = EntityState.Detached; + } } - + else + { + foreach (var pointer in hasManyRelationship.Value) + { + _context.Entry(pointer).State = EntityState.Detached; + } + } + // HACK: detaching has many relationships doesn't appear to be sufficient // the navigation property actually needs to be nulled out, otherwise // EF adds duplicate instances to the collection @@ -202,14 +214,27 @@ private void AttachHasManyPointers(TEntity entity) if (relationship.Key is HasManyThroughAttribute hasManyThrough) AttachHasManyThrough(entity, hasManyThrough, relationship.Value); else - AttachHasMany(relationship.Key as HasManyAttribute, relationship.Value); + AttachHasMany(entity, relationship.Key as HasManyAttribute, relationship.Value); } } - private void AttachHasMany(HasManyAttribute relationship, IList pointers) + private void AttachHasMany(TEntity entity, HasManyAttribute relationship, IList pointers) { - foreach (var pointer in pointers) - _context.Entry(pointer).State = EntityState.Unchanged; + if (relationship.EntityPropertyName != null) + { + var relatedList = (IList)entity.GetType().GetProperty(relationship.EntityPropertyName)?.GetValue(entity); + foreach (var related in relatedList) + { + _context.Entry(related).State = EntityState.Unchanged; + } + } + else + { + foreach (var pointer in pointers) + { + _context.Entry(pointer).State = EntityState.Unchanged; + } + } } private void AttachHasManyThrough(TEntity entity, HasManyThroughAttribute hasManyThrough, IList pointers) diff --git a/src/JsonApiDotNetCore/Models/HasManyAttribute.cs b/src/JsonApiDotNetCore/Models/HasManyAttribute.cs index 7dc4c9ae65..2d69b0e9c7 100644 --- a/src/JsonApiDotNetCore/Models/HasManyAttribute.cs +++ b/src/JsonApiDotNetCore/Models/HasManyAttribute.cs @@ -11,6 +11,7 @@ public class HasManyAttribute : RelationshipAttribute /// 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 name of the entity mapped property, defaults to null /// /// /// @@ -23,8 +24,8 @@ public class HasManyAttribute : RelationshipAttribute /// /// /// - public HasManyAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true) - : base(publicName, documentLinks, canInclude) + public HasManyAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true, string mappedBy = null) + : base(publicName, documentLinks, canInclude, mappedBy) { } /// diff --git a/src/JsonApiDotNetCore/Models/HasManyThroughAttribute.cs b/src/JsonApiDotNetCore/Models/HasManyThroughAttribute.cs index 49e18a43b3..c6f5bc47db 100644 --- a/src/JsonApiDotNetCore/Models/HasManyThroughAttribute.cs +++ b/src/JsonApiDotNetCore/Models/HasManyThroughAttribute.cs @@ -1,5 +1,6 @@ using System; using System.Reflection; +using System.Security; namespace JsonApiDotNetCore.Models { @@ -30,14 +31,15 @@ public class HasManyThroughAttribute : HasManyAttribute /// The name of the navigation property that will be used to get the HasMany relationship /// Which links are available. Defaults to /// Whether or not this relationship can be included using the ?include=public-name query string + /// The name of the entity mapped property, defaults to null /// /// /// /// [HasManyThrough(nameof(ArticleTags), documentLinks: Link.All, canInclude: true)] /// /// - public HasManyThroughAttribute(string internalThroughName, Link documentLinks = Link.All, bool canInclude = true) - : base(null, documentLinks, canInclude) + public HasManyThroughAttribute(string internalThroughName, Link documentLinks = Link.All, bool canInclude = true, string mappedBy = null) + : base(null, documentLinks, canInclude, mappedBy) { InternalThroughName = internalThroughName; } @@ -50,14 +52,15 @@ public HasManyThroughAttribute(string internalThroughName, Link documentLinks = /// The name of the navigation property that will be used to get the HasMany relationship /// Which links are available. Defaults to /// Whether or not this relationship can be included using the ?include=public-name query string + /// The name of the entity mapped property, defaults to null /// /// /// /// [HasManyThrough("tags", nameof(ArticleTags), documentLinks: Link.All, canInclude: true)] /// /// - public HasManyThroughAttribute(string publicName, string internalThroughName, Link documentLinks = Link.All, bool canInclude = true) - : base(publicName, documentLinks, canInclude) + public HasManyThroughAttribute(string publicName, string internalThroughName, Link documentLinks = Link.All, bool canInclude = true, string mappedBy = null) + : base(publicName, documentLinks, canInclude, mappedBy) { InternalThroughName = internalThroughName; } @@ -161,4 +164,4 @@ public HasManyThroughAttribute(string publicName, string internalThroughName, Li /// public override string RelationshipPath => $"{InternalThroughName}.{RightProperty.Name}"; } -} \ No newline at end of file +} diff --git a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs b/src/JsonApiDotNetCore/Models/HasOneAttribute.cs index a306fc10cf..45b08590b3 100644 --- a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs +++ b/src/JsonApiDotNetCore/Models/HasOneAttribute.cs @@ -13,7 +13,7 @@ public class HasOneAttribute : RelationshipAttribute /// 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" - /// If the entity model of this relationship refers to a different type, specify that here + /// The name of the entity mapped property, defaults to null /// /// /// Using an alternative foreign key: @@ -28,15 +28,13 @@ public class HasOneAttribute : RelationshipAttribute /// /// /// - public HasOneAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true, string withForeignKey = null, string withEntity = null) - : base(publicName, documentLinks, canInclude) + public HasOneAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true, string withForeignKey = null, string mappedBy = null) + : base(publicName, documentLinks, canInclude, mappedBy) { _explicitIdentifiablePropertyName = withForeignKey; - EntityPropertyName = withEntity; } private readonly string _explicitIdentifiablePropertyName; - private readonly string _relatedEntityPropertyName; /// /// The independent resource identifier. @@ -45,11 +43,6 @@ public HasOneAttribute(string publicName = null, Link documentLinks = Link.All, ? JsonApiOptions.RelatedIdMapper.GetRelatedIdPropertyName(InternalRelationshipName) : _explicitIdentifiablePropertyName; - /// - /// For use in entity / resource separation when the related property is also separated - /// - public string EntityPropertyName { get; } - /// /// Sets the value of the property identified by this attribute /// diff --git a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs b/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs index 9646493210..bc6982f062 100644 --- a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs +++ b/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs @@ -6,15 +6,16 @@ namespace JsonApiDotNetCore.Models { public abstract class RelationshipAttribute : Attribute { - protected RelationshipAttribute(string publicName, Link documentLinks, bool canInclude) + protected RelationshipAttribute(string publicName, Link documentLinks, bool canInclude, string mappedBy) { PublicRelationshipName = publicName; DocumentLinks = documentLinks; CanInclude = canInclude; + EntityPropertyName = mappedBy; } public string PublicRelationshipName { get; internal set; } - public string InternalRelationshipName { get; internal set; } + public string InternalRelationshipName { get; internal set; } /// /// The related entity type. This does not necessarily match the navigation property type. @@ -31,6 +32,7 @@ protected RelationshipAttribute(string publicName, Link documentLinks, bool canI public bool IsHasOne => GetType() == typeof(HasOneAttribute); public Link DocumentLinks { get; } = Link.All; public bool CanInclude { get; } + public string EntityPropertyName { get; } public bool TryGetHasOne(out HasOneAttribute result) { diff --git a/test/ResourceEntitySeparationExampleTests/Acceptance/AddTests.cs b/test/ResourceEntitySeparationExampleTests/Acceptance/AddTests.cs index da8bfd27aa..f50ad734b7 100644 --- a/test/ResourceEntitySeparationExampleTests/Acceptance/AddTests.cs +++ b/test/ResourceEntitySeparationExampleTests/Acceptance/AddTests.cs @@ -1,7 +1,9 @@ using JsonApiDotNetCoreExample.Models.Resources; using System.Collections.Generic; +using System.Linq; using System.Net; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization.Infrastructure; using Xunit; namespace ResourceEntitySeparationExampleTests.Acceptance @@ -121,6 +123,62 @@ public async Task Can_Create_Department() Assert.NotNull(data); Assert.Equal(dept.Name, data.Name); } + + [Fact] + public async Task Can_Create_Department_With_Courses() + { + // arrange + var route = $"/api/v1/departments/"; + var dept = _fixture.DepartmentFaker.Generate(); + + var one = _fixture.CourseFaker.Generate(); + var two = _fixture.CourseFaker.Generate(); + _fixture.Context.Courses.Add(one); + _fixture.Context.Courses.Add(two); + _fixture.Context.SaveChanges(); + + var content = new + { + data = new + { + type = "departments", + attributes = new Dictionary + { + { "name", dept.Name } + }, + relationships = new + { + courses = new + { + data = new[] + { + new + { + type = "courses", + id = one.Id + }, + new + { + type = "courses", + id = two.Id + } + } + } + } + } + }; + + // act + var (response, data) = await _fixture.PostAsync(route, content); + + // assert + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + Assert.NotNull(data); + Assert.Equal(dept.Name, data.Name); + Assert.NotEmpty(data.Courses); + Assert.NotNull(data.Courses.SingleOrDefault(c => c.Id == one.Id)); + Assert.NotNull(data.Courses.SingleOrDefault(c => c.Id == two.Id)); + } [Fact] public async Task Can_Create_Student() From bf3cec422a32bb41a9cd78d64204d711cb4ed14c Mon Sep 17 00:00:00 2001 From: Rob Lankey Date: Sun, 18 Nov 2018 03:59:13 -0500 Subject: [PATCH 12/12] fix copy pasta --- src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index 1ddced3d26..fd3a8b48e8 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -178,7 +178,7 @@ public void DetachRelationshipPointers(TEntity entity) foreach (var hasManyRelationship in _jsonApiContext.HasManyRelationshipPointers.Get()) { - var hasMany = (HasOneAttribute) hasManyRelationship.Key; + var hasMany = (HasManyAttribute) hasManyRelationship.Key; if (hasMany.EntityPropertyName != null) { var relatedList = (IList)entity.GetType().GetProperty(hasMany.EntityPropertyName)?.GetValue(entity);