-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathCachedTopicMappingService.cs
217 lines (191 loc) · 11.7 KB
/
CachedTopicMappingService.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
/*==============================================================================================================================
| Author Ignia, LLC
| Client Ignia, LLC
| Project Topics Library
\=============================================================================================================================*/
using System.Collections.Concurrent;
using OnTopic.Mapping.Annotations;
namespace OnTopic.Mapping {
/*============================================================================================================================
| CLASS: CACHED TOPIC MAPPING SERVICE
\---------------------------------------------------------------------------------------------------------------------------*/
/// <summary>
/// Provides a caching interface for an underlying <see cref="ITopicMappingService"/>, by caching entire mapped object
/// graphs based on the <see cref="Topic.Id"/> of the root topic.
/// </summary>
public class CachedTopicMappingService : ITopicMappingService {
/*==========================================================================================================================
| PRIVATE VARIABLES
\-------------------------------------------------------------------------------------------------------------------------*/
private readonly ITopicMappingService _topicMappingService;
/*==========================================================================================================================
| ESTABLISH CACHE
\-------------------------------------------------------------------------------------------------------------------------*/
private readonly ConcurrentDictionary<(int, Type?, AssociationTypes), object> _cache = new();
/*==========================================================================================================================
| CONSTRUCTOR
\-------------------------------------------------------------------------------------------------------------------------*/
/// <summary>
/// Establishes a new instance of a <see cref="CachedTopicMappingService"/> with required dependencies.
/// </summary>
public CachedTopicMappingService(ITopicMappingService topicMappingService) {
Contract.Requires(topicMappingService, "An instance of topicMappingService is required.");
_topicMappingService = topicMappingService;
}
/*==========================================================================================================================
| METHOD: MAP
\-------------------------------------------------------------------------------------------------------------------------*/
/// <inheritdoc />
public async Task<object?> MapAsync(Topic? topic, AssociationTypes associations = AssociationTypes.All) {
/*------------------------------------------------------------------------------------------------------------------------
| Handle null source
\-----------------------------------------------------------------------------------------------------------------------*/
if (topic is null) return null;
/*------------------------------------------------------------------------------------------------------------------------
| Ensure cache is populated
\-----------------------------------------------------------------------------------------------------------------------*/
var cacheKey = (topic.Id, (Type?)null, associations);
if(_cache.TryGetValue(cacheKey, out var viewModel)) {
return viewModel;
}
/*------------------------------------------------------------------------------------------------------------------------
| Process result
\-----------------------------------------------------------------------------------------------------------------------*/
viewModel = await _topicMappingService.MapAsync(topic, associations).ConfigureAwait(false);
/*------------------------------------------------------------------------------------------------------------------------
| Return (cached) result
\-----------------------------------------------------------------------------------------------------------------------*/
if (viewModel is null) {
return null;
}
return CacheViewModel(
topic.ContentType,
viewModel,
cacheKey
);
}
/*==========================================================================================================================
| METHOD: MAP (T)
\-------------------------------------------------------------------------------------------------------------------------*/
/// <inheritdoc />
public async Task<T?> MapAsync<T>(Topic? topic, AssociationTypes associations = AssociationTypes.All) where T : class {
/*------------------------------------------------------------------------------------------------------------------------
| Handle null source
\-----------------------------------------------------------------------------------------------------------------------*/
if (topic is null) return null;
/*------------------------------------------------------------------------------------------------------------------------
| Ensure cache is populated
\-----------------------------------------------------------------------------------------------------------------------*/
var cacheKey = (topic.Id, typeof(T), associations);
if (_cache.TryGetValue(cacheKey, out var viewModel)) {
return (T)viewModel;
}
/*------------------------------------------------------------------------------------------------------------------------
| Process result
\-----------------------------------------------------------------------------------------------------------------------*/
viewModel = await _topicMappingService.MapAsync<T>(topic, associations).ConfigureAwait(false);
/*------------------------------------------------------------------------------------------------------------------------
| Return (cached) result
\-----------------------------------------------------------------------------------------------------------------------*/
if (viewModel is null) {
return null;
}
return CacheViewModel(
topic.ContentType,
viewModel,
cacheKey
) as T;
}
/*==========================================================================================================================
| METHOD: MAP (OBJECTS)
\-------------------------------------------------------------------------------------------------------------------------*/
/// <inheritdoc />
public async Task<object?> MapAsync(Topic? topic, object target, AssociationTypes associations = AssociationTypes.All) {
/*------------------------------------------------------------------------------------------------------------------------
| Handle null source
\-----------------------------------------------------------------------------------------------------------------------*/
if (topic is null) return null;
/*------------------------------------------------------------------------------------------------------------------------
| Validate parameters
\-----------------------------------------------------------------------------------------------------------------------*/
Contract.Requires(target, nameof(target));
/*------------------------------------------------------------------------------------------------------------------------
| Ensure cache is populated
\-----------------------------------------------------------------------------------------------------------------------*/
var cacheKey = (topic.Id, target.GetType(), associations);
if (_cache.TryGetValue(cacheKey, out var viewModel)) {
return viewModel;
}
/*------------------------------------------------------------------------------------------------------------------------
| Process result
\-----------------------------------------------------------------------------------------------------------------------*/
viewModel = await _topicMappingService.MapAsync(topic, associations).ConfigureAwait(false);
/*------------------------------------------------------------------------------------------------------------------------
| Return (cached) result
\-----------------------------------------------------------------------------------------------------------------------*/
if (viewModel is null) {
return null;
}
return CacheViewModel(
topic.ContentType,
viewModel,
cacheKey
);
}
/*==========================================================================================================================
| METHOD: CACHE VIEW MODEL
\-------------------------------------------------------------------------------------------------------------------------*/
/// <summary>
/// Given a view model, determines if it is appropriate to cache and, if so, adds it to the cache. Regardless, returns the
/// view model back to the consumer.
/// </summary>
/// <remarks>
/// The internal will potentially add two entries to the cache for every view model.
/// <list type="number">
/// <item>
/// The first will be bound to the <see cref="Topic.Id"/>, view model <see cref="Type"/>, and the <see cref="
/// AssociationTypes"/> mapped.
/// </item>
/// <item>
/// The second will assume a null <see cref="Type"/>, and can be used for scenarios where the <see cref="Type"/> is
/// not known—and, thus, assumed to be the default mapping.
/// </item>
/// </list>
/// In all cases, the <see cref="Topic.Id"/> must be greater than zero, to ensure that it's a saved entity (otherwise,
/// multiple distinct entities will have the default <see cref="Topic.Id"/> of <c>-1</c>). In addition, the following
/// conditions apply, respectively, to each of the caches:
/// <list type="number">
/// <item>
/// The first must have a view model type that is not an <see cref="Object"/>, since it is meant to map to a specific
/// view model type.
/// </item>
/// <item>
/// The second will assume that the view model type of the naming convention <c>{ContentType}TopicViewModel</c>—i.e.,
/// it is the default implementation for the given view model type, and not a specially cast version such as e.g.,
/// <c>NavigationTopicViewModel</c> or <c>TopicViewModel</c>.
/// </item>
/// </list>
/// </remarks>
/// <param name="contentType">The content type associated with the associated <see cref="Topic"/>.</param>
/// <param name="viewModel">The view model object to cache; can be any POCO object.</param>
/// <param name="cacheKey">A Tuple{T1, T2, T3} representing the cache key.</param>
/// <returns>The <paramref name="viewModel"/>.</returns>
private object? CacheViewModel(string contentType, object viewModel, (int, Type?, AssociationTypes) cacheKey) {
if (cacheKey.Item1 > 0 && cacheKey.Item2 is not null && !viewModel.GetType().Equals(typeof(object))) {
_cache.TryAdd(cacheKey, viewModel);
}
if (cacheKey.Item2 is not null) {
cacheKey = (cacheKey.Item1, null, cacheKey.Item3);
}
if (
cacheKey.Item1 > 0 && (
viewModel.GetType().Name == $"{contentType}ViewModel" ||
viewModel.GetType().Name == $"{contentType}TopicViewModel"
)
) {
_cache.TryAdd(cacheKey, viewModel);
}
return viewModel;
}
} //Class
} //Namespace