Skip to content

Commit 1da949c

Browse files
committed
fix test
1 parent edbbd42 commit 1da949c

File tree

10 files changed

+126
-23
lines changed

10 files changed

+126
-23
lines changed

src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,13 +188,23 @@ protected virtual List<RelationshipAttribute> GetRelationships(Type entityType)
188188

189189
var throughProperties = hasManyThroughAttribute.ThroughType.GetProperties();
190190

191-
// Article → ArticleTag.Article
191+
// ArticleTag.Article
192192
hasManyThroughAttribute.LeftProperty = throughProperties.SingleOrDefault(x => x.PropertyType == entityType)
193193
?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a navigation property to type {entityType}");
194194

195+
// ArticleTag.ArticleId
196+
var leftIdPropertyName = JsonApiOptions.RelatedIdMapper.GetRelatedIdPropertyName(hasManyThroughAttribute.LeftProperty.Name);
197+
hasManyThroughAttribute.LeftIdProperty = throughProperties.SingleOrDefault(x => x.Name == leftIdPropertyName)
198+
?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a relationship id property to type {entityType} with name {leftIdPropertyName}");
199+
195200
// Article → ArticleTag.Tag
196201
hasManyThroughAttribute.RightProperty = throughProperties.SingleOrDefault(x => x.PropertyType == hasManyThroughAttribute.Type)
197202
?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a navigation property to type {hasManyThroughAttribute.Type}");
203+
204+
// ArticleTag.TagId
205+
var rightIdPropertyName = JsonApiOptions.RelatedIdMapper.GetRelatedIdPropertyName(hasManyThroughAttribute.RightProperty.Name);
206+
hasManyThroughAttribute.RightIdProperty = throughProperties.SingleOrDefault(x => x.Name == rightIdPropertyName)
207+
?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a relationship id property to type {hasManyThroughAttribute.Type} with name {rightIdPropertyName}");
198208
}
199209
}
200210

src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ public class JsonApiOptions
2222
/// </summary>
2323
public static IResourceNameFormatter ResourceNameFormatter { get; set; } = new DefaultResourceNameFormatter();
2424

25+
/// <summary>
26+
/// Provides an interface for formatting relationship id properties given the navigation property name
27+
/// </summary>
28+
public static IRelatedIdMapper RelatedIdMapper { get; set; } = new DefaultRelatedIdMapper();
29+
2530
/// <summary>
2631
/// Whether or not stack traces should be serialized in Error objects
2732
/// </summary>

src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,15 @@ public virtual async Task<TEntity> UpdateAsync(TId id, TEntity entity)
259259
/// <inheritdoc />
260260
public async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable<string> relationshipIds)
261261
{
262-
var genericProcessor = _genericProcessorFactory.GetProcessor<IGenericProcessor>(typeof(GenericProcessor<>), relationship.Type);
262+
// TODO: it would be better to let this be determined within the relationship attribute...
263+
// need to think about the right way to do that since HasMany doesn't need to think about this
264+
// and setting the HasManyThrough.Type to the join type (ArticleTag instead of Tag) for this changes the semantics
265+
// of the property...
266+
var typeToUpdate = (relationship is HasManyThroughAttribute hasManyThrough)
267+
? hasManyThrough.ThroughType
268+
: relationship.Type;
269+
270+
var genericProcessor = _genericProcessorFactory.GetProcessor<IGenericProcessor>(typeof(GenericProcessor<>), typeToUpdate);
263271
await genericProcessor.UpdateRelationshipsAsync(parent, relationship, relationshipIds);
264272
}
265273

src/JsonApiDotNetCore/Extensions/TypeExtensions.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,5 +79,17 @@ public static bool Implements<T>(this Type concreteType)
7979
/// </summary>
8080
public static bool Implements(this Type concreteType, Type interfaceType)
8181
=> interfaceType?.IsAssignableFrom(concreteType) == true;
82+
83+
/// <summary>
84+
/// Whether or not a type inherits a base type.
85+
/// </summary>
86+
public static bool Inherits<T>(this Type concreteType)
87+
=> Inherits(concreteType, typeof(T));
88+
89+
/// <summary>
90+
/// Whether or not a type inherits a base type.
91+
/// </summary>
92+
public static bool Inherits(this Type concreteType, Type interfaceType)
93+
=> interfaceType?.IsAssignableFrom(concreteType) == true;
8294
}
8395
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
namespace JsonApiDotNetCore.Graph
2+
{
3+
/// <summary>
4+
/// Provides an interface for formatting relationship identifiers from the navigation property name
5+
/// </summary>
6+
public interface IRelatedIdMapper
7+
{
8+
/// <summary>
9+
/// Get the internal property name for the database mapped identifier property
10+
/// </summary>
11+
///
12+
/// <example>
13+
/// <code>
14+
/// DefaultResourceNameFormatter.FormatId("Article");
15+
/// // "ArticleId"
16+
/// </code>
17+
/// </example>
18+
string GetRelatedIdPropertyName(string propertyName);
19+
}
20+
21+
/// <inheritdoc />
22+
public class DefaultRelatedIdMapper : IRelatedIdMapper
23+
{
24+
/// <inheritdoc />
25+
public string GetRelatedIdPropertyName(string propertyName) => propertyName + "Id";
26+
}
27+
}

src/JsonApiDotNetCore/Internal/Generics/GenericProcessor.cs

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public interface IGenericProcessor
1616
void SetRelationships(object parent, RelationshipAttribute relationship, IEnumerable<string> relationshipIds);
1717
}
1818

19-
public class GenericProcessor<T> : IGenericProcessor where T : class, IIdentifiable
19+
public class GenericProcessor<T> : IGenericProcessor where T : class
2020
{
2121
private readonly DbContext _context;
2222
public GenericProcessor(IDbContextResolver contextResolver)
@@ -33,39 +33,51 @@ public virtual async Task UpdateRelationshipsAsync(object parent, RelationshipAt
3333

3434
public virtual void SetRelationships(object parent, RelationshipAttribute relationship, IEnumerable<string> relationshipIds)
3535
{
36-
if (relationship is HasManyThroughAttribute hasManyThrough)
36+
if (relationship is HasManyThroughAttribute hasManyThrough && parent is IIdentifiable identifiableParent)
3737
{
38-
var parentId = ((IIdentifiable)parent).StringId;
39-
ParameterExpression parameter = Expression.Parameter(hasManyThrough.Type);
40-
Expression property = Expression.Property(parameter, hasManyThrough.LeftProperty);
38+
// ArticleTag
39+
ParameterExpression parameter = Expression.Parameter(hasManyThrough.ThroughType);
40+
41+
// ArticleTag.ArticleId
42+
Expression property = Expression.Property(parameter, hasManyThrough.LeftIdProperty);
43+
44+
// article.Id
45+
var parentId = TypeHelper.ConvertType(identifiableParent.StringId, hasManyThrough.LeftIdProperty.PropertyType);
4146
Expression target = Expression.Constant(parentId);
42-
Expression toString = Expression.Call(property, "ToString", null, null);
43-
Expression equals = Expression.Call(toString, "Equals", null, target);
44-
Expression<Func<object, bool>> lambda = Expression.Lambda<Func<object, bool>>(equals, parameter);
47+
48+
// ArticleTag.ArticleId.Equals(article.Id)
49+
Expression equals = Expression.Call(property, "Equals", null, target);
50+
51+
var lambda = Expression.Lambda<Func<T, bool>>(equals, parameter);
4552

4653
var oldLinks = _context
47-
.Set(hasManyThrough.ThroughType)
54+
.Set<T>()
4855
.Where(lambda.Compile())
4956
.ToList();
5057

51-
_context.Remove(oldLinks);
58+
// TODO: we shouldn't need to do this and it especially shouldn't happen outside a transaction
59+
// instead we should try updating the existing?
60+
_context.RemoveRange(oldLinks);
5261

5362
var newLinks = relationshipIds.Select(x => {
5463
var link = Activator.CreateInstance(hasManyThrough.ThroughType);
55-
hasManyThrough.LeftProperty.SetValue(link, TypeHelper.ConvertType(parent, hasManyThrough.LeftProperty.PropertyType));
56-
hasManyThrough.RightProperty.SetValue(link, TypeHelper.ConvertType(x, hasManyThrough.RightProperty.PropertyType));
64+
hasManyThrough.LeftIdProperty.SetValue(link, TypeHelper.ConvertType(parentId, hasManyThrough.LeftIdProperty.PropertyType));
65+
hasManyThrough.RightIdProperty.SetValue(link, TypeHelper.ConvertType(x, hasManyThrough.RightIdProperty.PropertyType));
5766
return link;
5867
});
68+
5969
_context.AddRange(newLinks);
6070
}
6171
else if (relationship.IsHasMany)
6272
{
63-
var entities = _context.Set<T>().Where(x => relationshipIds.Contains(x.StringId)).ToList();
73+
// TODO: need to handle the failure mode when the relationship does not implement IIdentifiable
74+
var entities = _context.Set<T>().Where(x => relationshipIds.Contains(((IIdentifiable)x).StringId)).ToList();
6475
relationship.SetValue(parent, entities);
6576
}
6677
else
6778
{
68-
var entity = _context.Set<T>().SingleOrDefault(x => relationshipIds.First() == x.StringId);
79+
// TODO: need to handle the failure mode when the relationship does not implement IIdentifiable
80+
var entity = _context.Set<T>().SingleOrDefault(x => relationshipIds.First() == ((IIdentifiable)x).StringId);
6981
relationship.SetValue(parent, entity);
7082
}
7183
}

src/JsonApiDotNetCore/Models/HasManyThroughAttribute.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,21 @@ public HasManyThroughAttribute(string publicName, string internalThroughName, Li
9595
/// </example>
9696
public PropertyInfo LeftProperty { get; internal set; }
9797

98+
/// <summary>
99+
/// The id property back to the parent resource from the join type.
100+
/// </summary>
101+
///
102+
/// <example>
103+
/// In the `[HasManyThrough("tags", nameof(ArticleTags))]` example
104+
/// this would point to the `Article.ArticleTags.ArticleId` property
105+
///
106+
/// <code>
107+
/// public int ArticleId { get; set; }
108+
/// </code>
109+
///
110+
/// </example>
111+
public PropertyInfo LeftIdProperty { get; internal set; }
112+
98113
/// <summary>
99114
/// The navigation property to the related resource from the join type.
100115
/// </summary>
@@ -110,6 +125,21 @@ public HasManyThroughAttribute(string publicName, string internalThroughName, Li
110125
/// </example>
111126
public PropertyInfo RightProperty { get; internal set; }
112127

128+
/// <summary>
129+
/// The id property to the related resource from the join type.
130+
/// </summary>
131+
///
132+
/// <example>
133+
/// In the `[HasManyThrough("tags", nameof(ArticleTags))]` example
134+
/// this would point to the `Article.ArticleTags.TagId` property
135+
///
136+
/// <code>
137+
/// public int TagId { get; set; }
138+
/// </code>
139+
///
140+
/// </example>
141+
public PropertyInfo RightIdProperty { get; internal set; }
142+
113143
/// <summary>
114144
/// The join entity property on the parent resource.
115145
/// </summary>

src/JsonApiDotNetCore/Models/HasOneAttribute.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using JsonApiDotNetCore.Configuration;
23

34
namespace JsonApiDotNetCore.Models
45
{
@@ -38,7 +39,7 @@ public HasOneAttribute(string publicName = null, Link documentLinks = Link.All,
3839
/// The independent resource identifier.
3940
/// </summary>
4041
public string IdentifiablePropertyName => string.IsNullOrWhiteSpace(_explicitIdentifiablePropertyName)
41-
? $"{InternalRelationshipName}Id"
42+
? JsonApiOptions.RelatedIdMapper.GetRelatedIdPropertyName(InternalRelationshipName)
4243
: _explicitIdentifiablePropertyName;
4344

4445
/// <summary>

src/JsonApiDotNetCore/Models/RelationshipAttribute.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Reflection;
3+
using JsonApiDotNetCore.Extensions;
34

45
namespace JsonApiDotNetCore.Models
56
{
@@ -22,11 +23,11 @@ protected RelationshipAttribute(string publicName, Link documentLinks, bool canI
2223
///
2324
/// <example>
2425
/// <code>
25-
/// public List&lt;Articles&gt; Articles { get; set; } // Type => Article
26+
/// public List&lt;Tag&gt; Tags { get; set; } // Type => Tag
2627
/// </code>
2728
/// </example>
2829
public Type Type { get; internal set; }
29-
public bool IsHasMany => GetType() == typeof(HasManyAttribute) || typeof(HasManyAttribute).IsAssignableFrom(GetType());
30+
public bool IsHasMany => GetType() == typeof(HasManyAttribute) || GetType().Inherits(typeof(HasManyAttribute));
3031
public bool IsHasOne => GetType() == typeof(HasOneAttribute);
3132
public Link DocumentLinks { get; } = Link.All;
3233
public bool CanInclude { get; }

test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -210,13 +210,10 @@ public async Task Can_Update_Many_To_Many_Through_Relationship_Link()
210210
var body = await response.Content.ReadAsStringAsync();
211211
Assert.True(HttpStatusCode.OK == response.StatusCode, $"{route} returned {response.StatusCode} status code with payload: {body}");
212212

213-
var articleResponse = _fixture.GetService<IJsonApiDeSerializer>().Deserialize<Article>(body);
214-
Assert.NotNull(articleResponse);
215-
216213
_fixture.ReloadDbContext();
217214
var persistedArticle = await _fixture.Context.Articles
218215
.Include(a => a.ArticleTags)
219-
.SingleAsync(a => a.Id == articleResponse.Id);
216+
.SingleAsync(a => a.Id == article.Id);
220217

221218
var persistedArticleTag = Assert.Single(persistedArticle.ArticleTags);
222219
Assert.Equal(tag.Id, persistedArticleTag.TagId);

0 commit comments

Comments
 (0)