Feature Activation Site Scope

Jul 20, 2011 at 6:49 AM
Edited Jul 20, 2011 at 11:08 AM

Hi,

I want to have a Site Scoped Feature that registers all it's dependencies when the Feature is Activated, not installed

I have created the Site Scoped Feature and added the Event Receiver as follows.

Note i am overwriting the default ActivatingServiceLocatorFactory to use the UnityServiceLocatorFactory from

http://www.erwinvandervalk.net/2010/04/sharepointservicelocator-unity.html

 public override void FeatureActivated(SPFeatureReceiverProperties properties)
        {
            SPSecurity.RunWithElevatedPrivileges(() =>
                {
                    var parentSite = properties.Feature.Parent as SPSite;

                   var serviceLocator = SharePointServiceLocator.GetCurrent(parentSite); // ACCESS DENIED!!
                   var serviceLocatorConfig = serviceLocator.GetInstance<IServiceLocatorConfig>();
                   serviceLocatorConfig.Site = parentSite;

                    serviceLocatorConfig.RemoveTypeMappings<IServiceLocatorFactory>();
                    serviceLocatorConfig.RegisterTypeMapping<IServiceLocatorFactory, UnityServiceLocatorFactory>();

                    //// Common Services Registrations
                    //// ///////////////////////////////////////////////////////////
                    serviceLocatorConfig.RegisterTypeMapping<IMessageCoordinator, MessageCoordinator>();
                    serviceLocatorConfig.RegisterTypeMapping<ILogger, SharePointLogger>();                   
                     //SharePointServiceLocator.Reset();
                });
        }

Using the above method i will get the Access Denied message,

To overcome this i add the following to the Feature Installed Event

public override void FeatureInstalled(SPFeatureReceiverProperties properties)
{
            System.Diagnostics.Debugger.Break();
           
            var serviceLocator = SharePointServiceLocator.GetCurrent();
            var serviceLocatorConfig = serviceLocator.GetInstance<IServiceLocatorConfig>()

;}

It appears that i need to have at least one entry in the Sharepoint Config Database first before i can start registering dependencies in the Feature Activated event, which is created by the FeatureInstalled event above.

Verified by the following:

select * from [Objects] Where Name like '%pnpFarmConfig%'

However when i then activate the feature and make a request to the Current Site ServiceLocator Instance

var result = SharePointServiceLocator.GetCurrent(SPContext.Current.Site);

the result always comes back as the ActivatingServiceLocator instance and NOT my Unity one. It appears that the Site Scoped settings are not being used, but in fact the Farm Level ones created by the initial FeatureInstalled event.

So basically my question is how can i get my Site Scoped Feature to correctly register and use it's settings when ACTIVATED

Thanks for your help.

Developer
Jul 20, 2011 at 4:48 PM
Edited Jul 20, 2011 at 4:54 PM

Please remove the following line from your code and try again.

              serviceLocatorConfig.Site = parentSite;
to be safe, you may replace the above line with

             serviceLocatorConfig.Site = null;
The reason for that is that SharePoint Service Locator Type mapping can be stored either in SPFarm property bag or in SPSite property bag. (see http://msdn.microsoft.com/en-us/library/ff798346.aspx). To set the the type mappings at site collection level, you have to specify typeMappings.Site before you call typeMappings.RegisterTypeMappings(). To set the type mapping at the farm level, you have to make sure that typeMappings.Site == null (which is the case if you never specify that value) before you call typeMappings.RegisterTypeMappings(). (see http://msdn.microsoft.com/en-us/library/ff798451.aspx).

In your case, your are replacing the type mapping at the site level with your own UnityServiceLocatorFactory and later retrieve the typemapping at the farm level which you have never changed. The farm level typemapping still points to the default ActivatingServiceLocator.

Please let me know if this fix your issue.

Jul 20, 2011 at 9:52 PM

In this case i want the config settings stored in the SPSite not the SPFarm.

Doesn't serviceLocatorConfig.Site = parentSite ensure that the settings are stored in SPSite? I make this call before i do any of the RegisterTypeMappings()

And then i am requesting the SharePointServiceLocator.GetCurrent(SPContent.Current.Site) which i expect is requesting the SPSite type mappings and not the SPFarm ones?

Thanks for your help.

Coordinator
Jul 20, 2011 at 10:11 PM

Have you registered the unity container at the farm level?  The service locator type to use must be specified at the farm level, this is where we look for the type mapping when we initially create the service locator.  You can't map this at the site level.  Once this is mapped, you should be able then then to map settings at the site collection level.

We did not test with unity as the container.  It looked like Erwin ran into some problems when he attempted to use this with the site collection settings, although he didn't provide any specifics on what those problems were.

Do you have any more detail on where the security error occurs?  Not sure what is happening, but may be that farm config is being written to.  This will cause a security exception when in the context of a content web (which you will be in the feature activated logic).

Jul 22, 2011 at 1:28 AM

So after debugging the Service Locator code i got the sucker to work..

1) Registered the Unity Container at the Farm Level

  public override void FeatureInstalled(SPFeatureReceiverProperties properties)
        {
            //System.Diagnostics.Debugger.Break();

            var serviceLocator = SharePointServiceLocator.GetCurrent();
            var serviceLocatorConfig = serviceLocator.GetInstance<IServiceLocatorConfig>();
            serviceLocatorConfig.SetSiteCacheInterval(1); // REQUIRED LATER

            serviceLocatorConfig.RemoveTypeMappings<IServiceLocatorFactory>();
            serviceLocatorConfig.RegisterTypeMapping<IServiceLocatorFactory, UnityServiceLocatorFactory>();
        }

2) Registered Type mappings at the Site Level

public override void FeatureActivated(SPFeatureReceiverProperties properties)
        {
            SPSecurity.RunWithElevatedPrivileges(() =>
            {
                var parentSite = properties.Feature.Parent as SPSite;
                var serviceLocator = SharePointServiceLocator.GetCurrent();

                var serviceLocatorConfig = serviceLocator.GetInstance<IServiceLocatorConfig>();
                serviceLocatorConfig.Site = parentSite;
               
                // Common Services Registrations
                // ///////////////////////////////////////////////////////////
                serviceLocatorConfig.RegisterTypeMapping<IMessageCoordinator, MessageCoordinator>();
                serviceLocatorConfig.RegisterTypeMapping<ILogger, SharePointLogger>();
            });
        }

3) Changed ConfigManager.cs

Changed the parameterless constructor to

        [InjectionConstructor]
        public ConfigManager()

So Unity Resolves this as the default constructor. By default Unity will try to resolve the Constructor with the most amount of parameters, this overwrites it to resolve the parameter less one first.

4) Set The Site Cache Interval to 1 

serviceLocatorConfig.SetSiteCacheInterval(1); // REQUIRED LATER

This is required because the first call to

var result = SharePointServiceLocator.GetCurrent(SPContext.Current.Site);

after activating the feature will go through the following code path

protected IServiceLocator DoGetCurrent(SPSite site)
        {
            Validation.ArgumentNotNull(site, "site");

            IServiceLocator locator = null;

            if (siteLocators.ContainsKey(site.ID))
            {
                SiteLocatorEntry entry = siteLocators[site.ID];

                if (DateTime.Now.Subtract(entry.LoadTime).TotalSeconds < SiteCachingTimeoutInSeconds) // WILL CONTAIN THE FARM TYPE MAPPINGS UNTIL THE CACHE TIMES OUT
                {
                    locator = siteLocators[site.ID].locator;
                }
                else
                {
                    locator = RefreshServiceLocatorInstance(site); // WILL REFRESH OUR CUSTOM UNITY LOCATOR WITH THE SITE LEVEL MAPPINGS
                }
            }
            else
            {
                locator = CreateServiceLocatorInstance(site);
            }

            return locator;
        }

5) Update RefreshServiceLocatorInstance

The call to RefreshServiceLocatorInstance needed to be modified to check for LastUpdate == null, since that value was never populated the Refresh of type mappings never took place.

  private IServiceLocator RefreshServiceLocatorInstance(SPSite site)
        {
            var siteserviceLocatorConfig = GetServiceLocatorConfig(site);
            SiteLocatorEntry entry = siteLocators[site.ID];

            //only update if changed since last time we loaded...
            if (siteserviceLocatorConfig.LastUpdate == null || siteserviceLocatorConfig.LastUpdate > entry.LoadTime) **
            {
                if(entry.locator.GetType() == typeof(ActivatingServiceLocator))
                {
                    // get any mappings added programmatically.  Assume this behavior is unique to
                    //Activating service locator, so ignore if not activating.
                    var activatingLocator = (ActivatingServiceLocator)entry.locator;
                    activatingLocator.Refresh(GetDefaultTypeMappings(),
                        FarmLocatorConfig.GetTypeMappings(),
                        entry.SiteMappings,
                        siteserviceLocatorConfig.GetTypeMappings());
                }
                else
                {
                    IServiceLocatorFactory factory = GetServiceLocatorFactory(this.FarmLocatorConfig.GetTypeMappings());
                    SetupCustomLocator(factory, entry.locator, siteserviceLocatorConfig.GetTypeMappings());
                }
                entry.SiteMappings = siteserviceLocatorConfig.GetTypeMappings();
            }

            entry.LoadTime = DateTime.Now;
            return entry.locator;
        }

Seems to be working well now. For the next version can i suggest you look into improving the support for DI frameworks.