The problem
I experienced this problem also with EPiServer PageProviders in EPiServer 6, and it still exists in EPiServer 7. Well, it’s only a problem when you want Editors/Admins to be able to add ContentProviders "on the fly", and on a load balanced environment it poses even more of a problem, as configs are not usually replicated between environments.
What I needed
I needed Editors/Admins to be able to add a ContentProvider whenever and wherever they wanted. Imagine a big container with pages that need to be reused on several sites e.g.
I also needed the provider not to be configured in the episerver.config, because of load balancing considerations.
The solution
I decided to use the Dynamic Data Store to store the settings.
I created an admin module to add and remove the providers.
The code
In the SDK you can read more about configuring ContentProviders. The complete code is on GitHub.
I used the ClonedContentProvider from Alloy for this, as it’s quite a useful one, I think anyway. But you can use this technique on every provider you want of course.
Let’s start with the, what I have named, settings repository. It’s a singleton that handles the requests to the Data Store, e.g. Get, Delete
public Collection<ClonedContentProviderSettings> GetAll() { return new Collection<ClonedContentProviderSettings>(Store.Items<ClonedContentProviderSettings>().ToList()); }
It uses a class called ClonedContentProviderSettings that holds the settings for the attached providers.
public string CategoryList { get; set; } public int EntryPoint { get; set; } public string Name { get; set; } public int Root { get; set; }
The Initialization Module loads the providers from the store on startup and adds them to the ProviderMap.
private static bool LoadProviders() { IContentProviderManager providerManager = ServiceLocator.Current.GetInstance<IContentProviderManager>(); Collection<ClonedContentProviderSettings> providerCollection = SettingsRepository.Instance.GetAll(); foreach (ClonedContentProviderSettings providerSettings in providerCollection) { try { ContentProvider contentProvider = providerManager.GetProvider(providerSettings.Name); if (contentProvider != null) { continue; } CategoryList categoryList = new CategoryList( providerSettings.CategoryList.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) .Select(int.Parse) .ToArray()); ClonedContentProvider provider = new ClonedContentProvider( providerSettings.Name, new PageReference(providerSettings.Root), new PageReference(providerSettings.EntryPoint), categoryList); providerManager.ProviderMap.AddProvider(provider); } catch (ArgumentNullException) { return false; } catch (ArgumentException) { return false; } catch (NotSupportedException) { return false; } } return true; }
Two things about the initialization:
The providers can only be attached after the initialization is complete.
context.InitComplete += this.InitComplete;
When the cache is updated on a remote machine, in a load balanced environment, the providers should be reloaded.
Event removeFromCacheEvent = Event.Get(CacheManager.RemoveFromCacheEventId); removeFromCacheEvent.Raised += RemoveFromCacheEventRaised; private static void RemoveFromCacheEventRaised(object sender, EventNotificationEventArgs e) { // We don't want to process events raised on this machine so we will check the raiser id. if (e.RaiserId != CacheManager.LocalCacheManagerRaiserId) { LoadProviders(); } }
The Administration module, which is an embedded aspx, adds/deletes providers to/from the store and ProviderMap
string name = string.Format(CultureInfo.InvariantCulture, "{0}-ClonedContent-{1}-{2}", providerName, root, entryPoint); IQueryable<ClonedContentProviderSettings> providerCollection = SettingsRepository.Instance.GetAll().AsQueryable(); if (providerCollection.Count(pc => pc.EntryPoint.Equals(entryPoint)) > 0) { // A provider is already attached to this node. throw new InvalidOperationException("Content provider for this node already attached."); } if (providerCollection.Count(pc => pc.Name.Equals(name)) > 0) { // A provider with the same name already exists. throw new InvalidOperationException("Content provider with same name already attached."); } CategoryList categories = categoryList ?? new CategoryList(); ClonedContentProvider provider = new ClonedContentProvider( name, new PageReference(root), new PageReference(entryPoint), categories); IContentProviderManager providerManager = ServiceLocator.Current.GetInstance<IContentProviderManager>(); ClonedContentProviderSettings contentProviderSettings = new ClonedContentProviderSettings { Name = name, EntryPoint = entryPoint, Root = root, CategoryList = string.Join( ",", categories) }; providerManager.ProviderMap.AddProvider(provider); SettingsRepository.Instance.SaveSettings(contentProviderSettings); CacheManager.Clear();
IContentProviderManager providerManager = ServiceLocator.Current.GetInstance<IContentProviderManager>(); ContentProvider pageProvider = providerManager.ProviderMap.GetProvider(providerName); try { if (pageProvider != null) { providerManager.ProviderMap.RemoveProvider(providerName); } ClonedContentProviderSettings providerSetting = SettingsRepository.Instance.Get(providerName); if (providerSetting != null) { SettingsRepository.Instance.Delete(providerSetting); } CacheManager.Clear(); } catch (Exception exception) { Logger.Error(exception.Message, exception); throw new InvalidOperationException(exception.Message); }
That’s basically it.
Requirements
- EPiServer 7
- Net 4.0
- log4net
Deploy
- Just compile the project
- Drop the dll in the bin
One thought on “Attaching ContentProviders programmaticly”