Showing posts with label Client object model. Show all posts
Showing posts with label Client object model. Show all posts

Monday, December 14, 2020

How to read Sharepoint Online sites sharing capabilities via CSOM

Sharepoint Online allows to share your sites with external users. At first administrator should enable external sharing on the tenant (organization) level (see Manage sharing settings). After that you may set external capabilities for each individual site (see Set sharing capabilities of Sharepoint site collection via client object model for how to do that). In provided example SharingCapabilities is enum which has the following values (they are quite self-descriptive so I won’t describe they here):

  • Disabled
  • ExternalUserSharingOnly
  • ExternalUserAndGuestSharing
  • ExistingExternalUserSharingOnly

In order to read sharing capabilities of existing Sharepoint Online sites via CSOM you may use the following example:

string username = ...;
string password = ...;
var adminContext = new ClientContext("https://{tenant}-admin.sharepoint.com");
var secure = new SecureString();
foreach (char c in password)
{
    secure.AppendChar(c);
}
var credentials = new SharePointOnlineCredentials(username, secure);
adminContext.Credentials = credentials;
adminContext.Load(adminContext.Site);
adminContext.ExecuteQuery();

var tenant = new Tenant(adminContext);
var properties = tenant.GetSiteProperties(0, true);
adminContext.Load(properties);
adminContext.ExecuteQuery();
foreach (SiteProperties p in properties)
{
    Console.WriteLine(p.Url + ": " + p.SharingCapability);
}

This example outputs all sites with their SharingCapability properties. If you need to get SharingCapability for single specific site add condition on site url to the last loop. Hope it will help you.

Thursday, November 5, 2020

Call WebExtensions.WebExistsFullUrl method from OfficeDevPnP library for different site collection

Few years ago I wrote about one problem in OfficeDevPnP method WebExtensions.WebExistsFullUrl: shortly when it was called with url which belongs to non-existent site collection from different managed path it returned true. Full description of the problem is available here: How to check does site collection exist by absolute url using CSOM in Sharepoint. Also I posted issue to PnP-Sites-Core in github: WebExtensions.WebExistsFullUrl returns true for non-existent sites from different managed path in Sharepoint 2013 on-premise. I checked this issue with the latest PnP version (which is 3.26.2010 at the moment of writing of this article) and found that this problem is fixed now by MS. I.e. code of WebExtensions.WebExistsFullUrl method is still the same:

public static bool WebExistsFullUrl(ClientRuntimeContext context, string webFullUrl)
{
    bool exists = false;
    try
    {
        using (ClientContext testContext = context.Clone(webFullUrl))
        {
            testContext.Load(testContext.Web, w => w.Title);
            testContext.ExecuteQueryRetry();
            exists = true;
        }
    }
    catch (Exception ex)
    {
        if (IsUnableToAccessSiteException(ex) || IsCannotGetSiteException(ex))
        {
            // Site exists, but you don't have access .. not sure if this is really valid
            // (I guess if checking if URL is already taken, e.g. want to create a new site
            // then this makes sense).
            exists = true;
        }
    }
    return exists;
}

but now when we try to clone ClientContext in this line:

ClientContext testContext = context.Clone(webFullUrl))

and webFullUrl corresponds to non-existent site collection from another managed path (e.g. if context was created from the root site collection http://example.com and we pass webFullUrl = http://example.com/sites/som-non-existent-url) it throws System.Exception (System.Net.WebException) now:

The remote server returned an error: (404) Not Found

while in earlier CSOM version it created ClientContext for the root site in this case. And as result WebExtensions.WebExistsFullUrl returns false as expected. So fortunately issue was fixed and we can call this method for checking existence of different site collections.

Tuesday, June 25, 2019

How to get localized field titles via CSOM in Sharepoint

Some time ago I wrote article which shows how to localize web part titles via CSOM. If you are not familiar with it I recommend to read it before continue as it has useful information about Sharepoint MUI feature in general: Localize web part titles via client object model in Sharepoint. In current post I will show how to get localized field titles via CSOM in Sharepoint. This technique can be used both in on-prem and online versions.

Currently CSOM has Field.TitleResource property and it looks suitable when you want to get localized title of some field. However this is not the case. In order to get localized fields’ titles you still have to use Field.Title property but with little tuning of ClientContext – similar to those which is mentioned in the article above. More specifically you need to specify target language via “Accept-Language” HTTP header associated with ClientContext and then request field titles (of course assuming that fields have these localized titles provisioned. See e.g. Provision multilingual sites with PnP templates to see how to provision multilingual fields’ titles using PnP templates). Here is the code which shows this concept:

string siteUrl = "...";
string clientId = "...";
string clientSecret = "...";
int lcid = ...;
using (var ctx = new OfficeDevPnP.Core.AuthenticationManager().GetAppOnlyAuthenticatedContext(siteUrl, clientId, clientSecret))
{
 ctx.PendingRequest.RequestExecutor.WebRequest.Headers["Accept-Language"] = new CultureInfo(lcid).Name;
 ctx.Load(ctx.Web);
 ctx.Load(ctx.Web.Fields, f => f.Include(c => c.Id, c => c.Title));
 ctx.ExecuteQueryRetry();
 ...
}

As result when you will iterate through site columns retrieved this way they will contain titles localized for language specified with lcid parameter.

Monday, May 28, 2018

Set O365 group classification via Graph API

Some time ago MS announced possibility to specify groups classifications – see e.g. Classifications for Office365 Groups and Microsoft Teams. After you configured classifications on tenant level like shown in mentioned article when you go to Sharepoint app from your Office 365 App launcher (it will lead to https://{tenant}.sharepoint.com/_layouts/15/sharepoint.aspx) and choose “+ Create site” from the header – you will see dropdown list with specified classifications:

2018-05-28_17-14-43

When you will create new site classification will be shown in the header:

2018-05-28_17-16-26

Also if you will get related group via Graph API – classification will be returned with other group’s properties:

GET https://graph.microsoft.com/v1.0/groups/{id}/classification

will return

{
  "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#groups('{id}')/classification",
  "value": "internal"
}

But is it possible to update group’s classification programmatically? If we just try to send PACTH request on https://graph.microsoft.com/v1.0/groups/{id} endpoint we will get the following error:

{
    "error": {
        "code": "Request_BadRequest",
        "message": "Property 'classification' is read-only and cannot be set.",
        "innerError": {
            "request-id": "...",
            "date": "2018-05-28T14:22:55"
        }
    }
}

The answer is however yes it is possible: in order to set classification of O365 group via Graph API you need to use beta endpoint instead of v1.0: https://graph.microsoft.com/beta/groups/{id}. In this case property should be successfully updated.

Note that team site classification is stored in own property Site.Classification. So if you updated group’s classification you may also want to update classification of related site. If you use SiteExtension.SetSiteClassification from OfficeDevPnP both actions will be done automatically.

Friday, April 20, 2018

List Azure AD groups via CSOM in Powershell

Recently I showed how to list Azure AD/O365 groups in Powershell using REST Graph API: see List Azure AD groups via Rest Graph API in Powershell. In this post I will show how to do the same thing using CSOM. It may be interesting especially in part of how to obtain access token based on client id and client secret via CSOM and how it differs from REST approach.

In order to fully understand the code you may also need to check another article: Avoiding StackOverflowException when use assembly binding redirect in PowerShell. It shows how to use C#-based assembly binding redirect in PowerShell (native Powershell assembly binding redirect which is described here Use specific version of Sharepoint client object model in PowerShell via assembly binding redirection causes StackOverflowException when you try to use Graph API).

So here is the script:

param
(
	[Parameter(Mandatory=$true)]
	$Tenant,
	[Parameter(Mandatory=$true)]
	$ClientId,
	[Parameter(Mandatory=$true)]
	$ClientSecret
)

$currentDir = Convert-Path(Get-Location)
$dllCommonDir = resolve-path($currentDir + "\..\Assemblies\Common\")
$pnpCoreDir = resolve-path($currentDir + "\..\packages\SharePointPnPCoreOnline.2.22.1801.0\lib\net45\")
$graphDir = resolve-path($currentDir + "\..\packages\Microsoft.Graph.1.7.0\lib\net45\")
$graphCoreDir = resolve-path($currentDir + "\..\packages\Microsoft.Graph.Core.1.7.0\lib\net45\")
$newtonJsonDir = resolve-path($currentDir + "\..\packages\Newtonsoft.Json.10.0.3\lib\net45\")

$AssemblyNewtonJson = [System.Reflection.Assembly]::LoadFile([System.IO.Path]::Combine($newtonJsonDir, "Newtonsoft.Json.dll"))
$AssemblyGraphCore = [System.Reflection.Assembly]::LoadFile([System.IO.Path]::Combine($graphCoreDir, "Microsoft.Graph.Core.dll"))
$AssemblyGraph = [System.Reflection.Assembly]::LoadFile([System.IO.Path]::Combine($graphDir, "Microsoft.Graph.dll"))
[System.Reflection.Assembly]::LoadFile([System.IO.Path]::Combine($dllCommonDir, "Microsoft.Identity.Client.dll"))
[System.Reflection.Assembly]::LoadFile([System.IO.Path]::Combine($pnpCoreDir, "OfficeDevPnP.Core.dll"))

if (!("Redirector" -as [type]))
{
$source = 
@'
using System;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;

public class Redirector
{
    public readonly string[] ExcludeList;

    public Redirector(string[] ExcludeList = null)
    {
        this.ExcludeList  = ExcludeList;
        this.EventHandler = new ResolveEventHandler(AssemblyResolve);
    }

    public readonly ResolveEventHandler EventHandler;

    protected Assembly AssemblyResolve(object sender, ResolveEventArgs resolveEventArgs)
    {
        //Console.WriteLine("Attempting to resolve: " + resolveEventArgs.Name); // remove this after its verified to work
        foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
        {
            var pattern  = "PublicKeyToken=(.*)$";
            var info     = assembly.GetName();
            var included = ExcludeList == null || !ExcludeList.Contains(resolveEventArgs.Name.Split(',')[0], StringComparer.InvariantCultureIgnoreCase);

            if (included && resolveEventArgs.Name.StartsWith(info.Name, StringComparison.InvariantCultureIgnoreCase))
            {
                if (Regex.IsMatch(info.FullName, pattern))
                {
                    var Matches        = Regex.Matches(info.FullName, pattern);
                    var publicKeyToken = Matches[0].Groups[1];

                    if (resolveEventArgs.Name.EndsWith("PublicKeyToken=" + publicKeyToken, StringComparison.InvariantCultureIgnoreCase))
                    {
                        //Console.WriteLine("Redirecting lib to: " + info.FullName); // remove this after its verified to work
                        return assembly;
                    }
                }
            }
        }

        return null;
    }
}
'@

    $type = Add-Type -TypeDefinition $source -PassThru 
}

try
{
    $redirector = [Redirector]::new($null)
    [System.AppDomain]::CurrentDomain.add_AssemblyResolve($redirector.EventHandler)
}
catch
{
    #.net core uses a different redirect method
    Write-Warning "Unable to register assembly redirect(s). Are you on ARM (.Net Core)?"
}

function GetAccessToken($tenant, $clientId, $clientSecret)
{
	$appCredentials = New-Object Microsoft.Identity.Client.ClientCredential -ArgumentList $clientSecret
	$aadLoginUri = New-Object System.Uri -ArgumentList "https://login.microsoftonline.com/"
	$authorityUri = New-Object System.Uri -ArgumentList $aadLoginUri, $tenant
	$authority = $authorityUri.AbsoluteUri
	$redirectUri = "urn:ietf:wg:oauth:2.0:oob"
	$clientApplication = New-Object Microsoft.Identity.Client.ConfidentialClientApplication($clientId, $authority, $redirectUri, $appCredentials, $null, $null)
	[string[]]$defaultScope = @("https://graph.microsoft.com/.default")
	$authenticationResult = $clientApplication.AcquireTokenForClientAsync($defaultScope).Result
	return $authenticationResult.AccessToken
}

function ListGroups($accessToken)
{
	$existingGroups = [OfficeDevPnP.Core.Framework.Graph.UnifiedGroupsUtility]::ListUnifiedGroups($accessToken, "", "")
	foreach($group in $existingGroups)
	{
		Write-Host $group.DisplayName
	}
}

$accessToken = GetAccessToken $Tenant $ClientId $ClientSecret
ListGroups $accessToken

Thursday, March 1, 2018

How to check does site collection exist by absolute url using CSOM in Sharepoint

Suppose that we need to check whether site collection with specified absolute url exists or not using client side object model. In OfficeDevPnP library there is convenient extension method for ClientContext WebExtensions.WebExistsFullUrl:

        public static bool WebExistsFullUrl(this ClientRuntimeContext context, string webFullUrl)
        {
            bool exists = false;
            try
            {
                using (ClientContext testContext = context.Clone(webFullUrl))
                {
                    testContext.Load(testContext.Web, w => w.Title);
                    testContext.ExecuteQueryRetry();
                    exists = true;
                }
            }
            catch (Exception ex)
            {
                if (IsUnableToAccessSiteException(ex) || IsCannotGetSiteException(ex))
                {
                    exists = true;
                }
            }
            return exists;
}

Unfortunately this method works properly only within single site collection i.e. when client context (which is extended with this extension method) and url to check belong to the same managed path.

Example:
context is created from the root site http://example.com. This site has http://example.com/test sub site. In this case WebExistsFullUrl returns true for http://example.com/test url and false for some non-existent sub site's url like http://example.com/test123. I.e. behavior is correct.

But if context and url to check belong to different managed path then it always returns true.

Example:
context is created from the root site http://example.com and we call WebExistsFullUrl for some url which may belong to other site collection (e.g. which uses different managed path like http://example.com/teams/some-not-real-url. Suppose that at the moment of call we don't know whether this collection exists or not - we want to determine it by WebExistsFullUrl call). In this case WebExistsFullUrl returns true even if site collection with specified url doesn't exists.

When I analyzed the code I found the following. When we call this method with context and url which belong to the same managed path it throws exception like expected. But when it is called with context and url which belong to different managed paths exception is not thrown. Instead context is created for the root site http://example.com and method returns true and caller thinks that site exists. In order to fix this problem for different managed paths I applied the following fix in our local version:

        public static bool WebExistsFullUrl(this ClientRuntimeContext context, string webFullUrl)
        {
            bool exists = false;
            try
            {
                using (ClientContext testContext = context.Clone(webFullUrl))
                {
                    testContext.Load(testContext.Web, w => w.Title, w => w.Url);
                    testContext.ExecuteQueryRetry();
                    exists = (string.Compare(testContext.Web.Url, webFullUrl, true) == 0);
                }
            }
            catch (Exception ex)
            {
                if (IsUnableToAccessSiteException(ex) || IsCannotGetSiteException(ex))
                {
                    // Site exists, but you don't have access .. not sure if this is really valid
                    // (I guess if checking if URL is already taken, e.g. want to create a new site
                    // then this makes sense).
                    exists = true;
                }
            }
            return exists;
}

I.e. before to return true method also checks whether loaded url is the same as url to be checked. If they are different (which happens in scenario with different managed paths) it will return false like it should.

Described behavior was found when root site collection (http://example.com) was host-named site collection, but most probably it will also work same way for regular site collections.

This problem is also submitted to OfficeDevPnP Core issues section on GitHub: WebExtensions.WebExistsFullUrl returns true for non-existent sites from different managed path in Sharepoint 2013 on-premise.

Update 2020-11-05: this problem is fixed now in CSOM. See Call WebExtensions.WebExistsFullUrl method from OfficeDevPnP library for different site collection.

Friday, February 23, 2018

Camlex 4.3 and Camlex.Client 2.3 for Sharepoint are released

I’m glad to announce that today next versions of Camlex open source library have been releases:

  • Camlex 4.3 – for basic Sharepoint server object model
  • Camlex.Client 2.3 – for client-side object model (CSOM)

Camlex is a free library which simplifies creation of CAML queries (both static CAML queries when number of conditions is known at compile time and dynamic CAML queries when number of conditions will be known only in runtime).

In new version support for native float and decimal types was added. I.e. now you may use these types in queries explicitly without need to cast to double which was the only floating type supported before today’s release. And similar to double float and decimal values will be translated to Number type:

string caml = Camlex.Query().Where(x => (float)x["Rate"] == 1.5f).ToString();

will produce:

<Where>
  <Eq>
    <FieldRef Name="Rate" />
    <Value Type="Number">1.5</Value>
  </Eq>
</Where>

And with decimal:

string caml = Camlex.Query().Where(x => (decimal)x["Rate"] == 1.5m).ToString();

output result will be the same.

New packages are also updated in Nuget: they have Validating status now and will be available after some time when Nuget will complete antivirus checks.

And I’m especially glad today because this is the first release since Camlex got new home at GitHub https://github.com/sadomovalex/camlex (it was moved there last year from Codeplex since last one is discontinued now). Development of the library is on going and I will continue to do it more valuable for Sharepoint community in future as well.

Monday, January 29, 2018

Internal details of setting Access request settings for Sharepoint sites

In Sharepoint you may configure Access request settings which will allow users to ask for access to the site from appropriate responsible person (I intentially don’t tell site owner here because recipient of access requests may be different person – see below). In order to do it first of all you need to configure outgoing email settings in Central administration. Access request settings for web site may be configured from Site settings > Site permissions > Access request settings:

When you click this link Sharepoint loads setrqacc.aspx into modal window. If we will check its codebehind class Microsoft.SharePoint.ApplicationPages.SetRequestAccess we will see that it first checks SPWebApplication.RequestAccessEnabled internal property:

In this property it checks another property IsEmailServerSet:

which in turn checks outgoing email server address:

That’s why setting outgoing email settings should be done before to configure Access request settings.

Now let’s check SetRequestAccess.OnLoad method again and see how “Allow access requests” checkbox is initialized with checked/unckecked values. As it is shown above OnLoad method calls ToggleSelectedAndSetObjectType method:

and the last one checks “Allow access requests” checkbox when SPWeb.RequestAccessEnabled property set to true. Let’s check code of SPWeb.RequestAccessEnabled property:

I.e. it is readonly property which returns true only when Access request email is set, i.e. other property SPWeb.RequestAccessEmail. So basically in order to enable Access requests you need to set SPWeb.RequestAccessEmail for your web site.

The problem however is that Sharepoint for some sites sets SPWeb.RequestAccessEmail to default value someone@example.com – see first picture in this post. There is internal method SPWeb.EnableAccessRequestsIfNeeded where it is done:

Here it’s code as it doesn’t fit to post column’s width:

   1:  
   2: private void EnableAccessRequestsIfNeeded()
   3: {
   4:     if (string.IsNullOrWhiteSpace(this.RequestAccessEmail) &&
   5:         this.HasUniqueRoleAssignments && this.WebTemplateId != 3
   6:         && this.WebTemplateId != 16 && this.WebTemplateId != 17 &&
   7:         this.WebTemplateId != 18 && this.Site.WebApplication.RequestAccessEnabled)
   8:     {
   9:         this.RequestAccessEmail = "someone@example.com";
  10:     }
  11: }

So for root webs of new site collections which have HasUniqueRoleAssignments = true and which use web template id except 3, 16, 17, 18 (and of course if SPWebApplication.RequestAccessEnabled is set to true – see above) Sharepoint will set SPWeb.RequestAccessEmail to default value someone@example.com which will mean that Access requests will be enabled there by default. In the following forum post Access Requests recipient by Default it is mentioned that in this case, i.e. when default email address someone@example.com is used, SPWeb.Site.Owner.Email will be used for sending access requests, although I didn’t check it by myself.

And one more thing: if you are working with CSOM note that Web.RequestAccessEmail property is available there only starting with version 16.1.4727.1200 onwards. Basically it means that it is not possible to change Access requests settings by CSOM for Sharepoint 2013 on-premise. It is still possible though via basic server object model.

Monday, November 27, 2017

Problem with slow setting of composed look for Sharepoint site via OfficeDevPnP SetComposedLookByUrl method

In order to set composed look for Sharepoint site we may use SetComposedLookByUrl extension method from OfficeDevPnP library. According to documentation it does the following:

Retrieves the named composed look, overrides with specified palette, font, background and master page, and then recursively sets the specified values.

Here is the code of this method:

   1: public static void SetComposedLookByUrl(this Web web, string lookName,
   2:     string paletteServerRelativeUrl = null, string fontServerRelativeUrl = null,
   3:     string backgroundServerRelativeUrl = null, string masterServerRelativeUrl = null,
   4:     bool resetSubsitesToInherit = false, bool updateRootOnly = true)
   5: {
   6:     var paletteUrl = default(string);
   7:     var fontUrl = default(string);
   8:     var backgroundUrl = default(string);
   9:     var masterUrl = default(string);
  10:  
  11:     if (!string.IsNullOrWhiteSpace(lookName))
  12:     {
  13:         var composedLooksList = web.GetCatalog((int)ListTemplateType.DesignCatalog);
  14:  
  15:         // Check for existing, by name
  16:         CamlQuery query = new CamlQuery();
  17:         query.ViewXml = string.Format(CAML_QUERY_FIND_BY_FILENAME, lookName);
  18:         var existingCollection = composedLooksList.GetItems(query);
  19:         web.Context.Load(existingCollection);
  20:         web.Context.ExecuteQueryRetry();
  21:         var item = existingCollection.FirstOrDefault();
  22:  
  23:         if (item != null)
  24:         {
  25:             var lookPaletteUrl = item["ThemeUrl"] as FieldUrlValue;
  26:             if (lookPaletteUrl != null)
  27:             {
  28:                 paletteUrl = new Uri(lookPaletteUrl.Url).AbsolutePath;
  29:             }
  30:             var lookFontUrl = item["FontSchemeUrl"] as FieldUrlValue;
  31:             if (lookFontUrl != null)
  32:             {
  33:                 fontUrl = new Uri(lookFontUrl.Url).AbsolutePath;
  34:             }
  35:             var lookBackgroundUrl = item["ImageUrl"] as FieldUrlValue;
  36:             if (lookBackgroundUrl != null)
  37:             {
  38:                 backgroundUrl = new Uri(lookBackgroundUrl.Url).AbsolutePath;
  39:             }
  40:             var lookMasterUrl = item["MasterPageUrl"] as FieldUrlValue;
  41:             if (lookMasterUrl != null)
  42:             {
  43:                 masterUrl = new Uri(lookMasterUrl.Url).AbsolutePath;
  44:             }
  45:         }
  46:         else
  47:         {
  48:             Log.Error(Constants.LOGGING_SOURCE,
  49: CoreResources.BrandingExtension_ComposedLookMissing, lookName);
  50:             throw new Exception($"Composed look '{lookName}' can not be found; pass " +
  51: "null or empty to set look directly (not based on an existing entry)");
  52:         }
  53:     }
  54:  
  55:     if (!string.IsNullOrEmpty(paletteServerRelativeUrl))
  56:     {
  57:         paletteUrl = paletteServerRelativeUrl;
  58:     }
  59:     if (!string.IsNullOrEmpty(fontServerRelativeUrl))
  60:     {
  61:         fontUrl = fontServerRelativeUrl;
  62:     }
  63:     if (!string.IsNullOrEmpty(backgroundServerRelativeUrl))
  64:     {
  65:         backgroundUrl = backgroundServerRelativeUrl;
  66:     }
  67:     if (!string.IsNullOrEmpty(masterServerRelativeUrl))
  68:     {
  69:         masterUrl = masterServerRelativeUrl;
  70:     }
  71:  
  72:     //URL decode retrieved url's
  73:     paletteUrl = System.Net.WebUtility.UrlDecode(paletteUrl);
  74:     fontUrl = System.Net.WebUtility.UrlDecode(fontUrl);
  75:     backgroundUrl = System.Net.WebUtility.UrlDecode(backgroundUrl);
  76:     masterUrl = System.Net.WebUtility.UrlDecode(masterUrl);
  77:  
  78:     web.SetMasterPageByUrl(masterUrl, resetSubsitesToInherit, updateRootOnly);
  79:     web.SetCustomMasterPageByUrl(masterUrl, resetSubsitesToInherit, updateRootOnly);
  80:     web.SetThemeByUrl(paletteUrl, fontUrl, backgroundUrl, resetSubsitesToInherit,
  81:         updateRootOnly);
  82:  
  83:     // Update/create the "Current" reference in the composed looks gallery
  84:     string currentLookName = GetLocalizedCurrentValue(web);
  85:     web.CreateComposedLookByUrl(currentLookName, paletteUrl, fontUrl, backgroundUrl,
  86:         masterUrl, displayOrder: 0);
  87: }

The problem is that on the real production environments this method may work very slow (we needed to set composed look on all sub sites, i.e. pass resetSubsitesToInherit = true and updateRootOnly = false). When apply it on the site with many sub sites (about 4000 sub site) it worked almost 15 hours and then was interrupted with network connectivity exception.

Another problematic moment is that method doesn’t show any progress indicator, i.e. you just have to wait watching black screen all that time. At the moment I see only 1 workaround which at least will allow to see progress of the work: iterate through sub sites in own external loop, print url of currently updated web and call SetComposedLookByUrl for each web site separately with resetSubsitesToInherit = false and updateRootOnly = true. If you know other solutions please share them in comments.

Tuesday, October 17, 2017

One way to avoid “Term update failed because of save conflict” error when create managed metadata terms in Sharepoint

In one of the project we used the following PowerShell CSOM code for creating managed metadata navigation terms in Sharepoint Online:

   1: $newTerm = $navTermSet.CreateTerm($web.Title,
   2:     [Microsoft.SharePoint.Client.Publishing.Navigation.NavigationLinkType]::SimpleLink,
   3:     [System.Guid]::NewGuid())
   4: $newTerm.SimpleLinkUrl = $web.ServerRelativeUrl
   5: $termStore.CommitAll()
   6: $ctx.ExecuteQuery()

It worked successfully for many tenants but for one tenant it gave the following error:

Exception calling "ExecuteQuery" with "0" argument(s): "Term update failed because of save conflict."

Error appeared randomly, i.e. for the same sub site some time term has been created successfully and some time it failed with above error. There were no other changes so the reason was not in pending changes.

In order to avoid this error we applied the following workaround:

   1: do
   2: {
   3:     Try
   4:     {
   5:         $newTerm = $navTermSet.CreateTerm($web.Title,
   6:             [Microsoft.SharePoint.Client.Publishing.Navigation.NavigationLinkType]::SimpleLink,
   7:             [System.Guid]::NewGuid())
   8:         $newTerm.SimpleLinkUrl = $web.ServerRelativeUrl
   9:         $termStore.CommitAll()
  10:         $ctx.ExecuteQuery()
  11:         break
  12:     }
  13:     Catch
  14:     {
  15:         Write-Host "Error occured, try one more time" -foregroundcolor yellow
  16:     }
  17: } while ($true)

I.e. instead of single call to CreateTerm and ExecuteQuery we call it in the loop until call will be successful. Log showed that with this approach all navigation terms have been created properly at the end although for some sub sites it failed 1 time, for other 2 and for some even 3 times until it created term successfully, while for most of sub sites there were no errors at all. Hope that this engineering approach will help some one:).