Skip to content

Commit ca79de7

Browse files
author
Bart Koelman
committed
Cleanup collections
1 parent bd4dda1 commit ca79de7

File tree

10 files changed

+63
-77
lines changed

10 files changed

+63
-77
lines changed

src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ protected virtual List<RelationshipAttribute> GetRelationships(Type entityType)
139139
if (throughProperty == null)
140140
throw new JsonApiSetupException($"Invalid {nameof(HasManyThroughAttribute)} on '{entityType}.{attribute.PropertyInfo.Name}': Resource does not contain a property named '{hasManyThroughAttribute.ThroughPropertyName}'.");
141141

142-
var throughType = TryGetManyThroughType(throughProperty);
142+
var throughType = TryGetThroughType(throughProperty);
143143
if (throughType == null)
144144
throw new JsonApiSetupException($"Invalid {nameof(HasManyThroughAttribute)} on '{entityType}.{attribute.PropertyInfo.Name}': Referenced property '{throughProperty.Name}' does not implement 'ICollection<T>'.");
145145

@@ -149,32 +149,32 @@ protected virtual List<RelationshipAttribute> GetRelationships(Type entityType)
149149
// ArticleTag
150150
hasManyThroughAttribute.ThroughType = throughType;
151151

152-
var throughProperties = hasManyThroughAttribute.ThroughType.GetProperties();
152+
var throughProperties = throughType.GetProperties();
153153

154154
// ArticleTag.Article
155155
hasManyThroughAttribute.LeftProperty = throughProperties.SingleOrDefault(x => x.PropertyType == entityType)
156-
?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a navigation property to type {entityType}");
156+
?? throw new JsonApiSetupException($"{throughType} does not contain a navigation property to type {entityType}");
157157

158158
// ArticleTag.ArticleId
159159
var leftIdPropertyName = JsonApiOptions.RelatedIdMapper.GetRelatedIdPropertyName(hasManyThroughAttribute.LeftProperty.Name);
160160
hasManyThroughAttribute.LeftIdProperty = throughProperties.SingleOrDefault(x => x.Name == leftIdPropertyName)
161-
?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a relationship id property to type {entityType} with name {leftIdPropertyName}");
161+
?? throw new JsonApiSetupException($"{throughType} does not contain a relationship id property to type {entityType} with name {leftIdPropertyName}");
162162

163163
// ArticleTag.Tag
164164
hasManyThroughAttribute.RightProperty = throughProperties.SingleOrDefault(x => x.PropertyType == hasManyThroughAttribute.RightType)
165-
?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a navigation property to type {hasManyThroughAttribute.RightType}");
165+
?? throw new JsonApiSetupException($"{throughType} does not contain a navigation property to type {hasManyThroughAttribute.RightType}");
166166

167167
// ArticleTag.TagId
168168
var rightIdPropertyName = JsonApiOptions.RelatedIdMapper.GetRelatedIdPropertyName(hasManyThroughAttribute.RightProperty.Name);
169169
hasManyThroughAttribute.RightIdProperty = throughProperties.SingleOrDefault(x => x.Name == rightIdPropertyName)
170-
?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a relationship id property to type {hasManyThroughAttribute.RightType} with name {rightIdPropertyName}");
170+
?? throw new JsonApiSetupException($"{throughType} does not contain a relationship id property to type {hasManyThroughAttribute.RightType} with name {rightIdPropertyName}");
171171
}
172172
}
173173

174174
return attributes;
175175
}
176176

177-
private static Type TryGetManyThroughType(PropertyInfo throughProperty)
177+
private static Type TryGetThroughType(PropertyInfo throughProperty)
178178
{
179179
if (throughProperty.PropertyType.IsGenericType)
180180
{

src/JsonApiDotNetCore/Extensions/TypeExtensions.cs

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

98
namespace JsonApiDotNetCore.Extensions
@@ -17,17 +16,10 @@ internal static class TypeExtensions
1716
/// ((IList)myList).CopyToList(targetType).
1817
/// </code>
1918
/// </summary>
20-
public static IEnumerable CopyToList(this IEnumerable source, Type type)
19+
public static IList CopyToList(this IEnumerable source, Type type)
2120
{
22-
if (source == null) throw new ArgumentNullException(nameof(source));
23-
if (type == null) throw new ArgumentNullException(nameof(type));
24-
25-
var list = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(type));
26-
foreach (var item in source.Cast<object>())
27-
{
28-
list.Add(TypeHelper.ConvertType(item, type));
29-
}
30-
return list;
21+
Type collectionType = typeof(List<>).MakeGenericType(type);
22+
return (IList)CopyToTypedCollection(source, collectionType);
3123
}
3224

3325
/// <summary>

src/JsonApiDotNetCore/Graph/TypeLocator.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,8 @@ namespace JsonApiDotNetCore.Graph
1313
internal static class TypeLocator
1414
{
1515
/// <summary>
16-
/// Determine whether or not this is a json:api resource by checking if it implements <see cref="IIdentifiable"/>.
17-
/// Returns the status and the resultant id type, either `(true, Type)` OR `(false, null)`
18-
/// </summary>
16+
/// Determine whether or not this is a json:api resource by checking if it implements <see cref="IIdentifiable{T}"/>.
17+
/// </summary>
1918
public static Type GetIdType(Type resourceType)
2019
{
2120
var identifiableInterface = resourceType.GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IIdentifiable<>));

src/JsonApiDotNetCore/Hooks/Execution/HookExecutorHelper.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,12 @@ public IResourceHookContainer<TResource> GetResourceHookContainer<TResource>(Res
7676

7777
public IEnumerable LoadDbValues(LeftType entityTypeForRepository, IEnumerable entities, ResourceHook hook, params RelationshipAttribute[] relationshipsToNextLayer)
7878
{
79-
var idType = TypeHelper.GetIdentifierType(entityTypeForRepository);
79+
var idType = TypeHelper.GetIdType(entityTypeForRepository);
8080
var parameterizedGetWhere = GetType()
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).CopyToList(idType);
84+
var ids = cast.Select(e => TypeHelper.ConvertType(e.StringId, idType)).CopyToList(idType);
8585
var values = (IEnumerable)parameterizedGetWhere.Invoke(this, new object[] { ids, relationshipsToNextLayer });
8686
if (values == null) return null;
8787
return (IEnumerable)Activator.CreateInstance(typeof(HashSet<>).MakeGenericType(entityTypeForRepository), values.CopyToList(entityTypeForRepository));

src/JsonApiDotNetCore/Internal/Generics/RepositoryRelationshipUpdateHelper.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ private async Task UpdateOneToOneAsync(IIdentifiable parent, RelationshipAttribu
5555
TRelatedResource value = null;
5656
if (relationshipIds.Any())
5757
{ // newOwner.id
58-
var target = Expression.Constant(TypeHelper.ConvertType(relationshipIds.First(), TypeHelper.GetIdentifierType(relationship.RightType)));
58+
var target = Expression.Constant(TypeHelper.ConvertType(relationshipIds.First(), TypeHelper.GetIdType(relationship.RightType)));
5959
// (Person p) => ...
6060
ParameterExpression parameter = Expression.Parameter(typeof(TRelatedResource));
6161
// (Person p) => p.Id
@@ -80,7 +80,7 @@ private async Task UpdateOneToManyAsync(IIdentifiable parent, RelationshipAttrib
8080
{
8181
// [1, 2, 3]
8282
var target = Expression.Constant(TypeHelper.ConvertListType(relationshipIds,
83-
TypeHelper.GetIdentifierType(relationship.RightType)));
83+
TypeHelper.GetIdType(relationship.RightType)));
8484
// (Person p) => ...
8585
ParameterExpression parameter = Expression.Parameter(typeof(TRelatedResource));
8686
// (Person p) => p.Id

src/JsonApiDotNetCore/Internal/TypeHelper.cs

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,26 @@
55
using System.Reflection;
66
using System.Linq.Expressions;
77
using JsonApiDotNetCore.Extensions;
8+
using JsonApiDotNetCore.Graph;
89
using JsonApiDotNetCore.Models;
910

1011
namespace JsonApiDotNetCore.Internal
1112
{
1213
internal static class TypeHelper
1314
{
14-
private static bool IsNullable(Type type)
15-
{
16-
return !type.IsValueType || Nullable.GetUnderlyingType(type) != null;
17-
}
18-
1915
public static object ConvertType(object value, Type type)
2016
{
21-
if (value == null && !IsNullable(type))
17+
if (value == null && !CanBeNull(type))
2218
throw new FormatException("Cannot convert null to a non-nullable type");
2319

2420
if (value == null)
2521
return null;
2622

27-
Type typeOfValue = value.GetType();
23+
Type runtimeType = value.GetType();
2824

2925
try
3026
{
31-
if (typeOfValue == type || type.IsAssignableFrom(typeOfValue))
27+
if (runtimeType == type || type.IsAssignableFrom(runtimeType))
3228
return value;
3329

3430
type = Nullable.GetUnderlyingType(type) ?? type;
@@ -44,7 +40,6 @@ public static object ConvertType(object value, Type type)
4440
if (type == typeof(DateTimeOffset))
4541
return DateTimeOffset.Parse(stringValue);
4642

47-
4843
if (type == typeof(TimeSpan))
4944
return TimeSpan.Parse(stringValue);
5045

@@ -53,12 +48,17 @@ public static object ConvertType(object value, Type type)
5348

5449
return Convert.ChangeType(stringValue, type);
5550
}
56-
catch (Exception e)
51+
catch (Exception exception)
5752
{
58-
throw new FormatException($"{typeOfValue} cannot be converted to {type}", e);
53+
throw new FormatException($"Failed to convert '{value}' of type '{runtimeType}' to type '{type}'.", exception);
5954
}
6055
}
6156

57+
private static bool CanBeNull(Type type)
58+
{
59+
return !type.IsValueType || Nullable.GetUnderlyingType(type) != null;
60+
}
61+
6262
internal static object GetDefaultValue(this Type type)
6363
{
6464
return type.IsValueType ? type.New() : null;
@@ -238,19 +238,11 @@ public static Type ToConcreteCollectionType(this Type collectionType)
238238
/// <summary>
239239
/// Gets the type (Guid or int) of the Id of a type that implements IIdentifiable
240240
/// </summary>
241-
public static Type GetIdentifierType(Type entityType)
242-
{
243-
var property = entityType.GetProperty("Id");
244-
if (property == null) throw new ArgumentException("Type does not have a property Id");
245-
return entityType.GetProperty("Id").PropertyType;
246-
}
247-
248-
/// <summary>
249-
/// Gets the type (Guid or int) of the Id of a type that implements IIdentifiable
250-
/// </summary>
251-
public static Type GetIdentifierType<T>() where T : IIdentifiable
241+
public static Type GetIdType(Type resourceType)
252242
{
253-
return typeof(T).GetProperty("Id").PropertyType;
243+
var property = resourceType.GetProperty(nameof(Identifiable.Id));
244+
if (property == null) throw new ArgumentException("Type does not have 'Id' property.");
245+
return property.PropertyType;
254246
}
255247
}
256248
}

src/JsonApiDotNetCore/Models/Annotation/HasManyThroughAttribute.cs

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using System.Linq;
55
using System.Reflection;
66
using JsonApiDotNetCore.Extensions;
7-
using JsonApiDotNetCore.Internal;
87
using JsonApiDotNetCore.Models.Links;
98

109
namespace JsonApiDotNetCore.Models
@@ -76,22 +75,13 @@ public HasManyThroughAttribute(string publicName, string throughPropertyName, Li
7675
/// </summary>
7776
public override object GetValue(object entity)
7877
{
79-
var throughNavigationProperty = entity.GetType()
80-
.GetProperties()
81-
.Single(p => p.Name == ThroughProperty.Name);
78+
IEnumerable joinEntities = (IEnumerable)ThroughProperty.GetValue(entity) ?? Array.Empty<object>();
8279

83-
var throughEntities = throughNavigationProperty.GetValue(entity);
80+
IEnumerable<object> rightEntities = joinEntities
81+
.Cast<object>()
82+
.Select(rightEntity => RightProperty.GetValue(rightEntity));
8483

85-
if (throughEntities == null)
86-
// return an empty list for the right-type of the property.
87-
return TypeHelper.CreateListFor(RightType);
88-
89-
// the right entities are included on the navigation/through entities. Extract and return them.
90-
var rightEntities = new List<IIdentifiable>();
91-
foreach (var rightEntity in (IEnumerable)throughEntities)
92-
rightEntities.Add((IIdentifiable)RightProperty.GetValue(rightEntity));
93-
94-
return rightEntities.CopyToList(RightType);
84+
return rightEntities.CopyToTypedCollection(PropertyInfo.PropertyType);
9585
}
9686

9787
/// <inheritdoc />
@@ -105,16 +95,16 @@ public override void SetValue(object entity, object newValue)
10595
}
10696
else
10797
{
108-
List<object> instances = new List<object>();
98+
List<object> joinEntities = new List<object>();
10999
foreach (IIdentifiable resource in (IEnumerable)newValue)
110100
{
111-
object throughInstance = ThroughType.New();
112-
LeftProperty.SetValue(throughInstance, entity);
113-
RightProperty.SetValue(throughInstance, resource);
114-
instances.Add(throughInstance);
101+
object joinEntity = ThroughType.New();
102+
LeftProperty.SetValue(joinEntity, entity);
103+
RightProperty.SetValue(joinEntity, resource);
104+
joinEntities.Add(joinEntity);
115105
}
116106

117-
var typedCollection = instances.CopyToTypedCollection(ThroughProperty.PropertyType);
107+
var typedCollection = joinEntities.CopyToTypedCollection(ThroughProperty.PropertyType);
118108
ThroughProperty.SetValue(entity, typedCollection);
119109
}
120110
}

test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@ public async Task PatchResource_ModelWithEntityFrameworkInheritance_IsPatched()
4747
{
4848
// Arrange
4949
var dbContext = PrepareTest<Startup>();
50-
var serializer = GetSerializer<SuperUser>(e => new { e.SecurityLevel, e.Username, e.Password });
50+
51+
var builder = new WebHostBuilder().UseStartup<Startup>();
52+
var server = new TestServer(builder);
53+
54+
var serializer = TestFixture<Startup>.GetSerializer<SuperUser>(server.Host.Services, e => new { e.SecurityLevel, e.Username, e.Password });
5155
var superUser = new SuperUser { SecurityLevel = 1337, Username = "Super", Password = "User", LastPasswordChange = DateTime.Now.AddMinutes(-15) };
5256
dbContext.Set<SuperUser>().Add(superUser);
5357
dbContext.SaveChanges();
@@ -87,7 +91,7 @@ public async Task Response422IfUpdatingNotSettableAttribute()
8791
_context.TodoItems.Add(todoItem);
8892
_context.SaveChanges();
8993

90-
var serializer = _fixture.GetSerializer<TodoItem>(ti => new { ti.CalculatedValue });
94+
var serializer = TestFixture<Startup>.GetSerializer<TodoItem>(server.Host.Services, ti => new { ti.CalculatedValue });
9195
var content = serializer.Serialize(todoItem);
9296
var request = PrepareRequest("PATCH", $"/api/v1/todoItems/{todoItem.Id}", content);
9397

@@ -129,7 +133,7 @@ public async Task Respond_404_If_EntityDoesNotExist()
129133
var server = new TestServer(builder);
130134
var client = server.CreateClient();
131135

132-
var serializer = _fixture.GetSerializer<TodoItem>(ti => new { ti.Description, ti.Ordinal, ti.CreatedDate });
136+
var serializer = TestFixture<Startup>.GetSerializer<TodoItem>(server.Host.Services, ti => new { ti.Description, ti.Ordinal, ti.CreatedDate });
133137
var content = serializer.Serialize(todoItem);
134138
var request = PrepareRequest("PATCH", $"/api/v1/todoItems/{todoItem.Id}", content);
135139

@@ -159,7 +163,7 @@ public async Task Respond_422_If_IdNotInAttributeList()
159163

160164
var server = new TestServer(builder);
161165
var client = server.CreateClient();
162-
var serializer = _fixture.GetSerializer<TodoItem>(ti => new {ti.Description, ti.Ordinal, ti.CreatedDate});
166+
var serializer = TestFixture<Startup>.GetSerializer<TodoItem>(server.Host.Services, ti => new {ti.Description, ti.Ordinal, ti.CreatedDate});
163167
var content = serializer.Serialize(todoItem);
164168
var request = PrepareRequest("PATCH", $"/api/v1/todoItems/{maxPersonId}", content);
165169

@@ -194,7 +198,7 @@ public async Task Respond_409_If_IdInUrlIsDifferentFromIdInRequestBody()
194198
var builder = new WebHostBuilder().UseStartup<Startup>();
195199
var server = new TestServer(builder);
196200
var client = server.CreateClient();
197-
var serializer = _fixture.GetSerializer<TodoItem>(ti => new {ti.Description, ti.Ordinal, ti.CreatedDate});
201+
var serializer = TestFixture<Startup>.GetSerializer<TodoItem>(server.Host.Services, ti => new {ti.Description, ti.Ordinal, ti.CreatedDate});
198202
var content = serializer.Serialize(todoItem);
199203
var request = PrepareRequest("PATCH", $"/api/v1/todoItems/{wrongTodoItemId}", content);
200204

@@ -263,7 +267,7 @@ public async Task Can_Patch_Entity()
263267
var builder = new WebHostBuilder().UseStartup<Startup>();
264268
var server = new TestServer(builder);
265269
var client = server.CreateClient();
266-
var serializer = _fixture.GetSerializer<TodoItem>(p => new { p.Description, p.Ordinal });
270+
var serializer = TestFixture<Startup>.GetSerializer<TodoItem>(server.Host.Services, p => new { p.Description, p.Ordinal });
267271

268272
var request = PrepareRequest("PATCH", $"/api/v1/todoItems/{todoItem.Id}", serializer.Serialize(newTodoItem));
269273

@@ -306,7 +310,7 @@ public async Task Patch_Entity_With_HasMany_Does_Not_Include_Relationships()
306310
var builder = new WebHostBuilder().UseStartup<Startup>();
307311
var server = new TestServer(builder);
308312
var client = server.CreateClient();
309-
var serializer = _fixture.GetSerializer<Person>(p => new { p.LastName, p.FirstName });
313+
var serializer = TestFixture<Startup>.GetSerializer<Person>(server.Host.Services, p => new { p.LastName, p.FirstName });
310314

311315
var request = PrepareRequest("PATCH", $"/api/v1/people/{person.Id}", serializer.Serialize(newPerson));
312316

test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ public TestFixture()
3535
public HttpClient Client { get; set; }
3636
public AppDbContext Context { get; private set; }
3737

38+
public static IRequestSerializer GetSerializer<TResource>(IServiceProvider serviceProvider, Expression<Func<TResource, dynamic>> attributes = null, Expression<Func<TResource, dynamic>> relationships = null) where TResource : class, IIdentifiable
39+
{
40+
var serializer = (IRequestSerializer)serviceProvider.GetService(typeof(IRequestSerializer));
41+
var graph = (IResourceGraph)serviceProvider.GetService(typeof(IResourceGraph));
42+
serializer.AttributesToSerialize = attributes != null ? graph.GetAttributes(attributes) : null;
43+
serializer.RelationshipsToSerialize = relationships != null ? graph.GetRelationships(relationships) : null;
44+
return serializer;
45+
}
46+
3847
public IRequestSerializer GetSerializer<TResource>(Expression<Func<TResource, dynamic>> attributes = null, Expression<Func<TResource, dynamic>> relationships = null) where TResource : class, IIdentifiable
3948
{
4049
var serializer = GetService<IRequestSerializer>();

0 commit comments

Comments
 (0)