Thursday, March 24, 2022

How to identify Sharepoint Online sites which belongs to Teams private channels

In MS Teams team owner may create private channels: only members of these channels will have access to these channels. What happens under the hood is that for each private channel Teams creates separate SPO site collection with own permissions. E.g. if we have team with 2 private channels channel1 and channel2:

it will create 2 SPO sites with the following titles:

  • {team name} - channel1
  • {team name} - channel2

If we will visit these sites in browser we will notice that there will be teams icon near site title and "Private channel | Internal" site classification label:


How we may identify such SPO sites which correspond to teams private channels? E.g. if want to fetch all such sites via search.

At first I tried to check web property bag of these sites because this is how we may identify that site belongs to O365 group (see Fetch Sharepoint Online sites associated with O365 groups via Sharepoint Search KQL) but didn't find anything there. The I used Sharepoint Search Query Tool and found that these sites have specific WebTemplate = TEAMCHANNEL:

So in order to identify SPO sites which correspond to teams private channels we may use the following KQL:

WebTemplate:TEAMCHANNEL

It will return all sites for teams private channels.

Friday, March 18, 2022

Several ways to reduce JSON response size from Web API

If you have Web API which returns some data in JSON format at some point you may need to optimize its performance by reducing response size. It doesn't important on which technology Web API is implemented (ASP.Net Web API, .Net Azure Functions or something else). For this article the only important thing is that it is implemented on .Net stack.

Let's assume that we have an endpoint which returns array of objects of the following class which have many public properties:

public class Foo
{
    public string PropA { get; set; }
    public string PropB { get; set; }
    public string PropC { get; set; }
    public string PropD { get; set; }
    ...
}

By default all these properties will be returned from our API (including those properties which contain nulls):

[{
        "PropA": "test1",
        "PropB": null,
        "PropC": null,
        "PropD": null,
        ...
    }, {
        "PropA": "test2",
        "PropB": null,
        "PropC": null,
        "PropD": null,
        ...
    },
    ...
]

As you can see these properties with nulls still add many bytes to the response. If we want to exclude properties which contain nulls from response (which in turn may significantly reduce response size) we may add special class-level and property-level attributes to our class. Note however that it will help only if you serialize response with Json.Net lib (Newtonsoft.Json):

[JsonObject(MemberSerialization.OptIn)]
public class Foo
{
    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
    public string PropA { get; set; }
    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
    public string PropB { get; set; }
    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
    public string PropC { get; set; }
    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
    public string PropD { get; set; }
}

Attribute JsonObject(MemberSerialization.OptIn) means that it should serialize only those properties which are explicitly decorated by JsonProperty attribute. After that response will look like that:

[{
        "PropA": "test1",
    }, {
        "PropA": "test2",
    },
	...
]

i.e. it will only contain those properties which have not-null value.

As you can see response size got significantly reduced. The drawback of this approach is that it will work only with Json.Net lib (JsonObject and JsonProperty attributes classes are defined in Newtonsoft.Json assembly). If we don't use Newtonsoft.Json we need to use another approach which is based on .Net reflection and on the fact that Dictionary is serialized to JSON in the same way as object with the properties. Here is the code which does it:

// get array of items of Foo class
var items = ...;

// map function for removing null values
var propertiesMembers = new List<PropertyInfo>(typeof(Foo).GetProperties(BindingFlags.Instance | BindingFlags.Public));
Func<Foo, Dictionary<string, object>> map = (item) =>
{
    if (item == null)
    {
        return new Dictionary<string, object>();
    }
    var dict = new Dictionary<string, object>();
    foreach (string prop in selectProperties)
    {
        if (string.IsNullOrEmpty(prop) || !propertiesMembers.Any(p => p.Name == prop))
        {
            continue;
        }
		
        var val = item.GetPublicInstancePropertyValue(prop);
        if (val == null)
        {
            // skip properties with null
            continue;
        }
        dict.Add(prop, val);
    }
    return dict;
};

// convert original array to array of Dictionary objects each of which contains only not null properties
return JsonConvert.SerializeObject(items.Select(i => map(i)));

At first we get original array of items of Foo class. Then we define map function which creates Dictionary from Foo object and this dictionary contains only those properties which don't contain null value (it is done via reflection). Here I used helper method GetPublicInstancePropertyValue from PnP.Framework but it is quite easy to implement by yourself also:

public static Object GetPublicInstancePropertyValue(this object source, string propertyName)
{
    return (source?.GetType()?.GetProperty(propertyName,
            System.Reflection.BindingFlags.Instance |
            System.Reflection.BindingFlags.Public |
            System.Reflection.BindingFlags.IgnoreCase)?
        .GetValue(source));
}

With this approach we will also get reduced response which will contain only not-null properties and it will work also without Newtonsoft.Json:

[{
        "PropA": "test1",
    }, {
        "PropA": "test2",
    },
	...
]


Tuesday, March 15, 2022

Handle errors when load images on a web page

Regular html images (<img> tag) have quite powerfull mechanism of error handling and practice shows that it is not that well-known. E.g. if we have image on some web page (it may be any html web page regardless of underlying technology which rendered it) defined like that:

<img src="http://example.com/image.png" />

Now suppose that browser couldn't fetch this image because of some reason: e.g. image could not be found in specified url location (404 Not Found) or current user doesn't have permissions to this location (403 Forbidden). How to handle such situations and add graceful fallback logic (e.g. show some predefined image placeholder or use more advanced technique to fetch the image)?

There is standard html mechanism which allows to handle errors which may occur during loading of the images. Within img tag we may define error handler like that:

<img src="http://example.com/image.png" onerror="imageOnError()" />

In this case imageOnError() function will be called when error will occur during loading of the image. Knowing this technique we may implement graceful fallback for some scenarios when initial image was not successfully loaded because of some reason. E.g. some time ago I wrote an article where showed how to fetch image from Sharepoint site collection where current user doesn't have access: Return image stored in Sharepoint Online doclib from Azure function and show it in SPFx web part. If we will combine these 2 posts we may implement logic which by default shows images from predefined location (e.g. from Sharepoint doclib). But if current user doesn't have access to this doclib we may use onerror handler and fetch image via Azure function and app permissions and then show it in base64 encoded format. This is only one example how described technique may help to achieve more user friendly experience and provide functionality for end users which otherwise wouldn't be available.

Thursday, March 10, 2022

Disable PnP telemetry

PnP components send telemetry data to PnP team which helps to get more statistics for making various technical and architectural decisions. However sometimes you may want to disable it for the customer (e.g. due to GDPR or related topics). In this case you need to disable telemetry in the following components:

  • PnP.PowerShell
  • PnP.Framework
  • PnP.Core
  • pnp js

For disabling telemetry in PnP.PowerShell run the following command:

$env:PNPPOWERSHELL_DISABLETELEMETRY = $true

Note that it will disable PnP.PowerShell telemetry only in the current PowerShell session.

PnP.Framework itself doesn't send telemetry but it uses PnP.Core in some scenarios (e.g. modern pages API) which in turn sends telemetry. I.e. if you use PnP.Framework (and not create PnP.Core context by yourself) there is currently no option to disable it. However PnP team is working over it and probably soon there will be update about it. For disabling telemetry in PnP.Core you need to set property DisableTelemetry to false during creation of the context e.g. like this:

public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        var config = builder.GetContext().Configuration;
        var settings = new Settings();
        config.Bind(settings);
        builder.Services.AddPnPCore(options =>
        {
            options.DisableTelemetry = true;
            ...
        });
    }
}

For disabling telemetry in pnp js use the following code:

import PnPTelemetry from "@pnp/telemetry-js";

const telemetry = PnPTelemetry.getInstance();
telemetry.optOut();

Tuesday, March 1, 2022

Return image stored in Sharepoint Online doclib from Azure function and show it in SPFx web part

Imagine that we need to display image which is stored e.g. in Style library doclib of Sharepoint Online site collection (SiteA) on another site collection (SiteB) and that users from SiteB may not have permissions on SiteA. One solution is to return this image in binary form from Azure function (which in turn will read it via CSOM and app permissions) and display in SPFx web part in base64 format. With this approach way we can avoid SPO permissions limitation (assuming that Azure functions are secured via AAD: Call Azure AD secured Azure functions from C#).

At first we need to implement http-triggered Azure function (C#) which will return requested image (we will send image url in query string param). It may look like this (for simplicity I removed errors handling):

[FunctionName("GetImage")]
public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)]HttpRequestMessage req, TraceWriter log)
{
	string url = req.GetQueryNameValuePairs().FirstOrDefault(q => string.Compare(q.Key, "url", true) == 0).Value;
	var bytes = ImageHelper.GetImage(url);
	
	var content = new StreamContent(new MemoryStream(bytes));
	content.Headers.ContentType = new MediaTypeHeaderValue(ImageHelper.GetMediaTypeByFileUrl(url));

	return new HttpResponseMessage(HttpStatusCode.OK)
	{
		Content = content
	};
}

Here 2 helper functions are used: one which gets actual image as bytes array and another which returns media type based on file extension:

public static class ImageHelper
{
	public static byte[] GetImage(string url)
	{
		using (var ctx = ...) // get ClientContext
		{
			var web = ctx.Web;
			var file = web.GetFileByServerRelativeUrl(new Uri(url).AbsolutePath);
			ctx.Load(file);
			
			var fileStream = file.OpenBinaryStream();
			ctx.ExecuteQuery();
			
			byte[] bytes = new byte[fileStream.Value.Length];
			fileStream.Value.Read(bytes, 0, (int)fileStream.Value.Length);
			return bytes;
		}
	}

	public static string GetMediaTypeByFileUrl(string url)
	{
		string ext = url.Substring(url.LastIndexOf(".") + 1);
		switch (ext.ToLower())
		{
			case "jpg":
				return "image/jpeg";
			case "png":
				return "image/png";
			... // enum all supported media types here
			default:
				return string.Empty;
		}
	}
}

Now we can call this AF from SPFx:

return await this.httpClient.get(url, SPHttpClient.configurations.v1,
	{
		headers: ..., // add necessary headers to request
		method: "get"
	}
).then(async (result: SPHttpClientResponse) => {
	if (result.ok) {
		let binaryResult = await result.arrayBuffer();
		return new Promise((resolve) => {
			resolve({ data: binaryResult, type: result.headers.get("Content-Type") });
	});
})

And the last step is to encode it to base64 and add to img src attribute:

let img = ...; // get image element from DOM
let result = await getImage(url);
img.setAttribute("src", `data:${result.type};base64,${Buffer.from(result.data, "binary").toString("base64")}`)

After that image from SiteA will be shown on SiteB even if users don't have access to SiteA directly.