SharePoint 2013: how get the managed navigation from CSOM and REST

SharePoint 2013 has introduced the Managed Metadata Navigation.

To get the managed navigation from client side code, an excellent introduction for JSOM is presented in Get Navigation Taxonomy term tree in SharePoint App, where we have the following points:

  1. get the taxonomy session from the current client context
  2. get the current navigation settings, to retrieve the term store id and the term set id of the terms storing the navigation for the current site
  3. load the term set
  4. load the navigation term set, i.e. the term set "casted" as navigation term set, so offering the additional navigational properties (note: the navigation term set doesn't inherit from from the standard term set)

I needed to do the same from C# code, containing CSOM calls, and here are more or less the same steps, even if we need first to convert the claim token to a Windows identity. Here is the CSOM file: SharePointNavigationProvider_CSOM.cs (7.4KB)

        private WindowsIdentity GetWindowsIdentity()
        {
            WindowsIdentity windowsIdentity = null;

            SPSecurity.RunWithElevatedPrivileges(() =>
            {
                ClaimsIdentity identity = (ClaimsIdentity)Thread.CurrentPrincipal.Identity;
                string upnFromClaim = identity.Claims.Where(c => c.ClaimType == ClaimTypes.Upn).First().Value;

                try
                {
// To get the current Windows identity, we need the Claims to Windows Token Service started on all frontends of the SharePoint farm
                    windowsIdentity = S4UClient.UpnLogon(upnFromClaim);
                    if (windowsIdentity == null)
                        throw new ApplicationException("windowsIdentity == null");
                }
                catch (System.ServiceModel.EndpointNotFoundException ex)
                {
                    throw new ApplicationException("The Claims to Windows Token Service is not running on this frontend", ex);
                }
                catch (System.ServiceModel.Security.SecurityAccessDeniedException ex)
                {
                    throw new ApplicationException("Unable to communicate with the domain controller", ex);
                }
            });

            return windowsIdentity;
        }

        private TermSet GetTermSet(ClientContext clientContext)
        {
            TaxonomySession taxonomySession = TaxonomySession.GetTaxonomySession(clientContext);
            StandardNavigationSettings navigationSettings = new WebNavigationSettings(clientContext, clientContext.Web).CurrentNavigation;
            clientContext.Load(navigationSettings,
                navSettings => navSettings.TermStoreId,
                navSettings => navSettings.TermSetId);
            clientContext.ExecuteQuery();

            TermStore termStore = taxonomySession.TermStores.GetById(navigationSettings.TermStoreId);
            TermSet termSet = termStore.GetTermSet(navigationSettings.TermSetId);
            clientContext.Load(termSet);
            return termSet;
        }

        private void GetNavigationTermSet(ClientContext clientContext, TermSet termSet,
            out NavigationTermCollection navTerms, out NavigationTermCollection allNavTerms)
        {
            NavigationTermSet navigationTermSet = NavigationTermSet.GetAsResolvedByWeb(clientContext, termSet, clientContext.Web, "GlobalNavigationTaxonomyProvider");
            clientContext.Load(navigationTermSet);

            navTerms = navigationTermSet.Terms;
            clientContext.Load(navTerms,
                terms => terms.Include(term => term.Id));

            allNavTerms = navigationTermSet.GetAllTerms();
            clientContext.Load(allNavTerms,
                terms => terms.Include(
                    term => term.FriendlyUrlSegment,
                    term => term.HoverText,
                    term => term.Id,
                    term => term.LinkType,
                    term => term.SimpleLinkUrl,
                    term => term.Terms,
                    term => term.Title));

            clientContext.ExecuteQuery();
        }

        List<NavigationNodeInfo> BuildRecursiveNodeList(string url, NavigationTermCollection allNavTerms, NavigationTermCollection currentNavTerms, int maxDepth)
        {
            List<NavigationNodeInfo> Result = new List<NavigationNodeInfo>(currentNavTerms.Count);
            foreach (NavigationTerm navTerm in currentNavTerms)
            {
                NavigationTerm currentTermInAllTerms = allNavTerms.First(term => term.Id == navTerm.Id);
                NavigationNodeInfo navNodeInfo = new NavigationNodeInfo
                {
                    Title = currentTermInAllTerms.Title.Value,
                    Url = currentTermInAllTerms.LinkType == NavigationLinkType.FriendlyUrl ?
                        SPUtility.ConcatUrls(url, currentTermInAllTerms.FriendlyUrlSegment.Value) :
                        currentTermInAllTerms.SimpleLinkUrl,
                    HoverText = currentTermInAllTerms.HoverText
                };

                if (maxDepth > 1 && currentTermInAllTerms.Terms.Any())
                    navNodeInfo.Childs = BuildRecursiveNodeList(url, allNavTerms, currentTermInAllTerms.Terms, maxDepth - 1);
                
                Result.Add(navNodeInfo);
            }
            return Result;
        }


This solution, even if technically correct, has the disadvantage that these multiple CSOM calls are very slow.
An alternative to improve the performances is to use the REST calls (still after having impersonated the calling user). Here is the REST file: SharePointNavigationProvider_REST.cs (5.3KB)

private XDocument GetWebAppNavigationRest(WindowsIdentity windowsIdentity, string url)
{
using (new SPMonitoredScope("SZH.IntraZueri.Core.Providers.Navigation.SharePointNavigationProvider.GetWebAppNavigationRest"))
{
XDocument Result;

// Impersonating the user requires Kerberos in place
using (WindowsImpersonationContext ctx = windowsIdentity.Impersonate())
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(SPUtility.ConcatUrls(url,
"_api/navigation/menustate(mapprovidername='GlobalNavigationTaxonomyProvider')")); //CurrentNavigationSwitchableProvider
request.Method = "GET";
//request.ContentType = "application/json";
request.Credentials = CredentialCache.DefaultNetworkCredentials;

using (new SPMonitoredScope("SZH.IntraZueri.Core.Providers.Navigation.SharePointNavigationProvider.GetWebAppNavigationRest.ServiceInvoke"))
//using (WebResponse webResponse = request.GetResponse())
using (Task<WebResponse> asyncResponse = webReq.GetResponseAsync())
{
asyncResponse.Wait();
using (WebResponse response = asyncResponse.Result)
using (Stream webStream = webResponse.GetResponseStream())
Result = XDocument.Load(webStream);
}

return Result;
}
}
}

private List<NavigationNodeInfo> BuildRecursiveNodeList(XElement parentNode, XNamespace ns, string url, int maxDepth)
{
List<NavigationNodeInfo> Result = new List<NavigationNodeInfo>();

foreach (XElement currElement in parentNode.Elements())
{
// Check isHidden flag. If it's hidden, don't include in result set
string isHiddenStr = currElement.Element(ns + "IsHidden").Value;
bool isHidden;
bool.TryParse(isHiddenStr, out isHidden);
if (isHidden)
continue;

string title = currElement.Element(ns + "Title").Value;
string friendlyUrlSegment = currElement.Element(ns + "FriendlyUrlSegment").Value;
string simpleUrl = currElement.Element(ns + "SimpleUrl").Value;
string linkUrl;
if (!string.IsNullOrEmpty(friendlyUrlSegment))
linkUrl = SPUtility.ConcatUrls(url, friendlyUrlSegment);
else
linkUrl = SPUtility.ConcatUrls(url, simpleUrl);
List<NavigationNodeInfo> childs;
if (maxDepth > 1)
childs = BuildRecursiveNodeList(currElement.Element(ns + "Nodes"), ns, url, maxDepth - 1);
else
childs = new List<NavigationNodeInfo>();

NavigationNodeInfo navNodeInfo = new NavigationNodeInfo
{
Title = title,
Url = linkUrl,
Childs = childs
};
Result.Add(navNodeInfo);
}
return Result;
}

SharePoint: how check if a user has "Replicating Directory Changes" permission

In the User Profile Synchronization job, the user running it must have in Active Directory the permission "Replicating Directory Changes".

By one client, there was a debate between the client itself and one of its supplier offering the AD services about if the user had this permission.

To check this, I have found a PowerShell script doing this test in Checking Replication Directory Changes for account by PowerShell.

For reference I have copied the script here: CheckRDC.ps1 (1.4KB), then you can run it with:

     .\CheckRDC.ps1 “DOMAIN\username”

SharePoint: wrong domain name after user profile synchronization

If there is a NetBIOS domain name, the user profile synchronization in SharePoint will give incorrect results, because the users will be prefixed with the domain name (for example: "AD" in "ad.mycompany.com") instead of the NetBIOS domain name (for example: "MYCOMPANY-AD").

This problem is described well in WRONG DOMAIN NAME IN ACCOUNTNAME AFTER USER PROFILE SYNC.

The solution is to change the property of the User Profile Application to the value 1, and then recreate the connection and do a full synchronization.

This topic is described in the Microsoft Technet article Synchronize user and group profiles in SharePoint Server 2013.

SharePoint 2013: Minimal Download Strategy requirements

To ensure that MDS (Minimal Download Strategy) continues to work after deployment of custom solution, these have to be done following the rules described in Modify SharePoint components for MDS.


In short they are:

  • include your content in SharePoint:AjaxDelta control (probably already done in the master page, but worth to check)
  • include css with SharePoint:CssLink and SharePoint:CssRegistration
  • include JavaScript with SharePoint:ScriptLink
  • include inline JavaScript code in SharePoint:ScriptBlock
  • mark your assembly with
    [assembly: Microsoft.SharePoint.WebControls.MdsCompliantAttribute(IsCompliant = true)]
  • mark your controls and web parts with:
    [Microsoft.SharePoint.WebControls.MdsCompliantAttribute(IsCompliant = true)]

In addition to that:

  • ensure the JavaScript file names don't contain '-' characters, because the file name is used as an identifier by init.js

SharePoint 2013: how create host named site collection in C#

In C# you can create a hosted named site collection in C# using the method SPSiteCollection.Add in the overload reported in the link itself.

The two options (with PowerShell too) are described in Host-Named Site Collections (Provisioning by Code and in Specific Content Database).

Considerations about the rights needed from the user (eventually the app pool user with RunWith ElevatedPrivileges) to create site collection can be found in Custom SharePoint 2007 Site Collection Creation Page.

SharePoint: workflow performance

In SharePoint, there can be multiple factors that can affect workflow performance:

  • having dedicated history list and task lists, for each workflow
  • making sure that in both history lists and task lists there are maximum 2,000 items per folder (eventually create a folder structure);
  • respect the boundary of maximum 8k bytes for metadata storage;
  • keep ULS log file sizes small

Some interesting links are:


SharePoint 2013: design search between farms

The Microsoft article discussing the topic about WAN deployments for SharePoint 2013 is Global architectures for SharePoint Server 2013.

The idea is that each SharePoint farm crawls its content, and then the content index are reused by other farms.

This is described in the blog posts from Steve Peschka Setting Up an oAuth Trust Between Farms in SharePoint 2013 and Getting a Full Result Set from a Remote SharePoint Index in SharePoint 2013.

The same concept, with UI explanation, is in Search Scopes in SharePoint 2013 Using Result Sources.

SharePoint 2013: troubleshooting Popularity Trends

Popularity Trends in SharePoint 2013 replace the Analytics in SharePoint 2010. Now it is part of the search component.

Keep in mind that after you visit a page, the counter are not updated in real-time, as results come after 2 days after the midnight (so in the end you get your results on your third day).

If still you get 0 results, there is an excellent troubleshooting guide: Troubleshooting SharePoint 2013 Web Analytics.

It's worth saying that the data is saved in the SharePoint_ServiceApps_UsageAndHealth database (or equivalent name), view dbo.RequestUsage, to be selected with (NOLOCK) clause.


A script fixing event receivers on page is described in Sharepoint 2013 Analytics Reports empty.

And how get data programmatically: How to get Search Analytics Reports programmatically in SharePoint 2013.