From 259257f1c11e8fbb95bb538fc10529573e18621e Mon Sep 17 00:00:00 2001 From: Jeremie Desautels Date: Wed, 6 Mar 2019 15:27:04 -0500 Subject: [PATCH 01/41] Refresh resources --- build.cake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.cake b/build.cake index 4a102b41..6edb3ea0 100644 --- a/build.cake +++ b/build.cake @@ -2,10 +2,10 @@ #addin nuget:?package=Cake.Coveralls&version=0.9.0 // Install tools. -#tool nuget:?package=GitVersion.CommandLine&version=5.0.0-beta1-72 +#tool nuget:?package=GitVersion.CommandLine&version=5.0.0-beta2-6 #tool nuget:?package=GitReleaseManager&version=0.8.0 #tool nuget:?package=OpenCover&version=4.7.922 -#tool nuget:?package=ReportGenerator&version=4.0.13.1 +#tool nuget:?package=ReportGenerator&version=4.0.15 #tool nuget:?package=coveralls.io&version=1.4.2 #tool nuget:?package=xunit.runner.console&version=2.4.1 From c4b32dcaf820ce5fc70e2480bdbd2bb0c87c8e66 Mon Sep 17 00:00:00 2001 From: Jeremie Desautels Date: Sat, 30 Mar 2019 14:34:42 -0400 Subject: [PATCH 02/41] Basic structure --- .../ConsoleLogProvider.cs | 48 + .../ZoomNet.IntegrationTests/ConsoleUtils.cs | 32 + .../ZoomNet.IntegrationTests/NativeMethods.cs | 52 + Source/ZoomNet.IntegrationTests/Program.cs | 147 ++ .../ZoomNet.IntegrationTests.csproj | 16 + .../ZoomNet.UnitTests.csproj | 27 + Source/ZoomNet.sln | 23 +- Source/ZoomNet/Class1.cs | 8 - Source/ZoomNet/Client.cs | 236 ++ Source/ZoomNet/IClient.cs | 64 + Source/ZoomNet/Logging/LibLog.cs | 2329 +++++++++++++++++ Source/ZoomNet/Models/PaginatedResponse.cs | 45 + Source/ZoomNet/Properties/AssemblyInfo.cs | 13 + Source/ZoomNet/Utilities/DiagnosticHandler.cs | 150 ++ Source/ZoomNet/Utilities/Extensions.cs | 484 ++++ Source/ZoomNet/Utilities/JwtTokenHandler.cs | 71 + Source/ZoomNet/Utilities/ZoomErrorHandler.cs | 81 + Source/ZoomNet/ZoomNet.csproj | 15 +- 18 files changed, 3822 insertions(+), 19 deletions(-) create mode 100644 Source/ZoomNet.IntegrationTests/ConsoleLogProvider.cs create mode 100644 Source/ZoomNet.IntegrationTests/ConsoleUtils.cs create mode 100644 Source/ZoomNet.IntegrationTests/NativeMethods.cs create mode 100644 Source/ZoomNet.IntegrationTests/Program.cs create mode 100644 Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj create mode 100644 Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj delete mode 100644 Source/ZoomNet/Class1.cs create mode 100644 Source/ZoomNet/Client.cs create mode 100644 Source/ZoomNet/IClient.cs create mode 100644 Source/ZoomNet/Logging/LibLog.cs create mode 100644 Source/ZoomNet/Models/PaginatedResponse.cs create mode 100644 Source/ZoomNet/Properties/AssemblyInfo.cs create mode 100644 Source/ZoomNet/Utilities/DiagnosticHandler.cs create mode 100644 Source/ZoomNet/Utilities/Extensions.cs create mode 100644 Source/ZoomNet/Utilities/JwtTokenHandler.cs create mode 100644 Source/ZoomNet/Utilities/ZoomErrorHandler.cs diff --git a/Source/ZoomNet.IntegrationTests/ConsoleLogProvider.cs b/Source/ZoomNet.IntegrationTests/ConsoleLogProvider.cs new file mode 100644 index 00000000..8990507b --- /dev/null +++ b/Source/ZoomNet.IntegrationTests/ConsoleLogProvider.cs @@ -0,0 +1,48 @@ +namespace ZoomNet.IntegrationTests +{ + using Logging; + using System; + using System.Globalization; + + // Inspired by: https://github.com/damianh/LibLog/blob/master/src/LibLog.Example.ColoredConsoleLogProvider/ColoredConsoleLogProvider.cs + public class ConsoleLogProvider : ILogProvider + { + public Logger GetLogger(string name) + { + return (logLevel, messageFunc, exception, formatParameters) => + { + if (messageFunc == null) + { + return true; // All log levels are enabled + } + + var message = string.Format(CultureInfo.InvariantCulture, messageFunc(), formatParameters); + if (exception != null) + { + message = $"{message} | {exception}"; + } + Console.WriteLine($"{DateTime.UtcNow} | {logLevel} | {name} | {message}"); + + return true; + }; + } + + public IDisposable OpenNestedContext(string message) + { + return NullDisposable.Instance; + } + + public IDisposable OpenMappedContext(string key, string value) + { + return NullDisposable.Instance; + } + + private class NullDisposable : IDisposable + { + internal static readonly IDisposable Instance = new NullDisposable(); + + public void Dispose() + { } + } + } +} diff --git a/Source/ZoomNet.IntegrationTests/ConsoleUtils.cs b/Source/ZoomNet.IntegrationTests/ConsoleUtils.cs new file mode 100644 index 00000000..f8a28bdb --- /dev/null +++ b/Source/ZoomNet.IntegrationTests/ConsoleUtils.cs @@ -0,0 +1,32 @@ +using System; + +namespace ZoomNet.IntegrationTests +{ + public static class ConsoleUtils + { + public static void CenterConsole() + { + var hWin = NativeMethods.GetConsoleWindow(); + if (hWin == IntPtr.Zero) return; + + var monitor = NativeMethods.MonitorFromWindow(hWin, NativeMethods.MONITOR_DEFAULT_TO_NEAREST); + if (monitor == IntPtr.Zero) return; + + var monitorInfo = new NativeMethods.NativeMonitorInfo(); + NativeMethods.GetMonitorInfo(monitor, monitorInfo); + + NativeMethods.GetWindowRect(hWin, out NativeMethods.NativeRectangle consoleInfo); + + var monitorWidth = monitorInfo.Monitor.Right - monitorInfo.Monitor.Left; + var monitorHeight = monitorInfo.Monitor.Bottom - monitorInfo.Monitor.Top; + + var consoleWidth = consoleInfo.Right - consoleInfo.Left; + var consoleHeight = consoleInfo.Bottom - consoleInfo.Top; + + var left = monitorInfo.Monitor.Left + (monitorWidth - consoleWidth) / 2; + var top = monitorInfo.Monitor.Top + (monitorHeight - consoleHeight) / 2; + + NativeMethods.MoveWindow(hWin, left, top, consoleWidth, consoleHeight, false); + } + } +} diff --git a/Source/ZoomNet.IntegrationTests/NativeMethods.cs b/Source/ZoomNet.IntegrationTests/NativeMethods.cs new file mode 100644 index 00000000..b572fc0c --- /dev/null +++ b/Source/ZoomNet.IntegrationTests/NativeMethods.cs @@ -0,0 +1,52 @@ +using System; +using System.Runtime.InteropServices; + +namespace ZoomNet.IntegrationTests +{ + public static class NativeMethods + { + public const Int32 MONITOR_DEFAULT_TO_PRIMARY = 0x00000001; + public const Int32 MONITOR_DEFAULT_TO_NEAREST = 0x00000002; + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern IntPtr GetConsoleWindow(); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool GetWindowRect(IntPtr hWnd, out NativeRectangle rc); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool MoveWindow(IntPtr hWnd, int x, int y, int w, int h, bool repaint); + + [DllImport("user32.dll")] + public static extern IntPtr MonitorFromWindow(IntPtr handle, Int32 flags); + + [DllImport("user32.dll")] + public static extern Boolean GetMonitorInfo(IntPtr hMonitor, NativeMonitorInfo lpmi); + + [Serializable, StructLayout(LayoutKind.Sequential)] + public struct NativeRectangle + { + public Int32 Left; + public Int32 Top; + public Int32 Right; + public Int32 Bottom; + + public NativeRectangle(Int32 left, Int32 top, Int32 right, Int32 bottom) + { + this.Left = left; + this.Top = top; + this.Right = right; + this.Bottom = bottom; + } + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public sealed class NativeMonitorInfo + { + public Int32 Size = Marshal.SizeOf(typeof(NativeMonitorInfo)); + public NativeRectangle Monitor; + public NativeRectangle Work; + public Int32 Flags; + } + } +} diff --git a/Source/ZoomNet.IntegrationTests/Program.cs b/Source/ZoomNet.IntegrationTests/Program.cs new file mode 100644 index 00000000..9129e75a --- /dev/null +++ b/Source/ZoomNet.IntegrationTests/Program.cs @@ -0,0 +1,147 @@ +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using ZoomNet.Logging; +using ZoomNet.Utilities; + +namespace ZoomNet.IntegrationTests +{ + class Program + { + private const int MAX_ZOOM_API_CONCURRENCY = 5; + + private enum ResultCodes + { + Success = 0, + Exception = 1, + Cancelled = 1223 + } + + static async Task Main() + { + // ----------------------------------------------------------------------------- + // Do you want to proxy requests through Fiddler (useful for debugging)? + var useFiddler = true; + + // As an alternative to Fiddler, you can display debug information about + // every HTTP request/response in the console. This is useful for debugging + // purposes but the amount of information can be overwhelming. + var debugHttpMessagesToConsole = false; + // ----------------------------------------------------------------------------- + + var apiKey = Environment.GetEnvironmentVariable("ZOOM_APIKEY"); + var apiSecret = Environment.GetEnvironmentVariable("ZOOM_APISECRET"); + var userId = Environment.GetEnvironmentVariable("ZOOM_USERID"); + var client = useFiddler ? new Client(apiKey, apiSecret, new WebProxy("http://localhost:8888")) : new Client(apiKey, apiSecret); + + if (debugHttpMessagesToConsole) + { + LogProvider.SetCurrentLogProvider(new ConsoleLogProvider()); + } + + var source = new CancellationTokenSource(); + Console.CancelKeyPress += (s, e) => + { + e.Cancel = true; + source.Cancel(); + }; + + // Ensure the Console is tall enough and centered on the screen + Console.WindowHeight = Math.Min(60, Console.LargestWindowHeight); + ConsoleUtils.CenterConsole(); + + // These are the integration tests that we will execute + var integrationTests = new Func[] + { + }; + + + // Execute the async tests in parallel (with max degree of parallelism) + var results = await integrationTests.ForEachAsync( + async integrationTest => + { + var log = new StringWriter(); + + try + { + await integrationTest(userId, client, log, source.Token).ConfigureAwait(false); + return (TestName: integrationTest.Method.Name, ResultCode: ResultCodes.Success, Message: string.Empty); + } + catch (OperationCanceledException) + { + await log.WriteLineAsync($"-----> TASK CANCELLED").ConfigureAwait(false); + return (TestName: integrationTest.Method.Name, ResultCode: ResultCodes.Cancelled, Message: "Task cancelled"); + } + catch (Exception e) + { + var exceptionMessage = e.GetBaseException().Message; + await log.WriteLineAsync($"-----> AN EXCEPTION OCCURED: {exceptionMessage}").ConfigureAwait(false); + return (TestName: integrationTest.Method.Name, ResultCode: ResultCodes.Exception, Message: exceptionMessage); + } + finally + { + await Console.Out.WriteLineAsync(log.ToString()).ConfigureAwait(false); + } + }, MAX_ZOOM_API_CONCURRENCY) + .ConfigureAwait(false); + + // Display summary + var summary = new StringWriter(); + await summary.WriteLineAsync("\n\n**************************************************").ConfigureAwait(false); + await summary.WriteLineAsync("******************** SUMMARY *********************").ConfigureAwait(false); + await summary.WriteLineAsync("**************************************************").ConfigureAwait(false); + + var resultsWithMessage = results + .Where(r => !string.IsNullOrEmpty(r.Message)) + .ToArray(); + + if (resultsWithMessage.Any()) + { + foreach (var (TestName, ResultCode, Message) in resultsWithMessage) + { + const int TEST_NAME_MAX_LENGTH = 25; + var name = TestName.Length <= TEST_NAME_MAX_LENGTH ? TestName : TestName.Substring(0, TEST_NAME_MAX_LENGTH - 3) + "..."; + await summary.WriteLineAsync($"{name.PadRight(TEST_NAME_MAX_LENGTH, ' ')} : {Message}").ConfigureAwait(false); + } + } + else + { + await summary.WriteLineAsync("All tests completed succesfully").ConfigureAwait(false); + } + + await summary.WriteLineAsync("**************************************************").ConfigureAwait(false); + await Console.Out.WriteLineAsync(summary.ToString()).ConfigureAwait(false); + + // Prompt user to press a key in order to allow reading the log in the console + var promptLog = new StringWriter(); + await promptLog.WriteLineAsync("\n\n**************************************************").ConfigureAwait(false); + await promptLog.WriteLineAsync("Press any key to exit").ConfigureAwait(false); + Prompt(promptLog.ToString()); + + // Return code indicating success/failure + var resultCode = (int)ResultCodes.Success; + if (results.Any(result => result.ResultCode != ResultCodes.Success)) + { + if (results.Any(result => result.ResultCode == ResultCodes.Exception)) resultCode = (int)ResultCodes.Exception; + else if (results.Any(result => result.ResultCode == ResultCodes.Cancelled)) resultCode = (int)ResultCodes.Cancelled; + else resultCode = (int)results.First(result => result.ResultCode != ResultCodes.Success).ResultCode; + } + + return await Task.FromResult(resultCode); + } + + private static char Prompt(string prompt) + { + while (Console.KeyAvailable) + { + Console.ReadKey(false); + } + Console.Out.WriteLine(prompt); + var result = Console.ReadKey(); + return result.KeyChar; + } + } +} diff --git a/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj b/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj new file mode 100644 index 00000000..fbaea4aa --- /dev/null +++ b/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj @@ -0,0 +1,16 @@ + + + + Exe + netcoreapp2.1 + + + + latest + + + + + + + diff --git a/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj b/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj new file mode 100644 index 00000000..dee552a4 --- /dev/null +++ b/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj @@ -0,0 +1,27 @@ + + + + net452;netcoreapp2.0 + StrongGrid.UnitTests + StrongGrid.UnitTests + false + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + diff --git a/Source/ZoomNet.sln b/Source/ZoomNet.sln index d67e6038..e75cf978 100644 --- a/Source/ZoomNet.sln +++ b/Source/ZoomNet.sln @@ -3,7 +3,15 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.28307.421 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZoomNet", "ZoomNet\ZoomNet.csproj", "{1F1336D3-20EE-4EFD-868B-A5FD6E9F260D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ZoomNet", "ZoomNet\ZoomNet.csproj", "{1F1336D3-20EE-4EFD-868B-A5FD6E9F260D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZoomNet.IntegrationTests", "ZoomNet.IntegrationTests\ZoomNet.IntegrationTests.csproj", "{86BE46FC-FD82-45B3-8092-0C9AC1A94E8A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Source", "Source", "{8D45A893-7A48-4D45-9FD7-424B12D4C672}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{85E5BB2F-54EA-4C92-9F02-4F8DD73F8975}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ZoomNet.UnitTests", "ZoomNet.UnitTests\ZoomNet.UnitTests.csproj", "{51646A3A-2B1E-43A6-A37D-7E9B3B965CCD}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -15,10 +23,23 @@ Global {1F1336D3-20EE-4EFD-868B-A5FD6E9F260D}.Debug|Any CPU.Build.0 = Debug|Any CPU {1F1336D3-20EE-4EFD-868B-A5FD6E9F260D}.Release|Any CPU.ActiveCfg = Release|Any CPU {1F1336D3-20EE-4EFD-868B-A5FD6E9F260D}.Release|Any CPU.Build.0 = Release|Any CPU + {86BE46FC-FD82-45B3-8092-0C9AC1A94E8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {86BE46FC-FD82-45B3-8092-0C9AC1A94E8A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {86BE46FC-FD82-45B3-8092-0C9AC1A94E8A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {86BE46FC-FD82-45B3-8092-0C9AC1A94E8A}.Release|Any CPU.Build.0 = Release|Any CPU + {51646A3A-2B1E-43A6-A37D-7E9B3B965CCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {51646A3A-2B1E-43A6-A37D-7E9B3B965CCD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {51646A3A-2B1E-43A6-A37D-7E9B3B965CCD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {51646A3A-2B1E-43A6-A37D-7E9B3B965CCD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {1F1336D3-20EE-4EFD-868B-A5FD6E9F260D} = {8D45A893-7A48-4D45-9FD7-424B12D4C672} + {86BE46FC-FD82-45B3-8092-0C9AC1A94E8A} = {85E5BB2F-54EA-4C92-9F02-4F8DD73F8975} + {51646A3A-2B1E-43A6-A37D-7E9B3B965CCD} = {85E5BB2F-54EA-4C92-9F02-4F8DD73F8975} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BA63CED4-60E8-45F7-BC86-116E79B9CB86} EndGlobalSection diff --git a/Source/ZoomNet/Class1.cs b/Source/ZoomNet/Class1.cs deleted file mode 100644 index 7518d107..00000000 --- a/Source/ZoomNet/Class1.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace ZoomNet -{ - public class Class1 - { - } -} diff --git a/Source/ZoomNet/Client.cs b/Source/ZoomNet/Client.cs new file mode 100644 index 00000000..d1e2b819 --- /dev/null +++ b/Source/ZoomNet/Client.cs @@ -0,0 +1,236 @@ +using Pathoschild.Http.Client; +using Pathoschild.Http.Client.Extensibility; +using System; +using System.Net; +using System.Net.Http; +using System.Reflection; +using ZoomNet.Utilities; + +namespace ZoomNet +{ + /// + /// REST client for interacting with ZoomNet's API. + /// + public class Client : IClient, IDisposable + { + #region FIELDS + + private const string ZOOM_V2_BASE_URI = "https://api.zoom.us/v2"; + private readonly bool _mustDisposeHttpClient; + + private HttpClient _httpClient; + private Pathoschild.Http.Client.IClient _fluentClient; + + #endregion + + #region PROPERTIES + + /// + /// Gets the resource which allows you to manage sub accounts. + /// + /// + /// The accounts resource. + /// + //public IAccounts Accounts { get; private set; } + + /// + /// Gets the resource which allows you to manage billing information. + /// + /// + /// The billing resource. + /// + //public IBillingInformation BillingInformation { get; private set; } + + /// + /// Gets the resource which allows you to manage users. + /// + /// + /// The users resource. + /// + //public IUsers Users { get; private set; } + + /// + /// Gets the resource wich alloes you to manage roles. + /// + /// + /// The roles resource. + /// + //public IRoles Roles { get; private set; } + + /// + /// Gets the resource which allows you to manage meetings. + /// + /// + /// The meetings resource. + /// + //public IMeetings Meetings { get; private set; } + + /// + /// Gets the resource which allows you to manage webinars. + /// + /// + /// The webinars resource. + /// + //public IWebinars Webinars { get; private set; } + + /// + /// Gets the Version. + /// + /// + /// The version. + /// + public string Version { get; private set; } + + #endregion + + #region CTOR + + /// + /// Initializes a new instance of the class. + /// + /// Your Zoom API Key. + /// Your Zoom API Secret. + public Client(string apiKey, string apiSecret) + : this(apiKey, apiSecret, null, false) + { + } + + /// + /// Initializes a new instance of the class with a specific proxy. + /// + /// Your Zoom API Key. + /// Your Zoom API Secret. + /// Allows you to specify a proxy. + public Client(string apiKey, string apiSecret, IWebProxy proxy) + : this(apiKey, apiSecret, new HttpClient(new HttpClientHandler { Proxy = proxy, UseProxy = proxy != null }), true) + { + } + + /// + /// Initializes a new instance of the class with a specific handler. + /// + /// Your Zoom API Key. + /// Your Zoom API Secret. + /// TThe HTTP handler stack to use for sending requests. + public Client(string apiKey, string apiSecret, HttpMessageHandler handler) + : this(apiKey, apiSecret, new HttpClient(handler), true) + { + } + + /// + /// Initializes a new instance of the class with a specific http client. + /// + /// Your Zoom API Key. + /// Your Zoom API Secret. + /// Allows you to inject your own HttpClient. This is useful, for example, to setup the HtppClient with a proxy. + public Client(string apiKey, string apiSecret, HttpClient httpClient) + : this(apiKey, apiSecret, httpClient, false) + { + } + + private Client(string apiKey, string apiSecret, HttpClient httpClient, bool disposeClient) + { + _mustDisposeHttpClient = disposeClient; + _httpClient = httpClient; + + Version = typeof(Client).GetTypeInfo().Assembly.GetName().Version.ToString(3); +#if DEBUG + Version = "DEBUG"; +#endif + + _fluentClient = new FluentClient(new Uri(ZOOM_V2_BASE_URI), httpClient) + .SetUserAgent($"ZoomNet/{Version} (+https://github.com/Jericho/ZoomNet)"); + //.SetRequestCoordinator(new ZoomRetryStrategy()); + + _fluentClient.Filters.Remove(); + _fluentClient.Filters.Add(new JwtTokenHandler(apiKey, apiSecret)); + _fluentClient.Filters.Add(new DiagnosticHandler()); + _fluentClient.Filters.Add(new ZoomErrorHandler()); + + _fluentClient.SetOptions(new FluentClientOptions() + { + IgnoreHttpErrors = false, + IgnoreNullArguments = true + }); + + //Accounts = new Accounts(_fluentClient); + //BillingInformation = new BillingInformation(_fluentClient); + //Users = new Users(_fluentClient); + //Roles = new Roles(_fluentClient); + //Meetings = new Meetings(_fluentClient); + //Webinars = new Webinars(_fluentClient); + } + + /// + /// Finalizes an instance of the class. + /// + ~Client() + { + // The object went out of scope and finalized is called. + // Call 'Dispose' to release unmanaged resources + // Managed resources will be released when GC runs the next time. + Dispose(false); + } + + #endregion + + #region PUBLIC METHODS + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + // Call 'Dispose' to release resources + Dispose(true); + + // Tell the GC that we have done the cleanup and there is nothing left for the Finalizer to do + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + ReleaseManagedResources(); + } + else + { + // The object went out of scope and the Finalizer has been called. + // The GC will take care of releasing managed resources, therefore there is nothing to do here. + } + + ReleaseUnmanagedResources(); + } + + #endregion + + #region PRIVATE METHODS + + private void ReleaseManagedResources() + { + if (_fluentClient != null) + { + _fluentClient.Dispose(); + _fluentClient = null; + } + + if (_httpClient != null && _mustDisposeHttpClient) + { + _httpClient.Dispose(); + _httpClient = null; + } + } + + private void ReleaseUnmanagedResources() + { + // We do not hold references to unmanaged resources + } + + #endregion + } +} diff --git a/Source/ZoomNet/IClient.cs b/Source/ZoomNet/IClient.cs new file mode 100644 index 00000000..17d67667 --- /dev/null +++ b/Source/ZoomNet/IClient.cs @@ -0,0 +1,64 @@ +namespace ZoomNet +{ + /// + /// Interface for the Zoom REST client. + /// + public interface IClient + { + /// + /// Gets the resource which allows you to manage sub accounts. + /// + /// + /// The accounts resource. + /// + //IAccounts Accounts { get; } + + /// + /// Gets the resource which allows you to manage billing information. + /// + /// + /// The billing resource. + /// + //IBillingInformation BillingInformation { get; } + + /// + /// Gets the resource which allows you to manage users. + /// + /// + /// The users resource. + /// + //IUsers Users { get; } + + /// + /// Gets the resource wich alloes you to manage roles. + /// + /// + /// The roles resource. + /// + //IRoles Roles { get; } + + /// + /// Gets the resource which allows you to manage meetings. + /// + /// + /// The meetings resource. + /// + //IMeetings Meetings { get; } + + /// + /// Gets the resource which allows you to manage webinars. + /// + /// + /// The webinars resource. + /// + //IWebinars Webinars { get; } + + /// + /// Gets the Version. + /// + /// + /// The version. + /// + string Version { get; } + } +} diff --git a/Source/ZoomNet/Logging/LibLog.cs b/Source/ZoomNet/Logging/LibLog.cs new file mode 100644 index 00000000..4eac410d --- /dev/null +++ b/Source/ZoomNet/Logging/LibLog.cs @@ -0,0 +1,2329 @@ +//------------------------------------------------------------------------------ +// +// This tag ensures the content of this file is not analyzed by StyleCop.Analyzers +// +//------------------------------------------------------------------------------ + +//=============================================================================== +// LibLog +// +// https://github.com/damianh/LibLog +//=============================================================================== +// Copyright © 2011-2015 Damian Hickey. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +//=============================================================================== + +// ReSharper disable PossibleNullReferenceException + +// Define LIBLOG_PORTABLE conditional compilation symbol for PCL compatibility +// +// Define LIBLOG_PUBLIC to enable ability to GET a logger (LogProvider.For<>() etc) from outside this library. NOTE: +// this can have unintended consequences of consumers of your library using your library to resolve a logger. If the +// reason is because you want to open this functionality to other projects within your solution, +// consider [InternalsVisibleTo] instead. +// +// Define LIBLOG_PROVIDERS_ONLY if your library provides its own logging API and you just want to use the +// LibLog providers internally to provide built in support for popular logging frameworks. + +#pragma warning disable 1591 + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "ZoomNet.Logging")] +[assembly: SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Scope = "member", Target = "ZoomNet.Logging.Logger.#Invoke(ZoomNet.Logging.LogLevel,System.Func`1,System.Exception,System.Object[])")] + +// If you copied this file manually, you need to change all "ZoomNet.Logging" so not to clash with other libraries +// that use LibLog +#if LIBLOG_PROVIDERS_ONLY +namespace ZoomNet.Logging.LibLog +#else +namespace ZoomNet.Logging +#endif +{ + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; +#if LIBLOG_PROVIDERS_ONLY + using ZoomNet.Logging.LibLog.LogProviders; +#else + using ZoomNet.Logging.LogProviders; +#endif + using System; +#if !LIBLOG_PROVIDERS_ONLY + using System.Diagnostics; +#if !LIBLOG_PORTABLE + using System.Runtime.CompilerServices; +#endif +#endif + +#if LIBLOG_PROVIDERS_ONLY + internal +#else + public +#endif + delegate bool Logger(LogLevel logLevel, Func messageFunc, Exception exception = null, params object[] formatParameters); + +#if !LIBLOG_PROVIDERS_ONLY + /// + /// Simple interface that represent a logger. + /// +#if LIBLOG_PUBLIC + public +#else + internal +#endif + interface ILog + { + /// + /// Log a message the specified log level. + /// + /// The log level. + /// The message function. + /// An optional exception. + /// Optional format parameters for the message generated by the messagefunc. + /// true if the message was logged. Otherwise false. + /// + /// Note to implementers: the message func should not be called if the loglevel is not enabled + /// so as not to incur performance penalties. + /// + /// To check IsEnabled call Log with only LogLevel and check the return value, no event will be written. + /// + bool Log(LogLevel logLevel, Func messageFunc, Exception exception = null, params object[] formatParameters); + } +#endif + + /// + /// The log level. + /// +#if LIBLOG_PROVIDERS_ONLY + internal +#else + public +#endif + enum LogLevel + { + Trace, + Debug, + Info, + Warn, + Error, + Fatal + } + +#if !LIBLOG_PROVIDERS_ONLY +#if LIBLOG_PUBLIC + public +#else + internal +#endif + static partial class LogExtensions + { + public static bool IsDebugEnabled(this ILog logger) + { + GuardAgainstNullLogger(logger); + return logger.Log(LogLevel.Debug, null); + } + + public static bool IsErrorEnabled(this ILog logger) + { + GuardAgainstNullLogger(logger); + return logger.Log(LogLevel.Error, null); + } + + public static bool IsFatalEnabled(this ILog logger) + { + GuardAgainstNullLogger(logger); + return logger.Log(LogLevel.Fatal, null); + } + + public static bool IsInfoEnabled(this ILog logger) + { + GuardAgainstNullLogger(logger); + return logger.Log(LogLevel.Info, null); + } + + public static bool IsTraceEnabled(this ILog logger) + { + GuardAgainstNullLogger(logger); + return logger.Log(LogLevel.Trace, null); + } + + public static bool IsWarnEnabled(this ILog logger) + { + GuardAgainstNullLogger(logger); + return logger.Log(LogLevel.Warn, null); + } + + public static void Debug(this ILog logger, Func messageFunc) + { + GuardAgainstNullLogger(logger); + logger.Log(LogLevel.Debug, messageFunc); + } + + public static void Debug(this ILog logger, string message) + { + if (logger.IsDebugEnabled()) + { + logger.Log(LogLevel.Debug, message.AsFunc()); + } + } + + public static void Debug(this ILog logger, string message, params object[] args) + { + logger.DebugFormat(message, args); + } + + public static void Debug(this ILog logger, Exception exception, string message, params object[] args) + { + logger.DebugException(message, exception, args); + } + + public static void DebugFormat(this ILog logger, string message, params object[] args) + { + if (logger.IsDebugEnabled()) + { + logger.LogFormat(LogLevel.Debug, message, args); + } + } + + public static void DebugException(this ILog logger, string message, Exception exception) + { + if (logger.IsDebugEnabled()) + { + logger.Log(LogLevel.Debug, message.AsFunc(), exception); + } + } + + public static void DebugException(this ILog logger, string message, Exception exception, params object[] formatParams) + { + if (logger.IsDebugEnabled()) + { + logger.Log(LogLevel.Debug, message.AsFunc(), exception, formatParams); + } + } + + public static void Error(this ILog logger, Func messageFunc) + { + GuardAgainstNullLogger(logger); + logger.Log(LogLevel.Error, messageFunc); + } + + public static void Error(this ILog logger, string message) + { + if (logger.IsErrorEnabled()) + { + logger.Log(LogLevel.Error, message.AsFunc()); + } + } + + public static void Error(this ILog logger, string message, params object[] args) + { + logger.ErrorFormat(message, args); + } + + public static void Error(this ILog logger, Exception exception, string message, params object[] args) + { + logger.ErrorException(message, exception, args); + } + + public static void ErrorFormat(this ILog logger, string message, params object[] args) + { + if (logger.IsErrorEnabled()) + { + logger.LogFormat(LogLevel.Error, message, args); + } + } + + public static void ErrorException(this ILog logger, string message, Exception exception, params object[] formatParams) + { + if (logger.IsErrorEnabled()) + { + logger.Log(LogLevel.Error, message.AsFunc(), exception, formatParams); + } + } + + public static void Fatal(this ILog logger, Func messageFunc) + { + logger.Log(LogLevel.Fatal, messageFunc); + } + + public static void Fatal(this ILog logger, string message) + { + if (logger.IsFatalEnabled()) + { + logger.Log(LogLevel.Fatal, message.AsFunc()); + } + } + + public static void Fatal(this ILog logger, string message, params object[] args) + { + logger.FatalFormat(message, args); + } + + public static void Fatal(this ILog logger, Exception exception, string message, params object[] args) + { + logger.FatalException(message, exception, args); + } + + public static void FatalFormat(this ILog logger, string message, params object[] args) + { + if (logger.IsFatalEnabled()) + { + logger.LogFormat(LogLevel.Fatal, message, args); + } + } + + public static void FatalException(this ILog logger, string message, Exception exception, params object[] formatParams) + { + if (logger.IsFatalEnabled()) + { + logger.Log(LogLevel.Fatal, message.AsFunc(), exception, formatParams); + } + } + + public static void Info(this ILog logger, Func messageFunc) + { + GuardAgainstNullLogger(logger); + logger.Log(LogLevel.Info, messageFunc); + } + + public static void Info(this ILog logger, string message) + { + if (logger.IsInfoEnabled()) + { + logger.Log(LogLevel.Info, message.AsFunc()); + } + } + + public static void Info(this ILog logger, string message, params object[] args) + { + logger.InfoFormat(message, args); + } + + public static void Info(this ILog logger, Exception exception, string message, params object[] args) + { + logger.InfoException(message, exception, args); + } + + public static void InfoFormat(this ILog logger, string message, params object[] args) + { + if (logger.IsInfoEnabled()) + { + logger.LogFormat(LogLevel.Info, message, args); + } + } + + public static void InfoException(this ILog logger, string message, Exception exception, params object[] formatParams) + { + if (logger.IsInfoEnabled()) + { + logger.Log(LogLevel.Info, message.AsFunc(), exception, formatParams); + } + } + + public static void Trace(this ILog logger, Func messageFunc) + { + GuardAgainstNullLogger(logger); + logger.Log(LogLevel.Trace, messageFunc); + } + + public static void Trace(this ILog logger, string message) + { + if (logger.IsTraceEnabled()) + { + logger.Log(LogLevel.Trace, message.AsFunc()); + } + } + + public static void Trace(this ILog logger, string message, params object[] args) + { + logger.TraceFormat(message, args); + } + + public static void Trace(this ILog logger, Exception exception, string message, params object[] args) + { + logger.TraceException(message, exception, args); + } + + public static void TraceFormat(this ILog logger, string message, params object[] args) + { + if (logger.IsTraceEnabled()) + { + logger.LogFormat(LogLevel.Trace, message, args); + } + } + + public static void TraceException(this ILog logger, string message, Exception exception, params object[] formatParams) + { + if (logger.IsTraceEnabled()) + { + logger.Log(LogLevel.Trace, message.AsFunc(), exception, formatParams); + } + } + + public static void Warn(this ILog logger, Func messageFunc) + { + GuardAgainstNullLogger(logger); + logger.Log(LogLevel.Warn, messageFunc); + } + + public static void Warn(this ILog logger, string message) + { + if (logger.IsWarnEnabled()) + { + logger.Log(LogLevel.Warn, message.AsFunc()); + } + } + + public static void Warn(this ILog logger, string message, params object[] args) + { + logger.WarnFormat(message, args); + } + + public static void Warn(this ILog logger, Exception exception, string message, params object[] args) + { + logger.WarnException(message, exception, args); + } + + public static void WarnFormat(this ILog logger, string message, params object[] args) + { + if (logger.IsWarnEnabled()) + { + logger.LogFormat(LogLevel.Warn, message, args); + } + } + + public static void WarnException(this ILog logger, string message, Exception exception, params object[] formatParams) + { + if (logger.IsWarnEnabled()) + { + logger.Log(LogLevel.Warn, message.AsFunc(), exception, formatParams); + } + } + + // ReSharper disable once UnusedParameter.Local + private static void GuardAgainstNullLogger(ILog logger) + { + if (logger == null) + { + throw new ArgumentNullException("logger"); + } + } + + private static void LogFormat(this ILog logger, LogLevel logLevel, string message, params object[] args) + { + logger.Log(logLevel, message.AsFunc(), null, args); + } + + // Avoid the closure allocation, see https://gist.github.com/AArnott/d285feef75c18f6ecd2b + private static Func AsFunc(this T value) where T : class + { + return value.Return; + } + + private static T Return(this T value) + { + return value; + } + } +#endif + + /// + /// Represents a way to get a + /// +#if LIBLOG_PROVIDERS_ONLY + internal +#else + public +#endif + interface ILogProvider + { + /// + /// Gets the specified named logger. + /// + /// Name of the logger. + /// The logger reference. + Logger GetLogger(string name); + + /// + /// Opens a nested diagnostics context. Not supported in EntLib logging. + /// + /// The message to add to the diagnostics context. + /// A disposable that when disposed removes the message from the context. + IDisposable OpenNestedContext(string message); + + /// + /// Opens a mapped diagnostics context. Not supported in EntLib logging. + /// + /// A key. + /// A value. + /// A disposable that when disposed removes the map from the context. + IDisposable OpenMappedContext(string key, string value); + } + + /// + /// Provides a mechanism to create instances of objects. + /// +#if LIBLOG_PROVIDERS_ONLY + internal +#else + public +#endif + static class LogProvider + { +#if !LIBLOG_PROVIDERS_ONLY + private const string NullLogProvider = "Current Log Provider is not set. Call SetCurrentLogProvider " + + "with a non-null value first."; + private static dynamic s_currentLogProvider; + private static Action s_onCurrentLogProviderSet; + + [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline")] + static LogProvider() + { + IsDisabled = false; + } + + /// + /// Sets the current log provider. + /// + /// The log provider. + public static void SetCurrentLogProvider(ILogProvider logProvider) + { + s_currentLogProvider = logProvider; + + RaiseOnCurrentLogProviderSet(); + } + + /// + /// Gets or sets a value indicating whether this is logging is disabled. + /// + /// + /// true if logging is disabled; otherwise, false. + /// + public static bool IsDisabled { get; set; } + + /// + /// Sets an action that is invoked when a consumer of your library has called SetCurrentLogProvider. It is + /// important that hook into this if you are using child libraries (especially ilmerged ones) that are using + /// LibLog (or other logging abstraction) so you adapt and delegate to them. + /// + /// + internal static Action OnCurrentLogProviderSet + { + set + { + s_onCurrentLogProviderSet = value; + RaiseOnCurrentLogProviderSet(); + } + } + + internal static ILogProvider CurrentLogProvider + { + get + { + return s_currentLogProvider; + } + } + + /// + /// Gets a logger for the specified type. + /// + /// The type whose name will be used for the logger. + /// An instance of +#if LIBLOG_PUBLIC + public +#else + internal +#endif + static ILog For() + { + return GetLogger(typeof(T)); + } + +#if !LIBLOG_PORTABLE + /// + /// Gets a logger for the current class. + /// + /// An instance of + [MethodImpl(MethodImplOptions.NoInlining)] +#if LIBLOG_PUBLIC + public +#else + internal +#endif + static ILog GetCurrentClassLogger() + { + var stackFrame = new StackFrame(1, false); + return GetLogger(stackFrame.GetMethod().DeclaringType); + } +#endif + + /// + /// Gets a logger for the specified type. + /// + /// The type whose name will be used for the logger. + /// If the type is null then this name will be used as the log name instead + /// An instance of +#if LIBLOG_PUBLIC + public +#else + internal +#endif + static ILog GetLogger(Type type, string fallbackTypeName = "System.Object") + { + // If the type passed in is null then fallback to the type name specified + return GetLogger(type != null ? type.FullName : fallbackTypeName); + } + + /// + /// Gets a logger with the specified name. + /// + /// The name. + /// An instance of +#if LIBLOG_PUBLIC + public +#else + internal +#endif + static ILog GetLogger(string name) + { + ILogProvider logProvider = CurrentLogProvider ?? ResolveLogProvider(); + return logProvider == null + ? NoOpLogger.Instance + : (ILog)new LoggerExecutionWrapper(logProvider.GetLogger(name), () => IsDisabled); + } + + /// + /// Opens a nested diagnostics context. + /// + /// A message. + /// An that closes context when disposed. + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "SetCurrentLogProvider")] +#if LIBLOG_PUBLIC + public +#else + internal +#endif + static IDisposable OpenNestedContext(string message) + { + ILogProvider logProvider = CurrentLogProvider ?? ResolveLogProvider(); + + return logProvider == null + ? new DisposableAction(() => { }) + : logProvider.OpenNestedContext(message); + } + + /// + /// Opens a mapped diagnostics context. + /// + /// A key. + /// A value. + /// An that closes context when disposed. + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "SetCurrentLogProvider")] +#if LIBLOG_PUBLIC + public +#else + internal +#endif + static IDisposable OpenMappedContext(string key, string value) + { + ILogProvider logProvider = CurrentLogProvider ?? ResolveLogProvider(); + + return logProvider == null + ? new DisposableAction(() => { }) + : logProvider.OpenMappedContext(key, value); + } +#endif + +#if LIBLOG_PROVIDERS_ONLY + private +#else + internal +#endif + delegate bool IsLoggerAvailable(); + +#if LIBLOG_PROVIDERS_ONLY + private +#else + internal +#endif + delegate ILogProvider CreateLogProvider(); + +#if LIBLOG_PROVIDERS_ONLY + private +#else + internal +#endif + static readonly List> LogProviderResolvers = + new List> + { + new Tuple(SerilogLogProvider.IsLoggerAvailable, () => new SerilogLogProvider()), + new Tuple(NLogLogProvider.IsLoggerAvailable, () => new NLogLogProvider()), + new Tuple(Log4NetLogProvider.IsLoggerAvailable, () => new Log4NetLogProvider()), + new Tuple(EntLibLogProvider.IsLoggerAvailable, () => new EntLibLogProvider()), + new Tuple(LoupeLogProvider.IsLoggerAvailable, () => new LoupeLogProvider()), + }; + +#if !LIBLOG_PROVIDERS_ONLY + private static void RaiseOnCurrentLogProviderSet() + { + if (s_onCurrentLogProviderSet != null) + { + s_onCurrentLogProviderSet(s_currentLogProvider); + } + } +#endif + + [SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.Console.WriteLine(System.String,System.Object,System.Object)")] + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + internal static ILogProvider ResolveLogProvider() + { + try + { + foreach (var providerResolver in LogProviderResolvers) + { + if (providerResolver.Item1()) + { + return providerResolver.Item2(); + } + } + } + catch (Exception ex) + { +#if LIBLOG_PORTABLE + Debug.WriteLine( +#else + Console.WriteLine( +#endif + "Exception occurred resolving a log provider. Logging for this assembly {0} is disabled. {1}", + typeof(LogProvider).GetAssemblyPortable().FullName, + ex); + } + return null; + } + +#if !LIBLOG_PROVIDERS_ONLY + internal class NoOpLogger : ILog + { + internal static readonly NoOpLogger Instance = new NoOpLogger(); + + public bool Log(LogLevel logLevel, Func messageFunc, Exception exception, params object[] formatParameters) + { + return false; + } + } +#endif + } + +#if !LIBLOG_PROVIDERS_ONLY + internal class LoggerExecutionWrapper : ILog + { + private readonly Logger _logger; + private readonly Func _getIsDisabled; + internal const string FailedToGenerateLogMessage = "Failed to generate log message"; + + internal LoggerExecutionWrapper(Logger logger, Func getIsDisabled = null) + { + _logger = logger; + _getIsDisabled = getIsDisabled ?? (() => false); + } + + internal Logger WrappedLogger + { + get { return _logger; } + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + public bool Log(LogLevel logLevel, Func messageFunc, Exception exception = null, params object[] formatParameters) + { + if (_getIsDisabled()) + { + return false; + } + if (messageFunc == null) + { + return _logger(logLevel, null); + } + + Func wrappedMessageFunc = () => + { + try + { + return messageFunc(); + } + catch (Exception ex) + { + Log(LogLevel.Error, () => FailedToGenerateLogMessage, ex); + } + return null; + }; + return _logger(logLevel, wrappedMessageFunc, exception, formatParameters); + } + } +#endif +} + +#if LIBLOG_PROVIDERS_ONLY +namespace ZoomNet.Logging.LibLog.LogProviders +#else +namespace ZoomNet.Logging.LogProviders +#endif +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; +#if !LIBLOG_PORTABLE + using System.Diagnostics; +#endif + using System.Globalization; + using System.Linq; + using System.Linq.Expressions; + using System.Reflection; +#if !LIBLOG_PORTABLE + using System.Text; +#endif + using System.Text.RegularExpressions; + + internal abstract class LogProviderBase : ILogProvider + { + protected delegate IDisposable OpenNdc(string message); + protected delegate IDisposable OpenMdc(string key, string value); + + private readonly Lazy _lazyOpenNdcMethod; + private readonly Lazy _lazyOpenMdcMethod; + private static readonly IDisposable NoopDisposableInstance = new DisposableAction(); + + protected LogProviderBase() + { + _lazyOpenNdcMethod + = new Lazy(GetOpenNdcMethod); + _lazyOpenMdcMethod + = new Lazy(GetOpenMdcMethod); + } + + public abstract Logger GetLogger(string name); + + public IDisposable OpenNestedContext(string message) + { + return _lazyOpenNdcMethod.Value(message); + } + + public IDisposable OpenMappedContext(string key, string value) + { + return _lazyOpenMdcMethod.Value(key, value); + } + + protected virtual OpenNdc GetOpenNdcMethod() + { + return _ => NoopDisposableInstance; + } + + protected virtual OpenMdc GetOpenMdcMethod() + { + return (_, __) => NoopDisposableInstance; + } + } + + internal class NLogLogProvider : LogProviderBase + { + private readonly Func _getLoggerByNameDelegate; + private static bool s_providerIsAvailableOverride = true; + + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "LogManager")] + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "NLog")] + public NLogLogProvider() + { + if (!IsLoggerAvailable()) + { + throw new InvalidOperationException("NLog.LogManager not found"); + } + _getLoggerByNameDelegate = GetGetLoggerMethodCall(); + } + + public static bool ProviderIsAvailableOverride + { + get { return s_providerIsAvailableOverride; } + set { s_providerIsAvailableOverride = value; } + } + + public override Logger GetLogger(string name) + { + return new NLogLogger(_getLoggerByNameDelegate(name)).Log; + } + + public static bool IsLoggerAvailable() + { + return ProviderIsAvailableOverride && GetLogManagerType() != null; + } + + protected override OpenNdc GetOpenNdcMethod() + { + Type ndcContextType = Type.GetType("NLog.NestedDiagnosticsContext, NLog"); + MethodInfo pushMethod = ndcContextType.GetMethodPortable("Push", typeof(string)); + ParameterExpression messageParam = Expression.Parameter(typeof(string), "message"); + MethodCallExpression pushMethodCall = Expression.Call(null, pushMethod, messageParam); + return Expression.Lambda(pushMethodCall, messageParam).Compile(); + } + + protected override OpenMdc GetOpenMdcMethod() + { + Type mdcContextType = Type.GetType("NLog.MappedDiagnosticsContext, NLog"); + + MethodInfo setMethod = mdcContextType.GetMethodPortable("Set", typeof(string), typeof(string)); + MethodInfo removeMethod = mdcContextType.GetMethodPortable("Remove", typeof(string)); + ParameterExpression keyParam = Expression.Parameter(typeof(string), "key"); + ParameterExpression valueParam = Expression.Parameter(typeof(string), "value"); + + MethodCallExpression setMethodCall = Expression.Call(null, setMethod, keyParam, valueParam); + MethodCallExpression removeMethodCall = Expression.Call(null, removeMethod, keyParam); + + Action set = Expression + .Lambda>(setMethodCall, keyParam, valueParam) + .Compile(); + Action remove = Expression + .Lambda>(removeMethodCall, keyParam) + .Compile(); + + return (key, value) => + { + set(key, value); + return new DisposableAction(() => remove(key)); + }; + } + + private static Type GetLogManagerType() + { + return Type.GetType("NLog.LogManager, NLog"); + } + + private static Func GetGetLoggerMethodCall() + { + Type logManagerType = GetLogManagerType(); + MethodInfo method = logManagerType.GetMethodPortable("GetLogger", typeof(string)); + ParameterExpression nameParam = Expression.Parameter(typeof(string), "name"); + MethodCallExpression methodCall = Expression.Call(null, method, nameParam); + return Expression.Lambda>(methodCall, nameParam).Compile(); + } + + internal class NLogLogger + { + private readonly dynamic _logger; + + private static Func _logEventInfoFact; + + private static readonly object _levelTrace; + private static readonly object _levelDebug; + private static readonly object _levelInfo; + private static readonly object _levelWarn; + private static readonly object _levelError; + private static readonly object _levelFatal; + + static NLogLogger() + { + try + { + var logEventLevelType = Type.GetType("NLog.LogLevel, NLog"); + if (logEventLevelType == null) + { + throw new InvalidOperationException("Type NLog.LogLevel was not found."); + } + + var levelFields = logEventLevelType.GetFieldsPortable().ToList(); + _levelTrace = levelFields.First(x => x.Name == "Trace").GetValue(null); + _levelDebug = levelFields.First(x => x.Name == "Debug").GetValue(null); + _levelInfo = levelFields.First(x => x.Name == "Info").GetValue(null); + _levelWarn = levelFields.First(x => x.Name == "Warn").GetValue(null); + _levelError = levelFields.First(x => x.Name == "Error").GetValue(null); + _levelFatal = levelFields.First(x => x.Name == "Fatal").GetValue(null); + + var logEventInfoType = Type.GetType("NLog.LogEventInfo, NLog"); + if (logEventInfoType == null) + { + throw new InvalidOperationException("Type NLog.LogEventInfo was not found."); + } + MethodInfo createLogEventInfoMethodInfo = logEventInfoType.GetMethodPortable("Create", + logEventLevelType, typeof(string), typeof(Exception), typeof(IFormatProvider), typeof(string), typeof(object[])); + ParameterExpression loggerNameParam = Expression.Parameter(typeof(string)); + ParameterExpression levelParam = Expression.Parameter(typeof(object)); + ParameterExpression messageParam = Expression.Parameter(typeof(string)); + ParameterExpression exceptionParam = Expression.Parameter(typeof(Exception)); + UnaryExpression levelCast = Expression.Convert(levelParam, logEventLevelType); + MethodCallExpression createLogEventInfoMethodCall = Expression.Call(null, + createLogEventInfoMethodInfo, + levelCast, loggerNameParam, exceptionParam, + Expression.Constant(null, typeof(IFormatProvider)), messageParam, Expression.Constant(null, typeof(object[]))); + _logEventInfoFact = Expression.Lambda>(createLogEventInfoMethodCall, + loggerNameParam, levelParam, messageParam, exceptionParam).Compile(); + } + catch { } + } + + internal NLogLogger(dynamic logger) + { + _logger = logger; + } + + [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")] + public bool Log(LogLevel logLevel, Func messageFunc, Exception exception, params object[] formatParameters) + { + if (messageFunc == null) + { + return IsLogLevelEnable(logLevel); + } + messageFunc = LogMessageFormatter.SimulateStructuredLogging(messageFunc, formatParameters); + + if (_logEventInfoFact != null) + { + if (IsLogLevelEnable(logLevel)) + { + var nlogLevel = this.TranslateLevel(logLevel); + Type s_callerStackBoundaryType; +#if !LIBLOG_PORTABLE + StackTrace stack = new StackTrace(); + Type thisType = GetType(); + Type knownType0 = typeof(LoggerExecutionWrapper); + Type knownType1 = typeof(LogExtensions); + //Maybe inline, so we may can't found any LibLog classes in stack + s_callerStackBoundaryType = null; + for (var i = 0; i < stack.FrameCount; i++) + { + var declaringType = stack.GetFrame(i).GetMethod().DeclaringType; + if (!IsInTypeHierarchy(thisType, declaringType) && + !IsInTypeHierarchy(knownType0, declaringType) && + !IsInTypeHierarchy(knownType1, declaringType)) + { + if (i > 1) + s_callerStackBoundaryType = stack.GetFrame(i - 1).GetMethod().DeclaringType; + break; + } + } +#else + s_callerStackBoundaryType = null; +#endif + if (s_callerStackBoundaryType != null) + _logger.Log(s_callerStackBoundaryType, _logEventInfoFact(_logger.Name, nlogLevel, messageFunc(), exception)); + else + _logger.Log(_logEventInfoFact(_logger.Name, nlogLevel, messageFunc(), exception)); + return true; + } + return false; + } + + if (exception != null) + { + return LogException(logLevel, messageFunc, exception); + } + switch (logLevel) + { + case LogLevel.Debug: + if (_logger.IsDebugEnabled) + { + _logger.Debug(messageFunc()); + return true; + } + break; + case LogLevel.Info: + if (_logger.IsInfoEnabled) + { + _logger.Info(messageFunc()); + return true; + } + break; + case LogLevel.Warn: + if (_logger.IsWarnEnabled) + { + _logger.Warn(messageFunc()); + return true; + } + break; + case LogLevel.Error: + if (_logger.IsErrorEnabled) + { + _logger.Error(messageFunc()); + return true; + } + break; + case LogLevel.Fatal: + if (_logger.IsFatalEnabled) + { + _logger.Fatal(messageFunc()); + return true; + } + break; + default: + if (_logger.IsTraceEnabled) + { + _logger.Trace(messageFunc()); + return true; + } + break; + } + return false; + } + + private static bool IsInTypeHierarchy(Type currentType, Type checkType) + { + while (currentType != null && currentType != typeof(object)) + { + if (currentType == checkType) + { + return true; + } + currentType = currentType.GetBaseTypePortable(); + } + return false; + } + + [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")] + private bool LogException(LogLevel logLevel, Func messageFunc, Exception exception) + { + switch (logLevel) + { + case LogLevel.Debug: + if (_logger.IsDebugEnabled) + { + _logger.DebugException(messageFunc(), exception); + return true; + } + break; + case LogLevel.Info: + if (_logger.IsInfoEnabled) + { + _logger.InfoException(messageFunc(), exception); + return true; + } + break; + case LogLevel.Warn: + if (_logger.IsWarnEnabled) + { + _logger.WarnException(messageFunc(), exception); + return true; + } + break; + case LogLevel.Error: + if (_logger.IsErrorEnabled) + { + _logger.ErrorException(messageFunc(), exception); + return true; + } + break; + case LogLevel.Fatal: + if (_logger.IsFatalEnabled) + { + _logger.FatalException(messageFunc(), exception); + return true; + } + break; + default: + if (_logger.IsTraceEnabled) + { + _logger.TraceException(messageFunc(), exception); + return true; + } + break; + } + return false; + } + + private bool IsLogLevelEnable(LogLevel logLevel) + { + switch (logLevel) + { + case LogLevel.Debug: + return _logger.IsDebugEnabled; + case LogLevel.Info: + return _logger.IsInfoEnabled; + case LogLevel.Warn: + return _logger.IsWarnEnabled; + case LogLevel.Error: + return _logger.IsErrorEnabled; + case LogLevel.Fatal: + return _logger.IsFatalEnabled; + default: + return _logger.IsTraceEnabled; + } + } + + private object TranslateLevel(LogLevel logLevel) + { + switch (logLevel) + { + case LogLevel.Trace: + return _levelTrace; + case LogLevel.Debug: + return _levelDebug; + case LogLevel.Info: + return _levelInfo; + case LogLevel.Warn: + return _levelWarn; + case LogLevel.Error: + return _levelError; + case LogLevel.Fatal: + return _levelFatal; + default: + throw new ArgumentOutOfRangeException("logLevel", logLevel, null); + } + } + } + } + + internal class Log4NetLogProvider : LogProviderBase + { + private readonly Func _getLoggerByNameDelegate; + private static bool s_providerIsAvailableOverride = true; + + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "LogManager")] + public Log4NetLogProvider() + { + if (!IsLoggerAvailable()) + { + throw new InvalidOperationException("log4net.LogManager not found"); + } + _getLoggerByNameDelegate = GetGetLoggerMethodCall(); + } + + public static bool ProviderIsAvailableOverride + { + get { return s_providerIsAvailableOverride; } + set { s_providerIsAvailableOverride = value; } + } + + public override Logger GetLogger(string name) + { + return new Log4NetLogger(_getLoggerByNameDelegate(name)).Log; + } + + internal static bool IsLoggerAvailable() + { + return ProviderIsAvailableOverride && GetLogManagerType() != null; + } + + protected override OpenNdc GetOpenNdcMethod() + { + Type logicalThreadContextType = Type.GetType("log4net.LogicalThreadContext, log4net"); + PropertyInfo stacksProperty = logicalThreadContextType.GetPropertyPortable("Stacks"); + Type logicalThreadContextStacksType = stacksProperty.PropertyType; + PropertyInfo stacksIndexerProperty = logicalThreadContextStacksType.GetPropertyPortable("Item"); + Type stackType = stacksIndexerProperty.PropertyType; + MethodInfo pushMethod = stackType.GetMethodPortable("Push"); + + ParameterExpression messageParameter = + Expression.Parameter(typeof(string), "message"); + + // message => LogicalThreadContext.Stacks.Item["NDC"].Push(message); + MethodCallExpression callPushBody = + Expression.Call( + Expression.Property(Expression.Property(null, stacksProperty), + stacksIndexerProperty, + Expression.Constant("NDC")), + pushMethod, + messageParameter); + + OpenNdc result = + Expression.Lambda(callPushBody, messageParameter) + .Compile(); + + return result; + } + + protected override OpenMdc GetOpenMdcMethod() + { + Type logicalThreadContextType = Type.GetType("log4net.LogicalThreadContext, log4net"); + PropertyInfo propertiesProperty = logicalThreadContextType.GetPropertyPortable("Properties"); + Type logicalThreadContextPropertiesType = propertiesProperty.PropertyType; + PropertyInfo propertiesIndexerProperty = logicalThreadContextPropertiesType.GetPropertyPortable("Item"); + + MethodInfo removeMethod = logicalThreadContextPropertiesType.GetMethodPortable("Remove"); + + ParameterExpression keyParam = Expression.Parameter(typeof(string), "key"); + ParameterExpression valueParam = Expression.Parameter(typeof(string), "value"); + + MemberExpression propertiesExpression = Expression.Property(null, propertiesProperty); + + // (key, value) => LogicalThreadContext.Properties.Item[key] = value; + BinaryExpression setProperties = Expression.Assign(Expression.Property(propertiesExpression, propertiesIndexerProperty, keyParam), valueParam); + + // key => LogicalThreadContext.Properties.Remove(key); + MethodCallExpression removeMethodCall = Expression.Call(propertiesExpression, removeMethod, keyParam); + + Action set = Expression + .Lambda>(setProperties, keyParam, valueParam) + .Compile(); + + Action remove = Expression + .Lambda>(removeMethodCall, keyParam) + .Compile(); + + return (key, value) => + { + set(key, value); + return new DisposableAction(() => remove(key)); + }; + } + + private static Type GetLogManagerType() + { + return Type.GetType("log4net.LogManager, log4net"); + } + + private static Func GetGetLoggerMethodCall() + { + Type logManagerType = GetLogManagerType(); + MethodInfo method = logManagerType.GetMethodPortable("GetLogger", typeof(string)); + ParameterExpression nameParam = Expression.Parameter(typeof(string), "name"); + MethodCallExpression methodCall = Expression.Call(null, method, nameParam); + return Expression.Lambda>(methodCall, nameParam).Compile(); + } + + internal class Log4NetLogger + { + private readonly dynamic _logger; + private static Type s_callerStackBoundaryType; + private static readonly object CallerStackBoundaryTypeSync = new object(); + + private readonly object _levelDebug; + private readonly object _levelInfo; + private readonly object _levelWarn; + private readonly object _levelError; + private readonly object _levelFatal; + private readonly Func _isEnabledForDelegate; + private readonly Action _logDelegate; + private readonly Func _createLoggingEvent; + private Action _loggingEventPropertySetter; + + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "ILogger")] + internal Log4NetLogger(dynamic logger) + { + _logger = logger.Logger; + + var logEventLevelType = Type.GetType("log4net.Core.Level, log4net"); + if (logEventLevelType == null) + { + throw new InvalidOperationException("Type log4net.Core.Level was not found."); + } + + var levelFields = logEventLevelType.GetFieldsPortable().ToList(); + _levelDebug = levelFields.First(x => x.Name == "Debug").GetValue(null); + _levelInfo = levelFields.First(x => x.Name == "Info").GetValue(null); + _levelWarn = levelFields.First(x => x.Name == "Warn").GetValue(null); + _levelError = levelFields.First(x => x.Name == "Error").GetValue(null); + _levelFatal = levelFields.First(x => x.Name == "Fatal").GetValue(null); + + // Func isEnabledFor = (logger, level) => { return ((log4net.Core.ILogger)logger).IsEnabled(level); } + var loggerType = Type.GetType("log4net.Core.ILogger, log4net"); + if (loggerType == null) + { + throw new InvalidOperationException("Type log4net.Core.ILogger, was not found."); + } + ParameterExpression instanceParam = Expression.Parameter(typeof(object)); + UnaryExpression instanceCast = Expression.Convert(instanceParam, loggerType); + ParameterExpression levelParam = Expression.Parameter(typeof(object)); + UnaryExpression levelCast = Expression.Convert(levelParam, logEventLevelType); + _isEnabledForDelegate = GetIsEnabledFor(loggerType, logEventLevelType, instanceCast, levelCast, instanceParam, levelParam); + + Type loggingEventType = Type.GetType("log4net.Core.LoggingEvent, log4net"); + + _createLoggingEvent = GetCreateLoggingEvent(instanceParam, instanceCast, levelParam, levelCast, loggingEventType); + + _logDelegate = GetLogDelegate(loggerType, loggingEventType, instanceCast, instanceParam); + + _loggingEventPropertySetter = GetLoggingEventPropertySetter(loggingEventType); + } + + private static Action GetLogDelegate(Type loggerType, Type loggingEventType, UnaryExpression instanceCast, + ParameterExpression instanceParam) + { + //Action Log = + //(logger, callerStackBoundaryDeclaringType, level, message, exception) => { ((ILogger)logger).Log(new LoggingEvent(callerStackBoundaryDeclaringType, logger.Repository, logger.Name, level, message, exception)); } + MethodInfo writeExceptionMethodInfo = loggerType.GetMethodPortable("Log", + loggingEventType); + + ParameterExpression loggingEventParameter = + Expression.Parameter(typeof(object), "loggingEvent"); + + UnaryExpression loggingEventCasted = + Expression.Convert(loggingEventParameter, loggingEventType); + + var writeMethodExp = Expression.Call( + instanceCast, + writeExceptionMethodInfo, + loggingEventCasted); + + var logDelegate = Expression.Lambda>( + writeMethodExp, + instanceParam, + loggingEventParameter).Compile(); + + return logDelegate; + } + + private static Func GetCreateLoggingEvent(ParameterExpression instanceParam, UnaryExpression instanceCast, ParameterExpression levelParam, UnaryExpression levelCast, Type loggingEventType) + { + ParameterExpression callerStackBoundaryDeclaringTypeParam = Expression.Parameter(typeof(Type)); + ParameterExpression messageParam = Expression.Parameter(typeof(string)); + ParameterExpression exceptionParam = Expression.Parameter(typeof(Exception)); + + PropertyInfo repositoryProperty = loggingEventType.GetPropertyPortable("Repository"); + PropertyInfo levelProperty = loggingEventType.GetPropertyPortable("Level"); + + ConstructorInfo loggingEventConstructor = + loggingEventType.GetConstructorPortable(typeof(Type), repositoryProperty.PropertyType, typeof(string), levelProperty.PropertyType, typeof(object), typeof(Exception)); + + //Func Log = + //(logger, callerStackBoundaryDeclaringType, level, message, exception) => new LoggingEvent(callerStackBoundaryDeclaringType, ((ILogger)logger).Repository, ((ILogger)logger).Name, (Level)level, message, exception); } + NewExpression newLoggingEventExpression = + Expression.New(loggingEventConstructor, + callerStackBoundaryDeclaringTypeParam, + Expression.Property(instanceCast, "Repository"), + Expression.Property(instanceCast, "Name"), + levelCast, + messageParam, + exceptionParam); + + var createLoggingEvent = + Expression.Lambda>( + newLoggingEventExpression, + instanceParam, + callerStackBoundaryDeclaringTypeParam, + levelParam, + messageParam, + exceptionParam) + .Compile(); + + return createLoggingEvent; + } + + private static Func GetIsEnabledFor(Type loggerType, Type logEventLevelType, + UnaryExpression instanceCast, + UnaryExpression levelCast, + ParameterExpression instanceParam, + ParameterExpression levelParam) + { + MethodInfo isEnabledMethodInfo = loggerType.GetMethodPortable("IsEnabledFor", logEventLevelType); + MethodCallExpression isEnabledMethodCall = Expression.Call(instanceCast, isEnabledMethodInfo, levelCast); + + Func result = + Expression.Lambda>(isEnabledMethodCall, instanceParam, levelParam) + .Compile(); + + return result; + } + + private static Action GetLoggingEventPropertySetter(Type loggingEventType) + { + ParameterExpression loggingEventParameter = Expression.Parameter(typeof(object), "loggingEvent"); + ParameterExpression keyParameter = Expression.Parameter(typeof(string), "key"); + ParameterExpression valueParameter = Expression.Parameter(typeof(object), "value"); + + PropertyInfo propertiesProperty = loggingEventType.GetPropertyPortable("Properties"); + PropertyInfo item = propertiesProperty.PropertyType.GetPropertyPortable("Item"); + + // ((LoggingEvent)loggingEvent).Properties[key] = value; + var body = + Expression.Assign( + Expression.Property( + Expression.Property(Expression.Convert(loggingEventParameter, loggingEventType), + propertiesProperty), item, keyParameter), valueParameter); + + Action result = + Expression.Lambda> + (body, loggingEventParameter, keyParameter, + valueParameter) + .Compile(); + + return result; + } + + public bool Log(LogLevel logLevel, Func messageFunc, Exception exception, params object[] formatParameters) + { + if (messageFunc == null) + { + return IsLogLevelEnable(logLevel); + } + + if (!IsLogLevelEnable(logLevel)) + { + return false; + } + + string message = messageFunc(); + + IEnumerable patternMatches; + + string formattedMessage = + LogMessageFormatter.FormatStructuredMessage(message, + formatParameters, + out patternMatches); + + // determine correct caller - this might change due to jit optimizations with method inlining + if (s_callerStackBoundaryType == null) + { + lock (CallerStackBoundaryTypeSync) + { +#if !LIBLOG_PORTABLE + StackTrace stack = new StackTrace(); + Type thisType = GetType(); + s_callerStackBoundaryType = Type.GetType("LoggerExecutionWrapper"); + for (var i = 1; i < stack.FrameCount; i++) + { + if (!IsInTypeHierarchy(thisType, stack.GetFrame(i).GetMethod().DeclaringType)) + { + s_callerStackBoundaryType = stack.GetFrame(i - 1).GetMethod().DeclaringType; + break; + } + } +#else + s_callerStackBoundaryType = typeof(LoggerExecutionWrapper); +#endif + } + } + + var translatedLevel = TranslateLevel(logLevel); + + object loggingEvent = _createLoggingEvent(_logger, s_callerStackBoundaryType, translatedLevel, formattedMessage, exception); + + PopulateProperties(loggingEvent, patternMatches, formatParameters); + + _logDelegate(_logger, loggingEvent); + + return true; + } + + private void PopulateProperties(object loggingEvent, IEnumerable patternMatches, object[] formatParameters) + { + IEnumerable> keyToValue = + patternMatches.Zip(formatParameters, + (key, value) => new KeyValuePair(key, value)); + + foreach (KeyValuePair keyValuePair in keyToValue) + { + _loggingEventPropertySetter(loggingEvent, keyValuePair.Key, keyValuePair.Value); + } + } + + private static bool IsInTypeHierarchy(Type currentType, Type checkType) + { + while (currentType != null && currentType != typeof(object)) + { + if (currentType == checkType) + { + return true; + } + currentType = currentType.GetBaseTypePortable(); + } + return false; + } + + private bool IsLogLevelEnable(LogLevel logLevel) + { + var level = TranslateLevel(logLevel); + return _isEnabledForDelegate(_logger, level); + } + + private object TranslateLevel(LogLevel logLevel) + { + switch (logLevel) + { + case LogLevel.Trace: + case LogLevel.Debug: + return _levelDebug; + case LogLevel.Info: + return _levelInfo; + case LogLevel.Warn: + return _levelWarn; + case LogLevel.Error: + return _levelError; + case LogLevel.Fatal: + return _levelFatal; + default: + throw new ArgumentOutOfRangeException("logLevel", logLevel, null); + } + } + } + } + + internal class EntLibLogProvider : LogProviderBase + { + private const string TypeTemplate = "Microsoft.Practices.EnterpriseLibrary.Logging.{0}, Microsoft.Practices.EnterpriseLibrary.Logging"; + private static bool s_providerIsAvailableOverride = true; + private static readonly Type LogEntryType; + private static readonly Type LoggerType; + private static readonly Type TraceEventTypeType; + private static readonly Action WriteLogEntry; + private static readonly Func ShouldLogEntry; + + [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline")] + static EntLibLogProvider() + { + LogEntryType = Type.GetType(string.Format(CultureInfo.InvariantCulture, TypeTemplate, "LogEntry")); + LoggerType = Type.GetType(string.Format(CultureInfo.InvariantCulture, TypeTemplate, "Logger")); + TraceEventTypeType = TraceEventTypeValues.Type; + if (LogEntryType == null + || TraceEventTypeType == null + || LoggerType == null) + { + return; + } + WriteLogEntry = GetWriteLogEntry(); + ShouldLogEntry = GetShouldLogEntry(); + } + + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "EnterpriseLibrary")] + public EntLibLogProvider() + { + if (!IsLoggerAvailable()) + { + throw new InvalidOperationException("Microsoft.Practices.EnterpriseLibrary.Logging.Logger not found"); + } + } + + public static bool ProviderIsAvailableOverride + { + get { return s_providerIsAvailableOverride; } + set { s_providerIsAvailableOverride = value; } + } + + public override Logger GetLogger(string name) + { + return new EntLibLogger(name, WriteLogEntry, ShouldLogEntry).Log; + } + + internal static bool IsLoggerAvailable() + { + return ProviderIsAvailableOverride + && TraceEventTypeType != null + && LogEntryType != null; + } + + private static Action GetWriteLogEntry() + { + // new LogEntry(...) + var logNameParameter = Expression.Parameter(typeof(string), "logName"); + var messageParameter = Expression.Parameter(typeof(string), "message"); + var severityParameter = Expression.Parameter(typeof(int), "severity"); + + MemberInitExpression memberInit = GetWriteLogExpression( + messageParameter, + Expression.Convert(severityParameter, TraceEventTypeType), + logNameParameter); + + //Logger.Write(new LogEntry(....)); + MethodInfo writeLogEntryMethod = LoggerType.GetMethodPortable("Write", LogEntryType); + var writeLogEntryExpression = Expression.Call(writeLogEntryMethod, memberInit); + + return Expression.Lambda>( + writeLogEntryExpression, + logNameParameter, + messageParameter, + severityParameter).Compile(); + } + + private static Func GetShouldLogEntry() + { + // new LogEntry(...) + var logNameParameter = Expression.Parameter(typeof(string), "logName"); + var severityParameter = Expression.Parameter(typeof(int), "severity"); + + MemberInitExpression memberInit = GetWriteLogExpression( + Expression.Constant("***dummy***"), + Expression.Convert(severityParameter, TraceEventTypeType), + logNameParameter); + + //Logger.Write(new LogEntry(....)); + MethodInfo writeLogEntryMethod = LoggerType.GetMethodPortable("ShouldLog", LogEntryType); + var writeLogEntryExpression = Expression.Call(writeLogEntryMethod, memberInit); + + return Expression.Lambda>( + writeLogEntryExpression, + logNameParameter, + severityParameter).Compile(); + } + + private static MemberInitExpression GetWriteLogExpression(Expression message, + Expression severityParameter, ParameterExpression logNameParameter) + { + var entryType = LogEntryType; + MemberInitExpression memberInit = Expression.MemberInit(Expression.New(entryType), + Expression.Bind(entryType.GetPropertyPortable("Message"), message), + Expression.Bind(entryType.GetPropertyPortable("Severity"), severityParameter), + Expression.Bind( + entryType.GetPropertyPortable("TimeStamp"), + Expression.Property(null, typeof(DateTime).GetPropertyPortable("UtcNow"))), + Expression.Bind( + entryType.GetPropertyPortable("Categories"), + Expression.ListInit( + Expression.New(typeof(List)), + typeof(List).GetMethodPortable("Add", typeof(string)), + logNameParameter))); + return memberInit; + } + + internal class EntLibLogger + { + private readonly string _loggerName; + private readonly Action _writeLog; + private readonly Func _shouldLog; + + internal EntLibLogger(string loggerName, Action writeLog, Func shouldLog) + { + _loggerName = loggerName; + _writeLog = writeLog; + _shouldLog = shouldLog; + } + + public bool Log(LogLevel logLevel, Func messageFunc, Exception exception, params object[] formatParameters) + { + var severity = MapSeverity(logLevel); + if (messageFunc == null) + { + return _shouldLog(_loggerName, severity); + } + + + messageFunc = LogMessageFormatter.SimulateStructuredLogging(messageFunc, formatParameters); + if (exception != null) + { + return LogException(logLevel, messageFunc, exception); + } + _writeLog(_loggerName, messageFunc(), severity); + return true; + } + + public bool LogException(LogLevel logLevel, Func messageFunc, Exception exception) + { + var severity = MapSeverity(logLevel); + var message = messageFunc() + Environment.NewLine + exception; + _writeLog(_loggerName, message, severity); + return true; + } + + private static int MapSeverity(LogLevel logLevel) + { + switch (logLevel) + { + case LogLevel.Fatal: + return TraceEventTypeValues.Critical; + case LogLevel.Error: + return TraceEventTypeValues.Error; + case LogLevel.Warn: + return TraceEventTypeValues.Warning; + case LogLevel.Info: + return TraceEventTypeValues.Information; + default: + return TraceEventTypeValues.Verbose; + } + } + } + } + + internal class SerilogLogProvider : LogProviderBase + { + private readonly Func _getLoggerByNameDelegate; + private static bool s_providerIsAvailableOverride = true; + + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "Serilog")] + public SerilogLogProvider() + { + if (!IsLoggerAvailable()) + { + throw new InvalidOperationException("Serilog.Log not found"); + } + _getLoggerByNameDelegate = GetForContextMethodCall(); + } + + public static bool ProviderIsAvailableOverride + { + get { return s_providerIsAvailableOverride; } + set { s_providerIsAvailableOverride = value; } + } + + public override Logger GetLogger(string name) + { + return new SerilogLogger(_getLoggerByNameDelegate(name)).Log; + } + + internal static bool IsLoggerAvailable() + { + return ProviderIsAvailableOverride && GetLogManagerType() != null; + } + + protected override OpenNdc GetOpenNdcMethod() + { + return message => GetPushProperty()("NDC", message); + } + + protected override OpenMdc GetOpenMdcMethod() + { + return (key, value) => GetPushProperty()(key, value); + } + + private static Func GetPushProperty() + { + Type ndcContextType = Type.GetType("Serilog.Context.LogContext, Serilog") ?? + Type.GetType("Serilog.Context.LogContext, Serilog.FullNetFx"); + + MethodInfo pushPropertyMethod = ndcContextType.GetMethodPortable( + "PushProperty", + typeof(string), + typeof(object), + typeof(bool)); + + ParameterExpression nameParam = Expression.Parameter(typeof(string), "name"); + ParameterExpression valueParam = Expression.Parameter(typeof(object), "value"); + ParameterExpression destructureObjectParam = Expression.Parameter(typeof(bool), "destructureObjects"); + MethodCallExpression pushPropertyMethodCall = Expression + .Call(null, pushPropertyMethod, nameParam, valueParam, destructureObjectParam); + var pushProperty = Expression + .Lambda>( + pushPropertyMethodCall, + nameParam, + valueParam, + destructureObjectParam) + .Compile(); + + return (key, value) => pushProperty(key, value, false); + } + + private static Type GetLogManagerType() + { + return Type.GetType("Serilog.Log, Serilog"); + } + + private static Func GetForContextMethodCall() + { + Type logManagerType = GetLogManagerType(); + MethodInfo method = logManagerType.GetMethodPortable("ForContext", typeof(string), typeof(object), typeof(bool)); + ParameterExpression propertyNameParam = Expression.Parameter(typeof(string), "propertyName"); + ParameterExpression valueParam = Expression.Parameter(typeof(object), "value"); + ParameterExpression destructureObjectsParam = Expression.Parameter(typeof(bool), "destructureObjects"); + MethodCallExpression methodCall = Expression.Call(null, method, new Expression[] + { + propertyNameParam, + valueParam, + destructureObjectsParam + }); + var func = Expression.Lambda>( + methodCall, + propertyNameParam, + valueParam, + destructureObjectsParam) + .Compile(); + return name => func("SourceContext", name, false); + } + + internal class SerilogLogger + { + private readonly object _logger; + private static readonly object DebugLevel; + private static readonly object ErrorLevel; + private static readonly object FatalLevel; + private static readonly object InformationLevel; + private static readonly object VerboseLevel; + private static readonly object WarningLevel; + private static readonly Func IsEnabled; + private static readonly Action Write; + private static readonly Action WriteException; + + [SuppressMessage("Microsoft.Design", "CA1065:DoNotRaiseExceptionsInUnexpectedLocations")] + [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline")] + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "ILogger")] + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "LogEventLevel")] + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "Serilog")] + static SerilogLogger() + { + var logEventLevelType = Type.GetType("Serilog.Events.LogEventLevel, Serilog"); + if (logEventLevelType == null) + { + throw new InvalidOperationException("Type Serilog.Events.LogEventLevel was not found."); + } + DebugLevel = Enum.Parse(logEventLevelType, "Debug", false); + ErrorLevel = Enum.Parse(logEventLevelType, "Error", false); + FatalLevel = Enum.Parse(logEventLevelType, "Fatal", false); + InformationLevel = Enum.Parse(logEventLevelType, "Information", false); + VerboseLevel = Enum.Parse(logEventLevelType, "Verbose", false); + WarningLevel = Enum.Parse(logEventLevelType, "Warning", false); + + // Func isEnabled = (logger, level) => { return ((SeriLog.ILogger)logger).IsEnabled(level); } + var loggerType = Type.GetType("Serilog.ILogger, Serilog"); + if (loggerType == null) + { + throw new InvalidOperationException("Type Serilog.ILogger was not found."); + } + MethodInfo isEnabledMethodInfo = loggerType.GetMethodPortable("IsEnabled", logEventLevelType); + ParameterExpression instanceParam = Expression.Parameter(typeof(object)); + UnaryExpression instanceCast = Expression.Convert(instanceParam, loggerType); + ParameterExpression levelParam = Expression.Parameter(typeof(object)); + UnaryExpression levelCast = Expression.Convert(levelParam, logEventLevelType); + MethodCallExpression isEnabledMethodCall = Expression.Call(instanceCast, isEnabledMethodInfo, levelCast); + IsEnabled = Expression.Lambda>(isEnabledMethodCall, instanceParam, levelParam).Compile(); + + // Action Write = + // (logger, level, message, params) => { ((SeriLog.ILoggerILogger)logger).Write(level, message, params); } + MethodInfo writeMethodInfo = loggerType.GetMethodPortable("Write", logEventLevelType, typeof(string), typeof(object[])); + ParameterExpression messageParam = Expression.Parameter(typeof(string)); + ParameterExpression propertyValuesParam = Expression.Parameter(typeof(object[])); + MethodCallExpression writeMethodExp = Expression.Call( + instanceCast, + writeMethodInfo, + levelCast, + messageParam, + propertyValuesParam); + var expression = Expression.Lambda>( + writeMethodExp, + instanceParam, + levelParam, + messageParam, + propertyValuesParam); + Write = expression.Compile(); + + // Action WriteException = + // (logger, level, exception, message) => { ((ILogger)logger).Write(level, exception, message, new object[]); } + MethodInfo writeExceptionMethodInfo = loggerType.GetMethodPortable("Write", + logEventLevelType, + typeof(Exception), + typeof(string), + typeof(object[])); + ParameterExpression exceptionParam = Expression.Parameter(typeof(Exception)); + writeMethodExp = Expression.Call( + instanceCast, + writeExceptionMethodInfo, + levelCast, + exceptionParam, + messageParam, + propertyValuesParam); + WriteException = Expression.Lambda>( + writeMethodExp, + instanceParam, + levelParam, + exceptionParam, + messageParam, + propertyValuesParam).Compile(); + } + + internal SerilogLogger(object logger) + { + _logger = logger; + } + + public bool Log(LogLevel logLevel, Func messageFunc, Exception exception, params object[] formatParameters) + { + var translatedLevel = TranslateLevel(logLevel); + if (messageFunc == null) + { + return IsEnabled(_logger, translatedLevel); + } + + if (!IsEnabled(_logger, translatedLevel)) + { + return false; + } + + if (exception != null) + { + LogException(translatedLevel, messageFunc, exception, formatParameters); + } + else + { + LogMessage(translatedLevel, messageFunc, formatParameters); + } + + return true; + } + + private void LogMessage(object translatedLevel, Func messageFunc, object[] formatParameters) + { + Write(_logger, translatedLevel, messageFunc(), formatParameters); + } + + private void LogException(object logLevel, Func messageFunc, Exception exception, object[] formatParams) + { + WriteException(_logger, logLevel, exception, messageFunc(), formatParams); + } + + private static object TranslateLevel(LogLevel logLevel) + { + switch (logLevel) + { + case LogLevel.Fatal: + return FatalLevel; + case LogLevel.Error: + return ErrorLevel; + case LogLevel.Warn: + return WarningLevel; + case LogLevel.Info: + return InformationLevel; + case LogLevel.Trace: + return VerboseLevel; + default: + return DebugLevel; + } + } + } + } + + internal class LoupeLogProvider : LogProviderBase + { + /// + /// The form of the Loupe Log.Write method we're using + /// + internal delegate void WriteDelegate( + int severity, + string logSystem, + int skipFrames, + Exception exception, + bool attributeToException, + int writeMode, + string detailsXml, + string category, + string caption, + string description, + params object[] args + ); + + private static bool s_providerIsAvailableOverride = true; + private readonly WriteDelegate _logWriteDelegate; + + public LoupeLogProvider() + { + if (!IsLoggerAvailable()) + { + throw new InvalidOperationException("Gibraltar.Agent.Log (Loupe) not found"); + } + + _logWriteDelegate = GetLogWriteDelegate(); + } + + /// + /// Gets or sets a value indicating whether [provider is available override]. Used in tests. + /// + /// + /// true if [provider is available override]; otherwise, false. + /// + public static bool ProviderIsAvailableOverride + { + get { return s_providerIsAvailableOverride; } + set { s_providerIsAvailableOverride = value; } + } + + public override Logger GetLogger(string name) + { + return new LoupeLogger(name, _logWriteDelegate).Log; + } + + public static bool IsLoggerAvailable() + { + return ProviderIsAvailableOverride && GetLogManagerType() != null; + } + + private static Type GetLogManagerType() + { + return Type.GetType("Gibraltar.Agent.Log, Gibraltar.Agent"); + } + + private static WriteDelegate GetLogWriteDelegate() + { + Type logManagerType = GetLogManagerType(); + Type logMessageSeverityType = Type.GetType("Gibraltar.Agent.LogMessageSeverity, Gibraltar.Agent"); + Type logWriteModeType = Type.GetType("Gibraltar.Agent.LogWriteMode, Gibraltar.Agent"); + + MethodInfo method = logManagerType.GetMethodPortable( + "Write", + logMessageSeverityType, typeof(string), typeof(int), typeof(Exception), typeof(bool), + logWriteModeType, typeof(string), typeof(string), typeof(string), typeof(string), typeof(object[])); + + var callDelegate = (WriteDelegate)method.CreateDelegate(typeof(WriteDelegate)); + return callDelegate; + } + + internal class LoupeLogger + { + private const string LogSystem = "LibLog"; + + private readonly string _category; + private readonly WriteDelegate _logWriteDelegate; + private readonly int _skipLevel; + + internal LoupeLogger(string category, WriteDelegate logWriteDelegate) + { + _category = category; + _logWriteDelegate = logWriteDelegate; +#if DEBUG + _skipLevel = 2; +#else + _skipLevel = 1; +#endif + } + + public bool Log(LogLevel logLevel, Func messageFunc, Exception exception, params object[] formatParameters) + { + if (messageFunc == null) + { + //nothing to log.. + return true; + } + + messageFunc = LogMessageFormatter.SimulateStructuredLogging(messageFunc, formatParameters); + + _logWriteDelegate(ToLogMessageSeverity(logLevel), LogSystem, _skipLevel, exception, true, 0, null, + _category, null, messageFunc.Invoke()); + + return true; + } + + private static int ToLogMessageSeverity(LogLevel logLevel) + { + switch (logLevel) + { + case LogLevel.Trace: + return TraceEventTypeValues.Verbose; + case LogLevel.Debug: + return TraceEventTypeValues.Verbose; + case LogLevel.Info: + return TraceEventTypeValues.Information; + case LogLevel.Warn: + return TraceEventTypeValues.Warning; + case LogLevel.Error: + return TraceEventTypeValues.Error; + case LogLevel.Fatal: + return TraceEventTypeValues.Critical; + default: + throw new ArgumentOutOfRangeException("logLevel"); + } + } + } + } + + internal static class TraceEventTypeValues + { + internal static readonly Type Type; + internal static readonly int Verbose; + internal static readonly int Information; + internal static readonly int Warning; + internal static readonly int Error; + internal static readonly int Critical; + + [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline")] + static TraceEventTypeValues() + { + var assembly = typeof(Uri).GetAssemblyPortable(); // This is to get to the System.dll assembly in a PCL compatible way. + if (assembly == null) + { + return; + } + Type = assembly.GetType("System.Diagnostics.TraceEventType"); + if (Type == null) return; + Verbose = (int)Enum.Parse(Type, "Verbose", false); + Information = (int)Enum.Parse(Type, "Information", false); + Warning = (int)Enum.Parse(Type, "Warning", false); + Error = (int)Enum.Parse(Type, "Error", false); + Critical = (int)Enum.Parse(Type, "Critical", false); + } + } + + internal static class LogMessageFormatter + { + //private static readonly Regex Pattern = new Regex(@"\{@?\w{1,}\}"); +#if LIBLOG_PORTABLE + private static readonly Regex Pattern = new Regex(@"(?[^\d{][^ }]*)}"); +#else + private static readonly Regex Pattern = new Regex(@"(?[^ :{}]+)(?:[^}]+)?}", RegexOptions.Compiled); +#endif + + /// + /// Some logging frameworks support structured logging, such as serilog. This will allow you to add names to structured data in a format string: + /// For example: Log("Log message to {user}", user). This only works with serilog, but as the user of LibLog, you don't know if serilog is actually + /// used. So, this class simulates that. it will replace any text in {curly braces} with an index number. + /// + /// "Log {message} to {user}" would turn into => "Log {0} to {1}". Then the format parameters are handled using regular .net string.Format. + /// + /// The message builder. + /// The format parameters. + /// + public static Func SimulateStructuredLogging(Func messageBuilder, object[] formatParameters) + { + if (formatParameters == null || formatParameters.Length == 0) + { + return messageBuilder; + } + + return () => + { + string targetMessage = messageBuilder(); + IEnumerable patternMatches; + return FormatStructuredMessage(targetMessage, formatParameters, out patternMatches); + }; + } + + private static string ReplaceFirst(string text, string search, string replace) + { + int pos = text.IndexOf(search, StringComparison.Ordinal); + if (pos < 0) + { + return text; + } + return text.Substring(0, pos) + replace + text.Substring(pos + search.Length); + } + + public static string FormatStructuredMessage(string targetMessage, object[] formatParameters, out IEnumerable patternMatches) + { + if (formatParameters.Length == 0) + { + patternMatches = Enumerable.Empty(); + return targetMessage; + } + + List processedArguments = new List(); + patternMatches = processedArguments; + + foreach (Match match in Pattern.Matches(targetMessage)) + { + var arg = match.Groups["arg"].Value; + + int notUsed; + if (!int.TryParse(arg, out notUsed)) + { + int argumentIndex = processedArguments.IndexOf(arg); + if (argumentIndex == -1) + { + argumentIndex = processedArguments.Count; + processedArguments.Add(arg); + } + + targetMessage = ReplaceFirst(targetMessage, match.Value, + "{" + argumentIndex + match.Groups["format"].Value + "}"); + } + } + try + { + return string.Format(CultureInfo.InvariantCulture, targetMessage, formatParameters); + } + catch (FormatException ex) + { + throw new FormatException("The input string '" + targetMessage + "' could not be formatted using string.Format", ex); + } + } + } + + internal static class TypeExtensions + { + internal static ConstructorInfo GetConstructorPortable(this Type type, params Type[] types) + { +#if LIBLOG_PORTABLE + return type.GetTypeInfo().DeclaredConstructors.FirstOrDefault + (constructor => + constructor.GetParameters() + .Select(parameter => parameter.ParameterType) + .SequenceEqual(types)); +#else + return type.GetConstructor(types); +#endif + } + + internal static MethodInfo GetMethodPortable(this Type type, string name) + { +#if LIBLOG_PORTABLE + return type.GetRuntimeMethods().SingleOrDefault(m => m.Name == name); +#else + return type.GetMethod(name); +#endif + } + + internal static MethodInfo GetMethodPortable(this Type type, string name, params Type[] types) + { +#if LIBLOG_PORTABLE + return type.GetRuntimeMethod(name, types); +#else + return type.GetMethod(name, types); +#endif + } + + internal static PropertyInfo GetPropertyPortable(this Type type, string name) + { +#if LIBLOG_PORTABLE + return type.GetRuntimeProperty(name); +#else + return type.GetProperty(name); +#endif + } + + internal static IEnumerable GetFieldsPortable(this Type type) + { +#if LIBLOG_PORTABLE + return type.GetRuntimeFields(); +#else + return type.GetFields(); +#endif + } + + internal static Type GetBaseTypePortable(this Type type) + { +#if LIBLOG_PORTABLE + return type.GetTypeInfo().BaseType; +#else + return type.BaseType; +#endif + } + +#if LIBLOG_PORTABLE + internal static MethodInfo GetGetMethod(this PropertyInfo propertyInfo) + { + return propertyInfo.GetMethod; + } + + internal static MethodInfo GetSetMethod(this PropertyInfo propertyInfo) + { + return propertyInfo.SetMethod; + } +#endif + +#if !LIBLOG_PORTABLE + internal static object CreateDelegate(this MethodInfo methodInfo, Type delegateType) + { + return Delegate.CreateDelegate(delegateType, methodInfo); + } +#endif + + internal static Assembly GetAssemblyPortable(this Type type) + { +#if LIBLOG_PORTABLE + return type.GetTypeInfo().Assembly; +#else + return type.Assembly; +#endif + } + } + + internal class DisposableAction : IDisposable + { + private readonly Action _onDispose; + + public DisposableAction(Action onDispose = null) + { + _onDispose = onDispose; + } + + public void Dispose() + { + if (_onDispose != null) + { + _onDispose(); + } + } + } +} diff --git a/Source/ZoomNet/Models/PaginatedResponse.cs b/Source/ZoomNet/Models/PaginatedResponse.cs new file mode 100644 index 00000000..6e9ade76 --- /dev/null +++ b/Source/ZoomNet/Models/PaginatedResponse.cs @@ -0,0 +1,45 @@ +using Newtonsoft.Json; + +namespace ZoomNet.Models +{ + /// + /// Pagination Object. + /// + /// The type of records. + public class PaginatedResponse + { + /// + /// Gets or sets the number of items returned on this page. + /// + /// The number of items returned on this page. + [JsonProperty(PropertyName = "page_count")] + public int PageCount { get; set; } + + /// + /// Gets or sets the page number of current results. + /// + /// The page number of current results. + [JsonProperty(PropertyName = "page_number")] + public int PageNumber { get; set; } + + /// + /// Gets or sets the number of records returned within a single API call. + /// + /// The number of records returned within a single API call. + [JsonProperty(PropertyName = "page_size")] + public int PageSize { get; set; } + + /// + /// Gets or sets the number of all records available across pages. + /// + /// The number of all records available across pages. + [JsonProperty(PropertyName = "total_records")] + public int? TotalRecords { get; set; } + + /// + /// Gets or sets the records. + /// + /// The records. + public T[] Records { get; set; } + } +} diff --git a/Source/ZoomNet/Properties/AssemblyInfo.cs b/Source/ZoomNet/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..50fdc1c8 --- /dev/null +++ b/Source/ZoomNet/Properties/AssemblyInfo.cs @@ -0,0 +1,13 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +[assembly: CLSCompliant(true)] +[assembly: InternalsVisibleTo("ZoomNet.UnitTests")] +[assembly: InternalsVisibleTo("ZoomNet.IntegrationTests")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] diff --git a/Source/ZoomNet/Utilities/DiagnosticHandler.cs b/Source/ZoomNet/Utilities/DiagnosticHandler.cs new file mode 100644 index 00000000..7d4be825 --- /dev/null +++ b/Source/ZoomNet/Utilities/DiagnosticHandler.cs @@ -0,0 +1,150 @@ +using Pathoschild.Http.Client; +using Pathoschild.Http.Client.Extensibility; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net.Http; +using System.Text; +using ZoomNet.Logging; + +namespace ZoomNet.Utilities +{ + /// + /// Diagnostic handler for requests dispatched to the SendGrid API. + /// + /// + internal class DiagnosticHandler : IHttpFilter + { + #region FIELDS + + private const string DIAGNOSTIC_ID_HEADER_NAME = "StrongGrid-DiagnosticId"; + private static readonly ILog _logger = LogProvider.For(); + private readonly IDictionary, Tuple> _diagnostics = new Dictionary, Tuple>(); + + #endregion + + #region PUBLIC METHODS + + /// Method invoked just before the HTTP request is submitted. This method can modify the outgoing HTTP request. + /// The HTTP request. + public void OnRequest(IRequest request) + { + request.WithHeader(DIAGNOSTIC_ID_HEADER_NAME, Guid.NewGuid().ToString("N")); + + var httpRequest = request.Message; + + var diagnosticMessage = new StringBuilder(); + diagnosticMessage.AppendLine($"Request: {httpRequest}"); + diagnosticMessage.AppendLine($"Request Content: {httpRequest.Content?.ReadAsStringAsync(null).GetAwaiter().GetResult() ?? ""}"); + + lock (_diagnostics) + { + _diagnostics.Add(new WeakReference(request.Message), new Tuple(diagnosticMessage, Stopwatch.StartNew())); + } + } + + /// Method invoked just after the HTTP response is received. This method can modify the incoming HTTP response. + /// The HTTP response. + /// Whether HTTP error responses should be raised as exceptions. + public void OnResponse(IResponse response, bool httpErrorAsException) + { + var diagnosticMessage = string.Empty; + + try + { + var diagnosticInfo = GetDiagnosticInfo(response.Message.RequestMessage); + var diagnosticStringBuilder = diagnosticInfo.Item1; + var diagnosticTimer = diagnosticInfo.Item2; + if (diagnosticTimer != null) diagnosticTimer?.Stop(); + + var httpResponse = response.Message; + + try + { + diagnosticStringBuilder.AppendLine($"Response: {httpResponse}"); + diagnosticStringBuilder.AppendLine($"Response.Content is null: {httpResponse.Content == null}"); + diagnosticStringBuilder.AppendLine($"Response.Content.Headers is null: {httpResponse.Content?.Headers == null}"); + diagnosticStringBuilder.AppendLine($"Response.Content.Headers.ContentType is null: {httpResponse.Content?.Headers?.ContentType == null}"); + diagnosticStringBuilder.AppendLine($"Response.Content.Headers.ContentType.CharSet: {httpResponse.Content?.Headers?.ContentType?.CharSet ?? ""}"); + diagnosticStringBuilder.AppendLine($"Response.Content: {httpResponse.Content?.ReadAsStringAsync(null).GetAwaiter().GetResult() ?? ""}"); + } + catch + { + // Intentionally ignore errors that may occur when attempting to log the content of the response + } + + if (diagnosticTimer != null) + { + diagnosticStringBuilder.AppendLine($"The request took {diagnosticTimer.Elapsed.ToDurationString()}"); + } + + diagnosticMessage = diagnosticStringBuilder.ToString(); + } + catch (Exception e) + { + Debug.WriteLine("{0}\r\nAN EXCEPTION OCCURED: {1}\r\n{0}", new string('=', 25), e.GetBaseException().Message); + + if (_logger != null && _logger.IsErrorEnabled()) + { + _logger.Error(e, "An exception occured when inspecting the response from SendGrid"); + } + } + finally + { + if (!string.IsNullOrEmpty(diagnosticMessage)) + { + Debug.WriteLine("{0}\r\n{1}{0}", new string('=', 25), diagnosticMessage); + + if (_logger != null && _logger.IsDebugEnabled()) + { + _logger.Debug(diagnosticMessage + .Replace("{", "{{") + .Replace("}", "}}")); + } + } + } + } + + #endregion + + #region PRIVATE METHODS + + private Tuple GetDiagnosticInfo(HttpRequestMessage requestMessage) + { + lock (_diagnostics) + { + var diagnosticId = requestMessage.Headers.GetValues(DIAGNOSTIC_ID_HEADER_NAME).FirstOrDefault(); + + foreach (WeakReference key in _diagnostics.Keys.ToArray()) + { + // Check if garbage collected + if (!key.TryGetTarget(out HttpRequestMessage request)) + { + _diagnostics.Remove(key); + continue; + } + + // Check if different request + var requestDiagnosticId = request.Headers.GetValues(DIAGNOSTIC_ID_HEADER_NAME).FirstOrDefault(); + if (requestDiagnosticId != diagnosticId) + { + continue; + } + + // Get the diagnostic info from dictionary + var diagnosticInfo = _diagnostics[key]; + + // Remove the diagnostic info from dictionary + _diagnostics.Remove(key); + + return diagnosticInfo; + } + } + + return new Tuple(new StringBuilder(), null); + } + + #endregion + } +} diff --git a/Source/ZoomNet/Utilities/Extensions.cs b/Source/ZoomNet/Utilities/Extensions.cs new file mode 100644 index 00000000..29f25824 --- /dev/null +++ b/Source/ZoomNet/Utilities/Extensions.cs @@ -0,0 +1,484 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Pathoschild.Http.Client; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using ZoomNet.Models; + +namespace ZoomNet.Utilities +{ + /// + /// Extension methods. + /// + internal static class Extensions + { + private static readonly DateTime EPOCH = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + /// + /// Converts a 'unix time' (which is expressed as the number of seconds since midnight on + /// January 1st 1970) to a .Net . + /// + /// The unix time. + /// + /// The . + /// + public static DateTime FromUnixTime(this long unixTime) + { + return EPOCH.AddSeconds(unixTime); + } + + /// + /// Converts a .Net into a 'Unix time' (which is expressed as the number + /// of seconds since midnight on January 1st 1970). + /// + /// The date. + /// + /// The numer of seconds since midnight on January 1st 1970. + /// + public static long ToUnixTime(this DateTime date) + { + return Convert.ToInt64((date.ToUniversalTime() - EPOCH).TotalSeconds); + } + + /// + /// Reads the content of the HTTP response as string asynchronously. + /// + /// The content. + /// The encoding. You can leave this parameter null and the encoding will be + /// automatically calculated based on the charset in the response. Also, UTF-8 + /// encoding will be used if the charset is absent from the response, is blank + /// or contains an invalid value. + /// The string content of the response. + /// + /// This method is an improvement over the built-in ReadAsStringAsync method + /// because it can handle invalid charset returned in the response. For example + /// you may be sending a request to an API that returns a blank charset or a + /// mispelled one like 'utf8' instead of the correctly spelled 'utf-8'. The + /// built-in method throws an exception if an invalid charset is specified + /// while this method uses the UTF-8 encoding in that situation. + /// + /// My motivation for writing this extension method was to work around a situation + /// where the 3rd party API I was sending requests to would sometimes return 'utf8' + /// as the charset and an exception would be thrown when I called the ReadAsStringAsync + /// method to get the content of the response into a string because the .Net HttpClient + /// would attempt to determine the proper encoding to use but it would fail due to + /// the fact that the charset was misspelled. I contacted the vendor, asking them + /// to either omit the charset or fix the misspelling but they didn't feel the need + /// to fix this issue because: + /// "in some programming languages, you can use the syntax utf8 instead of utf-8". + /// In other words, they are happy to continue using the misspelled value which is + /// supported by "some" programming languages instead of using the properly spelled + /// value which is supported by all programming languages. + /// + /// + /// + /// var httpRequest = new HttpRequestMessage + /// { + /// Method = HttpMethod.Get, + /// RequestUri = new Uri("https://api.vendor.com/v1/endpoint") + /// }; + /// var httpClient = new HttpClient(); + /// var response = await httpClient.SendAsync(httpRequest, CancellationToken.None).ConfigureAwait(false); + /// var responseContent = await response.Content.ReadAsStringAsync(null).ConfigureAwait(false); + /// + /// + public static async Task ReadAsStringAsync(this HttpContent content, Encoding encoding) + { + var responseStream = await content.ReadAsStreamAsync().ConfigureAwait(false); + var responseContent = string.Empty; + + if (encoding == null) encoding = content.GetEncoding(Encoding.UTF8); + + // This is important: we must make a copy of the response stream otherwise we would get an + // exception on subsequent attempts to read the content of the stream + using (var ms = new MemoryStream()) + { + await content.CopyToAsync(ms).ConfigureAwait(false); + ms.Position = 0; + using (var sr = new StreamReader(ms, encoding)) + { + responseContent = await sr.ReadToEndAsync().ConfigureAwait(false); + } + } + + return responseContent; + } + + /// + /// Gets the encoding. + /// + /// The content. + /// The default encoding. + /// + /// The encoding. + /// + /// + /// This method tries to get the encoding based on the charset or uses the + /// 'defaultEncoding' if the charset is empty or contains an invalid value. + /// + /// + /// + /// var httpRequest = new HttpRequestMessage + /// { + /// Method = HttpMethod.Get, + /// RequestUri = new Uri("https://my.api.com/v1/myendpoint") + /// }; + /// var httpClient = new HttpClient(); + /// var response = await httpClient.SendAsync(httpRequest, CancellationToken.None).ConfigureAwait(false); + /// var encoding = response.Content.GetEncoding(Encoding.UTF8); + /// + /// + public static Encoding GetEncoding(this HttpContent content, Encoding defaultEncoding) + { + var encoding = defaultEncoding; + var charset = content.Headers.ContentType.CharSet; + if (!string.IsNullOrEmpty(charset)) + { + try + { + encoding = Encoding.GetEncoding(charset); + } + catch + { + encoding = defaultEncoding; + } + } + + return encoding; + } + + /// Asynchronously retrieve the JSON encoded response body and convert it to an object of the desired type. + /// The response model to deserialize into. + /// The response. + /// The name of the JSON property (or null if not applicable) where the desired data is stored. + /// Converter that will be used during deserialization. + /// Returns the strongly typed object. + /// An error occurred processing the response. + public static Task AsObject(this IResponse response, string propertyName = null, JsonConverter jsonConverter = null) + { + return response.Message.Content.AsObject(propertyName, jsonConverter); + } + + /// Asynchronously retrieve the JSON encoded response body and convert it to an object of the desired type. + /// The response model to deserialize into. + /// The request. + /// The name of the JSON property (or null if not applicable) where the desired data is stored. + /// Converter that will be used during deserialization. + /// Returns the strongly typed object. + /// An error occurred processing the response. + public static async Task AsObject(this IRequest request, string propertyName = null, JsonConverter jsonConverter = null) + { + var response = await request.AsMessage().ConfigureAwait(false); + return await response.Content.AsObject(propertyName, jsonConverter).ConfigureAwait(false); + } + + /// Asynchronously retrieve the JSON encoded response body and convert it to a 'PaginatedResponse' object. + /// The response model to deserialize into. + /// The response. + /// The name of the JSON property (or null if not applicable) where the desired data is stored. + /// Converter that will be used during deserialization. + /// Returns the paginated response. + /// An error occurred processing the response. + public static Task> AsPaginatedResponse(this IResponse response, string propertyName, JsonConverter jsonConverter = null) + { + return response.Message.Content.AsPaginatedResponse(propertyName, jsonConverter); + } + + /// Asynchronously retrieve the JSON encoded response body and convert it to a 'PaginatedResponse' object. + /// The response model to deserialize into. + /// The request. + /// The name of the JSON property (or null if not applicable) where the desired data is stored. + /// Converter that will be used during deserialization. + /// Returns the paginated response. + /// An error occurred processing the response. + public static async Task> AsPaginatedResponse(this IRequest request, string propertyName, JsonConverter jsonConverter = null) + { + var response = await request.AsMessage().ConfigureAwait(false); + return await response.Content.AsPaginatedResponse(propertyName, jsonConverter).ConfigureAwait(false); + } + + /// Set the body content of the HTTP request. + /// The type of object to serialize into a JSON string. + /// The request. + /// The value to serialize into the HTTP body content. + /// Returns the request builder for chaining. + /// + /// This method is equivalent to IRequest.AsBody<T>(T body) because omitting the media type + /// causes the first formatter in MediaTypeFormatterCollection to be used by default and the first + /// formatter happens to be the JSON formatter. However, I don't feel good about relying on the + /// default ordering of the items in the MediaTypeFormatterCollection. + /// + public static IRequest WithJsonBody(this IRequest request, T body) + { + return request.WithBody(body, new MediaTypeHeaderValue("application/json")); + } + + /// Asynchronously retrieve the response body as a . + /// The response. + /// The encoding. You can leave this parameter null and the encoding will be + /// automatically calculated based on the charset in the response. Also, UTF-8 + /// encoding will be used if the charset is absent from the response, is blank + /// or contains an invalid value. + /// Returns the response body, or null if the response has no body. + /// An error occurred processing the response. + public static Task AsString(this IResponse response, Encoding encoding) + { + return response.Message.Content.ReadAsStringAsync(encoding); + } + + /// Asynchronously retrieve the response body as a . + /// The request. + /// The encoding. You can leave this parameter null and the encoding will be + /// automatically calculated based on the charset in the response. Also, UTF-8 + /// encoding will be used if the charset is absent from the response, is blank + /// or contains an invalid value. + /// Returns the response body, or null if the response has no body. + /// An error occurred processing the response. + public static async Task AsString(this IRequest request, Encoding encoding) + { + IResponse response = await request.AsResponse().ConfigureAwait(false); + return await response.AsString(encoding).ConfigureAwait(false); + } + + /// + /// Converts the value of the current System.TimeSpan object to its equivalent string + /// representation by using a human readable format. + /// + /// The time span. + /// Returns the human readable representation of the TimeSpan. + public static string ToDurationString(this TimeSpan timeSpan) + { + void AppendFormatIfNecessary(StringBuilder stringBuilder, string timePart, int value) + { + if (value <= 0) return; + stringBuilder.AppendFormat($" {value} {timePart}{(value > 1 ? "s" : string.Empty)}"); + } + + // In case the TimeSpan is extremely short + if (timeSpan.TotalMilliseconds <= 1) return "1 millisecond"; + + var result = new StringBuilder(); + AppendFormatIfNecessary(result, "day", timeSpan.Days); + AppendFormatIfNecessary(result, "hour", timeSpan.Hours); + AppendFormatIfNecessary(result, "minute", timeSpan.Days); + AppendFormatIfNecessary(result, "second", timeSpan.Days); + AppendFormatIfNecessary(result, "millisecond", timeSpan.Days); + return result.ToString().Trim(); + } + + /// + /// Ensure that a string starts with a given prefix. + /// + /// The value. + /// The prefix. + /// The value including the prefix. + public static string EnsureStartsWith(this string value, string prefix) + { + return !string.IsNullOrEmpty(value) && value.StartsWith(prefix) ? value : string.Concat(prefix, value); + } + + /// + /// Ensure that a string ends with a given suffix. + /// + /// The value. + /// The sufix. + /// The value including the suffix. + public static string EnsureEndsWith(this string value, string suffix) + { + return !string.IsNullOrEmpty(value) && value.EndsWith(suffix) ? value : string.Concat(value, suffix); + } + + public static void AddPropertyIfValue(this JObject jsonObject, string propertyName, string value) + { + if (string.IsNullOrEmpty(value)) return; + jsonObject.Add(propertyName, value); + } + + public static void AddPropertyIfValue(this JObject jsonObject, string propertyName, T value, JsonConverter converter = null) + { + if (EqualityComparer.Default.Equals(value, default(T))) return; + + var jsonSerializer = new JsonSerializer(); + if (converter != null) + { + jsonSerializer.Converters.Add(converter); + } + + jsonObject.Add(propertyName, JToken.FromObject(value, jsonSerializer)); + } + + public static void AddPropertyIfValue(this JObject jsonObject, string propertyName, IEnumerable value, JsonConverter converter = null) + { + if (value == null || !value.Any()) return; + + var jsonSerializer = new JsonSerializer(); + if (converter != null) + { + jsonSerializer.Converters.Add(converter); + } + + jsonObject.Add(propertyName, JArray.FromObject(value.ToArray(), jsonSerializer)); + } + + public static T GetPropertyValue(this JToken item, string name) + { + if (item[name] == null) return default(T); + return item[name].Value(); + } + + public static async Task ForEachAsync(this IEnumerable items, Func> action, int maxDegreeOfParalellism) + { + var allTasks = new List>(); + var throttler = new SemaphoreSlim(initialCount: maxDegreeOfParalellism); + foreach (var item in items) + { + await throttler.WaitAsync(); + allTasks.Add( + Task.Run(async () => + { + try + { + return await action(item).ConfigureAwait(false); + } + finally + { + throttler.Release(); + } + })); + } + + var results = await Task.WhenAll(allTasks).ConfigureAwait(false); + return results; + } + + public static async Task ForEachAsync(this IEnumerable items, Func action, int maxDegreeOfParalellism) + { + var allTasks = new List(); + var throttler = new SemaphoreSlim(initialCount: maxDegreeOfParalellism); + foreach (var item in items) + { + await throttler.WaitAsync(); + allTasks.Add( + Task.Run(async () => + { + try + { + await action(item).ConfigureAwait(false); + } + finally + { + throttler.Release(); + } + })); + } + + await Task.WhenAll(allTasks).ConfigureAwait(false); + } + + /// + /// Gets the attribute of the specified type. + /// + /// The type of the desired attribute. + /// The enum value. + /// The attribute. + public static T GetAttributeOfType(this Enum enumVal) + where T : Attribute + { + return enumVal.GetType() + .GetTypeInfo() + .DeclaredMembers + .SingleOrDefault(x => x.Name == enumVal.ToString()) + ?.GetCustomAttribute(false); + } + + /// + /// Indicates if an object contain a numerical value. + /// + /// The object. + /// A boolean indicating if the object contains a numerical value. + public static bool IsNumber(this object value) + { + return value is sbyte + || value is byte + || value is short + || value is ushort + || value is int + || value is uint + || value is long + || value is ulong + || value is float + || value is double + || value is decimal; + } + + /// Asynchronously converts the JSON encoded content and converts it to an object of the desired type. + /// The response model to deserialize into. + /// The content. + /// The name of the JSON property (or null if not applicable) where the desired data is stored. + /// Converter that will be used during deserialization. + /// Returns the strongly typed object. + /// An error occurred processing the response. + private static async Task AsObject(this HttpContent httpContent, string propertyName = null, JsonConverter jsonConverter = null) + { + var responseContent = await httpContent.ReadAsStringAsync(null).ConfigureAwait(false); + + var serializer = new JsonSerializer(); + if (jsonConverter != null) serializer.Converters.Add(jsonConverter); + + if (!string.IsNullOrEmpty(propertyName)) + { + var jObject = JObject.Parse(responseContent); + var jProperty = jObject.Property(propertyName); + if (jProperty == null) + { + throw new ArgumentException($"The response does not contain a field called '{propertyName}'", nameof(propertyName)); + } + + return jProperty.Value.ToObject(serializer); + } + else if (typeof(T).IsArray) + { + return JArray.Parse(responseContent).ToObject(serializer); + } + else + { + return JObject.Parse(responseContent).ToObject(serializer); + } + } + + /// Asynchronously retrieve the JSON encoded content and converts it to a 'PaginatedResponse' object. + /// The response model to deserialize into. + /// The content. + /// The name of the JSON property (or null if not applicable) where the desired data is stored. + /// Converter that will be used during deserialization. + /// Returns the response body, or null if the response has no body. + /// An error occurred processing the response. + private static async Task> AsPaginatedResponse(this HttpContent httpContent, string propertyName, JsonConverter jsonConverter = null) + { + var responseContent = await httpContent.ReadAsStringAsync(null).ConfigureAwait(false); + var jObject = JObject.Parse(responseContent); + + var serializer = new JsonSerializer(); + if (jsonConverter != null) serializer.Converters.Add(jsonConverter); + + var result = new PaginatedResponse() + { + PageCount = jObject.Property("page_count").Value.ToObject(), + PageNumber = jObject.Property("page_number").Value.ToObject(), + PageSize = jObject.Property("page_size").Value.ToObject(), + Records = jObject.Property(propertyName).Value.ToObject(serializer), + TotalRecords = jObject.Property("total_records").Value.ToObject() + }; + + return result; + } + } +} diff --git a/Source/ZoomNet/Utilities/JwtTokenHandler.cs b/Source/ZoomNet/Utilities/JwtTokenHandler.cs new file mode 100644 index 00000000..fc78ae61 --- /dev/null +++ b/Source/ZoomNet/Utilities/JwtTokenHandler.cs @@ -0,0 +1,71 @@ +using Jose; +using Pathoschild.Http.Client; +using Pathoschild.Http.Client.Extensibility; +using System; +using System.Collections.Generic; +using System.Text; + +namespace ZoomNet.Utilities +{ + /// + /// Handler to ensure requests to the Zoom API include a valid JWT token. + /// + /// + internal class JwtTokenHandler : IHttpFilter + { + private static readonly object _lock = new object(); + + private readonly string _apiKey; + private readonly string _apiSecret; + private readonly TimeSpan _clockSkew = TimeSpan.FromMinutes(5); + private readonly TimeSpan _tokenLifeSpan = TimeSpan.FromMinutes(30); + private readonly DateTime _jwtTokenExpiration = DateTime.MinValue; + + private string _jwtToken; + + public JwtTokenHandler(string apiKey, string apiSecret, TimeSpan? tokenLifeSpan = null, TimeSpan? clockSkew = null) + { + _apiKey = apiKey; + _apiSecret = apiSecret; + _tokenLifeSpan = tokenLifeSpan.GetValueOrDefault(TimeSpan.FromMinutes(30)); + _clockSkew = clockSkew.GetValueOrDefault(TimeSpan.FromMinutes(5)); + } + + /// Method invoked just before the HTTP request is submitted. This method can modify the outgoing HTTP request. + /// The HTTP request. + public void OnRequest(IRequest request) + { + RefreshTokenIfNecessary(); + request.WithBearerAuthentication(_jwtToken); + } + + /// Method invoked just after the HTTP response is received. This method can modify the incoming HTTP response. + /// The HTTP response. + /// Whether HTTP error responses should be raised as exceptions. + public void OnResponse(IResponse response, bool httpErrorAsException) { } + + private void RefreshTokenIfNecessary() + { + if (TokenIsExpired()) + { + lock (_lock) + { + if (TokenIsExpired()) + { + var jwtPayload = new Dictionary() + { + { "iss", _apiKey }, + { "exp", DateTime.UtcNow.Add(_tokenLifeSpan).ToUnixTime() } + }; + _jwtToken = JWT.Encode(jwtPayload, Encoding.ASCII.GetBytes(_apiSecret), JwsAlgorithm.HS256); + } + } + } + } + + private bool TokenIsExpired() + { + return _jwtTokenExpiration <= DateTime.UtcNow.Add(_clockSkew); + } + } +} diff --git a/Source/ZoomNet/Utilities/ZoomErrorHandler.cs b/Source/ZoomNet/Utilities/ZoomErrorHandler.cs new file mode 100644 index 00000000..4db5acfe --- /dev/null +++ b/Source/ZoomNet/Utilities/ZoomErrorHandler.cs @@ -0,0 +1,81 @@ +using Newtonsoft.Json.Linq; +using Pathoschild.Http.Client; +using Pathoschild.Http.Client.Extensibility; +using System; +using System.Net.Http; +using System.Threading.Tasks; + +namespace ZoomNet.Utilities +{ + /// + /// Error handler for requests dispatched to the Zoom API. + /// + /// + internal class ZoomErrorHandler : IHttpFilter + { + #region PUBLIC METHODS + + /// Method invoked just before the HTTP request is submitted. This method can modify the outgoing HTTP request. + /// The HTTP request. + public void OnRequest(IRequest request) { } + + /// Method invoked just after the HTTP response is received. This method can modify the incoming HTTP response. + /// The HTTP response. + /// Whether HTTP error responses should be raised as exceptions. + public void OnResponse(IResponse response, bool httpErrorAsException) + { + if (response.Message.IsSuccessStatusCode) return; + + var errorMessage = GetErrorMessage(response.Message).Result; + throw new Exception(errorMessage); + } + + private static async Task GetErrorMessage(HttpResponseMessage message) + { + // Default error message + var errorMessage = $"{(int)message.StatusCode}: {message.ReasonPhrase}"; + + if (message.Content != null) + { + /* + In case of an error, the Zoom API returns a JSON string that looks like this: + { + "code": 300, + "message": "This meeting has not registration required: 544993922" + } + */ + + var responseContent = await message.Content.ReadAsStringAsync(null).ConfigureAwait(false); + + if (!string.IsNullOrEmpty(responseContent)) + { + try + { + var jObject = JObject.Parse(responseContent); + var codeProperty = jObject["code"]; + var messageProperty = jObject["message"]; + + if (messageProperty != null) + { + errorMessage = messageProperty.Value(); + } + else if (codeProperty != null) + { + errorMessage = $"Error code: {codeProperty.Value()}"; + } +#pragma warning disable RECS0022 // A catch clause that catches System.Exception and has an empty body + } + catch +#pragma warning restore RECS0022 // A catch clause that catches System.Exception and has an empty body + { + // Intentionally ignore parsing errors + } + } + } + + return errorMessage; + } + + #endregion + } +} diff --git a/Source/ZoomNet/ZoomNet.csproj b/Source/ZoomNet/ZoomNet.csproj index 661bfdfe..528a653c 100644 --- a/Source/ZoomNet/ZoomNet.csproj +++ b/Source/ZoomNet/ZoomNet.csproj @@ -1,7 +1,7 @@  - net452;netstandard1.3;netstandard2.0 + net452;netstandard2.0 anycpu true Library @@ -26,7 +26,7 @@ - + @@ -41,19 +41,14 @@ - - $(DefineConstants);LIBLOG_PORTABLE;NETFULL + $(DefineConstants);NETFULL - - $(DefineConstants);LIBLOG_PORTABLE;NETSTANDARD;NETSTANDARD1 - - - - $(DefineConstants);LIBLOG_PORTABLE;NETSTANDARD;NETSTANDARD2 + + $(DefineConstants);NETSTANDARD From 3fe1e6ee743cdd8252f5199fc4bfcf050b6a37c8 Mon Sep 17 00:00:00 2001 From: Jeremie Desautels Date: Sat, 30 Mar 2019 15:11:06 -0400 Subject: [PATCH 03/41] Fix typo --- Source/ZoomNet/Utilities/DiagnosticHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/ZoomNet/Utilities/DiagnosticHandler.cs b/Source/ZoomNet/Utilities/DiagnosticHandler.cs index 7d4be825..83070dee 100644 --- a/Source/ZoomNet/Utilities/DiagnosticHandler.cs +++ b/Source/ZoomNet/Utilities/DiagnosticHandler.cs @@ -18,7 +18,7 @@ internal class DiagnosticHandler : IHttpFilter { #region FIELDS - private const string DIAGNOSTIC_ID_HEADER_NAME = "StrongGrid-DiagnosticId"; + private const string DIAGNOSTIC_ID_HEADER_NAME = "ZoomNet-DiagnosticId"; private static readonly ILog _logger = LogProvider.For(); private readonly IDictionary, Tuple> _diagnostics = new Dictionary, Tuple>(); From 1e1f4449a07b5e464888df397049e0af3d96e29a Mon Sep 17 00:00:00 2001 From: Jeremie Desautels Date: Wed, 5 Jun 2019 12:50:21 -0400 Subject: [PATCH 04/41] Refresh resources --- .editorconfig | 51 +++++++++++++++++++++++++++++++++++++++++++++++---- .gitignore | 14 ++++++++------ appveyor.yml | 2 +- build.cake | 4 ++-- build.ps1 | 38 ++++++++++++++++++++++++++------------ 5 files changed, 84 insertions(+), 25 deletions(-) diff --git a/.editorconfig b/.editorconfig index 586b27c1..20a49b63 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,11 +1,54 @@ -# EditorConfig is awesome: http://EditorConfig.org - -# top-most EditorConfig file +# This file is the top-most EditorConfig file root = true -# Windows-style newlines with a newline ending every file +# All Files [*] +charset = utf-8 end_of_line = crlf +indent_style = space +indent_size = 4 +insert_final_newline = false +trim_trailing_whitespace = true + +# .NET Code files +[*.{cs,csx,cake,vb}] indent_style = tab +tab_width = 4 insert_final_newline = true + +# Visual Studio Solution Files +[*.sln] +indent_style = tab tab_width = 4 + +# Visual Studio XML Project Files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# Various XML Configuration Files +[*.{xml,config,props,targets,nuspec,resx,ruleset,vsixmanifest,vsct}] +indent_size = 2 + +# JSON Files +[*.{json,json5}] +indent_size = 2 + +# YAML Files +[*.{yml,yaml}] +indent_size = 2 + +# Markdown Files +[*.md] +trim_trailing_whitespace = false + +# Web Files +[*.{htm,html,js,ts,tsx,css,sass,scss,less,svg,vue}] +indent_size = 2 +insert_final_newline = true + +# Batch Files +[*.{cmd,bat}] + +# Bash Files +[*.sh] +end_of_line = lf diff --git a/.gitignore b/.gitignore index 345b611f..d6ffc67f 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,9 @@ # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs +# Mono auto generated files +mono_crash.* + # Build results [Dd]ebug/ [Dd]ebugPublic/ @@ -210,6 +213,8 @@ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt *.appx +*.appxbundle +*.appxupload # Visual Studio cache files # files ending in .cache can be ignored @@ -235,8 +240,6 @@ orleans.codegen.cs # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ -# ASP.NET Core default setup: bower directory is configured as wwwroot/lib/ and bower restore is true -**/wwwroot/lib/ # RIA/Silverlight projects Generated_Code/ @@ -297,10 +300,6 @@ paket-files/ # FAKE - F# Make .fake/ -# JetBrains Rider -.idea/ -*.sln.iml - # CodeRush personal settings .cr/personal @@ -345,6 +344,9 @@ ASALocalRun/ # BeatPulse healthcheck temp database healthchecksdb +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + # WinMerge *.bak diff --git a/appveyor.yml b/appveyor.yml index 7ce10ec6..82c4365e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,7 +4,7 @@ init: # Build script build_script: - - ps: .\build.ps1 -Target "AppVeyor" + - ps: .\build.ps1 -Target AppVeyor # Tests test: off diff --git a/build.cake b/build.cake index 6edb3ea0..57c67c15 100644 --- a/build.cake +++ b/build.cake @@ -2,10 +2,10 @@ #addin nuget:?package=Cake.Coveralls&version=0.9.0 // Install tools. -#tool nuget:?package=GitVersion.CommandLine&version=5.0.0-beta2-6 +#tool nuget:?package=GitVersion.CommandLine&version=4.0.0 #tool nuget:?package=GitReleaseManager&version=0.8.0 #tool nuget:?package=OpenCover&version=4.7.922 -#tool nuget:?package=ReportGenerator&version=4.0.15 +#tool nuget:?package=ReportGenerator&version=4.1.10 #tool nuget:?package=coveralls.io&version=1.4.2 #tool nuget:?package=xunit.runner.console&version=2.4.1 diff --git a/build.ps1 b/build.ps1 index c6c91b25..7f1f813d 100644 --- a/build.ps1 +++ b/build.ps1 @@ -59,7 +59,10 @@ try { # Use integers because the enumeration values for TLS 1.2 and TLS 1.1 won't # exist in .NET 4.0, even though they are addressable if .NET 4.5+ is # installed (.NET 4.5 is an in-place upgrade). - [System.Net.ServicePointManager]::SecurityProtocol = 3072 -bor 768 -bor 192 -bor 48 + # PowerShell Core already has support for TLS 1.2 so we can skip this if running in that. + if (-not $IsCoreCLR) { + [System.Net.ServicePointManager]::SecurityProtocol = 3072 -bor 768 -bor 192 -bor 48 + } } catch { Write-Output 'Unable to set PowerShell to use TLS 1.2 and TLS 1.1 due to old .NET Framework installed. If you see underlying connection closed or trust errors, you may need to upgrade to .NET Framework 4.5+ and PowerShell v3' } @@ -118,7 +121,7 @@ $MODULES_PACKAGES_CONFIG = Join-Path $MODULES_DIR "packages.config" # Make sure tools folder exists if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) { Write-Verbose -Message "Creating tools directory..." - New-Item -Path $TOOLS_DIR -Type directory | out-null + New-Item -Path $TOOLS_DIR -Type Directory | Out-Null } # Make sure that packages.config exist. @@ -155,7 +158,12 @@ if (!(Test-Path $NUGET_EXE)) { } # Save nuget.exe path to environment to be available to child processed -$ENV:NUGET_EXE = $NUGET_EXE +$env:NUGET_EXE = $NUGET_EXE +$env:NUGET_EXE_INVOCATION = if ($IsLinux -or $IsMacOS) { + "mono `"$NUGET_EXE`"" +} else { + "`"$NUGET_EXE`"" +} # Restore tools from NuGet? if(-Not $SkipToolPackageRestore.IsPresent) { @@ -163,16 +171,17 @@ if(-Not $SkipToolPackageRestore.IsPresent) { Set-Location $TOOLS_DIR # Check for changes in packages.config and remove installed tools if true. - [string] $md5Hash = MD5HashFile($PACKAGES_CONFIG) + [string] $md5Hash = MD5HashFile $PACKAGES_CONFIG if((!(Test-Path $PACKAGES_CONFIG_MD5)) -Or - ($md5Hash -ne (Get-Content $PACKAGES_CONFIG_MD5 ))) { + ($md5Hash -ne (Get-Content $PACKAGES_CONFIG_MD5 ))) { Write-Verbose -Message "Missing or changed package.config hash..." Get-ChildItem -Exclude packages.config,nuget.exe,Cake.Bakery | Remove-Item -Recurse } Write-Verbose -Message "Restoring tools from NuGet..." - $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$TOOLS_DIR`"" + + $NuGetOutput = Invoke-Expression "& $env:NUGET_EXE_INVOCATION install -ExcludeVersion -OutputDirectory `"$TOOLS_DIR`"" if ($LASTEXITCODE -ne 0) { Throw "An error occurred while restoring NuGet tools." @@ -181,7 +190,7 @@ if(-Not $SkipToolPackageRestore.IsPresent) { { $md5Hash | Out-File $PACKAGES_CONFIG_MD5 -Encoding "ASCII" } - Write-Verbose -Message ($NuGetOutput | out-string) + Write-Verbose -Message ($NuGetOutput | Out-String) Pop-Location } @@ -192,13 +201,13 @@ if (Test-Path $ADDINS_PACKAGES_CONFIG) { Set-Location $ADDINS_DIR Write-Verbose -Message "Restoring addins from NuGet..." - $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$ADDINS_DIR`"" + $NuGetOutput = Invoke-Expression "& $env:NUGET_EXE_INVOCATION install -ExcludeVersion -OutputDirectory `"$ADDINS_DIR`"" if ($LASTEXITCODE -ne 0) { Throw "An error occurred while restoring NuGet addins." } - Write-Verbose -Message ($NuGetOutput | out-string) + Write-Verbose -Message ($NuGetOutput | Out-String) Pop-Location } @@ -209,13 +218,13 @@ if (Test-Path $MODULES_PACKAGES_CONFIG) { Set-Location $MODULES_DIR Write-Verbose -Message "Restoring modules from NuGet..." - $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$MODULES_DIR`"" + $NuGetOutput = Invoke-Expression "& $env:NUGET_EXE_INVOCATION install -ExcludeVersion -OutputDirectory `"$MODULES_DIR`"" if ($LASTEXITCODE -ne 0) { Throw "An error occurred while restoring NuGet modules." } - Write-Verbose -Message ($NuGetOutput | out-string) + Write-Verbose -Message ($NuGetOutput | Out-String) Pop-Location } @@ -225,6 +234,11 @@ if (!(Test-Path $CAKE_EXE)) { Throw "Could not find Cake.exe at $CAKE_EXE" } +$CAKE_EXE_INVOCATION = if ($IsLinux -or $IsMacOS) { + "mono `"$CAKE_EXE`"" +} else { + "`"$CAKE_EXE`"" +} # Build Cake arguments @@ -238,5 +252,5 @@ $cakeArguments += $ScriptArgs # Start Cake Write-Host "Running build script..." -&$CAKE_EXE $cakeArguments +Invoke-Expression "& $CAKE_EXE_INVOCATION $($cakeArguments -join " ")" exit $LASTEXITCODE From 8a2d52e84ea793b0c37e6974f11da85e0d4ad97f Mon Sep 17 00:00:00 2001 From: Jeremie Desautels Date: Wed, 5 Jun 2019 12:53:54 -0400 Subject: [PATCH 05/41] Generate symbols --- Source/ZoomNet/ZoomNet.csproj | 4 ++-- build.cake | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Source/ZoomNet/ZoomNet.csproj b/Source/ZoomNet/ZoomNet.csproj index 528a653c..69a1a4a3 100644 --- a/Source/ZoomNet/ZoomNet.csproj +++ b/Source/ZoomNet/ZoomNet.csproj @@ -1,4 +1,4 @@ - + net452;netstandard2.0 @@ -6,7 +6,7 @@ true Library $(SemVer) - full + portable diff --git a/build.cake b/build.cake index 57c67c15..cefbb906 100644 --- a/build.cake +++ b/build.cake @@ -229,13 +229,14 @@ Task("Create-NuGet-Package") { Configuration = configuration, IncludeSource = false, - IncludeSymbols = false, + IncludeSymbols = true, NoBuild = true, NoDependencies = true, OutputDirectory = outputDir, ArgumentCustomization = (args) => { return args + .Append("/p:SymbolPackageFormat=snupkg") .Append("/p:Version={0}", versionInfo.LegacySemVerPadded) .Append("/p:AssemblyVersion={0}", versionInfo.MajorMinorPatch) .Append("/p:FileVersion={0}", versionInfo.MajorMinorPatch) From 1824391934217eed35564681d918b91f3bde78db Mon Sep 17 00:00:00 2001 From: Jeremie Desautels Date: Wed, 5 Jun 2019 12:55:09 -0400 Subject: [PATCH 06/41] Support net461, net472 and netstandard2.0 --- Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj | 4 ++-- Source/ZoomNet/ZoomNet.csproj | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj b/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj index dee552a4..7d0682d4 100644 --- a/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj +++ b/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj @@ -1,7 +1,7 @@ - + - net452;netcoreapp2.0 + net461;net472;netcoreapp2.0 StrongGrid.UnitTests StrongGrid.UnitTests false diff --git a/Source/ZoomNet/ZoomNet.csproj b/Source/ZoomNet/ZoomNet.csproj index 69a1a4a3..a3459683 100644 --- a/Source/ZoomNet/ZoomNet.csproj +++ b/Source/ZoomNet/ZoomNet.csproj @@ -1,7 +1,7 @@ - + - net452;netstandard2.0 + net461;net472;netstandard2.0 anycpu true Library From 8cd3dc191df87a2608ddc61ef5388122f9ec40d5 Mon Sep 17 00:00:00 2001 From: Jeremie Desautels Date: Wed, 5 Jun 2019 12:58:48 -0400 Subject: [PATCH 07/41] Fix csproj --- Source/ZoomNet/ZoomNet.csproj | 44 ++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/Source/ZoomNet/ZoomNet.csproj b/Source/ZoomNet/ZoomNet.csproj index a3459683..50dba188 100644 --- a/Source/ZoomNet/ZoomNet.csproj +++ b/Source/ZoomNet/ZoomNet.csproj @@ -10,19 +10,25 @@ - ZoomNet - ZoomNet - ZoomNet - Jeremie Desautels - - ZoomNet is a strongly typed .NET client for Zoom's API. - Copyright © Jeremie Desautels and contributors 2019 - Present - http://jericho.mit-license.org - https://github.com/Jericho/ZoomNet - https://github.com/identicons/jericho.png - false - ZoomNet zoom meeting webinar - + true + true + snupkg + + + + ZoomNet + ZoomNet + ZoomNet + Jeremie Desautels + + ZoomNet is a strongly typed .NET client for Zoom's API. + Copyright © Jeremie Desautels and contributors 2019 - Present + MIT + https://github.com/Jericho/ZoomNet + https://github.com/identicons/jericho.png + false + ZoomNet zoom meeting webinar + @@ -33,21 +39,21 @@ - - - + + + - + - + $(DefineConstants);NETFULL - + $(DefineConstants);NETSTANDARD From ab7a66f534bcddff07f064a6bc66dfaac1f9c334 Mon Sep 17 00:00:00 2001 From: Jeremie Desautels Date: Wed, 5 Jun 2019 12:59:41 -0400 Subject: [PATCH 08/41] Upgrade nuget packages --- Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj | 4 ++-- Source/ZoomNet/ZoomNet.csproj | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj b/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj index 7d0682d4..37f6403b 100644 --- a/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj +++ b/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/Source/ZoomNet/ZoomNet.csproj b/Source/ZoomNet/ZoomNet.csproj index 50dba188..175419a2 100644 --- a/Source/ZoomNet/ZoomNet.csproj +++ b/Source/ZoomNet/ZoomNet.csproj @@ -34,8 +34,8 @@ - - + + From cdcdbe08502925e5654392894db30a14cbbc8f46 Mon Sep 17 00:00:00 2001 From: Jeremie Desautels Date: Wed, 5 Jun 2019 13:00:25 -0400 Subject: [PATCH 09/41] Replace LibLog.cs with nuget package --- Source/ZoomNet/Logging/LibLog.cs | 2329 ------------------------------ Source/ZoomNet/ZoomNet.csproj | 4 + 2 files changed, 4 insertions(+), 2329 deletions(-) delete mode 100644 Source/ZoomNet/Logging/LibLog.cs diff --git a/Source/ZoomNet/Logging/LibLog.cs b/Source/ZoomNet/Logging/LibLog.cs deleted file mode 100644 index 4eac410d..00000000 --- a/Source/ZoomNet/Logging/LibLog.cs +++ /dev/null @@ -1,2329 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This tag ensures the content of this file is not analyzed by StyleCop.Analyzers -// -//------------------------------------------------------------------------------ - -//=============================================================================== -// LibLog -// -// https://github.com/damianh/LibLog -//=============================================================================== -// Copyright © 2011-2015 Damian Hickey. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -//=============================================================================== - -// ReSharper disable PossibleNullReferenceException - -// Define LIBLOG_PORTABLE conditional compilation symbol for PCL compatibility -// -// Define LIBLOG_PUBLIC to enable ability to GET a logger (LogProvider.For<>() etc) from outside this library. NOTE: -// this can have unintended consequences of consumers of your library using your library to resolve a logger. If the -// reason is because you want to open this functionality to other projects within your solution, -// consider [InternalsVisibleTo] instead. -// -// Define LIBLOG_PROVIDERS_ONLY if your library provides its own logging API and you just want to use the -// LibLog providers internally to provide built in support for popular logging frameworks. - -#pragma warning disable 1591 - -using System.Diagnostics.CodeAnalysis; - -[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "ZoomNet.Logging")] -[assembly: SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Scope = "member", Target = "ZoomNet.Logging.Logger.#Invoke(ZoomNet.Logging.LogLevel,System.Func`1,System.Exception,System.Object[])")] - -// If you copied this file manually, you need to change all "ZoomNet.Logging" so not to clash with other libraries -// that use LibLog -#if LIBLOG_PROVIDERS_ONLY -namespace ZoomNet.Logging.LibLog -#else -namespace ZoomNet.Logging -#endif -{ - using System.Collections.Generic; - using System.Diagnostics.CodeAnalysis; -#if LIBLOG_PROVIDERS_ONLY - using ZoomNet.Logging.LibLog.LogProviders; -#else - using ZoomNet.Logging.LogProviders; -#endif - using System; -#if !LIBLOG_PROVIDERS_ONLY - using System.Diagnostics; -#if !LIBLOG_PORTABLE - using System.Runtime.CompilerServices; -#endif -#endif - -#if LIBLOG_PROVIDERS_ONLY - internal -#else - public -#endif - delegate bool Logger(LogLevel logLevel, Func messageFunc, Exception exception = null, params object[] formatParameters); - -#if !LIBLOG_PROVIDERS_ONLY - /// - /// Simple interface that represent a logger. - /// -#if LIBLOG_PUBLIC - public -#else - internal -#endif - interface ILog - { - /// - /// Log a message the specified log level. - /// - /// The log level. - /// The message function. - /// An optional exception. - /// Optional format parameters for the message generated by the messagefunc. - /// true if the message was logged. Otherwise false. - /// - /// Note to implementers: the message func should not be called if the loglevel is not enabled - /// so as not to incur performance penalties. - /// - /// To check IsEnabled call Log with only LogLevel and check the return value, no event will be written. - /// - bool Log(LogLevel logLevel, Func messageFunc, Exception exception = null, params object[] formatParameters); - } -#endif - - /// - /// The log level. - /// -#if LIBLOG_PROVIDERS_ONLY - internal -#else - public -#endif - enum LogLevel - { - Trace, - Debug, - Info, - Warn, - Error, - Fatal - } - -#if !LIBLOG_PROVIDERS_ONLY -#if LIBLOG_PUBLIC - public -#else - internal -#endif - static partial class LogExtensions - { - public static bool IsDebugEnabled(this ILog logger) - { - GuardAgainstNullLogger(logger); - return logger.Log(LogLevel.Debug, null); - } - - public static bool IsErrorEnabled(this ILog logger) - { - GuardAgainstNullLogger(logger); - return logger.Log(LogLevel.Error, null); - } - - public static bool IsFatalEnabled(this ILog logger) - { - GuardAgainstNullLogger(logger); - return logger.Log(LogLevel.Fatal, null); - } - - public static bool IsInfoEnabled(this ILog logger) - { - GuardAgainstNullLogger(logger); - return logger.Log(LogLevel.Info, null); - } - - public static bool IsTraceEnabled(this ILog logger) - { - GuardAgainstNullLogger(logger); - return logger.Log(LogLevel.Trace, null); - } - - public static bool IsWarnEnabled(this ILog logger) - { - GuardAgainstNullLogger(logger); - return logger.Log(LogLevel.Warn, null); - } - - public static void Debug(this ILog logger, Func messageFunc) - { - GuardAgainstNullLogger(logger); - logger.Log(LogLevel.Debug, messageFunc); - } - - public static void Debug(this ILog logger, string message) - { - if (logger.IsDebugEnabled()) - { - logger.Log(LogLevel.Debug, message.AsFunc()); - } - } - - public static void Debug(this ILog logger, string message, params object[] args) - { - logger.DebugFormat(message, args); - } - - public static void Debug(this ILog logger, Exception exception, string message, params object[] args) - { - logger.DebugException(message, exception, args); - } - - public static void DebugFormat(this ILog logger, string message, params object[] args) - { - if (logger.IsDebugEnabled()) - { - logger.LogFormat(LogLevel.Debug, message, args); - } - } - - public static void DebugException(this ILog logger, string message, Exception exception) - { - if (logger.IsDebugEnabled()) - { - logger.Log(LogLevel.Debug, message.AsFunc(), exception); - } - } - - public static void DebugException(this ILog logger, string message, Exception exception, params object[] formatParams) - { - if (logger.IsDebugEnabled()) - { - logger.Log(LogLevel.Debug, message.AsFunc(), exception, formatParams); - } - } - - public static void Error(this ILog logger, Func messageFunc) - { - GuardAgainstNullLogger(logger); - logger.Log(LogLevel.Error, messageFunc); - } - - public static void Error(this ILog logger, string message) - { - if (logger.IsErrorEnabled()) - { - logger.Log(LogLevel.Error, message.AsFunc()); - } - } - - public static void Error(this ILog logger, string message, params object[] args) - { - logger.ErrorFormat(message, args); - } - - public static void Error(this ILog logger, Exception exception, string message, params object[] args) - { - logger.ErrorException(message, exception, args); - } - - public static void ErrorFormat(this ILog logger, string message, params object[] args) - { - if (logger.IsErrorEnabled()) - { - logger.LogFormat(LogLevel.Error, message, args); - } - } - - public static void ErrorException(this ILog logger, string message, Exception exception, params object[] formatParams) - { - if (logger.IsErrorEnabled()) - { - logger.Log(LogLevel.Error, message.AsFunc(), exception, formatParams); - } - } - - public static void Fatal(this ILog logger, Func messageFunc) - { - logger.Log(LogLevel.Fatal, messageFunc); - } - - public static void Fatal(this ILog logger, string message) - { - if (logger.IsFatalEnabled()) - { - logger.Log(LogLevel.Fatal, message.AsFunc()); - } - } - - public static void Fatal(this ILog logger, string message, params object[] args) - { - logger.FatalFormat(message, args); - } - - public static void Fatal(this ILog logger, Exception exception, string message, params object[] args) - { - logger.FatalException(message, exception, args); - } - - public static void FatalFormat(this ILog logger, string message, params object[] args) - { - if (logger.IsFatalEnabled()) - { - logger.LogFormat(LogLevel.Fatal, message, args); - } - } - - public static void FatalException(this ILog logger, string message, Exception exception, params object[] formatParams) - { - if (logger.IsFatalEnabled()) - { - logger.Log(LogLevel.Fatal, message.AsFunc(), exception, formatParams); - } - } - - public static void Info(this ILog logger, Func messageFunc) - { - GuardAgainstNullLogger(logger); - logger.Log(LogLevel.Info, messageFunc); - } - - public static void Info(this ILog logger, string message) - { - if (logger.IsInfoEnabled()) - { - logger.Log(LogLevel.Info, message.AsFunc()); - } - } - - public static void Info(this ILog logger, string message, params object[] args) - { - logger.InfoFormat(message, args); - } - - public static void Info(this ILog logger, Exception exception, string message, params object[] args) - { - logger.InfoException(message, exception, args); - } - - public static void InfoFormat(this ILog logger, string message, params object[] args) - { - if (logger.IsInfoEnabled()) - { - logger.LogFormat(LogLevel.Info, message, args); - } - } - - public static void InfoException(this ILog logger, string message, Exception exception, params object[] formatParams) - { - if (logger.IsInfoEnabled()) - { - logger.Log(LogLevel.Info, message.AsFunc(), exception, formatParams); - } - } - - public static void Trace(this ILog logger, Func messageFunc) - { - GuardAgainstNullLogger(logger); - logger.Log(LogLevel.Trace, messageFunc); - } - - public static void Trace(this ILog logger, string message) - { - if (logger.IsTraceEnabled()) - { - logger.Log(LogLevel.Trace, message.AsFunc()); - } - } - - public static void Trace(this ILog logger, string message, params object[] args) - { - logger.TraceFormat(message, args); - } - - public static void Trace(this ILog logger, Exception exception, string message, params object[] args) - { - logger.TraceException(message, exception, args); - } - - public static void TraceFormat(this ILog logger, string message, params object[] args) - { - if (logger.IsTraceEnabled()) - { - logger.LogFormat(LogLevel.Trace, message, args); - } - } - - public static void TraceException(this ILog logger, string message, Exception exception, params object[] formatParams) - { - if (logger.IsTraceEnabled()) - { - logger.Log(LogLevel.Trace, message.AsFunc(), exception, formatParams); - } - } - - public static void Warn(this ILog logger, Func messageFunc) - { - GuardAgainstNullLogger(logger); - logger.Log(LogLevel.Warn, messageFunc); - } - - public static void Warn(this ILog logger, string message) - { - if (logger.IsWarnEnabled()) - { - logger.Log(LogLevel.Warn, message.AsFunc()); - } - } - - public static void Warn(this ILog logger, string message, params object[] args) - { - logger.WarnFormat(message, args); - } - - public static void Warn(this ILog logger, Exception exception, string message, params object[] args) - { - logger.WarnException(message, exception, args); - } - - public static void WarnFormat(this ILog logger, string message, params object[] args) - { - if (logger.IsWarnEnabled()) - { - logger.LogFormat(LogLevel.Warn, message, args); - } - } - - public static void WarnException(this ILog logger, string message, Exception exception, params object[] formatParams) - { - if (logger.IsWarnEnabled()) - { - logger.Log(LogLevel.Warn, message.AsFunc(), exception, formatParams); - } - } - - // ReSharper disable once UnusedParameter.Local - private static void GuardAgainstNullLogger(ILog logger) - { - if (logger == null) - { - throw new ArgumentNullException("logger"); - } - } - - private static void LogFormat(this ILog logger, LogLevel logLevel, string message, params object[] args) - { - logger.Log(logLevel, message.AsFunc(), null, args); - } - - // Avoid the closure allocation, see https://gist.github.com/AArnott/d285feef75c18f6ecd2b - private static Func AsFunc(this T value) where T : class - { - return value.Return; - } - - private static T Return(this T value) - { - return value; - } - } -#endif - - /// - /// Represents a way to get a - /// -#if LIBLOG_PROVIDERS_ONLY - internal -#else - public -#endif - interface ILogProvider - { - /// - /// Gets the specified named logger. - /// - /// Name of the logger. - /// The logger reference. - Logger GetLogger(string name); - - /// - /// Opens a nested diagnostics context. Not supported in EntLib logging. - /// - /// The message to add to the diagnostics context. - /// A disposable that when disposed removes the message from the context. - IDisposable OpenNestedContext(string message); - - /// - /// Opens a mapped diagnostics context. Not supported in EntLib logging. - /// - /// A key. - /// A value. - /// A disposable that when disposed removes the map from the context. - IDisposable OpenMappedContext(string key, string value); - } - - /// - /// Provides a mechanism to create instances of objects. - /// -#if LIBLOG_PROVIDERS_ONLY - internal -#else - public -#endif - static class LogProvider - { -#if !LIBLOG_PROVIDERS_ONLY - private const string NullLogProvider = "Current Log Provider is not set. Call SetCurrentLogProvider " + - "with a non-null value first."; - private static dynamic s_currentLogProvider; - private static Action s_onCurrentLogProviderSet; - - [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline")] - static LogProvider() - { - IsDisabled = false; - } - - /// - /// Sets the current log provider. - /// - /// The log provider. - public static void SetCurrentLogProvider(ILogProvider logProvider) - { - s_currentLogProvider = logProvider; - - RaiseOnCurrentLogProviderSet(); - } - - /// - /// Gets or sets a value indicating whether this is logging is disabled. - /// - /// - /// true if logging is disabled; otherwise, false. - /// - public static bool IsDisabled { get; set; } - - /// - /// Sets an action that is invoked when a consumer of your library has called SetCurrentLogProvider. It is - /// important that hook into this if you are using child libraries (especially ilmerged ones) that are using - /// LibLog (or other logging abstraction) so you adapt and delegate to them. - /// - /// - internal static Action OnCurrentLogProviderSet - { - set - { - s_onCurrentLogProviderSet = value; - RaiseOnCurrentLogProviderSet(); - } - } - - internal static ILogProvider CurrentLogProvider - { - get - { - return s_currentLogProvider; - } - } - - /// - /// Gets a logger for the specified type. - /// - /// The type whose name will be used for the logger. - /// An instance of -#if LIBLOG_PUBLIC - public -#else - internal -#endif - static ILog For() - { - return GetLogger(typeof(T)); - } - -#if !LIBLOG_PORTABLE - /// - /// Gets a logger for the current class. - /// - /// An instance of - [MethodImpl(MethodImplOptions.NoInlining)] -#if LIBLOG_PUBLIC - public -#else - internal -#endif - static ILog GetCurrentClassLogger() - { - var stackFrame = new StackFrame(1, false); - return GetLogger(stackFrame.GetMethod().DeclaringType); - } -#endif - - /// - /// Gets a logger for the specified type. - /// - /// The type whose name will be used for the logger. - /// If the type is null then this name will be used as the log name instead - /// An instance of -#if LIBLOG_PUBLIC - public -#else - internal -#endif - static ILog GetLogger(Type type, string fallbackTypeName = "System.Object") - { - // If the type passed in is null then fallback to the type name specified - return GetLogger(type != null ? type.FullName : fallbackTypeName); - } - - /// - /// Gets a logger with the specified name. - /// - /// The name. - /// An instance of -#if LIBLOG_PUBLIC - public -#else - internal -#endif - static ILog GetLogger(string name) - { - ILogProvider logProvider = CurrentLogProvider ?? ResolveLogProvider(); - return logProvider == null - ? NoOpLogger.Instance - : (ILog)new LoggerExecutionWrapper(logProvider.GetLogger(name), () => IsDisabled); - } - - /// - /// Opens a nested diagnostics context. - /// - /// A message. - /// An that closes context when disposed. - [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "SetCurrentLogProvider")] -#if LIBLOG_PUBLIC - public -#else - internal -#endif - static IDisposable OpenNestedContext(string message) - { - ILogProvider logProvider = CurrentLogProvider ?? ResolveLogProvider(); - - return logProvider == null - ? new DisposableAction(() => { }) - : logProvider.OpenNestedContext(message); - } - - /// - /// Opens a mapped diagnostics context. - /// - /// A key. - /// A value. - /// An that closes context when disposed. - [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "SetCurrentLogProvider")] -#if LIBLOG_PUBLIC - public -#else - internal -#endif - static IDisposable OpenMappedContext(string key, string value) - { - ILogProvider logProvider = CurrentLogProvider ?? ResolveLogProvider(); - - return logProvider == null - ? new DisposableAction(() => { }) - : logProvider.OpenMappedContext(key, value); - } -#endif - -#if LIBLOG_PROVIDERS_ONLY - private -#else - internal -#endif - delegate bool IsLoggerAvailable(); - -#if LIBLOG_PROVIDERS_ONLY - private -#else - internal -#endif - delegate ILogProvider CreateLogProvider(); - -#if LIBLOG_PROVIDERS_ONLY - private -#else - internal -#endif - static readonly List> LogProviderResolvers = - new List> - { - new Tuple(SerilogLogProvider.IsLoggerAvailable, () => new SerilogLogProvider()), - new Tuple(NLogLogProvider.IsLoggerAvailable, () => new NLogLogProvider()), - new Tuple(Log4NetLogProvider.IsLoggerAvailable, () => new Log4NetLogProvider()), - new Tuple(EntLibLogProvider.IsLoggerAvailable, () => new EntLibLogProvider()), - new Tuple(LoupeLogProvider.IsLoggerAvailable, () => new LoupeLogProvider()), - }; - -#if !LIBLOG_PROVIDERS_ONLY - private static void RaiseOnCurrentLogProviderSet() - { - if (s_onCurrentLogProviderSet != null) - { - s_onCurrentLogProviderSet(s_currentLogProvider); - } - } -#endif - - [SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.Console.WriteLine(System.String,System.Object,System.Object)")] - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] - internal static ILogProvider ResolveLogProvider() - { - try - { - foreach (var providerResolver in LogProviderResolvers) - { - if (providerResolver.Item1()) - { - return providerResolver.Item2(); - } - } - } - catch (Exception ex) - { -#if LIBLOG_PORTABLE - Debug.WriteLine( -#else - Console.WriteLine( -#endif - "Exception occurred resolving a log provider. Logging for this assembly {0} is disabled. {1}", - typeof(LogProvider).GetAssemblyPortable().FullName, - ex); - } - return null; - } - -#if !LIBLOG_PROVIDERS_ONLY - internal class NoOpLogger : ILog - { - internal static readonly NoOpLogger Instance = new NoOpLogger(); - - public bool Log(LogLevel logLevel, Func messageFunc, Exception exception, params object[] formatParameters) - { - return false; - } - } -#endif - } - -#if !LIBLOG_PROVIDERS_ONLY - internal class LoggerExecutionWrapper : ILog - { - private readonly Logger _logger; - private readonly Func _getIsDisabled; - internal const string FailedToGenerateLogMessage = "Failed to generate log message"; - - internal LoggerExecutionWrapper(Logger logger, Func getIsDisabled = null) - { - _logger = logger; - _getIsDisabled = getIsDisabled ?? (() => false); - } - - internal Logger WrappedLogger - { - get { return _logger; } - } - - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] - public bool Log(LogLevel logLevel, Func messageFunc, Exception exception = null, params object[] formatParameters) - { - if (_getIsDisabled()) - { - return false; - } - if (messageFunc == null) - { - return _logger(logLevel, null); - } - - Func wrappedMessageFunc = () => - { - try - { - return messageFunc(); - } - catch (Exception ex) - { - Log(LogLevel.Error, () => FailedToGenerateLogMessage, ex); - } - return null; - }; - return _logger(logLevel, wrappedMessageFunc, exception, formatParameters); - } - } -#endif -} - -#if LIBLOG_PROVIDERS_ONLY -namespace ZoomNet.Logging.LibLog.LogProviders -#else -namespace ZoomNet.Logging.LogProviders -#endif -{ - using System; - using System.Collections.Generic; - using System.Diagnostics.CodeAnalysis; -#if !LIBLOG_PORTABLE - using System.Diagnostics; -#endif - using System.Globalization; - using System.Linq; - using System.Linq.Expressions; - using System.Reflection; -#if !LIBLOG_PORTABLE - using System.Text; -#endif - using System.Text.RegularExpressions; - - internal abstract class LogProviderBase : ILogProvider - { - protected delegate IDisposable OpenNdc(string message); - protected delegate IDisposable OpenMdc(string key, string value); - - private readonly Lazy _lazyOpenNdcMethod; - private readonly Lazy _lazyOpenMdcMethod; - private static readonly IDisposable NoopDisposableInstance = new DisposableAction(); - - protected LogProviderBase() - { - _lazyOpenNdcMethod - = new Lazy(GetOpenNdcMethod); - _lazyOpenMdcMethod - = new Lazy(GetOpenMdcMethod); - } - - public abstract Logger GetLogger(string name); - - public IDisposable OpenNestedContext(string message) - { - return _lazyOpenNdcMethod.Value(message); - } - - public IDisposable OpenMappedContext(string key, string value) - { - return _lazyOpenMdcMethod.Value(key, value); - } - - protected virtual OpenNdc GetOpenNdcMethod() - { - return _ => NoopDisposableInstance; - } - - protected virtual OpenMdc GetOpenMdcMethod() - { - return (_, __) => NoopDisposableInstance; - } - } - - internal class NLogLogProvider : LogProviderBase - { - private readonly Func _getLoggerByNameDelegate; - private static bool s_providerIsAvailableOverride = true; - - [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "LogManager")] - [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "NLog")] - public NLogLogProvider() - { - if (!IsLoggerAvailable()) - { - throw new InvalidOperationException("NLog.LogManager not found"); - } - _getLoggerByNameDelegate = GetGetLoggerMethodCall(); - } - - public static bool ProviderIsAvailableOverride - { - get { return s_providerIsAvailableOverride; } - set { s_providerIsAvailableOverride = value; } - } - - public override Logger GetLogger(string name) - { - return new NLogLogger(_getLoggerByNameDelegate(name)).Log; - } - - public static bool IsLoggerAvailable() - { - return ProviderIsAvailableOverride && GetLogManagerType() != null; - } - - protected override OpenNdc GetOpenNdcMethod() - { - Type ndcContextType = Type.GetType("NLog.NestedDiagnosticsContext, NLog"); - MethodInfo pushMethod = ndcContextType.GetMethodPortable("Push", typeof(string)); - ParameterExpression messageParam = Expression.Parameter(typeof(string), "message"); - MethodCallExpression pushMethodCall = Expression.Call(null, pushMethod, messageParam); - return Expression.Lambda(pushMethodCall, messageParam).Compile(); - } - - protected override OpenMdc GetOpenMdcMethod() - { - Type mdcContextType = Type.GetType("NLog.MappedDiagnosticsContext, NLog"); - - MethodInfo setMethod = mdcContextType.GetMethodPortable("Set", typeof(string), typeof(string)); - MethodInfo removeMethod = mdcContextType.GetMethodPortable("Remove", typeof(string)); - ParameterExpression keyParam = Expression.Parameter(typeof(string), "key"); - ParameterExpression valueParam = Expression.Parameter(typeof(string), "value"); - - MethodCallExpression setMethodCall = Expression.Call(null, setMethod, keyParam, valueParam); - MethodCallExpression removeMethodCall = Expression.Call(null, removeMethod, keyParam); - - Action set = Expression - .Lambda>(setMethodCall, keyParam, valueParam) - .Compile(); - Action remove = Expression - .Lambda>(removeMethodCall, keyParam) - .Compile(); - - return (key, value) => - { - set(key, value); - return new DisposableAction(() => remove(key)); - }; - } - - private static Type GetLogManagerType() - { - return Type.GetType("NLog.LogManager, NLog"); - } - - private static Func GetGetLoggerMethodCall() - { - Type logManagerType = GetLogManagerType(); - MethodInfo method = logManagerType.GetMethodPortable("GetLogger", typeof(string)); - ParameterExpression nameParam = Expression.Parameter(typeof(string), "name"); - MethodCallExpression methodCall = Expression.Call(null, method, nameParam); - return Expression.Lambda>(methodCall, nameParam).Compile(); - } - - internal class NLogLogger - { - private readonly dynamic _logger; - - private static Func _logEventInfoFact; - - private static readonly object _levelTrace; - private static readonly object _levelDebug; - private static readonly object _levelInfo; - private static readonly object _levelWarn; - private static readonly object _levelError; - private static readonly object _levelFatal; - - static NLogLogger() - { - try - { - var logEventLevelType = Type.GetType("NLog.LogLevel, NLog"); - if (logEventLevelType == null) - { - throw new InvalidOperationException("Type NLog.LogLevel was not found."); - } - - var levelFields = logEventLevelType.GetFieldsPortable().ToList(); - _levelTrace = levelFields.First(x => x.Name == "Trace").GetValue(null); - _levelDebug = levelFields.First(x => x.Name == "Debug").GetValue(null); - _levelInfo = levelFields.First(x => x.Name == "Info").GetValue(null); - _levelWarn = levelFields.First(x => x.Name == "Warn").GetValue(null); - _levelError = levelFields.First(x => x.Name == "Error").GetValue(null); - _levelFatal = levelFields.First(x => x.Name == "Fatal").GetValue(null); - - var logEventInfoType = Type.GetType("NLog.LogEventInfo, NLog"); - if (logEventInfoType == null) - { - throw new InvalidOperationException("Type NLog.LogEventInfo was not found."); - } - MethodInfo createLogEventInfoMethodInfo = logEventInfoType.GetMethodPortable("Create", - logEventLevelType, typeof(string), typeof(Exception), typeof(IFormatProvider), typeof(string), typeof(object[])); - ParameterExpression loggerNameParam = Expression.Parameter(typeof(string)); - ParameterExpression levelParam = Expression.Parameter(typeof(object)); - ParameterExpression messageParam = Expression.Parameter(typeof(string)); - ParameterExpression exceptionParam = Expression.Parameter(typeof(Exception)); - UnaryExpression levelCast = Expression.Convert(levelParam, logEventLevelType); - MethodCallExpression createLogEventInfoMethodCall = Expression.Call(null, - createLogEventInfoMethodInfo, - levelCast, loggerNameParam, exceptionParam, - Expression.Constant(null, typeof(IFormatProvider)), messageParam, Expression.Constant(null, typeof(object[]))); - _logEventInfoFact = Expression.Lambda>(createLogEventInfoMethodCall, - loggerNameParam, levelParam, messageParam, exceptionParam).Compile(); - } - catch { } - } - - internal NLogLogger(dynamic logger) - { - _logger = logger; - } - - [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")] - public bool Log(LogLevel logLevel, Func messageFunc, Exception exception, params object[] formatParameters) - { - if (messageFunc == null) - { - return IsLogLevelEnable(logLevel); - } - messageFunc = LogMessageFormatter.SimulateStructuredLogging(messageFunc, formatParameters); - - if (_logEventInfoFact != null) - { - if (IsLogLevelEnable(logLevel)) - { - var nlogLevel = this.TranslateLevel(logLevel); - Type s_callerStackBoundaryType; -#if !LIBLOG_PORTABLE - StackTrace stack = new StackTrace(); - Type thisType = GetType(); - Type knownType0 = typeof(LoggerExecutionWrapper); - Type knownType1 = typeof(LogExtensions); - //Maybe inline, so we may can't found any LibLog classes in stack - s_callerStackBoundaryType = null; - for (var i = 0; i < stack.FrameCount; i++) - { - var declaringType = stack.GetFrame(i).GetMethod().DeclaringType; - if (!IsInTypeHierarchy(thisType, declaringType) && - !IsInTypeHierarchy(knownType0, declaringType) && - !IsInTypeHierarchy(knownType1, declaringType)) - { - if (i > 1) - s_callerStackBoundaryType = stack.GetFrame(i - 1).GetMethod().DeclaringType; - break; - } - } -#else - s_callerStackBoundaryType = null; -#endif - if (s_callerStackBoundaryType != null) - _logger.Log(s_callerStackBoundaryType, _logEventInfoFact(_logger.Name, nlogLevel, messageFunc(), exception)); - else - _logger.Log(_logEventInfoFact(_logger.Name, nlogLevel, messageFunc(), exception)); - return true; - } - return false; - } - - if (exception != null) - { - return LogException(logLevel, messageFunc, exception); - } - switch (logLevel) - { - case LogLevel.Debug: - if (_logger.IsDebugEnabled) - { - _logger.Debug(messageFunc()); - return true; - } - break; - case LogLevel.Info: - if (_logger.IsInfoEnabled) - { - _logger.Info(messageFunc()); - return true; - } - break; - case LogLevel.Warn: - if (_logger.IsWarnEnabled) - { - _logger.Warn(messageFunc()); - return true; - } - break; - case LogLevel.Error: - if (_logger.IsErrorEnabled) - { - _logger.Error(messageFunc()); - return true; - } - break; - case LogLevel.Fatal: - if (_logger.IsFatalEnabled) - { - _logger.Fatal(messageFunc()); - return true; - } - break; - default: - if (_logger.IsTraceEnabled) - { - _logger.Trace(messageFunc()); - return true; - } - break; - } - return false; - } - - private static bool IsInTypeHierarchy(Type currentType, Type checkType) - { - while (currentType != null && currentType != typeof(object)) - { - if (currentType == checkType) - { - return true; - } - currentType = currentType.GetBaseTypePortable(); - } - return false; - } - - [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")] - private bool LogException(LogLevel logLevel, Func messageFunc, Exception exception) - { - switch (logLevel) - { - case LogLevel.Debug: - if (_logger.IsDebugEnabled) - { - _logger.DebugException(messageFunc(), exception); - return true; - } - break; - case LogLevel.Info: - if (_logger.IsInfoEnabled) - { - _logger.InfoException(messageFunc(), exception); - return true; - } - break; - case LogLevel.Warn: - if (_logger.IsWarnEnabled) - { - _logger.WarnException(messageFunc(), exception); - return true; - } - break; - case LogLevel.Error: - if (_logger.IsErrorEnabled) - { - _logger.ErrorException(messageFunc(), exception); - return true; - } - break; - case LogLevel.Fatal: - if (_logger.IsFatalEnabled) - { - _logger.FatalException(messageFunc(), exception); - return true; - } - break; - default: - if (_logger.IsTraceEnabled) - { - _logger.TraceException(messageFunc(), exception); - return true; - } - break; - } - return false; - } - - private bool IsLogLevelEnable(LogLevel logLevel) - { - switch (logLevel) - { - case LogLevel.Debug: - return _logger.IsDebugEnabled; - case LogLevel.Info: - return _logger.IsInfoEnabled; - case LogLevel.Warn: - return _logger.IsWarnEnabled; - case LogLevel.Error: - return _logger.IsErrorEnabled; - case LogLevel.Fatal: - return _logger.IsFatalEnabled; - default: - return _logger.IsTraceEnabled; - } - } - - private object TranslateLevel(LogLevel logLevel) - { - switch (logLevel) - { - case LogLevel.Trace: - return _levelTrace; - case LogLevel.Debug: - return _levelDebug; - case LogLevel.Info: - return _levelInfo; - case LogLevel.Warn: - return _levelWarn; - case LogLevel.Error: - return _levelError; - case LogLevel.Fatal: - return _levelFatal; - default: - throw new ArgumentOutOfRangeException("logLevel", logLevel, null); - } - } - } - } - - internal class Log4NetLogProvider : LogProviderBase - { - private readonly Func _getLoggerByNameDelegate; - private static bool s_providerIsAvailableOverride = true; - - [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "LogManager")] - public Log4NetLogProvider() - { - if (!IsLoggerAvailable()) - { - throw new InvalidOperationException("log4net.LogManager not found"); - } - _getLoggerByNameDelegate = GetGetLoggerMethodCall(); - } - - public static bool ProviderIsAvailableOverride - { - get { return s_providerIsAvailableOverride; } - set { s_providerIsAvailableOverride = value; } - } - - public override Logger GetLogger(string name) - { - return new Log4NetLogger(_getLoggerByNameDelegate(name)).Log; - } - - internal static bool IsLoggerAvailable() - { - return ProviderIsAvailableOverride && GetLogManagerType() != null; - } - - protected override OpenNdc GetOpenNdcMethod() - { - Type logicalThreadContextType = Type.GetType("log4net.LogicalThreadContext, log4net"); - PropertyInfo stacksProperty = logicalThreadContextType.GetPropertyPortable("Stacks"); - Type logicalThreadContextStacksType = stacksProperty.PropertyType; - PropertyInfo stacksIndexerProperty = logicalThreadContextStacksType.GetPropertyPortable("Item"); - Type stackType = stacksIndexerProperty.PropertyType; - MethodInfo pushMethod = stackType.GetMethodPortable("Push"); - - ParameterExpression messageParameter = - Expression.Parameter(typeof(string), "message"); - - // message => LogicalThreadContext.Stacks.Item["NDC"].Push(message); - MethodCallExpression callPushBody = - Expression.Call( - Expression.Property(Expression.Property(null, stacksProperty), - stacksIndexerProperty, - Expression.Constant("NDC")), - pushMethod, - messageParameter); - - OpenNdc result = - Expression.Lambda(callPushBody, messageParameter) - .Compile(); - - return result; - } - - protected override OpenMdc GetOpenMdcMethod() - { - Type logicalThreadContextType = Type.GetType("log4net.LogicalThreadContext, log4net"); - PropertyInfo propertiesProperty = logicalThreadContextType.GetPropertyPortable("Properties"); - Type logicalThreadContextPropertiesType = propertiesProperty.PropertyType; - PropertyInfo propertiesIndexerProperty = logicalThreadContextPropertiesType.GetPropertyPortable("Item"); - - MethodInfo removeMethod = logicalThreadContextPropertiesType.GetMethodPortable("Remove"); - - ParameterExpression keyParam = Expression.Parameter(typeof(string), "key"); - ParameterExpression valueParam = Expression.Parameter(typeof(string), "value"); - - MemberExpression propertiesExpression = Expression.Property(null, propertiesProperty); - - // (key, value) => LogicalThreadContext.Properties.Item[key] = value; - BinaryExpression setProperties = Expression.Assign(Expression.Property(propertiesExpression, propertiesIndexerProperty, keyParam), valueParam); - - // key => LogicalThreadContext.Properties.Remove(key); - MethodCallExpression removeMethodCall = Expression.Call(propertiesExpression, removeMethod, keyParam); - - Action set = Expression - .Lambda>(setProperties, keyParam, valueParam) - .Compile(); - - Action remove = Expression - .Lambda>(removeMethodCall, keyParam) - .Compile(); - - return (key, value) => - { - set(key, value); - return new DisposableAction(() => remove(key)); - }; - } - - private static Type GetLogManagerType() - { - return Type.GetType("log4net.LogManager, log4net"); - } - - private static Func GetGetLoggerMethodCall() - { - Type logManagerType = GetLogManagerType(); - MethodInfo method = logManagerType.GetMethodPortable("GetLogger", typeof(string)); - ParameterExpression nameParam = Expression.Parameter(typeof(string), "name"); - MethodCallExpression methodCall = Expression.Call(null, method, nameParam); - return Expression.Lambda>(methodCall, nameParam).Compile(); - } - - internal class Log4NetLogger - { - private readonly dynamic _logger; - private static Type s_callerStackBoundaryType; - private static readonly object CallerStackBoundaryTypeSync = new object(); - - private readonly object _levelDebug; - private readonly object _levelInfo; - private readonly object _levelWarn; - private readonly object _levelError; - private readonly object _levelFatal; - private readonly Func _isEnabledForDelegate; - private readonly Action _logDelegate; - private readonly Func _createLoggingEvent; - private Action _loggingEventPropertySetter; - - [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "ILogger")] - internal Log4NetLogger(dynamic logger) - { - _logger = logger.Logger; - - var logEventLevelType = Type.GetType("log4net.Core.Level, log4net"); - if (logEventLevelType == null) - { - throw new InvalidOperationException("Type log4net.Core.Level was not found."); - } - - var levelFields = logEventLevelType.GetFieldsPortable().ToList(); - _levelDebug = levelFields.First(x => x.Name == "Debug").GetValue(null); - _levelInfo = levelFields.First(x => x.Name == "Info").GetValue(null); - _levelWarn = levelFields.First(x => x.Name == "Warn").GetValue(null); - _levelError = levelFields.First(x => x.Name == "Error").GetValue(null); - _levelFatal = levelFields.First(x => x.Name == "Fatal").GetValue(null); - - // Func isEnabledFor = (logger, level) => { return ((log4net.Core.ILogger)logger).IsEnabled(level); } - var loggerType = Type.GetType("log4net.Core.ILogger, log4net"); - if (loggerType == null) - { - throw new InvalidOperationException("Type log4net.Core.ILogger, was not found."); - } - ParameterExpression instanceParam = Expression.Parameter(typeof(object)); - UnaryExpression instanceCast = Expression.Convert(instanceParam, loggerType); - ParameterExpression levelParam = Expression.Parameter(typeof(object)); - UnaryExpression levelCast = Expression.Convert(levelParam, logEventLevelType); - _isEnabledForDelegate = GetIsEnabledFor(loggerType, logEventLevelType, instanceCast, levelCast, instanceParam, levelParam); - - Type loggingEventType = Type.GetType("log4net.Core.LoggingEvent, log4net"); - - _createLoggingEvent = GetCreateLoggingEvent(instanceParam, instanceCast, levelParam, levelCast, loggingEventType); - - _logDelegate = GetLogDelegate(loggerType, loggingEventType, instanceCast, instanceParam); - - _loggingEventPropertySetter = GetLoggingEventPropertySetter(loggingEventType); - } - - private static Action GetLogDelegate(Type loggerType, Type loggingEventType, UnaryExpression instanceCast, - ParameterExpression instanceParam) - { - //Action Log = - //(logger, callerStackBoundaryDeclaringType, level, message, exception) => { ((ILogger)logger).Log(new LoggingEvent(callerStackBoundaryDeclaringType, logger.Repository, logger.Name, level, message, exception)); } - MethodInfo writeExceptionMethodInfo = loggerType.GetMethodPortable("Log", - loggingEventType); - - ParameterExpression loggingEventParameter = - Expression.Parameter(typeof(object), "loggingEvent"); - - UnaryExpression loggingEventCasted = - Expression.Convert(loggingEventParameter, loggingEventType); - - var writeMethodExp = Expression.Call( - instanceCast, - writeExceptionMethodInfo, - loggingEventCasted); - - var logDelegate = Expression.Lambda>( - writeMethodExp, - instanceParam, - loggingEventParameter).Compile(); - - return logDelegate; - } - - private static Func GetCreateLoggingEvent(ParameterExpression instanceParam, UnaryExpression instanceCast, ParameterExpression levelParam, UnaryExpression levelCast, Type loggingEventType) - { - ParameterExpression callerStackBoundaryDeclaringTypeParam = Expression.Parameter(typeof(Type)); - ParameterExpression messageParam = Expression.Parameter(typeof(string)); - ParameterExpression exceptionParam = Expression.Parameter(typeof(Exception)); - - PropertyInfo repositoryProperty = loggingEventType.GetPropertyPortable("Repository"); - PropertyInfo levelProperty = loggingEventType.GetPropertyPortable("Level"); - - ConstructorInfo loggingEventConstructor = - loggingEventType.GetConstructorPortable(typeof(Type), repositoryProperty.PropertyType, typeof(string), levelProperty.PropertyType, typeof(object), typeof(Exception)); - - //Func Log = - //(logger, callerStackBoundaryDeclaringType, level, message, exception) => new LoggingEvent(callerStackBoundaryDeclaringType, ((ILogger)logger).Repository, ((ILogger)logger).Name, (Level)level, message, exception); } - NewExpression newLoggingEventExpression = - Expression.New(loggingEventConstructor, - callerStackBoundaryDeclaringTypeParam, - Expression.Property(instanceCast, "Repository"), - Expression.Property(instanceCast, "Name"), - levelCast, - messageParam, - exceptionParam); - - var createLoggingEvent = - Expression.Lambda>( - newLoggingEventExpression, - instanceParam, - callerStackBoundaryDeclaringTypeParam, - levelParam, - messageParam, - exceptionParam) - .Compile(); - - return createLoggingEvent; - } - - private static Func GetIsEnabledFor(Type loggerType, Type logEventLevelType, - UnaryExpression instanceCast, - UnaryExpression levelCast, - ParameterExpression instanceParam, - ParameterExpression levelParam) - { - MethodInfo isEnabledMethodInfo = loggerType.GetMethodPortable("IsEnabledFor", logEventLevelType); - MethodCallExpression isEnabledMethodCall = Expression.Call(instanceCast, isEnabledMethodInfo, levelCast); - - Func result = - Expression.Lambda>(isEnabledMethodCall, instanceParam, levelParam) - .Compile(); - - return result; - } - - private static Action GetLoggingEventPropertySetter(Type loggingEventType) - { - ParameterExpression loggingEventParameter = Expression.Parameter(typeof(object), "loggingEvent"); - ParameterExpression keyParameter = Expression.Parameter(typeof(string), "key"); - ParameterExpression valueParameter = Expression.Parameter(typeof(object), "value"); - - PropertyInfo propertiesProperty = loggingEventType.GetPropertyPortable("Properties"); - PropertyInfo item = propertiesProperty.PropertyType.GetPropertyPortable("Item"); - - // ((LoggingEvent)loggingEvent).Properties[key] = value; - var body = - Expression.Assign( - Expression.Property( - Expression.Property(Expression.Convert(loggingEventParameter, loggingEventType), - propertiesProperty), item, keyParameter), valueParameter); - - Action result = - Expression.Lambda> - (body, loggingEventParameter, keyParameter, - valueParameter) - .Compile(); - - return result; - } - - public bool Log(LogLevel logLevel, Func messageFunc, Exception exception, params object[] formatParameters) - { - if (messageFunc == null) - { - return IsLogLevelEnable(logLevel); - } - - if (!IsLogLevelEnable(logLevel)) - { - return false; - } - - string message = messageFunc(); - - IEnumerable patternMatches; - - string formattedMessage = - LogMessageFormatter.FormatStructuredMessage(message, - formatParameters, - out patternMatches); - - // determine correct caller - this might change due to jit optimizations with method inlining - if (s_callerStackBoundaryType == null) - { - lock (CallerStackBoundaryTypeSync) - { -#if !LIBLOG_PORTABLE - StackTrace stack = new StackTrace(); - Type thisType = GetType(); - s_callerStackBoundaryType = Type.GetType("LoggerExecutionWrapper"); - for (var i = 1; i < stack.FrameCount; i++) - { - if (!IsInTypeHierarchy(thisType, stack.GetFrame(i).GetMethod().DeclaringType)) - { - s_callerStackBoundaryType = stack.GetFrame(i - 1).GetMethod().DeclaringType; - break; - } - } -#else - s_callerStackBoundaryType = typeof(LoggerExecutionWrapper); -#endif - } - } - - var translatedLevel = TranslateLevel(logLevel); - - object loggingEvent = _createLoggingEvent(_logger, s_callerStackBoundaryType, translatedLevel, formattedMessage, exception); - - PopulateProperties(loggingEvent, patternMatches, formatParameters); - - _logDelegate(_logger, loggingEvent); - - return true; - } - - private void PopulateProperties(object loggingEvent, IEnumerable patternMatches, object[] formatParameters) - { - IEnumerable> keyToValue = - patternMatches.Zip(formatParameters, - (key, value) => new KeyValuePair(key, value)); - - foreach (KeyValuePair keyValuePair in keyToValue) - { - _loggingEventPropertySetter(loggingEvent, keyValuePair.Key, keyValuePair.Value); - } - } - - private static bool IsInTypeHierarchy(Type currentType, Type checkType) - { - while (currentType != null && currentType != typeof(object)) - { - if (currentType == checkType) - { - return true; - } - currentType = currentType.GetBaseTypePortable(); - } - return false; - } - - private bool IsLogLevelEnable(LogLevel logLevel) - { - var level = TranslateLevel(logLevel); - return _isEnabledForDelegate(_logger, level); - } - - private object TranslateLevel(LogLevel logLevel) - { - switch (logLevel) - { - case LogLevel.Trace: - case LogLevel.Debug: - return _levelDebug; - case LogLevel.Info: - return _levelInfo; - case LogLevel.Warn: - return _levelWarn; - case LogLevel.Error: - return _levelError; - case LogLevel.Fatal: - return _levelFatal; - default: - throw new ArgumentOutOfRangeException("logLevel", logLevel, null); - } - } - } - } - - internal class EntLibLogProvider : LogProviderBase - { - private const string TypeTemplate = "Microsoft.Practices.EnterpriseLibrary.Logging.{0}, Microsoft.Practices.EnterpriseLibrary.Logging"; - private static bool s_providerIsAvailableOverride = true; - private static readonly Type LogEntryType; - private static readonly Type LoggerType; - private static readonly Type TraceEventTypeType; - private static readonly Action WriteLogEntry; - private static readonly Func ShouldLogEntry; - - [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline")] - static EntLibLogProvider() - { - LogEntryType = Type.GetType(string.Format(CultureInfo.InvariantCulture, TypeTemplate, "LogEntry")); - LoggerType = Type.GetType(string.Format(CultureInfo.InvariantCulture, TypeTemplate, "Logger")); - TraceEventTypeType = TraceEventTypeValues.Type; - if (LogEntryType == null - || TraceEventTypeType == null - || LoggerType == null) - { - return; - } - WriteLogEntry = GetWriteLogEntry(); - ShouldLogEntry = GetShouldLogEntry(); - } - - [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "EnterpriseLibrary")] - public EntLibLogProvider() - { - if (!IsLoggerAvailable()) - { - throw new InvalidOperationException("Microsoft.Practices.EnterpriseLibrary.Logging.Logger not found"); - } - } - - public static bool ProviderIsAvailableOverride - { - get { return s_providerIsAvailableOverride; } - set { s_providerIsAvailableOverride = value; } - } - - public override Logger GetLogger(string name) - { - return new EntLibLogger(name, WriteLogEntry, ShouldLogEntry).Log; - } - - internal static bool IsLoggerAvailable() - { - return ProviderIsAvailableOverride - && TraceEventTypeType != null - && LogEntryType != null; - } - - private static Action GetWriteLogEntry() - { - // new LogEntry(...) - var logNameParameter = Expression.Parameter(typeof(string), "logName"); - var messageParameter = Expression.Parameter(typeof(string), "message"); - var severityParameter = Expression.Parameter(typeof(int), "severity"); - - MemberInitExpression memberInit = GetWriteLogExpression( - messageParameter, - Expression.Convert(severityParameter, TraceEventTypeType), - logNameParameter); - - //Logger.Write(new LogEntry(....)); - MethodInfo writeLogEntryMethod = LoggerType.GetMethodPortable("Write", LogEntryType); - var writeLogEntryExpression = Expression.Call(writeLogEntryMethod, memberInit); - - return Expression.Lambda>( - writeLogEntryExpression, - logNameParameter, - messageParameter, - severityParameter).Compile(); - } - - private static Func GetShouldLogEntry() - { - // new LogEntry(...) - var logNameParameter = Expression.Parameter(typeof(string), "logName"); - var severityParameter = Expression.Parameter(typeof(int), "severity"); - - MemberInitExpression memberInit = GetWriteLogExpression( - Expression.Constant("***dummy***"), - Expression.Convert(severityParameter, TraceEventTypeType), - logNameParameter); - - //Logger.Write(new LogEntry(....)); - MethodInfo writeLogEntryMethod = LoggerType.GetMethodPortable("ShouldLog", LogEntryType); - var writeLogEntryExpression = Expression.Call(writeLogEntryMethod, memberInit); - - return Expression.Lambda>( - writeLogEntryExpression, - logNameParameter, - severityParameter).Compile(); - } - - private static MemberInitExpression GetWriteLogExpression(Expression message, - Expression severityParameter, ParameterExpression logNameParameter) - { - var entryType = LogEntryType; - MemberInitExpression memberInit = Expression.MemberInit(Expression.New(entryType), - Expression.Bind(entryType.GetPropertyPortable("Message"), message), - Expression.Bind(entryType.GetPropertyPortable("Severity"), severityParameter), - Expression.Bind( - entryType.GetPropertyPortable("TimeStamp"), - Expression.Property(null, typeof(DateTime).GetPropertyPortable("UtcNow"))), - Expression.Bind( - entryType.GetPropertyPortable("Categories"), - Expression.ListInit( - Expression.New(typeof(List)), - typeof(List).GetMethodPortable("Add", typeof(string)), - logNameParameter))); - return memberInit; - } - - internal class EntLibLogger - { - private readonly string _loggerName; - private readonly Action _writeLog; - private readonly Func _shouldLog; - - internal EntLibLogger(string loggerName, Action writeLog, Func shouldLog) - { - _loggerName = loggerName; - _writeLog = writeLog; - _shouldLog = shouldLog; - } - - public bool Log(LogLevel logLevel, Func messageFunc, Exception exception, params object[] formatParameters) - { - var severity = MapSeverity(logLevel); - if (messageFunc == null) - { - return _shouldLog(_loggerName, severity); - } - - - messageFunc = LogMessageFormatter.SimulateStructuredLogging(messageFunc, formatParameters); - if (exception != null) - { - return LogException(logLevel, messageFunc, exception); - } - _writeLog(_loggerName, messageFunc(), severity); - return true; - } - - public bool LogException(LogLevel logLevel, Func messageFunc, Exception exception) - { - var severity = MapSeverity(logLevel); - var message = messageFunc() + Environment.NewLine + exception; - _writeLog(_loggerName, message, severity); - return true; - } - - private static int MapSeverity(LogLevel logLevel) - { - switch (logLevel) - { - case LogLevel.Fatal: - return TraceEventTypeValues.Critical; - case LogLevel.Error: - return TraceEventTypeValues.Error; - case LogLevel.Warn: - return TraceEventTypeValues.Warning; - case LogLevel.Info: - return TraceEventTypeValues.Information; - default: - return TraceEventTypeValues.Verbose; - } - } - } - } - - internal class SerilogLogProvider : LogProviderBase - { - private readonly Func _getLoggerByNameDelegate; - private static bool s_providerIsAvailableOverride = true; - - [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "Serilog")] - public SerilogLogProvider() - { - if (!IsLoggerAvailable()) - { - throw new InvalidOperationException("Serilog.Log not found"); - } - _getLoggerByNameDelegate = GetForContextMethodCall(); - } - - public static bool ProviderIsAvailableOverride - { - get { return s_providerIsAvailableOverride; } - set { s_providerIsAvailableOverride = value; } - } - - public override Logger GetLogger(string name) - { - return new SerilogLogger(_getLoggerByNameDelegate(name)).Log; - } - - internal static bool IsLoggerAvailable() - { - return ProviderIsAvailableOverride && GetLogManagerType() != null; - } - - protected override OpenNdc GetOpenNdcMethod() - { - return message => GetPushProperty()("NDC", message); - } - - protected override OpenMdc GetOpenMdcMethod() - { - return (key, value) => GetPushProperty()(key, value); - } - - private static Func GetPushProperty() - { - Type ndcContextType = Type.GetType("Serilog.Context.LogContext, Serilog") ?? - Type.GetType("Serilog.Context.LogContext, Serilog.FullNetFx"); - - MethodInfo pushPropertyMethod = ndcContextType.GetMethodPortable( - "PushProperty", - typeof(string), - typeof(object), - typeof(bool)); - - ParameterExpression nameParam = Expression.Parameter(typeof(string), "name"); - ParameterExpression valueParam = Expression.Parameter(typeof(object), "value"); - ParameterExpression destructureObjectParam = Expression.Parameter(typeof(bool), "destructureObjects"); - MethodCallExpression pushPropertyMethodCall = Expression - .Call(null, pushPropertyMethod, nameParam, valueParam, destructureObjectParam); - var pushProperty = Expression - .Lambda>( - pushPropertyMethodCall, - nameParam, - valueParam, - destructureObjectParam) - .Compile(); - - return (key, value) => pushProperty(key, value, false); - } - - private static Type GetLogManagerType() - { - return Type.GetType("Serilog.Log, Serilog"); - } - - private static Func GetForContextMethodCall() - { - Type logManagerType = GetLogManagerType(); - MethodInfo method = logManagerType.GetMethodPortable("ForContext", typeof(string), typeof(object), typeof(bool)); - ParameterExpression propertyNameParam = Expression.Parameter(typeof(string), "propertyName"); - ParameterExpression valueParam = Expression.Parameter(typeof(object), "value"); - ParameterExpression destructureObjectsParam = Expression.Parameter(typeof(bool), "destructureObjects"); - MethodCallExpression methodCall = Expression.Call(null, method, new Expression[] - { - propertyNameParam, - valueParam, - destructureObjectsParam - }); - var func = Expression.Lambda>( - methodCall, - propertyNameParam, - valueParam, - destructureObjectsParam) - .Compile(); - return name => func("SourceContext", name, false); - } - - internal class SerilogLogger - { - private readonly object _logger; - private static readonly object DebugLevel; - private static readonly object ErrorLevel; - private static readonly object FatalLevel; - private static readonly object InformationLevel; - private static readonly object VerboseLevel; - private static readonly object WarningLevel; - private static readonly Func IsEnabled; - private static readonly Action Write; - private static readonly Action WriteException; - - [SuppressMessage("Microsoft.Design", "CA1065:DoNotRaiseExceptionsInUnexpectedLocations")] - [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline")] - [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "ILogger")] - [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "LogEventLevel")] - [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "Serilog")] - static SerilogLogger() - { - var logEventLevelType = Type.GetType("Serilog.Events.LogEventLevel, Serilog"); - if (logEventLevelType == null) - { - throw new InvalidOperationException("Type Serilog.Events.LogEventLevel was not found."); - } - DebugLevel = Enum.Parse(logEventLevelType, "Debug", false); - ErrorLevel = Enum.Parse(logEventLevelType, "Error", false); - FatalLevel = Enum.Parse(logEventLevelType, "Fatal", false); - InformationLevel = Enum.Parse(logEventLevelType, "Information", false); - VerboseLevel = Enum.Parse(logEventLevelType, "Verbose", false); - WarningLevel = Enum.Parse(logEventLevelType, "Warning", false); - - // Func isEnabled = (logger, level) => { return ((SeriLog.ILogger)logger).IsEnabled(level); } - var loggerType = Type.GetType("Serilog.ILogger, Serilog"); - if (loggerType == null) - { - throw new InvalidOperationException("Type Serilog.ILogger was not found."); - } - MethodInfo isEnabledMethodInfo = loggerType.GetMethodPortable("IsEnabled", logEventLevelType); - ParameterExpression instanceParam = Expression.Parameter(typeof(object)); - UnaryExpression instanceCast = Expression.Convert(instanceParam, loggerType); - ParameterExpression levelParam = Expression.Parameter(typeof(object)); - UnaryExpression levelCast = Expression.Convert(levelParam, logEventLevelType); - MethodCallExpression isEnabledMethodCall = Expression.Call(instanceCast, isEnabledMethodInfo, levelCast); - IsEnabled = Expression.Lambda>(isEnabledMethodCall, instanceParam, levelParam).Compile(); - - // Action Write = - // (logger, level, message, params) => { ((SeriLog.ILoggerILogger)logger).Write(level, message, params); } - MethodInfo writeMethodInfo = loggerType.GetMethodPortable("Write", logEventLevelType, typeof(string), typeof(object[])); - ParameterExpression messageParam = Expression.Parameter(typeof(string)); - ParameterExpression propertyValuesParam = Expression.Parameter(typeof(object[])); - MethodCallExpression writeMethodExp = Expression.Call( - instanceCast, - writeMethodInfo, - levelCast, - messageParam, - propertyValuesParam); - var expression = Expression.Lambda>( - writeMethodExp, - instanceParam, - levelParam, - messageParam, - propertyValuesParam); - Write = expression.Compile(); - - // Action WriteException = - // (logger, level, exception, message) => { ((ILogger)logger).Write(level, exception, message, new object[]); } - MethodInfo writeExceptionMethodInfo = loggerType.GetMethodPortable("Write", - logEventLevelType, - typeof(Exception), - typeof(string), - typeof(object[])); - ParameterExpression exceptionParam = Expression.Parameter(typeof(Exception)); - writeMethodExp = Expression.Call( - instanceCast, - writeExceptionMethodInfo, - levelCast, - exceptionParam, - messageParam, - propertyValuesParam); - WriteException = Expression.Lambda>( - writeMethodExp, - instanceParam, - levelParam, - exceptionParam, - messageParam, - propertyValuesParam).Compile(); - } - - internal SerilogLogger(object logger) - { - _logger = logger; - } - - public bool Log(LogLevel logLevel, Func messageFunc, Exception exception, params object[] formatParameters) - { - var translatedLevel = TranslateLevel(logLevel); - if (messageFunc == null) - { - return IsEnabled(_logger, translatedLevel); - } - - if (!IsEnabled(_logger, translatedLevel)) - { - return false; - } - - if (exception != null) - { - LogException(translatedLevel, messageFunc, exception, formatParameters); - } - else - { - LogMessage(translatedLevel, messageFunc, formatParameters); - } - - return true; - } - - private void LogMessage(object translatedLevel, Func messageFunc, object[] formatParameters) - { - Write(_logger, translatedLevel, messageFunc(), formatParameters); - } - - private void LogException(object logLevel, Func messageFunc, Exception exception, object[] formatParams) - { - WriteException(_logger, logLevel, exception, messageFunc(), formatParams); - } - - private static object TranslateLevel(LogLevel logLevel) - { - switch (logLevel) - { - case LogLevel.Fatal: - return FatalLevel; - case LogLevel.Error: - return ErrorLevel; - case LogLevel.Warn: - return WarningLevel; - case LogLevel.Info: - return InformationLevel; - case LogLevel.Trace: - return VerboseLevel; - default: - return DebugLevel; - } - } - } - } - - internal class LoupeLogProvider : LogProviderBase - { - /// - /// The form of the Loupe Log.Write method we're using - /// - internal delegate void WriteDelegate( - int severity, - string logSystem, - int skipFrames, - Exception exception, - bool attributeToException, - int writeMode, - string detailsXml, - string category, - string caption, - string description, - params object[] args - ); - - private static bool s_providerIsAvailableOverride = true; - private readonly WriteDelegate _logWriteDelegate; - - public LoupeLogProvider() - { - if (!IsLoggerAvailable()) - { - throw new InvalidOperationException("Gibraltar.Agent.Log (Loupe) not found"); - } - - _logWriteDelegate = GetLogWriteDelegate(); - } - - /// - /// Gets or sets a value indicating whether [provider is available override]. Used in tests. - /// - /// - /// true if [provider is available override]; otherwise, false. - /// - public static bool ProviderIsAvailableOverride - { - get { return s_providerIsAvailableOverride; } - set { s_providerIsAvailableOverride = value; } - } - - public override Logger GetLogger(string name) - { - return new LoupeLogger(name, _logWriteDelegate).Log; - } - - public static bool IsLoggerAvailable() - { - return ProviderIsAvailableOverride && GetLogManagerType() != null; - } - - private static Type GetLogManagerType() - { - return Type.GetType("Gibraltar.Agent.Log, Gibraltar.Agent"); - } - - private static WriteDelegate GetLogWriteDelegate() - { - Type logManagerType = GetLogManagerType(); - Type logMessageSeverityType = Type.GetType("Gibraltar.Agent.LogMessageSeverity, Gibraltar.Agent"); - Type logWriteModeType = Type.GetType("Gibraltar.Agent.LogWriteMode, Gibraltar.Agent"); - - MethodInfo method = logManagerType.GetMethodPortable( - "Write", - logMessageSeverityType, typeof(string), typeof(int), typeof(Exception), typeof(bool), - logWriteModeType, typeof(string), typeof(string), typeof(string), typeof(string), typeof(object[])); - - var callDelegate = (WriteDelegate)method.CreateDelegate(typeof(WriteDelegate)); - return callDelegate; - } - - internal class LoupeLogger - { - private const string LogSystem = "LibLog"; - - private readonly string _category; - private readonly WriteDelegate _logWriteDelegate; - private readonly int _skipLevel; - - internal LoupeLogger(string category, WriteDelegate logWriteDelegate) - { - _category = category; - _logWriteDelegate = logWriteDelegate; -#if DEBUG - _skipLevel = 2; -#else - _skipLevel = 1; -#endif - } - - public bool Log(LogLevel logLevel, Func messageFunc, Exception exception, params object[] formatParameters) - { - if (messageFunc == null) - { - //nothing to log.. - return true; - } - - messageFunc = LogMessageFormatter.SimulateStructuredLogging(messageFunc, formatParameters); - - _logWriteDelegate(ToLogMessageSeverity(logLevel), LogSystem, _skipLevel, exception, true, 0, null, - _category, null, messageFunc.Invoke()); - - return true; - } - - private static int ToLogMessageSeverity(LogLevel logLevel) - { - switch (logLevel) - { - case LogLevel.Trace: - return TraceEventTypeValues.Verbose; - case LogLevel.Debug: - return TraceEventTypeValues.Verbose; - case LogLevel.Info: - return TraceEventTypeValues.Information; - case LogLevel.Warn: - return TraceEventTypeValues.Warning; - case LogLevel.Error: - return TraceEventTypeValues.Error; - case LogLevel.Fatal: - return TraceEventTypeValues.Critical; - default: - throw new ArgumentOutOfRangeException("logLevel"); - } - } - } - } - - internal static class TraceEventTypeValues - { - internal static readonly Type Type; - internal static readonly int Verbose; - internal static readonly int Information; - internal static readonly int Warning; - internal static readonly int Error; - internal static readonly int Critical; - - [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline")] - static TraceEventTypeValues() - { - var assembly = typeof(Uri).GetAssemblyPortable(); // This is to get to the System.dll assembly in a PCL compatible way. - if (assembly == null) - { - return; - } - Type = assembly.GetType("System.Diagnostics.TraceEventType"); - if (Type == null) return; - Verbose = (int)Enum.Parse(Type, "Verbose", false); - Information = (int)Enum.Parse(Type, "Information", false); - Warning = (int)Enum.Parse(Type, "Warning", false); - Error = (int)Enum.Parse(Type, "Error", false); - Critical = (int)Enum.Parse(Type, "Critical", false); - } - } - - internal static class LogMessageFormatter - { - //private static readonly Regex Pattern = new Regex(@"\{@?\w{1,}\}"); -#if LIBLOG_PORTABLE - private static readonly Regex Pattern = new Regex(@"(?[^\d{][^ }]*)}"); -#else - private static readonly Regex Pattern = new Regex(@"(?[^ :{}]+)(?:[^}]+)?}", RegexOptions.Compiled); -#endif - - /// - /// Some logging frameworks support structured logging, such as serilog. This will allow you to add names to structured data in a format string: - /// For example: Log("Log message to {user}", user). This only works with serilog, but as the user of LibLog, you don't know if serilog is actually - /// used. So, this class simulates that. it will replace any text in {curly braces} with an index number. - /// - /// "Log {message} to {user}" would turn into => "Log {0} to {1}". Then the format parameters are handled using regular .net string.Format. - /// - /// The message builder. - /// The format parameters. - /// - public static Func SimulateStructuredLogging(Func messageBuilder, object[] formatParameters) - { - if (formatParameters == null || formatParameters.Length == 0) - { - return messageBuilder; - } - - return () => - { - string targetMessage = messageBuilder(); - IEnumerable patternMatches; - return FormatStructuredMessage(targetMessage, formatParameters, out patternMatches); - }; - } - - private static string ReplaceFirst(string text, string search, string replace) - { - int pos = text.IndexOf(search, StringComparison.Ordinal); - if (pos < 0) - { - return text; - } - return text.Substring(0, pos) + replace + text.Substring(pos + search.Length); - } - - public static string FormatStructuredMessage(string targetMessage, object[] formatParameters, out IEnumerable patternMatches) - { - if (formatParameters.Length == 0) - { - patternMatches = Enumerable.Empty(); - return targetMessage; - } - - List processedArguments = new List(); - patternMatches = processedArguments; - - foreach (Match match in Pattern.Matches(targetMessage)) - { - var arg = match.Groups["arg"].Value; - - int notUsed; - if (!int.TryParse(arg, out notUsed)) - { - int argumentIndex = processedArguments.IndexOf(arg); - if (argumentIndex == -1) - { - argumentIndex = processedArguments.Count; - processedArguments.Add(arg); - } - - targetMessage = ReplaceFirst(targetMessage, match.Value, - "{" + argumentIndex + match.Groups["format"].Value + "}"); - } - } - try - { - return string.Format(CultureInfo.InvariantCulture, targetMessage, formatParameters); - } - catch (FormatException ex) - { - throw new FormatException("The input string '" + targetMessage + "' could not be formatted using string.Format", ex); - } - } - } - - internal static class TypeExtensions - { - internal static ConstructorInfo GetConstructorPortable(this Type type, params Type[] types) - { -#if LIBLOG_PORTABLE - return type.GetTypeInfo().DeclaredConstructors.FirstOrDefault - (constructor => - constructor.GetParameters() - .Select(parameter => parameter.ParameterType) - .SequenceEqual(types)); -#else - return type.GetConstructor(types); -#endif - } - - internal static MethodInfo GetMethodPortable(this Type type, string name) - { -#if LIBLOG_PORTABLE - return type.GetRuntimeMethods().SingleOrDefault(m => m.Name == name); -#else - return type.GetMethod(name); -#endif - } - - internal static MethodInfo GetMethodPortable(this Type type, string name, params Type[] types) - { -#if LIBLOG_PORTABLE - return type.GetRuntimeMethod(name, types); -#else - return type.GetMethod(name, types); -#endif - } - - internal static PropertyInfo GetPropertyPortable(this Type type, string name) - { -#if LIBLOG_PORTABLE - return type.GetRuntimeProperty(name); -#else - return type.GetProperty(name); -#endif - } - - internal static IEnumerable GetFieldsPortable(this Type type) - { -#if LIBLOG_PORTABLE - return type.GetRuntimeFields(); -#else - return type.GetFields(); -#endif - } - - internal static Type GetBaseTypePortable(this Type type) - { -#if LIBLOG_PORTABLE - return type.GetTypeInfo().BaseType; -#else - return type.BaseType; -#endif - } - -#if LIBLOG_PORTABLE - internal static MethodInfo GetGetMethod(this PropertyInfo propertyInfo) - { - return propertyInfo.GetMethod; - } - - internal static MethodInfo GetSetMethod(this PropertyInfo propertyInfo) - { - return propertyInfo.SetMethod; - } -#endif - -#if !LIBLOG_PORTABLE - internal static object CreateDelegate(this MethodInfo methodInfo, Type delegateType) - { - return Delegate.CreateDelegate(delegateType, methodInfo); - } -#endif - - internal static Assembly GetAssemblyPortable(this Type type) - { -#if LIBLOG_PORTABLE - return type.GetTypeInfo().Assembly; -#else - return type.Assembly; -#endif - } - } - - internal class DisposableAction : IDisposable - { - private readonly Action _onDispose; - - public DisposableAction(Action onDispose = null) - { - _onDispose = onDispose; - } - - public void Dispose() - { - if (_onDispose != null) - { - _onDispose(); - } - } - } -} diff --git a/Source/ZoomNet/ZoomNet.csproj b/Source/ZoomNet/ZoomNet.csproj index 175419a2..e1430ef4 100644 --- a/Source/ZoomNet/ZoomNet.csproj +++ b/Source/ZoomNet/ZoomNet.csproj @@ -33,6 +33,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + From 4653989b8008c1b6922eac1bb3c78f1cf3334227 Mon Sep 17 00:00:00 2001 From: Jeremie Desautels Date: Wed, 5 Jun 2019 13:41:46 -0400 Subject: [PATCH 10/41] Improvements to match recent improvements in StrongGrid --- .../ConsoleLogProvider.cs | 48 ---- .../ZoomNet.IntegrationTests/ConsoleUtils.cs | 6 +- Source/ZoomNet.IntegrationTests/Program.cs | 61 +++-- .../ZoomNet.IntegrationTests.csproj | 8 + Source/ZoomNet/Client.cs | 79 +++++-- Source/ZoomNet/IClient.cs | 10 +- Source/ZoomNet/Utilities/DiagnosticHandler.cs | 208 +++++++++++------- .../Utilities/ExcludeFromCodeCoverage.cs | 9 + Source/ZoomNet/Utilities/Extensions.cs | 146 +++++++++--- Source/ZoomNet/Utilities/ZoomClientOptions.cs | 20 ++ Source/ZoomNet/Utilities/ZoomErrorHandler.cs | 55 +---- Source/ZoomNet/Utilities/ZoomException.cs | 41 ++++ 12 files changed, 435 insertions(+), 256 deletions(-) delete mode 100644 Source/ZoomNet.IntegrationTests/ConsoleLogProvider.cs create mode 100644 Source/ZoomNet/Utilities/ExcludeFromCodeCoverage.cs create mode 100644 Source/ZoomNet/Utilities/ZoomClientOptions.cs create mode 100644 Source/ZoomNet/Utilities/ZoomException.cs diff --git a/Source/ZoomNet.IntegrationTests/ConsoleLogProvider.cs b/Source/ZoomNet.IntegrationTests/ConsoleLogProvider.cs deleted file mode 100644 index 8990507b..00000000 --- a/Source/ZoomNet.IntegrationTests/ConsoleLogProvider.cs +++ /dev/null @@ -1,48 +0,0 @@ -namespace ZoomNet.IntegrationTests -{ - using Logging; - using System; - using System.Globalization; - - // Inspired by: https://github.com/damianh/LibLog/blob/master/src/LibLog.Example.ColoredConsoleLogProvider/ColoredConsoleLogProvider.cs - public class ConsoleLogProvider : ILogProvider - { - public Logger GetLogger(string name) - { - return (logLevel, messageFunc, exception, formatParameters) => - { - if (messageFunc == null) - { - return true; // All log levels are enabled - } - - var message = string.Format(CultureInfo.InvariantCulture, messageFunc(), formatParameters); - if (exception != null) - { - message = $"{message} | {exception}"; - } - Console.WriteLine($"{DateTime.UtcNow} | {logLevel} | {name} | {message}"); - - return true; - }; - } - - public IDisposable OpenNestedContext(string message) - { - return NullDisposable.Instance; - } - - public IDisposable OpenMappedContext(string key, string value) - { - return NullDisposable.Instance; - } - - private class NullDisposable : IDisposable - { - internal static readonly IDisposable Instance = new NullDisposable(); - - public void Dispose() - { } - } - } -} diff --git a/Source/ZoomNet.IntegrationTests/ConsoleUtils.cs b/Source/ZoomNet.IntegrationTests/ConsoleUtils.cs index f8a28bdb..b80b4bf7 100644 --- a/Source/ZoomNet.IntegrationTests/ConsoleUtils.cs +++ b/Source/ZoomNet.IntegrationTests/ConsoleUtils.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace ZoomNet.IntegrationTests { @@ -23,8 +23,8 @@ public static void CenterConsole() var consoleWidth = consoleInfo.Right - consoleInfo.Left; var consoleHeight = consoleInfo.Bottom - consoleInfo.Top; - var left = monitorInfo.Monitor.Left + (monitorWidth - consoleWidth) / 2; - var top = monitorInfo.Monitor.Top + (monitorHeight - consoleHeight) / 2; + var left = monitorInfo.Monitor.Left + ((monitorWidth - consoleWidth) / 2); + var top = monitorInfo.Monitor.Top + ((monitorHeight - consoleHeight) / 2); NativeMethods.MoveWindow(hWin, left, top, consoleWidth, consoleHeight, false); } diff --git a/Source/ZoomNet.IntegrationTests/Program.cs b/Source/ZoomNet.IntegrationTests/Program.cs index 9129e75a..281456e6 100644 --- a/Source/ZoomNet.IntegrationTests/Program.cs +++ b/Source/ZoomNet.IntegrationTests/Program.cs @@ -1,15 +1,18 @@ -using System; +using Logzio.DotNet.NLog; +using NLog; +using NLog.Config; +using NLog.Targets; +using System; using System.IO; using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; -using ZoomNet.Logging; using ZoomNet.Utilities; namespace ZoomNet.IntegrationTests { - class Program + public class Program { private const int MAX_ZOOM_API_CONCURRENCY = 5; @@ -23,25 +26,46 @@ private enum ResultCodes static async Task Main() { // ----------------------------------------------------------------------------- - // Do you want to proxy requests through Fiddler (useful for debugging)? - var useFiddler = true; + // Do you want to proxy requests through Fiddler? Can be useful for debugging. + var useFiddler = false; - // As an alternative to Fiddler, you can display debug information about - // every HTTP request/response in the console. This is useful for debugging - // purposes but the amount of information can be overwhelming. - var debugHttpMessagesToConsole = false; + // Logging options. + var options = new ZoomClientOptions() + { + LogLevelFailedCalls = Logging.LogLevel.Error, + LogLevelSuccessfulCalls = Logging.LogLevel.Debug + }; // ----------------------------------------------------------------------------- - var apiKey = Environment.GetEnvironmentVariable("ZOOM_APIKEY"); - var apiSecret = Environment.GetEnvironmentVariable("ZOOM_APISECRET"); - var userId = Environment.GetEnvironmentVariable("ZOOM_USERID"); - var client = useFiddler ? new Client(apiKey, apiSecret, new WebProxy("http://localhost:8888")) : new Client(apiKey, apiSecret); + // Configure logging + var nLogConfig = new LoggingConfiguration(); - if (debugHttpMessagesToConsole) + // Send logs to logz.io + var logzioToken = Environment.GetEnvironmentVariable("LOGZIO_TOKEN"); + if (!string.IsNullOrEmpty(logzioToken)) { - LogProvider.SetCurrentLogProvider(new ConsoleLogProvider()); + var logzioTarget = new LogzioTarget { Token = logzioToken }; + logzioTarget.ContextProperties.Add(new TargetPropertyWithContext("source", "StrongGrid_integration_tests")); + logzioTarget.ContextProperties.Add(new TargetPropertyWithContext("StrongGrid-Version", Client.Version)); + + nLogConfig.AddTarget("Logzio", logzioTarget); + nLogConfig.AddRule(NLog.LogLevel.Debug, NLog.LogLevel.Fatal, "Logzio", "*"); } + // Send logs to console + var consoleTarget = new ColoredConsoleTarget(); + nLogConfig.AddTarget("ColoredConsole", consoleTarget); + nLogConfig.AddRule(NLog.LogLevel.Warn, NLog.LogLevel.Fatal, "ColoredConsole", "*"); + + LogManager.Configuration = nLogConfig; + + // Configure ZoomNet client + var apiKey = Environment.GetEnvironmentVariable("ZOOM_APIKEY"); + var apiSecret = Environment.GetEnvironmentVariable("ZOOM_APISECRET"); + var proxy = useFiddler ? new WebProxy("http://localhost:8888") : null; + var client = new Client(apiKey, apiSecret, proxy, options); + + // Configure Console var source = new CancellationTokenSource(); Console.CancelKeyPress += (s, e) => { @@ -54,11 +78,10 @@ static async Task Main() ConsoleUtils.CenterConsole(); // These are the integration tests that we will execute - var integrationTests = new Func[] + var integrationTests = new Func[] { }; - // Execute the async tests in parallel (with max degree of parallelism) var results = await integrationTests.ForEachAsync( async integrationTest => @@ -67,7 +90,7 @@ static async Task Main() try { - await integrationTest(userId, client, log, source.Token).ConfigureAwait(false); + await integrationTest(client, log, source.Token).ConfigureAwait(false); return (TestName: integrationTest.Method.Name, ResultCode: ResultCodes.Success, Message: string.Empty); } catch (OperationCanceledException) @@ -78,7 +101,7 @@ static async Task Main() catch (Exception e) { var exceptionMessage = e.GetBaseException().Message; - await log.WriteLineAsync($"-----> AN EXCEPTION OCCURED: {exceptionMessage}").ConfigureAwait(false); + await log.WriteLineAsync($"-----> AN EXCEPTION OCCURRED: {exceptionMessage}").ConfigureAwait(false); return (TestName: integrationTest.Method.Name, ResultCode: ResultCodes.Exception, Message: exceptionMessage); } finally diff --git a/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj b/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj index fbaea4aa..2e38804e 100644 --- a/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj +++ b/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj @@ -3,12 +3,20 @@ Exe netcoreapp2.1 + ZoomNet.IntegrationTests + ZoomNet.IntegrationTests latest + + + + + + diff --git a/Source/ZoomNet/Client.cs b/Source/ZoomNet/Client.cs index d1e2b819..3a7bb21a 100644 --- a/Source/ZoomNet/Client.cs +++ b/Source/ZoomNet/Client.cs @@ -1,9 +1,10 @@ -using Pathoschild.Http.Client; +using Pathoschild.Http.Client; using Pathoschild.Http.Client.Extensibility; using System; using System.Net; using System.Net.Http; using System.Reflection; +using ZoomNet.Logging; using ZoomNet.Utilities; namespace ZoomNet @@ -16,7 +17,9 @@ public class Client : IClient, IDisposable #region FIELDS private const string ZOOM_V2_BASE_URI = "https://api.zoom.us/v2"; + private readonly bool _mustDisposeHttpClient; + private readonly ZoomClientOptions _options; private HttpClient _httpClient; private Pathoschild.Http.Client.IClient _fluentClient; @@ -25,6 +28,19 @@ public class Client : IClient, IDisposable #region PROPERTIES + /// + /// Gets the Version. + /// + /// + /// The version. + /// + public static string Version { get; private set; } + + /// + /// Gets the user agent. + /// + public static string UserAgent { get; private set; } + /// /// Gets the resource which allows you to manage sub accounts. /// @@ -73,25 +89,30 @@ public class Client : IClient, IDisposable /// //public IWebinars Webinars { get; private set; } - /// - /// Gets the Version. - /// - /// - /// The version. - /// - public string Version { get; private set; } - #endregion #region CTOR + /// + /// Initializes static members of the class. + /// + static Client() + { + Version = typeof(Client).GetTypeInfo().Assembly.GetName().Version.ToString(3); +#if DEBUG + Version = "DEBUG"; +#endif + UserAgent = $"ZoomNet/{Version} (+https://github.com/Jericho/ZoomNet)"; + } + /// /// Initializes a new instance of the class. /// /// Your Zoom API Key. /// Your Zoom API Secret. - public Client(string apiKey, string apiSecret) - : this(apiKey, apiSecret, null, false) + /// Options for the Zoom client. + public Client(string apiKey, string apiSecret, ZoomClientOptions options = null) + : this(apiKey, apiSecret, null, false, options) { } @@ -101,8 +122,9 @@ public Client(string apiKey, string apiSecret) /// Your Zoom API Key. /// Your Zoom API Secret. /// Allows you to specify a proxy. - public Client(string apiKey, string apiSecret, IWebProxy proxy) - : this(apiKey, apiSecret, new HttpClient(new HttpClientHandler { Proxy = proxy, UseProxy = proxy != null }), true) + /// Options for the Zoom client. + public Client(string apiKey, string apiSecret, IWebProxy proxy, ZoomClientOptions options = null) + : this(apiKey, apiSecret, new HttpClient(new HttpClientHandler { Proxy = proxy, UseProxy = proxy != null }), true, options) { } @@ -112,8 +134,9 @@ public Client(string apiKey, string apiSecret, IWebProxy proxy) /// Your Zoom API Key. /// Your Zoom API Secret. /// TThe HTTP handler stack to use for sending requests. - public Client(string apiKey, string apiSecret, HttpMessageHandler handler) - : this(apiKey, apiSecret, new HttpClient(handler), true) + /// Options for the Zoom client. + public Client(string apiKey, string apiSecret, HttpMessageHandler handler, ZoomClientOptions options = null) + : this(apiKey, apiSecret, new HttpClient(handler), true, options) { } @@ -123,15 +146,17 @@ public Client(string apiKey, string apiSecret, HttpMessageHandler handler) /// Your Zoom API Key. /// Your Zoom API Secret. /// Allows you to inject your own HttpClient. This is useful, for example, to setup the HtppClient with a proxy. - public Client(string apiKey, string apiSecret, HttpClient httpClient) - : this(apiKey, apiSecret, httpClient, false) + /// Options for the Zoom client. + public Client(string apiKey, string apiSecret, HttpClient httpClient, ZoomClientOptions options = null) + : this(apiKey, apiSecret, httpClient, false, options) { } - private Client(string apiKey, string apiSecret, HttpClient httpClient, bool disposeClient) + private Client(string apiKey, string apiSecret, HttpClient httpClient, bool disposeClient, ZoomClientOptions options) { _mustDisposeHttpClient = disposeClient; _httpClient = httpClient; + _options = options ?? GetDefaultOptions(); Version = typeof(Client).GetTypeInfo().Assembly.GetName().Version.ToString(3); #if DEBUG @@ -139,12 +164,15 @@ private Client(string apiKey, string apiSecret, HttpClient httpClient, bool disp #endif _fluentClient = new FluentClient(new Uri(ZOOM_V2_BASE_URI), httpClient) - .SetUserAgent($"ZoomNet/{Version} (+https://github.com/Jericho/ZoomNet)"); - //.SetRequestCoordinator(new ZoomRetryStrategy()); + .SetUserAgent(Client.UserAgent); + // .SetRequestCoordinator(new ZoomRetryStrategy()); _fluentClient.Filters.Remove(); + + // Order is important: JwtTokenHandler, must be first, followed by DiagnosticHandler and then by ErrorHandler. + // Also, the list of filters must be kept in sync with the filters in Utils.GetFluentClient in the unit testing project. _fluentClient.Filters.Add(new JwtTokenHandler(apiKey, apiSecret)); - _fluentClient.Filters.Add(new DiagnosticHandler()); + _fluentClient.Filters.Add(new DiagnosticHandler(_options.LogLevelSuccessfulCalls, _options.LogLevelFailedCalls)); _fluentClient.Filters.Add(new ZoomErrorHandler()); _fluentClient.SetOptions(new FluentClientOptions() @@ -231,6 +259,15 @@ private void ReleaseUnmanagedResources() // We do not hold references to unmanaged resources } + private ZoomClientOptions GetDefaultOptions() + { + return new ZoomClientOptions() + { + LogLevelSuccessfulCalls = LogLevel.Debug, + LogLevelFailedCalls = LogLevel.Debug + }; + } + #endregion } } diff --git a/Source/ZoomNet/IClient.cs b/Source/ZoomNet/IClient.cs index 17d67667..469a991e 100644 --- a/Source/ZoomNet/IClient.cs +++ b/Source/ZoomNet/IClient.cs @@ -1,4 +1,4 @@ -namespace ZoomNet +namespace ZoomNet { /// /// Interface for the Zoom REST client. @@ -52,13 +52,5 @@ public interface IClient /// The webinars resource. /// //IWebinars Webinars { get; } - - /// - /// Gets the Version. - /// - /// - /// The version. - /// - string Version { get; } } } diff --git a/Source/ZoomNet/Utilities/DiagnosticHandler.cs b/Source/ZoomNet/Utilities/DiagnosticHandler.cs index 83070dee..2fe4b585 100644 --- a/Source/ZoomNet/Utilities/DiagnosticHandler.cs +++ b/Source/ZoomNet/Utilities/DiagnosticHandler.cs @@ -1,26 +1,44 @@ -using Pathoschild.Http.Client; +using Pathoschild.Http.Client; using Pathoschild.Http.Client.Extensibility; using System; -using System.Collections.Generic; +using System.Collections.Concurrent; using System.Diagnostics; using System.Linq; using System.Net.Http; +using System.Net.Http.Headers; using System.Text; using ZoomNet.Logging; namespace ZoomNet.Utilities { /// - /// Diagnostic handler for requests dispatched to the SendGrid API. + /// Diagnostic handler for requests dispatched to the Zoom API. /// /// internal class DiagnosticHandler : IHttpFilter { #region FIELDS - private const string DIAGNOSTIC_ID_HEADER_NAME = "ZoomNet-DiagnosticId"; + internal const string DIAGNOSTIC_ID_HEADER_NAME = "ZoomNet-Diagnostic-Id"; private static readonly ILog _logger = LogProvider.For(); - private readonly IDictionary, Tuple> _diagnostics = new Dictionary, Tuple>(); + private readonly LogLevel _logLevelSuccessfulCalls; + private readonly LogLevel _logLevelFailedCalls; + + #endregion + + #region PROPERTIES + + internal static ConcurrentDictionary RequestReference, string Diagnostic, long RequestTimestamp, long ResponseTimeStamp)> DiagnosticsInfo { get; } = new ConcurrentDictionary, string, long, long)>(); + + #endregion + + #region CTOR + + public DiagnosticHandler(LogLevel logLevelSuccessfulCalls, LogLevel logLevelFailedCalls) + { + _logLevelSuccessfulCalls = logLevelSuccessfulCalls; + _logLevelFailedCalls = logLevelFailedCalls; + } #endregion @@ -30,18 +48,21 @@ internal class DiagnosticHandler : IHttpFilter /// The HTTP request. public void OnRequest(IRequest request) { - request.WithHeader(DIAGNOSTIC_ID_HEADER_NAME, Guid.NewGuid().ToString("N")); + // Add a unique ID to the request header + var diagnosticId = Guid.NewGuid().ToString("N"); + request.WithHeader(DIAGNOSTIC_ID_HEADER_NAME, diagnosticId); + // Log the request var httpRequest = request.Message; + var diagnostic = new StringBuilder(); - var diagnosticMessage = new StringBuilder(); - diagnosticMessage.AppendLine($"Request: {httpRequest}"); - diagnosticMessage.AppendLine($"Request Content: {httpRequest.Content?.ReadAsStringAsync(null).GetAwaiter().GetResult() ?? ""}"); + diagnostic.AppendLine("REQUEST:"); + diagnostic.AppendLine($" {httpRequest.Method.Method} {httpRequest.RequestUri}"); + LogHeaders(diagnostic, httpRequest.Headers); + LogContent(diagnostic, httpRequest.Content); - lock (_diagnostics) - { - _diagnostics.Add(new WeakReference(request.Message), new Tuple(diagnosticMessage, Stopwatch.StartNew())); - } + // Add the diagnotic info to our cache + DiagnosticsInfo.TryAdd(diagnosticId, (new WeakReference(request.Message), diagnostic.ToString(), Stopwatch.GetTimestamp(), long.MinValue)); } /// Method invoked just after the HTTP response is received. This method can modify the incoming HTTP response. @@ -49,100 +70,137 @@ public void OnRequest(IRequest request) /// Whether HTTP error responses should be raised as exceptions. public void OnResponse(IResponse response, bool httpErrorAsException) { - var diagnosticMessage = string.Empty; + var responseTimestamp = Stopwatch.GetTimestamp(); + var httpResponse = response.Message; - try + var diagnosticId = response.Message.RequestMessage.Headers.GetValue(DIAGNOSTIC_ID_HEADER_NAME); + if (DiagnosticsInfo.TryGetValue(diagnosticId, out (WeakReference RequestReference, string Diagnostic, long RequestTimestamp, long ResponseTimestamp) diagnosticInfo)) { - var diagnosticInfo = GetDiagnosticInfo(response.Message.RequestMessage); - var diagnosticStringBuilder = diagnosticInfo.Item1; - var diagnosticTimer = diagnosticInfo.Item2; - if (diagnosticTimer != null) diagnosticTimer?.Stop(); - - var httpResponse = response.Message; - + var updatedDiagnostic = new StringBuilder(diagnosticInfo.Diagnostic); try { - diagnosticStringBuilder.AppendLine($"Response: {httpResponse}"); - diagnosticStringBuilder.AppendLine($"Response.Content is null: {httpResponse.Content == null}"); - diagnosticStringBuilder.AppendLine($"Response.Content.Headers is null: {httpResponse.Content?.Headers == null}"); - diagnosticStringBuilder.AppendLine($"Response.Content.Headers.ContentType is null: {httpResponse.Content?.Headers?.ContentType == null}"); - diagnosticStringBuilder.AppendLine($"Response.Content.Headers.ContentType.CharSet: {httpResponse.Content?.Headers?.ContentType?.CharSet ?? ""}"); - diagnosticStringBuilder.AppendLine($"Response.Content: {httpResponse.Content?.ReadAsStringAsync(null).GetAwaiter().GetResult() ?? ""}"); + // Log the response + updatedDiagnostic.AppendLine(); + updatedDiagnostic.AppendLine("RESPONSE:"); + updatedDiagnostic.AppendLine($" HTTP/{httpResponse.Version} {(int)httpResponse.StatusCode} {httpResponse.ReasonPhrase}"); + LogHeaders(updatedDiagnostic, httpResponse.Headers); + LogContent(updatedDiagnostic, httpResponse.Content); + + // Calculate how much time elapsed between request and response + var elapsed = TimeSpan.FromTicks(responseTimestamp - diagnosticInfo.RequestTimestamp); + + // Log diagnostic + updatedDiagnostic.AppendLine(); + updatedDiagnostic.AppendLine("DIAGNOSTIC:"); + updatedDiagnostic.AppendLine($" The request took {elapsed.ToDurationString()}"); } - catch + catch (Exception e) { - // Intentionally ignore errors that may occur when attempting to log the content of the response - } + Debug.WriteLine("{0}\r\nAN EXCEPTION OCCURRED: {1}\r\n{0}", new string('=', 50), e.GetBaseException().Message); + updatedDiagnostic.AppendLine($"AN EXCEPTION OCCURRED: {e.GetBaseException().Message}"); - if (diagnosticTimer != null) - { - diagnosticStringBuilder.AppendLine($"The request took {diagnosticTimer.Elapsed.ToDurationString()}"); + if (_logger != null && _logger.IsErrorEnabled()) + { + _logger.Error(e, "An exception occurred when inspecting the response from SendGrid"); + } } + finally + { + var diagnosticMessage = updatedDiagnostic.ToString(); - diagnosticMessage = diagnosticStringBuilder.ToString(); - } - catch (Exception e) - { - Debug.WriteLine("{0}\r\nAN EXCEPTION OCCURED: {1}\r\n{0}", new string('=', 25), e.GetBaseException().Message); + LogDiagnostic(response.IsSuccessStatusCode, _logLevelSuccessfulCalls, diagnosticMessage); + LogDiagnostic(!response.IsSuccessStatusCode, _logLevelFailedCalls, diagnosticMessage); - if (_logger != null && _logger.IsErrorEnabled()) - { - _logger.Error(e, "An exception occured when inspecting the response from SendGrid"); + DiagnosticsInfo.TryUpdate( + diagnosticId, + (diagnosticInfo.RequestReference, updatedDiagnostic.ToString(), diagnosticInfo.RequestTimestamp, responseTimestamp), + (diagnosticInfo.RequestReference, diagnosticInfo.Diagnostic, diagnosticInfo.RequestTimestamp, diagnosticInfo.ResponseTimestamp)); } } - finally - { - if (!string.IsNullOrEmpty(diagnosticMessage)) - { - Debug.WriteLine("{0}\r\n{1}{0}", new string('=', 25), diagnosticMessage); - if (_logger != null && _logger.IsDebugEnabled()) - { - _logger.Debug(diagnosticMessage - .Replace("{", "{{") - .Replace("}", "}}")); - } - } - } + Cleanup(); } #endregion #region PRIVATE METHODS - private Tuple GetDiagnosticInfo(HttpRequestMessage requestMessage) + private void LogHeaders(StringBuilder diagnostic, HttpHeaders httpHeaders) { - lock (_diagnostics) + if (httpHeaders != null) { - var diagnosticId = requestMessage.Headers.GetValues(DIAGNOSTIC_ID_HEADER_NAME).FirstOrDefault(); - - foreach (WeakReference key in _diagnostics.Keys.ToArray()) + foreach (var header in httpHeaders) { - // Check if garbage collected - if (!key.TryGetTarget(out HttpRequestMessage request)) + if (header.Key.Equals("authorization", StringComparison.OrdinalIgnoreCase)) { - _diagnostics.Remove(key); - continue; + diagnostic.AppendLine($" {header.Key}: ...redacted for security reasons..."); } - - // Check if different request - var requestDiagnosticId = request.Headers.GetValues(DIAGNOSTIC_ID_HEADER_NAME).FirstOrDefault(); - if (requestDiagnosticId != diagnosticId) + else { - continue; + diagnostic.AppendLine($" {header.Key}: {string.Join(", ", header.Value)}"); } + } + } + } - // Get the diagnostic info from dictionary - var diagnosticInfo = _diagnostics[key]; + private void LogContent(StringBuilder diagnostic, HttpContent httpContent) + { + if (httpContent == null) + { + diagnostic.AppendLine(" Content-Length: 0"); + } + else + { + LogHeaders(diagnostic, httpContent.Headers); - // Remove the diagnostic info from dictionary - _diagnostics.Remove(key); + var contentLength = httpContent.Headers?.ContentLength.GetValueOrDefault(0) ?? 0; + if (!httpContent.Headers?.Contains("Content-Length") ?? false) + { + diagnostic.AppendLine($" Content-Length: {contentLength}"); + } - return diagnosticInfo; + if (contentLength > 0) + { + diagnostic.AppendLine(); + diagnostic.AppendLine(httpContent.ReadAsStringAsync(null).GetAwaiter().GetResult() ?? ""); } } + } - return new Tuple(new StringBuilder(), null); + private void LogDiagnostic(bool shouldLog, LogLevel logLEvel, string diagnosticMessage) + { + if (shouldLog && _logger != null) + { + var logLevelEnabled = _logger.Log(logLEvel, null, null, Array.Empty()); + if (logLevelEnabled) + { + _logger.Log(logLEvel, () => diagnosticMessage + .Replace("{", "{{") + .Replace("}", "}}")); + } + } + } + + private void Cleanup() + { + try + { + // Remove diagnostic information for requests that have been garbage collected + foreach (string key in DiagnosticHandler.DiagnosticsInfo.Keys.ToArray()) + { + if (DiagnosticHandler.DiagnosticsInfo.TryGetValue(key, out (WeakReference RequestReference, string Diagnostic, long RequestTimeStamp, long ResponseTimestamp) diagnosticInfo)) + { + if (!diagnosticInfo.RequestReference.TryGetTarget(out HttpRequestMessage request)) + { + DiagnosticsInfo.TryRemove(key, out _); + } + } + } + } + catch + { + // Intentionally left empty + } } #endregion diff --git a/Source/ZoomNet/Utilities/ExcludeFromCodeCoverage.cs b/Source/ZoomNet/Utilities/ExcludeFromCodeCoverage.cs new file mode 100644 index 00000000..5eb4c233 --- /dev/null +++ b/Source/ZoomNet/Utilities/ExcludeFromCodeCoverage.cs @@ -0,0 +1,9 @@ +using System; + +namespace StrongGrid.Utilities +{ + [StrongGrid.Utilities.ExcludeFromCodeCoverage] + internal class ExcludeFromCodeCoverage : Attribute + { + } +} diff --git a/Source/ZoomNet/Utilities/Extensions.cs b/Source/ZoomNet/Utilities/Extensions.cs index 29f25824..10bb1af6 100644 --- a/Source/ZoomNet/Utilities/Extensions.cs +++ b/Source/ZoomNet/Utilities/Extensions.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Pathoschild.Http.Client; using System; @@ -51,7 +51,7 @@ public static long ToUnixTime(this DateTime date) /// /// Reads the content of the HTTP response as string asynchronously. /// - /// The content. + /// The content. /// The encoding. You can leave this parameter null and the encoding will be /// automatically calculated based on the charset in the response. Also, UTF-8 /// encoding will be used if the charset is absent from the response, is blank @@ -90,26 +90,33 @@ public static long ToUnixTime(this DateTime date) /// var responseContent = await response.Content.ReadAsStringAsync(null).ConfigureAwait(false); /// /// - public static async Task ReadAsStringAsync(this HttpContent content, Encoding encoding) + public static async Task ReadAsStringAsync(this HttpContent httpContent, Encoding encoding) { - var responseStream = await content.ReadAsStreamAsync().ConfigureAwait(false); - var responseContent = string.Empty; + var content = string.Empty; - if (encoding == null) encoding = content.GetEncoding(Encoding.UTF8); - - // This is important: we must make a copy of the response stream otherwise we would get an - // exception on subsequent attempts to read the content of the stream - using (var ms = new MemoryStream()) + if (httpContent != null) { - await content.CopyToAsync(ms).ConfigureAwait(false); - ms.Position = 0; - using (var sr = new StreamReader(ms, encoding)) + var contentStream = await httpContent.ReadAsStreamAsync().ConfigureAwait(false); + + if (encoding == null) encoding = httpContent.GetEncoding(Encoding.UTF8); + + // This is important: we must make a copy of the response stream otherwise we would get an + // exception on subsequent attempts to read the content of the stream + using (var ms = new MemoryStream()) { - responseContent = await sr.ReadToEndAsync().ConfigureAwait(false); + await contentStream.CopyToAsync(ms).ConfigureAwait(false); + ms.Position = 0; + using (var sr = new StreamReader(ms, encoding)) + { + content = await sr.ReadToEndAsync().ConfigureAwait(false); + } + + // It's important to rewind the stream + if (contentStream.CanSeek) contentStream.Position = 0; } } - return responseContent; + return content; } /// @@ -139,17 +146,17 @@ public static async Task ReadAsStringAsync(this HttpContent content, Enc public static Encoding GetEncoding(this HttpContent content, Encoding defaultEncoding) { var encoding = defaultEncoding; - var charset = content.Headers.ContentType.CharSet; - if (!string.IsNullOrEmpty(charset)) + try { - try + var charset = content?.Headers?.ContentType?.CharSet; + if (!string.IsNullOrEmpty(charset)) { encoding = Encoding.GetEncoding(charset); } - catch - { - encoding = defaultEncoding; - } + } + catch + { + encoding = defaultEncoding; } return encoding; @@ -218,7 +225,7 @@ public static async Task> AsPaginatedResponse(this IRequ /// public static IRequest WithJsonBody(this IRequest request, T body) { - return request.WithBody(body, new MediaTypeHeaderValue("application/json")); + return request.WithBody(bodyBuilder => bodyBuilder.Model(body, new MediaTypeHeaderValue("application/json"))); } /// Asynchronously retrieve the response body as a . @@ -268,9 +275,9 @@ void AppendFormatIfNecessary(StringBuilder stringBuilder, string timePart, int v var result = new StringBuilder(); AppendFormatIfNecessary(result, "day", timeSpan.Days); AppendFormatIfNecessary(result, "hour", timeSpan.Hours); - AppendFormatIfNecessary(result, "minute", timeSpan.Days); - AppendFormatIfNecessary(result, "second", timeSpan.Days); - AppendFormatIfNecessary(result, "millisecond", timeSpan.Days); + AppendFormatIfNecessary(result, "minute", timeSpan.Minutes); + AppendFormatIfNecessary(result, "second", timeSpan.Seconds); + AppendFormatIfNecessary(result, "millisecond", timeSpan.Milliseconds); return result.ToString().Trim(); } @@ -304,7 +311,7 @@ public static void AddPropertyIfValue(this JObject jsonObject, string propertyNa public static void AddPropertyIfValue(this JObject jsonObject, string propertyName, T value, JsonConverter converter = null) { - if (EqualityComparer.Default.Equals(value, default(T))) return; + if (EqualityComparer.Default.Equals(value, default)) return; var jsonSerializer = new JsonSerializer(); if (converter != null) @@ -330,7 +337,7 @@ public static void AddPropertyIfValue(this JObject jsonObject, string propert public static T GetPropertyValue(this JToken item, string name) { - if (item[name] == null) return default(T); + if (item[name] == null) return default; return item[name].Value(); } @@ -419,6 +426,89 @@ public static bool IsNumber(this object value) || value is decimal; } + /// + /// Returns the first value for a specified header stored in the System.Net.Http.Headers.HttpHeaderscollection. + /// + /// The HTTP headers. + /// The specified header to return value for. + /// A string. + public static string GetValue(this HttpHeaders headers, string name) + { + if (headers == null) return null; + + if (headers.TryGetValues(name, out IEnumerable values)) + { + return values.FirstOrDefault(); + } + + return null; + } + + public static void CheckForZoomErrors(this IResponse response) + { + var (isError, errorMessage) = GetErrorMessage(response.Message).GetAwaiter().GetResult(); + if (!isError) return; + + var diagnosticId = response.Message.RequestMessage.Headers.GetValue(DiagnosticHandler.DIAGNOSTIC_ID_HEADER_NAME); + if (DiagnosticHandler.DiagnosticsInfo.TryGetValue(diagnosticId, out (WeakReference RequestReference, string Diagnostic, long RequestTimeStamp, long ResponseTimestamp) diagnosticInfo)) + { + throw new ZoomException(errorMessage, response.Message, diagnosticInfo.Diagnostic); + } + else + { + throw new ZoomException(errorMessage, response.Message, "Diagnostic log unavailable"); + } + } + + private static async Task<(bool, string)> GetErrorMessage(HttpResponseMessage message) + { + // Assume there is no error + var isError = false; + + // Default error message + var errorMessage = $"{(int)message.StatusCode}: {message.ReasonPhrase}"; + + /* + In case of an error, the Zoom API returns a JSON string that looks like this: + { + "code": 300, + "message": "This meeting has not registration required: 544993922" + } + */ + + var responseContent = await message.Content.ReadAsStringAsync(null).ConfigureAwait(false); + + if (!string.IsNullOrEmpty(responseContent)) + { + try + { + // Check for the presence of property called 'errors' + var jObject = JObject.Parse(responseContent); + var codeProperty = jObject["code"]; + var messageProperty = jObject["message"]; + + if (messageProperty != null) + { + errorMessage = messageProperty.Value(); + isError = true; + } + else if (codeProperty != null) + { + errorMessage = $"Error code: {codeProperty.Value()}"; + isError = true; + } +#pragma warning disable RECS0022 // A catch clause that catches System.Exception and has an empty body + } + catch +#pragma warning restore RECS0022 // A catch clause that catches System.Exception and has an empty body + { + // Intentionally ignore parsing errors + } + } + + return (isError, errorMessage); + } + /// Asynchronously converts the JSON encoded content and converts it to an object of the desired type. /// The response model to deserialize into. /// The content. diff --git a/Source/ZoomNet/Utilities/ZoomClientOptions.cs b/Source/ZoomNet/Utilities/ZoomClientOptions.cs new file mode 100644 index 00000000..beab2e05 --- /dev/null +++ b/Source/ZoomNet/Utilities/ZoomClientOptions.cs @@ -0,0 +1,20 @@ +using ZoomNet.Logging; + +namespace ZoomNet.Utilities +{ + /// + /// Options for the Zoom client. + /// + public class ZoomClientOptions + { + /// + /// Gets or sets the log levels for successful calls (HTTP status code in the 200-299 range). + /// + public LogLevel LogLevelSuccessfulCalls { get; set; } + + /// + /// Gets or sets the log levels for failed calls (HTTP status code outside of the 200-299 range). + /// + public LogLevel LogLevelFailedCalls { get; set; } + } +} diff --git a/Source/ZoomNet/Utilities/ZoomErrorHandler.cs b/Source/ZoomNet/Utilities/ZoomErrorHandler.cs index 4db5acfe..20c53013 100644 --- a/Source/ZoomNet/Utilities/ZoomErrorHandler.cs +++ b/Source/ZoomNet/Utilities/ZoomErrorHandler.cs @@ -1,9 +1,5 @@ -using Newtonsoft.Json.Linq; using Pathoschild.Http.Client; using Pathoschild.Http.Client.Extensibility; -using System; -using System.Net.Http; -using System.Threading.Tasks; namespace ZoomNet.Utilities { @@ -24,56 +20,9 @@ public void OnRequest(IRequest request) { } /// Whether HTTP error responses should be raised as exceptions. public void OnResponse(IResponse response, bool httpErrorAsException) { - if (response.Message.IsSuccessStatusCode) return; + if (response.IsSuccessStatusCode) return; - var errorMessage = GetErrorMessage(response.Message).Result; - throw new Exception(errorMessage); - } - - private static async Task GetErrorMessage(HttpResponseMessage message) - { - // Default error message - var errorMessage = $"{(int)message.StatusCode}: {message.ReasonPhrase}"; - - if (message.Content != null) - { - /* - In case of an error, the Zoom API returns a JSON string that looks like this: - { - "code": 300, - "message": "This meeting has not registration required: 544993922" - } - */ - - var responseContent = await message.Content.ReadAsStringAsync(null).ConfigureAwait(false); - - if (!string.IsNullOrEmpty(responseContent)) - { - try - { - var jObject = JObject.Parse(responseContent); - var codeProperty = jObject["code"]; - var messageProperty = jObject["message"]; - - if (messageProperty != null) - { - errorMessage = messageProperty.Value(); - } - else if (codeProperty != null) - { - errorMessage = $"Error code: {codeProperty.Value()}"; - } -#pragma warning disable RECS0022 // A catch clause that catches System.Exception and has an empty body - } - catch -#pragma warning restore RECS0022 // A catch clause that catches System.Exception and has an empty body - { - // Intentionally ignore parsing errors - } - } - } - - return errorMessage; + response.CheckForZoomErrors(); } #endregion diff --git a/Source/ZoomNet/Utilities/ZoomException.cs b/Source/ZoomNet/Utilities/ZoomException.cs new file mode 100644 index 00000000..cfdd2133 --- /dev/null +++ b/Source/ZoomNet/Utilities/ZoomException.cs @@ -0,0 +1,41 @@ +using System; +using System.Net; +using System.Net.Http; + +namespace ZoomNet.Utilities +{ + /// + /// Exception that includes both the formatted message and the status code. + /// + public class ZoomException : Exception + { + /// + /// Gets the status code of the non-successful call. + /// + public HttpStatusCode StatusCode => ResponseMessage.StatusCode; + + /// + /// Gets the HTTP response message which caused the exception. + /// + public HttpResponseMessage ResponseMessage { get; } + + /// + /// Gets the human readable representation of the request/response. + /// + public string DiagnosticLog { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + /// The response message of the non-successful call. + /// The human readable representation of the request/response. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public ZoomException(string message, HttpResponseMessage responseMessage, string diagnosticLog, Exception innerException = null) + : base(message, innerException) + { + ResponseMessage = responseMessage; + DiagnosticLog = diagnosticLog; + } + } +} From 95888428f2feda2353ec3e179334796fa9b6c053 Mon Sep 17 00:00:00 2001 From: Jeremie Desautels Date: Wed, 5 Jun 2019 14:24:45 -0400 Subject: [PATCH 11/41] Restore Zoom specific code that was mistakenly removed --- Source/ZoomNet.IntegrationTests/Program.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Source/ZoomNet.IntegrationTests/Program.cs b/Source/ZoomNet.IntegrationTests/Program.cs index 281456e6..2ec91c57 100644 --- a/Source/ZoomNet.IntegrationTests/Program.cs +++ b/Source/ZoomNet.IntegrationTests/Program.cs @@ -62,6 +62,7 @@ static async Task Main() // Configure ZoomNet client var apiKey = Environment.GetEnvironmentVariable("ZOOM_APIKEY"); var apiSecret = Environment.GetEnvironmentVariable("ZOOM_APISECRET"); + var userId = Environment.GetEnvironmentVariable("ZOOM_USERID"); var proxy = useFiddler ? new WebProxy("http://localhost:8888") : null; var client = new Client(apiKey, apiSecret, proxy, options); @@ -78,7 +79,7 @@ static async Task Main() ConsoleUtils.CenterConsole(); // These are the integration tests that we will execute - var integrationTests = new Func[] + var integrationTests = new Func[] { }; @@ -90,7 +91,7 @@ static async Task Main() try { - await integrationTest(client, log, source.Token).ConfigureAwait(false); + await integrationTest(userId, client, log, source.Token).ConfigureAwait(false); return (TestName: integrationTest.Method.Name, ResultCode: ResultCodes.Success, Message: string.Empty); } catch (OperationCanceledException) From 84c85022e1db5d0ca78b503aa63782787238dbb0 Mon Sep 17 00:00:00 2001 From: Jeremie Desautels Date: Tue, 18 Jun 2019 10:42:12 -0400 Subject: [PATCH 12/41] REfresh resources and upgrade build server environment to VS.NET 2019 --- appveyor.yml | 2 +- build.cake | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 82c4365e..f04be9ad 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -23,4 +23,4 @@ cache: - tools -> build.cake, tools\packages.config # Environment configuration -image: Visual Studio 2017 +image: Visual Studio 2019 diff --git a/build.cake b/build.cake index cefbb906..f1b4360c 100644 --- a/build.cake +++ b/build.cake @@ -5,7 +5,7 @@ #tool nuget:?package=GitVersion.CommandLine&version=4.0.0 #tool nuget:?package=GitReleaseManager&version=0.8.0 #tool nuget:?package=OpenCover&version=4.7.922 -#tool nuget:?package=ReportGenerator&version=4.1.10 +#tool nuget:?package=ReportGenerator&version=4.2.1 #tool nuget:?package=coveralls.io&version=1.4.2 #tool nuget:?package=xunit.runner.console&version=2.4.1 From a2a2b701b0a0ec63465541d1a4e942e251f9e279 Mon Sep 17 00:00:00 2001 From: Jericho Date: Fri, 1 May 2020 12:02:10 -0400 Subject: [PATCH 13/41] Update resource files and build script --- .gitignore | 10 +- GitReleaseManager.yaml | 38 ++-- GitVersion.yml | 3 + .../ZoomNet.UnitTests.csproj | 1 - appveyor.yml | 1 + build.cake | 168 ++++++++++++++---- build.ps1 | 10 +- tools/packages.config | 2 +- 8 files changed, 174 insertions(+), 59 deletions(-) diff --git a/.gitignore b/.gitignore index d6ffc67f..4e35409d 100644 --- a/.gitignore +++ b/.gitignore @@ -46,9 +46,10 @@ Generated\ Files/ [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* -# NUNIT +# NUnit *.VisualState.xml TestResult.xml +nunit-*.xml # Build Results of an ATL Project [Dd]ebugPS/ @@ -71,7 +72,6 @@ StyleCopReport.xml *_p.c *_h.h *.ilk -*.meta *.obj *.iobj *.pch @@ -189,6 +189,8 @@ PublishScripts/ # NuGet Packages *.nupkg +# NuGet Symbol Packages +*.snupkg # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. @@ -264,7 +266,9 @@ ServiceFabricBackup/ *.bim.layout *.bim_*.settings *.rptproj.rsuser -*- Backup*.rdl +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl # Microsoft Fakes FakesAssemblies/ diff --git a/GitReleaseManager.yaml b/GitReleaseManager.yaml index c7541793..e11a3d19 100644 --- a/GitReleaseManager.yaml +++ b/GitReleaseManager.yaml @@ -4,6 +4,17 @@ create: footer-content: You can download this release from [nuget.org](https://www.nuget.org/packages/ZoomNet/{milestone}) footer-includes-milestone: true milestone-replace-text: '{milestone}' +close: + use-issue-comments: true + issue-comment: |- + :tada: This issue has been resolved in version {milestone} :tada: + + The release is available on: + + - [GitHub Release](https://github.com/{owner}/{repository}/releases/tag/{milestone}) + - [NuGet Package](https://www.nuget.org/packages/ZoomNet/{milestone}) + + Your **[GitReleaseManager](https://github.com/GitTools/GitReleaseManager)** bot :package::rocket: export: include-created-date-in-title: true created-date-string-format: MMMM dd, yyyy @@ -11,17 +22,20 @@ export: regex-text: '### Where to get it(\r\n)*You can .*\)' multiline-regex: true issue-labels-include: -- Breaking Change -- Bug -- New Feature -- Improvement -- Documentation + - Breaking Change + - Bug + - New Feature + - Improvement + - Documentation + - Security issue-labels-exclude: -- Question -- Duplicate -- Invalid -- Wontfix + - Question + - Duplicate + - Invalid + - Wontfix + - Build + - Internal Refactoring issue-labels-alias: - - name: Documentation - header: Documentation - plural: Documentation + - name: Documentation + header: Documentation + plural: Documentation diff --git a/GitVersion.yml b/GitVersion.yml index dede53e4..366d8f27 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -1,5 +1,8 @@ mode: ContinuousDelivery branches: + feature: + regex: feature(s)?[/-] + mode: ContinuousDeployment develop: regex: dev(elop)?(ment)?$ mode: ContinuousDeployment diff --git a/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj b/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj index 37f6403b..bb24e382 100644 --- a/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj +++ b/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj @@ -17,7 +17,6 @@ all runtime; build; native; contentfiles; analyzers - diff --git a/appveyor.yml b/appveyor.yml index f04be9ad..3950b479 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,6 +4,7 @@ init: # Build script build_script: + - ps: .\build.ps1 -bootstrap - ps: .\build.ps1 -Target AppVeyor # Tests diff --git a/build.cake b/build.cake index f1b4360c..b7b5236b 100644 --- a/build.cake +++ b/build.cake @@ -1,14 +1,20 @@ -// Install addins. -#addin nuget:?package=Cake.Coveralls&version=0.9.0 +// Install modules +#module nuget:?package=Cake.DotNetTool.Module&version=0.4.0 + +// Install .NET tools +#tool dotnet:?package=BenchmarkDotNet.Tool&version=0.12.0 // Install tools. -#tool nuget:?package=GitVersion.CommandLine&version=4.0.0 -#tool nuget:?package=GitReleaseManager&version=0.8.0 +#tool nuget:?package=GitVersion.CommandLine&version=5.2.4 +#tool nuget:?package=GitReleaseManager&version=0.11.0 #tool nuget:?package=OpenCover&version=4.7.922 -#tool nuget:?package=ReportGenerator&version=4.2.1 +#tool nuget:?package=ReportGenerator&version=4.5.6 #tool nuget:?package=coveralls.io&version=1.4.2 #tool nuget:?package=xunit.runner.console&version=2.4.1 +// Install addins. +#addin nuget:?package=Cake.Coveralls&version=0.10.1 + /////////////////////////////////////////////////////////////////////////////// // ARGUMENTS @@ -29,32 +35,37 @@ var testCoverageFilter = "+[ZoomNet]* -[ZoomNet]ZoomNet.Properties.* -[ZoomNet]Z var testCoverageExcludeByAttribute = "*.ExcludeFromCodeCoverage*"; var testCoverageExcludeByFile = "*/*Designer.cs;*/*AssemblyInfo.cs"; -var nuGetApiUrl = EnvironmentVariable("NUGET_API_URL"); -var nuGetApiKey = EnvironmentVariable("NUGET_API_KEY"); +var nuGetApiUrl = Argument("NUGET_API_URL", EnvironmentVariable("NUGET_API_URL")); +var nuGetApiKey = Argument("NUGET_API_KEY", EnvironmentVariable("NUGET_API_KEY")); -var myGetApiUrl = EnvironmentVariable("MYGET_API_URL"); -var myGetApiKey = EnvironmentVariable("MYGET_API_KEY"); +var myGetApiUrl = Argument("MYGET_API_URL", EnvironmentVariable("MYGET_API_URL")); +var myGetApiKey = Argument("MYGET_API_KEY", EnvironmentVariable("MYGET_API_KEY")); -var gitHubUserName = EnvironmentVariable("GITHUB_USERNAME"); -var gitHubPassword = EnvironmentVariable("GITHUB_PASSWORD"); +var gitHubToken = Argument("GITHUB_TOKEN", EnvironmentVariable("GITHUB_TOKEN")); +var gitHubUserName = Argument("GITHUB_USERNAME", EnvironmentVariable("GITHUB_USERNAME")); +var gitHubPassword = Argument("GITHUB_PASSWORD", EnvironmentVariable("GITHUB_PASSWORD")); +var gitHubRepoOwner = Argument("GITHUB_REPOOWNER", EnvironmentVariable("GITHUB_REPOOWNER") ?? gitHubUserName); var sourceFolder = "./Source/"; - var outputDir = "./artifacts/"; -var codeCoverageDir = outputDir + "CodeCoverage/"; -var unitTestsProject = sourceFolder + libraryName + ".UnitTests/" + libraryName + ".UnitTests.csproj"; +var codeCoverageDir = $"{outputDir}CodeCoverage/"; +var benchmarkDir = $"{outputDir}Benchmark/"; + +var unitTestsProject = $"{sourceFolder}{libraryName}.UnitTests/{libraryName}.UnitTests.csproj"; +var benchmarkProject = $"{sourceFolder}{libraryName}.Benchmark/{libraryName}.Benchmark.csproj"; var versionInfo = GitVersion(new GitVersionSettings() { OutputType = GitVersionOutput.Json }); -var milestone = string.Concat("v", versionInfo.MajorMinorPatch); +var milestone = versionInfo.MajorMinorPatch; var cakeVersion = typeof(ICakeContext).Assembly.GetName().Version.ToString(); var isLocalBuild = BuildSystem.IsLocalBuild; var isMainBranch = StringComparer.OrdinalIgnoreCase.Equals("master", BuildSystem.AppVeyor.Environment.Repository.Branch); -var isMainRepo = StringComparer.OrdinalIgnoreCase.Equals(gitHubUserName + "/" + gitHubRepo, BuildSystem.AppVeyor.Environment.Repository.Name); +var isMainRepo = StringComparer.OrdinalIgnoreCase.Equals($"{gitHubRepoOwner}/{gitHubRepo}", BuildSystem.AppVeyor.Environment.Repository.Name); var isPullRequest = BuildSystem.AppVeyor.Environment.PullRequest.IsPullRequest; var isTagged = ( BuildSystem.AppVeyor.Environment.Repository.Tag.IsTag && !string.IsNullOrWhiteSpace(BuildSystem.AppVeyor.Environment.Repository.Tag.Name) ); +var isBenchmarkPresent = FileExists(benchmarkProject); /////////////////////////////////////////////////////////////////////////////// @@ -95,11 +106,22 @@ Setup(context => string.IsNullOrEmpty(nuGetApiKey) ? "[NULL]" : new string('*', nuGetApiKey.Length) ); - Information("GitHub Info:\r\n\tRepo: {0}\r\n\tUserName: {1}\r\n\tPassword: {2}", - gitHubRepo, - gitHubUserName, - string.IsNullOrEmpty(gitHubPassword) ? "[NULL]" : new string('*', gitHubPassword.Length) - ); + if (!string.IsNullOrEmpty(gitHubToken)) + { + Information("GitHub Info:\r\n\tRepo: {0}\r\n\tUserName: {1}\r\n\tToken: {2}", + $"{gitHubRepoOwner}/{gitHubRepo}", + gitHubUserName, + new string('*', gitHubToken.Length) + ); + } + else + { + Information("GitHub Info:\r\n\tRepo: {0}\r\n\tUserName: {1}\r\n\tPassword: {2}", + $"{gitHubRepoOwner}/{gitHubRepo}", + gitHubUserName, + string.IsNullOrEmpty(gitHubPassword) ? "[NULL]" : new string('*', gitHubPassword.Length) + ); + } }); Teardown(context => @@ -130,8 +152,8 @@ Task("Clean") { // Clean solution directories. Information("Cleaning {0}", sourceFolder); - CleanDirectories(sourceFolder + "*/bin/" + configuration); - CleanDirectories(sourceFolder + "*/obj/" + configuration); + CleanDirectories($"{sourceFolder}*/bin/{configuration}"); + CleanDirectories($"{sourceFolder}*/obj/{configuration}"); // Clean previous artifacts Information("Cleaning {0}", outputDir); @@ -149,7 +171,6 @@ Task("Restore-NuGet-Packages") DotNetCoreRestore("./Source/", new DotNetCoreRestoreSettings { Sources = new [] { - "https://www.myget.org/F/xunit/api/v3/index.json", "https://api.nuget.org/v3/index.json", } }); @@ -174,6 +195,7 @@ Task("Run-Unit-Tests") DotNetCoreTest(unitTestsProject, new DotNetCoreTestSettings { NoBuild = true, + NoRestore = true, Configuration = configuration }); }); @@ -185,11 +207,12 @@ Task("Run-Code-Coverage") Action testAction = ctx => ctx.DotNetCoreTest(unitTestsProject, new DotNetCoreTestSettings { NoBuild = true, + NoRestore = true, Configuration = configuration }); OpenCover(testAction, - codeCoverageDir + "coverage.xml", + $"{codeCoverageDir}coverage.xml", new OpenCoverSettings { OldStyle = true, @@ -205,7 +228,7 @@ Task("Run-Code-Coverage") Task("Upload-Coverage-Result") .Does(() => { - CoverallsIo(codeCoverageDir + "coverage.xml"); + CoverallsIo($"{codeCoverageDir}coverage.xml"); }); Task("Generate-Code-Coverage-Report") @@ -213,7 +236,7 @@ Task("Generate-Code-Coverage-Report") .Does(() => { ReportGenerator( - codeCoverageDir + "coverage.xml", + $"{codeCoverageDir}coverage.xml", codeCoverageDir, new ReportGeneratorSettings() { ClassFilters = new[] { "*.UnitTests*" } @@ -231,6 +254,7 @@ Task("Create-NuGet-Package") IncludeSource = false, IncludeSymbols = true, NoBuild = true, + NoRestore = true, NoDependencies = true, OutputDirectory = outputDir, ArgumentCustomization = (args) => @@ -251,7 +275,11 @@ Task("Upload-AppVeyor-Artifacts") .WithCriteria(() => AppVeyor.IsRunningOnAppVeyor) .Does(() => { - foreach (var file in GetFiles(outputDir + "*.*")) + foreach (var file in GetFiles($"{outputDir}*.*")) + { + AppVeyor.UploadArtifact(file.FullPath); + } + foreach (var file in GetFiles($"{benchmarkDir}results/*.*")) { AppVeyor.UploadArtifact(file.FullPath); } @@ -302,15 +330,25 @@ Task("Publish-MyGet") Task("Create-Release-Notes") .Does(() => { - if(string.IsNullOrEmpty(gitHubUserName)) throw new InvalidOperationException("Could not resolve GitHub user name."); - if(string.IsNullOrEmpty(gitHubPassword)) throw new InvalidOperationException("Could not resolve GitHub password."); - - GitReleaseManagerCreate(gitHubUserName, gitHubPassword, gitHubUserName, gitHubRepo, new GitReleaseManagerCreateSettings { + var settings = new GitReleaseManagerCreateSettings + { Name = milestone, Milestone = milestone, Prerelease = false, TargetCommitish = "master" - }); + }; + + if (!string.IsNullOrEmpty(gitHubToken)) + { + GitReleaseManagerCreate(gitHubToken, gitHubRepoOwner, gitHubRepo, settings); + } + else + { + if(string.IsNullOrEmpty(gitHubUserName)) throw new InvalidOperationException("Could not resolve GitHub user name."); + if(string.IsNullOrEmpty(gitHubPassword)) throw new InvalidOperationException("Could not resolve GitHub password."); + + GitReleaseManagerCreate(gitHubUserName, gitHubPassword, gitHubRepoOwner, gitHubRepo, settings); + } }); Task("Publish-GitHub-Release") @@ -321,10 +359,56 @@ Task("Publish-GitHub-Release") .WithCriteria(() => isTagged) .Does(() => { - if(string.IsNullOrEmpty(gitHubUserName)) throw new InvalidOperationException("Could not resolve GitHub user name."); - if(string.IsNullOrEmpty(gitHubPassword)) throw new InvalidOperationException("Could not resolve GitHub password."); + var settings = new GitReleaseManagerCreateSettings + { + Name = milestone, + Milestone = milestone, + Prerelease = false, + TargetCommitish = "master" + }; + + if (!string.IsNullOrEmpty(gitHubToken)) + { + GitReleaseManagerClose(gitHubToken, gitHubRepoOwner, gitHubRepo, milestone); + } + else + { + if(string.IsNullOrEmpty(gitHubUserName)) throw new InvalidOperationException("Could not resolve GitHub user name."); + if(string.IsNullOrEmpty(gitHubPassword)) throw new InvalidOperationException("Could not resolve GitHub password."); + + GitReleaseManagerClose(gitHubUserName, gitHubPassword, gitHubRepoOwner, gitHubRepo, milestone); + } +}); + +Task("Generate-Benchmark-Report") + .IsDependentOn("Build") + .WithCriteria(isBenchmarkPresent) + .Does(() => +{ + var publishDirectory = $"{benchmarkDir}Publish/"; + + DotNetCorePublish(benchmarkProject, new DotNetCorePublishSettings + { + Configuration = configuration, + NoRestore = true, + NoBuild = true, + OutputDirectory = publishDirectory + }); + + var assemblyLocation = MakeAbsolute(File($"{publishDirectory}{libraryName}.Benchmark.dll")).FullPath; + var artifactsLocation = MakeAbsolute(File(benchmarkDir)).FullPath; + var benchmarkToolLocation = Context.Tools.Resolve("dotnet-benchmark.exe"); - GitReleaseManagerClose(gitHubUserName, gitHubPassword, gitHubUserName, gitHubRepo, milestone); + var processResult = StartProcess( + benchmarkToolLocation, + new ProcessSettings() + { + Arguments = $"{assemblyLocation} -f * --artifacts={artifactsLocation}" + }); + if (processResult != 0) + { + throw new Exception($"dotnet-benchmark.exe did not complete successfully. Result code: {processResult}"); + } }); @@ -336,7 +420,16 @@ Task("Coverage") .IsDependentOn("Generate-Code-Coverage-Report") .Does(() => { - StartProcess("cmd", "/c start " + codeCoverageDir + "index.htm"); + StartProcess("cmd", $"/c start {codeCoverageDir}index.htm"); +}); + +Task("Benchmark") + .IsDependentOn("Generate-Benchmark-Report") + .WithCriteria(isBenchmarkPresent) + .Does(() => +{ + var htmlReport = GetFiles($"{benchmarkDir}results/*-report.html", new GlobberSettings { IsCaseSensitive = false }).FirstOrDefault(); + StartProcess("cmd", $"/c start {htmlReport}"); }); Task("ReleaseNotes") @@ -345,6 +438,7 @@ Task("ReleaseNotes") Task("AppVeyor") .IsDependentOn("Run-Code-Coverage") .IsDependentOn("Upload-Coverage-Result") + .IsDependentOn("Generate-Benchmark-Report") .IsDependentOn("Create-NuGet-Package") .IsDependentOn("Upload-AppVeyor-Artifacts") .IsDependentOn("Publish-MyGet") diff --git a/build.ps1 b/build.ps1 index 7f1f813d..a336e298 100644 --- a/build.ps1 +++ b/build.ps1 @@ -176,7 +176,7 @@ if(-Not $SkipToolPackageRestore.IsPresent) { ($md5Hash -ne (Get-Content $PACKAGES_CONFIG_MD5 ))) { Write-Verbose -Message "Missing or changed package.config hash..." Get-ChildItem -Exclude packages.config,nuget.exe,Cake.Bakery | - Remove-Item -Recurse + Remove-Item -Recurse -Force } Write-Verbose -Message "Restoring tools from NuGet..." @@ -240,10 +240,10 @@ $CAKE_EXE_INVOCATION = if ($IsLinux -or $IsMacOS) { "`"$CAKE_EXE`"" } - -# Build Cake arguments -$cakeArguments = @("$Script"); -if ($Target) { $cakeArguments += "-target=$Target" } + # Build an array (not a string) of Cake arguments to be joined later +$cakeArguments = @() +if ($Script) { $cakeArguments += "`"$Script`"" } +if ($Target) { $cakeArguments += "-target=`"$Target`"" } if ($Configuration) { $cakeArguments += "-configuration=$Configuration" } if ($Verbosity) { $cakeArguments += "-verbosity=$Verbosity" } if ($ShowDescription) { $cakeArguments += "-showdescription" } diff --git a/tools/packages.config b/tools/packages.config index 997c0e1b..0247344d 100644 --- a/tools/packages.config +++ b/tools/packages.config @@ -1,4 +1,4 @@ - + From bfcbac9660509d814238e8420616f6d87afcdb21 Mon Sep 17 00:00:00 2001 From: Jericho Date: Fri, 1 May 2020 14:40:22 -0400 Subject: [PATCH 14/41] Update client --- .../IIntegrationTest.cs | 11 ++ Source/ZoomNet.IntegrationTests/Program.cs | 153 +++--------------- .../ZoomNet.IntegrationTests/TestsRunner.cs | 129 +++++++++++++++ Source/ZoomNet.IntegrationTests/Utils.cs | 43 +++++ .../ZoomNet.IntegrationTests.csproj | 14 +- Source/ZoomNet/Client.cs | 54 +++---- Source/ZoomNet/Properties/AssemblyInfo.cs | 2 - Source/ZoomNet/Utilities/DiagnosticHandler.cs | 39 ++--- Source/ZoomNet/Utilities/ZoomClientOptions.cs | 2 +- Source/ZoomNet/ZoomNet.csproj | 12 +- 10 files changed, 255 insertions(+), 204 deletions(-) create mode 100644 Source/ZoomNet.IntegrationTests/IIntegrationTest.cs create mode 100644 Source/ZoomNet.IntegrationTests/TestsRunner.cs create mode 100644 Source/ZoomNet.IntegrationTests/Utils.cs diff --git a/Source/ZoomNet.IntegrationTests/IIntegrationTest.cs b/Source/ZoomNet.IntegrationTests/IIntegrationTest.cs new file mode 100644 index 00000000..7e956ef0 --- /dev/null +++ b/Source/ZoomNet.IntegrationTests/IIntegrationTest.cs @@ -0,0 +1,11 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace ZoomNet.IntegrationTests +{ + public interface IIntegrationTest + { + Task RunAsync(IClient client, TextWriter log, CancellationToken cancellationToken); + } +} diff --git a/Source/ZoomNet.IntegrationTests/Program.cs b/Source/ZoomNet.IntegrationTests/Program.cs index 2ec91c57..c881343d 100644 --- a/Source/ZoomNet.IntegrationTests/Program.cs +++ b/Source/ZoomNet.IntegrationTests/Program.cs @@ -1,42 +1,33 @@ using Logzio.DotNet.NLog; -using NLog; +using Microsoft.Extensions.DependencyInjection; using NLog.Config; +using NLog.Extensions.Logging; using NLog.Targets; using System; -using System.IO; -using System.Linq; -using System.Net; -using System.Threading; using System.Threading.Tasks; -using ZoomNet.Utilities; namespace ZoomNet.IntegrationTests { public class Program { - private const int MAX_ZOOM_API_CONCURRENCY = 5; - - private enum ResultCodes + public static async Task Main(string[] args) { - Success = 0, - Exception = 1, - Cancelled = 1223 + var services = new ServiceCollection(); + ConfigureServices(services); + await using var serviceProvider = services.BuildServiceProvider(); + var app = serviceProvider.GetService(); + return await app.RunAsync().ConfigureAwait(false); } - static async Task Main() + private static void ConfigureServices(ServiceCollection services) { - // ----------------------------------------------------------------------------- - // Do you want to proxy requests through Fiddler? Can be useful for debugging. - var useFiddler = false; - - // Logging options. - var options = new ZoomClientOptions() - { - LogLevelFailedCalls = Logging.LogLevel.Error, - LogLevelSuccessfulCalls = Logging.LogLevel.Debug - }; - // ----------------------------------------------------------------------------- + services + .AddLogging(loggingBuilder => loggingBuilder.AddNLog(GetNLogConfiguration())) + .AddTransient(); + } + private static LoggingConfiguration GetNLogConfiguration() + { // Configure logging var nLogConfig = new LoggingConfiguration(); @@ -45,8 +36,8 @@ static async Task Main() if (!string.IsNullOrEmpty(logzioToken)) { var logzioTarget = new LogzioTarget { Token = logzioToken }; - logzioTarget.ContextProperties.Add(new TargetPropertyWithContext("source", "StrongGrid_integration_tests")); - logzioTarget.ContextProperties.Add(new TargetPropertyWithContext("StrongGrid-Version", Client.Version)); + logzioTarget.ContextProperties.Add(new TargetPropertyWithContext("source", "ZoomNet_integration_tests")); + logzioTarget.ContextProperties.Add(new TargetPropertyWithContext("ZoomNet-Version", ZoomNet.Client.Version)); nLogConfig.AddTarget("Logzio", logzioTarget); nLogConfig.AddRule(NLog.LogLevel.Debug, NLog.LogLevel.Fatal, "Logzio", "*"); @@ -57,115 +48,7 @@ static async Task Main() nLogConfig.AddTarget("ColoredConsole", consoleTarget); nLogConfig.AddRule(NLog.LogLevel.Warn, NLog.LogLevel.Fatal, "ColoredConsole", "*"); - LogManager.Configuration = nLogConfig; - - // Configure ZoomNet client - var apiKey = Environment.GetEnvironmentVariable("ZOOM_APIKEY"); - var apiSecret = Environment.GetEnvironmentVariable("ZOOM_APISECRET"); - var userId = Environment.GetEnvironmentVariable("ZOOM_USERID"); - var proxy = useFiddler ? new WebProxy("http://localhost:8888") : null; - var client = new Client(apiKey, apiSecret, proxy, options); - - // Configure Console - var source = new CancellationTokenSource(); - Console.CancelKeyPress += (s, e) => - { - e.Cancel = true; - source.Cancel(); - }; - - // Ensure the Console is tall enough and centered on the screen - Console.WindowHeight = Math.Min(60, Console.LargestWindowHeight); - ConsoleUtils.CenterConsole(); - - // These are the integration tests that we will execute - var integrationTests = new Func[] - { - }; - - // Execute the async tests in parallel (with max degree of parallelism) - var results = await integrationTests.ForEachAsync( - async integrationTest => - { - var log = new StringWriter(); - - try - { - await integrationTest(userId, client, log, source.Token).ConfigureAwait(false); - return (TestName: integrationTest.Method.Name, ResultCode: ResultCodes.Success, Message: string.Empty); - } - catch (OperationCanceledException) - { - await log.WriteLineAsync($"-----> TASK CANCELLED").ConfigureAwait(false); - return (TestName: integrationTest.Method.Name, ResultCode: ResultCodes.Cancelled, Message: "Task cancelled"); - } - catch (Exception e) - { - var exceptionMessage = e.GetBaseException().Message; - await log.WriteLineAsync($"-----> AN EXCEPTION OCCURRED: {exceptionMessage}").ConfigureAwait(false); - return (TestName: integrationTest.Method.Name, ResultCode: ResultCodes.Exception, Message: exceptionMessage); - } - finally - { - await Console.Out.WriteLineAsync(log.ToString()).ConfigureAwait(false); - } - }, MAX_ZOOM_API_CONCURRENCY) - .ConfigureAwait(false); - - // Display summary - var summary = new StringWriter(); - await summary.WriteLineAsync("\n\n**************************************************").ConfigureAwait(false); - await summary.WriteLineAsync("******************** SUMMARY *********************").ConfigureAwait(false); - await summary.WriteLineAsync("**************************************************").ConfigureAwait(false); - - var resultsWithMessage = results - .Where(r => !string.IsNullOrEmpty(r.Message)) - .ToArray(); - - if (resultsWithMessage.Any()) - { - foreach (var (TestName, ResultCode, Message) in resultsWithMessage) - { - const int TEST_NAME_MAX_LENGTH = 25; - var name = TestName.Length <= TEST_NAME_MAX_LENGTH ? TestName : TestName.Substring(0, TEST_NAME_MAX_LENGTH - 3) + "..."; - await summary.WriteLineAsync($"{name.PadRight(TEST_NAME_MAX_LENGTH, ' ')} : {Message}").ConfigureAwait(false); - } - } - else - { - await summary.WriteLineAsync("All tests completed succesfully").ConfigureAwait(false); - } - - await summary.WriteLineAsync("**************************************************").ConfigureAwait(false); - await Console.Out.WriteLineAsync(summary.ToString()).ConfigureAwait(false); - - // Prompt user to press a key in order to allow reading the log in the console - var promptLog = new StringWriter(); - await promptLog.WriteLineAsync("\n\n**************************************************").ConfigureAwait(false); - await promptLog.WriteLineAsync("Press any key to exit").ConfigureAwait(false); - Prompt(promptLog.ToString()); - - // Return code indicating success/failure - var resultCode = (int)ResultCodes.Success; - if (results.Any(result => result.ResultCode != ResultCodes.Success)) - { - if (results.Any(result => result.ResultCode == ResultCodes.Exception)) resultCode = (int)ResultCodes.Exception; - else if (results.Any(result => result.ResultCode == ResultCodes.Cancelled)) resultCode = (int)ResultCodes.Cancelled; - else resultCode = (int)results.First(result => result.ResultCode != ResultCodes.Success).ResultCode; - } - - return await Task.FromResult(resultCode); - } - - private static char Prompt(string prompt) - { - while (Console.KeyAvailable) - { - Console.ReadKey(false); - } - Console.Out.WriteLine(prompt); - var result = Console.ReadKey(); - return result.KeyChar; + return nLogConfig; } } } diff --git a/Source/ZoomNet.IntegrationTests/TestsRunner.cs b/Source/ZoomNet.IntegrationTests/TestsRunner.cs new file mode 100644 index 00000000..7fffdb16 --- /dev/null +++ b/Source/ZoomNet.IntegrationTests/TestsRunner.cs @@ -0,0 +1,129 @@ +using Microsoft.Extensions.Logging; +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using ZoomNet.Utilities; + +namespace ZoomNet.IntegrationTests +{ + internal class TestsRunner + { + private const int MAX_SENDGRID_API_CONCURRENCY = 5; + private const int TEST_NAME_MAX_LENGTH = 25; + private const string SUCCESSFUL_TEST_MESSAGE = "Completed successfully"; + + private enum ResultCodes + { + Success = 0, + Exception = 1, + Cancelled = 1223 + } + + private readonly ILoggerFactory _loggerFactory; + + public TestsRunner(ILoggerFactory loggerFactory) + { + _loggerFactory = loggerFactory; + } + + public async Task RunAsync() + { + // ----------------------------------------------------------------------------- + // Do you want to proxy requests through Fiddler? Can be useful for debugging. + var useFiddler = true; + // ----------------------------------------------------------------------------- + + // Configure ZoomNet client + var apiKey = Environment.GetEnvironmentVariable("ZOOM_APIKEY"); + var apiSecret = Environment.GetEnvironmentVariable("ZOOM_APISECRET"); + var userId = Environment.GetEnvironmentVariable("ZOOM_USERID"); + var proxy = useFiddler ? new WebProxy("http://localhost:8888") : null; + var client = new Client(apiKey, apiSecret, proxy); + + // Configure Console + var source = new CancellationTokenSource(); + Console.CancelKeyPress += (s, e) => + { + e.Cancel = true; + source.Cancel(); + }; + + // Ensure the Console is tall enough and centered on the screen + Console.WindowHeight = Math.Min(60, Console.LargestWindowHeight); + Utils.CenterConsole(); + + // These are the integration tests that we will execute + var integrationTests = new Type[] + { + }; + + // Execute the async tests in parallel (with max degree of parallelism) + var results = await integrationTests.ForEachAsync( + async testType => + { + var log = new StringWriter(); + + try + { + var integrationTest = (IIntegrationTest)Activator.CreateInstance(testType); + await integrationTest.RunAsync(client, log, source.Token).ConfigureAwait(false); + return (TestName: testType.Name, ResultCode: ResultCodes.Success, Message: SUCCESSFUL_TEST_MESSAGE); + } + catch (OperationCanceledException) + { + await log.WriteLineAsync($"-----> TASK CANCELLED").ConfigureAwait(false); + return (TestName: testType.Name, ResultCode: ResultCodes.Cancelled, Message: "Task cancelled"); + } + catch (Exception e) + { + var exceptionMessage = e.GetBaseException().Message; + await log.WriteLineAsync($"-----> AN EXCEPTION OCCURRED: {exceptionMessage}").ConfigureAwait(false); + return (TestName: testType.Name, ResultCode: ResultCodes.Exception, Message: exceptionMessage); + } + finally + { + lock (Console.Out) + { + Console.Out.WriteLine(log.ToString()); + } + } + }, MAX_SENDGRID_API_CONCURRENCY) + .ConfigureAwait(false); + + // Display summary + var summary = new StringWriter(); + await summary.WriteLineAsync("\n\n**************************************************").ConfigureAwait(false); + await summary.WriteLineAsync("******************** SUMMARY *********************").ConfigureAwait(false); + await summary.WriteLineAsync("**************************************************").ConfigureAwait(false); + + foreach (var (TestName, ResultCode, Message) in results.OrderBy(r => r.TestName).ToArray()) + { + var name = TestName.Length <= TEST_NAME_MAX_LENGTH ? TestName : TestName.Substring(0, TEST_NAME_MAX_LENGTH - 3) + "..."; + await summary.WriteLineAsync($"{name.PadRight(TEST_NAME_MAX_LENGTH, ' ')} : {Message}").ConfigureAwait(false); + } + + await summary.WriteLineAsync("**************************************************").ConfigureAwait(false); + await Console.Out.WriteLineAsync(summary.ToString()).ConfigureAwait(false); + + // Prompt user to press a key in order to allow reading the log in the console + var promptLog = new StringWriter(); + await promptLog.WriteLineAsync("\n\n**************************************************").ConfigureAwait(false); + await promptLog.WriteLineAsync("Press any key to exit").ConfigureAwait(false); + Utils.Prompt(promptLog.ToString()); + + // Return code indicating success/failure + var resultCode = (int)ResultCodes.Success; + if (results.Any(result => result.ResultCode != ResultCodes.Success)) + { + if (results.Any(result => result.ResultCode == ResultCodes.Exception)) resultCode = (int)ResultCodes.Exception; + else if (results.Any(result => result.ResultCode == ResultCodes.Cancelled)) resultCode = (int)ResultCodes.Cancelled; + else resultCode = (int)results.First(result => result.ResultCode != ResultCodes.Success).ResultCode; + } + + return await Task.FromResult(resultCode); + } + } +} diff --git a/Source/ZoomNet.IntegrationTests/Utils.cs b/Source/ZoomNet.IntegrationTests/Utils.cs new file mode 100644 index 00000000..d3eee156 --- /dev/null +++ b/Source/ZoomNet.IntegrationTests/Utils.cs @@ -0,0 +1,43 @@ +using System; + +namespace ZoomNet.IntegrationTests +{ + public static class Utils + { + public static void CenterConsole() + { + var hWin = NativeMethods.GetConsoleWindow(); + if (hWin == IntPtr.Zero) return; + + var monitor = NativeMethods.MonitorFromWindow(hWin, NativeMethods.MONITOR_DEFAULT_TO_NEAREST); + if (monitor == IntPtr.Zero) return; + + var monitorInfo = new NativeMethods.NativeMonitorInfo(); + NativeMethods.GetMonitorInfo(monitor, monitorInfo); + + NativeMethods.GetWindowRect(hWin, out NativeMethods.NativeRectangle consoleInfo); + + var monitorWidth = monitorInfo.Monitor.Right - monitorInfo.Monitor.Left; + var monitorHeight = monitorInfo.Monitor.Bottom - monitorInfo.Monitor.Top; + + var consoleWidth = consoleInfo.Right - consoleInfo.Left; + var consoleHeight = consoleInfo.Bottom - consoleInfo.Top; + + var left = monitorInfo.Monitor.Left + ((monitorWidth - consoleWidth) / 2); + var top = monitorInfo.Monitor.Top + ((monitorHeight - consoleHeight) / 2); + + NativeMethods.MoveWindow(hWin, left, top, consoleWidth, consoleHeight, false); + } + + public static char Prompt(string prompt) + { + while (Console.KeyAvailable) + { + Console.ReadKey(false); + } + Console.Out.WriteLine(prompt); + var result = Console.ReadKey(); + return result.KeyChar; + } + } +} diff --git a/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj b/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj index 2e38804e..bc39a4e3 100644 --- a/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj +++ b/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj @@ -1,8 +1,8 @@ - + Exe - netcoreapp2.1 + netcoreapp3.1 ZoomNet.IntegrationTests ZoomNet.IntegrationTests @@ -12,13 +12,17 @@ - - - + + + + + + + diff --git a/Source/ZoomNet/Client.cs b/Source/ZoomNet/Client.cs index 3a7bb21a..64030142 100644 --- a/Source/ZoomNet/Client.cs +++ b/Source/ZoomNet/Client.cs @@ -1,10 +1,11 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Pathoschild.Http.Client; using Pathoschild.Http.Client.Extensibility; using System; using System.Net; using System.Net.Http; using System.Reflection; -using ZoomNet.Logging; using ZoomNet.Utilities; namespace ZoomNet @@ -18,8 +19,11 @@ public class Client : IClient, IDisposable private const string ZOOM_V2_BASE_URI = "https://api.zoom.us/v2"; + private static string _version; + private readonly bool _mustDisposeHttpClient; private readonly ZoomClientOptions _options; + private readonly ILogger _logger; private HttpClient _httpClient; private Pathoschild.Http.Client.IClient _fluentClient; @@ -34,7 +38,21 @@ public class Client : IClient, IDisposable /// /// The version. /// - public static string Version { get; private set; } + public static string Version + { + get + { + if (string.IsNullOrEmpty(_version)) + { + _version = typeof(Client).GetTypeInfo().Assembly.GetName().Version.ToString(3); +#if DEBUG + _version = "DEBUG"; +#endif + } + + return _version; + } + } /// /// Gets the user agent. @@ -93,18 +111,6 @@ public class Client : IClient, IDisposable #region CTOR - /// - /// Initializes static members of the class. - /// - static Client() - { - Version = typeof(Client).GetTypeInfo().Assembly.GetName().Version.ToString(3); -#if DEBUG - Version = "DEBUG"; -#endif - UserAgent = $"ZoomNet/{Version} (+https://github.com/Jericho/ZoomNet)"; - } - /// /// Initializes a new instance of the class. /// @@ -152,20 +158,14 @@ public Client(string apiKey, string apiSecret, HttpClient httpClient, ZoomClient { } - private Client(string apiKey, string apiSecret, HttpClient httpClient, bool disposeClient, ZoomClientOptions options) + private Client(string apiKey, string apiSecret, HttpClient httpClient, bool disposeClient, ZoomClientOptions options, ILogger logger = null) { _mustDisposeHttpClient = disposeClient; _httpClient = httpClient; _options = options ?? GetDefaultOptions(); - - Version = typeof(Client).GetTypeInfo().Assembly.GetName().Version.ToString(3); -#if DEBUG - Version = "DEBUG"; -#endif - + _logger = logger ?? NullLogger.Instance; _fluentClient = new FluentClient(new Uri(ZOOM_V2_BASE_URI), httpClient) - .SetUserAgent(Client.UserAgent); - // .SetRequestCoordinator(new ZoomRetryStrategy()); + .SetUserAgent($"ZoomNet/{Version} (+https://github.com/Jericho/ZoomNet)"); _fluentClient.Filters.Remove(); @@ -175,12 +175,6 @@ private Client(string apiKey, string apiSecret, HttpClient httpClient, bool disp _fluentClient.Filters.Add(new DiagnosticHandler(_options.LogLevelSuccessfulCalls, _options.LogLevelFailedCalls)); _fluentClient.Filters.Add(new ZoomErrorHandler()); - _fluentClient.SetOptions(new FluentClientOptions() - { - IgnoreHttpErrors = false, - IgnoreNullArguments = true - }); - //Accounts = new Accounts(_fluentClient); //BillingInformation = new BillingInformation(_fluentClient); //Users = new Users(_fluentClient); @@ -264,7 +258,7 @@ private ZoomClientOptions GetDefaultOptions() return new ZoomClientOptions() { LogLevelSuccessfulCalls = LogLevel.Debug, - LogLevelFailedCalls = LogLevel.Debug + LogLevelFailedCalls = LogLevel.Error }; } diff --git a/Source/ZoomNet/Properties/AssemblyInfo.cs b/Source/ZoomNet/Properties/AssemblyInfo.cs index 50fdc1c8..4938fd13 100644 --- a/Source/ZoomNet/Properties/AssemblyInfo.cs +++ b/Source/ZoomNet/Properties/AssemblyInfo.cs @@ -1,8 +1,6 @@ -using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -[assembly: CLSCompliant(true)] [assembly: InternalsVisibleTo("ZoomNet.UnitTests")] [assembly: InternalsVisibleTo("ZoomNet.IntegrationTests")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/Source/ZoomNet/Utilities/DiagnosticHandler.cs b/Source/ZoomNet/Utilities/DiagnosticHandler.cs index 2fe4b585..ca1c17f6 100644 --- a/Source/ZoomNet/Utilities/DiagnosticHandler.cs +++ b/Source/ZoomNet/Utilities/DiagnosticHandler.cs @@ -1,3 +1,5 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Pathoschild.Http.Client; using Pathoschild.Http.Client.Extensibility; using System; @@ -7,7 +9,6 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Text; -using ZoomNet.Logging; namespace ZoomNet.Utilities { @@ -20,7 +21,7 @@ internal class DiagnosticHandler : IHttpFilter #region FIELDS internal const string DIAGNOSTIC_ID_HEADER_NAME = "ZoomNet-Diagnostic-Id"; - private static readonly ILog _logger = LogProvider.For(); + private readonly ILogger _logger; private readonly LogLevel _logLevelSuccessfulCalls; private readonly LogLevel _logLevelFailedCalls; @@ -34,10 +35,11 @@ internal class DiagnosticHandler : IHttpFilter #region CTOR - public DiagnosticHandler(LogLevel logLevelSuccessfulCalls, LogLevel logLevelFailedCalls) + public DiagnosticHandler(LogLevel logLevelSuccessfulCalls, LogLevel logLevelFailedCalls, ILogger logger = null) { _logLevelSuccessfulCalls = logLevelSuccessfulCalls; _logLevelFailedCalls = logLevelFailedCalls; + _logger = logger ?? NullLogger.Instance; } #endregion @@ -61,7 +63,7 @@ public void OnRequest(IRequest request) LogHeaders(diagnostic, httpRequest.Headers); LogContent(diagnostic, httpRequest.Content); - // Add the diagnotic info to our cache + // Add the diagnostic info to our cache DiagnosticsInfo.TryAdd(diagnosticId, (new WeakReference(request.Message), diagnostic.ToString(), Stopwatch.GetTimestamp(), long.MinValue)); } @@ -99,17 +101,20 @@ public void OnResponse(IResponse response, bool httpErrorAsException) Debug.WriteLine("{0}\r\nAN EXCEPTION OCCURRED: {1}\r\n{0}", new string('=', 50), e.GetBaseException().Message); updatedDiagnostic.AppendLine($"AN EXCEPTION OCCURRED: {e.GetBaseException().Message}"); - if (_logger != null && _logger.IsErrorEnabled()) + if (_logger.IsEnabled(LogLevel.Error)) { - _logger.Error(e, "An exception occurred when inspecting the response from SendGrid"); + _logger.LogError(e, "An exception occurred when inspecting the response from Zoom"); } } finally { - var diagnosticMessage = updatedDiagnostic.ToString(); - - LogDiagnostic(response.IsSuccessStatusCode, _logLevelSuccessfulCalls, diagnosticMessage); - LogDiagnostic(!response.IsSuccessStatusCode, _logLevelFailedCalls, diagnosticMessage); + var logLevel = response.IsSuccessStatusCode ? _logLevelSuccessfulCalls : _logLevelFailedCalls; + if (_logger.IsEnabled(logLevel)) + { + _logger.Log(logLevel, updatedDiagnostic.ToString() + .Replace("{", "{{") + .Replace("}", "}}")); + } DiagnosticsInfo.TryUpdate( diagnosticId, @@ -167,20 +172,6 @@ private void LogContent(StringBuilder diagnostic, HttpContent httpContent) } } - private void LogDiagnostic(bool shouldLog, LogLevel logLEvel, string diagnosticMessage) - { - if (shouldLog && _logger != null) - { - var logLevelEnabled = _logger.Log(logLEvel, null, null, Array.Empty()); - if (logLevelEnabled) - { - _logger.Log(logLEvel, () => diagnosticMessage - .Replace("{", "{{") - .Replace("}", "}}")); - } - } - } - private void Cleanup() { try diff --git a/Source/ZoomNet/Utilities/ZoomClientOptions.cs b/Source/ZoomNet/Utilities/ZoomClientOptions.cs index beab2e05..8fbcc770 100644 --- a/Source/ZoomNet/Utilities/ZoomClientOptions.cs +++ b/Source/ZoomNet/Utilities/ZoomClientOptions.cs @@ -1,4 +1,4 @@ -using ZoomNet.Logging; +using Microsoft.Extensions.Logging; namespace ZoomNet.Utilities { diff --git a/Source/ZoomNet/ZoomNet.csproj b/Source/ZoomNet/ZoomNet.csproj index e1430ef4..b50c54c1 100644 --- a/Source/ZoomNet/ZoomNet.csproj +++ b/Source/ZoomNet/ZoomNet.csproj @@ -1,4 +1,4 @@ - + net461;net472;netstandard2.0 @@ -33,12 +33,10 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + - + @@ -50,7 +48,7 @@ - + From c8c2cf2192857ec85e5f81def5b149f98a3bcaac Mon Sep 17 00:00:00 2001 From: Jericho Date: Fri, 1 May 2020 15:05:35 -0400 Subject: [PATCH 15/41] Fix copy/paste mistake --- Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj | 6 +++--- Source/ZoomNet/Utilities/ExcludeFromCodeCoverage.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj b/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj index bb24e382..7ab79231 100644 --- a/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj +++ b/Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj @@ -1,9 +1,9 @@ - + net461;net472;netcoreapp2.0 - StrongGrid.UnitTests - StrongGrid.UnitTests + ZoomNet.UnitTests + ZoomNet.UnitTests false diff --git a/Source/ZoomNet/Utilities/ExcludeFromCodeCoverage.cs b/Source/ZoomNet/Utilities/ExcludeFromCodeCoverage.cs index 5eb4c233..0e673682 100644 --- a/Source/ZoomNet/Utilities/ExcludeFromCodeCoverage.cs +++ b/Source/ZoomNet/Utilities/ExcludeFromCodeCoverage.cs @@ -1,8 +1,8 @@ using System; -namespace StrongGrid.Utilities +namespace ZoomNet.Utilities { - [StrongGrid.Utilities.ExcludeFromCodeCoverage] + [ZoomNet.Utilities.ExcludeFromCodeCoverage] internal class ExcludeFromCodeCoverage : Attribute { } From 9424887f4f1b0267269c865ed6414679139ef29b Mon Sep 17 00:00:00 2001 From: Jericho Date: Fri, 1 May 2020 15:08:39 -0400 Subject: [PATCH 16/41] UserId must be passed to integration tests --- Source/ZoomNet.IntegrationTests/IIntegrationTest.cs | 2 +- Source/ZoomNet.IntegrationTests/TestsRunner.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/ZoomNet.IntegrationTests/IIntegrationTest.cs b/Source/ZoomNet.IntegrationTests/IIntegrationTest.cs index 7e956ef0..3ba21d53 100644 --- a/Source/ZoomNet.IntegrationTests/IIntegrationTest.cs +++ b/Source/ZoomNet.IntegrationTests/IIntegrationTest.cs @@ -6,6 +6,6 @@ namespace ZoomNet.IntegrationTests { public interface IIntegrationTest { - Task RunAsync(IClient client, TextWriter log, CancellationToken cancellationToken); + Task RunAsync(string userId, IClient client, TextWriter log, CancellationToken cancellationToken); } } diff --git a/Source/ZoomNet.IntegrationTests/TestsRunner.cs b/Source/ZoomNet.IntegrationTests/TestsRunner.cs index 7fffdb16..92288b7c 100644 --- a/Source/ZoomNet.IntegrationTests/TestsRunner.cs +++ b/Source/ZoomNet.IntegrationTests/TestsRunner.cs @@ -69,7 +69,7 @@ public async Task RunAsync() try { var integrationTest = (IIntegrationTest)Activator.CreateInstance(testType); - await integrationTest.RunAsync(client, log, source.Token).ConfigureAwait(false); + await integrationTest.RunAsync(userId, client, log, source.Token).ConfigureAwait(false); return (TestName: testType.Name, ResultCode: ResultCodes.Success, Message: SUCCESSFUL_TEST_MESSAGE); } catch (OperationCanceledException) From 789557c13adb16db742d24b0180db47eb417a446 Mon Sep 17 00:00:00 2001 From: Jericho Date: Fri, 1 May 2020 15:12:54 -0400 Subject: [PATCH 17/41] Fix another copy/paste mistake --- Source/ZoomNet.IntegrationTests/TestsRunner.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/ZoomNet.IntegrationTests/TestsRunner.cs b/Source/ZoomNet.IntegrationTests/TestsRunner.cs index 92288b7c..1f4c2f06 100644 --- a/Source/ZoomNet.IntegrationTests/TestsRunner.cs +++ b/Source/ZoomNet.IntegrationTests/TestsRunner.cs @@ -11,7 +11,7 @@ namespace ZoomNet.IntegrationTests { internal class TestsRunner { - private const int MAX_SENDGRID_API_CONCURRENCY = 5; + private const int MAX_ZOOM_API_CONCURRENCY = 5; private const int TEST_NAME_MAX_LENGTH = 25; private const string SUCCESSFUL_TEST_MESSAGE = "Completed successfully"; @@ -90,7 +90,7 @@ public async Task RunAsync() Console.Out.WriteLine(log.ToString()); } } - }, MAX_SENDGRID_API_CONCURRENCY) + }, MAX_ZOOM_API_CONCURRENCY) .ConfigureAwait(false); // Display summary From 2be15de628ccf4c87f34f209e16a3626a9ce66dc Mon Sep 17 00:00:00 2001 From: Jericho Date: Fri, 1 May 2020 16:18:26 -0400 Subject: [PATCH 18/41] Retry strategy to handle HTTP429 --- Source/ZoomNet/Client.cs | 3 +- Source/ZoomNet/Utilities/ISystemClock.cs | 28 +++++ Source/ZoomNet/Utilities/SystemClock.cs | 53 ++++++++ Source/ZoomNet/Utilities/ZoomRetryStrategy.cs | 118 ++++++++++++++++++ 4 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 Source/ZoomNet/Utilities/ISystemClock.cs create mode 100644 Source/ZoomNet/Utilities/SystemClock.cs create mode 100644 Source/ZoomNet/Utilities/ZoomRetryStrategy.cs diff --git a/Source/ZoomNet/Client.cs b/Source/ZoomNet/Client.cs index 64030142..d6175e40 100644 --- a/Source/ZoomNet/Client.cs +++ b/Source/ZoomNet/Client.cs @@ -165,7 +165,8 @@ private Client(string apiKey, string apiSecret, HttpClient httpClient, bool disp _options = options ?? GetDefaultOptions(); _logger = logger ?? NullLogger.Instance; _fluentClient = new FluentClient(new Uri(ZOOM_V2_BASE_URI), httpClient) - .SetUserAgent($"ZoomNet/{Version} (+https://github.com/Jericho/ZoomNet)"); + .SetUserAgent($"ZoomNet/{Version} (+https://github.com/Jericho/ZoomNet)") + .SetRequestCoordinator(new ZoomRetryStrategy()); _fluentClient.Filters.Remove(); diff --git a/Source/ZoomNet/Utilities/ISystemClock.cs b/Source/ZoomNet/Utilities/ISystemClock.cs new file mode 100644 index 00000000..8f9da881 --- /dev/null +++ b/Source/ZoomNet/Utilities/ISystemClock.cs @@ -0,0 +1,28 @@ +using System; + +namespace ZoomNet.Utilities +{ + /// + /// Interface for the SystemClock. + /// + internal interface ISystemClock + { + /// + /// Gets a object that is set to the current date and time on this + /// computer, expressed as the local time. + /// + /// + /// The current date and time, expressed as the local time. + /// + DateTime Now { get; } + + /// + /// Gets a System.DateTime object that is set to the current date and time on this + /// computer, expressed as the Coordinated Universal Time (UTC). + /// + /// + /// The current date and time, expressed as the Coordinated Universal Time (UTC). + /// + DateTime UtcNow { get; } + } +} diff --git a/Source/ZoomNet/Utilities/SystemClock.cs b/Source/ZoomNet/Utilities/SystemClock.cs new file mode 100644 index 00000000..060c952a --- /dev/null +++ b/Source/ZoomNet/Utilities/SystemClock.cs @@ -0,0 +1,53 @@ +using System; + +namespace ZoomNet.Utilities +{ + /// + /// A replacement for .Net and . + /// + /// + internal class SystemClock : ISystemClock + { + #region FIELDS + + private static readonly Lazy _instance = new Lazy(() => new SystemClock(), true); + + #endregion + + #region PROPERTIES + + /// + /// Gets the instance. + /// + /// + /// The instance. + /// + public static ISystemClock Instance => _instance.Value; + + /// + /// Gets a object that is set to the current date and time on this + /// computer, expressed as the local time. + /// + /// + /// The current date and time, expressed as the local time. + /// + public DateTime Now => DateTime.Now; + + /// + /// Gets a System.DateTime object that is set to the current date and time on this + /// computer, expressed as the Coordinated Universal Time (UTC). + /// + /// + /// The current date and time, expressed as the Coordinated Universal Time (UTC). + /// + public DateTime UtcNow => DateTime.UtcNow; + + #endregion + + #region CONSTRUCTOR + + private SystemClock() { } + + #endregion + } +} diff --git a/Source/ZoomNet/Utilities/ZoomRetryStrategy.cs b/Source/ZoomNet/Utilities/ZoomRetryStrategy.cs new file mode 100644 index 00000000..f543f523 --- /dev/null +++ b/Source/ZoomNet/Utilities/ZoomRetryStrategy.cs @@ -0,0 +1,118 @@ +using Pathoschild.Http.Client.Retry; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; + +namespace ZoomNet.Utilities +{ + /// + /// Implements IRetryConfig with back off based on a wait time derived from the + /// "Retry-After" response header. The value in this header contains the date + /// and time when the next attempt can take place. + /// + /// + internal class ZoomRetryStrategy : IRetryConfig + { + #region FIELDS + + private const int DEFAULT_MAX_RETRIES = 4; + private const HttpStatusCode TOO_MANY_REQUESTS = (HttpStatusCode)429; + private static readonly TimeSpan DEFAULT_DELAY = TimeSpan.FromSeconds(1); + + private readonly ISystemClock _systemClock; + + #endregion + + #region PROPERTIES + + /// Gets the maximum number of times to retry a request before failing. + public int MaxRetries { get; } + + #endregion + + #region CTOR + + /// + /// Initializes a new instance of the class. + /// + public ZoomRetryStrategy() + : this(DEFAULT_MAX_RETRIES, null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The maximum attempts. + public ZoomRetryStrategy(int maxAttempts) + : this(maxAttempts, null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The maximum attempts. + /// The system clock. This is for unit testing only. + internal ZoomRetryStrategy(int maxAttempts, ISystemClock systemClock = null) + { + MaxRetries = maxAttempts; + _systemClock = systemClock ?? SystemClock.Instance; + } + + #endregion + + #region PUBLIC METHODS + + /// + /// Checks if we should retry an operation. + /// + /// The Http response of the previous request. + /// + /// true if another attempt should be made; otherwise, false. + /// + public bool ShouldRetry(HttpResponseMessage response) + { + return response != null && response.StatusCode == TOO_MANY_REQUESTS; + } + + /// + /// Gets a TimeSpan value which defines how long to wait before trying again after an unsuccessful attempt. + /// + /// The number of attempts carried out so far. That is, after the first attempt (for + /// the first retry), attempt will be set to 1, after the second attempt it is set to 2, and so on. + /// The Http response of the previous request. + /// + /// A TimeSpan value which defines how long to wait before the next attempt. + /// + public TimeSpan GetDelay(int attempt, HttpResponseMessage response) + { + // Default value in case the 'reset' time is missing from HTTP headers + var waitTime = DEFAULT_DELAY; + + // Get the 'retry-after' time from the HTTP headers (if present) + if (response?.Headers != null) + { + if (response.Headers.TryGetValues("Retry-After", out IEnumerable values)) + { + if (DateTime.TryParse(values?.FirstOrDefault(), out DateTime nextRetryDateTime)) + { + waitTime = nextRetryDateTime.Subtract(_systemClock.UtcNow); + } + } + } + + // Make sure the wait time is valid + if (waitTime.TotalMilliseconds < 0) waitTime = DEFAULT_DELAY; + + // Totally arbitrary. Make sure we don't wait more than a 'reasonable' amount of time + if (waitTime.TotalSeconds > 5) waitTime = TimeSpan.FromSeconds(5); + + return waitTime; + } + + #endregion + } +} From 70f60b88dc2fdcdb774785aff113bc6790e6e471 Mon Sep 17 00:00:00 2001 From: Jericho Date: Sun, 3 May 2020 22:01:14 -0400 Subject: [PATCH 19/41] Allow the logger to be injected in the Client constructor --- Source/ZoomNet/Client.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Source/ZoomNet/Client.cs b/Source/ZoomNet/Client.cs index d6175e40..05a053c3 100644 --- a/Source/ZoomNet/Client.cs +++ b/Source/ZoomNet/Client.cs @@ -117,8 +117,8 @@ public static string Version /// Your Zoom API Key. /// Your Zoom API Secret. /// Options for the Zoom client. - public Client(string apiKey, string apiSecret, ZoomClientOptions options = null) - : this(apiKey, apiSecret, null, false, options) + public Client(string apiKey, string apiSecret, ZoomClientOptions options = null, ILogger logger = null) + : this(apiKey, apiSecret, null, false, options, logger) { } @@ -129,8 +129,8 @@ public Client(string apiKey, string apiSecret, ZoomClientOptions options = null) /// Your Zoom API Secret. /// Allows you to specify a proxy. /// Options for the Zoom client. - public Client(string apiKey, string apiSecret, IWebProxy proxy, ZoomClientOptions options = null) - : this(apiKey, apiSecret, new HttpClient(new HttpClientHandler { Proxy = proxy, UseProxy = proxy != null }), true, options) + public Client(string apiKey, string apiSecret, IWebProxy proxy, ZoomClientOptions options = null, ILogger logger = null) + : this(apiKey, apiSecret, new HttpClient(new HttpClientHandler { Proxy = proxy, UseProxy = proxy != null }), true, options, logger) { } @@ -141,8 +141,8 @@ public Client(string apiKey, string apiSecret, IWebProxy proxy, ZoomClientOption /// Your Zoom API Secret. /// TThe HTTP handler stack to use for sending requests. /// Options for the Zoom client. - public Client(string apiKey, string apiSecret, HttpMessageHandler handler, ZoomClientOptions options = null) - : this(apiKey, apiSecret, new HttpClient(handler), true, options) + public Client(string apiKey, string apiSecret, HttpMessageHandler handler, ZoomClientOptions options = null, ILogger logger = null) + : this(apiKey, apiSecret, new HttpClient(handler), true, options, logger) { } @@ -153,8 +153,8 @@ public Client(string apiKey, string apiSecret, HttpMessageHandler handler, ZoomC /// Your Zoom API Secret. /// Allows you to inject your own HttpClient. This is useful, for example, to setup the HtppClient with a proxy. /// Options for the Zoom client. - public Client(string apiKey, string apiSecret, HttpClient httpClient, ZoomClientOptions options = null) - : this(apiKey, apiSecret, httpClient, false, options) + public Client(string apiKey, string apiSecret, HttpClient httpClient, ZoomClientOptions options = null, ILogger logger = null) + : this(apiKey, apiSecret, httpClient, false, options, logger) { } From 9eeae2fd784528a527ce4b87df3453c4bb292bcd Mon Sep 17 00:00:00 2001 From: Jericho Date: Sun, 3 May 2020 22:04:41 -0400 Subject: [PATCH 20/41] Inject logger into Client during integration testing --- Source/ZoomNet.IntegrationTests/TestsRunner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/ZoomNet.IntegrationTests/TestsRunner.cs b/Source/ZoomNet.IntegrationTests/TestsRunner.cs index 1f4c2f06..4b4c83ef 100644 --- a/Source/ZoomNet.IntegrationTests/TestsRunner.cs +++ b/Source/ZoomNet.IntegrationTests/TestsRunner.cs @@ -41,7 +41,7 @@ public async Task RunAsync() var apiSecret = Environment.GetEnvironmentVariable("ZOOM_APISECRET"); var userId = Environment.GetEnvironmentVariable("ZOOM_USERID"); var proxy = useFiddler ? new WebProxy("http://localhost:8888") : null; - var client = new Client(apiKey, apiSecret, proxy); + var client = new Client(apiKey, apiSecret, proxy, null, _loggerFactory.CreateLogger()); // Configure Console var source = new CancellationTokenSource(); From 59ff6b56273f81e75edd782b2d061f62e2f8a4ea Mon Sep 17 00:00:00 2001 From: Jericho Date: Mon, 4 May 2020 21:44:36 -0400 Subject: [PATCH 21/41] Add XML comment for the logger constructor parameter --- Source/ZoomNet/Client.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Source/ZoomNet/Client.cs b/Source/ZoomNet/Client.cs index 05a053c3..3a3abaec 100644 --- a/Source/ZoomNet/Client.cs +++ b/Source/ZoomNet/Client.cs @@ -117,6 +117,7 @@ public static string Version /// Your Zoom API Key. /// Your Zoom API Secret. /// Options for the Zoom client. + /// Logger. public Client(string apiKey, string apiSecret, ZoomClientOptions options = null, ILogger logger = null) : this(apiKey, apiSecret, null, false, options, logger) { @@ -129,6 +130,7 @@ public Client(string apiKey, string apiSecret, ZoomClientOptions options = null, /// Your Zoom API Secret. /// Allows you to specify a proxy. /// Options for the Zoom client. + /// Logger. public Client(string apiKey, string apiSecret, IWebProxy proxy, ZoomClientOptions options = null, ILogger logger = null) : this(apiKey, apiSecret, new HttpClient(new HttpClientHandler { Proxy = proxy, UseProxy = proxy != null }), true, options, logger) { @@ -141,6 +143,7 @@ public Client(string apiKey, string apiSecret, IWebProxy proxy, ZoomClientOption /// Your Zoom API Secret. /// TThe HTTP handler stack to use for sending requests. /// Options for the Zoom client. + /// Logger. public Client(string apiKey, string apiSecret, HttpMessageHandler handler, ZoomClientOptions options = null, ILogger logger = null) : this(apiKey, apiSecret, new HttpClient(handler), true, options, logger) { @@ -153,6 +156,7 @@ public Client(string apiKey, string apiSecret, HttpMessageHandler handler, ZoomC /// Your Zoom API Secret. /// Allows you to inject your own HttpClient. This is useful, for example, to setup the HtppClient with a proxy. /// Options for the Zoom client. + /// Logger. public Client(string apiKey, string apiSecret, HttpClient httpClient, ZoomClientOptions options = null, ILogger logger = null) : this(apiKey, apiSecret, httpClient, false, options, logger) { From d1e1e25d3aa765a34d0e6308c34018166494b6ca Mon Sep 17 00:00:00 2001 From: Jericho Date: Tue, 5 May 2020 10:51:01 -0400 Subject: [PATCH 22/41] Remove commented out code that is causing warnings which prevent build from completing successfully --- Source/ZoomNet/Client.cs | 48 --------------------------------------- Source/ZoomNet/IClient.cs | 47 -------------------------------------- 2 files changed, 95 deletions(-) diff --git a/Source/ZoomNet/Client.cs b/Source/ZoomNet/Client.cs index 3a3abaec..bd0b9b50 100644 --- a/Source/ZoomNet/Client.cs +++ b/Source/ZoomNet/Client.cs @@ -59,54 +59,6 @@ public static string Version /// public static string UserAgent { get; private set; } - /// - /// Gets the resource which allows you to manage sub accounts. - /// - /// - /// The accounts resource. - /// - //public IAccounts Accounts { get; private set; } - - /// - /// Gets the resource which allows you to manage billing information. - /// - /// - /// The billing resource. - /// - //public IBillingInformation BillingInformation { get; private set; } - - /// - /// Gets the resource which allows you to manage users. - /// - /// - /// The users resource. - /// - //public IUsers Users { get; private set; } - - /// - /// Gets the resource wich alloes you to manage roles. - /// - /// - /// The roles resource. - /// - //public IRoles Roles { get; private set; } - - /// - /// Gets the resource which allows you to manage meetings. - /// - /// - /// The meetings resource. - /// - //public IMeetings Meetings { get; private set; } - - /// - /// Gets the resource which allows you to manage webinars. - /// - /// - /// The webinars resource. - /// - //public IWebinars Webinars { get; private set; } - #endregion #region CTOR diff --git a/Source/ZoomNet/IClient.cs b/Source/ZoomNet/IClient.cs index 469a991e..852dc1d3 100644 --- a/Source/ZoomNet/IClient.cs +++ b/Source/ZoomNet/IClient.cs @@ -5,52 +5,5 @@ namespace ZoomNet /// public interface IClient { - /// - /// Gets the resource which allows you to manage sub accounts. - /// - /// - /// The accounts resource. - /// - //IAccounts Accounts { get; } - - /// - /// Gets the resource which allows you to manage billing information. - /// - /// - /// The billing resource. - /// - //IBillingInformation BillingInformation { get; } - - /// - /// Gets the resource which allows you to manage users. - /// - /// - /// The users resource. - /// - //IUsers Users { get; } - - /// - /// Gets the resource wich alloes you to manage roles. - /// - /// - /// The roles resource. - /// - //IRoles Roles { get; } - - /// - /// Gets the resource which allows you to manage meetings. - /// - /// - /// The meetings resource. - /// - //IMeetings Meetings { get; } - - /// - /// Gets the resource which allows you to manage webinars. - /// - /// - /// The webinars resource. - /// - //IWebinars Webinars { get; } } } From d2d755dd09bfa375a5d2bd16973e93aeb506ac54 Mon Sep 17 00:00:00 2001 From: Jericho Date: Tue, 5 May 2020 10:58:07 -0400 Subject: [PATCH 23/41] Remove commented out code that is causing warnings which prevent build from completing successfully --- Source/ZoomNet/Client.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Source/ZoomNet/Client.cs b/Source/ZoomNet/Client.cs index bd0b9b50..9fde3003 100644 --- a/Source/ZoomNet/Client.cs +++ b/Source/ZoomNet/Client.cs @@ -131,13 +131,6 @@ private Client(string apiKey, string apiSecret, HttpClient httpClient, bool disp _fluentClient.Filters.Add(new JwtTokenHandler(apiKey, apiSecret)); _fluentClient.Filters.Add(new DiagnosticHandler(_options.LogLevelSuccessfulCalls, _options.LogLevelFailedCalls)); _fluentClient.Filters.Add(new ZoomErrorHandler()); - - //Accounts = new Accounts(_fluentClient); - //BillingInformation = new BillingInformation(_fluentClient); - //Users = new Users(_fluentClient); - //Roles = new Roles(_fluentClient); - //Meetings = new Meetings(_fluentClient); - //Webinars = new Webinars(_fluentClient); } /// From 5cf69845c32e433eeefa202440bb071a2e78958e Mon Sep 17 00:00:00 2001 From: Jeremie Desautels Date: Sat, 30 Mar 2019 15:10:25 -0400 Subject: [PATCH 24/41] Meetings resource Regarding #1 --- Source/ZoomNet/Client.cs | 11 + Source/ZoomNet/IClient.cs | 9 + Source/ZoomNet/Models/InstantMeeting.cs | 10 + Source/ZoomNet/Models/Meeting.cs | 125 +++++ Source/ZoomNet/Models/MeetingApprovalType.cs | 23 + Source/ZoomNet/Models/MeetingAudioType.cs | 31 ++ Source/ZoomNet/Models/MeetingListType.cs | 31 ++ Source/ZoomNet/Models/MeetingOccurrence.cs | 41 ++ .../ZoomNet/Models/MeetingRegistrationType.cs | 23 + Source/ZoomNet/Models/MeetingSettings.cs | 137 ++++++ Source/ZoomNet/Models/MeetingStatus.cs | 31 ++ Source/ZoomNet/Models/MeetingType.cs | 28 ++ Source/ZoomNet/Models/RecurrenceInfo.cs | 65 +++ Source/ZoomNet/Models/RecurringMeeting.cs | 26 ++ Source/ZoomNet/Models/Registrant.cs | 144 ++++++ Source/ZoomNet/Models/RegistrantStatus.cs | 31 ++ Source/ZoomNet/Models/ScheduledMeeting.cs | 35 ++ Source/ZoomNet/Resources/IMeetings.cs | 224 +++++++++ Source/ZoomNet/Resources/Meetings.cs | 432 ++++++++++++++++++ Source/ZoomNet/Utilities/MeetingConverter.cs | 119 +++++ 20 files changed, 1576 insertions(+) create mode 100644 Source/ZoomNet/Models/InstantMeeting.cs create mode 100644 Source/ZoomNet/Models/Meeting.cs create mode 100644 Source/ZoomNet/Models/MeetingApprovalType.cs create mode 100644 Source/ZoomNet/Models/MeetingAudioType.cs create mode 100644 Source/ZoomNet/Models/MeetingListType.cs create mode 100644 Source/ZoomNet/Models/MeetingOccurrence.cs create mode 100644 Source/ZoomNet/Models/MeetingRegistrationType.cs create mode 100644 Source/ZoomNet/Models/MeetingSettings.cs create mode 100644 Source/ZoomNet/Models/MeetingStatus.cs create mode 100644 Source/ZoomNet/Models/MeetingType.cs create mode 100644 Source/ZoomNet/Models/RecurrenceInfo.cs create mode 100644 Source/ZoomNet/Models/RecurringMeeting.cs create mode 100644 Source/ZoomNet/Models/Registrant.cs create mode 100644 Source/ZoomNet/Models/RegistrantStatus.cs create mode 100644 Source/ZoomNet/Models/ScheduledMeeting.cs create mode 100644 Source/ZoomNet/Resources/IMeetings.cs create mode 100644 Source/ZoomNet/Resources/Meetings.cs create mode 100644 Source/ZoomNet/Utilities/MeetingConverter.cs diff --git a/Source/ZoomNet/Client.cs b/Source/ZoomNet/Client.cs index 9fde3003..01695f55 100644 --- a/Source/ZoomNet/Client.cs +++ b/Source/ZoomNet/Client.cs @@ -6,6 +6,7 @@ using System.Net; using System.Net.Http; using System.Reflection; +using ZoomNet.Resources; using ZoomNet.Utilities; namespace ZoomNet @@ -59,6 +60,14 @@ public static string Version /// public static string UserAgent { get; private set; } + /// + /// Gets the resource which allows you to manage meetings. + /// + /// + /// The meetings resource. + /// + public IMeetings Meetings { get; private set; } + #endregion #region CTOR @@ -131,6 +140,8 @@ private Client(string apiKey, string apiSecret, HttpClient httpClient, bool disp _fluentClient.Filters.Add(new JwtTokenHandler(apiKey, apiSecret)); _fluentClient.Filters.Add(new DiagnosticHandler(_options.LogLevelSuccessfulCalls, _options.LogLevelFailedCalls)); _fluentClient.Filters.Add(new ZoomErrorHandler()); + + Meetings = new Meetings(_fluentClient); } /// diff --git a/Source/ZoomNet/IClient.cs b/Source/ZoomNet/IClient.cs index 852dc1d3..b02fb22a 100644 --- a/Source/ZoomNet/IClient.cs +++ b/Source/ZoomNet/IClient.cs @@ -1,3 +1,5 @@ +using ZoomNet.Resources; + namespace ZoomNet { /// @@ -5,5 +7,12 @@ namespace ZoomNet /// public interface IClient { + /// + /// Gets the resource which allows you to manage meetings. + /// + /// + /// The meetings resource. + /// + IMeetings Meetings { get; } } } diff --git a/Source/ZoomNet/Models/InstantMeeting.cs b/Source/ZoomNet/Models/InstantMeeting.cs new file mode 100644 index 00000000..0c8665f7 --- /dev/null +++ b/Source/ZoomNet/Models/InstantMeeting.cs @@ -0,0 +1,10 @@ +namespace ZoomNet.Models +{ + /// + /// An instant meeting. + /// + /// + public class InstantMeeting : Meeting + { + } +} diff --git a/Source/ZoomNet/Models/Meeting.cs b/Source/ZoomNet/Models/Meeting.cs new file mode 100644 index 00000000..ee3ea81d --- /dev/null +++ b/Source/ZoomNet/Models/Meeting.cs @@ -0,0 +1,125 @@ +using Newtonsoft.Json; +using ZoomNet.Models; +using System; + +namespace ZoomNet.Models +{ + /// + /// A meeting. + /// + public abstract class Meeting + { + /// + /// Gets or sets the unique id. + /// + /// + /// The unique id. + /// + [JsonProperty("uuid", NullValueHandling = NullValueHandling.Ignore)] + public string Uuid { get; set; } + + /// + /// Gets or sets the meeting id, also known as the meeting number. + /// + /// + /// The id. + /// + [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] + public long Id { get; set; } + + /// + /// Gets or sets the ID of the user who is set as the host of the meeting. + /// + /// + /// The user id. + /// + [JsonProperty("host_id", NullValueHandling = NullValueHandling.Ignore)] + public string HostId { get; set; } + + /// + /// Gets or sets the topic of the meeting. + /// + /// + /// The topic. + /// + [JsonProperty("topic", NullValueHandling = NullValueHandling.Ignore)] + public string Topic { get; set; } + + /// + /// Gets or sets the meeting type. + /// + /// The meeting type. + [JsonProperty(PropertyName = "type", NullValueHandling = NullValueHandling.Ignore)] + public MeetingType Type { get; set; } + + /// + /// Gets or sets the status. + /// + /// + /// The status. + /// + [JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)] + public MeetingStatus Status { get; set; } + + /// + /// Gets or sets the meeting description. + /// + /// Meeting description. + [JsonProperty(PropertyName = "agenda", NullValueHandling = NullValueHandling.Ignore)] + public string Agenda { get; set; } + + /// + /// Gets or sets the date and time when the meeting was created. + /// + /// The meeting created time. + [JsonProperty(PropertyName = "created_at", NullValueHandling = NullValueHandling.Ignore)] + public DateTime CreatedOn { get; set; } + + /// + /// Gets or sets the URL for the host to start the meeting. + /// + /// The start URL. + [JsonProperty(PropertyName = "start_url", NullValueHandling = NullValueHandling.Ignore)] + public string StartUrl { get; set; } + + /// + /// Gets or sets the URL to join the meeting. + /// + /// The join URL. + [JsonProperty(PropertyName = "join_url", NullValueHandling = NullValueHandling.Ignore)] + public string JoinUrl { get; set; } + + /// + /// Gets or sets the password to join the meeting. + /// Password may only contain the following characters: [a-z A-Z 0-9 @ - _ *]. + /// Max of 10 characters. + /// + /// Password to join the meeting. Password may only contain the following characters: [a-z A-Z 0-9 @ - _ *]. Max of 10 characters. + [JsonProperty(PropertyName = "password", NullValueHandling = NullValueHandling.Ignore)] + public string Password { get; set; } + + /// + /// Gets or sets the H.323/SIP room system password. + /// + /// + /// The h.323 password. + /// + [JsonProperty("h323_password", NullValueHandling = NullValueHandling.Ignore)] + public string H323Password { get; set; } + + /// + /// Gets or sets the password to join the phone session. + /// + /// + /// The pstn password. + /// + [JsonProperty("pstn_password", NullValueHandling = NullValueHandling.Ignore)] + public string PstnPassword { get; set; } + + /// + /// Gets or Sets the meeting settings. + /// + [JsonProperty(PropertyName = "settings", NullValueHandling = NullValueHandling.Ignore)] + public MeetingSettings Settings { get; set; } + } +} diff --git a/Source/ZoomNet/Models/MeetingApprovalType.cs b/Source/ZoomNet/Models/MeetingApprovalType.cs new file mode 100644 index 00000000..eb711925 --- /dev/null +++ b/Source/ZoomNet/Models/MeetingApprovalType.cs @@ -0,0 +1,23 @@ +namespace ZoomNet.Models +{ + /// + /// Enumeration to indicate the type of approval for a meeting. + /// + public enum MeetingApprovalType + { + /// + /// Automatically approve. + /// + Automatic = 0, + + /// + /// Manually approve. + /// + Manual = 1, + + /// + /// No registration required. + /// + None = 2 + } +} diff --git a/Source/ZoomNet/Models/MeetingAudioType.cs b/Source/ZoomNet/Models/MeetingAudioType.cs new file mode 100644 index 00000000..13ba10ea --- /dev/null +++ b/Source/ZoomNet/Models/MeetingAudioType.cs @@ -0,0 +1,31 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System.Runtime.Serialization; + +namespace ZoomNet.Models +{ + /// + /// Enumeration to indicate the type of audio available to attendees. + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum MeetingAudioType + { + /// + /// VOIP. + /// + [EnumMember(Value = "voip")] + Voip, + + /// + /// Telephony. + /// + [EnumMember(Value = "telephony")] + Telephony, + + /// + /// Both telephony and voip. + /// + [EnumMember(Value = "both")] + Both + } +} diff --git a/Source/ZoomNet/Models/MeetingListType.cs b/Source/ZoomNet/Models/MeetingListType.cs new file mode 100644 index 00000000..c09bd13b --- /dev/null +++ b/Source/ZoomNet/Models/MeetingListType.cs @@ -0,0 +1,31 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System.Runtime.Serialization; + +namespace ZoomNet.Models +{ + /// + /// Enumeration to indicate the type of meeting to be listed. + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum MeetingListType + { + /// + /// Scheduled. + /// + [EnumMember(Value = "scheduled")] + Scheduled, + + /// + /// Live. + /// + [EnumMember(Value = "live")] + Live, + + /// + /// Upcoming. + /// + [EnumMember(Value = "upcoming")] + Upcoming + } +} diff --git a/Source/ZoomNet/Models/MeetingOccurrence.cs b/Source/ZoomNet/Models/MeetingOccurrence.cs new file mode 100644 index 00000000..ac6eff22 --- /dev/null +++ b/Source/ZoomNet/Models/MeetingOccurrence.cs @@ -0,0 +1,41 @@ +using Newtonsoft.Json; +using System; + +namespace ZoomNet.Models +{ + /// + /// Meeting occurrence. + /// + public class MeetingOccurrence + { + /// + /// Gets or sets the occurrence Id. + /// + /// The ID. + [JsonProperty(PropertyName = "occurrence_id")] + public string OccurrenceId { get; set; } + + /// + /// Gets or sets the start time. + /// + /// The occurrence start time. + [JsonProperty(PropertyName = "start_time", NullValueHandling = NullValueHandling.Ignore)] + public DateTime StartTime { get; set; } + + /// + /// Gets or sets the duration in minutes. + /// + /// The duration in minutes. + [JsonProperty(PropertyName = "duration", NullValueHandling = NullValueHandling.Ignore)] + public int Duration { get; set; } + + /// + /// Gets or sets the status. + /// + /// + /// The occurrence status. + /// + [JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)] + public string Status { get; set; } + } +} diff --git a/Source/ZoomNet/Models/MeetingRegistrationType.cs b/Source/ZoomNet/Models/MeetingRegistrationType.cs new file mode 100644 index 00000000..7e4a96fb --- /dev/null +++ b/Source/ZoomNet/Models/MeetingRegistrationType.cs @@ -0,0 +1,23 @@ +namespace ZoomNet.Models +{ + /// + /// Enumeration to indicate the type of registration for a meeting. + /// + public enum MeetingRegistrationType + { + /// + /// Attendees register once and can attend any of the occurrences. + /// + RegisterOnceAttendAll = 1, + + /// + /// Attendees need to register for each occurrence to attend. + /// + RegisterForEachOccurrence = 2, + + /// + /// Attendees register once and can choose one or more occurrence to attend. + /// + RegisterOnceAttendOnce = 3 + } +} diff --git a/Source/ZoomNet/Models/MeetingSettings.cs b/Source/ZoomNet/Models/MeetingSettings.cs new file mode 100644 index 00000000..e48bef2e --- /dev/null +++ b/Source/ZoomNet/Models/MeetingSettings.cs @@ -0,0 +1,137 @@ +using Newtonsoft.Json; +using ZoomNet.Models; + +namespace ZoomNet.Models +{ + /// + /// Meeting Settings. + /// + public class MeetingSettings + { + /// + /// Gets or sets the value indicating whether to start video when host joins the meeting. + /// + [JsonProperty(PropertyName = "host_video")] + public bool? StartVideoWhenHostJoins { get; set; } + + /// + /// Gets or sets the value indicating whether to start video when participants join the meeting. + /// + [JsonProperty(PropertyName = "participant_video")] + public bool? StartVideoWhenParticipantsJoin { get; set; } + + /// + /// Gets or sets the value indicating whether the meeting should be hosted in China. + /// + [JsonProperty(PropertyName = "cn_meeting")] + public bool? HostInChina { get; set; } + + /// + /// Gets or sets the value indicating whether the meeting should be hosted in India. + /// + [JsonProperty(PropertyName = "in_meeting")] + public bool? HostInIndia { get; set; } + + /// + /// Gets or sets the value indicating whether participants can join the meeting before the host starts the meeting. Only used for scheduled or recurring meetings. + /// + [JsonProperty(PropertyName = "join_before_host")] + public bool? JoinBeforeHost { get; set; } + + /// + /// Gets or sets the value indicating whether participants are muted upon entry. + /// + [JsonProperty(PropertyName = "mute_upon_entry")] + public bool? MuteUponEntry { get; set; } + + /// + /// Gets or sets the value indicating whether a watermark should be displayed when viewing shared screen. + /// + [JsonProperty(PropertyName = "watermark")] + public bool? Watermark { get; set; } + + /// + /// Gets or sets the value indicating whether to use Personal Meeting ID. Only used for scheduled meetings and recurring meetings with no fixed time. + /// + [JsonProperty(PropertyName = "use_pmi")] + public bool? UsePmi { get; set; } + + /// + /// Gets or sets the approval type. + /// + [JsonProperty(PropertyName = "approval_type")] + public MeetingApprovalType? ApprovalType { get; set; } + + /// + /// Gets or sets the registration type. Used for recurring meeting with fixed time only. + /// + [JsonProperty(PropertyName = "registration_type")] + public MeetingRegistrationType? RegistrationType { get; set; } + + /// + /// Gets or sets the value indicating how participants can join the audio portion of the meeting. + /// + [JsonProperty(PropertyName = "audio")] + public MeetingAudioType? Audio { get; set; } + + /// + /// Gets or sets AutoRecording. + /// + [JsonProperty(PropertyName = "auto_recording")] + public string AutoRecording { get; set; } + + /// + /// Gets or sets the value indicating that only signed-in users can join this meeting. + /// + [JsonProperty(PropertyName = "enforce_login")] + public bool? EnforceLogin { get; set; } + + /// + /// Gets or sets the value indicating only signed-in users with specified domains can join this meeting. + /// + [JsonProperty(PropertyName = "enforce_login_domains")] + public string EnforceLoginDomains { get; set; } + + /// + /// Gets or sets the value indicating alternative hosts emails or IDs. Multiple value separated by comma. + /// + [JsonProperty(PropertyName = "alternative_hosts")] + public string AlternativeHosts { get; set; } + + /// + /// Gets or sets the value indicating whether registration is closed after event date. + /// + [JsonProperty(PropertyName = "close_registration")] + public bool? CloseRegistration { get; set; } + + /// + /// Gets or sets the value indicating whether a confirmation email is sent when a participant registers. + /// + [JsonProperty(PropertyName = "registrants_confirmation_email")] + public bool? SendRegistrationConfirmationEmail { get; set; } + + /// + /// Gets or sets the value indicating whether to use a waiting room. + /// + [JsonProperty(PropertyName = "waiting_room")] + public bool? WaitingRoom { get; set; } + + /// + /// Gets or sets the list of global dial-in countries. + /// + [JsonProperty(PropertyName = "global_dial_in_countries")] + public string[] GlobalDialInCountries { get; set; } + + /// + /// Gets or sets the contact name for registration. + /// + [JsonProperty(PropertyName = "contact_name")] + public string ContactName { get; set; } + + /// + /// Gets or sets the contat email for registration. + /// + [JsonProperty(PropertyName = "contact_email")] + public string ContactEmail { get; set; } + } +} diff --git a/Source/ZoomNet/Models/MeetingStatus.cs b/Source/ZoomNet/Models/MeetingStatus.cs new file mode 100644 index 00000000..86fc3357 --- /dev/null +++ b/Source/ZoomNet/Models/MeetingStatus.cs @@ -0,0 +1,31 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System.Runtime.Serialization; + +namespace ZoomNet.Models +{ + /// + /// Enumeration to indicate the status of a meeting. + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum MeetingStatus + { + /// + /// Waiting. + /// + [EnumMember(Value = "waiting")] + Waiting, + + /// + /// Started. + /// + [EnumMember(Value = "started")] + Started, + + /// + /// Finished. + /// + [EnumMember(Value = "finished")] + Finished + } +} diff --git a/Source/ZoomNet/Models/MeetingType.cs b/Source/ZoomNet/Models/MeetingType.cs new file mode 100644 index 00000000..80764168 --- /dev/null +++ b/Source/ZoomNet/Models/MeetingType.cs @@ -0,0 +1,28 @@ +namespace ZoomNet.Models +{ + /// + /// Enumeration to indicate the type of meeting. + /// + public enum MeetingType + { + /// + /// Instant. + /// + Instant = 1, + + /// + /// Scheduled. + /// + Scheduled = 2, + + /// + /// Recurring meeting with no fixed time. + /// + RecurringNoFixedTime = 3, + + /// + /// Recurring meeting with fixed time. + /// + RecurringFixedTime = 8 + } +} diff --git a/Source/ZoomNet/Models/RecurrenceInfo.cs b/Source/ZoomNet/Models/RecurrenceInfo.cs new file mode 100644 index 00000000..636d44aa --- /dev/null +++ b/Source/ZoomNet/Models/RecurrenceInfo.cs @@ -0,0 +1,65 @@ +using Newtonsoft.Json; +using System; + +namespace ZoomNet.Models +{ + /// + /// Recurrence. + /// + public class RecurrenceInfo + { + /// + /// Gets or sets the recurrence type. + /// + [JsonProperty(PropertyName = "type")] + public int? Type { get; set; } + + /// + /// Gets or sets the interval should the meeting repeat. + /// For a daily meeting, max of 90 days. + /// For a weekly meeting, max of 12 weeks. + /// For a monthly meeting, max of 3 months. + /// + [JsonProperty(PropertyName = "repeat_interval")] + public int? RepeatInterval { get; set; } + + /// + /// Gets or sets the days of the week the meeting should repeat, multiple values separated by comma. + /// + [JsonProperty(PropertyName = "weekly_days")] + public int? WeeklyDays { get; set; } + + /// + /// Gets or sets the day of the month for the meeting to be scheduled. + /// The value range is from 1 to 31. + /// + [JsonProperty(PropertyName = "monthly_day")] + public int? MonthlyDay { get; set; } + + /// + /// Gets or sets the week for which the meeting should recur each month, + /// + [JsonProperty(PropertyName = "monthly_week")] + public int? MonthlyWeek { get; set; } + + /// + /// Gets or sets the day for which the meeting should recur each month. + /// + [JsonProperty(PropertyName = "monthly_week_day")] + public int? MonthlyWeekDay { get; set; } + + /// + /// Gets or sets the select how many times the meeting will occur before it is canceled. + /// Cannot be used with "end_date_time". + /// + [JsonProperty(PropertyName = "end_times")] + public int? EndTimes { get; set; } + + /// + /// Gets or sets the date the meeting will canceled. + /// Cannot be used with "end_times". + /// + [JsonProperty(PropertyName = "end_date_time")] + public DateTime? EndDateTime { get; set; } + } +} diff --git a/Source/ZoomNet/Models/RecurringMeeting.cs b/Source/ZoomNet/Models/RecurringMeeting.cs new file mode 100644 index 00000000..457016b1 --- /dev/null +++ b/Source/ZoomNet/Models/RecurringMeeting.cs @@ -0,0 +1,26 @@ +using Newtonsoft.Json; + +namespace ZoomNet.Models +{ + /// + /// A meeting. + /// + /// + public class RecurringMeeting : Meeting + { + /// + /// Gets or sets the timezone. + /// For example, "America/Los_Angeles". + /// Please reference our timezone list for supported timezones and their formats. + /// + /// The meeting timezone. For example, "America/Los_Angeles". Please reference our timezone list for supported timezones and their formats. + [JsonProperty(PropertyName = "timezone", NullValueHandling = NullValueHandling.Ignore)] + public string Timezone { get; set; } + + /// + /// Gets or sets the occurrences. + /// + [JsonProperty(PropertyName = "occurrences", NullValueHandling = NullValueHandling.Ignore)] + public MeetingOccurrence[] Occurrences { get; set; } + } +} diff --git a/Source/ZoomNet/Models/Registrant.cs b/Source/ZoomNet/Models/Registrant.cs new file mode 100644 index 00000000..1b969b84 --- /dev/null +++ b/Source/ZoomNet/Models/Registrant.cs @@ -0,0 +1,144 @@ +using Newtonsoft.Json; +using ZoomNet.Models; +using System; +using System.Collections.Generic; + +namespace ZoomNet.Models +{ + /// + /// Registrant. + /// + public class Registrant + { + /// + /// Gets or sets the registrant id. + /// + /// + /// The id. + /// + [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] + public long Id { get; set; } + + /// + /// Gets or sets a valid email address. + /// + [JsonProperty(PropertyName = "email")] + public string Email { get; set; } + + /// + /// Gets or sets the first name. + /// + [JsonProperty(PropertyName = "first_name")] + public string FirstName { get; set; } + + /// + /// Gets or sets the last name. + /// + [JsonProperty(PropertyName = "last_name")] + public string LastName { get; set; } + + /// + /// Gets or sets the address. + /// + [JsonProperty(PropertyName = "address")] + public string Address { get; set; } + + /// + /// Gets or sets the city. + /// + [JsonProperty(PropertyName = "city")] + public string City { get; set; } + + /// + /// Gets or sets the country. + /// + [JsonProperty(PropertyName = "country")] + public string Country { get; set; } + + /// + /// Gets or sets the zip/postal Code. + /// + [JsonProperty(PropertyName = "zip")] + public string Zip { get; set; } + + /// + /// Gets or sets the state/Province. + /// + [JsonProperty(PropertyName = "state")] + public string State { get; set; } + + /// + /// Gets or sets the phone. + /// + [JsonProperty(PropertyName = "phone")] + public string Phone { get; set; } + + /// + /// Gets or sets the industry. + /// + [JsonProperty(PropertyName = "industry")] + public string Industry { get; set; } + + /// + /// Gets or sets the organization. + /// + [JsonProperty(PropertyName = "org")] + public string Organization { get; set; } + + /// + /// Gets or sets the job Title. + /// + [JsonProperty(PropertyName = "job_title")] + public string JobTitle { get; set; } + + /// + /// Gets or sets the purchasing Time Frame. + /// + [JsonProperty(PropertyName = "purchasing_time_frame")] + public string PurchasingTimeFrame { get; set; } + + /// + /// Gets or sets the role in purchase process. + /// + [JsonProperty(PropertyName = "role_in_purchase_process")] + public string RoleInPurchaseProcess { get; set; } + + /// + /// Gets or sets the number of employees. + /// + [JsonProperty(PropertyName = "no_of_employees")] + public string NumberOfEmployees { get; set; } + + /// + /// Gets or sets the questions & comments. + /// + [JsonProperty(PropertyName = "comments")] + public string Comments { get; set; } + + /// + /// Gets or sets the custom questions. + /// + [JsonProperty(PropertyName = "custom_questions")] + public KeyValuePair[] CustomQuestions { get; set; } + + /// + /// Gets or sets the status. + /// + [JsonProperty(PropertyName = "comments")] + public RegistrantStatus Status { get; set; } + + /// + /// Gets or sets the date and time when the registrant was created. + /// + /// The registrant created time. + [JsonProperty(PropertyName = "created_time", NullValueHandling = NullValueHandling.Ignore)] + public DateTime CreatedOn { get; set; } + + /// + /// Gets or sets the URL for this registrant to join the meeting. + /// + /// The join URL. + [JsonProperty(PropertyName = "join_url", NullValueHandling = NullValueHandling.Ignore)] + public string JoinUrl { get; set; } + } +} diff --git a/Source/ZoomNet/Models/RegistrantStatus.cs b/Source/ZoomNet/Models/RegistrantStatus.cs new file mode 100644 index 00000000..2dcb815e --- /dev/null +++ b/Source/ZoomNet/Models/RegistrantStatus.cs @@ -0,0 +1,31 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System.Runtime.Serialization; + +namespace ZoomNet.Models +{ + /// + /// Enumeration to indicate the status of a registrant. + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum RegistrantStatus + { + /// + /// Pending. + /// + [EnumMember(Value = "pending")] + Pending, + + /// + /// Approved. + /// + [EnumMember(Value = "approved")] + Approved, + + /// + /// Denied. + /// + [EnumMember(Value = "denied")] + Denied + } +} diff --git a/Source/ZoomNet/Models/ScheduledMeeting.cs b/Source/ZoomNet/Models/ScheduledMeeting.cs new file mode 100644 index 00000000..06dc5e77 --- /dev/null +++ b/Source/ZoomNet/Models/ScheduledMeeting.cs @@ -0,0 +1,35 @@ +using Newtonsoft.Json; +using System; + +namespace ZoomNet.Models +{ + /// + /// A scheduled meeting. + /// + /// + public class ScheduledMeeting : Meeting + { + /// + /// Gets or sets the meeting start time. + /// + /// The meeting start time. Only used for scheduled meetings and recurring meetings with fixed time. + [JsonProperty(PropertyName = "start_time", NullValueHandling = NullValueHandling.Ignore)] + public DateTime StartTime { get; set; } + + /// + /// Gets or sets the meeting duration in minutes. + /// + /// The meeting duration in minutes. + [JsonProperty(PropertyName = "duration", NullValueHandling = NullValueHandling.Ignore)] + public int Duration { get; set; } + + /// + /// Gets or sets the timezone. + /// For example, "America/Los_Angeles". + /// Please reference our timezone list for supported timezones and their formats. + /// + /// The meeting timezone. For example, "America/Los_Angeles". Please reference our timezone list for supported timezones and their formats. + [JsonProperty(PropertyName = "timezone", NullValueHandling = NullValueHandling.Ignore)] + public string Timezone { get; set; } + } +} diff --git a/Source/ZoomNet/Resources/IMeetings.cs b/Source/ZoomNet/Resources/IMeetings.cs new file mode 100644 index 00000000..ff25661c --- /dev/null +++ b/Source/ZoomNet/Resources/IMeetings.cs @@ -0,0 +1,224 @@ +using ZoomNet.Models; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using ZoomNet.Models; + +namespace ZoomNet.Resources +{ + /// + /// Allows you to manage meetings. + /// + /// + /// See Zoom documentation for more information. + /// + public interface IMeetings + { + /// + /// Retrieve all meetings of the specified type for a user. + /// + /// The user Id or email address. + /// The type of meetings. Allowed values: Scheduled, Live, Upcoming. + /// The number of records returned within a single API call. + /// The current page number of returned records. + /// The cancellation token. + /// + /// An array of meetings. + /// + /// + /// This call omits 'occurrences'. Therefore the 'Occurrences' property will be empty. + /// + Task> GetAllAsync(string userId, MeetingListType type = MeetingListType.Scheduled, int recordsPerPage = 30, int page = 1, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Creates an instant meeting for a user. + /// + /// The user Id or email address. + /// Meeting topic. + /// Meeting description. + /// Password to join the meeting. Password may only contain the following characters: [a-z A-Z 0-9 @ - _ *]. Max of 10 characters. + /// Meeting settings. + /// Tracking fields. + /// The cancellation token. + /// + /// The new meeting. + /// + /// Thrown when an exception occured while creating the meeting. + Task CreateInstantMeetingAsync(string userId, string topic, string agenda, string password = null, MeetingSettings settings = null, IDictionary trackingFields = null, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Creates a scheduled meeting for a user. + /// + /// The user Id or email address. + /// Meeting topic. + /// Meeting description. + /// Meeting start time. + /// Meeting duration (minutes). + /// Password to join the meeting. Password may only contain the following characters: [a-z A-Z 0-9 @ - _ *]. Max of 10 characters. + /// Meeting settings. + /// Tracking fields. + /// The cancellation token. + /// + /// The new meeting. + /// + /// Thrown when an exception occured while creating the meeting. + Task CreateScheduledMeetingAsync(string userId, string topic, string agenda, DateTime start, int duration, string password = null, MeetingSettings settings = null, IDictionary trackingFields = null, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Creates a recurring meeting for a user. + /// + /// The user Id or email address. + /// Meeting topic. + /// Meeting description. + /// Meeting start time. If omitted, a 'Recurring meeting with no fixed time' will be created. + /// Meeting duration (minutes). + /// Recurrence information. + /// Password to join the meeting. Password may only contain the following characters: [a-z A-Z 0-9 @ - _ *]. Max of 10 characters. + /// Meeting settings. + /// Tracking fields. + /// The cancellation token. + /// + /// The new meeting. + /// + /// Thrown when an exception occured while creating the meeting. + Task CreateRecurringMeetingAsync(string userId, string topic, string agenda, DateTime? start, int duration, RecurrenceInfo recurrence, string password = null, MeetingSettings settings = null, IDictionary trackingFields = null, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Retrieve the details of a meeting. + /// + /// The user Id or email address. + /// The meeting ID. + /// The meeting occurrence id. + /// The cancellation token. + /// + /// The . + /// + Task GetAsync(string userId, long meetingId, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Delete a meeting. + /// + /// The user Id or email address. + /// The meeting ID. + /// The meeting occurrence id. + /// The cancellation token. + /// + /// The async task. + /// + Task DeleteAsync(string userId, long meetingId, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// End a meeting. + /// + /// The meeting ID. + /// The cancellation token. + /// + /// The async task. + /// + Task EndAsync(long meetingId, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// List registrants of a meeting. + /// + /// The meeting ID. + /// The registrant status. + /// The meeting occurrence id. + /// The number of records returned within a single API call. + /// The current page number of returned records. + /// The cancellation token. + /// + /// An array of . + /// + Task> GetRegistrantsAsync(long meetingId, RegistrantStatus status, string occurrenceId = null, int recordsPerPage = 30, int page = 1, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// List registrants of a meeting. + /// + /// The meeting ID. + /// A valid email address. + /// User's first name. + /// User's last name. + /// The meeting occurrence id. + /// The cancellation token. + /// + /// A . + /// + Task AddRegistrantsAsync(long meetingId, string email, string firstName, string lastName, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Approve a registration for a meeting. + /// + /// The meeting ID. + /// The registrant ID. + /// The registrant's email address. + /// The meeting occurrence id. + /// The cancellation token. + /// + /// The async task. + /// + Task ApproveRegistrantAsync(long meetingId, string registrantId, string registrantEmail, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Approve multiple registrations for a meeting. + /// + /// The meeting ID. + /// ID and email for each registrant to be approved. + /// The meeting occurrence id. + /// The cancellation token. + /// + /// The async task. + /// + Task ApproveRegistrantsAsync(long meetingId, IEnumerable<(string RegistrantId, string RegistrantEmail)> registrantsInfo, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Reject a registration for a meeting. + /// + /// The meeting ID. + /// The registrant ID. + /// The registrant's email address. + /// The meeting occurrence id. + /// The cancellation token. + /// + /// The async task. + /// + Task RejectRegistrantAsync(long meetingId, string registrantId, string registrantEmail, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Reject multiple registrations for a meeting. + /// + /// The meeting ID. + /// ID and email for each registrant to be rejected. + /// The meeting occurrence id. + /// The cancellation token. + /// + /// The async task. + /// + Task RejectRegistrantsAsync(long meetingId, IEnumerable<(string RegistrantId, string RegistrantEmail)> registrantsInfo, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Cancel a registration for a meeting. + /// + /// The meeting ID. + /// The registrant ID. + /// The registrant's email address. + /// The meeting occurrence id. + /// The cancellation token. + /// + /// The async task. + /// + Task CancelRegistrantAsync(long meetingId, string registrantId, string registrantEmail, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Cancel multiple registrations for a meeting. + /// + /// The meeting ID. + /// ID and email for each registrant to be cancelled. + /// The meeting occurrence id. + /// The cancellation token. + /// + /// The async task. + /// + Task CancelRegistrantsAsync(long meetingId, IEnumerable<(string RegistrantId, string RegistrantEmail)> registrantsInfo, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)); + } +} diff --git a/Source/ZoomNet/Resources/Meetings.cs b/Source/ZoomNet/Resources/Meetings.cs new file mode 100644 index 00000000..eac31173 --- /dev/null +++ b/Source/ZoomNet/Resources/Meetings.cs @@ -0,0 +1,432 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Pathoschild.Http.Client; +using ZoomNet.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using ZoomNet.Models; +using ZoomNet.Utilities; + +namespace ZoomNet.Resources +{ + /// + /// Allows you to manage meetings. + /// + /// + /// + /// See Zoom documentation for more information. + /// + public class Meetings : IMeetings + { + private readonly Pathoschild.Http.Client.IClient _client; + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP client. + internal Meetings(Pathoschild.Http.Client.IClient client) + { + _client = client; + } + + /// + /// Retrieve all meetings of the specified type for a user. + /// + /// The user Id or email address. + /// The type of meetings. Allowed values: Scheduled, Live, Upcoming. + /// The number of records returned within a single API call. + /// The current page number of returned records. + /// The cancellation token. + /// + /// An array of . + /// + public Task> GetAllAsync(string userId, MeetingListType type = MeetingListType.Scheduled, int recordsPerPage = 30, int page = 1, CancellationToken cancellationToken = default(CancellationToken)) + { + if (recordsPerPage < 1 || recordsPerPage > 300) + { + throw new ArgumentOutOfRangeException(nameof(recordsPerPage), "Records per page must be between 1 and 300"); + } + + return _client + .GetAsync($"users/{userId}/meetings") + .WithArgument("type", JToken.Parse(JsonConvert.SerializeObject(type)).ToString()) + .WithArgument("page_size", recordsPerPage) + .WithArgument("page", page) + .WithCancellationToken(cancellationToken) + .AsPaginatedResponse("meetings", new MeetingConverter()); + } + + /// + /// Creates an instant meeting for a user. + /// + /// The user Id or email address. + /// Meeting topic. + /// Meeting description. + /// Password to join the meeting. Password may only contain the following characters: [a-z A-Z 0-9 @ - _ *]. Max of 10 characters. + /// Meeting settings. + /// Tracking fields. + /// The cancellation token. + /// + /// The new meeting. + /// + /// Thrown when an exception occured while creating the meeting. + public Task CreateInstantMeetingAsync( + string userId, + string topic, + string agenda, + string password = null, + MeetingSettings settings = null, + IDictionary trackingFields = null, + CancellationToken cancellationToken = default(CancellationToken)) + { + var data = new JObject() + { + { "type", 1 } + }; + data.AddPropertyIfValue("topic", topic); + data.AddPropertyIfValue("password", password); + data.AddPropertyIfValue("agenda", agenda); + data.AddPropertyIfValue("settings", settings); + data.AddPropertyIfValue("tracking_fields", trackingFields?.Select(tf => new JObject() { { "field", tf.Key }, { "value", tf.Value } })); + + return _client + .PostAsync($"users/{userId}/meetings") + .WithJsonBody(data) + .WithCancellationToken(cancellationToken) + .AsObject(); + } + + /// + /// Creates a scheduled meeting for a user. + /// + /// The user Id or email address. + /// Meeting topic. + /// Meeting description. + /// Meeting start time. + /// Meeting duration (minutes). + /// Password to join the meeting. Password may only contain the following characters: [a-z A-Z 0-9 @ - _ *]. Max of 10 characters. + /// Meeting settings. + /// Tracking fields. + /// The cancellation token. + /// + /// The new meeting. + /// + /// Thrown when an exception occured while creating the meeting. + public Task CreateScheduledMeetingAsync( + string userId, + string topic, + string agenda, + DateTime start, + int duration, + string password = null, + MeetingSettings settings = null, + IDictionary trackingFields = null, + CancellationToken cancellationToken = default(CancellationToken)) + { + var data = new JObject() + { + { "type", 2 } + }; + data.AddPropertyIfValue("topic", topic); + data.AddPropertyIfValue("password", password); + data.AddPropertyIfValue("agenda", agenda); + data.AddPropertyIfValue("start_time", start.ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'")); + data.AddPropertyIfValue("duration", duration); + data.AddPropertyIfValue("timezone", "UTC"); + data.AddPropertyIfValue("settings", settings); + data.AddPropertyIfValue("tracking_fields", trackingFields?.Select(tf => new JObject() { { "field", tf.Key }, { "value", tf.Value } })); + + return _client + .PostAsync($"users/{userId}/meetings") + .WithJsonBody(data) + .WithCancellationToken(cancellationToken) + .AsObject(); + } + + /// + /// Creates a recurring meeting for a user. + /// + /// The user Id or email address. + /// Meeting topic. + /// Meeting description. + /// Meeting start time. If omitted, a 'Recurring meeting with no fixed time' will be created. + /// Meeting duration (minutes). + /// Recurrence information. + /// Password to join the meeting. Password may only contain the following characters: [a-z A-Z 0-9 @ - _ *]. Max of 10 characters. + /// Meeting settings. + /// Tracking fields. + /// The cancellation token. + /// + /// The new meeting. + /// + /// Thrown when an exception occured while creating the meeting. + public Task CreateRecurringMeetingAsync( + string userId, + string topic, + string agenda, + DateTime? start, + int duration, + RecurrenceInfo recurrence, + string password = null, + MeetingSettings settings = null, + IDictionary trackingFields = null, + CancellationToken cancellationToken = default(CancellationToken)) + { + var data = new JObject() + { + // 3 = Recurring with no fixed time + // 8 = Recurring with fixed time + { "type", start.HasValue ? 8 : 3 } + }; + data.AddPropertyIfValue("topic", topic); + data.AddPropertyIfValue("password", password); + data.AddPropertyIfValue("agenda", agenda); + data.AddPropertyIfValue("start_time", start?.ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'")); + data.AddPropertyIfValue("duration", duration); + data.AddPropertyIfValue("recurrence", recurrence); + data.AddPropertyIfValue("timezone", "UTC"); + data.AddPropertyIfValue("settings", settings); + data.AddPropertyIfValue("tracking_fields", trackingFields?.Select(tf => new JObject() { { "field", tf.Key }, { "value", tf.Value } })); + + return _client + .PostAsync($"users/{userId}/meetings") + .WithJsonBody(data) + .WithCancellationToken(cancellationToken) + .AsObject(); + } + + /// + /// Retrieve the details of a meeting. + /// + /// The user Id or email address. + /// The meeting ID. + /// The meeting occurrence id. + /// The cancellation token. + /// + /// The . + /// + public Task GetAsync(string userId, long meetingId, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)) + { + return _client + .GetAsync($"meetings/{meetingId}") + .WithArgument("occurrence_id", occurrenceId) + .WithCancellationToken(cancellationToken) + .AsObject(null, new MeetingConverter()); + } + + /// + /// Delete a meeting. + /// + /// The user Id or email address. + /// The meeting ID. + /// The meeting occurrence id. + /// The cancellation token. + /// + /// The async task. + /// + public Task DeleteAsync(string userId, long meetingId, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)) + { + return _client + .DeleteAsync($"meetings/{meetingId}") + .WithArgument("occurrence_id", occurrenceId) + .WithCancellationToken(cancellationToken) + .AsMessage(); + } + + /// + /// End a meeting. + /// + /// The meeting ID. + /// The cancellation token. + /// + /// The async task. + /// + public Task EndAsync(long meetingId, CancellationToken cancellationToken = default(CancellationToken)) + { + return _client + .PutAsync($"meetings/{meetingId}/status") + .WithArgument("action", "end") + .WithCancellationToken(cancellationToken) + .AsMessage(); + } + + /// + /// List registrants of a meeting. + /// + /// The meeting ID. + /// The registrant status. + /// The meeting occurrence id. + /// The number of records returned within a single API call. + /// The current page number of returned records. + /// The cancellation token. + /// + /// An array of . + /// + public Task> GetRegistrantsAsync(long meetingId, RegistrantStatus status, string occurrenceId = null, int recordsPerPage = 30, int page = 1, CancellationToken cancellationToken = default(CancellationToken)) + { + if (recordsPerPage < 1 || recordsPerPage > 300) + { + throw new ArgumentOutOfRangeException(nameof(recordsPerPage), "Records per page must be between 1 and 300"); + } + + return _client + .GetAsync($"meetings/{meetingId}/registrants") + .WithArgument("status", JToken.Parse(JsonConvert.SerializeObject(status)).ToString()) + .WithArgument("occurrence_id", occurrenceId) + .WithArgument("page_size", recordsPerPage) + .WithArgument("page", page) + .WithCancellationToken(cancellationToken) + .AsPaginatedResponse("registrants"); + } + + /// + /// List registrants of a meeting. + /// + /// The meeting ID. + /// A valid email address. + /// User's first name. + /// User's last name. + /// The meeting occurrence id. + /// The cancellation token. + /// + /// A . + /// + public Task AddRegistrantsAsync(long meetingId, string email, string firstName, string lastName, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)) + { + var data = new JObject(); + data.AddPropertyIfValue("email", email); + data.AddPropertyIfValue("first_name", firstName); + data.AddPropertyIfValue("last_name", lastName); + + return _client + .PostAsync($"meetings/{meetingId}/registrants") + .WithArgument("occurence_id", occurrenceId) + .WithJsonBody(data) + .WithCancellationToken(cancellationToken) + .AsObject(); + } + + /// + /// Approve a registration for a meeting. + /// + /// The meeting ID. + /// The registrant ID. + /// The registrant's email address. + /// The meeting occurrence id. + /// The cancellation token. + /// + /// The async task. + /// + public Task ApproveRegistrantAsync(long meetingId, string registrantId, string registrantEmail, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)) + { + return ApproveRegistrantsAsync(meetingId, new[] { (registrantId, registrantEmail) }, occurrenceId, cancellationToken); + } + + /// + /// Approve multiple registrations for a meeting. + /// + /// The meeting ID. + /// ID and email for each registrant to be approved. + /// The meeting occurrence id. + /// The cancellation token. + /// + /// The async task. + /// + public Task ApproveRegistrantsAsync(long meetingId, IEnumerable<(string RegistrantId, string RegistrantEmail)> registrantsInfo, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)) + { + var data = new JObject(); + data.AddPropertyIfValue("action", "approve"); + data.AddPropertyIfValue("registrants", registrantsInfo.Select(ri => new { id = ri.RegistrantId, email = ri.RegistrantEmail }).ToArray()); + + return _client + .PostAsync($"meetings/{meetingId}/registrants/status") + .WithArgument("occurence_id", occurrenceId) + .WithJsonBody(data) + .WithCancellationToken(cancellationToken) + .AsMessage(); + } + + /// + /// Reject a registration for a meeting. + /// + /// The meeting ID. + /// The registrant ID. + /// The registrant's email address. + /// The meeting occurrence id. + /// The cancellation token. + /// + /// The async task. + /// + public Task RejectRegistrantAsync(long meetingId, string registrantId, string registrantEmail, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)) + { + return ApproveRegistrantsAsync(meetingId, new[] { (registrantId, registrantEmail) }, occurrenceId, cancellationToken); + } + + /// + /// Reject multiple registrations for a meeting. + /// + /// The meeting ID. + /// ID and email for each registrant to be rejected. + /// The meeting occurrence id. + /// The cancellation token. + /// + /// The async task. + /// + public Task RejectRegistrantsAsync(long meetingId, IEnumerable<(string RegistrantId, string RegistrantEmail)> registrantsInfo, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)) + { + var data = new JObject(); + data.AddPropertyIfValue("action", "deny"); + data.AddPropertyIfValue("registrants", registrantsInfo.Select(ri => new { id = ri.RegistrantId, email = ri.RegistrantEmail }).ToArray()); + + return _client + .PostAsync($"meetings/{meetingId}/registrants/status") + .WithArgument("occurence_id", occurrenceId) + .WithJsonBody(data) + .WithCancellationToken(cancellationToken) + .AsMessage(); + } + + /// + /// Cancel a registration for a meeting. + /// + /// The meeting ID. + /// The registrant ID. + /// The registrant's email address. + /// The meeting occurrence id. + /// The cancellation token. + /// + /// The async task. + /// + public Task CancelRegistrantAsync(long meetingId, string registrantId, string registrantEmail, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)) + { + return ApproveRegistrantsAsync(meetingId, new[] { (registrantId, registrantEmail) }, occurrenceId, cancellationToken); + } + + /// + /// Cancel multiple registrations for a meeting. + /// + /// The meeting ID. + /// ID and email for each registrant to be cancelled. + /// The meeting occurrence id. + /// The cancellation token. + /// + /// The async task. + /// + public Task CancelRegistrantsAsync(long meetingId, IEnumerable<(string RegistrantId, string RegistrantEmail)> registrantsInfo, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)) + { + var data = new JObject(); + data.AddPropertyIfValue("action", "approve"); + data.AddPropertyIfValue("registrants", registrantsInfo.Select(ri => new { id = ri.RegistrantId, email = ri.RegistrantEmail }).ToArray()); + + return _client + .PostAsync($"meetings/{meetingId}/registrants/status") + .WithArgument("occurence_id", occurrenceId) + .WithJsonBody(data) + .WithCancellationToken(cancellationToken) + .AsMessage(); + } + } +} diff --git a/Source/ZoomNet/Utilities/MeetingConverter.cs b/Source/ZoomNet/Utilities/MeetingConverter.cs new file mode 100644 index 00000000..edb06970 --- /dev/null +++ b/Source/ZoomNet/Utilities/MeetingConverter.cs @@ -0,0 +1,119 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using ZoomNet.Models; +using System; +using System.Linq; +using ZoomNet.Models; + +namespace ZoomNet.Utilities +{ + /// + /// Converts a JSON string into and array of meetings. + /// + /// + internal class MeetingConverter : JsonConverter + { + /// + /// Determines whether this instance can convert the specified object type. + /// + /// Type of the object. + /// + /// true if this instance can convert the specified object type; otherwise, false. + /// + public override bool CanConvert(Type objectType) + { + return objectType == typeof(Meeting); + } + + /// + /// Gets a value indicating whether this can read JSON. + /// + /// + /// true if this can read JSON; otherwise, false. + /// + public override bool CanRead + { + get { return true; } + } + + /// + /// Gets a value indicating whether this can write JSON. + /// + /// + /// true if this can write JSON; otherwise, false. + /// + public override bool CanWrite + { + get { return false; } + } + + /// + /// Writes the JSON representation of the object. + /// + /// The to write to. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + + /// + /// Reads the JSON representation of the object. + /// + /// The to read from. + /// Type of the object. + /// The existing value of object being read. + /// The calling serializer. + /// + /// The object value. + /// + /// Unable to determine the field type. + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.StartArray) + { + var jArray = JArray.Load(reader); + var items = jArray + .OfType() + .Select(item => Convert(item, serializer)) + .Where(item => item != null) + .ToArray(); + + return items; + } + else if (reader.TokenType == JsonToken.StartObject) + { + var jObject = JObject.Load(reader); + return Convert(jObject, serializer); + } + + throw new Exception("Unable to convert to Meeting"); + } + + private Meeting Convert(JObject jsonObject, JsonSerializer serializer) + { + jsonObject.TryGetValue("type", StringComparison.OrdinalIgnoreCase, out JToken meetingTypeJsonProperty); + var meetingType = (MeetingType)meetingTypeJsonProperty.ToObject(typeof(MeetingType)); + + var meeting = (Meeting)null; + switch (meetingType) + { + case MeetingType.Instant: + meeting = jsonObject.ToObject(serializer); + break; + case MeetingType.Scheduled: + meeting = jsonObject.ToObject(serializer); + break; + case MeetingType.RecurringFixedTime: + case MeetingType.RecurringNoFixedTime: + meeting = jsonObject.ToObject(serializer); + break; + default: + throw new Exception($"{meetingTypeJsonProperty.ToString()} is an unknown meeting type"); + } + + return meeting; + } + } +} From 844a8fc185fe61e5720ee78c208162f064e93142 Mon Sep 17 00:00:00 2001 From: Jeremie Desautels Date: Wed, 5 Jun 2019 14:42:34 -0400 Subject: [PATCH 25/41] Fix "'default' expression can be simplified" --- Source/ZoomNet/Resources/IMeetings.cs | 31 ++++++++++++------------- Source/ZoomNet/Resources/Meetings.cs | 33 +++++++++++++-------------- 2 files changed, 31 insertions(+), 33 deletions(-) diff --git a/Source/ZoomNet/Resources/IMeetings.cs b/Source/ZoomNet/Resources/IMeetings.cs index ff25661c..0b86616a 100644 --- a/Source/ZoomNet/Resources/IMeetings.cs +++ b/Source/ZoomNet/Resources/IMeetings.cs @@ -1,4 +1,3 @@ -using ZoomNet.Models; using System; using System.Collections.Generic; using System.Threading; @@ -29,7 +28,7 @@ public interface IMeetings /// /// This call omits 'occurrences'. Therefore the 'Occurrences' property will be empty. /// - Task> GetAllAsync(string userId, MeetingListType type = MeetingListType.Scheduled, int recordsPerPage = 30, int page = 1, CancellationToken cancellationToken = default(CancellationToken)); + Task> GetAllAsync(string userId, MeetingListType type = MeetingListType.Scheduled, int recordsPerPage = 30, int page = 1, CancellationToken cancellationToken = default); /// /// Creates an instant meeting for a user. @@ -45,7 +44,7 @@ public interface IMeetings /// The new meeting. /// /// Thrown when an exception occured while creating the meeting. - Task CreateInstantMeetingAsync(string userId, string topic, string agenda, string password = null, MeetingSettings settings = null, IDictionary trackingFields = null, CancellationToken cancellationToken = default(CancellationToken)); + Task CreateInstantMeetingAsync(string userId, string topic, string agenda, string password = null, MeetingSettings settings = null, IDictionary trackingFields = null, CancellationToken cancellationToken = default); /// /// Creates a scheduled meeting for a user. @@ -63,7 +62,7 @@ public interface IMeetings /// The new meeting. /// /// Thrown when an exception occured while creating the meeting. - Task CreateScheduledMeetingAsync(string userId, string topic, string agenda, DateTime start, int duration, string password = null, MeetingSettings settings = null, IDictionary trackingFields = null, CancellationToken cancellationToken = default(CancellationToken)); + Task CreateScheduledMeetingAsync(string userId, string topic, string agenda, DateTime start, int duration, string password = null, MeetingSettings settings = null, IDictionary trackingFields = null, CancellationToken cancellationToken = default); /// /// Creates a recurring meeting for a user. @@ -82,7 +81,7 @@ public interface IMeetings /// The new meeting. /// /// Thrown when an exception occured while creating the meeting. - Task CreateRecurringMeetingAsync(string userId, string topic, string agenda, DateTime? start, int duration, RecurrenceInfo recurrence, string password = null, MeetingSettings settings = null, IDictionary trackingFields = null, CancellationToken cancellationToken = default(CancellationToken)); + Task CreateRecurringMeetingAsync(string userId, string topic, string agenda, DateTime? start, int duration, RecurrenceInfo recurrence, string password = null, MeetingSettings settings = null, IDictionary trackingFields = null, CancellationToken cancellationToken = default); /// /// Retrieve the details of a meeting. @@ -94,7 +93,7 @@ public interface IMeetings /// /// The . /// - Task GetAsync(string userId, long meetingId, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)); + Task GetAsync(string userId, long meetingId, string occurrenceId = null, CancellationToken cancellationToken = default); /// /// Delete a meeting. @@ -106,7 +105,7 @@ public interface IMeetings /// /// The async task. /// - Task DeleteAsync(string userId, long meetingId, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)); + Task DeleteAsync(string userId, long meetingId, string occurrenceId = null, CancellationToken cancellationToken = default); /// /// End a meeting. @@ -116,7 +115,7 @@ public interface IMeetings /// /// The async task. /// - Task EndAsync(long meetingId, CancellationToken cancellationToken = default(CancellationToken)); + Task EndAsync(long meetingId, CancellationToken cancellationToken = default); /// /// List registrants of a meeting. @@ -130,7 +129,7 @@ public interface IMeetings /// /// An array of . /// - Task> GetRegistrantsAsync(long meetingId, RegistrantStatus status, string occurrenceId = null, int recordsPerPage = 30, int page = 1, CancellationToken cancellationToken = default(CancellationToken)); + Task> GetRegistrantsAsync(long meetingId, RegistrantStatus status, string occurrenceId = null, int recordsPerPage = 30, int page = 1, CancellationToken cancellationToken = default); /// /// List registrants of a meeting. @@ -144,7 +143,7 @@ public interface IMeetings /// /// A . /// - Task AddRegistrantsAsync(long meetingId, string email, string firstName, string lastName, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)); + Task AddRegistrantsAsync(long meetingId, string email, string firstName, string lastName, string occurrenceId = null, CancellationToken cancellationToken = default); /// /// Approve a registration for a meeting. @@ -157,7 +156,7 @@ public interface IMeetings /// /// The async task. /// - Task ApproveRegistrantAsync(long meetingId, string registrantId, string registrantEmail, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)); + Task ApproveRegistrantAsync(long meetingId, string registrantId, string registrantEmail, string occurrenceId = null, CancellationToken cancellationToken = default); /// /// Approve multiple registrations for a meeting. @@ -169,7 +168,7 @@ public interface IMeetings /// /// The async task. /// - Task ApproveRegistrantsAsync(long meetingId, IEnumerable<(string RegistrantId, string RegistrantEmail)> registrantsInfo, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)); + Task ApproveRegistrantsAsync(long meetingId, IEnumerable<(string RegistrantId, string RegistrantEmail)> registrantsInfo, string occurrenceId = null, CancellationToken cancellationToken = default); /// /// Reject a registration for a meeting. @@ -182,7 +181,7 @@ public interface IMeetings /// /// The async task. /// - Task RejectRegistrantAsync(long meetingId, string registrantId, string registrantEmail, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)); + Task RejectRegistrantAsync(long meetingId, string registrantId, string registrantEmail, string occurrenceId = null, CancellationToken cancellationToken = default); /// /// Reject multiple registrations for a meeting. @@ -194,7 +193,7 @@ public interface IMeetings /// /// The async task. /// - Task RejectRegistrantsAsync(long meetingId, IEnumerable<(string RegistrantId, string RegistrantEmail)> registrantsInfo, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)); + Task RejectRegistrantsAsync(long meetingId, IEnumerable<(string RegistrantId, string RegistrantEmail)> registrantsInfo, string occurrenceId = null, CancellationToken cancellationToken = default); /// /// Cancel a registration for a meeting. @@ -207,7 +206,7 @@ public interface IMeetings /// /// The async task. /// - Task CancelRegistrantAsync(long meetingId, string registrantId, string registrantEmail, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)); + Task CancelRegistrantAsync(long meetingId, string registrantId, string registrantEmail, string occurrenceId = null, CancellationToken cancellationToken = default); /// /// Cancel multiple registrations for a meeting. @@ -219,6 +218,6 @@ public interface IMeetings /// /// The async task. /// - Task CancelRegistrantsAsync(long meetingId, IEnumerable<(string RegistrantId, string RegistrantEmail)> registrantsInfo, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)); + Task CancelRegistrantsAsync(long meetingId, IEnumerable<(string RegistrantId, string RegistrantEmail)> registrantsInfo, string occurrenceId = null, CancellationToken cancellationToken = default); } } diff --git a/Source/ZoomNet/Resources/Meetings.cs b/Source/ZoomNet/Resources/Meetings.cs index eac31173..51cc749c 100644 --- a/Source/ZoomNet/Resources/Meetings.cs +++ b/Source/ZoomNet/Resources/Meetings.cs @@ -1,7 +1,6 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Pathoschild.Http.Client; -using ZoomNet.Models; using System; using System.Collections.Generic; using System.Linq; @@ -43,7 +42,7 @@ internal Meetings(Pathoschild.Http.Client.IClient client) /// /// An array of . /// - public Task> GetAllAsync(string userId, MeetingListType type = MeetingListType.Scheduled, int recordsPerPage = 30, int page = 1, CancellationToken cancellationToken = default(CancellationToken)) + public Task> GetAllAsync(string userId, MeetingListType type = MeetingListType.Scheduled, int recordsPerPage = 30, int page = 1, CancellationToken cancellationToken = default) { if (recordsPerPage < 1 || recordsPerPage > 300) { @@ -80,7 +79,7 @@ public Task CreateInstantMeetingAsync( string password = null, MeetingSettings settings = null, IDictionary trackingFields = null, - CancellationToken cancellationToken = default(CancellationToken)) + CancellationToken cancellationToken = default) { var data = new JObject() { @@ -124,7 +123,7 @@ public Task CreateScheduledMeetingAsync( string password = null, MeetingSettings settings = null, IDictionary trackingFields = null, - CancellationToken cancellationToken = default(CancellationToken)) + CancellationToken cancellationToken = default) { var data = new JObject() { @@ -173,7 +172,7 @@ public Task CreateRecurringMeetingAsync( string password = null, MeetingSettings settings = null, IDictionary trackingFields = null, - CancellationToken cancellationToken = default(CancellationToken)) + CancellationToken cancellationToken = default) { var data = new JObject() { @@ -208,7 +207,7 @@ public Task CreateRecurringMeetingAsync( /// /// The . /// - public Task GetAsync(string userId, long meetingId, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)) + public Task GetAsync(string userId, long meetingId, string occurrenceId = null, CancellationToken cancellationToken = default) { return _client .GetAsync($"meetings/{meetingId}") @@ -227,7 +226,7 @@ public Task CreateRecurringMeetingAsync( /// /// The async task. /// - public Task DeleteAsync(string userId, long meetingId, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)) + public Task DeleteAsync(string userId, long meetingId, string occurrenceId = null, CancellationToken cancellationToken = default) { return _client .DeleteAsync($"meetings/{meetingId}") @@ -244,7 +243,7 @@ public Task CreateRecurringMeetingAsync( /// /// The async task. /// - public Task EndAsync(long meetingId, CancellationToken cancellationToken = default(CancellationToken)) + public Task EndAsync(long meetingId, CancellationToken cancellationToken = default) { return _client .PutAsync($"meetings/{meetingId}/status") @@ -265,7 +264,7 @@ public Task CreateRecurringMeetingAsync( /// /// An array of . /// - public Task> GetRegistrantsAsync(long meetingId, RegistrantStatus status, string occurrenceId = null, int recordsPerPage = 30, int page = 1, CancellationToken cancellationToken = default(CancellationToken)) + public Task> GetRegistrantsAsync(long meetingId, RegistrantStatus status, string occurrenceId = null, int recordsPerPage = 30, int page = 1, CancellationToken cancellationToken = default) { if (recordsPerPage < 1 || recordsPerPage > 300) { @@ -294,7 +293,7 @@ public Task CreateRecurringMeetingAsync( /// /// A . /// - public Task AddRegistrantsAsync(long meetingId, string email, string firstName, string lastName, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)) + public Task AddRegistrantsAsync(long meetingId, string email, string firstName, string lastName, string occurrenceId = null, CancellationToken cancellationToken = default) { var data = new JObject(); data.AddPropertyIfValue("email", email); @@ -320,7 +319,7 @@ public Task CreateRecurringMeetingAsync( /// /// The async task. /// - public Task ApproveRegistrantAsync(long meetingId, string registrantId, string registrantEmail, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)) + public Task ApproveRegistrantAsync(long meetingId, string registrantId, string registrantEmail, string occurrenceId = null, CancellationToken cancellationToken = default) { return ApproveRegistrantsAsync(meetingId, new[] { (registrantId, registrantEmail) }, occurrenceId, cancellationToken); } @@ -335,7 +334,7 @@ public Task CreateRecurringMeetingAsync( /// /// The async task. /// - public Task ApproveRegistrantsAsync(long meetingId, IEnumerable<(string RegistrantId, string RegistrantEmail)> registrantsInfo, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)) + public Task ApproveRegistrantsAsync(long meetingId, IEnumerable<(string RegistrantId, string RegistrantEmail)> registrantsInfo, string occurrenceId = null, CancellationToken cancellationToken = default) { var data = new JObject(); data.AddPropertyIfValue("action", "approve"); @@ -360,7 +359,7 @@ public Task CreateRecurringMeetingAsync( /// /// The async task. /// - public Task RejectRegistrantAsync(long meetingId, string registrantId, string registrantEmail, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)) + public Task RejectRegistrantAsync(long meetingId, string registrantId, string registrantEmail, string occurrenceId = null, CancellationToken cancellationToken = default) { return ApproveRegistrantsAsync(meetingId, new[] { (registrantId, registrantEmail) }, occurrenceId, cancellationToken); } @@ -375,7 +374,7 @@ public Task CreateRecurringMeetingAsync( /// /// The async task. /// - public Task RejectRegistrantsAsync(long meetingId, IEnumerable<(string RegistrantId, string RegistrantEmail)> registrantsInfo, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)) + public Task RejectRegistrantsAsync(long meetingId, IEnumerable<(string RegistrantId, string RegistrantEmail)> registrantsInfo, string occurrenceId = null, CancellationToken cancellationToken = default) { var data = new JObject(); data.AddPropertyIfValue("action", "deny"); @@ -400,7 +399,7 @@ public Task CreateRecurringMeetingAsync( /// /// The async task. /// - public Task CancelRegistrantAsync(long meetingId, string registrantId, string registrantEmail, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)) + public Task CancelRegistrantAsync(long meetingId, string registrantId, string registrantEmail, string occurrenceId = null, CancellationToken cancellationToken = default) { return ApproveRegistrantsAsync(meetingId, new[] { (registrantId, registrantEmail) }, occurrenceId, cancellationToken); } @@ -415,7 +414,7 @@ public Task CreateRecurringMeetingAsync( /// /// The async task. /// - public Task CancelRegistrantsAsync(long meetingId, IEnumerable<(string RegistrantId, string RegistrantEmail)> registrantsInfo, string occurrenceId = null, CancellationToken cancellationToken = default(CancellationToken)) + public Task CancelRegistrantsAsync(long meetingId, IEnumerable<(string RegistrantId, string RegistrantEmail)> registrantsInfo, string occurrenceId = null, CancellationToken cancellationToken = default) { var data = new JObject(); data.AddPropertyIfValue("action", "approve"); From edf687ed0b56b296b95b4739bc45986d5b09706e Mon Sep 17 00:00:00 2001 From: Jeremie Desautels Date: Wed, 5 Jun 2019 14:42:53 -0400 Subject: [PATCH 26/41] Remove unused 'using' statements --- Source/ZoomNet/Models/Meeting.cs | 3 +-- Source/ZoomNet/Models/RecurrenceInfo.cs | 4 ++-- Source/ZoomNet/Models/Registrant.cs | 3 +-- Source/ZoomNet/Utilities/MeetingConverter.cs | 3 +-- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/Source/ZoomNet/Models/Meeting.cs b/Source/ZoomNet/Models/Meeting.cs index ee3ea81d..ee0be0ce 100644 --- a/Source/ZoomNet/Models/Meeting.cs +++ b/Source/ZoomNet/Models/Meeting.cs @@ -1,5 +1,4 @@ -using Newtonsoft.Json; -using ZoomNet.Models; +using Newtonsoft.Json; using System; namespace ZoomNet.Models diff --git a/Source/ZoomNet/Models/RecurrenceInfo.cs b/Source/ZoomNet/Models/RecurrenceInfo.cs index 636d44aa..7c526ee7 100644 --- a/Source/ZoomNet/Models/RecurrenceInfo.cs +++ b/Source/ZoomNet/Models/RecurrenceInfo.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using System; namespace ZoomNet.Models @@ -37,7 +37,7 @@ public class RecurrenceInfo public int? MonthlyDay { get; set; } /// - /// Gets or sets the week for which the meeting should recur each month, + /// Gets or sets the week for which the meeting should recur each month. /// [JsonProperty(PropertyName = "monthly_week")] public int? MonthlyWeek { get; set; } diff --git a/Source/ZoomNet/Models/Registrant.cs b/Source/ZoomNet/Models/Registrant.cs index 1b969b84..99e5e6f6 100644 --- a/Source/ZoomNet/Models/Registrant.cs +++ b/Source/ZoomNet/Models/Registrant.cs @@ -1,5 +1,4 @@ -using Newtonsoft.Json; -using ZoomNet.Models; +using Newtonsoft.Json; using System; using System.Collections.Generic; diff --git a/Source/ZoomNet/Utilities/MeetingConverter.cs b/Source/ZoomNet/Utilities/MeetingConverter.cs index edb06970..241813e9 100644 --- a/Source/ZoomNet/Utilities/MeetingConverter.cs +++ b/Source/ZoomNet/Utilities/MeetingConverter.cs @@ -1,6 +1,5 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using ZoomNet.Models; using System; using System.Linq; using ZoomNet.Models; From c8869d635df0e27dadff64cdb0c12812dd01e8b3 Mon Sep 17 00:00:00 2001 From: Jeremie Desautels Date: Wed, 5 Jun 2019 14:53:46 -0400 Subject: [PATCH 27/41] Integration test to get all meetings --- .../Tests/Meetings.cs | 38 +++++++++++++++++++ .../ZoomNet.IntegrationTests/TestsRunner.cs | 2 + .../ZoomNet.IntegrationTests.csproj | 4 -- 3 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 Source/ZoomNet.IntegrationTests/Tests/Meetings.cs diff --git a/Source/ZoomNet.IntegrationTests/Tests/Meetings.cs b/Source/ZoomNet.IntegrationTests/Tests/Meetings.cs new file mode 100644 index 00000000..7465cefb --- /dev/null +++ b/Source/ZoomNet.IntegrationTests/Tests/Meetings.cs @@ -0,0 +1,38 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using ZoomNet.Models; + +namespace ZoomNet.IntegrationTests.Tests +{ + public class Meetings : IIntegrationTest + { + public async Task RunAsync(IClient client, TextWriter log, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) return; + + await log.WriteLineAsync("\n***** MEETINGS *****\n").ConfigureAwait(false); + + // GET ALL THE MEETINGS + var paginatedScheduledMeetings = await client.Meetings.GetAllAsync(userId, MeetingListType.Scheduled, 100, 1, cancellationToken).ConfigureAwait(false); + await log.WriteLineAsync($"There are {paginatedScheduledMeetings.TotalRecords} scheduled meetings").ConfigureAwait(false); + + var paginatedLiveMeetings = await client.Meetings.GetAllAsync(userId, MeetingListType.Live, 100, 1, cancellationToken).ConfigureAwait(false); + await log.WriteLineAsync($"There are {paginatedLiveMeetings.TotalRecords} live meetings").ConfigureAwait(false); + + var paginatedUpcomingMeetings = await client.Meetings.GetAllAsync(userId, MeetingListType.Upcoming, 100, 1, cancellationToken).ConfigureAwait(false); + await log.WriteLineAsync($"There are {paginatedUpcomingMeetings.TotalRecords} upcoming meetings").ConfigureAwait(false); + + // CLEANUP PREVIOUS INTEGRATION TESTS THAT MIGHT HAVE BEEN INTERRUPTED BEFORE THEY HAD TIME TO CLEANUP AFTER THEMSELVES + //var cleanUpTasks = paginatedScheduledMeetings.Records + // .Where(m => m.Name.StartsWith("ZoomNet Integration Testing:")) + // .Select(async oldMeeting => + // { + // await client.Meetings.DeleteAsync(userId, oldAccount.Id, null, cancellationToken).ConfigureAwait(false); + // await log.WriteLineAsync($"Meeting {oldMeeting.Id} deleted").ConfigureAwait(false); + // await Task.Delay(250).ConfigureAwait(false); // Brief pause to ensure Zoom has time to catch up + // }); + //await Task.WhenAll(cleanUpTasks).ConfigureAwait(false); + } + } +} diff --git a/Source/ZoomNet.IntegrationTests/TestsRunner.cs b/Source/ZoomNet.IntegrationTests/TestsRunner.cs index 4b4c83ef..230b3d46 100644 --- a/Source/ZoomNet.IntegrationTests/TestsRunner.cs +++ b/Source/ZoomNet.IntegrationTests/TestsRunner.cs @@ -5,6 +5,7 @@ using System.Net; using System.Threading; using System.Threading.Tasks; +using ZoomNet.Resources; using ZoomNet.Utilities; namespace ZoomNet.IntegrationTests @@ -58,6 +59,7 @@ public async Task RunAsync() // These are the integration tests that we will execute var integrationTests = new Type[] { + typeof(Meetings), }; // Execute the async tests in parallel (with max degree of parallelism) diff --git a/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj b/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj index bc39a4e3..da61362f 100644 --- a/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj +++ b/Source/ZoomNet.IntegrationTests/ZoomNet.IntegrationTests.csproj @@ -21,8 +21,4 @@ - - - - From 416b448efa9082c1c290ee74c954ef37c3459e63 Mon Sep 17 00:00:00 2001 From: Jeremie Desautels Date: Wed, 5 Jun 2019 15:40:53 -0400 Subject: [PATCH 28/41] Create meetings during integration testing --- .../Tests/Meetings.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Source/ZoomNet.IntegrationTests/Tests/Meetings.cs b/Source/ZoomNet.IntegrationTests/Tests/Meetings.cs index 7465cefb..b3ec1709 100644 --- a/Source/ZoomNet.IntegrationTests/Tests/Meetings.cs +++ b/Source/ZoomNet.IntegrationTests/Tests/Meetings.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -33,6 +35,23 @@ public async Task RunAsync(IClient client, TextWriter log, CancellationToken can // await Task.Delay(250).ConfigureAwait(false); // Brief pause to ensure Zoom has time to catch up // }); //await Task.WhenAll(cleanUpTasks).ConfigureAwait(false); + + var settings = new MeetingSettings() + { + ApprovalType = MeetingApprovalType.Manual + }; + var trackingFields = new Dictionary() + { + { "field1", "value1"}, + { "field2", "value2"} + }; + var newInstantMeeting = await client.Meetings.CreateInstantMeetingAsync(userId, "ZoomNet Integration Testing: instant meeting", "The agenda", "p@ss!w0rd", settings, trackingFields, cancellationToken).ConfigureAwait(false); + await log.WriteLineAsync($"Instant meeting {newInstantMeeting.Id} created").ConfigureAwait(false); + + var start = DateTime.UtcNow.AddMonths(1); + var duration = 30; + var newScheduledMeeting = await client.Meetings.CreateScheduledMeetingAsync(userId, "ZoomNet Integration Testing: scheduled meeting", "The agenda", start, duration, "p@ss!w0rd", settings, trackingFields, cancellationToken).ConfigureAwait(false); + await log.WriteLineAsync($"Scheduled meeting {newScheduledMeeting.Id} created").ConfigureAwait(false); } } } From bac3f4fd9b08e842ac5f823582deb0b532e450c5 Mon Sep 17 00:00:00 2001 From: Jeremie Desautels Date: Mon, 17 Jun 2019 16:29:17 -0400 Subject: [PATCH 29/41] Add recurrence info to RecurringMeeting Regarding #1 --- .../Tests/Meetings.cs | 9 + Source/ZoomNet/Models/RecurrenceInfo.cs | 25 +- Source/ZoomNet/Models/RecurrenceType.cs | 23 ++ Source/ZoomNet/Models/RecurringMeeting.cs | 8 +- .../ZoomNet/Utilities/DaysOfWeekConverter.cs | 229 ++++++++++++++++++ 5 files changed, 282 insertions(+), 12 deletions(-) create mode 100644 Source/ZoomNet/Models/RecurrenceType.cs create mode 100644 Source/ZoomNet/Utilities/DaysOfWeekConverter.cs diff --git a/Source/ZoomNet.IntegrationTests/Tests/Meetings.cs b/Source/ZoomNet.IntegrationTests/Tests/Meetings.cs index b3ec1709..69e499ce 100644 --- a/Source/ZoomNet.IntegrationTests/Tests/Meetings.cs +++ b/Source/ZoomNet.IntegrationTests/Tests/Meetings.cs @@ -52,6 +52,15 @@ public async Task RunAsync(IClient client, TextWriter log, CancellationToken can var duration = 30; var newScheduledMeeting = await client.Meetings.CreateScheduledMeetingAsync(userId, "ZoomNet Integration Testing: scheduled meeting", "The agenda", start, duration, "p@ss!w0rd", settings, trackingFields, cancellationToken).ConfigureAwait(false); await log.WriteLineAsync($"Scheduled meeting {newScheduledMeeting.Id} created").ConfigureAwait(false); + + var recurrenceInfo = new RecurrenceInfo() + { + EndTimes = 2, + WeeklyDays = new[] { DayOfWeek.Monday, DayOfWeek.Friday }, + Type = RecurrenceType.Weekly + }; + var newRecurringMeeting = await client.Meetings.CreateRecurringMeetingAsync(userId, "ZoomNet Integration Testing: recurring meeting", "The agenda", start, duration, recurrenceInfo, "p@ss!w0rd", settings, trackingFields, cancellationToken).ConfigureAwait(false); + await log.WriteLineAsync($"Recurring meeting {newRecurringMeeting.Id} created").ConfigureAwait(false); } } } diff --git a/Source/ZoomNet/Models/RecurrenceInfo.cs b/Source/ZoomNet/Models/RecurrenceInfo.cs index 7c526ee7..3e313122 100644 --- a/Source/ZoomNet/Models/RecurrenceInfo.cs +++ b/Source/ZoomNet/Models/RecurrenceInfo.cs @@ -1,5 +1,6 @@ using Newtonsoft.Json; using System; +using ZoomNet.Utilities; namespace ZoomNet.Models { @@ -11,8 +12,8 @@ public class RecurrenceInfo /// /// Gets or sets the recurrence type. /// - [JsonProperty(PropertyName = "type")] - public int? Type { get; set; } + [JsonProperty(PropertyName = "type", NullValueHandling = NullValueHandling.Ignore)] + public RecurrenceType Type { get; set; } /// /// Gets or sets the interval should the meeting repeat. @@ -20,46 +21,48 @@ public class RecurrenceInfo /// For a weekly meeting, max of 12 weeks. /// For a monthly meeting, max of 3 months. /// - [JsonProperty(PropertyName = "repeat_interval")] + [JsonProperty(PropertyName = "repeat_interval", NullValueHandling = NullValueHandling.Ignore)] public int? RepeatInterval { get; set; } /// /// Gets or sets the days of the week the meeting should repeat, multiple values separated by comma. /// - [JsonProperty(PropertyName = "weekly_days")] - public int? WeeklyDays { get; set; } + [JsonProperty(PropertyName = "weekly_days", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(DaysOfWeekConverter))] + public DayOfWeek[] WeeklyDays { get; set; } /// /// Gets or sets the day of the month for the meeting to be scheduled. /// The value range is from 1 to 31. /// - [JsonProperty(PropertyName = "monthly_day")] + [JsonProperty(PropertyName = "monthly_day", NullValueHandling = NullValueHandling.Ignore)] public int? MonthlyDay { get; set; } /// /// Gets or sets the week for which the meeting should recur each month. /// - [JsonProperty(PropertyName = "monthly_week")] + [JsonProperty(PropertyName = "monthly_week", NullValueHandling = NullValueHandling.Ignore)] public int? MonthlyWeek { get; set; } /// /// Gets or sets the day for which the meeting should recur each month. /// - [JsonProperty(PropertyName = "monthly_week_day")] - public int? MonthlyWeekDay { get; set; } + [JsonProperty(PropertyName = "monthly_week_day", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(DaysOfWeekConverter))] + public DayOfWeek? MonthlyWeekDay { get; set; } /// /// Gets or sets the select how many times the meeting will occur before it is canceled. /// Cannot be used with "end_date_time". /// - [JsonProperty(PropertyName = "end_times")] + [JsonProperty(PropertyName = "end_times", NullValueHandling = NullValueHandling.Ignore)] public int? EndTimes { get; set; } /// /// Gets or sets the date the meeting will canceled. /// Cannot be used with "end_times". /// - [JsonProperty(PropertyName = "end_date_time")] + [JsonProperty(PropertyName = "end_date_time", NullValueHandling = NullValueHandling.Ignore)] public DateTime? EndDateTime { get; set; } } } diff --git a/Source/ZoomNet/Models/RecurrenceType.cs b/Source/ZoomNet/Models/RecurrenceType.cs new file mode 100644 index 00000000..c80afe76 --- /dev/null +++ b/Source/ZoomNet/Models/RecurrenceType.cs @@ -0,0 +1,23 @@ +namespace ZoomNet.Models +{ + /// + /// Enumeration to indicate the type of recurrence. + /// + public enum RecurrenceType + { + /// + /// Daily. + /// + Daily = 1, + + /// + /// Weekly. + /// + Weekly = 2, + + /// + /// Monthly. + /// + Monthly = 3 + } +} diff --git a/Source/ZoomNet/Models/RecurringMeeting.cs b/Source/ZoomNet/Models/RecurringMeeting.cs index 457016b1..f7f5c99c 100644 --- a/Source/ZoomNet/Models/RecurringMeeting.cs +++ b/Source/ZoomNet/Models/RecurringMeeting.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; namespace ZoomNet.Models { @@ -22,5 +22,11 @@ public class RecurringMeeting : Meeting /// [JsonProperty(PropertyName = "occurrences", NullValueHandling = NullValueHandling.Ignore)] public MeetingOccurrence[] Occurrences { get; set; } + + /// + /// Gets or sets the recurrence info. + /// + [JsonProperty(PropertyName = "recurrence", NullValueHandling = NullValueHandling.Ignore)] + public RecurrenceInfo RecurrenceInfo { get; set; } } } diff --git a/Source/ZoomNet/Utilities/DaysOfWeekConverter.cs b/Source/ZoomNet/Utilities/DaysOfWeekConverter.cs new file mode 100644 index 00000000..228dc4f1 --- /dev/null +++ b/Source/ZoomNet/Utilities/DaysOfWeekConverter.cs @@ -0,0 +1,229 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Linq; + +namespace ZoomNet.Utilities +{ + /// + /// Converts a JSON string into and array of days of the week. + /// + /// + internal class DaysOfWeekConverter : JsonConverter + { + /// + /// Determines whether this instance can convert the specified object type. + /// + /// Type of the object. + /// + /// true if this instance can convert the specified object type; otherwise, false. + /// + public override bool CanConvert(Type objectType) + { + return objectType == typeof(DayOfWeek) || objectType == typeof(DayOfWeek[]); + } + + /// + /// Gets a value indicating whether this can read JSON. + /// + /// + /// true if this can read JSON; otherwise, false. + /// + public override bool CanRead + { + get { return true; } + } + + /// + /// Gets a value indicating whether this can write JSON. + /// + /// + /// true if this can write JSON; otherwise, false. + /// + public override bool CanWrite + { + get { return true; } + } + + /// + /// Writes the JSON representation of the object. + /// + /// The to write to. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + /* + IMPORTANT: the values in System.DayOfWeek start at zero (i.e.: Sunday=0, Monday=1, ..., Saturday=6) + but the values expected by the Zoom API start at one (i.e.: Sunday=1, Monday=2, ..., Saturday=7). + */ + switch (value) + { + case DayOfWeek dayOfWeek: + var singleDay = (Convert.ToInt32(dayOfWeek) + 1).ToString(); + serializer.Serialize(writer, singleDay); + break; + case DayOfWeek[] daysOfWeek: + var multipleDays = string.Join(",", daysOfWeek.Select(day => (Convert.ToInt32(day) + 1).ToString())); + serializer.Serialize(writer, multipleDays); + break; + default: + throw new Exception("Unable to serialize the value"); + } + } + + /// + /// Reads the JSON representation of the object. + /// + /// The to read from. + /// Type of the object. + /// The existing value of object being read. + /// The calling serializer. + /// + /// The object value. + /// + /// Unable to determine the field type. + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + /* + IMPORTANT: the values in System.DayOfWeek start at zero (i.e.: Sunday=0, Monday=1, ..., Saturday=6) + but the values returned by the Zoom API start at one (i.e.: Sunday=1, Monday=2, ..., Saturday=7). + */ + + var rawValue = reader.Value as string; + + if (objectType == typeof(DayOfWeek)) + { + var value = Convert.ToInt32(rawValue) - 1; + return (DayOfWeek)value; + } + else if (objectType == typeof(DayOfWeek[])) + { + var values = rawValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + return values + .Select(value => Convert.ToInt32(value) - 1) + .Select(value => (DayOfWeek)value) + .ToArray(); + } + + JValue jValue = new JValue(reader.Value); + switch (reader.TokenType) + { + //case JsonToken.Integer: + // value = Convert.ToInt32(reader.Value); + // break; + //case JsonToken.Float: + // value = Convert.ToDecimal(reader.Value); + // break; + //case JsonToken.String: + // value = Convert.ToString(reader.Value); + // break; + //case JsonToken.Boolean: + // value = Convert.ToBoolean(reader.Value); + // break; + //case JsonToken.Null: + // value = null; + // break; + //case JsonToken.Date: + // value = Convert.ToDateTime(reader.Value); + // break; + //case JsonToken.Bytes: + // value = Convert.ToByte(reader.Value); + // break; + default: + Console.WriteLine("Default case"); + Console.WriteLine(reader.TokenType.ToString()); + break; + } + + //if (reader.TokenType == JsonToken.String) + //{ + // var jValue = JV JArray.Load(reader); + + // if (objectType == typeof(DayOfWeek)) + // { + // } + // else if objectType == typeof(DayOfWeek[]) + // { + // } + // case DayOfWeek dayOfWeek: + // var singleDay = (Convert.ToInt32(dayOfWeek) + 1).ToString(); + // serializer.Serialize(writer, singleDay); + // break; + // case DayOfWeek[] daysOfWeek: + // var multipleDays = string.Join(",", daysOfWeek.Select(day => (Convert.ToInt32(day) + 1).ToString())); + // serializer.Serialize(writer, multipleDays); + // break; + // default: + // throw new Exception("Unable to serialize the value"); + //} + + //if (reader.TokenType == JsonToken.String) + //{ + // var jArray = JArray.Load(reader); + // var items = jArray + // .OfType() + // .Select(item => Convert(item, serializer)) + // .Where(item => item != null) + // .ToArray(); + + // return items; + //} + //else if (reader.TokenType == JsonToken.StartObject) + //{ + // var jObject = JObject.Load(reader); + // return Convert(jObject, serializer); + //} + + throw new Exception("Unable to deserialize the value"); + } + + //private Event Convert(JObject jsonObject, JsonSerializer serializer) + //{ + // jsonObject.TryGetValue("event_name", StringComparison.OrdinalIgnoreCase, out JToken eventTypeJsonProperty); + // var eventType = (EventType)eventTypeJsonProperty.ToObject(typeof(EventType)); + + // var emailActivityEvent = (Event)null; + // switch (eventType) + // { + // case EventType.Bounce: + // emailActivityEvent = jsonObject.ToObject(serializer); + // break; + // case EventType.Open: + // emailActivityEvent = jsonObject.ToObject(serializer); + // break; + // case EventType.Click: + // emailActivityEvent = jsonObject.ToObject(serializer); + // break; + // case EventType.Processed: + // emailActivityEvent = jsonObject.ToObject(serializer); + // break; + // case EventType.Dropped: + // emailActivityEvent = jsonObject.ToObject(serializer); + // break; + // case EventType.Delivered: + // emailActivityEvent = jsonObject.ToObject(serializer); + // break; + // case EventType.Deferred: + // emailActivityEvent = jsonObject.ToObject(serializer); + // break; + // case EventType.SpamReport: + // emailActivityEvent = jsonObject.ToObject(serializer); + // break; + // case EventType.Unsubscribe: + // emailActivityEvent = jsonObject.ToObject(serializer); + // break; + // case EventType.GroupUnsubscribe: + // emailActivityEvent = jsonObject.ToObject(serializer); + // break; + // case EventType.GroupResubscribe: + // emailActivityEvent = jsonObject.ToObject(serializer); + // break; + // default: + // throw new Exception($"{eventTypeJsonProperty.ToString()} is an unknown event type"); + // } + + // return emailActivityEvent; + //} + } +} From 39744f58aed75f48d8cecc08b1c677ca3af2df8f Mon Sep 17 00:00:00 2001 From: Jeremie Desautels Date: Tue, 18 Jun 2019 10:40:56 -0400 Subject: [PATCH 30/41] Simplify MeetingConverter --- Source/ZoomNet/Utilities/MeetingConverter.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/Source/ZoomNet/Utilities/MeetingConverter.cs b/Source/ZoomNet/Utilities/MeetingConverter.cs index 241813e9..68b87cf4 100644 --- a/Source/ZoomNet/Utilities/MeetingConverter.cs +++ b/Source/ZoomNet/Utilities/MeetingConverter.cs @@ -95,24 +95,18 @@ private Meeting Convert(JObject jsonObject, JsonSerializer serializer) jsonObject.TryGetValue("type", StringComparison.OrdinalIgnoreCase, out JToken meetingTypeJsonProperty); var meetingType = (MeetingType)meetingTypeJsonProperty.ToObject(typeof(MeetingType)); - var meeting = (Meeting)null; switch (meetingType) { case MeetingType.Instant: - meeting = jsonObject.ToObject(serializer); - break; + return jsonObject.ToObject(serializer); case MeetingType.Scheduled: - meeting = jsonObject.ToObject(serializer); - break; + return jsonObject.ToObject(serializer); case MeetingType.RecurringFixedTime: case MeetingType.RecurringNoFixedTime: - meeting = jsonObject.ToObject(serializer); - break; + return jsonObject.ToObject(serializer); default: throw new Exception($"{meetingTypeJsonProperty.ToString()} is an unknown meeting type"); } - - return meeting; } } } From 4a32243e3900bc704c6eb6f97111765c25115aa5 Mon Sep 17 00:00:00 2001 From: Jeremie Desautels Date: Tue, 18 Jun 2019 11:13:29 -0400 Subject: [PATCH 31/41] Cleanup during integration tests --- .../Tests/Meetings.cs | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/Source/ZoomNet.IntegrationTests/Tests/Meetings.cs b/Source/ZoomNet.IntegrationTests/Tests/Meetings.cs index 69e499ce..37ca307f 100644 --- a/Source/ZoomNet.IntegrationTests/Tests/Meetings.cs +++ b/Source/ZoomNet.IntegrationTests/Tests/Meetings.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using ZoomNet.Models; @@ -26,15 +27,16 @@ public async Task RunAsync(IClient client, TextWriter log, CancellationToken can await log.WriteLineAsync($"There are {paginatedUpcomingMeetings.TotalRecords} upcoming meetings").ConfigureAwait(false); // CLEANUP PREVIOUS INTEGRATION TESTS THAT MIGHT HAVE BEEN INTERRUPTED BEFORE THEY HAD TIME TO CLEANUP AFTER THEMSELVES - //var cleanUpTasks = paginatedScheduledMeetings.Records - // .Where(m => m.Name.StartsWith("ZoomNet Integration Testing:")) - // .Select(async oldMeeting => - // { - // await client.Meetings.DeleteAsync(userId, oldAccount.Id, null, cancellationToken).ConfigureAwait(false); - // await log.WriteLineAsync($"Meeting {oldMeeting.Id} deleted").ConfigureAwait(false); - // await Task.Delay(250).ConfigureAwait(false); // Brief pause to ensure Zoom has time to catch up - // }); - //await Task.WhenAll(cleanUpTasks).ConfigureAwait(false); + var cleanUpTasks = paginatedScheduledMeetings.Records + .Union(paginatedLiveMeetings.Records) + .Where(m => m.Topic.StartsWith("ZoomNet Integration Testing:")) + .Select(async oldMeeting => + { + await client.Meetings.DeleteAsync(userId, oldMeeting.Id, null, cancellationToken).ConfigureAwait(false); + await log.WriteLineAsync($"Meeting {oldMeeting.Id} deleted").ConfigureAwait(false); + await Task.Delay(250).ConfigureAwait(false); // Brief pause to ensure Zoom has time to catch up + }); + await Task.WhenAll(cleanUpTasks).ConfigureAwait(false); var settings = new MeetingSettings() { @@ -61,6 +63,10 @@ public async Task RunAsync(IClient client, TextWriter log, CancellationToken can }; var newRecurringMeeting = await client.Meetings.CreateRecurringMeetingAsync(userId, "ZoomNet Integration Testing: recurring meeting", "The agenda", start, duration, recurrenceInfo, "p@ss!w0rd", settings, trackingFields, cancellationToken).ConfigureAwait(false); await log.WriteLineAsync($"Recurring meeting {newRecurringMeeting.Id} created").ConfigureAwait(false); + + await client.Meetings.DeleteAsync(userId, newInstantMeeting.Id, null, cancellationToken).ConfigureAwait(false); + await client.Meetings.DeleteAsync(userId, newScheduledMeeting.Id, null, cancellationToken).ConfigureAwait(false); + await client.Meetings.DeleteAsync(userId, newRecurringMeeting.Id, null, cancellationToken).ConfigureAwait(false); } } } From ce67e6426c488c2eb028823ea0df2f8984d1aa96 Mon Sep 17 00:00:00 2001 From: Jeremie Desautels Date: Tue, 18 Jun 2019 12:42:16 -0400 Subject: [PATCH 32/41] Fix Meetings.EndAsync --- .../ZoomNet.IntegrationTests/Tests/Meetings.cs | 16 ++++++++++++++-- Source/ZoomNet/Resources/Meetings.cs | 7 ++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/Source/ZoomNet.IntegrationTests/Tests/Meetings.cs b/Source/ZoomNet.IntegrationTests/Tests/Meetings.cs index 37ca307f..a36e674d 100644 --- a/Source/ZoomNet.IntegrationTests/Tests/Meetings.cs +++ b/Source/ZoomNet.IntegrationTests/Tests/Meetings.cs @@ -47,14 +47,27 @@ public async Task RunAsync(IClient client, TextWriter log, CancellationToken can { "field1", "value1"}, { "field2", "value2"} }; + + // Instant meeting var newInstantMeeting = await client.Meetings.CreateInstantMeetingAsync(userId, "ZoomNet Integration Testing: instant meeting", "The agenda", "p@ss!w0rd", settings, trackingFields, cancellationToken).ConfigureAwait(false); await log.WriteLineAsync($"Instant meeting {newInstantMeeting.Id} created").ConfigureAwait(false); + await client.Meetings.EndAsync(newInstantMeeting.Id, cancellationToken).ConfigureAwait(false); + await log.WriteLineAsync($"Instant meeting {newInstantMeeting.Id} ended").ConfigureAwait(false); + + await client.Meetings.DeleteAsync(userId, newInstantMeeting.Id, null, cancellationToken).ConfigureAwait(false); + await log.WriteLineAsync($"Instant meeting {newInstantMeeting.Id} deleted").ConfigureAwait(false); + + // Scheduled meeting var start = DateTime.UtcNow.AddMonths(1); var duration = 30; var newScheduledMeeting = await client.Meetings.CreateScheduledMeetingAsync(userId, "ZoomNet Integration Testing: scheduled meeting", "The agenda", start, duration, "p@ss!w0rd", settings, trackingFields, cancellationToken).ConfigureAwait(false); await log.WriteLineAsync($"Scheduled meeting {newScheduledMeeting.Id} created").ConfigureAwait(false); + await client.Meetings.DeleteAsync(userId, newScheduledMeeting.Id, null, cancellationToken).ConfigureAwait(false); + await log.WriteLineAsync($"Scheduled meeting {newScheduledMeeting.Id} deleted").ConfigureAwait(false); + + // Recurring meeting var recurrenceInfo = new RecurrenceInfo() { EndTimes = 2, @@ -64,9 +77,8 @@ public async Task RunAsync(IClient client, TextWriter log, CancellationToken can var newRecurringMeeting = await client.Meetings.CreateRecurringMeetingAsync(userId, "ZoomNet Integration Testing: recurring meeting", "The agenda", start, duration, recurrenceInfo, "p@ss!w0rd", settings, trackingFields, cancellationToken).ConfigureAwait(false); await log.WriteLineAsync($"Recurring meeting {newRecurringMeeting.Id} created").ConfigureAwait(false); - await client.Meetings.DeleteAsync(userId, newInstantMeeting.Id, null, cancellationToken).ConfigureAwait(false); - await client.Meetings.DeleteAsync(userId, newScheduledMeeting.Id, null, cancellationToken).ConfigureAwait(false); await client.Meetings.DeleteAsync(userId, newRecurringMeeting.Id, null, cancellationToken).ConfigureAwait(false); + await log.WriteLineAsync($"Recurring meeting {newRecurringMeeting.Id} deleted").ConfigureAwait(false); } } } diff --git a/Source/ZoomNet/Resources/Meetings.cs b/Source/ZoomNet/Resources/Meetings.cs index 51cc749c..1a014f7e 100644 --- a/Source/ZoomNet/Resources/Meetings.cs +++ b/Source/ZoomNet/Resources/Meetings.cs @@ -245,9 +245,14 @@ public Task DeleteAsync(string userId, long meetingId, string occurrenceId = nul /// public Task EndAsync(long meetingId, CancellationToken cancellationToken = default) { + var data = new JObject() + { + { "action", "end" } + }; + return _client .PutAsync($"meetings/{meetingId}/status") - .WithArgument("action", "end") + .WithJsonBody(data) .WithCancellationToken(cancellationToken) .AsMessage(); } From bf2c758a99a2dc57b0364425f637ff21b63b4ced Mon Sep 17 00:00:00 2001 From: Jericho Date: Fri, 1 May 2020 15:09:51 -0400 Subject: [PATCH 33/41] Pass userId to integration tests --- Source/ZoomNet.IntegrationTests/Tests/Meetings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/ZoomNet.IntegrationTests/Tests/Meetings.cs b/Source/ZoomNet.IntegrationTests/Tests/Meetings.cs index a36e674d..66d896a7 100644 --- a/Source/ZoomNet.IntegrationTests/Tests/Meetings.cs +++ b/Source/ZoomNet.IntegrationTests/Tests/Meetings.cs @@ -10,7 +10,7 @@ namespace ZoomNet.IntegrationTests.Tests { public class Meetings : IIntegrationTest { - public async Task RunAsync(IClient client, TextWriter log, CancellationToken cancellationToken) + public async Task RunAsync(string userId, IClient client, TextWriter log, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) return; From ff86803b0c099762caeee5cb791a1741ae38b12c Mon Sep 17 00:00:00 2001 From: Jericho Date: Fri, 1 May 2020 15:12:13 -0400 Subject: [PATCH 34/41] Fix incorrect using statement --- Source/ZoomNet.IntegrationTests/TestsRunner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/ZoomNet.IntegrationTests/TestsRunner.cs b/Source/ZoomNet.IntegrationTests/TestsRunner.cs index 230b3d46..50003950 100644 --- a/Source/ZoomNet.IntegrationTests/TestsRunner.cs +++ b/Source/ZoomNet.IntegrationTests/TestsRunner.cs @@ -5,7 +5,7 @@ using System.Net; using System.Threading; using System.Threading.Tasks; -using ZoomNet.Resources; +using ZoomNet.IntegrationTests.Tests; using ZoomNet.Utilities; namespace ZoomNet.IntegrationTests From a4503506c429bc1fe089d75dadc62a44b464bde0 Mon Sep 17 00:00:00 2001 From: Jericho Date: Fri, 1 May 2020 15:25:34 -0400 Subject: [PATCH 35/41] Fix typos and reduce code duplication --- Source/ZoomNet/Resources/IMeetings.cs | 4 +-- Source/ZoomNet/Resources/Meetings.cs | 41 +++++++++------------------ 2 files changed, 16 insertions(+), 29 deletions(-) diff --git a/Source/ZoomNet/Resources/IMeetings.cs b/Source/ZoomNet/Resources/IMeetings.cs index 0b86616a..7ea60ec2 100644 --- a/Source/ZoomNet/Resources/IMeetings.cs +++ b/Source/ZoomNet/Resources/IMeetings.cs @@ -132,7 +132,7 @@ public interface IMeetings Task> GetRegistrantsAsync(long meetingId, RegistrantStatus status, string occurrenceId = null, int recordsPerPage = 30, int page = 1, CancellationToken cancellationToken = default); /// - /// List registrants of a meeting. + /// Add a registrant to a meeting. /// /// The meeting ID. /// A valid email address. @@ -143,7 +143,7 @@ public interface IMeetings /// /// A . /// - Task AddRegistrantsAsync(long meetingId, string email, string firstName, string lastName, string occurrenceId = null, CancellationToken cancellationToken = default); + Task AddRegistrantAsync(long meetingId, string email, string firstName, string lastName, string occurrenceId = null, CancellationToken cancellationToken = default); /// /// Approve a registration for a meeting. diff --git a/Source/ZoomNet/Resources/Meetings.cs b/Source/ZoomNet/Resources/Meetings.cs index 1a014f7e..e81cc4cc 100644 --- a/Source/ZoomNet/Resources/Meetings.cs +++ b/Source/ZoomNet/Resources/Meetings.cs @@ -287,7 +287,7 @@ public Task> GetRegistrantsAsync(long meetingId, R } /// - /// List registrants of a meeting. + /// Add a registrant to a meeting. /// /// The meeting ID. /// A valid email address. @@ -298,7 +298,7 @@ public Task> GetRegistrantsAsync(long meetingId, R /// /// A . /// - public Task AddRegistrantsAsync(long meetingId, string email, string firstName, string lastName, string occurrenceId = null, CancellationToken cancellationToken = default) + public Task AddRegistrantAsync(long meetingId, string email, string firstName, string lastName, string occurrenceId = null, CancellationToken cancellationToken = default) { var data = new JObject(); data.AddPropertyIfValue("email", email); @@ -341,16 +341,7 @@ public Task ApproveRegistrantAsync(long meetingId, string registrantId, string r /// public Task ApproveRegistrantsAsync(long meetingId, IEnumerable<(string RegistrantId, string RegistrantEmail)> registrantsInfo, string occurrenceId = null, CancellationToken cancellationToken = default) { - var data = new JObject(); - data.AddPropertyIfValue("action", "approve"); - data.AddPropertyIfValue("registrants", registrantsInfo.Select(ri => new { id = ri.RegistrantId, email = ri.RegistrantEmail }).ToArray()); - - return _client - .PostAsync($"meetings/{meetingId}/registrants/status") - .WithArgument("occurence_id", occurrenceId) - .WithJsonBody(data) - .WithCancellationToken(cancellationToken) - .AsMessage(); + return UpdateRegistrantsStatusAsync(meetingId, registrantsInfo, "approve", occurrenceId, cancellationToken); } /// @@ -366,7 +357,7 @@ public Task ApproveRegistrantsAsync(long meetingId, IEnumerable<(string Registra /// public Task RejectRegistrantAsync(long meetingId, string registrantId, string registrantEmail, string occurrenceId = null, CancellationToken cancellationToken = default) { - return ApproveRegistrantsAsync(meetingId, new[] { (registrantId, registrantEmail) }, occurrenceId, cancellationToken); + return RejectRegistrantsAsync(meetingId, new[] { (registrantId, registrantEmail) }, occurrenceId, cancellationToken); } /// @@ -381,20 +372,11 @@ public Task RejectRegistrantAsync(long meetingId, string registrantId, string re /// public Task RejectRegistrantsAsync(long meetingId, IEnumerable<(string RegistrantId, string RegistrantEmail)> registrantsInfo, string occurrenceId = null, CancellationToken cancellationToken = default) { - var data = new JObject(); - data.AddPropertyIfValue("action", "deny"); - data.AddPropertyIfValue("registrants", registrantsInfo.Select(ri => new { id = ri.RegistrantId, email = ri.RegistrantEmail }).ToArray()); - - return _client - .PostAsync($"meetings/{meetingId}/registrants/status") - .WithArgument("occurence_id", occurrenceId) - .WithJsonBody(data) - .WithCancellationToken(cancellationToken) - .AsMessage(); + return UpdateRegistrantsStatusAsync(meetingId, registrantsInfo, "deny", occurrenceId, cancellationToken); } /// - /// Cancel a registration for a meeting. + /// Cancel a previously approved registration for a meeting. /// /// The meeting ID. /// The registrant ID. @@ -406,11 +388,11 @@ public Task RejectRegistrantsAsync(long meetingId, IEnumerable<(string Registran /// public Task CancelRegistrantAsync(long meetingId, string registrantId, string registrantEmail, string occurrenceId = null, CancellationToken cancellationToken = default) { - return ApproveRegistrantsAsync(meetingId, new[] { (registrantId, registrantEmail) }, occurrenceId, cancellationToken); + return CancelRegistrantsAsync(meetingId, new[] { (registrantId, registrantEmail) }, occurrenceId, cancellationToken); } /// - /// Cancel multiple registrations for a meeting. + /// Cancel multiple previously approved registrations for a meeting. /// /// The meeting ID. /// ID and email for each registrant to be cancelled. @@ -420,9 +402,14 @@ public Task CancelRegistrantAsync(long meetingId, string registrantId, string re /// The async task. /// public Task CancelRegistrantsAsync(long meetingId, IEnumerable<(string RegistrantId, string RegistrantEmail)> registrantsInfo, string occurrenceId = null, CancellationToken cancellationToken = default) + { + return UpdateRegistrantsStatusAsync(meetingId, registrantsInfo, "cancel", occurrenceId, cancellationToken); + } + + private Task UpdateRegistrantsStatusAsync(long meetingId, IEnumerable<(string RegistrantId, string RegistrantEmail)> registrantsInfo, string status, string occurrenceId = null, CancellationToken cancellationToken = default) { var data = new JObject(); - data.AddPropertyIfValue("action", "approve"); + data.AddPropertyIfValue("action", status); data.AddPropertyIfValue("registrants", registrantsInfo.Select(ri => new { id = ri.RegistrantId, email = ri.RegistrantEmail }).ToArray()); return _client From 22915463d0a50ab536642b35f95f2b183604bcc0 Mon Sep 17 00:00:00 2001 From: Jericho Date: Fri, 1 May 2020 16:17:08 -0400 Subject: [PATCH 36/41] Improve XML comments --- Source/ZoomNet/Resources/IMeetings.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/ZoomNet/Resources/IMeetings.cs b/Source/ZoomNet/Resources/IMeetings.cs index 7ea60ec2..010508f4 100644 --- a/Source/ZoomNet/Resources/IMeetings.cs +++ b/Source/ZoomNet/Resources/IMeetings.cs @@ -196,7 +196,7 @@ public interface IMeetings Task RejectRegistrantsAsync(long meetingId, IEnumerable<(string RegistrantId, string RegistrantEmail)> registrantsInfo, string occurrenceId = null, CancellationToken cancellationToken = default); /// - /// Cancel a registration for a meeting. + /// Cancel a previously approved registration for a meeting. /// /// The meeting ID. /// The registrant ID. @@ -209,7 +209,7 @@ public interface IMeetings Task CancelRegistrantAsync(long meetingId, string registrantId, string registrantEmail, string occurrenceId = null, CancellationToken cancellationToken = default); /// - /// Cancel multiple registrations for a meeting. + /// Cancel multiple previously approved registrations for a meeting. /// /// The meeting ID. /// ID and email for each registrant to be cancelled. From 5ffd265997636f4344f0d09edb562a8b2b1d73c9 Mon Sep 17 00:00:00 2001 From: Jericho Date: Sun, 3 May 2020 22:28:07 -0400 Subject: [PATCH 37/41] Add a resource with methods to manage meetings that occurred in the past --- Source/ZoomNet/Client.cs | 9 ++ Source/ZoomNet/IClient.cs | 8 ++ Source/ZoomNet/Models/MeetingFile.cs | 37 ++++++ .../Models/PaginatedResponseWithToken.cs | 46 +++++++ Source/ZoomNet/Models/Participant.cs | 31 +++++ Source/ZoomNet/Models/PastMeeting.cs | 103 +++++++++++++++ Source/ZoomNet/Models/PastMeetingInstance.cs | 27 ++++ Source/ZoomNet/Models/PollAnswer.cs | 28 ++++ Source/ZoomNet/Models/PollResult.cs | 40 ++++++ Source/ZoomNet/Resources/IPastMeetings.cs | 70 ++++++++++ Source/ZoomNet/Resources/PastMeetings.cs | 122 ++++++++++++++++++ Source/ZoomNet/Utilities/Extensions.cs | 52 ++++++++ 12 files changed, 573 insertions(+) create mode 100644 Source/ZoomNet/Models/MeetingFile.cs create mode 100644 Source/ZoomNet/Models/PaginatedResponseWithToken.cs create mode 100644 Source/ZoomNet/Models/Participant.cs create mode 100644 Source/ZoomNet/Models/PastMeeting.cs create mode 100644 Source/ZoomNet/Models/PastMeetingInstance.cs create mode 100644 Source/ZoomNet/Models/PollAnswer.cs create mode 100644 Source/ZoomNet/Models/PollResult.cs create mode 100644 Source/ZoomNet/Resources/IPastMeetings.cs create mode 100644 Source/ZoomNet/Resources/PastMeetings.cs diff --git a/Source/ZoomNet/Client.cs b/Source/ZoomNet/Client.cs index 01695f55..dfb427dd 100644 --- a/Source/ZoomNet/Client.cs +++ b/Source/ZoomNet/Client.cs @@ -68,6 +68,14 @@ public static string Version /// public IMeetings Meetings { get; private set; } + /// + /// Gets the resource which allows you to manage meetings that occured in the past. + /// + /// + /// The past meetings resource. + /// + public IPastMeetings PastMeetings { get; private set; } + #endregion #region CTOR @@ -142,6 +150,7 @@ private Client(string apiKey, string apiSecret, HttpClient httpClient, bool disp _fluentClient.Filters.Add(new ZoomErrorHandler()); Meetings = new Meetings(_fluentClient); + PastMeetings = new PastMeetings(_fluentClient); } /// diff --git a/Source/ZoomNet/IClient.cs b/Source/ZoomNet/IClient.cs index b02fb22a..b9eafa6d 100644 --- a/Source/ZoomNet/IClient.cs +++ b/Source/ZoomNet/IClient.cs @@ -14,5 +14,13 @@ public interface IClient /// The meetings resource. /// IMeetings Meetings { get; } + + /// + /// Gets the resource which allows you to manage meetings that occured in the past. + /// + /// + /// The past meetings resource. + /// + IPastMeetings PastMeetings { get; } } } diff --git a/Source/ZoomNet/Models/MeetingFile.cs b/Source/ZoomNet/Models/MeetingFile.cs new file mode 100644 index 00000000..9064bc2a --- /dev/null +++ b/Source/ZoomNet/Models/MeetingFile.cs @@ -0,0 +1,37 @@ +using Newtonsoft.Json; + +namespace ZoomNet.Models +{ + /// + /// A file sent via in-meeting chat during a meeting. + /// + public class MeetingFile + { + /// + /// Gets or sets the name of the file. + /// + /// + /// The name. + /// + [JsonProperty("file_name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; set; } + + /// + /// Gets or sets the URL to download the file. + /// + /// + /// The URL. + /// + [JsonProperty("download_url", NullValueHandling = NullValueHandling.Ignore)] + public string DownloadUrl { get; set; } + + /// + /// Gets or sets the size of the file (in bytes). + /// + /// + /// The size. + /// + [JsonProperty("file_size", NullValueHandling = NullValueHandling.Ignore)] + public long Size { get; set; } + } +} diff --git a/Source/ZoomNet/Models/PaginatedResponseWithToken.cs b/Source/ZoomNet/Models/PaginatedResponseWithToken.cs new file mode 100644 index 00000000..4514d6f2 --- /dev/null +++ b/Source/ZoomNet/Models/PaginatedResponseWithToken.cs @@ -0,0 +1,46 @@ +using Newtonsoft.Json; + +namespace ZoomNet.Models +{ + /// + /// Pagination Object. + /// + /// The type of records. + public class PaginatedResponseWithToken + { + /// + /// Gets or sets the number of items returned on this page. + /// + /// The number of items returned on this page. + [JsonProperty(PropertyName = "page_count")] + public int PageCount { get; set; } + + /// + /// Gets or sets the number of records returned within a single API call. + /// + /// The number of records returned within a single API call. + [JsonProperty(PropertyName = "page_size")] + public int PageSize { get; set; } + + /// + /// Gets or sets the number of all records available across pages. + /// + /// The number of all records available across pages. + [JsonProperty(PropertyName = "total_records")] + public int? TotalRecords { get; set; } + + /// + /// Gets or sets the token to retrieve the next page. + /// + /// The page token. + /// This token expires after 15 minutes. + [JsonProperty(PropertyName = "next_page_token")] + public string NextPageToken { get; set; } + + /// + /// Gets or sets the records. + /// + /// The records. + public T[] Records { get; set; } + } +} diff --git a/Source/ZoomNet/Models/Participant.cs b/Source/ZoomNet/Models/Participant.cs new file mode 100644 index 00000000..95820cbb --- /dev/null +++ b/Source/ZoomNet/Models/Participant.cs @@ -0,0 +1,31 @@ +using Newtonsoft.Json; + +namespace ZoomNet.Models +{ + /// + /// Participant. + /// + public class Participant + { + /// + /// Gets or sets the participant uuid. + /// + /// + /// The id. + /// + [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] + public string Uuid { get; set; } + + /// + /// Gets or sets the participant's email address. + /// + [JsonProperty(PropertyName = "user_email")] + public string Email { get; set; } + + /// + /// Gets or sets the participant's display name. + /// + [JsonProperty(PropertyName = "name")] + public string DisplayName { get; set; } + } +} diff --git a/Source/ZoomNet/Models/PastMeeting.cs b/Source/ZoomNet/Models/PastMeeting.cs new file mode 100644 index 00000000..dfd7f7dd --- /dev/null +++ b/Source/ZoomNet/Models/PastMeeting.cs @@ -0,0 +1,103 @@ +using Newtonsoft.Json; +using System; + +namespace ZoomNet.Models +{ + /// + /// A meeting that occured in the past. + /// + public class PastMeeting + { + /// + /// Gets or sets the unique id. + /// + /// + /// The unique id. + /// + [JsonProperty("uuid", NullValueHandling = NullValueHandling.Ignore)] + public string Uuid { get; set; } + + /// + /// Gets or sets the meeting id, also known as the meeting number. + /// + /// + /// The id. + /// + [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] + public long Id { get; set; } + + /// + /// Gets or sets the ID of the user who is set as the host of the meeting. + /// + /// + /// The user id. + /// + [JsonProperty("host_id", NullValueHandling = NullValueHandling.Ignore)] + public string HostId { get; set; } + + /// + /// Gets or sets the topic of the meeting. + /// + /// + /// The topic. + /// + [JsonProperty("topic", NullValueHandling = NullValueHandling.Ignore)] + public string Topic { get; set; } + + /// + /// Gets or sets the meeting type. + /// + /// The meeting type. + [JsonProperty(PropertyName = "type", NullValueHandling = NullValueHandling.Ignore)] + public MeetingType Type { get; set; } + + /// + /// Gets or sets the user display name. + /// + /// The user display name. + [JsonProperty(PropertyName = "user_name", NullValueHandling = NullValueHandling.Ignore)] + public string UserName { get; set; } + + /// + /// Gets or sets the user email. + /// + /// The user email. + [JsonProperty(PropertyName = "user_email", NullValueHandling = NullValueHandling.Ignore)] + public string UserEmail { get; set; } + + /// + /// Gets or sets the date and time when the meeting started. + /// + /// The meeting start time. + [JsonProperty(PropertyName = "start_time", NullValueHandling = NullValueHandling.Ignore)] + public DateTime StartedOn { get; set; } + + /// + /// Gets or sets the date and time when the meeting ended. + /// + /// The meeting end time. + [JsonProperty(PropertyName = "end_time", NullValueHandling = NullValueHandling.Ignore)] + public DateTime EndedOn { get; set; } + + /// + /// Gets or sets the meeting duration in minutes. + /// + /// The meeting duration. + [JsonProperty(PropertyName = "duration", NullValueHandling = NullValueHandling.Ignore)] + public long Duration { get; set; } + + /// + /// Gets or sets the sum of meeting minutes from all participants. + /// + /// The total meeting minutes. + [JsonProperty(PropertyName = "total_minutes", NullValueHandling = NullValueHandling.Ignore)] + public long TotalMinutes { get; set; } + + /// + /// Gets or sets the number of participants. + /// + /// The number of participants. + [JsonProperty(PropertyName = "participants_count", NullValueHandling = NullValueHandling.Ignore)] + public long ParticipantsCount { get; set; } + } +} diff --git a/Source/ZoomNet/Models/PastMeetingInstance.cs b/Source/ZoomNet/Models/PastMeetingInstance.cs new file mode 100644 index 00000000..848ca17f --- /dev/null +++ b/Source/ZoomNet/Models/PastMeetingInstance.cs @@ -0,0 +1,27 @@ +using Newtonsoft.Json; +using System; + +namespace ZoomNet.Models +{ + /// + /// A meeting instance that occured in the past. + /// + public class PastMeetingInstance + { + /// + /// Gets or sets the meeting uuid. + /// + /// + /// The uuid. + /// + [JsonProperty("uuid", NullValueHandling = NullValueHandling.Ignore)] + public string Uuid { get; set; } + + /// + /// Gets or sets the date and time when the meeting instance started. + /// + /// The meeting start time. + [JsonProperty(PropertyName = "start_time", NullValueHandling = NullValueHandling.Ignore)] + public DateTime StartedOn { get; set; } + } +} diff --git a/Source/ZoomNet/Models/PollAnswer.cs b/Source/ZoomNet/Models/PollAnswer.cs new file mode 100644 index 00000000..16a3c12e --- /dev/null +++ b/Source/ZoomNet/Models/PollAnswer.cs @@ -0,0 +1,28 @@ +using Newtonsoft.Json; + +namespace ZoomNet.Models +{ + /// + /// The answer to a question asked during a poll. + /// + public class PollAnswer + { + /// + /// Gets or sets the question asked during the poll. + /// + /// + /// The question. + /// + [JsonProperty("question", NullValueHandling = NullValueHandling.Ignore)] + public string Question { get; set; } + + /// + /// Gets or sets the answer submitted by the participant. + /// + /// + /// The question. + /// + [JsonProperty("answer", NullValueHandling = NullValueHandling.Ignore)] + public string Answer { get; set; } + } +} diff --git a/Source/ZoomNet/Models/PollResult.cs b/Source/ZoomNet/Models/PollResult.cs new file mode 100644 index 00000000..74596a47 --- /dev/null +++ b/Source/ZoomNet/Models/PollResult.cs @@ -0,0 +1,40 @@ +using Newtonsoft.Json; + +namespace ZoomNet.Models +{ + /// + /// The result of a poll for a given participant. + /// + public class PollResult + { + /// + /// Gets or sets the name of the user who submitted answers to the poll. + /// + /// + /// If "anonymous" option is enabled for a poll, the participant's polling information will be kept anonymous and the value of 'name' field will be "Anonymous Attendee". + /// + /// + /// The name of the participant. + /// + [JsonProperty("file_name", NullValueHandling = NullValueHandling.Ignore)] + public string ParticipantName { get; set; } + + /// + /// Gets or sets the email address of the user who submitted answers to the poll. + /// + /// + /// The email address of the participant. + /// + [JsonProperty("email", NullValueHandling = NullValueHandling.Ignore)] + public string ParticipantEmail { get; set; } + + /// + /// Gets or sets the answers to questions asked during the poll. + /// + /// + /// The answers. + /// + [JsonProperty("question_details", NullValueHandling = NullValueHandling.Ignore)] + public PollAnswer[] Details { get; set; } + } +} diff --git a/Source/ZoomNet/Resources/IPastMeetings.cs b/Source/ZoomNet/Resources/IPastMeetings.cs new file mode 100644 index 00000000..4ef2a512 --- /dev/null +++ b/Source/ZoomNet/Resources/IPastMeetings.cs @@ -0,0 +1,70 @@ +using System.Threading; +using System.Threading.Tasks; +using ZoomNet.Models; + +namespace ZoomNet.Resources +{ + /// + /// Allows you to manage meetings that occured in the past. + /// + /// + /// See Zoom documentation for more information. + /// + public interface IPastMeetings + { + /// + /// Retrieve the details of a meeting that occured in the past. + /// + /// The meeting UUID. + /// The cancellation token. + /// + /// The . + /// + Task GetAsync(string uuid, CancellationToken cancellationToken = default); + + /// + /// List participants of a meeting that occured in the past. + /// + /// The meeting UUID. + /// The number of records to return. + /// The page token. + /// The cancellation token. + /// + /// An array of . + /// + Task> GetParticipantsAsync(string uuid, int recordsPerPage = 30, string pageToken = null, CancellationToken cancellationToken = default); + + /// + /// Get a list of ended meeting instance. + /// + /// The meeting identifier. + /// The cancellation token. + /// + /// An array of . + /// + Task GetInstancesAsync(long meetingId, CancellationToken cancellationToken = default); + + /// + /// Get a list of poll results for a meeting that occured in the past. + /// + /// The meeting identifier. + /// The cancellation token. + /// + /// An array of . + /// + Task GetPollResultsAsync(long meetingId, CancellationToken cancellationToken = default); + + /// + /// Get a list of files sent via in-meeting chat during a meeting. + /// + /// + /// The in-meeting files are deleted after 24 hours of the meeting completion time. + /// + /// The meeting identifier. + /// The cancellation token. + /// + /// An array of . + /// + Task GetFilesAsync(long meetingId, CancellationToken cancellationToken = default); + } +} diff --git a/Source/ZoomNet/Resources/PastMeetings.cs b/Source/ZoomNet/Resources/PastMeetings.cs new file mode 100644 index 00000000..0a00f7ef --- /dev/null +++ b/Source/ZoomNet/Resources/PastMeetings.cs @@ -0,0 +1,122 @@ +using Pathoschild.Http.Client; +using System; +using System.Threading; +using System.Threading.Tasks; +using ZoomNet.Models; +using ZoomNet.Utilities; + +namespace ZoomNet.Resources +{ + /// + /// Allows you to manage meetings that occured in the past. + /// + /// + /// + /// See Zoom documentation for more information. + /// + public class PastMeetings : IPastMeetings + { + private readonly Pathoschild.Http.Client.IClient _client; + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP client. + internal PastMeetings(Pathoschild.Http.Client.IClient client) + { + _client = client; + } + + /// + /// Retrieve the details of a meeting that occured in the past. + /// + /// The meeting UUID. + /// The cancellation token. + /// + /// The . + /// + public Task GetAsync(string uuid, CancellationToken cancellationToken = default) + { + return _client + .GetAsync($"past_meetings/{uuid}") + .WithCancellationToken(cancellationToken) + .AsObject(null, new MeetingConverter()); + } + + /// + /// List participants of a meeting that occured in the past. + /// + /// The meeting UUID. + /// The number of records to return. + /// The page token. + /// The cancellation token. + /// + /// An array of . + /// + public Task> GetParticipantsAsync(string uuid, int recordsPerPage = 30, string pageToken = null, CancellationToken cancellationToken = default) + { + if (recordsPerPage < 1 || recordsPerPage > 300) + { + throw new ArgumentOutOfRangeException(nameof(recordsPerPage), "Records per page must be between 1 and 300"); + } + + return _client + .GetAsync($"past_meetings/{uuid}/participants") + .WithArgument("page_size", recordsPerPage) + .WithArgument("next_page_token", pageToken) + .WithCancellationToken(cancellationToken) + .AsPaginatedResponseWithToken("participants"); + } + + /// + /// Get a list of ended meeting instance. + /// + /// The meeting identifier. + /// The cancellation token. + /// + /// An array of . + /// + public Task GetInstancesAsync(long meetingId, CancellationToken cancellationToken = default) + { + return _client + .GetAsync($"past_meetings/{meetingId}/instances") + .WithCancellationToken(cancellationToken) + .AsObject("meetings"); + } + + /// + /// Get a list of poll results for a meeting that occured in the past. + /// + /// The meeting identifier. + /// The cancellation token. + /// + /// An array of . + /// + public Task GetPollResultsAsync(long meetingId, CancellationToken cancellationToken = default) + { + return _client + .GetAsync($"past_meetings/{meetingId}/polls") + .WithCancellationToken(cancellationToken) + .AsObject("questions"); + } + + /// + /// Get a list of files sent via in-meeting chat during a meeting. + /// + /// + /// The in-meeting files are deleted after 24 hours of the meeting completion time. + /// + /// The meeting identifier. + /// The cancellation token. + /// + /// An array of . + /// + public Task GetFilesAsync(long meetingId, CancellationToken cancellationToken = default) + { + return _client + .GetAsync($"past_meetings/{meetingId}/files") + .WithCancellationToken(cancellationToken) + .AsObject("in_meeting_files"); + } + } +} diff --git a/Source/ZoomNet/Utilities/Extensions.cs b/Source/ZoomNet/Utilities/Extensions.cs index 10bb1af6..2b8c4d0b 100644 --- a/Source/ZoomNet/Utilities/Extensions.cs +++ b/Source/ZoomNet/Utilities/Extensions.cs @@ -212,6 +212,31 @@ public static async Task> AsPaginatedResponse(this IRequ return await response.Content.AsPaginatedResponse(propertyName, jsonConverter).ConfigureAwait(false); } + /// Asynchronously retrieve the JSON encoded response body and convert it to a 'PaginatedResponseWithToken' object. + /// The response model to deserialize into. + /// The response. + /// The name of the JSON property (or null if not applicable) where the desired data is stored. + /// Converter that will be used during deserialization. + /// Returns the paginated response. + /// An error occurred processing the response. + public static Task> AsPaginatedResponseWithToken(this IResponse response, string propertyName, JsonConverter jsonConverter = null) + { + return response.Message.Content.AsPaginatedResponseWithToken(propertyName, jsonConverter); + } + + /// Asynchronously retrieve the JSON encoded response body and convert it to a 'PaginatedResponseWithToken' object. + /// The response model to deserialize into. + /// The request. + /// The name of the JSON property (or null if not applicable) where the desired data is stored. + /// Converter that will be used during deserialization. + /// Returns the paginated response. + /// An error occurred processing the response. + public static async Task> AsPaginatedResponseWithToken(this IRequest request, string propertyName, JsonConverter jsonConverter = null) + { + var response = await request.AsMessage().ConfigureAwait(false); + return await response.Content.AsPaginatedResponseWithToken(propertyName, jsonConverter).ConfigureAwait(false); + } + /// Set the body content of the HTTP request. /// The type of object to serialize into a JSON string. /// The request. @@ -570,5 +595,32 @@ private static async Task> AsPaginatedResponse(this Http return result; } + + /// Asynchronously retrieve the JSON encoded content and converts it to a 'PaginatedResponseWithToken' object. + /// The response model to deserialize into. + /// The content. + /// The name of the JSON property (or null if not applicable) where the desired data is stored. + /// Converter that will be used during deserialization. + /// Returns the response body, or null if the response has no body. + /// An error occurred processing the response. + private static async Task> AsPaginatedResponseWithToken(this HttpContent httpContent, string propertyName, JsonConverter jsonConverter = null) + { + var responseContent = await httpContent.ReadAsStringAsync(null).ConfigureAwait(false); + var jObject = JObject.Parse(responseContent); + + var serializer = new JsonSerializer(); + if (jsonConverter != null) serializer.Converters.Add(jsonConverter); + + var result = new PaginatedResponseWithToken() + { + NextPageToken = jObject.Property("next_page_token").Value.ToString(), + PageCount = jObject.Property("page_count").Value.ToObject(), + PageSize = jObject.Property("page_size").Value.ToObject(), + Records = jObject.Property(propertyName).Value.ToObject(serializer), + TotalRecords = jObject.Property("total_records").Value.ToObject() + }; + + return result; + } } } From cec81a0edb369607704b28b6622e03e00cc01b7b Mon Sep 17 00:00:00 2001 From: Jericho Date: Mon, 4 May 2020 21:37:01 -0400 Subject: [PATCH 38/41] Add methods to the Meetings resource to manage polls and livestream --- Source/ZoomNet/Models/Poll.cs | 46 +++++++ Source/ZoomNet/Models/PollQuestion.cs | 37 ++++++ Source/ZoomNet/Models/PollStatus.cs | 37 ++++++ Source/ZoomNet/Models/QuestionType.cs | 25 ++++ Source/ZoomNet/Resources/IMeetings.cs | 92 +++++++++++++ Source/ZoomNet/Resources/Meetings.cs | 179 ++++++++++++++++++++++++++ 6 files changed, 416 insertions(+) create mode 100644 Source/ZoomNet/Models/Poll.cs create mode 100644 Source/ZoomNet/Models/PollQuestion.cs create mode 100644 Source/ZoomNet/Models/PollStatus.cs create mode 100644 Source/ZoomNet/Models/QuestionType.cs diff --git a/Source/ZoomNet/Models/Poll.cs b/Source/ZoomNet/Models/Poll.cs new file mode 100644 index 00000000..6c47218c --- /dev/null +++ b/Source/ZoomNet/Models/Poll.cs @@ -0,0 +1,46 @@ +using Newtonsoft.Json; + +namespace ZoomNet.Models +{ + /// + /// A poll. + /// + public class Poll + { + /// + /// Gets or sets the unique identifier. + /// + /// + /// The ID. + /// + [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] + public string Id { get; set; } + + /// + /// Gets or sets the status of the poll. + /// + /// + /// The status. + /// + [JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)] + public PollStatus Status { get; set; } + + /// + /// Gets or sets the title of the poll. + /// + /// + /// The title. + /// + [JsonProperty("title", NullValueHandling = NullValueHandling.Ignore)] + public string Title { get; set; } + + /// + /// Gets or sets the questions. + /// + /// + /// The questions. + /// + [JsonProperty("questions", NullValueHandling = NullValueHandling.Ignore)] + public PollQuestion[] Questions { get; set; } + } +} diff --git a/Source/ZoomNet/Models/PollQuestion.cs b/Source/ZoomNet/Models/PollQuestion.cs new file mode 100644 index 00000000..0b412944 --- /dev/null +++ b/Source/ZoomNet/Models/PollQuestion.cs @@ -0,0 +1,37 @@ +using Newtonsoft.Json; + +namespace ZoomNet.Models +{ + /// + /// The answer to a question asked during a poll. + /// + public class PollQuestion + { + /// + /// Gets or sets the question asked during the poll. + /// + /// + /// The question. + /// + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Question { get; set; } + + /// + /// Gets or sets the type of question. + /// + /// + /// The type. + /// + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + public QuestionType Type { get; set; } + + /// + /// Gets or sets the answers to the question. + /// + /// + /// The answers. + /// + [JsonProperty("answer", NullValueHandling = NullValueHandling.Ignore)] + public string[] Answers { get; set; } + } +} diff --git a/Source/ZoomNet/Models/PollStatus.cs b/Source/ZoomNet/Models/PollStatus.cs new file mode 100644 index 00000000..17bfc7a0 --- /dev/null +++ b/Source/ZoomNet/Models/PollStatus.cs @@ -0,0 +1,37 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System.Runtime.Serialization; + +namespace ZoomNet.Models +{ + /// + /// Enumeration to indicate the status of a poll. + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum PollStatus + { + /// + /// Poll has not started. + /// + [EnumMember(Value = "notstart")] + NotStarted, + + /// + /// Poll has started. + /// + [EnumMember(Value = "started")] + Started, + + /// + /// Poll has ended. + /// + [EnumMember(Value = "ended")] + Ended, + + /// + /// Sharing poll results. + /// + [EnumMember(Value = "sharing")] + SharingResults + } +} diff --git a/Source/ZoomNet/Models/QuestionType.cs b/Source/ZoomNet/Models/QuestionType.cs new file mode 100644 index 00000000..59273303 --- /dev/null +++ b/Source/ZoomNet/Models/QuestionType.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System.Runtime.Serialization; + +namespace ZoomNet.Models +{ + /// + /// Enumeration to indicate the type of poll question. + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum QuestionType + { + /// + /// Single. + /// + [EnumMember(Value = "single")] + SingleChoice, + + /// + /// Multiple. + /// + [EnumMember(Value = "multiple")] + MultipleChoice + } +} diff --git a/Source/ZoomNet/Resources/IMeetings.cs b/Source/ZoomNet/Resources/IMeetings.cs index 010508f4..3fd31aad 100644 --- a/Source/ZoomNet/Resources/IMeetings.cs +++ b/Source/ZoomNet/Resources/IMeetings.cs @@ -219,5 +219,97 @@ public interface IMeetings /// The async task. /// Task CancelRegistrantsAsync(long meetingId, IEnumerable<(string RegistrantId, string RegistrantEmail)> registrantsInfo, string occurrenceId = null, CancellationToken cancellationToken = default); + + /// + /// Create a poll for a meeting. + /// + /// The meeting ID. + /// Title for the poll. + /// The poll questions. + /// The cancellation token. + /// + /// The async task. + /// + Task CreatePoll(long meetingId, string title, IEnumerable questions, CancellationToken cancellationToken = default); + + /// + /// Retrieve the details of a meeting. + /// + /// The meeting id + /// The poll id. + /// The cancellation token. + /// + /// The . + /// + Task GetPollAsync(long meetingId, long pollId, CancellationToken cancellationToken = default); + + /// + /// Update a poll for a meeting. + /// + /// The meeting ID. + /// The poll id. + /// Title for the poll. + /// The poll questions. + /// The cancellation token. + /// + /// The async task. + /// + Task UpdatePollAsync(long meetingId, long pollId, string title, IEnumerable questions, CancellationToken cancellationToken = default); + + /// + /// Delete a poll for a meeting. + /// + /// The meeting ID. + /// The poll id. + /// The cancellation token. + /// + /// The async task. + /// + Task DeletePollAsync(long meetingId, long pollId, CancellationToken cancellationToken = default); + + /// + /// Get the meeting invite note that was sent for a specific meeting. + /// + /// The meeting ID. + /// The cancellation token. + /// + /// The invite note. + /// + Task GetInvitationAsync(long meetingId, CancellationToken cancellationToken = default); + + /// + /// Update a meeting’s live stream information. + /// + /// The meeting ID. + /// Streaming URL. + /// Stream name and key + /// The live stream page URL. + /// The cancellation token. + /// + /// The async task. + /// + Task UpdateLiveStreamAsync(long meetingId, string streamUrl, string streamKey, string pageUrl, CancellationToken cancellationToken = default); + + /// + /// Start a meeting’s live stream. + /// + /// The meeting ID. + /// Display the name of the active speaker during a live stream. + /// The name of the speaker. + /// The cancellation token. + /// + /// The async task. + /// + Task StartLiveStreamAsync(long meetingId, bool displaySpeakerName, string speakerName, CancellationToken cancellationToken = default); + + /// + /// Stop a meeting’s live stream. + /// + /// The meeting ID. + /// The cancellation token. + /// + /// The async task. + /// + Task StopLiveStreamAsync(long meetingId, CancellationToken cancellationToken = default); } } diff --git a/Source/ZoomNet/Resources/Meetings.cs b/Source/ZoomNet/Resources/Meetings.cs index e81cc4cc..434f33fc 100644 --- a/Source/ZoomNet/Resources/Meetings.cs +++ b/Source/ZoomNet/Resources/Meetings.cs @@ -406,6 +406,185 @@ public Task CancelRegistrantsAsync(long meetingId, IEnumerable<(string Registran return UpdateRegistrantsStatusAsync(meetingId, registrantsInfo, "cancel", occurrenceId, cancellationToken); } + /// + /// Create a poll for a meeting. + /// + /// The meeting ID. + /// Title for the poll. + /// The poll questions. + /// The cancellation token. + /// + /// The async task. + /// + public Task CreatePoll(long meetingId, string title, IEnumerable questions, CancellationToken cancellationToken = default) + { + var data = new JObject() + { + { "title", title } + }; + data.AddPropertyIfValue("questions", questions); + + return _client + .PostAsync($"meetings/{meetingId}/polls") + .WithJsonBody(data) + .WithCancellationToken(cancellationToken) + .AsObject(); + } + + /// + /// Retrieve the details of a meeting. + /// + /// The meeting id + /// The poll id. + /// The cancellation token. + /// + /// The . + /// + public Task GetPollAsync(long meetingId, long pollId, CancellationToken cancellationToken = default) + { + return _client + .GetAsync($"meetings/{meetingId}/polls/{pollId}") + .WithCancellationToken(cancellationToken) + .AsObject(); + } + + /// + /// Update a poll for a meeting. + /// + /// The meeting ID. + /// The poll id. + /// Title for the poll. + /// The poll questions. + /// The cancellation token. + /// + /// The async task. + /// + public Task UpdatePollAsync(long meetingId, long pollId, string title, IEnumerable questions, CancellationToken cancellationToken = default) + { + var data = new JObject(); + data.AddPropertyIfValue("title", title); + data.AddPropertyIfValue("questions", questions); + + return _client + .PutAsync($"meetings/{meetingId}/polls/{pollId}") + .WithJsonBody(data) + .WithCancellationToken(cancellationToken) + .AsMessage(); + } + + /// + /// Delete a poll for a meeting. + /// + /// The meeting ID. + /// The poll id. + /// The cancellation token. + /// + /// The async task. + /// + public Task DeletePollAsync(long meetingId, long pollId, CancellationToken cancellationToken = default) + { + return _client + .DeleteAsync($"meetings/{meetingId}/polls/{pollId}") + .WithCancellationToken(cancellationToken) + .AsMessage(); + } + + /// + /// Get the meeting invite note that was sent for a specific meeting. + /// + /// The meeting ID. + /// The cancellation token. + /// + /// The invite note. + /// + public Task GetInvitationAsync(long meetingId, CancellationToken cancellationToken = default) + { + return _client + .GetAsync($"meetings/{meetingId}/invitation") + .WithCancellationToken(cancellationToken) + .AsObject("invitation"); + } + + /// + /// Update a meeting’s live stream information. + /// + /// The meeting ID. + /// Streaming URL. + /// Stream name and key + /// The live stream page URL. + /// The cancellation token. + /// + /// The async task. + /// + public Task UpdateLiveStreamAsync(long meetingId, string streamUrl, string streamKey, string pageUrl, CancellationToken cancellationToken = default) + { + var data = new JObject() + { + { "stream_url", streamUrl }, + { "stream_key", streamKey }, + { "page_url", pageUrl } + }; + + return _client + .PatchAsync($"meetings/{meetingId}/livestream") + .WithJsonBody(data) + .WithCancellationToken(cancellationToken) + .AsMessage(); + } + + /// + /// Start a meeting’s live stream. + /// + /// The meeting ID. + /// Display the name of the active speaker during a live stream. + /// The name of the speaker. + /// The cancellation token. + /// + /// The async task. + /// + public Task StartLiveStreamAsync(long meetingId, bool displaySpeakerName, string speakerName, CancellationToken cancellationToken = default) + { + var data = new JObject() + { + { "action", "Start" }, + { + "settings", new JObject() + { + { "active_speaker_name", displaySpeakerName }, + { "display_name", speakerName } + } + } + }; + + return _client + .PatchAsync($"meetings/{meetingId}/livestream/status") + .WithJsonBody(data) + .WithCancellationToken(cancellationToken) + .AsMessage(); + } + + /// + /// Stop a meeting’s live stream. + /// + /// The meeting ID. + /// The cancellation token. + /// + /// The async task. + /// + public Task StopLiveStreamAsync(long meetingId, CancellationToken cancellationToken = default) + { + var data = new JObject() + { + { "action", "stop" } + }; + + return _client + .PatchAsync($"meetings/{meetingId}/livestream/status") + .WithJsonBody(data) + .WithCancellationToken(cancellationToken) + .AsMessage(); + } + private Task UpdateRegistrantsStatusAsync(long meetingId, IEnumerable<(string RegistrantId, string RegistrantEmail)> registrantsInfo, string status, string occurrenceId = null, CancellationToken cancellationToken = default) { var data = new JObject(); From e51fa8d67e6d2c215c1415d9a4b6072c275bf753 Mon Sep 17 00:00:00 2001 From: Jericho Date: Tue, 5 May 2020 11:01:09 -0400 Subject: [PATCH 39/41] Fix SA1629 Documentation text should end with a period --- Source/ZoomNet/Resources/Meetings.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/ZoomNet/Resources/Meetings.cs b/Source/ZoomNet/Resources/Meetings.cs index 434f33fc..a1732509 100644 --- a/Source/ZoomNet/Resources/Meetings.cs +++ b/Source/ZoomNet/Resources/Meetings.cs @@ -434,7 +434,7 @@ public Task CreatePoll(long meetingId, string title, IEnumerable /// Retrieve the details of a meeting. /// - /// The meeting id + /// The meeting id. /// The poll id. /// The cancellation token. /// @@ -510,7 +510,7 @@ public Task GetInvitationAsync(long meetingId, CancellationToken cancell /// /// The meeting ID. /// Streaming URL. - /// Stream name and key + /// Stream name and key. /// The live stream page URL. /// The cancellation token. /// From 7f1cd45d93e14710d9fde8aaa6b012255f715981 Mon Sep 17 00:00:00 2001 From: Jericho Date: Tue, 5 May 2020 15:36:40 -0400 Subject: [PATCH 40/41] Remove dead code --- .../ZoomNet/Utilities/DaysOfWeekConverter.cs | 118 ------------------ 1 file changed, 118 deletions(-) diff --git a/Source/ZoomNet/Utilities/DaysOfWeekConverter.cs b/Source/ZoomNet/Utilities/DaysOfWeekConverter.cs index 228dc4f1..3738c873 100644 --- a/Source/ZoomNet/Utilities/DaysOfWeekConverter.cs +++ b/Source/ZoomNet/Utilities/DaysOfWeekConverter.cs @@ -1,5 +1,4 @@ using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using System; using System.Linq; @@ -106,124 +105,7 @@ but the values returned by the Zoom API start at one (i.e.: Sunday=1, Monday=2, .ToArray(); } - JValue jValue = new JValue(reader.Value); - switch (reader.TokenType) - { - //case JsonToken.Integer: - // value = Convert.ToInt32(reader.Value); - // break; - //case JsonToken.Float: - // value = Convert.ToDecimal(reader.Value); - // break; - //case JsonToken.String: - // value = Convert.ToString(reader.Value); - // break; - //case JsonToken.Boolean: - // value = Convert.ToBoolean(reader.Value); - // break; - //case JsonToken.Null: - // value = null; - // break; - //case JsonToken.Date: - // value = Convert.ToDateTime(reader.Value); - // break; - //case JsonToken.Bytes: - // value = Convert.ToByte(reader.Value); - // break; - default: - Console.WriteLine("Default case"); - Console.WriteLine(reader.TokenType.ToString()); - break; - } - - //if (reader.TokenType == JsonToken.String) - //{ - // var jValue = JV JArray.Load(reader); - - // if (objectType == typeof(DayOfWeek)) - // { - // } - // else if objectType == typeof(DayOfWeek[]) - // { - // } - // case DayOfWeek dayOfWeek: - // var singleDay = (Convert.ToInt32(dayOfWeek) + 1).ToString(); - // serializer.Serialize(writer, singleDay); - // break; - // case DayOfWeek[] daysOfWeek: - // var multipleDays = string.Join(",", daysOfWeek.Select(day => (Convert.ToInt32(day) + 1).ToString())); - // serializer.Serialize(writer, multipleDays); - // break; - // default: - // throw new Exception("Unable to serialize the value"); - //} - - //if (reader.TokenType == JsonToken.String) - //{ - // var jArray = JArray.Load(reader); - // var items = jArray - // .OfType() - // .Select(item => Convert(item, serializer)) - // .Where(item => item != null) - // .ToArray(); - - // return items; - //} - //else if (reader.TokenType == JsonToken.StartObject) - //{ - // var jObject = JObject.Load(reader); - // return Convert(jObject, serializer); - //} - throw new Exception("Unable to deserialize the value"); } - - //private Event Convert(JObject jsonObject, JsonSerializer serializer) - //{ - // jsonObject.TryGetValue("event_name", StringComparison.OrdinalIgnoreCase, out JToken eventTypeJsonProperty); - // var eventType = (EventType)eventTypeJsonProperty.ToObject(typeof(EventType)); - - // var emailActivityEvent = (Event)null; - // switch (eventType) - // { - // case EventType.Bounce: - // emailActivityEvent = jsonObject.ToObject(serializer); - // break; - // case EventType.Open: - // emailActivityEvent = jsonObject.ToObject(serializer); - // break; - // case EventType.Click: - // emailActivityEvent = jsonObject.ToObject(serializer); - // break; - // case EventType.Processed: - // emailActivityEvent = jsonObject.ToObject(serializer); - // break; - // case EventType.Dropped: - // emailActivityEvent = jsonObject.ToObject(serializer); - // break; - // case EventType.Delivered: - // emailActivityEvent = jsonObject.ToObject(serializer); - // break; - // case EventType.Deferred: - // emailActivityEvent = jsonObject.ToObject(serializer); - // break; - // case EventType.SpamReport: - // emailActivityEvent = jsonObject.ToObject(serializer); - // break; - // case EventType.Unsubscribe: - // emailActivityEvent = jsonObject.ToObject(serializer); - // break; - // case EventType.GroupUnsubscribe: - // emailActivityEvent = jsonObject.ToObject(serializer); - // break; - // case EventType.GroupResubscribe: - // emailActivityEvent = jsonObject.ToObject(serializer); - // break; - // default: - // throw new Exception($"{eventTypeJsonProperty.ToString()} is an unknown event type"); - // } - - // return emailActivityEvent; - //} } } From 7a8cef4a7c380ce2ce254d6827bb0b1ccffad00e Mon Sep 17 00:00:00 2001 From: Jericho Date: Tue, 5 May 2020 15:40:17 -0400 Subject: [PATCH 41/41] Fix typos in XML comments --- Source/ZoomNet/Resources/IMeetings.cs | 4 ++-- Source/ZoomNet/Resources/PastMeetings.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/ZoomNet/Resources/IMeetings.cs b/Source/ZoomNet/Resources/IMeetings.cs index 3fd31aad..ffe46c97 100644 --- a/Source/ZoomNet/Resources/IMeetings.cs +++ b/Source/ZoomNet/Resources/IMeetings.cs @@ -235,7 +235,7 @@ public interface IMeetings /// /// Retrieve the details of a meeting. /// - /// The meeting id + /// The meeting id. /// The poll id. /// The cancellation token. /// @@ -282,7 +282,7 @@ public interface IMeetings /// /// The meeting ID. /// Streaming URL. - /// Stream name and key + /// Stream name and key. /// The live stream page URL. /// The cancellation token. /// diff --git a/Source/ZoomNet/Resources/PastMeetings.cs b/Source/ZoomNet/Resources/PastMeetings.cs index 0a00f7ef..a21eab35 100644 --- a/Source/ZoomNet/Resources/PastMeetings.cs +++ b/Source/ZoomNet/Resources/PastMeetings.cs @@ -19,7 +19,7 @@ public class PastMeetings : IPastMeetings private readonly Pathoschild.Http.Client.IClient _client; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The HTTP client. internal PastMeetings(Pathoschild.Http.Client.IClient client)