├── 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 | ![Application Registration](docs/register_app.png) 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 | ![Api Permissions](docs/api_permissions.png) 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 | ![Solution Explorer](docs/solution_explorer.png) 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 | ![Screenshot of the App](docs/app1.png) 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 |