Skip to content

Commit 3814d5e

Browse files
committed
port @jasolko 's changes
1 parent 61372ff commit 3814d5e

File tree

9 files changed

+152
-4
lines changed

9 files changed

+152
-4
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
.DS_Store
2+
13
## Ignore Visual Studio temporary files, build results, and
24
## files generated by popular Visual Studio add-ons.
35

src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
5353
public DbSet<DepartmentEntity> Departments { get; set; }
5454
public DbSet<CourseStudentEntity> Registrations { get; set; }
5555
public DbSet<StudentEntity> Students { get; set; }
56-
5756
public DbSet<PersonRole> PersonRoles { get; set; }
57+
public DbSet<ArticleTag> ArticleTags { get; set; }
58+
public DbSet<Tag> Tags { get; set; }
5859
}
5960
}

src/Examples/JsonApiDotNetCoreExample/Models/Article.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.Collections.Generic;
2+
using System.ComponentModel.DataAnnotations.Schema;
13
using JsonApiDotNetCore.Models;
24

35
namespace JsonApiDotNetCoreExample.Models
@@ -10,5 +12,10 @@ public class Article : Identifiable
1012
[HasOne("author")]
1113
public Author Author { get; set; }
1214
public int AuthorId { get; set; }
15+
16+
[NotMapped]
17+
[HasManyThrough("tags", nameof(ArticleTags))]
18+
public List<Tag> Tags { get; set; }
19+
public List<ArticleTag> ArticleTags { get; set; }
1320
}
1421
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using JsonApiDotNetCore.Models;
2+
3+
namespace JsonApiDotNetCoreExample.Models
4+
{
5+
public class ArticleTag : Identifiable
6+
{
7+
public int ArticleId { get; set; }
8+
public Article Article { get; set; }
9+
10+
public int TagId { get; set; }
11+
public Tag Tag { get; set; }
12+
}
13+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using JsonApiDotNetCore.Models;
2+
3+
namespace JsonApiDotNetCoreExample.Models
4+
{
5+
public class Tag : Identifiable
6+
{
7+
public string Name { get; set; }
8+
}
9+
}

src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,25 @@ protected virtual List<RelationshipAttribute> GetRelationships(Type entityType)
166166
attribute.InternalRelationshipName = prop.Name;
167167
attribute.Type = GetRelationshipType(attribute, prop);
168168
attributes.Add(attribute);
169+
170+
if(attribute is HasManyThroughAttribute hasManyThroughAttribute) {
171+
var throughProperty = properties.SingleOrDefault(p => p.Name == hasManyThroughAttribute.InternalThroughName);
172+
if(throughProperty == null)
173+
throw new JsonApiSetupException($"Invalid '{nameof(HasManyThroughAttribute)}' on type '{entityType}'. Type does not contain a property named '{hasManyThroughAttribute.InternalThroughName}'.");
174+
175+
// assumption: the property should be a generic collection, e.g. List<ArticleTag>
176+
if(prop.PropertyType.IsGenericType == false)
177+
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];
179+
180+
var throughProperties = hasManyThroughAttribute.ThroughType.GetProperties();
181+
// Article → ArticleTag.Article
182+
hasManyThroughAttribute.LeftProperty = throughProperties.Single(x => x.PropertyType == entityType);
183+
// Article → ArticleTag.Tag
184+
hasManyThroughAttribute.RightProperty = throughProperties.Single(x => x.PropertyType == hasManyThroughAttribute.Type);
185+
}
169186
}
187+
170188
return attributes;
171189
}
172190

src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@ public static class DbContextExtensions
1111
public static DbSet<T> GetDbSet<T>(this DbContext context) where T : class
1212
=> context.Set<T>();
1313

14+
/// <summary>
15+
/// Get the DbSet when the model type is unknown until runtime
16+
/// </summary>
17+
public static IQueryable<object> Set(this DbContext context, Type t)
18+
=> (IQueryable<object>)context
19+
.GetType()
20+
.GetMethod("Set")
21+
.MakeGenericMethod(t) // TODO: will caching help runtime performance?
22+
.Invoke(context, null);
23+
1424
/// <summary>
1525
/// Determines whether or not EF is already tracking an entity of the same Type and Id
1626
/// </summary>

src/JsonApiDotNetCore/Internal/Generics/GenericProcessor.cs

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
using System;
12
using System.Collections.Generic;
23
using System.Linq;
4+
using System.Linq.Expressions;
35
using System.Threading.Tasks;
46
using JsonApiDotNetCore.Data;
7+
using JsonApiDotNetCore.Extensions;
58
using JsonApiDotNetCore.Models;
69
using Microsoft.EntityFrameworkCore;
710

@@ -21,16 +24,41 @@ public GenericProcessor(IDbContextResolver contextResolver)
2124
_context = contextResolver.GetContext();
2225
}
2326

24-
public async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable<string> relationshipIds)
27+
public virtual async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable<string> relationshipIds)
2528
{
2629
SetRelationships(parent, relationship, relationshipIds);
2730

2831
await _context.SaveChangesAsync();
2932
}
3033

31-
public void SetRelationships(object parent, RelationshipAttribute relationship, IEnumerable<string> relationshipIds)
34+
public virtual void SetRelationships(object parent, RelationshipAttribute relationship, IEnumerable<string> relationshipIds)
3235
{
33-
if (relationship.IsHasMany)
36+
if (relationship is HasManyThroughAttribute hasManyThrough)
37+
{
38+
var parentId = ((IIdentifiable)parent).StringId;
39+
ParameterExpression parameter = Expression.Parameter(hasManyThrough.Type);
40+
Expression property = Expression.Property(parameter, hasManyThrough.LeftProperty);
41+
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);
45+
46+
var oldLinks = _context
47+
.Set(hasManyThrough.ThroughType)
48+
.Where(lambda.Compile())
49+
.ToList();
50+
51+
_context.Remove(oldLinks);
52+
53+
var newLinks = relationshipIds.Select(x => {
54+
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));
57+
return link;
58+
});
59+
_context.AddRange(newLinks);
60+
}
61+
else if (relationship.IsHasMany)
3462
{
3563
var entities = _context.Set<T>().Where(x => relationshipIds.Contains(x.StringId)).ToList();
3664
relationship.SetValue(parent, entities);
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using System;
2+
using System.Reflection;
3+
4+
namespace JsonApiDotNetCore.Models
5+
{
6+
/// <summary>
7+
///
8+
/// </summary>
9+
///
10+
/// <example>
11+
///
12+
/// </example>
13+
public class HasManyThroughAttribute : HasManyAttribute
14+
{
15+
/// <summary>
16+
///
17+
/// </summary>
18+
/// <example>
19+
///
20+
/// </example>
21+
public HasManyThroughAttribute(string publicName, string internalThroughName, Link documentLinks = Link.All, bool canInclude = true)
22+
: base(publicName, documentLinks, canInclude)
23+
{
24+
InternalThroughName = internalThroughName;
25+
}
26+
27+
public string InternalThroughName { get; private set; }
28+
public Type ThroughType { get; internal set; }
29+
30+
/// <summary>
31+
/// The navigation property back to the parent resource.
32+
/// </summary>
33+
///
34+
/// <example>
35+
/// In the `[HasManyThrough("tags", nameof(ArticleTags))]` example
36+
/// this would point to the `Article.ArticleTags.Article` property
37+
///
38+
/// <code>
39+
/// public Article Article { get; set; }
40+
/// </code>
41+
///
42+
/// </example>
43+
public PropertyInfo LeftProperty { get; internal set; }
44+
45+
/// <summary>
46+
/// The navigation property to the related resource.
47+
/// </summary>
48+
///
49+
/// <example>
50+
/// In the `[HasManyThrough("tags", nameof(ArticleTags))]` example
51+
/// this would point to the `Article.ArticleTags.Tag` property
52+
///
53+
/// <code>
54+
/// public Tag Tag { get; set; }
55+
/// </code>
56+
///
57+
/// </example>
58+
public PropertyInfo RightProperty { get; internal set; }
59+
}
60+
}

0 commit comments

Comments
 (0)