Skip to content

Commit 542b021

Browse files
committed
implement creation of HasManyThrough relationships
1 parent 985342f commit 542b021

File tree

6 files changed

+100
-8
lines changed

6 files changed

+100
-8
lines changed

src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections;
23
using System.Collections.Generic;
34
using System.Linq;
45
using System.Reflection;
@@ -172,10 +173,17 @@ protected virtual List<RelationshipAttribute> GetRelationships(Type entityType)
172173
if(throughProperty == null)
173174
throw new JsonApiSetupException($"Invalid '{nameof(HasManyThroughAttribute)}' on type '{entityType}'. Type does not contain a property named '{hasManyThroughAttribute.InternalThroughName}'.");
174175

176+
if(throughProperty.PropertyType.Implements<IList>() == false)
177+
throw new JsonApiSetupException($"Invalid '{nameof(HasManyThroughAttribute)}' on type '{entityType}.{throughProperty.Name}'. Property type does not implement IList.");
178+
175179
// assumption: the property should be a generic collection, e.g. List<ArticleTag>
176180
if(throughProperty.PropertyType.IsGenericType == false)
177181
throw new JsonApiSetupException($"Invalid '{nameof(HasManyThroughAttribute)}' on type '{entityType}'. Expected through entity to be a generic type, such as List<{prop.PropertyType}>.");
178182

183+
// Article → List<ArticleTag>
184+
hasManyThroughAttribute.ThroughProperty = throughProperty;
185+
186+
// ArticleTag
179187
hasManyThroughAttribute.ThroughType = throughProperty.PropertyType.GetGenericArguments()[0];
180188

181189
var throughProperties = hasManyThroughAttribute.ThroughType.GetProperties();

src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections;
23
using System.Collections.Generic;
34
using System.Linq;
45
using System.Threading.Tasks;
@@ -143,17 +144,17 @@ public virtual async Task<TEntity> GetAndIncludeAsync(TId id, string relationshi
143144
/// <inheritdoc />
144145
public virtual async Task<TEntity> CreateAsync(TEntity entity)
145146
{
146-
AttachRelationships();
147+
AttachRelationships(entity);
147148
_dbSet.Add(entity);
148149

149150
await _context.SaveChangesAsync();
150151

151152
return entity;
152153
}
153154

154-
protected virtual void AttachRelationships()
155+
protected virtual void AttachRelationships(TEntity entity = null)
155156
{
156-
AttachHasManyPointers();
157+
AttachHasManyPointers(entity);
157158
AttachHasOnePointers();
158159
}
159160

@@ -183,15 +184,42 @@ public void DetachRelationshipPointers(TEntity entity)
183184
/// This is used to allow creation of HasMany relationships when the
184185
/// dependent side of the relationship already exists.
185186
/// </summary>
186-
private void AttachHasManyPointers()
187+
private void AttachHasManyPointers(TEntity entity)
187188
{
188189
var relationships = _jsonApiContext.HasManyRelationshipPointers.Get();
189190
foreach (var relationship in relationships)
190191
{
191-
foreach (var pointer in relationship.Value)
192-
{
193-
_context.Entry(pointer).State = EntityState.Unchanged;
194-
}
192+
if(relationship.Key is HasManyThroughAttribute hasManyThrough)
193+
AttachHasManyThrough(entity, hasManyThrough, relationship.Value);
194+
else
195+
AttachHasMany(relationship.Key as HasManyAttribute, relationship.Value);
196+
}
197+
}
198+
199+
private void AttachHasMany(HasManyAttribute relationship, IList pointers)
200+
{
201+
foreach (var pointer in pointers)
202+
{
203+
_context.Entry(pointer).State = EntityState.Unchanged;
204+
}
205+
}
206+
207+
private void AttachHasManyThrough(TEntity entity, HasManyThroughAttribute hasManyThrough, IList pointers)
208+
{
209+
// create the collection (e.g. List<ArticleTag>)
210+
// this type MUST implement IList so we can build the collection
211+
// if this is problematic, we _could_ reflect on the type and find an Add method
212+
// or we might be able to create a proxy type and implement the enumerator
213+
var throughRelationshipCollection = Activator.CreateInstance(hasManyThrough.ThroughProperty.PropertyType) as IList;
214+
hasManyThrough.ThroughProperty.SetValue(entity, throughRelationshipCollection);
215+
foreach (var pointer in pointers)
216+
{
217+
_context.Entry(pointer).State = EntityState.Unchanged;
218+
219+
var throughInstance = Activator.CreateInstance(hasManyThrough.ThroughType);
220+
hasManyThrough.LeftProperty.SetValue(throughInstance, entity);
221+
hasManyThrough.RightProperty.SetValue(throughInstance, pointer);
222+
throughRelationshipCollection.Add(throughInstance);
195223
}
196224
}
197225

src/JsonApiDotNetCore/Extensions/TypeExtensions.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,17 @@ private static object CreateNewInstance(Type type)
6767
throw new JsonApiException(500, $"Type '{type}' cannot be instantiated using the default constructor.", e);
6868
}
6969
}
70+
71+
/// <summary>
72+
/// Whether or not a type implements an interface.
73+
/// </summary>
74+
public static bool Implements<T>(this Type concreteType)
75+
=> Implements(concreteType, typeof(T));
76+
77+
/// <summary>
78+
/// Whether or not a type implements an interface.
79+
/// </summary>
80+
public static bool Implements(this Type concreteType, Type interfaceType)
81+
=> interfaceType?.IsAssignableFrom(concreteType) == true;
7082
}
7183
}

src/JsonApiDotNetCore/Models/HasManyThroughAttribute.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ namespace JsonApiDotNetCore.Models
55
{
66
/// <summary>
77
/// Create a HasMany relationship through a many-to-many join relationship.
8+
/// This type can only be applied on types that implement IList.
89
/// </summary>
910
///
1011
/// <example>
@@ -109,6 +110,21 @@ public HasManyThroughAttribute(string publicName, string internalThroughName, Li
109110
/// </example>
110111
public PropertyInfo RightProperty { get; internal set; }
111112

113+
/// <summary>
114+
/// The join entity property on the parent resource.
115+
/// </summary>
116+
///
117+
/// <example>
118+
/// In the `[HasManyThrough("tags", nameof(ArticleTags))]` example
119+
/// this would point to the `Article.ArticleTags` property
120+
///
121+
/// <code>
122+
/// public List&lt;ArticleTags&gt; ArticleTags { get; set; }
123+
/// </code>
124+
///
125+
/// </example>
126+
public PropertyInfo ThroughProperty { get; internal set; }
127+
112128
/// <inheritdoc />
113129
/// <example>
114130
/// "ArticleTags.Tag"

src/JsonApiDotNetCore/Models/RelationshipAttribute.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Reflection;
23

34
namespace JsonApiDotNetCore.Models
45
{

test/UnitTests/Extensions/TypeExtensions_Tests.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Collections.Generic;
23
using JsonApiDotNetCore.Extensions;
34
using JsonApiDotNetCore.Models;
@@ -36,6 +37,32 @@ public void New_Creates_An_Instance_If_T_Implements_Interface()
3637
Assert.IsType<Model>(instance);
3738
}
3839

40+
[Fact]
41+
public void Implements_Returns_True_If_Type_Implements_Interface()
42+
{
43+
// arrange
44+
var type = typeof(Model);
45+
46+
// act
47+
var result = type.Implements<IIdentifiable>();
48+
49+
// assert
50+
Assert.True(result);
51+
}
52+
53+
[Fact]
54+
public void Implements_Returns_False_If_Type_DoesNot_Implement_Interface()
55+
{
56+
// arrange
57+
var type = typeof(String);
58+
59+
// act
60+
var result = type.Implements<IIdentifiable>();
61+
62+
// assert
63+
Assert.False(result);
64+
}
65+
3966
private class Model : IIdentifiable
4067
{
4168
public string StringId { get; set; }

0 commit comments

Comments
 (0)