Blog Series: Episerver Settings Tab Improvements – A Simpler Version

Blog Series: Episerver Settings Tab Improvements – A Simpler Version

Up to this point we have been able to improve the original Settings Service implementation, which is part of the Episerver Foundation and Episerver Foundation CMS solutions, so it is capable of working in balanced environments (two or more servers) and able to handle multiple languages. However, most of this issues are related with the concurrent dictionary which is a core component of the Settings Service class. What we decided to do in this post is to keep the dictionary but use it to save Guid references to the corresponding site setting items instead of saving the setting items themselves. This will probably impact performance because we will use the Episerver content repository instead of recovering the items in memory, but it will greatly simplify the code to achieve multi language support, the code for balanced environment support will be kept the same

For this implementation we will modify the original SettingService.cs class which is not capable of handling balanced environments nor multiple languages. The changes we made to handle these features are the following:

First, we will modify the ISettingsService interface so the GetSiteSettings method generic parameter T is restricted to the SettingsBase type. Then, we will modify the signature for the concurrent dictionary used in the class from a Guid, Dictionary<Type, Object> to a Guid, Dictionary<Type, Guid>

1
2
T GetSiteSettings<T>(Guid? siteId = null) where T : SettingsBase; 
ConcurrentDictionary<Guid, Dictionary<Type, Guid>> SiteSettings { get; }

We then modify the concurrent dictionary in the Setting Service class implementation

1
public ConcurrentDictionary<Guid, Dictionary<Type, Guid>> SiteSettings { get; } = new ConcurrentDictionary<Guid, Dictionary<Type, Guid>>();

Then, we will modify the method GetSiteSettings. In order to get the correct item for the specific language we use the content repository get event with the content guid

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
public T GetSiteSettings<T>(Guid? siteId = null) where T : SettingsBase
{
    if (!siteId.HasValue)
    {
        siteId = ResolveSiteId();
        if (siteId == Guid.Empty)
        {
            return default;
        }
    }
 
    try
    {
        if (SiteSettings.TryGetValue(siteId.Value, out var siteSettings) &&
            siteSettings.TryGetValue(typeof(T), out var settingId))
        {
            return _contentRepository.Get<T>(settingId);
        }
    }
    catch (KeyNotFoundException keyNotFoundException)
    {
        _log.Error($"[Settings] {keyNotFoundException.Message}", exception: keyNotFoundException);
    }
    catch (ArgumentNullException argumentNullException)
    {
        _log.Error($"[Settings] {argumentNullException.Message}", exception: argumentNullException);
    }
 
    return default;
}

Later, we will modify the method UpdateSettings with parameters which will use a similar implementation to the original one, but when the setting item is updated we do not save the setting item itself, but is guid reference

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void UpdateSettings(Guid siteId, IContent content)
{
    var contentType = content.GetOriginalType();
    try
    {
        if (!SiteSettings.ContainsKey(siteId))
        {
            SiteSettings[siteId] = new Dictionary<Type, Guid>();
        }
 
        SiteSettings[siteId][contentType] = content.ContentGuid;
    }
    catch (KeyNotFoundException keyNotFoundException)
    {
        _log.Error($"[Settings] {keyNotFoundException.Message}", exception: keyNotFoundException);
    }
    catch (ArgumentNullException argumentNullException)
    {
        _log.Error($"[Settings] {argumentNullException.Message}", exception: argumentNullException);
    }
}

For this settings service to work in balanced environments we will also need to add the same code that we implemented for the version with the concurrent dictionary that saves the site setting items in memory. You can find how to do it step by step in the previous blog post of this blog series. Here we will explain it in less detail.

We will first have to add the two needed variables two the Settings Service class, raiser id and event id.

1
2
3
//Generate unique id for your event and the raiser
private readonly Guid _raiserId;
private static Guid EventId => new Guid("888B5C89-9B0F-4E67-A3B0-6E660AB9A60F");

Then, we will add a new dependency to the Event registry service and add it to the constructor of the Setting Service class. In addition, we will also initialize the raiser Id local variable with a new Guid value

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
private readonly IEventRegistry _eventRegistry;
 
public SettingsService(
    IContentRepository contentRepository,
    ContentRootService contentRootService,
    ITypeScannerLookup typeScannerLookup,
    IContentTypeRepository contentTypeRepository,
    IContentEvents contentEvents,
    ISiteDefinitionEvents siteDefinitionEvents,
    ISiteDefinitionRepository siteDefinitionRepository,
    ISiteDefinitionResolver siteDefinitionResolver,
    ServiceAccessor<HttpContextBase> httpContext,
        IEventRegistry eventRegistry)
{
    _contentRepository = contentRepository;
    _contentRootService = contentRootService;
    _typeScannerLookup = typeScannerLookup;
    _contentTypeRepository = contentTypeRepository;
    _contentEvents = contentEvents;
    _siteDefinitionEvents = siteDefinitionEvents;
    _siteDefinitionRepository = siteDefinitionRepository;
    _siteDefinitionResolver = siteDefinitionResolver;
    _httpContext = httpContext;
    _eventRegistry = eventRegistry;
 
    _raiserId = Guid.NewGuid();
}

We also need to add two private events to the Settings Service class, one to handle the raised event and another one raise the event.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void SettingsEvent_Raised(object sender, EventNotificationEventArgs e)
{
    // don't process events locally raised
    if (e.RaiserId != _raiserId)
    {
        //Do something, e.g. invalidate cache
        if (e.Param is SettingEventData settingUpdate)
        {
            var content = _contentRepository.Get<IContent>(Guid.Parse(settingUpdate.ContentId));
            if (content != null)
            {
                UpdateSettings(Guid.Parse(settingUpdate.SiteId), content);
            }
        }
    }
}
 
private void RaiseEvent(SettingEventData message)
{
    _eventRegistry.Get(EventId).Raise(_raiserId, message);
}

Next, we will add a new SettingEventData class which is a little bit different to the one used in the previous blog post because we do not require the language property anymore

1
2
3
4
5
6
7
8
9
[DataContract]
[EventsServiceKnownType]
public class SettingEventData
{
    [DataMember]
    public string SiteId { get; set; }
    [DataMember]
    public string ContentId { get; set; }
}

After setting up those two methods, we will subscribe and unsubscribe to the raiser event handler method in the initialize and uninitialize methods of the Setting Service class.

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
public void InitializeSettings()
{
    try
    {
        RegisterContentRoots();
    }
    catch (NotSupportedException notSupportedException)
    {
        _log.Error($"[Settings] {notSupportedException.Message}", exception: notSupportedException);
        throw;
    }
 
    _contentEvents.PublishedContent += PublishedContent;
    _siteDefinitionEvents.SiteCreated += SiteCreated;
    _siteDefinitionEvents.SiteUpdated += SiteUpdated;
    _siteDefinitionEvents.SiteDeleted += SiteDeleted;
 
    var settingsEvent = _eventRegistry.Get(EventId);
    settingsEvent.Raised += SettingsEvent_Raised;
}
 
public void UnInitializeSettings()
{
    _contentEvents.PublishedContent -= PublishedContent;
    _siteDefinitionEvents.SiteCreated -= SiteCreated;
    _siteDefinitionEvents.SiteUpdated -= SiteUpdated;
    _siteDefinitionEvents.SiteDeleted -= SiteDeleted;
 
    var settingsEvent = _eventRegistry.Get(EventId);
    settingsEvent.Raised -= SettingsEvent_Raised;
}

Finally, we will modify the Publish Content method to raise the event at the end of the method and after the content is updated in the first server.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void PublishedContent(object sender, ContentEventArgs e)
{
    if (!(e?.Content is SettingsBase))
    {
        return;
    }
 
    var parent = _contentRepository.Get<IContent>(e.Content.ParentLink);
    var site = _siteDefinitionRepository.Get(parent.Name);
 
    var id = site?.Id;
    if (id == null || id == Guid.Empty)
    {
        return;
    }
    UpdateSettings(id.Value, e.Content);
    RaiseEvent(new SettingEventData
    {
        SiteId = id.ToString(),
        ContentId = e.Content.ContentGuid.ToString()
    });
}

After all those changes the final implementation of the Settings Service class will look like this

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
using EPiServer;
using EPiServer.Core;
using EPiServer.DataAbstraction;
using EPiServer.DataAccess;
using EPiServer.Events;
using EPiServer.Events.Clients;
using EPiServer.Framework.TypeScanner;
using EPiServer.Logging;
using EPiServer.Security;
using EPiServer.ServiceLocation;
using EPiServer.Web;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Web;
 
namespace Foundation.Cms.Settings
{
    public interface ISettingsService
    {
        ContentReference GlobalSettingsRoot { get; set; }
        ConcurrentDictionary<Guid, Dictionary<Type, Guid>> SiteSettings { get; }
        T GetSiteSettings<T>(Guid? siteId = null) where T : SettingsBase;
        void InitializeSettings();
        void UnInitializeSettings();
        void UpdateSettings(Guid siteId, IContent content);
        void UpdateSettings();
    }
 
    public static class ISettingsServiceExtensions
    {
        public static T GetSiteSettingsOrThrow<T>(this ISettingsService settingsService,
            Func<T, bool> shouldThrow,
            string message) where T : SettingsBase
        {
            var settings = settingsService.GetSiteSettings<T>();
            if (settings == null || (shouldThrow?.Invoke(settings) ?? false))
            {
                throw new InvalidOperationException(message);
            }
 
            return settings;
        }
 
        public static bool TryGetSiteSettings<T>(this ISettingsService settingsService, out T value) where T : SettingsBase
        {
            value = settingsService.GetSiteSettings<T>();
            return value != null;
        }
    }
 
    public class SettingsService : ISettingsService
    {
        //Generate unique id for your event and the raiser
        private readonly Guid _raiserId;
        private static Guid EventId => new Guid("888B5C89-9B0F-4E67-A3B0-6E660AB9A60F");
 
 
        private readonly IContentRepository _contentRepository;
        private readonly ContentRootService _contentRootService;
        private readonly IContentTypeRepository _contentTypeRepository;
        private readonly ILogger _log = LogManager.GetLogger();
        private readonly ITypeScannerLookup _typeScannerLookup;
        private readonly IContentEvents _contentEvents;
        private readonly ISiteDefinitionEvents _siteDefinitionEvents;
        private readonly ISiteDefinitionRepository _siteDefinitionRepository;
        private readonly ISiteDefinitionResolver _siteDefinitionResolver;
        private readonly ServiceAccessor<HttpContextBase> _httpContext;
        private readonly IEventRegistry _eventRegistry;
 
        public SettingsService(
            IContentRepository contentRepository,
            ContentRootService contentRootService,
            ITypeScannerLookup typeScannerLookup,
            IContentTypeRepository contentTypeRepository,
            IContentEvents contentEvents,
            ISiteDefinitionEvents siteDefinitionEvents,
            ISiteDefinitionRepository siteDefinitionRepository,
            ISiteDefinitionResolver siteDefinitionResolver,
            ServiceAccessor<HttpContextBase> httpContext,
                IEventRegistry eventRegistry)
        {
            _contentRepository = contentRepository;
            _contentRootService = contentRootService;
            _typeScannerLookup = typeScannerLookup;
            _contentTypeRepository = contentTypeRepository;
            _contentEvents = contentEvents;
            _siteDefinitionEvents = siteDefinitionEvents;
            _siteDefinitionRepository = siteDefinitionRepository;
            _siteDefinitionResolver = siteDefinitionResolver;
            _httpContext = httpContext;
            _eventRegistry = eventRegistry;
 
            _raiserId = Guid.NewGuid();
        }
 
        public ConcurrentDictionary<Guid, Dictionary<Type, Guid>> SiteSettings { get; } = new ConcurrentDictionary<Guid, Dictionary<Type, Guid>>();
 
        public ContentReference GlobalSettingsRoot { get; set; }
 
        public T GetSiteSettings<T>(Guid? siteId = null) where T : SettingsBase
        {
            if (!siteId.HasValue)
            {
                siteId = ResolveSiteId();
                if (siteId == Guid.Empty)
                {
                    return default;
                }
            }
 
            try
            {
                if (SiteSettings.TryGetValue(siteId.Value, out var siteSettings) &&
                    siteSettings.TryGetValue(typeof(T), out var settingId))
                {
                    return _contentRepository.Get<T>(settingId);
                }
            }
            catch (KeyNotFoundException keyNotFoundException)
            {
                _log.Error($"[Settings] {keyNotFoundException.Message}", exception: keyNotFoundException);
            }
            catch (ArgumentNullException argumentNullException)
            {
                _log.Error($"[Settings] {argumentNullException.Message}", exception: argumentNullException);
            }
 
            return default;
        }
 
        public void UpdateSettings(Guid siteId, IContent content)
        {
            var contentType = content.GetOriginalType();
            try
            {
                if (!SiteSettings.ContainsKey(siteId))
                {
                    SiteSettings[siteId] = new Dictionary<Type, Guid>();
                }
 
                SiteSettings[siteId][contentType] = content.ContentGuid;
            }
            catch (KeyNotFoundException keyNotFoundException)
            {
                _log.Error($"[Settings] {keyNotFoundException.Message}", exception: keyNotFoundException);
            }
            catch (ArgumentNullException argumentNullException)
            {
                _log.Error($"[Settings] {argumentNullException.Message}", exception: argumentNullException);
            }
        }
 
        public void InitializeSettings()
        {
            try
            {
                RegisterContentRoots();
            }
            catch (NotSupportedException notSupportedException)
            {
                _log.Error($"[Settings] {notSupportedException.Message}", exception: notSupportedException);
                throw;
            }
 
            _contentEvents.PublishedContent += PublishedContent;
            _siteDefinitionEvents.SiteCreated += SiteCreated;
            _siteDefinitionEvents.SiteUpdated += SiteUpdated;
            _siteDefinitionEvents.SiteDeleted += SiteDeleted;
 
            var settingsEvent = _eventRegistry.Get(EventId);
            settingsEvent.Raised += SettingsEvent_Raised;
        }
 
        public void UnInitializeSettings()
        {
            _contentEvents.PublishedContent -= PublishedContent;
            _siteDefinitionEvents.SiteCreated -= SiteCreated;
            _siteDefinitionEvents.SiteUpdated -= SiteUpdated;
            _siteDefinitionEvents.SiteDeleted -= SiteDeleted;
 
            var settingsEvent = _eventRegistry.Get(EventId);
            settingsEvent.Raised -= SettingsEvent_Raised;
        }
 
        public void UpdateSettings()
        {
            var root = _contentRepository.GetItems(_contentRootService.List(), new LoaderOptions())
                 .FirstOrDefault(x => x.ContentGuid == SettingsFolder.SettingsRootGuid);
 
            if (root == null)
            {
                return;
            }
 
            GlobalSettingsRoot = root.ContentLink;
            var children = _contentRepository.GetChildren<SettingsFolder>(GlobalSettingsRoot).ToList();
            foreach (var site in _siteDefinitionRepository.List())
            {
                var folder = children.Find(x => x.Name.Equals(site.Name, StringComparison.InvariantCultureIgnoreCase));
                if (folder != null)
                {
                    foreach (var child in _contentRepository.GetChildren<SettingsBase>(folder.ContentLink))
                    {
                        UpdateSettings(site.Id, child);
                    }
                    continue;
                }
                CreateSiteFolder(site);
            }
        }
 
        private void RegisterContentRoots()
        {
            var registeredRoots = _contentRepository.GetItems(_contentRootService.List(), new LoaderOptions());
            var settingsRootRegistered = registeredRoots.Any(x => x.ContentGuid == SettingsFolder.SettingsRootGuid && x.Name.Equals(SettingsFolder.SettingsRootName));
 
            if (!settingsRootRegistered)
            {
                _contentRootService.Register<SettingsFolder>(SettingsFolder.SettingsRootName, SettingsFolder.SettingsRootGuid, ContentReference.RootPage);
            }
 
            UpdateSettings();
        }
 
        private void CreateSiteFolder(SiteDefinition siteDefinition)
        {
            var folder = _contentRepository.GetDefault<SettingsFolder>(GlobalSettingsRoot);
            folder.Name = siteDefinition.Name;
            var reference = _contentRepository.Save(folder, SaveAction.Publish, AccessLevel.NoAccess);
 
            var settingsModelTypes = _typeScannerLookup.AllTypes
                .Where(t => t.GetCustomAttributes(typeof(SettingsContentTypeAttribute), false).Length > 0);
 
            foreach (var settingsType in settingsModelTypes)
            {
                if (!(settingsType.GetCustomAttributes(typeof(SettingsContentTypeAttribute), false)
                    .FirstOrDefault() is SettingsContentTypeAttribute attribute))
                {
                    continue;
                }
 
                var contentType = _contentTypeRepository.Load(settingsType);
                var newSettings = _contentRepository.GetDefault<IContent>(reference, contentType.ID);
                newSettings.Name = attribute.SettingsName;
                _contentRepository.Save(newSettings, SaveAction.Publish, AccessLevel.NoAccess);
                UpdateSettings(siteDefinition.Id, newSettings);
            }
        }
 
        private void SiteCreated(object sender, SiteDefinitionEventArgs e)
        {
            if (_contentRepository.GetChildren<SettingsFolder>(GlobalSettingsRoot)
                .Any(x => x.Name.Equals(e.Site.Name, StringComparison.InvariantCultureIgnoreCase)))
            {
                return;
            }
 
            CreateSiteFolder(e.Site);
        }
 
        private void SiteDeleted(object sender, SiteDefinitionEventArgs e)
        {
            var folder = _contentRepository.GetChildren<SettingsFolder>(GlobalSettingsRoot)
                .FirstOrDefault(x => x.Name.Equals(e.Site.Name, StringComparison.InvariantCultureIgnoreCase));
 
            if (folder == null)
            {
                return;
            }
 
            _contentRepository.Delete(folder.ContentLink, true, AccessLevel.NoAccess);
        }
 
        private void SiteUpdated(object sender, SiteDefinitionEventArgs e)
        {
            if (e is SiteDefinitionUpdatedEventArgs updatedArgs)
            {
                var prevSite = updatedArgs.PreviousSite;
                var updatedSite = updatedArgs.Site;
                var settingsRoot = GlobalSettingsRoot;
                if (_contentRepository.GetChildren<IContent>(settingsRoot)
                    .FirstOrDefault(x => x.Name.Equals(prevSite.Name, StringComparison.InvariantCultureIgnoreCase)) is ContentFolder currentSettingsFolder)
                {
                    var cloneFolder = currentSettingsFolder.CreateWritableClone();
                    cloneFolder.Name = updatedSite.Name;
                    _contentRepository.Save(cloneFolder);
                    return;
                }
            }
 
 
            CreateSiteFolder(e.Site);
        }
 
        private void PublishedContent(object sender, ContentEventArgs e)
        {
            if (!(e?.Content is SettingsBase))
            {
                return;
            }
 
            var parent = _contentRepository.Get<IContent>(e.Content.ParentLink);
            var site = _siteDefinitionRepository.Get(parent.Name);
 
            var id = site?.Id;
            if (id == null || id == Guid.Empty)
            {
                return;
            }
            UpdateSettings(id.Value, e.Content);
            RaiseEvent(new SettingEventData
            {
                SiteId = id.ToString(),
                ContentId = e.Content.ContentGuid.ToString()
            });
        }
 
        private Guid ResolveSiteId()
        {
            var request = _httpContext()?.Request;
            if (request == null)
            {
                return Guid.Empty;
            }
            var site = _siteDefinitionResolver.Get(request);
            return site?.Id ?? Guid.Empty;
        }
 
        private void SettingsEvent_Raised(object sender, EventNotificationEventArgs e)
        {
            // don't process events locally raised
            if (e.RaiserId != _raiserId)
            {
                //Do something, e.g. invalidate cache
                if (e.Param is SettingEventData settingUpdate)
                {
                    var content = _contentRepository.Get<IContent>(Guid.Parse(settingUpdate.ContentId));
                    if (content != null)
                    {
                        UpdateSettings(Guid.Parse(settingUpdate.SiteId), content);
                    }
                }
            }
        }
 
        private void RaiseEvent(SettingEventData message)
        {
            _eventRegistry.Get(EventId).Raise(_raiserId, message);
        }
    }
}
 
[DataContract]
[EventsServiceKnownType]
public class SettingEventData
{
    [DataMember]
    public string SiteId { get; set; }
    [DataMember]
    public string ContentId { get; set; }
}

And that is it. Now, your site settings implementation will be able to handle languages and balanced environments with a simplified version of the original one. If you have any question let me know. I hope it will help someone and as always keep learning !!!

Written by:

Jorge Cardenas

Developer with several years of experience who is passionate about technology and how to solve problems through it.

View All Posts

1 COMMENT

Leave a Reply