Consume the Graph API and create O365 groups/modern team sites with CSOM C#

In this post, we will talk about how to get the access token in CSOM C# and then talk to Graph.

From Interact with Graph and make O365 Groups with AzureFunctions PowerShell

What is the Trick?

The problem is to authenticate with Azure AD and get an access token that we can use to talk to the Office 365 Graph. The trick, is a little not-well known thing called Resource Owner grant.

grant_type=password

I have a few links about the Resource Owner grant type at the end. Basically, this grant_type lets you use username/password to obtain an access token.

First of all we will setup Azure App registration.

We need the permission to Read and Write All groups (Group.ReadWrite.All).

If you are familiar with it, just skip ahead to the code section.

If not, you need to follow the below steps:

First of all, navigate to your Azure Portal site or Azure AD portal. You need to use the account which has access to the Azure AD.

Once logged in, go to the App registration page as below:

Azure Active Directory page > App registrations > New Application registration.

AzurePortal.PNG

Inside that, enter your details by providing some name and a valid Sign-on URL. Keep the Application type as Web App / API. Then click on Create. See below screengrab :

AppRegister.PNG

Once the application is created, navigate to it.

Please copy the application ID. This will be our client Id.

ApplicationID

Now, go to the required permission and click on Add.

Capture

Capture2.PNG

Capture3

Capture4

Inside the delegated permissions, click on the Read and write all groups

Capture5

Click the awesome Grant Permissions button at the top of the permissions registration, this grants it for users in your Active Directory.

Capture6

Capture7

You will need some clientsecrets – create them from Keys. I like them not expiring for a long time. So I pick the Never expires option. It’ll expire when I’m long gone.

Keys

keys2

Copy down your ClientSecret.

Also, copy the Application ID which will be you client Id.

Now, lets do some coding 🙂

For this demo purposes, I am using a console application.

You will need the below Nuget packages:

Nuget

We are going to make use to PnP Core’s UnifiedGroupsUtility.CreateUnifiedGroup method to create a Unified group.

We are also going to make a POST request to get the Graph API access token, for that we are going to make use of the

System.Net.Http's HttpClient.

First we will create a class named AuthenticationResponse to map the JSON response.


public class AuthenticationResponse
{
        public string token_type { get; set; }
        public string scope { get; set; }
        public int expires_in { get; set; }
        public int expires_on { get; set; }
        public int not_before { get; set; }
        public string resource { get; set; }
        public string access_token { get; set; }
        public string refresh_token { get; set; }
        public string id_token { get; set; }
}

Now, in our main method, it will be as below:


class Program
{
	static void Main(string[] args)
    {
        Task.Run(() => MainAsync());
        Console.ReadLine();
	}
}


static async Task MainAsync()
{

	string userName = "user@tenantName.onmicrosoft.com";
	string password = "password";

	List<KeyValuePair<string, string>> vals = new List<KeyValuePair<string, string>>();

	string tenantName = "tenantName.OnMicrosoft.com";
	string authString = "https://login.microsoftonline.com/" + tenantName;
	string resource = "https://graph.microsoft.com";

	AuthenticationContext authenticationContext = new AuthenticationContext(authString, false);

	string clientId = "<client-id>";
	string key = "<client-secret>";

	vals.Add(new KeyValuePair<string, string>("client_id", clientId));
	vals.Add(new KeyValuePair<string, string>("resource", resource));
	vals.Add(new KeyValuePair<string, string>("username", userName));
	vals.Add(new KeyValuePair<string, string>("password", password));
	vals.Add(new KeyValuePair<string, string>("grant_type", "password"));
	vals.Add(new KeyValuePair<string, string>("client_secret", key));
				
	string url = string.Format("https://login.windows.net/{0}/oauth2/token", tenantName);

	using (HttpClient httpClient = new HttpClient())
	{
		httpClient.DefaultRequestHeaders.Add("Cache-Control", "no-cache");
		HttpContent content = new FormUrlEncodedContent(vals);
		HttpResponseMessage hrm = httpClient.PostAsync(url, content).Result;

		AuthenticationResponse authenticationResponse = null;
		if (hrm.IsSuccessStatusCode)
		{
			Stream data = await hrm.Content.ReadAsStreamAsync();
			DataContractJsonSerializer serializer = new
			DataContractJsonSerializer(typeof(AuthenticationResponse));
			authenticationResponse = (AuthenticationResponse)serializer.ReadObject(data);

			var accessToken = authenticationResponse.access_token;

			Stream groupLogoStream = new FileStream("C:\\groupassets\\logo-original.png", 
                                            FileMode.Open, FileAccess.Read);
											
			var group = UnifiedGroupsUtility.CreateUnifiedGroup("displayName", "description", 
                            "mymodernteamsite", accessToken, groupLogo: groupLogoStream);

			
			string groupUrl = group.SiteUrl;
			string groupId = group.GroupId;

								
		}
	}

}

Done. This will create a Modern Unified team site with Office 365 group enabled.

References – Provisioning a “modern” team site programmatically

Interact with Graph and make O365 Groups with AzureFunctions PowerShell

Advertisements

SharePoint Online – Create communication sites using REST API and jQuery

At this year’s SharePoint Virtual Summit, Microsoft announced several improvements to SharePoint Online and One Drive. One of the important and interesting additions to that is new “Communication Sites”.

So, what is a communication site ?

According to this

A SharePoint communication site is a great place to share information with others. You can share news, reports, statuses, and other information in a visually compelling format.

Communication site is like Team Site in SharePoint site that anyone can create in the organization from SharePoint Home (it is available only in Office 365 and not in the on premise environment, neither 2013 nor 2016). So, this is for internal users of organization.
Communication sites are beautiful, dynamic sites that let you reach a broad internal audience, and that appear great on the web, in the SharePoint mobile app, on PC and of course on Mac.

So, how do I create communication site ?

There are 2 options –

1) Using the UI.

To create a communication site from the UI, simply go to your app launcher and click on the SharePoint tile and then click on create a site. Follow the below 2 screenshots:

3264-2017_2d00_07_2d00_28_5f00_13_2d00_52_2d00_02

8054-create-site

I am using a first release tenant. In case you dont see these options, you need to wait for the general release of communication sites.

2) Using REST API endpoint and jQuery.

You can programmatically create communication sites using the REST API endpoint provided by SharePoint. Below is a simple example using jQuery, however its easily portable to angular or any other framework that you use.


var body={
	'request':{
		"__metadata":{"type":"SP.Publishing.PublishingSiteCreationRequest"},
		"AllowFileSharingForGuestUsers":false,
		"Classification":"My Test Communication Site",
		"Description":"Here is my communication site",
        // "SiteDesignId" - use either of the below 3 options
        //"SiteDesignId":"f6cc5403-0d63-442e-96c0-285923709ffc",
        //"SiteDesignId":null,
		"SiteDesignId":"6142d2a0-63a5-4ba0-aede-d9fefca2c767",
		"Title":"My Test Communication Site",
		"Url":"https://tenantname.sharepoint.com/sites/testcommunicationsite",            
		"lcid":1033
	}
};


jQuery.ajax({
	type: 'POST',
	url: "https://tenantname.sharepoint.com/_api/sitepages/publishingsite/create",
	contentType: 'application/json',
	processData: false,
	data: JSON.stringify(body),
	headers:{
		"accept":"application/json;odata=verbose",
		"content-type":"application/json;odata=verbose",
		"X-RequestDigest": document.getElementById("__REQUESTDIGEST").value
	},        
	success: function () {
		console.log('creation request submitted');
	},
	error: function(data){
		console.log('failure');
		console.log(data);
	}
});


Very important note – This will work currently only from the root site. You need to be a site collection administrator on the root site and also i guess, need to be a SharePoint admin for this code to work.

There are 3 flavors available when you create a communication site, namely, Topic, Showcase and Blank.

Note: New in this API is the concept of SiteDesignID. Much like the in-product site creation flow, the SiteDesignID parameter maps to the included site designs. They are as below, just modify the above endpoint’s SiteDesignID parameter to create a communication site of your flavour:

  • Topic: null
  • Showcase: 6142d2a0-63a5-4ba0-aede-d9fefca2c767
  • Blank: f6cc5403-0d63-442e-96c0-285923709ffc
  • SPFx – how to detect modern or classic page in SharePoint Online

    When you create a webpart using the new SPFx model, you are able to create and deploy the webpart not only in “Modern” sites like Team sites or Communication sites, but also in the classic sites.

    The rendering of pages in classic sites is quite different from that in Modern sites. Because of that, there could be some issues related to css and html. So, sometimes you need to inject specific css or html in the webpart.

    Now, to do that, you first need to detect whether the webpart is present in classic site or modern site. Fortunately, SPFx provides us with Environment type variable, present inside the Microsoft sp-core-library, using which you can determine whether it is a classic site or modern site or if its your workbench page.

    The code for that is as below. Do note, it is available only if your SPFx version is 1.2 or higher.

    Add the below import statement in your SPFx webpart:

    import { Environment, EnvironmentType } from '@microsoft/sp-core-library';
    

    Now, in your method, you can detect the environment as below:

    private _renderHTML(): void {
    	if(Environment.type == EnvironmentType.ClassicSharePoint){
    		//do some stuff on classic page
    	}else if(Environment.type === EnvironmentType.SharePoint){
    		//do some stuff on modern page
    	}else if(Environment.type === EnvironmentType.Local){
    		//do some stuff on SharePoint workbench page
    	}
    }
    

    Reference – EnvironmentType enumeration

    Add custom columns (metadata) to “modern” SharePoint pages

    In your Site Pages library let’s say you have a Wiki Page, a Webpart Page and a Site Page (i.e. modern page).

    When I add a choice column, let’s call it TestChoiceCol, I can see the field in the edit form for the Wiki Page and Webpart page and can select a value and Save it as well.

    However, the field does not show for the Site Page. This tells me metadata cannot be added to modern pages. That’s bad news for someone who wants to create information architecture and enable better search with refiners.

    But turns out there’s a twist.

    We have bad news and we have good news.

    Let me dish out the bad news first.

    You cant add the metadata (custom columns) via UI. Neither from edit properties page nor from the Modern page itself OOTB.

    But we also have good news.

    We can do it programmatically using CSOM C# or via PowerShell.

    How will we do it?

    Well according to Programming modern pages ,

    creating a “modern” page comes down to creating a list item in the site pages library and assigning it the correct content type combined with setting some additional properties.

    So, leveraging the code mentioned in the link, I added my custom column to it.

    In the Site Pages library, I created a simple choice column named TestChoiceCol.
    The options were – Oreo,Good-day and Parle-G.

    In the library settings, I could see that this column was not associated with Site Pages content type.

    Test Choice column

    So, I ran the below code to add a modern page:

    Web web = context.Site.RootWeb;
    List pagesLibrary = context.Web.Lists.GetByTitle("Site Pages");
    				
    ListItem item = pagesLibrary.RootFolder.Files.AddTemplateFile("/sites/test/SitePages/mymodernpage.aspx", TemplateFileType.ClientSidePage).ListItemAllFields;
    
    // Make this page a "modern" page
    item["ContentTypeId"] = "0x0101009D1CB255DA76424F860D91F20E6C4118";
    item["Title"] = "My Modern Page";
    item["ClientSideApplicationId"] = "b6917cb1-93a0-4b97-a84d-7cf49975d4ec";
    item["PageLayoutType"] = "Article";
    item["PromotedState"] = "0";
    item["CanvasContent1"] = "<div></div>";
    item["BannerImageUrl"] = "/_layouts/15/images/sitepagethumbnail.png";
    
    //custom choice column named Category
    //add/update your custom columns here
    item["TestChoiceCol"] = "Oreo";
    
    item.Update();
    context.Load(item);
    context.ExecuteQuery();
    

    To update the modern page custom column, you can the run the below snippet:

    Web web = context.Site.RootWeb;
    List pagesLibrary = context.Web.Lists.GetByTitle("Site Pages");
    
    ListItem item = pagesLibrary.GetItemById(1);
    												
    //custom choice column named Category
    //add/update your custom columns here
    item["TestChoiceCol"] = "Good-day";
    
    item.Update();
    context.Load(item);
    context.ExecuteQuery();
    

    Now,this code executes successfully. However, if I check the edit/view properties page, I couldn’t see the column.

    Modern Site page Edit properties

    But then I re-indexed the Site Pages library and mapped the crawled property ows_TestChoiceCol to a standard RefinableString managed property.

    Mapping crawled property to managed property

    Then I re-indexed the library and the site collection again, yes quite impatient aren’t we !!

    After that, I created a standard search page with a search results webpart and a refiner webpart.

    In the refiner webpart, I added my custom refinablestring property.

    In the search results webpart, I filtered it to get only modern pages.

    The search query was –

    {searchboxquery} ContentTypeId:0x0101009D1CB255DA76424F860D91F20E6C4118*

    Once done, we are able to now filter modern pages based on custom columns.

    Modern page filter

    Summarizing

    The trick here is to treat the modern page as just another list item. You can then add as many custom columns or metadata that you want and then perform normal CRUD operations like the ones we do with standard list items.

    The only drawback here, is that this not possible via UI for now.

    That needs to change. To ensure that your voices are heard by Microsoft, please do upvote on the user voice link mentioned below:

    Enable custom columns for modern pages

    SharePoint Online – Check current user permission on a SharePoint list.

    There are many scenarios wherein we need to check the permission of the current user – most likely, in a custom webpart , we need to show/hide some content based on whether the user has edit/delete permissions on the list.

    To, check whether current user has Edit, permission, we have a great friend in the SP.PermissionKind enum.

    We also make use of the SPList.EffectiveBasePermissions property

    To use it in a JSOM call, you can use and modify the below code:

    SP.SOD.executeFunc('sp.js', 'SP.ClientContext', function () {
        getCurrentUserPermission('Custom List');
    });
    
    
    function getCurrentUserPermission(listName)
    {    
        var web,clientContext,currentUser,list;
    
        clientContext = new SP.ClientContext.get_current();
        web = clientContext.get_web();
        currentUser = web.get_currentUser();   
        list = web.get_lists().getByTitle(listName);
        clientContext.load(list,'EffectiveBasePermissions');
        clientContext.load(currentUser); 
        clientContext.load(web);           
    
        clientContext.executeQueryAsync(function(){
            if (list.get_effectiveBasePermissions().has(SP.PermissionKind.editListItems)){
                console.log("user has edit permission");
            }else{
                 console.log("user doesn't have edit permission");
            }   
        }, function(sender, args){
            console.log('request failed ' + args.get_message() + '\n'+ args.get_stackTrace());
        });
    }
    

    To use the same code, but for in CSOM C#, for example in provider hosted-app, you can try and modify the below code:

    using (var context = new ClientContext("https://sitecollection-url"))
    {
        Site site = context.Site;
        context.Load(site);
        context.ExecuteQuery();
    
        Web web = site.RootWeb;
        context.Load(web);
        context.ExecuteQuery();
    
        List list = web.Lists.GetByTitle("Custom List");
        context.Load(list, l => l.EffectiveBasePermissions); 
        context.ExecuteQuery();
        
        bool hasEditPermission = list.EffectiveBasePermissions.Has(PermissionKind.EditListItems);
    	if(hasEditPermission)
    	{
    		//do something
    	}
    	else
    	{
    		//user doesnt have edit permission
    	}
    	
    }
    

    SharePoint Online – Changing the ViewFields, CAML query of a list view webpart using CSOM. ( C# and PowerShell)

    Usually, we use some provisioning method, usually using PnP Provisioning tool or a custom product, to provision a large number of sites. More often than not, these sites contain pages or site pages. Now some of these pages contain webparts which are quite easy to modify like SEWP/CEWP, search webparts, content query webparts etc. But many times, they also contain List view webparts(LVWP).

    Sometimes, after a large number of sites have been provisioned, we are asked to modify some CAML query or View Fields or Row limit or something or the other in these LVWPs. To modify those values, its a bit tricky and not straightforward.

    So, to fix this issue, I have written a PS script as well as CSOM C# managed code to overcome this issue.

    Once you run the code, you will be able to change the view settings programatically(see below picture).

    qqg1y

    To run the code, you need to download and install the CSOM SDK and SharePoint Online PowerShell. If they are already installed, you can skip that and scroll below directly to the code. If not, then the download links are mentioned below:

    SharePoint Online Client Components SDK

    SharePoint Online Management Shell

    Some background info:

    By default when you add a list view webpart, a local instance of the view is created. This view is hidden from the UI and is not visible via list settings as well. However, each such view has a GUID.

    Now, that GUID which is equal to the webpart definition’s ID. Each webpart that is present on the page also has a GUID.

    Using this GUID, you can get the local instance of the listView and modify its ViewQuery,Viewfields and other such view properties.

    This is possible using both PowerShell as well as C# console application using CSOM.

    So, let’s get right to the code. Please modify it as per your SP list name, page name and webpart title.

    If you want to do it programatically using PowerShell, you can use the below code:

    
    Add-Type -Path (Resolve-Path "$env:CommonProgramFiles\Microsoft Shared\Web Server Extensions\16\ISAPI\Microsoft.SharePoint.Client.dll")
    Add-Type -Path (Resolve-Path "$env:CommonProgramFiles\Microsoft Shared\Web Server Extensions\16\ISAPI\Microsoft.SharePoint.Client.Runtime.dll")
    
    #Site collection URL
    $siteurl = "https://tenantname.sharepoint.com/sites/testsitecollection"
    
    $userName ="user.name@tenantname.com"
    $password ="password" 
    
    #client context object and setting the credentials
    [Microsoft.SharePoint.Client.ClientContext]$clientContext = New-Object Microsoft.SharePoint.Client.ClientContext($siteurl)   
    
    # convert password into secure string
    $securedpassword = ConvertTo-SecureString $password -AsPlainText -Force   
    
    $clientContext.Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($userName, $securedpassword) 
    
    #change as per your page name
    
    $PageUrl = "$SiteUrl/SitePages/Home.aspx"
    $page = $clientContext.Web.GetFileByUrl($PageUrl)
    $clientContext.Load($page)
    $clientContext.ExecuteQuery()
    
    #load the list and all the list views
    
    $list = $clientContext.Web.Lists.GetByTitle("Custom List");
    $clientContext.Load($list);
    $clientContext.Load($list.Views);
    $clientContext.ExecuteQuery();
    
    $webpartmanager = $page.GetLimitedWebPartManager([Microsoft.Sharepoint.Client.WebParts.PersonalizationScope]::Shared)
    
    #load the webpart manager
    
    $clientContext.Load($webpartmanager)
    $webParts = $webpartmanager.WebParts
    $clientContext.Load($webParts)
    $clientContext.ExecuteQuery()
    
    #iterate through the webparts on the page
    foreach($webpart in $webparts){
        $clientContext.Load($webpart.WebPart.Properties)
        $clientContext.ExecuteQuery()
        $propValues = $webpart.WebPart.Properties.FieldValues
    
    	#enter the title of the list view webpart
        if($propvalues["Title"] -eq "Custom List"){
    
            $webPartID = $webpart.ID
    
            Write-Host "webpart found"
    
            #view guid is the same as webpart definition id
            $listView = $list.Views.GetById($webpart.ID);
    
            $clientContext.Load($listView);
            $clientContext.ExecuteQuery();
    
            #remove all the viewfields
            $listView.ViewFields.RemoveAll();
    
            #Array of column internal names
            $array = @("Title", "Description");
    
            for ($i=0; $i -lt $array.length; $i++) {
                $listView.ViewFields.Add($array[$i]);
            }
    
            # changing the CAML query and setting other properties (strictly optional)      
    
            $listView.ViewQuery = "<OrderBy><FieldRef Name='ID' /></OrderBy><Where><IsNotNull><FieldRef Name='ID' /></IsNotNull></Where>";
            $listView.RowLimit = 30
    
            $listView.Update();
            $clientContext.ExecuteQuery();
    
        }
    }
    

    If, like me, you like to use CSOM C# code, you can use it as below:

    string siteUrl = "https://tenantname.sharepoint.com/sites/testsitecollection";
    
    string userName = "user.name@tenantname.com";
    string password = "password";
    
    using (ClientContext context = new ClientContext(siteUrl))
    {
    	SecureString securePassword = new SecureString();
    	foreach (char c in password.ToCharArray())
    	{
    		securePassword.AppendChar(c);
    	}	
    
    	context.AuthenticationMode = ClientAuthenticationMode.Default;
    	context.Credentials = new SharePointOnlineCredentials(userName, securePassword);
    
    	Web web = context.Site.RootWeb;
    	context.Load(web);
    	context.ExecuteQuery();	
    
    	var pageUrl = "/sites/testsitecollection/Pages/home.aspx";
    	var page = context.Web.GetFileByUrl(pageUrl);
    	context.Load(page);
    	context.ExecuteQuery();
    
    	var list = web.Lists.GetByTitle("Custom List");
    	context.Load(list, l => l.Views);
    	context.ExecuteQuery();
    
    	var webpartManager = page.GetLimitedWebPartManager(Microsoft.SharePoint.Client.WebParts.PersonalizationScope.Shared);
    	context.Load(webpartManager);
    
    	var webparts = webpartManager.WebParts;
    	context.Load(webparts);
    	context.ExecuteQuery();
    
    	string[] viewFields = { "Title", "Description" };
    
    	foreach (var webpart in webparts)
    	{
    		context.Load(webpart.WebPart.Properties);
    		context.ExecuteQuery();
    
    		var propertyValue = webpart.WebPart.Properties.FieldValues;
    
    		if (propertyValue["Title"].Equals("Custom List"))
    		{
    
    			var listView = list.Views.GetById(webpart.Id);
    
    			context.Load(listView);
    			context.ExecuteQuery();
    
    			listView.ViewFields.RemoveAll();
    			foreach (var viewField in viewFields)
    			{
    				listView.ViewFields.Add(viewField);
    			}
    
    			listView.ViewQuery = "<OrderBy><FieldRef Name='ID' /></OrderBy><Where><IsNotNull><FieldRef Name='ID' /></IsNotNull></Where>";
    			listView.RowLimit = 30;
    			listView.Update();
    			context.ExecuteQuery();
    
    		}
    
    	}
    }