├── docs
├── app1.png
├── register_app.png
├── api_permissions.png
├── platform_config.png
└── solution_explorer.png
├── MsGraphSamples.WPF
├── Assets
│ └── MSGraph.png
├── App.xaml.cs
├── App.xaml
├── MsGraphSamples.WPF.csproj
├── Helpers
│ ├── WebHyperlink.cs
│ └── Converters.cs
├── ViewModels
│ ├── ViewModelLocator.cs
│ └── MainViewModel.cs
└── Views
│ ├── MainWindow.xaml.cs
│ └── MainWindow.xaml
├── MsGraphSamples.WinUI
├── Assets
│ ├── MSGraph.ico
│ ├── SplashScreen.scale-200.png
│ ├── LockScreenLogo.scale-200.png
│ ├── Square44x44Logo.scale-200.png
│ ├── Wide310x150Logo.scale-200.png
│ ├── Square150x150Logo.scale-200.png
│ └── Square44x44Logo.targetsize-24_altform-unplated.png
├── Properties
│ └── launchsettings.json
├── Views
│ ├── MainWindow.xaml.cs
│ ├── MainWindow.xaml
│ ├── MainPage.xaml.cs
│ └── MainPage.xaml
├── Helpers
│ ├── Debouncer.cs
│ ├── DialogService.cs
│ ├── Converters.cs
│ └── AsyncLoadingCollection.cs
├── App.xaml
├── app.manifest
├── Package.appxmanifest
├── MsGraphSamples.WinUI.csproj
├── App.xaml.cs
└── ViewModels
│ └── MainViewModel.cs
├── .github
├── dependabot.yml
└── workflows
│ └── dotnet.yml
├── CODE_OF_CONDUCT.md
├── MsGraphSamples.Services
├── MSGraphSamples.Services.csproj
├── AuthService.cs
├── ExtensionMethods.cs
├── GraphExtensions.cs
├── GraphDataService.cs
└── AsyncEnumerableGraphDataService.cs
├── LICENSE
├── SECURITY.md
├── MsGraph Samples.sln
├── .gitignore
└── README.md
/docs/app1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftgraph/dotnet-aad-query-sample/main/docs/app1.png
--------------------------------------------------------------------------------
/docs/register_app.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftgraph/dotnet-aad-query-sample/main/docs/register_app.png
--------------------------------------------------------------------------------
/docs/api_permissions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftgraph/dotnet-aad-query-sample/main/docs/api_permissions.png
--------------------------------------------------------------------------------
/docs/platform_config.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftgraph/dotnet-aad-query-sample/main/docs/platform_config.png
--------------------------------------------------------------------------------
/docs/solution_explorer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftgraph/dotnet-aad-query-sample/main/docs/solution_explorer.png
--------------------------------------------------------------------------------
/MsGraphSamples.WPF/Assets/MSGraph.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftgraph/dotnet-aad-query-sample/main/MsGraphSamples.WPF/Assets/MSGraph.png
--------------------------------------------------------------------------------
/MsGraphSamples.WinUI/Assets/MSGraph.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftgraph/dotnet-aad-query-sample/main/MsGraphSamples.WinUI/Assets/MSGraph.ico
--------------------------------------------------------------------------------
/MsGraphSamples.WinUI/Assets/SplashScreen.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftgraph/dotnet-aad-query-sample/main/MsGraphSamples.WinUI/Assets/SplashScreen.scale-200.png
--------------------------------------------------------------------------------
/MsGraphSamples.WinUI/Assets/LockScreenLogo.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftgraph/dotnet-aad-query-sample/main/MsGraphSamples.WinUI/Assets/LockScreenLogo.scale-200.png
--------------------------------------------------------------------------------
/MsGraphSamples.WinUI/Assets/Square44x44Logo.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftgraph/dotnet-aad-query-sample/main/MsGraphSamples.WinUI/Assets/Square44x44Logo.scale-200.png
--------------------------------------------------------------------------------
/MsGraphSamples.WinUI/Assets/Wide310x150Logo.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftgraph/dotnet-aad-query-sample/main/MsGraphSamples.WinUI/Assets/Wide310x150Logo.scale-200.png
--------------------------------------------------------------------------------
/MsGraphSamples.WinUI/Assets/Square150x150Logo.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftgraph/dotnet-aad-query-sample/main/MsGraphSamples.WinUI/Assets/Square150x150Logo.scale-200.png
--------------------------------------------------------------------------------
/MsGraphSamples.WinUI/Assets/Square44x44Logo.targetsize-24_altform-unplated.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftgraph/dotnet-aad-query-sample/main/MsGraphSamples.WinUI/Assets/Square44x44Logo.targetsize-24_altform-unplated.png
--------------------------------------------------------------------------------
/MsGraphSamples.WPF/App.xaml.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | using System.Windows;
5 |
6 | namespace MsGraphSamples;
7 |
8 | public partial class App : Application
9 | {
10 | }
--------------------------------------------------------------------------------
/MsGraphSamples.WinUI/Properties/launchsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "MsGraphSamples.WinUI (Package)": {
4 | "commandName": "MsixPackage"
5 | },
6 | "MsGraphSamples.WinUI (Unpackaged)": {
7 | "commandName": "Project"
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/MsGraphSamples.WinUI/Views/MainWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.UI.Xaml;
2 |
3 | namespace MsGraphSamples.WinUI;
4 |
5 | public sealed partial class MainWindow : Window
6 | {
7 | public MainWindow()
8 | {
9 | this.InitializeComponent();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "nuget"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 | pull_request:
8 | labels:
9 | - automerge
10 | auto_merge: true
11 |
12 | - package-ecosystem: "github-actions"
13 | directory: "/"
14 | schedule:
15 | interval: "daily"
16 | pull_request:
17 | labels:
18 | - automerge
19 | auto_merge: true
20 |
--------------------------------------------------------------------------------
/MsGraphSamples.WPF/App.xaml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/MsGraphSamples.WinUI/Views/MainWindow.xaml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Microsoft Open Source Code of Conduct
2 |
3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
4 |
5 | Resources:
6 |
7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)
8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns
10 | - Employees can reach out at [aka.ms/opensource/moderation-support](https://aka.ms/opensource/moderation-support)
11 |
--------------------------------------------------------------------------------
/MsGraphSamples.WinUI/Helpers/Debouncer.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.UI.Xaml;
2 |
3 | namespace MsGraphSamples.WinUI.Helpers;
4 |
5 | public class Debouncer
6 | {
7 | private readonly DispatcherTimer timer;
8 | private Action? action;
9 |
10 | public Debouncer(TimeSpan delay)
11 | {
12 | timer = new DispatcherTimer { Interval = delay };
13 | timer.Tick += Timer_Tick;
14 | }
15 |
16 | public void Debounce(Action action)
17 | {
18 | this.action = action;
19 | timer.Stop();
20 | timer.Start();
21 | }
22 |
23 | private void Timer_Tick(object? sender, object e)
24 | {
25 | timer.Stop();
26 | action?.Invoke();
27 | }
28 | }
--------------------------------------------------------------------------------
/MsGraphSamples.Services/MSGraphSamples.Services.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0-windows10.0.22000.0
5 | enable
6 | enable
7 | 42125e81-2956-48ae-a133-1633482ae5e8
8 | 10.0.22000.0
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.github/workflows/dotnet.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a .NET project
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
3 |
4 | name: .NET
5 |
6 | on:
7 | push:
8 | branches: [ "main" ]
9 | pull_request:
10 | branches: [ "main" ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: windows-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v5
19 | - name: Setup .NET
20 | uses: actions/setup-dotnet@v4
21 | with:
22 | dotnet-version: 8.0.x
23 | - name: Restore dependencies
24 | run: dotnet restore
25 | - name: Build
26 | run: dotnet build --no-restore
27 | - name: Test
28 | run: dotnet test --no-build --verbosity normal
29 |
--------------------------------------------------------------------------------
/MsGraphSamples.WinUI/App.xaml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/MsGraphSamples.WinUI/app.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | PerMonitorV2
17 |
18 |
19 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Microsoft Graph
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MsGraphSamples.WPF/MsGraphSamples.WPF.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | WinExe
5 | net8.0-windows10.0.22000.0
6 | enable
7 | enable
8 | true
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | PreserveNewest
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/MsGraphSamples.WPF/Helpers/WebHyperlink.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using System.Windows;
3 | using System.Windows.Documents;
4 | using System.Windows.Navigation;
5 |
6 | namespace MsGraphSamples.WPF.Helpers;
7 |
8 | public class HyperlinkExtensions
9 | {
10 | public static bool GetIsWeb(DependencyObject obj)
11 | {
12 | return (bool)obj.GetValue(IsWebProperty);
13 | }
14 |
15 | public static void SetIsWeb(DependencyObject obj, bool value)
16 | {
17 | obj.SetValue(IsWebProperty, value);
18 | }
19 |
20 | public static readonly DependencyProperty IsWebProperty = DependencyProperty.RegisterAttached(
21 | "IsWeb",
22 | typeof(bool),
23 | typeof(HyperlinkExtensions),
24 | new UIPropertyMetadata(false, OnIsExternalChanged));
25 |
26 | private static void OnIsExternalChanged(object sender, DependencyPropertyChangedEventArgs args)
27 | {
28 | var hyperlink = (Hyperlink)sender;
29 |
30 | if ((bool)args.NewValue)
31 | hyperlink.RequestNavigate += Hyperlink_RequestNavigate;
32 | else
33 | hyperlink.RequestNavigate -= Hyperlink_RequestNavigate;
34 | }
35 | private static void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e)
36 | {
37 | var psi = new ProcessStartInfo(e.Uri.AbsoluteUri) { UseShellExecute = true };
38 | Process.Start(psi);
39 | e.Handled = true;
40 | }
41 | }
--------------------------------------------------------------------------------
/MsGraphSamples.WPF/ViewModels/ViewModelLocator.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | using System.Diagnostics.CodeAnalysis;
5 | using System.Windows;
6 | using CommunityToolkit.Mvvm.DependencyInjection;
7 | using Microsoft.Extensions.DependencyInjection;
8 | using MsGraphSamples.Services;
9 |
10 | namespace MsGraphSamples.WPF.ViewModels;
11 |
12 | [SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Binding Parameters")]
13 | public class ViewModelLocator
14 | {
15 | public static bool IsInDesignMode => Application.Current.MainWindow == null;
16 |
17 | public MainViewModel? MainVM => Ioc.Default.GetService();
18 |
19 | public ViewModelLocator()
20 | {
21 | Ioc.Default.ConfigureServices(GetServices());
22 | }
23 |
24 | private static ServiceProvider GetServices()
25 | {
26 | var serviceCollection = new ServiceCollection();
27 |
28 | if (!IsInDesignMode)
29 | {
30 | var authService = new AuthService();
31 | serviceCollection.AddSingleton(authService);
32 |
33 | var graphDataService = new GraphDataService(authService.GraphClient);
34 | serviceCollection.AddSingleton(graphDataService);
35 | }
36 |
37 | serviceCollection.AddTransient();
38 |
39 | return serviceCollection.BuildServiceProvider();
40 | }
41 | }
--------------------------------------------------------------------------------
/MsGraphSamples.WinUI/Helpers/DialogService.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.UI.Xaml;
2 | using Microsoft.UI.Xaml.Controls;
3 |
4 | namespace MsGraphSamples.WinUI.Helpers;
5 |
6 | public interface IDialogService
7 | {
8 | XamlRoot? Root { get; set; }
9 |
10 | Task ShowAsync(string title, string? text, string closeButtonText = "Ok", string? primaryButtonText = null, string? secondaryButtonText = null);
11 | }
12 |
13 | public class DialogService() : IDialogService
14 | {
15 | public XamlRoot? Root { get; set; }
16 |
17 | ///
18 | /// Shows a content dialog
19 | ///
20 | /// The text of the content dialog
21 | /// The title of the content dialog
22 | /// The text of the close button
23 | /// The text of the primary button (optional)
24 | /// The text of the secondary button (optional)
25 | /// The ContentDialogResult
26 | public async Task ShowAsync(string title, string? text, string closeButtonText = "Ok", string? primaryButtonText = null, string? secondaryButtonText = null)
27 | {
28 | var dialog = new ContentDialog()
29 | {
30 | Title = title,
31 | Content = text,
32 | CloseButtonText = closeButtonText,
33 | PrimaryButtonText = primaryButtonText,
34 | SecondaryButtonText = secondaryButtonText,
35 | XamlRoot = Root
36 | };
37 |
38 | return await dialog.ShowAsync();
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/MsGraphSamples.WinUI/Helpers/Converters.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | using Microsoft.UI.Xaml.Data;
5 | using System.Collections;
6 |
7 | namespace MsGraphSamples.WinUI.Converters;
8 |
9 | public partial class AdditionalDataConverter : IValueConverter
10 | {
11 | public object Convert(object value, Type targetType, object parameter, string language)
12 | {
13 | var additionalData = (IDictionary)value;
14 | additionalData.TryGetValue((string)parameter, out var extensionValue);
15 |
16 | return extensionValue?.ToString() ?? string.Empty;
17 | }
18 |
19 | public object ConvertBack(object value, Type targetType, object parameter, string language) => throw new NotImplementedException();
20 | }
21 | public class CollectionToCommaSeparatedStringConverter : IValueConverter
22 | {
23 | public object Convert(object value, Type targetType, object parameter, string language)
24 | {
25 | if (value is IEnumerable stringEnumerable)
26 | {
27 | return string.Join(", ", stringEnumerable);
28 | }
29 | if (value is IEnumerable enumerable && value is not string)
30 | {
31 | var items = new List();
32 | foreach (var item in enumerable)
33 | {
34 | if (item != null)
35 | items.Add(item.ToString());
36 | }
37 | return string.Join(", ", items);
38 | }
39 | return value?.ToString() ?? string.Empty;
40 | }
41 |
42 | public object ConvertBack(object value, Type targetType, object parameter, string language)
43 | {
44 | throw new NotImplementedException();
45 | }
46 | }
--------------------------------------------------------------------------------
/MsGraphSamples.WinUI/Package.appxmanifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
14 |
15 |
16 |
17 |
18 | MSGraphSamples.WinUI
19 | lucaspol
20 | Assets\StoreLogo.png
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
36 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/MsGraphSamples.WinUI/MsGraphSamples.WinUI.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | WinExe
4 | net8.0-windows10.0.26100.0
5 | 10.0.17763.0
6 | app.manifest
7 | x86;x64;arm64
8 | win-x86;win-x64;win-arm64
9 | win-$(Platform).pubxml
10 | true
11 | true
12 | enable
13 | enable
14 | x64
15 | 10.0.26100.0
16 | true
17 | Assets\MSGraph.ico
18 |
19 |
20 |
21 |
22 |
23 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
56 |
57 | true
58 |
59 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## Security
4 |
5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
6 |
7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below.
8 |
9 | ## Reporting Security Issues
10 |
11 | **Please do not report security vulnerabilities through public GitHub issues.**
12 |
13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report).
14 |
15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey).
16 |
17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc).
18 |
19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
20 |
21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
22 | * Full paths of source file(s) related to the manifestation of the issue
23 | * The location of the affected source code (tag/branch/commit or direct URL)
24 | * Any special configuration required to reproduce the issue
25 | * Step-by-step instructions to reproduce the issue
26 | * Proof-of-concept or exploit code (if possible)
27 | * Impact of the issue, including how an attacker might exploit the issue
28 |
29 | This information will help us triage your report more quickly.
30 |
31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs.
32 |
33 | ## Preferred Languages
34 |
35 | We prefer all communications to be in English.
36 |
37 | ## Policy
38 |
39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd).
40 |
41 |
42 |
--------------------------------------------------------------------------------
/MsGraphSamples.Services/AuthService.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | using Azure.Core;
5 | using Azure.Identity;
6 | using Microsoft.Extensions.Configuration;
7 | using Microsoft.Graph;
8 |
9 | namespace MsGraphSamples.Services;
10 |
11 | public interface IAuthService
12 | {
13 | GraphServiceClient GraphClient { get; }
14 | void Logout();
15 | }
16 |
17 | public class AuthService : IAuthService
18 | {
19 | private readonly IConfiguration _configuration = new ConfigurationBuilder().AddUserSecrets().Build();
20 |
21 | private readonly string _tokenPath;
22 | private static readonly string[] _scopes = ["Directory.Read.All"];
23 |
24 | private GraphServiceClient? _graphClient;
25 |
26 | //public GraphServiceClient GraphClient => _graphClient ??= new GraphServiceClient(GetAppCredential());
27 | public GraphServiceClient GraphClient => _graphClient ??= new GraphServiceClient(GetBrowserCredential());
28 |
29 | public AuthService()
30 | {
31 | var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
32 | _tokenPath = Path.Combine(localAppData, AppDomain.CurrentDomain.FriendlyName, "authToken.bin");
33 | }
34 |
35 | private ClientSecretCredential GetAppCredential() => new(
36 | _configuration["tenantId"],
37 | _configuration["clientId"],
38 | _configuration["clientSecret"]);
39 |
40 | private InteractiveBrowserCredential GetBrowserCredential()
41 | {
42 | var credentialOptions = new InteractiveBrowserCredentialOptions
43 | {
44 | ClientId = _configuration["clientId"],
45 | TokenCachePersistenceOptions = new TokenCachePersistenceOptions()
46 | };
47 |
48 | if (File.Exists(_tokenPath))
49 | {
50 | // use the cached token
51 | using var authRecordStream = File.OpenRead(_tokenPath);
52 | var authRecord = AuthenticationRecord.Deserialize(authRecordStream);
53 | credentialOptions.AuthenticationRecord = authRecord;
54 | return new InteractiveBrowserCredential(credentialOptions);
55 | }
56 | else
57 | {
58 | // create and cache the token
59 | var browserCredential = new InteractiveBrowserCredential(credentialOptions);
60 | var tokenRequestContext = new TokenRequestContext(_scopes);
61 | var authRecord = browserCredential.Authenticate(tokenRequestContext);
62 |
63 | Directory.CreateDirectory(Path.GetDirectoryName(_tokenPath)!);
64 | using var authRecordStream = File.OpenWrite(_tokenPath);
65 | authRecord.Serialize(authRecordStream);
66 |
67 | return browserCredential;
68 | }
69 | }
70 |
71 | public void Logout()
72 | {
73 | File.Delete(_tokenPath);
74 | _graphClient = null;
75 | }
76 | }
--------------------------------------------------------------------------------
/MsGraphSamples.Services/ExtensionMethods.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | namespace MsGraphSamples;
5 |
6 | public static class ExtensionMethods
7 | {
8 | public static bool In(this string s, params string[] items) => items.Any(i => i.Trim().Equals(s, StringComparison.InvariantCultureIgnoreCase));
9 | public static bool IsNullOrEmpty(this string? s) => string.IsNullOrEmpty(s);
10 |
11 | public static int NthIndexOf(this string input, char value, int nth, int startIndex = 0)
12 | {
13 | if (nth <= 0)
14 | throw new ArgumentException("Input must be greater than 0", nameof(nth));
15 | if (nth == 1)
16 | return input.IndexOf(value, startIndex);
17 |
18 | return input.NthIndexOf(value, --nth, input.IndexOf(value, startIndex) + 1);
19 | }
20 |
21 | ///
22 | /// Awaits a task without blocking the main thread. (From PRISM framework)
23 | ///
24 | /// Primarily used to replace async void scenarios such as ctor's and ICommands.
25 | /// The task to be awaited
26 | /// The action to perform when the task is complete.
27 | /// The action to perform when an error occurs executing the task.
28 | /// Configures an awaiter used to await this task
29 | public static async void Await(this Task task, Action? completedCallback = null, Action? errorCallback = null, bool configureAwait = false)
30 | {
31 | try
32 | {
33 | await task.ConfigureAwait(configureAwait);
34 | completedCallback?.Invoke();
35 | }
36 | catch (Exception ex)
37 | {
38 | errorCallback?.Invoke(ex);
39 | }
40 | }
41 |
42 | ///
43 | /// Awaits a task without blocking the main thread. (From PRISM framework)
44 | ///
45 | /// Primarily used to replace async void scenarios such as ctor's and ICommands.
46 | /// The task to be awaited
47 | /// The action to perform when the task is complete.
48 | /// The action to perform when an error occurs executing the task.
49 | /// Configures an awaiter used to await this task
50 | public static async void Await(this Task task, Action? completedCallback = null, Action? errorCallback = null, bool configureAwait = false)
51 | {
52 | try
53 | {
54 | var result = await task.ConfigureAwait(configureAwait);
55 | completedCallback?.Invoke(result);
56 | }
57 | catch (Exception ex)
58 | {
59 | errorCallback?.Invoke(ex);
60 | }
61 | }
62 | }
--------------------------------------------------------------------------------
/MsGraphSamples.WPF/Views/MainWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | using MsGraphSamples.WPF.Converters;
5 | using MsGraphSamples.WPF.ViewModels;
6 | using System.ComponentModel;
7 | using System.Windows;
8 | using System.Windows.Controls;
9 | using System.Windows.Data;
10 | using System.Windows.Input;
11 |
12 | namespace MsGraphSamples.WPF.Views;
13 |
14 | public partial class MainWindow : Window
15 | {
16 | private MainViewModel ViewModel => (MainViewModel)DataContext;
17 |
18 | public MainWindow()
19 | {
20 | InitializeComponent();
21 | }
22 |
23 | private void TextBox_SelectAll(object sender, RoutedEventArgs e)
24 | {
25 | var textBox = (TextBox)sender;
26 | textBox.SelectAll();
27 | }
28 |
29 | private void TextBox_PreviewMouseDown(object sender, MouseButtonEventArgs e)
30 | {
31 | var textBox = (TextBox)sender;
32 | if (!textBox.IsKeyboardFocusWithin)
33 | {
34 | textBox.Focus();
35 | e.Handled = true;
36 | }
37 | }
38 |
39 | private void LoadButton_Click(object sender, RoutedEventArgs e)
40 | {
41 | // Workaround to prevent IsDefault button to execute before TextBox Bindings
42 | var button = (Button)sender;
43 | button.Focus();
44 | }
45 |
46 | private void ResultsDataGrid_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
47 | {
48 | if (ViewModel.SplittedSelect.Length == 0)
49 | return;
50 |
51 | e.Cancel = !e.PropertyName.In(ViewModel.SplittedSelect);
52 | if (!e.Cancel)
53 | {
54 | e.Column.Width = new DataGridLength(1, DataGridLengthUnitType.Star);
55 |
56 | // If the property is a string collection, use the converter
57 | var property = e.PropertyType;
58 | if (typeof(System.Collections.IEnumerable).IsAssignableFrom(property) && property != typeof(string))
59 | {
60 | if (e.Column is DataGridTextColumn textColumn)
61 | {
62 | textColumn.Binding = new Binding(e.PropertyName)
63 | {
64 | Converter = new CollectionToCommaSeparatedStringConverter()
65 | };
66 | }
67 | }
68 |
69 | var orderByProperty = ViewModel.OrderBy?.Split(' ')[0];
70 | var direction = ViewModel.OrderBy?.Split(' ').ElementAtOrDefault(1) ?? "asc";
71 | if (e.PropertyName.Equals(orderByProperty, StringComparison.InvariantCultureIgnoreCase))
72 | {
73 | e.Column.SortDirection = direction.Equals("asc", StringComparison.InvariantCultureIgnoreCase)
74 | ? ListSortDirection.Ascending
75 | : ListSortDirection.Descending;
76 | }
77 | }
78 | }
79 |
80 | private void ResultsDataGrid_AutoGeneratedColumns(object sender, System.EventArgs e)
81 | {
82 | var dg = (DataGrid)sender;
83 | foreach (var column in dg.Columns)
84 | {
85 | column.DisplayIndex = Array.FindIndex(
86 | ViewModel.SplittedSelect,
87 | p => p.Equals(column.Header.ToString(), StringComparison.OrdinalIgnoreCase));
88 | }
89 | }
90 | }
--------------------------------------------------------------------------------
/MsGraphSamples.WinUI/App.xaml.cs:
--------------------------------------------------------------------------------
1 | using CommunityToolkit.Mvvm.DependencyInjection;
2 | using Microsoft.Extensions.DependencyInjection;
3 | using Microsoft.UI.Xaml;
4 | using Microsoft.UI.Xaml.Controls;
5 | using Microsoft.UI.Xaml.Navigation;
6 | using MsGraphSamples.Services;
7 | using MsGraphSamples.WinUI.Helpers;
8 | using MsGraphSamples.WinUI.ViewModels;
9 | using MsGraphSamples.WinUI.Views;
10 | using Windows.ApplicationModel;
11 |
12 | // To learn more about WinUI, the WinUI project structure,
13 | // and more about our project templates, see: http://aka.ms/winui-project-info.
14 |
15 | namespace MsGraphSamples.WinUI;
16 |
17 |
18 | ///
19 | /// Provides application-specific behavior to supplement the default Application class.
20 | ///
21 | public partial class App : Application
22 | {
23 | ///
24 | /// Initializes the singleton application object. This is the first line of authored code
25 | /// executed, and as such is the logical equivalent of main() or WinMain().
26 | ///
27 | public App()
28 | {
29 | this.InitializeComponent();
30 | Ioc.Default.ConfigureServices(GetServices());
31 | }
32 |
33 | private static ServiceProvider GetServices()
34 | {
35 | var serviceCollection = new ServiceCollection();
36 |
37 | if (!DesignMode.DesignModeEnabled)
38 | {
39 | var authService = new AuthService();
40 | serviceCollection.AddSingleton(authService);
41 |
42 | var asyncEnumerableGraphDataService = new AsyncEnumerableGraphDataService(authService.GraphClient);
43 | serviceCollection.AddSingleton(asyncEnumerableGraphDataService);
44 |
45 | serviceCollection.AddSingleton();
46 | }
47 |
48 | serviceCollection.AddTransient();
49 |
50 | return serviceCollection.BuildServiceProvider();
51 | }
52 |
53 |
54 | ///
55 | /// Invoked when the application is launched.
56 | ///
57 | /// Details about the launch request and process.
58 | protected override void OnLaunched(LaunchActivatedEventArgs args)
59 | {
60 | var mainWindow = new MainWindow { ExtendsContentIntoTitleBar = true };
61 |
62 | // Create a Frame to act as the navigation context and navigate to the first page
63 | var rootFrame = new Frame();
64 | rootFrame.Loaded += Root_Loaded;
65 | rootFrame.NavigationFailed += OnNavigationFailed;
66 |
67 | // Navigate to the first page, configuring the new page
68 | // by passing required information as a navigation parameter
69 | rootFrame.Navigate(typeof(MainPage), args.Arguments);
70 |
71 | // Place the frame in the current Window
72 | mainWindow.Content = rootFrame;
73 |
74 | // Ensure the MainWindow is active
75 | mainWindow.Activate();
76 | }
77 |
78 | private void Root_Loaded(object sender, RoutedEventArgs e)
79 | {
80 | // ContentDialog requires a reference to XamlRoot that is only present after Load.
81 | var dialogService = Ioc.Default.GetRequiredService();
82 | dialogService.Root = ((Frame)sender).XamlRoot;
83 | }
84 |
85 | void OnNavigationFailed(object sender, NavigationFailedEventArgs e)
86 | {
87 | throw new Exception("Failed to load Page " + e.SourcePageType.FullName);
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/MsGraphSamples.WinUI/Views/MainPage.xaml.cs:
--------------------------------------------------------------------------------
1 | using CommunityToolkit.Mvvm.DependencyInjection;
2 | using CommunityToolkit.Mvvm.Messaging;
3 | using CommunityToolkit.WinUI.UI.Controls;
4 | using Microsoft.UI.Xaml;
5 | using Microsoft.UI.Xaml.Controls;
6 | using Microsoft.UI.Xaml.Data;
7 | using MsGraphSamples.WinUI.Converters;
8 | using MsGraphSamples.WinUI.Helpers;
9 | using MsGraphSamples.WinUI.ViewModels;
10 | using System.Collections.Immutable;
11 | using System.Reflection;
12 | using Windows.Foundation;
13 |
14 | namespace MsGraphSamples.WinUI.Views;
15 | public sealed partial class MainPage : Page, IRecipient>
16 | {
17 | public MainViewModel ViewModel { get; } = Ioc.Default.GetRequiredService();
18 | private readonly Debouncer debouncer = new(TimeSpan.FromMilliseconds(600));
19 |
20 | public MainPage()
21 | {
22 | this.InitializeComponent();
23 | WeakReferenceMessenger.Default.Register(this);
24 | }
25 |
26 | public void Receive(ImmutableSortedDictionary properties)
27 | {
28 | DirectoryObjectsGrid.Columns.Clear();
29 |
30 | foreach (var property in properties)
31 | {
32 | // handle extension properties
33 | if (property.Key.StartsWith("extension_"))
34 | {
35 | DirectoryObjectsGrid.Columns.Add(new DataGridTextColumn
36 | {
37 | Header = property.Key.Split('_')[2],
38 | Binding = new Binding() { Path = new PropertyPath("AdditionalData"), Converter = new AdditionalDataConverter(), ConverterParameter = property.Key },
39 | SortDirection = property.Value,
40 | Width = new DataGridLength(1, DataGridLengthUnitType.Star)
41 | });
42 | }
43 | else
44 | {
45 | DirectoryObjectsGrid.Columns.Add(new DataGridTextColumn
46 | {
47 | Header = property.Key,
48 | Binding = new Binding() { Path = new PropertyPath(property.Key), Converter = new CollectionToCommaSeparatedStringConverter() },
49 | SortDirection = property.Value,
50 | Width = new DataGridLength(1, DataGridLengthUnitType.Star)
51 | });
52 | }
53 | }
54 | }
55 |
56 | private void TextBox_SelectAll(object sender, RoutedEventArgs _)
57 | {
58 | var textBox = (TextBox)sender;
59 | textBox.SelectAll();
60 | }
61 |
62 | private void DirectoryObjectsGrid_SizeChanged(object sender, SizeChangedEventArgs e)
63 | {
64 | debouncer.Debounce(SetPageSize);
65 | }
66 |
67 | private void SetPageSize()
68 | {
69 | var rowsPresenterAvailableSizeObj = typeof(DataGrid)
70 | .GetField("_rowsPresenterAvailableSize", BindingFlags.NonPublic | BindingFlags.Instance)
71 | ?.GetValue(DirectoryObjectsGrid);
72 |
73 | var rowHeightEstimateObj = typeof(DataGrid)
74 | .GetProperty("RowHeightEstimate", BindingFlags.NonPublic | BindingFlags.Instance)
75 | ?.GetValue(DirectoryObjectsGrid);
76 |
77 | if (rowsPresenterAvailableSizeObj is Size rowPresenterSize &&
78 | rowHeightEstimateObj is double rowHeight)
79 | {
80 | ViewModel.PageSize = (ushort)Math.Min(Math.Round(DirectoryObjectsGrid.DataFetchSize * rowPresenterSize.Height / rowHeight), 999);
81 | }
82 | }
83 | }
--------------------------------------------------------------------------------
/MsGraphSamples.WPF/Helpers/Converters.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | using Microsoft.Graph.Models;
5 | using MsGraphSamples.Services;
6 | using System.Globalization;
7 | using System.Windows.Data;
8 | using System;
9 | using System.Collections;
10 | using System.Collections.Generic;
11 | using System.Linq;
12 |
13 | namespace MsGraphSamples.WPF.Converters;
14 |
15 | public class AdditionalDataConverter : IValueConverter
16 | {
17 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
18 | {
19 | var additionalData = (IDictionary)value;
20 | additionalData.TryGetValue((string)parameter, out var extensionValue);
21 |
22 | return extensionValue?.ToString() ?? string.Empty;
23 | }
24 |
25 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException();
26 | }
27 | public class DirectoryObjectsCountConverter : IValueConverter
28 | {
29 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
30 | {
31 | var directoryObjects = (BaseCollectionPaginationCountResponse?)value;
32 |
33 | if (directoryObjects == null)
34 | return string.Empty;
35 |
36 | var directoryObjectCollection = directoryObjects.BackingStore.Get>("value") ?? [];
37 | return $"{directoryObjectCollection.Count()} / {directoryObjects.OdataCount}";
38 | }
39 |
40 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException();
41 | }
42 |
43 | public class DirectoryObjectsValueConverter : IValueConverter
44 | {
45 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
46 | {
47 | var directoryObjects = (BaseCollectionPaginationCountResponse?)value;
48 | return directoryObjects switch
49 | {
50 | UserCollectionResponse => directoryObjects.GetValue(),
51 | GroupCollectionResponse => directoryObjects.GetValue(),
52 | ApplicationCollectionResponse => directoryObjects.GetValue(),
53 | ServicePrincipalCollectionResponse => directoryObjects.GetValue(),
54 | DeviceCollectionResponse => directoryObjects.GetValue(),
55 | _ => Enumerable.Empty()
56 | };
57 | }
58 |
59 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
60 | {
61 | throw new NotImplementedException();
62 | }
63 | }
64 |
65 | public class CollectionToCommaSeparatedStringConverter : IValueConverter
66 | {
67 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
68 | {
69 | if (value is IEnumerable stringEnumerable)
70 | {
71 | return string.Join(", ", stringEnumerable);
72 | }
73 | if (value is IEnumerable enumerable && value is not string)
74 | {
75 | var items = new List();
76 | foreach (var item in enumerable)
77 | {
78 | if (item != null)
79 | items.Add(item.ToString());
80 | }
81 | return string.Join(", ", items);
82 | }
83 | return value?.ToString() ?? string.Empty;
84 | }
85 |
86 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
87 | => throw new NotImplementedException();
88 | }
89 |
--------------------------------------------------------------------------------
/MsGraphSamples.WinUI/Helpers/AsyncLoadingCollection.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | using System.Collections.ObjectModel;
5 | using System.ComponentModel;
6 | using Microsoft.UI.Xaml.Data;
7 | using Windows.Foundation;
8 |
9 | namespace MsGraphSamples.WinUI.Helpers;
10 |
11 | ///
12 | /// Initializes a new instance of the class.
13 | ///
14 | /// The source of items to load asynchronously.
15 | /// The maximum number of items to load per page.
16 | /// The action to invoke when loading starts.
17 | /// The action to invoke when loading ends.
18 | /// The action to invoke when an error occurs during loading.
19 | /// The cancellation token to cancel the loading operation.
20 | public class AsyncLoadingCollection(
21 | IAsyncEnumerable source,
22 | uint defaultItemsPerPage = 25,
23 | CancellationToken cancellationToken = default)
24 | : ObservableCollection, ISupportIncrementalLoading
25 | {
26 | private IAsyncEnumerator? _asyncEnumerator = source
27 | .GetAsyncEnumerator()
28 | .WithCancellation(cancellationToken);
29 |
30 | private readonly SemaphoreSlim _mutex = new(1, 1);
31 | public Action? OnStartLoading { get; set; }
32 | public Action? OnEndLoading { get; set; }
33 | public Action? OnError { get; set; }
34 |
35 | public bool HasMoreItems => _asyncEnumerator != null;
36 |
37 | private bool _isLoading;
38 | ///
39 | /// Gets a value indicating whether new items are being loaded.
40 | ///
41 | public bool IsLoading
42 | {
43 | get => _isLoading;
44 |
45 | private set
46 | {
47 | if (value == _isLoading)
48 | return;
49 |
50 | _isLoading = value;
51 | OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsLoading)));
52 |
53 | if (_isLoading)
54 | OnStartLoading?.Invoke();
55 | else
56 | OnEndLoading?.Invoke();
57 | }
58 | }
59 | public IAsyncOperation LoadMoreItemsAsync(uint count) =>
60 | LoadMoreItemsAsyncInternal(count)
61 | .AsAsyncOperation();
62 |
63 | private async Task LoadMoreItemsAsyncInternal(uint count)
64 | {
65 | await _mutex.WaitAsync(cancellationToken);
66 |
67 | uint itemsLoaded = 0;
68 | IsLoading = true;
69 |
70 | try
71 | {
72 | while (itemsLoaded < count && HasMoreItems)
73 | {
74 | if (await _asyncEnumerator!.MoveNextAsync().ConfigureAwait(false))
75 | {
76 | Add(_asyncEnumerator!.Current);
77 | itemsLoaded++;
78 | }
79 | else
80 | {
81 | // Dispose the enumerator when we're done
82 | await _asyncEnumerator!.DisposeAsync();
83 | _asyncEnumerator = null;
84 | }
85 | }
86 | }
87 | catch (OperationCanceledException)
88 | {
89 | // The operation has been canceled using the Cancellation Token.
90 | await _asyncEnumerator!.DisposeAsync();
91 | _asyncEnumerator = null;
92 | }
93 | catch (Exception ex)
94 | {
95 | OnError?.Invoke(ex);
96 | await _asyncEnumerator!.DisposeAsync();
97 | _asyncEnumerator = null;
98 | throw;
99 | }
100 | finally
101 | {
102 | IsLoading = false;
103 | _mutex.Release();
104 | }
105 |
106 | return new LoadMoreItemsResult(itemsLoaded);
107 | }
108 |
109 | ///
110 | /// Clears the collection and triggers/forces a reload of the first page
111 | ///
112 | ///
113 | /// An object of the that specifies how many items have been actually retrieved.
114 | ///
115 | public async Task RefreshAsync()
116 | {
117 | await _mutex.WaitAsync(cancellationToken);
118 |
119 | Clear();
120 | _asyncEnumerator = source
121 | .GetAsyncEnumerator()
122 | .WithCancellation(cancellationToken);
123 |
124 | _mutex.Release();
125 |
126 | return await LoadMoreItemsAsync(defaultItemsPerPage);
127 | }
128 | }
--------------------------------------------------------------------------------
/MsGraph Samples.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.10.35013.160
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MsGraphSamples.Services", "MsGraphSamples.Services\MsGraphSamples.Services.csproj", "{1E9C3189-31E3-4D46-B9B2-CD221E6882B3}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MsGraphSamples.WPF", "MsGraphSamples.WPF\MsGraphSamples.WPF.csproj", "{C0A150FB-2AB2-4818-AF59-E6C0BD71D510}"
9 | EndProject
10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MsGraphSamples.WinUI", "MsGraphSamples.WinUI\MsGraphSamples.WinUI.csproj", "{9CA749CB-90C9-4338-8812-2D5939B77233}"
11 | EndProject
12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{71BC6297-6E86-4824-8F28-38A8EA8E5451}"
13 | ProjectSection(SolutionItems) = preProject
14 | LICENSE = LICENSE
15 | README.md = README.md
16 | SECURITY.md = SECURITY.md
17 | EndProjectSection
18 | EndProject
19 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{85C2D888-42F7-43EE-9D5B-FC07E031085D}"
20 | ProjectSection(SolutionItems) = preProject
21 | docs\api_permissions.png = docs\api_permissions.png
22 | docs\app1.png = docs\app1.png
23 | docs\register_app.png = docs\register_app.png
24 | docs\solution_explorer.png = docs\solution_explorer.png
25 | EndProjectSection
26 | EndProject
27 | Global
28 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
29 | Debug|Any CPU = Debug|Any CPU
30 | Debug|ARM64 = Debug|ARM64
31 | Debug|x64 = Debug|x64
32 | Debug|x86 = Debug|x86
33 | Release|Any CPU = Release|Any CPU
34 | Release|ARM64 = Release|ARM64
35 | Release|x64 = Release|x64
36 | Release|x86 = Release|x86
37 | EndGlobalSection
38 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
39 | {1E9C3189-31E3-4D46-B9B2-CD221E6882B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
40 | {1E9C3189-31E3-4D46-B9B2-CD221E6882B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
41 | {1E9C3189-31E3-4D46-B9B2-CD221E6882B3}.Debug|ARM64.ActiveCfg = Debug|Any CPU
42 | {1E9C3189-31E3-4D46-B9B2-CD221E6882B3}.Debug|ARM64.Build.0 = Debug|Any CPU
43 | {1E9C3189-31E3-4D46-B9B2-CD221E6882B3}.Debug|x64.ActiveCfg = Debug|Any CPU
44 | {1E9C3189-31E3-4D46-B9B2-CD221E6882B3}.Debug|x64.Build.0 = Debug|Any CPU
45 | {1E9C3189-31E3-4D46-B9B2-CD221E6882B3}.Debug|x86.ActiveCfg = Debug|Any CPU
46 | {1E9C3189-31E3-4D46-B9B2-CD221E6882B3}.Debug|x86.Build.0 = Debug|Any CPU
47 | {1E9C3189-31E3-4D46-B9B2-CD221E6882B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
48 | {1E9C3189-31E3-4D46-B9B2-CD221E6882B3}.Release|Any CPU.Build.0 = Release|Any CPU
49 | {1E9C3189-31E3-4D46-B9B2-CD221E6882B3}.Release|ARM64.ActiveCfg = Release|Any CPU
50 | {1E9C3189-31E3-4D46-B9B2-CD221E6882B3}.Release|ARM64.Build.0 = Release|Any CPU
51 | {1E9C3189-31E3-4D46-B9B2-CD221E6882B3}.Release|x64.ActiveCfg = Release|Any CPU
52 | {1E9C3189-31E3-4D46-B9B2-CD221E6882B3}.Release|x64.Build.0 = Release|Any CPU
53 | {1E9C3189-31E3-4D46-B9B2-CD221E6882B3}.Release|x86.ActiveCfg = Release|Any CPU
54 | {1E9C3189-31E3-4D46-B9B2-CD221E6882B3}.Release|x86.Build.0 = Release|Any CPU
55 | {C0A150FB-2AB2-4818-AF59-E6C0BD71D510}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
56 | {C0A150FB-2AB2-4818-AF59-E6C0BD71D510}.Debug|Any CPU.Build.0 = Debug|Any CPU
57 | {C0A150FB-2AB2-4818-AF59-E6C0BD71D510}.Debug|ARM64.ActiveCfg = Debug|Any CPU
58 | {C0A150FB-2AB2-4818-AF59-E6C0BD71D510}.Debug|ARM64.Build.0 = Debug|Any CPU
59 | {C0A150FB-2AB2-4818-AF59-E6C0BD71D510}.Debug|x64.ActiveCfg = Debug|Any CPU
60 | {C0A150FB-2AB2-4818-AF59-E6C0BD71D510}.Debug|x64.Build.0 = Debug|Any CPU
61 | {C0A150FB-2AB2-4818-AF59-E6C0BD71D510}.Debug|x86.ActiveCfg = Debug|Any CPU
62 | {C0A150FB-2AB2-4818-AF59-E6C0BD71D510}.Debug|x86.Build.0 = Debug|Any CPU
63 | {C0A150FB-2AB2-4818-AF59-E6C0BD71D510}.Release|Any CPU.ActiveCfg = Release|Any CPU
64 | {C0A150FB-2AB2-4818-AF59-E6C0BD71D510}.Release|Any CPU.Build.0 = Release|Any CPU
65 | {C0A150FB-2AB2-4818-AF59-E6C0BD71D510}.Release|ARM64.ActiveCfg = Release|Any CPU
66 | {C0A150FB-2AB2-4818-AF59-E6C0BD71D510}.Release|ARM64.Build.0 = Release|Any CPU
67 | {C0A150FB-2AB2-4818-AF59-E6C0BD71D510}.Release|x64.ActiveCfg = Release|Any CPU
68 | {C0A150FB-2AB2-4818-AF59-E6C0BD71D510}.Release|x64.Build.0 = Release|Any CPU
69 | {C0A150FB-2AB2-4818-AF59-E6C0BD71D510}.Release|x86.ActiveCfg = Release|Any CPU
70 | {C0A150FB-2AB2-4818-AF59-E6C0BD71D510}.Release|x86.Build.0 = Release|Any CPU
71 | {9CA749CB-90C9-4338-8812-2D5939B77233}.Debug|Any CPU.ActiveCfg = Debug|x64
72 | {9CA749CB-90C9-4338-8812-2D5939B77233}.Debug|Any CPU.Build.0 = Debug|x64
73 | {9CA749CB-90C9-4338-8812-2D5939B77233}.Debug|Any CPU.Deploy.0 = Debug|x64
74 | {9CA749CB-90C9-4338-8812-2D5939B77233}.Debug|ARM64.ActiveCfg = Debug|ARM64
75 | {9CA749CB-90C9-4338-8812-2D5939B77233}.Debug|ARM64.Build.0 = Debug|ARM64
76 | {9CA749CB-90C9-4338-8812-2D5939B77233}.Debug|ARM64.Deploy.0 = Debug|ARM64
77 | {9CA749CB-90C9-4338-8812-2D5939B77233}.Debug|x64.ActiveCfg = Debug|x64
78 | {9CA749CB-90C9-4338-8812-2D5939B77233}.Debug|x64.Build.0 = Debug|x64
79 | {9CA749CB-90C9-4338-8812-2D5939B77233}.Debug|x64.Deploy.0 = Debug|x64
80 | {9CA749CB-90C9-4338-8812-2D5939B77233}.Debug|x86.ActiveCfg = Debug|x86
81 | {9CA749CB-90C9-4338-8812-2D5939B77233}.Debug|x86.Build.0 = Debug|x86
82 | {9CA749CB-90C9-4338-8812-2D5939B77233}.Debug|x86.Deploy.0 = Debug|x86
83 | {9CA749CB-90C9-4338-8812-2D5939B77233}.Release|Any CPU.ActiveCfg = Release|x64
84 | {9CA749CB-90C9-4338-8812-2D5939B77233}.Release|Any CPU.Build.0 = Release|x64
85 | {9CA749CB-90C9-4338-8812-2D5939B77233}.Release|Any CPU.Deploy.0 = Release|x64
86 | {9CA749CB-90C9-4338-8812-2D5939B77233}.Release|ARM64.ActiveCfg = Release|ARM64
87 | {9CA749CB-90C9-4338-8812-2D5939B77233}.Release|ARM64.Build.0 = Release|ARM64
88 | {9CA749CB-90C9-4338-8812-2D5939B77233}.Release|ARM64.Deploy.0 = Release|ARM64
89 | {9CA749CB-90C9-4338-8812-2D5939B77233}.Release|x64.ActiveCfg = Release|x64
90 | {9CA749CB-90C9-4338-8812-2D5939B77233}.Release|x64.Build.0 = Release|x64
91 | {9CA749CB-90C9-4338-8812-2D5939B77233}.Release|x64.Deploy.0 = Release|x64
92 | {9CA749CB-90C9-4338-8812-2D5939B77233}.Release|x86.ActiveCfg = Release|x86
93 | {9CA749CB-90C9-4338-8812-2D5939B77233}.Release|x86.Build.0 = Release|x86
94 | {9CA749CB-90C9-4338-8812-2D5939B77233}.Release|x86.Deploy.0 = Release|x86
95 | EndGlobalSection
96 | GlobalSection(SolutionProperties) = preSolution
97 | HideSolutionNode = FALSE
98 | EndGlobalSection
99 | GlobalSection(ExtensibilityGlobals) = postSolution
100 | SolutionGuid = {3AB8D896-57B8-48FB-A381-4E918796CB34}
101 | EndGlobalSection
102 | EndGlobal
103 |
--------------------------------------------------------------------------------
/MsGraphSamples.Services/GraphExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Graph.Models;
2 | using Microsoft.Graph;
3 | using Microsoft.Kiota.Abstractions.Serialization;
4 | using Microsoft.Kiota.Abstractions;
5 | using Microsoft.Graph.Models.ODataErrors;
6 | using System.Runtime.CompilerServices;
7 |
8 | namespace MsGraphSamples.Services;
9 |
10 | public static class GraphExtensions
11 | {
12 | private static readonly Dictionary> ErrorMappings = new() { { "XXX", ODataError.CreateFromDiscriminatorValue } };
13 |
14 | ///
15 | /// Transform a generic RequestInformation into an AsyncEnumerable to efficiently iterate through the collection in case there are several pages.
16 | ///
17 | ///
18 | ///
19 | ///
20 | /// IAsyncEnumerable
21 | public static async IAsyncEnumerable ToAsyncEnumerable(this RequestInformation requestInfo, IRequestAdapter requestAdapter, Action? countAction = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
22 | where TEntity : Entity
23 | where TCollectionResponse : BaseCollectionPaginationCountResponse, new()
24 | {
25 | // Send the asynchronous request and get the response
26 | var collectionResponse = await requestAdapter
27 | .SendAsync(requestInfo, parseNode => new TCollectionResponse(), ErrorMappings, cancellationToken);
28 |
29 | // Iterate through the collection response asynchronously
30 | await foreach (var item in collectionResponse.ToAsyncEnumerable(requestAdapter, countAction, cancellationToken))
31 | {
32 | yield return item;
33 | }
34 | }
35 |
36 | ///
37 | /// Transform a generic BaseCollectionPaginationCountResponse into an AsyncEnumerable to efficiently iterate through the collection in case there are several pages.
38 | ///
39 | /// Microsoft Graph Entity of the CollectionResponse
40 | /// Specialized BaseCollectionPaginationCountResponse
41 | /// The CollectionResponse to convert to IAsyncEnumerable
42 | /// The IRequestAdapter from GraphServiceClient used to make requests
43 | ///
44 | ///
45 | public static async IAsyncEnumerable ToAsyncEnumerable(this TCollectionResponse? collectionResponse, IRequestAdapter requestAdapter, Action? countAction = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
46 | where TEntity : Entity
47 | where TCollectionResponse : BaseCollectionPaginationCountResponse, new()
48 | {
49 | countAction?.Invoke(collectionResponse?.OdataCount);
50 |
51 | while (collectionResponse != null)
52 | {
53 | var entities = collectionResponse.GetValue();
54 | foreach (var entity in entities)
55 | {
56 | yield return entity;
57 | }
58 |
59 | collectionResponse = await collectionResponse
60 | .GetNextPageAsync(requestAdapter, cancellationToken)
61 | .ConfigureAwait(false);
62 | }
63 | }
64 |
65 | public static IList GetValue(this BaseCollectionPaginationCountResponse? collectionResponse) where TEntity : Entity
66 | {
67 | return collectionResponse?.BackingStore.Get>("value") ?? [];
68 | }
69 |
70 | public static async Task GetNextPageAsync(this TCollectionResponse? collectionResponse, IRequestAdapter requestAdapter, CancellationToken cancellationToken = default)
71 | where TCollectionResponse : BaseCollectionPaginationCountResponse, new()
72 | {
73 | if (collectionResponse?.OdataNextLink == null)
74 | return null;
75 |
76 | var nextPageRequestInformation = new RequestInformation
77 | {
78 | HttpMethod = Method.GET,
79 | UrlTemplate = collectionResponse.OdataNextLink,
80 | };
81 | var previousCount = collectionResponse.OdataCount;
82 |
83 | var nextPage = await requestAdapter
84 | .SendAsync(nextPageRequestInformation, parseNode => new TCollectionResponse(), ErrorMappings, cancellationToken)
85 | .ConfigureAwait(false);
86 |
87 | // fix count property not present in pages other than the first one
88 | if (nextPage != null)
89 | nextPage.OdataCount = previousCount;
90 |
91 | return nextPage;
92 | }
93 |
94 |
95 | public static async IAsyncEnumerable Batch(this GraphServiceClient graphClient, [EnumeratorCancellation] CancellationToken cancellationToken = default, params RequestInformation[] requests)
96 | where TEntity : Entity
97 | where TCollectionResponse : BaseCollectionPaginationCountResponse, new()
98 | {
99 | await foreach (var response in graphClient.Batch(cancellationToken, requests))
100 | {
101 | await foreach (var entity in response
102 | .ToAsyncEnumerable(graphClient.RequestAdapter)
103 | .WithCancellation(cancellationToken)
104 | .ConfigureAwait(false))
105 | {
106 | yield return entity;
107 | }
108 | }
109 | }
110 |
111 | public static async IAsyncEnumerable Batch(
112 | this GraphServiceClient graphClient,
113 | [EnumeratorCancellation] CancellationToken cancellationToken = default,
114 | params RequestInformation[] requests)
115 | where T : IParsable, new()
116 | {
117 | var batchRequestContent = new BatchRequestContentCollection(graphClient);
118 |
119 | var addBatchTasks = requests.Select(request => batchRequestContent.AddBatchRequestStepAsync(request));
120 | var requestIds = await Task.WhenAll(addBatchTasks);
121 |
122 | var batchResponse = await graphClient.Batch.PostAsync(batchRequestContent, cancellationToken, ErrorMappings);
123 |
124 | var responseTasks = requestIds.Select(id => batchResponse.GetResponseByIdAsync(id)).ToList();
125 |
126 | // return first response as soon as it's available
127 | while (responseTasks.Count > 0)
128 | {
129 | var completedTask = await Task.WhenAny(responseTasks);
130 | yield return await completedTask;
131 | responseTasks.Remove(completedTask);
132 | }
133 | }
134 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Aa][Rr][Mm]/
27 | [Aa][Rr][Mm]64/
28 | bld/
29 | [Bb]in/
30 | [Oo]bj/
31 | [Ll]og/
32 | [Ll]ogs/
33 |
34 | # Visual Studio 2015/2017 cache/options directory
35 | .vs/
36 |
37 | # VS Code cache/options directory
38 | .vscode/
39 |
40 | # Uncomment if you have tasks that create the project's static files in wwwroot
41 | #wwwroot/
42 |
43 | # Visual Studio 2017 auto generated files
44 | Generated\ Files/
45 |
46 | # MSTest test Results
47 | [Tt]est[Rr]esult*/
48 | [Bb]uild[Ll]og.*
49 |
50 | # NUnit
51 | *.VisualState.xml
52 | TestResult.xml
53 | nunit-*.xml
54 |
55 | # Build Results of an ATL Project
56 | [Dd]ebugPS/
57 | [Rr]eleasePS/
58 | dlldata.c
59 |
60 | # Benchmark Results
61 | BenchmarkDotNet.Artifacts/
62 |
63 | # .NET Core
64 | project.lock.json
65 | project.fragment.lock.json
66 | artifacts/
67 |
68 | # StyleCop
69 | StyleCopReport.xml
70 |
71 | # Files built by Visual Studio
72 | *_i.c
73 | *_p.c
74 | *_h.h
75 | *.ilk
76 | *.meta
77 | *.obj
78 | *.iobj
79 | *.pch
80 | *.pdb
81 | *.ipdb
82 | *.pgc
83 | *.pgd
84 | *.rsp
85 | *.sbr
86 | *.tlb
87 | *.tli
88 | *.tlh
89 | *.tmp
90 | *.tmp_proj
91 | *_wpftmp.csproj
92 | *.log
93 | *.vspscc
94 | *.vssscc
95 | .builds
96 | *.pidb
97 | *.svclog
98 | *.scc
99 |
100 | # Chutzpah Test files
101 | _Chutzpah*
102 |
103 | # Visual C++ cache files
104 | ipch/
105 | *.aps
106 | *.ncb
107 | *.opendb
108 | *.opensdf
109 | *.sdf
110 | *.cachefile
111 | *.VC.db
112 | *.VC.VC.opendb
113 |
114 | # Visual Studio profiler
115 | *.psess
116 | *.vsp
117 | *.vspx
118 | *.sap
119 |
120 | # Visual Studio Trace Files
121 | *.e2e
122 |
123 | # TFS 2012 Local Workspace
124 | $tf/
125 |
126 | # Guidance Automation Toolkit
127 | *.gpState
128 |
129 | # ReSharper is a .NET coding add-in
130 | _ReSharper*/
131 | *.[Rr]e[Ss]harper
132 | *.DotSettings.user
133 |
134 | # TeamCity is a build add-in
135 | _TeamCity*
136 |
137 | # DotCover is a Code Coverage Tool
138 | *.dotCover
139 |
140 | # AxoCover is a Code Coverage Tool
141 | .axoCover/*
142 | !.axoCover/settings.json
143 |
144 | # Visual Studio code coverage results
145 | *.coverage
146 | *.coveragexml
147 |
148 | # NCrunch
149 | _NCrunch_*
150 | .*crunch*.local.xml
151 | nCrunchTemp_*
152 |
153 | # MightyMoose
154 | *.mm.*
155 | AutoTest.Net/
156 |
157 | # Web workbench (sass)
158 | .sass-cache/
159 |
160 | # Installshield output folder
161 | [Ee]xpress/
162 |
163 | # DocProject is a documentation generator add-in
164 | DocProject/buildhelp/
165 | DocProject/Help/*.HxT
166 | DocProject/Help/*.HxC
167 | DocProject/Help/*.hhc
168 | DocProject/Help/*.hhk
169 | DocProject/Help/*.hhp
170 | DocProject/Help/Html2
171 | DocProject/Help/html
172 |
173 | # Click-Once directory
174 | publish/
175 |
176 | # Publish Web Output
177 | *.[Pp]ublish.xml
178 | *.azurePubxml
179 | # Note: Comment the next line if you want to checkin your web deploy settings,
180 | # but database connection strings (with potential passwords) will be unencrypted
181 | *.pubxml
182 | *.publishproj
183 |
184 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
185 | # checkin your Azure Web App publish settings, but sensitive information contained
186 | # in these scripts will be unencrypted
187 | PublishScripts/
188 |
189 | # NuGet Packages
190 | *.nupkg
191 | # NuGet Symbol Packages
192 | *.snupkg
193 | # The packages folder can be ignored because of Package Restore
194 | **/[Pp]ackages/*
195 | # except build/, which is used as an MSBuild target.
196 | !**/[Pp]ackages/build/
197 | # Uncomment if necessary however generally it will be regenerated when needed
198 | #!**/[Pp]ackages/repositories.config
199 | # NuGet v3's project.json files produces more ignorable files
200 | *.nuget.props
201 | *.nuget.targets
202 |
203 | # Microsoft Azure Build Output
204 | csx/
205 | *.build.csdef
206 |
207 | # Microsoft Azure Emulator
208 | ecf/
209 | rcf/
210 |
211 | # Windows Store app package directories and files
212 | AppPackages/
213 | BundleArtifacts/
214 | Package.StoreAssociation.xml
215 | _pkginfo.txt
216 | *.appx
217 | *.appxbundle
218 | *.appxupload
219 |
220 | # Visual Studio cache files
221 | # files ending in .cache can be ignored
222 | *.[Cc]ache
223 | # but keep track of directories ending in .cache
224 | !?*.[Cc]ache/
225 |
226 | # Others
227 | ClientBin/
228 | ~$*
229 | *~
230 | *.dbmdl
231 | *.dbproj.schemaview
232 | *.jfm
233 | *.pfx
234 | *.publishsettings
235 | orleans.codegen.cs
236 |
237 | # Including strong name files can present a security risk
238 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
239 | #*.snk
240 |
241 | # Since there are multiple workflows, uncomment next line to ignore bower_components
242 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
243 | #bower_components/
244 |
245 | # RIA/Silverlight projects
246 | Generated_Code/
247 |
248 | # Backup & report files from converting an old project file
249 | # to a newer Visual Studio version. Backup files are not needed,
250 | # because we have git ;-)
251 | _UpgradeReport_Files/
252 | Backup*/
253 | UpgradeLog*.XML
254 | UpgradeLog*.htm
255 | ServiceFabricBackup/
256 | *.rptproj.bak
257 |
258 | # SQL Server files
259 | *.mdf
260 | *.ldf
261 | *.ndf
262 |
263 | # Business Intelligence projects
264 | *.rdl.data
265 | *.bim.layout
266 | *.bim_*.settings
267 | *.rptproj.rsuser
268 | *- [Bb]ackup.rdl
269 | *- [Bb]ackup ([0-9]).rdl
270 | *- [Bb]ackup ([0-9][0-9]).rdl
271 |
272 | # Microsoft Fakes
273 | FakesAssemblies/
274 |
275 | # GhostDoc plugin setting file
276 | *.GhostDoc.xml
277 |
278 | # Node.js Tools for Visual Studio
279 | .ntvs_analysis.dat
280 | node_modules/
281 |
282 | # Visual Studio 6 build log
283 | *.plg
284 |
285 | # Visual Studio 6 workspace options file
286 | *.opt
287 |
288 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
289 | *.vbw
290 |
291 | # Visual Studio LightSwitch build output
292 | **/*.HTMLClient/GeneratedArtifacts
293 | **/*.DesktopClient/GeneratedArtifacts
294 | **/*.DesktopClient/ModelManifest.xml
295 | **/*.Server/GeneratedArtifacts
296 | **/*.Server/ModelManifest.xml
297 | _Pvt_Extensions
298 |
299 | # Paket dependency manager
300 | .paket/paket.exe
301 | paket-files/
302 |
303 | # FAKE - F# Make
304 | .fake/
305 |
306 | # CodeRush personal settings
307 | .cr/personal
308 |
309 | # Python Tools for Visual Studio (PTVS)
310 | __pycache__/
311 | *.pyc
312 |
313 | # Cake - Uncomment if you are using it
314 | # tools/**
315 | # !tools/packages.config
316 |
317 | # Tabs Studio
318 | *.tss
319 |
320 | # Telerik's JustMock configuration file
321 | *.jmconfig
322 |
323 | # BizTalk build output
324 | *.btp.cs
325 | *.btm.cs
326 | *.odx.cs
327 | *.xsd.cs
328 |
329 | # OpenCover UI analysis results
330 | OpenCover/
331 |
332 | # Azure Stream Analytics local run output
333 | ASALocalRun/
334 |
335 | # MSBuild Binary and Structured Log
336 | *.binlog
337 |
338 | # NVidia Nsight GPU debugger configuration file
339 | *.nvuser
340 |
341 | # MFractors (Xamarin productivity tool) working folder
342 | .mfractor/
343 |
344 | # Local History for Visual Studio
345 | .localhistory/
346 |
347 | # BeatPulse healthcheck temp database
348 | healthchecksdb
349 |
350 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
351 | MigrationBackup/
352 |
353 | # Ionide (cross platform F# VS Code tools) working folder
354 | .ionide/
355 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | uid: dotnet-aad-query-sample
3 | description: Learn how to use .NET Graph SDK to query Directory Objects
4 | page_type: sample
5 | createdDate: 09/22/2020 00:00:00 AM
6 | languages:
7 | - csharp
8 | technologies:
9 | - Microsoft Graph
10 | - Microsoft identity platform
11 | authors:
12 | - id: Licantrop0
13 | displayName: Luca Spolidoro
14 | products:
15 | - ms-graph
16 | - dotnet-core
17 | - windows-wpf
18 | extensions:
19 | contentType: samples
20 | technologies:
21 | - Microsoft Graph
22 | - Microsoft identity platform
23 | createdDate: 09/22/2020
24 | codeUrl: https://github.com/microsoftgraph/dotnet-aad-query-sample
25 | zipUrl: https://github.com/microsoftgraph/dotnet-aad-query-sample/archive/master.zip
26 | description: "This sample demonstrates a .NET Desktop (WPF) application showcasing advanced Microsoft Graph Query Capabilities for Directory Objects with .NET"
27 | ---
28 | # Explore advanced Microsoft Graph Query Capabilities on Microsoft Entra ID Objects with .NET SDK
29 |
30 | - [Overview](#overview)
31 | - [Prerequisites](#prerequisites)
32 | - [Registration](#registration)
33 | - [Step 1: Register your application](#step-1-register-your-application)
34 | - [Step 2: Set the MS Graph permissions](#step-2-set-the-ms-graph-permissions)
35 | - [Setup](#setup)
36 | - [Step 1: Clone or download this repository](#step-1--clone-or-download-this-repository)
37 | - [Step 2: Configure the ClientId using the Secret Manager](#step-2-configure-the-clientid-using-the-secret-manager)
38 | - [Run the sample](#run-the-sample)
39 | - [On Visual Studio](#on-visual-studio)
40 | - [On Visual Studio Code](#on-visual-studio-code)
41 | - [Using the app](#using-the-app)
42 | - [Code Architecture](#code-architecture)
43 |
44 | ## Overview
45 |
46 | This sample helps you explore the Microsoft Graph's [new query capabilities](https://aka.ms/graph-docs/advanced-queries) of the identity APIs using the [Microsoft Graph .NET Client Library v5](https://github.com/microsoftgraph/msgraph-sdk-dotnet) to query Microsoft Entra ID.
47 | The main code is in [AsyncEnumerableGraphDataService.cs](MsGraphSamples.Services/AsyncEnumerableGraphDataService.cs) file where, for every request:
48 |
49 | - The required `$count=true` QueryString parameter is added
50 | - The required `ConsistencyLevel=eventual` header is added
51 | - The request URL is extracted and displayed in the UI
52 | - The results are converted to an `IAsyncEnumerable` using the [`ToAsyncEnumerable`](MsGraphSamples.Services/AsyncEnumerableGraphDataService.cs#LL34C51-L34C68) extension method for an easier pagination.
53 |
54 | ## Prerequisites
55 |
56 | - Either [Visual Studio (>v16.8)](https://aka.ms/vsdownload) *or* [Visual Studio Code](https://code.visualstudio.com/) with [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) and [C# for Visual Studio Code Extension](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp)
57 | - A Microsoft Entra ID tenant. For more information, see [How to get an Microsoft Entra ID tenant](https://azure.microsoft.com/documentation/articles/active-directory-howto-tenant/)
58 | - A user account in your Microsoft Entra ID tenant. This sample will not work with a personal Microsoft account (formerly Windows Live account). Therefore, if you signed in to the [Azure portal](https://portal.azure.com) with a Microsoft account and have never created a user account in your directory before, you need to do that now.
59 |
60 | ## Registration
61 |
62 | ### Step 1: Register your application
63 |
64 | Use the [Microsoft Application Registration Portal](https://aka.ms/appregistrations) to register your application with the Microsoft Graph APIs.
65 | Click New Registration.
66 |
67 | 
68 | **Note:** Make sure to set the right **Redirect URI** (`http://localhost`) and application type is **Public client/native (mobile & desktop)**.
69 |
70 | ### Step 2: Set the MS Graph permissions
71 |
72 | Add the [delegated permissions](https://docs.microsoft.com/graph/permissions-reference#delegated-permissions-20) for `Directory.Read.All`, and grant admin consent.
73 | We advise you to register and use this sample on a Dev/Test tenant and not on your production tenant.
74 |
75 | 
76 |
77 | ## Setup
78 |
79 | ### Step 1: Clone or download this repository
80 |
81 | From your shell or command line:
82 |
83 | ```Shell
84 | git clone https://github.com/microsoftgraph/dotnet-aad-query-sample.git
85 | ```
86 |
87 | or download and extract the repository .zip file.
88 |
89 | ### Step 2: Configure the ClientId using the Secret Manager
90 |
91 | This application use the [.NET Core Secret Manager](https://docs.microsoft.com/aspnet/core/security/app-secrets) to store the **ClientId**.
92 | To add the **ClientId** created on step 1 of registration:
93 |
94 | 1. Open a **Developer Command Prompt** or an **Integrated Terminal** and locate the `dotnet-aad-query-sample\MsGraphSamples.Services\` directory.
95 | 1. Type `dotnet user-secrets set "clientId" ""`
96 |
97 | ## Run the sample
98 |
99 | ### On Visual Studio
100 |
101 | Press F5. This will restore the missing nuget packages, build the solution and run the project.
102 |
103 | ### On Visual Studio Code
104 |
105 | Shortly after you open the project folder in VS Code, a prompt by C# extension will appear on bottom right corner:
106 | `Required assets to build and debug are missing from 'dotnet-aad-query-sample'. Add them?`.
107 | Select **Yes** and the [C# Dev Kit extension](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit) should install.
108 | Use The Solution Explorer to browse the projects (WinUI or WPF), right-click and debug:
109 |
110 | 
111 |
112 | > Note: Make sure to select `x64` Platform in the lower left corner of the window if you need to build WinUI, as `AnyCPU` is not supported.
113 |
114 | ### Using the app
115 |
116 | If everything was configured correctly, you should be able to see the login prompt opening in a web browser.
117 | The auth token will be cached in a file for the subsequent runs thanks to [GetBrowserCredential](MsGraphSamples.Services/AuthService.cs#L41C42-L41C62) Method.
118 | You can query your tenant by typing the arguments of the standard OData `$select`, `$filter`, `$orderBy`, `$search` clauses in the relative text boxes.
119 | In the screenshot below you can see the $search operator in action:
120 |
121 | 
122 |
123 | - If you double click on a row, a default drill-down will happen (for example by showing the list of transitive groups a user is part of).
124 | - If you click on a header, the results will be sorted by that column. **Note: not all columns are supported and you may receive an error**.
125 | - If any query error happen, it will displayed with a Message box.
126 |
127 | The generated URL will appear in the readonly Url textbox. You can click the Graph Explorer button to open the current query in Graph Explorer.
128 |
129 | ## Code Architecture
130 |
131 | This app provides a good starting point for enterprise desktop applications that connects to Microsoft Graph.
132 | The uses [MVVM](https://docs.microsoft.com/windows/uwp/data-binding/data-binding-and-mvvm) pattern and [.NET Community Toolkit](https://github.com/CommunityToolkit/dotnet).
133 | There are two UI projects, one for [WPF](MsGraphSamples.WPF/) and one for [WinUI](MsGraphSamples.WinUI/). The WinUI project implements an advanced technique to iterate all pages of a response using [IAsyncEnumerable](https://learn.microsoft.com/en-us/archive/msdn-magazine/2019/november/csharp-iterating-with-async-enumerables-in-csharp-8) and [ISupportIncrementalLoading](https://learn.microsoft.com/uwp/api/windows.ui.xaml.data.isupportincrementalloading).
134 | Dependency Injection is implemented using [Microsoft.Extensions.DependencyInjection](https://docs.microsoft.com/aspnet/core/fundamentals/dependency-injection).
135 | **Nullable** and **Code Analysis** are enabled to enforce code quality.
136 |
--------------------------------------------------------------------------------
/MsGraphSamples.WPF/ViewModels/MainViewModel.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | using System.ComponentModel;
5 | using System.Diagnostics;
6 | using System.Net;
7 | using System.Text;
8 | using System.Windows.Controls;
9 | using CommunityToolkit.Mvvm.ComponentModel;
10 | using CommunityToolkit.Mvvm.Input;
11 | using Microsoft.Graph.Models;
12 | using Microsoft.Graph.Models.ODataErrors;
13 | using Microsoft.Kiota.Abstractions;
14 | using MsGraphSamples.Services;
15 |
16 | namespace MsGraphSamples.WPF.ViewModels;
17 |
18 | public partial class MainViewModel(IAuthService authService, IGraphDataService graphDataService) : ObservableObject
19 | {
20 | private readonly ushort pageSize = 25;
21 | private readonly Stopwatch _stopWatch = new();
22 | public long ElapsedMs => _stopWatch.ElapsedMilliseconds;
23 |
24 | [ObservableProperty]
25 | private bool _isBusy;
26 |
27 | [ObservableProperty]
28 | private string? _userName;
29 |
30 | public string? LastUrl => graphDataService.LastUrl;
31 |
32 | public static IReadOnlyList Entities => ["Users", "Groups", "Applications", "ServicePrincipals", "Devices"];
33 |
34 | [ObservableProperty]
35 | private string _selectedEntity = "Users";
36 |
37 | [ObservableProperty]
38 | [NotifyCanExecuteChangedFor(nameof(DrillDownCommand))]
39 | private DirectoryObject? _selectedObject;
40 |
41 | [ObservableProperty]
42 | [NotifyCanExecuteChangedFor(nameof(LaunchGraphExplorerCommand))]
43 | [NotifyCanExecuteChangedFor(nameof(LoadNextPageCommand))]
44 | [NotifyPropertyChangedFor(nameof(LastUrl))]
45 | private BaseCollectionPaginationCountResponse? _directoryObjects;
46 |
47 | #region OData Operators
48 |
49 | public string[] SplittedSelect => Select.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
50 |
51 | [ObservableProperty]
52 | public string _select = "id,displayName,mail,userPrincipalName";
53 |
54 | [ObservableProperty]
55 | public string? _filter;
56 |
57 | public string[]? SplittedOrderBy => OrderBy?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
58 |
59 | [ObservableProperty]
60 | public string? _orderBy;
61 |
62 | private string? _search;
63 | public string? Search
64 | {
65 | get => _search;
66 | set
67 | {
68 | if (_search != value)
69 | {
70 | _search = FixSearchSyntax(value);
71 | OnPropertyChanged();
72 | }
73 | }
74 | }
75 |
76 | private static string? FixSearchSyntax(string? searchValue)
77 | {
78 | if (searchValue == null)
79 | return null;
80 |
81 | if (searchValue.Contains('"'))
82 | return searchValue; // Assume already correctly formatted
83 |
84 | var elements = searchValue.Trim().Split(' ');
85 | var sb = new StringBuilder(elements.Length);
86 |
87 | foreach (var element in elements)
88 | {
89 | string? newElement;
90 |
91 | if (element.Contains(':'))
92 | newElement = $"\"{element}\""; // Search clause needs to be wrapped by double quotes
93 | else if (element.In("AND", "OR"))
94 | newElement = $" {element.ToUpperInvariant()} "; // [AND, OR] Operators need to be uppercase
95 | else
96 | newElement = element;
97 |
98 | sb.Append(newElement);
99 | }
100 |
101 | return sb.ToString();
102 | }
103 |
104 | #endregion
105 |
106 | [RelayCommand]
107 | public async Task PageLoaded()
108 | {
109 | var user = await graphDataService.GetUserAsync(["displayName"]);
110 | UserName = user?.DisplayName;
111 |
112 | await Load();
113 | }
114 |
115 | [RelayCommand]
116 | private async Task Load()
117 | {
118 | await IsBusyWrapper(async () => DirectoryObjects = SelectedEntity switch
119 | {
120 | "Users" => await graphDataService.GetUserCollectionAsync(SplittedSelect, Filter, SplittedOrderBy, Search, pageSize),
121 | "Groups" => await graphDataService.GetGroupCollectionAsync(SplittedSelect, Filter, SplittedOrderBy, Search, pageSize),
122 | "Applications" => await graphDataService.GetApplicationCollectionAsync(SplittedSelect, Filter, SplittedOrderBy, Search, pageSize),
123 | "ServicePrincipals" => await graphDataService.GetServicePrincipalCollectionAsync(SplittedSelect, Filter, SplittedOrderBy, Search, pageSize),
124 | "Devices" => await graphDataService.GetDeviceCollectionAsync(SplittedSelect, Filter, SplittedOrderBy, Search, pageSize),
125 | _ => throw new NotImplementedException("Can't find selected entity")
126 | });
127 | }
128 |
129 | private bool CanDrillDown() => SelectedObject is not null;
130 | [RelayCommand(CanExecute = nameof(CanDrillDown))]
131 | private async Task DrillDown()
132 | {
133 | ArgumentNullException.ThrowIfNull(SelectedObject);
134 |
135 | OrderBy = null;
136 | Filter = null;
137 | Search = null;
138 |
139 | await IsBusyWrapper(async () => DirectoryObjects = DirectoryObjects switch
140 | {
141 | UserCollectionResponse => await graphDataService.GetTransitiveMemberOfAsGroupCollectionAsync(SelectedObject.Id!, SplittedSelect, pageSize),
142 | GroupCollectionResponse => await graphDataService.GetTransitiveMembersAsUserCollectionAsync(SelectedObject.Id!, SplittedSelect, pageSize),
143 | ApplicationCollectionResponse => await graphDataService.GetApplicationOwnersAsUserCollectionAsync(SelectedObject.Id!, SplittedSelect, pageSize),
144 | ServicePrincipalCollectionResponse => await graphDataService.GetServicePrincipalOwnersAsUserCollectionAsync(SelectedObject.Id!, SplittedSelect, pageSize),
145 | DeviceCollectionResponse => await graphDataService.GetDeviceOwnersAsUserCollectionAsync(SelectedObject.Id!, SplittedSelect, pageSize),
146 | _ => throw new NotImplementedException("Can't find Entity Type")
147 | });
148 | }
149 |
150 | private bool CanGoNextPage => DirectoryObjects?.OdataNextLink is not null;
151 | [RelayCommand(CanExecute = nameof(CanGoNextPage))]
152 | private async Task LoadNextPage()
153 | {
154 | await IsBusyWrapper(async () => DirectoryObjects = DirectoryObjects switch
155 | {
156 | UserCollectionResponse userCollection => await graphDataService.GetNextPageAsync(userCollection),
157 | GroupCollectionResponse groupCollection => await graphDataService.GetNextPageAsync(groupCollection),
158 | ApplicationCollectionResponse applicationCollection => await graphDataService.GetNextPageAsync(applicationCollection),
159 | ServicePrincipalCollectionResponse servicePrincipalCollection => await graphDataService.GetNextPageAsync(servicePrincipalCollection),
160 | DeviceCollectionResponse deviceCollection => await graphDataService.GetNextPageAsync(deviceCollection),
161 | _ => throw new NotImplementedException("Can't find Entity Type")
162 | });
163 | }
164 |
165 | [RelayCommand]
166 | private Task Sort(DataGridSortingEventArgs e)
167 | {
168 | OrderBy = e.Column.SortDirection == null || e.Column.SortDirection == ListSortDirection.Descending
169 | ? $"{e.Column.Header} asc"
170 | : $"{e.Column.Header} desc";
171 |
172 | // Prevent client-side sorting
173 | e.Handled = true;
174 |
175 | return Load();
176 | }
177 |
178 | private bool CanLaunchGraphExplorer => LastUrl is not null;
179 | [RelayCommand(CanExecute = nameof(CanLaunchGraphExplorer))]
180 | private void LaunchGraphExplorer()
181 | {
182 | ArgumentNullException.ThrowIfNull(LastUrl);
183 |
184 | var geBaseUrl = "https://developer.microsoft.com/en-us/graph/graph-explorer";
185 | var graphUrl = "https://graph.microsoft.com";
186 | var version = "v1.0";
187 | var startOfQuery = LastUrl.NthIndexOf('/', 4) + 1;
188 | var encodedUrl = WebUtility.UrlEncode(LastUrl[startOfQuery..]);
189 | var encodedHeaders = "W3sibmFtZSI6IkNvbnNpc3RlbmN5TGV2ZWwiLCJ2YWx1ZSI6ImV2ZW50dWFsIn1d"; // ConsistencyLevel = eventual
190 |
191 | var url = $"{geBaseUrl}?request={encodedUrl}&method=GET&version={version}&GraphUrl={graphUrl}&headers={encodedHeaders}";
192 |
193 | var psi = new ProcessStartInfo { FileName = url, UseShellExecute = true };
194 | System.Diagnostics.Process.Start(psi);
195 | }
196 |
197 | [RelayCommand]
198 | private void Logout()
199 | {
200 | authService.Logout();
201 | App.Current.Shutdown();
202 | }
203 |
204 | private async Task IsBusyWrapper(Func loadOperation)
205 | {
206 | IsBusy = true;
207 | _stopWatch.Restart();
208 |
209 | try
210 | {
211 | await loadOperation();
212 |
213 | SelectedEntity = DirectoryObjects switch
214 | {
215 | UserCollectionResponse => "Users",
216 | GroupCollectionResponse => "Groups",
217 | ApplicationCollectionResponse => "Applications",
218 | ServicePrincipalCollectionResponse => "ServicePrincipals",
219 | DeviceCollectionResponse => "Devices",
220 | _ => SelectedEntity,
221 | };
222 | }
223 | catch (ODataError ex)
224 | {
225 | await ShowDialogAsync(ex.Error?.Code ?? "OData Error", ex.Error?.Message);
226 | }
227 | catch (ApiException ex)
228 | {
229 | await ShowDialogAsync(ex.Message, Enum.GetName((HttpStatusCode)ex.ResponseStatusCode));
230 | }
231 | finally
232 | {
233 | _stopWatch.Stop();
234 | OnPropertyChanged(nameof(ElapsedMs));
235 | IsBusy = false;
236 | }
237 | }
238 |
239 | ///
240 | /// Shows a content dialog
241 | ///
242 | /// The text of the content dialog
243 | /// The title of the content dialog
244 | public static Task ShowDialogAsync(string title, string? text)
245 | {
246 | return Task.Run(() => System.Windows.MessageBox.Show(text, title));
247 | }
248 | }
--------------------------------------------------------------------------------
/MsGraphSamples.WinUI/ViewModels/MainViewModel.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using System.Net;
3 | using System.Text;
4 | using CommunityToolkit.Mvvm.ComponentModel;
5 | using CommunityToolkit.Mvvm.Input;
6 | using CommunityToolkit.Mvvm.Messaging;
7 | using CommunityToolkit.WinUI.UI.Controls;
8 | using Microsoft.Graph.Models;
9 | using Microsoft.Graph.Models.ODataErrors;
10 | using Microsoft.Kiota.Abstractions;
11 | using MsGraphSamples.Services;
12 | using MsGraphSamples.WinUI.Helpers;
13 | using System.Collections.Immutable;
14 | using System.Reflection;
15 |
16 | namespace MsGraphSamples.WinUI.ViewModels;
17 |
18 | public partial class MainViewModel(
19 | IAuthService authService,
20 | IAsyncEnumerableGraphDataService graphDataService,
21 | IDialogService dialogService) : ObservableRecipient
22 | {
23 | public ushort PageSize { get; set; } = 25;
24 | private readonly Stopwatch _stopWatch = new();
25 | public long ElapsedMs => _stopWatch.ElapsedMilliseconds;
26 |
27 | [ObservableProperty]
28 | [NotifyPropertyChangedFor(nameof(IsIndeterminate))]
29 | private bool _isBusy = false;
30 |
31 | [ObservableProperty]
32 | [NotifyPropertyChangedFor(nameof(IsIndeterminate))]
33 | private bool _isError = false;
34 |
35 | public bool IsIndeterminate => IsBusy || IsError;
36 |
37 | [ObservableProperty]
38 | private string? _userName;
39 |
40 | [ObservableProperty]
41 | [NotifyCanExecuteChangedFor(nameof(LaunchGraphExplorerCommand))]
42 | [NotifyPropertyChangedFor(nameof(LastUrl))]
43 | [NotifyPropertyChangedFor(nameof(LastCount))]
44 | private AsyncLoadingCollection? _directoryObjects;
45 |
46 | [ObservableProperty]
47 | private DirectoryObject? _selectedObject;
48 |
49 | public static IReadOnlyList Entities => ["Users", "Groups", "Applications", "ServicePrincipals", "Devices"];
50 |
51 | [ObservableProperty]
52 | private string _selectedEntity = "Users";
53 | public string? LastUrl => graphDataService.LastUrl;
54 | public long? LastCount => graphDataService.LastCount;
55 |
56 | #region OData Operators
57 |
58 | public string[] SplittedSelect => Select.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
59 |
60 | [ObservableProperty]
61 | public string _select = "id,displayName,mail,userPrincipalName";
62 |
63 | [ObservableProperty]
64 | public string? _filter;
65 |
66 | public string[]? SplittedOrderBy => OrderBy?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
67 |
68 | [ObservableProperty]
69 | public string? _orderBy;
70 |
71 | private string? _search;
72 | public string? Search
73 | {
74 | get => _search;
75 | set
76 | {
77 | if (_search != value)
78 | {
79 | _search = FixSearchSyntax(value);
80 | OnPropertyChanged();
81 | }
82 | }
83 | }
84 |
85 | private static string? FixSearchSyntax(string? searchValue)
86 | {
87 | if (searchValue == null)
88 | return null;
89 |
90 | if (searchValue.Contains('"'))
91 | return searchValue; // Assume already correctly formatted
92 |
93 | var elements = searchValue.Trim().Split(' ');
94 | var sb = new StringBuilder(elements.Length);
95 |
96 | foreach (var element in elements)
97 | {
98 | string? newElement;
99 |
100 | if (element.Contains(':'))
101 | newElement = $"\"{element}\""; // Search clause needs to be wrapped by double quotes
102 | else if (element.In("AND", "OR"))
103 | newElement = $" {element.ToUpperInvariant()} "; // [AND, OR] Operators need to be uppercase
104 | else
105 | newElement = element;
106 |
107 | sb.Append(newElement);
108 | }
109 |
110 | return sb.ToString();
111 | }
112 |
113 | #endregion
114 |
115 | public async Task PageLoaded()
116 | {
117 | var user = await graphDataService.GetUserAsync(["displayName"]);
118 | UserName = user?.DisplayName;
119 | await Load();
120 | }
121 |
122 | [RelayCommand]
123 | private Task Load()
124 | {
125 | return IsBusyWrapper(SelectedEntity switch
126 | {
127 | //"Users" => _graphDataService.GetUsersInBatch(SplittedSelect, pageSize),
128 | "Users" => graphDataService.GetUsers(SplittedSelect, Filter, SplittedOrderBy, Search, PageSize),
129 | "Groups" => graphDataService.GetGroups(SplittedSelect, Filter, SplittedOrderBy, Search, PageSize),
130 | "Applications" => graphDataService.GetApplications(SplittedSelect, Filter, SplittedOrderBy, Search, PageSize),
131 | "ServicePrincipals" => graphDataService.GetServicePrincipals(SplittedSelect, Filter, SplittedOrderBy, Search, PageSize),
132 | "Devices" => graphDataService.GetDevices(SplittedSelect, Filter, SplittedOrderBy, Search, PageSize),
133 | _ => throw new NotImplementedException("Can't find selected entity")
134 | });
135 | }
136 |
137 | public Task DrillDown()
138 | {
139 | ArgumentNullException.ThrowIfNull(SelectedObject);
140 |
141 | OrderBy = null;
142 | Filter = null;
143 | Search = null;
144 |
145 | return IsBusyWrapper(SelectedEntity switch
146 | {
147 | "Users" => graphDataService.GetTransitiveMemberOfAsGroups(SelectedObject.Id!, SplittedSelect, PageSize),
148 | "Groups" => graphDataService.GetTransitiveMembersAsUsers(SelectedObject.Id!, SplittedSelect, PageSize),
149 | "Applications" => graphDataService.GetApplicationOwnersAsUsers(SelectedObject.Id!, SplittedSelect, PageSize),
150 | "ServicePrincipals" => graphDataService.GetServicePrincipalOwnersAsUsers(SelectedObject.Id!, SplittedSelect, PageSize),
151 | "Devices" => graphDataService.GetDeviceOwnersAsUsers(SelectedObject.Id!, SplittedSelect, PageSize),
152 | _ => throw new NotImplementedException("Can't find selected entity")
153 | });
154 | }
155 |
156 | public Task Sort(object sender, DataGridColumnEventArgs e)
157 | {
158 | OrderBy = e.Column.SortDirection == null || e.Column.SortDirection == DataGridSortDirection.Descending
159 | ? $"{e.Column.Header} asc"
160 | : $"{e.Column.Header} desc";
161 |
162 | return Load();
163 | }
164 |
165 | private bool CanLaunchGraphExplorer() => LastUrl is not null;
166 | [RelayCommand(CanExecute = nameof(CanLaunchGraphExplorer))]
167 | private void LaunchGraphExplorer()
168 | {
169 | ArgumentNullException.ThrowIfNull(LastUrl);
170 |
171 | var geBaseUrl = "https://developer.microsoft.com/en-us/graph/graph-explorer";
172 | var graphUrl = "https://graph.microsoft.com";
173 | var version = "v1.0";
174 | var startOfQuery = LastUrl.NthIndexOf('/', 4) + 1;
175 | var encodedUrl = WebUtility.UrlEncode(LastUrl[startOfQuery..]);
176 | var encodedHeaders = "W3sibmFtZSI6IkNvbnNpc3RlbmN5TGV2ZWwiLCJ2YWx1ZSI6ImV2ZW50dWFsIn1d"; // ConsistencyLevel = eventual
177 |
178 | var url = $"{geBaseUrl}?request={encodedUrl}&method=GET&version={version}&GraphUrl={graphUrl}&headers={encodedHeaders}";
179 |
180 | var psi = new ProcessStartInfo { FileName = url, UseShellExecute = true };
181 | System.Diagnostics.Process.Start(psi);
182 | }
183 |
184 | [RelayCommand]
185 | private void Logout()
186 | {
187 | authService.Logout();
188 | App.Current.Exit();
189 | }
190 |
191 | private async Task IsBusyWrapper(IAsyncEnumerable directoryObjects)
192 | {
193 | IsError = false;
194 | IsBusy = true;
195 | _stopWatch.Restart();
196 |
197 | try
198 | {
199 | // Sending message to generate DataGridColumns according to the selected properties
200 | await GetPropertiesAndSortDirection(directoryObjects);
201 |
202 | DirectoryObjects = new(directoryObjects, PageSize);
203 |
204 | // Trigger load due to bug https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/3584
205 | await DirectoryObjects.RefreshAsync();
206 |
207 | SelectedEntity = DirectoryObjects.FirstOrDefault() switch
208 | {
209 | User => "Users",
210 | Group => "Groups",
211 | Application => "Applications",
212 | ServicePrincipal => "ServicePrincipals",
213 | Device => "Devices",
214 | _ => SelectedEntity,
215 | };
216 | }
217 | catch (ODataError ex)
218 | {
219 | IsError = true;
220 | await dialogService.ShowAsync(ex.Error?.Code ?? "OData Error", ex.Error?.Message);
221 | }
222 | catch (ApiException ex)
223 | {
224 | IsError = true;
225 | await dialogService.ShowAsync(Enum.GetName((HttpStatusCode)ex.ResponseStatusCode)!, ex.Message);
226 | }
227 | finally
228 | {
229 | _stopWatch.Stop();
230 | OnPropertyChanged(nameof(ElapsedMs));
231 | IsBusy = false;
232 | }
233 | }
234 |
235 | private async Task GetPropertiesAndSortDirection(IAsyncEnumerable directoryObjects)
236 | {
237 | var item = await directoryObjects.FirstOrDefaultAsync();
238 | if (item == null)
239 | return;
240 |
241 | var propertiesAndSortDirection = item.GetType()
242 | .GetProperties(BindingFlags.Public | BindingFlags.Instance)
243 | .Select(p => p.Name)
244 | .Where(p => p.In(SplittedSelect))
245 | .ToImmutableSortedDictionary(kv => kv, GetSortDirection);
246 |
247 | WeakReferenceMessenger.Default.Send(propertiesAndSortDirection);
248 |
249 | DataGridSortDirection? GetSortDirection(string propertyName)
250 | {
251 | var property = OrderBy?.Split(' ')[0];
252 | var direction = OrderBy?.Split(' ').ElementAtOrDefault(1) ?? "asc";
253 |
254 | if (propertyName.Equals(property, StringComparison.InvariantCultureIgnoreCase))
255 | return direction.Equals("asc", StringComparison.InvariantCultureIgnoreCase)
256 | ? DataGridSortDirection.Ascending
257 | : DataGridSortDirection.Descending;
258 |
259 | return null;
260 | }
261 | }
262 | }
263 |
--------------------------------------------------------------------------------
/MsGraphSamples.WinUI/Views/MainPage.xaml:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
42 |
46 | $select
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
61 |
65 | $filter
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
99 |
100 |
101 |
102 |
106 |
110 | $orderBy
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
126 |
130 | $search
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
155 |
156 |
157 |
164 |
174 |
175 |
176 |
177 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
211 |
212 |
218 |
219 |
225 |
226 |
230 |
231 |
232 |
233 |
--------------------------------------------------------------------------------
/MsGraphSamples.WPF/Views/MainWindow.xaml:
--------------------------------------------------------------------------------
1 |
24 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
76 |
77 |
78 | $select
79 |
80 |
81 | // List properties you want to select separated by a comma
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
94 |
95 |
96 | $orderBy
97 |
98 |
99 | // Enabled only on few properties, see documentation for details
100 |
101 | property [asc|desc]
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
114 |
115 |
116 | $filter
117 |
118 |
119 | property eq 'filterValue'
120 | property ne 'filterValue'
121 | not propertyCollection/any(p:p eq 'filterValue')
122 | startsWith(property, 'filterValue')
123 | endsWith(property, 'filterValue')
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
136 |
137 |
138 | $search
139 |
140 |
141 | // Enabled on displayName and Description
142 |
143 | // All the other properties will fallback to startsWith filter behavior
144 |
145 | "property1:value1" [AND|OR] "property2:value2"
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
158 |
159 |
164 |
165 |
166 |
167 |
171 |
179 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
202 |
209 |
210 |
211 |
212 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
255 |
256 |
257 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
--------------------------------------------------------------------------------
/MsGraphSamples.Services/GraphDataService.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | using Microsoft.Graph;
5 | using Microsoft.Graph.Models;
6 | using Microsoft.Graph.Models.ODataErrors;
7 | using Microsoft.Kiota.Abstractions;
8 | using Microsoft.Kiota.Abstractions.Serialization;
9 | using System.Net;
10 |
11 | namespace MsGraphSamples.Services;
12 |
13 | public interface IGraphDataService
14 | {
15 | string? LastUrl { get; }
16 |
17 | Task GetNextPageAsync(TCollectionResponse? collectionResponse) where TCollectionResponse : BaseCollectionPaginationCountResponse, new();
18 |
19 | Task GetUserAsync(string[] select, string? id = null);
20 | Task GetUsersRawCountAsync(string filter, string search);
21 |
22 | Task GetApplicationCollectionAsync(string[] select, string? filter = null, string[]? orderBy = null, string? search = null, ushort top = 999);
23 | Task GetServicePrincipalCollectionAsync(string[] select, string? filter = null, string[]? orderBy = null, string? search = null, ushort top = 999);
24 | Task GetDeviceCollectionAsync(string[] select, string? filter = null, string[]? orderBy = null, string? search = null, ushort top = 999);
25 | Task GetGroupCollectionAsync(string[] select, string? filter = null, string[]? orderBy = null, string? search = null, ushort top = 999);
26 | Task GetUserCollectionAsync(string[] select, string? filter = null, string[]? orderBy = null, string? search = null, ushort top = 999);
27 |
28 | Task GetApplicationOwnersAsUserCollectionAsync(string id, string[] select, ushort top = 999);
29 | Task GetServicePrincipalOwnersAsUserCollectionAsync(string id, string[] select, ushort top = 999);
30 | Task GetDeviceOwnersAsUserCollectionAsync(string id, string[] select, ushort top = 999);
31 | Task GetGroupOwnersAsUserCollectionAsync(string id, string[] select, ushort top = 999);
32 |
33 | Task GetTransitiveMemberOfAsGroupCollectionAsync(string id, string[] select, ushort top = 999);
34 | Task GetTransitiveMembersAsUserCollectionAsync(string id, string[] select, ushort top = 999);
35 |
36 | Task WriteExtensionProperty(string propertyName, object propertyValue, string userId);
37 | }
38 |
39 | public class GraphDataService(GraphServiceClient graphClient) : IGraphDataService
40 | {
41 | private readonly RequestHeaders EventualConsistencyHeader = new() { { "ConsistencyLevel", "eventual" } };
42 | private readonly Dictionary> ErrorMapping = new() { { "XXX", ODataError.CreateFromDiscriminatorValue } };
43 |
44 | public string? LastUrl { get; private set; } = null;
45 |
46 | public Task GetUserAsync(string[] select, string? id = null)
47 | {
48 | return id == null
49 | ? graphClient.Me.GetAsync(rc => rc.QueryParameters.Select = select)
50 | : graphClient.Users[id].GetAsync(rc => rc.QueryParameters.Select = select);
51 | }
52 |
53 | public Task GetUsersRawCountAsync(string? filter = null, string? search = null)
54 | {
55 | var requestInfo = graphClient.Users
56 | .Count
57 | .ToGetRequestInformation(rc =>
58 | {
59 | rc.Headers = EventualConsistencyHeader;
60 | rc.QueryParameters.Filter = filter;
61 | rc.QueryParameters.Search = search;
62 | });
63 |
64 | LastUrl = WebUtility.UrlDecode(requestInfo.URI.AbsoluteUri);
65 | return graphClient.RequestAdapter.SendPrimitiveAsync(requestInfo);
66 | }
67 |
68 | public Task GetNextPageAsync(TCollectionResponse? collectionResponse) where TCollectionResponse : BaseCollectionPaginationCountResponse, new()
69 | {
70 | return collectionResponse.GetNextPageAsync(graphClient.RequestAdapter);
71 | }
72 |
73 | public Task GetApplicationCollectionAsync(string[] select, string? filter = null, string[]? orderBy = null, string? search = null, ushort top = 999)
74 | {
75 | var requestInfo = graphClient.Applications
76 | .ToGetRequestInformation(rc =>
77 | {
78 | rc.Headers = EventualConsistencyHeader;
79 | rc.QueryParameters.Count = true;
80 | rc.QueryParameters.Select = select;
81 | rc.QueryParameters.Filter = filter;
82 | rc.QueryParameters.Orderby = orderBy;
83 | rc.QueryParameters.Search = search;
84 | rc.QueryParameters.Top = top;
85 | });
86 |
87 | LastUrl = WebUtility.UrlDecode(requestInfo.URI.AbsoluteUri);
88 | return graphClient.RequestAdapter.SendAsync(requestInfo, ApplicationCollectionResponse.CreateFromDiscriminatorValue, ErrorMapping);
89 | }
90 |
91 | public Task GetServicePrincipalCollectionAsync(string[] select, string? filter = null, string[]? orderBy = null, string? search = null, ushort top = 999)
92 | {
93 | var requestInfo = graphClient.ServicePrincipals
94 | .ToGetRequestInformation(rc =>
95 | {
96 | rc.Headers = EventualConsistencyHeader;
97 | rc.QueryParameters.Count = true;
98 | rc.QueryParameters.Select = select;
99 | rc.QueryParameters.Filter = filter;
100 | rc.QueryParameters.Orderby = orderBy;
101 | rc.QueryParameters.Search = search;
102 | rc.QueryParameters.Top = top;
103 | });
104 |
105 | LastUrl = WebUtility.UrlDecode(requestInfo.URI.AbsoluteUri);
106 | return graphClient.RequestAdapter.SendAsync(requestInfo, ServicePrincipalCollectionResponse.CreateFromDiscriminatorValue, ErrorMapping);
107 | }
108 |
109 | public Task GetDeviceCollectionAsync(string[] select, string? filter = null, string[]? orderBy = null, string? search = null, ushort top = 999)
110 | {
111 | var requestInfo = graphClient.Devices
112 | .ToGetRequestInformation(rc =>
113 | {
114 | rc.Headers = EventualConsistencyHeader;
115 | rc.QueryParameters.Count = true;
116 | rc.QueryParameters.Select = select;
117 | rc.QueryParameters.Filter = filter;
118 | rc.QueryParameters.Orderby = orderBy;
119 | rc.QueryParameters.Search = search;
120 | rc.QueryParameters.Top = top;
121 | });
122 |
123 | LastUrl = WebUtility.UrlDecode(requestInfo.URI.AbsoluteUri);
124 | return graphClient.RequestAdapter.SendAsync(requestInfo, DeviceCollectionResponse.CreateFromDiscriminatorValue, ErrorMapping);
125 | }
126 |
127 | public Task GetGroupCollectionAsync(string[] select, string? filter = null, string[]? orderBy = null, string? search = null, ushort top = 999)
128 | {
129 | var requestInfo = graphClient.Groups
130 | .ToGetRequestInformation(rc =>
131 | {
132 | rc.Headers = EventualConsistencyHeader;
133 | rc.QueryParameters.Count = true;
134 | rc.QueryParameters.Select = select;
135 | rc.QueryParameters.Filter = filter;
136 | rc.QueryParameters.Orderby = orderBy;
137 | rc.QueryParameters.Search = search;
138 | rc.QueryParameters.Top = top;
139 | });
140 |
141 | LastUrl = WebUtility.UrlDecode(requestInfo.URI.AbsoluteUri);
142 | return graphClient.RequestAdapter.SendAsync(requestInfo, GroupCollectionResponse.CreateFromDiscriminatorValue, ErrorMapping);
143 | }
144 |
145 | public Task GetUserCollectionAsync(string[] select, string? filter = null, string[]? orderBy = null, string? search = null, ushort top = 999)
146 | {
147 | var requestInfo = graphClient.Users
148 | .ToGetRequestInformation(rc =>
149 | {
150 | rc.Headers = EventualConsistencyHeader;
151 | rc.QueryParameters.Count = true;
152 | rc.QueryParameters.Select = select;
153 | rc.QueryParameters.Filter = filter;
154 | rc.QueryParameters.Orderby = orderBy;
155 | rc.QueryParameters.Search = search;
156 | rc.QueryParameters.Top = top;
157 | });
158 |
159 | LastUrl = WebUtility.UrlDecode(requestInfo.URI.AbsoluteUri);
160 | return graphClient.RequestAdapter.SendAsync(requestInfo, UserCollectionResponse.CreateFromDiscriminatorValue, ErrorMapping);
161 | }
162 |
163 | public Task GetApplicationOwnersAsUserCollectionAsync(string id, string[] select, ushort top = 999)
164 | {
165 | var requestInfo = graphClient.Applications[id]
166 | .Owners.GraphUser
167 | .ToGetRequestInformation(rc =>
168 | {
169 | rc.Headers = EventualConsistencyHeader;
170 | rc.QueryParameters.Count = true;
171 | rc.QueryParameters.Select = select;
172 | rc.QueryParameters.Top = top;
173 | });
174 |
175 | LastUrl = WebUtility.UrlDecode(requestInfo.URI.AbsoluteUri);
176 | return graphClient.RequestAdapter.SendAsync(requestInfo, UserCollectionResponse.CreateFromDiscriminatorValue, ErrorMapping);
177 | }
178 |
179 | public Task GetServicePrincipalOwnersAsUserCollectionAsync(string id, string[] select, ushort top = 999)
180 | {
181 | var requestInfo = graphClient.ServicePrincipals[id]
182 | .Owners.GraphUser
183 | .ToGetRequestInformation(rc =>
184 | {
185 | rc.Headers = EventualConsistencyHeader;
186 | rc.QueryParameters.Count = true;
187 | rc.QueryParameters.Select = select;
188 | rc.QueryParameters.Top = top;
189 | });
190 |
191 | LastUrl = WebUtility.UrlDecode(requestInfo.URI.AbsoluteUri);
192 | return graphClient.RequestAdapter.SendAsync(requestInfo, UserCollectionResponse.CreateFromDiscriminatorValue, ErrorMapping);
193 | }
194 |
195 | public Task GetDeviceOwnersAsUserCollectionAsync(string id, string[] select, ushort top = 999)
196 | {
197 | var requestInfo = graphClient.Devices[id]
198 | .RegisteredOwners.GraphUser
199 | .ToGetRequestInformation(rc =>
200 | {
201 | rc.Headers = EventualConsistencyHeader;
202 | rc.QueryParameters.Count = true;
203 | rc.QueryParameters.Select = select;
204 | rc.QueryParameters.Top = top;
205 | });
206 |
207 | LastUrl = WebUtility.UrlDecode(requestInfo.URI.AbsoluteUri);
208 | return graphClient.RequestAdapter.SendAsync(requestInfo, UserCollectionResponse.CreateFromDiscriminatorValue, ErrorMapping);
209 | }
210 |
211 | public Task GetGroupOwnersAsUserCollectionAsync(string id, string[] select, ushort top = 999)
212 | {
213 | var requestInfo = graphClient.Groups[id]
214 | .Owners.GraphUser
215 | .ToGetRequestInformation(rc =>
216 | {
217 | rc.Headers = EventualConsistencyHeader;
218 | rc.QueryParameters.Count = true;
219 | rc.QueryParameters.Select = select;
220 | rc.QueryParameters.Top = top;
221 | });
222 |
223 | LastUrl = WebUtility.UrlDecode(requestInfo.URI.AbsoluteUri);
224 | return graphClient.RequestAdapter.SendAsync(requestInfo, UserCollectionResponse.CreateFromDiscriminatorValue, ErrorMapping);
225 | }
226 |
227 | public Task GetTransitiveMemberOfAsGroupCollectionAsync(string id, string[] select, ushort top = 999)
228 | {
229 | var requestInfo = graphClient.Users[id]
230 | .TransitiveMemberOf.GraphGroup
231 | .ToGetRequestInformation(rc =>
232 | {
233 | rc.Headers = EventualConsistencyHeader;
234 | rc.QueryParameters.Count = true;
235 | rc.QueryParameters.Select = select;
236 | rc.QueryParameters.Top = top;
237 | });
238 |
239 | LastUrl = WebUtility.UrlDecode(requestInfo.URI.AbsoluteUri);
240 | return graphClient.RequestAdapter.SendAsync(requestInfo, GroupCollectionResponse.CreateFromDiscriminatorValue, ErrorMapping);
241 | }
242 |
243 | public Task GetTransitiveMembersAsUserCollectionAsync(string id, string[] select, ushort top = 999)
244 | {
245 | var requestInfo = graphClient.Groups[id]
246 | .TransitiveMembers.GraphUser
247 | .ToGetRequestInformation(rc =>
248 | {
249 | rc.Headers = EventualConsistencyHeader;
250 | rc.QueryParameters.Count = true;
251 | rc.QueryParameters.Select = select;
252 | rc.QueryParameters.Top = top;
253 | });
254 |
255 | LastUrl = WebUtility.UrlDecode(requestInfo.URI.AbsoluteUri);
256 | return graphClient.RequestAdapter.SendAsync(requestInfo, UserCollectionResponse.CreateFromDiscriminatorValue, ErrorMapping);
257 | }
258 |
259 | public Task WriteExtensionProperty(string propertyName, object propertyValue, string userId)
260 | {
261 | var userRequestBody = new User();
262 | userRequestBody.AdditionalData[propertyName] = propertyValue;
263 | return graphClient.Users[userId].PatchAsync(userRequestBody);
264 | }
265 | }
--------------------------------------------------------------------------------
/MsGraphSamples.Services/AsyncEnumerableGraphDataService.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | using System.Net;
5 | using Microsoft.Graph;
6 | using Microsoft.Graph.Models;
7 | using Microsoft.Kiota.Abstractions;
8 |
9 | namespace MsGraphSamples.Services;
10 |
11 | public interface IAsyncEnumerableGraphDataService
12 | {
13 | string? LastUrl { get; }
14 | public long? LastCount { get; }
15 |
16 | Task GetUserAsync(string[] select, string? id = null);
17 | IAsyncEnumerable GetUsersInBatch(string[] select, ushort top = 999, CancellationToken cancellationToken = default);
18 | IAsyncEnumerable GetApplications(string[] select, string? filter = null, string[]? orderBy = null, string? search = null, ushort top = 999, CancellationToken cancellationToken = default);
19 | IAsyncEnumerable GetServicePrincipals(string[] splittedSelect, string? filter, string[]? splittedOrderBy, string? search, ushort top = 999, CancellationToken cancellationToken = default);
20 | IAsyncEnumerable GetDevices(string[] select, string? filter = null, string[]? orderBy = null, string? search = null, ushort top = 999, CancellationToken cancellationToken = default);
21 | IAsyncEnumerable GetGroups(string[] select, string? filter = null, string[]? orderBy = null, string? search = null, ushort top = 999, CancellationToken cancellationToken = default);
22 | IAsyncEnumerable GetUsers(string[] select, string? filter = null, string[]? orderBy = null, string? search = null, ushort top = 999, CancellationToken cancellationToken = default);
23 | IAsyncEnumerable GetApplicationOwnersAsUsers(string id, string[] select, ushort top = 999, CancellationToken cancellationToken = default);
24 | IAsyncEnumerable GetServicePrincipalOwnersAsUsers(string id, string[] select, ushort top = 999, CancellationToken cancellationToken = default);
25 | IAsyncEnumerable GetDeviceOwnersAsUsers(string id, string[] select, ushort top = 999, CancellationToken cancellationToken = default);
26 | IAsyncEnumerable GetGroupOwnersAsUsers(string id, string[] select, ushort top = 999, CancellationToken cancellationToken = default);
27 | IAsyncEnumerable GetTransitiveMemberOfAsGroups(string id, string[] select, ushort top = 999, CancellationToken cancellationToken = default);
28 | IAsyncEnumerable GetTransitiveMembersAsUsers(string id, string[] select, ushort top = 999, CancellationToken cancellationToken = default);
29 | }
30 |
31 | public class AsyncEnumerableGraphDataService(GraphServiceClient graphClient) : IAsyncEnumerableGraphDataService
32 | {
33 | private readonly RequestHeaders EventualConsistencyHeader = new() { { "ConsistencyLevel", "eventual" } };
34 |
35 | public string? LastUrl { get; private set; } = null;
36 | public long? LastCount { get; private set; } = null;
37 | private void SetCount(long? count) => LastCount = count;
38 |
39 | public Task GetUserAsync(string[] select, string? id = null)
40 | {
41 | return id == null
42 | ? graphClient.Me.GetAsync(rc => rc.QueryParameters.Select = select)
43 | : graphClient.Users[id].GetAsync(rc => rc.QueryParameters.Select = select);
44 | }
45 |
46 | public IAsyncEnumerable GetUsersInBatch(string[] select, ushort top = 999, CancellationToken cancellationToken = default)
47 | {
48 | return graphClient.Batch(cancellationToken,
49 | graphClient.Users.ToGetRequestInformation(rc =>
50 | {
51 | rc.Headers = EventualConsistencyHeader;
52 | rc.QueryParameters.Count = true;
53 | rc.QueryParameters.Select = select;
54 | rc.QueryParameters.Filter = "startsWith(displayName, 'x')";
55 | rc.QueryParameters.Top = top;
56 | }),
57 | graphClient.Users.ToGetRequestInformation(rc =>
58 | {
59 | rc.Headers = EventualConsistencyHeader;
60 | rc.QueryParameters.Count = true;
61 | rc.QueryParameters.Select = select;
62 | rc.QueryParameters.Filter = "startsWith(displayName, 'y')";
63 | rc.QueryParameters.Top = top;
64 | }),
65 | graphClient.Users.ToGetRequestInformation(rc =>
66 | {
67 | rc.Headers = EventualConsistencyHeader;
68 | rc.QueryParameters.Count = true;
69 | rc.QueryParameters.Select = select;
70 | rc.QueryParameters.Filter = "startsWith(displayName, 'z')";
71 | rc.QueryParameters.Top = top;
72 | }));
73 | }
74 |
75 | public IAsyncEnumerable GetApplications(string[] select, string? filter = null, string[]? orderBy = null, string? search = null, ushort top = 999, CancellationToken cancellationToken = default)
76 | {
77 | var requestInfo = graphClient.Applications
78 | .ToGetRequestInformation(rc =>
79 | {
80 | rc.Headers = EventualConsistencyHeader;
81 | rc.QueryParameters.Count = true;
82 | rc.QueryParameters.Select = select;
83 | rc.QueryParameters.Filter = filter;
84 | rc.QueryParameters.Orderby = orderBy;
85 | rc.QueryParameters.Search = search;
86 | rc.QueryParameters.Top = top;
87 | });
88 |
89 | LastUrl = WebUtility.UrlDecode(requestInfo.URI.AbsoluteUri);
90 | return requestInfo.ToAsyncEnumerable(graphClient.RequestAdapter, SetCount, cancellationToken);
91 | }
92 |
93 | public IAsyncEnumerable GetServicePrincipals(string[] select, string? filter, string[]? orderBy, string? search = null, ushort top = 999, CancellationToken cancellationToken = default)
94 | {
95 | var requestInfo = graphClient.ServicePrincipals
96 | .ToGetRequestInformation(rc =>
97 | {
98 | rc.Headers = EventualConsistencyHeader;
99 | rc.QueryParameters.Count = true;
100 | rc.QueryParameters.Select = select;
101 | rc.QueryParameters.Filter = filter;
102 | rc.QueryParameters.Orderby = orderBy;
103 | rc.QueryParameters.Search = search;
104 | rc.QueryParameters.Top = top;
105 | });
106 |
107 | LastUrl = WebUtility.UrlDecode(requestInfo.URI.AbsoluteUri);
108 | return requestInfo.ToAsyncEnumerable(graphClient.RequestAdapter, SetCount, cancellationToken);
109 | }
110 |
111 | public IAsyncEnumerable GetDevices(string[] select, string? filter = null, string[]? orderBy = null, string? search = null, ushort top = 999, CancellationToken cancellationToken = default)
112 | {
113 | var requestInfo = graphClient.Devices
114 | .ToGetRequestInformation(rc =>
115 | {
116 | rc.Headers = EventualConsistencyHeader;
117 | rc.QueryParameters.Count = true;
118 | rc.QueryParameters.Select = select;
119 | rc.QueryParameters.Filter = filter;
120 | rc.QueryParameters.Orderby = orderBy;
121 | rc.QueryParameters.Search = search;
122 | rc.QueryParameters.Top = top;
123 | });
124 |
125 | LastUrl = WebUtility.UrlDecode(requestInfo.URI.AbsoluteUri);
126 | return requestInfo.ToAsyncEnumerable(graphClient.RequestAdapter, SetCount, cancellationToken);
127 | }
128 |
129 | public IAsyncEnumerable GetGroups(string[] select, string? filter = null, string[]? orderBy = null, string? search = null, ushort top = 999, CancellationToken cancellationToken = default)
130 | {
131 | var requestInfo = graphClient.Groups
132 | .ToGetRequestInformation(rc =>
133 | {
134 | rc.Headers = EventualConsistencyHeader;
135 | rc.QueryParameters.Count = true;
136 | rc.QueryParameters.Select = select;
137 | rc.QueryParameters.Filter = filter;
138 | rc.QueryParameters.Orderby = orderBy;
139 | rc.QueryParameters.Search = search;
140 | rc.QueryParameters.Top = top;
141 | });
142 |
143 | LastUrl = WebUtility.UrlDecode(requestInfo.URI.AbsoluteUri);
144 | return requestInfo.ToAsyncEnumerable(graphClient.RequestAdapter, SetCount, cancellationToken);
145 | }
146 |
147 | public IAsyncEnumerable GetUsers(string[] select, string? filter = null, string[]? orderBy = null, string? search = null, ushort top = 999, CancellationToken cancellationToken = default)
148 | {
149 | var requestInfo = graphClient.Users
150 | .ToGetRequestInformation(rc =>
151 | {
152 | rc.Headers = EventualConsistencyHeader;
153 | rc.QueryParameters.Count = true;
154 | rc.QueryParameters.Select = select;
155 | rc.QueryParameters.Filter = filter;
156 | rc.QueryParameters.Orderby = orderBy;
157 | rc.QueryParameters.Search = search;
158 | rc.QueryParameters.Top = top;
159 | });
160 |
161 | LastUrl = WebUtility.UrlDecode(requestInfo.URI.AbsoluteUri);
162 | return requestInfo.ToAsyncEnumerable(graphClient.RequestAdapter, SetCount, cancellationToken);
163 | }
164 |
165 | public IAsyncEnumerable GetApplicationOwnersAsUsers(string id, string[] select, ushort top = 999, CancellationToken cancellationToken = default)
166 | {
167 | var requestInfo = graphClient.Applications[id]
168 | .Owners.GraphUser
169 | .ToGetRequestInformation(rc =>
170 | {
171 | rc.Headers = EventualConsistencyHeader;
172 | rc.QueryParameters.Count = true;
173 | rc.QueryParameters.Select = select;
174 | rc.QueryParameters.Top = top;
175 | });
176 |
177 | LastUrl = WebUtility.UrlDecode(requestInfo.URI.AbsoluteUri);
178 | return requestInfo.ToAsyncEnumerable(graphClient.RequestAdapter, SetCount, cancellationToken);
179 | }
180 |
181 | public IAsyncEnumerable GetServicePrincipalOwnersAsUsers(string id, string[] select, ushort top = 999, CancellationToken cancellationToken = default)
182 | {
183 | var requestInfo = graphClient.ServicePrincipals[id]
184 | .Owners.GraphUser
185 | .ToGetRequestInformation(rc =>
186 | {
187 | rc.Headers = EventualConsistencyHeader;
188 | rc.QueryParameters.Count = true;
189 | rc.QueryParameters.Select = select;
190 | rc.QueryParameters.Top = top;
191 | });
192 |
193 | LastUrl = WebUtility.UrlDecode(requestInfo.URI.AbsoluteUri);
194 | return requestInfo.ToAsyncEnumerable(graphClient.RequestAdapter, SetCount, cancellationToken);
195 | }
196 |
197 | public IAsyncEnumerable GetDeviceOwnersAsUsers(string id, string[] select, ushort top = 999, CancellationToken cancellationToken = default)
198 | {
199 | var requestInfo = graphClient.Devices[id]
200 | .RegisteredOwners.GraphUser
201 | .ToGetRequestInformation(rc =>
202 | {
203 | rc.Headers = EventualConsistencyHeader;
204 | rc.QueryParameters.Count = true;
205 | rc.QueryParameters.Select = select;
206 | rc.QueryParameters.Top = top;
207 | });
208 |
209 | LastUrl = WebUtility.UrlDecode(requestInfo.URI.AbsoluteUri);
210 | return requestInfo.ToAsyncEnumerable(graphClient.RequestAdapter, SetCount, cancellationToken);
211 | }
212 |
213 | public IAsyncEnumerable GetGroupOwnersAsUsers(string id, string[] select, ushort top = 999, CancellationToken cancellationToken = default)
214 | {
215 | var requestInfo = graphClient.Groups[id]
216 | .Owners.GraphUser
217 | .ToGetRequestInformation(rc =>
218 | {
219 |
220 | rc.Headers = EventualConsistencyHeader;
221 | rc.QueryParameters.Count = true;
222 | rc.QueryParameters.Select = select;
223 | rc.QueryParameters.Top = top;
224 | });
225 |
226 | LastUrl = WebUtility.UrlDecode(requestInfo.URI.AbsoluteUri);
227 | return requestInfo.ToAsyncEnumerable(graphClient.RequestAdapter, SetCount, cancellationToken);
228 | }
229 |
230 | public IAsyncEnumerable GetTransitiveMemberOfAsGroups(string id, string[] select, ushort top = 999, CancellationToken cancellationToken = default)
231 | {
232 | var requestInfo = graphClient.Users[id]
233 | .TransitiveMemberOf.GraphGroup
234 | .ToGetRequestInformation(rc =>
235 | {
236 | rc.Headers = EventualConsistencyHeader;
237 | rc.QueryParameters.Count = true;
238 | rc.QueryParameters.Select = select;
239 | rc.QueryParameters.Top = top;
240 | });
241 |
242 | LastUrl = WebUtility.UrlDecode(requestInfo.URI.AbsoluteUri);
243 | return requestInfo.ToAsyncEnumerable(graphClient.RequestAdapter, SetCount, cancellationToken);
244 | }
245 |
246 | public IAsyncEnumerable GetTransitiveMembersAsUsers(string id, string[] select, ushort top = 999, CancellationToken cancellationToken = default)
247 | {
248 | var requestInfo = graphClient.Groups[id]
249 | .TransitiveMembers.GraphUser
250 | .ToGetRequestInformation(rc =>
251 | {
252 | rc.Headers = EventualConsistencyHeader;
253 | rc.QueryParameters.Count = true;
254 | rc.QueryParameters.Select = select;
255 | rc.QueryParameters.Top = top;
256 | });
257 |
258 | LastUrl = WebUtility.UrlDecode(requestInfo.URI.AbsoluteUri);
259 | return requestInfo.ToAsyncEnumerable(graphClient.RequestAdapter, SetCount, cancellationToken);
260 | }
261 | }
--------------------------------------------------------------------------------