diff --git a/src/lib/PnP.Framework.Test/Graph/UnifiedGroupsUtilityTests.cs b/src/lib/PnP.Framework.Test/Graph/UnifiedGroupsUtilityTests.cs new file mode 100644 index 000000000..6a7ca862c --- /dev/null +++ b/src/lib/PnP.Framework.Test/Graph/UnifiedGroupsUtilityTests.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using PnP.Framework.Entities; +using PnP.Framework.Graph; + +namespace PnP.Framework.Test.Graph +{ + [TestClass] + public class UnifiedGroupsUtilityTests + { + [TestMethod] + public void GetUnifiedGroupMembers_WhenCalled_ShouldReturnData() + { + // Arrange + TestCommon.RegisterPnPHttpClientMock(); + //var fakeHandler = new MockHttpHandler(""); + + // Act + var results = UnifiedGroupsUtility.GetUnifiedGroupMembers( + new UnifiedGroupEntity() { GroupId = "abc", }, + "testt" + ); + + // Assert + Assert.IsNotNull(results); + Assert.IsTrue(results.Count > 1); + + AssertAllPropertiesHaveBeenAssigned(results); + } + + private static void AssertAllPropertiesHaveBeenAssigned(List results) + { + foreach (PropertyInfo pi in typeof(T).GetProperties()) + { + Assert.IsTrue( + results.Any(r => + pi.GetValue(r, null) != (pi.PropertyType.IsValueType ? Activator.CreateInstance(pi.PropertyType) : null) + ) + ); + } + } + } +} diff --git a/src/lib/PnP.Framework.Test/Graph/UnifiedGroupsUtilityTests/GetUnifiedGroupMembers_WhenCalled_ShouldReturnData-http.json b/src/lib/PnP.Framework.Test/Graph/UnifiedGroupsUtilityTests/GetUnifiedGroupMembers_WhenCalled_ShouldReturnData-http.json new file mode 100644 index 000000000..f37948c43 --- /dev/null +++ b/src/lib/PnP.Framework.Test/Graph/UnifiedGroupsUtilityTests/GetUnifiedGroupMembers_WhenCalled_ShouldReturnData-http.json @@ -0,0 +1,7 @@ +[ + { + "Url": "https://graph.microsoft.com/v1.0/groups/abc/members/microsoft.graph.user", + "Body": "", + "Content": "{\r\n \"@odata.context\": \"https:\/\/graph.microsoft.com\/v1.0\/$metadata#directoryObjects\",\r\n \"value\": [\r\n {\r\n \"@odata.type\": \"#microsoft.graph.user\",\r\n \"id\": \"87d349ed-44d7-43e1-9a83-5f2406dee5bd\",\r\n \"businessPhones\": [\r\n \"+1 425 555 0109\"\r\n ],\r\n \"displayName\": \"Adele Vance\",\r\n \"givenName\": \"Adele\",\r\n \"jobTitle\": \"Product Marketing Manager\",\r\n \"mail\": \"AdeleV@M365x214355.onmicrosoft.com\",\r\n \"mobilePhone\": \"1234\",\r\n \"officeLocation\": \"18\/2111\",\r\n \"preferredLanguage\": \"en-US\",\r\n \"surname\": \"Vance\",\r\n \"userPrincipalName\": \"AdeleV@M365x214355.onmicrosoft.com\"\r\n },\r\n {\r\n \"@odata.type\": \"#microsoft.graph.user\",\r\n \"id\": \"626cbf8c-5dde-46b0-8385-9e40d64736fe\",\r\n \"businessPhones\": [\r\n \"+1 502 555 0102\"\r\n ],\r\n \"displayName\": \"Johanna Lorenz\",\r\n \"givenName\": \"Johanna\",\r\n \"jobTitle\": \"CVP Engineering\",\r\n \"mail\": \"JohannaL@M365x214355.onmicrosoft.com\",\r\n \"mobilePhone\": null,\r\n \"officeLocation\": \"23\/2102\",\r\n \"preferredLanguage\": \"en-US\",\r\n \"surname\": \"Lorenz\",\r\n \"userPrincipalName\": \"JohannaL@M365x214355.onmicrosoft.com\"\r\n },\r\n {\r\n \"@odata.type\": \"#microsoft.graph.user\",\r\n \"id\": \"40079818-3808-4585-903b-02605f061225\",\r\n \"businessPhones\": [\r\n \"+1 502 555 0144\"\r\n ],\r\n \"displayName\": \"Patti Fernandez\",\r\n \"givenName\": \"Patti\",\r\n \"jobTitle\": \"President\",\r\n \"mail\": \"PattiF@M365x214355.onmicrosoft.com\",\r\n \"mobilePhone\": null,\r\n \"officeLocation\": \"15\/1102\",\r\n \"preferredLanguage\": \"en-US\",\r\n \"surname\": \"Fernandez\",\r\n \"userPrincipalName\": \"PattiF@M365x214355.onmicrosoft.com\"\r\n },\r\n {\r\n \"@odata.type\": \"#microsoft.graph.user\",\r\n \"id\": \"074e56ea-0b50-4461-89e5-c67ae14a2c0b\",\r\n \"businessPhones\": [\r\n \"+1 913 555 0101\"\r\n ],\r\n \"displayName\": \"Lee Gu\",\r\n \"givenName\": \"Lee\",\r\n \"jobTitle\": \"CVP Research & Development\",\r\n \"mail\": \"LeeG@M365x214355.onmicrosoft.com\",\r\n \"mobilePhone\": null,\r\n \"officeLocation\": \"23\/3101\",\r\n \"preferredLanguage\": \"en-US\",\r\n \"surname\": \"Gu\",\r\n \"userPrincipalName\": \"LeeG@M365x214355.onmicrosoft.com\"\r\n },\r\n {\r\n \"@odata.type\": \"#microsoft.graph.user\",\r\n \"id\": \"16cfe710-1625-4806-9990-91b8f0afee35\",\r\n \"businessPhones\": [\r\n \"+1 205 555 0103\"\r\n ],\r\n \"displayName\": \"Enrico Cattaneo\",\r\n \"givenName\": \"Enrico\",\r\n \"jobTitle\": \"Attorney\",\r\n \"mail\": \"EnricoC@M365x214355.onmicrosoft.com\",\r\n \"mobilePhone\": null,\r\n \"officeLocation\": \"14\/1102\",\r\n \"preferredLanguage\": \"en-US\",\r\n \"surname\": \"Cattaneo\",\r\n \"userPrincipalName\": \"EnricoC@M365x214355.onmicrosoft.com\"\r\n },\r\n {\r\n \"@odata.type\": \"#microsoft.graph.user\",\r\n \"id\": \"089a6bb8-e8cb-492c-aa41-c078aa0b5120\",\r\n \"businessPhones\": [\r\n \"+1 206 555 0105\"\r\n ],\r\n \"displayName\": \"Nestor Wilke\",\r\n \"givenName\": \"Nestor\",\r\n \"jobTitle\": \"CVP Operations\",\r\n \"mail\": \"NestorW@M365x214355.onmicrosoft.com\",\r\n \"mobilePhone\": null,\r\n \"officeLocation\": \"36\/2121\",\r\n \"preferredLanguage\": \"en-US\",\r\n \"surname\": \"Wilke\",\r\n \"userPrincipalName\": \"NestorW@M365x214355.onmicrosoft.com\"\r\n },\r\n {\r\n \"@odata.type\": \"#microsoft.graph.user\",\r\n \"id\": \"2804bc07-1e1f-4938-9085-ce6d756a32d2\",\r\n \"businessPhones\": [\r\n \"+81 345550115\"\r\n ],\r\n \"displayName\": \"Emily Braun\",\r\n \"givenName\": \"Emily\",\r\n \"jobTitle\": \"Budget Analyst\",\r\n \"mail\": \"EmilyB@M365x214355.onmicrosoft.com\",\r\n \"mobilePhone\": null,\r\n \"officeLocation\": \"97\/2302\",\r\n \"preferredLanguage\": \"en-US\",\r\n \"surname\": \"Braun\",\r\n \"userPrincipalName\": \"EmilyB@M365x214355.onmicrosoft.com\"\r\n },\r\n {\r\n \"@odata.type\": \"#microsoft.graph.user\",\r\n \"id\": \"b66ecf79-a093-4d51-86e0-efcc4531f37a\",\r\n \"businessPhones\": [\r\n \"+1 858 555 0111\"\r\n ],\r\n \"displayName\": \"Christie Cline\",\r\n \"givenName\": \"Christie\",\r\n \"jobTitle\": \"Sr. VP Sales & Marketing\",\r\n \"mail\": \"ChristieC@M365x214355.onmicrosoft.com\",\r\n \"mobilePhone\": null,\r\n \"officeLocation\": \"131\/2105\",\r\n \"preferredLanguage\": \"en-US\",\r\n \"surname\": \"Cline\",\r\n \"userPrincipalName\": \"ChristieC@M365x214355.onmicrosoft.com\"\r\n },\r\n {\r\n \"@odata.type\": \"#microsoft.graph.user\",\r\n \"id\": \"8b209ac8-08ff-4ef1-896d-3b9fde0bbf04\",\r\n \"businessPhones\": [\r\n \"+1 980 555 0101\"\r\n ],\r\n \"displayName\": \"Joni Sherman\",\r\n \"givenName\": \"Joni\",\r\n \"jobTitle\": \"Paralegal\",\r\n \"mail\": \"JoniS@M365x214355.onmicrosoft.com\",\r\n \"mobilePhone\": null,\r\n \"officeLocation\": \"20\/1109\",\r\n \"preferredLanguage\": \"en-US\",\r\n \"surname\": \"Sherman\",\r\n \"userPrincipalName\": \"JoniS@M365x214355.onmicrosoft.com\"\r\n },\r\n {\r\n \"@odata.type\": \"#microsoft.graph.user\",\r\n \"id\": \"4782e723-f4f4-4af3-a76e-25e3bab0d896\",\r\n \"businessPhones\": [\r\n \"+1 858 555 0110\"\r\n ],\r\n \"displayName\": \"Alex Wilber\",\r\n \"givenName\": \"Alex\",\r\n \"jobTitle\": \"Marketing Assistant\",\r\n \"mail\": \"AlexW@M365x214355.onmicrosoft.com\",\r\n \"mobilePhone\": null,\r\n \"officeLocation\": \"131\/1104\",\r\n \"preferredLanguage\": \"en-US\",\r\n \"surname\": \"Wilber\",\r\n \"userPrincipalName\": \"AlexW@M365x214355.onmicrosoft.com\"\r\n },\r\n {\r\n \"@odata.type\": \"#microsoft.graph.user\",\r\n \"id\": \"08fa38e4-cbfa-4488-94ed-c834da6539df\",\r\n \"businessPhones\": [\r\n \"+1 858 555 0109\"\r\n ],\r\n \"displayName\": \"Miriam Graham\",\r\n \"givenName\": \"Miriam\",\r\n \"jobTitle\": \"VP Marketing\",\r\n \"mail\": \"MiriamG@M365x214355.onmicrosoft.com\",\r\n \"mobilePhone\": null,\r\n \"officeLocation\": \"131\/2103\",\r\n \"preferredLanguage\": \"en-US\",\r\n \"surname\": \"Graham\",\r\n \"userPrincipalName\": \"MiriamG@M365x214355.onmicrosoft.com\"\r\n },\r\n {\r\n \"@odata.type\": \"#microsoft.graph.user\",\r\n \"id\": \"24fcbca3-c3e2-48bf-9ffc-c7f81b81483d\",\r\n \"businessPhones\": [\r\n \"+1 205 555 0108\"\r\n ],\r\n \"displayName\": \"Diego Siciliani\",\r\n \"givenName\": \"Diego\",\r\n \"jobTitle\": \"CVP Finance\",\r\n \"mail\": \"DiegoS@M365x214355.onmicrosoft.com\",\r\n \"mobilePhone\": null,\r\n \"officeLocation\": \"14\/1108\",\r\n \"preferredLanguage\": \"en-US\",\r\n \"surname\": \"Siciliani\",\r\n \"userPrincipalName\": \"DiegoS@M365x214355.onmicrosoft.com\"\r\n },\r\n {\r\n \"@odata.type\": \"#microsoft.graph.user\",\r\n \"id\": \"2ed03dfd-01d8-4005-a9ef-fa8ee546dc6c\",\r\n \"businessPhones\": [\r\n \"+1 918 555 0107\"\r\n ],\r\n \"displayName\": \"Lidia Holloway\",\r\n \"givenName\": \"Lidia\",\r\n \"jobTitle\": \"Product Manager\",\r\n \"mail\": \"LidiaH@M365x214355.onmicrosoft.com\",\r\n \"mobilePhone\": null,\r\n \"officeLocation\": \"20\/2107\",\r\n \"preferredLanguage\": \"en-US\",\r\n \"surname\": \"Holloway\",\r\n \"userPrincipalName\": \"LidiaH@M365x214355.onmicrosoft.com\"\r\n },\r\n {\r\n \"@odata.type\": \"#microsoft.graph.user\",\r\n \"id\": \"df043ff1-49d5-414e-86a4-0c7f239c36cf\",\r\n \"businessPhones\": [\r\n \"+1 309 555 0104\"\r\n ],\r\n \"displayName\": \"Grady Archie\",\r\n \"givenName\": \"Grady\",\r\n \"jobTitle\": \"CVP Legal\",\r\n \"mail\": \"GradyA@M365x214355.onmicrosoft.com\",\r\n \"mobilePhone\": null,\r\n \"officeLocation\": \"19\/2109\",\r\n \"preferredLanguage\": \"en-US\",\r\n \"surname\": \"Archie\",\r\n \"userPrincipalName\": \"GradyA@M365x214355.onmicrosoft.com\"\r\n },\r\n {\r\n \"@odata.type\": \"#microsoft.graph.user\",\r\n \"id\": \"ec63c778-24e1-4240-bea3-d12a167d5232\",\r\n \"businessPhones\": [\r\n \"+20 255501070\"\r\n ],\r\n \"displayName\": \"Pradeep Gupta\",\r\n \"givenName\": \"Pradeep\",\r\n \"jobTitle\": \"Accountant II\",\r\n \"mail\": \"PradeepG@M365x214355.onmicrosoft.com\",\r\n \"mobilePhone\": null,\r\n \"officeLocation\": \"98\/2202\",\r\n \"preferredLanguage\": \"en-US\",\r\n \"surname\": \"Gupta\",\r\n \"userPrincipalName\": \"PradeepG@M365x214355.onmicrosoft.com\"\r\n },\r\n {\r\n \"@odata.type\": \"#microsoft.graph.user\",\r\n \"id\": \"48d31887-5fad-4d73-a9f5-3c356e68a038\",\r\n \"businessPhones\": [\r\n \"+1 412 555 0109\"\r\n ],\r\n \"displayName\": \"Megan Bowen\",\r\n \"givenName\": \"Megan\",\r\n \"jobTitle\": \"Auditor\",\r\n \"mail\": \"MeganB@M365x214355.onmicrosoft.com\",\r\n \"mobilePhone\": null,\r\n \"officeLocation\": \"12\/1110\",\r\n \"preferredLanguage\": \"en-US\",\r\n \"surname\": \"Bowen\",\r\n \"userPrincipalName\": \"MeganB@M365x214355.onmicrosoft.com\"\r\n },\r\n {\r\n \"@odata.type\": \"#microsoft.graph.user\",\r\n \"id\": \"e8a02cc7-df4d-4778-956d-784cc9506e5a\",\r\n \"businessPhones\": [\r\n \"+1 918 555 0104\"\r\n ],\r\n \"displayName\": \"Lynne Robbins\",\r\n \"givenName\": \"Lynne\",\r\n \"jobTitle\": \"Product Manager\",\r\n \"mail\": \"LynneR@M365x214355.onmicrosoft.com\",\r\n \"mobilePhone\": null,\r\n \"officeLocation\": \"20\/1104\",\r\n \"preferredLanguage\": \"en-US\",\r\n \"surname\": \"Robbins\",\r\n \"userPrincipalName\": \"LynneR@M365x214355.onmicrosoft.com\"\r\n },\r\n {\r\n \"@odata.type\": \"#microsoft.graph.user\",\r\n \"id\": \"c8913c86-ceea-4d39-b1ea-f63a5b675166\",\r\n \"businessPhones\": [\r\n \"+1 954 555 0118\"\r\n ],\r\n \"displayName\": \"Henrietta Mueller\",\r\n \"givenName\": \"Henrietta\",\r\n \"jobTitle\": \"Marketing Assistant\",\r\n \"mail\": \"HenriettaM@M365x214355.onmicrosoft.com\",\r\n \"mobilePhone\": null,\r\n \"officeLocation\": \"18\/1106\",\r\n \"preferredLanguage\": \"en-US\",\r\n \"surname\": \"Mueller\",\r\n \"userPrincipalName\": \"HenriettaM@M365x214355.onmicrosoft.com\"\r\n },\r\n {\r\n \"@odata.type\": \"#microsoft.graph.user\",\r\n \"id\": \"e3d0513b-449e-4198-ba6f-bd97ae7cae85\",\r\n \"businessPhones\": [\r\n \"+1 918 555 0101\"\r\n ],\r\n \"displayName\": \"Isaiah Langer\",\r\n \"givenName\": \"Isaiah\",\r\n \"jobTitle\": \"Web Marketing Manager\",\r\n \"mail\": \"IsaiahL@M365x214355.onmicrosoft.com\",\r\n \"mobilePhone\": null,\r\n \"officeLocation\": \"20\/1101\",\r\n \"preferredLanguage\": \"en-US\",\r\n \"surname\": \"Langer\",\r\n \"userPrincipalName\": \"IsaiahL@M365x214355.onmicrosoft.com\"\r\n }\r\n ]\r\n}" + } +] \ No newline at end of file diff --git a/src/lib/PnP.Framework.Test/Graph/UsersUtilityTests.cs b/src/lib/PnP.Framework.Test/Graph/UsersUtilityTests.cs new file mode 100644 index 000000000..b2cc75eff --- /dev/null +++ b/src/lib/PnP.Framework.Test/Graph/UsersUtilityTests.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using PnP.Framework.Entities; +using PnP.Framework.Graph; + +namespace PnP.Framework.Test.Graph +{ + [TestClass] + public class UsersUtilityTests + { + [TestMethod] + public void ListUsers_WhenCalled_ShouldReturnData() + { + // Arrange + TestCommon.RegisterPnPHttpClientMock(); + + // Act + var results = UsersUtility.ListUsers( + "123", + new[] {"postalCode"} + ); + + // Assert + Assert.IsNotNull(results); + Assert.IsTrue(results.Count > 1); + + AssertAllPropertiesHaveBeenAssigned(results); + } + + private static void AssertAllPropertiesHaveBeenAssigned(List results) + { + foreach (PropertyInfo pi in typeof(T).GetProperties()) + { + Assert.IsTrue( + results.Any(r => + pi.GetValue(r, null) != (pi.PropertyType.IsValueType ? Activator.CreateInstance(pi.PropertyType) : null) + ), + pi.Name + ); + } + } + } +} diff --git a/src/lib/PnP.Framework.Test/Graph/UsersUtilityTests/ListUsers_WhenCalled_ShouldReturnData-http.json b/src/lib/PnP.Framework.Test/Graph/UsersUtilityTests/ListUsers_WhenCalled_ShouldReturnData-http.json new file mode 100644 index 000000000..1f195086d --- /dev/null +++ b/src/lib/PnP.Framework.Test/Graph/UsersUtilityTests/ListUsers_WhenCalled_ShouldReturnData-http.json @@ -0,0 +1,7 @@ +[ + { + "Url": "https://graph.microsoft.com/v1.0/users?$top=999&$select=BusinessPhones,DisplayName,GivenName,JobTitle,Mail,MobilePhone,OfficeLocation,PreferredLanguage,Surname,UserPrincipalName,Id,AccountEnabled,postalCode", + "Body": "", + "Content": "{\r\n \"@odata.context\": \"https:\/\/graph.microsoft.com\/v1.0\/$metadata#users\",\r\n \"value\": [\r\n {\r\n \"businessPhones\": [],\r\n \"displayName\": \"Conf Room Adams\",\r\n \"givenName\": null,\r\n \"jobTitle\": null,\r\n \"mail\": \"Adams@M365x214355.onmicrosoft.com\",\r\n \"mobilePhone\": \"132\",\r\n \"officeLocation\": null,\r\n \"preferredLanguage\": null,\r\n \"surname\": null,\r\n \"userPrincipalName\": \"Adams@M365x214355.onmicrosoft.com\",\r\n \"id\": \"6e7b768e-07e2-4810-8459-485f84f8f204\",\r\n \"extraProp1\": \"xyz\",\r\n \"extraProp2\": 99\r\n\r\n },\r\n {\r\n \"businessPhones\": [\r\n \"+1 425 555 0109\"\r\n ],\r\n \"displayName\": \"Adele Vance\",\r\n \"givenName\": \"Adele\",\r\n \"jobTitle\": \"Product Marketing Manager\",\r\n \"mail\": \"AdeleV@M365x214355.onmicrosoft.com\",\r\n \"mobilePhone\": null,\r\n \"officeLocation\": \"18\/2111\",\r\n \"preferredLanguage\": \"en-US\",\r\n \"surname\": \"Vance\",\r\n \"userPrincipalName\": \"AdeleV@M365x214355.onmicrosoft.com\",\r\n \"id\": \"87d349ed-44d7-43e1-9a83-5f2406dee5bd\"\r\n },\r\n {\r\n \"businessPhones\": [\r\n \"8006427676\"\r\n ],\r\n \"displayName\": \"MOD Administrator\",\r\n \"givenName\": \"MOD\",\r\n \"jobTitle\": null,\r\n \"mail\": \"admin@M365x214355.onmicrosoft.com\",\r\n \"mobilePhone\": \"5555555555\",\r\n \"officeLocation\": null,\r\n \"preferredLanguage\": \"en-US\",\r\n \"surname\": \"Administrator\",\r\n \"userPrincipalName\": \"admin@M365x214355.onmicrosoft.com\",\r\n \"id\": \"5bde3e51-d13b-4db1-9948-fe4b109d11a7\",\r\n \"accountEnabled\": true\r\n },\r\n {\r\n \"businessPhones\": [\r\n \"+1 858 555 0110\"\r\n ],\r\n \"displayName\": \"Alex Wilber\",\r\n \"givenName\": \"Alex\",\r\n \"jobTitle\": \"Marketing Assistant\",\r\n \"mail\": \"AlexW@M365x214355.onmicrosoft.com\",\r\n \"mobilePhone\": null,\r\n \"officeLocation\": \"131\/1104\",\r\n \"preferredLanguage\": \"en-US\",\r\n \"surname\": \"Wilber\",\r\n \"userPrincipalName\": \"AlexW@M365x214355.onmicrosoft.com\",\r\n \"id\": \"4782e723-f4f4-4af3-a76e-25e3bab0d896\"\r\n },\r\n {\r\n \"businessPhones\": [\r\n \"+1 262 555 0106\"\r\n ],\r\n \"displayName\": \"Allan Deyoung\",\r\n \"givenName\": \"Allan\",\r\n \"jobTitle\": \"Corporate Security Officer\",\r\n \"mail\": \"AllanD@M365x214355.onmicrosoft.com\",\r\n \"mobilePhone\": null,\r\n \"officeLocation\": \"24\/1106\",\r\n \"preferredLanguage\": \"en-US\",\r\n \"surname\": \"Deyoung\",\r\n \"userPrincipalName\": \"AllanD@M365x214355.onmicrosoft.com\",\r\n \"id\": \"c03e6eaa-b6ab-46d7-905b-73ec7ea1f755\"\r\n }\r\n ]\r\n}" + } +] \ No newline at end of file diff --git a/src/lib/PnP.Framework/Entities/GroupEntity.cs b/src/lib/PnP.Framework/Entities/GroupEntity.cs index 0eae93898..2b50747f3 100644 --- a/src/lib/PnP.Framework/Entities/GroupEntity.cs +++ b/src/lib/PnP.Framework/Entities/GroupEntity.cs @@ -1,4 +1,5 @@ using System; +using System.Text.Json.Serialization; namespace PnP.Framework.Entities { @@ -10,6 +11,7 @@ public class GroupEntity /// /// Group id /// + [JsonPropertyName("id")] public string GroupId { get; set; } /// /// Group display name diff --git a/src/lib/PnP.Framework/Entities/GroupUser.cs b/src/lib/PnP.Framework/Entities/GroupUser.cs index dfcb15916..81c5114f8 100644 --- a/src/lib/PnP.Framework/Entities/GroupUser.cs +++ b/src/lib/PnP.Framework/Entities/GroupUser.cs @@ -1,4 +1,9 @@ using PnP.Framework.Enums; +using System; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; namespace PnP.Framework.Entities { diff --git a/src/lib/PnP.Framework/Entities/GroupUserParser.cs b/src/lib/PnP.Framework/Entities/GroupUserParser.cs new file mode 100644 index 000000000..9e2634d73 --- /dev/null +++ b/src/lib/PnP.Framework/Entities/GroupUserParser.cs @@ -0,0 +1,41 @@ +using PnP.Framework.Enums; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace PnP.Framework.Entities +{ + public class GroupUserParser + { + private class GroupUserRaw + { + + public string Id { get; set; } + public string UserPrincipalName { get; set; } + + public string DisplayName { get; set; } + + [JsonPropertyName("@odata.type")] + public string Type { get; set; } + } + public static GroupUser[] ReadListFromJsonNode(JsonNode inputJson) + { + var rawList = inputJson.Deserialize(); + return rawList.Select(raw => raw.Type.Contains("microsoft.graph.user") + ? new GroupUser + { + DisplayName = raw.DisplayName, + UserPrincipalName = raw.UserPrincipalName, + Type = GroupUserType.User, + } + : new GroupUser + { + DisplayName = raw.DisplayName, + UserPrincipalName = raw.Id, + Type = GroupUserType.Group, + }) + .ToArray(); + } + } +} diff --git a/src/lib/PnP.Framework/Entities/UnifiedGroupUser.cs b/src/lib/PnP.Framework/Entities/UnifiedGroupUser.cs index 66c44a7ff..df3395f70 100644 --- a/src/lib/PnP.Framework/Entities/UnifiedGroupUser.cs +++ b/src/lib/PnP.Framework/Entities/UnifiedGroupUser.cs @@ -1,4 +1,5 @@ using System; +using System.Text.Json.Serialization; namespace PnP.Framework.Entities { @@ -30,6 +31,7 @@ public class UnifiedGroupUser /// /// Group user's e-mail address /// + [JsonPropertyName("mail")] public string Email { get; set; } /// /// Group user's mobile phone number diff --git a/src/lib/PnP.Framework/Extensions/SiteExtensions.cs b/src/lib/PnP.Framework/Extensions/SiteExtensions.cs index 31a5dd7bb..a178cc919 100644 --- a/src/lib/PnP.Framework/Extensions/SiteExtensions.cs +++ b/src/lib/PnP.Framework/Extensions/SiteExtensions.cs @@ -1,5 +1,6 @@ using PnP.Framework; using PnP.Framework.Graph; +using PnP.Framework.Utilities; using System; using System.Collections.Generic; using System.Linq; @@ -42,7 +43,7 @@ public static void SetSiteClassification(this Site site, string classificationVa { classification = classificationValue }, - contentType: "application/json", + contentType: HttpHelper.JsonContentType, accessToken: accessToken); // Still update the local value to give prompt feedback to the user diff --git a/src/lib/PnP.Framework/Graph/GraphUtility.cs b/src/lib/PnP.Framework/Graph/GraphUtility.cs index 76e1ef47b..43c4734ba 100644 --- a/src/lib/PnP.Framework/Graph/GraphUtility.cs +++ b/src/lib/PnP.Framework/Graph/GraphUtility.cs @@ -1,8 +1,10 @@ -using Microsoft.Graph; -using PnP.Framework.Diagnostics; +using PnP.Framework.Diagnostics; +using PnP.Framework.Graph.Model; +using PnP.Framework.Utilities; using System; -using System.Net.Http.Headers; -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; namespace PnP.Framework.Graph { @@ -13,39 +15,7 @@ public static class GraphUtility { private const int defaultRetryCount = 10; private const int defaultDelay = 500; - - /// - /// Creates a new GraphServiceClient instance using a custom PnPHttpProvider - /// - /// The OAuth 2.0 Access Token to configure the HTTP bearer Authorization Header - /// Number of times to retry the request in case of throttling - /// Milliseconds to wait before retrying the request. - /// Defines the Azure Cloud deployment to use. - /// Indicates if the v1.0 (false) or beta (true) endpoint should be used at Microsoft Graph - /// -#pragma warning disable CA2000 - public static GraphServiceClient CreateGraphClient(string accessToken, int retryCount = defaultRetryCount, int delay = defaultDelay, AzureEnvironment azureEnvironment = AzureEnvironment.Production, bool useBetaEndPoint = false) - { - var baseUrl = $"https://{AuthenticationManager.GetGraphEndPoint(azureEnvironment)}/{(useBetaEndPoint ? "beta" : "v1.0")}"; - // Creates a new GraphServiceClient instance using a custom PnPHttpProvider - // which natively supports retry logic for throttled requests - // Default are 10 retries with a base delay of 500ms - var result = new GraphServiceClient(baseUrl, new DelegateAuthenticationProvider( - async (requestMessage) => - { - await Task.Run(() => - { - if (!string.IsNullOrEmpty(accessToken)) - { - // Configure the HTTP bearer Authorization Header - requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken); - } - }); - }), new PnPHttpProvider(retryCount, delay)); - - return (result); - } -#pragma warning restore CA2000 + public static JsonSerializerOptions CaseInsensitiveJsonOptions { get; } = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, }; /// /// This method sends an Azure guest user invitation to the provided email address. @@ -57,13 +27,11 @@ await Task.Run(() => /// Display name of the Guest user. /// Defines the Azure Cloud Deployment. This is used to determine the MS Graph EndPoint to call which differs per Azure Cloud deployments. Defaults to Production (graph.microsoft.com). /// - public static Invitation InviteGuestUser(string accessToken, string guestUserEmail, string redirectUri, string customizedMessage = "", string guestUserDisplayName = "", AzureEnvironment azureEnvironment = AzureEnvironment.Production) + public static Invite InviteGuestUser(string accessToken, string guestUserEmail, string redirectUri, string customizedMessage = "", string guestUserDisplayName = "", AzureEnvironment azureEnvironment = AzureEnvironment.Production) { - Invitation inviteUserResponse = null; - try { - Invitation invite = new Invitation + var invite = new Invite { InvitedUserEmailAddress = guestUserEmail }; @@ -77,23 +45,42 @@ public static Invitation InviteGuestUser(string accessToken, string guestUserEma // Form the invite email message body if (!string.IsNullOrWhiteSpace(customizedMessage)) { - InvitedUserMessageInfo inviteMsgInfo = new InvitedUserMessageInfo + var inviteMsgInfo = new Model.InvitedUserMessageInfo { CustomizedMessageBody = customizedMessage }; invite.InvitedUserMessageInfo = inviteMsgInfo; } - // Create the graph client and send the invitation. - GraphServiceClient graphClient = CreateGraphClient(accessToken, azureEnvironment: azureEnvironment); - inviteUserResponse = graphClient.Invitations.Request().AddAsync(invite).Result; + var requestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}invitations"; + var responseAsString = HttpHelper.MakePostRequestForString(requestUrl, accessToken); + return JsonSerializer.Deserialize(responseAsString); } - catch (ServiceException ex) + catch (HttpResponseException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } - return inviteUserResponse; + } + + public static IEnumerable ReadPagedDataFromRequest(string requestUrl, string accessToken, int retryCount, int delay, Func customDeserialise = null) + { + while (requestUrl != null) { + var responseData = HttpHelper.MakeGetRequestForString(requestUrl, accessToken, retryCount: retryCount, delay: delay); + + var jsonNode = JsonNode.Parse(responseData); + JsonNode valueNode = jsonNode["value"]; + var results = customDeserialise == null + ? valueNode.Deserialize(CaseInsensitiveJsonOptions) + : customDeserialise(valueNode); + + foreach (var r in results) + { + yield return r; + } + + requestUrl = jsonNode["@odata.nextLink"]?.ToString(); + }; } } } diff --git a/src/lib/PnP.Framework/Graph/GroupsUtility.cs b/src/lib/PnP.Framework/Graph/GroupsUtility.cs index d028267bf..354f67f1f 100644 --- a/src/lib/PnP.Framework/Graph/GroupsUtility.cs +++ b/src/lib/PnP.Framework/Graph/GroupsUtility.cs @@ -1,5 +1,4 @@ -using Microsoft.Graph; -using Newtonsoft.Json; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using PnP.Framework.Diagnostics; using PnP.Framework.Entities; @@ -8,8 +7,9 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Net.Http.Headers; -using System.Threading.Tasks; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Nodes; namespace PnP.Framework.Graph { @@ -18,38 +18,6 @@ namespace PnP.Framework.Graph /// public static class GroupsUtility { - private const int defaultRetryCount = 10; - private const int defaultDelay = 500; - - /// - /// Creates a new GraphServiceClient instance using a custom PnPHttpProvider - /// - /// The OAuth 2.0 Access Token to configure the HTTP bearer Authorization Header - /// Number of times to retry the request in case of throttling - /// Milliseconds to wait before retrying the request. - /// Azure environment to use, needed to get the correct Microsoft Graph URL - /// - private static GraphServiceClient CreateGraphClient(string accessToken, int retryCount = defaultRetryCount, int delay = defaultDelay, AzureEnvironment azureEnvironment = AzureEnvironment.Production) - { - // Creates a new GraphServiceClient instance using a custom PnPHttpProvider - // which natively supports retry logic for throttled requests - // Default are 10 retries with a base delay of 500ms - var result = new GraphServiceClient($"{AuthenticationManager.GetGraphBaseEndPoint(azureEnvironment)}v1.0", new DelegateAuthenticationProvider( - async (requestMessage) => - { - await Task.Run(() => - { - if (!String.IsNullOrEmpty(accessToken)) - { - // Configure the HTTP bearer Authorization Header - requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken); - } - }); - }), new PnPHttpProvider(retryCount, delay)); - - return (result); - } - /// /// Creates a new Azure Active Directory Group /// @@ -87,107 +55,79 @@ public static GroupEntity CreateGroup(string displayName, string description, st try { - // Use a synchronous model to invoke the asynchronous process - result = Task.Run(async () => - { - var group = new GroupEntity(); - - var graphClient = CreateGraphClient(accessToken, retryCount, delay, azureEnvironment); - - // Prepare the group resource object - var newGroup = new GroupExtended - { - DisplayName = displayName, - Description = string.IsNullOrEmpty(description) ? null : description, - MailNickname = mailNickname, - MailEnabled = mailEnabled, - SecurityEnabled = securityEnabled - }; - if (owners != null && owners.Length > 0) - { - var users = GetUsers(graphClient, owners); - if (users != null && users.Count > 0) - { - newGroup.OwnersODataBind = users.Select(u => string.Format("{1}/users/{0}", u.Id, graphClient.BaseUrl)).ToArray(); - } - } + // Prepare the group resource object + var newGroup = new Model.Group + { + DisplayName = displayName, + Description = string.IsNullOrEmpty(description) ? null : description, + MailNickname = mailNickname, + MailEnabled = mailEnabled, + SecurityEnabled = securityEnabled, + }; - if (members != null && members.Length > 0) + if (owners != null && owners.Length > 0) + { + var userIds = GetUserIds(accessToken, owners, retryCount, delay, azureEnvironment); + if (userIds != null && userIds.Count > 0) { - var users = GetUsers(graphClient, members); - if (users != null && users.Count > 0) - { - newGroup.MembersODataBind = users.Select(u => string.Format("{1}/users/{0}", u.Id, graphClient.BaseUrl)).ToArray(); - } + newGroup.OwnersODataBind = userIds.Select(u => string.Format("{1}/users/{0}", u, GraphHttpClient.GetGraphEndPointUrl(azureEnvironment))).ToArray(); } + } - // Create the group - Microsoft.Graph.Group addedGroup = await graphClient.Groups.Request().AddAsync(newGroup); - - if (addedGroup != null) + if (members != null && members.Length > 0) + { + var userIds = GetUserIds(accessToken, members, retryCount, delay, azureEnvironment); + if (userIds != null && userIds.Count > 0) { - group.DisplayName = addedGroup.DisplayName; - group.Description = addedGroup.Description; - group.GroupId = addedGroup.Id; - group.Mail = addedGroup.Mail; - group.MailNickname = addedGroup.MailNickname; - group.MailEnabled = addedGroup.MailEnabled; - group.SecurityEnabled = addedGroup.SecurityEnabled; - group.GroupTypes = addedGroup.GroupTypes != null ? addedGroup.GroupTypes.ToArray() : null; + newGroup.MembersODataBind = userIds.Select(u => string.Format("{1}/users/{0}", u, GraphHttpClient.GetGraphEndPointUrl(azureEnvironment))).ToArray(); } + } - return (group); - - }).GetAwaiter().GetResult(); + // Create the group + var requestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}groups"; + var responseAsString = HttpHelper.MakePostRequestForString(requestUrl, newGroup, accessToken: accessToken, retryCount: retryCount, delay: delay); + return System.Text.Json.JsonSerializer.Deserialize(responseAsString); } - catch (ServiceException ex) + catch (HttpRequestException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } - return (result); } /// /// Updates the members of an Azure Active Directory Group /// /// UPNs of users that need to be added as a member to the group - /// GraphClient instance to use to communicate with the Microsoft Graph /// Id of the group which needs the owners added /// If set to true, all existing members which are not specified through will be removed as a member from the group - private static async Task UpdateMembers(string[] members, GraphServiceClient graphClient, string groupId, bool removeOtherMembers) + private static void UpdateMembers(string[] members, string groupId, bool removeOtherMembers, string accessToken, int retryCount, int delay, AzureEnvironment azureEnvironment) { + var userRequestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}users"; + var groupRequestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}groups/{groupId}"; foreach (var m in members) { // Search for the user object - var memberQuery = await graphClient.Users - .Request() - .Filter($"userPrincipalName eq '{Uri.EscapeDataString(m.Replace("'", "''"))}'") - .GetAsync(); - - var member = memberQuery.FirstOrDefault(); - - if (member != null) + string upn = Uri.EscapeDataString(m.Replace("'", "''")); + var requestUrl = $"{userRequestUrl}?$filter=userPrincipalName eq '{upn}'&$select=id"; + var responseAsString = HttpHelper.MakeGetRequestForString(requestUrl, accessToken, retryCount: retryCount, delay: delay); + var jsonNode = JsonNode.Parse(responseAsString); + var userListString = jsonNode["value"]; + var user = userListString.AsArray().FirstOrDefault()?.Deserialize(); + + if (user != null) { try { - // And if any, add it to the collection of group's owners - await graphClient.Groups[groupId].Members.References.Request().AddAsync(member); + // And if any, add it to the collection of group's members + var memberUrl = $"{groupRequestUrl}/members/{user.Id}/ref"; + HttpHelper.MakePostRequest(memberUrl, accessToken, retryCount: retryCount, delay: delay); } - catch (Exception ex) - { - if (ex.Message.Contains("Request_BadRequest") && + catch (Exception ex) when (ex.Message.Contains("Request_BadRequest") && ex.Message.Contains("added object references already exist")) - { - // Skip any already existing member - } - else - { -#pragma warning disable CA2200 - throw ex; -#pragma warning restore CA2200 - } + { + // Skip any already existing member } } } @@ -198,44 +138,25 @@ private static async Task UpdateMembers(string[] members, GraphServiceClient gra return; } - // Remove any leftover member - var fullListOfMembers = await graphClient.Groups[groupId].Members.Request().Select("userPrincipalName, Id").GetAsync(); - var pageExists = true; - - while (pageExists) + // Remove any leftover members + var listMembersUrl = $"{groupRequestUrl}/members/microsoft.graph.user?$select=id,userPrincipalName"; + var allMembersInGroup = GraphUtility.ReadPagedDataFromRequest(listMembersUrl, accessToken, retryCount, delay); + foreach (var member in allMembersInGroup) { - foreach (var member in fullListOfMembers) + var currentMemberPrincipalName = member.UserPrincipalName; + if (!string.IsNullOrEmpty(currentMemberPrincipalName) && + !members.Contains(currentMemberPrincipalName, StringComparer.InvariantCultureIgnoreCase)) { - var currentMemberPrincipalName = (member as Microsoft.Graph.User)?.UserPrincipalName; - if (!string.IsNullOrEmpty(currentMemberPrincipalName) && - !members.Contains(currentMemberPrincipalName, StringComparer.InvariantCultureIgnoreCase)) + try { - try - { - // If it is not in the list of current owners, just remove it - await graphClient.Groups[groupId].Members[member.Id].Reference.Request().DeleteAsync(); - } - catch (ServiceException ex) - { - if (ex.Error.Code == "Request_BadRequest") - { - // Skip any failing removal - } - else - { - throw ex; - } - } + // If it is not in the list of current members, just remove it + var memberUrl = $"{groupRequestUrl}/members/{member.Id}/ref"; + HttpHelper.MakeDeleteRequest(memberUrl, accessToken, retryCount: retryCount, delay: delay); + } + catch (HttpResponseException ex) when (ex.StatusCode == 400) + { + // Skip any failing removal } - } - - if (fullListOfMembers.NextPageRequest != null) - { - fullListOfMembers = await fullListOfMembers.NextPageRequest.GetAsync(); - } - else - { - pageExists = false; } } } @@ -247,38 +168,32 @@ private static async Task UpdateMembers(string[] members, GraphServiceClient gra /// GraphClient instance to use to communicate with the Microsoft Graph /// Id of the group which needs the owners added /// If set to true, all existing owners which are not specified through will be removed as an owner from the group - private static async Task UpdateOwners(string[] owners, GraphServiceClient graphClient, string groupId, bool removeOtherOwners) + private static void UpdateOwners(string[] owners, string groupId, bool removeOtherOwners, string accessToken, int retryCount, int delay, AzureEnvironment azureEnvironment) { + var userRequestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}users"; + var groupRequestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}groups/{groupId}"; foreach (var o in owners) { // Search for the user object - var ownerQuery = await graphClient.Users - .Request() - .Filter($"userPrincipalName eq '{Uri.EscapeDataString(o.Replace("'", "''"))}'") - .GetAsync(); - - var owner = ownerQuery.FirstOrDefault(); - - if (owner != null) + string upn = Uri.EscapeDataString(o.Replace("'", "''")); + var requestUrl = $"{userRequestUrl}?$filter=userPrincipalName eq '{upn}'&$select=id"; + var responseAsString = HttpHelper.MakeGetRequestForString(requestUrl, accessToken, retryCount: retryCount, delay: delay); + var jsonNode = JsonNode.Parse(responseAsString); + var userListString = jsonNode["value"]; + var user = userListString.AsArray().FirstOrDefault()?.Deserialize(); + + if (user != null) { try { // And if any, add it to the collection of group's owners - await graphClient.Groups[groupId].Owners.References.Request().AddAsync(owner); + var memberUrl = $"{groupRequestUrl}/owners/{user.Id}/ref"; + HttpHelper.MakePostRequest(memberUrl, accessToken, retryCount: retryCount, delay: delay); } - catch (Exception ex) - { - if (ex.Message.Contains("Request_BadRequest") && + catch (Exception ex) when (ex.Message.Contains("Request_BadRequest") && ex.Message.Contains("added object references already exist")) - { - // Skip any already existing owner - } - else - { -#pragma warning disable CA2200 - throw ex; -#pragma warning restore CA2200 - } + { + // Skip any already existing member } } } @@ -290,43 +205,24 @@ private static async Task UpdateOwners(string[] owners, GraphServiceClient graph } // Remove any leftover owner - var fullListOfOwners = await graphClient.Groups[groupId].Owners.Request().Select("userPrincipalName, Id").GetAsync(); - var pageExists = true; - - while (pageExists) + var listOwnersUrl = $"{groupRequestUrl}/members/microsoft.graph.user?$select=id,userPrincipalName"; + var allOwnersInGroup = GraphUtility.ReadPagedDataFromRequest(listOwnersUrl, accessToken, retryCount, delay); + foreach (var owner in allOwnersInGroup) { - foreach (var owner in fullListOfOwners) + var currentOwnerPrincipalName = owner.UserPrincipalName; + if (!string.IsNullOrEmpty(currentOwnerPrincipalName) && + !owners.Contains(currentOwnerPrincipalName, StringComparer.InvariantCultureIgnoreCase)) { - var currentOwnerPrincipalName = (owner as Microsoft.Graph.User)?.UserPrincipalName; - if (!string.IsNullOrEmpty(currentOwnerPrincipalName) && - !owners.Contains(currentOwnerPrincipalName, StringComparer.InvariantCultureIgnoreCase)) + try { - try - { - // If it is not in the list of current owners, just remove it - await graphClient.Groups[groupId].Owners[owner.Id].Reference.Request().DeleteAsync(); - } - catch (ServiceException ex) - { - if (ex.Error.Code == "Request_BadRequest") - { - // Skip any failing removal - } - else - { - throw ex; - } - } + // If it is not in the list of current owners, just remove it + var memberUrl = $"{groupRequestUrl}/owners/{owner.Id}/ref"; + HttpHelper.MakeDeleteRequest(memberUrl, accessToken, retryCount: retryCount, delay: delay); + } + catch (HttpResponseException ex) when (ex.StatusCode == 400) + { + // Skip any failing removal } - } - - if (fullListOfOwners.NextPageRequest != null) - { - fullListOfOwners = await fullListOfOwners.NextPageRequest.GetAsync(); - } - else - { - pageExists = false; } } } @@ -359,7 +255,7 @@ public static void SetGroupVisibility(string groupId, string accessToken, bool? try { string updateGroupUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}groups/{groupId}"; - var groupRequest = new Model.Group + var groupRequest = new Model.GroupPatchModel { HideFromAddressLists = hideFromAddressLists, HideFromOutlookClients = hideFromOutlookClients @@ -368,12 +264,12 @@ public static void SetGroupVisibility(string groupId, string accessToken, bool? var response = GraphHttpClient.MakePatchRequestForString( requestUrl: updateGroupUrl, content: JsonConvert.SerializeObject(groupRequest), - contentType: "application/json", + contentType: HttpHelper.JsonContentType, accessToken: accessToken); } - catch (ServiceException ex) + catch (HttpResponseException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } } @@ -400,93 +296,84 @@ public static bool UpdateGroup(string groupId, bool result; try { - // Use a synchronous model to invoke the asynchronous process - result = Task.Run(async () => - { - var graphClient = CreateGraphClient(accessToken, retryCount, delay, azureEnvironment); + var groupRequestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}groups/{groupId}"; + var responseAsString = HttpHelper.MakeGetRequestForString(groupRequestUrl, accessToken, retryCount: retryCount, delay: delay); + var groupJson = JsonNode.Parse(responseAsString); + var groupToUpdate = groupJson.Deserialize(); - var groupToUpdate = await graphClient.Groups[groupId] - .Request() - .GetAsync(); - - // Workaround for the PATCH request, needed after update to Graph Library - var clonedGroup = new Group - { - Id = groupToUpdate.Id - }; - - #region Logic to update the group DisplayName and Description - - var updateGroup = false; - var groupUpdated = false; + // Workaround for the PATCH request, needed after update to Graph Library + var clonedGroup = new Model.Group + { + GroupId = groupToUpdate.GroupId, + }; - // Check if we have to update the DisplayName - if (!String.IsNullOrEmpty(displayName) && groupToUpdate.DisplayName != displayName) - { - clonedGroup.DisplayName = displayName; - updateGroup = true; - } + #region Logic to update the group DisplayName and Description - // Check if we have to update the Description - if (!String.IsNullOrEmpty(description) && groupToUpdate.Description != description) - { - clonedGroup.Description = description; - updateGroup = true; - } + var updateGroup = false; + var groupUpdated = false; - // Check if we need to update owners - if (owners != null && owners.Length > 0) - { - // For each and every owner - await UpdateOwners(owners, graphClient, groupToUpdate.Id, true); - updateGroup = true; - } + // Check if we have to update the DisplayName + if (!String.IsNullOrEmpty(displayName) && groupToUpdate.DisplayName != displayName) + { + clonedGroup.DisplayName = displayName; + updateGroup = true; + } - // Check if we need to update members - if (members != null && members.Length > 0) - { - // For each and every owner - await UpdateMembers(members, graphClient, groupToUpdate.Id, true); - updateGroup = true; - } + // Check if we have to update the Description + if (!String.IsNullOrEmpty(description) && groupToUpdate.Description != description) + { + clonedGroup.Description = description; + updateGroup = true; + } - // Check if we have to update the MailEnabled property - if (groupToUpdate.MailEnabled.HasValue) - { - clonedGroup.MailEnabled = groupToUpdate.MailEnabled.Value; - updateGroup = true; - } + // Check if we need to update owners + if (owners != null && owners.Length > 0) + { + // For each and every owner + UpdateOwners(owners, groupToUpdate.GroupId, true, accessToken, retryCount, delay, azureEnvironment); + updateGroup = true; + } - // Check if we have to update the SecurityEnabled property - if (groupToUpdate.SecurityEnabled.HasValue) - { - clonedGroup.SecurityEnabled = groupToUpdate.SecurityEnabled.Value; - updateGroup = true; - } + // Check if we need to update members + if (members != null && members.Length > 0) + { + // For each and every owner + UpdateMembers(members, groupToUpdate.GroupId, true, accessToken, retryCount, delay, azureEnvironment); + updateGroup = true; + } - // If the Group has to be updated, just do it - if (updateGroup) - { - var updatedGroup = await graphClient.Groups[groupId] - .Request() - .UpdateAsync(clonedGroup); + // Check if we have to update the MailEnabled property + if (mailEnabled.HasValue && mailEnabled != groupToUpdate.MailEnabled) + { + clonedGroup.MailEnabled = mailEnabled.Value; + updateGroup = true; + } - groupUpdated = true; - } + // Check if we have to update the SecurityEnabled property + if (securityEnabled.HasValue && securityEnabled != groupToUpdate.SecurityEnabled) + { + clonedGroup.SecurityEnabled = securityEnabled.Value; + updateGroup = true; + } - #endregion + // If the Group has to be updated, just do it + if (updateGroup) + { + var updatedGroup = HttpHelper.MakePatchRequestForString(groupRequestUrl, clonedGroup, HttpHelper.JsonContentType, accessToken, retryCount: retryCount, delay: delay); + groupUpdated = true; + } - // If any of the previous update actions has been completed - return groupUpdated; + #endregion - }).GetAwaiter().GetResult(); + // If any of the previous update actions has been completed + return groupUpdated; } - catch (ServiceException ex) + catch (HttpResponseException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } - return (result); + return result; } /// @@ -510,17 +397,12 @@ public static void DeleteGroup(string groupId, string accessToken, int retryCoun } try { - // Use a synchronous model to invoke the asynchronous process - Task.Run(async () => - { - var graphClient = CreateGraphClient(accessToken, retryCount, delay, azureEnvironment); - await graphClient.Groups[groupId].Request().DeleteAsync(); - - }).GetAwaiter().GetResult(); + var requestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}groups/{groupId}"; + HttpHelper.MakeDeleteRequest(requestUrl, accessToken, retryCount: retryCount, delay: delay); } - catch (ServiceException ex) + catch (HttpResponseException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } } @@ -535,6 +417,12 @@ public static void DeleteGroup(string groupId, string accessToken, int retryCoun /// Azure environment to use, needed to get the correct Microsoft Graph URL /// Group instance if found public static GroupEntity GetGroup(string groupId, string accessToken, int retryCount = 10, int delay = 500, AzureEnvironment azureEnvironment = AzureEnvironment.Production) + { + var group = GetRawGroup(groupId, accessToken, retryCount, delay, azureEnvironment); + return group.AsEntity(); + } + + internal static PnP.Framework.Graph.Model.Group GetRawGroup(string groupId, string accessToken, int retryCount = 10, int delay = 500, AzureEnvironment azureEnvironment = AzureEnvironment.Production) { if (string.IsNullOrEmpty(groupId)) { @@ -546,40 +434,19 @@ public static GroupEntity GetGroup(string groupId, string accessToken, int retry throw new ArgumentNullException(nameof(accessToken)); } - GroupEntity result = null; try { - // Use a synchronous model to invoke the asynchronous process - result = Task.Run(async () => - { - GroupEntity group = null; - - var graphClient = CreateGraphClient(accessToken, retryCount, delay, azureEnvironment); - - var g = await graphClient.Groups[groupId].Request().GetAsync(); - - group = new GroupEntity - { - GroupId = g.Id, - DisplayName = g.DisplayName, - Description = g.Description, - Mail = g.Mail, - MailNickname = g.MailNickname, - MailEnabled = g.MailEnabled, - SecurityEnabled = g.SecurityEnabled, - GroupTypes = g.GroupTypes != null ? g.GroupTypes.ToArray() : null - }; - - return (group); - - }).GetAwaiter().GetResult(); + var requestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}groups/{groupId}"; + var responseAsString = HttpHelper.MakeGetRequestForString(requestUrl, accessToken, retryCount: retryCount, delay: delay); + var groupJson = JsonNode.Parse(responseAsString); + var group = groupJson.Deserialize(); + return group; } - catch (ServiceException ex) + catch (HttpResponseException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } - return (result); } /// @@ -608,68 +475,34 @@ public static List GetGroups(string accessToken, List result = null; try { - // Use a synchronous model to invoke the asynchronous process - result = Task.Run(async () => - { - List groups = new List(); + var requestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}groups?$top={pageSize}"; + var filter = string.Empty; + filter += !string.IsNullOrEmpty(displayName) ? $"(DisplayName eq '{Uri.EscapeDataString(displayName.Replace("'", "''"))}')" : string.Empty; + filter += !string.IsNullOrEmpty(mailNickname) ? $"(MailNickname eq '{Uri.EscapeDataString(mailNickname.Replace("'", "''"))}')" : string.Empty; - var graphClient = CreateGraphClient(accessToken, retryCount, delay, azureEnvironment); - - // Apply the DisplayName filter, if any - var displayNameFilter = !string.IsNullOrEmpty(displayName) ? $"(DisplayName eq '{Uri.EscapeDataString(displayName.Replace("'", "''"))}')" : string.Empty; - var mailNicknameFilter = !string.IsNullOrEmpty(mailNickname) ? $"(MailNickname eq '{Uri.EscapeDataString(mailNickname.Replace("'", "''"))}')" : string.Empty; - - var pagedGroups = await graphClient.Groups - .Request() - .Filter($"{displayNameFilter}{(!string.IsNullOrEmpty(displayNameFilter) && !string.IsNullOrEmpty(mailNicknameFilter) ? " and " : "")}{mailNicknameFilter}") - .Top(pageSize) - .GetAsync(); - - Int32 pageCount = 0; - Int32 currentIndex = 0; + if (!string.IsNullOrWhiteSpace(filter)) + { + requestUrl += $"&$filter={filter}"; + } - while (true) + List groups = new List(); + int currentIndex = 0; + foreach (var g in GraphUtility.ReadPagedDataFromRequest(requestUrl, accessToken, retryCount, delay)) + { + if (groups.Count >= endIndex) { - pageCount++; - - foreach (var g in pagedGroups) - { - currentIndex++; - - if (currentIndex >= startIndex) - { - var group = new GroupEntity - { - GroupId = g.Id, - DisplayName = g.DisplayName, - Description = g.Description, - Mail = g.Mail, - MailNickname = g.MailNickname, - MailEnabled = g.MailEnabled, - SecurityEnabled = g.SecurityEnabled, - GroupTypes = g.GroupTypes != null ? g.GroupTypes.ToArray() : null - }; - - groups.Add(group); - } - } - - if (pagedGroups.NextPageRequest != null && (endIndex == null || groups.Count < endIndex)) - { - pagedGroups = await pagedGroups.NextPageRequest.GetAsync(); - } - else - { - break; - } + break; } - - return (groups); - }).GetAwaiter().GetResult(); + if (currentIndex >= startIndex) + { + groups.Add(g.AsEntity()); + } + currentIndex++; + } } - catch (ServiceException ex) + catch (HttpResponseException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } return (result); @@ -686,9 +519,6 @@ public static List GetGroups(string accessToken, /// Members of an Azure Active Directory group public static List GetGroupMembers(GroupEntity group, string accessToken, int retryCount = 10, int delay = 500, AzureEnvironment azureEnvironment = AzureEnvironment.Production) { - List groupUsers = null; - List groupGraphUsers = null; - IGroupMembersCollectionWithReferencesPage groupUsersCollection = null; if (String.IsNullOrEmpty(accessToken)) { @@ -701,69 +531,22 @@ public static List GetGroupMembers(GroupEntity group, string accessTo try { - var result = Task.Run(async () => - { - var graphClient = CreateGraphClient(accessToken, retryCount, delay, azureEnvironment); - - // Get the members of the group - groupUsersCollection = await graphClient.Groups[group.GroupId].Members.Request().GetAsync(); - if (groupUsersCollection.CurrentPage != null && groupUsersCollection.CurrentPage.Count > 0) - { - groupGraphUsers = new List(); - groupGraphUsers.AddRange(groupUsersCollection.CurrentPage); - //GenerateGraphUserCollection(groupUsersCollection.CurrentPage, groupGraphUsers); - } - - // Retrieve users when the results are paged. - while (groupUsersCollection.NextPageRequest != null) - { - groupUsersCollection = groupUsersCollection.NextPageRequest.GetAsync().GetAwaiter().GetResult(); - if (groupUsersCollection.CurrentPage != null && groupUsersCollection.CurrentPage.Count > 0) - { - groupGraphUsers.AddRange(groupUsersCollection.CurrentPage); - //GenerateGraphUserCollection(groupUsersCollection.CurrentPage, groupGraphUsers); - } - } - - // Create the collection of type OfficeDevPnP groupuser after all users are retrieved, including paged data. - if (groupGraphUsers != null && groupGraphUsers.Count > 0) - { - groupUsers = new List(); - foreach (DirectoryObject usr in groupGraphUsers) - { - switch(usr) - { - case Microsoft.Graph.User userType: - groupUsers.Add(new GroupUser - { - UserPrincipalName = userType.UserPrincipalName != null ? userType.UserPrincipalName : string.Empty, - DisplayName = userType.DisplayName != null ? userType.DisplayName : string.Empty, - Type = Enums.GroupUserType.User - }); - break; - - case Microsoft.Graph.Group groupType: - groupUsers.Add(new GroupUser - { - UserPrincipalName = groupType.Id != null ? groupType.Id : string.Empty, - DisplayName = groupType.DisplayName != null ? groupType.DisplayName : string.Empty, - Type = Enums.GroupUserType.Group - }); - break; - } - - } - } - return groupUsers; - - }).GetAwaiter().GetResult(); + var groupRequestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}groups/{group.GroupId}"; + var listMembersUrl = $"{groupRequestUrl}/members"; + var results = GraphUtility.ReadPagedDataFromRequest( + listMembersUrl, + accessToken, + retryCount, + delay, + GroupUserParser.ReadListFromJsonNode + ); + return results.ToList(); } - catch (ServiceException ex) + catch (HttpResponseException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } - return groupUsers; } /// @@ -785,17 +568,11 @@ public static void AddGroupOwners(string groupId, string[] owners, string access try { - Task.Run(async () => - { - var graphClient = CreateGraphClient(accessToken, retryCount, delay, azureEnvironment); - - await UpdateOwners(owners, graphClient, groupId, removeExistingOwners); - - }).GetAwaiter().GetResult(); + UpdateOwners(owners, groupId, removeExistingOwners, accessToken, retryCount, delay, azureEnvironment); } - catch (ServiceException ex) + catch (HttpResponseException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } } @@ -819,17 +596,11 @@ public static void AddGroupMembers(string groupId, string[] members, string acce try { - Task.Run(async () => - { - var graphClient = CreateGraphClient(accessToken, retryCount, delay, azureEnvironment); - - await UpdateMembers(members, graphClient, groupId, removeExistingMembers); - - }).GetAwaiter().GetResult(); + UpdateMembers(members, groupId, removeExistingMembers, accessToken, retryCount, delay, azureEnvironment); } - catch (ServiceException ex) + catch (HttpResponseException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } } @@ -852,46 +623,37 @@ public static void RemoveGroupMembers(string groupId, string[] members, string a try { - Task.Run(async () => + var userRequestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}users"; + var groupRequestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}groups/{groupId}"; + + foreach (var m in members) { - var graphClient = CreateGraphClient(accessToken, retryCount, delay, azureEnvironment); + // Search for the user object + string upn = Uri.EscapeDataString(m.Replace("'", "''")); + var requestUrl = $"{userRequestUrl}?$filter=userPrincipalName eq '{upn}'&$select=id"; + var responseAsString = HttpHelper.MakeGetRequestForString(requestUrl, accessToken, retryCount: retryCount, delay: delay); + var jsonNode = JsonNode.Parse(responseAsString); + var userListString= jsonNode["value"]; + var userId = userListString.AsArray().FirstOrDefault()?["id"]; - foreach (var m in members) + if (userId != null) { - // Search for the user object - var memberQuery = await graphClient.Users - .Request() - .Filter($"userPrincipalName eq '{Uri.EscapeDataString(m.Replace("'", "''"))}'") - .GetAsync(); - - var member = memberQuery.FirstOrDefault(); - - if (member != null) + try { - try - { - // If it is not in the list of current members, just remove it - await graphClient.Groups[groupId].Members[member.Id].Reference.Request().DeleteAsync(); - } - catch (ServiceException ex) - { - if (ex.Error.Code == "Request_BadRequest") - { - // Skip any failing removal - } - else - { - throw ex; - } - } + // If it is not in the list of current members, just remove it + var deleteGroupMemberUrl = $"{groupRequestUrl}/members/{userId}/ref"; + HttpHelper.MakeDeleteRequest(deleteGroupMemberUrl, accessToken, retryCount: retryCount, delay: delay); + } + catch (HttpResponseException ex) when (ex.StatusCode == 400) + { + // Skip any failing removal } } - - }).GetAwaiter().GetResult(); + } } - catch (ServiceException ex) + catch (HttpResponseException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } } @@ -914,32 +676,37 @@ public static void RemoveGroupOwners(string groupId, string[] owners, string acc try { - Task.Run(async () => + var userRequestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}users"; + var groupRequestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}groups/{groupId}"; + + foreach (var m in owners) { - var graphClient = CreateGraphClient(accessToken, retryCount, delay, azureEnvironment); + // Search for the user object + string upn = Uri.EscapeDataString(m.Replace("'", "''")); + var requestUrl = $"{userRequestUrl}?$filter=userPrincipalName eq '{upn}'&$select=id"; + var responseAsString = HttpHelper.MakeGetRequestForString(requestUrl, accessToken, retryCount: retryCount, delay: delay); + var jsonNode = JsonNode.Parse(responseAsString); + var userListString = jsonNode["value"]; + var userId = userListString.AsArray().FirstOrDefault()?["id"]; - foreach (var m in owners) + if (userId != null) { - // Search for the user object - var memberQuery = await graphClient.Users - .Request() - .Filter($"userPrincipalName eq '{Uri.EscapeDataString(m.Replace("'", "''"))}'") - .GetAsync(); - - var member = memberQuery.FirstOrDefault(); - - if (member != null) + try { // If it is not in the list of current owners, just remove it - await graphClient.Groups[groupId].Owners[member.Id].Reference.Request().DeleteAsync(); + var deleteGroupMemberUrl = $"{groupRequestUrl}/owners/{userId}/ref"; + HttpHelper.MakeDeleteRequest(deleteGroupMemberUrl, accessToken, retryCount: retryCount, delay: delay); + } + catch (HttpResponseException ex) when (ex.StatusCode == 400) + { + // Skip any failing removal } } - - }).GetAwaiter().GetResult(); + } } - catch (ServiceException ex) + catch (HttpResponseException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } } @@ -963,9 +730,9 @@ public static void ClearGroupOwners(string groupId, string accessToken, int retr var currentOwners = GetGroupOwners(new GroupEntity { GroupId = groupId }, accessToken, retryCount, delay); RemoveGroupOwners(groupId, currentOwners.Select(o => o.UserPrincipalName).ToArray(), accessToken, retryCount, delay); } - catch (ServiceException ex) + catch (HttpResponseException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } } @@ -991,9 +758,9 @@ public static void ClearGroupMembers(string groupId, string accessToken, int ret RemoveGroupMembers(groupId, currentMembers.Select(o => o.UserPrincipalName).ToArray(), accessToken, retryCount, delay); } - catch (ServiceException ex) + catch (HttpResponseException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } } @@ -1009,10 +776,6 @@ public static void ClearGroupMembers(string groupId, string accessToken, int ret /// Owners of an Azure Active Directory group public static List GetGroupOwners(GroupEntity group, string accessToken, int retryCount = 10, int delay = 500, AzureEnvironment azureEnvironment = AzureEnvironment.Production) { - List groupUsers = null; - List groupGraphUsers = null; - IGroupOwnersCollectionWithReferencesPage groupUsersCollection = null; - if (String.IsNullOrEmpty(accessToken)) { throw new ArgumentNullException(nameof(accessToken)); @@ -1020,191 +783,63 @@ public static List GetGroupOwners(GroupEntity group, string accessTok try { - var result = Task.Run(async () => - { - var graphClient = CreateGraphClient(accessToken, retryCount, delay, azureEnvironment); - - // Get the owners of an Office 365 group. - groupUsersCollection = await graphClient.Groups[group.GroupId].Owners.Request().GetAsync(); - if (groupUsersCollection.CurrentPage != null && groupUsersCollection.CurrentPage.Count > 0) - { - groupGraphUsers = new List(); - GenerateGraphUserCollection(groupUsersCollection.CurrentPage, groupGraphUsers); - } - - // Retrieve users when the results are paged. - while (groupUsersCollection.NextPageRequest != null) - { - groupUsersCollection = groupUsersCollection.NextPageRequest.GetAsync().GetAwaiter().GetResult(); - if (groupUsersCollection.CurrentPage != null && groupUsersCollection.CurrentPage.Count > 0) - { - GenerateGraphUserCollection(groupUsersCollection.CurrentPage, groupGraphUsers); - } - } - - // Create the collection of type OfficeDevPnP 'UnifiedGroupUser' after all users are retrieved, including paged data. - if (groupGraphUsers != null && groupGraphUsers.Count > 0) - { - groupUsers = new List(); - foreach (DirectoryObject usr in groupGraphUsers) - { - switch(usr) - { - case Microsoft.Graph.User userType: - groupUsers.Add(new GroupUser - { - UserPrincipalName = userType.UserPrincipalName != null ? userType.UserPrincipalName : string.Empty, - DisplayName = userType.DisplayName != null ? userType.DisplayName : string.Empty, - Type = Enums.GroupUserType.User - }); - break; - - case Microsoft.Graph.Group groupType: - groupUsers.Add(new GroupUser - { - UserPrincipalName = groupType.Id != null ? groupType.Id : string.Empty, - DisplayName = groupType.DisplayName != null ? groupType.DisplayName : string.Empty, - Type = Enums.GroupUserType.Group - }); - break; - } - - } - } - return groupUsers; - - }).GetAwaiter().GetResult(); + var groupRequestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}groups/{group.GroupId}"; + var listMembersUrl = $"{groupRequestUrl}/owners"; + var results = GraphUtility.ReadPagedDataFromRequest( + listMembersUrl, + accessToken, + retryCount, + delay, + GroupUserParser.ReadListFromJsonNode + ); + return results.ToList(); } - catch (ServiceException ex) + catch (HttpResponseException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } - return groupUsers; - } - - /// - /// Helper method. Generates a collection of Microsoft.Graph.User entity from directory objects. - /// - /// - /// - /// Returns a collection of Microsoft.Graph.User entity - private static List GenerateGraphUserCollection(IList page, List groupGraphUsers) - { - // Create a collection of Microsoft.Graph.User type - foreach (User usr in page) - { - if (usr != null) - { - groupGraphUsers.Add(usr); - } - } - - return groupGraphUsers; } /// - /// Helper method. Generates a neseted collection of Microsoft.Graph.User entity from directory objects. + /// Helper method. Generates a collection of Ids from string array /// - /// - /// - /// /// + /// String array of users + /// + /// /// - private static List GenerateNestedGraphUserCollection(IList page, List groupGraphUsers, List groupUsers, string accessToken) + private static List GetUserIds(string accessToken, string[] groupUsers, int retryCount, int delay, AzureEnvironment azureEnvironment) { - // Create a collection of Microsoft.Graph.User type - foreach (var usr in page) + if (groupUsers == null || groupUsers.Length == 0) { - - if (usr != null) - { - if (usr.GetType() == typeof(User)) - { - groupGraphUsers.Add((User)usr); - } - } + return new List(); } - // Get groups within the group and users in that group - List unifiedGroupGraphGroups = new List(); - GenerateGraphGroupCollection(page, unifiedGroupGraphGroups); - foreach (Group unifiedGroupGraphGroup in unifiedGroupGraphGroups) + var usersResult = new List(); + foreach (var groupUser in groupUsers) { - var grp = GetGroup(unifiedGroupGraphGroup.Id, accessToken); - groupUsers.AddRange(GetGroupMembers(grp, accessToken)); - } - - return groupGraphUsers; - } + try + { + var requestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}users?$select=Id&$filter=userPrincipalName eq '{Uri.EscapeDataString(groupUser.Replace("'", "''"))}'"; + var responseAsString = HttpHelper.MakeGetRequestForString(requestUrl, accessToken, retryCount: retryCount, delay: delay); - /// - /// Helper method. Generates a collection of Microsoft.Graph.Group entity from directory objects. - /// - /// - /// - /// - private static List GenerateGraphGroupCollection(IList page, List groupGraphGroups) - { - // Create a collection of Microsoft.Graph.Group type - foreach (var grp in page) - { + var jsonNode = JsonNode.Parse(responseAsString); + var usersArray = jsonNode["value"].AsArray(); + var id = usersArray.FirstOrDefault()?["id"]?.GetValue(); - if (grp != null) - { - if (grp.GetType() == typeof(Group)) + if (id != null) { - groupGraphGroups.Add((Group)grp); + usersResult.Add(id.Value.ToString()); } } - } - - return groupGraphGroups; - } - - /// - /// Helper method. Generates a collection of Microsoft.Graph.User entity from string array - /// - /// Graph service client - /// String array of users - /// - - private static List GetUsers(GraphServiceClient graphClient, string[] groupUsers) - { - if (groupUsers == null || groupUsers.Length == 0) - { - return new List(); - } - - var result = Task.Run(async () => - { - var usersResult = new List(); - foreach (string groupUser in groupUsers) + catch (HttpResponseException) { - try - { - // Search for the user object - IGraphServiceUsersCollectionPage userQuery = await graphClient.Users - .Request() - .Select("Id") - .Filter($"userPrincipalName eq '{Uri.EscapeDataString(groupUser.Replace("'", "''"))}'") - .GetAsync(); - - User user = userQuery.FirstOrDefault(); - if (user != null) - { - usersResult.Add(user); - } - } - catch (ServiceException) - { - // skip, group provisioning shouldnt stop because of error in user object - } + // skip, group provisioning shouldnt stop because of error in user object } - return usersResult; - }).GetAwaiter().GetResult(); - return result; + } + return usersResult; } /// @@ -1319,7 +954,7 @@ public static void RestoreDeletedGroup(string groupId, string accessToken, Uri g graphBaseUri = new Uri(GraphHelper.MicrosoftGraphBaseURI); } - HttpHelper.MakePostRequest($"{graphBaseUri}v1.0/directory/deleteditems/{groupId}/restore", contentType: "application/json", accessToken: accessToken); + HttpHelper.MakePostRequest($"{graphBaseUri}v1.0/directory/deleteditems/{groupId}/restore", contentType: HttpHelper.JsonContentType, accessToken: accessToken); } catch (Exception e) { diff --git a/src/lib/PnP.Framework/Graph/Model/Group.cs b/src/lib/PnP.Framework/Graph/Model/Group.cs index 06fe723cd..700713107 100644 --- a/src/lib/PnP.Framework/Graph/Model/Group.cs +++ b/src/lib/PnP.Framework/Graph/Model/Group.cs @@ -1,22 +1,48 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; +using PnP.Framework.Entities; namespace PnP.Framework.Graph.Model { - /// - /// Defines a Microsoft Graph Group - /// - public class Group + internal class Group { + [JsonPropertyName("id")] + public string GroupId { get; set; } + public string DisplayName { get; set; } + public string Description { get; set; } + public string MailNickname { get; set; } /// - /// True if the group is not displayed in certain parts of the Outlook UI: the Address Book, address lists for selecting message recipients, and the Browse Groups dialog for searching groups; otherwise, false. Default value is false. + /// Group e-mail address /// - [JsonProperty("hideFromAddressLists", NullValueHandling = NullValueHandling.Ignore)] - public bool? HideFromAddressLists { get; set; } - + public string Mail { get; set; } + public bool? MailEnabled { get; set; } /// - /// True if the group is not displayed in Outlook clients, such as Outlook for Windows and Outlook on the web; otherwise, false. Default value is false. + /// Can the group be used to set permissions /// - [JsonProperty("hideFromOutlookClients", NullValueHandling = NullValueHandling.Ignore)] - public bool? HideFromOutlookClients { get; set; } + public bool? SecurityEnabled { get; set; } + public string[] GroupTypes { get; set; } + public string Visibility { get; set; } + public string Classification { get; set; } + +#pragma warning disable CA1819 + [JsonPropertyName("owners@odata.bind")] + public string[] OwnersODataBind { get; set; } + [JsonPropertyName("members@odata.bind")] + public string[] MembersODataBind { get; set; } +#pragma warning restore CA1819 + + public GroupEntity AsEntity() + { + return new GroupEntity() + { + Description = Description, + DisplayName = DisplayName, + GroupId = GroupId, + GroupTypes = GroupTypes, + MailNickname = MailNickname, + Mail = Mail, + MailEnabled = MailEnabled, + SecurityEnabled = SecurityEnabled, + }; + } } } diff --git a/src/lib/PnP.Framework/Graph/Model/GroupExtended.cs b/src/lib/PnP.Framework/Graph/Model/GroupExtended.cs new file mode 100644 index 000000000..f431f9169 --- /dev/null +++ b/src/lib/PnP.Framework/Graph/Model/GroupExtended.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace PnP.Framework.Graph.Model +{ + internal class GroupExtended : Group + { +#pragma warning disable CA1819 + [JsonProperty("owners@odata.bind", NullValueHandling = NullValueHandling.Ignore)] + public string[] OwnersODataBind { get; set; } + [JsonProperty("members@odata.bind", NullValueHandling = NullValueHandling.Ignore)] + public string[] MembersODataBind { get; set; } +#pragma warning restore CA1819 + + public List AssignedLabels { get; set; } + public string PreferredDataLocation { get; set; } + + public Dictionary AdditionalData { get; set; } + } +} diff --git a/src/lib/PnP.Framework/Graph/Model/GroupLabel.cs b/src/lib/PnP.Framework/Graph/Model/GroupLabel.cs new file mode 100644 index 000000000..63009a4e4 --- /dev/null +++ b/src/lib/PnP.Framework/Graph/Model/GroupLabel.cs @@ -0,0 +1,8 @@ + +namespace PnP.Framework.Graph.Model +{ + public class GroupLabel + { + public string LabelId { get; set; } + } +} diff --git a/src/lib/PnP.Framework/Graph/Model/GroupPatchModel.cs b/src/lib/PnP.Framework/Graph/Model/GroupPatchModel.cs new file mode 100644 index 000000000..0941e015e --- /dev/null +++ b/src/lib/PnP.Framework/Graph/Model/GroupPatchModel.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace PnP.Framework.Graph.Model +{ + /// + /// Defines a Microsoft Graph Group + /// + public class GroupPatchModel + { + /// + /// True if the group is not displayed in certain parts of the Outlook UI: the Address Book, address lists for selecting message recipients, and the Browse Groups dialog for searching groups; otherwise, false. Default value is false. + /// + [JsonProperty("hideFromAddressLists", NullValueHandling = NullValueHandling.Ignore)] + public bool? HideFromAddressLists { get; set; } + + /// + /// True if the group is not displayed in Outlook clients, such as Outlook for Windows and Outlook on the web; otherwise, false. Default value is false. + /// + [JsonProperty("hideFromOutlookClients", NullValueHandling = NullValueHandling.Ignore)] + public bool? HideFromOutlookClients { get; set; } + } +} diff --git a/src/lib/PnP.Framework/Graph/Model/Invite.cs b/src/lib/PnP.Framework/Graph/Model/Invite.cs new file mode 100644 index 000000000..3839ca16b --- /dev/null +++ b/src/lib/PnP.Framework/Graph/Model/Invite.cs @@ -0,0 +1,11 @@ +namespace PnP.Framework.Graph.Model +{ + public class Invite + { + public string InvitedUserEmailAddress { get; set; } + public string InvitedUserDisplayName { get; set; } + public string InviteRedirectUrl { get; set; } + public bool SendInvitationMessage { get; set; } + public InvitedUserMessageInfo InvitedUserMessageInfo { get; set; } + } +} diff --git a/src/lib/PnP.Framework/Graph/Model/InvitedUserMessageInfo.cs b/src/lib/PnP.Framework/Graph/Model/InvitedUserMessageInfo.cs new file mode 100644 index 000000000..c25d8fefa --- /dev/null +++ b/src/lib/PnP.Framework/Graph/Model/InvitedUserMessageInfo.cs @@ -0,0 +1,7 @@ +namespace PnP.Framework.Graph.Model +{ + public class InvitedUserMessageInfo + { + public string CustomizedMessageBody { get; set; } + } +} diff --git a/src/lib/PnP.Framework/Graph/Model/User.cs b/src/lib/PnP.Framework/Graph/Model/User.cs index 650101127..c4c390196 100644 --- a/src/lib/PnP.Framework/Graph/Model/User.cs +++ b/src/lib/PnP.Framework/Graph/Model/User.cs @@ -73,6 +73,7 @@ public class User /// /// Additional properties requested regarding the user and included in the response /// + [System.Text.Json.Serialization.JsonExtensionData] public IDictionary AdditionalProperties { get; set; } } } diff --git a/src/lib/PnP.Framework/Graph/PnPHttpProvider.cs b/src/lib/PnP.Framework/Graph/PnPHttpProvider.cs deleted file mode 100644 index 8a4ab3e66..000000000 --- a/src/lib/PnP.Framework/Graph/PnPHttpProvider.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Microsoft.Graph; -using PnP.Framework.Utilities; -using System; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - -namespace PnP.Framework.Graph -{ - /// - /// Class that deals with PnPHttpProvider methods - /// - public class PnPHttpProvider : HttpProvider, IHttpProvider - { - private readonly string _userAgent; - private readonly PnPHttpRetryHandler _retryHandler; - - /// - /// Constructor for the PnPHttpProvider class - /// - /// Maximum retry Count - /// Delay Time - /// User-Agent string to set - public PnPHttpProvider(int retryCount = 10, int delay = 500, string userAgent = null) : base() - { - if (retryCount <= 0) - throw new ArgumentException("Provide a retry count greater than zero."); - - if (delay <= 0) - throw new ArgumentException("Provide a delay greater than zero."); - - this._userAgent = userAgent; - this._retryHandler = new PnPHttpRetryHandler(retryCount, delay); - } - - /// - /// Custom implementation of the IHttpProvider.SendAsync method to handle retry logic - /// - /// The HTTP Request Message - /// The completion option - /// The cancellation token - /// The result of the asynchronous request - /// See here for further details: https://graph.microsoft.io/en-us/docs/overview/errors - async Task IHttpProvider.SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken) - { - // Add the PnP User Agent string - request.Headers.UserAgent.TryParseAdd(string.IsNullOrEmpty(_userAgent) ? $"{PnPCoreUtilities.PnPCoreUserAgent}" : _userAgent); - - return await _retryHandler.SendRetryAsync(request, (r) => base.SendAsync(r, completionOption, cancellationToken), cancellationToken); - } - } -} diff --git a/src/lib/PnP.Framework/Graph/SiteClassificationUtility.cs b/src/lib/PnP.Framework/Graph/SiteClassificationUtility.cs index a05240cb0..b61b1d0ed 100644 --- a/src/lib/PnP.Framework/Graph/SiteClassificationUtility.cs +++ b/src/lib/PnP.Framework/Graph/SiteClassificationUtility.cs @@ -1,5 +1,6 @@ using Newtonsoft.Json; using PnP.Framework.Graph.Model; +using PnP.Framework.Utilities; using System; using System.Collections.Generic; using System.Linq; @@ -128,7 +129,7 @@ public static void EnableSiteClassifications(string accessToken, IEnumerable - { - var graphClient = GraphUtility.CreateGraphClient(accessToken, retryCount, delay, azureEnvironment: azureEnvironment); - - var subscription = await graphClient.Subscriptions[subscriptionId.ToString()] - .Request() - .GetAsync(); + var requestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}subscriptions/{subscriptionId}"; - var subscriptionModel = MapGraphEntityToModel(subscription); - return subscriptionModel; - }).GetAwaiter().GetResult(); + var responseAsString = HttpHelper.MakeGetRequestForString(requestUrl, accessToken, retryCount: retryCount, delay: delay); + var jsonNode = JsonNode.Parse(responseAsString); + var subscription = jsonNode["value"]; - return result; + var subscriptionModel = subscription.Deserialize(); + return subscriptionModel; } - catch (ServiceException ex) + catch (ApplicationException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, ex.Message); throw; } } @@ -66,54 +62,41 @@ public static Model.Subscription GetSubscription(string accessToken, Guid subscr throw new ArgumentNullException(nameof(accessToken)); } - List result = null; + List result = new(); try { - // Use a synchronous model to invoke the asynchronous process - result = Task.Run(async () => - { - List subscriptions = new List(); + var requestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}subscriptions"; - var graphClient = GraphUtility.CreateGraphClient(accessToken, retryCount, delay, azureEnvironment: azureEnvironment); + int currentIndex = 0; - var pagedSubscriptions = await graphClient.Subscriptions - .Request() - .GetAsync(); + do + { + var responseAsString = HttpHelper.MakeGetRequestForString(requestUrl, accessToken, retryCount: retryCount, delay: delay); + var jsonNode = JsonNode.Parse(responseAsString); + var subscriptionListString = jsonNode["value"]; - int pageCount = 0; - int currentIndex = 0; + var subscriptionsPage = subscriptionListString.Deserialize(); - while (true) - { - pageCount++; + var startIndexForPage = Math.Min(0, startIndex - (currentIndex)); + var numToTake = Math.Min(subscriptionsPage.Length, endIndex - currentIndex); - foreach (var s in pagedSubscriptions) - { - currentIndex++; + result.AddRange(subscriptionsPage.Skip(startIndexForPage).Take(numToTake)); - if (currentIndex >= startIndex) - { - var subscription = MapGraphEntityToModel(s); - subscriptions.Add(subscription); - } - } + currentIndex += subscriptionsPage.Length; - if (pagedSubscriptions.NextPageRequest != null && currentIndex < endIndex) - { - pagedSubscriptions = await pagedSubscriptions.NextPageRequest.GetAsync(); - } - else - { - break; - } + if (currentIndex >= endIndex) + { + break; } - return subscriptions; - }).GetAwaiter().GetResult(); + requestUrl = jsonNode["@odata.nextLink"]?.ToString(); + + } while (!string.IsNullOrEmpty(requestUrl)); + } - catch (ServiceException ex) + catch (HttpResponseException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } return result; @@ -146,45 +129,31 @@ public static Model.Subscription CreateSubscription(Enums.GraphSubscriptionChang throw new ArgumentNullException(nameof(resource)); } - Model.Subscription result = null; - try { - // Use a synchronous model to invoke the asynchronous process - result = Task.Run(async () => + var requestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}subscriptions"; + var newSubscription = new { - var graphClient = GraphUtility.CreateGraphClient(accessToken, retryCount, delay, azureEnvironment: azureEnvironment); - - // Prepare the subscription resource object - var newSubscription = new Subscription - { ChangeType = changeType.ToString().Replace(" ", ""), NotificationUrl = notificationUrl, Resource = resource, ExpirationDateTime = expirationDateTime, ClientState = clientState }; + var stringContent = JsonSerializer.Serialize(newSubscription); + var content = new StringContent(stringContent); - var subscription = await graphClient.Subscriptions - .Request() - .AddAsync(newSubscription); + var responseAsString = HttpHelper.MakePostRequestForString(requestUrl, content, HttpHelper.JsonContentType, accessToken, retryCount: retryCount, delay: delay); - if (subscription == null) - { - return null; + // Todo - check that the returned data does actually deserialise correctly + var model = JsonSerializer.Deserialize(responseAsString); + return model; } - - var subscriptionModel = MapGraphEntityToModel(subscription); - return subscriptionModel; - - }).GetAwaiter().GetResult(); - } - catch (ServiceException ex) + catch (HttpResponseException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } - return result; } /// @@ -208,37 +177,25 @@ public static Model.Subscription UpdateSubscription(string subscriptionId, DateT try { - // Use a synchronous model to invoke the asynchronous process - result = Task.Run(async () => + var requestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}subscriptions/{subscriptionId}"; + var updatedSubscription = new Model.Subscription { - var graphClient = GraphUtility.CreateGraphClient(accessToken, retryCount, delay, azureEnvironment: azureEnvironment); + ExpirationDateTime = expirationDateTime + }; + var contentString = JsonSerializer.Serialize(updatedSubscription); + var content = new StringContent(contentString); - // Prepare the subscription resource object - var updatedSubscription = new Subscription - { - ExpirationDateTime = expirationDateTime - }; - - var subscription = await graphClient.Subscriptions[subscriptionId] - .Request() - .UpdateAsync(updatedSubscription); - - if (subscription == null) - { - return null; - } + var responseAsString = HttpHelper.MakePatchRequestForString(requestUrl, content, HttpHelper.JsonContentType, accessToken, retryCount: retryCount, delay: delay); - var subscriptionModel = MapGraphEntityToModel(subscription); - return subscriptionModel; - - }).GetAwaiter().GetResult(); + // Todo - check that the returned data does actually deserialise correctly + var model = JsonSerializer.Deserialize(responseAsString); + return model; } - catch (ServiceException ex) + catch (HttpRequestException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } - return result; } /// @@ -258,41 +215,15 @@ public static void DeleteSubscription(string subscriptionId, try { - // Use a synchronous model to invoke the asynchronous process - Task.Run(async () => - { - var graphClient = GraphUtility.CreateGraphClient(accessToken, retryCount, delay); - - await graphClient.Subscriptions[subscriptionId] - .Request() - .DeleteAsync(); + var requestUrl = $"{GraphHttpClient.GetGraphEndPointUrl()}subscriptions/{subscriptionId}"; - }).GetAwaiter().GetResult(); + HttpHelper.MakeDeleteRequest(requestUrl, accessToken, retryCount: retryCount, delay: delay); } - catch (ServiceException ex) + catch (HttpRequestException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } } - - /// - /// Maps an entity returned by Microsoft Graph to its equivallent Model maintained within this library - /// - /// Microsoft Graph Subscription entity - /// Subscription Model - private static Model.Subscription MapGraphEntityToModel(Subscription subscription) - { - var subscriptionModel = new Model.Subscription - { - Id = subscription.Id, - ChangeType = subscription.ChangeType.Split(',').Select(ct => (Enums.GraphSubscriptionChangeType)Enum.Parse(typeof(Enums.GraphSubscriptionChangeType), ct, true)).Aggregate((prev, next) => prev | next), - NotificationUrl = subscription.NotificationUrl, - Resource = subscription.Resource, - ExpirationDateTime = subscription.ExpirationDateTime, - ClientState = subscription.ClientState - }; - return subscriptionModel; - } } } diff --git a/src/lib/PnP.Framework/Graph/UnifiedGroupsUtility.cs b/src/lib/PnP.Framework/Graph/UnifiedGroupsUtility.cs index 3980eb854..e0f6f0c2a 100644 --- a/src/lib/PnP.Framework/Graph/UnifiedGroupsUtility.cs +++ b/src/lib/PnP.Framework/Graph/UnifiedGroupsUtility.cs @@ -1,5 +1,4 @@ -using Microsoft.Graph; -using Newtonsoft.Json; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using PnP.Framework.Diagnostics; using PnP.Framework.Entities; @@ -8,21 +7,12 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Net.Http.Headers; using System.Text.Json; +using System.Text.Json.Nodes; using System.Threading.Tasks; namespace PnP.Framework.Graph { - public class GroupExtended : Group - { -#pragma warning disable CA1819 - [JsonProperty("owners@odata.bind", NullValueHandling = NullValueHandling.Ignore)] - public string[] OwnersODataBind { get; set; } - [JsonProperty("members@odata.bind", NullValueHandling = NullValueHandling.Ignore)] - public string[] MembersODataBind { get; set; } -#pragma warning restore CA1819 - } /// /// Class that deals with Unified group CRUD operations. /// @@ -31,38 +21,6 @@ public static class UnifiedGroupsUtility private const int defaultRetryCount = 10; private const int defaultDelay = 500; - /// - /// Creates a new GraphServiceClient instance using a custom PnPHttpProvider - /// - /// The OAuth 2.0 Access Token to configure the HTTP bearer Authorization Header - /// Number of times to retry the request in case of throttling - /// Milliseconds to wait before retrying the request. - /// Azure environment to use - /// - private static GraphServiceClient CreateGraphClient(String accessToken, int retryCount = defaultRetryCount, int delay = defaultDelay, AzureEnvironment azureEnvironment = AzureEnvironment.Production) - { - // Creates a new GraphServiceClient instance using a custom PnPHttpProvider - // which natively supports retry logic for throttled requests - // Default are 10 retries with a base delay of 500ms - - var baseUrl = $"https://{AuthenticationManager.GetGraphEndPoint(azureEnvironment)}/v1.0"; - - var result = new GraphServiceClient(baseUrl, new DelegateAuthenticationProvider( - async (requestMessage) => - { - await Task.Run(() => - { - if (!String.IsNullOrEmpty(accessToken)) - { - // Configure the HTTP bearer Authorization Header - requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken); - } - }); - }), new PnPHttpProvider(retryCount, delay)); - - return (result); - } - /// /// Returns the URL of the Modern SharePoint Site backing an Office 365 Group (i.e. Unified Group) /// @@ -91,9 +49,9 @@ public static string GetUnifiedGroupSiteUrl(string groupId, string accessToken, result = Convert.ToString(response["webUrl"]); } - catch (ServiceException ex) + catch (ApplicationException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, ex.Message); throw; } return (result); @@ -127,8 +85,6 @@ public static UnifiedGroupEntity CreateUnifiedGroup(string displayName, string d bool isPrivate = false, bool createTeam = false, int retryCount = 10, int delay = 500, AzureEnvironment azureEnvironment = AzureEnvironment.Production, Enums.Office365Geography? preferredDataLocation = null, Guid[] assignedLabels = null, bool welcomeEmailDisabled = false, string siteAlias = "", uint lcid = 0, Guid hubSiteId = new Guid(), Guid siteDesignId = new Guid()) { - UnifiedGroupEntity result = null; - if (string.IsNullOrEmpty(displayName)) { throw new ArgumentNullException(nameof(displayName)); @@ -144,14 +100,14 @@ public static UnifiedGroupEntity CreateUnifiedGroup(string displayName, string d throw new ArgumentNullException(nameof(accessToken)); } - var labels = new List(); + var labels = new List(); if (assignedLabels != null) { foreach (var label in assignedLabels) { if (!Guid.Empty.Equals(label)) { - labels.Add(new AssignedLabel + labels.Add(new Model.GroupLabel { LabelId = label.ToString() }); @@ -162,52 +118,47 @@ public static UnifiedGroupEntity CreateUnifiedGroup(string displayName, string d try { - // Use a synchronous model to invoke the asynchronous process - result = Task.Run(async () => - { - var group = new UnifiedGroupEntity(); + var group = new UnifiedGroupEntity(); - var graphClient = CreateGraphClient(accessToken, retryCount, delay, azureEnvironment); - - // Prepare the group resource object - var newGroup = new GroupExtended - { - DisplayName = displayName, - Description = string.IsNullOrEmpty(description) ? null : description, - MailNickname = mailNickname, - MailEnabled = true, - SecurityEnabled = false, - Visibility = isPrivate == true ? "Private" : "Public", - GroupTypes = new List { "Unified" }, - }; + // Prepare the group resource object + var newGroup = new Model.GroupExtended + { + DisplayName = displayName, + Description = string.IsNullOrEmpty(description) ? null : description, + MailNickname = mailNickname, + MailEnabled = true, + SecurityEnabled = false, + Visibility = isPrivate == true ? "Private" : "Public", + GroupTypes = new string[] { "Unified" } + }; - if (labels.Any()) - { - newGroup.AssignedLabels = labels; - } + if (labels.Any()) + { + newGroup.AssignedLabels = labels; + } - if (preferredDataLocation.HasValue) - { - newGroup.PreferredDataLocation = preferredDataLocation.Value.ToString(); - } + if (preferredDataLocation.HasValue) + { + newGroup.PreferredDataLocation = preferredDataLocation.Value.ToString(); + } - if (owners != null && owners.Length > 0) + if (owners != null && owners.Length > 0) + { + var users = GetUserIds(accessToken, owners, retryCount, delay, azureEnvironment); + if (users != null && users.Count > 0) { - var users = GetUsers(graphClient, owners); - if (users != null && users.Count > 0) - { - newGroup.OwnersODataBind = users.Select(u => $"https://{AuthenticationManager.GetGraphEndPoint(azureEnvironment)}/v1.0/users/{u.Id}").ToArray(); - } + newGroup.OwnersODataBind = users.Select(u => $"https://{AuthenticationManager.GetGraphEndPoint(azureEnvironment)}/v1.0/users/{u}").ToArray(); } + } - if (members != null && members.Length > 0) + if (members != null && members.Length > 0) + { + var users = GetUserIds(accessToken, owners, retryCount, delay, azureEnvironment); + if (users != null && users.Count > 0) { - var users = GetUsers(graphClient, members); - if (users != null && users.Count > 0) - { - newGroup.MembersODataBind = users.Select(u => $"https://{AuthenticationManager.GetGraphEndPoint(azureEnvironment)}/v1.0/users/{u.Id}").ToArray(); - } + newGroup.MembersODataBind = users.Select(u => $"https://{AuthenticationManager.GetGraphEndPoint(azureEnvironment)}/v1.0/users/{u}").ToArray(); } + } if (welcomeEmailDisabled) { @@ -241,105 +192,103 @@ public static UnifiedGroupEntity CreateUnifiedGroup(string displayName, string d newGroup.AdditionalData.Add("creationOptions", siteCreationOptions.ToArray()); - Microsoft.Graph.Group addedGroup = null; string modernSiteUrl = null; - // Add the group to the collection of groups (if it does not exist) - if (addedGroup == null) - { - addedGroup = await graphClient.Groups.Request().AddAsync(newGroup); + // Add the group to the collection of groups (if it does not exist) + var groupRequestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}groups"; + var responseAsString = HttpHelper.MakePostRequestForString(groupRequestUrl, newGroup, HttpHelper.JsonContentType, accessToken, retryCount: retryCount, delay: delay); + var addedGroup = JsonConvert.DeserializeObject(responseAsString); - if (addedGroup != null) - { - group.DisplayName = addedGroup.DisplayName; - group.Description = addedGroup.Description; - group.GroupId = addedGroup.Id; - group.Mail = addedGroup.Mail; - group.MailNickname = addedGroup.MailNickname; + if (addedGroup != null) + { + group.DisplayName = addedGroup.DisplayName; + group.Description = addedGroup.Description; + group.GroupId = addedGroup.GroupId; + group.Mail = addedGroup.Mail; + group.MailNickname = addedGroup.MailNickname; - int imageRetryCount = retryCount; + int imageRetryCount = retryCount; - if (groupLogo != null) + if (groupLogo != null) + { + using (var memGroupLogo = new MemoryStream()) + { + groupLogo.CopyTo(memGroupLogo); + + while (imageRetryCount > 0) { - using (var memGroupLogo = new MemoryStream()) + bool groupLogoUpdated = false; + memGroupLogo.Position = 0; + + using (var tempGroupLogo = new MemoryStream()) { - groupLogo.CopyTo(memGroupLogo); + memGroupLogo.CopyTo(tempGroupLogo); + tempGroupLogo.Position = 0; - while (imageRetryCount > 0) + try { - bool groupLogoUpdated = false; - memGroupLogo.Position = 0; - - using (var tempGroupLogo = new MemoryStream()) - { - memGroupLogo.CopyTo(tempGroupLogo); - tempGroupLogo.Position = 0; - - try - { - groupLogoUpdated = UpdateUnifiedGroup(addedGroup.Id, accessToken, groupLogo: tempGroupLogo, azureEnvironment: azureEnvironment); - } - catch - { - // Skip any exception and simply retry - } - } - - // In case of failure retry up to 10 times, with 500ms delay in between - if (!groupLogoUpdated) - { - // Pop up the delay for the group image - await Task.Delay(delay * (retryCount - imageRetryCount)); - imageRetryCount--; - } - else - { - break; - } + groupLogoUpdated = UpdateUnifiedGroup(addedGroup.GroupId, accessToken, groupLogo: tempGroupLogo, azureEnvironment: azureEnvironment); + } + catch + { + // Skip any exception and simply retry } } - } - int driveRetryCount = retryCount; - - while (driveRetryCount > 0 && string.IsNullOrEmpty(modernSiteUrl)) - { - try - { - modernSiteUrl = GetUnifiedGroupSiteUrl(addedGroup.Id, accessToken, azureEnvironment: azureEnvironment); - } - catch + // In case of failure retry up to 10 times, with 500ms delay in between + if (!groupLogoUpdated) { - // Skip any exception and simply retry + // Pop up the delay for the group image + Task.Delay(delay * (retryCount - imageRetryCount)).GetAwaiter().GetResult(); + imageRetryCount--; } - - // In case of failure retry up to 10 times, with 500ms delay in between - if (string.IsNullOrEmpty(modernSiteUrl)) + else { - await Task.Delay(delay * (retryCount - driveRetryCount)); - driveRetryCount--; + break; } } - - group.SiteUrl = modernSiteUrl; } } - if (createTeam) + int driveRetryCount = retryCount; + + while (driveRetryCount > 0 && string.IsNullOrEmpty(modernSiteUrl)) { - await CreateTeam(group.GroupId, accessToken, azureEnvironment: azureEnvironment); + try + { + modernSiteUrl = GetUnifiedGroupSiteUrl(addedGroup.GroupId, accessToken, azureEnvironment: azureEnvironment); + } + catch + { + // Skip any exception and simply retry + } + + // In case of failure retry up to 10 times, with 500ms delay in between + if (string.IsNullOrEmpty(modernSiteUrl)) + { + Task.Delay(delay * (retryCount - driveRetryCount)).GetAwaiter().GetResult(); + driveRetryCount--; + } } - return (group); + group.SiteUrl = modernSiteUrl; + } + + if (createTeam) + { + Task.Run(async () => + { + await CreateTeam(group.GroupId, accessToken, azureEnvironment: azureEnvironment); + }).GetAwaiter().GetResult(); + } - }).GetAwaiter().GetResult(); + return group; } - catch (ServiceException ex) + catch (HttpResponseException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } - return (result); } /// @@ -349,88 +298,61 @@ public static UnifiedGroupEntity CreateUnifiedGroup(string displayName, string d /// GraphClient instance to use to communicate with the Microsoft Graph /// Id of the group which needs the owners added /// If set to true, all existing members which are not specified through will be removed as a member from the group - private static async Task UpdateMembers(string[] members, GraphServiceClient graphClient, string groupId, bool removeOtherMembers) + private static void UpdateMembers(string[] members, string groupId, bool removeOtherMembers, string accessToken, int retryCount, int delay, AzureEnvironment azureEnvironment) { - if (members != null && members.Length > 0) + var userRequestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}users"; + var groupRequestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}groups/{groupId}"; + foreach (var m in members) { - foreach (var m in members) + // Search for the user object + string upn = Uri.EscapeDataString(m.Replace("'", "''")); + var requestUrl = $"{userRequestUrl}?$filter=userPrincipalName eq '{upn}'&$select=id"; + var responseAsString = HttpHelper.MakeGetRequestForString(requestUrl, accessToken, retryCount: retryCount, delay: delay); + var jsonNode = JsonNode.Parse(responseAsString); + var userListString = jsonNode["value"]; + var user = userListString.AsArray().FirstOrDefault()?.Deserialize(); + + if (user != null) { - // Search for the user object - var memberQuery = await graphClient.Users - .Request() - .Filter($"userPrincipalName eq '{Uri.EscapeDataString(m.Replace("'", "''"))}'") - .GetAsync(); - - var member = memberQuery.FirstOrDefault(); - - if (member != null) + try { - try - { - // And if any, add it to the collection of group's owners - await graphClient.Groups[groupId].Members.References.Request().AddAsync(member); - } - catch (Exception ex) - { - if (ex.Message.Contains("Request_BadRequest") && - ex.Message.Contains("added object references already exist")) - { - // Skip any already existing member - } - else - { -#pragma warning disable CA2200 - throw ex; -#pragma warning restore CA2200 - } - } + // And if any, add it to the collection of group's members + var memberUrl = $"{groupRequestUrl}/members/{user.Id}/ref"; + HttpHelper.MakePostRequest(memberUrl, accessToken, retryCount: retryCount, delay: delay); + } + catch (Exception ex) when (ex.Message.Contains("Request_BadRequest") && + ex.Message.Contains("added object references already exist")) + { + // Skip any already existing member } } } + // Check if all other members not provided should be removed if (!removeOtherMembers) { return; } - // Remove any leftover member - var fullListOfMembers = await graphClient.Groups[groupId].Members.Request().Select("userPrincipalName, Id").GetAsync(); - var pageExists = true; - - while (pageExists) + // Remove any leftover members + var listMembersUrl = $"{groupRequestUrl}/members/microsoft.graph.user?$select=id,userPrincipalName"; + var allMembersInGroup = GraphUtility.ReadPagedDataFromRequest(listMembersUrl, accessToken, retryCount, delay); + foreach (var member in allMembersInGroup) { - foreach (var member in fullListOfMembers) + var currentMemberPrincipalName = member.UserPrincipalName; + if (!string.IsNullOrEmpty(currentMemberPrincipalName) && + !members.Contains(currentMemberPrincipalName, StringComparer.InvariantCultureIgnoreCase)) { - var currentMemberPrincipalName = (member as Microsoft.Graph.User)?.UserPrincipalName; - if (!string.IsNullOrEmpty(currentMemberPrincipalName) && - !members.Contains(currentMemberPrincipalName, StringComparer.InvariantCultureIgnoreCase)) + try { - try - { - // If it is not in the list of current owners, just remove it - await graphClient.Groups[groupId].Members[member.Id].Reference.Request().DeleteAsync(); - } - catch (ServiceException ex) - { - if (ex.Error.Code == "Request_BadRequest") - { - // Skip any failing removal - } - else - { - throw ex; - } - } + // If it is not in the list of current members, just remove it + var memberUrl = $"{groupRequestUrl}/members/{member.Id}/ref"; + HttpHelper.MakeDeleteRequest(memberUrl, accessToken, retryCount: retryCount, delay: delay); + } + catch (HttpResponseException ex) when (ex.StatusCode == 400) + { + // Skip any failing removal } - } - - if (fullListOfMembers.NextPageRequest != null) - { - fullListOfMembers = await fullListOfMembers.NextPageRequest.GetAsync(); - } - else - { - pageExists = false; } } } @@ -442,38 +364,32 @@ private static async Task UpdateMembers(string[] members, GraphServiceClient gra /// GraphClient instance to use to communicate with the Microsoft Graph /// Id of the group which needs the owners added /// If set to true, all existing owners which are not specified through will be removed as an owner from the group - private static async Task UpdateOwners(string[] owners, GraphServiceClient graphClient, string groupId, bool removeOtherOwners) + private static void UpdateOwners(string[] owners, string groupId, bool removeOtherOwners, string accessToken, int retryCount, int delay, AzureEnvironment azureEnvironment) { + var userRequestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}users"; + var groupRequestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}groups/{groupId}"; foreach (var o in owners) { // Search for the user object - var ownerQuery = await graphClient.Users - .Request() - .Filter($"userPrincipalName eq '{Uri.EscapeDataString(o.Replace("'", "''"))}'") - .GetAsync(); - - var owner = ownerQuery.FirstOrDefault(); - - if (owner != null) + string upn = Uri.EscapeDataString(o.Replace("'", "''")); + var requestUrl = $"{userRequestUrl}?$filter=userPrincipalName eq '{upn}'&$select=id"; + var responseAsString = HttpHelper.MakeGetRequestForString(requestUrl, accessToken, retryCount: retryCount, delay: delay); + var jsonNode = JsonNode.Parse(responseAsString); + var userListString = jsonNode["value"]; + var user = userListString.AsArray().FirstOrDefault()?.Deserialize(); + + if (user != null) { try { // And if any, add it to the collection of group's owners - await graphClient.Groups[groupId].Owners.References.Request().AddAsync(owner); + var memberUrl = $"{groupRequestUrl}/owners/{user.Id}/ref"; + HttpHelper.MakePostRequest(memberUrl, accessToken, retryCount: retryCount, delay: delay); } - catch (Exception ex) - { - if (ex.Message.Contains("Request_BadRequest") && + catch (Exception ex) when (ex.Message.Contains("Request_BadRequest") && ex.Message.Contains("added object references already exist")) - { - // Skip any already existing owner - } - else - { -#pragma warning disable CA2200 - throw ex; -#pragma warning restore CA2200 - } + { + // Skip any already existing member } } } @@ -485,47 +401,29 @@ private static async Task UpdateOwners(string[] owners, GraphServiceClient graph } // Remove any leftover owner - var fullListOfOwners = await graphClient.Groups[groupId].Owners.Request().Select("userPrincipalName, Id").GetAsync(); - var pageExists = true; - - while (pageExists) + var listOwnersUrl = $"{groupRequestUrl}/members/microsoft.graph.user?$select=id,userPrincipalName"; + var allOwnersInGroup = GraphUtility.ReadPagedDataFromRequest(listOwnersUrl, accessToken, retryCount, delay); + foreach (var owner in allOwnersInGroup) { - foreach (var owner in fullListOfOwners) + var currentOwnerPrincipalName = owner.UserPrincipalName; + if (!string.IsNullOrEmpty(currentOwnerPrincipalName) && + !owners.Contains(currentOwnerPrincipalName, StringComparer.InvariantCultureIgnoreCase)) { - var currentOwnerPrincipalName = (owner as Microsoft.Graph.User)?.UserPrincipalName; - if (!string.IsNullOrEmpty(currentOwnerPrincipalName) && - !owners.Contains(currentOwnerPrincipalName, StringComparer.InvariantCultureIgnoreCase)) + try { - try - { - // If it is not in the list of current owners, just remove it - await graphClient.Groups[groupId].Owners[owner.Id].Reference.Request().DeleteAsync(); - } - catch (ServiceException ex) - { - if (ex.Error.Code == "Request_BadRequest") - { - // Skip any failing removal - } - else - { - throw ex; - } - } + // If it is not in the list of current owners, just remove it + var memberUrl = $"{groupRequestUrl}/owners/{owner.Id}/ref"; + HttpHelper.MakeDeleteRequest(memberUrl, accessToken, retryCount: retryCount, delay: delay); + } + catch (HttpResponseException ex) when (ex.StatusCode == 400) + { + // Skip any failing removal } - } - - if (fullListOfOwners.NextPageRequest != null) - { - fullListOfOwners = await fullListOfOwners.NextPageRequest.GetAsync(); - } - else - { - pageExists = false; } } } + /// /// Sets the visibility of a Group /// @@ -555,7 +453,7 @@ public static void SetUnifiedGroupVisibility(string groupId, string accessToken, { // PATCH https://graph.microsoft.com/v1.0/groups/{id} string updateGroupUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}groups/{groupId}"; - var groupRequest = new Model.Group + var groupRequest = new Model.GroupPatchModel { HideFromAddressLists = hideFromAddressLists, HideFromOutlookClients = hideFromOutlookClients @@ -564,12 +462,12 @@ public static void SetUnifiedGroupVisibility(string groupId, string accessToken, var response = GraphHttpClient.MakePatchRequestForString( requestUrl: updateGroupUrl, content: JsonConvert.SerializeObject(groupRequest), - contentType: "application/json", + contentType: HttpHelper.JsonContentType, accessToken: accessToken); } - catch (ServiceException ex) + catch (ApplicationException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, ex.Message); throw; } } @@ -587,20 +485,12 @@ public static void RenewUnifiedGroup(string groupId, { try { - // Use a synchronous model to invoke the asynchronous process - Task.Run(async () => - { - var graphClient = CreateGraphClient(accessToken, retryCount, delay, azureEnvironment); - - await graphClient.Groups[groupId] - .Renew() - .Request() - .PostAsync(); - }).GetAwaiter().GetResult(); + var renewRequestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}groups/{groupId}/renew"; + HttpHelper.MakePostRequest(renewRequestUrl, null, null, accessToken, retryCount: retryCount, delay: delay); } - catch (ServiceException ex) + catch (HttpResponseException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } } @@ -626,108 +516,103 @@ public static bool UpdateUnifiedGroup(string groupId, string displayName = null, string description = null, string[] owners = null, string[] members = null, Stream groupLogo = null, bool? isPrivate = null, bool createTeam = false, AzureEnvironment azureEnvironment = AzureEnvironment.Production) { - bool result; try { - // Use a synchronous model to invoke the asynchronous process - result = Task.Run(async () => - { - var graphClient = CreateGraphClient(accessToken, retryCount, delay, azureEnvironment); - - var groupToUpdate = await graphClient.Groups[groupId] - .Request() - .GetAsync(); + var groupRequestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}groups/{groupId}"; + var responseAsString = HttpHelper.MakeGetRequestForString(groupRequestUrl, accessToken, retryCount: retryCount, delay: delay); + var groupJson = JsonNode.Parse(responseAsString); + var groupToUpdate = groupJson.Deserialize(); - // Workaround for the PATCH request, needed after update to Graph Library - var clonedGroup = new Group - { - Id = groupToUpdate.Id - }; + // Workaround for the PATCH request, needed after update to Graph Library + var clonedGroup = new Model.Group + { + GroupId = groupToUpdate.GroupId + }; - #region Logic to update the group DisplayName and Description + #region Logic to update the group DisplayName and Description - var updateGroup = false; - var groupUpdated = false; + var updateGroup = false; + var groupUpdated = false; - // Check if we have to update the DisplayName - if (!String.IsNullOrEmpty(displayName) && groupToUpdate.DisplayName != displayName) - { - clonedGroup.DisplayName = displayName; - updateGroup = true; - } + // Check if we have to update the DisplayName + if (!String.IsNullOrEmpty(displayName) && groupToUpdate.DisplayName != displayName) + { + clonedGroup.DisplayName = displayName; + updateGroup = true; + } - // Check if we have to update the Description - if (!String.IsNullOrEmpty(description) && groupToUpdate.Description != description) - { - clonedGroup.Description = description; - updateGroup = true; - } + // Check if we have to update the Description + if (!String.IsNullOrEmpty(description) && groupToUpdate.Description != description) + { + clonedGroup.Description = description; + updateGroup = true; + } - // Check if visibility has changed for the Group - bool existingIsPrivate = groupToUpdate.Visibility == "Private"; - if (isPrivate.HasValue && existingIsPrivate != isPrivate) - { - clonedGroup.Visibility = isPrivate == true ? "Private" : "Public"; - updateGroup = true; - } + // Check if visibility has changed for the Group + bool existingIsPrivate = groupToUpdate.Visibility == "Private"; + if (isPrivate.HasValue && existingIsPrivate != isPrivate) + { + clonedGroup.Visibility = isPrivate == true ? "Private" : "Public"; + updateGroup = true; + } - // Check if we need to update owners - if (owners != null && owners.Length > 0) - { - // For each and every owner - await UpdateOwners(owners, graphClient, groupToUpdate.Id, true); - updateGroup = true; - } + // Check if we need to update owners + if (owners != null && owners.Length > 0) + { + // For each and every owner + UpdateOwners(owners, groupToUpdate.GroupId, true, accessToken, retryCount, delay, azureEnvironment); + updateGroup = true; + } - // Check if we need to update members - if (members != null && members.Length > 0) - { - // For each and every owner - await UpdateMembers(members, graphClient, groupToUpdate.Id, true); - updateGroup = true; - } + // Check if we need to update members + if (members != null && members.Length > 0) + { + // For each and every owner + UpdateMembers(members, groupToUpdate.GroupId, true, accessToken, retryCount, delay, azureEnvironment); + updateGroup = true; + } - if (createTeam) + if (createTeam) + { + // Use a synchronous model to invoke the asynchronous process + Task.Run(async () => { await CreateTeam(groupId, accessToken); - updateGroup = true; - } - // If the Group has to be updated, just do it - if (updateGroup) - { - var updatedGroup = await graphClient.Groups[groupId] - .Request() - .UpdateAsync(clonedGroup); - - groupUpdated = true; - } + }).GetAwaiter().GetResult(); + updateGroup = true; + } - #endregion + // If the Group has to be updated, just do it + if (updateGroup) + { + var updatedGroup = HttpHelper.MakePatchRequestForString(groupRequestUrl, clonedGroup, HttpHelper.JsonContentType, accessToken, retryCount: retryCount, delay: delay); + groupUpdated = true; + } - #region Logic to update the group Logo + #endregion - var logoUpdated = false; + #region Logic to update the group Logo - if (groupLogo != null) - { - await graphClient.Groups[groupId].Photo.Content.Request().PutAsync(groupLogo); - logoUpdated = true; - } + var logoUpdated = false; - #endregion + if (groupLogo != null) + { + var photoUpdateUrl = $"{groupRequestUrl}/photo/$value"; + HttpHelper.MakePutRequest(photoUpdateUrl, groupLogo, "image/jpeg", accessToken, retryCount: retryCount, delay: delay); + logoUpdated = true; + } - // If any of the previous update actions has been completed - return (groupUpdated || logoUpdated); + #endregion - }).GetAwaiter().GetResult(); + // If any of the previous update actions has been completed + return (groupUpdated || logoUpdated); } - catch (ServiceException ex) + catch (HttpResponseException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } - return (result); } /// @@ -821,17 +706,13 @@ public static void DeleteUnifiedGroup(string groupId, string accessToken, int re } try { - // Use a synchronous model to invoke the asynchronous process - Task.Run(async () => - { - var graphClient = CreateGraphClient(accessToken, retryCount, delay, azureEnvironment); - await graphClient.Groups[groupId].Request().DeleteAsync(); - - }).GetAwaiter().GetResult(); + //// DELETE https://graph.microsoft.com/v1.0/groups/{id} + string deleteGroupUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}groups/{groupId}"; + GraphHttpClient.MakeDeleteRequest(deleteGroupUrl, accessToken); } - catch (ServiceException ex) + catch (ApplicationException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, ex.Message); throw; } } @@ -859,62 +740,45 @@ public static UnifiedGroupEntity GetUnifiedGroup(string groupId, string accessTo throw new ArgumentNullException(nameof(accessToken)); } - UnifiedGroupEntity result = null; - try - { - // Use a synchronous model to invoke the asynchronous process - result = Task.Run(async () => - { - UnifiedGroupEntity group = null; - - var graphClient = CreateGraphClient(accessToken, retryCount, delay, azureEnvironment); - - var g = await graphClient.Groups[groupId].Request().GetAsync(); - - if (g.GroupTypes.Contains("Unified")) - { - group = new UnifiedGroupEntity - { - GroupId = g.Id, - DisplayName = g.DisplayName, - Description = g.Description, - Mail = g.Mail, - MailNickname = g.MailNickname, - Visibility = g.Visibility - }; - if (includeSite) - { - try - { - group.SiteUrl = GetUnifiedGroupSiteUrl(groupId, accessToken); - } - catch (ServiceException e) - { - group.SiteUrl = e.Error.Message; - } - } + var g = GroupsUtility.GetRawGroup(groupId, accessToken, retryCount, delay, azureEnvironment); - if (includeClassification) - { - group.Classification = g.Classification; - } + if (!g.GroupTypes.Contains("Unified")) + { + return null; + } - if (includeHasTeam) - { - group.HasTeam = HasTeamsTeam(group.GroupId, accessToken, azureEnvironment); - } - } + var group = new UnifiedGroupEntity + { + GroupId = g.GroupId, + DisplayName = g.DisplayName, + Description = g.Description, + Mail = g.Mail, + MailNickname = g.MailNickname, + Visibility = g.Visibility + }; - return (group); + if (includeSite) + { + try + { + group.SiteUrl = GetUnifiedGroupSiteUrl(groupId, accessToken); + } + catch (HttpResponseException e) + { + group.SiteUrl = e.Message; + } + } - }).GetAwaiter().GetResult(); + if (includeClassification) + { + group.Classification = g.Classification; } - catch (ServiceException ex) + + if (includeHasTeam) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); - throw; + group.HasTeam = HasTeamsTeam(group.GroupId, accessToken, azureEnvironment); } - return (result); + return group; } /// @@ -944,91 +808,67 @@ public static List ListUnifiedGroups(string accessToken, throw new ArgumentNullException(nameof(accessToken)); } - List result = null; + List result = new List(); try { - // Use a synchronous model to invoke the asynchronous process - result = Task.Run(async () => - { - List groups = new List(); - - var graphClient = CreateGraphClient(accessToken, retryCount, delay, azureEnvironment); - - // Apply the DisplayName filter, if any - var displayNameFilter = !string.IsNullOrEmpty(displayName) ? $" and (DisplayName eq '{Uri.EscapeDataString(displayName.Replace("'", "''"))}')" : string.Empty; - var mailNicknameFilter = !string.IsNullOrEmpty(mailNickname) ? $" and (MailNickname eq '{Uri.EscapeDataString(mailNickname.Replace("'", "''"))}')" : string.Empty; + // Apply the DisplayName filter, if any + var displayNameFilter = !string.IsNullOrEmpty(displayName) ? $" and (DisplayName eq '{Uri.EscapeDataString(displayName.Replace("'", "''"))}')" : string.Empty; + var mailNicknameFilter = !string.IsNullOrEmpty(mailNickname) ? $" and (MailNickname eq '{Uri.EscapeDataString(mailNickname.Replace("'", "''"))}')" : string.Empty; + var filterString = $"groupTypes/any(grp: grp eq 'Unified'){displayNameFilter}{mailNicknameFilter}"; - var pagedGroups = await graphClient.Groups - .Request() - .Filter($"groupTypes/any(grp: grp eq 'Unified'){displayNameFilter}{mailNicknameFilter}") - .Top(endIndex) - .GetAsync(); + var requestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}groups?$top={endIndex}&$filter={filterString}"; + var groups = GraphUtility.ReadPagedDataFromRequest(requestUrl, accessToken, retryCount, delay); - Int32 pageCount = 0; - Int32 currentIndex = 0; + Int32 currentIndex = 0; - while (true) + foreach (var g in groups) + { + currentIndex++; + if (currentIndex >= endIndex) { - pageCount++; - - foreach (var g in pagedGroups) + break; + } + if (currentIndex >= startIndex) + { + var group = new UnifiedGroupEntity { - currentIndex++; + GroupId = g.GroupId, + DisplayName = g.DisplayName, + Description = g.Description, + Mail = g.Mail, + MailNickname = g.MailNickname, + Visibility = g.Visibility + }; - if (currentIndex >= startIndex) + if (includeSite) + { + try { - var group = new UnifiedGroupEntity - { - GroupId = g.Id, - DisplayName = g.DisplayName, - Description = g.Description, - Mail = g.Mail, - MailNickname = g.MailNickname, - Visibility = g.Visibility - }; - - if (includeSite) - { - try - { - group.SiteUrl = GetUnifiedGroupSiteUrl(g.Id, accessToken); - } - catch (ServiceException e) - { - group.SiteUrl = e.Error.Message; - } - } - - if (includeClassification) - { - group.Classification = g.Classification; - } - - if (includeHasTeam) - { - group.HasTeam = HasTeamsTeam(group.GroupId, accessToken); - } - - groups.Add(group); + group.SiteUrl = GetUnifiedGroupSiteUrl(g.GroupId, accessToken); + } + catch (ApplicationException e) + { + group.SiteUrl = e.Message; } } - if (pagedGroups.NextPageRequest != null && groups.Count < endIndex) + if (includeClassification) { - pagedGroups = await pagedGroups.NextPageRequest.GetAsync(); + group.Classification = g.Classification; } - else + + if (includeHasTeam) { - break; + group.HasTeam = HasTeamsTeam(group.GroupId, accessToken); } - } - return (groups); - }).GetAwaiter().GetResult(); + result.Add(group); + } + } } - catch (ServiceException ex) + catch (HttpResponseException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } return (result); @@ -1060,91 +900,67 @@ public static List GetUnifiedGroups(string accessToken, throw new ArgumentNullException(nameof(accessToken)); } - List result = null; + List result = new List(); try { - // Use a synchronous model to invoke the asynchronous process - result = Task.Run(async () => - { - List groups = new List(); + // Apply the DisplayName filter, if any + var displayNameFilter = !string.IsNullOrEmpty(displayName) ? $" and (DisplayName eq '{Uri.EscapeDataString(displayName.Replace("'", "''"))}')" : string.Empty; + var mailNicknameFilter = !string.IsNullOrEmpty(mailNickname) ? $" and (MailNickname eq '{Uri.EscapeDataString(mailNickname.Replace("'", "''"))}')" : string.Empty; + var filterString = $"groupTypes/any(grp: grp eq 'Unified'){displayNameFilter}{mailNicknameFilter}"; - var graphClient = CreateGraphClient(accessToken, retryCount, delay, azureEnvironment); + var requestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}groups?$top={pageSize}&$filter={filterString}"; + var groups = GraphUtility.ReadPagedDataFromRequest(requestUrl, accessToken, retryCount, delay); - // Apply the DisplayName filter, if any - var displayNameFilter = !string.IsNullOrEmpty(displayName) ? $" and (DisplayName eq '{Uri.EscapeDataString(displayName.Replace("'", "''"))}')" : string.Empty; - var mailNicknameFilter = !string.IsNullOrEmpty(mailNickname) ? $" and (MailNickname eq '{Uri.EscapeDataString(mailNickname.Replace("'", "''"))}')" : string.Empty; + Int32 currentIndex = 0; - var pagedGroups = await graphClient.Groups - .Request() - .Filter($"groupTypes/any(grp: grp eq 'Unified'){displayNameFilter}{mailNicknameFilter}") - .Top(pageSize) - .GetAsync(); - - Int32 pageCount = 0; - Int32 currentIndex = 0; - - while (true) + foreach (var g in groups) + { + currentIndex++; + if (currentIndex >= endIndex) { - pageCount++; - - foreach (var g in pagedGroups) + break; + } + if (currentIndex >= startIndex) + { + var group = new UnifiedGroupEntity { - currentIndex++; + GroupId = g.GroupId, + DisplayName = g.DisplayName, + Description = g.Description, + Mail = g.Mail, + MailNickname = g.MailNickname, + Visibility = g.Visibility + }; - if (currentIndex >= startIndex) + if (includeSite) + { + try { - var group = new UnifiedGroupEntity - { - GroupId = g.Id, - DisplayName = g.DisplayName, - Description = g.Description, - Mail = g.Mail, - MailNickname = g.MailNickname, - Visibility = g.Visibility - }; - - if (includeSite) - { - try - { - group.SiteUrl = GetUnifiedGroupSiteUrl(g.Id, accessToken); - } - catch (ServiceException e) - { - group.SiteUrl = e.Error.Message; - } - } - - if (includeClassification) - { - group.Classification = g.Classification; - } - - if (includeHasTeam) - { - group.HasTeam = HasTeamsTeam(group.GroupId, accessToken, azureEnvironment); - } - - groups.Add(group); + group.SiteUrl = GetUnifiedGroupSiteUrl(g.GroupId, accessToken); + } + catch (ApplicationException e) + { + group.SiteUrl = e.Message; } } - if (pagedGroups.NextPageRequest != null && (endIndex == null || groups.Count < endIndex)) + if (includeClassification) { - pagedGroups = await pagedGroups.NextPageRequest.GetAsync(); + group.Classification = g.Classification; } - else + + if (includeHasTeam) { - break; + group.HasTeam = HasTeamsTeam(group.GroupId, accessToken); } - } - return (groups); - }).GetAwaiter().GetResult(); + result.Add(group); + } + } } - catch (ServiceException ex) + catch (HttpResponseException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } return (result); @@ -1161,10 +977,6 @@ public static List GetUnifiedGroups(string accessToken, /// Members of an Office 365 group as a list of UnifiedGroupUser entity public static List GetUnifiedGroupMembers(UnifiedGroupEntity group, string accessToken, int retryCount = 10, int delay = 500, AzureEnvironment azureEnvironment = AzureEnvironment.Production) { - List unifiedGroupUsers = null; - List unifiedGroupGraphUsers = null; - IGroupMembersCollectionWithReferencesPage groupUsers = null; - if (String.IsNullOrEmpty(accessToken)) { throw new ArgumentNullException(nameof(accessToken)); @@ -1176,61 +988,29 @@ public static List GetUnifiedGroupMembers(UnifiedGroupEntity g try { - var result = Task.Run(async () => - { - var graphClient = CreateGraphClient(accessToken, retryCount, delay, azureEnvironment); - - // Get the members of an Office 365 group. - groupUsers = await graphClient.Groups[group.GroupId].Members.Request().GetAsync(); - if (groupUsers.CurrentPage != null && groupUsers.CurrentPage.Count > 0) - { - unifiedGroupGraphUsers = new List(); + var requestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}groups/{group.GroupId}/members/microsoft.graph.user"; + var users = GraphUtility.ReadPagedDataFromRequest(requestUrl, accessToken, retryCount: retryCount, delay: delay).ToList(); - GenerateGraphUserCollection(groupUsers.CurrentPage, unifiedGroupGraphUsers); - } - - // Retrieve users when the results are paged. - while (groupUsers.NextPageRequest != null) - { - groupUsers = groupUsers.NextPageRequest.GetAsync().GetAwaiter().GetResult(); - if (groupUsers.CurrentPage != null && groupUsers.CurrentPage.Count > 0) - { - GenerateGraphUserCollection(groupUsers.CurrentPage, unifiedGroupGraphUsers); - } - } + foreach (var u in users) + { + u.UserPrincipalName ??= string.Empty; + u.DisplayName ??= string.Empty; + u.GivenName ??= string.Empty; + u.Surname ??= string.Empty; + u.Email ??= string.Empty; + u.MobilePhone ??= string.Empty; + u.PreferredLanguage ??= string.Empty; + u.JobTitle ??= string.Empty; + } - // Create the collection of type OfficeDevPnP 'UnifiedGroupUser' after all users are retrieved, including paged data. - if (unifiedGroupGraphUsers != null && unifiedGroupGraphUsers.Count > 0) - { - unifiedGroupUsers = new List(); - foreach (User usr in unifiedGroupGraphUsers) - { - UnifiedGroupUser groupUser = new UnifiedGroupUser - { - Id = usr.Id, - UserPrincipalName = usr.UserPrincipalName != null ? usr.UserPrincipalName : string.Empty, - DisplayName = usr.DisplayName != null ? usr.DisplayName : string.Empty, - GivenName = usr.GivenName != null ? usr.GivenName : string.Empty, - Surname = usr.Surname != null ? usr.Surname : string.Empty, - Email = usr.Mail != null ? usr.Mail : string.Empty, - MobilePhone = usr.MobilePhone != null ? usr.DisplayName : string.Empty, - PreferredLanguage = usr.PreferredLanguage != null ? usr.PreferredLanguage : string.Empty, - JobTitle = usr.JobTitle != null ? usr.DisplayName : string.Empty, - BusinessPhones = usr.BusinessPhones != null ? usr.BusinessPhones.ToArray() : null - }; - unifiedGroupUsers.Add(groupUser); - } - } - return unifiedGroupUsers; + return users.ToList(); - }).GetAwaiter().GetResult(); } - catch (ServiceException ex) + catch (HttpResponseException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } - return unifiedGroupUsers; } /// @@ -1244,10 +1024,6 @@ public static List GetUnifiedGroupMembers(UnifiedGroupEntity g /// public static List GetNestedUnifiedGroupMembers(UnifiedGroupEntity group, string accessToken, int retryCount = 10, int delay = 500, AzureEnvironment azureEnvironment = AzureEnvironment.Production) { - List unifiedGroupUsers = new List(); - List unifiedGroupGraphUsers = null; - IGroupMembersCollectionWithReferencesPage groupUsers = null; - if (String.IsNullOrEmpty(accessToken)) { throw new ArgumentNullException(nameof(accessToken)); @@ -1259,60 +1035,28 @@ public static List GetNestedUnifiedGroupMembers(UnifiedGroupEn try { - var result = Task.Run(async () => - { - var graphClient = CreateGraphClient(accessToken, retryCount, delay, azureEnvironment); + var requestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}groups/{group.GroupId}/transitiveMembers/microsoft.graph.user"; + var users = GraphUtility.ReadPagedDataFromRequest(requestUrl, accessToken, retryCount: retryCount, delay: delay).ToList(); - // Get the members of an Office 365 group. - groupUsers = await graphClient.Groups[group.GroupId].Members.Request().GetAsync(); - if (groupUsers.CurrentPage != null && groupUsers.CurrentPage.Count > 0) - { - unifiedGroupGraphUsers = new List(); - - GenerateNestedGraphUserCollection(groupUsers.CurrentPage, unifiedGroupGraphUsers, unifiedGroupUsers, accessToken); - } - - // Retrieve users when the results are paged. - while (groupUsers.NextPageRequest != null) - { - groupUsers = groupUsers.NextPageRequest.GetAsync().GetAwaiter().GetResult(); - if (groupUsers.CurrentPage != null && groupUsers.CurrentPage.Count > 0) - { - GenerateNestedGraphUserCollection(groupUsers.CurrentPage, unifiedGroupGraphUsers, unifiedGroupUsers, accessToken); - } - } - - // Create the collection of type OfficeDevPnP 'UnifiedGroupUser' after all users are retrieved, including paged data. - if (unifiedGroupGraphUsers != null && unifiedGroupGraphUsers.Count > 0) - { - foreach (User usr in unifiedGroupGraphUsers) - { - UnifiedGroupUser groupUser = new UnifiedGroupUser - { - Id = usr.Id, - UserPrincipalName = usr.UserPrincipalName != null ? usr.UserPrincipalName : string.Empty, - DisplayName = usr.DisplayName != null ? usr.DisplayName : string.Empty, - GivenName = usr.GivenName != null ? usr.GivenName : string.Empty, - Surname = usr.Surname != null ? usr.Surname : string.Empty, - Email = usr.Mail != null ? usr.Mail : string.Empty, - MobilePhone = usr.MobilePhone != null ? usr.DisplayName : string.Empty, - PreferredLanguage = usr.PreferredLanguage != null ? usr.PreferredLanguage : string.Empty, - JobTitle = usr.JobTitle != null ? usr.DisplayName : string.Empty, - BusinessPhones = usr.BusinessPhones != null ? usr.BusinessPhones.ToArray() : null - }; - unifiedGroupUsers.Add(groupUser); - } - } - return unifiedGroupUsers; + foreach (var u in users) + { + u.UserPrincipalName ??= string.Empty; + u.DisplayName ??= string.Empty; + u.GivenName ??= string.Empty; + u.Surname ??= string.Empty; + u.Email ??= string.Empty; + u.MobilePhone ??= string.Empty; + u.PreferredLanguage ??= string.Empty; + u.JobTitle ??= string.Empty; + } - }).GetAwaiter().GetResult(); + return users.ToList(); } - catch (ServiceException ex) + catch (HttpResponseException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } - return unifiedGroupUsers; } /// @@ -1334,17 +1078,12 @@ public static void AddUnifiedGroupOwners(string groupId, string[] owners, string try { - Task.Run(async () => - { - var graphClient = CreateGraphClient(accessToken, retryCount, delay, azureEnvironment); - - await UpdateOwners(owners, graphClient, groupId, removeExistingOwners); - }).GetAwaiter().GetResult(); + UpdateOwners(owners, groupId, removeExistingOwners, accessToken, retryCount, delay, azureEnvironment); } - catch (ServiceException ex) + catch (HttpResponseException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } } @@ -1368,17 +1107,11 @@ public static void AddUnifiedGroupMembers(string groupId, string[] members, stri try { - Task.Run(async () => - { - var graphClient = CreateGraphClient(accessToken, retryCount, delay, azureEnvironment); - - await UpdateMembers(members, graphClient, groupId, removeExistingMembers); - - }).GetAwaiter().GetResult(); + UpdateMembers(members, groupId, removeExistingMembers, accessToken, retryCount, delay, azureEnvironment); } - catch (ServiceException ex) + catch (HttpResponseException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } } @@ -1401,46 +1134,38 @@ public static void RemoveUnifiedGroupMembers(string groupId, string[] members, s try { - Task.Run(async () => - { - var graphClient = CreateGraphClient(accessToken, retryCount, delay, azureEnvironment); - - foreach (var m in members) - { - // Search for the user object - var memberQuery = await graphClient.Users - .Request() - .Filter($"userPrincipalName eq '{Uri.EscapeDataString(m.Replace("'", "''"))}'") - .GetAsync(); + var userRequestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}users"; + var groupRequestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}groups/{groupId}"; - var member = memberQuery.FirstOrDefault(); - if (member != null) + foreach (var m in members) + { + // Search for the user object + string upn = Uri.EscapeDataString(m.Replace("'", "''")); + var requestUrl = $"{userRequestUrl}?$filter=userPrincipalName eq '{upn}'&$select=id"; + var responseAsString = HttpHelper.MakeGetRequestForString(requestUrl, accessToken, retryCount: retryCount, delay: delay); + var jsonNode = JsonNode.Parse(responseAsString); + var userListString = jsonNode["value"]; + var userId = userListString.AsArray().FirstOrDefault()?["id"]; + + if (userId != null) + { + try { - try - { - // If it is not in the list of current members, just remove it - await graphClient.Groups[groupId].Members[member.Id].Reference.Request().DeleteAsync(); - } - catch (ServiceException ex) - { - if (ex.Error.Code == "Request_BadRequest") - { - // Skip any failing removal - } - else - { - throw ex; - } - } + // If it is not in the list of current members, just remove it + var deleteGroupMemberUrl = $"{groupRequestUrl}/members/{userId}/ref"; + HttpHelper.MakeDeleteRequest(deleteGroupMemberUrl, accessToken, retryCount: retryCount, delay: delay); + } + catch (HttpResponseException ex) when (ex.StatusCode == 400) + { + // Skip any failing removal } } - - }).GetAwaiter().GetResult(); + } } - catch (ServiceException ex) + catch (HttpResponseException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } } @@ -1463,46 +1188,37 @@ public static void RemoveUnifiedGroupOwners(string groupId, string[] owners, str try { - Task.Run(async () => - { - var graphClient = CreateGraphClient(accessToken, retryCount, delay, azureEnvironment); + var userRequestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}users"; + var groupRequestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}groups/{groupId}"; - foreach (var m in owners) + foreach (var m in owners) + { + // Search for the user object + string upn = Uri.EscapeDataString(m.Replace("'", "''")); + var requestUrl = $"{userRequestUrl}?$filter=userPrincipalName eq '{upn}'&$select=id"; + var responseAsString = HttpHelper.MakeGetRequestForString(requestUrl, accessToken, retryCount: retryCount, delay: delay); + var jsonNode = JsonNode.Parse(responseAsString); + var userListString = jsonNode["value"]; + var userId = userListString.AsArray().FirstOrDefault()?["id"]; + + if (userId != null) { - // Search for the user object - var memberQuery = await graphClient.Users - .Request() - .Filter($"userPrincipalName eq '{Uri.EscapeDataString(m.Replace("'", "''"))}'") - .GetAsync(); - - var member = memberQuery.FirstOrDefault(); - - if (member != null) + try { - try - { - // If it is not in the list of current owners, just remove it - await graphClient.Groups[groupId].Owners[member.Id].Reference.Request().DeleteAsync(); - } - catch (ServiceException ex) - { - if (ex.Error.Code == "Request_BadRequest") - { - // Skip any failing removal - } - else - { - throw ex; - } - } + // If it is not in the list of current owners, just remove it + var deleteGroupMemberUrl = $"{groupRequestUrl}/owners/{userId}/ref"; + HttpHelper.MakeDeleteRequest(deleteGroupMemberUrl, accessToken, retryCount: retryCount, delay: delay); + } + catch (HttpResponseException ex) when (ex.StatusCode == 400) + { + // Skip any failing removal } } - - }).GetAwaiter().GetResult(); + } } - catch (ServiceException ex) + catch (HttpResponseException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } } @@ -1527,9 +1243,9 @@ public static void ClearUnifiedGroupOwners(string groupId, string accessToken, i var currentOwners = GetUnifiedGroupOwners(new UnifiedGroupEntity { GroupId = groupId }, accessToken, retryCount, delay, azureEnvironment); RemoveUnifiedGroupOwners(groupId, currentOwners.Select(o => o.UserPrincipalName).ToArray(), accessToken, retryCount, delay, azureEnvironment); } - catch (ServiceException ex) + catch (HttpResponseException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } } @@ -1554,9 +1270,9 @@ public static void ClearUnifiedGroupMembers(string groupId, string accessToken, var currentMembers = GetUnifiedGroupMembers(new UnifiedGroupEntity { GroupId = groupId }, accessToken, retryCount, delay, azureEnvironment); RemoveUnifiedGroupMembers(groupId, currentMembers.Select(o => o.UserPrincipalName).ToArray(), accessToken, retryCount, delay, azureEnvironment); } - catch (ServiceException ex) + catch (HttpResponseException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } } @@ -1572,10 +1288,6 @@ public static void ClearUnifiedGroupMembers(string groupId, string accessToken, /// Owners of an Office 365 group as a list of UnifiedGroupUser entity public static List GetUnifiedGroupOwners(UnifiedGroupEntity group, string accessToken, int retryCount = 10, int delay = 500, AzureEnvironment azureEnvironment = AzureEnvironment.Production) { - List unifiedGroupUsers = null; - List unifiedGroupGraphUsers = null; - IGroupOwnersCollectionWithReferencesPage groupUsers = null; - if (String.IsNullOrEmpty(accessToken)) { throw new ArgumentNullException(nameof(accessToken)); @@ -1583,184 +1295,65 @@ public static List GetUnifiedGroupOwners(UnifiedGroupEntity gr try { - var result = Task.Run(async () => - { - var graphClient = CreateGraphClient(accessToken, retryCount, delay, azureEnvironment); - - // Get the owners of an Office 365 group. - groupUsers = await graphClient.Groups[group.GroupId].Owners.Request().GetAsync(); - if (groupUsers.CurrentPage != null && groupUsers.CurrentPage.Count > 0) - { - unifiedGroupGraphUsers = new List(); - GenerateGraphUserCollection(groupUsers.CurrentPage, unifiedGroupGraphUsers); - } - - // Retrieve users when the results are paged. - while (groupUsers.NextPageRequest != null) - { - groupUsers = groupUsers.NextPageRequest.GetAsync().GetAwaiter().GetResult(); - if (groupUsers.CurrentPage != null && groupUsers.CurrentPage.Count > 0) - { - GenerateGraphUserCollection(groupUsers.CurrentPage, unifiedGroupGraphUsers); - } - } - - // Create the collection of type OfficeDevPnP 'UnifiedGroupUser' after all users are retrieved, including paged data. - if (unifiedGroupGraphUsers != null && unifiedGroupGraphUsers.Count > 0) - { - unifiedGroupUsers = new List(); - foreach (User usr in unifiedGroupGraphUsers) - { - UnifiedGroupUser groupUser = new UnifiedGroupUser - { - Id = usr.Id, - UserPrincipalName = usr.UserPrincipalName != null ? usr.UserPrincipalName : string.Empty, - DisplayName = usr.DisplayName != null ? usr.DisplayName : string.Empty, - GivenName = usr.GivenName != null ? usr.GivenName : string.Empty, - Surname = usr.Surname != null ? usr.Surname : string.Empty, - Email = usr.Mail != null ? usr.Mail : string.Empty, - MobilePhone = usr.MobilePhone != null ? usr.DisplayName : string.Empty, - PreferredLanguage = usr.PreferredLanguage != null ? usr.PreferredLanguage : string.Empty, - JobTitle = usr.JobTitle != null ? usr.DisplayName : string.Empty, - BusinessPhones = usr.BusinessPhones != null ? usr.BusinessPhones.ToArray() : null - }; - unifiedGroupUsers.Add(groupUser); - } - } - return unifiedGroupUsers; + var requestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}groups/{group.GroupId}/owners/microsoft.graph.user"; + var owners = GraphUtility.ReadPagedDataFromRequest(requestUrl, accessToken, retryCount: retryCount, delay: delay).ToList(); - }).GetAwaiter().GetResult(); - } - catch (ServiceException ex) - { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); - throw; - } - return unifiedGroupUsers; - } - - /// - /// Helper method. Generates a collection of Microsoft.Graph.User entity from directory objects. - /// - /// - /// - /// Returns a collection of Microsoft.Graph.User entity - private static List GenerateGraphUserCollection(IList page, List unifiedGroupGraphUsers) - { - // Create a collection of Microsoft.Graph.User type - foreach (User usr in page) - { - if (usr != null) + foreach (var u in owners) { - unifiedGroupGraphUsers.Add(usr); + u.UserPrincipalName ??= string.Empty; + u.DisplayName ??= string.Empty; + u.GivenName ??= string.Empty; + u.Surname ??= string.Empty; + u.Email ??= string.Empty; + u.MobilePhone ??= string.Empty; + u.PreferredLanguage ??= string.Empty; + u.JobTitle ??= string.Empty; } - } - return unifiedGroupGraphUsers; - } - - /// - /// Helper method. Generates a neseted collection of Microsoft.Graph.User entity from directory objects. - /// - /// - /// - /// - /// - /// Defines the Azure Cloud Deployment. This is used to determine the MS Graph EndPoint to call which differs per Azure Cloud deployments. Defaults to Production (graph.microsoft.com). - /// - private static List GenerateNestedGraphUserCollection(IList page, List unifiedGroupGraphUsers, List unifiedGroupUsers, string accessToken, AzureEnvironment azureEnvironment = AzureEnvironment.Production) - { - // Create a collection of Microsoft.Graph.User type - foreach (var usr in page) - { - - if (usr != null) - { - if (usr.GetType() == typeof(User)) - { - unifiedGroupGraphUsers.Add((User)usr); - } - } + return owners; } - - //Get groups within the group and users in that group - List unifiedGroupGraphGroups = new List(); - GenerateGraphGroupCollection(page, unifiedGroupGraphGroups); - foreach (Group unifiedGroupGraphGroup in unifiedGroupGraphGroups) - { - var grp = GetUnifiedGroup(unifiedGroupGraphGroup.Id, accessToken, azureEnvironment: azureEnvironment); - unifiedGroupUsers.AddRange(GetUnifiedGroupMembers(grp, accessToken)); - } - - return unifiedGroupGraphUsers; - } - - /// - /// Helper method. Generates a collection of Microsoft.Graph.Group entity from directory objects. - /// - /// - /// - /// - private static List GenerateGraphGroupCollection(IList page, List unifiedGroupGraphGroups) - { - // Create a collection of Microsoft.Graph.Group type - foreach (var grp in page) + catch (HttpResponseException ex) { - - if (grp != null) - { - if (grp.GetType() == typeof(Group)) - { - unifiedGroupGraphGroups.Add((Group)grp); - } - } + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); + throw; } - - return unifiedGroupGraphGroups; } /// - /// Helper method. Generates a collection of Microsoft.Graph.User entity from string array + /// Helper method. Generates a collection of User Ids from string array /// - /// Graph service client /// String array of users /// - - private static List GetUsers(GraphServiceClient graphClient, string[] groupUsers) + private static List GetUserIds(string accessToken, string[] groupUsers, int retryCount, int delay, AzureEnvironment azureEnvironment) { if (groupUsers == null || groupUsers.Length == 0) { - return new List(); + return new List(); } - var result = Task.Run(async () => + var usersResult = new List(); + foreach (var groupUser in groupUsers) { - var usersResult = new List(); - foreach (string groupUser in groupUsers) + try { - try - { - // Search for the user object - IGraphServiceUsersCollectionPage userQuery = await graphClient.Users - .Request() - .Select("Id") - .Filter($"userPrincipalName eq '{Uri.EscapeDataString(groupUser.Replace("'", "''"))}'") - .GetAsync(); - - User user = userQuery.FirstOrDefault(); - if (user != null) - { - usersResult.Add(user); - } - } - catch (ServiceException) + var requestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}users?$select=Id&$filter=userPrincipalName eq '{Uri.EscapeDataString(groupUser.Replace("'", "''"))}'"; + var responseAsString = HttpHelper.MakeGetRequestForString(requestUrl, accessToken, retryCount: retryCount, delay: delay); + + var jsonNode = JsonNode.Parse(responseAsString); + var usersArray = jsonNode["value"].AsArray(); + var id = usersArray.FirstOrDefault()?["id"]?.GetValue(); + + if (id != null) { - // skip, group provisioning shouldnt stop because of error in user object + usersResult.Add(id.Value.ToString()); } } - return usersResult; - }).GetAwaiter().GetResult(); - return result; + catch (HttpResponseException) + { + // skip, group provisioning shouldnt stop because of error in user object + } + } + return usersResult; } /// @@ -1800,9 +1393,9 @@ public static string GetGroupClassification(string groupId, string accessToken, } } - catch (ServiceException e) + catch (ApplicationException e) { - classification = e.Error.Message; + classification = e.Message; } return classification; @@ -1848,9 +1441,9 @@ public static bool HasTeamsTeam(string groupId, string accessToken, AzureEnviron } } } - catch (ServiceException ex) + catch (ApplicationException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, ex.Message); throw; } @@ -1884,14 +1477,11 @@ public static async Task CreateTeam(string groupId, string accessToken, int time iterations++; try { - await Task.Run(() => + var teamid = HttpHelper.MakePutRequestForString(createTeamEndPoint, new { }, HttpHelper.JsonContentType, accessToken); + if (!string.IsNullOrEmpty(teamid)) { - var teamid = HttpHelper.MakePutRequestForString(createTeamEndPoint, new { }, "application/json", accessToken); - if (!string.IsNullOrEmpty(teamid)) - { - wait = false; - } - }); + wait = false; + } } catch (Exception ex) { @@ -2005,7 +1595,7 @@ public static void RestoreDeletedUnifiedGroup(string groupId, string accessToken { try { - HttpHelper.MakePostRequest($"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}directory/deleteditems/{groupId}/restore", contentType: "application/json", accessToken: accessToken); + HttpHelper.MakePostRequest($"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}directory/deleteditems/{groupId}/restore", contentType: HttpHelper.JsonContentType, accessToken: accessToken); } catch (Exception e) { diff --git a/src/lib/PnP.Framework/Graph/UsersUtility.cs b/src/lib/PnP.Framework/Graph/UsersUtility.cs index f0cf3fa7d..7f6ad2c35 100644 --- a/src/lib/PnP.Framework/Graph/UsersUtility.cs +++ b/src/lib/PnP.Framework/Graph/UsersUtility.cs @@ -1,14 +1,12 @@ -using Microsoft.Graph; -using Newtonsoft.Json; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using PnP.Framework.Diagnostics; using PnP.Framework.Utilities; using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; -using System.Threading.Tasks; -using System.Web; +using System.Text.Json.Nodes; +using System.Text.Json; namespace PnP.Framework.Graph { @@ -108,68 +106,40 @@ public static Model.User GetUser(string accessToken, string userPrincipalName, s } } - List result = null; try { - // Use a synchronous model to invoke the asynchronous process - result = Task.Run(async () => + var requestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}users"; + var queryStringParams = new List() { - List users = new List(); - - var graphClient = GraphUtility.CreateGraphClient(accessToken, retryCount, delay, useBetaEndPoint: useBetaEndPoint, azureEnvironment: azureEnvironment); - - IGraphServiceUsersCollectionPage pagedUsers; - - // Retrieve the first batch of users. 999 is the maximum amount of users that Graph allows to be trieved in 1 go. Use maximum size batches to lessen the chance of throttling when retrieving larger amounts of users. - pagedUsers = await graphClient.Users.Request() - .Select(string.Join(",", propertiesToSelect)) - .Filter(filter) - .OrderBy(orderby) - .Top(!endIndex.HasValue ? 999 : endIndex.Value >= 999 ? 999 : endIndex.Value) - .GetAsync(); - - int pageCount = 0; - int currentIndex = 0; - - while (true) - { - pageCount++; - - foreach (var pagedUser in pagedUsers) - { - currentIndex++; - - if(endIndex.HasValue && endIndex.Value < currentIndex) - { - break; - } - - if (currentIndex >= startIndex) - { - users.Add(MapUserEntity(pagedUser, selectProperties)); - } - } - - if (pagedUsers.NextPageRequest != null && (!endIndex.HasValue || currentIndex < endIndex.Value)) - { - // Retrieve the next batch of users. The possible oData instructions such as select and filter are already incorporated in the nextLink provided by Graph and thus do not need to be specified again. - pagedUsers = await pagedUsers.NextPageRequest.GetAsync(); - } - else - { - break; - } - } + $"$top={(!endIndex.HasValue ? 999 : endIndex.Value >= 999 ? 999 : endIndex.Value)}" + }; + if (propertiesToSelect.Count > 0) + { + queryStringParams.Add("$select=" + string.Join(",", propertiesToSelect)); + } + if (!string.IsNullOrEmpty(filter)) + { + queryStringParams.Add($"$filter={filter}"); + } + if (!string.IsNullOrEmpty(orderby)) + { + queryStringParams.Add($"orderby={orderby}"); + } + requestUrl += $"?{string.Join("&", queryStringParams)}"; + IEnumerable users = GraphUtility.ReadPagedDataFromRequest(requestUrl, accessToken, retryCount: retryCount, delay: delay) + .Skip(startIndex); + if (endIndex.HasValue) + { + users = users.Take(endIndex.Value - startIndex); + } + return users.ToList(); - return users; - }).GetAwaiter().GetResult(); } - catch (ServiceException ex) + catch (HttpResponseException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } - return result; } /// @@ -196,103 +166,94 @@ public static Model.UserDelta ListUserDelta(string accessToken, string deltaToke } // Rewrite AdditionalProperties to Additional Data var propertiesToSelect = ignoreDefaultProperties ? new List() : new List { "BusinessPhones", "DisplayName", "GivenName", "JobTitle", "Mail", "MobilePhone", "OfficeLocation", "PreferredLanguage", "Surname", "UserPrincipalName", "Id", "AccountEnabled" }; - + selectProperties = selectProperties?.Select(p => p == "AdditionalProperties" ? "AdditionalData" : p).ToArray(); - - if(selectProperties != null) + + if (selectProperties != null) { - foreach(var property in selectProperties) + foreach (var property in selectProperties) { - if(!propertiesToSelect.Contains(property)) + if (!propertiesToSelect.Contains(property)) { propertiesToSelect.Add(property); } } } + List users = new List(); - var queryOptions = new List(); - - if(!string.IsNullOrWhiteSpace(deltaToken)) + var requestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}users"; + var queryStringParams = new List() + { + $"$top={(!endIndex.HasValue ? 999 : endIndex.Value >= 999 ? 999 : endIndex.Value)}", + $"$skiptoken={deltaToken}", + }; + if (propertiesToSelect.Count > 0) + { + queryStringParams.Add("$select=" + string.Join(",", propertiesToSelect)); + } + if (!string.IsNullOrEmpty(filter)) { - queryOptions.Add(new QueryOption("$skiptoken", deltaToken)); + queryStringParams.Add($"$filter={filter}"); } + if (!string.IsNullOrEmpty(orderby)) + { + queryStringParams.Add($"orderby={orderby}"); + } + requestUrl += $"?{string.Join("&", queryStringParams)}"; - Model.UserDelta result = null; try { - // Use a synchronous model to invoke the asynchronous process - result = Task.Run(async () => - { - var usersDelta = new Model.UserDelta(); - usersDelta.Users = new List(); + int currentIndex = 0; + var usersDelta = new Model.UserDelta(); + usersDelta.Users = users; - var graphClient = GraphUtility.CreateGraphClient(accessToken, retryCount, delay, useBetaEndPoint: useBetaEndPoint, azureEnvironment: azureEnvironment); - IUserDeltaCollectionPage pagedUsers; - - // Retrieve the first batch of users. 999 is the maximum amount of users that Graph allows to be trieved in 1 go. Use maximum size batches to lessen the chance of throttling when retrieving larger amounts of users. - pagedUsers = await graphClient.Users.Delta() - .Request(queryOptions) - .Select(string.Join(",", propertiesToSelect)) - .Filter(filter) - .OrderBy(orderby) - .Top(!endIndex.HasValue ? 999 : endIndex.Value >= 999 ? 999 : endIndex.Value) - .GetAsync(); + while (requestUrl != null) + { + var responseData = HttpHelper.MakeGetRequestForString(requestUrl, accessToken, retryCount: retryCount, delay: delay); - int pageCount = 0; - int currentIndex = 0; + var jsonNode = JsonNode.Parse(responseData); + JsonNode valueNode = jsonNode["value"]; + var results = valueNode.Deserialize(GraphUtility.CaseInsensitiveJsonOptions); - while (true) + foreach (var r in results) { - pageCount++; + currentIndex++; - foreach (var pagedUser in pagedUsers) + if (endIndex.HasValue && endIndex.Value < currentIndex) { - currentIndex++; - - if(endIndex.HasValue && endIndex.Value < currentIndex) - { - break; - } - - if (currentIndex >= startIndex) - { - usersDelta.Users.Add(MapUserEntity(pagedUser, selectProperties)); - } + break; } - - if (pagedUsers.NextPageRequest != null && (!endIndex.HasValue || currentIndex < endIndex.Value)) + if (currentIndex >= startIndex) { - // Retrieve the next batch of users. The possible oData instructions such as select and filter are already incorporated in the nextLink provided by Graph and thus do not need to be specified again. - pagedUsers = await pagedUsers.NextPageRequest.GetAsync(); + users.Add(r); } - else - { - // Check if the deltaLink is provided in the response - if(pagedUsers.AdditionalData.TryGetValue("@odata.deltaLink", out object deltaLinkObject)) - { - // Use a regular expression to fetch just the deltatoken part from the deltalink. The base of the URL will thereby be cut off. This is the only part we need to use it in a subsequent run. - var deltaLinkMatch = System.Text.RegularExpressions.Regex.Match(deltaLinkObject.ToString(), @"(?<=\$deltatoken=)(.*?)(?=$|&)", System.Text.RegularExpressions.RegexOptions.IgnoreCase); + } - if(deltaLinkMatch.Success && !string.IsNullOrWhiteSpace(deltaLinkMatch.Value)) - { - // Successfully extracted the deltatoken part from the link, assign it to the return variable - usersDelta.DeltaToken = deltaLinkMatch.Value; - } - } - break; + usersDelta.NextLink = jsonNode["@odata.nextLink"]?.ToString(); + requestUrl = (endIndex.HasValue && endIndex.Value < currentIndex) ? null : usersDelta.NextLink; + + var deltaLink = jsonNode["@odata.deltalink"]?.ToString(); + + if (string.IsNullOrWhiteSpace(deltaLink)) + { + // Use a regular expression to fetch just the deltatoken part from the deltalink. The base of the URL will thereby be cut off. This is the only part we need to use it in a subsequent run. + var deltaLinkMatch = System.Text.RegularExpressions.Regex.Match(deltaLink, @"(?<=\$deltatoken=)(.*?)(?=$|&)", System.Text.RegularExpressions.RegexOptions.IgnoreCase); + + if (deltaLinkMatch.Success && !string.IsNullOrWhiteSpace(deltaLinkMatch.Value)) + { + // Successfully extracted the deltatoken part from the link, assign it to the return variable + usersDelta.DeltaToken = deltaLinkMatch.Value; } } - - return usersDelta; - }).GetAwaiter().GetResult(); + } + return usersDelta; } - catch (ServiceException ex) + catch (HttpResponseException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } - return result; } /// @@ -327,100 +288,33 @@ public static Model.UserDelta ListUserDelta(string accessToken, string deltaToke } } - List result = null; try { - // Use a synchronous model to invoke the asynchronous process - result = Task.Run(async () => + List users = new List(); + var requestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}directory/deleteditems/microsoft.graph.user"; + if (propertiesToSelect.Count > 0) { - List users = new List(); - var requestUrl = $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment)}directory/deleteditems/microsoft.graph.user"; - if (propertiesToSelect.Count > 0) - { - requestUrl += $"?$select={string.Join(",", propertiesToSelect)}"; - } + requestUrl += $"?$select={string.Join(",", propertiesToSelect)}"; + } - var responseAsString = HttpHelper.MakeGetRequestForString(requestUrl, accessToken, retryCount: retryCount, delay: delay); + var responseAsString = HttpHelper.MakeGetRequestForString(requestUrl, accessToken, retryCount: retryCount, delay: delay); - var response = JToken.Parse(responseAsString); - var deletedUsers = response["value"]; + var response = JToken.Parse(responseAsString); + var deletedUsers = response["value"]; - foreach (var deletedUser in deletedUsers) - { - var user = deletedUser.ToObject(); - var modelUser = MapUserEntity(user, selectProperties); - users.Add(modelUser); - } + foreach (var deletedUser in deletedUsers) + { + var user = deletedUser.ToObject(); + users.Add(user); + } - return users; - }).GetAwaiter().GetResult(); + return users; } - catch (ServiceException ex) + catch (ApplicationException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, ex.Message); throw; } - return result; - } - - /// - /// Maps a Graph User result to a local User model - /// - /// Graph User entity - /// Properties to copy over from the Graph model to the local User model - /// Local User model filled with the information Graph User entity - private static Model.User MapUserEntity(User graphUser, string[] selectProperties) - { - var user = new Model.User - { - Id = Guid.TryParse(graphUser.Id, out Guid idGuid) ? (Guid?)idGuid : null, - DisplayName = graphUser.DisplayName, - GivenName = graphUser.GivenName, - JobTitle = graphUser.JobTitle, - MobilePhone = graphUser.MobilePhone, - OfficeLocation = graphUser.OfficeLocation, - PreferredLanguage = graphUser.PreferredLanguage, - Surname = graphUser.Surname, - UserPrincipalName = graphUser.UserPrincipalName, - BusinessPhones = graphUser.BusinessPhones, - AdditionalProperties = graphUser.AdditionalData, - Mail = graphUser.Mail, - AccountEnabled = graphUser.AccountEnabled, - }; - - // If additional properties have been provided, ensure their output gets added to the AdditionalProperties dictonary of the output - if (selectProperties != null) - { - // Ensure we have the AdditionalProperties dictionary available to fill, if necessary - if(user.AdditionalProperties == null) - { - user.AdditionalProperties = new Dictionary< - string, object>(); - } - - foreach (var selectProperty in selectProperties) - { - // Ensure the requested property has been returned in the response - var property = graphUser.GetType().GetProperty(selectProperty, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); - if (property != null) - { - // First check if we have the property natively on the User model - var userProperty = user.GetType().GetProperty(selectProperty, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); - if(userProperty != null) - { - // Set the property on the User model - userProperty.SetValue(user, property.GetValue(graphUser), null); - } - else - { - // Property does not exist on the User model, add the property to the AdditionalProperties dictionary - user.AdditionalProperties.Add(selectProperty, property.GetValue(graphUser)); - } - } - } - } - - return user; } /// @@ -460,7 +354,7 @@ public static Model.TemporaryAccessPassResponse RequestTemporaryAccessPass(strin var response = GraphHttpClient.MakePostRequestForString( requestUrl: $"{GraphHttpClient.GetGraphEndPointUrl(azureEnvironment, beta: true)}users/{userId}/authentication/temporaryAccessPassMethods", content: temporaryAccessPassAuthenticationMethod, - contentType: "application/json", + contentType: HttpHelper.JsonContentType, accessToken: accessToken); // Parse and return the response @@ -468,9 +362,9 @@ public static Model.TemporaryAccessPassResponse RequestTemporaryAccessPass(strin return accessPassResponse; } - catch (ServiceException ex) + catch (HttpResponseException ex) { - Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Error(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); throw; } } diff --git a/src/lib/PnP.Framework/PnP.Framework.csproj b/src/lib/PnP.Framework/PnP.Framework.csproj index 49eebe7b9..2f3d01173 100644 --- a/src/lib/PnP.Framework/PnP.Framework.csproj +++ b/src/lib/PnP.Framework/PnP.Framework.csproj @@ -188,7 +188,6 @@ - @@ -197,13 +196,11 @@ - - @@ -217,7 +214,6 @@ - @@ -241,13 +237,12 @@ - - - + + diff --git a/src/lib/PnP.Framework/Provisioning/ObjectHandlers/TokenParser.cs b/src/lib/PnP.Framework/Provisioning/ObjectHandlers/TokenParser.cs index 7bfe81a48..ebdc7b703 100644 --- a/src/lib/PnP.Framework/Provisioning/ObjectHandlers/TokenParser.cs +++ b/src/lib/PnP.Framework/Provisioning/ObjectHandlers/TokenParser.cs @@ -545,10 +545,10 @@ private void AddGroupTokens(Web web) } } } - catch (Microsoft.Graph.ServiceException ex) + catch (HttpResponseException ex) { // If we don't have permission to access the O365 groups, just skip it - Log.Warning(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Error.Message); + Log.Warning(Constants.LOGGING_SOURCE, CoreResources.GraphExtensions_ErrorOccured, ex.Message); } } diff --git a/src/lib/PnP.Framework/Utilities/HttpHelper.cs b/src/lib/PnP.Framework/Utilities/HttpHelper.cs index 24285ccf3..34b54778b 100644 --- a/src/lib/PnP.Framework/Utilities/HttpHelper.cs +++ b/src/lib/PnP.Framework/Utilities/HttpHelper.cs @@ -7,6 +7,7 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Text; +using System.Threading.Tasks; namespace PnP.Framework.Utilities { @@ -496,6 +497,47 @@ internal static TResult MakeHttpRequest( int delay = 500, string userAgent = null, ClientContext spContext = null) + { + var result = MakeHttpRequestAsync(httpMethod, requestUrl, accessToken, accept, content, contentType, referer, resultPredicate, requestHeaders, cookies, retryCount, delay, userAgent, spContext).Result; + responseHeaders = result.ResponseHeaders; + return (result.Result); + } + + /// + /// This helper method makes an HTTP request and eventually returns a result + /// + /// The HTTP method for the request + /// The URL of the request + /// The response headers of the HTTP request (output argument) + /// The OAuth 2.0 Access Token for the request, if authorization is required + /// The content type of the accepted response + /// The content of the request + /// The content type of the request + /// The URL Referer for the request + /// The predicate to retrieve the result, if any + /// A collection of any custom request headers + /// Any request cookies values + /// Number of times to retry the request + /// Milliseconds to wait before retrying the request. The delay will be increased (doubled) every retry + /// UserAgent string value to insert for this request. You can define this value in your app's config file using key="SharePointPnPUserAgent" value="PnPRocks" + /// An optional SharePoint client context + /// The type of the result, if any + /// The value of the result, if any + internal static async Task> MakeHttpRequestAsync( + string httpMethod, + string requestUrl, + string accessToken = null, + string accept = null, + object content = null, + string contentType = null, + string referer = null, + Func resultPredicate = null, + Dictionary requestHeaders = null, + Dictionary cookies = null, + int retryCount = 1, + int delay = 500, + string userAgent = null, + ClientContext spContext = null) { //HttpClient client = HttpHelper.httpClient; HttpClient client; @@ -514,7 +556,7 @@ internal static TResult MakeHttpRequest( // Prepare the variable to hold the result, if any TResult result = default; - responseHeaders = null; + HttpResponseHeaders responseHeaders = null; if (!string.IsNullOrEmpty(referer)) { @@ -578,7 +620,7 @@ internal static TResult MakeHttpRequest( } // Fire the HTTP request - HttpResponseMessage response = client.SendAsync(request).Result; + HttpResponseMessage response = await client.SendAsync(request); if (response.IsSuccessStatusCode) { @@ -594,13 +636,14 @@ internal static TResult MakeHttpRequest( } else { - throw new ApplicationException( + throw new HttpResponseException( string.Format("Exception while invoking endpoint {0}.", requestUrl), - new Exception(response.Content.ReadAsStringAsync().Result)); + new Exception(response.Content.ReadAsStringAsync().Result), + (int)response.StatusCode); } } - return (result); + return new HttpResult(result, responseHeaders); } } diff --git a/src/lib/PnP.Framework/Utilities/HttpResponseException.cs b/src/lib/PnP.Framework/Utilities/HttpResponseException.cs new file mode 100644 index 000000000..3a3d19c4f --- /dev/null +++ b/src/lib/PnP.Framework/Utilities/HttpResponseException.cs @@ -0,0 +1,14 @@ +using System; + +namespace PnP.Framework.Utilities +{ + public class HttpResponseException : ApplicationException + { + public HttpResponseException(string message, Exception exception, int statusCode) :base(message, exception) + { + StatusCode = statusCode; + } + + public int StatusCode { get; } + } +} diff --git a/src/lib/PnP.Framework/Utilities/HttpResult.cs b/src/lib/PnP.Framework/Utilities/HttpResult.cs new file mode 100644 index 000000000..b22b0b841 --- /dev/null +++ b/src/lib/PnP.Framework/Utilities/HttpResult.cs @@ -0,0 +1,16 @@ +using System.Net.Http.Headers; + +namespace PnP.Framework.Utilities +{ + public class HttpResult + { + public HttpResult(T Result, HttpResponseHeaders ResponseHeaders) + { + this.Result = Result; + this.ResponseHeaders = ResponseHeaders; + } + + public T Result { get; } + public HttpResponseHeaders ResponseHeaders { get; } + } +} \ No newline at end of file