Tuesday, May 19, 2020

Fix javascript heap out of memory error when running local SPFx web server via gulp

If you develop SPFx web part for Sharepoint and run local web server on dev env:

gulp serve –nobrowser

then you may face with famous “JaavaScript heap out of memory” error:

In order to avoid this error we need to increase nodejs heap size. On Windows it can be done by the following method:

  1. Run the following command in your terminal client:
    setx -m NODE_OPTIONS "--max-old-space-size=8192"

    (after that NOTE_OPTIONS should be added to PC > Advanced properties > Environment variables)
  2. Restart terminal session which is used for running gulp to ensure that new env variables will be applied.
  3. Run web server:
         gulp serve --nobrowser

After that your solution should be built without heap out of memory error.

Monday, May 18, 2020

Query limits in Azure Table storage API

As you probably know Table API used by Azure table storage is limited by the following operators which can be used in queries:

  • =
  • <>
  • >
  • <
  • <=
  • >=

We needed to run analogue of NOT IN Sql operator against RowKey column:

RowKey NOT IN (id1, id2, …, id_n)

where these ids were guids represented by strings. In order to do that with mentioned operators we needed to use not equal operator (<>) and construct query like that:

RowKey <> id1 AND RowKey <> id2 AND … RowKey <> id_n

The problem is that this list of guids didn’t have fixed size and could be potentially big. So it was interesting is there limit for max query length and if yes – what is exact value of this maximum query length.

For testing I created test app and generated conditions over guids like that (in addition to mentioned conditions on RowKey we needed to get groups from particular partition only – so this extra condition was added there as well. Exact partition name is not important – we only need to know that its length was 8 symbols – it will be used for overall query length calculation below):

var tableClient = storageAccount.CreateCloudTableClient(new TableClientConfiguration());

var table = tableClient.GetTableReference("MyTable");
var queryConditions = new List();
queryConditions.Add(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, "Test1234"));
int numOfConditions = 50;
for (int i = 0; i < numOfConditions; i++)
 queryConditions.Add(TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.NotEqual, Guid.NewGuid().ToString().ToLower()));
string filter = CombineUsingAnd(queryConditions);
var query = new TableQuery().Where(filter);

var watch = System.Diagnostics.Stopwatch.StartNew();
var result = table.ExecuteQuery(query).ToList();

public static string CombineUsingAnd(List queryConditions)
 if (queryConditions == null || queryConditions.Count == 0)
  return string.Empty;
 string filter = string.Empty;
 foreach (string queryCondition in queryConditions)
  if (string.IsNullOrEmpty(queryCondition))

  if (string.IsNullOrEmpty(filter))
   filter = queryCondition;
   filter = TableQuery.CombineFilters(filter, TableOperators.And, queryCondition);

 return filter;

E.g. here how query looks like with condition on 50 guids:

((((((((((((((((((((((((((((((((((((((((((((((((((PartitionKey eq 'Teamwork') and (RowKey ne '60712ffe-061b-4a46-8402-01381177c4da')) and (RowKey ne '85694313-2776-485c-833d-ae78c185adba')) and (RowKey ne 'dac6f7fd-1198-486c-b3ea-e90337ee9711')) and (RowKey ne '5faf85cb-4662-4843-87f6-ac6dc20b04e1')) and (RowKey ne '8f524940-9822-46e1-b961-59a2e8fa51b4')) and (RowKey ne 'bacd765e-64d3-464a-a57e-bb30eeca0c2f')) and (RowKey ne '75ba6126-f529-4b30-ae41-882f2b223bff')) and (RowKey ne '5e9950ac-e611-4d58-8c91-8fcf16f2886a')) and (RowKey ne '8f08bd7b-7d6e-4c21-8b94-d5ca2a15f849')) and (RowKey ne 'a965f6f6-f03c-4b4a-91be-98481a35ba11')) and (RowKey ne '5858de75-addb-4f5a-acc1-6f9f55078b4f')) and (RowKey ne '9ada1d04-dd03-4b1f-ae26-bc51b7f35ec3')) and (RowKey ne 'cbb976e7-87c9-4b83-8007-355f3ce1061d')) and (RowKey ne '59e06da8-f343-4a98-9382-0bbf1ce21d3f')) and (RowKey ne 'ea0763d9-cb64-4750-a7e0-c1869ca82a15')) and (RowKey ne '926106f9-bf64-42db-b27f-be9bcb45907e')) and (RowKey ne '3cdcaf1e-ff36-4243-b865-9424e1f46427')) and (RowKey ne '8fcfc4ca-7510-4fa6-95d7-3ff2999de509')) and (RowKey ne '21b04d3a-32e0-4a59-900a-30babfa8fbd2')) and (RowKey ne 'df181ee8-4dc0-4232-b2bc-7e13dc8f2abf')) and (RowKey ne '6a0038d0-40a4-4a11-b626-9850b8178caa')) and (RowKey ne 'ffb78a96-85c9-4648-aba7-3baa56be6c1f')) and (RowKey ne '0c56afd8-164e-4574-9054-8824e6fc1969')) and (RowKey ne '51d95602-cfee-40bf-b9e3-b0f935083742')) and (RowKey ne 'f286d1fb-b9bb-4850-a911-58a77ffc7991')) and (RowKey ne '50280089-e61b-4874-8f99-8dc437f74181')) and (RowKey ne '05f72847-3936-4efc-9902-540a9d9c5ca8')) and (RowKey ne '066fcbe4-afee-46ff-b6d2-18e072f850a0')) and (RowKey ne '602f7cd4-7697-49ca-af51-e96c70eefed5')) and (RowKey ne '6858f832-a58e-4bcb-b57b-7c40d32cbcb6')) and (RowKey ne 'f9e2cc4f-a579-46f4-820b-e7d8ab9711c8')) and (RowKey ne 'cbb68c38-7e73-463b-8839-bef040803c37')) and (RowKey ne '1bf0d7ff-ece6-4ef1-9d48-ae424feea108')) and (RowKey ne 'de1dbdbf-8a75-40b4-bb9b-81e95ff4a542')) and (RowKey ne '31f5c94e-3eea-4c5a-8e60-8899eddc7829')) and (RowKey ne '94bc24a8-28e4-4b6d-965e-9360104bff21')) and (RowKey ne '93d7f84e-eb4b-4c7d-826b-196e8b46255e')) and (RowKey ne 'bd0d0989-7256-4ffc-8904-6135955b8aae')) and (RowKey ne '9c38865a-913c-468c-bd26-25e606f9b73a')) and (RowKey ne 'c2f9b899-76c1-4b04-aa68-fb3f903d81d4')) and (RowKey ne '9b478051-78b2-4a67-a25f-6cc09251ec33')) and (RowKey ne '908f80ea-e31f-4a85-9ff8-8cb80b601434')) and (RowKey ne '7775f8d4-2b9c-4903-a191-b54d214e380a')) and (RowKey ne '55b24a87-3259-4ca4-8869-f6d68f9c95c3')) and (RowKey ne '0476263d-cf8a-49b3-89f1-0bc8df9fd906')) and (RowKey ne 'f7a45c30-eba7-4944-b08b-a875d4b871e0')) and (RowKey ne '4136dc3c-10d3-4d58-afb9-23d8b422822e')) and (RowKey ne 'b6a40f8e-5284-4cbd-9356-2296df415650')) and (RowKey ne 'd63ecb62-5f48-4314-8a15-08c0b62ee835')) and (RowKey ne 'e3acdc4f-70a5-4a73-b023-1845ccf9ea51')

Testing results are summarized in the following table:

Num of conditions Filter length Query elpsed time
50 2876 12075
100 5726 10550
112 6410 10361
113 6467 10967
114 6524 Bad request
> 114   Bad request

So Table storage query maximum length is between 6467 and 6524 symbols. For us this knowledge was enough to proceed. Hope that this information will help someone.

Tuesday, May 12, 2020

How to run handler on ribbon open event and access ribbon elements via javascript in Sharepoint

Sometimes we need to run code which will be executed when Sharepoint ribbon is opened. In many cases ribbon is closed by default and you can’t access its elements in regular document.ready handler because ribbon elements are not created in the DOM yet. In this case you need to subscribe somehow on ribbon.open even and access elements there. This is how it can be done:

ExecuteOrDelayUntilScriptLoaded(function() {
  console.log("Ribbon is opened");
}, "sp.ribbon.js");

Interesting that even inside this handler you can’t access elements via jQuery selector because it doesn’t find these dynamically added elements. So e.g. the following code which tries to access New document tab in doclib will return 0:


In order to access these dynamically added ribbon elements we need to use pure javascript call document.getElementById. Here is the full code:

ExecuteOrDelayUntilScriptLoaded(function() {
  var el = document.getElementById("Ribbon.Documents.New");
  if (el) {
   console.log("New document tab is found");
}, "sp.ribbon.js");

Using this approach you will be able to access and manipulate ribbon elements via javascript.

Monday, April 20, 2020

Create Azure Notification Hub with configured Apple APNS and Google FCM using PowerShell and ARM templates

If you have Azure notification hub with configured Apple APNS and Google FCM and try to export ARM template of this notification hub you will find that APNS and FCM configuration won’t be included to this template. In order to add APNS and FCM you need to add the following properties to ARM template:

  • GcmCredential for FCM
  • ApnsCredential for APNS

If you use Token authentication type and Production endpoint for Apple APNS template will look like this:

    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "",
    "parameters": {
        "hubName": {
            "type": "string"
        "namespaceName": {
            "type": "string"
        "googleApiKey": {
            "type": "string"
        "appId": {
            "type": "string"
        "appName": {
            "type": "string"
        "keyId": {
            "type": "string"
        "token": {
            "type": "string"
    "variables": {},
    "resources": [
            "type": "Microsoft.NotificationHubs/namespaces/notificationHubs",
            "apiVersion": "2017-04-01",
            "name": "[concat(parameters('namespaceName'), '/', parameters('hubName'))]",
            "location": "North Europe",
            "properties": {
                "authorizationRules": [],
    "GcmCredential": {
     "properties": {
     "googleApiKey": "[parameters('googleApiKey')]",
     "gcmEndpoint": "https://android.googleapis.com/gcm/send"
    "ApnsCredential": {
     "properties": {
      "appId": "[parameters('appId')]",
      "appName": "[parameters('appName')]",
      "keyId": "[parameters('keyId')]",
      "token": "[parameters('token')]",
      "endpoint": "https://api.push.apple.com:443/3/device"
            "type": "Microsoft.NotificationHubs/namespaces/notificationHubs/authorizationRules",
            "apiVersion": "2017-04-01",
            "name": "[concat(parameters('namespaceName'), '/', parameters('hubName'), '/DefaultFullSharedAccessSignature')]",
            "dependsOn": [
                "[resourceId('Microsoft.NotificationHubs/namespaces/notificationHubs', parameters('namespaceName'), parameters('hubName'))]"
            "properties": {
                "rights": [
            "type": "Microsoft.NotificationHubs/namespaces/notificationHubs/authorizationRules",
            "apiVersion": "2017-04-01",
            "name": "[concat(parameters('namespaceName'), '/', parameters('hubName'), '/DefaultListenSharedAccessSignature')]",
            "dependsOn": [
                "[resourceId('Microsoft.NotificationHubs/namespaces/notificationHubs', parameters('namespaceName'), parameters('hubName'))]"
            "properties": {
                "rights": [

In order to create new notification hub using this ARM template we need to call New-AzResourceGroupDeployment cmdlet and provide params specified in parameters section of the template:

$resourceGroupName = ...
$hubNamespaceName = ...
$hubName = ...
$googleApiKey = ...
$appId = ...
$appName = ...
$keyId = ...
$token = ...

New-AzResourceGroupDeployment -ResourceGroupName $resourceGroupName -TemplateFile ./template.json -hubName $hubName -namespaceName $hubNamespaceName -googleApiKey $googleApiKey -appId $appId -appName $appName -keyId $keyId -token $token

It will create new Azure notification hub with specified name in specified namespace with configured Apple APNS (Token authentication type) and Googke FCM.

Thursday, April 16, 2020

Fix ResourceUnavailable error when try to install PowerShell module via Install-Module cmdlet

If you try to install PowerShell module via Install-Module cmdlet, e.g.:

Install-Module -Name SharePointPnPPowerShellOnline

you may get the following error:

WARNING: Source Location 'https://www.powershellgallery.com/api/v2/package/SharePointPnPPowerShellOnline/3.20.2004' is not valid.
PackageManagement\Install-Package : Package 'SharePointPnPPowerShellOnline' failed to download.
     + CategoryInfo          : ResourceUnavailable: (C:\Users\Develo...20.2004.0.nupkg:String) [Install-Package], Exception
     + FullyQualifiedErrorId : PackageFailedInstallOrDownload,Microsoft.PowerShell.PackageManagement.Cmdlets.InstallPackage

In order to fix it run the following command in your PowerShell session:

[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

After that run Install-Module again – error should disappear.

Sunday, April 5, 2020

Fix problem “Security token service cannot be activated” in Sharepoint farm after inplace upgrade Windows Server 2012 to R2

If you performed inplace upgrade of Windows Server 2012 to Windows Server 2012 R2 with Sharepoint Server running you may face with the following error after upgrade will be completed: when you will try to open any Sharepoint web application the following exception will be shown:

WebHost failed to process a request.
  Sender Information: System.ServiceModel.ServiceHostingEnvironment+HostingManager/12547953
  Exception: System.ServiceModel.ServiceActivationException: The service '/SecurityTokenServiceApplication/securitytoken.svc' cannot be activated due to an exception during compilation.  The exception message is: Exception has been thrown by the target of an invocation.. ---> System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.ArgumentNullException: Value cannot be null.
Parameter name: certificate
    at System.IdentityModel.Tokens.X509SecurityToken..ctor(X509Certificate2 certificate, String id, Boolean clone, Boolean disposable)
    at System.IdentityModel.Tokens.X509SecurityToken..ctor(X509Certificate2 certificate)

The error says that certificate for Secure token service is not specified. In order to fix this error you need to replace certificate for STS:

  1. Open IIS manager > Server certificates > Create Self-Signed Certificate
  2. After that export created certificate to local folder:

Next run the following PowerShell script which will update certificate for STS:

$pfxPath = "path to pfx"
$pfxPass = "certificate password"
$stsCertificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $pfxPath, $pfxPass, 20
Set-SPSecurityTokenServiceConfig -ImportSigningCertificate $stsCertificate
certutil -addstore -enterprise -f -v root $stsCertificate
net stop SPTimerV4
net start SPTimerV4

After that open Sharepoint web app again.

Host React web app on Azure

In this article I will show how to host React web app on Azure cloud platform. We will create new test React web app from scratch, will build it, then will create in Azure new Resource group and App service and after that will transfer our React web app there. Before to start ensure that you have working Azure account with valid subscription (you may create free subscription for 1 month to try things out).

First of all we need to create React app. Before to do that you need to install nodejs on your PC. By default it will also install popular npm package manager and will add its folders to PATH env variable. When it is done run the following command in your working folder:

npx create-react-app test-react-app

It will create test-react-app folder and new test React app inside it. In order to test it on local dev web server run the following command:

npm start

It will launch web server and will open default browser with React app running on http://localhost:3000:

In order to host our React app on Azure we need to build it first. In order to do that run the following command:

npm run build

It will create ready for deployment build subfolder and will copy web app files there.

Once React app is ready we need to configure Azure for hosting it. Go to Azure portal https://portal.azure.com > Resource groups > Create resource group. On the opened window give name for the new resource group and select closer location:

When new resource group will be created choose New > Web App:

On the opened Window choose created resource group, give name for new web app, set the following parameters:
Publish = Code
Runtime stack = Node 10.14
Operating system = Windows
also select region (usually the same region which was used for creating resource group) and App service plan name. As we create new Web app for testing purposes we will use Free app service plan: under Sku and size click Change size and change App service plan to Dev/Test > F1 free:

and click Create. After that go to created App service > Deployment center > FTP > Dashboard > App credentials and copy FTP host name, username and password:

Use your favorite FTP client in order to connect to Azure app service with credentials copied on previous step. When connection will be established copy files from local /test-react-app/build folder to FTP /site/wwwroot folder. When it will be done try to open https://{name-of-your-web-app}.azurewebsites.net in web browser (instead of {name-of-your-web-app} use name of Azure web app which was used during it’s creation). If everything was done correctly you will see your React web app hosted in Azure:

Thursday, April 2, 2020

Problem with fetching photos of private O365 groups

As you probably know in O365 we may create groups which has one of the following visibilities:

  • Public
  • Private

Everybody in your organization may join/leave public groups while for private groups only owners of this group may add you to the group. In this article I will describe one problem related with fetching private group photos via Graph (see also my previous article where I mentioned another problem related with groups images: Why you should be careful with /groups/{id}/photo and /users/{id}/photo endpoints in MS Graph or unintentional getting photos of big sizes in Graph).

In order to fetch group photo the following Graph endpoint should be used:


First of all we need to mention that this endpoint is available only via user delegated permissions (it doesn’t work with app-only permissions). If we will try to fetch photo of some group in Graph explorer using service account which is not member of this group we will get 404 Not found error:

After we will add the same service account to members of the same group image will be retrieved successfully:

Even if we will try to fetch photos of private group under global admin account which is not member of this group – we will still get 404 Not found. So the only way to fetch photo of the private group is to add user account to members or owners of this group. Be aware of this problem when will plan groups images fetching functionality.

Wednesday, April 1, 2020

One problem with AllowToAddGuests and AllowGuestsToAccessGroups O365 groups tenant settings

As you probably know AllowToAddGuests and AllowGuestsToAccessGroups tenant settings determine whether or not external users are able to access O365 groups in your tenant. You may view or change them via PowerShell:


Which will show something like that:

If you will try to update them then you may face with different behavior on different tenants. E.g. if we will try to change them to true like this:

$groupsConfig = Get-AzureADDirectorySetting -Id {settingId}
$groupsConfig["AllowToAddGuests"] = $true
$groupsConfig["AllowGuestsToAccessGroups"] = $true
Set-AzureADDirectorySetting -Id {settingId} -DirectorySetting $groupsConfig

we may get the following result:

Note that in script we used lowercase $true while in result we got True with capital T. But if you will try to run the same script on another tenant it may save values using the same letters registry as used in script. I.e. $true will be saved as true and $True will be saved as True. And if software uses case-sensitive comparison it will cause problems. So be aware of this problem – hope it will be help someone.

Wednesday, March 18, 2020

How to provision modern Sharepoint page with custom SPFx web part via PnP template

If you developed custom SPFx web part and tested it on dev env you will most probably want to automate it’s provisioning to customers environments. In this article I will show how to do that via PnP template.

When you need to create PnP template the simplest option is to export it form existing site and copy those components which you need. This is much faster than trying to remember whole PnP schema.

So first of all we need to build sppkg in release mode (with “--ship” parameter) and upload it to App catalog:

gulp clean
gulp bundle --ship
gulp package-solution --ship

After that add your web part to some modern page – we will use test.aspx for example here. Now you have modern site running with your custom web part.

Next step is to export PnP template from your site:

Get-PnPProvisioningTemplate -Out template.xml

When export will be done edit template.xml and copy section pnp:ClientSidePages which may look like this (of course if you have several SPFx web parts there will be several pnp:CanvasControl instances):

 <pnp:ClientSidePage PageName="test.aspx" EnableComments="false" Publish="true">
   <pnp:CanvasControl WebPartType="Custom" JsonControlData="..." ControlId="..." Order="1" Column="1" />

and paste it to PnP template which you are going to use for provisioning on customers environments. When this template will be applied to the site it will have modern page with your custom SPFx web part.

Monday, March 16, 2020

Move Mercurial repository to Git on Bitbucket

As you probably know Bitbucket will discontinue Mercurial support – all Mercurial repositories will be removed at June 1, 2020. So if you have Mercurial repositories there it is good to take care about them in advance and move them to Git which becomes basic source control in Bitbucket. Articles which I found had lack of some important information so it still took time to go through them. So I decided to write separate post and summarize all steps which are needed for moving Mercurial repositories to Git on Windows 10 PC:

1. Install latest version of TortoiseHG (older versions may not have HgGit plugin which will be needed below)
2. Install Git for Windows 3. Rename repository and make hg clone from new address
4. In HR repository folder enable HgGit plugin by adding following section to .hg/.hgrc file:
5. Go to "C:\Program Files\Git\usr\bin" and run ssh-keygen.exe
Use default settings - it will add 2 files to C:\Users\{username}\.ssh: id_rsa and id_rsa.pub
6. Copy content of id_rsa.pub file
7. Login to bitbucket.org > Profile page > Settings > Security > SSH Keys > Add key > Insert content of copied id_rsa.pub
8. Go to hg repositoy folder:
8.1. in .hg/.hgrc add following line under [ui]:
ssh = "ssh.exe"
8.2. ensure that path "C:\Program Files\Git\usr\bin" (which is path to ssh.exe) is added to PATH environment variable 8.3 run the following command:
hg push git+ssh://{username}@bitbucket.org/{username}/{repository}.git
(git repository url can be copied from bitbucket and then replace https by git+ssh)

After that your repository should be available on Git with all version history and branches you had in Mercurial.

Thursday, March 12, 2020

Disable redirection from user details page UserDisp.aspx to MySite Person.aspx in Sharepoint

As you probably know Sharepoint has built in Created By and Modified By fields which are automatically added to all list items and documents which added to Sharepoint. Under the hood it is done by defining these fields in base content type derived by all other content types. If you add these fields to list views they will be clickable – it will be possible to click on user name and go to user details page UserDisp.aspx which will show user’s attributes (synced with AD if you use Windows authentication).

However in some cases you may notice that instead of UserDisp.aspx Sharepoint redirects to user profile page Person.aspx in MySites web application. If you are not planning to use user profiles and MySites in your Sharepoint site then this behavior may be unwanted and it may be needed to disable this automatic redirection.

Technically redirect is made by MySiteRedirection.ascx user control which is added by MySite farm scope feature (FeatureId = 69cc9662-d373-47fc-9449-f18d11ff732c) which has the following elements.xml file:

<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
    <Control Id="ProfileRedirection" Sequence="100" ControlSrc="~/_controltemplates/mysiteredirection.ascx"/>
    <Control Id="MobileSiteNavigationLink1" Sequence="100"
        ControlAssembly="Microsoft.SharePoint.Portal, Version=, Culture=neutral, PublicKeyToken=71e9bce111e9429c">

In order to disable redirection we need to deactivate this feature:

Disable-SPFeature -Id 69cc9662-d373-47fc-9449-f18d11ff732c

After that after clicking user names in Created By/Modified By columns (and all other clickable columns of User or group type) user will be redirected to regular UserDisp.aspx page

Monday, February 17, 2020

Several mandatory steps for every SQL Server installation

Recently I’ve installed MS Sql Server (don’t remember how many times I’ve done that already :) ) on the new virtual machine and realized that I always perform the following steps after each Sql Server installation:

1. Disable “sa” login

This step was added to mandatory tasks list after sad story: in hte times of beginning of my IT carrier one of my Sql Server instances ran with enabled “sa” login. Password was not very strong and after some time it was brute forced and I got malicious Sql Server job which tried to download and execute remote code on my PC. Fortunately I noticed that in time and made necessary actions. After that I always disable built-in “sa” login on Sql Server – this is the first things hackers will try to brute force on your instance.

2. Configure backups

This step is quite obvious. Remember that you should not only test that your backups work by specified schedule (e.g. if you configured nightly backups – check after few days that backups are really made during last nights) but also test restore scenario. Some time these simple steps will save you from a lot of problems. You may configure backups in Sql Server Management Studio > Management > Maintenance plans > New maintenance plan > Add Back Up Database Task from toolbox: After configuring backup set schedule in the same window.

Another important note is that you should not store backups on the same PC. Safest option is to move backups from local PC to the cloud storage using some cloud backup tool.

3. Limit Sql Server trace log size

If Sql Server runs long period of time it may flood hard drive with trace logs (don’t mix it with transaction logs – these are different). By default they are stored in MSSQL/Logs subfolders under your Sql Server instance folder. In order to configure their size go to Sql Server Management studio > Management > Right click on SQL Server Logs > Configure. Set some value in “Limit the number of error log files before they are recycled” and/or “Maximum size for error log file in KB”:

This simple action will allow to keep Sql Server logs size controlled. If you have similar mandatory steps in your practice please share them in comments.

Tuesday, February 11, 2020

Resolve “Everyone except external users” group on old tenants

Some time ago I wrote how to get login name for special group “Everyone except external users” in Sharepoint Online: Get login name of special group “Everyone except external users” programmatically in Sharepoint. To remind: it is constructed like this:

"c:0-.f|rolemanager|spo-grid-all-users/” + tenantId

Today we faced with scenario when such login name could not be resolved on one customer’s tenant. After research it was found that similar issue was also reported on OfficeDevPnP github project page (see here). So as it turned out on old tenants this way of getting “Everyone except external users” may not work. As workaround you may use the following solution from OfficeDevPnP:

public override string GetEveryoneExceptExternalUsersName(Web web)
 string userIdentity = "";
  // New tenant
  userIdentity = $"c:0-.f|rolemanager|spo-grid-all-users/{web.GetAuthenticationRealm()}";
  var spReader = web.EnsureUser(userIdentity);
 catch (ServerException)
  // Old tenants
  string claimName = web.GetEveryoneExceptExternalUsersClaimName();
  var claim = Utility.ResolvePrincipal(web.Context, web, claimName, PrincipalType.SecurityGroup, PrincipalSource.RoleProvider, null, false);
  userIdentity = claim.Value.LoginName;

 return userIdentity;

I.e. at first we try to get login name using "c:0-.f|rolemanager|spo-grid-all-users/” + tenantId and try to resolve group with this name. If it fails we call GetEveryoneExceptExternalUsersClaimName() extension method which returns localized name of “Everyone except external users” group for current tenant (it has translations of group name for all supported languages) and tries to resolve this special group using this name. This code will work both on new and old tenants.

Friday, January 24, 2020

How to set Always On for Azure Function app via PowerShell

If you run Azure functions on dedicated App service plan (vs Consumption plan) of Basic pricing tier and above you may speed up warmup time of Azure functions by enabling Always On setting (Azure Function app > Configuration > General settings):

If you need to automate this process here is the PowerShell script which can be used in order to set Always On for Azure Function app:

$resourceGroupName = ...
$functionAppName = ...
$webAppPropertiesObject = @{"siteConfig" = @{"AlwaysOn" = $true}}
$webAppResource = Get-AzureRmResource -ResourceType "microsoft.web/sites" -ResourceGroupName $resourceGroupName -ResourceName $functionAppName
$webAppResource | Set-AzureRmResource -PropertyObject $webAppPropertiesObject -Force

After that you Function app will have Always On setting enabled.

Tuesday, January 21, 2020

Get Azure AD groups images from Graph API using delegated permissions

In order to get Azure AD group image you need to use the following Graph API endpoint:

GET /groups/{id}/photo/$value

If you checked my previous blog post Why you should be careful with /groups/{id}/photo and /users/{id}/photo endpoints in MS Graph or unintentional getting photos of big sizes in Graph then you probably know that it is better to specify group size in endpoint – otherwise you will get biggest available high resolution image for the group. E.g. this is how you may get image with 64x64 px size:

GET /groups/{id}/photos/64x64//$value

However there is another problem with fetching groups images from Graph API which you have to care about: it should be done using delegated permissions. I.e. it is not possible to retrieve AAD groups images from Graph API using application permissions (at least on the moment of writing this blog post).

If you use Graph client library for C# you first need to create GraphServiceClient object and provide instance of class which implements IAuthenticationProvider interface and contains logic for authenticating requests using delegated permissions (via username and password). Here is how it may look like:

public class AzureAuthenticationProviderDelegatedPermissions : IAuthenticationProvider
 public async Task AuthenticateRequestAsync(HttpRequestMessage request)
  var delegatedAccessToken = await GetGraphAccessTokenForDelegatedPermissionsAsync();

  request.Headers.Add("Authorization", "Bearer " + delegatedAccessToken);

 public async Task<string> GetGraphAccessTokenForDelegatedPermissionsAsync()
  string clientId = ...
  string userName = ...
  string password = ...
  string tenant = ...
  var creds = new UserPasswordCredential(userName, password);
  var authContext = new AuthenticationContext(string.Format("https://login.microsoftonline.com/{0}", tenant));
  var authResult = await authContext.AcquireTokenAsync("https://graph.microsoft.com", clientId, creds);
  return authResult.AccessToken;

var graphClientDelegated = new GraphServiceClient(new AzureAuthenticationProviderDelegatedPermissions());

After that we may fetch group image from Graph API like that:

                var stream = Task.Run(async () =>
                    var photo = await graphClient.Groups[groupId].Photos["64x64"].Content.Request().GetAsync();
                    return photo;


Actual user account which is used for fetching groups images doesn’t need any special permissions: it may be regular user account without any admin rights.

Friday, January 17, 2020

Problems with Teams creation via beta teams Graph endpoint with owner without O365 license

When you create Team by sending HTTP POST request to beta Graph endpoint /beta/teams (see Create team) you need to specify exactly 1 user as an owner of the new team:

POST https://graph.microsoft.com/beta/teams
Content-Type: application/json
  "displayName": "Test",
  "owners@odata.bind": [

where userId is login name of the user which will be owner of the group. However this request may fail with the following error:

Invoking endpoint 'https://graph.microsoft.com/beta/teams/' didn't succeed
Response status code 'Forbidden', reason phrase 'Forbidden'
Response content '
"code": "AccessDenied",
”message": "Failed to execute Templates backend request CreateTeamFromTemplateRequest

It may happen if user which is specified as owner of the team doesn’t have O365 license. In order to avoid this error use users with O365 license as team owners.