From 56c2a56aad0be6216376c0ac21b072b18ec0889c Mon Sep 17 00:00:00 2001 From: Per Kops Date: Fri, 17 May 2024 21:25:22 +0200 Subject: [PATCH 01/21] feat: add initial empty services and GraphServiceClientWrapper --- .../Services/GraphServiceClientWrapper.cs | 5 +++++ .../Services/OneDrive/IOneDriveGraphService.cs | 5 +++++ .../Services/OneDrive/OneDriveGraphService.cs | 5 +++++ .../Services/Outlook/IOutlookGraphService.cs | 5 +++++ .../Services/Outlook/OutlookGraphService.cs | 5 +++++ .../Services/Sharepoint/ISharepointGraphService.cs | 5 +++++ .../Services/Sharepoint/SharepointGraphService.cs | 5 +++++ .../Services/Teams/ITeamsGraphService.cs | 5 +++++ .../Services/Teams/TeamsGraphService.cs | 5 +++++ 9 files changed, 45 insertions(+) create mode 100644 src/Atc.Microsoft.Graph.Client/Services/GraphServiceClientWrapper.cs create mode 100644 src/Atc.Microsoft.Graph.Client/Services/OneDrive/IOneDriveGraphService.cs create mode 100644 src/Atc.Microsoft.Graph.Client/Services/OneDrive/OneDriveGraphService.cs create mode 100644 src/Atc.Microsoft.Graph.Client/Services/Outlook/IOutlookGraphService.cs create mode 100644 src/Atc.Microsoft.Graph.Client/Services/Outlook/OutlookGraphService.cs create mode 100644 src/Atc.Microsoft.Graph.Client/Services/Sharepoint/ISharepointGraphService.cs create mode 100644 src/Atc.Microsoft.Graph.Client/Services/Sharepoint/SharepointGraphService.cs create mode 100644 src/Atc.Microsoft.Graph.Client/Services/Teams/ITeamsGraphService.cs create mode 100644 src/Atc.Microsoft.Graph.Client/Services/Teams/TeamsGraphService.cs diff --git a/src/Atc.Microsoft.Graph.Client/Services/GraphServiceClientWrapper.cs b/src/Atc.Microsoft.Graph.Client/Services/GraphServiceClientWrapper.cs new file mode 100644 index 0000000..17fee5e --- /dev/null +++ b/src/Atc.Microsoft.Graph.Client/Services/GraphServiceClientWrapper.cs @@ -0,0 +1,5 @@ +namespace Atc.Microsoft.Graph.Client.Services; + +public abstract class GraphServiceClientWrapper +{ +} \ No newline at end of file diff --git a/src/Atc.Microsoft.Graph.Client/Services/OneDrive/IOneDriveGraphService.cs b/src/Atc.Microsoft.Graph.Client/Services/OneDrive/IOneDriveGraphService.cs new file mode 100644 index 0000000..52f48ec --- /dev/null +++ b/src/Atc.Microsoft.Graph.Client/Services/OneDrive/IOneDriveGraphService.cs @@ -0,0 +1,5 @@ +namespace Atc.Microsoft.Graph.Client.Services.OneDrive; + +public interface IOneDriveGraphService +{ +} \ No newline at end of file diff --git a/src/Atc.Microsoft.Graph.Client/Services/OneDrive/OneDriveGraphService.cs b/src/Atc.Microsoft.Graph.Client/Services/OneDrive/OneDriveGraphService.cs new file mode 100644 index 0000000..90d90ec --- /dev/null +++ b/src/Atc.Microsoft.Graph.Client/Services/OneDrive/OneDriveGraphService.cs @@ -0,0 +1,5 @@ +namespace Atc.Microsoft.Graph.Client.Services.OneDrive; + +public sealed class OneDriveGraphService : GraphServiceClientWrapper, IOneDriveGraphService +{ +} \ No newline at end of file diff --git a/src/Atc.Microsoft.Graph.Client/Services/Outlook/IOutlookGraphService.cs b/src/Atc.Microsoft.Graph.Client/Services/Outlook/IOutlookGraphService.cs new file mode 100644 index 0000000..78a98b7 --- /dev/null +++ b/src/Atc.Microsoft.Graph.Client/Services/Outlook/IOutlookGraphService.cs @@ -0,0 +1,5 @@ +namespace Atc.Microsoft.Graph.Client.Services.Outlook; + +public interface IOutlookGraphService +{ +} \ No newline at end of file diff --git a/src/Atc.Microsoft.Graph.Client/Services/Outlook/OutlookGraphService.cs b/src/Atc.Microsoft.Graph.Client/Services/Outlook/OutlookGraphService.cs new file mode 100644 index 0000000..be39a09 --- /dev/null +++ b/src/Atc.Microsoft.Graph.Client/Services/Outlook/OutlookGraphService.cs @@ -0,0 +1,5 @@ +namespace Atc.Microsoft.Graph.Client.Services.Outlook; + +public sealed class OutlookGraphService : GraphServiceClientWrapper, IOutlookGraphService +{ +} \ No newline at end of file diff --git a/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/ISharepointGraphService.cs b/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/ISharepointGraphService.cs new file mode 100644 index 0000000..da4634e --- /dev/null +++ b/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/ISharepointGraphService.cs @@ -0,0 +1,5 @@ +namespace Atc.Microsoft.Graph.Client.Services.Sharepoint; + +public interface ISharepointGraphService +{ +} \ No newline at end of file diff --git a/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/SharepointGraphService.cs b/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/SharepointGraphService.cs new file mode 100644 index 0000000..f64aed0 --- /dev/null +++ b/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/SharepointGraphService.cs @@ -0,0 +1,5 @@ +namespace Atc.Microsoft.Graph.Client.Services.Sharepoint; + +public sealed class SharepointGraphService : GraphServiceClientWrapper, ISharepointGraphService +{ +} \ No newline at end of file diff --git a/src/Atc.Microsoft.Graph.Client/Services/Teams/ITeamsGraphService.cs b/src/Atc.Microsoft.Graph.Client/Services/Teams/ITeamsGraphService.cs new file mode 100644 index 0000000..4b9350a --- /dev/null +++ b/src/Atc.Microsoft.Graph.Client/Services/Teams/ITeamsGraphService.cs @@ -0,0 +1,5 @@ +namespace Atc.Microsoft.Graph.Client.Services.Teams; + +public interface ITeamsGraphService +{ +} \ No newline at end of file diff --git a/src/Atc.Microsoft.Graph.Client/Services/Teams/TeamsGraphService.cs b/src/Atc.Microsoft.Graph.Client/Services/Teams/TeamsGraphService.cs new file mode 100644 index 0000000..7b927a8 --- /dev/null +++ b/src/Atc.Microsoft.Graph.Client/Services/Teams/TeamsGraphService.cs @@ -0,0 +1,5 @@ +namespace Atc.Microsoft.Graph.Client.Services.Teams; + +public sealed class TeamsGraphService : GraphServiceClientWrapper, ITeamsGraphService +{ +} \ No newline at end of file From dfa0aa902677f252c7deb12c131af7763a943669 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Fri, 17 May 2024 22:27:36 +0200 Subject: [PATCH 02/21] feat: add nuget packages for Microsoft.Graph, Atc, Logging and Resilience --- .../Atc.Microsoft.Graph.Client.csproj | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Atc.Microsoft.Graph.Client/Atc.Microsoft.Graph.Client.csproj b/src/Atc.Microsoft.Graph.Client/Atc.Microsoft.Graph.Client.csproj index 483e0bd..96a5248 100644 --- a/src/Atc.Microsoft.Graph.Client/Atc.Microsoft.Graph.Client.csproj +++ b/src/Atc.Microsoft.Graph.Client/Atc.Microsoft.Graph.Client.csproj @@ -12,4 +12,11 @@ + + + + + + + From 849a4e43025ed35b16425f0a84c8d592350c1cc4 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Fri, 17 May 2024 22:27:50 +0200 Subject: [PATCH 03/21] build: suppress a few rules in root .editorconfig --- .editorconfig | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index b1b0306..c39e8a1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -519,4 +519,17 @@ dotnet_diagnostic.S6605.severity = none # Collection-specific "Exist ########################################## # Custom - Code Analyzers Rules ########################################## -[*.{cs,csx,cake}] \ No newline at end of file +[*.{cs,csx,cake}] + +dotnet_diagnostic.SA1010.severity = none # +dotnet_diagnostic.SA1402.severity = none # +dotnet_diagnostic.SA1615.severity = none # + +dotnet_diagnostic.CA1002.severity = none # +dotnet_diagnostic.CA1031.severity = none # +dotnet_diagnostic.CA2007.severity = none # Consider calling ConfigureAwait on the awaited task + +dotnet_diagnostic.MA0004.severity = none # Use Task.ConfigureAwait(false) as the current SynchronizationContext is not needed +dotnet_diagnostic.MA0016.severity = none # Prefer returning collection abstraction instead of implementation + +dotnet_diagnostic.S3267.severity = none # \ No newline at end of file From 545b15e952a2cffefa224866e9b0b5ee8c398f1d Mon Sep 17 00:00:00 2001 From: Per Kops Date: Fri, 17 May 2024 22:28:06 +0200 Subject: [PATCH 04/21] feat: Introduce MicrosoftGraphConstants --- src/Atc.Microsoft.Graph.Client/MicrosoftGraphConstants.cs | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/Atc.Microsoft.Graph.Client/MicrosoftGraphConstants.cs diff --git a/src/Atc.Microsoft.Graph.Client/MicrosoftGraphConstants.cs b/src/Atc.Microsoft.Graph.Client/MicrosoftGraphConstants.cs new file mode 100644 index 0000000..fa3f9d5 --- /dev/null +++ b/src/Atc.Microsoft.Graph.Client/MicrosoftGraphConstants.cs @@ -0,0 +1,6 @@ +namespace Atc.Microsoft.Graph.Client; + +public static class MicrosoftGraphConstants +{ + public const int RetryWaitDelayInMs = 500; +} \ No newline at end of file From b82420d12406a36d073786ab114b2c0ce9ebb3cc Mon Sep 17 00:00:00 2001 From: Per Kops Date: Fri, 17 May 2024 22:34:25 +0200 Subject: [PATCH 05/21] build: configuring MA0051 maximum_lines_per_method in root .editorconfig --- .editorconfig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.editorconfig b/.editorconfig index c39e8a1..9994d68 100644 --- a/.editorconfig +++ b/.editorconfig @@ -521,6 +521,8 @@ dotnet_diagnostic.S6605.severity = none # Collection-specific "Exist ########################################## [*.{cs,csx,cake}] +MA0051.maximum_lines_per_method = 100 + dotnet_diagnostic.SA1010.severity = none # dotnet_diagnostic.SA1402.severity = none # dotnet_diagnostic.SA1615.severity = none # From 2e669301324052d1a591940de9e576fc29fc3e58 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Fri, 17 May 2024 22:34:44 +0200 Subject: [PATCH 06/21] feat: working on initial services and base class --- .../GlobalUsings.cs | 10 + .../Services/GraphServiceClientWrapper.cs | 42 +- ...GraphServiceClientWrapperLoggerMessages.cs | 87 +++ .../OneDrive/IOneDriveGraphService.cs | 33 ++ .../Services/OneDrive/OneDriveGraphService.cs | 358 ++++++++++++ .../Services/Outlook/IOutlookGraphService.cs | 38 ++ .../Services/Outlook/OutlookGraphService.cs | 534 ++++++++++++++++++ .../Sharepoint/ISharepointGraphService.cs | 5 + .../Sharepoint/SharepointGraphService.cs | 82 +++ .../Services/Teams/ITeamsGraphService.cs | 5 + .../Services/Teams/TeamsGraphService.cs | 82 +++ .../Services/Users/IUsersGraphService.cs | 10 + .../Services/Users/UsersGraphService.cs | 87 +++ 13 files changed, 1372 insertions(+), 1 deletion(-) create mode 100644 src/Atc.Microsoft.Graph.Client/GlobalUsings.cs create mode 100644 src/Atc.Microsoft.Graph.Client/Services/GraphServiceClientWrapperLoggerMessages.cs create mode 100644 src/Atc.Microsoft.Graph.Client/Services/Users/IUsersGraphService.cs create mode 100644 src/Atc.Microsoft.Graph.Client/Services/Users/UsersGraphService.cs diff --git a/src/Atc.Microsoft.Graph.Client/GlobalUsings.cs b/src/Atc.Microsoft.Graph.Client/GlobalUsings.cs new file mode 100644 index 0000000..50c5f3d --- /dev/null +++ b/src/Atc.Microsoft.Graph.Client/GlobalUsings.cs @@ -0,0 +1,10 @@ +global using System.Diagnostics.CodeAnalysis; +global using System.Net; +global using System.Runtime.CompilerServices; +global using Microsoft.Extensions.Logging; +global using Microsoft.Graph; +global using Microsoft.Graph.Models; +global using Microsoft.Graph.Models.ODataErrors; +global using Microsoft.Kiota.Abstractions; +global using Polly; +global using Polly.Retry; \ No newline at end of file diff --git a/src/Atc.Microsoft.Graph.Client/Services/GraphServiceClientWrapper.cs b/src/Atc.Microsoft.Graph.Client/Services/GraphServiceClientWrapper.cs index 17fee5e..344f3c2 100644 --- a/src/Atc.Microsoft.Graph.Client/Services/GraphServiceClientWrapper.cs +++ b/src/Atc.Microsoft.Graph.Client/Services/GraphServiceClientWrapper.cs @@ -1,5 +1,45 @@ namespace Atc.Microsoft.Graph.Client.Services; -public abstract class GraphServiceClientWrapper +public abstract partial class GraphServiceClientWrapper { + protected GraphServiceClient Client { get; } + + protected ResiliencePipeline DownloadResiliencePipeline { get; } + + public GraphServiceClientWrapper( + ILoggerFactory loggerFactory, + GraphServiceClient client) + { + this.logger = loggerFactory.CreateLogger(); + this.Client = client; + + var retryStrategyOptions = new RetryStrategyOptions + { + ShouldHandle = new PredicateBuilder() + .Handle() + .Handle(), + BackoffType = DelayBackoffType.Exponential, + UseJitter = true, + MaxRetryAttempts = 3, + Delay = TimeSpan.FromSeconds(3), + OnRetry = args => + { + var errorMessage = args.Outcome.Result switch + { + ODataError oDataError => oDataError.Error?.Message, + Exception ex => ex.GetLastInnerMessage(), + _ => null, + }; + + errorMessage ??= args.Outcome.Exception?.GetLastInnerMessage() ?? "Unknown Exception"; + + LogDownloadFileRetrying(errorMessage); + return ValueTask.CompletedTask; + }, + }; + + DownloadResiliencePipeline = new ResiliencePipelineBuilder() + .AddRetry(retryStrategyOptions) + .Build(); + } } \ No newline at end of file diff --git a/src/Atc.Microsoft.Graph.Client/Services/GraphServiceClientWrapperLoggerMessages.cs b/src/Atc.Microsoft.Graph.Client/Services/GraphServiceClientWrapperLoggerMessages.cs new file mode 100644 index 0000000..3be8dd5 --- /dev/null +++ b/src/Atc.Microsoft.Graph.Client/Services/GraphServiceClientWrapperLoggerMessages.cs @@ -0,0 +1,87 @@ +namespace Atc.Microsoft.Graph.Client.Services; + +/// +/// GraphServiceClientWrapper LoggerMessages. +/// +[SuppressMessage("Design", "MA0048:File name must match type name", Justification = "OK - By Design")] +public abstract partial class GraphServiceClientWrapper +{ + private readonly ILogger logger; + + [LoggerMessage( + EventId = LoggingEventIdConstants.GraphServiceClientWrapper.GetFailure, + Level = LogLevel.Error, + Message = "{callerMethodName}({callerLineNumber}) - Failed to retrieve data: '{errorMessage}'.")] + protected partial void LogGetFailure( + string? errorMessage, + [CallerMemberName] string callerMethodName = "", + [CallerLineNumber] int callerLineNumber = 0); + + [LoggerMessage( + EventId = LoggingEventIdConstants.GraphServiceClientWrapper.DownloadFileFailed, + Level = LogLevel.Error, + Message = "{callerMethodName}({callerLineNumber}) - Failed to download file with id: '{fileId}': '{errorMessage}'.")] + protected partial void LogDownloadFileFailed( + string fileId, + string? errorMessage, + [CallerMemberName] string callerMethodName = "", + [CallerLineNumber] int callerLineNumber = 0); + + [LoggerMessage( + EventId = LoggingEventIdConstants.GraphServiceClientWrapper.DownloadFileRetrying, + Level = LogLevel.Warning, + Message = "{callerMethodName}({callerLineNumber}) - Retrying download of file: '{errorMessage}'.")] + protected partial void LogDownloadFileRetrying( + string? errorMessage, + [CallerMemberName] string callerMethodName = "", + [CallerLineNumber] int callerLineNumber = 0); + + [LoggerMessage( + EventId = LoggingEventIdConstants.GraphServiceClientWrapper.DownloadFileEmpty, + Level = LogLevel.Warning, + Message = "{callerMethodName}({callerLineNumber}) - File to download is empty - id: '{fileId}'.")] + protected partial void LogDownloadFileEmpty( + string fileId, + [CallerMemberName] string callerMethodName = "", + [CallerLineNumber] int callerLineNumber = 0); + + [LoggerMessage( + EventId = LoggingEventIdConstants.GraphServiceClientWrapper.DeltaLinkNotFoundForDrive, + Level = LogLevel.Warning, + Message = "{callerMethodName}({callerLineNumber}) - Could not find Delta Link for drive with id: '{driveId}': '{errorMessage}'.")] + protected partial void LogDeltaLinkNotFoundForDrive( + string driveId, + string? errorMessage, + [CallerMemberName] string callerMethodName = "", + [CallerLineNumber] int callerLineNumber = 0); + + [LoggerMessage( + EventId = LoggingEventIdConstants.GraphServiceClientWrapper.DriveNotFoundForTeam, + Level = LogLevel.Warning, + Message = "{callerMethodName}({callerLineNumber}) - Could not find drive for team with id: '{teamId}': '{errorMessage}'.")] + protected partial void LogDriveNotFoundForTeam( + string teamId, + string? errorMessage, + [CallerMemberName] string callerMethodName = "", + [CallerLineNumber] int callerLineNumber = 0); + + [LoggerMessage( + EventId = LoggingEventIdConstants.GraphServiceClientWrapper.PageIteratorCount, + Level = LogLevel.Debug, + Message = "{callerMethodName}({callerLineNumber}) - {area} Iterator processed {count} items.")] + protected partial void LogPageIteratorCount( + string area, + int count, + [CallerMemberName] string callerMethodName = "", + [CallerLineNumber] int callerLineNumber = 0); + + [LoggerMessage( + EventId = LoggingEventIdConstants.GraphServiceClientWrapper.PageIteratorTotalCount, + Level = LogLevel.Debug, + Message = "{callerMethodName}({callerLineNumber}) - {area} Iterator processed a total of {count} items.")] + protected partial void LogPageIteratorTotalCount( + string area, + int count, + [CallerMemberName] string callerMethodName = "", + [CallerLineNumber] int callerLineNumber = 0); +} \ No newline at end of file diff --git a/src/Atc.Microsoft.Graph.Client/Services/OneDrive/IOneDriveGraphService.cs b/src/Atc.Microsoft.Graph.Client/Services/OneDrive/IOneDriveGraphService.cs index 52f48ec..f9591fb 100644 --- a/src/Atc.Microsoft.Graph.Client/Services/OneDrive/IOneDriveGraphService.cs +++ b/src/Atc.Microsoft.Graph.Client/Services/OneDrive/IOneDriveGraphService.cs @@ -2,4 +2,37 @@ namespace Atc.Microsoft.Graph.Client.Services.OneDrive; public interface IOneDriveGraphService { + Task<(HttpStatusCode StatusCode, IList Data)> GetDrivesBySiteId( + Guid siteId, + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters, + CancellationToken cancellationToken); + + Task GetDriveByTeamId( + string teamId, + CancellationToken cancellationToken); + + Task GetDeltaTokenForDriveItemsByDriveId( + string driveId, + CancellationToken cancellationToken); + + Task<(HttpStatusCode StatusCode, IList Data)> GetDriveItemsByDriveId( + string driveId, + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters, + CancellationToken cancellationToken); + + Task<(HttpStatusCode StatusCode, IList Data)> GetDriveItemsByDriveIdAndDeltaToken( + string driveId, + string deltaToken, + string? filterQueryParameter, + List? selectQueryParameters, + CancellationToken cancellationToken); + + Task DownloadFile( + string driveId, + string fileId, + CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Atc.Microsoft.Graph.Client/Services/OneDrive/OneDriveGraphService.cs b/src/Atc.Microsoft.Graph.Client/Services/OneDrive/OneDriveGraphService.cs index 90d90ec..7759e62 100644 --- a/src/Atc.Microsoft.Graph.Client/Services/OneDrive/OneDriveGraphService.cs +++ b/src/Atc.Microsoft.Graph.Client/Services/OneDrive/OneDriveGraphService.cs @@ -2,4 +2,362 @@ namespace Atc.Microsoft.Graph.Client.Services.OneDrive; public sealed class OneDriveGraphService : GraphServiceClientWrapper, IOneDriveGraphService { + public OneDriveGraphService( + ILoggerFactory loggerFactory, + GraphServiceClient client) + : base(loggerFactory, client) + { + } + + public async Task<(HttpStatusCode StatusCode, IList Data)> GetDrivesBySiteId( + Guid siteId, + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters, + CancellationToken cancellationToken) + { + List pagedItems = []; + var count = 0; + + try + { + var requestInformation = Client + .Sites[siteId.ToString()] + .Drives + .ToGetRequestInformation( + RequestConfigurationFactory.CreateForDrives( + expandQueryParameters, + filterQueryParameter, + selectQueryParameters)); + + var response = await Client.RequestAdapter.SendAsync( + requestInformation, + (_) => new DriveCollectionResponse(), + cancellationToken: cancellationToken); + + if (response is null) + { + return (HttpStatusCode.InternalServerError, pagedItems); + } + + var pageIterator = PageIterator.CreatePageIterator( + Client, + response, + item => + { + pagedItems.Add(item); + + count++; + if (count % 1000 == 0) + { + LogPageIteratorCount(nameof(Drive), count); + } + + return true; + }); + + try + { + await pageIterator.IterateAsync(cancellationToken); + } + catch (ODataError odataError) when (odataError.ResponseStatusCode == (int)HttpStatusCode.TooManyRequests) + { + await Task.Delay(MicrosoftGraphConstants.RetryWaitDelayInMs, cancellationToken); + + await pageIterator.IterateAsync(cancellationToken); + } + catch (ODataError odataError) when (odataError.ResponseStatusCode == (int)HttpStatusCode.Gone) + { + return (HttpStatusCode.Gone, pagedItems); + } + + LogPageIteratorTotalCount(nameof(Drive), count); + + return (HttpStatusCode.OK, pagedItems); + } + catch (ODataError odataError) + { + LogGetFailure(odataError.Error?.Message); + return (HttpStatusCode.InternalServerError, pagedItems); + } + catch (Exception ex) + { + LogGetFailure(ex.GetLastInnerMessage()); + return (HttpStatusCode.InternalServerError, pagedItems); + } + } + + public async Task GetDriveByTeamId( + string teamId, + CancellationToken cancellationToken) + { + try + { + var drive = await Client + .Groups[teamId] + .Drive + .GetAsync(cancellationToken: cancellationToken); + + if (drive is not null) + { + return drive; + } + + LogDriveNotFoundForTeam(teamId, errorMessage: null); + return null; + } + catch (ODataError odataError) + { + LogDriveNotFoundForTeam(teamId, odataError.Error?.Message); + return null; + } + catch (Exception ex) + { + LogGetFailure(ex.GetLastInnerMessage()); + return null; + } + } + + public async Task GetDeltaTokenForDriveItemsByDriveId( + string driveId, + CancellationToken cancellationToken) + { + try + { + var deltaWithTokenResponse = await Client + .Drives[driveId] + .Items["root"] + .DeltaWithToken("latest") + .GetAsDeltaWithTokenGetResponseAsync(cancellationToken: cancellationToken); + + if (deltaWithTokenResponse?.OdataDeltaLink is null) + { + LogDeltaLinkNotFoundForDrive(driveId, errorMessage: null); + return null; + } + + var sa = deltaWithTokenResponse.OdataDeltaLink.Split("token='", StringSplitOptions.RemoveEmptyEntries); + if (sa.Length == 2) + { + return sa[1].Replace("')", string.Empty, StringComparison.OrdinalIgnoreCase); + } + + LogDeltaLinkNotFoundForDrive(driveId, errorMessage: null); + return null; + } + catch (ODataError odataError) + { + LogDeltaLinkNotFoundForDrive(driveId, odataError.Error?.Message); + return null; + } + catch (Exception ex) + { + LogGetFailure(ex.GetLastInnerMessage()); + return null; + } + } + + public async Task<(HttpStatusCode StatusCode, IList Data)> GetDriveItemsByDriveId( + string driveId, + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters, + CancellationToken cancellationToken) + { + var requestInformation = Client + .Drives[driveId] + .List + .Items + .ToGetRequestInformation( + RequestConfigurationFactory.CreateForItems( + expandQueryParameters, + filterQueryParameter, + selectQueryParameters)); + + var (httpStatusCode, data) = await GetAllListItemsByDriveId(requestInformation, cancellationToken); + + var driveItems = data + .Where(x => x.DriveItem is not null) + .Select(x => x.DriveItem!) + .ToList(); + + return (httpStatusCode, driveItems); + } + + public async Task<(HttpStatusCode StatusCode, IList Data)> GetDriveItemsByDriveIdAndDeltaToken( + string driveId, + string deltaToken, + string? filterQueryParameter, + List? selectQueryParameters, + CancellationToken cancellationToken) + { + List pagedItems = []; + var count = 0; + + try + { + var requestInformation = Client + .Drives[driveId] + .Items["root"] + .DeltaWithToken(deltaToken) + .ToGetRequestInformation( + RequestConfigurationFactory.CreateForItemsWithDelta( + filterQueryParameter, + selectQueryParameters)); + + var response = await Client.RequestAdapter.SendAsync( + requestInformation, + (_) => new DriveItemCollectionResponse(), + cancellationToken: cancellationToken); + + if (response is null) + { + return (HttpStatusCode.InternalServerError, pagedItems); + } + + var pageIterator = PageIterator.CreatePageIterator( + Client, + response, + item => + { + pagedItems.Add(item); + + count++; + if (count % 1000 == 0) + { + LogPageIteratorCount(nameof(DriveItem), count); + } + + return true; + }); + + try + { + await pageIterator.IterateAsync(cancellationToken); + } + catch (ODataError odataError) when (odataError.ResponseStatusCode == (int)HttpStatusCode.TooManyRequests) + { + await Task.Delay(MicrosoftGraphConstants.RetryWaitDelayInMs, cancellationToken); + + await pageIterator.IterateAsync(cancellationToken); + } + catch (ODataError odataError) when (odataError.ResponseStatusCode == (int)HttpStatusCode.Gone) + { + return (HttpStatusCode.Gone, pagedItems); + } + + LogPageIteratorTotalCount(nameof(DriveItem), count); + + return (HttpStatusCode.OK, pagedItems); + } + catch (ODataError odataError) + { + LogGetFailure(odataError.Error?.Message); + return (HttpStatusCode.InternalServerError, pagedItems); + } + catch (Exception ex) + { + LogGetFailure(ex.GetLastInnerMessage()); + return (HttpStatusCode.InternalServerError, pagedItems); + } + } + + public async Task DownloadFile( + string driveId, + string fileId, + CancellationToken cancellationToken) + { + try + { + return await DownloadResiliencePipeline.ExecuteAsync( + async context => + { + var stream = await Client + .Drives[driveId] + .Items[fileId] + .Content + .GetAsync(cancellationToken: context); + + if (stream is null) + { + LogDownloadFileEmpty(fileId); + } + + return stream; + }, + cancellationToken); + } + catch (Exception ex) + { + LogDownloadFileFailed(fileId, ex.GetLastInnerMessage()); + return null; + } + } + + private async Task<(HttpStatusCode StatusCode, IList Data)> GetAllListItemsByDriveId( + RequestInformation requestInformation, + CancellationToken cancellationToken) + { + List pagedItems = []; + var count = 0; + + try + { + var response = await Client.RequestAdapter.SendAsync( + requestInformation, + (_) => new ListItemCollectionResponse(), + cancellationToken: cancellationToken); + + if (response is null) + { + return (HttpStatusCode.InternalServerError, pagedItems); + } + + var pageIterator = PageIterator.CreatePageIterator( + Client, + response, + item => + { + pagedItems.Add(item); + + count++; + if (count % 1000 == 0) + { + LogPageIteratorCount(nameof(ListItem), count); + } + + return true; + }); + + try + { + await pageIterator.IterateAsync(cancellationToken); + } + catch (ODataError odataError) when (odataError.ResponseStatusCode == (int)HttpStatusCode.TooManyRequests) + { + await Task.Delay(MicrosoftGraphConstants.RetryWaitDelayInMs, cancellationToken); + + await pageIterator.IterateAsync(cancellationToken); + } + catch (ODataError odataError) when (odataError.ResponseStatusCode == (int)HttpStatusCode.Gone) + { + return (HttpStatusCode.Gone, pagedItems); + } + + LogPageIteratorTotalCount(nameof(ListItem), count); + + return (HttpStatusCode.OK, pagedItems); + } + catch (ODataError odataError) + { + LogGetFailure(odataError.Error?.Message); + return (HttpStatusCode.InternalServerError, pagedItems); + } + catch (Exception ex) + { + var errorMessage = ex.GetLastInnerMessage(); + LogGetFailure(errorMessage); + return (HttpStatusCode.InternalServerError, pagedItems); + } + } } \ No newline at end of file diff --git a/src/Atc.Microsoft.Graph.Client/Services/Outlook/IOutlookGraphService.cs b/src/Atc.Microsoft.Graph.Client/Services/Outlook/IOutlookGraphService.cs index 78a98b7..5637551 100644 --- a/src/Atc.Microsoft.Graph.Client/Services/Outlook/IOutlookGraphService.cs +++ b/src/Atc.Microsoft.Graph.Client/Services/Outlook/IOutlookGraphService.cs @@ -2,4 +2,42 @@ namespace Atc.Microsoft.Graph.Client.Services.Outlook; public interface IOutlookGraphService { + Task<(HttpStatusCode StatusCode, IList Data)> GetRootMailFoldersByUserId( + string userId, + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters, + CancellationToken cancellationToken); + + Task<(HttpStatusCode StatusCode, IList Data)> GetMailFoldersByUserIdAndFolderId( + string userId, + string folderId, + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters, + CancellationToken cancellationToken); + + Task<(HttpStatusCode StatusCode, IList Data)> GetMessagesByUserId( + string userId, + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters, + CancellationToken cancellationToken); + + Task<(HttpStatusCode StatusCode, IList Data, string? DeltaToken)> GetMessagesByUserIdAndFolderId( + string userId, + string folderId, + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters, + CancellationToken cancellationToken, + string? deltaToken = null); + + Task<(HttpStatusCode StatusCode, IList Data)> GetFileAttachmentsByUserIdAndMessageId( + string userId, + string messageId, + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters, + CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Atc.Microsoft.Graph.Client/Services/Outlook/OutlookGraphService.cs b/src/Atc.Microsoft.Graph.Client/Services/Outlook/OutlookGraphService.cs index be39a09..52b326c 100644 --- a/src/Atc.Microsoft.Graph.Client/Services/Outlook/OutlookGraphService.cs +++ b/src/Atc.Microsoft.Graph.Client/Services/Outlook/OutlookGraphService.cs @@ -2,4 +2,538 @@ namespace Atc.Microsoft.Graph.Client.Services.Outlook; public sealed class OutlookGraphService : GraphServiceClientWrapper, IOutlookGraphService { + public OutlookGraphService( + ILoggerFactory loggerFactory, + GraphServiceClient client) + : base(loggerFactory, client) + { + } + + public async Task<(HttpStatusCode StatusCode, IList Data)> GetRootMailFoldersByUserId( + string userId, + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters, + CancellationToken cancellationToken) + { + List pagedItems = []; + var count = 0; + + try + { + var requestInformation = Client + .Users[userId] + .MailFolders + .ToGetRequestInformation( + RequestConfigurationFactory.CreateForMailFolders( + expandQueryParameters, + filterQueryParameter, + selectQueryParameters)); + + var response = await Client.RequestAdapter.SendAsync( + requestInformation, + (_) => new MailFolderCollectionResponse(), + cancellationToken: cancellationToken); + + if (response is null) + { + return (HttpStatusCode.InternalServerError, pagedItems); + } + + var pageIterator = PageIterator.CreatePageIterator( + Client, + response, + item => + { + pagedItems.Add(item); + + count++; + if (count % 1000 == 0) + { + LogPageIteratorCount(nameof(MailFolder), count); + } + + return true; + }); + + try + { + await pageIterator.IterateAsync(cancellationToken); + } + catch (ODataError odataError) when (odataError.ResponseStatusCode == (int)HttpStatusCode.TooManyRequests) + { + await Task.Delay(MicrosoftGraphConstants.RetryWaitDelayInMs, cancellationToken); + + await pageIterator.IterateAsync(cancellationToken); + } + catch (ODataError odataError) when (odataError.ResponseStatusCode == (int)HttpStatusCode.Gone) + { + return (HttpStatusCode.Gone, pagedItems); + } + + LogPageIteratorTotalCount(nameof(MailFolder), count); + + return (HttpStatusCode.OK, pagedItems); + } + catch (ODataError odataError) + { + LogGetFailure(odataError.Error?.Message); + return (HttpStatusCode.InternalServerError, pagedItems); + } + catch (Exception ex) + { + LogGetFailure(ex.GetLastInnerMessage()); + return (HttpStatusCode.InternalServerError, pagedItems); + } + } + + public async Task<(HttpStatusCode StatusCode, IList Data)> GetMailFoldersByUserIdAndFolderId( + string userId, + string folderId, + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters, + CancellationToken cancellationToken) + { + List pagedItems = []; + var count = 0; + + try + { + var requestInformation = Client + .Users[userId] + .MailFolders[folderId] + .ChildFolders + .ToGetRequestInformation( + RequestConfigurationFactory.CreateForChildFolders( + expandQueryParameters, + filterQueryParameter, + selectQueryParameters)); + + var response = await Client.RequestAdapter.SendAsync( + requestInformation, + (_) => new MailFolderCollectionResponse(), + cancellationToken: cancellationToken); + + if (response is null) + { + return (HttpStatusCode.InternalServerError, pagedItems); + } + + var pageIterator = PageIterator.CreatePageIterator( + Client, + response, + item => + { + pagedItems.Add(item); + + count++; + if (count % 1000 == 0) + { + LogPageIteratorCount(nameof(MailFolder), count); + } + + return true; + }); + + try + { + await pageIterator.IterateAsync(cancellationToken); + } + catch (ODataError odataError) when (odataError.ResponseStatusCode == (int)HttpStatusCode.TooManyRequests) + { + await Task.Delay(MicrosoftGraphConstants.RetryWaitDelayInMs, cancellationToken); + + await pageIterator.IterateAsync(cancellationToken); + } + catch (ODataError odataError) when (odataError.ResponseStatusCode == (int)HttpStatusCode.Gone) + { + return (HttpStatusCode.Gone, pagedItems); + } + + LogPageIteratorTotalCount(nameof(MailFolder), count); + + return (HttpStatusCode.OK, pagedItems); + } + catch (ODataError odataError) + { + LogGetFailure(odataError.Error?.Message); + return (HttpStatusCode.InternalServerError, pagedItems); + } + catch (Exception ex) + { + LogGetFailure(ex.GetLastInnerMessage()); + return (HttpStatusCode.InternalServerError, pagedItems); + } + } + + public async Task<(HttpStatusCode StatusCode, IList Data)> GetMessagesByUserId( + string userId, + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters, + CancellationToken cancellationToken) + { + List pagedItems = []; + var count = 0; + + try + { + var requestInformation = Client + .Users[userId] + .Messages + .ToGetRequestInformation( + RequestConfigurationFactory.CreateForMessagesUserId( + expandQueryParameters, + filterQueryParameter, + selectQueryParameters)); + + var response = await Client.RequestAdapter.SendAsync( + requestInformation, + (_) => new MessageCollectionResponse(), + cancellationToken: cancellationToken); + + if (response is null) + { + return (HttpStatusCode.InternalServerError, pagedItems); + } + + var pageIterator = PageIterator.CreatePageIterator( + Client, + response, + item => + { + pagedItems.Add(item); + + count++; + if (count % 1000 == 0) + { + LogPageIteratorCount(nameof(Message), count); + } + + return true; + }); + + try + { + await pageIterator.IterateAsync(cancellationToken); + } + catch (ODataError odataError) when (odataError.ResponseStatusCode == (int)HttpStatusCode.TooManyRequests) + { + await Task.Delay(MicrosoftGraphConstants.RetryWaitDelayInMs, cancellationToken); + + await pageIterator.IterateAsync(cancellationToken); + } + catch (ODataError odataError) when (odataError.ResponseStatusCode == (int)HttpStatusCode.Gone) + { + return (HttpStatusCode.Gone, pagedItems); + } + + LogPageIteratorTotalCount(nameof(Message), count); + + return (HttpStatusCode.OK, pagedItems); + } + catch (ODataError odataError) + { + LogGetFailure(odataError.Error?.Message); + return (HttpStatusCode.InternalServerError, pagedItems); + } + catch (Exception ex) + { + LogGetFailure(ex.GetLastInnerMessage()); + return (HttpStatusCode.InternalServerError, pagedItems); + } + } + + public async Task<(HttpStatusCode StatusCode, IList Data, string? DeltaToken)> GetMessagesByUserIdAndFolderId( + string userId, + string folderId, + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters, + CancellationToken cancellationToken, + string? deltaToken = null) + { + List pagedItems = []; + + try + { + return string.IsNullOrEmpty(deltaToken) + ? await GetMessagesByUserIdAndFolderIdWithoutDeltaToken( + userId, + folderId, + filterQueryParameter, + selectQueryParameters, + cancellationToken) + : await GetMessagesByUserIdAndFolderIdWithDeltaToken( + userId, + folderId, + deltaToken, + expandQueryParameters, + filterQueryParameter, + selectQueryParameters, + cancellationToken); + } + catch (ODataError odataError) + { + LogGetFailure(odataError.Error?.Message); + return (HttpStatusCode.InternalServerError, pagedItems, null); + } + catch (Exception ex) + { + LogGetFailure(ex.GetLastInnerMessage()); + return (HttpStatusCode.InternalServerError, pagedItems, null); + } + } + + public async Task<(HttpStatusCode StatusCode, IList Data)> GetFileAttachmentsByUserIdAndMessageId( + string userId, + string messageId, + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters, + CancellationToken cancellationToken) + { + List pagedItems = []; + var count = 0; + + try + { + var requestInformation = Client + .Users[userId] + .Messages[messageId] + .Attachments + .ToGetRequestInformation( + RequestConfigurationFactory.CreateForAttachments( + expandQueryParameters, + filterQueryParameter, + selectQueryParameters)); + + var response = await Client.RequestAdapter.SendAsync( + requestInformation, + (_) => new AttachmentCollectionResponse(), + cancellationToken: cancellationToken); + + if (response is null) + { + return (HttpStatusCode.InternalServerError, pagedItems); + } + + var pageIterator = PageIterator.CreatePageIterator( + Client, + response, + item => + { + if (item is FileAttachment attachment) + { + pagedItems.Add(attachment); + } + + count++; + if (count % 1000 == 0) + { + LogPageIteratorCount(nameof(FileAttachment), count); + } + + return true; + }); + + try + { + await pageIterator.IterateAsync(cancellationToken); + } + catch (ODataError odataError) when (odataError.ResponseStatusCode == (int)HttpStatusCode.TooManyRequests) + { + await Task.Delay(MicrosoftGraphConstants.RetryWaitDelayInMs, cancellationToken); + + await pageIterator.IterateAsync(cancellationToken); + } + catch (ODataError odataError) when (odataError.ResponseStatusCode == (int)HttpStatusCode.Gone) + { + return (HttpStatusCode.Gone, pagedItems); + } + + LogPageIteratorTotalCount(nameof(FileAttachment), count); + + return (HttpStatusCode.OK, pagedItems); + } + catch (ODataError odataError) + { + LogGetFailure(odataError.Error?.Message); + return (HttpStatusCode.InternalServerError, pagedItems); + } + catch (Exception ex) + { + LogGetFailure(ex.GetLastInnerMessage()); + return (HttpStatusCode.InternalServerError, pagedItems); + } + } + + private async Task<(HttpStatusCode StatusCode, IList Data, string? DeltaToken)> GetMessagesByUserIdAndFolderIdWithoutDeltaToken( + string userId, + string folderId, + string? filterQueryParameter, + List? selectQueryParameters, + CancellationToken cancellationToken) + { + List pagedItems = []; + var count = 0; + + var requestInformation = Client + .Users[userId] + .MailFolders[folderId] + .Messages + .Delta // To get a delta Link included in the response + .ToGetRequestInformation( + RequestConfigurationFactory.CreateForMessagesDelta( + filterQueryParameter, + selectQueryParameters)); + + var response = await Client.RequestAdapter.SendAsync( + requestInformation, + (_) => new MessageCollectionResponse(), + cancellationToken: cancellationToken); + + if (response is null) + { + return (HttpStatusCode.InternalServerError, pagedItems, null); + } + + var pageIterator = PageIterator.CreatePageIterator( + Client, + response, + item => + { + pagedItems.Add(item); + + count++; + if (count % 1000 == 0) + { + LogPageIteratorCount(nameof(Message), count); + } + + return true; + }); + + try + { + await pageIterator.IterateAsync(cancellationToken); + } + catch (ODataError odataError) when (odataError.ResponseStatusCode == (int)HttpStatusCode.TooManyRequests) + { + await Task.Delay(MicrosoftGraphConstants.RetryWaitDelayInMs, cancellationToken); + + await pageIterator.IterateAsync(cancellationToken); + } + catch (ODataError odataError) when (odataError.ResponseStatusCode == (int)HttpStatusCode.Gone) + { + return (HttpStatusCode.Gone, pagedItems, null); + } + catch (Exception ex) + { + LogGetFailure(ex.GetLastInnerMessage()); + return (HttpStatusCode.InternalServerError, pagedItems, null); + } + + LogPageIteratorTotalCount(nameof(Message), count); + + if (!response.AdditionalData.TryGetValue("@odata.deltaLink", out var deltaLink) || + deltaLink is null) + { + return (HttpStatusCode.InternalServerError, pagedItems, null); + } + + var sa = deltaLink.ToString()!.Split("deltatoken=", StringSplitOptions.RemoveEmptyEntries); + return sa.Length == 2 + ? (HttpStatusCode.OK, pagedItems, sa[1]) + : (HttpStatusCode.InternalServerError, pagedItems, null); + } + + private async Task<(HttpStatusCode StatusCode, IList Data, string? DeltaToken)> GetMessagesByUserIdAndFolderIdWithDeltaToken( + string userId, + string folderId, + string deltaToken, + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters, + CancellationToken cancellationToken) + { + List pagedItems = []; + var count = 0; + + var url = Client + .Users[userId] + .MailFolders[folderId] + .Messages + .ToGetRequestInformation( + RequestConfigurationFactory.CreateForMessagesMailFolder( + expandQueryParameters, + filterQueryParameter, + selectQueryParameters)) + .URI + .ToString(); + + var saUrl = url.Split('?', StringSplitOptions.RemoveEmptyEntries); + url = saUrl.Length == 1 + ? $"{url}/delta?$skipToken={deltaToken}" + : $"{saUrl[0]}/delta?$skipToken={deltaToken}&{saUrl[1]}"; + + var deltaRequestBuilder = new global::Microsoft.Graph.Users.Item.MailFolders.Item.Messages.Delta.DeltaRequestBuilder( + url, + Client.RequestAdapter); + + var response = await deltaRequestBuilder.GetAsDeltaGetResponseAsync(cancellationToken: cancellationToken); + if (response is null) + { + return (HttpStatusCode.InternalServerError, pagedItems, null); + } + + var pageIterator = PageIterator.CreatePageIterator( + Client, + response, + item => + { + pagedItems.Add(item); + + count++; + if (count % 1000 == 0) + { + LogPageIteratorCount(nameof(Message), count); + } + + return true; + }); + + try + { + await pageIterator.IterateAsync(cancellationToken); + } + catch (ODataError odataError) when (odataError.ResponseStatusCode == (int)HttpStatusCode.TooManyRequests) + { + await Task.Delay(MicrosoftGraphConstants.RetryWaitDelayInMs, cancellationToken); + + await pageIterator.IterateAsync(cancellationToken); + } + catch (ODataError odataError) when (odataError.ResponseStatusCode == (int)HttpStatusCode.Gone) + { + return (HttpStatusCode.Gone, pagedItems, null); + } + catch (Exception ex) + { + LogGetFailure(ex.GetLastInnerMessage()); + return (HttpStatusCode.InternalServerError, pagedItems, null); + } + + LogPageIteratorTotalCount(nameof(Message), count); + + if (string.IsNullOrEmpty(response.OdataDeltaLink)) + { + return (HttpStatusCode.InternalServerError, pagedItems, null); + } + + var sa = response.OdataDeltaLink.Split("deltatoken=", StringSplitOptions.RemoveEmptyEntries); + return sa.Length == 2 + ? (HttpStatusCode.OK, pagedItems, sa[1]) + : (HttpStatusCode.InternalServerError, pagedItems, null); + } } \ No newline at end of file diff --git a/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/ISharepointGraphService.cs b/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/ISharepointGraphService.cs index da4634e..efd4e75 100644 --- a/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/ISharepointGraphService.cs +++ b/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/ISharepointGraphService.cs @@ -2,4 +2,9 @@ namespace Atc.Microsoft.Graph.Client.Services.Sharepoint; public interface ISharepointGraphService { + Task<(HttpStatusCode StatusCode, IList Data)> GetSites( + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters, + CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/SharepointGraphService.cs b/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/SharepointGraphService.cs index f64aed0..ca25fcc 100644 --- a/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/SharepointGraphService.cs +++ b/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/SharepointGraphService.cs @@ -2,4 +2,86 @@ namespace Atc.Microsoft.Graph.Client.Services.Sharepoint; public sealed class SharepointGraphService : GraphServiceClientWrapper, ISharepointGraphService { + public SharepointGraphService( + ILoggerFactory loggerFactory, + GraphServiceClient client) + : base(loggerFactory, client) + { + } + + public async Task<(HttpStatusCode StatusCode, IList Data)> GetSites( + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters, + CancellationToken cancellationToken) + { + List pagedItems = []; + var count = 0; + + try + { + var requestInformation = Client + .Sites + .ToGetRequestInformation( + RequestConfigurationFactory.CreateForSites( + expandQueryParameters, + filterQueryParameter, + selectQueryParameters)); + + var response = await Client.RequestAdapter.SendAsync( + requestInformation, + (_) => new SiteCollectionResponse(), + cancellationToken: cancellationToken); + + if (response is null) + { + return (HttpStatusCode.InternalServerError, pagedItems); + } + + var pageIterator = PageIterator.CreatePageIterator( + Client, + response, + item => + { + pagedItems.Add(item); + + count++; + if (count % 1000 == 0) + { + LogPageIteratorCount(nameof(Site), count); + } + + return true; + }); + + try + { + await pageIterator.IterateAsync(cancellationToken); + } + catch (ODataError odataError) when (odataError.ResponseStatusCode == (int)HttpStatusCode.TooManyRequests) + { + await Task.Delay(MicrosoftGraphConstants.RetryWaitDelayInMs, cancellationToken); + + await pageIterator.IterateAsync(cancellationToken); + } + catch (ODataError odataError) when (odataError.ResponseStatusCode == (int)HttpStatusCode.Gone) + { + return (HttpStatusCode.Gone, pagedItems); + } + + LogPageIteratorTotalCount(nameof(Site), count); + + return (HttpStatusCode.OK, pagedItems); + } + catch (ODataError odataError) + { + LogGetFailure(odataError.Error?.Message); + return (HttpStatusCode.InternalServerError, pagedItems); + } + catch (Exception ex) + { + LogGetFailure(ex.GetLastInnerMessage()); + return (HttpStatusCode.InternalServerError, pagedItems); + } + } } \ No newline at end of file diff --git a/src/Atc.Microsoft.Graph.Client/Services/Teams/ITeamsGraphService.cs b/src/Atc.Microsoft.Graph.Client/Services/Teams/ITeamsGraphService.cs index 4b9350a..3866436 100644 --- a/src/Atc.Microsoft.Graph.Client/Services/Teams/ITeamsGraphService.cs +++ b/src/Atc.Microsoft.Graph.Client/Services/Teams/ITeamsGraphService.cs @@ -2,4 +2,9 @@ namespace Atc.Microsoft.Graph.Client.Services.Teams; public interface ITeamsGraphService { + Task<(HttpStatusCode StatusCode, IList Data)> GetTeams( + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters, + CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Atc.Microsoft.Graph.Client/Services/Teams/TeamsGraphService.cs b/src/Atc.Microsoft.Graph.Client/Services/Teams/TeamsGraphService.cs index 7b927a8..cda4c30 100644 --- a/src/Atc.Microsoft.Graph.Client/Services/Teams/TeamsGraphService.cs +++ b/src/Atc.Microsoft.Graph.Client/Services/Teams/TeamsGraphService.cs @@ -2,4 +2,86 @@ namespace Atc.Microsoft.Graph.Client.Services.Teams; public sealed class TeamsGraphService : GraphServiceClientWrapper, ITeamsGraphService { + public TeamsGraphService( + ILoggerFactory loggerFactory, + GraphServiceClient client) + : base(loggerFactory, client) + { + } + + public async Task<(HttpStatusCode StatusCode, IList Data)> GetTeams( + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters, + CancellationToken cancellationToken) + { + List pagedItems = []; + var count = 0; + + try + { + var requestInformation = Client + .Teams + .ToGetRequestInformation( + RequestConfigurationFactory.CreateForTeams( + expandQueryParameters, + filterQueryParameter, + selectQueryParameters)); + + var response = await Client.RequestAdapter.SendAsync( + requestInformation, + (_) => new TeamCollectionResponse(), + cancellationToken: cancellationToken); + + if (response is null) + { + return (HttpStatusCode.InternalServerError, pagedItems); + } + + var pageIterator = PageIterator.CreatePageIterator( + Client, + response, + item => + { + pagedItems.Add(item); + + count++; + if (count % 1000 == 0) + { + LogPageIteratorCount(nameof(Team), count); + } + + return true; + }); + + try + { + await pageIterator.IterateAsync(cancellationToken); + } + catch (ODataError odataError) when (odataError.ResponseStatusCode == (int)HttpStatusCode.TooManyRequests) + { + await Task.Delay(MicrosoftGraphConstants.RetryWaitDelayInMs, cancellationToken); + + await pageIterator.IterateAsync(cancellationToken); + } + catch (ODataError odataError) when (odataError.ResponseStatusCode == (int)HttpStatusCode.Gone) + { + return (HttpStatusCode.Gone, pagedItems); + } + + LogPageIteratorTotalCount(nameof(Team), count); + + return (HttpStatusCode.OK, pagedItems); + } + catch (ODataError odataError) + { + LogGetFailure(odataError.Error?.Message); + return (HttpStatusCode.InternalServerError, pagedItems); + } + catch (Exception ex) + { + LogGetFailure(ex.GetLastInnerMessage()); + return (HttpStatusCode.InternalServerError, pagedItems); + } + } } \ No newline at end of file diff --git a/src/Atc.Microsoft.Graph.Client/Services/Users/IUsersGraphService.cs b/src/Atc.Microsoft.Graph.Client/Services/Users/IUsersGraphService.cs new file mode 100644 index 0000000..ae32f9b --- /dev/null +++ b/src/Atc.Microsoft.Graph.Client/Services/Users/IUsersGraphService.cs @@ -0,0 +1,10 @@ +namespace Atc.Microsoft.Graph.Client.Services.Users; + +public interface IUsersGraphService +{ + Task<(HttpStatusCode StatusCode, IList Data)> GetUsers( + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters, + CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/Atc.Microsoft.Graph.Client/Services/Users/UsersGraphService.cs b/src/Atc.Microsoft.Graph.Client/Services/Users/UsersGraphService.cs new file mode 100644 index 0000000..8f97e13 --- /dev/null +++ b/src/Atc.Microsoft.Graph.Client/Services/Users/UsersGraphService.cs @@ -0,0 +1,87 @@ +namespace Atc.Microsoft.Graph.Client.Services.Users; + +public sealed class UsersGraphService : GraphServiceClientWrapper, IUsersGraphService +{ + public UsersGraphService( + ILoggerFactory loggerFactory, + GraphServiceClient client) + : base(loggerFactory, client) + { + } + + public async Task<(HttpStatusCode StatusCode, IList Data)> GetUsers( + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters, + CancellationToken cancellationToken) + { + List pagedItems = []; + var count = 0; + + try + { + var requestInformation = Client + .Users + .ToGetRequestInformation( + RequestConfigurationFactory.CreateForUsers( + expandQueryParameters, + filterQueryParameter, + selectQueryParameters)); + + var response = await Client.RequestAdapter.SendAsync( + requestInformation, + (_) => new UserCollectionResponse(), + cancellationToken: cancellationToken); + + if (response is null) + { + return (HttpStatusCode.InternalServerError, pagedItems); + } + + var pageIterator = PageIterator.CreatePageIterator( + Client, + response, + item => + { + pagedItems.Add(item); + + count++; + if (count % 1000 == 0) + { + LogPageIteratorCount(nameof(User), count); + } + + return true; + }); + + try + { + await pageIterator.IterateAsync(cancellationToken); + } + catch (ODataError odataError) when (odataError.ResponseStatusCode == (int)HttpStatusCode.TooManyRequests) + { + await Task.Delay(MicrosoftGraphConstants.RetryWaitDelayInMs, cancellationToken); + + await pageIterator.IterateAsync(cancellationToken); + } + catch (ODataError odataError) when (odataError.ResponseStatusCode == (int)HttpStatusCode.Gone) + { + return (HttpStatusCode.Gone, pagedItems); + } + + LogPageIteratorTotalCount(nameof(User), count); + + return (HttpStatusCode.OK, pagedItems); + } + catch (ODataError odataError) + { + LogGetFailure(odataError.Error?.Message); + return (HttpStatusCode.InternalServerError, pagedItems); + } + catch (Exception ex) + { + LogGetFailure(ex.GetLastInnerMessage()); + return (HttpStatusCode.InternalServerError, pagedItems); + } + } +} \ No newline at end of file From 76424a80feb53af08c83c074e71950cba472c396 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Fri, 17 May 2024 22:38:02 +0200 Subject: [PATCH 07/21] feat: add default to all cancellationTokens --- .../Services/OneDrive/IOneDriveGraphService.cs | 12 ++++++------ .../Services/OneDrive/OneDriveGraphService.cs | 14 +++++++------- .../Services/Outlook/IOutlookGraphService.cs | 12 ++++++------ .../Services/Outlook/OutlookGraphService.cs | 16 ++++++++-------- .../Sharepoint/ISharepointGraphService.cs | 2 +- .../Sharepoint/SharepointGraphService.cs | 2 +- .../Services/Teams/ITeamsGraphService.cs | 2 +- .../Services/Teams/TeamsGraphService.cs | 2 +- .../Services/Users/IUsersGraphService.cs | 2 +- .../Services/Users/UsersGraphService.cs | 2 +- 10 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/Atc.Microsoft.Graph.Client/Services/OneDrive/IOneDriveGraphService.cs b/src/Atc.Microsoft.Graph.Client/Services/OneDrive/IOneDriveGraphService.cs index f9591fb..1a5e0ec 100644 --- a/src/Atc.Microsoft.Graph.Client/Services/OneDrive/IOneDriveGraphService.cs +++ b/src/Atc.Microsoft.Graph.Client/Services/OneDrive/IOneDriveGraphService.cs @@ -7,32 +7,32 @@ public interface IOneDriveGraphService List? expandQueryParameters, string? filterQueryParameter, List? selectQueryParameters, - CancellationToken cancellationToken); + CancellationToken cancellationToken = default); Task GetDriveByTeamId( string teamId, - CancellationToken cancellationToken); + CancellationToken cancellationToken = default); Task GetDeltaTokenForDriveItemsByDriveId( string driveId, - CancellationToken cancellationToken); + CancellationToken cancellationToken = default); Task<(HttpStatusCode StatusCode, IList Data)> GetDriveItemsByDriveId( string driveId, List? expandQueryParameters, string? filterQueryParameter, List? selectQueryParameters, - CancellationToken cancellationToken); + CancellationToken cancellationToken = default); Task<(HttpStatusCode StatusCode, IList Data)> GetDriveItemsByDriveIdAndDeltaToken( string driveId, string deltaToken, string? filterQueryParameter, List? selectQueryParameters, - CancellationToken cancellationToken); + CancellationToken cancellationToken = default); Task DownloadFile( string driveId, string fileId, - CancellationToken cancellationToken); + CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/Atc.Microsoft.Graph.Client/Services/OneDrive/OneDriveGraphService.cs b/src/Atc.Microsoft.Graph.Client/Services/OneDrive/OneDriveGraphService.cs index 7759e62..dc25d7b 100644 --- a/src/Atc.Microsoft.Graph.Client/Services/OneDrive/OneDriveGraphService.cs +++ b/src/Atc.Microsoft.Graph.Client/Services/OneDrive/OneDriveGraphService.cs @@ -14,7 +14,7 @@ public OneDriveGraphService( List? expandQueryParameters, string? filterQueryParameter, List? selectQueryParameters, - CancellationToken cancellationToken) + CancellationToken cancellationToken = default) { List pagedItems = []; var count = 0; @@ -88,8 +88,8 @@ public OneDriveGraphService( } public async Task GetDriveByTeamId( - string teamId, - CancellationToken cancellationToken) + string teamId, + CancellationToken cancellationToken = default) { try { @@ -120,7 +120,7 @@ public OneDriveGraphService( public async Task GetDeltaTokenForDriveItemsByDriveId( string driveId, - CancellationToken cancellationToken) + CancellationToken cancellationToken = default) { try { @@ -162,7 +162,7 @@ public OneDriveGraphService( List? expandQueryParameters, string? filterQueryParameter, List? selectQueryParameters, - CancellationToken cancellationToken) + CancellationToken cancellationToken = default) { var requestInformation = Client .Drives[driveId] @@ -189,7 +189,7 @@ public OneDriveGraphService( string deltaToken, string? filterQueryParameter, List? selectQueryParameters, - CancellationToken cancellationToken) + CancellationToken cancellationToken = default) { List pagedItems = []; var count = 0; @@ -265,7 +265,7 @@ public OneDriveGraphService( public async Task DownloadFile( string driveId, string fileId, - CancellationToken cancellationToken) + CancellationToken cancellationToken = default) { try { diff --git a/src/Atc.Microsoft.Graph.Client/Services/Outlook/IOutlookGraphService.cs b/src/Atc.Microsoft.Graph.Client/Services/Outlook/IOutlookGraphService.cs index 5637551..25eee9b 100644 --- a/src/Atc.Microsoft.Graph.Client/Services/Outlook/IOutlookGraphService.cs +++ b/src/Atc.Microsoft.Graph.Client/Services/Outlook/IOutlookGraphService.cs @@ -7,7 +7,7 @@ public interface IOutlookGraphService List? expandQueryParameters, string? filterQueryParameter, List? selectQueryParameters, - CancellationToken cancellationToken); + CancellationToken cancellationToken = default); Task<(HttpStatusCode StatusCode, IList Data)> GetMailFoldersByUserIdAndFolderId( string userId, @@ -15,14 +15,14 @@ public interface IOutlookGraphService List? expandQueryParameters, string? filterQueryParameter, List? selectQueryParameters, - CancellationToken cancellationToken); + CancellationToken cancellationToken = default); Task<(HttpStatusCode StatusCode, IList Data)> GetMessagesByUserId( string userId, List? expandQueryParameters, string? filterQueryParameter, List? selectQueryParameters, - CancellationToken cancellationToken); + CancellationToken cancellationToken = default); Task<(HttpStatusCode StatusCode, IList Data, string? DeltaToken)> GetMessagesByUserIdAndFolderId( string userId, @@ -30,8 +30,8 @@ public interface IOutlookGraphService List? expandQueryParameters, string? filterQueryParameter, List? selectQueryParameters, - CancellationToken cancellationToken, - string? deltaToken = null); + string? deltaToken = null, + CancellationToken cancellationToken = default); Task<(HttpStatusCode StatusCode, IList Data)> GetFileAttachmentsByUserIdAndMessageId( string userId, @@ -39,5 +39,5 @@ public interface IOutlookGraphService List? expandQueryParameters, string? filterQueryParameter, List? selectQueryParameters, - CancellationToken cancellationToken); + CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/Atc.Microsoft.Graph.Client/Services/Outlook/OutlookGraphService.cs b/src/Atc.Microsoft.Graph.Client/Services/Outlook/OutlookGraphService.cs index 52b326c..8b41ae2 100644 --- a/src/Atc.Microsoft.Graph.Client/Services/Outlook/OutlookGraphService.cs +++ b/src/Atc.Microsoft.Graph.Client/Services/Outlook/OutlookGraphService.cs @@ -14,7 +14,7 @@ public OutlookGraphService( List? expandQueryParameters, string? filterQueryParameter, List? selectQueryParameters, - CancellationToken cancellationToken) + CancellationToken cancellationToken = default) { List pagedItems = []; var count = 0; @@ -93,7 +93,7 @@ public OutlookGraphService( List? expandQueryParameters, string? filterQueryParameter, List? selectQueryParameters, - CancellationToken cancellationToken) + CancellationToken cancellationToken = default) { List pagedItems = []; var count = 0; @@ -172,7 +172,7 @@ public OutlookGraphService( List? expandQueryParameters, string? filterQueryParameter, List? selectQueryParameters, - CancellationToken cancellationToken) + CancellationToken cancellationToken = default) { List pagedItems = []; var count = 0; @@ -251,8 +251,8 @@ public OutlookGraphService( List? expandQueryParameters, string? filterQueryParameter, List? selectQueryParameters, - CancellationToken cancellationToken, - string? deltaToken = null) + string? deltaToken = null, + CancellationToken cancellationToken = default) { List pagedItems = []; @@ -292,7 +292,7 @@ public OutlookGraphService( List? expandQueryParameters, string? filterQueryParameter, List? selectQueryParameters, - CancellationToken cancellationToken) + CancellationToken cancellationToken = default) { List pagedItems = []; var count = 0; @@ -374,7 +374,7 @@ public OutlookGraphService( string folderId, string? filterQueryParameter, List? selectQueryParameters, - CancellationToken cancellationToken) + CancellationToken cancellationToken = default) { List pagedItems = []; var count = 0; @@ -456,7 +456,7 @@ public OutlookGraphService( List? expandQueryParameters, string? filterQueryParameter, List? selectQueryParameters, - CancellationToken cancellationToken) + CancellationToken cancellationToken = default) { List pagedItems = []; var count = 0; diff --git a/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/ISharepointGraphService.cs b/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/ISharepointGraphService.cs index efd4e75..1f86749 100644 --- a/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/ISharepointGraphService.cs +++ b/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/ISharepointGraphService.cs @@ -6,5 +6,5 @@ public interface ISharepointGraphService List? expandQueryParameters, string? filterQueryParameter, List? selectQueryParameters, - CancellationToken cancellationToken); + CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/SharepointGraphService.cs b/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/SharepointGraphService.cs index ca25fcc..a3c813f 100644 --- a/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/SharepointGraphService.cs +++ b/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/SharepointGraphService.cs @@ -13,7 +13,7 @@ public SharepointGraphService( List? expandQueryParameters, string? filterQueryParameter, List? selectQueryParameters, - CancellationToken cancellationToken) + CancellationToken cancellationToken = default) { List pagedItems = []; var count = 0; diff --git a/src/Atc.Microsoft.Graph.Client/Services/Teams/ITeamsGraphService.cs b/src/Atc.Microsoft.Graph.Client/Services/Teams/ITeamsGraphService.cs index 3866436..3aa4bc0 100644 --- a/src/Atc.Microsoft.Graph.Client/Services/Teams/ITeamsGraphService.cs +++ b/src/Atc.Microsoft.Graph.Client/Services/Teams/ITeamsGraphService.cs @@ -6,5 +6,5 @@ public interface ITeamsGraphService List? expandQueryParameters, string? filterQueryParameter, List? selectQueryParameters, - CancellationToken cancellationToken); + CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/Atc.Microsoft.Graph.Client/Services/Teams/TeamsGraphService.cs b/src/Atc.Microsoft.Graph.Client/Services/Teams/TeamsGraphService.cs index cda4c30..5caf889 100644 --- a/src/Atc.Microsoft.Graph.Client/Services/Teams/TeamsGraphService.cs +++ b/src/Atc.Microsoft.Graph.Client/Services/Teams/TeamsGraphService.cs @@ -13,7 +13,7 @@ public TeamsGraphService( List? expandQueryParameters, string? filterQueryParameter, List? selectQueryParameters, - CancellationToken cancellationToken) + CancellationToken cancellationToken = default) { List pagedItems = []; var count = 0; diff --git a/src/Atc.Microsoft.Graph.Client/Services/Users/IUsersGraphService.cs b/src/Atc.Microsoft.Graph.Client/Services/Users/IUsersGraphService.cs index ae32f9b..9bc96fa 100644 --- a/src/Atc.Microsoft.Graph.Client/Services/Users/IUsersGraphService.cs +++ b/src/Atc.Microsoft.Graph.Client/Services/Users/IUsersGraphService.cs @@ -6,5 +6,5 @@ public interface IUsersGraphService List? expandQueryParameters, string? filterQueryParameter, List? selectQueryParameters, - CancellationToken cancellationToken); + CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/Atc.Microsoft.Graph.Client/Services/Users/UsersGraphService.cs b/src/Atc.Microsoft.Graph.Client/Services/Users/UsersGraphService.cs index 8f97e13..2516220 100644 --- a/src/Atc.Microsoft.Graph.Client/Services/Users/UsersGraphService.cs +++ b/src/Atc.Microsoft.Graph.Client/Services/Users/UsersGraphService.cs @@ -13,7 +13,7 @@ public UsersGraphService( List? expandQueryParameters, string? filterQueryParameter, List? selectQueryParameters, - CancellationToken cancellationToken) + CancellationToken cancellationToken = default) { List pagedItems = []; var count = 0; From 45a0f2e923d3bd3ba9c5875edb777e711ac84eb3 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Sat, 18 May 2024 09:53:45 +0200 Subject: [PATCH 08/21] build: adding Sharepoint to team-dictionary --- Atc.Microsoft.Graph.Client.sln.DotSettings | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Atc.Microsoft.Graph.Client.sln.DotSettings diff --git a/Atc.Microsoft.Graph.Client.sln.DotSettings b/Atc.Microsoft.Graph.Client.sln.DotSettings new file mode 100644 index 0000000..9e78504 --- /dev/null +++ b/Atc.Microsoft.Graph.Client.sln.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file From 182c73d8ce7af1e7be57d1866b42d5cfd9f2b2c4 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Sat, 18 May 2024 09:53:56 +0200 Subject: [PATCH 09/21] feat: adding Nuget package Azure.Identity --- src/Atc.Microsoft.Graph.Client/Atc.Microsoft.Graph.Client.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Atc.Microsoft.Graph.Client/Atc.Microsoft.Graph.Client.csproj b/src/Atc.Microsoft.Graph.Client/Atc.Microsoft.Graph.Client.csproj index 96a5248..77db47a 100644 --- a/src/Atc.Microsoft.Graph.Client/Atc.Microsoft.Graph.Client.csproj +++ b/src/Atc.Microsoft.Graph.Client/Atc.Microsoft.Graph.Client.csproj @@ -14,6 +14,7 @@ + From c338bd1c3a500a729b812789a80e24fb26796d5d Mon Sep 17 00:00:00 2001 From: Per Kops Date: Sat, 18 May 2024 09:54:27 +0200 Subject: [PATCH 10/21] feat: introduce RequestConfigurationFactory --- .../Factories/RequestConfigurationFactory.cs | 278 ++++++++++++++++++ .../GlobalUsings.cs | 12 + 2 files changed, 290 insertions(+) create mode 100644 src/Atc.Microsoft.Graph.Client/Factories/RequestConfigurationFactory.cs diff --git a/src/Atc.Microsoft.Graph.Client/Factories/RequestConfigurationFactory.cs b/src/Atc.Microsoft.Graph.Client/Factories/RequestConfigurationFactory.cs new file mode 100644 index 0000000..e186db2 --- /dev/null +++ b/src/Atc.Microsoft.Graph.Client/Factories/RequestConfigurationFactory.cs @@ -0,0 +1,278 @@ +namespace Atc.Microsoft.Graph.Client.Factories; + +public static class RequestConfigurationFactory +{ + public static Action> CreateForAttachments( + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters) => + rc => + { + if (expandQueryParameters is not null && + expandQueryParameters.Any()) + { + rc.QueryParameters.Expand = [.. expandQueryParameters]; + } + + if (!string.IsNullOrEmpty(filterQueryParameter)) + { + rc.QueryParameters.Filter = filterQueryParameter; + } + + if (selectQueryParameters is not null && + selectQueryParameters.Any()) + { + rc.QueryParameters.Select = [.. selectQueryParameters]; + } + }; + + public static Action> CreateForDrives( + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters) => + rc => + { + if (expandQueryParameters is not null && + expandQueryParameters.Any()) + { + rc.QueryParameters.Expand = [.. expandQueryParameters]; + } + + if (!string.IsNullOrEmpty(filterQueryParameter)) + { + rc.QueryParameters.Filter = filterQueryParameter; + } + + if (selectQueryParameters is not null && + selectQueryParameters.Any()) + { + rc.QueryParameters.Select = [.. selectQueryParameters]; + } + }; + + public static Action> CreateForItems( + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters) => + rc => + { + if (expandQueryParameters is not null && + expandQueryParameters.Any()) + { + rc.QueryParameters.Expand = [.. expandQueryParameters]; + } + + if (!string.IsNullOrEmpty(filterQueryParameter)) + { + rc.QueryParameters.Filter = filterQueryParameter; + } + + if (selectQueryParameters is not null && + selectQueryParameters.Any()) + { + rc.QueryParameters.Select = [.. selectQueryParameters]; + } + }; + + public static Action> CreateForItemsWithDelta( + string? filterQueryParameter, + List? selectQueryParameters) => + rc => + { + if (!string.IsNullOrEmpty(filterQueryParameter)) + { + rc.QueryParameters.Filter = filterQueryParameter; + } + + if (selectQueryParameters is not null && + selectQueryParameters.Any()) + { + rc.QueryParameters.Select = [.. selectQueryParameters]; + } + }; + + public static Action> CreateForChildFolders( + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters) => + rc => + { + if (expandQueryParameters is not null && + expandQueryParameters.Any()) + { + rc.QueryParameters.Expand = [.. expandQueryParameters]; + } + + if (!string.IsNullOrEmpty(filterQueryParameter)) + { + rc.QueryParameters.Filter = filterQueryParameter; + } + + if (selectQueryParameters is not null && + selectQueryParameters.Any()) + { + rc.QueryParameters.Select = [.. selectQueryParameters]; + } + }; + + public static Action> CreateForMailFolders( + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters) => + rc => + { + if (expandQueryParameters is not null && + expandQueryParameters.Any()) + { + rc.QueryParameters.Expand = [.. expandQueryParameters]; + } + + if (!string.IsNullOrEmpty(filterQueryParameter)) + { + rc.QueryParameters.Filter = filterQueryParameter; + } + + if (selectQueryParameters is not null && + selectQueryParameters.Any()) + { + rc.QueryParameters.Select = [.. selectQueryParameters]; + } + }; + + public static Action> CreateForMessagesMailFolder( + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters) => + rc => + { + if (expandQueryParameters is not null && + expandQueryParameters.Any()) + { + rc.QueryParameters.Expand = [.. expandQueryParameters]; + } + + if (!string.IsNullOrEmpty(filterQueryParameter)) + { + rc.QueryParameters.Filter = filterQueryParameter; + } + + if (selectQueryParameters is not null && + selectQueryParameters.Any()) + { + rc.QueryParameters.Select = [.. selectQueryParameters]; + } + }; + + public static Action> CreateForMessagesUserId( + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters) => + rc => + { + if (expandQueryParameters is not null && + expandQueryParameters.Any()) + { + rc.QueryParameters.Expand = [.. expandQueryParameters]; + } + + if (!string.IsNullOrEmpty(filterQueryParameter)) + { + rc.QueryParameters.Filter = filterQueryParameter; + } + + if (selectQueryParameters is not null && + selectQueryParameters.Any()) + { + rc.QueryParameters.Select = [.. selectQueryParameters]; + } + }; + + public static Action> CreateForMessagesDelta( + string? filterQueryParameter, + List? selectQueryParameters) => + rc => + { + if (!string.IsNullOrEmpty(filterQueryParameter)) + { + rc.QueryParameters.Filter = filterQueryParameter; + } + + if (selectQueryParameters is not null && + selectQueryParameters.Any()) + { + rc.QueryParameters.Select = [.. selectQueryParameters]; + } + }; + + public static Action> CreateForSites( + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters) => + rc => + { + if (expandQueryParameters is not null && + expandQueryParameters.Any()) + { + rc.QueryParameters.Expand = [.. expandQueryParameters]; + } + + if (!string.IsNullOrEmpty(filterQueryParameter)) + { + rc.QueryParameters.Filter = filterQueryParameter; + } + + if (selectQueryParameters is not null && + selectQueryParameters.Any()) + { + rc.QueryParameters.Select = [.. selectQueryParameters]; + } + }; + + public static Action> CreateForTeams( + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters) => + rc => + { + if (expandQueryParameters is not null && + expandQueryParameters.Any()) + { + rc.QueryParameters.Expand = [.. expandQueryParameters]; + } + + if (!string.IsNullOrEmpty(filterQueryParameter)) + { + rc.QueryParameters.Filter = filterQueryParameter; + } + + if (selectQueryParameters is not null && + selectQueryParameters.Any()) + { + rc.QueryParameters.Select = [.. selectQueryParameters]; + } + }; + + public static Action> CreateForUsers( + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters) => + rc => + { + if (expandQueryParameters is not null && + expandQueryParameters.Any()) + { + rc.QueryParameters.Expand = [.. expandQueryParameters]; + } + + if (!string.IsNullOrEmpty(filterQueryParameter)) + { + rc.QueryParameters.Filter = filterQueryParameter; + } + + if (selectQueryParameters is not null && + selectQueryParameters.Any()) + { + rc.QueryParameters.Select = [.. selectQueryParameters]; + } + }; +} \ No newline at end of file diff --git a/src/Atc.Microsoft.Graph.Client/GlobalUsings.cs b/src/Atc.Microsoft.Graph.Client/GlobalUsings.cs index 50c5f3d..1a2bb1a 100644 --- a/src/Atc.Microsoft.Graph.Client/GlobalUsings.cs +++ b/src/Atc.Microsoft.Graph.Client/GlobalUsings.cs @@ -1,10 +1,22 @@ global using System.Diagnostics.CodeAnalysis; global using System.Net; global using System.Runtime.CompilerServices; +global using Atc.Microsoft.Graph.Client.Factories; +global using Atc.Microsoft.Graph.Client.Services; +global using Azure.Identity; +global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.Logging; global using Microsoft.Graph; +global using Microsoft.Graph.Drives.Item.List.Items; global using Microsoft.Graph.Models; global using Microsoft.Graph.Models.ODataErrors; +global using Microsoft.Graph.Sites; +global using Microsoft.Graph.Sites.Item.Drives; +global using Microsoft.Graph.Teams; +global using Microsoft.Graph.Users; +global using Microsoft.Graph.Users.Item.MailFolders; +global using Microsoft.Graph.Users.Item.MailFolders.Item.ChildFolders; +global using Microsoft.Graph.Users.Item.Messages.Item.Attachments; global using Microsoft.Kiota.Abstractions; global using Polly; global using Polly.Retry; \ No newline at end of file From 1d7af5d6a9ed8a945dd3abebfbb6cd3950c99a02 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Sat, 18 May 2024 09:54:45 +0200 Subject: [PATCH 11/21] feat: add initial draft for ServiceCollectionExtensions --- .../Extensions/ServiceCollectionExtensions.cs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/Atc.Microsoft.Graph.Client/Extensions/ServiceCollectionExtensions.cs diff --git a/src/Atc.Microsoft.Graph.Client/Extensions/ServiceCollectionExtensions.cs b/src/Atc.Microsoft.Graph.Client/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..3767014 --- /dev/null +++ b/src/Atc.Microsoft.Graph.Client/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,33 @@ +namespace Atc.Microsoft.Graph.Client.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddMicrosoftGraphServiceClient( + this IServiceCollection services, + GraphServiceOptions graphServiceOptions) + { + var scopes = new[] { "https://graph.microsoft.com/.default" }; + + services.AddSingleton(_ => + { + var options = new TokenCredentialOptions + { + AuthorityHost = AzureAuthorityHosts.AzurePublicCloud, + }; + + var clientSecretCredential = new ClientSecretCredential( + graphServiceOptions.TenantId, + graphServiceOptions.ClientId, + graphServiceOptions.ClientSecret, + options); + + return new GraphServiceClient(clientSecretCredential, scopes); + }); + + services.AddSingleton(s => new GraphServiceClientWrapper( + s.GetRequiredService(), + s.GetRequiredService())); + + return services; + } +} From 2e2875f2ab087496a0468848b5d61a71e161d4f1 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Sat, 18 May 2024 09:56:01 +0200 Subject: [PATCH 12/21] feat: adding LoggingEventIdConstants and 3 more logger statements --- .../LoggingEventIdConstants.cs | 24 ++++++++++++++ ...GraphServiceClientWrapperLoggerMessages.cs | 31 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 src/Atc.Microsoft.Graph.Client/LoggingEventIdConstants.cs diff --git a/src/Atc.Microsoft.Graph.Client/LoggingEventIdConstants.cs b/src/Atc.Microsoft.Graph.Client/LoggingEventIdConstants.cs new file mode 100644 index 0000000..2d291b1 --- /dev/null +++ b/src/Atc.Microsoft.Graph.Client/LoggingEventIdConstants.cs @@ -0,0 +1,24 @@ +namespace Atc.Microsoft.Graph.Client; + +internal static class LoggingEventIdConstants +{ + internal static class GraphServiceClientWrapper + { + public const int GetFailure = 10_000; + + public const int SubscriptionSetupFailed = 10_100; + public const int SubscriptionRenewalFailed = 10_101; + public const int SubscriptionDeletionFailed = 10_102; + + public const int DownloadFileFailed = 10_200; + public const int DownloadFileRetrying = 10_201; + public const int DownloadFileEmpty = 10_202; + + public const int DeltaLinkNotFoundForDrive = 10_300; + + public const int DriveNotFoundForTeam = 10_400; + + public const int PageIteratorCount = 10_500; + public const int PageIteratorTotalCount = 10_501; + } +} \ No newline at end of file diff --git a/src/Atc.Microsoft.Graph.Client/Services/GraphServiceClientWrapperLoggerMessages.cs b/src/Atc.Microsoft.Graph.Client/Services/GraphServiceClientWrapperLoggerMessages.cs index 3be8dd5..1678be2 100644 --- a/src/Atc.Microsoft.Graph.Client/Services/GraphServiceClientWrapperLoggerMessages.cs +++ b/src/Atc.Microsoft.Graph.Client/Services/GraphServiceClientWrapperLoggerMessages.cs @@ -17,6 +17,37 @@ protected partial void LogGetFailure( [CallerMemberName] string callerMethodName = "", [CallerLineNumber] int callerLineNumber = 0); + [LoggerMessage( + EventId = LoggingEventIdConstants.GraphServiceClientWrapper.SubscriptionSetupFailed, + Level = LogLevel.Error, + Message = "{callerMethodName}({callerLineNumber}) - Failed to setup subscription for the resource '{resource}': '{errorMessage}'.")] + protected partial void LogSubscriptionSetupFailed( + string? resource, + string? errorMessage, + [CallerMemberName] string callerMethodName = "", + [CallerLineNumber] int callerLineNumber = 0); + + [LoggerMessage( + EventId = LoggingEventIdConstants.GraphServiceClientWrapper.SubscriptionRenewalFailed, + Level = LogLevel.Error, + Message = "{callerMethodName}({callerLineNumber}) - Failed to renew subscription with id '{subscriptionId}' with expirationDate '{expirationDate}': '{errorMessage}'.")] + protected partial void LogSubscriptionRenewalFailed( + Guid subscriptionId, + DateTimeOffset expirationDate, + string? errorMessage, + [CallerMemberName] string callerMethodName = "", + [CallerLineNumber] int callerLineNumber = 0); + + [LoggerMessage( + EventId = LoggingEventIdConstants.GraphServiceClientWrapper.SubscriptionDeletionFailed, + Level = LogLevel.Error, + Message = "{callerMethodName}({callerLineNumber}) - Failed to delete subscription with id '{subscriptionId}': '{errorMessage}'.")] + protected partial void LogSubscriptionDeletionFailed( + Guid subscriptionId, + string? errorMessage, + [CallerMemberName] string callerMethodName = "", + [CallerLineNumber] int callerLineNumber = 0); + [LoggerMessage( EventId = LoggingEventIdConstants.GraphServiceClientWrapper.DownloadFileFailed, Level = LogLevel.Error, From 9dd860a233026f1a08c6b1165da4a023fc766112 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Sat, 18 May 2024 09:57:29 +0200 Subject: [PATCH 13/21] feat: introduce GraphServiceOptions --- src/Atc.Microsoft.Graph.Client/GlobalUsings.cs | 1 + .../Options/GraphServiceOptions.cs | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 src/Atc.Microsoft.Graph.Client/Options/GraphServiceOptions.cs diff --git a/src/Atc.Microsoft.Graph.Client/GlobalUsings.cs b/src/Atc.Microsoft.Graph.Client/GlobalUsings.cs index 1a2bb1a..ba2cebc 100644 --- a/src/Atc.Microsoft.Graph.Client/GlobalUsings.cs +++ b/src/Atc.Microsoft.Graph.Client/GlobalUsings.cs @@ -2,6 +2,7 @@ global using System.Net; global using System.Runtime.CompilerServices; global using Atc.Microsoft.Graph.Client.Factories; +global using Atc.Microsoft.Graph.Client.Options; global using Atc.Microsoft.Graph.Client.Services; global using Azure.Identity; global using Microsoft.Extensions.DependencyInjection; diff --git a/src/Atc.Microsoft.Graph.Client/Options/GraphServiceOptions.cs b/src/Atc.Microsoft.Graph.Client/Options/GraphServiceOptions.cs new file mode 100644 index 0000000..196c7c9 --- /dev/null +++ b/src/Atc.Microsoft.Graph.Client/Options/GraphServiceOptions.cs @@ -0,0 +1,10 @@ +namespace Atc.Microsoft.Graph.Client.Options; + +public sealed class GraphServiceOptions +{ + public string TenantId { get; set; } = string.Empty; + + public string? ClientId { get; set; } + + public string? ClientSecret { get; set; } +} \ No newline at end of file From 50306955030a5e4b7eb8276181854c622e3f9ec1 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Sat, 18 May 2024 10:11:53 +0200 Subject: [PATCH 14/21] feat: add SetupSubscription, RenewSubscription and DeleteSubscription to ISharepointGraphService --- .../Sharepoint/ISharepointGraphService.cs | 13 +++ .../Sharepoint/SharepointGraphService.cs | 103 ++++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/ISharepointGraphService.cs b/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/ISharepointGraphService.cs index 1f86749..791706a 100644 --- a/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/ISharepointGraphService.cs +++ b/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/ISharepointGraphService.cs @@ -7,4 +7,17 @@ public interface ISharepointGraphService string? filterQueryParameter, List? selectQueryParameters, CancellationToken cancellationToken = default); + + Task<(HttpStatusCode StatusCode, Guid? SubscriptionId)> SetupSubscription( + Subscription subscription, + CancellationToken cancellationToken = default); + + Task<(HttpStatusCode StatusCode, bool Succeeded)> RenewSubscription( + Guid subscriptionId, + DateTimeOffset expirationDate, + CancellationToken cancellationToken = default); + + Task<(HttpStatusCode StatusCode, bool Succeeded)> DeleteSubscription( + Guid subscriptionId, + CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/SharepointGraphService.cs b/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/SharepointGraphService.cs index a3c813f..cebf045 100644 --- a/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/SharepointGraphService.cs +++ b/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/SharepointGraphService.cs @@ -84,4 +84,107 @@ public SharepointGraphService( return (HttpStatusCode.InternalServerError, pagedItems); } } + + public async Task<(HttpStatusCode StatusCode, Guid? SubscriptionId)> SetupSubscription( + Subscription subscription, + CancellationToken cancellationToken = default) + { + try + { + Guid? subscriptionId = null; + + await DownloadResiliencePipeline.ExecuteAsync( + async context => + { + var graphSubscription = await Client.Subscriptions + .PostAsync(subscription, cancellationToken: context); + + subscriptionId = graphSubscription?.Id is not null + ? Guid.Parse(graphSubscription.Id) + : null; + + if (subscriptionId is null) + { + LogSubscriptionSetupFailed(subscription.Resource, "Subscription ID is null"); + } + + return subscriptionId; + }, + cancellationToken); + + return (HttpStatusCode.OK, subscriptionId); + } + catch (ODataError odataError) + { + if (odataError.Error?.Message?.Contains("timed out", StringComparison.OrdinalIgnoreCase) == true) + { + return (HttpStatusCode.RequestTimeout, null); + } + + LogSubscriptionSetupFailed(subscription.Resource, odataError.Error?.Message); + return (HttpStatusCode.InternalServerError, null); + } + catch (Exception ex) + { + LogSubscriptionSetupFailed(subscription.Resource, ex.GetLastInnerMessage()); + return (HttpStatusCode.InternalServerError, null); + } + } + + public async Task<(HttpStatusCode StatusCode, bool Succeeded)> RenewSubscription( + Guid subscriptionId, + DateTimeOffset expirationDate, + CancellationToken cancellationToken = default) + { + try + { + var newSubscription = new Subscription + { + ExpirationDateTime = expirationDate, + }; + + await Client + .Subscriptions[subscriptionId.ToString()] + .PatchAsync(newSubscription, cancellationToken: cancellationToken); + + return (HttpStatusCode.OK, true); + } + catch (ODataError odataError) + { + LogSubscriptionRenewalFailed(subscriptionId, expirationDate, odataError.Error?.Message); + return (HttpStatusCode.InternalServerError, false); + } + catch (Exception ex) + { + LogSubscriptionRenewalFailed(subscriptionId, expirationDate, ex.GetLastInnerMessage()); + return (HttpStatusCode.InternalServerError, false); + } + } + + public async Task<(HttpStatusCode StatusCode, bool Succeeded)> DeleteSubscription( + Guid subscriptionId, + CancellationToken cancellationToken = default) + { + try + { + await Client.Subscriptions[subscriptionId.ToString()] + .DeleteAsync(cancellationToken: cancellationToken); + + return (HttpStatusCode.OK, true); + } + catch (ODataError odataError) when (odataError.ResponseStatusCode == (int)HttpStatusCode.NotFound) + { + return (HttpStatusCode.OK, true); + } + catch (ODataError odataError) + { + LogSubscriptionDeletionFailed(subscriptionId, odataError.Error?.Message); + return (HttpStatusCode.InternalServerError, false); + } + catch (Exception ex) + { + LogSubscriptionDeletionFailed(subscriptionId, ex.GetLastInnerMessage()); + return (HttpStatusCode.InternalServerError, false); + } + } } \ No newline at end of file From f841fbce1e1b293ac2e7ac5ab8fbb904f58d6372 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Sat, 18 May 2024 10:22:20 +0200 Subject: [PATCH 15/21] feat: finish first draft on AddMicrosoftGraphServices in ServiceCollectionExtensions --- .../Extensions/ServiceCollectionExtensions.cs | 23 +++++++++++++++---- .../GlobalUsings.cs | 5 ++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/Atc.Microsoft.Graph.Client/Extensions/ServiceCollectionExtensions.cs b/src/Atc.Microsoft.Graph.Client/Extensions/ServiceCollectionExtensions.cs index 3767014..cadb7b6 100644 --- a/src/Atc.Microsoft.Graph.Client/Extensions/ServiceCollectionExtensions.cs +++ b/src/Atc.Microsoft.Graph.Client/Extensions/ServiceCollectionExtensions.cs @@ -2,7 +2,7 @@ namespace Atc.Microsoft.Graph.Client.Extensions; public static class ServiceCollectionExtensions { - public static IServiceCollection AddMicrosoftGraphServiceClient( + public static IServiceCollection AddMicrosoftGraphServices( this IServiceCollection services, GraphServiceOptions graphServiceOptions) { @@ -24,10 +24,25 @@ public static IServiceCollection AddMicrosoftGraphServiceClient( return new GraphServiceClient(clientSecretCredential, scopes); }); - services.AddSingleton(s => new GraphServiceClientWrapper( + services.AddGraphService(); + services.AddGraphService(); + services.AddGraphService(); + services.AddGraphService(); + services.AddGraphService(); + + return services; + } + + private static IServiceCollection AddGraphService( + this IServiceCollection services) + where TService : class + where TImplementation : GraphServiceClientWrapper, TService + { + services.AddSingleton(s => (TImplementation)Activator.CreateInstance( + typeof(TImplementation), s.GetRequiredService(), - s.GetRequiredService())); + s.GetRequiredService())!); return services; } -} +} \ No newline at end of file diff --git a/src/Atc.Microsoft.Graph.Client/GlobalUsings.cs b/src/Atc.Microsoft.Graph.Client/GlobalUsings.cs index ba2cebc..067d753 100644 --- a/src/Atc.Microsoft.Graph.Client/GlobalUsings.cs +++ b/src/Atc.Microsoft.Graph.Client/GlobalUsings.cs @@ -4,6 +4,11 @@ global using Atc.Microsoft.Graph.Client.Factories; global using Atc.Microsoft.Graph.Client.Options; global using Atc.Microsoft.Graph.Client.Services; +global using Atc.Microsoft.Graph.Client.Services.OneDrive; +global using Atc.Microsoft.Graph.Client.Services.Outlook; +global using Atc.Microsoft.Graph.Client.Services.Sharepoint; +global using Atc.Microsoft.Graph.Client.Services.Teams; +global using Atc.Microsoft.Graph.Client.Services.Users; global using Azure.Identity; global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.Logging; From 263833ffe48832b8d2bd4de847afb036a1f91d5d Mon Sep 17 00:00:00 2001 From: Per Kops Date: Sat, 18 May 2024 10:25:52 +0200 Subject: [PATCH 16/21] fix: checking subscription for null in SetupSubscription in SharepointGraphService --- .../Services/Sharepoint/SharepointGraphService.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/SharepointGraphService.cs b/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/SharepointGraphService.cs index cebf045..d5b9823 100644 --- a/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/SharepointGraphService.cs +++ b/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/SharepointGraphService.cs @@ -89,6 +89,8 @@ public SharepointGraphService( Subscription subscription, CancellationToken cancellationToken = default) { + ArgumentNullException.ThrowIfNull(subscription); + try { Guid? subscriptionId = null; From 583bd6cd45bc54334ebc2e7dd4267dffffb9b193 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Sat, 18 May 2024 10:26:12 +0200 Subject: [PATCH 17/21] chore: fixing up for rule CA1860 in RequestConfigurationFactory --- .../Factories/RequestConfigurationFactory.cs | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/Atc.Microsoft.Graph.Client/Factories/RequestConfigurationFactory.cs b/src/Atc.Microsoft.Graph.Client/Factories/RequestConfigurationFactory.cs index e186db2..a1db8d7 100644 --- a/src/Atc.Microsoft.Graph.Client/Factories/RequestConfigurationFactory.cs +++ b/src/Atc.Microsoft.Graph.Client/Factories/RequestConfigurationFactory.cs @@ -9,7 +9,7 @@ public static class RequestConfigurationFactory rc => { if (expandQueryParameters is not null && - expandQueryParameters.Any()) + expandQueryParameters.Count != 0) { rc.QueryParameters.Expand = [.. expandQueryParameters]; } @@ -20,7 +20,7 @@ public static class RequestConfigurationFactory } if (selectQueryParameters is not null && - selectQueryParameters.Any()) + selectQueryParameters.Count != 0) { rc.QueryParameters.Select = [.. selectQueryParameters]; } @@ -33,7 +33,7 @@ public static class RequestConfigurationFactory rc => { if (expandQueryParameters is not null && - expandQueryParameters.Any()) + expandQueryParameters.Count != 0) { rc.QueryParameters.Expand = [.. expandQueryParameters]; } @@ -44,7 +44,7 @@ public static class RequestConfigurationFactory } if (selectQueryParameters is not null && - selectQueryParameters.Any()) + selectQueryParameters.Count != 0) { rc.QueryParameters.Select = [.. selectQueryParameters]; } @@ -57,7 +57,7 @@ public static class RequestConfigurationFactory rc => { if (expandQueryParameters is not null && - expandQueryParameters.Any()) + expandQueryParameters.Count != 0) { rc.QueryParameters.Expand = [.. expandQueryParameters]; } @@ -68,7 +68,7 @@ public static class RequestConfigurationFactory } if (selectQueryParameters is not null && - selectQueryParameters.Any()) + selectQueryParameters.Count != 0) { rc.QueryParameters.Select = [.. selectQueryParameters]; } @@ -85,7 +85,7 @@ public static class RequestConfigurationFactory } if (selectQueryParameters is not null && - selectQueryParameters.Any()) + selectQueryParameters.Count != 0) { rc.QueryParameters.Select = [.. selectQueryParameters]; } @@ -98,7 +98,7 @@ public static class RequestConfigurationFactory rc => { if (expandQueryParameters is not null && - expandQueryParameters.Any()) + expandQueryParameters.Count != 0) { rc.QueryParameters.Expand = [.. expandQueryParameters]; } @@ -109,7 +109,7 @@ public static class RequestConfigurationFactory } if (selectQueryParameters is not null && - selectQueryParameters.Any()) + selectQueryParameters.Count != 0) { rc.QueryParameters.Select = [.. selectQueryParameters]; } @@ -122,7 +122,7 @@ public static class RequestConfigurationFactory rc => { if (expandQueryParameters is not null && - expandQueryParameters.Any()) + expandQueryParameters.Count != 0) { rc.QueryParameters.Expand = [.. expandQueryParameters]; } @@ -133,7 +133,7 @@ public static class RequestConfigurationFactory } if (selectQueryParameters is not null && - selectQueryParameters.Any()) + selectQueryParameters.Count != 0) { rc.QueryParameters.Select = [.. selectQueryParameters]; } @@ -146,7 +146,7 @@ public static class RequestConfigurationFactory rc => { if (expandQueryParameters is not null && - expandQueryParameters.Any()) + expandQueryParameters.Count != 0) { rc.QueryParameters.Expand = [.. expandQueryParameters]; } @@ -157,7 +157,7 @@ public static class RequestConfigurationFactory } if (selectQueryParameters is not null && - selectQueryParameters.Any()) + selectQueryParameters.Count != 0) { rc.QueryParameters.Select = [.. selectQueryParameters]; } @@ -170,7 +170,7 @@ public static class RequestConfigurationFactory rc => { if (expandQueryParameters is not null && - expandQueryParameters.Any()) + expandQueryParameters.Count != 0) { rc.QueryParameters.Expand = [.. expandQueryParameters]; } @@ -181,7 +181,7 @@ public static class RequestConfigurationFactory } if (selectQueryParameters is not null && - selectQueryParameters.Any()) + selectQueryParameters.Count != 0) { rc.QueryParameters.Select = [.. selectQueryParameters]; } @@ -198,7 +198,7 @@ public static class RequestConfigurationFactory } if (selectQueryParameters is not null && - selectQueryParameters.Any()) + selectQueryParameters.Count != 0) { rc.QueryParameters.Select = [.. selectQueryParameters]; } @@ -211,7 +211,7 @@ public static class RequestConfigurationFactory rc => { if (expandQueryParameters is not null && - expandQueryParameters.Any()) + expandQueryParameters.Count != 0) { rc.QueryParameters.Expand = [.. expandQueryParameters]; } @@ -222,7 +222,7 @@ public static class RequestConfigurationFactory } if (selectQueryParameters is not null && - selectQueryParameters.Any()) + selectQueryParameters.Count != 0) { rc.QueryParameters.Select = [.. selectQueryParameters]; } @@ -235,7 +235,7 @@ public static class RequestConfigurationFactory rc => { if (expandQueryParameters is not null && - expandQueryParameters.Any()) + expandQueryParameters.Count != 0) { rc.QueryParameters.Expand = [.. expandQueryParameters]; } @@ -246,7 +246,7 @@ public static class RequestConfigurationFactory } if (selectQueryParameters is not null && - selectQueryParameters.Any()) + selectQueryParameters.Count != 0) { rc.QueryParameters.Select = [.. selectQueryParameters]; } @@ -259,7 +259,7 @@ public static class RequestConfigurationFactory rc => { if (expandQueryParameters is not null && - expandQueryParameters.Any()) + expandQueryParameters.Count != 0) { rc.QueryParameters.Expand = [.. expandQueryParameters]; } @@ -270,7 +270,7 @@ public static class RequestConfigurationFactory } if (selectQueryParameters is not null && - selectQueryParameters.Any()) + selectQueryParameters.Count != 0) { rc.QueryParameters.Select = [.. selectQueryParameters]; } From 66f8f1228b2e8e26631e3e051645b06fbf74a77e Mon Sep 17 00:00:00 2001 From: Per Kops Date: Sat, 18 May 2024 10:27:28 +0200 Subject: [PATCH 18/21] fix: ensuring GraphServiceClientWrapper constructor is protected --- .../Services/GraphServiceClientWrapper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Atc.Microsoft.Graph.Client/Services/GraphServiceClientWrapper.cs b/src/Atc.Microsoft.Graph.Client/Services/GraphServiceClientWrapper.cs index 344f3c2..2dbead4 100644 --- a/src/Atc.Microsoft.Graph.Client/Services/GraphServiceClientWrapper.cs +++ b/src/Atc.Microsoft.Graph.Client/Services/GraphServiceClientWrapper.cs @@ -6,7 +6,7 @@ public abstract partial class GraphServiceClientWrapper protected ResiliencePipeline DownloadResiliencePipeline { get; } - public GraphServiceClientWrapper( + protected GraphServiceClientWrapper( ILoggerFactory loggerFactory, GraphServiceClient client) { From 69a93fae20159e7acba78e84c9e049b46138e6b4 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Sat, 18 May 2024 11:02:30 +0200 Subject: [PATCH 19/21] refact: change GraphServiceOptions to have all parameters as required - since its being used for construction of ClientSecretCredential --- .../Options/GraphServiceOptions.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Atc.Microsoft.Graph.Client/Options/GraphServiceOptions.cs b/src/Atc.Microsoft.Graph.Client/Options/GraphServiceOptions.cs index 196c7c9..8b346b6 100644 --- a/src/Atc.Microsoft.Graph.Client/Options/GraphServiceOptions.cs +++ b/src/Atc.Microsoft.Graph.Client/Options/GraphServiceOptions.cs @@ -4,7 +4,14 @@ public sealed class GraphServiceOptions { public string TenantId { get; set; } = string.Empty; - public string? ClientId { get; set; } + public string ClientId { get; set; } = string.Empty; - public string? ClientSecret { get; set; } + public string ClientSecret { get; set; } = string.Empty; + + public bool IsValid() => !string.IsNullOrEmpty(TenantId) && + !string.IsNullOrEmpty(ClientId) && + !string.IsNullOrEmpty(ClientSecret); + + public override string ToString() + => $"{nameof(TenantId)}: {TenantId}, {nameof(ClientId)}: {ClientId}, {nameof(ClientSecret)}: {ClientSecret}"; } \ No newline at end of file From 8a87ce25b537e2bdff6a22ae13b8f25432c99f53 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Sat, 18 May 2024 11:02:46 +0200 Subject: [PATCH 20/21] chore: suppress IDE0039 in src .editorconfig --- src/.editorconfig | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/.editorconfig b/src/.editorconfig index 970bed4..03eafd5 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -44,4 +44,6 @@ ########################################## # Custom - Code Analyzers Rules -########################################## \ No newline at end of file +########################################## + +dotnet_diagnostic.IDE0039.severity = none # \ No newline at end of file From de37df148ca246e95bfe54c6741ca173091530ba Mon Sep 17 00:00:00 2001 From: Per Kops Date: Sat, 18 May 2024 11:03:24 +0200 Subject: [PATCH 21/21] feat: add more overloads in ServiceCollectionExtensions for supplying own GraphServiceClient, and/or TokenCredential with or without scopes --- .../Extensions/ServiceCollectionExtensions.cs | 76 +++++++++++++++++-- .../GlobalUsings.cs | 1 + 2 files changed, 69 insertions(+), 8 deletions(-) diff --git a/src/Atc.Microsoft.Graph.Client/Extensions/ServiceCollectionExtensions.cs b/src/Atc.Microsoft.Graph.Client/Extensions/ServiceCollectionExtensions.cs index cadb7b6..01531d6 100644 --- a/src/Atc.Microsoft.Graph.Client/Extensions/ServiceCollectionExtensions.cs +++ b/src/Atc.Microsoft.Graph.Client/Extensions/ServiceCollectionExtensions.cs @@ -1,12 +1,68 @@ +// ReSharper disable ConvertToLocalFunction namespace Atc.Microsoft.Graph.Client.Extensions; public static class ServiceCollectionExtensions { + private static readonly string[] DefaultScopes = { "https://graph.microsoft.com/.default" }; + + /// + /// Adds the to the service collection. + /// + /// The instance to augment. + /// to use for the service. If null, one must be available in the service provider when this service is resolved. + /// The same instance as . public static IServiceCollection AddMicrosoftGraphServices( this IServiceCollection services, - GraphServiceOptions graphServiceOptions) + GraphServiceClient? graphServiceClient = null) { - var scopes = new[] { "https://graph.microsoft.com/.default" }; + Func factory = (serviceProvider) + => graphServiceClient ?? serviceProvider.GetRequiredService(); + + services.AddSingleton(factory); + + RegisterGraphServices(services); + + return services; + } + + /// + /// Adds the to the service collection using the provided and optional scopes. + /// + /// The instance to augment. + /// The to use for authentication. + /// Optional array of scopes for the . + /// The same instance as . + public static IServiceCollection AddMicrosoftGraphServices( + this IServiceCollection services, + TokenCredential tokenCredential, + string[]? scopes = null) + { + services.AddSingleton(_ => new GraphServiceClient(tokenCredential, scopes ?? DefaultScopes)); + + RegisterGraphServices(services); + + return services; + } + + /// + /// Adds the to the service collection using the provided and optional scopes. + /// + /// The instance to augment. + /// The containing configuration for the service. + /// Optional array of scopes for the . + /// The same instance as . + /// Thrown if the are invalid. + public static IServiceCollection AddMicrosoftGraphServices( + this IServiceCollection services, + GraphServiceOptions graphServiceOptions, + string[]? scopes = null) + { + ArgumentNullException.ThrowIfNull(graphServiceOptions); + + if (!graphServiceOptions.IsValid()) + { + throw new InvalidOperationException($"Required service '{nameof(GraphServiceOptions)}' is not registered"); + } services.AddSingleton(_ => { @@ -21,19 +77,25 @@ public static IServiceCollection AddMicrosoftGraphServices( graphServiceOptions.ClientSecret, options); - return new GraphServiceClient(clientSecretCredential, scopes); + return new GraphServiceClient(clientSecretCredential, scopes ?? DefaultScopes); }); + RegisterGraphServices(services); + + return services; + } + + private static void RegisterGraphServices( + IServiceCollection services) + { services.AddGraphService(); services.AddGraphService(); services.AddGraphService(); services.AddGraphService(); services.AddGraphService(); - - return services; } - private static IServiceCollection AddGraphService( + private static void AddGraphService( this IServiceCollection services) where TService : class where TImplementation : GraphServiceClientWrapper, TService @@ -42,7 +104,5 @@ private static IServiceCollection AddGraphService( typeof(TImplementation), s.GetRequiredService(), s.GetRequiredService())!); - - return services; } } \ No newline at end of file diff --git a/src/Atc.Microsoft.Graph.Client/GlobalUsings.cs b/src/Atc.Microsoft.Graph.Client/GlobalUsings.cs index 067d753..9b6cd30 100644 --- a/src/Atc.Microsoft.Graph.Client/GlobalUsings.cs +++ b/src/Atc.Microsoft.Graph.Client/GlobalUsings.cs @@ -9,6 +9,7 @@ global using Atc.Microsoft.Graph.Client.Services.Sharepoint; global using Atc.Microsoft.Graph.Client.Services.Teams; global using Atc.Microsoft.Graph.Client.Services.Users; +global using Azure.Core; global using Azure.Identity; global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.Logging;