Showing posts with label CSOM. Show all posts
Showing posts with label CSOM. Show all posts

Friday, April 8, 2022

Create folders with special characters in Sharepoint Online programmatically via CSOM

If you worked with SP on-prem you probably know that some special characters are not allowed in folders names there. In Sharepoint Online however it is possible to use some special characters in folders names:

How to create such folders with special characters programmatically via CSOM? If we will try to do it using the same approach which we used for SP on-prem it will implicitly remove all parts in folder names which come after special characters i.e. in above example abc, def, ghi:

public static ListItem AddFolder(ClientContext ctx, List list, string parentFolderUrl, string folderName)
{
    var lici = new ListItemCreationInformation
    {
        UnderlyingObjectType = FileSystemObjectType.Folder,
        LeafName = folderName.Trim(),
        FolderUrl = parentFolderUrl
    };

    var folder = list.AddItem(lici);
    folder["Title"] = lici.LeafName;
    folder.Update();
    ctx.ExecuteQueryRetry();

    return folder;
}

If we want to create folders with special characters we need to use another CSOM method Folder.AddSubFolderUsingPath:

public static void AddFolderUsingPath(ClientContext ctx, Folder parentFolder, string folderName)
{
    parentFolder.AddSubFolderUsingPath(ResourcePath.FromDecodedUrl(folderName));
    ctx.ExecuteQueryRetry();
}

With this method folders with special characters will be created successfully in Sharepoint Online.

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.

Monday, January 31, 2022

How to get all items from large lists in Sharepoint Online

As you probably know Sharepoint lists have default throttling limit 5000 items which means that if query result to more than 5000 items it will throw resource throttling exception. One solution which you may try is to use RowLimit:

<View>
    <Query>
        <Where>
            <Eq>
                <FieldRef Name=\"Title\" />
                <Value Type=\"Text\">test</Value>
            </Eq>
        </Where>
    </Query>
    <RowLimit>5000</RowLimit>
</View>

Or with Camlex:

var camlQuery = Camlex.Query()
    .Where(x => (string)x["Title"] == "test")
    .Take(5000)
    .ToCamlQuery();

However if list contains more than 5000 items this query will anyway result in resource throttling exception regardless of RowLimit being set. Another possible solution is to try to add index to set some field as indexed and use condition against this field in query. But what if you can't do that (e.g. if you don't have control over the list schema)? How to get all items from large list then?

For that you may use query ID > 0 (which is always true since ID is auto generated integer) and CamlQuery.ListItemCollectionPosition property which allows to get paginated results. Code will look like this:

ListItemCollectionPosition position = null;
var items = new List<ListItem>();
do
{
    var camlQuery = Camlex.Query()
        .Where(x => (int)x["ID"] > 0)
        .ViewFields(x => new []
        {
            x["ID"],
            x["Title"]
        })
        .Take(5000)
        .ToCamlQuery();
    camlQuery.ListItemCollectionPosition = position;
    var listItems = list.GetItems(camlQuery);
    ctx.Load(listItems);
    ctx.ExecuteQueryRetry();

    items.AddRange(listItems);
    position = listItems.ListItemCollectionPosition;

    if (position == null)
    {
        break;
    }
} while (true);

It will fetch items by 5000 items per page until all items from large list will be fetched.

Friday, December 4, 2020

How to add and remove user from site collection admins in Sharepoint Online using CSOM

Using the following code you may add user to site collection admins in Sharepoint Online via CSOM:

var adminContext = ...; // ClientContext for https://{tenant}-admin.sharepoint.com
string siteUrl = "https://{tenant}.sharepoint.com/sites/foo";
string loginName = ...; // user name which should be added to site collection admins
var tenant = new Tenant(adminContext);
tenant.SetSiteAdmin(siteUrl, loginName, true);
adminContext.ExecuteQueryRetry();

If you need to remove user from site collection admins use the following code:

var adminContext = ...; // ClientContext for https://{tenant}-admin.sharepoint.com
string siteUrl = "https://{tenant}.sharepoint.com/sites/foo";
string loginName = ...; // user name which should be added to site collection admins
var tenant = new Tenant(adminContext);
tenant.SetSiteAdmin(siteUrl, loginName, false);
adminContext.ExecuteQueryRetry();

(the difference is only that you need to pass false in tenant.SetSiteAdmin() method). Hope it will help in your work.

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.