Provision SPFx Web Parts to Classic Sites – Part 3: Install SPFx Web Part to SharePoint Site / Web

Today I want to show you my favourite step in this collection of blog posts how you could make provisioning of SPFx Web Parts to Classic SharePoint Sites inside of your WSP solution package:

  1. Include SPFx Assets & Package inside of WSP
  2. Deploy SPFx Web Part to SharePoint Server 2016 App Catalog with WSP
  3. Install SPFx Web Part to SharePoint Site/Web (this blog post)
  4. Include SPFx Web Part inside of Web Template

In previous posts we have already done SPFx Web Parts with custom gulp tasks to copy Assets & Package files automatically from SPFx Projects to Module Item of our WSP solution and deploying of SPFx Web Parts to SharePoint Server 2016 App Catalog with WSP Feature.
So today we continue with problem how to programmatically install SPFx Web Part to specific SharePoint Site or Web or Sub-Web (or any kind of Web 🙂 ) inside of some WSP Feature (so we use SSOM).
SPFx WebParts looks pretty same as SharePoint Apps/Add-ins. They have to be installed in Web-Scope. So we have to make Web Scope Feature.

But firstly, let’s see what options we have in these days for programmatically installing SPFx Web Part into SharePoint Server (On-Prem)? We have no options. [ Link ]
If you try to install your SPFx Web Part with LoadAndInstallApp method you will get error like this: “Value cannot be null. Parameter name: xeAppPermissionRequests.”As I mention before, SPFx Web Parts looks pretty same as SharePoint Apps/Add-ins in their AppManifest.xml structure. So we could modify this Manifest file to look as SharePoint App/Add-in. More about that soon below.

After that we could use LoadAndInstallApp method which works only as installer for SharePoint Apps/Add-ins to specific Web. With that method we could install this SPFx Web Parts faked as SharePoint Apps/Add-ins. The problem is that this method only works on Root Web of specific Site Collection and not in a sub sites (Sub-Webs). In that case you will get error like this: “A different version of this App is already installed with the same version number.”[ Link ]

We could use ALM API but not on SharePoint On-Prem versions. My goal is that I want to install SPFx programatically on SharePoint Server On-Prem, inside of some Web Scope Feature because I want to add this feature in next blog post into the Web Template for SharePoint Web/Site.

So, we could start. I found inside of Microsoft.SharePoint.dll that we have SPApp class with CreateAppUsingPackageMetadata internal method. This method create SPApp object for our SPFx App from App Catalog. After that we have to create App Instance for specific SPWeb with CreateAppInstance method and we have to install it with Install method.

We want to add that code to Web-Scope Feature Event Receiver into the FeatureActivated method. All works great until we activate that feature manually from Manage site features. The problem becomes when we include that Feature into the Web Template – so that means that code will run under the System Account because we have to create Site Collection inside of SharePoint Central Administration. In that case you will get error like this: “The System Account cannot perform this action.”. [ Link ]

You could impersonate other users or use runwithelevatedprivileges but with no effects. If you run code with person which have Site Admin permission you will get same error message. When you run code with person which have less then Site Admin permission you will get error like this: “Only site administrators may install or uninstall Apps.”.
I found inside of SPSecurity static class (inside of Microsoft.SharePoint.dll) that there is internal disposable class SPAppAllowRunAsSystemAccountScope which helps me to sort this problem out.

So, inside of our SPFxHelper helper class, which we made it in previous post, we create new LoadAndInstallSPFxAppPackages method which I want to share with you below:

public static void LoadAndInstallSPFxAppPackages(string dstWebUrl)
{
    //Assembly ass = Assembly.Load("Microsoft.SharePoint");
    Assembly ass = Assembly.LoadFile(@"C:\Program Files\Common Files\microsoft shared\Web Server Extensions\16\ISAPI\Microsoft.SharePoint.dll");
    Type spAppType = ass.GetType("Microsoft.SharePoint.Administration.SPApp");
    MethodInfo method = spAppType.GetMethod("CreateAppUsingPackageMetadata", BindingFlags.NonPublic | BindingFlags.Static);

    Type spSecurityType = ass.GetType("Microsoft.SharePoint.SPSecurity");
    Type spAppAllowRunAsSystemAccountScopeType = spSecurityType.GetNestedType("SPAppAllowRunAsSystemAccountScope", BindingFlags.NonPublic | BindingFlags.Static);
    ConstructorInfo spAppAllowRunASSystemAcountConstructor = spAppAllowRunAsSystemAccountScopeType.GetConstructors()[0];
    MethodInfo spAppAllowRunASSystemAcountDispose = spAppAllowRunAsSystemAccountScopeType.GetMethod("Dispose");

    using (SPSite site = new SPSite("http://mysitecollection/sites/appcatalog"))
    {
        using (SPWeb web = site.RootWeb)
        {
            SPList list = web.FindListByName("AppCatalog");

            foreach (Guid spfxItem in SPFxWebParts)
            {
                SPQuery query = new SPQuery
                {
                    Query = "<Where><Eq><FieldRef Name='AppProductID'/><Value Type='Text'>" + spfxItem.ToString("B") + "</Value></Eq></Where>",
                };

                SPListItem li = list.GetItems(query)[0];

                byte[] fileByteArray = li.File.OpenBinary();

                using (MemoryStream ms = new MemoryStream())
                {
                    ms.Write(fileByteArray, 0, fileByteArray.Length);

                    using (Package package = Package.Open(ms, FileMode.Open, FileAccess.ReadWrite))
                    {
                        Uri manifestUri = new Uri("/AppManifest.xml", UriKind.Relative);
                        Uri partUri = PackUriHelper.CreatePartUri(manifestUri);
                        PackagePart part = package.GetPart(partUri);

                        string content = null;

                        using (Stream partStream = part.GetStream(FileMode.Open, FileAccess.Read))
                        {
                            using (StreamReader reader = new StreamReader(partStream))
                            {
                                content = reader.ReadToEnd();
                                if (content.IndexOf("<StartPage>") == -1)
                                    content = content.Replace("</Title>", "</Title><StartPage>/</StartPage>");
                                if (content.IndexOf("<AppPrincipal>") == -1)
                                    content = content.Replace("</Properties>", "</Properties><AppPrincipal><Internal></Internal></AppPrincipal>");
                            }
                        }

                        using (Stream partStream = part.GetStream(FileMode.Open, FileAccess.Write))
                        {
                            using (StreamWriter writer = new StreamWriter(partStream))
                            {
                                writer.Write(content);
                                writer.Flush();
                            }
                        }
                    }

                    ms.Seek(0, SeekOrigin.Begin);

                    object spAppAllowRunASSystemAcountObject = null;
                    try
                    {
                        spAppAllowRunASSystemAcountObject = spAppAllowRunASSystemAcountConstructor.Invoke(null);

                        using (SPSite dstSite = new SPSite(dstWebUrl))
                        {
                            using (SPWeb dstWeb = dstSite.OpenWeb())
                            {
                                SPApp spApp = null;
                                try
                                {
                                    spApp = (SPApp)method.Invoke(null, new object[] { ms, dstWeb, 2, false, null, null });
                                }
                                catch (Exception ex)
                                {
                                    if (ex.HResult == -2146232828)
                                    {
                                        foreach (SPWeb dstWebTemp in dstSite.AllWebs)
                                        {
                                            var spAppInstances = dstWebTemp.GetAppInstancesByProductId(spfxItem);
                                            if (spAppInstances.Count > 0)
                                            {
                                                spApp = spAppInstances.First().App;
                                                break;
                                            }
                                        }
                                    }
                                }

                                if (spApp != null)
                                {
                                    var appInstanceID = spApp.CreateAppInstance(dstWeb);
                                    var appInstance = dstWeb.GetAppInstanceById(appInstanceID);
                                    appInstance.Install();
                                }
                            }
                        }
                    }
                    catch (Exception _ex)
                    {
                        Logger.ToLog(_ex, "Error SPFxHelper");
                        throw new SPException(_ex.Message);
                    }
                    finally
                    {
                        spAppAllowRunASSystemAcountDispose.Invoke(spAppAllowRunASSystemAcountObject, null);
                    }
                }
            }
        }
    }
}

This method get url of destination SPWeb, where we want to install our SPFx Web Parts.

As you can see I use Reflection in C# because CreateAppUsingPackageMetadata and SPAppAllowRunAsSystemAccountScope are just internal for Microsoft.SharePoint assembly. So I have to load assembly. Because I want to use this code from Central Administration App Pool Account I used full path to assembly.

Then I load CreateAppUsingPackageMetadata method, SPAppAllowRunAsSystemAccountScope constructor and Dispose method for SPAppAllowRunAsSystemAccountScope.

Next line of codes load SPSite and SPWeb object for our App Catalog and open all sppkg packages  into Stream. Because sppkg packages are archive file I use Package class and I take only AppManifest.xml file.
Then I have to modified it to SharePoint App/Add-In “look”. I have to add StartPage and AppPrincipal with Internal element.

Then I run SPAppAllowRunAsSystemAccountScope which set AllowRunAsSystemAccount property to true and I open destination SPWeb, where I want to install SPFx Web Parts. I try to create SPApp from my AppManifest stream. If failed with code “-2146232828 then we have App already installed on one of SPWeb of current Site Collection. In that scenario I search all this SPWebs for that App Instance -> SPApp.
And finally I create new App Instance on destination SPWeb and install it.

At the end I have to set AllowRunAsSystemAccount back to false with Dispose method.

Last but not least I create new Web Scope feature with Feature Event Receiver.

2018-04-26_1052

In FeatureActivated method I simply call that LoadAndInstallSPFxAppPackages from SPFxHelper static class.

public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
    string cWebUrl = (properties.Feature.Parent as SPWeb).Url;
    SPFxHelper.LoadAndInstallSPFxAppPackages(cWebUrl);
}

So thats all. Deploy solution and activate SPFxWebPartsFeature Site Feature inside of your destination Web or Sub-Web. KaBOOM, all apps are installed correctly without any errors.

[ Complete SPFxHelper class on GitHub ]

Previous step -> Deploy SPFx Web Part to SharePoint Server 2016 App Catalog with WSP
Next step -> Include SPFx Web Part inside of Web Template

Cheers!
Gašper Rupnik

{End.}

Advertisements

15 thoughts on “Provision SPFx Web Parts to Classic Sites – Part 3: Install SPFx Web Part to SharePoint Site / Web

Add yours

  1. Thank you for sharing this. We have thousands of site to deploy apps to and this solution has helped.

    We’ve ran into an issue when we need to upgrade an existing app. Have you encountered this and found a solution?

    For example if we update the app on the app catalog and added another webpart to it. How do we get an existing site to get that update? The install method doesn’t work, is there an internal upgrade method in that library?

    1. Dan, no problem, I am pleased you use this solution and it helps you too.

      If you add another webpart to it you have to reactivate SPFxWebPartsFeature feature on destination Web. If you just update your web part code you don’t have to do that but just clear your browser cache.

      Best regards, Gašper

      1. Thanks Gasper. We noticed if we just changed the code no upgrade was needed on the web.

        But in the instance of adding a webpart, you’re saying we need to turn off and on a feature on the web? I havent been able to find feature that corresponds to SPFx on 2016.

      2. Dan, if I understand you correct … if you added additional webpart and you want to have it on your X SPWeb you have to:
        – add Guid of newly added SPFx Web Part to SPFxWebParts Guid Arrays inside of SPFxHelper static class made in Part 2 of this batch of blog posts (https://rasper87.wordpress.com/2018/04/25/provision-spfx-web-parts-to-classic-sites-part-2-deploy-spfx-assets-package-to-sharepoint-server-2016-with-wsp/)
        – upgrade WSP solution in your tenant
        – reactivate (deactivate/activate) SPFxFeature Site Collection Feature inside of your App Catalog which we made it in Part 2 of this batch of blog posts (https://rasper87.wordpress.com/2018/04/25/provision-spfx-web-parts-to-classic-sites-part-2-deploy-spfx-assets-package-to-sharepoint-server-2016-with-wsp/)
        – reactivate SPFxWebPartsFeature Site Feature inside of your X SPWeb which we made it in this blog post

  2. Did you mean the feature of the app itself? For example our SPFx apps show up as separate features on the site they are installed on. I tried to using the web interface deactivate and activate an app that has a higher version / new webpart. It didnt seem to change the version of the app on that site. This is at a subsite level if that matters.

    1. Hello Everton, this is not a problem, you can have as many subsites you want to have (or subsite of subsite) and you can reuse the spfx webparts in those subsites.
      All you need to do is to activate SPFxWebPartsFeature Site Feature on all of those subsites.

      1. Thanks Rasper… So, I only need to create the custom feature SPFxWebPartsFeature (like above) and active the feature in all subsites… But, do I need to use the step 1 (Include SPFx Assets & Package inside of WSP)? Thank you!!!

      2. Yes, you need to create SPFxWebPartsFeature and activate it on all subsites and yes, you have to use all steps from 1 to this part 3 -> you could skip only part 4 which is upgrade of part 3 with additional stuff -> web tempate.
        No problem Everton, I am pleased if I save some of your problems with this solution. 😉

  3. Hi Gasper… We have been successful running your code in a DEV environment(Single Server Farm). However when we run the same code in a TEST environment(Multiple Server Farm) we still get a similar error mentioned in your post:
    Value cannot be null.
    Parameter name: xeAppPermissionRequests : at Microsoft.SharePoint.SPAppPrincipalPermissionsManager.GetOrCreateAppPrincipal(SPWeb web, XElement xeAppPermissionRequests, XElement xeAppPrincipal, String appTitle)
    at Microsoft.SharePoint.Administration.SPAppInstance.EnsureOAuthAppId(SPWeb web)
    at Microsoft.SharePoint.Administration.SPAppInstance.InstallCore(SPWeb web)
    at Microsoft.SharePoint.Administration.SPAppInstance.Install(Boolean administratorOperationMode)
    at Sierra.SharePoint.Library.SSO.SPUtility.LoadAndInstallSPFxAppPackages(String dstWebUrl, AppSettings settings)
    The error occurs in the line
    appInstance.Install();
    When using a System Account it prevents me from doing so thus I had to use a non-system account. What is the minimum permission requirement for this code to run? Any other ideas why I’m getting this error?
    Thanks so much.

  4. Hello,
    i receive allows in this ligne “spApp = (SPApp)method.Invoke(null, new object[] { ms, dstWeb, 2, false, null, null });”
    + InnerException {“Stream was not readable.\r\nParameter name: stream”} System.Exception {System.ArgumentException}
    and spAppInstances.Count is 0
    Help Please

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 )

Google photo

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

Twitter picture

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

Facebook photo

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

Connecting to %s

Powered by WordPress.com.

Up ↑

%d bloggers like this: