diff --git a/tools/Custom/EventExtensions.cs b/tools/Custom/EventExtensions.cs
new file mode 100644
index 00000000000..df05433e553
--- /dev/null
+++ b/tools/Custom/EventExtensions.cs
@@ -0,0 +1,38 @@
+// ------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
+// ------------------------------------------------------------------------------
+
+namespace Microsoft.Graph.PowerShell
+{
+ using System;
+ using System.Text.RegularExpressions;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Microsoft.Graph.PowerShell.Runtime;
+
+ public static class EventExtensions
+ {
+ ///
+ /// Print event details to the provided stream
+ ///
+ /// The event data to print
+ /// The delegate for signaling events to the runtime
+ /// The cancellation token for the request
+ /// The name of the stream to print data to
+ /// The name of the event to be printed
+ public static async void Print(this Func getEventData, Func, Task> signal, CancellationToken token, string streamName, string eventName)
+ {
+ var eventDisplayName = EventFactory.SplitPascalCase(eventName).ToUpperInvariant();
+ var data = EventDataConverter.ConvertFrom(getEventData()); // also, we manually use our TypeConverter to return an appropriate type
+ if (data.Id != Events.Verbose && data.Id != Events.Warning && data.Id != Events.Debug && data.Id != Events.Information && data.Id != Events.Error)
+ {
+ await signal(streamName, token, () => EventFactory.CreateLogEvent($"{eventDisplayName} The contents are '{data?.Id}' and '{data?.Message}'"));
+ if (data != null)
+ {
+ await signal(streamName, token, () => EventFactory.CreateLogEvent($"{eventDisplayName} Parameter: '{data.Parameter}'\n{eventDisplayName} RequestMessage '{data.RequestMessage}'\n{eventDisplayName} Response: '{data.ResponseMessage}'\n{eventDisplayName} Value: '{data.Value}'"));
+ await signal(streamName, token, () => EventFactory.CreateLogEvent($"{eventDisplayName} ExtendedData Type: '{data.ExtendedData?.GetType()}'\n{eventDisplayName} ExtendedData '{data.ExtendedData}'"));
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tools/Custom/EventFactory.cs b/tools/Custom/EventFactory.cs
new file mode 100644
index 00000000000..2e2966de6e8
--- /dev/null
+++ b/tools/Custom/EventFactory.cs
@@ -0,0 +1,73 @@
+// ------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
+// ------------------------------------------------------------------------------
+
+using System;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Graph.PowerShell.Runtime;
+
+namespace Microsoft.Graph.PowerShell
+{
+ public static class EventFactory
+ {
+ ///
+ /// Create a tracing event containing a string message
+ ///
+ /// The string message to include in event data
+ /// Valid EventData containing the message
+ public static EventData CreateLogEvent(Task message)
+ {
+ return new EventData
+ {
+ Id = Guid.NewGuid().ToString(),
+ Message = message.Result
+ };
+ }
+
+ ///
+ /// Create a new debug message event
+ ///
+ /// The message
+ /// An event containing the debug message
+ public static EventData CreateDebugEvent(string message)
+ {
+ return new EventData
+ {
+ Id = Events.Debug,
+ Message = message
+ };
+ }
+
+ ///
+ /// Create a new debug message event
+ ///
+ /// The message
+ /// An event containing the debug message
+ public static EventData CreateWarningEvent(string message)
+ {
+ return new EventData
+ {
+ Id = Events.Warning,
+ Message = message
+ };
+ }
+ public static string SplitPascalCase(string word)
+ {
+ var regex = new Regex("([a-z]+)([A-Z])");
+ var output = regex.Replace(word, "$1 $2");
+ regex = new Regex("([A-Z])([A-Z][a-z])");
+ return regex.Replace(output, "$1 $2");
+ }
+
+ public static EventArgs CreateLogEvent(string message)
+ {
+ return new EventData
+ {
+ Id = Guid.NewGuid().ToString(),
+ Message = message
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tools/Custom/HttpMessageFormatter.cs b/tools/Custom/HttpMessageFormatter.cs
new file mode 100644
index 00000000000..8bd25ea08a8
--- /dev/null
+++ b/tools/Custom/HttpMessageFormatter.cs
@@ -0,0 +1,360 @@
+// ------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
+// ------------------------------------------------------------------------------
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.Contracts;
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Microsoft.Graph.PowerShell
+{
+ ///
+ /// Derived class which can encapsulate an
+ /// or an as an entity with media type "application/http".
+ ///
+ internal class HttpMessageFormatter : HttpContent
+ {
+ private const string SP = " ";
+ private const string ColonSP = ": ";
+ private const string CRLF = "\r\n";
+ private const string CommaSeparator = ", ";
+
+ private const int DefaultHeaderAllocation = 2 * 1024;
+
+ private const string DefaultMediaType = "application/http";
+
+ private const string MsgTypeParameter = "msgtype";
+ private const string DefaultRequestMsgType = "request";
+ private const string DefaultResponseMsgType = "response";
+
+ // Set of header fields that only support single values such as Set-Cookie.
+ private static readonly HashSet _singleValueHeaderFields = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "Cookie",
+ "Set-Cookie",
+ "X-Powered-By",
+ };
+
+ // Set of header fields that should get serialized as space-separated values such as User-Agent.
+ private static readonly HashSet _spaceSeparatedValueHeaderFields = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "User-Agent",
+ };
+
+ // Set of header fields that should not get serialized
+ private static readonly HashSet _neverSerializedHeaderFields = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "SdkVersion",
+ "FeatureFlag",
+ "Authorization",
+ "Cache-Control",
+ "Transfer-Encoding",
+ "Duration",
+ "Strict-Transport-Security",
+ "Date"
+ };
+
+ private bool _contentConsumed;
+ private Lazy> _streamTask;
+
+ ///
+ /// Initializes a new instance of the class encapsulating an
+ /// .
+ ///
+ /// The instance to encapsulate.
+ public HttpMessageFormatter(HttpRequestMessage httpRequest)
+ {
+ HttpRequestMessage = httpRequest ?? throw new ArgumentNullException(nameof(httpRequest));
+ Headers.ContentType = new MediaTypeHeaderValue(DefaultMediaType);
+ Headers.ContentType.Parameters.Add(new NameValueHeaderValue(MsgTypeParameter, DefaultRequestMsgType));
+
+ InitializeStreamTask();
+ }
+
+ ///
+ /// Initializes a new instance of the class encapsulating an
+ /// .
+ ///
+ /// The instance to encapsulate.
+ public HttpMessageFormatter(HttpResponseMessage httpResponse)
+ {
+ HttpResponseMessage = httpResponse ?? throw new ArgumentNullException(nameof(httpResponse));
+ Headers.ContentType = new MediaTypeHeaderValue(DefaultMediaType);
+ Headers.ContentType.Parameters.Add(new NameValueHeaderValue(MsgTypeParameter, DefaultResponseMsgType));
+
+ InitializeStreamTask();
+ }
+
+ private HttpContent Content
+ {
+ get { return HttpRequestMessage != null ? HttpRequestMessage.Content : HttpResponseMessage.Content; }
+ }
+
+ ///
+ /// Gets the HTTP request message.
+ ///
+ public HttpRequestMessage HttpRequestMessage { get; private set; }
+
+ ///
+ /// Gets the HTTP response message.
+ ///
+ public HttpResponseMessage HttpResponseMessage { get; private set; }
+
+ private void InitializeStreamTask()
+ {
+ _streamTask = new Lazy>(() => Content?.ReadAsStreamAsync());
+ }
+
+ ///
+ /// Validates whether the content contains an HTTP Request or an HTTP Response.
+ ///
+ /// The content to validate.
+ /// if set to true if the content is either an HTTP Request or an HTTP Response.
+ /// Indicates whether validation failure should result in an or not.
+ /// true if content is either an HTTP Request or an HTTP Response
+ internal static bool ValidateHttpMessageContent(HttpContent content, bool isRequest, bool throwOnError)
+ {
+ if (content == null)
+ {
+ throw new ArgumentNullException(nameof(content));
+ }
+
+ MediaTypeHeaderValue contentType = content.Headers.ContentType;
+ if (contentType != null)
+ {
+ if (!contentType.MediaType.Equals(DefaultMediaType, StringComparison.OrdinalIgnoreCase))
+ {
+ if (throwOnError)
+ {
+ throw new ArgumentException("HttpMessageInvalidMediaType", nameof(content));
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ foreach (NameValueHeaderValue parameter in contentType.Parameters)
+ {
+ if (parameter.Name.Equals(MsgTypeParameter, StringComparison.OrdinalIgnoreCase))
+ {
+ string msgType = UnquoteToken(parameter.Value);
+ if (!msgType.Equals(isRequest ? DefaultRequestMsgType : DefaultResponseMsgType, StringComparison.OrdinalIgnoreCase))
+ {
+ if (throwOnError)
+ {
+ throw new ArgumentException("HttpMessageInvalidMediaType", nameof(content));
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+ }
+
+ if (throwOnError)
+ {
+ throw new ArgumentException("HttpMessageInvalidMediaType", nameof(content));
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+
+ public static string UnquoteToken(string token)
+ {
+ if (String.IsNullOrWhiteSpace(token))
+ {
+ return token;
+ }
+
+ if (token.StartsWith("\"", StringComparison.Ordinal) && token.EndsWith("\"", StringComparison.Ordinal) && token.Length > 1)
+ {
+ return token.Substring(1, token.Length - 2);
+ }
+
+ return token;
+ }
+
+
+ ///
+ /// Asynchronously serializes the object's content to the given .
+ ///
+ /// The to which to write.
+ /// The associated .
+ /// A instance that is asynchronously serializing the object's content.
+ protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context)
+ {
+ if (stream == null)
+ {
+ throw new ArgumentNullException(nameof(stream));
+ }
+
+ byte[] header = SerializeHeader();
+ await stream.WriteAsync(header, 0, header.Length);
+
+ if (Content != null)
+ {
+ Stream readStream = await _streamTask.Value;
+ ValidateStreamForReading(readStream);
+ await Content.CopyToAsync(stream);
+ }
+ }
+
+ ///
+ /// Computes the length of the stream if possible.
+ ///
+ /// The computed length of the stream.
+ /// true if the length has been computed; otherwise false.
+ protected override bool TryComputeLength(out long length)
+ {
+ // We have four states we could be in:
+ // 1. We have content, but the task is still running or finished without success
+ // 2. We have content, the task has finished successfully, and the stream came back as a null or non-seekable
+ // 3. We have content, the task has finished successfully, and the stream is seekable, so we know its length
+ // 4. We don't have content (streamTask.Value == null)
+ //
+ // For #1 and #2, we return false.
+ // For #3, we return true & the size of our headers + the content length
+ // For #4, we return true & the size of our headers
+
+ bool hasContent = _streamTask.Value != null;
+ length = 0;
+
+ // Cases #1, #2, #3
+ // We serialize header to a StringBuilder so that we can determine the length
+ // following the pattern for HttpContent to try and determine the message length.
+ // The perf overhead is no larger than for the other HttpContent implementations.
+ byte[] header = SerializeHeader();
+ length += header.Length;
+ return true;
+ }
+
+ ///
+ /// Serializes the HTTP request line.
+ ///
+ /// Where to write the request line.
+ /// The HTTP request.
+ private static void SerializeRequestLine(StringBuilder message, HttpRequestMessage httpRequest)
+ {
+ Contract.Assert(message != null, "message cannot be null");
+ message.Append(httpRequest.Method + SP);
+ message.Append(httpRequest.RequestUri.PathAndQuery + SP);
+ message.Append($"HTTP/{(httpRequest.Version != null ? httpRequest.Version.ToString(2) : "1.1")}{CRLF}");
+
+ // Only insert host header if not already present.
+ if (httpRequest.Headers.Host == null)
+ {
+ message.Append($"HTTP{ColonSP}{httpRequest.RequestUri.Authority}{CRLF}");
+ }
+ }
+
+ ///
+ /// Serializes the HTTP status line.
+ ///
+ /// Where to write the status line.
+ /// The HTTP response.
+ private static void SerializeStatusLine(StringBuilder message, HttpResponseMessage httpResponse)
+ {
+ Contract.Assert(message != null, "message cannot be null");
+ message.Append($"HTTP/{(httpResponse.Version != null ? httpResponse.Version.ToString(2) : "1.1")}{SP}");
+ message.Append((int)httpResponse.StatusCode + SP);
+ message.Append(httpResponse.ReasonPhrase + CRLF);
+ }
+
+ ///
+ /// Serializes the header fields.
+ ///
+ /// Where to write the status line.
+ /// The headers to write.
+ private static void SerializeHeaderFields(StringBuilder message, HttpHeaders headers)
+ {
+ Contract.Assert(message != null, "message cannot be null");
+ if (headers != null)
+ {
+ foreach (KeyValuePair> header in headers)
+ {
+ if (_neverSerializedHeaderFields.Contains(header.Key))
+ {
+ continue;
+ }
+ if (_singleValueHeaderFields.Contains(header.Key))
+ {
+ foreach (string value in header.Value)
+ {
+ message.Append(header.Key + ColonSP + value + CRLF);
+ }
+ }
+ else if (_spaceSeparatedValueHeaderFields.Contains(header.Key))
+ {
+ message.Append(header.Key + ColonSP + String.Join(SP, header.Value) + CRLF);
+ }
+ else
+ {
+ message.Append(header.Key + ColonSP + String.Join(CommaSeparator, header.Value) + CRLF);
+ }
+ }
+ }
+ }
+
+ private byte[] SerializeHeader()
+ {
+ StringBuilder message = new StringBuilder(DefaultHeaderAllocation);
+ HttpHeaders headers;
+ HttpContent content;
+ if (HttpRequestMessage != null)
+ {
+ SerializeRequestLine(message, HttpRequestMessage);
+ headers = HttpRequestMessage.Headers;
+ content = HttpRequestMessage.Content;
+ }
+ else
+ {
+ SerializeStatusLine(message, HttpResponseMessage);
+ headers = HttpResponseMessage.Headers;
+ content = HttpResponseMessage.Content;
+ }
+
+ SerializeHeaderFields(message, headers);
+ if (content != null)
+ {
+ SerializeHeaderFields(message, content.Headers);
+ }
+
+ message.Append(CRLF);
+ return Encoding.UTF8.GetBytes(message.ToString());
+ }
+
+ private void ValidateStreamForReading(Stream stream)
+ {
+ // If the content needs to be written to a target stream a 2nd time, then the stream must support
+ // seeking (e.g. a FileStream), otherwise the stream can't be copied a second time to a target
+ // stream (e.g. a NetworkStream).
+ if (_contentConsumed)
+ {
+ if (stream != null && stream.CanRead)
+ {
+ stream.Position = 0;
+ }
+ else
+ {
+ throw new InvalidOperationException("HttpMessageContentAlreadyRead");
+ }
+ }
+
+ _contentConsumed = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tools/Custom/Module.cs b/tools/Custom/Module.cs
index 1646a82143d..4766b09682c 100644
--- a/tools/Custom/Module.cs
+++ b/tools/Custom/Module.cs
@@ -1,11 +1,19 @@
// ------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
// ------------------------------------------------------------------------------
+
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Microsoft.Graph.PowerShell.Runtime;
+
namespace Microsoft.Graph.PowerShell
{
using Microsoft.Graph.PowerShell.Authentication;
using Microsoft.Graph.PowerShell.Authentication.Helpers;
using Microsoft.Graph.PowerShell.Authentication.Models;
+
using System;
using System.Linq;
using System.Management.Automation;
@@ -19,7 +27,70 @@ public partial class Module
public string ProfileName { get; set; } = "v1.0-beta";
partial void BeforeCreatePipeline(System.Management.Automation.InvocationInfo invocationInfo, ref Runtime.HttpPipeline pipeline)
{
+ // Call Init to trigger any custom initialization needed after
+ // module load and before pipeline is setup and used.
+ Init();
pipeline = new Runtime.HttpPipeline(new Runtime.HttpClientFactory(HttpHelpers.GetGraphHttpClient()));
}
+
+ ///
+ /// Any needed Custom Initialization.
+ ///
+ partial void CustomInit()
+ {
+ this.EventListener = EventHandler;
+ }
+
+ ///
+ /// Common Module Event Listener, allows to handle emitted by CmdLets
+ ///
+ /// The ID of the event
+ /// The cancellation token for the event
+ /// A delegate to get the detailed event data
+ /// The callback for the event dispatcher
+ /// The from the cmdlet
+ /// The cmdlet's parameterset name
+ /// the exception that is being thrown (if available)
+ ///
+ /// A that will be complete when handling of the event is completed.
+ ///
+ public async Task EventHandler(string id, CancellationToken cancellationToken, Func getEventData, Func, Task> signal, InvocationInfo invocationInfo, string parameterSetName, System.Exception exception)
+ {
+ switch (id)
+ {
+ case Events.Finally:
+ await Finally(id, cancellationToken, getEventData, signal);
+ break;
+ default:
+ getEventData.Print(signal, cancellationToken, Events.Information, id);
+ break;
+ }
+ }
+
+ ///
+ /// Handles the Finally event, which is called just before Request and Response are disposed.
+ ///
+ /// The ID of the event
+ /// The cancellation token for the event
+ /// A delegate to get the detailed event data
+ /// The callback for the event dispatcher
+ ///
+ /// A that will be complete when handling of the event is completed.
+ ///
+ private async Task Finally(string id, CancellationToken cancellationToken, Func getEventData, Func, Task> signal)
+ {
+ using (Extensions.NoSynchronizationContext)
+ {
+ var eventData = EventDataConverter.ConvertFrom(getEventData());
+ using (var requestFormatter = new HttpMessageFormatter(eventData.RequestMessage as HttpRequestMessage))
+ using (var responseFormatter = new HttpMessageFormatter(eventData.ResponseMessage as HttpResponseMessage))
+ {
+ var requestString = await requestFormatter.ReadAsStringAsync();
+ var responseString = await responseFormatter.ReadAsStringAsync();
+ await signal(Events.Debug, cancellationToken, () => EventFactory.CreateLogEvent(requestString));
+ await signal(Events.Debug, cancellationToken, () => EventFactory.CreateLogEvent(responseString));
+ }
+ }
+ }
}
}