Skip to content

Commit c0ed9a9

Browse files
author
Bart Koelman
committed
Tests for cyclic relationships
1 parent 8e46750 commit c0ed9a9

13 files changed

+980
-221
lines changed

src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs

+26-6
Original file line numberDiff line numberDiff line change
@@ -187,22 +187,42 @@ private IReadOnlyCollection<RelationshipAttribute> GetRelationships(Type resourc
187187
var throughProperties = throughType.GetProperties();
188188

189189
// ArticleTag.Article
190-
hasManyThroughAttribute.LeftProperty = throughProperties.SingleOrDefault(x => x.PropertyType.IsAssignableFrom(resourceType))
191-
?? throw new InvalidConfigurationException($"{throughType} does not contain a navigation property to type {resourceType}");
190+
if (hasManyThroughAttribute.LeftPropertyName != null)
191+
{
192+
// In case of a self-referencing many-to-many relationship, the left property name must be specified.
193+
hasManyThroughAttribute.LeftProperty = hasManyThroughAttribute.ThroughType.GetProperty(hasManyThroughAttribute.LeftPropertyName)
194+
?? throw new InvalidConfigurationException($"'{throughType}' does not contain a navigation property named '{hasManyThroughAttribute.LeftPropertyName}'.");
195+
}
196+
else
197+
{
198+
// In case of a non-self-referencing many-to-many relationship, we just pick the single compatible type.
199+
hasManyThroughAttribute.LeftProperty = throughProperties.SingleOrDefault(x => x.PropertyType.IsAssignableFrom(resourceType))
200+
?? throw new InvalidConfigurationException($"'{throughType}' does not contain a navigation property to type '{resourceType}'.");
201+
}
192202

193203
// ArticleTag.ArticleId
194204
var leftIdPropertyName = hasManyThroughAttribute.LeftIdPropertyName ?? hasManyThroughAttribute.LeftProperty.Name + "Id";
195205
hasManyThroughAttribute.LeftIdProperty = throughProperties.SingleOrDefault(x => x.Name == leftIdPropertyName)
196-
?? throw new InvalidConfigurationException($"{throughType} does not contain a relationship ID property to type {resourceType} with name {leftIdPropertyName}");
206+
?? throw new InvalidConfigurationException($"'{throughType}' does not contain a relationship ID property to type '{resourceType}' with name '{leftIdPropertyName}'.");
197207

198208
// ArticleTag.Tag
199-
hasManyThroughAttribute.RightProperty = throughProperties.SingleOrDefault(x => x.PropertyType == hasManyThroughAttribute.RightType)
200-
?? throw new InvalidConfigurationException($"{throughType} does not contain a navigation property to type {hasManyThroughAttribute.RightType}");
209+
if (hasManyThroughAttribute.RightPropertyName != null)
210+
{
211+
// In case of a self-referencing many-to-many relationship, the right property name must be specified.
212+
hasManyThroughAttribute.RightProperty = hasManyThroughAttribute.ThroughType.GetProperty(hasManyThroughAttribute.RightPropertyName)
213+
?? throw new InvalidConfigurationException($"'{throughType}' does not contain a navigation property named '{hasManyThroughAttribute.RightPropertyName}'.");
214+
}
215+
else
216+
{
217+
// In case of a non-self-referencing many-to-many relationship, we just pick the single compatible type.
218+
hasManyThroughAttribute.RightProperty = throughProperties.SingleOrDefault(x => x.PropertyType == hasManyThroughAttribute.RightType)
219+
?? throw new InvalidConfigurationException($"'{throughType}' does not contain a navigation property to type '{hasManyThroughAttribute.RightType}'.");
220+
}
201221

202222
// ArticleTag.TagId
203223
var rightIdPropertyName = hasManyThroughAttribute.RightIdPropertyName ?? hasManyThroughAttribute.RightProperty.Name + "Id";
204224
hasManyThroughAttribute.RightIdProperty = throughProperties.SingleOrDefault(x => x.Name == rightIdPropertyName)
205-
?? throw new InvalidConfigurationException($"{throughType} does not contain a relationship ID property to type {hasManyThroughAttribute.RightType} with name {rightIdPropertyName}");
225+
?? throw new InvalidConfigurationException($"'{throughType}' does not contain a relationship ID property to type '{hasManyThroughAttribute.RightType}' with name '{rightIdPropertyName}'.");
206226
}
207227
}
208228

src/JsonApiDotNetCore/Resources/Annotations/HasManyThroughAttribute.cs

+12
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,18 @@ public sealed class HasManyThroughAttribute : HasManyAttribute
9191
/// </summary>
9292
public override string RelationshipPath => $"{ThroughProperty.Name}.{RightProperty.Name}";
9393

94+
/// <summary>
95+
/// Required for a self-referencing many-to-many relationship.
96+
/// Contains the name of the property back to the parent resource from the through type.
97+
/// </summary>
98+
public string LeftPropertyName { get; set; }
99+
100+
/// <summary>
101+
/// Required for a self-referencing many-to-many relationship.
102+
/// Contains the name of the property to the related resource from the through type.
103+
/// </summary>
104+
public string RightPropertyName { get; set; }
105+
94106
/// <summary>
95107
/// Optional. Can be used to indicate a non-default name for the ID property back to the parent resource from the through type.
96108
/// Defaults to the name of <see cref="LeftProperty"/> suffixed with "Id".

test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingRelationshipsTests.cs

-214
This file was deleted.

test/JsonApiDotNetCoreExampleTests/IntegrationTests/Writing/Updating/Relationships/AddToToManyRelationshipTests.cs

+103
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Collections.Generic;
12
using System.Linq;
23
using System.Net;
34
using System.Threading.Tasks;
@@ -660,5 +661,107 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
660661
workItemInDatabase.Subscribers.Should().HaveCount(0);
661662
});
662663
}
664+
665+
[Fact]
666+
public async Task Can_add_self_to_cyclic_HasMany_relationship()
667+
{
668+
// Arrange
669+
var existingWorkItem = _fakers.WorkItem.Generate();
670+
existingWorkItem.Children = _fakers.WorkItem.Generate(1);
671+
672+
await _testContext.RunOnDatabaseAsync(async dbContext =>
673+
{
674+
dbContext.WorkItems.Add(existingWorkItem);
675+
await dbContext.SaveChangesAsync();
676+
});
677+
678+
var requestBody = new
679+
{
680+
data = new[]
681+
{
682+
new
683+
{
684+
type = "workItems",
685+
id = existingWorkItem.StringId
686+
}
687+
}
688+
};
689+
690+
var route = $"/workItems/{existingWorkItem.StringId}/relationships/children";
691+
692+
// Act
693+
var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync<string>(route, requestBody);
694+
695+
// Assert
696+
httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent);
697+
698+
responseDocument.Should().BeEmpty();
699+
700+
await _testContext.RunOnDatabaseAsync(async dbContext =>
701+
{
702+
var workItemInDatabase = await dbContext.WorkItems
703+
.Include(workItem => workItem.Children)
704+
.FirstAsync(workItem => workItem.Id == existingWorkItem.Id);
705+
706+
workItemInDatabase.Children.Should().HaveCount(2);
707+
workItemInDatabase.Children.Should().ContainSingle(workItem => workItem.Id == existingWorkItem.Children[0].Id);
708+
workItemInDatabase.Children.Should().ContainSingle(workItem => workItem.Id == existingWorkItem.Id);
709+
});
710+
}
711+
712+
[Fact]
713+
public async Task Can_add_self_to_cyclic_HasManyThrough_relationship()
714+
{
715+
// Arrange
716+
var existingWorkItem = _fakers.WorkItem.Generate();
717+
existingWorkItem.RelatedToItems = new List<WorkItemToWorkItem>
718+
{
719+
new WorkItemToWorkItem
720+
{
721+
ToItem = _fakers.WorkItem.Generate()
722+
}
723+
};
724+
725+
await _testContext.RunOnDatabaseAsync(async dbContext =>
726+
{
727+
dbContext.WorkItems.Add(existingWorkItem);
728+
await dbContext.SaveChangesAsync();
729+
});
730+
731+
var requestBody = new
732+
{
733+
data = new[]
734+
{
735+
new
736+
{
737+
type = "workItems",
738+
id = existingWorkItem.StringId
739+
}
740+
}
741+
};
742+
743+
var route = $"/workItems/{existingWorkItem.StringId}/relationships/relatedTo";
744+
745+
// Act
746+
var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync<string>(route, requestBody);
747+
748+
// Assert
749+
httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent);
750+
751+
responseDocument.Should().BeEmpty();
752+
753+
await _testContext.RunOnDatabaseAsync(async dbContext =>
754+
{
755+
var workItemInDatabase = await dbContext.WorkItems
756+
.Include(workItem => workItem.RelatedToItems)
757+
.ThenInclude(workItemToWorkItem => workItemToWorkItem.ToItem)
758+
.FirstAsync(workItem => workItem.Id == existingWorkItem.Id);
759+
760+
workItemInDatabase.RelatedToItems.Should().HaveCount(2);
761+
workItemInDatabase.RelatedToItems.Should().OnlyContain(workItemToWorkItem => workItemToWorkItem.FromItem.Id == existingWorkItem.Id);
762+
workItemInDatabase.RelatedToItems.Should().ContainSingle(workItemToWorkItem => workItemToWorkItem.ToItem.Id == existingWorkItem.Id);
763+
workItemInDatabase.RelatedToItems.Should().ContainSingle(workItemToWorkItem => workItemToWorkItem.ToItem.Id == existingWorkItem.RelatedToItems[0].ToItem.Id);
764+
});
765+
}
663766
}
664767
}

0 commit comments

Comments
 (0)