Skip to content
This repository was archived by the owner on Dec 1, 2021. It is now read-only.

Commit 19e425c

Browse files
authored
Merge pull request #13 from trainline/dev
Merge Dev into Master
2 parents 731860b + 3c235db commit 19e425c

File tree

15 files changed

+323
-47
lines changed

15 files changed

+323
-47
lines changed

src/NJsonApiCore/Infrastructure/Delta.cs

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,48 +5,57 @@
55
using System.Linq;
66
using System.Linq.Expressions;
77
using System.Reflection;
8+
using NJsonApi.Conventions.Impl;
89

910
namespace NJsonApi.Infrastructure
1011
{
1112
public class Delta<T> : IDelta<T> where T : new()
1213
{
13-
private readonly Dictionary<string, Action<T, object>> currentTypeSetters;
14-
private readonly Dictionary<string, Action<T, object>> typeSettersTemplates;
14+
private Dictionary<string, Action<T, object>> _currentTypeSetters;
15+
private Dictionary<string, Action<T, object>> _typeSettersTemplates;
1516

16-
private readonly Dictionary<string, CollectionInfo<T>> currentCollectionInfos;
17-
private readonly Dictionary<string, CollectionInfo<T>> collectionInfoTemplates;
17+
private Dictionary<string, CollectionInfo<T>> _currentCollectionInfos;
18+
private Dictionary<string, CollectionInfo<T>> _collectionInfoTemplates;
1819

1920
public Dictionary<string, object> ObjectPropertyValues { get; set; }
2021
public Dictionary<string, ICollectionDelta> CollectionDeltas { get; set; }
2122
public IMetaData TopLevelMetaData { get; set; }
2223
public IMetaData ObjectMetaData { get; set; }
24+
private bool _scanned;
2325

2426
public Delta()
2527
{
26-
if (typeSettersTemplates == null)
28+
ObjectPropertyValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
29+
CollectionDeltas = new Dictionary<string, ICollectionDelta>();
30+
TopLevelMetaData = null;
31+
ObjectMetaData = null;
32+
}
33+
34+
public void Scan()
35+
{
36+
if (_typeSettersTemplates == null)
2737
{
28-
typeSettersTemplates = ScanForProperties();
38+
_typeSettersTemplates = ScanForProperties();
2939
}
30-
if (collectionInfoTemplates == null)
40+
if (_collectionInfoTemplates == null)
3141
{
32-
collectionInfoTemplates = ScanForCollections();
42+
_collectionInfoTemplates = ScanForCollections();
3343
}
3444

35-
currentTypeSetters = typeSettersTemplates.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
36-
currentCollectionInfos = collectionInfoTemplates.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
37-
38-
ObjectPropertyValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
39-
CollectionDeltas = new Dictionary<string, ICollectionDelta>();
40-
TopLevelMetaData = null;
41-
ObjectMetaData = null;
45+
_currentTypeSetters = _typeSettersTemplates.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
46+
_currentCollectionInfos = _collectionInfoTemplates.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
47+
_scanned = true;
4248
}
4349

4450
public void FilterOut<TProperty>(params Expression<Func<T, TProperty>>[] filter)
4551
{
52+
ThrowExceptionIfNotScanned();
4653
foreach (var f in filter)
4754
{
48-
currentTypeSetters.Remove(f.GetPropertyInfo().Name);
49-
currentCollectionInfos.Remove(f.GetPropertyInfo().Name);
55+
var propertyName = f.GetPropertyInfo().Name;
56+
if (_currentTypeSetters.ContainsKey(propertyName))
57+
_currentTypeSetters.Remove(propertyName);
58+
_currentCollectionInfos.Remove(propertyName);
5059
}
5160
}
5261

@@ -71,24 +80,26 @@ public static string ToProperCase(string the_string)
7180

7281
public void ApplySimpleProperties(T inputObject)
7382
{
83+
ThrowExceptionIfNotScanned();
7484
if (ObjectPropertyValues == null) return;
7585
foreach (var objectPropertyNameValue in ObjectPropertyValues)
7686
{
7787
Action<T, object> setter;
7888

79-
currentTypeSetters.TryGetValue(ToProperCase(objectPropertyNameValue.Key), out setter);
89+
_currentTypeSetters.TryGetValue(ToProperCase(objectPropertyNameValue.Key), out setter);
8090
if (setter != null)
8191
setter(inputObject, objectPropertyNameValue.Value);
8292
}
8393
}
8494

8595
public void ApplyCollections(T inputObject)
8696
{
97+
ThrowExceptionIfNotScanned();
8798
if (ObjectPropertyValues == null) return;
8899
foreach (var colDelta in CollectionDeltas)
89100
{
90101
CollectionInfo<T> info;
91-
currentCollectionInfos.TryGetValue(ToProperCase(colDelta.Key), out info);
102+
_currentCollectionInfos.TryGetValue(ToProperCase(colDelta.Key), out info);
92103
if (info != null)
93104
{
94105
var existingCollection = info.Getter(inputObject);
@@ -122,15 +133,16 @@ private Dictionary<string, Action<T, object>> ScanForProperties()
122133
{
123134
return typeof(T)
124135
.GetProperties()
125-
.Where(pi => !(typeof(ICollection).IsAssignableFrom(pi.PropertyType)))
136+
.Where(pi => ObjectPropertyValues.ContainsKey(pi.Name))
126137
.ToDictionary(pi => pi.Name, pi => pi.ToCompiledSetterAction<T, object>());
127138
}
128139

129140
private Dictionary<string, CollectionInfo<T>> ScanForCollections()
130141
{
131142
return typeof(T)
132143
.GetProperties()
133-
.Where(pi => (typeof(ICollection).IsAssignableFrom(pi.PropertyType)))
144+
.Where(pi => CollectionDeltas.ContainsKey(pi.Name) &&
145+
(typeof(ICollection).IsAssignableFrom(pi.PropertyType)))
134146
.ToDictionary(pi => pi.Name, pi => new CollectionInfo<T>
135147
{
136148
Getter = pi.ToCompiledGetterFunc<T, ICollection>(),
@@ -145,5 +157,10 @@ private class CollectionInfo<TOwner>
145157
public Func<TOwner, ICollection> Getter { get; set; }
146158
public Action<TOwner, ICollection> Setter { get; set; }
147159
}
160+
161+
private void ThrowExceptionIfNotScanned()
162+
{
163+
if (!_scanned) throw new Exception("Scan must be called before this method");
164+
}
148165
}
149166
}

src/NJsonApiCore/Infrastructure/IDelta.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@ public interface IDelta
1717
Dictionary<string, ICollectionDelta> CollectionDeltas { get; set; }
1818
IMetaData TopLevelMetaData { get; set; }
1919
IMetaData ObjectMetaData { get; set; }
20+
void Scan();
2021
}
2122
}

src/NJsonApiCore/Infrastructure/IPropertyHandle.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@ public interface IPropertyHandle<TResource, TProperty> : IPropertyHandle
1414
{
1515
Expression<Func<TResource, TProperty>> Expression { get; }
1616
Func<TResource, TProperty> Getter { get; }
17-
Action<TResource, TProperty> Setter { get; }
17+
Action<TResource, object> Setter { get; }
1818
}
1919
}

src/NJsonApiCore/Infrastructure/PropertyHandle.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public PropertyHandle(Expression<Func<TResource, TProperty>> expression)
1818

1919
public Expression<Func<TResource, TProperty>> Expression { get; private set; }
2020
public Func<TResource, TProperty> Getter { get; private set; }
21-
public Action<TResource, TProperty> Setter { get; private set; }
21+
public Action<TResource, object> Setter { get; private set; }
2222
public string Name { get; private set; }
2323

2424
public Delegate GetterDelegate { get { return Getter; } }

src/NJsonApiCore/Serialization/JsonApiTransformer.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@ public IDelta TransformBack(UpdateDocument updateDocument, Type type, Context co
149149
delta.ObjectMetaData = updateDocument.Data.MetaData;
150150
}
151151

152+
delta.Scan();
153+
152154
return delta;
153155
}
154156
}

src/NJsonApiCore/Utils/ExpressionUtils.cs

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
24
using System.Linq;
35
using System.Linq.Expressions;
46
using System.Reflection;
@@ -12,6 +14,8 @@ public static class ExpressionUtils
1214
// JObject.ToObject(value) method info
1315
private static readonly MethodInfo JObjectToObjectMethodInfo =
1416
typeof(JObject).GetMethods().Single(x => x.Name == "ToObject" && !x.ContainsGenericParameters && x.GetParameters().Length == 1);
17+
private static readonly MethodInfo JArrayToObjectMethodInfo =
18+
typeof(JArray).GetMethods().Single(x => x.Name == "ToObject" && !x.ContainsGenericParameters && x.GetParameters().Length == 1);
1519
#else
1620
// JObject.ToObject(value) method info
1721
private static readonly MethodInfo JObjectToObjectMethodInfo =
@@ -20,6 +24,13 @@ public static class ExpressionUtils
2024
null,
2125
CallingConventions.HasThis,
2226
new[] { typeof(Type) }, null);
27+
28+
private static readonly MethodInfo JArrayToObjectMethodInfo =
29+
typeof(JArray).GetMethod("ToObject",
30+
BindingFlags.Instance | BindingFlags.Public,
31+
null,
32+
CallingConventions.HasThis,
33+
new[] { typeof(Type) }, null);
2334
#endif
2435

2536
public static PropertyInfo GetPropertyInfo(this LambdaExpression propertyExpression)
@@ -84,40 +95,50 @@ public static Func<TInstance, TResult> ToCompiledGetterFunc<TInstance, TResult>(
8495
return (Func<TInstance, TResult>)ToCompiledGetterDelegate(pi, typeof(TInstance), typeof(TResult));
8596
}
8697

98+
public static bool IsGenericType(Type type)
99+
{
100+
return type.GetTypeInfo().IsGenericType;
101+
}
102+
87103
public static Delegate ToCompiledSetterDelegate(this PropertyInfo pi, Type tInstance, Type tValue)
88104
{
89105
if (!tValue.IsAssignableFrom(pi.PropertyType) && !pi.PropertyType.IsAssignableFrom(tValue))
90106
throw new InvalidOperationException($"Unsupported type combination: {tValue} and {pi.GetType()}.");
91107

92108
var instanceParameter = Expression.Parameter(tInstance);
93-
var valueParameter = Expression.Parameter(tValue);
109+
var valueParameter = Expression.Parameter(typeof(object));
94110

95-
if (Type.GetTypeCode(tValue) == TypeCode.Object)
111+
Expression exp;
112+
if (Type.GetTypeCode(pi.PropertyType) == TypeCode.Object)
96113
{
97-
var canConvertToJObject = Expression.Equal(Expression.TypeAs(valueParameter, typeof(JObject)),
98-
Expression.Constant(null));
99-
100-
var exp = Expression.IfThenElse(canConvertToJObject,
101-
CreateSimpleTypeSetterExpression(pi, instanceParameter, valueParameter),
102-
CreateJObjectTypeSetterExpression(pi, instanceParameter, valueParameter));
103-
104-
return Expression.Lambda(exp, instanceParameter, valueParameter).Compile();
114+
if (pi.PropertyType.GetInterfaces().Any(x => x == typeof(IEnumerable)))
115+
{
116+
exp = CreateJArrayTypeSetterExpression(pi, instanceParameter, valueParameter);
117+
}
118+
else
119+
{
120+
var canConvertToJObject = Expression.Equal(Expression.TypeAs(valueParameter, typeof(JObject)),
121+
Expression.Constant(null));
122+
123+
exp = Expression.IfThenElse(canConvertToJObject,
124+
CreateSimpleTypeSetterExpression(pi, instanceParameter, valueParameter),
125+
CreateJObjectTypeSetterExpression(pi, instanceParameter, valueParameter));
126+
}
105127
}
106128
else
107129
{
108-
var exp = CreateSimpleTypeSetterExpression(pi, instanceParameter, valueParameter);
109-
return Expression.Lambda(exp, instanceParameter, valueParameter).Compile();
130+
exp = CreateSimpleTypeSetterExpression(pi, instanceParameter, valueParameter);
110131
}
132+
return Expression.Lambda(exp, instanceParameter, valueParameter).Compile();
111133
}
112134

113-
public static Action<TInstance, TValue> ToCompiledSetterAction<TInstance, TValue>(this PropertyInfo pi)
135+
public static Action<TInstance, object> ToCompiledSetterAction<TInstance, TValue>(this PropertyInfo pi)
114136
{
115-
return (Action<TInstance, TValue>)ToCompiledSetterDelegate(pi, typeof(TInstance), typeof(TValue));
137+
return (Action<TInstance, object>)ToCompiledSetterDelegate(pi, typeof(TInstance), typeof(TValue));
116138
}
117139

118140
private static Expression CreateSimpleTypeSetterExpression(PropertyInfo pi, ParameterExpression instanceParameter, ParameterExpression valueParameter)
119141
{
120-
121142
var mi = pi.GetSetMethod();
122143
Expression valueExpression = valueParameter;
123144

@@ -129,14 +150,27 @@ private static Expression CreateSimpleTypeSetterExpression(PropertyInfo pi, Para
129150
return body;
130151
}
131152

132-
private static Expression CreateJObjectTypeSetterExpression(PropertyInfo pi, ParameterExpression instanceParameter, ParameterExpression valueParameter)
153+
private static Expression CreateJObjectTypeSetterExpression(PropertyInfo pi,
154+
ParameterExpression instanceParameter, ParameterExpression valueParameter)
155+
{
156+
return CreateJTokenSetterExpression<JObject>(pi, instanceParameter, valueParameter, JObjectToObjectMethodInfo);
157+
}
158+
159+
private static Expression CreateJArrayTypeSetterExpression(PropertyInfo pi,
160+
ParameterExpression instanceParameter, ParameterExpression valueParameter)
161+
{
162+
return CreateJTokenSetterExpression<JArray>(pi, instanceParameter, valueParameter, JArrayToObjectMethodInfo);
163+
}
164+
165+
private static Expression CreateJTokenSetterExpression<TJTokenType>(PropertyInfo pi,
166+
ParameterExpression instanceParameter, ParameterExpression valueParameter, MethodInfo method) where TJTokenType : JToken
133167
{
134-
// Use "(targetType) JObject.ToObject(value)" to get deserialized object
168+
// Use "(targetType) {JObject/JArray}.ToObject(value)" to get deserialized object
135169
var mi = pi.GetSetMethod();
136170
var typeConstant = Expression.Constant(pi.PropertyType);
137171

138-
var convertToJObjectExpression = Expression.Convert(valueParameter, typeof(JObject));
139-
var toObjectCall = Expression.Call(convertToJObjectExpression, JObjectToObjectMethodInfo, typeConstant);
172+
var convertToJObjectExpression = Expression.Convert(valueParameter, typeof(TJTokenType));
173+
var toObjectCall = Expression.Call(convertToJObjectExpression, method, typeConstant);
140174
var convertToTargetTypeExpression = Expression.Convert(toObjectCall, pi.PropertyType);
141175
var body = Expression.Call(instanceParameter, mi, convertToTargetTypeExpression);
142176

test/NJsonApiCore.Test/Builders/TestModelConfigurationBuilder.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using NJsonApi;
66
using NJsonApi.Test.TestModel;
77
using NJsonApi.Test.TestControllers;
8+
using NJsonApiCore.Test.TestModel;
89

910
namespace NJsonApi.Test.Builders
1011
{
@@ -32,6 +33,11 @@ public static ConfigurationBuilder BuilderForEverything
3233
.WithAllSimpleProperties()
3334
.WithSimpleProperty(x => x.Dimensions);
3435

36+
builder
37+
.Resource<Widget, WidgetsController>()
38+
.WithAllSimpleProperties()
39+
.WithSimpleProperty(x => x.Parts);
40+
3541
return builder;
3642
}
3743
}

test/NJsonApiCore.Test/Infrastructure/DeltaTest.cs

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,19 @@ public class DeltaTest
1111
[Fact]
1212
public void GIVEN_IncompleteProperties_WHEN_DeltaApply_THEN_OnlyThoseSpecifiedApplied()
1313
{
14-
//Arange
14+
//Arrange
1515
var author = new Author();
1616
var classUnderTest = new Delta<Author>();
17-
18-
classUnderTest.FilterOut(t => t.Name);
17+
1918
classUnderTest.ObjectPropertyValues =
2019
new Dictionary<string, object>()
2120
{
2221
{"Id", 1},
2322
{"DateTimeCreated", new DateTime(2016,1,1)}
2423
};
24+
classUnderTest.Scan();
25+
classUnderTest.FilterOut(t => t.Name);
26+
2527
//Act
2628
classUnderTest.ApplySimpleProperties(author);
2729

@@ -37,6 +39,7 @@ public void GIVEN_NoProperties_WHEN_DeltaApply_THEN_OutputsAreDefault()
3739
//Arrange
3840
var simpleObject = new Author();
3941
var objectUnderTest = new Delta<Author>();
42+
objectUnderTest.Scan();
4043

4144
//Act
4245
objectUnderTest.ApplySimpleProperties(simpleObject);
@@ -46,5 +49,47 @@ public void GIVEN_NoProperties_WHEN_DeltaApply_THEN_OutputsAreDefault()
4649
Assert.Null(simpleObject.Name);
4750
Assert.Equal(simpleObject.DateTimeCreated, new DateTime());
4851
}
52+
53+
[Fact]
54+
public void GIVEN_ScanNotCalled_WHEN_DeltaFilterOut_THEN_ExceptionThrown()
55+
{
56+
//Arrange
57+
var classUnderTest = new Delta<Author>();
58+
59+
classUnderTest.ObjectPropertyValues =
60+
new Dictionary<string, object>()
61+
{
62+
{"Id", 1},
63+
{"DateTimeCreated", new DateTime(2016,1,1)}
64+
};
65+
66+
//Act/Assert
67+
var ex = Assert.Throws<Exception>(()=> classUnderTest.FilterOut(t => t.Name));
68+
Assert.Equal("Scan must be called before this method", ex.Message);
69+
}
70+
71+
[Fact]
72+
public void GIVEN_ScanNotCalled_WHEN_DeltaApplySimpleProperties_THEN_ExceptionThrown()
73+
{
74+
//Arrange
75+
var author = new Author();
76+
var classUnderTest = new Delta<Author>();
77+
78+
//Act/Assert
79+
var ex = Assert.Throws<Exception>(() => classUnderTest.ApplySimpleProperties(author));
80+
Assert.Equal("Scan must be called before this method", ex.Message);
81+
}
82+
83+
[Fact]
84+
public void GIVEN_ScanNotCalled_WHEN_DeltaApplyCollections_THEN_ExceptionThrown()
85+
{
86+
//Arrange
87+
var author = new Author();
88+
var classUnderTest = new Delta<Author>();
89+
90+
//Act/Assert
91+
var ex = Assert.Throws<Exception>(() => classUnderTest.ApplyCollections(author));
92+
Assert.Equal("Scan must be called before this method", ex.Message);
93+
}
4994
}
5095
}

0 commit comments

Comments
 (0)