From c16a939caade94f1a75b50d6864d5241269550e8 Mon Sep 17 00:00:00 2001 From: Release-Agent <> Date: Wed, 6 Nov 2024 19:55:54 +0000 Subject: [PATCH] '' --- src/Build.Common.core.props | 3 +- .../Client/ConnectionService.cs | 3 +- .../Client/DataverseTraceLogger.cs | 53 ++-- .../DataverseConnectionException.cs | 99 ++++++++ .../Exceptions/DataverseOperationException.cs | 100 ++++++++ .../Exceptions/HttpOperationException.cs | 56 +++++ .../Client/HttpUtils/HttpMessageWrapper.cs | 55 +++++ .../HttpUtils/HttpRequestMessageWrapper.cs | 61 +++++ .../Client/HttpUtils/HttpRequestSanitizer.cs | 64 +++++ .../HttpUtils/HttpResponseMessageWrapper.cs | 45 ++++ .../InternalExtensions/HttpExtensions.cs | 228 ++++++++++++++++++ ...soft.PowerPlatform.Dataverse.Client.csproj | 5 +- .../DataverseClient/Client/Utils/Utils.cs | 1 - ...rPlatform.Dataverse.Client.Dynamics.csproj | 1 + ...rm.Dataverse.ServiceClientConverter.csproj | 8 +- ...tform.Dataverse.ServiceClientConverter.sln | 25 ++ .../ServiceClientTests.cs | 6 +- src/Packages.props | 4 +- ...crosoft.PowerPlatform.Dataverse.Client.xml | 2 +- ...Platform.Dataverse.Client.ReleaseNotes.txt | 14 ++ ...soft.PowerPlatform.Dataverse.Client.nuspec | 30 ++- 21 files changed, 810 insertions(+), 53 deletions(-) create mode 100644 src/GeneralTools/DataverseClient/Client/Exceptions/DataverseConnectionException.cs create mode 100644 src/GeneralTools/DataverseClient/Client/Exceptions/DataverseOperationException.cs create mode 100644 src/GeneralTools/DataverseClient/Client/Exceptions/HttpOperationException.cs create mode 100644 src/GeneralTools/DataverseClient/Client/HttpUtils/HttpMessageWrapper.cs create mode 100644 src/GeneralTools/DataverseClient/Client/HttpUtils/HttpRequestMessageWrapper.cs create mode 100644 src/GeneralTools/DataverseClient/Client/HttpUtils/HttpRequestSanitizer.cs create mode 100644 src/GeneralTools/DataverseClient/Client/HttpUtils/HttpResponseMessageWrapper.cs create mode 100644 src/GeneralTools/DataverseClient/Client/InternalExtensions/HttpExtensions.cs create mode 100644 src/GeneralTools/DataverseClient/Extensions/Microsoft.PowerPlatform.Dataverse.ServiceClientConverter/Microsoft.PowerPlatform.Dataverse.ServiceClientConverter.sln diff --git a/src/Build.Common.core.props b/src/Build.Common.core.props index 0c2dc66..22cc767 100644 --- a/src/Build.Common.core.props +++ b/src/Build.Common.core.props @@ -5,8 +5,9 @@ - net462;net472;net48;netstandard2.0;net6.0 + net462;net472;net48;netstandard2.0;net6.0;net8.0 false + NU5104 diff --git a/src/GeneralTools/DataverseClient/Client/ConnectionService.cs b/src/GeneralTools/DataverseClient/Client/ConnectionService.cs index 1151ba3..ef39b09 100644 --- a/src/GeneralTools/DataverseClient/Client/ConnectionService.cs +++ b/src/GeneralTools/DataverseClient/Client/ConnectionService.cs @@ -9,10 +9,11 @@ using Microsoft.PowerPlatform.Dataverse.Client.Auth.TokenCache; using Microsoft.PowerPlatform.Dataverse.Client.Connector; using Microsoft.PowerPlatform.Dataverse.Client.Connector.OnPremises; +using Microsoft.PowerPlatform.Dataverse.Client.Exceptions; +using Microsoft.PowerPlatform.Dataverse.Client.HttpUtils; using Microsoft.PowerPlatform.Dataverse.Client.InternalExtensions; using Microsoft.PowerPlatform.Dataverse.Client.Model; using Microsoft.PowerPlatform.Dataverse.Client.Utils; -using Microsoft.Rest; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Discovery; using Microsoft.Xrm.Sdk.Messages; diff --git a/src/GeneralTools/DataverseClient/Client/DataverseTraceLogger.cs b/src/GeneralTools/DataverseClient/Client/DataverseTraceLogger.cs index 2dfae38..daf9ab3 100644 --- a/src/GeneralTools/DataverseClient/Client/DataverseTraceLogger.cs +++ b/src/GeneralTools/DataverseClient/Client/DataverseTraceLogger.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerPlatform.Dataverse.Client.Utils; -using Microsoft.Rest; using Microsoft.Xrm.Sdk; using Newtonsoft.Json.Linq; using System; @@ -12,6 +11,7 @@ using System.ServiceModel; using System.Linq; using System.Text; +using Microsoft.PowerPlatform.Dataverse.Client.Exceptions; namespace Microsoft.PowerPlatform.Dataverse.Client { @@ -21,9 +21,6 @@ namespace Microsoft.PowerPlatform.Dataverse.Client [LocalizableAttribute(false)] internal sealed class DataverseTraceLogger : TraceLoggerBase { - // Internal connection of exceptions since last clear. - private List _ActiveExceptionsList; - internal ILogger _logger; #region Properties @@ -79,8 +76,6 @@ public DataverseTraceLogger(string traceSourceName = "") TraceSourceName = traceSourceName; } - _ActiveExceptionsList = new List(); - base.Initialize(); } @@ -88,7 +83,6 @@ public DataverseTraceLogger(ILogger logger) { _logger = logger; TraceSourceName = DefaultTraceSourceName; - _ActiveExceptionsList = new List(); base.Initialize(); } @@ -98,7 +92,6 @@ public override void ResetLastError() if (base.LastError.Length > 0) base.LastError = base.LastError.Remove(0, LastError.Length - 1); LastException = null; - _ActiveExceptionsList.Clear(); } /// @@ -151,39 +144,34 @@ public override void Log(string message, TraceEventType eventType, Exception exc exception = new Exception(message); } - StringBuilder detailedDump = new StringBuilder(); - StringBuilder lastMessage = new StringBuilder(); + StringBuilder detailedDump = new StringBuilder(4096); + StringBuilder lastMessage = new StringBuilder(2048); lastMessage.AppendLine(message); // Added to fix missing last error line. detailedDump.AppendLine(message); // Added to fix missing error line. - if (!(exception != null && _ActiveExceptionsList.Contains(exception))) // Skip this line if its already been done. - GetExceptionDetail(exception, detailedDump, 0, lastMessage); + GetExceptionDetail(exception, detailedDump, 0, lastMessage); TraceEvent(eventType, (int)eventType, detailedDump.ToString(), exception); if (eventType == TraceEventType.Error) { base.LastError += lastMessage.ToString(); - if (!(exception != null && _ActiveExceptionsList.Contains(exception))) // Skip this line if its already been done. + // check and or alter the exception is its and HTTPOperationExecption. + if (exception is HttpOperationException httpOperationException) { - // check and or alter the exception is its and HTTPOperationExecption. - if (exception is HttpOperationException httpOperationException) + string errorMessage = "Not Provided"; + if (!string.IsNullOrWhiteSpace(httpOperationException.Response.Content)) { - string errorMessage = "Not Provided"; - if (!string.IsNullOrWhiteSpace(httpOperationException.Response.Content)) - { - JObject contentBody = JObject.Parse(httpOperationException.Response.Content); - errorMessage = string.IsNullOrEmpty(contentBody["error"]["message"]?.ToString()) ? "Not Provided" : GetFirstLineFromString(contentBody["error"]["message"]?.ToString()).Trim(); - } - - Utils.DataverseOperationException webApiExcept = new Utils.DataverseOperationException(errorMessage, httpOperationException); - LastException = webApiExcept; + JObject contentBody = JObject.Parse(httpOperationException.Response.Content); + errorMessage = string.IsNullOrEmpty(contentBody["error"]["message"]?.ToString()) ? "Not Provided" : GetFirstLineFromString(contentBody["error"]["message"]?.ToString()).Trim(); } - else - LastException = exception; + + Utils.DataverseOperationException webApiExcept = new Utils.DataverseOperationException(errorMessage, httpOperationException); + LastException = webApiExcept; } + else + LastException = exception; } - _ActiveExceptionsList.Add(exception); detailedDump.Clear(); lastMessage.Clear(); @@ -196,18 +184,13 @@ public override void Log(string message, TraceEventType eventType, Exception exc /// public override void Log(Exception exception) { - if (exception != null && _ActiveExceptionsList.Contains(exception)) - return; // already logged this one . - - StringBuilder detailedDump = new StringBuilder(); - StringBuilder lastMessage = new StringBuilder(); + StringBuilder detailedDump = new StringBuilder(4096); + StringBuilder lastMessage = new StringBuilder(2048); GetExceptionDetail(exception, detailedDump, 0, lastMessage); TraceEvent(TraceEventType.Error, (int)TraceEventType.Error, detailedDump.ToString(), exception); base.LastError += lastMessage.ToString(); LastException = exception; - _ActiveExceptionsList.Add(exception); - detailedDump.Clear(); lastMessage.Clear(); } @@ -594,7 +577,7 @@ private static string GenerateOrgErrorDetailsInfo(ErrorDetailCollection errorDet { if (errorDetails != null && errorDetails.Count > 0) { - StringBuilder sw = new StringBuilder(); + StringBuilder sw = new StringBuilder(2048); sw.AppendLine("Error Details\t:"); foreach (var itm in errorDetails) { diff --git a/src/GeneralTools/DataverseClient/Client/Exceptions/DataverseConnectionException.cs b/src/GeneralTools/DataverseClient/Client/Exceptions/DataverseConnectionException.cs new file mode 100644 index 0000000..21f5006 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Exceptions/DataverseConnectionException.cs @@ -0,0 +1,99 @@ +//using Microsoft.Rest; +using Microsoft.PowerPlatform.Dataverse.Client.Exceptions; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; + +namespace Microsoft.PowerPlatform.Dataverse.Client.Utils +{ + /// + /// Used to encompass a ServiceClient Connection Centric exceptions + /// + [Serializable] + public class DataverseConnectionException : Exception + { + /// + /// Creates a dataverse connection Exception + /// + /// Error Message + public DataverseConnectionException(string message) + : base(message) + { + } + + /// + /// Creates a dataverse connection Exception + /// + /// Error Message + /// Supporting Exception + public DataverseConnectionException(string message, Exception innerException) + : base(message, innerException) + { + this.HResult = innerException.HResult; + } + + /// + /// Creates a dataverse connection Exception + /// + /// Error Message + /// Error code + /// Data Properties + /// Help Link + /// + public DataverseConnectionException(string message, int errorCode, string helpLink, IDictionary data, HttpOperationException httpOperationException = null) + : base(message, httpOperationException) + { + HResult = errorCode; + HelpLink = helpLink; + Source = "Dataverse Server API"; + foreach (var itm in data) + { + this.Data.Add(itm.Key, itm.Value); + } + } + + /// + /// Creates a Dataverse Connection Exception from an httpOperationError + /// + /// + /// + public static DataverseConnectionException GenerateClientConnectionException(HttpOperationException httpOperationException) + { + string errorDetailPrefixString = "@Microsoft.PowerApps.CDS.ErrorDetails."; + Dictionary cdsErrorData = new Dictionary(); + + JToken ErrorBlock = null; + try + { + if (!string.IsNullOrWhiteSpace(httpOperationException.Response.Content)) + { + JObject contentBody = JObject.Parse(httpOperationException.Response.Content); + ErrorBlock = contentBody["error"]; + } + } + catch { } + + if (ErrorBlock != null) + { + string errorMessage = DataverseTraceLogger.GetFirstLineFromString(ErrorBlock["message"]?.ToString()).Trim(); + var code = ErrorBlock["code"]; + int HResult = code != null && !string.IsNullOrWhiteSpace(code.ToString()) ? Convert.ToInt32(code.ToString(), 16) : -1; + + string HelpLink = ErrorBlock["@Microsoft.PowerApps.CDS.HelpLink"]?.ToString(); + + foreach (var node in ErrorBlock.ToArray()) + { + if (node.Path.Contains(errorDetailPrefixString)) + { + cdsErrorData.Add(node.Value().Name.ToString().Replace(errorDetailPrefixString, string.Empty), node.HasValues ? node.Value().Value.ToString() : string.Empty); + } + } + return new DataverseConnectionException(errorMessage, HResult, HelpLink, cdsErrorData, httpOperationException); + } + else + return new DataverseConnectionException("Server Error, no error report generated from server", -1, string.Empty, cdsErrorData, httpOperationException); + } + } +} diff --git a/src/GeneralTools/DataverseClient/Client/Exceptions/DataverseOperationException.cs b/src/GeneralTools/DataverseClient/Client/Exceptions/DataverseOperationException.cs new file mode 100644 index 0000000..75408b0 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Exceptions/DataverseOperationException.cs @@ -0,0 +1,100 @@ +//using Microsoft.Rest; +using Microsoft.PowerPlatform.Dataverse.Client.Exceptions; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PowerPlatform.Dataverse.Client.Utils +{ + /// + /// Used to encompass a ServiceClient Operation Exception + /// + [Serializable] + public class DataverseOperationException : Exception + { + /// + /// Creates a CdsService Client Exception + /// + /// Error Message + public DataverseOperationException(string message) + : base(message) + { + } + + /// + /// Creates a CdsService Client Exception + /// + /// Error Message + /// Error code + /// Data Properties + /// Help Link + /// + public DataverseOperationException(string message, int errorCode, string helpLink, IDictionary data, HttpOperationException httpOperationException = null) + : base(message, httpOperationException) + { + HResult = errorCode; + HelpLink = helpLink; + Source = "Dataverse Server API"; + foreach (var itm in data) + { + this.Data.Add(itm.Key, itm.Value); + } + } + + /// + /// Creates a CdsService Client Exception from a httpOperationResult. + /// + /// + public static DataverseOperationException GenerateClientOperationException(HttpOperationException httpOperationException) + { + string errorDetailPrefixString = "@Microsoft.PowerApps.CDS.ErrorDetails."; + Dictionary cdsErrorData = new Dictionary(); + + JToken ErrorBlock = null; + try + { + if (!string.IsNullOrWhiteSpace(httpOperationException.Response.Content)) + { + JObject contentBody = JObject.Parse(httpOperationException.Response.Content); + ErrorBlock = contentBody["error"]; + } + } + catch { } + + if (ErrorBlock != null) + { + string errorMessage = DataverseTraceLogger.GetFirstLineFromString(ErrorBlock["message"]?.ToString()).Trim(); + var code = ErrorBlock["code"]; + int HResult = code != null && !string.IsNullOrWhiteSpace(code.ToString()) ? Convert.ToInt32(code.ToString(), 16) : -1; + + string HelpLink = ErrorBlock["@Microsoft.PowerApps.CDS.HelpLink"]?.ToString(); + + foreach (var node in ErrorBlock.ToArray()) + { + if (node.Path.Contains(errorDetailPrefixString)) + { + cdsErrorData.Add(node.Value().Name.ToString().Replace(errorDetailPrefixString, string.Empty), node.HasValues ? node.Value().Value.ToString() : string.Empty); + } + } + return new DataverseOperationException(errorMessage, HResult, HelpLink, cdsErrorData, httpOperationException); + } + else + return new DataverseOperationException("Server Error, no error report generated from server", -1, string.Empty, cdsErrorData, httpOperationException); + } + + /// + /// Creates a CdsService Client Exception + /// + /// Error Message + /// Supporting Exception + public DataverseOperationException(string message, Exception innerException) + : base(message, innerException) + { + } + + } +} diff --git a/src/GeneralTools/DataverseClient/Client/Exceptions/HttpOperationException.cs b/src/GeneralTools/DataverseClient/Client/Exceptions/HttpOperationException.cs new file mode 100644 index 0000000..1dbbe2a --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Exceptions/HttpOperationException.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using Microsoft.PowerPlatform.Dataverse.Client.HttpUtils; + + +namespace Microsoft.PowerPlatform.Dataverse.Client.Exceptions +{ + /// + /// Http Exception wrapper class + /// + public class HttpOperationException : Exception + { + /// + /// + /// + public HttpOperationException() + { + } + + /// + /// + /// + /// + public HttpOperationException(string message) : this(message, null) + { + } + + /// + /// + /// + /// + /// + public HttpOperationException(string message, Exception innerException) : base(message, innerException) + { + } + + // Properties + /// + /// + /// + public object Body { get; set; } + + /// + /// + /// + public HttpRequestMessageWrapper Request { get; set; } + + /// + /// + /// + public HttpResponseMessageWrapper Response { get; set; } + + + } +} diff --git a/src/GeneralTools/DataverseClient/Client/HttpUtils/HttpMessageWrapper.cs b/src/GeneralTools/DataverseClient/Client/HttpUtils/HttpMessageWrapper.cs new file mode 100644 index 0000000..6264677 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/HttpUtils/HttpMessageWrapper.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net.Http.Headers; + +namespace Microsoft.PowerPlatform.Dataverse.Client.HttpUtils +{ + /// + /// Base class used to wrap HTTP requests and responses to preserve data after disposal of + /// HttpClient. + /// + public abstract class HttpMessageWrapper + { + /// + /// Initializes a new instance of the HttpMessageWrapper class. + /// + protected HttpMessageWrapper() + { + Headers = new Dictionary>(); + } + + /// + /// Exposes the HTTP message contents. + /// + public string Content { get; set; } + + /// + /// Gets the collection of HTTP headers. + /// + public IDictionary> Headers { get; private set; } + + /// + /// Copies HTTP message headers to the error object. + /// + /// Collection of HTTP headers. + protected void CopyHeaders(HttpHeaders headers) + { + if (headers != null) + { + foreach (KeyValuePair> header in headers) + { + IEnumerable values = null; + if (Headers.TryGetValue(header.Key, out values)) + { + values = values.Concat(header.Value); + } + else + { + values = header.Value; + } + Headers[header.Key] = values; + } + } + } + } +} diff --git a/src/GeneralTools/DataverseClient/Client/HttpUtils/HttpRequestMessageWrapper.cs b/src/GeneralTools/DataverseClient/Client/HttpUtils/HttpRequestMessageWrapper.cs new file mode 100644 index 0000000..eed3bd6 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/HttpUtils/HttpRequestMessageWrapper.cs @@ -0,0 +1,61 @@ +using Microsoft.PowerPlatform.Dataverse.Client.InternalExtensions; +using System; +using System.Collections.Generic; +using System.Net.Http; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +namespace Microsoft.PowerPlatform.Dataverse.Client.HttpUtils +{ + /// + /// Wrapper around HttpRequestMessage type that copies properties of HttpRequestMessage so that + /// they are available after the HttpClient gets disposed. + /// + public class HttpRequestMessageWrapper : HttpMessageWrapper + { + /// + /// Initializes a new instance of the HttpRequestMessageWrapper class from HttpRequestMessage + /// and content. + /// + public HttpRequestMessageWrapper(HttpRequestMessage httpRequest, string content) + { + if (httpRequest == null) + { + throw new ArgumentNullException("httpRequest"); + } + + CopyHeaders(httpRequest.Headers); + CopyHeaders(httpRequest.GetContentHeaders()); + HttpRequestSanitizer.SanitizerHeaders(Headers); + + Content = content; + Method = httpRequest.Method; + RequestUri = httpRequest.RequestUri; +#pragma warning disable CS0618 // Options are only supported in .net 6 + if (httpRequest.Properties != null) + { + Properties = new Dictionary(); + foreach (KeyValuePair pair in httpRequest.Properties) + { + Properties[pair.Key] = pair.Value; + } + } +#pragma warning restore CS0618 + } + + /// + /// Gets or sets the HTTP method used by the HTTP request message. + /// + public HttpMethod Method { get; protected set; } + + /// + /// Gets or sets the Uri used for the HTTP request. + /// + public Uri RequestUri { get; protected set; } + + /// + /// Gets a set of properties for the HTTP request. + /// + public IDictionary Properties { get; private set; } + } +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member +} diff --git a/src/GeneralTools/DataverseClient/Client/HttpUtils/HttpRequestSanitizer.cs b/src/GeneralTools/DataverseClient/Client/HttpUtils/HttpRequestSanitizer.cs new file mode 100644 index 0000000..70ff835 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/HttpUtils/HttpRequestSanitizer.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PowerPlatform.Dataverse.Client.HttpUtils +{ + /// + /// Sanitizer used internal by . + /// + internal class HttpRequestSanitizer + { + private readonly static string _redactedPlaceholder = "REDACTED"; + private readonly static HashSet _allowedHeaders = new HashSet(new string[] + { + "x-ms-request-id", + "x-ms-client-request-id", + "x-ms-return-client-request-id", + "traceparent", + "MS-CV", + + "Accept", + "Cache-Control", + "Connection", + "Content-Length", + "Content-Type", + "Date", + "ETag", + "Expires", + "If-Match", + "If-Modified-Since", + "If-None-Match", + "If-Unmodified-Since", + "Last-Modified", + "Pragma", + "Request-Id", + "Retry-After", + "Server", + "Transfer-Encoding", + "User-Agent", + "WWW-Authenticate" // OAuth Challenge header. + }, StringComparer.OrdinalIgnoreCase); + + /// + /// Sanitize value of sensitive headers in the given . + /// + /// A collection of headers to sanitize. + public static void SanitizerHeaders(IDictionary> headers) + { + if (headers == null) + { + return; + } + + var namesOfHeaderToSanitize = headers.Keys.Except(_allowedHeaders, StringComparer.OrdinalIgnoreCase).ToList(); + + foreach (string name in namesOfHeaderToSanitize) + { + headers[name] = new string[] { _redactedPlaceholder }; + } + } + } +} diff --git a/src/GeneralTools/DataverseClient/Client/HttpUtils/HttpResponseMessageWrapper.cs b/src/GeneralTools/DataverseClient/Client/HttpUtils/HttpResponseMessageWrapper.cs new file mode 100644 index 0000000..e3d3d0e --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/HttpUtils/HttpResponseMessageWrapper.cs @@ -0,0 +1,45 @@ +using System; +using System.Net.Http; +using System.Net; +using Microsoft.PowerPlatform.Dataverse.Client.InternalExtensions; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +namespace Microsoft.PowerPlatform.Dataverse.Client.HttpUtils +{ + /// + /// Wrapper around HttpResponseMessage type that copies properties of HttpResponseMessage so that + /// they are available after the HttpClient gets disposed. + /// + public class HttpResponseMessageWrapper : HttpMessageWrapper + { + /// + /// Initializes a new instance of the HttpResponseMessageWrapper class from HttpResponseMessage + /// and content. + /// + public HttpResponseMessageWrapper(HttpResponseMessage httpResponse, string content) + { + if (httpResponse == null) + { + throw new ArgumentNullException("httpResponse"); + } + + CopyHeaders(httpResponse.Headers); + CopyHeaders(httpResponse.GetContentHeaders()); + + Content = content; + StatusCode = httpResponse.StatusCode; + ReasonPhrase = httpResponse.ReasonPhrase; + } + + /// + /// Gets or sets the status code of the HTTP response. + /// + public HttpStatusCode StatusCode { get; protected set; } + + /// + /// Exposes the reason phrase, typically sent along with the status code. + /// + public string ReasonPhrase { get; protected set; } + } +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member +} diff --git a/src/GeneralTools/DataverseClient/Client/InternalExtensions/HttpExtensions.cs b/src/GeneralTools/DataverseClient/Client/InternalExtensions/HttpExtensions.cs new file mode 100644 index 0000000..f77c04e --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/InternalExtensions/HttpExtensions.cs @@ -0,0 +1,228 @@ +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http.Headers; +using System.Net.Http; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Input; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +namespace Microsoft.PowerPlatform.Dataverse.Client.InternalExtensions +{ + /// + /// Extensions for manipulating HTTP request and response objects. + /// + internal static class HttpExtensions + { + /// + /// Formats an HttpContent object as String. + /// + /// The HttpContent to format. + /// The formatted string. + public static string AsString(this HttpContent content) + { + if (content != null) + { + // Await for the content. + return + content + .ReadAsStringAsync() + .ConfigureAwait(false) + .GetAwaiter() + .GetResult(); + } + + return null; + } + + /// + /// Get the content headers of an HtttRequestMessage. + /// + /// The request message. + /// The content headers. + public static HttpHeaders GetContentHeaders(this HttpRequestMessage request) + { + if (request != null && request.Content != null) + { + return request.Content.Headers; + } + + return null; + } + + /// + /// Get the content headers of an HttpResponseMessage. + /// + /// The response message. + /// The content headers. + public static HttpHeaders GetContentHeaders(this HttpResponseMessage response) + { + if (response != null && response.Content != null) + { + return response.Content.Headers; + } + + return null; + } + + /// + /// Returns string representation of a HttpRequestMessage. + /// + /// Request object to format. + /// The string, formatted into curly braces. + public static string AsFormattedString(this HttpRequestMessage httpRequest) + { + if (httpRequest == null) + { + throw new ArgumentNullException("httpRequest"); + } + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.AppendLine(httpRequest.ToString()); + if (httpRequest.Content != null) + { + stringBuilder.AppendLine(); + stringBuilder.AppendLine("Body:"); + stringBuilder.AppendLine("{"); + stringBuilder.AppendLine(httpRequest.Content.ReadAsStringAsync().ConfigureAwait(false).GetAwaiter().GetResult()); + stringBuilder.AppendLine("}"); + } + return stringBuilder.ToString(); + } + + /// + /// Returns string representation of a HttpResponseMessage. + /// + /// Response object to format. + /// The string, formatted into curly braces. + public static string AsFormattedString(this HttpResponseMessage httpResponse) + { + if (httpResponse == null) + { + throw new ArgumentNullException("httpResponse"); + } + var stringBuilder = new StringBuilder(); + stringBuilder.AppendLine(httpResponse.ToString()); + if (httpResponse.Content != null) + { + stringBuilder.AppendLine(); + stringBuilder.AppendLine("Body:"); + stringBuilder.AppendLine("{"); + stringBuilder.AppendLine(httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false).GetAwaiter().GetResult()); + stringBuilder.AppendLine("}"); + } + return stringBuilder.ToString(); + } + + /// + /// Converts given dictionary into a log string. + /// + /// The dictionary key type. + /// The dictionary value type. + /// The dictionary object. + /// The string, formatted into curly braces. + public static string AsFormattedString(this IDictionary dictionary) + { + if (dictionary == null) + { + return "{}"; + } + + return "{" + string.Join(",", + dictionary.Select(kv => kv.Key.ToString() + + "=" + + (kv.Value == null ? string.Empty : kv.Value.ToString())) + .ToArray()) + "}"; + } + + /// + /// Serializes HttpHeaders as Json dictionary. + /// + /// HttpHeaders + /// Json string + public static JObject ToJson(this HttpHeaders headers) + { + if (headers == null || !headers.Any()) + { + return new JObject(); + } + else + { + return headers.ToDictionary(h => h.Key, h => h.Value).ToJson(); + } + } + + /// + /// Serializes header dictionary as Json dictionary. + /// + /// Dictionary + /// Json string + public static JObject ToJson(this IDictionary> headers) + { + if (headers == null || !headers.Any()) + { + return new JObject(); + } + else + { + var jObject = new JObject(); + foreach (var httpResponseHeader in headers) + { + if (httpResponseHeader.Value.Count() > 1) + { + jObject[httpResponseHeader.Key] = new JArray(httpResponseHeader.Value); + } + else + { + jObject[httpResponseHeader.Key] = httpResponseHeader.Value.FirstOrDefault(); + } + } + return jObject; + } + } + + /// + /// Serializes HttpResponseHeaders and HttpContentHeaders as Json dictionary. + /// + /// HttpResponseMessage + /// Json string + public static JObject GetHeadersAsJson(this HttpResponseMessage message) + { + if (message == null) + { + return new JObject(); + } + + var jObject = new JObject(); + foreach (var httpResponseHeader in message.Headers) + { + if (httpResponseHeader.Value.Count() > 1) + { + jObject[httpResponseHeader.Key] = new JArray(httpResponseHeader.Value); + } + else + { + jObject[httpResponseHeader.Key] = httpResponseHeader.Value.FirstOrDefault(); + } + } + if (message.Content != null) + { + foreach (var httpResponseHeader in message.Content.Headers) + { + if (httpResponseHeader.Value.Count() > 1) + { + jObject[httpResponseHeader.Key] = new JArray(httpResponseHeader.Value); + } + else + { + jObject[httpResponseHeader.Key] = httpResponseHeader.Value.FirstOrDefault(); + } + } + } + return jObject; + } + } +} +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member diff --git a/src/GeneralTools/DataverseClient/Client/Microsoft.PowerPlatform.Dataverse.Client.csproj b/src/GeneralTools/DataverseClient/Client/Microsoft.PowerPlatform.Dataverse.Client.csproj index 1dbd599..3eea4c1 100644 --- a/src/GeneralTools/DataverseClient/Client/Microsoft.PowerPlatform.Dataverse.Client.csproj +++ b/src/GeneralTools/DataverseClient/Client/Microsoft.PowerPlatform.Dataverse.Client.csproj @@ -32,10 +32,11 @@ - + + @@ -44,7 +45,7 @@ - + diff --git a/src/GeneralTools/DataverseClient/Client/Utils/Utils.cs b/src/GeneralTools/DataverseClient/Client/Utils/Utils.cs index c5a8efd..e8542b9 100644 --- a/src/GeneralTools/DataverseClient/Client/Utils/Utils.cs +++ b/src/GeneralTools/DataverseClient/Client/Utils/Utils.cs @@ -3,7 +3,6 @@ using Microsoft.Extensions.Options; using Microsoft.PowerPlatform.Dataverse.Client.Model; using Microsoft.PowerPlatform.Dataverse.Client.Utils; -using Microsoft.Rest; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Discovery; using Microsoft.Xrm.Sdk.Metadata; diff --git a/src/GeneralTools/DataverseClient/Extensions/DynamicsExtension/Microsoft.PowerPlatform.Dataverse.Client.Dynamics.csproj b/src/GeneralTools/DataverseClient/Extensions/DynamicsExtension/Microsoft.PowerPlatform.Dataverse.Client.Dynamics.csproj index 3ea8778..96f9235 100644 --- a/src/GeneralTools/DataverseClient/Extensions/DynamicsExtension/Microsoft.PowerPlatform.Dataverse.Client.Dynamics.csproj +++ b/src/GeneralTools/DataverseClient/Extensions/DynamicsExtension/Microsoft.PowerPlatform.Dataverse.Client.Dynamics.csproj @@ -15,6 +15,7 @@ + diff --git a/src/GeneralTools/DataverseClient/Extensions/Microsoft.PowerPlatform.Dataverse.ServiceClientConverter/Microsoft.PowerPlatform.Dataverse.ServiceClientConverter.csproj b/src/GeneralTools/DataverseClient/Extensions/Microsoft.PowerPlatform.Dataverse.ServiceClientConverter/Microsoft.PowerPlatform.Dataverse.ServiceClientConverter.csproj index 23a9223..a7b0535 100644 --- a/src/GeneralTools/DataverseClient/Extensions/Microsoft.PowerPlatform.Dataverse.ServiceClientConverter/Microsoft.PowerPlatform.Dataverse.ServiceClientConverter.csproj +++ b/src/GeneralTools/DataverseClient/Extensions/Microsoft.PowerPlatform.Dataverse.ServiceClientConverter/Microsoft.PowerPlatform.Dataverse.ServiceClientConverter.csproj @@ -2,19 +2,19 @@ Microsoft.PowerPlatform.Dataverse.ServiceClientConverter - DataverseClient + DataverseClientConverter true + net462;net472;net48 false $(OutDir)\Microsoft.PowerPlatform.Dataverse.ServiceClientConverter.xml - net462;net472;net48 - - + + diff --git a/src/GeneralTools/DataverseClient/Extensions/Microsoft.PowerPlatform.Dataverse.ServiceClientConverter/Microsoft.PowerPlatform.Dataverse.ServiceClientConverter.sln b/src/GeneralTools/DataverseClient/Extensions/Microsoft.PowerPlatform.Dataverse.ServiceClientConverter/Microsoft.PowerPlatform.Dataverse.ServiceClientConverter.sln new file mode 100644 index 0000000..54d62b3 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Extensions/Microsoft.PowerPlatform.Dataverse.ServiceClientConverter/Microsoft.PowerPlatform.Dataverse.ServiceClientConverter.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35327.3 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerPlatform.Dataverse.ServiceClientConverter", "Microsoft.PowerPlatform.Dataverse.ServiceClientConverter.csproj", "{1222F666-B1B9-4219-9EA9-EDCCEFDB7687}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1222F666-B1B9-4219-9EA9-EDCCEFDB7687}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1222F666-B1B9-4219-9EA9-EDCCEFDB7687}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1222F666-B1B9-4219-9EA9-EDCCEFDB7687}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1222F666-B1B9-4219-9EA9-EDCCEFDB7687}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {6944BCB7-36A1-45BC-AA26-489DC42D4EDA} + EndGlobalSection +EndGlobal diff --git a/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/ServiceClientTests.cs b/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/ServiceClientTests.cs index 71823ea..602801a 100644 --- a/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/ServiceClientTests.cs +++ b/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/ServiceClientTests.cs @@ -7,7 +7,9 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerPlatform.Dataverse.Client; using Microsoft.PowerPlatform.Dataverse.Client.Auth; +using Microsoft.PowerPlatform.Dataverse.Client.Exceptions; using Microsoft.PowerPlatform.Dataverse.Client.Extensions; +using Microsoft.PowerPlatform.Dataverse.Client.HttpUtils; using Microsoft.PowerPlatform.Dataverse.Client.Model; using Microsoft.PowerPlatform.Dataverse.Client.Utils; using Microsoft.Xrm.Sdk; @@ -138,10 +140,10 @@ public void LogWriteTest() // error throw. - Microsoft.Rest.HttpOperationException operationException = new Microsoft.Rest.HttpOperationException("HTTPOPEXC"); + HttpOperationException operationException = new HttpOperationException("HTTPOPEXC"); HttpResponseMessage Resp500 = new HttpResponseMessage(System.Net.HttpStatusCode.ServiceUnavailable); Resp500.Headers.Add("REQ_ID", "39393F77-8F8B-4416-846E-28B4D2AA5667"); - operationException.Response = new Microsoft.Rest.HttpResponseMessageWrapper(Resp500, "{\"error\":{\"code\":\"0x80040203\",\"message\":\"Communication activity cannot have more than one Sender party\",\"@Microsoft.PowerApps.CDS.ErrorDetails.ApiExceptionSourceKey\":\"Plugin/Microsoft.Crm.Common.ObjectModel.PhoneCallService\",\"@Microsoft.PowerApps.CDS.ErrorDetails.ApiStepKey\":\"3ccabb1b-ea3e-db11-86a7-000a3a5473e8\",\"@Microsoft.PowerApps.CDS.ErrorDetails.ApiDepthKey\":\"1\",\"@Microsoft.PowerApps.CDS.ErrorDetails.ApiActivityIdKey\":\"1736f387-e025-4828-a2bb-74ea8ac768a2\",\"@Microsoft.PowerApps.CDS.ErrorDetails.ApiPluginSolutionNameKey\":\"System\",\"@Microsoft.PowerApps.CDS.ErrorDetails.ApiStepSolutionNameKey\":\"System\",\"@Microsoft.PowerApps.CDS.ErrorDetails.ApiExceptionCategory\":\"ClientError\",\"@Microsoft.PowerApps.CDS.ErrorDetails.ApiExceptionMesageName\":\"InvalidArgument\",\"@Microsoft.PowerApps.CDS.ErrorDetails.ApiExceptionHttpStatusCode\":\"400\",\"@Microsoft.PowerApps.CDS.HelpLink\":\"http://go.microsoft.com/fwlink/?LinkID=398563&error=Microsoft.Crm.CrmException%3a80040203&client=platform\",\"@Microsoft.PowerApps.CDS.InnerError.Message\":\"Communication activity cannot have more than one Sender party\"}}"); + operationException.Response = new HttpResponseMessageWrapper(Resp500, "{\"error\":{\"code\":\"0x80040203\",\"message\":\"Communication activity cannot have more than one Sender party\",\"@Microsoft.PowerApps.CDS.ErrorDetails.ApiExceptionSourceKey\":\"Plugin/Microsoft.Crm.Common.ObjectModel.PhoneCallService\",\"@Microsoft.PowerApps.CDS.ErrorDetails.ApiStepKey\":\"3ccabb1b-ea3e-db11-86a7-000a3a5473e8\",\"@Microsoft.PowerApps.CDS.ErrorDetails.ApiDepthKey\":\"1\",\"@Microsoft.PowerApps.CDS.ErrorDetails.ApiActivityIdKey\":\"1736f387-e025-4828-a2bb-74ea8ac768a2\",\"@Microsoft.PowerApps.CDS.ErrorDetails.ApiPluginSolutionNameKey\":\"System\",\"@Microsoft.PowerApps.CDS.ErrorDetails.ApiStepSolutionNameKey\":\"System\",\"@Microsoft.PowerApps.CDS.ErrorDetails.ApiExceptionCategory\":\"ClientError\",\"@Microsoft.PowerApps.CDS.ErrorDetails.ApiExceptionMesageName\":\"InvalidArgument\",\"@Microsoft.PowerApps.CDS.ErrorDetails.ApiExceptionHttpStatusCode\":\"400\",\"@Microsoft.PowerApps.CDS.HelpLink\":\"http://go.microsoft.com/fwlink/?LinkID=398563&error=Microsoft.Crm.CrmException%3a80040203&client=platform\",\"@Microsoft.PowerApps.CDS.InnerError.Message\":\"Communication activity cannot have more than one Sender party\"}}"); logger.Log(operationException); Assert.NotNull(logger.LastError); diff --git a/src/Packages.props b/src/Packages.props index d405fd3..ce92923 100644 --- a/src/Packages.props +++ b/src/Packages.props @@ -7,14 +7,14 @@ 9.2.24073.11611-master 13.0.1 2.3.24 - 9.0.2.55 + 9.0.2.56 9.0.2.34 3.0.8 1.1.22 3.1.0 3.1.8 6.0.0 - 7.0.3 + 8.0.4 7.0.0 4.5.5 6.0.0 diff --git a/src/SDK-IntelliSense/V9/PublishedXML/CE/Microsoft.PowerPlatform.Dataverse.Client.xml b/src/SDK-IntelliSense/V9/PublishedXML/CE/Microsoft.PowerPlatform.Dataverse.Client.xml index ea57d51..15c7347 100644 --- a/src/SDK-IntelliSense/V9/PublishedXML/CE/Microsoft.PowerPlatform.Dataverse.Client.xml +++ b/src/SDK-IntelliSense/V9/PublishedXML/CE/Microsoft.PowerPlatform.Dataverse.Client.xml @@ -1443,7 +1443,7 @@ - Direct the system to convert any matching unmanaged customizations into your managed solution + Obsolete. The system will convert unmanaged solution components to managed when you import a managed solution. diff --git a/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.ReleaseNotes.txt b/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.ReleaseNotes.txt index d8a2833..76e9f44 100644 --- a/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.ReleaseNotes.txt +++ b/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.ReleaseNotes.txt @@ -7,6 +7,20 @@ Notice: Note: Only AD on FullFramework, OAuth, Certificate, ClientSecret Authentication types are supported at this time. ++CURRENTRELEASEID++ +***** POSSIBLE Breaking Changes ***** + Minor Release Bump, + Added .net 8.0 Target. + .net 6.0 Target will be removed in a subsequent release. + Removed dependance on Microsoft.Rest.Client. this was primary used for exception handling, and the necessary components have been reworked in to DVSC Exception management classes. + +Fix memory consumption when too many exception are throw by DV client. Git: https://github.com/microsoft/PowerPlatform-DataverseServiceClient/issues/474 +Dependency Changes: + Modified: + System.Text.Json to 8.0.4 + Removed: + Microsoft.Rest.Client - Necessary carried over in client. + +1.1.32: Fix for endless retry loop issue in WebAPI calls when specific error states are encountered. Fix for Logging MSAL telemetry when using ILogger Previously, Logs for MSAL were not written to the configured ILogger, they would only go to Trace Source and InMemory Logs. diff --git a/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.nuspec b/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.nuspec index 8adbec8..351a0ac 100644 --- a/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.nuspec +++ b/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.nuspec @@ -21,7 +21,6 @@ - @@ -35,7 +34,6 @@ - @@ -48,7 +46,6 @@ - @@ -75,12 +72,37 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + +