Wednesday, September 9, 2020

How to identify whether SPFx web part is running in web browser or in Teams client

As you probably know it is possible to add Sharepoint Online page as a tab to Team’s channel so it will be shown inside Teams: both when you access it from web browser via https://teams.microsoft.com or from native client (desktop or mobile). It may be needed to identify from where exactly Teams are accessed in order to provide better user experience for this particular client (e.g. add extra css, use different caching mechanisms, etc). In order to do that we may inspect User-Agent header (navigator.userAgent in Typescript) for different clients. In the following table I summarized values of User-Agent header for mentioned scenarios:

Accessed fromUser agent

Desktop browser (Chrome)

Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36

Mobile browser (Chrome on Android) Mozilla/5.0 (Linux; Android 10; …) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.81 Mobile Safari/537.36
Teams web client in desktop browser (Chrome) Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 SPTeamsWeb
Teams web client in mobile browser (Safari on iPad). Chrome mobile browser on Android and Safari on iPhone are not supported browsers for Teams web client

Mozilla/5.0 (Macintosh; Intel Mac OS X …) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Safari/605.1.15 SPTeamsWeb

Teams native desktop client

Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Teams/1.3.00.21759 Chrome/69.0.3497.128 Electron/4.2.12 Safari/537.36

Teams native mobile client (Android)

Mozilla/5.0 (Linux; Android 10; …) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/85.0.4183.81 Mobile Safari/537.36 TeamsMobile-Android

Teams native mobile client (iPhone)

Mozilla/5.0 (iPhone; CPU iPhone OS … like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 TeamsMobile-iOS

Teams native mobile client (iPad)

Mozilla/5.0 (iPad; CPU OS … like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 TeamsMobile-iOS

Those parts which allow to identify current client type are highlighted by bold font. I.e. if we have TeamsMobile-Android or TeamsMobile-iOS in User-Agent it means that SPFx is running in Teams native mobile client. If we have SPTeamsWeb – web part is running in web client.

Wednesday, September 2, 2020

Redirect traffic from single site collection in Sharepoint on-prem via URL rewrite IIS module

Suppose that you have Sharepoint on-prem web application with multiple site collections which have own urls and need to perform maintenance work on one of these site collections so others will remain working. In order to do that you may temporary redirect traffic from this site collection to external page (e.g. to maintenance page hosted outside of this site collection). Technically it can be done by adding URL rewrite rule to Sharepoint web app in IIS. Rule will look like this:

This rule will redirect traffic from site collection with url /test to external site http://example.com. This is the same rule in xml which is added to web.config of Sharepoint web app:

<rewrite>
  <rules>
	<rule name="MyRule" enabled="true" stopProcessing="true">
	  <match url="test/*" />
	  <action type="Redirect" url="http://example.com" appendQueryString="false" redirectType="Found" />
	</rule>
  </rules>
</rewrite>

Note that if you perform temporary maintenance work on site collection then Redirect type should be set to Found (302) like shown above and not to Permanent (301). In case of permanent redirect browser will cache response and will redirect users from this site collection even after maintenance will be completed and rule will be removed from the site.

Friday, August 14, 2020

Camlex 5.2 and Camlex.Client 4.1: support for Membership and OffsetDays

Good news for Sharepoint developers who use Camlex library: new versions are released both for server and client object models. For client object model separate packages are available for Sharepoint online and on-premise: https://www.nuget.org/account/Packages:

Package Version Description
Camlex.NET.dll 5.2.0 Server object model (on-prem)
Camlex.Client.dll 4.1.0 Client object model (SP online)
Camlex.Client.2013 4.1.0 Client object model (SP 2013 on-prem)
Camlex.Client.2016 4.1.0 Client object model (SP 2016 on-prem)
Camlex.Client.2019 4.1.0 Client object model (SP 2019 on-prem)

In this release 2 new features were added: support for Membership element (many developers requested it so finally it is out) and OffsetDays. Main credits for this release go to Ivan Russo who made actual work with very good quality so I only needed validate PR, accept it and add few more unit tests. Then I merged new features to the client branch which keeps source code for client object model version (nowdays it is done manually as there are quite many changes between server and client object model versions).

1. Membership element support

Membership element allows to create CAML queries using different membership conditions. The following examples show how it can be done now with Camlex:

// SPWeb.AllUsers
var caml = Camlex.Query().Where(x => Camlex.Membership(
	x["Field"], new Camlex.SPWebAllUsers())).ToString();
var expected =
	"  " +
	"    " +
	"      " +
	"    " +
	"  ";

// SPGroup
var caml = Camlex.Query().Where(x => Camlex.Membership(
	x["Field"], new Camlex.SPGroup(3))).ToString();
var expected =
	"  " +
	"    " +
	"      " +
	"    " +
	"  ";

// SPWeb.Groups
var caml = Camlex.Query().Where(x => Camlex.Membership(
	x["Field"], new Camlex.SPWebGroups())).ToString();
var expected =
	"  " +
	"    " +
	"      " +
	"    " +
	"  ";

// CurrentUserGroups
var caml = Camlex.Query().Where(x => Camlex.Membership(
			x["Field"], new Camlex.CurrentUserGroups())).ToString();
var expected =
	"  " +
	"    " +
	"      " +
	"    " +
	"  ";

// SPWeb.Users
var caml = Camlex.Query().Where(x => Camlex.Membership(
	x["Field"], new Camlex.SPWebUsers())).ToString();
var expected =
	"  " +
	"    " +
	"      " +
	"    " +
	"  ";

2. OffsetDays support

OffsetDays attribute is used with Today element and allows to add or subtract number of days from today for constructing dynamic CAML queries based on current datetime offset. The following example shows how it can be done with Camlex:

string caml = Camlex.Query().Where(x =>
	x["Created"] > ((DataTypes.DateTime)Camlex.Today).OffsetDays(-1)).ToString();
const string expected =
	"  " +
	"    " +
	"        " +
	"        " +
	"           " +
	"        " +
	"    " +
	"  ";

Thank you for using Camlex and stay tuned for the further improvements.

Thursday, July 16, 2020

One problem with caching old web job binaries after Azure web job update with WAWSDeploy

WAWSDeploy is convenient console utility which allows to deploy a folder or a zip file to an Azure Website using WebDeploy (see https://github.com/davidebbo/WAWSDeploy). Using this tool it is possible to automate tasks in your Azure deployment. E.g. you may upload Azure web job binaries (they should be packaged to zip file for that). However you should be aware of one potential problem.

Triggered web job binaries are uploaded to "app_data/jobs/triggered/{JobName}" path (full path will be /site/wwwroot/app_data/jobs/triggered/{JobName}). You may connect to your Azure App Service via FTP (connection settings may be copied from Azure portal > App service > Deployment center > FTP) and check that your web jobs folders are there

Note that you need to use FTP protocol (not FTPS) if you will connect e.g. via WinSCP tool.

The problem is the following: you may create zip package with web job binaries just by selecting all files for web jobs and adding them to archive – in this case there won’t be root folder in archive and all files will be located inside archive root. Alternatively you may select their parent folder (e.g. bin/Release) and add whole folder to archive – in this case archive will contain Release root folder.

So if 1st time you created archive using 1st method (by archiving files), then created another archive using 2nd method (by archiving folder) and uploaded this 2nd zip archive via WAWSDeploy – it will just create subfolder Release inside app_data/jobs/triggered/{JobName}. I.e. there will be 2 versions of web job: one in the app_data/jobs/triggered/{JobName} and another in app_data/jobs/triggered/{JobName}/Release. Azure runtime will use 1st found exe file which will be exe file from the root (not from Release sub folder) and will use that for running web job.

One way to avoid this problem is to always delete existing web job before to update it (you may check how to delete web job via PowerShell here: How to remove Azure web job via Azure RM PowerShell). In this case old binaries will be deleted so selected archiving method won’t cause problems during update.

Wednesday, July 15, 2020

How to remove Azure web job via Azure RM PowerShell

Azure web jobs allow to run your code by scheduler. Schedule is configured via CRON expression. This is convinient tool to perform repeated actions. Sometimes we need to remove them via PowerShell in automation process. In this article I will show how to remove Azure web job using Azure RM PowerShell cmdlets.

If you will try to google this topic you will most probably find Remove-AzureWebsiteJob cmdlet. The problem however is that this cmdlet is used for classic resources and you will most probably have problems using it (e.g. because of error Select-AzureSubscription -Default). So we need some other way to remove Azure web jobs – preferably by using Azure RM cmdlets. Fortunately it is possible – we may remove web job by using another cmdlet Remove-AzureRmResource. Here is syntax for removing triggered web job:

$resourceName = $appServiceName + "/" + $webJobName
Remove-AzureRmResource -ResourceGroupName $resourceGroupName -ResourceName $resourceName -ResourceType microsoft.web/sites/triggeredwebjobs

The important thing to note here is how ResourceName param is specified for web job (app service name/web job name) and ResourceType which is set to microsoft.web/sites/triggeredwebjobs. If you need to delete continuous web job you need to use microsoft.web/sites/continuouswebjobs resourceType.

Using this command you will be able to delete Azure web jobs via Azure RM PowerShell.

Monday, July 6, 2020

Run Azure function as Web job without code change

Sometime it may be needed to test existing Azure function as Web job (e.g. if you decide to move some long-running process from AF to web job). In order to do that you need to create App service first. After that create new console app project in Visual Studio, reference your Azure functions project as it would be regular class library and from Program.cs call necessary Azure function (it is possible since AFs are created as static methods of the class).

The problem however is that if you read some app settings in Azure function code – you most probably do it like that:

string foo = Environment.GetEnvironmentVariable("Foo", EnvironmentVariableTarget.Process);

But if you will just copy AF app settings to appSettings section of app.config of the new console project then above code will return empty string for any app setting. So in order to call our Azure function from console application and run it as web job we need to perform additional step: iterate through all app settings and fill environment variables for the process:

var appSettings = ConfigurationManager.AppSettings;
foreach (var key in appSettings.AllKeys)
{
 Environment.SetEnvironmentVariable(key, appSettings[key],
  EnvironmentVariableTarget.Process);
}

After that app settings will be successfully read and Azure function may run as web job without changing its code.

Thursday, July 2, 2020

Sharepoint MVP 2020

Yesterday I got email from MS about my MVP status renewal. This is 10th award for me in a row and I would like to say thanks to MS and to whole community which gives me motivation and energy to continue sharing my experience and solutions from every-day work:

Sharepoint and MS ecosystem in general evolves: now we switched to Modern Sharepoint, tight integration with Azure which becomes more and more powerful. New technologies require new skills and experience so I think it is crucial that we will continue sharing our knowledge with colleagues all over the world. Global pandemic which we faced with this year introduced new challenges for IT world: tools which we build became as crucial as never before since people switched to remote work. Performance and stability went to the 1st plan. From this perspective it is very important to take part in IT community life by answering forum questions, contributing to open source software, speaking on events, writing technical articles and so on. Hope that my contributing helped someone and would like to say thanks one more time to MS for recognizing it.