Skip to content

Commit e58a48d

Browse files
authored
Merge pull request #446 from roblankey/fix/#445
fix/#445: create resource with relationship in Entity-Resource separation mode
2 parents cd02b20 + bf3cec4 commit e58a48d

File tree

11 files changed

+201
-39
lines changed

11 files changed

+201
-39
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -234,3 +234,6 @@ _Pvt_Extensions
234234

235235
# FAKE - F# Make
236236
.fake/
237+
238+
### Rider ###
239+
.idea/

src/Examples/JsonApiDotNetCoreExample/Models/Resources/CourseResource.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using JsonApiDotNetCore.Models;
22
using System.Collections.Generic;
33
using System.ComponentModel.DataAnnotations;
4+
using JsonApiDotNetCoreExample.Models.Entities;
45

56
namespace JsonApiDotNetCoreExample.Models.Resources
67
{
@@ -17,7 +18,7 @@ public class CourseResource : Identifiable
1718
[Attr("description")]
1819
public string Description { get; set; }
1920

20-
[HasOne("department")]
21+
[HasOne("department", mappedBy: "Department")]
2122
public DepartmentResource Department { get; set; }
2223
public int? DepartmentId { get; set; }
2324

src/Examples/JsonApiDotNetCoreExample/Models/Resources/DepartmentResource.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public class DepartmentResource : Identifiable
88
[Attr("name")]
99
public string Name { get; set; }
1010

11-
[HasMany("courses")]
11+
[HasMany("courses", mappedBy: "Courses")]
1212
public List<CourseResource> Courses { get; set; }
1313
}
1414
}

src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs

+3-8
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ protected virtual List<RelationshipAttribute> GetRelationships(Type entityType)
168168
attribute.Type = GetRelationshipType(attribute, prop);
169169
attributes.Add(attribute);
170170

171-
if(attribute is HasManyThroughAttribute hasManyThroughAttribute) {
171+
if (attribute is HasManyThroughAttribute hasManyThroughAttribute) {
172172
var throughProperty = properties.SingleOrDefault(p => p.Name == hasManyThroughAttribute.InternalThroughName);
173173
if(throughProperty == null)
174174
throw new JsonApiSetupException($"Invalid '{nameof(HasManyThroughAttribute)}' on type '{entityType}'. Type does not contain a property named '{hasManyThroughAttribute.InternalThroughName}'.");
@@ -211,13 +211,8 @@ protected virtual List<RelationshipAttribute> GetRelationships(Type entityType)
211211
return attributes;
212212
}
213213

214-
protected virtual Type GetRelationshipType(RelationshipAttribute relation, PropertyInfo prop)
215-
{
216-
if (relation.IsHasMany)
217-
return prop.PropertyType.GetGenericArguments()[0];
218-
else
219-
return prop.PropertyType;
220-
}
214+
protected virtual Type GetRelationshipType(RelationshipAttribute relation, PropertyInfo prop) =>
215+
relation.IsHasMany ? prop.PropertyType.GetGenericArguments()[0] : prop.PropertyType;
221216

222217
private Type GetResourceDefinitionType(Type entityType) => typeof(ResourceDefinition<>).MakeGenericType(entityType);
223218

src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs

+62-12
Original file line numberDiff line numberDiff line change
@@ -155,24 +155,46 @@ public virtual async Task<TEntity> CreateAsync(TEntity entity)
155155
protected virtual void AttachRelationships(TEntity entity = null)
156156
{
157157
AttachHasManyPointers(entity);
158-
AttachHasOnePointers();
158+
AttachHasOnePointers(entity);
159159
}
160160

161161
/// <inheritdoc />
162162
public void DetachRelationshipPointers(TEntity entity)
163163
{
164164
foreach (var hasOneRelationship in _jsonApiContext.HasOneRelationshipPointers.Get())
165165
{
166-
_context.Entry(hasOneRelationship.Value).State = EntityState.Detached;
166+
var hasOne = (HasOneAttribute) hasOneRelationship.Key;
167+
if (hasOne.EntityPropertyName != null)
168+
{
169+
var relatedEntity = entity.GetType().GetProperty(hasOne.EntityPropertyName)?.GetValue(entity);
170+
if (relatedEntity != null)
171+
_context.Entry(relatedEntity).State = EntityState.Detached;
172+
}
173+
else
174+
{
175+
_context.Entry(hasOneRelationship.Value).State = EntityState.Detached;
176+
}
167177
}
168178

169179
foreach (var hasManyRelationship in _jsonApiContext.HasManyRelationshipPointers.Get())
170180
{
171-
foreach (var pointer in hasManyRelationship.Value)
181+
var hasMany = (HasManyAttribute) hasManyRelationship.Key;
182+
if (hasMany.EntityPropertyName != null)
172183
{
173-
_context.Entry(pointer).State = EntityState.Detached;
184+
var relatedList = (IList)entity.GetType().GetProperty(hasMany.EntityPropertyName)?.GetValue(entity);
185+
foreach (var related in relatedList)
186+
{
187+
_context.Entry(related).State = EntityState.Detached;
188+
}
174189
}
175-
190+
else
191+
{
192+
foreach (var pointer in hasManyRelationship.Value)
193+
{
194+
_context.Entry(pointer).State = EntityState.Detached;
195+
}
196+
}
197+
176198
// HACK: detaching has many relationships doesn't appear to be sufficient
177199
// the navigation property actually needs to be nulled out, otherwise
178200
// EF adds duplicate instances to the collection
@@ -192,14 +214,27 @@ private void AttachHasManyPointers(TEntity entity)
192214
if (relationship.Key is HasManyThroughAttribute hasManyThrough)
193215
AttachHasManyThrough(entity, hasManyThrough, relationship.Value);
194216
else
195-
AttachHasMany(relationship.Key as HasManyAttribute, relationship.Value);
217+
AttachHasMany(entity, relationship.Key as HasManyAttribute, relationship.Value);
196218
}
197219
}
198220

199-
private void AttachHasMany(HasManyAttribute relationship, IList pointers)
221+
private void AttachHasMany(TEntity entity, HasManyAttribute relationship, IList pointers)
200222
{
201-
foreach (var pointer in pointers)
202-
_context.Entry(pointer).State = EntityState.Unchanged;
223+
if (relationship.EntityPropertyName != null)
224+
{
225+
var relatedList = (IList)entity.GetType().GetProperty(relationship.EntityPropertyName)?.GetValue(entity);
226+
foreach (var related in relatedList)
227+
{
228+
_context.Entry(related).State = EntityState.Unchanged;
229+
}
230+
}
231+
else
232+
{
233+
foreach (var pointer in pointers)
234+
{
235+
_context.Entry(pointer).State = EntityState.Unchanged;
236+
}
237+
}
203238
}
204239

205240
private void AttachHasManyThrough(TEntity entity, HasManyThroughAttribute hasManyThrough, IList pointers)
@@ -227,12 +262,27 @@ private void AttachHasManyThrough(TEntity entity, HasManyThroughAttribute hasMan
227262
/// This is used to allow creation of HasOne relationships when the
228263
/// independent side of the relationship already exists.
229264
/// </summary>
230-
private void AttachHasOnePointers()
265+
private void AttachHasOnePointers(TEntity entity)
231266
{
232267
var relationships = _jsonApiContext.HasOneRelationshipPointers.Get();
233268
foreach (var relationship in relationships)
234-
if (_context.Entry(relationship.Value).State == EntityState.Detached && _context.EntityIsTracked(relationship.Value) == false)
235-
_context.Entry(relationship.Value).State = EntityState.Unchanged;
269+
{
270+
if (relationship.Key.GetType() != typeof(HasOneAttribute))
271+
continue;
272+
273+
var hasOne = (HasOneAttribute) relationship.Key;
274+
if (hasOne.EntityPropertyName != null)
275+
{
276+
var relatedEntity = entity.GetType().GetProperty(hasOne.EntityPropertyName)?.GetValue(entity);
277+
if (relatedEntity != null && _context.Entry(relatedEntity).State == EntityState.Detached && _context.EntityIsTracked((IIdentifiable)relatedEntity) == false)
278+
_context.Entry(relatedEntity).State = EntityState.Unchanged;
279+
}
280+
else
281+
{
282+
if (_context.Entry(relationship.Value).State == EntityState.Detached && _context.EntityIsTracked(relationship.Value) == false)
283+
_context.Entry(relationship.Value).State = EntityState.Unchanged;
284+
}
285+
}
236286
}
237287

238288
/// <inheritdoc />

src/JsonApiDotNetCore/JsonApiDotNetCore.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<VersionPrefix>3.0.0</VersionPrefix>
3+
<VersionPrefix>3.0.1</VersionPrefix>
44
<TargetFrameworks>$(NetStandardVersion)</TargetFrameworks>
55
<AssemblyName>JsonApiDotNetCore</AssemblyName>
66
<PackageId>JsonApiDotNetCore</PackageId>

src/JsonApiDotNetCore/Models/HasManyAttribute.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ public class HasManyAttribute : RelationshipAttribute
1111
/// <param name="publicName">The relationship name as exposed by the API</param>
1212
/// <param name="documentLinks">Which links are available. Defaults to <see cref="Link.All"/></param>
1313
/// <param name="canInclude">Whether or not this relationship can be included using the <c>?include=public-name</c> query string</param>
14+
/// <param name="mappedBy">The name of the entity mapped property, defaults to null</param>
1415
///
1516
/// <example>
1617
///
@@ -23,8 +24,8 @@ public class HasManyAttribute : RelationshipAttribute
2324
/// </code>
2425
///
2526
/// </example>
26-
public HasManyAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true)
27-
: base(publicName, documentLinks, canInclude)
27+
public HasManyAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true, string mappedBy = null)
28+
: base(publicName, documentLinks, canInclude, mappedBy)
2829
{ }
2930

3031
/// <summary>

src/JsonApiDotNetCore/Models/HasManyThroughAttribute.cs

+8-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Reflection;
3+
using System.Security;
34

45
namespace JsonApiDotNetCore.Models
56
{
@@ -30,14 +31,15 @@ public class HasManyThroughAttribute : HasManyAttribute
3031
/// <param name="internalThroughName">The name of the navigation property that will be used to get the HasMany relationship</param>
3132
/// <param name="documentLinks">Which links are available. Defaults to <see cref="Link.All"/></param>
3233
/// <param name="canInclude">Whether or not this relationship can be included using the <c>?include=public-name</c> query string</param>
34+
/// <param name="mappedBy">The name of the entity mapped property, defaults to null</param>
3335
///
3436
/// <example>
3537
/// <code>
3638
/// [HasManyThrough(nameof(ArticleTags), documentLinks: Link.All, canInclude: true)]
3739
/// </code>
3840
/// </example>
39-
public HasManyThroughAttribute(string internalThroughName, Link documentLinks = Link.All, bool canInclude = true)
40-
: base(null, documentLinks, canInclude)
41+
public HasManyThroughAttribute(string internalThroughName, Link documentLinks = Link.All, bool canInclude = true, string mappedBy = null)
42+
: base(null, documentLinks, canInclude, mappedBy)
4143
{
4244
InternalThroughName = internalThroughName;
4345
}
@@ -50,14 +52,15 @@ public HasManyThroughAttribute(string internalThroughName, Link documentLinks =
5052
/// <param name="internalThroughName">The name of the navigation property that will be used to get the HasMany relationship</param>
5153
/// <param name="documentLinks">Which links are available. Defaults to <see cref="Link.All"/></param>
5254
/// <param name="canInclude">Whether or not this relationship can be included using the <c>?include=public-name</c> query string</param>
55+
/// <param name="mappedBy">The name of the entity mapped property, defaults to null</param>
5356
///
5457
/// <example>
5558
/// <code>
5659
/// [HasManyThrough("tags", nameof(ArticleTags), documentLinks: Link.All, canInclude: true)]
5760
/// </code>
5861
/// </example>
59-
public HasManyThroughAttribute(string publicName, string internalThroughName, Link documentLinks = Link.All, bool canInclude = true)
60-
: base(publicName, documentLinks, canInclude)
62+
public HasManyThroughAttribute(string publicName, string internalThroughName, Link documentLinks = Link.All, bool canInclude = true, string mappedBy = null)
63+
: base(publicName, documentLinks, canInclude, mappedBy)
6164
{
6265
InternalThroughName = internalThroughName;
6366
}
@@ -161,4 +164,4 @@ public HasManyThroughAttribute(string publicName, string internalThroughName, Li
161164
/// </example>
162165
public override string RelationshipPath => $"{InternalThroughName}.{RightProperty.Name}";
163166
}
164-
}
167+
}

src/JsonApiDotNetCore/Models/HasOneAttribute.cs

+5-4
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,29 @@ public class HasOneAttribute : RelationshipAttribute
1313
/// <param name="documentLinks">Which links are available. Defaults to <see cref="Link.All"/></param>
1414
/// <param name="canInclude">Whether or not this relationship can be included using the <c>?include=public-name</c> query string</param>
1515
/// <param name="withForeignKey">The foreign key property name. Defaults to <c>"{RelationshipName}Id"</c></param>
16+
/// <param name="mappedBy">The name of the entity mapped property, defaults to null</param>
1617
///
1718
/// <example>
1819
/// Using an alternative foreign key:
1920
///
2021
/// <code>
2122
/// public class Article : Identifiable
2223
/// {
23-
/// [HasOne("author", withForiegnKey: nameof(AuthorKey)]
24+
/// [HasOne("author", withForeignKey: nameof(AuthorKey)]
2425
/// public Author Author { get; set; }
2526
/// public int AuthorKey { get; set; }
2627
/// }
2728
/// </code>
2829
///
2930
/// </example>
30-
public HasOneAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true, string withForeignKey = null)
31-
: base(publicName, documentLinks, canInclude)
31+
public HasOneAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true, string withForeignKey = null, string mappedBy = null)
32+
: base(publicName, documentLinks, canInclude, mappedBy)
3233
{
3334
_explicitIdentifiablePropertyName = withForeignKey;
3435
}
3536

3637
private readonly string _explicitIdentifiablePropertyName;
37-
38+
3839
/// <summary>
3940
/// The independent resource identifier.
4041
/// </summary>

src/JsonApiDotNetCore/Models/RelationshipAttribute.cs

+7-5
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,16 @@ namespace JsonApiDotNetCore.Models
66
{
77
public abstract class RelationshipAttribute : Attribute
88
{
9-
protected RelationshipAttribute(string publicName, Link documentLinks, bool canInclude)
9+
protected RelationshipAttribute(string publicName, Link documentLinks, bool canInclude, string mappedBy)
1010
{
1111
PublicRelationshipName = publicName;
1212
DocumentLinks = documentLinks;
1313
CanInclude = canInclude;
14+
EntityPropertyName = mappedBy;
1415
}
1516

1617
public string PublicRelationshipName { get; internal set; }
17-
public string InternalRelationshipName { get; internal set; }
18+
public string InternalRelationshipName { get; internal set; }
1819

1920
/// <summary>
2021
/// 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
3132
public bool IsHasOne => GetType() == typeof(HasOneAttribute);
3233
public Link DocumentLinks { get; } = Link.All;
3334
public bool CanInclude { get; }
35+
public string EntityPropertyName { get; }
3436

3537
public bool TryGetHasOne(out HasOneAttribute result)
3638
{
@@ -55,10 +57,10 @@ public bool TryGetHasMany(out HasManyAttribute result)
5557
}
5658

5759
public abstract void SetValue(object entity, object newValue);
58-
60+
5961
public object GetValue(object entity) => entity
60-
?.GetType()
61-
.GetProperty(InternalRelationshipName)
62+
?.GetType()?
63+
.GetProperty(InternalRelationshipName)?
6264
.GetValue(entity);
6365

6466
public override string ToString()

0 commit comments

Comments
 (0)