Skip to content

Commit 6fc162f

Browse files
committed
add test for querying HasManyThrough
1 parent 8d887fa commit 6fc162f

File tree

10 files changed

+149
-22
lines changed

10 files changed

+149
-22
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using JsonApiDotNetCore.Controllers;
2+
using JsonApiDotNetCore.Services;
3+
using JsonApiDotNetCoreExample.Models;
4+
5+
namespace JsonApiDotNetCoreExample.Controllers
6+
{
7+
public class ArticlesController : JsonApiController<Article>
8+
{
9+
public ArticlesController(
10+
IJsonApiContext jsonApiContext,
11+
IResourceService<Article> resourceService)
12+
: base(jsonApiContext, resourceService)
13+
{ }
14+
}
15+
}

Diff for: src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs

+4-2
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,15 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
3131
modelBuilder.Entity<CourseStudentEntity>()
3232
.HasOne(r => r.Course)
3333
.WithMany(c => c.Students)
34-
.HasForeignKey(r => r.CourseId)
35-
;
34+
.HasForeignKey(r => r.CourseId);
3635

3736
modelBuilder.Entity<CourseStudentEntity>()
3837
.HasOne(r => r.Student)
3938
.WithMany(s => s.Courses)
4039
.HasForeignKey(r => r.StudentId);
40+
41+
modelBuilder.Entity<ArticleTag>()
42+
.HasKey(bc => new { bc.ArticleId, bc.TagId });
4143
}
4244

4345
public DbSet<TodoItem> TodoItems { get; set; }

Diff for: src/Examples/JsonApiDotNetCoreExample/Models/ArticleTag.cs

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
using JsonApiDotNetCore.Models;
2-
31
namespace JsonApiDotNetCoreExample.Models
42
{
5-
public class ArticleTag : Identifiable
3+
public class ArticleTag
64
{
75
public int ArticleId { get; set; }
86
public Article Article { get; set; }

Diff for: src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs

+9-4
Original file line numberDiff line numberDiff line change
@@ -173,15 +173,20 @@ protected virtual List<RelationshipAttribute> GetRelationships(Type entityType)
173173
throw new JsonApiSetupException($"Invalid '{nameof(HasManyThroughAttribute)}' on type '{entityType}'. Type does not contain a property named '{hasManyThroughAttribute.InternalThroughName}'.");
174174

175175
// assumption: the property should be a generic collection, e.g. List<ArticleTag>
176-
if(prop.PropertyType.IsGenericType == false)
176+
if(throughProperty.PropertyType.IsGenericType == false)
177177
throw new JsonApiSetupException($"Invalid '{nameof(HasManyThroughAttribute)}' on type '{entityType}'. Expected through entity to be a generic type, such as List<{prop.PropertyType}>.");
178-
hasManyThroughAttribute.ThroughType = prop.PropertyType.GetGenericArguments()[0];
178+
179+
hasManyThroughAttribute.ThroughType = throughProperty.PropertyType.GetGenericArguments()[0];
179180

180181
var throughProperties = hasManyThroughAttribute.ThroughType.GetProperties();
182+
181183
// Article → ArticleTag.Article
182-
hasManyThroughAttribute.LeftProperty = throughProperties.Single(x => x.PropertyType == entityType);
184+
hasManyThroughAttribute.LeftProperty = throughProperties.SingleOrDefault(x => x.PropertyType == entityType)
185+
?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a navigation property to type {entityType}");
186+
183187
// Article → ArticleTag.Tag
184-
hasManyThroughAttribute.RightProperty = throughProperties.Single(x => x.PropertyType == hasManyThroughAttribute.Type);
188+
hasManyThroughAttribute.RightProperty = throughProperties.SingleOrDefault(x => x.PropertyType == hasManyThroughAttribute.Type)
189+
?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a navigation property to type {hasManyThroughAttribute.Type}");
185190
}
186191
}
187192

Diff for: src/JsonApiDotNetCore/Builders/DocumentBuilder.cs

+8-9
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,9 @@ private RelationshipData GetRelationshipData(RelationshipAttribute attr, Context
177177
relationshipData.Links.Related = linkBuilder.GetRelatedRelationLink(contextEntity.EntityName, entity.StringId, attr.PublicRelationshipName);
178178
}
179179

180-
// this only includes the navigation property, we need to actually check the navigation property Id
181-
var navigationEntity = _jsonApiContext.ContextGraph.GetRelationship(entity, attr.InternalRelationshipName);
180+
// this only includes the navigation property, we need to actually check the navigation property Id
181+
var navigationEntity = _jsonApiContext.ContextGraph.GetRelationshipValue(entity, attr);
182+
182183
if (navigationEntity == null)
183184
relationshipData.SingleData = attr.IsHasOne
184185
? GetIndependentRelationshipIdentifier((HasOneAttribute)attr, entity)
@@ -213,7 +214,7 @@ private List<ResourceObject> IncludeRelationshipChain(
213214
{
214215
var requestedRelationship = relationshipChain[relationshipChainIndex];
215216
var relationship = parentEntity.Relationships.FirstOrDefault(r => r.PublicRelationshipName == requestedRelationship);
216-
var navigationEntity = _jsonApiContext.ContextGraph.GetRelationship(parentResource, relationship.InternalRelationshipName);
217+
var navigationEntity = _jsonApiContext.ContextGraph.GetRelationship(parentResource, relationship.InternalRelationshipName);
217218
if (navigationEntity is IEnumerable hasManyNavigationEntity)
218219
{
219220
foreach (IIdentifiable includedEntity in hasManyNavigationEntity)
@@ -226,7 +227,7 @@ private List<ResourceObject> IncludeRelationshipChain(
226227
{
227228
included = AddIncludedEntity(included, (IIdentifiable)navigationEntity);
228229
included = IncludeSingleResourceRelationships(included, (IIdentifiable)navigationEntity, relationship, relationshipChain, relationshipChainIndex);
229-
}
230+
}
230231

231232
return included;
232233
}
@@ -284,16 +285,14 @@ private ResourceObject GetIncludedEntity(IIdentifiable entity)
284285

285286
private List<ResourceIdentifierObject> GetRelationships(IEnumerable<object> entities)
286287
{
287-
var objType = entities.GetElementType();
288-
289-
var typeName = _jsonApiContext.ContextGraph.GetContextEntity(objType);
290-
288+
string typeName = null;
291289
var relationships = new List<ResourceIdentifierObject>();
292290
foreach (var entity in entities)
293291
{
292+
typeName = _jsonApiContext.ContextGraph.GetContextEntity(entity.GetType()).EntityName;
294293
relationships.Add(new ResourceIdentifierObject
295294
{
296-
Type = typeName.EntityName,
295+
Type = typeName,
297296
Id = ((IIdentifiable)entity).StringId
298297
});
299298
}

Diff for: src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -275,8 +275,8 @@ public virtual IQueryable<TEntity> Include(IQueryable<TEntity> entities, string
275275
}
276276

277277
internalRelationshipPath = (internalRelationshipPath == null)
278-
? relationship.InternalRelationshipName
279-
: $"{internalRelationshipPath}.{relationship.InternalRelationshipName}";
278+
? relationship.RelationshipPath
279+
: $"{internalRelationshipPath}.{relationship.RelationshipPath}";
280280

281281
if(i < relationshipChain.Length)
282282
entity = _jsonApiContext.ContextGraph.GetContextEntity(relationship.Type);

Diff for: src/JsonApiDotNetCore/Internal/ContextGraph.cs

+39-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using System;
2+
using System.Collections;
23
using System.Collections.Generic;
34
using System.Linq;
5+
using JsonApiDotNetCore.Models;
46

57
namespace JsonApiDotNetCore.Internal
68
{
@@ -19,6 +21,19 @@ public interface IContextGraph
1921
/// </example>
2022
object GetRelationship<TParent>(TParent resource, string propertyName);
2123

24+
/// <summary>
25+
/// Gets the value of the navigation property, defined by the relationshipName,
26+
/// on the provided instance.
27+
/// </summary>
28+
/// <param name="resource">The resource instance</param>
29+
/// <param name="relationship">The attribute used to define the relationship.</param>
30+
/// <example>
31+
/// <code>
32+
/// _graph.GetRelationshipValue(todoItem, nameof(TodoItem.Owner));
33+
/// </code>
34+
/// </example>
35+
object GetRelationshipValue<TParent>(TParent resource, RelationshipAttribute relationship) where TParent : IIdentifiable;
36+
2237
/// <summary>
2338
/// Get the internal navigation property name for the specified public
2439
/// relationship name.
@@ -107,6 +122,29 @@ public object GetRelationship<TParent>(TParent entity, string relationshipName)
107122
return navigationProperty.GetValue(entity);
108123
}
109124

125+
public object GetRelationshipValue<TParent>(TParent resource, RelationshipAttribute relationship) where TParent : IIdentifiable
126+
{
127+
if(relationship is HasManyThroughAttribute hasManyThroughRelationship)
128+
{
129+
return GetHasManyThrough(resource, hasManyThroughRelationship);
130+
}
131+
132+
return GetRelationship(resource, relationship.InternalRelationshipName);
133+
}
134+
135+
private IEnumerable<IIdentifiable> GetHasManyThrough(IIdentifiable parent, HasManyThroughAttribute hasManyThrough)
136+
{
137+
var throughProperty = GetRelationship(parent, hasManyThrough.InternalThroughName);
138+
if (throughProperty is IEnumerable hasManyNavigationEntity)
139+
{
140+
foreach (var includedEntity in hasManyNavigationEntity)
141+
{
142+
var targetValue = hasManyThrough.RightProperty.GetValue(includedEntity) as IIdentifiable;
143+
yield return targetValue;
144+
}
145+
}
146+
}
147+
110148
/// </ inheritdoc>
111149
public string GetRelationshipName<TParent>(string relationshipName)
112150
{
@@ -125,5 +163,5 @@ public string GetPublicAttributeName<TParent>(string internalAttributeName)
125163
.SingleOrDefault(a => a.InternalAttributeName == internalAttributeName)?
126164
.PublicAttributeName;
127165
}
128-
}
166+
}
129167
}

Diff for: src/JsonApiDotNetCore/Models/HasManyThroughAttribute.cs

+6
Original file line numberDiff line numberDiff line change
@@ -80,5 +80,11 @@ public HasManyThroughAttribute(string publicName, string internalThroughName, Li
8080
///
8181
/// </example>
8282
public PropertyInfo RightProperty { get; internal set; }
83+
84+
/// <inheritdoc />
85+
/// <example>
86+
/// "ArticleTags.Tag"
87+
/// </example>
88+
public override string RelationshipPath => $"{InternalThroughName}.{RightProperty.Name}";
8389
}
8490
}

Diff for: src/JsonApiDotNetCore/Models/RelationshipAttribute.cs

+9-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ protected RelationshipAttribute(string publicName, Link documentLinks, bool canI
2525
/// </code>
2626
/// </example>
2727
public Type Type { get; internal set; }
28-
public bool IsHasMany => GetType() == typeof(HasManyAttribute);
28+
public bool IsHasMany => GetType() == typeof(HasManyAttribute) || typeof(HasManyAttribute).IsAssignableFrom(GetType());
2929
public bool IsHasOne => GetType() == typeof(HasOneAttribute);
3030
public Link DocumentLinks { get; } = Link.All;
3131
public bool CanInclude { get; }
@@ -78,5 +78,13 @@ public override bool Equals(object obj)
7878
/// </summary>
7979
public virtual bool Is(string publicRelationshipName)
8080
=> string.Equals(publicRelationshipName, PublicRelationshipName, StringComparison.OrdinalIgnoreCase);
81+
82+
/// <summary>
83+
/// The internal navigation property path to the related entity.
84+
/// <summary>
85+
/// <remarks>
86+
/// In all cases except the HasManyThrough relationships, this will just be the <see cref"InternalRelationshipName" />.
87+
/// </remarks>
88+
public virtual string RelationshipPath => InternalRelationshipName;
8189
}
8290
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using System.Net;
2+
using System.Threading.Tasks;
3+
using Bogus;
4+
using JsonApiDotNetCore.Serialization;
5+
using JsonApiDotNetCoreExample.Data;
6+
using JsonApiDotNetCoreExample.Models;
7+
using Xunit;
8+
9+
namespace JsonApiDotNetCoreExampleTests.Acceptance
10+
{
11+
[Collection("WebHostCollection")]
12+
public class ManyToManyTests
13+
{
14+
private static readonly Faker<Article> _articleFaker = new Faker<Article>()
15+
.RuleFor(a => a.Name, f => f.Random.AlphaNumeric(10))
16+
.RuleFor(a => a.Author, f => new Author());
17+
private static readonly Faker<Tag> _tagFaker = new Faker<Tag>().RuleFor(a => a.Name, f => f.Random.AlphaNumeric(10));
18+
19+
private TestFixture<TestStartup> _fixture;
20+
public ManyToManyTests(TestFixture<TestStartup> fixture)
21+
{
22+
_fixture = fixture;
23+
}
24+
25+
[Fact]
26+
public async Task Can_Fetch_Many_To_Many_Through()
27+
{
28+
// arrange
29+
var context = _fixture.GetService<AppDbContext>();
30+
var article = _articleFaker.Generate();
31+
var tag = _tagFaker.Generate();
32+
var articleTag = new ArticleTag {
33+
Article = article,
34+
Tag = tag
35+
};
36+
context.ArticleTags.Add(articleTag);
37+
await context.SaveChangesAsync();
38+
39+
var route = $"/api/v1/articles/{article.Id}?include=tags";
40+
41+
// act
42+
var response = await _fixture.Client.GetAsync(route);
43+
44+
// assert
45+
var body = await response.Content.ReadAsStringAsync();
46+
Assert.True(HttpStatusCode.OK == response.StatusCode, $"{route} returned {response.StatusCode} status code with payload: {body}");
47+
48+
var articleResponse = _fixture.GetService<IJsonApiDeSerializer>().Deserialize<Article>(body);
49+
Assert.NotNull(articleResponse);
50+
Assert.Equal(article.Id, articleResponse.Id);
51+
52+
var tagResponse = Assert.Single(articleResponse.Tags);
53+
Assert.Equal(tag.Id, tagResponse.Id);
54+
}
55+
}
56+
}

0 commit comments

Comments
 (0)