Skip to content

Commit 5ee3068

Browse files
committed
add entity check to hasmany
1 parent 135853b commit 5ee3068

File tree

8 files changed

+110
-28
lines changed

8 files changed

+110
-28
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public class CourseResource : Identifiable
1818
[Attr("description")]
1919
public string Description { get; set; }
2020

21-
[HasOne("department", withEntity: "Department")]
21+
[HasOne("department", mappedBy: "Department")]
2222
public DepartmentResource Department { get; set; }
2323
public int? DepartmentId { get; set; }
2424

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

Lines changed: 1 addition & 1 deletion
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/Data/DefaultEntityRepository.cs

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -178,11 +178,23 @@ public void DetachRelationshipPointers(TEntity entity)
178178

179179
foreach (var hasManyRelationship in _jsonApiContext.HasManyRelationshipPointers.Get())
180180
{
181-
foreach (var pointer in hasManyRelationship.Value)
181+
var hasMany = (HasOneAttribute) hasManyRelationship.Key;
182+
if (hasMany.EntityPropertyName != null)
182183
{
183-
_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+
}
184189
}
185-
190+
else
191+
{
192+
foreach (var pointer in hasManyRelationship.Value)
193+
{
194+
_context.Entry(pointer).State = EntityState.Detached;
195+
}
196+
}
197+
186198
// HACK: detaching has many relationships doesn't appear to be sufficient
187199
// the navigation property actually needs to be nulled out, otherwise
188200
// EF adds duplicate instances to the collection
@@ -202,14 +214,27 @@ private void AttachHasManyPointers(TEntity entity)
202214
if (relationship.Key is HasManyThroughAttribute hasManyThrough)
203215
AttachHasManyThrough(entity, hasManyThrough, relationship.Value);
204216
else
205-
AttachHasMany(relationship.Key as HasManyAttribute, relationship.Value);
217+
AttachHasMany(entity, relationship.Key as HasManyAttribute, relationship.Value);
206218
}
207219
}
208220

209-
private void AttachHasMany(HasManyAttribute relationship, IList pointers)
221+
private void AttachHasMany(TEntity entity, HasManyAttribute relationship, IList pointers)
210222
{
211-
foreach (var pointer in pointers)
212-
_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+
}
213238
}
214239

215240
private void AttachHasManyThrough(TEntity entity, HasManyThroughAttribute hasManyThrough, IList pointers)

src/JsonApiDotNetCore/Models/HasManyAttribute.cs

Lines changed: 3 additions & 2 deletions
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

Lines changed: 8 additions & 5 deletions
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

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ 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="withEntity">If the entity model of this relationship refers to a different type, specify that here</param>
16+
/// <param name="mappedBy">The name of the entity mapped property, defaults to null</param>
1717
///
1818
/// <example>
1919
/// Using an alternative foreign key:
@@ -28,15 +28,13 @@ public class HasOneAttribute : RelationshipAttribute
2828
/// </code>
2929
///
3030
/// </example>
31-
public HasOneAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true, string withForeignKey = null, string withEntity = null)
32-
: 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)
3333
{
3434
_explicitIdentifiablePropertyName = withForeignKey;
35-
EntityPropertyName = withEntity;
3635
}
3736

3837
private readonly string _explicitIdentifiablePropertyName;
39-
private readonly string _relatedEntityPropertyName;
4038

4139
/// <summary>
4240
/// The independent resource identifier.
@@ -45,11 +43,6 @@ public HasOneAttribute(string publicName = null, Link documentLinks = Link.All,
4543
? JsonApiOptions.RelatedIdMapper.GetRelatedIdPropertyName(InternalRelationshipName)
4644
: _explicitIdentifiablePropertyName;
4745

48-
/// <summary>
49-
/// For use in entity / resource separation when the related property is also separated
50-
/// </summary>
51-
public string EntityPropertyName { get; }
52-
5346
/// <summary>
5447
/// Sets the value of the property identified by this attribute
5548
/// </summary>

src/JsonApiDotNetCore/Models/RelationshipAttribute.cs

Lines changed: 4 additions & 2 deletions
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
{

test/ResourceEntitySeparationExampleTests/Acceptance/AddTests.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using JsonApiDotNetCoreExample.Models.Resources;
22
using System.Collections.Generic;
3+
using System.Linq;
34
using System.Net;
45
using System.Threading.Tasks;
6+
using Microsoft.AspNetCore.Authorization.Infrastructure;
57
using Xunit;
68

79
namespace ResourceEntitySeparationExampleTests.Acceptance
@@ -121,6 +123,62 @@ public async Task Can_Create_Department()
121123
Assert.NotNull(data);
122124
Assert.Equal(dept.Name, data.Name);
123125
}
126+
127+
[Fact]
128+
public async Task Can_Create_Department_With_Courses()
129+
{
130+
// arrange
131+
var route = $"/api/v1/departments/";
132+
var dept = _fixture.DepartmentFaker.Generate();
133+
134+
var one = _fixture.CourseFaker.Generate();
135+
var two = _fixture.CourseFaker.Generate();
136+
_fixture.Context.Courses.Add(one);
137+
_fixture.Context.Courses.Add(two);
138+
_fixture.Context.SaveChanges();
139+
140+
var content = new
141+
{
142+
data = new
143+
{
144+
type = "departments",
145+
attributes = new Dictionary<string, string>
146+
{
147+
{ "name", dept.Name }
148+
},
149+
relationships = new
150+
{
151+
courses = new
152+
{
153+
data = new[]
154+
{
155+
new
156+
{
157+
type = "courses",
158+
id = one.Id
159+
},
160+
new
161+
{
162+
type = "courses",
163+
id = two.Id
164+
}
165+
}
166+
}
167+
}
168+
}
169+
};
170+
171+
// act
172+
var (response, data) = await _fixture.PostAsync<DepartmentResource>(route, content);
173+
174+
// assert
175+
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
176+
Assert.NotNull(data);
177+
Assert.Equal(dept.Name, data.Name);
178+
Assert.NotEmpty(data.Courses);
179+
Assert.NotNull(data.Courses.SingleOrDefault(c => c.Id == one.Id));
180+
Assert.NotNull(data.Courses.SingleOrDefault(c => c.Id == two.Id));
181+
}
124182

125183
[Fact]
126184
public async Task Can_Create_Student()

0 commit comments

Comments
 (0)