Repository navigation
Expand file tree
/
Copy pathNodeMappingEngine.cs
More file actions
512 lines (441 loc) · 18.7 KB
/
Copy pathNodeMappingEngine.cs
File metadata and controls
512 lines (441 loc) · 18.7 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
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
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using umbraco;
using umbraco.cms.businesslogic.web;
using umbraco.NodeFactory;
using System.Linq.Expressions;
using System.Web.Caching;
namespace uComponents.Mapping
{
/// <summary>
/// Handles the creation of map and the mapping of Umbraco <c>Node</c>s to strongly typed
/// models.
/// </summary>
public class NodeMappingEngine : INodeMappingEngine
{
/// <summary>
/// Gets or sets the node mappers.
/// </summary>
/// <value>
/// The node mappers.
/// </value>
public Dictionary<Type, NodeMapper> NodeMappers { get; set; }
private ICacheProvider _cacheProvider;
private content.DocumentCacheEventHandler _documentCacheEventHandler;
/// <summary>
/// Instantiates a new NodeMappingEngine
/// </summary>
public NodeMappingEngine()
{
this.NodeMappers = new Dictionary<Type, NodeMapper>();
}
#region Caching
/// <summary>
/// Instantiates a new NodeMappingEngine using a web cache.
/// </summary>
/// <exception cref="ArgumentNullException">
/// If <paramref name="cache"/> is null.
/// </exception>
public NodeMappingEngine(Cache cache)
: base()
{
if (cache == null)
{
throw new ArgumentNullException("cache");
}
SetCacheProvider(new DefaultCacheProvider(cache));
}
/// <see cref="INodeMappingEngine.IsCachingEnabled" />
public bool IsCachingEnabled
{
get
{
return _cacheProvider != null;
}
}
internal ICacheProvider CacheProvider
{
get
{
return _cacheProvider;
}
}
/// <see cref="INodeMappingEngine.SetCacheProvider"/>
public void SetCacheProvider(ICacheProvider cacheProvider)
{
if (_cacheProvider != null)
{
_cacheProvider.Clear();
content.AfterUpdateDocumentCache -= _documentCacheEventHandler;
content.AfterClearDocumentCache -= _documentCacheEventHandler;
_documentCacheEventHandler = null;
}
if (cacheProvider != null)
{
_documentCacheEventHandler = (sender, e) => cacheProvider.Clear();
// TODO test this
content.AfterUpdateDocumentCache += _documentCacheEventHandler;
content.AfterClearDocumentCache += _documentCacheEventHandler;
// TODO Support the below for custom mappings which use the cache, you never know
//Media.AfterSave += _documentCacheEventHandler;
//Media.AfterDelete += _documentCacheEventHandler;
//CMSNode.AfterMove += _documentCacheEventHandler;
//Member.AfterSave += _documentCacheEventHandler;
//Member.AfterDelete += _documentCacheEventHandler;
}
_cacheProvider = cacheProvider;
}
#endregion
#region Creating maps
/// <summary>
/// Creates a map to a strong type from an Umbraco document type
/// using the unqualified class name of <typeparamref name="TDestination"/>
/// as the document type alias.
/// </summary>
/// <typeparam name="TDestination">The type to map to.</typeparam>
/// <returns>Further mapping configuration</returns>
/// <exception cref="DocumentTypeNotFoundException">
/// If the document type with an alias of <typeparamref name="TDestination"/>'s
/// class name could not be found
/// </exception>
public INodeMappingExpression<TDestination> CreateMap<TDestination>()
where TDestination : class, new()
{
var destinationType = typeof(TDestination);
return this.CreateMap<TDestination>(destinationType.Name);
}
/// <summary>
/// Creates a map to a strong type from an Umbraco document type.
/// </summary>
/// <typeparam name="TDestination">The type to map to.</typeparam>
/// <param name="documentTypeAlias">The document type alias to map from.</param>
/// <returns>Further mapping configuration</returns>
/// <exception cref="DocumentTypeNotFoundException">
/// If the <paramref name="documentTypeAlias"/> could not be found
/// </exception>
public INodeMappingExpression<TDestination> CreateMap<TDestination>(string documentTypeAlias)
where TDestination : class, new()
{
var destinationType = typeof(TDestination);
// Remove current mapping if any
if (NodeMappers.ContainsKey(destinationType))
{
NodeMappers.Remove(destinationType);
}
// Get document type
var docType = DocumentType.GetAllAsList()
.SingleOrDefault(d => string.Equals(d.Alias, documentTypeAlias, StringComparison.InvariantCultureIgnoreCase));
if (docType == null)
{
throw new DocumentTypeNotFoundException(documentTypeAlias);
}
var nodeMapper = new NodeMapper(this, destinationType, docType);
NodeMappers[destinationType] = nodeMapper;
if (_cacheProvider != null)
{
_cacheProvider.Clear();
}
return new NodeMappingExpression<TDestination>(this, nodeMapper);
}
#endregion
#region Mapping
/// <summary>
/// Gets an Umbraco <c>Node</c> as a <paramref name="destinationType"/>, only including
/// specified relationship paths.
/// </summary>
/// <param name="sourceNode">The <c>Node</c> to map from.</param>
/// <param name="destinationType">The type to map to.</param>
/// <param name="paths">The relationship paths to include, or null to include
/// all relationship paths at the top level and none below.</param>
/// <returns>
/// <c>null</c> if the node does not exist or does not map to <paramref name="destinationType"/>.
/// </returns>
/// <exception cref="MapNotFoundException">If a suitable map for <paramref name="destinationType"/> has not
/// been created with <see cref="CreateMap()" />.</exception>
public object Map(Node sourceNode, Type destinationType, string[] paths)
{
if (sourceNode == null
|| string.IsNullOrEmpty(sourceNode.Name))
{
return null;
}
var context = new NodeMappingContext(sourceNode, paths, null);
return Map(context, destinationType);
}
/// <summary>
/// Gets an Umbraco <c>Node</c> as a <typeparamref name="TDestination"/>, only including
/// specified relationship paths.
/// </summary>
/// <typeparam name="TDestination">
/// The type of object that <paramref name="sourceNode"/> maps to.
/// </typeparam>
/// <param name="sourceNode">The <c>Node</c> to map from.</param>
/// <param name="paths">The relationship paths to include.</param>
/// <returns><c>null</c> if the node does not exist.</returns>
public TDestination Map<TDestination>(Node sourceNode, params string[] paths)
where TDestination : class, new()
{
return (TDestination)Map(sourceNode, typeof(TDestination), paths);
}
/// <summary>
/// Maps a node based on the <paramref name="context"/>.
/// </summary>
/// <param name="context">The context which describes the node mapping.</param>
/// <param name="destinationType">The type to map to.</param>
/// <returns><c>null</c> if the node does not exist.</returns>
internal object Map(NodeMappingContext context, Type destinationType)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
else if (destinationType == null)
{
throw new ArgumentNullException("destinationType");
}
else if (!NodeMappers.ContainsKey(destinationType))
{
throw new MapNotFoundException(destinationType);
}
string sourceNodeTypeAlias = null;
if (_cacheProvider != null
&& _cacheProvider.ContainsAlias(context.Id))
{
sourceNodeTypeAlias = _cacheProvider.GetAlias(context.Id);
if (sourceNodeTypeAlias == null)
{
// Node does not exist
return null;
}
}
if (sourceNodeTypeAlias == null)
{
var node = context.GetNode();
if (node == null || string.IsNullOrEmpty(node.Name))
{
// Node doesn't exist
if (_cacheProvider != null)
{
_cacheProvider.InsertAlias(context.Id, null);
}
return null;
}
if (_cacheProvider != null)
{
_cacheProvider.InsertAlias(context.Id, node.NodeTypeAlias);
}
sourceNodeTypeAlias = node.NodeTypeAlias;
}
var nodeMapper = GetMapper(sourceNodeTypeAlias, destinationType);
if (nodeMapper == null)
{
return null;
}
return nodeMapper.MapNode(context);
}
/// <summary>
/// Examines the engine's <see cref="NodeMappers"/> and returns node mapper
/// which maps to the closest base class of <paramref name="type"/>.
/// </summary>
/// <returns>
/// <c>null</c> if there are no mappers which map to a base class of
/// <paramref name="type"/>.
/// </returns>
internal NodeMapper GetBaseNodeMapperForType(Type type)
{
var ancestorMappers = new List<NodeMapper>();
foreach (var nodeMapper in NodeMappers)
{
if (nodeMapper.Value.DestinationType.IsAssignableFrom(type)
&& type != nodeMapper.Value.DestinationType)
{
ancestorMappers.Add(nodeMapper.Value);
}
}
// Sort by inheritance
ancestorMappers.Sort((x, y) =>
{
return x.DestinationType.IsAssignableFrom(y.DestinationType)
? 1
: -1;
});
return ancestorMappers.FirstOrDefault();
}
/// <summary>
/// Gets a node mapper which maps from <paramref name="sourceNodeTypeAlias"/>
/// to <paramref name="destinationType"/> or some class derived from
/// <paramref name="destinationType"/>.
/// </summary>
/// <param name="sourceNodeTypeAlias">Node type alias to map from.</param>
/// <param name="destinationType">The type which the mapped model must
/// cast to.</param>
/// <returns>The node mapper, or null if a suitable mapper could not be found</returns>
internal NodeMapper GetMapper(string sourceNodeTypeAlias, Type destinationType)
{
foreach (var nodeMapper in NodeMappers)
{
if (destinationType.IsAssignableFrom(nodeMapper.Value.DestinationType)
&& nodeMapper.Value.SourceDocumentType.Alias == sourceNodeTypeAlias)
{
return nodeMapper.Value;
}
}
return null;
}
/// <summary>
/// Gets all node type aliases which can map to <paramref name="destinationType"/>.
/// </summary>
internal string[] GetCompatibleNodeTypeAliases(Type destinationType)
{
var compatibleAliases = new List<string>();
foreach (var nodeMapper in NodeMappers)
{
if (destinationType.IsAssignableFrom(nodeMapper.Value.DestinationType))
{
compatibleAliases.Add(nodeMapper.Value.SourceDocumentType.Alias);
}
}
return compatibleAliases.Distinct().ToArray();
}
#endregion
#region Querying
/// <summary>
/// Gets a query for nodes which map to <typeparamref name="TDestination"/>.
/// </summary>
/// <typeparam name="TDestination">The type to map to.</typeparam>
/// <returns>A fluent configuration for the query.</returns>
/// <exception cref="MapNotFoundException">If a suitable map for <typeparamref name="TDestination"/> has not
/// been created with <see cref="CreateMap()" />.</exception>
public INodeQuery<TDestination> Query<TDestination>()
where TDestination : class, new()
{
return new NodeQuery<TDestination>(this);
}
#endregion
#region Legacy
/// <summary>
/// Maps an Umbraco <c>Node</c> as a strongly typed object.
/// </summary>
/// <param name="sourceNode">The <c>Node</c> to map from.</param>
/// <param name="destinationType">The type to map to.</param>
/// <param name="includedRelationships">The relationship properties to include, or <c>null</c> to
/// include all relationships.</param>
/// <returns>
/// <c>null</c> if the node does not exis or does not map to <paramref name="destinationType"/>.
/// </returns>
/// <exception cref="MapNotFoundException">If a suitable map for <paramref name="destinationType"/> has not
/// been created with <see cref="CreateMap()" />.</exception>
[Obsolete("Use Map with paths instead")]
public object Map(Node sourceNode, Type destinationType, PropertyInfo[] includedRelationships)
{
return Map(
sourceNode,
destinationType,
includedRelationships.Select(x => x.Name).ToArray()
);
}
/// <summary>
/// Gets an Umbraco <c>Node</c> as a <typeparamref name="TDestination"/>.
/// </summary>
/// <typeparam name="TDestination">
/// The type of object that <paramref name="sourceNode"/> maps to.
/// </typeparam>
/// <param name="sourceNode">The <c>Node</c> to map from.</param>
/// <param name="includeRelationships">Whether to include the <c>Node</c>'s relationships</param>
/// <returns>
/// <c>null</c> if the <c>Node</c> does not exist or does not map to
/// <typeparamref name="TDestination"/>.
/// </returns>
/// <exception cref="MapNotFoundException">If a suitable map for <typeparamref name="TDestination"/> has not
/// been created with <see cref="CreateMap()" />.</exception>
[Obsolete("Use Map with paths instead")]
public TDestination Map<TDestination>(Node sourceNode, bool includeRelationships)
where TDestination : class, new()
{
return (TDestination)Map(
sourceNode,
typeof(TDestination),
includeRelationships
? null // all relationships
: new string[0] // no relationships
);
}
/// <summary>
/// Gets an Umbraco <c>Node</c> as a <typeparamref name="TDestination"/>, only including
/// specified relationships.
/// </summary>
/// <typeparam name="TDestination">
/// The type of object that <paramref name="sourceNode"/> maps to.
/// </typeparam>
/// <param name="sourceNode">The <c>Node</c> to map from.</param>
/// <param name="includedRelationships">The relationship properties to include.</param>
/// <returns>
/// <c>null</c> if the node does not exist or does not map to
/// <typeparamref name="TDestination"/>.
/// </returns>
/// <exception cref="MapNotFoundException">If a suitable map for <typeparamref name="TDestination"/> has not
/// been created with <see cref="CreateMap()" />.</exception>
[Obsolete("Use paths instead")]
public TDestination Map<TDestination>(Node sourceNode, params Expression<Func<TDestination, object>>[] includedRelationships)
where TDestination : class, new()
{
// Get properties from included relationships expression
var properties = includedRelationships.Select(e => (e.Body as MemberExpression).Member as PropertyInfo).ToArray();
return (TDestination)Map(
sourceNode,
typeof(TDestination),
properties.Select(x => x.Name).ToArray()
);
}
#endregion
}
#region Exceptions
/// <summary>
/// The node type alias was not found in the current Umbraco instance.
/// </summary>
public class DocumentTypeNotFoundException : Exception
{
/// <param name="nodeTypeAlias">The node type alias requested and not found</param>
public DocumentTypeNotFoundException(string nodeTypeAlias)
: base(string.Format(@"The document type with alias '{0}' could not be found.
Consider using the overload of CreateMap which specifies a document type alias", nodeTypeAlias))
{
}
}
/// <summary>
/// No map exists for this engine for the destination type
/// </summary>
public class MapNotFoundException : Exception
{
/// <param name="destinationType">The requested and unfound destination type</param>
public MapNotFoundException(Type destinationType)
: base(string.Format(@"No map could be found for type '{0}'. Remember
to run CreateMap for every model type you are using.", destinationType.FullName))
{
}
}
/// <summary>
/// A collection which cannot be instiatated/populated by the mapping engine
/// is used.
/// </summary>
public class CollectionTypeNotSupportedException : Exception
{
/// <param name="type">The unsupported collection type.</param>
public CollectionTypeNotSupportedException(Type type)
: base(string.Format(
@"Could not map to collection of type '{0}'.
The property type must be assignable from IEnumerable<{1}>
or have a constructor which takes a single argument of IEnumerable<{1}>
(such as a List<{1}>).",
type.FullName,
type.Name))
{
}
}
#endregion
}