diff --git a/.editorconfig b/.editorconfig index b1b0306..9994d68 100644 --- a/.editorconfig +++ b/.editorconfig @@ -519,4 +519,19 @@ dotnet_diagnostic.S6605.severity = none # Collection-specific "Exist ########################################## # Custom - Code Analyzers Rules ########################################## -[*.{cs,csx,cake}] \ No newline at end of file +[*.{cs,csx,cake}] + +MA0051.maximum_lines_per_method = 100 + +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 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 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 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..77db47a 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,12 @@ + + + + + + + + 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..01531d6 --- /dev/null +++ b/src/Atc.Microsoft.Graph.Client/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,108 @@ +// 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, + GraphServiceClient? graphServiceClient = null) + { + 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(_ => + { + var options = new TokenCredentialOptions + { + AuthorityHost = AzureAuthorityHosts.AzurePublicCloud, + }; + + var clientSecretCredential = new ClientSecretCredential( + graphServiceOptions.TenantId, + graphServiceOptions.ClientId, + graphServiceOptions.ClientSecret, + options); + + 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(); + } + + private static void AddGraphService( + this IServiceCollection services) + where TService : class + where TImplementation : GraphServiceClientWrapper, TService + { + services.AddSingleton(s => (TImplementation)Activator.CreateInstance( + typeof(TImplementation), + s.GetRequiredService(), + s.GetRequiredService())!); + } +} \ No newline at end of file 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..a1db8d7 --- /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.Count != 0) + { + rc.QueryParameters.Expand = [.. expandQueryParameters]; + } + + if (!string.IsNullOrEmpty(filterQueryParameter)) + { + rc.QueryParameters.Filter = filterQueryParameter; + } + + if (selectQueryParameters is not null && + selectQueryParameters.Count != 0) + { + rc.QueryParameters.Select = [.. selectQueryParameters]; + } + }; + + public static Action> CreateForDrives( + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters) => + rc => + { + if (expandQueryParameters is not null && + expandQueryParameters.Count != 0) + { + rc.QueryParameters.Expand = [.. expandQueryParameters]; + } + + if (!string.IsNullOrEmpty(filterQueryParameter)) + { + rc.QueryParameters.Filter = filterQueryParameter; + } + + if (selectQueryParameters is not null && + selectQueryParameters.Count != 0) + { + rc.QueryParameters.Select = [.. selectQueryParameters]; + } + }; + + public static Action> CreateForItems( + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters) => + rc => + { + if (expandQueryParameters is not null && + expandQueryParameters.Count != 0) + { + rc.QueryParameters.Expand = [.. expandQueryParameters]; + } + + if (!string.IsNullOrEmpty(filterQueryParameter)) + { + rc.QueryParameters.Filter = filterQueryParameter; + } + + if (selectQueryParameters is not null && + selectQueryParameters.Count != 0) + { + 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.Count != 0) + { + rc.QueryParameters.Select = [.. selectQueryParameters]; + } + }; + + public static Action> CreateForChildFolders( + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters) => + rc => + { + if (expandQueryParameters is not null && + expandQueryParameters.Count != 0) + { + rc.QueryParameters.Expand = [.. expandQueryParameters]; + } + + if (!string.IsNullOrEmpty(filterQueryParameter)) + { + rc.QueryParameters.Filter = filterQueryParameter; + } + + if (selectQueryParameters is not null && + selectQueryParameters.Count != 0) + { + rc.QueryParameters.Select = [.. selectQueryParameters]; + } + }; + + public static Action> CreateForMailFolders( + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters) => + rc => + { + if (expandQueryParameters is not null && + expandQueryParameters.Count != 0) + { + rc.QueryParameters.Expand = [.. expandQueryParameters]; + } + + if (!string.IsNullOrEmpty(filterQueryParameter)) + { + rc.QueryParameters.Filter = filterQueryParameter; + } + + if (selectQueryParameters is not null && + selectQueryParameters.Count != 0) + { + rc.QueryParameters.Select = [.. selectQueryParameters]; + } + }; + + public static Action> CreateForMessagesMailFolder( + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters) => + rc => + { + if (expandQueryParameters is not null && + expandQueryParameters.Count != 0) + { + rc.QueryParameters.Expand = [.. expandQueryParameters]; + } + + if (!string.IsNullOrEmpty(filterQueryParameter)) + { + rc.QueryParameters.Filter = filterQueryParameter; + } + + if (selectQueryParameters is not null && + selectQueryParameters.Count != 0) + { + rc.QueryParameters.Select = [.. selectQueryParameters]; + } + }; + + public static Action> CreateForMessagesUserId( + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters) => + rc => + { + if (expandQueryParameters is not null && + expandQueryParameters.Count != 0) + { + rc.QueryParameters.Expand = [.. expandQueryParameters]; + } + + if (!string.IsNullOrEmpty(filterQueryParameter)) + { + rc.QueryParameters.Filter = filterQueryParameter; + } + + if (selectQueryParameters is not null && + selectQueryParameters.Count != 0) + { + 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.Count != 0) + { + rc.QueryParameters.Select = [.. selectQueryParameters]; + } + }; + + public static Action> CreateForSites( + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters) => + rc => + { + if (expandQueryParameters is not null && + expandQueryParameters.Count != 0) + { + rc.QueryParameters.Expand = [.. expandQueryParameters]; + } + + if (!string.IsNullOrEmpty(filterQueryParameter)) + { + rc.QueryParameters.Filter = filterQueryParameter; + } + + if (selectQueryParameters is not null && + selectQueryParameters.Count != 0) + { + rc.QueryParameters.Select = [.. selectQueryParameters]; + } + }; + + public static Action> CreateForTeams( + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters) => + rc => + { + if (expandQueryParameters is not null && + expandQueryParameters.Count != 0) + { + rc.QueryParameters.Expand = [.. expandQueryParameters]; + } + + if (!string.IsNullOrEmpty(filterQueryParameter)) + { + rc.QueryParameters.Filter = filterQueryParameter; + } + + if (selectQueryParameters is not null && + selectQueryParameters.Count != 0) + { + rc.QueryParameters.Select = [.. selectQueryParameters]; + } + }; + + public static Action> CreateForUsers( + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters) => + rc => + { + if (expandQueryParameters is not null && + expandQueryParameters.Count != 0) + { + rc.QueryParameters.Expand = [.. expandQueryParameters]; + } + + if (!string.IsNullOrEmpty(filterQueryParameter)) + { + rc.QueryParameters.Filter = filterQueryParameter; + } + + if (selectQueryParameters is not null && + selectQueryParameters.Count != 0) + { + 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 new file mode 100644 index 0000000..9b6cd30 --- /dev/null +++ b/src/Atc.Microsoft.Graph.Client/GlobalUsings.cs @@ -0,0 +1,29 @@ +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.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.Core; +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 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/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 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..8b346b6 --- /dev/null +++ b/src/Atc.Microsoft.Graph.Client/Options/GraphServiceOptions.cs @@ -0,0 +1,17 @@ +namespace Atc.Microsoft.Graph.Client.Options; + +public sealed class GraphServiceOptions +{ + public string TenantId { get; set; } = string.Empty; + + public string ClientId { get; set; } = string.Empty; + + 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 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..2dbead4 --- /dev/null +++ b/src/Atc.Microsoft.Graph.Client/Services/GraphServiceClientWrapper.cs @@ -0,0 +1,45 @@ +namespace Atc.Microsoft.Graph.Client.Services; + +public abstract partial class GraphServiceClientWrapper +{ + protected GraphServiceClient Client { get; } + + protected ResiliencePipeline DownloadResiliencePipeline { get; } + + protected 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..1678be2 --- /dev/null +++ b/src/Atc.Microsoft.Graph.Client/Services/GraphServiceClientWrapperLoggerMessages.cs @@ -0,0 +1,118 @@ +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.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, + 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 new file mode 100644 index 0000000..1a5e0ec --- /dev/null +++ b/src/Atc.Microsoft.Graph.Client/Services/OneDrive/IOneDriveGraphService.cs @@ -0,0 +1,38 @@ +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 = default); + + Task GetDriveByTeamId( + string teamId, + CancellationToken cancellationToken = default); + + Task GetDeltaTokenForDriveItemsByDriveId( + string driveId, + CancellationToken cancellationToken = default); + + Task<(HttpStatusCode StatusCode, IList Data)> GetDriveItemsByDriveId( + string driveId, + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters, + CancellationToken cancellationToken = default); + + Task<(HttpStatusCode StatusCode, IList Data)> GetDriveItemsByDriveIdAndDeltaToken( + string driveId, + string deltaToken, + string? filterQueryParameter, + List? selectQueryParameters, + CancellationToken cancellationToken = default); + + Task DownloadFile( + string driveId, + string fileId, + 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 new file mode 100644 index 0000000..dc25d7b --- /dev/null +++ b/src/Atc.Microsoft.Graph.Client/Services/OneDrive/OneDriveGraphService.cs @@ -0,0 +1,363 @@ +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 = default) + { + 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 = default) + { + 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 = default) + { + 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 = default) + { + 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 = default) + { + 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 = default) + { + 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 new file mode 100644 index 0000000..25eee9b --- /dev/null +++ b/src/Atc.Microsoft.Graph.Client/Services/Outlook/IOutlookGraphService.cs @@ -0,0 +1,43 @@ +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 = default); + + Task<(HttpStatusCode StatusCode, IList Data)> GetMailFoldersByUserIdAndFolderId( + string userId, + string folderId, + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters, + CancellationToken cancellationToken = default); + + Task<(HttpStatusCode StatusCode, IList Data)> GetMessagesByUserId( + string userId, + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters, + CancellationToken cancellationToken = default); + + Task<(HttpStatusCode StatusCode, IList Data, string? DeltaToken)> GetMessagesByUserIdAndFolderId( + string userId, + string folderId, + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters, + string? deltaToken = null, + CancellationToken cancellationToken = default); + + Task<(HttpStatusCode StatusCode, IList Data)> GetFileAttachmentsByUserIdAndMessageId( + string userId, + string messageId, + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters, + 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 new file mode 100644 index 0000000..8b41ae2 --- /dev/null +++ b/src/Atc.Microsoft.Graph.Client/Services/Outlook/OutlookGraphService.cs @@ -0,0 +1,539 @@ +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 = default) + { + 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 = default) + { + 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 = default) + { + 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, + string? deltaToken = null, + CancellationToken cancellationToken = default) + { + 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 = default) + { + 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 = default) + { + 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 = default) + { + 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 new file mode 100644 index 0000000..791706a --- /dev/null +++ b/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/ISharepointGraphService.cs @@ -0,0 +1,23 @@ +namespace Atc.Microsoft.Graph.Client.Services.Sharepoint; + +public interface ISharepointGraphService +{ + Task<(HttpStatusCode StatusCode, IList Data)> GetSites( + List? expandQueryParameters, + 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 new file mode 100644 index 0000000..d5b9823 --- /dev/null +++ b/src/Atc.Microsoft.Graph.Client/Services/Sharepoint/SharepointGraphService.cs @@ -0,0 +1,192 @@ +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 = default) + { + 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); + } + } + + public async Task<(HttpStatusCode StatusCode, Guid? SubscriptionId)> SetupSubscription( + Subscription subscription, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(subscription); + + 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 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..3aa4bc0 --- /dev/null +++ b/src/Atc.Microsoft.Graph.Client/Services/Teams/ITeamsGraphService.cs @@ -0,0 +1,10 @@ +namespace Atc.Microsoft.Graph.Client.Services.Teams; + +public interface ITeamsGraphService +{ + Task<(HttpStatusCode StatusCode, IList Data)> GetTeams( + List? expandQueryParameters, + string? filterQueryParameter, + List? selectQueryParameters, + 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 new file mode 100644 index 0000000..5caf889 --- /dev/null +++ b/src/Atc.Microsoft.Graph.Client/Services/Teams/TeamsGraphService.cs @@ -0,0 +1,87 @@ +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 = default) + { + 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..9bc96fa --- /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 = 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 new file mode 100644 index 0000000..2516220 --- /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 = default) + { + 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