Skip to content

Commit 74aa7df

Browse files
author
Bart Koelman
committed
Flexible collection types/interfaces on HasMany attributes
1 parent 958cc99 commit 74aa7df

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+235
-144
lines changed

src/Examples/GettingStarted/Models/Person.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ public sealed class Person : Identifiable
77
{
88
[Attr]
99
public string Name { get; set; }
10+
1011
[HasMany]
11-
public List<Article> Articles { get; set; }
12+
public ICollection<Article> Articles { get; set; }
1213
}
1314
}

src/Examples/JsonApiDotNetCoreExample/Models/Author.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ public sealed class Author : Identifiable
99
public string Name { get; set; }
1010

1111
[HasMany]
12-
public List<Article> Articles { get; set; }
12+
public IList<Article> Articles { get; set; }
1313
}
1414
}
1515

src/Examples/JsonApiDotNetCoreExample/Models/Person.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,13 @@ public string FirstName
4444
public Gender Gender { get; set; }
4545

4646
[HasMany]
47-
public List<TodoItem> TodoItems { get; set; }
47+
public ISet<TodoItem> TodoItems { get; set; }
4848

4949
[HasMany]
50-
public List<TodoItem> AssignedTodoItems { get; set; }
50+
public ISet<TodoItem> AssignedTodoItems { get; set; }
5151

5252
[HasMany]
53-
public List<TodoItemCollection> todoCollections { get; set; }
53+
public HashSet<TodoItemCollection> todoCollections { get; set; }
5454

5555
[HasOne]
5656
public PersonRole Role { get; set; }

src/Examples/JsonApiDotNetCoreExample/Models/TodoItem.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public string AlwaysChangingValue
6262
public int? OneToOnePersonId { get; set; }
6363

6464
[HasMany]
65-
public List<Person> StakeHolders { get; set; }
65+
public ISet<Person> StakeHolders { get; set; }
6666

6767
[HasOne]
6868
public TodoItemCollection Collection { get; set; }
@@ -80,6 +80,6 @@ public string AlwaysChangingValue
8080
public TodoItem ParentTodo { get; set; }
8181

8282
[HasMany]
83-
public List<TodoItem> ChildrenTodos { get; set; }
83+
public IList<TodoItem> ChildrenTodos { get; set; }
8484
}
8585
}

src/Examples/JsonApiDotNetCoreExample/Models/TodoItemCollection.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public sealed class TodoItemCollection : Identifiable<Guid>
1111
public string Name { get; set; }
1212

1313
[HasMany]
14-
public List<TodoItem> TodoItems { get; set; }
14+
public ISet<TodoItem> TodoItems { get; set; }
1515

1616
[HasOne]
1717
public Person Owner { get; set; }

src/JsonApiDotNetCore/Data/DefaultResourceRepository.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -244,20 +244,22 @@ private object GetTrackedRelationshipValue(RelationshipAttribute relationshipAtt
244244
}
245245

246246
// helper method used in GetTrackedRelationshipValue. See comments below.
247-
private IList GetTrackedManyRelationshipValue(IEnumerable<IIdentifiable> relationshipValueList, RelationshipAttribute relationshipAttr, ref bool wasAlreadyAttached)
247+
private IEnumerable GetTrackedManyRelationshipValue(IEnumerable<IIdentifiable> relationshipValueList, RelationshipAttribute relationshipAttr, ref bool wasAlreadyAttached)
248248
{
249249
if (relationshipValueList == null) return null;
250250
bool newWasAlreadyAttached = false;
251251
var trackedPointerCollection = relationshipValueList.Select(pointer =>
252-
{ // convert each element in the value list to relationshipAttr.DependentType.
252+
{
253+
// convert each element in the value list to relationshipAttr.DependentType.
253254
var tracked = AttachOrGetTracked(pointer);
254255
if (tracked != null) newWasAlreadyAttached = true;
255256
return Convert.ChangeType(tracked ?? pointer, relationshipAttr.RightType);
256257
})
257258
.ToList()
258-
.Cast(relationshipAttr.RightType);
259+
.CopyToTypedCollection(relationshipAttr.PropertyInfo.PropertyType);
260+
259261
if (newWasAlreadyAttached) wasAlreadyAttached = true;
260-
return (IList)trackedPointerCollection;
262+
return trackedPointerCollection;
261263
}
262264

263265
// helper method used in GetTrackedRelationshipValue. See comments there.

src/JsonApiDotNetCore/Extensions/QueryableExtensions.cs

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -377,15 +377,14 @@ private static IQueryable<TSource> CallGenericSelectMethod<TSource>(IQueryable<T
377377
Expression bindExpression;
378378
if (nestedPropertyType != typeof(string) && typeof(IEnumerable).IsAssignableFrom(nestedPropertyType))
379379
{
380-
// Concrete type of Collection
381-
var singleType = nestedPropertyType.GetGenericArguments().Single();
380+
var collectionElementType = nestedPropertyType.GetGenericArguments().Single();
382381
// {y}
383-
var nestedParameter = Expression.Parameter(singleType, "y");
382+
var nestedParameter = Expression.Parameter(collectionElementType, "y");
384383
nestedBindings = item.Value.Select(prop => Expression.Bind(
385-
singleType.GetProperty(prop), Expression.PropertyOrField(nestedParameter, prop))).ToList();
384+
collectionElementType.GetProperty(prop), Expression.PropertyOrField(nestedParameter, prop))).ToList();
386385

387386
// { new Item() }
388-
var newNestedExp = Expression.New(singleType);
387+
var newNestedExp = Expression.New(collectionElementType);
389388
var initNestedExp = Expression.MemberInit(newNestedExp, nestedBindings);
390389
// { y => new Item() {Id = y.Id, Name = y.Name}}
391390
var body = Expression.Lambda(initNestedExp, nestedParameter);
@@ -395,15 +394,15 @@ private static IQueryable<TSource> CallGenericSelectMethod<TSource>(IQueryable<T
395394
Expression selectMethod = Expression.Call(
396395
typeof(Enumerable),
397396
"Select",
398-
new[] { singleType, singleType },
397+
new[] { collectionElementType, collectionElementType },
399398
propertyExpression, body);
400399

401-
// { x.Items.Select(y => new Item() {Id = y.Id, Name = y.Name}).ToList() }
402-
bindExpression = Expression.Call(
403-
typeof(Enumerable),
404-
"ToList",
405-
new[] { singleType },
406-
selectMethod);
400+
var enumerableOfElementType = typeof(IEnumerable<>).MakeGenericType(collectionElementType);
401+
var typedCollection = nestedPropertyType.ToConcreteCollectionType();
402+
var typedCollectionConstructor = typedCollection.GetConstructor(new[] {enumerableOfElementType});
403+
404+
// { new HashSet<Item>(x.Items.Select(y => new Item() {Id = y.Id, Name = y.Name})) }
405+
bindExpression = Expression.New(typedCollectionConstructor, selectMethod);
407406
}
408407
// [HasOne] attribute
409408
else

src/JsonApiDotNetCore/Extensions/TypeExtensions.cs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Collections;
44
using System.Collections.Generic;
55
using System.Linq;
6+
using System.Reflection;
67
using JsonApiDotNetCore.Models;
78

89
namespace JsonApiDotNetCore.Extensions
@@ -13,10 +14,10 @@ internal static class TypeExtensions
1314
/// Extension to use the LINQ cast method in a non-generic way:
1415
/// <code>
1516
/// Type targetType = typeof(TResource)
16-
/// ((IList)myList).Cast(targetType).
17+
/// ((IList)myList).CopyToList(targetType).
1718
/// </code>
1819
/// </summary>
19-
public static IEnumerable Cast(this IEnumerable source, Type type)
20+
public static IEnumerable CopyToList(this IEnumerable source, Type type)
2021
{
2122
if (source == null) throw new ArgumentNullException(nameof(source));
2223
if (type == null) throw new ArgumentNullException(nameof(type));
@@ -29,6 +30,28 @@ public static IEnumerable Cast(this IEnumerable source, Type type)
2930
return list;
3031
}
3132

33+
/// <summary>
34+
/// Creates a collection instance based on the specified collection type and copies the specified elements into it.
35+
/// </summary>
36+
/// <param name="source">Source to copy from.</param>
37+
/// <param name="collectionType">Target collection type, for example: typeof(List{Article}) or typeof(ISet{Person}).</param>
38+
/// <returns></returns>
39+
public static IEnumerable CopyToTypedCollection(this IEnumerable source, Type collectionType)
40+
{
41+
if (source == null) throw new ArgumentNullException(nameof(source));
42+
if (collectionType == null) throw new ArgumentNullException(nameof(collectionType));
43+
44+
var concreteCollectionType = collectionType.ToConcreteCollectionType();
45+
dynamic concreteCollectionInstance = concreteCollectionType.New<dynamic>();
46+
47+
foreach (var item in source)
48+
{
49+
concreteCollectionInstance.Add((dynamic) item);
50+
}
51+
52+
return concreteCollectionInstance;
53+
}
54+
3255
/// <summary>
3356
/// Creates a List{TInterface} where TInterface is the generic for type specified by t
3457
/// </summary>
@@ -99,5 +122,10 @@ public static bool Inherits<T>(this Type concreteType)
99122
/// </summary>
100123
public static bool Inherits(this Type concreteType, Type interfaceType)
101124
=> interfaceType?.IsAssignableFrom(concreteType) == true;
125+
126+
public static bool ImplementsInterface(this Type source, Type interfaceType)
127+
{
128+
return source.GetInterfaces().Any(type => type == interfaceType);
129+
}
102130
}
103131
}

src/JsonApiDotNetCore/Hooks/Execution/DiffableEntityHashSet.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,11 @@ public IEnumerable<EntityDiffPair<TResource>> GetDiffs()
7575
{
7676
var propertyInfo = TypeHelper.ParseNavigationExpression(navigationAction);
7777
var propertyType = propertyInfo.PropertyType;
78-
if (propertyType.Inherits(typeof(IEnumerable))) propertyType = TypeHelper.GetTypeOfList(propertyType);
78+
if (propertyType.ImplementsInterface(typeof(IEnumerable)))
79+
{
80+
propertyType = TypeHelper.TryGetCollectionElementType(propertyType);
81+
}
82+
7983
if (propertyType.Implements<IIdentifiable>())
8084
{
8185
// the navigation action references a relationship. Redirect the call to the relationship dictionary.

src/JsonApiDotNetCore/Hooks/Execution/HookExecutorHelper.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,10 @@ public IEnumerable LoadDbValues(LeftType entityTypeForRepository, IEnumerable en
8181
.GetMethod(nameof(GetWhereAndInclude), BindingFlags.NonPublic | BindingFlags.Instance)
8282
.MakeGenericMethod(entityTypeForRepository, idType);
8383
var cast = ((IEnumerable<object>)entities).Cast<IIdentifiable>();
84-
var ids = cast.Select(e => e.StringId).Cast(idType);
84+
var ids = cast.Select(e => e.StringId).CopyToList(idType);
8585
var values = (IEnumerable)parameterizedGetWhere.Invoke(this, new object[] { ids, relationshipsToNextLayer });
8686
if (values == null) return null;
87-
return (IEnumerable)Activator.CreateInstance(typeof(HashSet<>).MakeGenericType(entityTypeForRepository), values.Cast(entityTypeForRepository));
87+
return (IEnumerable)Activator.CreateInstance(typeof(HashSet<>).MakeGenericType(entityTypeForRepository), values.CopyToList(entityTypeForRepository));
8888
}
8989

9090
public HashSet<TResource> LoadDbValues<TResource>(IEnumerable<TResource> entities, ResourceHook hook, params RelationshipAttribute[] relationships) where TResource : class, IIdentifiable

0 commit comments

Comments
 (0)