Skip to content

Commit 0ced9da

Browse files
committed
refactor: move HasManyThrough GetValue and SetValue logic to HasManyThroughAttribute
1 parent a94f318 commit 0ced9da

File tree

13 files changed

+184
-155
lines changed

13 files changed

+184
-155
lines changed

src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs

Lines changed: 14 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -24,26 +24,26 @@ public class DefaultEntityRepository<TEntity, TId> : IEntityRepository<TEntity,
2424
private readonly ITargetedFields _targetedFields;
2525
private readonly DbContext _context;
2626
private readonly DbSet<TEntity> _dbSet;
27-
private readonly IResourceGraph _resourceGraph;
27+
private readonly IContextEntityProvider _provider;
2828
private readonly IGenericProcessorFactory _genericProcessorFactory;
2929

3030
public DefaultEntityRepository(
3131
ITargetedFields targetedFields,
3232
IDbContextResolver contextResolver,
33-
IResourceGraph resourceGraph,
33+
IContextEntityProvider provider,
3434
IGenericProcessorFactory genericProcessorFactory)
35-
: this(targetedFields, contextResolver, resourceGraph, genericProcessorFactory, null)
35+
: this(targetedFields, contextResolver, provider, genericProcessorFactory, null)
3636
{ }
3737

3838
public DefaultEntityRepository(
3939
ITargetedFields targetedFields,
4040
IDbContextResolver contextResolver,
41-
IResourceGraph resourceGraph,
41+
IContextEntityProvider provider,
4242
IGenericProcessorFactory genericProcessorFactory,
4343
ILoggerFactory loggerFactory = null)
4444
{
4545
_targetedFields = targetedFields;
46-
_resourceGraph = resourceGraph;
46+
_provider = provider;
4747
_genericProcessorFactory = genericProcessorFactory;
4848
_context = contextResolver.GetContext();
4949
_dbSet = _context.Set<TEntity>();
@@ -85,17 +85,15 @@ public virtual async Task<TEntity> CreateAsync(TEntity entity)
8585
{
8686
foreach (var relationshipAttr in _targetedFields.Relationships)
8787
{
88-
object trackedRelationshipValue = GetTrackedRelationshipValue(relationshipAttr, entity, out bool wasAlreadyTracked);
88+
object trackedRelationshipValue = GetTrackedRelationshipValue(relationshipAttr, entity, out bool relationshipWasAlreadyTracked);
8989
LoadInverseRelationships(trackedRelationshipValue, relationshipAttr);
90-
if (wasAlreadyTracked)
90+
if (relationshipWasAlreadyTracked || relationshipAttr is HasManyThroughAttribute)
9191
/// We only need to reassign the relationship value to the to-be-added
92-
/// entity when we're using a different instance (because this different one
92+
/// entity when we're using a different instance of the relationship (because this different one
9393
/// was already tracked) than the one assigned to the to-be-created entity.
94-
AssignRelationshipValue(entity, trackedRelationshipValue, relationshipAttr);
95-
else if (relationshipAttr is HasManyThroughAttribute throughAttr)
96-
/// even if we don't have to reassign anything because of already tracked
94+
/// Alternatively, even if we don't have to reassign anything because of already tracked
9795
/// entities, we still need to assign the "through" entities in the case of many-to-many.
98-
AssignHasManyThrough(entity, throughAttr, (IList)trackedRelationshipValue);
96+
relationshipAttr.SetValue(entity, trackedRelationshipValue);
9997
}
10098
_dbSet.Add(entity);
10199
await _context.SaveChangesAsync();
@@ -139,7 +137,7 @@ private void LoadInverseRelationships(object trackedRelationshipValue, Relations
139137

140138
private bool IsHasOneRelationship(string internalRelationshipName, Type type)
141139
{
142-
var relationshipAttr = _resourceGraph.GetContextEntity(type).Relationships.FirstOrDefault(r => r.InternalRelationshipName == internalRelationshipName);
140+
var relationshipAttr = _provider.GetContextEntity(type).Relationships.FirstOrDefault(r => r.InternalRelationshipName == internalRelationshipName);
143141
if (relationshipAttr != null)
144142
{
145143
if (relationshipAttr is HasOneAttribute)
@@ -149,7 +147,7 @@ private bool IsHasOneRelationship(string internalRelationshipName, Type type)
149147
}
150148
// relationshipAttr is null when we don't put a [RelationshipAttribute] on the inverse navigation property.
151149
// In this case we use relfection to figure out what kind of relationship is pointing back.
152-
return !(type.GetProperty(internalRelationshipName).PropertyType.Inherits(typeof(IEnumerable)));
150+
return !type.GetProperty(internalRelationshipName).PropertyType.Inherits(typeof(IEnumerable));
153151
}
154152

155153
private void DetachRelationships(TEntity entity)
@@ -199,7 +197,8 @@ public virtual async Task<TEntity> UpdateAsync(TEntity updatedEntity)
199197
/// to the todoItems in trackedRelationshipValue
200198
LoadInverseRelationships(trackedRelationshipValue, relationshipAttr);
201199
/// assigns the updated relationship to the database entity
202-
AssignRelationshipValue(databaseEntity, trackedRelationshipValue, relationshipAttr);
200+
//AssignRelationshipValue(databaseEntity, trackedRelationshipValue, relationshipAttr);
201+
relationshipAttr.SetValue(databaseEntity, trackedRelationshipValue);
203202
}
204203

205204
await _context.SaveChangesAsync();
@@ -357,30 +356,13 @@ protected void LoadCurrentRelationships(TEntity oldEntity, RelationshipAttribute
357356
if (relationshipAttribute is HasManyThroughAttribute throughAttribute)
358357
{
359358
_context.Entry(oldEntity).Collection(throughAttribute.InternalThroughName).Load();
360-
361359
}
362360
else if (relationshipAttribute is HasManyAttribute hasManyAttribute)
363361
{
364362
_context.Entry(oldEntity).Collection(hasManyAttribute.InternalRelationshipName).Load();
365363
}
366364
}
367365

368-
/// <summary>
369-
/// Assigns the <paramref name="relationshipValue"/> to <paramref name="targetEntity"/>
370-
/// </summary>
371-
private void AssignRelationshipValue(TEntity targetEntity, object relationshipValue, RelationshipAttribute relationshipAttribute)
372-
{
373-
if (relationshipAttribute is HasManyThroughAttribute throughAttribute)
374-
{
375-
// todo: this logic should be put in the HasManyThrough attribute
376-
AssignHasManyThrough(targetEntity, throughAttribute, (IList)relationshipValue);
377-
}
378-
else
379-
{
380-
relationshipAttribute.SetValue(targetEntity, relationshipValue);
381-
}
382-
}
383-
384366
/// <summary>
385367
/// The relationshipValue parameter contains the dependent side of the relationship (Tags).
386368
/// We can't directly add them to the principal entity (Article): we need to

src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public static IServiceCollection AddJsonApi<TContext>(this IServiceCollection se
4444
/// </summary>
4545
/// <typeparam name="TContext"></typeparam>
4646
/// <param name="services"></param>
47-
/// <param name="configureAction"></param>
47+
/// <param name="configureOptions"></param>
4848
/// <returns></returns>
4949
public static IServiceCollection AddJsonApi<TContext>(this IServiceCollection services,
5050
Action<JsonApiOptions> configureOptions,

src/JsonApiDotNetCore/Internal/Contracts/IResourceGraph.cs

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,38 +11,6 @@ namespace JsonApiDotNetCore.Internal.Contracts
1111
public interface IResourceGraph : IContextEntityProvider
1212
{
1313
RelationshipAttribute GetInverseRelationship(RelationshipAttribute relationship);
14-
/// <summary>
15-
/// Gets the value of the navigation property, defined by the relationshipName,
16-
/// on the provided instance.
17-
/// </summary>
18-
/// <param name="resource">The resource instance</param>
19-
/// <param name="propertyName">The navigation property name.</param>
20-
/// <example>
21-
/// <code>
22-
/// _graph.GetRelationship(todoItem, nameof(TodoItem.Owner));
23-
/// </code>
24-
/// </example>
25-
/// <remarks>
26-
/// In the case of a `HasManyThrough` relationship, it will not traverse the relationship
27-
/// and will instead return the value of the shadow property (e.g. Articles.Tags).
28-
/// If you want to traverse the relationship, you should use <see cref="GetRelationshipValue" />.
29-
/// </remarks>
30-
object GetRelationship<TParent>(TParent resource, string propertyName);
31-
32-
/// <summary>
33-
/// Gets the value of the navigation property (defined by the <see cref="RelationshipAttribute" />)
34-
/// on the provided instance.
35-
/// In the case of `HasManyThrough` relationships, it will traverse the through entity and return the
36-
/// value of the relationship on the other side of a join entity (e.g. Articles.ArticleTags.Tag).
37-
/// </summary>
38-
/// <param name="resource">The resource instance</param>
39-
/// <param name="relationship">The attribute used to define the relationship.</param>
40-
/// <example>
41-
/// <code>
42-
/// _graph.GetRelationshipValue(todoItem, nameof(TodoItem.Owner));
43-
/// </code>
44-
/// </example>
45-
object GetRelationshipValue<TParent>(TParent resource, RelationshipAttribute relationship) where TParent : IIdentifiable;
4614

4715
/// <summary>
4816
/// Get the internal navigation property name for the specified public

src/JsonApiDotNetCore/Internal/ResourceGraph.cs

Lines changed: 1 addition & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -52,59 +52,14 @@ internal ResourceGraph(List<ContextEntity> entities, bool usesDbContext, List<Va
5252
/// <inheritdoc />
5353
public bool UsesDbContext { get; }
5454

55-
/// <inheritdoc />
56-
public object GetRelationship<TParent>(TParent entity, string relationshipName)
57-
{
58-
var parentEntityType = entity.GetType();
59-
60-
var navigationProperty = parentEntityType
61-
.GetProperties()
62-
.SingleOrDefault(p => string.Equals(p.Name, relationshipName, StringComparison.OrdinalIgnoreCase));
63-
64-
if (navigationProperty == null)
65-
throw new JsonApiException(400, $"{parentEntityType} does not contain a relationship named {relationshipName}");
66-
67-
return navigationProperty.GetValue(entity);
68-
}
69-
70-
public object GetRelationshipValue<TParent>(TParent resource, RelationshipAttribute relationship) where TParent : IIdentifiable
71-
{
72-
if (relationship is HasManyThroughAttribute hasManyThroughRelationship)
73-
{
74-
return GetHasManyThrough(resource, hasManyThroughRelationship);
75-
}
76-
77-
return GetRelationship(resource, relationship.InternalRelationshipName);
78-
}
79-
80-
private IEnumerable<IIdentifiable> GetHasManyThrough(IIdentifiable parent, HasManyThroughAttribute hasManyThrough)
81-
{
82-
var throughProperty = GetRelationship(parent, hasManyThrough.InternalThroughName);
83-
if (throughProperty is IEnumerable hasManyNavigationEntity)
84-
{
85-
// wrap "yield return" in a sub-function so we can correctly return null if the property is null.
86-
return GetHasManyThroughIter(hasManyThrough, hasManyNavigationEntity);
87-
}
88-
return null;
89-
}
90-
91-
private IEnumerable<IIdentifiable> GetHasManyThroughIter(HasManyThroughAttribute hasManyThrough, IEnumerable hasManyNavigationEntity)
92-
{
93-
foreach (var includedEntity in hasManyNavigationEntity)
94-
{
95-
var targetValue = hasManyThrough.RightProperty.GetValue(includedEntity) as IIdentifiable;
96-
yield return targetValue;
97-
}
98-
}
99-
10055
/// <inheritdoc />
10156
public string GetRelationshipName<TParent>(string relationshipName)
10257
{
10358
var entityType = typeof(TParent);
10459
return Entities
10560
.SingleOrDefault(e => e.EntityType == entityType)
10661
?.Relationships
107-
.SingleOrDefault(r => r.Is(relationshipName))
62+
  .SingleOrDefault(r => r.Is(relationshipName))
10863
?.InternalRelationshipName;
10964
}
11065

src/JsonApiDotNetCore/Internal/TypeHelper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ public static T ConvertType<T>(object value)
8080

8181
public static Type GetTypeOfList(Type type)
8282
{
83-
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))
83+
if (type != null && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))
8484
{
8585
return type.GetGenericArguments()[0];
8686
}

src/JsonApiDotNetCore/Models/Annotation/HasManyAttribute.cs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,31 @@ public HasManyAttribute(string publicName = null, Link relationshipLinks = Link.
3131
InverseNavigation = inverseNavigationProperty;
3232
}
3333

34+
/// <summary>
35+
/// Gets the value of the navigation property, defined by the relationshipName,
36+
/// on the provided instance.
37+
/// </summary>
38+
39+
public override object GetValue(object entity)
40+
{
41+
return entity?.GetType()?
42+
.GetProperty(InternalRelationshipName)?
43+
.GetValue(entity);
44+
}
45+
46+
3447
/// <summary>
3548
/// Sets the value of the property identified by this attribute
3649
/// </summary>
37-
/// <param name="resource">The target object</param>
50+
/// <param name="entity">The target object</param>
3851
/// <param name="newValue">The new property value</param>
39-
public override void SetValue(object resource, object newValue)
52+
public override void SetValue(object entity, object newValue)
4053
{
41-
var propertyInfo = resource
54+
var propertyInfo = entity
4255
.GetType()
4356
.GetProperty(InternalRelationshipName);
4457

45-
propertyInfo.SetValue(resource, newValue);
58+
propertyInfo.SetValue(entity, newValue);
4659
}
4760
}
4861
}

src/JsonApiDotNetCore/Models/Annotation/HasManyThroughAttribute.cs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
4+
using System.Linq;
25
using System.Reflection;
6+
using JsonApiDotNetCore.Extensions;
7+
using JsonApiDotNetCore.Internal;
38
using JsonApiDotNetCore.Models.Links;
49

510
namespace JsonApiDotNetCore.Models
@@ -65,6 +70,64 @@ public HasManyThroughAttribute(string publicName, string internalThroughName, Li
6570
InternalThroughName = internalThroughName;
6671
}
6772

73+
/// <summary>
74+
/// Traverses the through the provided entity and returns the
75+
/// value of the relationship on the other side of a join entity
76+
/// (e.g. Articles.ArticleTags.Tag).
77+
/// </summary>
78+
public override object GetValue(object entity)
79+
{
80+
var throughNavigationProperty = entity.GetType()
81+
.GetProperties()
82+
.SingleOrDefault(p => string.Equals(p.Name, InternalThroughName, StringComparison.OrdinalIgnoreCase));
83+
84+
var throughEntities = throughNavigationProperty.GetValue(entity);
85+
86+
if (throughEntities == null)
87+
// return an empty list for the right-type of the property.
88+
return TypeHelper.CreateListFor(DependentType);
89+
90+
// the right entities are included on the navigation/through entities. Extract and return them.
91+
var rightEntities = new List<IIdentifiable>();
92+
foreach (var rightEntity in (IList)throughEntities)
93+
rightEntities.Add((IIdentifiable)RightProperty.GetValue(rightEntity));
94+
95+
return rightEntities.Cast(DependentType);
96+
}
97+
98+
99+
/// <summary>
100+
/// Sets the value of the property identified by this attribute
101+
/// </summary>
102+
/// <param name="entity">The target object</param>
103+
/// <param name="newValue">The new property value</param>
104+
public override void SetValue(object entity, object newValue)
105+
{
106+
107+
var propertyInfo = entity
108+
.GetType()
109+
.GetProperty(InternalRelationshipName);
110+
propertyInfo.SetValue(entity, newValue);
111+
112+
if (newValue == null)
113+
{
114+
ThroughProperty.SetValue(entity, null);
115+
}
116+
else
117+
{
118+
var throughRelationshipCollection = (IList)Activator.CreateInstance(ThroughProperty.PropertyType);
119+
ThroughProperty.SetValue(entity, throughRelationshipCollection);
120+
121+
foreach (IIdentifiable pointer in (IList)newValue)
122+
{
123+
var throughInstance = Activator.CreateInstance(ThroughType);
124+
LeftProperty.SetValue(throughInstance, entity);
125+
RightProperty.SetValue(throughInstance, pointer);
126+
throughRelationshipCollection.Add(throughInstance);
127+
}
128+
}
129+
}
130+
68131
/// <summary>
69132
/// The name of the join property on the parent resource.
70133
/// </summary>

src/JsonApiDotNetCore/Models/Annotation/HasOneAttribute.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ public HasOneAttribute(string publicName = null, Link links = Link.NotConfigured
3737
InverseNavigation = inverseNavigationProperty;
3838
}
3939

40+
41+
public override object GetValue(object entity)
42+
{
43+
return entity?.GetType()?
44+
.GetProperty(InternalRelationshipName)?
45+
.GetValue(entity);
46+
}
47+
4048
private readonly string _explicitIdentifiablePropertyName;
4149

4250
/// <summary>
@@ -49,23 +57,23 @@ public HasOneAttribute(string publicName = null, Link links = Link.NotConfigured
4957
/// <summary>
5058
/// Sets the value of the property identified by this attribute
5159
/// </summary>
52-
/// <param name="resource">The target object</param>
60+
/// <param name="entity">The target object</param>
5361
/// <param name="newValue">The new property value</param>
54-
public override void SetValue(object resource, object newValue)
62+
public override void SetValue(object entity, object newValue)
5563
{
5664
string propertyName = InternalRelationshipName;
5765
// if we're deleting the relationship (setting it to null),
5866
// we set the foreignKey to null. We could also set the actual property to null,
5967
// but then we would first need to load the current relationship, which requires an extra query.
6068
if (newValue == null) propertyName = IdentifiablePropertyName;
61-
var resourceType = resource.GetType();
69+
var resourceType = entity.GetType();
6270
var propertyInfo = resourceType.GetProperty(propertyName);
6371
if (propertyInfo == null)
6472
{
6573
// we can't set the FK to null because there isn't any.
6674
propertyInfo = resourceType.GetProperty(RelationshipPath);
6775
}
68-
propertyInfo.SetValue(resource, newValue);
76+
propertyInfo.SetValue(entity, newValue);
6977
}
7078

7179
// HACK: this will likely require boxing

0 commit comments

Comments
 (0)