June 26, 2020

MS Flow: Simplest retry logic for SharePoint Online HTTP 400 errors

Problem

When setting SharePoint Online document properties from Flow, you will run into issues if the document is locked, i.e., someone has it open. In this case, SharePoint throws HTTP 400 that cannot be caught by the built-in retry-logic of the Update Item Flow action.

Solution

Simplest Do Until loop I came up with can be seen below. I didn’t find using Scope action necessary. In case you need to re-use this elsewhere in your Flow, it is quite straight forward to copy the Do Until action and paste it elsewhere. Just remember to add Set Variable action before each Do Until and set the fileLocked variable to true.

At first, the two Set variable actions were a bit confusing, the first one is only set to run after the SharePoint Update Item action has succeeded (and in that you set the fileLocked to false). The second one, however, is set to be run if the previous Set variable action is skipped (and in that you set the fileLocked to true), and as the first one is skipped if the Update Item fails, we then know it did NOT succeed.

It feels a bit weird to have the second Set variable (Set variable 2) as fileLocked variable value is not changing, but this is the high level logic people seem to do this so there may be some room for further improvement.

flowretry

May 14, 2020

Microsoft Flow: Using HTTP Webhook action with Azure Automation Runbook

Task

I needed to create new SharePoint Online Document Library and amongst other things set a Retention Label on that newly created Document Library using Microsoft Flow. Creating new doclib is straightforward using Flow, but I just couldn’t set the the Retention Label via REST from Flow. There is an API for that, but due to reasons I couldn’t get it to work from Flow.

We had an Azure Automation Runbook that was called at the end of the Flow anyway, so I decided to use that to set the Label on the SPO Library using SharePoint Online PnP’s Set-PnPLabel. No problem. However, it takes a while for the Label to be applied to the Library and as email was sent to users at the end of the Flow, they found themselves in the library too early, i.e., the Label was not yet set.

Solution

FLOW

I could’ve used  a “Do Until” loop in Flow and poll the list but that’s not something we like to do, right? We like events and triggers, so why not use the HTTP Webhook action, sounds exciting!


In the screenshot above, I’m calling the Azure Automation Runbook, but you can really call anything that is capable of listening to your request and at the and making a HTTP request back to the callback URL you define. You define your endpoint address in the Subscribe - URI field.

In the Subscribe - Body you must at least pass in the listCallbackUrl() so that you know the endpoint of this specific Flow instance you need to call in your backend code. Note that listCallbackUrl() is generated automatically, is specific to this running instance of the Flow, and can only be called once. Flow processing will halt at this action and it will continue when your backend code calls the listCallbackUrl(). You can define timeout of how long the action waits for the callback in the action settings.

Here I’m also passing in the title of the SharePoint Library as I’m also doing some tricks on the library but you would pass in anything you need in your scenario.

BACKEND

In the backend code you will do whatever you need, but when you’re done, make HTTP POST call to the URL you received as a parameter in your backend code, in my case that would look like this in PowerShell.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
param(
    [Parameter (Mandatory = $true)]
    [object]$webhookData
)

# If runbook was called from Webhook, WebhookData will not be null.
if ($WebhookData) {
    # Retrieve VMs from Webhook request body
    $body = (ConvertFrom-Json -InputObject $WebhookData.RequestBody)

    ####
    # Do something in your code...
    ###
    
    # ...and when you're done, call the callback URL
    Invoke-WebRequest $body.CallbackUrl -Method POST -UseBasicParsing
}

If you look at the Flow when it is running, you see it pause at the HTTP Webhook action, and continue as soon as the backend calls the callback URL. You can also manually call the callback URL using e.g., Postman, just paste in the callback URL and make sure method is POST. You will get HTTP 200 when the callback call succeeds.


April 14, 2020

Cloning OneNote Tab without /clone REST endpoint

Series

This is series of blog articles showing how to clone Teams Channels and Tabs without using the Clone REST endpoint or direct REST queries. Except we use the Microsoft.Graph (3.1.0). First post discusses things in general, details about different tab types are separated to individual articles.

  1. Cloning Teams Channels and Tabs without /clone REST endpoint
  2. Cloning Planner Tab without /clone REST endpoint
  3. Cloning OneNote Tab without /clone REST endpoint <<YOU ARE HERE>>
  4. Cloning Web Tab without /clone REST endpoint (TBD)
  5. Copying Teams Files tab content using MoveCopyUtil

Solution

In this piece of code, we first determine current source tab is of type OneNote. We must first create new Notebook for the tab, and for this we add simple retry logic as you cannot have Notebooks with duplicate name. This retry logic adds running integer to Notebook name until the creation succeeds.

Now, after we have successfully created the Notebook, it is time to create the Tab. Do note the special format of the EntityId parameter for the tab, as well as rather identical URLs.

Finally, note that you may end up getting ServiceException although tab creation succeeds, thus the scary Exception swallowing.

Using term cloning when it comes to this tab type can be a bit misleading, as we’re not cloning the tab content, but only the tab and creating new content. Perhaps think of this as cloning from the Team perspective, while the individual tabs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
if (sourceTab.TeamsApp.Id.Equals(TeamsAppId.OneNote))
{
logger?.LogInformation($"Creating OneNote. Channel: {sourceChannel.DisplayName}. {newTeamId}");

var newNotebook = await retry.ExecuteAsync(async () =>
{
    var i = 0;
    Notebook nb = null;
    string nbName;

    while (i < 100 && null == nb)
    {
        nbName = $"{TextTools.RemoveSpecialCharactersForNotebook(requestData.title)}{(i > 0 ? $" {i}" : "")} Notebook";

        try
        {
            nb = await graphClient.Groups[newGroup.Id].Onenote.Notebooks.Request().AddAsync(new Notebook()
            {
                DisplayName = nbName
            });

            break;
        }
        catch (ServiceException ex)
        {
            if ((int)ex.StatusCode == 409)
            {
                // this is fine, Notebook with such name already exists, let's find first available by appending integers to name
                logger?.LogWarning($"Notebook with name '{nbName}' already exists, trying next integer. Channel: {sourceChannel.DisplayName}. {newTeamId}");
            }
        }
        finally
        {
            i++;
        }
    }

    return nb;
});

try
{
    logger?.LogInformation($"Adding OneNote tab. Channel: {sourceChannel.DisplayName}. {newTeamId}");

    await retry.ExecuteAsync(async () =>
    {
        await graphClient.Teams[newTeamId].Channels[channelId].Tabs.Request().AddAsync(new TeamsTab()
        {
            ODataType = null,
            DisplayName = newNotebook.DisplayName,
            Configuration = new TeamsTabConfiguration()
            {
                EntityId = $"{Guid.NewGuid()}_{newNotebook.Id}",
                ContentUrl = $"https://www.onenote.com/teams/TabContent?entityid=%7BentityId%7D&subentityid=%7BsubEntityId%7D&auth_upn=%7Bupn%7D&notebookSource=New&notebookSelfUrl=https%3A%2F%2Fwww.onenote.com%2Fapi%2Fv1.0%2FmyOrganization%2Fgroups%2F{{groupId}}%2Fnotes%2Fnotebooks%2F{newNotebook.Id}&oneNoteWebUrl={newNotebook.Links.OneNoteWebUrl.Href}&notebookName={newNotebook.DisplayName}&ui={{locale}}&tenantId={{tid}}",
                RemoveUrl = $"https://www.onenote.com/teams/TabRemove?entityid=%7BentityId%7D&subentityid=%7BsubEntityId%7D&auth_upn=%7Bupn%7D&notebookSource=New&notebookSelfUrl=https%3A%2F%2Fwww.onenote.com%2Fapi%2Fv1.0%2FmyOrganization%2Fgroups%2F{{groupId}}%2Fnotes%2Fnotebooks%2F{newNotebook.Id}&oneNoteWebUrl={newNotebook.Links.OneNoteWebUrl.Href}&notebookName={newNotebook.DisplayName}&ui={{locale}}&tenantId={{tid}}",
                WebsiteUrl = $"https://www.onenote.com/teams/TabRedirect?redirectUrl={newNotebook.Links.OneNoteWebUrl.Href}"
            },
            AdditionalData = new Dictionary<string, object>()
            {
                {
                    "teamsApp@odata.bind", $"https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/{sourceTab.TeamsApp.Id}"
                }
            }
        });
    });
}
catch (ServiceException ex)
{
    HandleTabCreationException(ex, logger, sourceTab, newTeamId);
}
}

April 9, 2020

Cloning Planner Tab without /clone REST endpoint

Series

This is series of blog articles showing how to clone Teams Channels and Tabs without using the Clone REST endpoint or direct REST queries. Except we use the Microsoft.Graph (3.1.0). First post discusses things in general, details about different tab types are separated to individual articles.

  1. Cloning Teams Channels and Tabs without /clone REST endpoint
  2. Cloning Planner Tab without /clone REST endpoint <<YOU ARE HERE>>
  3. Cloning OneNote Tab without /clone REST endpoint
  4. Cloning Web Tab without /clone REST endpoint (TBD)
  5. Copying Teams Files tab content using MoveCopyUtil

Solution

In this piece of code, we first determine current source tab is of type Planner. Then comes the tricky part: you cannot use Application permissions to create the Planner Plan itself, but you must use Delegated permissions, so basically what you need is a dedicated user account, and use a GraphServiceClient that uses UsernamePasswordProvider as IAuthenticationProvider for the Graph API calls. Yes, you really gotta put username and password of that user in Key Vault and fetch those here.

Now, after we have successfully created the Planner Plan, we will do a small trick and do not create the contentUrl property for the new tab manually, but take the ContentUrl from the source Tab and just replace the old Planner Id with the Id of the newly created Planner. Lovely. Just note that the WebsiteUrl needs to be done separately, otherwise the “Go to website” link on top right of the Planner tab is broken and will be stuck loading and give error “Error loading user settings. Please try again. If this continues please contact customer support.” after few minutes.

For creating the new Tab, there’s not much to say. These are the parameters you need to define based on a bit of trial and error. As Graph API evolves and improves, this is the part that will most probably need updating, but at least with Microsoft.Graph v. 3.1.0 it works – which is already old version as 3.2.0 was released few hours ago.

Finally, note that you may end up getting ServiceException although tab creation succeeds, thus the scary Exception swallowing.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// Planner
if (sourceTab.TeamsApp.Id.Equals(TeamsAppId.Planner))
{
    logger?.LogInformation($"Creating Planner. Channel: {sourceChannel.DisplayName}. {newGroup.Id}");

    var newPlannerPlan = await retry.ExecuteAsync(async () =>
    {
        return await Authentication.GetGraphApiClientClientUsernamePasswordProvider(keyvault).Planner.Plans.Request()
            .WithUsernamePassword(keyvault.GetSecret("teamsClone--SPOUser"), new System.Net.NetworkCredential("", keyvault.GetSecret("teamsClone--SPOPassword")).SecurePassword)
            .AddAsync(new PlannerPlan()
            {
                Owner = newGroup.Id,
                Title = $"{TextTools.RemoveSpecialCharactersForNotebook(requestData.title)} Planner"
            });
    });

    try
    {
        string contentUrl = sourceTab.Configuration.ContentUrl.Replace(sourceTab.Configuration.EntityId, newPlannerPlan.Id);
        string websiteUrl = $"https://tasks.office.com/YOURTENANT.onmicrosoft.com/Home/Planner#/plantaskboard?groupId={newTeamId}&planId={newPlannerPlan.Id}";

        logger?.LogInformation($"Adding Planner tab. Channel: {sourceChannel.DisplayName}, ContentUrl: {contentUrl}. {newGroup.Id}");

        await retry.ExecuteAsync(async () =>
        {
            await graphClient.Teams[newTeamId].Channels[channelId].Tabs.Request().AddAsync(new TeamsTab()
            {
                ODataType = null,
                DisplayName = sourceTab.DisplayName,
                TeamsApp = sourceTab.TeamsApp,
                Configuration = new TeamsTabConfiguration()
                {
                    EntityId = newPlannerPlan.Id,
                    ContentUrl = contentUrl,
                    RemoveUrl = contentUrl,
                    WebsiteUrl = webSiteUrl
                },
                AdditionalData = new Dictionary<string, object>()
    {
        {
            "teamsApp@odata.bind", $"https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/{sourceTab.TeamsApp.Id}"
        }
    }
            });
        });
    }
    catch (ServiceException ex)
    {
        HandleTabCreationException(ex, logger, sourceTab);
    }
}

Cloning Teams Channels and Tabs without /clone REST endpoint

Series

This is series of blog articles showing how to clone Teams Channels and Tabs without using the Clone REST endpoint or direct REST queries. Except we use the Microsoft.Graph (3.1.0). First post discusses things in general, details about different tab types are separated to individual articles.

  1. Cloning Teams Channels and Tabs without /clone REST endpoint <<YOU ARE HERE>>
  2. Cloning Planner Tab without /clone REST endpoint
  3. Cloning OneNote Tab without /clone REST endpoint
  4. Cloning Web Tab without /clone REST endpoint (TBD)
  5. Copying Teams Files tab content using MoveCopyUtil

Task

As cloning a Microsoft Teams Team using the https://graph.microsoft.com/v1.0/teams/{sourceeamId}/clone has few things that might surprise you (such as described here), you will anyway need go through all cloned tabs to verify they’re working. Why not instead create a fresh Channel based on the source Team Channel, then enumerate source Team tabs and recreate new tabs in the destination Channel based on the properties of the source Tab?

Solution

Enough chit-chat, lets dive in!

What happens in the code is that we first get source Team, Channels, and their Tabs. Having already created new Group and related Team (out of scope of this specific article), we start creating Channels in the new Team based on source Team Channels.

Reason why I didn’t include details of creating new Group and related Team here is because you could apply this same logic on existing Teams as well. Maybe if you’d like to push new Channel or Tab to existing Teams? Welcome, this code will work just nice.

After creating each Channel, we loop through the source Channel Tabs. Based on the source Tab type, and also how it is eventually been configured, we recreate identical Tab.

Good thing in this way of doing this is that you have full control of the Tab cloning operation.Then again it also means that you will have to implement that full control logic and program code.

Depending on the tab type, term cloning might be a bit misleading, as in all cases we’re not cloning tab content but creating new background resource (such as Planner Plan or OneNote Notebook) and just binding to it from the tab. Maybe think of cloning here as cloning the Team structure including channels and tabs.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
logger?.LogInformation("Getting source team channels and tabs");

var sourceTeamChannels = await retry.ExecuteAsync(async () =>
{
    return await graphClient.Teams[sourceTeamId].Channels.Request().GetAsync();
});

foreach (var chan in sourceTeamChannels)
{
    chan.Tabs = await retry.ExecuteAsync(async () =>
    {
        return await graphClient.Teams[sourceTeamId].Channels[chan.Id].Tabs.Request().Expand("TeamsApp").GetAsync();
    });
}

foreach (var sourceChannel in sourceTeamChannels)
{
    var newChannel = await retry.ExecuteAsync(async () =>
    {
        logger?.LogInformation($"Adding channel {sourceChannel.DisplayName}. {newGroup.Id}");
        return await graphClient.Teams[newTeamId].Channels.Request().AddAsync(new Microsoft.Graph.Channel()
        {
            ODataType = null,
            DisplayName = sourceChannel.DisplayName,
            Description = sourceChannel.Description
        });
    });

    channelId = newChannel.Id;

    // here you should remove the Wiki tab (see separate article)

    foreach (var sourceTab in sourceChannel.Tabs)
    {
        // here you will have switch or if/else construct to determine source tab type
        if (sourceTab.TeamsApp.Id.Equals(TeamsAppId./*TAB TYPE*/))
        {
            // and actual tab type specific creation and configuration logic
        }
    }
}

By the way, you’ll be needing this Exception handler later, as tab creation will throw specific ServiceExceptions even though creation succeeds.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
private static void HandleTabCreationException(ServiceException ex, ILogger logger, TeamsTab sourceTab)
{
    if (ex.StatusCode.ToString() == "BadRequest" && ex.Error.Message == "Value cannot be null.\r\nParameter name: entity")
    {
        // this is fine: https://stackoverflow.com/questions/59784200/programmatically-creating-teamtab-used-to-work-but-now-gives-error-after-updati
        logger?.LogInformation($"Tab {sourceTab.TeamsApp.Id} created: {sourceTab.DisplayName}");
    }
    else
    {
        logger?.LogInformation($"Error creating tab: {sourceTab.TeamsApp.Id}: {sourceTab.DisplayName}");
    }
}

Also, extending Laura’s TeamsAppId class a bit will come handy, thank you Laura!

1
2
3
4
5
6
7
8
public static class TeamsAppId
{
    public static readonly string OneNote = "0d820ecd-def2-4297-adad-78056cde7c78";
    public static readonly string Planner = "com.microsoft.teamspace.tab.planner";
    public static readonly string SharePoint = "2a527703-1f6f-4559-a332-d8a7d288cd88";
    public static readonly string Web = "com.microsoft.teamspace.tab.web";
    public static readonly string Wiki = "com.microsoft.teamspace.tab.wiki";
}

April 8, 2020

Copying Teams Files tab content using MoveCopyUtil

Series

This is series of blog articles showing how to clone Teams Channels and Tabs without using the Clone REST endpoint or direct REST queries. Except we use the Microsoft.Graph (3.1.0). First post discusses things in general, details about different tab types are separated to individual articles.

  1. Cloning Teams Channels and Tabs without /clone REST endpoint
  2. Cloning Planner Tab without /clone REST endpoint
  3. Cloning OneNote Tab without /clone REST endpoint
  4. Cloning Web Tab without /clone REST endpoint (TBD)
  5. Copying Teams Files tab content using MoveCopyUtil <<YOU ARE HERE>>

Task

As part of Teams provisioning, content of Files tab needed to be copied from source Team to the destination Team. As you might know, in practice it just means copying folders and files across SharePoint site collections.

Solution

Microsoft.SharePoint.Client.MoveCopyUtil will do the trick UNLESS a folder name contains special characters, see here for example. This has been an issue for long in CSOM, and if at all possible, I recommend not having special characters in folder names, and not in file names either for that matter.

If you must support special character scenarios, it is doable, but will require carefully splitting and constructing the query parameters and effectively means you need to loop and create/copy folders and files one by one. I’ve spent some time creating SPFx solution that manages to do this, and I recommend thinking not twice, but ten times before agreeing to support special characters in this case. Good thing is that in this Teams provisioning scenario you will have control over the source Team and should be able to work around any needs for special characters.

Explanation of the code

What happens in this code can probably be read quite well by looking at the LogInformation entries, but the idea is that we get hold of the Drive item of the source and destination Group. Drive.WebUrl is then absolute URL to the document library where Teams File tab contents are in their own sub-folders.

As a reminder, each File tab in a Team Channel maps to one root level folder (Channel name = folder name) in that document library you find at Drive.WebUrl.

Another nice thing about Group.Drive is that whenever we get hold of that, and it is not returning HTTP 404, we know Group provisioning (the process that runs in the background and creates all back-end dependencies, such as the SharePoint site) has been completed.

retry object is Polly’s AsyncRetryPolicy and ensures retry logic in case provisioning would not be yet ready at this point. It will retry the call as configured, in my case every 5 seconds, until the .GetAsync() completes successfully.

Next up there is some ugly parsing of the source document library path, namely we’re removing the document library part from the URL, leaving us with URL of the SPWeb. There are many ways of doing this, and I admit this one is quite verbose way of doing it but doesn’t at least require additional HTTP query.

Then we get to the actual point, initializing SharePoint context using the source SPWeb, and credentials stored in Azure Key Vault.

After getting all the root level folders (each corresponding a Channel, yes, good), we loop through each folder item and use MoveCopyUtil.CopyFolder to copy it to destination document library. Just remember to set the MoveCopyOptions to KeepBoth, indicating this is a Copy operation and not Move operation.

I felt more comfortable doing .ExecuteQuery for each folder, although it means few more HTTP calls, but allows improved error handling in case of those special characters, or some other problems that might occur during the copy operation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public static async Task CopyDocuments(string srcGroupId, string dstGroupId, AsyncRetryPolicy retry, GraphServiceClient graphClient, KeyVault keyvault, ILogger logger)
{
    var sourceDrive = await retry.ExecuteAsync(async () =>
    {
        return await graphClient.Groups[srcGroupId].Drive.Request().GetAsync();
    });

    var destDrive = await retry.ExecuteAsync(async () =>
    {
        return await graphClient.Groups[dstGroupId].Drive.Request().GetAsync();
    });

    try
    {
        logger?.LogInformation($"Copying SPO files, source drive: {sourceDrive.WebUrl}");
        logger?.LogInformation($"Copying SPO files, destination drive: {destDrive.WebUrl}");

        var sourceUri = new Uri(sourceDrive.WebUrl);
        string sourceSiteUrlWithoutLastSegment = sourceUri.AbsoluteUri.Remove(sourceUri.AbsoluteUri.Length - sourceUri.Segments.Last().Length);

        using (var srcContext = new OfficeDevPnP.Core.AuthenticationManager()
            .GetSharePointOnlineAuthenticatedContextTenant(sourceSiteUrlWithoutLastSegment, keyvault.GetSecret("teamsClone--SPOUser"), keyvault.GetSecret("teamsClone--SPOPassword")))
        {
            logger?.LogInformation($"Copying SPO files, getting root folders");

            // get folders at root                   
            var targetList = srcContext.Web.Lists.GetByTitle(sourceDrive.Name);

            // This method only gets the folders which are on top level of the list/library
            FolderCollection oFolderCollection = targetList.RootFolder.Folders;

            // Load folder collection
            srcContext.Load(oFolderCollection);
            srcContext.ExecuteQuery();

            MoveCopyOptions option = new MoveCopyOptions
            {
                KeepBoth = true
            };

            logger?.LogInformation($"Copying SPO files, looping folders");

            foreach (var folder in oFolderCollection)
            {
                if (folder.Name.Equals("Forms"))
                {
                    continue;
                }

                try
                {
                    logger?.LogInformation($"Copying SPO files, copying folder '{sourceUri.GetLeftPart(UriPartial.Authority) + folder.ServerRelativeUrl}' -> '{destDrive.WebUrl}/{folder.Name}'");

                    MoveCopyUtil.CopyFolder(
                        srcContext,
                        sourceUri.GetLeftPart(UriPartial.Authority) + folder.ServerRelativeUrl,
                        $"{destDrive.WebUrl}/{folder.Name}",
                        option);

                    srcContext.ExecuteQuery();

                    logger?.LogInformation("Copying done");
                }
                catch (Exception ex)
                {
                    logger?.LogError($"Error copying SPO folder {folder.Name}. {ex.Message}");
                }
            }
        }
    }
    catch (Exception ex)
    {
        logger?.LogError($"Error copying SPO content. {ex.Message}");
    }
}

April 3, 2020

Remove Wiki tab during provisioning of Teams channels

Task

Remove Wiki tab during provisioning of Teams channels.

Solution

As I’m not cloning my Team, but instead enumerating channels and tabs of the source Team used as a template, it was quite straightforward to include the Wiki tab remove logic to the stage where I’m anyway fetching Tabs for each source Team Channel.

This one includes Polly retry-logic for the DELETE operation (my first time using Polly so there’s probably lot of room for improvement).

It would also be nice to bake the Polly retry-logic into the GraphServiceClient itself instead of surrounding all calls. If you know it has already been done, please comment below. I’m always looking forward to improve my code.

I’m currently using version 3.1.0 of Microsoft.Graph library.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// init polly
var retry = Policy
    .Handle<ServiceException>(ex =>
        (ex.StatusCode == System.Net.HttpStatusCode.NotFound
        || ex.StatusCode == System.Net.HttpStatusCode.TooManyRequests
        || ex.StatusCode == System.Net.HttpStatusCode.BadGateway)
        )
    .WaitAndRetryAsync(60, _ => TimeSpan.FromSeconds(5)); // retry every 5 secs for 5 minutes
    
// create new team

// get new team channels
var newTeamId = newTeam.Id;
var newTeamChannels = await graphClient.Groups[newTeamId].Team.Channels.Request().GetAsync();

foreach (var chan in newTeamChannels)
{
    chan.Tabs = await graphClient.Teams[newTeamId].Channels[chan.Id].Tabs.Request().Expand("TeamsApp").GetAsync();

    // remove wikis right here, right now
    for (int i = chan.Tabs.Count - 1; i >= 0; i--)
    {
        if (chan.Tabs[i].TeamsApp.Id == "com.microsoft.teamspace.tab.wiki")
        {
            try
            {
                await retry.ExecuteAsync(async () =>
                {
                    await graphClient.Teams[newTeamId].Channels[chan.Id].Tabs[chan.Tabs[i].Id].Request().DeleteAsync();
                });

                chan.Tabs.RemoveAt(i);
            }
            catch (Exception ex)
            {
                logger?.LogInformation($"Error removing wiki tab on channel: {chan.DisplayName}. {ex.Message}");
            }
        }                            
    }
}