Attaching ContentProviders programmaticly

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

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s