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”