Skip to content

Commit 46f2d07

Browse files
committed
feat(serialization): improve support for complex attrs
- add contract resolver - use resolver settings in serializer - provide IQueryAccessor - default to dasherized resolver
1 parent 40ffa82 commit 46f2d07

File tree

12 files changed

+419
-11
lines changed

12 files changed

+419
-11
lines changed

src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using System;
22
using JsonApiDotNetCore.Builders;
33
using JsonApiDotNetCore.Internal;
4+
using JsonApiDotNetCore.Serialization;
45
using Microsoft.EntityFrameworkCore;
6+
using Newtonsoft.Json.Serialization;
57

68
namespace JsonApiDotNetCore.Configuration
79
{
@@ -12,6 +14,7 @@ public class JsonApiOptions
1214
public bool IncludeTotalRecordCount { get; set; }
1315
public bool AllowClientGeneratedIds { get; set; }
1416
public IContextGraph ContextGraph { get; set; }
17+
public IContractResolver JsonContractResolver { get; set; } = new DasherizedResolver();
1518

1619
public void BuildContextGraph<TContext>(Action<IContextGraphBuilder> builder)
1720
where TContext : DbContext

src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ public static void AddJsonApiInternals(
107107
services.AddScoped<IJsonApiReader, JsonApiReader>();
108108
services.AddScoped<IGenericProcessorFactory, GenericProcessorFactory>();
109109
services.AddScoped(typeof(GenericProcessor<>));
110+
services.AddScoped<IQueryAccessor, QueryAccessor>();
110111
}
111112

112113
public static void SerializeAsJsonApi(this MvcOptions options, JsonApiOptions jsonApiOptions)

src/JsonApiDotNetCore/Internal/TypeHelper.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,10 @@ public static object ConvertType(object value, Type type)
3232
throw new FormatException($"{ value } cannot be converted to { type.GetTypeInfo().Name }", e);
3333
}
3434
}
35+
36+
public static T ConvertType<T>(object value)
37+
{
38+
return (T)ConvertType(value, typeof(T));
39+
}
3540
}
3641
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System.Reflection;
2+
using JsonApiDotNetCore.Extensions;
3+
using Newtonsoft.Json;
4+
using Newtonsoft.Json.Serialization;
5+
6+
namespace JsonApiDotNetCore.Serialization
7+
{
8+
public class DasherizedResolver : DefaultContractResolver
9+
{
10+
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
11+
{
12+
JsonProperty property = base.CreateProperty(member, memberSerialization);
13+
14+
property.PropertyName = property.PropertyName.Dasherize();
15+
16+
return property;
17+
}
18+
}
19+
}

src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@ namespace JsonApiDotNetCore.Serialization
1414
public class JsonApiDeSerializer : IJsonApiDeSerializer
1515
{
1616
private readonly IJsonApiContext _jsonApiContext;
17-
private readonly IGenericProcessorFactory _genericProcessorFactor;
17+
private readonly IGenericProcessorFactory _genericProcessorFactory;
1818

1919
public JsonApiDeSerializer(
2020
IJsonApiContext jsonApiContext,
2121
IGenericProcessorFactory genericProcessorFactory)
2222
{
2323
_jsonApiContext = jsonApiContext;
24-
_genericProcessorFactor = genericProcessorFactory;
24+
_genericProcessorFactory = genericProcessorFactory;
2525
}
2626

2727
public object Deserialize(string requestBody)
@@ -38,10 +38,8 @@ public object Deserialize(string requestBody)
3838
}
3939
}
4040

41-
public object Deserialize<TEntity>(string requestBody)
42-
{
43-
return (TEntity)Deserialize(requestBody);
44-
}
41+
public TEntity Deserialize<TEntity>(string requestBody)
42+
=> (TEntity)Deserialize(requestBody);
4543

4644
public object DeserializeRelationship(string requestBody)
4745
{
@@ -114,10 +112,9 @@ private object SetEntityAttributes(
114112
if (entityProperty == null)
115113
throw new ArgumentException($"{contextEntity.EntityType.Name} does not contain an attribute named {attr.InternalAttributeName}", nameof(entity));
116114

117-
object newValue;
118-
if (attributeValues.TryGetValue(attr.PublicAttributeName, out newValue))
115+
if (attributeValues.TryGetValue(attr.PublicAttributeName, out object newValue))
119116
{
120-
var convertedValue = TypeHelper.ConvertType(newValue, entityProperty.PropertyType);
117+
var convertedValue = ConvertAttrValue(newValue, entityProperty.PropertyType);
121118
entityProperty.SetValue(entity, convertedValue);
122119
_jsonApiContext.AttributesToUpdate[attr] = convertedValue;
123120
}
@@ -126,6 +123,25 @@ private object SetEntityAttributes(
126123
return entity;
127124
}
128125

126+
private object ConvertAttrValue(object newValue, Type targetType)
127+
{
128+
if (newValue is JContainer jObject)
129+
return DeserializeComplexType(jObject, targetType);
130+
131+
var convertedValue = TypeHelper.ConvertType(newValue, targetType);
132+
return convertedValue;
133+
}
134+
135+
private object DeserializeComplexType(JContainer obj, Type targetType)
136+
{
137+
var serializerSettings = new JsonSerializerSettings
138+
{
139+
ContractResolver = _jsonApiContext.Options.JsonContractResolver
140+
};
141+
142+
return obj.ToObject(targetType, JsonSerializer.Create(serializerSettings));
143+
}
144+
129145
private object SetRelationships(
130146
object entity,
131147
ContextEntity contextEntity,
@@ -198,7 +214,7 @@ private object SetHasManyRelationship(object entity,
198214

199215
if (data == null) return entity;
200216

201-
var genericProcessor = _genericProcessorFactor.GetProcessor(attr.Type);
217+
var genericProcessor = _genericProcessorFactory.GetProcessor(attr.Type);
202218
var ids = relationshipData.ManyData.Select(r => r["id"]);
203219
genericProcessor.SetRelationships(entity, attr, ids);
204220
}

src/JsonApiDotNetCore/Serialization/JsonApiSerializer.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ private string SerializeDocument(object entity)
8484
private string _serialize(object obj)
8585
{
8686
return JsonConvert.SerializeObject(obj, new JsonSerializerSettings {
87-
NullValueHandling = NullValueHandling.Ignore
87+
NullValueHandling = NullValueHandling.Ignore,
88+
ContractResolver = _jsonApiContext.Options.JsonContractResolver
8889
});
8990
}
9091
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using System.Collections.Generic;
2+
3+
namespace JsonApiDotNetCore.Services
4+
{
5+
public interface IQueryAccessor
6+
{
7+
bool TryGetValue<T>(string key, out T value);
8+
}
9+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using JsonApiDotNetCore.Internal;
5+
using Microsoft.Extensions.Logging;
6+
7+
namespace JsonApiDotNetCore.Services
8+
{
9+
public class QueryAccessor : IQueryAccessor
10+
{
11+
private readonly IJsonApiContext _jsonApiContext;
12+
private readonly ILogger<QueryAccessor> _logger;
13+
14+
public QueryAccessor(
15+
IJsonApiContext jsonApiContext,
16+
ILoggerFactory loggerFactory)
17+
{
18+
_jsonApiContext = jsonApiContext;
19+
_logger = loggerFactory.CreateLogger<QueryAccessor>();
20+
}
21+
22+
public bool TryGetValue<T>(string key, out T value)
23+
{
24+
value = default(T);
25+
26+
var stringValue = GetFilterValue(key);
27+
if(stringValue == null)
28+
{
29+
_logger.LogInformation($"'{key}' was not found in the query collection");
30+
return false;
31+
}
32+
33+
try
34+
{
35+
value = TypeHelper.ConvertType<T>(stringValue);
36+
return true;
37+
}
38+
catch (FormatException)
39+
{
40+
_logger.LogInformation($"'{value}' is not a valid guid value for query parameter {key}");
41+
return false;
42+
}
43+
}
44+
45+
private string GetFilterValue(string key) => _jsonApiContext.QuerySet
46+
.Filters
47+
.FirstOrDefault(f => f.Key == key)
48+
?.Value;
49+
}
50+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using JsonApiDotNetCore.Serialization;
2+
using Newtonsoft.Json;
3+
using Xunit;
4+
5+
namespace UnitTests.Serialization
6+
{
7+
public class DasherizedResolverTests
8+
{
9+
[Fact]
10+
public void Resolver_Dasherizes_Property_Names()
11+
{
12+
// arrange
13+
var obj = new
14+
{
15+
myProp = "val"
16+
};
17+
18+
// act
19+
var result = JsonConvert.SerializeObject(obj,
20+
Formatting.None,
21+
new JsonSerializerSettings { ContractResolver = new DasherizedResolver() }
22+
);
23+
24+
// assert
25+
Assert.Equal("{\"my-prop\":\"val\"}", result);
26+
}
27+
}
28+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
using System.Collections.Generic;
2+
using JsonApiDotNetCore.Builders;
3+
using JsonApiDotNetCore.Configuration;
4+
using JsonApiDotNetCore.Internal.Generics;
5+
using JsonApiDotNetCore.Models;
6+
using JsonApiDotNetCore.Serialization;
7+
using JsonApiDotNetCore.Services;
8+
using Moq;
9+
using Newtonsoft.Json;
10+
using Newtonsoft.Json.Serialization;
11+
using Xunit;
12+
13+
namespace UnitTests.Serialization
14+
{
15+
public class JsonApiDeSerializerTests
16+
{
17+
[Fact]
18+
public void Can_Deserialize_Complex_Types()
19+
{
20+
// arrange
21+
var contextGraphBuilder = new ContextGraphBuilder();
22+
contextGraphBuilder.AddResource<TestResource>("test-resource");
23+
var contextGraph = contextGraphBuilder.Build();
24+
25+
var jsonApiContextMock = new Mock<IJsonApiContext>();
26+
jsonApiContextMock.SetupAllProperties();
27+
jsonApiContextMock.Setup(m => m.ContextGraph).Returns(contextGraph);
28+
jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary<AttrAttribute, object>());
29+
jsonApiContextMock.Setup(m => m.Options).Returns(new JsonApiOptions {
30+
JsonContractResolver = new CamelCasePropertyNamesContractResolver()
31+
});
32+
33+
var genericProcessorFactoryMock = new Mock<IGenericProcessorFactory>();
34+
35+
var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object, genericProcessorFactoryMock.Object);
36+
37+
var content = new Document {
38+
Data = new DocumentData {
39+
Type = "test-resource",
40+
Id = "1",
41+
Attributes = new Dictionary<string, object> {
42+
{
43+
"complex-member", new { compoundName = "testName" }
44+
}
45+
}
46+
}
47+
};
48+
49+
// act
50+
var result = deserializer.Deserialize<TestResource>(JsonConvert.SerializeObject(content));
51+
52+
// assert
53+
Assert.NotNull(result.ComplexMember);
54+
Assert.Equal("testName", result.ComplexMember.CompoundName);
55+
}
56+
57+
[Fact]
58+
public void Can_Deserialize_Complex_List_Types()
59+
{
60+
// arrange
61+
var contextGraphBuilder = new ContextGraphBuilder();
62+
contextGraphBuilder.AddResource<TestResourceWithList>("test-resource");
63+
var contextGraph = contextGraphBuilder.Build();
64+
65+
var jsonApiContextMock = new Mock<IJsonApiContext>();
66+
jsonApiContextMock.SetupAllProperties();
67+
jsonApiContextMock.Setup(m => m.ContextGraph).Returns(contextGraph);
68+
jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary<AttrAttribute, object>());
69+
jsonApiContextMock.Setup(m => m.Options).Returns(new JsonApiOptions {
70+
JsonContractResolver = new CamelCasePropertyNamesContractResolver()
71+
});
72+
73+
var genericProcessorFactoryMock = new Mock<IGenericProcessorFactory>();
74+
75+
var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object, genericProcessorFactoryMock.Object);
76+
77+
var content = new Document {
78+
Data = new DocumentData {
79+
Type = "test-resource",
80+
Id = "1",
81+
Attributes = new Dictionary<string, object> {
82+
{
83+
"complex-members", new [] {
84+
new { compoundName = "testName" }
85+
}
86+
}
87+
}
88+
}
89+
};
90+
91+
// act
92+
var result = deserializer.Deserialize<TestResourceWithList>(JsonConvert.SerializeObject(content));
93+
94+
// assert
95+
Assert.NotNull(result.ComplexMembers);
96+
Assert.NotEmpty(result.ComplexMembers);
97+
Assert.Equal("testName", result.ComplexMembers[0].CompoundName);
98+
}
99+
100+
[Fact]
101+
public void Can_Deserialize_Complex_Types_With_Dasherized_Attrs()
102+
{
103+
// arrange
104+
var contextGraphBuilder = new ContextGraphBuilder();
105+
contextGraphBuilder.AddResource<TestResource>("test-resource");
106+
var contextGraph = contextGraphBuilder.Build();
107+
108+
var jsonApiContextMock = new Mock<IJsonApiContext>();
109+
jsonApiContextMock.SetupAllProperties();
110+
jsonApiContextMock.Setup(m => m.ContextGraph).Returns(contextGraph);
111+
jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary<AttrAttribute, object>());
112+
113+
jsonApiContextMock.Setup(m => m.Options).Returns(new JsonApiOptions {
114+
JsonContractResolver = new DasherizedResolver() // <---
115+
});
116+
117+
var genericProcessorFactoryMock = new Mock<IGenericProcessorFactory>();
118+
119+
var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object, genericProcessorFactoryMock.Object);
120+
121+
var content = new Document {
122+
Data = new DocumentData {
123+
Type = "test-resource",
124+
Id = "1",
125+
Attributes = new Dictionary<string, object> {
126+
{
127+
"complex-member", new Dictionary<string, string> { { "compound-name", "testName" } }
128+
}
129+
}
130+
}
131+
};
132+
133+
// act
134+
var result = deserializer.Deserialize<TestResource>(JsonConvert.SerializeObject(content));
135+
136+
// assert
137+
Assert.NotNull(result.ComplexMember);
138+
Assert.Equal("testName", result.ComplexMember.CompoundName);
139+
}
140+
141+
private class TestResource : Identifiable
142+
{
143+
[Attr("complex-member")]
144+
public ComplexType ComplexMember { get; set; }
145+
}
146+
147+
private class TestResourceWithList : Identifiable
148+
{
149+
[Attr("complex-members")]
150+
public List<ComplexType> ComplexMembers { get; set; }
151+
}
152+
153+
private class ComplexType
154+
{
155+
public string CompoundName { get; set; }
156+
}
157+
}
158+
}

0 commit comments

Comments
 (0)