-
Notifications
You must be signed in to change notification settings - Fork 23
Expand file tree
/
Copy pathEntityFrameworkResourceObjectMaterializer.cs
More file actions
221 lines (195 loc) · 10.5 KB
/
EntityFrameworkResourceObjectMaterializer.cs
File metadata and controls
221 lines (195 loc) · 10.5 KB
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
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using JSONAPI.Core;
using JSONAPI.Documents;
using JSONAPI.Json;
namespace JSONAPI.EntityFramework
{
/// <summary>
/// Default implementation of IEntityFrameworkResourceObjectMaterializer
/// </summary>
public class EntityFrameworkResourceObjectMaterializer : IEntityFrameworkResourceObjectMaterializer
{
private readonly DbContext _dbContext;
private readonly IResourceTypeRegistry _registry;
private readonly MethodInfo _openSetToManyRelationshipValueMethod;
private readonly MethodInfo _openGetExistingRecordGenericMethod;
/// <summary>
/// Creates a new EntityFrameworkEntityFrameworkResourceObjectMaterializer
/// </summary>
/// <param name="dbContext"></param>
/// <param name="registry"></param>
public EntityFrameworkResourceObjectMaterializer(DbContext dbContext, IResourceTypeRegistry registry)
{
_dbContext = dbContext;
_registry = registry;
_openSetToManyRelationshipValueMethod = GetType()
.GetMethod("SetToManyRelationshipValue", BindingFlags.NonPublic | BindingFlags.Instance);
_openGetExistingRecordGenericMethod = GetType()
.GetMethod("GetExistingRecordGeneric", BindingFlags.NonPublic | BindingFlags.Instance);
}
public async Task<object> MaterializeResourceObject(IResourceObject resourceObject, CancellationToken cancellationToken)
{
var registration = _registry.GetRegistrationForResourceTypeName(resourceObject.Type);
var relationshipsToInclude = new List<ResourceTypeRelationship>();
if (resourceObject.Relationships != null)
{
relationshipsToInclude.AddRange(
resourceObject.Relationships
.Select(relationshipObject => registration.GetFieldByName(relationshipObject.Key))
.OfType<ResourceTypeRelationship>());
}
var material = await GetExistingRecord(registration, resourceObject.Id, relationshipsToInclude.ToArray(), cancellationToken);
if (material == null)
{
material = Activator.CreateInstance(registration.Type);
await SetIdForNewResource(resourceObject, material, registration);
_dbContext.Set(registration.Type).Add(material);
}
await MergeFieldsIntoProperties(resourceObject, material, registration, cancellationToken);
return material;
}
/// <summary>
/// Allows implementers to control how a new resource's ID should be set.
/// </summary>
protected virtual Task SetIdForNewResource(IResourceObject resourceObject, object newObject, IResourceTypeRegistration typeRegistration)
{
if (resourceObject.Id != null)
{
typeRegistration.IdProperty.SetValue(newObject, Convert.ChangeType(resourceObject.Id, typeRegistration.IdProperty.PropertyType));
}
return Task.FromResult(0);
}
/// <summary>
/// Gets an existing record from the store by ID, if it exists
/// </summary>
/// <returns></returns>
protected virtual async Task<object> GetExistingRecord(IResourceTypeRegistration registration, string id,
ResourceTypeRelationship[] relationshipsToInclude, CancellationToken cancellationToken)
{
var method = _openGetExistingRecordGenericMethod.MakeGenericMethod(registration.Type);
var result = (dynamic) method.Invoke(this, new object[] {registration, id, relationshipsToInclude, cancellationToken}); // no convert needed => see GetExistingRecordGeneric => filterByIdFactory will do it
return await result;
}
/// <summary>
/// Merges the field values of the given resource object into the materialized object
/// </summary>
/// <param name="resourceObject"></param>
/// <param name="material"></param>
/// <param name="registration"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <exception cref="DeserializationException">Thrown when a semantically incorrect part of the document is encountered</exception>
protected virtual async Task MergeFieldsIntoProperties(IResourceObject resourceObject, object material,
IResourceTypeRegistration registration, CancellationToken cancellationToken)
{
foreach (var attributeValue in resourceObject.Attributes)
{
var attribute = registration.GetFieldByName(attributeValue.Key) as ResourceTypeAttribute;
if (attribute == null) continue;
attribute.SetValue(material, attributeValue.Value);
}
foreach (var relationshipValue in resourceObject.Relationships)
{
var linkage = relationshipValue.Value.Linkage;
var typeRelationship = registration.GetFieldByName(relationshipValue.Key) as ResourceTypeRelationship;
if (typeRelationship == null) continue;
if (typeRelationship.IsToMany)
{
if (linkage == null)
throw new DeserializationException("Missing linkage for to-many relationship",
"Expected an array for to-many linkage, but no linkage was specified.", "/data/relationships/" + relationshipValue.Key);
if (!linkage.IsToMany)
throw new DeserializationException("Invalid linkage for to-many relationship",
"Expected an array for to-many linkage.",
"/data/relationships/" + relationshipValue.Key + "/data");
// TODO: One query per related object is going to be slow. At the very least, we should be able to group the queries by type
var newCollection = new List<object>();
foreach (var resourceIdentifier in linkage.Identifiers)
{
var relatedObjectRegistration = _registry.GetRegistrationForResourceTypeName(resourceIdentifier.Type);
var relatedObject = await GetExistingRecord(relatedObjectRegistration, resourceIdentifier.Id, null, cancellationToken);
newCollection.Add(relatedObject);
}
var method = _openSetToManyRelationshipValueMethod.MakeGenericMethod(typeRelationship.RelatedType);
method.Invoke(this, new[] { material, newCollection, typeRelationship });
}
else
{
if (linkage == null)
throw new DeserializationException("Missing linkage for to-one relationship",
"Expected an object for to-one linkage, but no linkage was specified.", "/data/relationships/" + relationshipValue.Key);
if (linkage.IsToMany)
throw new DeserializationException("Invalid linkage for to-one relationship",
"Expected an object or null for to-one linkage",
"/data/relationships/" + relationshipValue.Key + "/data");
var identifier = linkage.Identifiers.FirstOrDefault();
if (identifier == null)
{
typeRelationship.Property.SetValue(material, null);
}
else
{
var relatedObjectRegistration = _registry.GetRegistrationForResourceTypeName(identifier.Type);
var relatedObject =
await GetExistingRecord(relatedObjectRegistration, identifier.Id, null, cancellationToken);
typeRelationship.Property.SetValue(material, relatedObject);
}
}
}
}
/// <summary>
/// Gets a record by ID
/// </summary>
protected async Task<TRecord> GetExistingRecordGeneric<TRecord>(IResourceTypeRegistration registration,
string id, ResourceTypeRelationship[] relationshipsToInclude, CancellationToken cancellationToken) where TRecord : class
{
var param = Expression.Parameter(registration.Type);
var filterExpression = registration.GetFilterByIdExpression(param, id); // no conversion of id => filterByIdFactory will do it
var lambda = Expression.Lambda<Func<TRecord, bool>>(filterExpression, param);
var query = _dbContext.Set<TRecord>().AsQueryable()
.Where(lambda);
if (relationshipsToInclude != null)
{
query = relationshipsToInclude.Aggregate(query,
(current, resourceTypeRelationship) => current.Include(resourceTypeRelationship.Property.Name));
}
return await query.FirstOrDefaultAsync(cancellationToken);
}
/// <summary>
/// Sets the value of a to-many relationship
/// </summary>
protected void SetToManyRelationshipValue<TRelated>(object material, IEnumerable<object> relatedObjects, ResourceTypeRelationship relationship)
{
var currentValue = relationship.Property.GetValue(material);
var typedArray = relatedObjects.Select(o => (TRelated) o).ToArray();
if (relationship.Property.PropertyType.IsAssignableFrom(typeof (List<TRelated>)))
{
if (currentValue == null)
{
relationship.Property.SetValue(material, typedArray.ToList());
}
else
{
var listCurrentValue = (ICollection<TRelated>) currentValue;
var itemsToAdd = typedArray.Except(listCurrentValue);
var itemsToRemove = listCurrentValue.Except(typedArray).ToList();
foreach (var related in itemsToAdd)
listCurrentValue.Add(related);
foreach (var related in itemsToRemove)
listCurrentValue.Remove(related);
}
}
else
{
relationship.Property.SetValue(material, typedArray);
}
}
}
}