diff --git a/src/GeneralTools/DataverseClient/Client/Builder/AbstractClientRequestBuilder.cs b/src/GeneralTools/DataverseClient/Client/Builder/AbstractClientRequestBuilder.cs new file mode 100644 index 0000000..815bd44 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Builder/AbstractClientRequestBuilder.cs @@ -0,0 +1,482 @@ +// Ignore Spelling: Dataverse Crm + +using Microsoft.Xrm.Sdk.Messages; +using Microsoft.Xrm.Sdk.Query; +using Microsoft.Xrm.Sdk; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using static Microsoft.PowerPlatform.Dataverse.Client.Utilities; +using Microsoft.PowerPlatform.Dataverse.Client.Utils; + +namespace Microsoft.PowerPlatform.Dataverse.Client.Builder +{ + /// + /// Internal use only. Request Builder base class. + /// + /// + public abstract class AbstractClientRequestBuilder : IOrganizationServiceAsync2 + where T : AbstractClientRequestBuilder + { + private IOrganizationServiceAsync2 _client; + private Guid? _correlationId; // this is the correlation id of the request + private Guid? _requestId; // this is the request id of the request + private Dictionary _headers = new Dictionary(); + private Guid? _aadOidId; // this ObjectID to use for the requesting user. + private Guid? _crmUserId; // this is the CRM user id to use for the requesting user. + + /// + /// Internal use only, used to build a base class for request builders. + /// + /// + internal AbstractClientRequestBuilder(IOrganizationServiceAsync2 client) + { + _client = client; + } + + /// + /// Adds a request id of your choosing to this request. This is used for tracing purposes. + /// + /// + /// + public T WithRequestId(Guid requestId) + { + _requestId = requestId; + return (T)this; + } + + /// + /// Adds a correlation id of your choosing to this request. This is used for tracing purposes. + /// + /// + /// + public T WithCorrelationId(Guid correlationId) + { + _correlationId = correlationId; + return (T)this; + } + + /// + /// Adds an individual header to the request. This works in conjunction with the custom headers request behavior. + /// + /// Header Key + /// Header Value + /// + public T WithHeader(string key, string value) + { + _headers.Add(key, value); + return (T)this; + } + + /// + /// Adds an array of headers to the request. This works in conjunction with the custom headers request behavior. + /// + /// Dictionary of Headers to add to there request. + /// + public T WithHeaders(IDictionary headers) + { + foreach (var itm in headers) + _headers.Add(itm.Key, itm.Value); + return (T)this; + } + + /// + /// Adds the AAD object ID to the request + /// + /// + /// + public T WithUserObjectId(Guid userObjectId) + { + _aadOidId = userObjectId; + return (T)this; + } + + /// + /// Adds the CrmUserId to the request. + /// + /// + /// + public T WithCrmUserId(Guid crmUserId) + { + _crmUserId = crmUserId; + return (T)this; + } + + /// + /// This configured the request to send to Dataverse. + /// + /// + /// + /// + internal OrganizationRequest BuildRequest(OrganizationRequest request) + { + if (request == null) + { + throw new ArgumentNullException("Request is not set"); + } + + ParameterCollection parameters = new ParameterCollection(); + Guid requestTracker = _requestId ?? Guid.NewGuid(); + request.RequestId = requestTracker; + + + if (_correlationId != null) + { + parameters.Add(RequestHeaders.X_MS_CORRELATION_REQUEST_ID, _correlationId.Value); + } + + if (_headers.Any()) + { + parameters.Add(RequestBinderUtil.HEADERLIST, new Dictionary(_headers)); + } + + request.Parameters.AddRange(parameters); + + // Clear in case this is reused. + ClearRequest(); + + return request; + } + + /// + /// Clear request parameters when the request is executed. + /// + private void ClearRequest() + { + _requestId = null; + _correlationId = null; + _headers.Clear(); + } + + #region IOrganization Services Interface Implementations + /// + /// Associate an entity with a set of entities + /// + /// + /// + /// + /// + /// Propagates notification that operations should be canceled. + public Task AssociateAsync(string entityName, Guid entityId, Relationship relationship, EntityReferenceCollection relatedEntities, CancellationToken cancellationToken) + { + AssociateRequest request = new AssociateRequest() + { + Target = new EntityReference(entityName, entityId), + Relationship = relationship, + RelatedEntities = relatedEntities + }; + request = (AssociateRequest)BuildRequest(request); + return _client.ExecuteAsync(request, cancellationToken); + } + + /// + /// Create an entity and process any related entities + /// + /// entity to create + /// Propagates notification that operations should be canceled. + /// The ID of the created record + public Task CreateAsync(Entity entity, CancellationToken cancellationToken) + { + CreateRequest request = new CreateRequest() + { + Target = entity + }; + request = (CreateRequest)BuildRequest(request); + return _client.ExecuteAsync(request, cancellationToken).ContinueWith((t) => + { + return ((CreateResponse)t.Result).id; + }); + } + + /// + /// Create an entity and process any related entities + /// + /// entity to create + /// Propagates notification that operations should be canceled. + /// Returns the newly created record + public Task CreateAndReturnAsync(Entity entity, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + /// + /// Delete instance of an entity + /// + /// Logical name of entity + /// Id of entity + /// Propagates notification that operations should be canceled. + public Task DeleteAsync(string entityName, Guid id, CancellationToken cancellationToken) + { + DeleteRequest request = new DeleteRequest() + { + Target = new EntityReference(entityName, id) + }; + request = (DeleteRequest)BuildRequest(request); + return _client.ExecuteAsync(request, cancellationToken); + } + + /// + /// Disassociate an entity with a set of entities + /// + /// + /// + /// + /// + /// Propagates notification that operations should be canceled. + public Task DisassociateAsync(string entityName, Guid entityId, Relationship relationship, EntityReferenceCollection relatedEntities, CancellationToken cancellationToken) + { + DisassociateRequest request = new DisassociateRequest() + { + Target = new EntityReference(entityName, entityId), + Relationship = relationship, + RelatedEntities = relatedEntities + }; + request = (DisassociateRequest)BuildRequest(request); + return _client.ExecuteAsync(request, cancellationToken); + } + + /// + /// Perform an action in an organization specified by the request. + /// + /// Refer to SDK documentation for list of messages that can be used. + /// Propagates notification that operations should be canceled. + /// Results from processing the request + public Task ExecuteAsync(OrganizationRequest request, CancellationToken cancellationToken) + { + request = BuildRequest(request); + return _client.ExecuteAsync(request, cancellationToken); + } + + /// + /// Retrieves instance of an entity + /// + /// Logical name of entity + /// Id of entity + /// Column Set collection to return with the request + /// Propagates notification that operations should be canceled. + /// Selected Entity + + public Task RetrieveAsync(string entityName, Guid id, ColumnSet columnSet, CancellationToken cancellationToken) + { + RetrieveRequest request = new RetrieveRequest() + { + ColumnSet = columnSet, + Target = new EntityReference(entityName, id) + }; + request = (RetrieveRequest)BuildRequest(request); + return _client.ExecuteAsync(request, cancellationToken).ContinueWith((t) => + { + return ((RetrieveResponse)t.Result).Entity; + }); + } + + /// + /// Retrieves a collection of entities + /// + /// + /// Propagates notification that operations should be canceled. + /// Returns an EntityCollection Object containing the results of the query + public Task RetrieveMultipleAsync(QueryBase query, CancellationToken cancellationToken) + { + RetrieveMultipleRequest request = new RetrieveMultipleRequest() + { + Query = query + }; + request = (RetrieveMultipleRequest)BuildRequest(request); + return _client.ExecuteAsync(request, cancellationToken).ContinueWith((t) => + { + return ((RetrieveMultipleResponse)t.Result).EntityCollection; + }); + + } + + /// + /// Updates an entity and process any related entities + /// + /// entity to update + /// Propagates notification that operations should be canceled. + public Task UpdateAsync(Entity entity, CancellationToken cancellationToken) + { + UpdateRequest request = new UpdateRequest() + { + Target = entity + }; + request = (UpdateRequest)BuildRequest(request); + return _client.ExecuteAsync(request, cancellationToken); + } + + /// + /// Create an entity and process any related entities + /// + /// entity to create + /// Returns the newly created record + public Task CreateAsync(Entity entity) + { + return CreateAsync(entity, CancellationToken.None); + } + + /// + /// Retrieves instance of an entity + /// + /// Logical name of entity + /// Id of entity + /// Column Set collection to return with the request + /// Selected Entity + public Task RetrieveAsync(string entityName, Guid id, ColumnSet columnSet) + { + return RetrieveAsync(entityName, id, columnSet, CancellationToken.None); + } + + /// + /// Updates an entity and process any related entities + /// + /// entity to update + public Task UpdateAsync(Entity entity) + { + return UpdateAsync(entity, CancellationToken.None); + } + + /// + /// Delete instance of an entity + /// + /// Logical name of entity + /// Id of entity + public Task DeleteAsync(string entityName, Guid id) + { + return DeleteAsync(entityName, id, CancellationToken.None); + } + + /// + /// Perform an action in an organization specified by the request. + /// + /// Refer to SDK documentation for list of messages that can be used. + /// Results from processing the request + public Task ExecuteAsync(OrganizationRequest request) + { + return ExecuteAsync(request, CancellationToken.None); + } + + /// + /// Associate an entity with a set of entities + /// + /// + /// + /// + /// + public Task AssociateAsync(string entityName, Guid entityId, Relationship relationship, EntityReferenceCollection relatedEntities) + { + return AssociateAsync(entityName, entityId, relationship, relatedEntities, CancellationToken.None); + } + + /// + /// Disassociate an entity with a set of entities + /// + /// + /// + /// + /// + public Task DisassociateAsync(string entityName, Guid entityId, Relationship relationship, EntityReferenceCollection relatedEntities) + { + return DisassociateAsync(entityName, entityId, relationship, relatedEntities, CancellationToken.None); + } + + /// + /// Retrieves a collection of entities + /// + /// + /// Returns an EntityCollection Object containing the results of the query + public Task RetrieveMultipleAsync(QueryBase query) + { + return RetrieveMultipleAsync(query, CancellationToken.None); + } + + /// + /// Create an entity and process any related entities + /// + /// entity to create + /// The ID of the created record + public Guid Create(Entity entity) + { + return CreateAsync(entity).Result; + } + + /// + /// Retrieves instance of an entity + /// + /// Logical name of entity + /// Id of entity + /// Column Set collection to return with the request + /// Selected Entity + public Entity Retrieve(string entityName, Guid id, ColumnSet columnSet) + { + return RetrieveAsync(entityName, id, columnSet).Result; + } + + /// + /// Updates an entity and process any related entities + /// + /// entity to update + public void Update(Entity entity) + { + UpdateAsync(entity).Wait(); + } + + /// + /// Delete instance of an entity + /// + /// Logical name of entity + /// Id of entity + public void Delete(string entityName, Guid id) + { + DeleteAsync(entityName, id).Wait(); + } + + /// + /// Perform an action in an organization specified by the request. + /// + /// Refer to SDK documentation for list of messages that can be used. + /// Results from processing the request + public OrganizationResponse Execute(OrganizationRequest request) + { + return ExecuteAsync(request).Result; + } + + /// + /// Associate an entity with a set of entities + /// + /// + /// + /// + /// + public void Associate(string entityName, Guid entityId, Relationship relationship, EntityReferenceCollection relatedEntities) + { + AssociateAsync(entityName, entityId, relationship, relatedEntities).Wait(); + } + + /// + /// Disassociate an entity with a set of entities + /// + /// + /// + /// + /// + public void Disassociate(string entityName, Guid entityId, Relationship relationship, EntityReferenceCollection relatedEntities) + { + DisassociateAsync(entityName, entityId, relationship, relatedEntities).Wait(); + } + + /// + /// Retrieves a collection of entities + /// + /// + /// Returns an EntityCollection Object containing the results of the query + public EntityCollection RetrieveMultiple(QueryBase query) + { + return RetrieveMultipleAsync(query).Result; + } + #endregion + } +} diff --git a/src/GeneralTools/DataverseClient/Client/Builder/ServiceClientRequestBuilder.cs b/src/GeneralTools/DataverseClient/Client/Builder/ServiceClientRequestBuilder.cs new file mode 100644 index 0000000..682ce45 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Builder/ServiceClientRequestBuilder.cs @@ -0,0 +1,20 @@ +// Ignore Spelling: Dataverse + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PowerPlatform.Dataverse.Client.Builder +{ + /// + /// Request builder class for submitting requests to Dataverse. + /// + public class ServiceClientRequestBuilder : AbstractClientRequestBuilder + { + internal ServiceClientRequestBuilder(IOrganizationServiceAsync2 client) + : base(client) + { } + } +} diff --git a/src/GeneralTools/DataverseClient/Client/ConnectionService.cs b/src/GeneralTools/DataverseClient/Client/ConnectionService.cs index 6dba71a..1b91da0 100644 --- a/src/GeneralTools/DataverseClient/Client/ConnectionService.cs +++ b/src/GeneralTools/DataverseClient/Client/ConnectionService.cs @@ -380,7 +380,7 @@ internal IEnumerable> GetAllLogs() /// /// if set to true, the log provider is set locally /// - public bool isLogEntryCreatedLocaly { get; set; } + public bool isLogEntryCreatedLocally { get; set; } /// /// Get and Set of network credentials... @@ -687,7 +687,7 @@ internal ConnectionService(IOrganizationService testIOrganziationSvc, string bas _testSupportIOrg = testIOrganziationSvc; WebApiHttpClient = mockClient; logEntry = new DataverseTraceLogger(logger); - isLogEntryCreatedLocaly = true; + isLogEntryCreatedLocally = true; _OrgDetail = new OrganizationDetail(); _OrgDetail.Endpoints.Add(EndpointType.OrganizationDataService, baseConnectUrl); @@ -708,12 +708,12 @@ internal ConnectionService(OrganizationWebProxyClientAsync externalOrgWebProxyCl if (logSink == null) { logEntry = new DataverseTraceLogger(); - isLogEntryCreatedLocaly = true; + isLogEntryCreatedLocally = true; } else { logEntry = logSink; - isLogEntryCreatedLocaly = false; + isLogEntryCreatedLocally = false; } // is this a clone request @@ -765,12 +765,12 @@ internal ConnectionService( if (logSink == null) { logEntry = new DataverseTraceLogger(); - isLogEntryCreatedLocaly = true; + isLogEntryCreatedLocally = true; } else { logEntry = logSink; - isLogEntryCreatedLocaly = false; + isLogEntryCreatedLocally = false; } // is this a clone request @@ -838,12 +838,12 @@ internal ConnectionService( if (logSink == null) { logEntry = new DataverseTraceLogger(); - isLogEntryCreatedLocaly = true; + isLogEntryCreatedLocally = true; } else { logEntry = logSink; - isLogEntryCreatedLocaly = false; + isLogEntryCreatedLocally = false; } // is this a clone request @@ -908,12 +908,12 @@ internal ConnectionService( if (logSink == null) { logEntry = new DataverseTraceLogger(); - isLogEntryCreatedLocaly = true; + isLogEntryCreatedLocally = true; } else { logEntry = logSink; - isLogEntryCreatedLocaly = false; + isLogEntryCreatedLocally = false; } // is this a clone request @@ -2086,6 +2086,8 @@ internal async Task Command_WebAPIProcess_ExecuteAsync(Org userProvidedRequestId = req.RequestId.Value; } + RequestBinderUtil.GetAdditionalHeaders(headers, req); + // Execute request var sResp = await Command_WebExecuteAsync(postUri, bodyOfRequest, methodToExecute, headers, "application/json", logMessageTag, callerId, disableConnectionLocking, maxRetryCount, retryPauseTime, uriOfInstance, cancellationToken: cancellationToken, requestTrackingId: userProvidedRequestId).ConfigureAwait(false); if (sResp != null && sResp.IsSuccessStatusCode) @@ -3008,6 +3010,11 @@ private static async Task QueryGlobalDiscoveryAsyn proInfo.SetValue(d, ep, null); } + if (!Utilities.IsValidOrganizationUrl(d)) + { + logSink.Log(string.Format(CultureInfo.InvariantCulture, "QueryGlobalDiscovery - Invalid Url returned from Discovery ({0})", d.Endpoints[EndpointType.OrganizationService])); + continue; + } orgList.Add(d); } dtStartQuery.Stop(); @@ -3762,7 +3769,7 @@ void Dispose(bool disposing) if (disposing) { disposedValue = true; - if (isLogEntryCreatedLocaly) + if (isLogEntryCreatedLocally) { logEntry?.Dispose(); } diff --git a/src/GeneralTools/DataverseClient/Client/Connector/OrganizationWebProxyClientAsync.cs b/src/GeneralTools/DataverseClient/Client/Connector/OrganizationWebProxyClientAsync.cs index 19b1498..ce9fccf 100644 --- a/src/GeneralTools/DataverseClient/Client/Connector/OrganizationWebProxyClientAsync.cs +++ b/src/GeneralTools/DataverseClient/Client/Connector/OrganizationWebProxyClientAsync.cs @@ -5,11 +5,14 @@ namespace Microsoft.PowerPlatform.Dataverse.Client.Connector using System.Net; using System.Reflection; using System.ServiceModel; + using System.ServiceModel.Channels; using System.ServiceModel.Description; using System.Threading.Tasks; using Microsoft.PowerPlatform.Dataverse.Client; + using Microsoft.PowerPlatform.Dataverse.Client.Utils; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Client; + using Microsoft.Xrm.Sdk.Messages; using Microsoft.Xrm.Sdk.Query; #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member @@ -189,12 +192,20 @@ protected internal virtual Task DeleteAsyncCore(string entityName, Guid id) protected internal virtual OrganizationResponse ExecuteCore(OrganizationRequest request) { - return ExecuteAction(() => Channel.Execute(request)); + return ExecuteAction(() => + { + ProcessRequestBinderProperties(request); + return Channel.Execute(request); + }); } protected internal virtual Task ExecuteAsyncCore(OrganizationRequest request) { - return ExecuteOperation(() => Channel.ExecuteAsync(request)); + return ExecuteOperation(() => + { + ProcessRequestBinderProperties(request); + return Channel.ExecuteAsync(request); + }); } protected internal virtual void AssociateCore(string entityName, Guid entityId, Relationship relationship, @@ -241,6 +252,15 @@ protected override WebProxyClientContextAsyncInitializer(Func action) } #if NETCOREAPP - protected async internal Task ExecuteOperation(Func> asyncAction) + protected internal Task ExecuteOperation(Func> asyncAction) { if (asyncAction == null) { @@ -92,7 +92,7 @@ protected async internal Task ExecuteOperation(Func> asyncAction) using (CreateNewInitializer()) { - return await asyncAction().ConfigureAwait(continueOnCapturedContext: true); + return asyncAction(); } } #else diff --git a/src/GeneralTools/DataverseClient/Client/InternalExtensions/RequestResponseExtenstions.cs b/src/GeneralTools/DataverseClient/Client/InternalExtensions/RequestResponseExtenstions.cs index 20c2e9d..9d067eb 100644 --- a/src/GeneralTools/DataverseClient/Client/InternalExtensions/RequestResponseExtenstions.cs +++ b/src/GeneralTools/DataverseClient/Client/InternalExtensions/RequestResponseExtenstions.cs @@ -11,7 +11,7 @@ namespace Microsoft.PowerPlatform.Dataverse.Client.InternalExtensions { /// - /// Organization request/response extenstions + /// Organization request/response extensions /// internal static class RequestResponseExtenstions { diff --git a/src/GeneralTools/DataverseClient/Client/Microsoft.PowerPlatform.Dataverse.Client.csproj b/src/GeneralTools/DataverseClient/Client/Microsoft.PowerPlatform.Dataverse.Client.csproj index c27794c..9203b93 100644 --- a/src/GeneralTools/DataverseClient/Client/Microsoft.PowerPlatform.Dataverse.Client.csproj +++ b/src/GeneralTools/DataverseClient/Client/Microsoft.PowerPlatform.Dataverse.Client.csproj @@ -35,15 +35,14 @@ - - + + - + - + diff --git a/src/GeneralTools/DataverseClient/Client/ServiceClient.cs b/src/GeneralTools/DataverseClient/Client/ServiceClient.cs index e33b48c..d2bd013 100644 --- a/src/GeneralTools/DataverseClient/Client/ServiceClient.cs +++ b/src/GeneralTools/DataverseClient/Client/ServiceClient.cs @@ -1,23 +1,19 @@ +// Ignore Spelling: Dataverse + #region using using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.IO; using System.Linq; using System.Security; using System.ServiceModel; using System.ServiceModel.Description; -using System.Xml; using Microsoft.Crm.Sdk.Messages; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Discovery; using Microsoft.Xrm.Sdk.Messages; -using Microsoft.Xrm.Sdk.Metadata; using Microsoft.Xrm.Sdk.Query; -using Microsoft.Xrm.Sdk.WebServiceClient; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using System.Net.Http; @@ -28,10 +24,9 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.PowerPlatform.Dataverse.Client.Model; -using System.Reflection; -using Microsoft.Extensions.Caching.Memory; using Microsoft.PowerPlatform.Dataverse.Client.Connector; using Microsoft.PowerPlatform.Dataverse.Client.Connector.OnPremises; +using Microsoft.PowerPlatform.Dataverse.Client.Builder; #endregion namespace Microsoft.PowerPlatform.Dataverse.Client @@ -1444,6 +1439,15 @@ public ServiceClient Clone(System.Reflection.Assembly strongTypeAsm, ILogger log } } + /// + /// Creates a ServiceClient Request builder that allows you to customize a specific request sent to dataverse. This should be used only for a single request and then released. + /// + /// Service Request builder that is used to create and submit a single request. + public ServiceClientRequestBuilder CreateRequestBuilder() + { + return new ServiceClientRequestBuilder(this); + } + #region Dataverse DiscoveryServerMethods /// diff --git a/src/GeneralTools/DataverseClient/Client/Utils/RequestBinderUtil.cs b/src/GeneralTools/DataverseClient/Client/Utils/RequestBinderUtil.cs new file mode 100644 index 0000000..1f04730 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Utils/RequestBinderUtil.cs @@ -0,0 +1,128 @@ +// Ignore Spelling: Dataverse Utils + +using Microsoft.Xrm.Sdk; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.ServiceModel.Channels; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PowerPlatform.Dataverse.Client.Utils +{ + internal static class RequestBinderUtil + { + internal static readonly string HEADERLIST = "HEADERLIST"; + /// + /// Populates the request builder Info into message headers. + /// + /// + /// + internal static void ProcessRequestBinderProperties(HttpRequestMessageProperty httpRequestMessageHeaders, OrganizationRequest request) + { + foreach (var itm in request.Parameters) + { + if (itm.Key == Utilities.RequestHeaders.X_MS_CORRELATION_REQUEST_ID) + { + AddorUpdateHeaderProperties(httpRequestMessageHeaders, Utilities.RequestHeaders.X_MS_CORRELATION_REQUEST_ID, itm.Value.ToString()); + continue; + } + if (itm.Key == Utilities.RequestHeaders.X_MS_CLIENT_SESSION_ID) + { + AddorUpdateHeaderProperties(httpRequestMessageHeaders, Utilities.RequestHeaders.X_MS_CLIENT_SESSION_ID, itm.Value.ToString()); + continue; + } + if (itm.Key == HEADERLIST) + { + if (itm.Value is Dictionary hrdList) + { + foreach (var hdr in hrdList) + { + AddorUpdateHeaderProperties(httpRequestMessageHeaders, hdr.Key, hdr.Value); + } + } + continue; + } + } + if ( request.Parameters.Count > 0 ) + { + request.Parameters.Remove(Utilities.RequestHeaders.X_MS_CORRELATION_REQUEST_ID); + request.Parameters.Remove(Utilities.RequestHeaders.X_MS_CLIENT_SESSION_ID); + request.Parameters.Remove(HEADERLIST); + } + } + + /// + /// + /// + /// + /// + internal static void GetAdditionalHeaders(Dictionary> customHeaders, OrganizationRequest request) + { + foreach (var itm in request.Parameters) + { + if (itm.Key == Utilities.RequestHeaders.X_MS_CORRELATION_REQUEST_ID) + { + AddorUpdateHeaderProperties(customHeaders, Utilities.RequestHeaders.X_MS_CORRELATION_REQUEST_ID, itm.Value.ToString()); + continue; + } + if (itm.Key == Utilities.RequestHeaders.X_MS_CLIENT_SESSION_ID) + { + AddorUpdateHeaderProperties(customHeaders, Utilities.RequestHeaders.X_MS_CLIENT_SESSION_ID, itm.Value.ToString()); + continue; + } + if (itm.Key == HEADERLIST) + { + if (itm.Value is Dictionary hrdList) + { + foreach (var hdr in hrdList) + { + AddorUpdateHeaderProperties(customHeaders, hdr.Key, hdr.Value); + } + } + continue; + } + } + if (request.Parameters.Count > 0) + { + request.Parameters.Remove(Utilities.RequestHeaders.X_MS_CORRELATION_REQUEST_ID); + request.Parameters.Remove(Utilities.RequestHeaders.X_MS_CLIENT_SESSION_ID); + request.Parameters.Remove(HEADERLIST); + } + } + + /// + /// Handle adding headers from request builder. + /// + /// + /// + /// + private static void AddorUpdateHeaderProperties(Dictionary> customHeaders, string hdrKey, string hrdValue) + { + if (customHeaders.Keys.Contains(hdrKey)) + { + if (customHeaders[hdrKey] == null) + customHeaders[hdrKey] = new List(); + + customHeaders[hdrKey].Add(hrdValue); + } + else + { + customHeaders.Add(hdrKey, new List() { hrdValue }); + } + } + + private static void AddorUpdateHeaderProperties(HttpRequestMessageProperty httpRequestMessageHeaders, string key, string value) + { + if (httpRequestMessageHeaders.Headers.AllKeys.Contains(key)) + { + httpRequestMessageHeaders.Headers.Add(key, value); + } + else + { + httpRequestMessageHeaders.Headers[key] = value; + } + } + } +} diff --git a/src/GeneralTools/DataverseClient/Client/Utils/Utils.cs b/src/GeneralTools/DataverseClient/Client/Utils/Utils.cs index a129de9..2ec7fb7 100644 --- a/src/GeneralTools/DataverseClient/Client/Utils/Utils.cs +++ b/src/GeneralTools/DataverseClient/Client/Utils/Utils.cs @@ -20,6 +20,7 @@ using System.Net.Http; using System.Reflection; using System.Text; +using System.Threading.Tasks; #endregion namespace Microsoft.PowerPlatform.Dataverse.Client @@ -226,7 +227,28 @@ public static DiscoveryServer DeterminDiscoveryDataFromOrgDetail(Uri serviceUri, return null; } return null; + } + public static bool IsValidOrganizationUrl(OrganizationDetail orgInfo) + { + if (orgInfo != null) + { + if (orgInfo.Endpoints != null) + { + foreach (var ep in orgInfo.Endpoints) + { + if (Uri.IsWellFormedUriString(ep.Value, UriKind.Absolute)) + { + return true; + } + else + { + return false; + } + } + } + } + return false; } /// @@ -431,7 +453,7 @@ internal static void RetryRequest(OrganizationRequest req, Guid requestTrackingI retryCount++; logEntry.LogFailure(req, requestTrackingId, sessionTrackingId, disableConnectionLocking, LockWait, logDt, ex, errorStringCheck, webUriMessageReq: webUriReq); logEntry.LogRetry(retryCount, req, retryPauseTimeRunning, isThrottled: isThrottled); - System.Threading.Thread.Sleep(retryPauseTimeRunning); + Task.Delay(retryPauseTimeRunning); } /// @@ -907,6 +929,10 @@ internal static class RequestHeaders /// public const string X_MS_CLIENT_REQUEST_ID = "x-ms-client-request-id"; /// + /// PreRequest CorrlationId to trace request into Dataverse + /// + public const string X_MS_CORRELATION_REQUEST_ID = "x-ms-correlation-request-id"; + /// /// Content type of WebAPI request. /// public const string CONTENT_TYPE = "Content-Type"; diff --git a/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/DataverseClient_Core_UnitTests.csproj b/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/DataverseClient_Core_UnitTests.csproj index 95f5f47..1df524e 100644 --- a/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/DataverseClient_Core_UnitTests.csproj +++ b/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/DataverseClient_Core_UnitTests.csproj @@ -12,9 +12,9 @@ - - - + + + diff --git a/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/ServiceClientTests.cs b/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/ServiceClientTests.cs index e5c9d27..d0517a4 100644 --- a/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/ServiceClientTests.cs +++ b/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/ServiceClientTests.cs @@ -823,6 +823,7 @@ public void ConnectUsingServiceIdentity_ClientSecret_CtorV1() // Validate connection ValidateConnection(client); + } [SkippableConnectionTest] @@ -1698,6 +1699,50 @@ public void StageSolution_File_DeleteAndPromote_TestAsync() } } + [SkippableConnectionTest] + [Trait("Category", "Live Connect Required")] + public async void RequestBuilder_Execute() + { + // System.Net.ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Tls12; + + var Conn_AppID = System.Environment.GetEnvironmentVariable("XUNITCONNTESTAPPID"); + var Conn_Secret = System.Environment.GetEnvironmentVariable("XUNITCONNTESTSECRET"); + var Conn_Url = System.Environment.GetEnvironmentVariable("XUNITCONNTESTURI"); + + // Connection params. + var client = new ServiceClient(new Uri(Conn_Url), Conn_AppID, Conn_Secret, true, Ilogger); + Assert.True(client.IsReady, "Failed to Create Connection via Constructor"); + + Entity a = new Entity("account"); + a["name"] = "Test Account"; + a.Id = Guid.NewGuid(); + + var trackingId = a.Id; + + var rslt = await client.CreateRequestBuilder().WithCorrelationId(Guid.NewGuid()).WithHeader("User-Agent", "TEST").WithHeader("Foo", "TEST1").CreateAsync(a).ConfigureAwait(false); + Assert.IsType(rslt); + + a["name"] = "Test Account - step 2"; + await client.CreateRequestBuilder().WithCorrelationId(Guid.NewGuid()).WithHeader("User-Agent", "TEST").WithHeader("Foo", "TEST1").UpdateAsync(a).ConfigureAwait(false); + + a["name"] = "Test Account - step 3"; + UpsertRequest upsert = new UpsertRequest(); + upsert.Target = a; + var upResp = (UpsertResponse) await client.CreateRequestBuilder().WithCorrelationId(Guid.NewGuid()).WithHeader("User-Agent", "TEST").WithHeader("Foo", "TEST1").ExecuteAsync(upsert).ConfigureAwait(false); + Assert.IsType(upResp); + Assert.False(upResp.RecordCreated); + + // retrieve + var ret = (Entity)await client.CreateRequestBuilder().WithCorrelationId(Guid.NewGuid()).WithHeader("User-Agent", "TEST").WithHeader("Foo", "TEST1").RetrieveAsync("account", trackingId, new ColumnSet(true)).ConfigureAwait(false); + ret.Should().NotBeNull(); + ret.Id.Should().Be(trackingId); + ret["name"].Should().Be("Test Account - step 3"); + + // delete + await client.CreateRequestBuilder().WithCorrelationId(Guid.NewGuid()).WithHeader("User-Agent", "TEST").WithHeader("Foo", "TEST1").DeleteAsync("account", trackingId).ConfigureAwait(false); + + } + // Not yet implemented //[SkippableConnectionTest] //[Trait("Category", "Live Connect Required")] diff --git a/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/LivePackageTestsConsole.csproj b/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/LivePackageTestsConsole.csproj index dbf5dc7..6a0e4de 100644 --- a/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/LivePackageTestsConsole.csproj +++ b/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/LivePackageTestsConsole.csproj @@ -30,7 +30,7 @@ - + diff --git a/src/GeneralTools/DataverseClient/UnitTests/LiveTestsConsole/LiveTestsConsole.csproj b/src/GeneralTools/DataverseClient/UnitTests/LiveTestsConsole/LiveTestsConsole.csproj index 31e247d..d7c7b2b 100644 --- a/src/GeneralTools/DataverseClient/UnitTests/LiveTestsConsole/LiveTestsConsole.csproj +++ b/src/GeneralTools/DataverseClient/UnitTests/LiveTestsConsole/LiveTestsConsole.csproj @@ -1,8 +1,9 @@ + true Exe - net462;netcoreapp3.1 + net462;net6.0 true DataverseClient-Tests false 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 723162c..bc6845b 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 @@ -1472,10 +1472,7 @@ Defaults to True. When true, this setting applies the default connection routing strategy to connections to Dataverse.This will 'prefer' a given node when interacting with Dataverse which improves overall connection performance.When set to false, each call to Dataverse will be routed to any given node supporting your organization.See https://docs.microsoft.com/en-us/powerapps/developer/data-platform/api-limits#remove-the-affinity-cookie for proper use. - - - MaxBufferPoolSize override. - Use under Microsoft Direction only. - + MaxFaultSize override. - Use under Microsoft Direction only. diff --git a/src/nuspecs/Microsoft.Dynamics.Sdk.Messages.nuspec b/src/nuspecs/Microsoft.Dynamics.Sdk.Messages.nuspec index 0f7ebf3..f90f158 100644 --- a/src/nuspecs/Microsoft.Dynamics.Sdk.Messages.nuspec +++ b/src/nuspecs/Microsoft.Dynamics.Sdk.Messages.nuspec @@ -15,19 +15,19 @@ Dynamics CommonDataService CDS PowerApps PowerPlatform - + - + - + - + - + @@ -38,9 +38,6 @@ - - - diff --git a/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.Dynamics.nuspec b/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.Dynamics.nuspec index 7ff9d3b..fecdfab 100644 --- a/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.Dynamics.nuspec +++ b/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.Dynamics.nuspec @@ -15,16 +15,16 @@ Dynamics CommonDataService CDS PowerApps PowerPlatform ServiceClient Dataverse - + - + - + - + @@ -35,9 +35,6 @@ - - - diff --git a/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.ReleaseNotes.txt b/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.ReleaseNotes.txt index 450d4bd..26058d0 100644 --- a/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.ReleaseNotes.txt +++ b/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.ReleaseNotes.txt @@ -7,6 +7,17 @@ Notice: Note: Only AD on FullFramework, OAuth, Certificate, ClientSecret Authentication types are supported at this time. ++CURRENTRELEASEID++ +Updated Core SDK to 9.2.24044.9795 +Added new ServiceClient method for creating requests called "RequestBuilder" - will allow you to create a request and customized header, user, and other properties as part of there request generation. +Dependency Changes: + Removed + System.Security.Cryptography.Algorithms + System.Security.Cryptography.ProtectedData + System.Drawing.Common + Modified + System.Configuration.ConfigurationManager to 6.0.0 + +1.1.17: Fix for Request ID not reflecting correctly for some requests. Fix for RequestAdditionalHeadersAsync interface not being forwarded to Cloned copies of DVSC. GitHub Reported - Fix #419 Fix for Clone being called concurrently causing unnecessary calls to dataverse. GitHub reported - Fix #422 diff --git a/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.nuspec b/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.nuspec index 99b2ca8..81de30c 100644 --- a/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.nuspec +++ b/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.nuspec @@ -15,37 +15,37 @@ Dynamics CommonDataService CDS PowerApps PowerPlatform ServiceClient Dataverse - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + @@ -58,23 +58,20 @@ - + - - - - - - - - + + + + + + + + - - + + - - - @@ -86,9 +83,6 @@ - - -