Monday, June 29, 2020

Problem with ReSharper unit test runner with NUnit 3.12: unit test is inconclusive

Some time ago we switched to NUnit 3.12 and NUnit3TestAdapter in Camlex library and noticed the following problem with ReSharper unit test runner: all unit tests were marked as “inconclusive” and didn’t run. At the same time built-in Visual Studio test runner worked. I used 2019 version of ReSharper when found this problem.

By searching the web I found similar problems from other people in JetBrains support site. In order to fix it I uninstalled 2019 version and installed latest available version of ReSharper JetBrains.ReSharperUltimate.2020.1.3. After that test become working again:

So if you will face with the same problem try to install last ReSharper version.

Friday, June 12, 2020

Problem with threads count grow when use OfficeDevPnP AuthenticationManager

Recently we faced with the following problem in our Azure function app: after some time of functioning Azure functions got stuck and the only way to make them work again was to restart them manually from Azure portal UI. Research showed that problem was related with threads count: because of some reason threads count permanently grew to 6K thread after which Azure function app became unresponsive:

We reviewed our code, made some optimizations, change some async calls to sync equivalents, but it didn’t fix the problem. Then we continued troubleshooting and found the following: in the code we used OfficeDevPnP AuthenticationManager in order to get access token for communicating with Sharepoint Online like that:

using (var ctx = new OfficeDevPnP.Core.AuthenticationManager().GetAppOnlyAuthenticatedContext(url, clientId, clientSecret))
{
    ...
}

Note that in this example new instance of AuthenticationManager is created each time when above code was executed. In our case it was inside queue-triggered Azure function which was called quite frequently. Elio Struyf (we worked over this problem together so credits for this finding go to him Smile) tried to make AuthenticationManager static – i.e. only one instance was created for the whole Azure function worker process:

public static class Helper
{
 private static OfficeDevPnP.Core.AuthenticationManager authMngr = new OfficeDevPnP.Core.AuthenticationManager();

 public static void QueueTriggeredFunc()
 {
  using (var ctx = authMngr.GetAppOnlyAuthenticatedContext(url, clientId, clientSecret))
  {
  ...
  }
 }
}

After that threads count got stabilized with avg 60 threads:

We reported this problem to OfficeDevPnP team and they confirmed that there is problem in AuthenticationManager which internally creates thread for monitoring access token’s lifetime, but this thread is not released when AuthenticationManager got recycled by garbage collector (also need to mention that AuthenticationManager itself was not disposable at the moment when this problem was found. We used OfficeDevPnP 3.20.2004.0 when found this problem). For now this workaround with making AuthenticationManager static was Ok and as far as I know currently OfficeDevPnP team works over correct fix for this problem. So hopefully it will be available soon globally.

Update 2020-06-15: from latest news I've got (thanks to Yannick Plenevaux :) ) OfficeDevPnP team addressed this issue in 202006.2 version and made AuthenticationManager disposable. So you may need to review your code in order to add using() around it.

Monday, June 8, 2020

Fix AccessToKeyVaultDenied error in Azure App service app settings which use KeyVault reference

Azure KeyVault is convenience safe storage for secrets (passwords, keys, etc.) which can be used in your apps instead of storing them in plain text in app settings. It adds number of advantages like access control, expiration policies, versioning, access history and others. However it has some gotchas which you should be aware. In this post I will describe one such gotcha.

First of all you need to configure access to key vault secret so your App service or Azure function will be able to read values from there. You may check how to do that e.g. here: Provide Key Vault authentication with an access control policy. After that go to App service > Configration and click Edit icon for the app setting which uses reference on KeyVault secret and check that there are no errors. Because even if you did everything correctly you may see Status = AccessToKeyVaultDenied and the following error description:

Key Vault reference was not able to be resolved because site was denied access to Key Vault reference's vault

In order to fix it try the following workaround:

  1. Delete app setting from UI
  2. Save changes
  3. Add the same app setting with KeyVault reference (i.e. with @Microsoft.KeyVault(SecretUri=…))
  4. Save changes again

After that if permissions are configured properly Status should be changed to Resolved:

and your app should be able to successfully resolve secret from KeyVault reference.

Monday, June 1, 2020

Upload WebJob to Azure App service with predefined schedule

As you probably know in Azure Portal it is possible to create Azure web jobs with Type = Triggered and specify CRON expression which will define schedule of the job. In this post I will describe how to create Azure web job with predefined CRON expression.

Before to start we need to create Azure app service in the tenant and prepare zip file with executable file for web job itself. Usually it can be created by archiving bin/Release (or bin/Debug) folder of the console project with web job logic. However there is one important detail: in order to set schedule of web job we need to add special file to the project called settings.job and define CRON expression inside this file. E.g. if we want our job to run every 15 minutes we need to define it like this:

{“schedule”: “* */15 * * * *”}

After that also go to File properties in Visual Studio and set "Copy to target directory value to “Copy Always”. After that compile project and create zip file from bin/Release (or bin/Debug) folder.

Next step is to create web job from zip file. It can be done by the following convenient command line util available on github: WAWSDeploy. With this tool web job can be created created e.g. from PowerShell by running the following command:

WAWSDeploy.exe MyPackage.zip publishSettings.txt /t "app_data\jobs\triggered\MyJob"

In this command publishSettings.txt is the file with publishing settings for your App service. It can be created by Get-AzureRmWebAppPublishingProfile cmdlet. This command will create new web job called MyJob based on provided zip file with predefined schedule.

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();
watch.Stop();


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))
  {
   continue;
  }

  if (string.IsNullOrEmpty(filter))
  {
   filter = queryCondition;
  }
  else
  {
   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() {
 SP.Ribbon.PageManager.get_instance().add_ribbonInited(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:

$("#Ribbon.Documents.New").length

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() {
 SP.Ribbon.PageManager.get_instance().add_ribbonInited(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.