/
ResourceConverter.cs
264 lines (238 loc) · 11.4 KB
/
ResourceConverter.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using WebApi.Hal.Interfaces;
namespace WebApi.Hal.JsonConverters
{
public class ResourceConverter : JsonConverter
{
private readonly JsonSerializerSettings _jsonSerializerSettings;
public ResourceConverter(JsonSerializerSettings jsonSerializerSettings)
{
_jsonSerializerSettings = jsonSerializerSettings;
}
public ResourceConverter(IHypermediaResolver hypermediaConfiguration, JsonSerializerSettings jsonSerializerSettings) : this(jsonSerializerSettings)
{
if (hypermediaConfiguration == null)
{
throw new ArgumentNullException(nameof(hypermediaConfiguration));
}
HypermediaResolver = hypermediaConfiguration;
}
public IHypermediaResolver HypermediaResolver { get; }
private HalJsonConverterContext GetResourceConverterContext()
{
var context = (HypermediaResolver == null)
? new HalJsonConverterContext()
: new HalJsonConverterContext(HypermediaResolver);
return context;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var resource = (IResource)value;
var linksBackup = resource.Links;
if (linksBackup.Count == 0)
resource.Links = null; // avoid serialization
resource.ConverterContext = GetResourceConverterContext();
var localJsonSerializer = JsonSerializer.Create(new JsonSerializerSettings
{
CheckAdditionalContent = _jsonSerializerSettings.CheckAdditionalContent,
ConstructorHandling = _jsonSerializerSettings.ConstructorHandling,
Context = serializer.Context,
ContractResolver = _jsonSerializerSettings.ContractResolver,
Converters = _jsonSerializerSettings.Converters.Where(converter => converter != this).ToList(),
Culture = _jsonSerializerSettings.Culture,
DateFormatHandling = _jsonSerializerSettings.DateFormatHandling,
DateFormatString = _jsonSerializerSettings.DateFormatString,
DateParseHandling = _jsonSerializerSettings.DateParseHandling,
DateTimeZoneHandling = _jsonSerializerSettings.DateTimeZoneHandling,
DefaultValueHandling = _jsonSerializerSettings.DefaultValueHandling,
EqualityComparer = _jsonSerializerSettings.EqualityComparer,
Error = _jsonSerializerSettings.Error,
FloatFormatHandling = _jsonSerializerSettings.FloatFormatHandling,
FloatParseHandling = _jsonSerializerSettings.FloatParseHandling,
Formatting = _jsonSerializerSettings.Formatting,
MaxDepth = _jsonSerializerSettings.MaxDepth,
MetadataPropertyHandling = _jsonSerializerSettings.MetadataPropertyHandling,
MissingMemberHandling = _jsonSerializerSettings.MissingMemberHandling,
NullValueHandling = _jsonSerializerSettings.NullValueHandling,
ObjectCreationHandling = _jsonSerializerSettings.ObjectCreationHandling,
PreserveReferencesHandling = _jsonSerializerSettings.PreserveReferencesHandling,
ReferenceLoopHandling = _jsonSerializerSettings.ReferenceLoopHandling,
ReferenceResolverProvider = _jsonSerializerSettings.ReferenceResolverProvider,
SerializationBinder = _jsonSerializerSettings.SerializationBinder,
StringEscapeHandling = _jsonSerializerSettings.StringEscapeHandling,
TraceWriter = _jsonSerializerSettings.TraceWriter,
TypeNameAssemblyFormatHandling = _jsonSerializerSettings.TypeNameAssemblyFormatHandling,
TypeNameHandling = _jsonSerializerSettings.TypeNameHandling
});
localJsonSerializer.Serialize(writer, resource);
if (linksBackup.Count == 0)
resource.Links = linksBackup;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
JsonSerializer serializer)
{
// let exceptions leak out of here so ordinary exception handling in the server or client pipeline can take place
return CreateResource(JObject.Load(reader), objectType);
}
private const string HalLinksName = "_links";
private const string HalEmbeddedName = "_embedded";
private static IResource CreateResource(JObject jObj, Type resourceType)
{
// remove _links and _embedded so those don't try to deserialize, because we know they will fail
if (jObj.TryGetValue(HalLinksName, out JToken links))
jObj.Remove(HalLinksName);
if (jObj.TryGetValue(HalEmbeddedName, out JToken embeddeds))
jObj.Remove(HalEmbeddedName);
// create value properties in base object
var resource = jObj.ToObject(resourceType) as IResource;
if (resource == null) return null;
// links are named properties, where the name is Link.Rel and the value is the rest of Link
if (links != null)
{
foreach (var rel in links.OfType<JProperty>())
CreateLinks(rel, resource);
var self = resource.Links.SingleOrDefault(l => l.Rel == "self");
if (self != null)
resource.Href = self.Href;
}
// embedded are named properties, where the name is the Rel, which needs to map to a Resource Type, and the value is the Resource
// recursive
if (embeddeds != null)
{
foreach (var prop in resourceType.GetProperties().Where(p => Representation.IsEmbeddedResourceType(p.PropertyType)))
{
// expects embedded collection of resources is implemented as an IList on the Representation-derived class
if (typeof (IEnumerable<IResource>).IsAssignableFrom(prop.PropertyType))
{
var lst = prop.GetValue(resource) as IList;
if (lst == null)
{
lst = ConstructResource(prop.PropertyType) as IList ??
Activator.CreateInstance(
typeof (List<>).MakeGenericType(prop.PropertyType.GenericTypeArguments)) as IList;
if (lst == null) continue;
prop.SetValue(resource, lst);
}
if (prop.PropertyType.GenericTypeArguments != null &&
prop.PropertyType.GenericTypeArguments.Length > 0)
CreateEmbedded(embeddeds, prop.PropertyType.GenericTypeArguments[0],
newRes => lst.Add(newRes));
}
else
{
var prop1 = prop;
CreateEmbedded(embeddeds, prop.PropertyType, newRes => prop1.SetValue(resource, newRes));
}
}
}
return resource;
}
static void CreateLinks(JProperty rel, IResource resource)
{
if (rel.Value.Type == JTokenType.Array)
{
if (rel.Value is JArray arr)
foreach (var link in arr.Select(item => item.ToObject<Link>()))
{
link.Rel = rel.Name;
resource.Links.Add(link);
}
}
else
{
var link = rel.Value.ToObject<Link>();
link.Rel = rel.Name;
resource.Links.Add(link);
}
}
static void CreateEmbedded(JToken embeddeds, Type resourceType, Action<IResource> addCreatedResource)
{
var rel = GetResourceTypeRel(resourceType);
if (!string.IsNullOrEmpty(rel))
{
var tok = embeddeds[rel];
if (tok != null)
{
switch (tok.Type)
{
case JTokenType.Array:
{
if (tok is JArray embeddedJArr)
{
foreach (var embeddedJObj in embeddedJArr.OfType<JObject>())
addCreatedResource(CreateResource(embeddedJObj, resourceType)); // recursion
}
}
break;
case JTokenType.Object:
{
if (tok is JObject embeddedJObj)
addCreatedResource(CreateResource(embeddedJObj, resourceType)); // recursion
}
break;
}
}
}
}
// this depends on IResource.Rel being set upon construction
static readonly IDictionary<string, string> ResourceTypeToRel = new Dictionary<string, string>();
static readonly object ResourceTypeToRelLock = new object();
static string GetResourceTypeRel(Type resourceType)
{
if (ResourceTypeToRel.ContainsKey(resourceType.FullName))
return ResourceTypeToRel[resourceType.FullName];
try
{
lock (ResourceTypeToRelLock)
{
if (ResourceTypeToRel.ContainsKey(resourceType.FullName))
return ResourceTypeToRel[resourceType.FullName];
if (ConstructResource(resourceType) is IResource res)
{
var rel = res.Rel;
ResourceTypeToRel.Add(resourceType.FullName, rel);
return rel;
}
}
return string.Empty;
}
catch
{
return string.Empty;
}
}
static object ConstructResource(Type resourceType)
{
// favor c-tor with zero params, but if it doesn't exist, use c-tor with fewest params and pass all null values
var ctors = resourceType.GetConstructors();
ConstructorInfo useThisCtor = null;
foreach (var ctor in ctors)
{
if (ctor.GetParameters().Length == 0)
{
useThisCtor = ctor;
break;
}
if (useThisCtor == null || useThisCtor.GetParameters().Length > ctor.GetParameters().Length)
useThisCtor = ctor;
}
if (useThisCtor == null) return null;
var ctorParams = new object[useThisCtor.GetParameters().Length];
return useThisCtor.Invoke(ctorParams);
}
public override bool CanConvert(Type objectType)
{
return IsResource(objectType);
}
private static bool IsResource(Type objectType)
{
return typeof(Representation).IsAssignableFrom(objectType);
}
}
}