diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..f9190c9 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,26 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/bin/Debug/netcoreapp3.1/LivePackageTestsConsole.dll", + "args": [], + "cwd": "${workspaceFolder}/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..d90b48e --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/LivePackageTestsConsole.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/LivePackageTestsConsole.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "${workspaceFolder}/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/LivePackageTestsConsole.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..ea54665 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,39 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + false + + + Debug + + + + $(MSBuildThisFileDirectory) + $(RepoRoot)src\ + + + + true + + + + + $(RepoRoot)nuget.dev.config + + + $(RepoRoot)obj\.nuget-cache + + + + + + + + + + diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 0000000..b180b2e --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,11 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + + + + + $(DefineConstants);PROD_CUSTOMER_TELEMETRY + + diff --git a/README.md b/README.md index 32f3b1f..cc15f83 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Current release notes and change log: [Microsoft.PowerPlatform.Dataverse.Client.Dynamics](src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.Dynamics.ReleaseNotes.txt) -[Microsoft.Dynamics.Sdk.Messages](src/nuspecs/Microsoft.Dynamics.Sdk.Messages.ReleaseNotes.txt) +This nuget package has been deprecated (for now) ~~[Microsoft.Dynamics.Sdk.Messages](src/nuspecs/Microsoft.Dynamics.Sdk.Messages.ReleaseNotes.txt)~~ ## Overview This repository contains the code for the Microsoft.PowerPlatform.Dataverse.Client and its supporting assemblies and classes. @@ -27,34 +27,33 @@ This encompasses the contents of the following nuget packages: [Microsoft.PowerPlatform.Dataverse.Client.Dynamics](https://www.nuget.org/packages/Microsoft.PowerPlatform.Dataverse.Client.Dynamics) -[Microsoft.Dynamics.Sdk.Messages](https://www.nuget.org/packages/Microsoft.Dynamics.Sdk.Messages) +This nuget package has been deprecated (for now) ~~[Microsoft.Dynamics.Sdk.Messages](https://www.nuget.org/packages/Microsoft.Dynamics.Sdk.Messages)~~ This library is and its supporting assemblies are a revision and update of the Microsoft.Xrm.Tooling.Connector.CrmServiceClient and the underlying Microsoft.Xrm.Sdk.Client libraries. We are using this effort to for a few key things we have wanted to get done for a number of years, -1. Refactor and update our client libraries to allow us to spit up Powerplatform Common Data Service SDK support from Microsoft Dynamics 365. +1. Refactor and update our client libraries to allow us to spit up PowerPlatform Common Data Service SDK support from Microsoft Dynamics 365. 2. Provide multi targeted library build that targets our supported .net client platforms. 3. Update connection patterns and behaviors to be consistent with many of the broadly accepted patterns. -4. Create a pattern to allow developers focus on the use of Common Data Service, or CDS + Dynamics as they need. +4. Create a pattern to allow developers focus on the use of Dataverse, or Dataverse + Dynamics as they need. We encourage you to read the release notes we provide with each nuget packages. As with most of our Nuget packages that are intended as tools or for developer consumption, we extensively comment in release notes. -At this time: (08/16/2021) +At this time: (03/06/2022) The Client SDK libs supports the following and has the following notices: +* 0.6.x is **expected** to be the final preview release, followed by 1.0 +* 0.6.x refactors much of the primary ServiceClient interface, narrowing its focus to primary operations against Dataverse. the ballance of the feature set has been moved to Microsoft.PowerPlatform.Dataverse.Client.Extensions. - We are seeking feedback on this refactor. * .net full framework 4.6.2, 4.7.2, 4.8 and .net core 3.0, 3.1, 5.0, 6.0 -* We now support all authentication types from CrmServiceClient, ( Client\Secret, Client\Cert, UID\PW Noninteractive, UID\PW interactive.) +* We now support all authentication types from CrmServiceClient for .net framework, ( Client\Secret, Client\Cert, UID\PW Noninteractive, UID\PW interactive.) +* We support the following authentication types from CrmServiceClient for .net core: Client\Secret, Client\Cert, UID\PW interactive. * MSAL Port has been completed, this Lib is now using MSAL 4.35+ -* The Message types that are part of the client have been reduced to Dataverse Core server messages only. Things like “QualifyLeadRequest” have been removed to their own Nuget package ( Microsoft.Dynamics.Sdk.Messages ) -* We will likely ship more extension packages that will contain the “CRM” messages, though over time, we will likely split the namespaces of those messages up based on service line, think Field Service or Sales or Customer Service, etc.. * Plugin Development using this Client is NOT supported at this time. From a scenario point of view, we are particularity interested in any issues or challenges when using these library in either Asp.net Core, Azure Functions, and Linux based scenarios. -We believe for the vast majority of applications working against the older dynamices sdk libs, you should only need Microsoft.PowerPlatform.Dataverse.Client and Microsoft.Dynamics.Sdk.Messages. - If your working against Dataverse Only and or custom entities and sdk messages, you should only need Microsoft.PowerPlatform.Dataverse.Client. If you do not experience that, or find missing messages in the Dataverse only scenarios, please let us know in the issues area. Note: We are currently providing support for these nuget packages primarily via GitHub and Microsoft Support. diff --git a/src/Build.Common.core.props b/src/Build.Common.core.props index 578c4c6..5648a45 100644 --- a/src/Build.Common.core.props +++ b/src/Build.Common.core.props @@ -5,7 +5,7 @@ - net462;net472;net48;netcoreapp3.0;netcoreapp3.1;netstandard2.0 + net462;net472;net48;netcoreapp3.1;netstandard2.0 false diff --git a/src/Build.Common.props b/src/Build.Common.props index 4676e46..a0e4c14 100644 --- a/src/Build.Common.props +++ b/src/Build.Common.props @@ -7,5 +7,7 @@ + diff --git a/src/Build.Shared.props b/src/Build.Shared.props index 080f0b0..2257292 100644 --- a/src/Build.Shared.props +++ b/src/Build.Shared.props @@ -5,7 +5,8 @@ 2.9.1 3.19.8 4.35.1 - 4.6.6061-weekly-2108.5 + 4.7.7698-v9.0-master.release + 4.7.7698-v9.0-master.release 4.6.6061-weekly-2108.5 4.7.6346-master 11.0.2 @@ -35,6 +36,22 @@ false + + + true + false + true + + + FORGOT-To-Set-ComponentAreaName @@ -43,7 +60,7 @@ Debug - $([System.IO.Path]::GetFullPath($([System.IO.Path]::Combine($(MSBuildThisFileDirectory), "..")))) + $([System.IO.Path]::GetFullPath($([System.IO.Path]::Combine($(MSBuildThisFileDirectory), "..")))) $(RepoRoot)\bin\$(Configuration)\$(ComponentAreaName)\$(TargetFramework) $(OutputRootDir.TrimEnd({'\\'})) @@ -63,6 +80,7 @@ AnyCPU 512 true + $(NoWarn);CS8032 true prompt 4 @@ -102,8 +120,4 @@ DEBUG;TRACE;CRMINTERNAL false - - - $(DefineConstants);PROD_CUSTOMER_TELEMETRY - diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 0000000..d370899 --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,16 @@ + + + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + + + + + + <_parentAreaFolderName>$([System.IO.Path]::GetFileName( $([System.IO.Path]::GetFullPath( $([System.IO.Path]::Combine($(MSBuildProjectDirectory), "..")) )) )) + + $(RepoRoot)\obj\$(_parentAreaFolderName)\$(MSBuildProjectName)\ + + diff --git a/src/GeneralTools/DataverseClient/Client/Auth/AuthProcessor.cs b/src/GeneralTools/DataverseClient/Client/Auth/AuthProcessor.cs index ea18821..b5b579c 100644 --- a/src/GeneralTools/DataverseClient/Client/Auth/AuthProcessor.cs +++ b/src/GeneralTools/DataverseClient/Client/Auth/AuthProcessor.cs @@ -1,8 +1,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Identity.Client; using Microsoft.PowerPlatform.Dataverse.Client.Auth.TokenCache; +using Microsoft.PowerPlatform.Dataverse.Client.InternalExtensions; using Microsoft.PowerPlatform.Dataverse.Client.Utils; -using Microsoft.Xrm.Sdk.WebServiceClient; using System; using System.Collections.Generic; using System.Diagnostics; @@ -452,12 +452,13 @@ internal static UriBuilder GetUriBuilderWithVersion(Uri discoveryServiceUri) /// URI to query /// Logger to write info too /// HTTP Client factory to use for this request. + /// if true, login is for an onprem server /// - private static async Task GetAuthorityFromTargetServiceAsync(IHttpClientFactory clientFactory, Uri targetServiceUrl, DataverseTraceLogger logger) + private static async Task GetAuthorityFromTargetServiceAsync(IHttpClientFactory clientFactory, Uri targetServiceUrl, DataverseTraceLogger logger, bool isOnPrem = false) { var client = clientFactory.CreateClient("DataverseHttpClientFactory"); var resolver = new AuthorityResolver(client, (t, msg) => logger.Log(msg, t)); - return await resolver.ProbeForExpectedAuthentication(targetServiceUrl); + return await resolver.ProbeForExpectedAuthentication(targetServiceUrl, isOnPrem); } /// diff --git a/src/GeneralTools/DataverseClient/Client/Auth/AuthorityResolver.cs b/src/GeneralTools/DataverseClient/Client/Auth/AuthorityResolver.cs index 663725f..f66f10e 100644 --- a/src/GeneralTools/DataverseClient/Client/Auth/AuthorityResolver.cs +++ b/src/GeneralTools/DataverseClient/Client/Auth/AuthorityResolver.cs @@ -60,10 +60,11 @@ public AuthorityResolver(HttpClient httpClient, Action l /// Attemtps to solicit a WWW-Authenticate reply using an unauthenticated GET call to the given endpoint. /// Parses returned header for details /// - /// + /// endpoint to challenge for authority and resource + /// if true, this is an OnPremsies server /// /// - public async Task ProbeForExpectedAuthentication(Uri endpoint) + public async Task ProbeForExpectedAuthentication(Uri endpoint, bool isOnPrem = false) { _ = endpoint ?? throw new ArgumentNullException(nameof(endpoint)); var details = new AuthenticationDetails(); @@ -94,53 +95,62 @@ public async Task ProbeForExpectedAuthentication(Uri endp if (response.Headers.Contains(AuthenticateHeader)) { - var authenticateHeader = response.Headers.GetValues(AuthenticateHeader).FirstOrDefault(); - authenticateHeader = authenticateHeader.Trim(); - - // This also checks for cases like "BearerXXXX authorization_uri=...." and "Bearer" and "Bearer " - if (!authenticateHeader.StartsWith(Bearer, StringComparison.OrdinalIgnoreCase) - || authenticateHeader.Length < Bearer.Length + 2 - || !char.IsWhiteSpace(authenticateHeader[Bearer.Length])) + var authenticateHeaders = response.Headers.GetValues(AuthenticateHeader); + // need to support OnPrem returning multiple Authentication headers. + foreach (var authenticateHeaderraw in authenticateHeaders) { - LogError($"Malformed 'Bearer' format: {authenticateHeader}"); - return details; - } + if (details.Success) + break; - authenticateHeader = authenticateHeader.Substring(Bearer.Length).Trim(); - - IDictionary authenticateHeaderItems = null; - try - { - authenticateHeaderItems = - EncodingHelper.ParseKeyValueListStrict(authenticateHeader, ',', false, true); - } - catch (ArgumentException) - { - LogError($"Malformed arguments in '{AuthenticateHeader}: {authenticateHeader}"); - return details; - } + string authenticateHeader = authenticateHeaderraw.Trim(); - if (authenticateHeaderItems != null) - { - if (!authenticateHeaderItems.TryGetValue(AuthorityKey, out var auth)) + // This also checks for cases like "BearerXXXX authorization_uri=...." and "Bearer" and "Bearer " + if (!authenticateHeader.StartsWith(Bearer, StringComparison.OrdinalIgnoreCase) + || authenticateHeader.Length < Bearer.Length + 2 + || !char.IsWhiteSpace(authenticateHeader[Bearer.Length])) { - LogError($"Response header from {endpoint} is missing expected key/value for {AuthorityKey}"); + if (isOnPrem) + continue; + + LogError($"Malformed 'Bearer' format: {authenticateHeader}"); return details; } - details.Authority = new Uri( - auth.Replace("oauth2/authorize", "") // swap out the old oAuth pattern. - .Replace("common", "organizations")); // swap common for organizations because MSAL reasons. - if (!authenticateHeaderItems.TryGetValue(ResourceKey, out var res)) + authenticateHeader = authenticateHeader.Substring(Bearer.Length).Trim(); + + IDictionary authenticateHeaderItems = null; + try + { + authenticateHeaderItems = + EncodingHelper.ParseKeyValueListStrict(authenticateHeader, ',', false, true); + } + catch (ArgumentException) { - LogError($"Response header from {endpoint} is missing expected key/value for {ResourceKey}"); + LogError($"Malformed arguments in '{AuthenticateHeader}: {authenticateHeader}"); return details; } - details.Resource = new Uri(res); - details.Success = true; + + if (authenticateHeaderItems != null) + { + if (!authenticateHeaderItems.TryGetValue(AuthorityKey, out var auth)) + { + LogError($"Response header from {endpoint} is missing expected key/value for {AuthorityKey}"); + return details; + } + details.Authority = new Uri( + auth.Replace("oauth2/authorize", "") // swap out the old oAuth pattern. + .Replace("common", "organizations")); // swap common for organizations because MSAL reasons. + + if (!authenticateHeaderItems.TryGetValue(ResourceKey, out var res)) + { + LogError($"Response header from {endpoint} is missing expected key/value for {ResourceKey}"); + return details; + } + details.Resource = new Uri(res); + details.Success = true; + } } } - return details; } diff --git a/src/GeneralTools/DataverseClient/Client/Auth/MSALHttpHelper.cs b/src/GeneralTools/DataverseClient/Client/Auth/MSALHttpHelper.cs index 1288d8f..d968a0e 100644 --- a/src/GeneralTools/DataverseClient/Client/Auth/MSALHttpHelper.cs +++ b/src/GeneralTools/DataverseClient/Client/Auth/MSALHttpHelper.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Microsoft.PowerPlatform.Dataverse.Client.Model; using Microsoft.PowerPlatform.Dataverse.Client.Utils; @@ -14,7 +14,7 @@ namespace Microsoft.PowerPlatform.Dataverse.Client.Auth { internal class MSALHttpRetryHandlerHelper : DelegatingHandler { - private readonly int MaxRetryCount = ClientServiceProviders.Instance.GetService>().Value.MsalRetryCount; + private readonly int MaxRetryCount = ClientServiceProviders.Instance.GetService>().Value.MsalRetryCount; /// /// Handel Failure and retry diff --git a/src/GeneralTools/DataverseClient/Client/Auth/OnPremises_Auth.cs b/src/GeneralTools/DataverseClient/Client/Auth/OnPremises_Auth.cs new file mode 100644 index 0000000..69d8fda --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Auth/OnPremises_Auth.cs @@ -0,0 +1,214 @@ +using Microsoft.PowerPlatform.Dataverse.Client.Connector.OnPremises; +using Microsoft.Xrm.Sdk.Client; +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.ServiceModel.Description; + +namespace Microsoft.PowerPlatform.Dataverse.Client.Auth +{ + /// + /// Authentication for Non-OAuth Onprem. + /// + internal static class OnPremises_Auth + { + /// + /// Creates and authenticates the Service Proxy for the organization service for OnPremises Dataverse + /// + /// Service Management Type + /// Initialized Service Management object or null. + /// URL to connect too + /// HomeRealm URI + /// User Credentials object + /// Log Preface string. + /// Max Connection timeout setting. + /// (optional) Initialized DataverseTraceLogger Object + /// + [SuppressMessage("Microsoft.Usage", "CA9888:DisposeObjectsCorrectly", MessageId = "OutObject")] + internal static object CreateAndAuthenticateProxy(IServiceManagement servicecfg, + Uri ServiceUri, + Uri homeRealm, + ClientCredentials userCredentials, + string LogString, + TimeSpan MaxConnectionTimeout, + DataverseTraceLogger logSink = null) + { + bool createdLogSource = false; + Stopwatch dtProxyCreate = new Stopwatch(); + dtProxyCreate.Start(); + Stopwatch dtConnectTimeCheck = new Stopwatch(); + try + { + if (logSink == null) + { + // when set, the log source is locally created. + createdLogSource = true; + logSink = new DataverseTraceLogger(); + } + + + object OutObject = null; + if (servicecfg == null) + { + logSink.Log(string.Format(CultureInfo.InvariantCulture, "{0} - attempting to connect to On-Premises Dataverse server @ {1}", LogString, ServiceUri.ToString()), TraceEventType.Verbose); + + // Create the Service configuration for that URL + dtConnectTimeCheck.Restart(); + servicecfg = ServiceConfigurationFactoryAsync.CreateManagement(ServiceUri); + dtConnectTimeCheck.Stop(); + if (servicecfg == null) + return null; + logSink.Log(string.Format(CultureInfo.InvariantCulture, "{0} - created Dataverse server proxy configuration for {1} - duration: {2}", LogString, ServiceUri.ToString(), dtConnectTimeCheck.Elapsed.ToString()), TraceEventType.Verbose); + } + else + { + logSink.Log(string.Format(CultureInfo.InvariantCulture, "{0} - will use user provided {1} to connect to Dataverse ", LogString, typeof(T).ToString()), TraceEventType.Verbose); + } + + // Auth + logSink.Log(string.Format(CultureInfo.InvariantCulture, "{0} - proxy requiring authentication type : {1} ", LogString, servicecfg.AuthenticationType), TraceEventType.Verbose); + // Determine the type of authentication required. + if (servicecfg.AuthenticationType != AuthenticationProviderType.ActiveDirectory) + { + // Connect via anything other then AD. + // Setup for Auth Check Performance. + dtConnectTimeCheck.Restart(); + + // Deal with IFD QurikyNess in ADFS configuration, where ADFS can be configured to fall though to Kerb Auth. + AuthenticationCredentials authCred = ClaimsIFDFailOverAuth(servicecfg, homeRealm, userCredentials); + dtConnectTimeCheck.Stop(); + + // If is Federation and HomeRealm is not null, and HomeRealm is Not the same as the SecureTokeServiceIdentifier + // Run Secondary auth to auth the Token to the right source. + SecurityTokenResponse AuthKey = null; + if (servicecfg.AuthenticationType == AuthenticationProviderType.Federation && + homeRealm != null && + !string.IsNullOrWhiteSpace(homeRealm.ToString()) && + (homeRealm.ToString() != servicecfg.PolicyConfiguration.SecureTokenServiceIdentifier)) + { + logSink.Log(string.Format(CultureInfo.InvariantCulture, "{0} - Initial Authenticated via {1} {3} . Auth Elapsed:{2}", LogString, servicecfg.AuthenticationType, dtConnectTimeCheck.Elapsed.ToString(), homeRealm.ToString()), TraceEventType.Verbose); + logSink.Log(string.Format(CultureInfo.InvariantCulture, "{0} - Relaying Auth to Resource Server: From {1} to {2}", LogString, homeRealm.ToString(), servicecfg.PolicyConfiguration.SecureTokenServiceIdentifier), TraceEventType.Verbose); + dtConnectTimeCheck.Restart(); + // Auth token against the correct server. + AuthenticationCredentials authCred2 = servicecfg.Authenticate(new AuthenticationCredentials() + { + SecurityTokenResponse = authCred.SecurityTokenResponse + }); + dtConnectTimeCheck.Stop(); + + if (authCred2 != null) + { + AuthKey = authCred2.SecurityTokenResponse; + logSink.Log(string.Format(CultureInfo.InvariantCulture, "{0} - Authenticated via {1}. Auth Elapsed:{2}", LogString, servicecfg.AuthenticationType, dtConnectTimeCheck.Elapsed.ToString()), TraceEventType.Verbose); + } + else + logSink.Log(string.Format(CultureInfo.InvariantCulture, "{0} - FAILED Authentication via {1}. Auth Elapsed:{2}", LogString, servicecfg.AuthenticationType, dtConnectTimeCheck.Elapsed.ToString()), TraceEventType.Verbose); + + } + else + { + if (authCred != null) + { + AuthKey = authCred.SecurityTokenResponse; + logSink.Log(string.Format(CultureInfo.InvariantCulture, "{0} - Authenticated via {1}. Auth Elapsed:{2}", LogString, servicecfg.AuthenticationType, dtConnectTimeCheck.Elapsed.ToString()), TraceEventType.Verbose); + } + else + logSink.Log(string.Format(CultureInfo.InvariantCulture, "{0} - Failed Authentication via {1}. Auth Elapsed:{2}", LogString, servicecfg.AuthenticationType, dtConnectTimeCheck.Elapsed.ToString()), TraceEventType.Verbose); + + } + //if (typeof(T) == typeof(IDiscoveryService)) + // OutObject = new DiscoveryServiceProxy((IServiceManagement)servicecfg, AuthKey); + + if (typeof(T) == typeof(IOrganizationServiceAsync)) + OutObject = new ManagedTokenOrganizationServiceProxy((IServiceManagement)servicecfg, AuthKey, userCredentials); + } + else + { + if (typeof(T) == typeof(IOrganizationServiceAsync)) + OutObject = new OrganizationServiceProxyAsync((IServiceManagement)servicecfg, userCredentials); + } + + logSink.Log(string.Format(CultureInfo.InvariantCulture, "{0} - service proxy created - total create duration: {1}", LogString, dtProxyCreate.Elapsed.ToString()), TraceEventType.Verbose); + + //Update the Timeout in case the MaxCrmConnectionTimeOutMinutes is Set in Config File. + if (OutObject != null) + { + if (OutObject is OrganizationServiceProxyAsync) + ((OrganizationServiceProxyAsync)OutObject).Timeout = MaxConnectionTimeout; + } + + return OutObject; + } + catch + { + throw; + } + finally + { + + if (createdLogSource) // Only dispose it if it was created locally. + logSink.Dispose(); + } + } + + /// + /// Handles direct authentication and fall though support to Kerb for Federation environments where configured for fall though + /// + /// Type of Service being authenticated + /// Service configuration + /// HomeRelam of the service + /// User Credentials + /// Internal Fall though switch + /// internal call back value + /// AuthenticationCredentials configured or null. + private static AuthenticationCredentials ClaimsIFDFailOverAuth(IServiceManagement servicecfg, Uri homeRealm, ClientCredentials userCredentials, int depthLevel = 0, bool tryNetworkCred = false) + { + AuthenticationCredentials authCred = new AuthenticationCredentials(); + + // Head off a runaway if one occurs. + if (depthLevel > 10) + return null; + + // If Im starting with a NetworkCred, try to turn that into a Client Cred User ID for federation. + if (tryNetworkCred == false && + servicecfg.AuthenticationType == AuthenticationProviderType.Federation && + userCredentials.Windows != null && + userCredentials.Windows.ClientCredential != null && + !string.IsNullOrWhiteSpace(userCredentials.Windows.ClientCredential.UserName)) + { + // Restructure user Account Creds for Federation + ClientCredentials userCredentials2 = new ClientCredentials(); + + // Updating the restructure process to remove corrective logic as, based on the configuration of the + userCredentials2.UserName.UserName = userCredentials.Windows.ClientCredential.UserName; + userCredentials2.UserName.Password = userCredentials.Windows.ClientCredential.Password; + authCred.ClientCredentials = userCredentials2; + } + else + authCred.ClientCredentials = userCredentials; + + // Claims + if (homeRealm != null) + authCred.HomeRealm = homeRealm; + + // Deal with an incorrect configuration here. + // Home Realm should not be used if the Service Identifier and the homeRealm are the same thing. + if (homeRealm != null && homeRealm.ToString() != servicecfg.PolicyConfiguration.SecureTokenServiceIdentifier) + authCred.AppliesTo = new Uri(servicecfg.PolicyConfiguration.SecureTokenServiceIdentifier); + + // Run Authentication + // Failure will generate Exceptions. + authCred = servicecfg.Authenticate(authCred); + if (authCred != null && authCred.SecurityTokenResponse == null && userCredentials.Windows != null && userCredentials.Windows.ClientCredential != null) + { + // This code exists to deal with Federation configurations that "fall though" to claims from ADFS.. more commonly known as IFD. + return ClaimsIFDFailOverAuth(servicecfg, homeRealm, userCredentials, ++depthLevel, true); + } + else + return authCred; + } + + + } +} diff --git a/src/GeneralTools/DataverseClient/Client/Client/OrganizationWebProxyClientAsync.cs b/src/GeneralTools/DataverseClient/Client/Client/OrganizationWebProxyClientAsync.cs deleted file mode 100644 index 3bb5a61..0000000 --- a/src/GeneralTools/DataverseClient/Client/Client/OrganizationWebProxyClientAsync.cs +++ /dev/null @@ -1,237 +0,0 @@ -namespace Microsoft.Xrm.Sdk.WebServiceClient -{ - using System; - using System.Diagnostics.CodeAnalysis; - using System.Reflection; - using System.Threading.Tasks; - using Microsoft.PowerPlatform.Dataverse.Client; - using Query; - -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member - - internal class OrganizationWebProxyClientAsync : WebProxyClient, IOrganizationServiceAsync - { - public OrganizationWebProxyClientAsync(Uri serviceUrl, bool useStrongTypes) - : base(serviceUrl, useStrongTypes) - { - } - - public OrganizationWebProxyClientAsync(Uri serviceUrl, Assembly strongTypeAssembly) - : base(serviceUrl, strongTypeAssembly) - { - } - - public OrganizationWebProxyClientAsync(Uri serviceUrl, TimeSpan timeout, bool useStrongTypes) - : base(serviceUrl, timeout, useStrongTypes) - { - } - - public OrganizationWebProxyClientAsync(Uri uri, TimeSpan timeout, Assembly strongTypeAssembly) - : base(uri, timeout, strongTypeAssembly) - { - } - - #region Properties - - internal bool OfflinePlayback { get; set; } - - public string SyncOperationType { get; set; } - - public Guid CallerId { get; set; } - - public UserType userType { get; set; } - - public Guid CallerRegardingObjectId { get; set; } - - internal int LanguageCodeOverride { get; set; } - - #endregion - - #region IOrganizationService implementation - - public void Associate(string entityName, Guid entityId, Relationship relationship, - EntityReferenceCollection relatedEntities) - { - AssociateCore(entityName, entityId, relationship, relatedEntities); - } - - public Task AssociateAsync(string entityName, Guid entityId, Relationship relationship, - EntityReferenceCollection relatedEntities) - { - return AssociateAsyncCore(entityName, entityId, relationship, relatedEntities); - } - - public Guid Create(Entity entity) - { - return CreateCore(entity); - } - - public Task CreateAsync(Entity entity) - { - return CreateAsyncCore(entity); - } - - public void Delete(string entityName, Guid id) - { - DeleteCore(entityName, id); - } - - public Task DeleteAsync(string entityName, Guid id) - { - return DeleteAsyncCore(entityName, id); - } - - public void Disassociate(string entityName, Guid entityId, Relationship relationship, - EntityReferenceCollection relatedEntities) - { - DisassociateCore(entityName, entityId, relationship, relatedEntities); - } - - public Task DisassociateAsync(string entityName, Guid entityId, Relationship relationship, - EntityReferenceCollection relatedEntities) - { - return DisassociateAsyncCore(entityName, entityId, relationship, relatedEntities); - } - - public OrganizationResponse Execute(OrganizationRequest request) - { - return ExecuteCore(request); - } - - public Task ExecuteAsync(OrganizationRequest request) - { - return ExecuteAsyncCore(request); - } - - public Entity Retrieve(string entityName, Guid id, ColumnSet columnSet) - { - return RetrieveCore(entityName, id, columnSet); - } - - public Task RetrieveAsync(string entityName, Guid id, ColumnSet columnSet) - { - return RetrieveAsyncCore(entityName, id, columnSet); - } - - public EntityCollection RetrieveMultiple(QueryBase query) - { - return RetrieveMultipleCore(query); - } - - public Task RetrieveMultipleAsync(QueryBase query) - { - return RetrieveMultipleAsyncCore(query); - } - - public void Update(Entity entity) - { - UpdateCore(entity); - } - - public Task UpdateAsync(Entity entity) - { - return UpdateAsyncCore(entity); - } - - #endregion - - #region Protected IOrganizationService CoreMembers - - protected internal virtual Guid CreateCore(Entity entity) - { - return ExecuteAction(() => Channel.Create(entity)); - } - - protected Task CreateAsyncCore(Entity entity) - { - return ExecuteAction(() => Channel.CreateAsync(entity)); - } - - protected internal virtual Entity RetrieveCore(string entityName, Guid id, ColumnSet columnSet) - { - return ExecuteAction(() => Channel.Retrieve(entityName, id, columnSet)); - } - - protected internal virtual Task RetrieveAsyncCore(string entityName, Guid id, ColumnSet columnSet) - { - return ExecuteAction(() => Channel.RetrieveAsync(entityName, id, columnSet)); - } - - protected internal virtual void UpdateCore(Entity entity) - { - ExecuteAction(() => Channel.Update(entity)); - } - - protected internal virtual Task UpdateAsyncCore(Entity entity) - { - return ExecuteAction(() => Channel.UpdateAsync(entity)); - } - - protected internal virtual void DeleteCore(string entityName, Guid id) - { - ExecuteAction(() => Channel.Delete(entityName, id)); - } - - protected internal virtual Task DeleteAsyncCore(string entityName, Guid id) - { - return ExecuteAction(() => Channel.DeleteAsync(entityName, id)); - } - - protected internal virtual OrganizationResponse ExecuteCore(OrganizationRequest request) - { - return ExecuteAction(() => Channel.Execute(request)); - } - - protected internal virtual Task ExecuteAsyncCore(OrganizationRequest request) - { - return ExecuteAction(() => Channel.ExecuteAsync(request)); - } - - protected internal virtual void AssociateCore(string entityName, Guid entityId, Relationship relationship, - EntityReferenceCollection relatedEntities) - { - ExecuteAction(() => Channel.Associate(entityName, entityId, relationship, relatedEntities)); - } - - protected internal virtual Task AssociateAsyncCore(string entityName, Guid entityId, Relationship relationship, - EntityReferenceCollection relatedEntities) - { - return ExecuteAction(() => Channel.AssociateAsync(entityName, entityId, relationship, relatedEntities)); - } - - protected internal virtual void DisassociateCore(string entityName, Guid entityId, Relationship relationship, - EntityReferenceCollection relatedEntities) - { - ExecuteAction(() => Channel.Disassociate(entityName, entityId, relationship, relatedEntities)); - } - - protected internal virtual Task DisassociateAsyncCore(string entityName, Guid entityId, Relationship relationship, - EntityReferenceCollection relatedEntities) - { - return ExecuteAction(() => Channel.DisassociateAsync(entityName, entityId, relationship, relatedEntities)); - } - - protected internal virtual EntityCollection RetrieveMultipleCore(QueryBase query) - { - return ExecuteAction(() => Channel.RetrieveMultiple(query)); - } - - protected internal virtual Task RetrieveMultipleAsyncCore(QueryBase query) - { - return ExecuteAction(() => Channel.RetrieveMultipleAsync(query)); - } - - #endregion Protected Members - - #region Protected Methods - - protected override WebProxyClientContextInitializer CreateNewInitializer() - { - return new OrganizationWebProxyClientAsyncContextInitializer(this); - } - - #endregion - } -#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member - -} diff --git a/src/GeneralTools/DataverseClient/Client/Client/OrganizationWebProxyClientContextInitializer.cs b/src/GeneralTools/DataverseClient/Client/Client/OrganizationWebProxyClientContextInitializer.cs deleted file mode 100644 index 92c70c9..0000000 --- a/src/GeneralTools/DataverseClient/Client/Client/OrganizationWebProxyClientContextInitializer.cs +++ /dev/null @@ -1,88 +0,0 @@ -namespace Microsoft.Xrm.Sdk.WebServiceClient -{ - using Client; - using Microsoft.PowerPlatform.Dataverse.Client; - using System; - using System.ServiceModel; - using System.ServiceModel.Channels; - using XmlNamespaces; - - /// - /// Manages context for sdk calls - /// - internal sealed class OrganizationWebProxyClientAsyncContextInitializer : - WebProxyClientContextInitializer - { - public OrganizationWebProxyClientAsyncContextInitializer(OrganizationWebProxyClientAsync proxy) - : base(proxy) - { - Initialize(); - } - - #region Properties - - private OrganizationWebProxyClientAsync OrganizationWebProxyClient - { - get { return ServiceProxy as OrganizationWebProxyClientAsync; } - } - - #endregion - - #region Private Methods - - private void Initialize() - { - if (ServiceProxy == null) - { - return; - } - - AddTokenToHeaders(); - - if (ServiceProxy != null) - { - if (OrganizationWebProxyClient.OfflinePlayback) - { - OperationContext.Current.OutgoingMessageHeaders.Add(MessageHeader.CreateHeader(SdkHeaders.IsOfflinePlayback, - V5.Contracts, true)); - } - - if (OrganizationWebProxyClient.CallerId != Guid.Empty) - { - OperationContext.Current.OutgoingMessageHeaders.Add(MessageHeader.CreateHeader(SdkHeaders.CallerId, - V5.Contracts, - OrganizationWebProxyClient.CallerId)); - } - - if (OrganizationWebProxyClient.CallerRegardingObjectId != Guid.Empty) - { - OperationContext.Current.OutgoingMessageHeaders.Add(MessageHeader.CreateHeader(SdkHeaders.CallerRegardingObjectId, - V5.Contracts, - OrganizationWebProxyClient.CallerRegardingObjectId)); - } - - if (OrganizationWebProxyClient.LanguageCodeOverride != 0) - { - OperationContext.Current.OutgoingMessageHeaders.Add(MessageHeader.CreateHeader(SdkHeaders.LanguageCodeOverride, - V5.Contracts, - OrganizationWebProxyClient.LanguageCodeOverride)); - } - - if (OrganizationWebProxyClient.SyncOperationType != null) - { - OperationContext.Current.OutgoingMessageHeaders.Add(MessageHeader.CreateHeader(SdkHeaders.OutlookSyncOperationType, - V5.Contracts, - OrganizationWebProxyClient.SyncOperationType)); - } - - OperationContext.Current.OutgoingMessageHeaders.Add(MessageHeader.CreateHeader(SdkHeaders.UserType, - V5.Contracts, - OrganizationWebProxyClient.userType)); - - AddCommonHeaders(); - } - } - - #endregion - } -} diff --git a/src/GeneralTools/DataverseClient/Client/ClientTokenCache.cs b/src/GeneralTools/DataverseClient/Client/ClientTokenCache.cs deleted file mode 100644 index caedc78..0000000 --- a/src/GeneralTools/DataverseClient/Client/ClientTokenCache.cs +++ /dev/null @@ -1,254 +0,0 @@ -using System; -using System.Threading.Tasks; -using System.IO; -using System.Security.Cryptography; -using System.Diagnostics; -using Microsoft.Identity.Client; - -namespace Microsoft.PowerPlatform.Dataverse.Client -{ - /// - /// Primary implementation of TokenCache - /// - internal class ClientTokenCache : IDisposable - { - private DataverseTraceLogger logEntry; - - private static string _cacheFilePath; - - private object _fileLocker = new object(); - /// - /// Flag to control if the protected data API should be used. - /// this flag needs to be disabled if running under Azure WebApp contexts as they do not provide access to the encryption feature. - /// - private bool _UseLocalFileEncryption = true; - - /// - /// Constructor with Parameter cacheFilePath - /// - /// - /// - public ClientTokenCache(ITokenCache tokenCache , string cacheFilePath) - { - logEntry = new DataverseTraceLogger(); - - //If cacheFilePath is provided - if (!string.IsNullOrEmpty(cacheFilePath)) - { - _cacheFilePath = cacheFilePath; - - // Register MSAL event handlers. - tokenCache.SetBeforeAccess(BeforeAccessNotification); - tokenCache.SetAfterAccess(AfterAccessNotification); - - // Need to revist this for adding support for other cache providers: - // https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-net-token-cache-serialization - - // // Try to encrypt some data to test if Protect is available. - // try - // { - //#pragma warning disable CS0618 // Type or member is obsolete - // ProtectedData.Protect(this.Serialize(), null, DataProtectionScope.CurrentUser); - //#pragma warning restore CS0618 // Type or member is obsolete - // } - // catch (Exception ex) - // { - // _UseLocalFileEncryption = false; - // logEntry.Log("Encryption System not available in this environment", TraceEventType.Warning, ex); - // } - - - // Create token cache file if one does not already exist. - if (!File.Exists(_cacheFilePath)) - { - string directoryName = Path.GetDirectoryName(_cacheFilePath); - - if (!Directory.Exists(directoryName)) - { - Directory.CreateDirectory(directoryName); - } - - //File.Create(string) returns an instance of the FileStream class. You need to use Close() method - //in order to close it and release resources which are using - try - { - File.Create(_cacheFilePath).Close(); - // Encrypt the file - try - { - // user is using a specified file directory... encrypt file to user using Machine / FS Locking. - // this will lock / prevent users other then the current user from accessing this file. - FileInfo fi = new FileInfo(_cacheFilePath); - if (_UseLocalFileEncryption) - fi.Encrypt(); - } - catch (IOException) - { - // This can happen when a certificate system on the host has failed. - // usually this can be fixed with the steps in this article : http://support.microsoft.com/kb/937536 - //logEntry.Log(string.Format("{0}\r\nException Details : {1}", "Failed to Encrypt Configuration File!", encrEX), TraceEventType.Error); - //logEntry.Log("This problem may be related to a domain certificate in windows being out of sync with the domain, please read http://support.microsoft.com/kb/937536"); - } - catch (Exception) - { - //logEntry.Log(string.Format("Failed to Encrypt Configuration File!", genEX), TraceEventType.Error); - } - } - catch (Exception exception) - { - logEntry.Log(string.Format("{0}\r\nException Details : {1}", "Error occurred in DataverseServiceClientTokenCache(). ", exception), TraceEventType.Error); - } - } - - //// Register ADAL event handlers. - //this.AfterAccess = AfterAccessNotification; - //this.BeforeAccess = BeforeAccessNotification; - -// lock (_fileLocker) -// { -// try -// { -// // Read token from the persistent store and supply it to ADAL's in memory cache. -// if (_UseLocalFileEncryption) -//#pragma warning disable CS0618 // Type or member is obsolete -// this.Deserialize(File.Exists(_cacheFilePath) && File.ReadAllBytes(_cacheFilePath).Length != 0 -// ? ProtectedData.Unprotect(File.ReadAllBytes(_cacheFilePath), null, DataProtectionScope.CurrentUser) -// : null); -//#pragma warning restore CS0618 // Type or member is obsolete -// else -//#pragma warning disable CS0618 // Type or member is obsolete -// this.Deserialize(File.Exists(_cacheFilePath) && File.ReadAllBytes(_cacheFilePath).Length != 0 -// ? File.ReadAllBytes(_cacheFilePath) : null); -//#pragma warning restore CS0618 // Type or member is obsolete -// } -// catch (Exception ex) -// { -// // Failed to access Local token cache file.. -// // Delete it. -// logEntry.Log("Failed to access token cache file, resetting the token cache file", TraceEventType.Warning, ex); -// Clear(_cacheFilePath); -// } -// } - } - - } - - /// - /// Empties the persistent and in-memory store. - /// - /// - /// - public bool Clear(string tokenFilePath) - { - string deletePath = tokenFilePath; - - //If both parameter and private var is null or empty need to pass false to caller function - if (string.IsNullOrWhiteSpace(deletePath)) - { - if (!string.IsNullOrEmpty(_cacheFilePath)) - deletePath = _cacheFilePath; - else - return false; - } - - try - { - //clear in-memory cache. - //base.Clear(); - // check if already exist or not - if (File.Exists(deletePath)) - { - // Delete persistent store first for integrity - File.Delete(deletePath); - } - } - catch (Exception exception) - { - logEntry.Log( - string.Format("{0}\r\nException Details : {1}", "Error occurred in clearing DataverseServiceClientTokenCache.Clear(). ", - exception), TraceEventType.Error); - return false; - } - return true; - } - - /// - /// Triggered right before ADAL needs to access the cache. - /// Reload the cache from the persistent store in case it changed since the last access. - /// - /// - private void BeforeAccessNotification(TokenCacheNotificationArgs args) - { - if (!args.HasStateChanged) return; // if the token state has not changed... skip this step. - - lock (_fileLocker) - { - // Read token from persistent store and supply it to ADAL's in memory cache. - if (_UseLocalFileEncryption) - args.TokenCache.DeserializeMsalV3(File.Exists(_cacheFilePath) && File.ReadAllBytes(_cacheFilePath).Length != 0 - ? ProtectedData.Unprotect(File.ReadAllBytes(_cacheFilePath), null, DataProtectionScope.CurrentUser) - : null); - else - args.TokenCache.DeserializeMsalV3(File.Exists(_cacheFilePath) && File.ReadAllBytes(_cacheFilePath).Length != 0 - ? File.ReadAllBytes(_cacheFilePath) : null); - } - } - - /// - /// Triggered right after ADAL accessed the cache. - /// - /// - private void AfterAccessNotification(TokenCacheNotificationArgs args) - { - // If the access operation resulted in a cache update. - if (!args.HasStateChanged) return; - - lock (_fileLocker) - { - try - { - // Reflect token changes in the persistent store. - if (_UseLocalFileEncryption) - File.WriteAllBytes(_cacheFilePath, ProtectedData.Protect(args.TokenCache.SerializeMsalV3(), null, DataProtectionScope.CurrentUser)); - else - File.WriteAllBytes(_cacheFilePath, args.TokenCache.SerializeMsalV3()); - // once the write operation took place, restore the HasStateChanged bit to false. - //args.HasStateChanged = false; - } - catch (Exception exception) - { - logEntry.Log(string.Format("{0}\r\nException Details : {1}", "Error occurred in DataverseServiceClientTokenCache.AfterAccessNotification(). ", exception), TraceEventType.Error); - } - } - } - - #region IDisposable Support - private bool disposedValue = false; // To detect redundant calls - - /// - /// Cleaning up the object. - /// - /// - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - if (logEntry != null) - logEntry.Dispose(); - } - disposedValue = true; - } - } - - /// - /// Clean up the object - /// - public void Dispose() - { - Dispose(true); - } - #endregion - } -} diff --git a/src/GeneralTools/DataverseClient/Client/ConnectionService.cs b/src/GeneralTools/DataverseClient/Client/ConnectionService.cs index 277e282..d45892c 100644 --- a/src/GeneralTools/DataverseClient/Client/ConnectionService.cs +++ b/src/GeneralTools/DataverseClient/Client/ConnectionService.cs @@ -7,6 +7,9 @@ using Microsoft.Identity.Client; using Microsoft.PowerPlatform.Dataverse.Client.Auth; 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.InternalExtensions; using Microsoft.PowerPlatform.Dataverse.Client.Model; using Microsoft.PowerPlatform.Dataverse.Client.Utils; using Microsoft.Rest; @@ -45,6 +48,10 @@ namespace Microsoft.PowerPlatform.Dataverse.Client /// public enum AuthenticationType { + /// + /// Active Directory Auth + /// + AD = 0, /// /// OAuth based Auth /// @@ -75,6 +82,7 @@ internal sealed class ConnectionService : IConnectionService, IDisposable #region variables [NonSerializedAttribute] private OrganizationWebProxyClientAsync _svcWebClientProxy; + private OrganizationServiceProxyAsync _svcOnPremClientProxy; private OrganizationWebProxyClientAsync _externalWebClientProxy; // OAuth specific web service proxy [NonSerializedAttribute] @@ -116,13 +124,7 @@ internal sealed class ConnectionService : IConnectionService, IDisposable /// /// Configuration /// - private IOptions _configuration = ClientServiceProviders.Instance.GetService>(); - - /// - /// If set to true, will relay any received cookie back to the server. - /// Defaulted to true. - /// - private bool _enableCookieRelay = Utils.AppSettingsHelper.GetAppSetting("PreferConnectionAffinity", true); + private IOptions _configuration = ClientServiceProviders.Instance.GetService>(); /// /// TimeSpan used to control the offset of the token reacquire behavior for none user Auth flows. @@ -424,6 +426,26 @@ internal OrganizationWebProxyClientAsync WebClient } } + /// + /// Returns the Dataverse Client for OnPrem. + /// + internal OrganizationServiceProxyAsync OnPremClient + { + get + { + if (_svcOnPremClientProxy != null) + { + RefreshClientTokenAsync().ConfigureAwait(false).GetAwaiter().GetResult(); // Only call this if the connection is not null + try + { + AttachProxyHander(_svcOnPremClientProxy); + } + catch { } + } + return _svcOnPremClientProxy; + } + } + /// /// Get / Set the Dataverse Organization that the customer exists in /// @@ -583,8 +605,8 @@ internal static TimeSpan MaxConnectionTimeout /// internal bool EnableCookieRelay { - get { return _enableCookieRelay; } - set { _enableCookieRelay = value; } + get => _configuration.Value.EnableAffinityCookie; + set => _configuration.Value.EnableAffinityCookie = value; } /// @@ -593,7 +615,7 @@ internal bool EnableCookieRelay internal Dictionary CurrentCookieCollection { get; set; } = null; /// - /// Server Hint for the number of concurrent threads that would provbide optimal processing. + /// Server Hint for the number of concurrent threads that would provide optimal processing. /// internal int RecommendedDegreesOfParallelism { get; set; } = 5; // Default value. @@ -652,6 +674,54 @@ internal ConnectionService(OrganizationWebProxyClientAsync externalOrgWebProxyCl _eAuthType = authType; } + /// + /// Sets up a Connection to Dataverse OnPremsie + /// + /// Type of Authentication to use, AD or IDF + /// Host name to connect too + /// Port Number to Connect too + /// Organization to Connect to + /// Credential to use to connect + /// flag that will tell the instance to create a Unique Name for the Dataverse Cache Objects. + /// Dataverse Org Detail object, this is is returned from a query to the Dataverse Discovery Server service. not required. + /// incoming LogSink + /// instance to connect too + internal ConnectionService( + AuthenticationType authType, // type of auth, AD + string hostName, // Host name your connecting too.. AD only + string port, // Host Port your connecting too.. AD only + string orgName, // Dataverse Organization Name your connecting too + System.Net.NetworkCredential providedCredential, // Network credential to use to connect to CRM + bool useUniqueCacheName, // tells the system to create a unique cache name for this instance. + OrganizationDetail orgDetail, // Tells the client connection to bypass all discovery server behaviors and use this detail object. + DataverseTraceLogger logSink = null, + Uri instanceToConnectToo = null) + { + if (authType != AuthenticationType.AD) + throw new ArgumentOutOfRangeException("authType", "Invalid Authentication type"); + + if (logSink == null) + { + logEntry = new DataverseTraceLogger(); + isLogEntryCreatedLocaly = true; + } + else + { + logEntry = logSink; + isLogEntryCreatedLocaly = false; + } + + UseExternalConnection = false; + _eAuthType = authType; + _hostname = hostName; + _port = port; + _organization = orgName; + _AccessCred = providedCredential; + _OrgDetail = orgDetail; + _targetInstanceUriToConnectTo = instanceToConnectToo; + GenerateCacheKeys(useUniqueCacheName); + } + /// /// Sets up and initializes the Dataverse Service interface using OAuth for user flows. /// @@ -675,8 +745,8 @@ internal ConnectionService(OrganizationWebProxyClientAsync externalOrgWebProxyCl internal ConnectionService( AuthenticationType authType, // Only OAuth is supported in this constructor. string orgName, // Organization Name your connecting too - string liveUserId, // Live ID - Live only - SecureString livePass, // Live Pw - Live Only + string liveUserId, // Live ID - Live only + SecureString livePass, // Live PW - Live Only string onlineRegion, bool useUniqueCacheName, // tells the system to create a unique cache name for this instance. OrganizationDetail orgDetail, @@ -827,7 +897,23 @@ private bool IntilizeService(out ConnectionService ConnectionObject) if (dvService != null) { - _svcWebClientProxy = (OrganizationWebProxyClientAsync)dvService; + if (_svcWebClientProxy != null) + _svcWebClientProxy.Dispose(); + + if (_svcOnPremClientProxy != null) + _svcOnPremClientProxy.Dispose(); + + if (dvService is OrganizationWebProxyClientAsync orgWebClient) + { + _svcWebClientProxy = orgWebClient; + } + else + { + if (dvService is OrganizationServiceProxyAsync orgOnPremClient) + { + _svcOnPremClientProxy = orgOnPremClient; + } + } return true; } else @@ -949,121 +1035,139 @@ private async Task InitServiceAsync() } else { - if ((_eAuthType == AuthenticationType.OAuth && _isOnPremOAuth == true) || (_eAuthType == AuthenticationType.Certificate && _isOnPremOAuth == true)) + if (_eAuthType == AuthenticationType.AD ||(_eAuthType == AuthenticationType.OAuth && _isOnPremOAuth == true) || (_eAuthType == AuthenticationType.Certificate && _isOnPremOAuth == true)) { #region AD or SPLA Auth try { - string DvUrl = string.Empty; - #region AD - if (_OrgDetail == null) + if (_targetInstanceUriToConnectTo == null) // This is TEMP until Discovery Services interface is working for OnPrem. + throw new DataverseConnectionException("Environment URL is required when connecting to On Premises"); // Block discovery. + + // Given Direct Url.. connect to the Direct URL + if (_targetInstanceUriToConnectTo != null) { - // Build Discovery Server Connection - if (!string.IsNullOrWhiteSpace(_port)) - { - // http://:/XRMServices/2011/Discovery.svc?wsdl - DvUrl = String.Format(CultureInfo.InvariantCulture, - "{0}://{1}:{2}/XRMServices/2011/Discovery.svc", - _InternetProtocalToUse, - _hostname, - _port); - } - else - { - DvUrl = String.Format(CultureInfo.InvariantCulture, - "{0}://{1}/XRMServices/2011/Discovery.svc", - _InternetProtocalToUse, - _hostname); - } - logEntry.Log(string.Format(CultureInfo.InvariantCulture, "Discovery URI is = {0}", DvUrl), TraceEventType.Information); - if (!Uri.IsWellFormedUriString(DvUrl, UriKind.Absolute)) + dvService = await DoDirectLoginAsync(true).ConfigureAwait(false); + } + else + { + //TODO :/// Remove Discovery support for Onprem. + #region TO BE DELETED + string DvUrl = string.Empty; + #region AD + if (_OrgDetail == null) { - // Throw error here. - logEntry.Log(string.Format(CultureInfo.InvariantCulture, "Discovery URI is malformed = {0}", DvUrl), TraceEventType.Error); + // Build Discovery Server Connection + if (!string.IsNullOrWhiteSpace(_port)) + { + // http://:/XRMServices/2011/Discovery.svc?wsdl + DvUrl = String.Format(CultureInfo.InvariantCulture, + "{0}://{1}:{2}/XRMServices/2011/Discovery.svc", + _InternetProtocalToUse, + _hostname, + _port); + } + else + { + DvUrl = String.Format(CultureInfo.InvariantCulture, + "{0}://{1}/XRMServices/2011/Discovery.svc", + _InternetProtocalToUse, + _hostname); + } + logEntry.Log(string.Format(CultureInfo.InvariantCulture, "Discovery URI is = {0}", DvUrl), TraceEventType.Information); + if (!Uri.IsWellFormedUriString(DvUrl, UriKind.Absolute)) + { + // Throw error here. + logEntry.Log(string.Format(CultureInfo.InvariantCulture, "Discovery URI is malformed = {0}", DvUrl), TraceEventType.Error); - return null; + return null; + } } - } - else - logEntry.Log(string.Format(CultureInfo.InvariantCulture, "Process is bypassed.. OrgDetail object was provided"), TraceEventType.Information); + else + logEntry.Log(string.Format(CultureInfo.InvariantCulture, "Process is bypassed.. OrgDetail object was provided"), TraceEventType.Information); - _UserClientCred = new ClientCredentials(); - Uri uUserHomeRealm = null; + _UserClientCred = new ClientCredentials(); + Uri uUserHomeRealm = null; - if (_eAuthType == AuthenticationType.Certificate) - { - // Certificate based .. get the Cert. - if (_certificateOfConnection == null && !string.IsNullOrEmpty(_certificateThumbprint)) + if (_eAuthType == AuthenticationType.Certificate) { - // Certificate is not passed in. Thumbprint found... try to acquire the cert. - _certificateOfConnection = FindCertificate(_certificateThumbprint, _certificateStoreLocation, logEntry); - if (_certificateOfConnection == null) + // Certificate based .. get the Cert. + if (_certificateOfConnection == null && !string.IsNullOrEmpty(_certificateThumbprint)) { - // Fail.. no Cert. - throw new Exception("Failed to locate or read certificate from passed thumbprint.", logEntry.LastException); + // Certificate is not passed in. Thumbprint found... try to acquire the cert. + _certificateOfConnection = FindCertificate(_certificateThumbprint, _certificateStoreLocation, logEntry); + if (_certificateOfConnection == null) + { + // Fail.. no Cert. + throw new Exception("Failed to locate or read certificate from passed thumbprint.", logEntry.LastException); + } } } - } - else - { - if (_eAuthType == AuthenticationType.OAuth) + else { - // oAuthBased. - _UserClientCred.UserName.Password = string.Empty; - _UserClientCred.UserName.UserName = string.Empty; + if (_eAuthType == AuthenticationType.OAuth) + { + // oAuthBased. + _UserClientCred.UserName.Password = string.Empty; + _UserClientCred.UserName.UserName = string.Empty; + } } - } - OrganizationDetail orgDetail = null; - if (_OrgDetail == null) - { - // Discover Orgs Url. - Uri uDvUrl = new Uri(DvUrl); + OrganizationDetail orgDetail = null; + if (_OrgDetail == null) + { + // Discover Orgs Url. + Uri uDvUrl = new Uri(DvUrl); - // This will try to discover any organizations that the user has access too, one way supports AD / IFD and the other supports Claims - DiscoverOrganizationsResult discoverOrganizationsResult = null; + // This will try to discover any organizations that the user has access too, one way supports AD / IFD and the other supports Claims + DiscoverOrganizationsResult discoverOrganizationsResult = null; - if (_eAuthType == AuthenticationType.OAuth) - { - discoverOrganizationsResult = await DiscoverOrganizationsAsync(uDvUrl, _UserClientCred, _clientId, _redirectUri, _promptBehavior, true, _authority, logSink: logEntry, tokenCacheStorePath: _tokenCachePath).ConfigureAwait(false); - } - else - { - if (_eAuthType == AuthenticationType.Certificate) + if (_eAuthType == AuthenticationType.OAuth) { - discoverOrganizationsResult = await DiscoverOrganizationsAsync(uDvUrl, _certificateOfConnection, _clientId, true, _authority, logEntry, tokenCacheStorePath: _tokenCachePath).ConfigureAwait(false); + discoverOrganizationsResult = await DiscoverOrganizationsAsync(uDvUrl, _UserClientCred, _clientId, _redirectUri, _promptBehavior, true, _authority, logSink: logEntry, tokenCacheStorePath: _tokenCachePath).ConfigureAwait(false); + } + else + { + if (_eAuthType == AuthenticationType.Certificate) + { + discoverOrganizationsResult = await DiscoverOrganizationsAsync(uDvUrl, _certificateOfConnection, _clientId, true, _authority, logEntry, tokenCacheStorePath: _tokenCachePath).ConfigureAwait(false); + } } - } - // Check the Result to see if we have Orgs back - if (discoverOrganizationsResult.OrganizationDetailCollection != null && discoverOrganizationsResult.OrganizationDetailCollection.Count > 0) - { - logEntry.Log(string.Format(CultureInfo.InvariantCulture, "Found {0} Org(s)", discoverOrganizationsResult.OrganizationDetailCollection.Count), TraceEventType.Information); - orgDetail = discoverOrganizationsResult.OrganizationDetailCollection.FirstOrDefault(o => string.Compare(o.UniqueName, _organization, StringComparison.CurrentCultureIgnoreCase) == 0); - if (orgDetail == null) - orgDetail = discoverOrganizationsResult.OrganizationDetailCollection.FirstOrDefault(o => string.Compare(o.FriendlyName, _organization, StringComparison.CurrentCultureIgnoreCase) == 0); + // Check the Result to see if we have Orgs back + if (discoverOrganizationsResult.OrganizationDetailCollection != null && discoverOrganizationsResult.OrganizationDetailCollection.Count > 0) + { + logEntry.Log(string.Format(CultureInfo.InvariantCulture, "Found {0} Org(s)", discoverOrganizationsResult.OrganizationDetailCollection.Count), TraceEventType.Information); + orgDetail = discoverOrganizationsResult.OrganizationDetailCollection.FirstOrDefault(o => string.Compare(o.UniqueName, _organization, StringComparison.CurrentCultureIgnoreCase) == 0); + if (orgDetail == null) + orgDetail = discoverOrganizationsResult.OrganizationDetailCollection.FirstOrDefault(o => string.Compare(o.FriendlyName, _organization, StringComparison.CurrentCultureIgnoreCase) == 0); - if (orgDetail == null) + if (orgDetail == null) + { + logEntry.Log(string.Format(CultureInfo.InvariantCulture, "Organization not found. Org = {0}", _organization), TraceEventType.Error); + return null; + } + } + else { - logEntry.Log(string.Format(CultureInfo.InvariantCulture, "Organization not found. Org = {0}", _organization), TraceEventType.Error); + // error here. + logEntry.Log("No Organizations found.", TraceEventType.Error); return null; } } else - { - // error here. - logEntry.Log("No Organizations found.", TraceEventType.Error); - return null; - } + orgDetail = _OrgDetail; // Assign to passed in value. + + // Try to connect to Dataverse here. + dvService = await ConnectAndInitServiceAsync(orgDetail, true, uUserHomeRealm).ConfigureAwait(false); + + #endregion + #endregion + } - else - orgDetail = _OrgDetail; // Assign to passed in value. - // Try to connect to Dataverse here. - dvService = await ConnectAndInitServiceAsync(orgDetail, true, uUserHomeRealm).ConfigureAwait(false); if (dvService == null) { @@ -1074,7 +1178,7 @@ private async Task InitServiceAsync() if (_eAuthType == AuthenticationType.OAuth || _eAuthType == AuthenticationType.Certificate || _eAuthType == AuthenticationType.ClientSecret) dvService = (OrganizationWebProxyClientAsync)dvService; - #endregion + } catch (Exception ex) @@ -1086,7 +1190,7 @@ private async Task InitServiceAsync() ((OrganizationWebProxyClient)dvService).Dispose(); dvService = null; } - return null; + throw; } #endregion } @@ -1289,16 +1393,24 @@ private async Task InitServiceAsync() /// /// Executes a direct login using the current configuration. /// + /// if set indicates an onPrem Authentication /// - private async Task DoDirectLoginAsync() + private async Task DoDirectLoginAsync(bool IsOnPrem = false) { logEntry.Log("Direct Login Process Started", TraceEventType.Verbose); Stopwatch sw = new Stopwatch(); sw.Start(); IOrganizationService dvService = null; - - Uri OrgWorkingURI = new Uri(string.Format(SoapOrgUriFormat, _targetInstanceUriToConnectTo.Scheme, _targetInstanceUriToConnectTo.DnsSafeHost)); + Uri OrgWorkingURI = null; + if (!IsOnPrem || _eAuthType == AuthenticationType.OAuth) // Use this even if its onPrem, when auth type == oauth. + { + OrgWorkingURI = new Uri(string.Format(SoapOrgUriFormat, _targetInstanceUriToConnectTo.Scheme, _targetInstanceUriToConnectTo.DnsSafeHost)); + } + else + { + OrgWorkingURI = new Uri(string.Format(SoapOrgUriFormat, _targetInstanceUriToConnectTo.Scheme, $"{_targetInstanceUriToConnectTo.DnsSafeHost}/{_organization}")); + } _targetInstanceUriToConnectTo = OrgWorkingURI; logEntry.Log(string.Format(CultureInfo.InvariantCulture, "Attempting to Connect to Uri {0}", _targetInstanceUriToConnectTo.ToString()), TraceEventType.Information); @@ -1306,7 +1418,71 @@ private async Task DoDirectLoginAsync() orgDetail.OrgDetail = new OrganizationDetail(); orgDetail.OrgDetail.Endpoints[EndpointType.OrganizationService] = _targetInstanceUriToConnectTo.ToString(); - dvService = await ConnectAndInitServiceAsync(orgDetail.OrgDetail, false, null).ConfigureAwait(false); + if (!IsOnPrem) + { + dvService = await ConnectAndInitServiceAsync(orgDetail.OrgDetail, false, null).ConfigureAwait(false); + } + else + { + _UserClientCred = new ClientCredentials(); + Uri uUserHomeRealm = null; + + if (_eAuthType == AuthenticationType.OAuth) + { + // oAuthBased. + if (_LivePass != null ) + _UserClientCred.UserName.Password = _LivePass.ToUnsecureString(); + _UserClientCred.UserName.UserName = _LiveID; + } + // You cannot use Default config for anything other then AD or IFD Setting's in the Auth Realm. + else + { + if (//_eAuthType == AuthenticationType.IFD || + _eAuthType == AuthenticationType.AD) + { + //IFD or AD ... + // Credentials support Default Credentials... + if (_AccessCred == null) + _AccessCred = System.Net.CredentialCache.DefaultNetworkCredentials; // Use default creds.. + else + _UserClientCred = GetClientCredentials(_AccessCred); + } + else + { + // CLAIMS FOR THE MOMENT + + //// Credentials Required. + //if (!string.IsNullOrWhiteSpace(_HomeRealmUrl)) + //{ + // if (!Uri.IsWellFormedUriString(_HomeRealmUrl, UriKind.RelativeOrAbsolute)) + // { + // logEntry.Log($"HomeRealm URL is bad : URL = {_HomeRealmUrl}", TraceEventType.Error); + // return null; + // } + // uUserHomeRealm = new Uri(_HomeRealmUrl); + + // logEntry.Log(string.Format(CultureInfo.InvariantCulture, "HomeRealm is = {0}", uUserHomeRealm.ToString()), TraceEventType.Information); + //} + //else + // uUserHomeRealm = null; + + //_DeviceCredentials = new ClientCredentials(); + //string userName = _ClaimsuserId.Split('@')[0]; + + //_DeviceCredentials.UserName.UserName = _ClaimsuserId; + //_DeviceCredentials.UserName.Password = _Claimspassword.ToUnsecureString(); + + //if (_UserClientCred == null) + // _UserClientCred = new ClientCredentials(); + + //_UserClientCred.UserName.UserName = userName; + //_UserClientCred.UserName.Password = _Claimspassword.ToUnsecureString(); + } + } + dvService = await ConnectAndInitServiceAsync(orgDetail.OrgDetail, true, uUserHomeRealm).ConfigureAwait(false); + } + + if (dvService != null) { await RefreshInstanceDetails(dvService, _targetInstanceUriToConnectTo).ConfigureAwait(false); @@ -2803,7 +2979,26 @@ private async Task ConnectAndInitServiceAsync(Organization } catch { }; + // uses the R7 added connection models to support faster and more precise control of Auth. + bool useR7Connect = true; + if (IsOnPrem) + { + try + { + if (!string.IsNullOrWhiteSpace(orgdata.OrganizationVersion)) + { + + if (OrganizationVersion != null) + if (OrganizationVersion.Major >= 5 && OrganizationVersion.Build > 9688) + useR7Connect = true; + } + } + catch { } // Do nothing here... if this fails, then the version number From CRM didn't return correctly or was not present. Use IServiceManagement + } + + OrganizationServiceProxyAsync proxy = null; OrganizationWebProxyClientAsync svcWebClientProxy = null; + if (_eAuthType == AuthenticationType.OAuth || _eAuthType == AuthenticationType.Certificate || _eAuthType == AuthenticationType.ExternalTokenManagement @@ -2856,11 +3051,82 @@ private async Task ConnectAndInitServiceAsync(Organization svcWebClientProxy.Endpoint.Binding.ReceiveTimeout = _MaxConnectionTimeout; } } + else if (useR7Connect) + { + // R7Connect flow + logEntry.Log("ConnectAndInitServiceAsync - OnPREM - Using ISerivceManagement"); + //var targetServiceUrl = AuthProcessor.GetUriBuilderWithVersion(_ActualDataverseOrgUri).Uri; + //svcWebClientProxy = new OrganizationWebProxyClientAsync(targetServiceUrl, true, _UserClientCred.Windows.ClientCredential); + //svcWebClientProxy.ClientCredentials.Windows.ClientCredential = new NetworkCredential(_UserClientCred.Windows.ClientCredential.UserName, _UserClientCred.Windows.ClientCredential.SecurePassword, _UserClientCred.Windows.ClientCredential.Domain); + //AttachWebProxyHander(svcWebClientProxy); + + Xrm.Sdk.Client.IServiceManagement organizationSvcConfig = null; + object objectRlst = OnPremises_Auth.CreateAndAuthenticateProxy(organizationSvcConfig, _ActualDataverseOrgUri, homeRealmUri, _UserClientCred, "ConnectAndInitServiceAsync - OnPREM - ", MaxConnectionTimeout, logEntry); + if (objectRlst != null && objectRlst is OrganizationServiceProxyAsync) + proxy = (OrganizationServiceProxyAsync)objectRlst; + + if (proxy != null) + AttachProxyHander(proxy); + } + else + { + logEntry.Log("ConnectAndInitCrmOrgService - Using ISerivceConfig"); + try + { + if (IsOnPrem) + { + if (homeRealmUri != null) + { + logEntry.Log("Connecting to CRM via Claims", TraceEventType.Information); + + if (homeRealmUri.Host.Contains("blank")) + proxy = new OrganizationServiceProxyAsync(_ActualDataverseOrgUri, null, _UserClientCred, null); + else + proxy = new OrganizationServiceProxyAsync(_ActualDataverseOrgUri, homeRealmUri, _UserClientCred, null); + } + else + { + // Create the CRM Service Connection. + logEntry.Log("Connecting to CRM via AD or IFD", TraceEventType.Information); + proxy = new OrganizationServiceProxyAsync(_ActualDataverseOrgUri, null, GetClientCredentials(_AccessCred), null); + } + } + else + { + //TODO:// RE-REMOVE THIS CODE. + + // Connecting to Online via Live. + if (string.IsNullOrWhiteSpace(_LiveID)) + { + // Error here .. Cannot use Default with Online. + proxy = null; + } + else + { + logEntry.Log("Connecting to CRM via Live", TraceEventType.Information); + proxy = new OrganizationServiceProxyAsync(_ActualDataverseOrgUri, null, _UserClientCred, null); + } + } + if (proxy != null) + AttachProxyHander(proxy); + } + catch + { + if (proxy != null) + proxy.Dispose(); + + throw; + } + } + logDt.Stop(); logEntry.Log(string.Format(CultureInfo.InvariantCulture, "ConnectAndInitService - Proxy created, total elapsed time: {0}", logDt.Elapsed.ToString())); - return svcWebClientProxy; + if (svcWebClientProxy != null) + return svcWebClientProxy; + else + return proxy; // OnPrem } /// @@ -2872,6 +3138,19 @@ internal void AttachWebProxyHander(OrganizationWebProxyClientAsync proxy) proxy.ChannelFactory.Opening += WebProxyChannelFactory_Opening; } + /// + /// This method us used to wire up the telemetry behaviors to the onPrem connection + /// + /// Connection proxy to attach telemetry too + internal void AttachProxyHander(OrganizationServiceProxyAsync proxy) + { + if (proxy.ServiceConfiguration != null && !proxy.ServiceConfiguration.CurrentServiceEndpoint.EndpointBehaviors.Contains(typeof(Xrm.Sdk.Client.ProxyTypesBehavior))) + proxy.ServiceConfiguration.CurrentServiceEndpoint.EndpointBehaviors.Add(new Xrm.Sdk.Client.ProxyTypesBehavior()); + + if (proxy.ServiceConfiguration != null) + proxy.ServiceConfiguration.CurrentServiceEndpoint.EndpointBehaviors.Add(new DataverseTelemetryBehaviors(this)); + } + /// /// Grab the Channel factory Open event and add the Telemetry behaviors. diff --git a/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/OrganizationServiceConfigurationAsync.cs b/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/OrganizationServiceConfigurationAsync.cs new file mode 100644 index 0000000..19145f8 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/OrganizationServiceConfigurationAsync.cs @@ -0,0 +1,321 @@ +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Client; +using Microsoft.Xrm.Sdk.Common; +using System; +using System.IdentityModel.Tokens; +using System.Net; +using System.Reflection; +using System.ServiceModel; +using System.ServiceModel.Description; +using System.Text; + +namespace Microsoft.PowerPlatform.Dataverse.Client.Connector.OnPremises +{ + internal sealed class OrganizationServiceConfigurationAsync : IServiceConfiguration, + IWebAuthentication, + IServiceManagement, + IEndpointSwitch + { + private const string XrmServicesRoot = "xrmservices/"; + private ServiceConfiguration service; + + private OrganizationServiceConfigurationAsync() + { + } + + internal OrganizationServiceConfigurationAsync(Uri serviceUri) + : this(serviceUri, false, null) + { + } + + internal OrganizationServiceConfigurationAsync(Uri serviceUri, bool enableProxyTypes, Assembly assembly) + { + try + { + service = new ServiceConfiguration(serviceUri, false); + if (enableProxyTypes && assembly != null) + { + EnableProxyTypes(assembly); + } + else if (enableProxyTypes) + { + EnableProxyTypes(); + } + } + catch (InvalidOperationException ioexp) + { + var rethrow = true; + var wexp = ioexp.InnerException as WebException; + if (wexp != null) + { + var response = wexp.Response as HttpWebResponse; + if (response != null && response.StatusCode == HttpStatusCode.Unauthorized) + { + rethrow = !AdjustServiceEndpoint(serviceUri); + } + } + + if (rethrow) + { + throw; + } + } + } + + /// + /// This method will enable support for the default strong proxy types. + /// + /// If you are using a shared Service Configuration instance, you must be careful if using + /// + public void EnableProxyTypes() + { + ClientExceptionHelper.ThrowIfNull(this.CurrentServiceEndpoint, "CurrentServiceEndpoint"); + + lock (_lockObject) + { + ProxyTypesBehavior behavior = CurrentServiceEndpoint.FindBehavior(); + if (behavior != null) + { + // Since we have no way of know if the assembly is different, always remove the old one. + CurrentServiceEndpoint.RemoveBehavior(behavior); + } + + CurrentServiceEndpoint.AddBehavior(new ProxyTypesBehavior()); + } + } + + /// + /// This method will enable support for the strong proxy types exposed in the passed assembly. + /// The assembly that will provide support for the desired strong types in the proxy. + /// + public void EnableProxyTypes(Assembly assembly) + { + ClientExceptionHelper.ThrowIfNull(assembly, "assembly"); + + ClientExceptionHelper.ThrowIfNull(this.CurrentServiceEndpoint, "CurrentServiceEndpoint"); + + lock (_lockObject) + { + ProxyTypesBehavior behavior = CurrentServiceEndpoint.FindBehavior(); + if (behavior != null) + { + // Since we have no way of know if the assembly is different, always remove the old one. + CurrentServiceEndpoint.RemoveBehavior(behavior); + } + + CurrentServiceEndpoint.AddBehavior(new ProxyTypesBehavior(assembly)); + } + } + + private object _lockObject = new object(); + #region IServiceConfiguration Members + + public ServiceEndpoint CurrentServiceEndpoint + { + get { return service.CurrentServiceEndpoint; } + set { service.CurrentServiceEndpoint = value; } + } + + public IssuerEndpoint CurrentIssuer + { + get { return service.CurrentIssuer; } + set { service.CurrentIssuer = value; } + } + + public AuthenticationProviderType AuthenticationType + { + get { return service.AuthenticationType; } + } + + public ServiceEndpointDictionary ServiceEndpoints + { + get { return service.ServiceEndpoints; } + } + + public IssuerEndpointDictionary IssuerEndpoints + { + get { return service.IssuerEndpoints; } + } + + public CrossRealmIssuerEndpointCollection CrossRealmIssuerEndpoints + { + get { return service.CrossRealmIssuerEndpoints; } + } + + public ChannelFactory CreateChannelFactory() + { + return service.CreateChannelFactory(ClientAuthenticationType.Kerberos); + } + + public ChannelFactory CreateChannelFactory(ClientAuthenticationType clientAuthenticationType) + { + return service.CreateChannelFactory(clientAuthenticationType); + } + + public ChannelFactory CreateChannelFactory(TokenServiceCredentialType endpointType) + { + return service.CreateChannelFactory(endpointType); + } + + public ChannelFactory CreateChannelFactory(ClientCredentials clientCredentials) + { + return service.CreateChannelFactory(clientCredentials); + } + + public SecurityTokenResponse Authenticate(ClientCredentials clientCredentials) + { + return service.Authenticate(clientCredentials); + } + + public SecurityTokenResponse Authenticate(SecurityToken securityToken) + { + return service.Authenticate(securityToken); + } + + public SecurityTokenResponse Authenticate(ClientCredentials clientCredentials, SecurityTokenResponse deviceSecurityTokenResponse) + { + throw new InvalidOperationException("Authentication to MSA services is not supported."); + } + + public SecurityTokenResponse AuthenticateDevice(ClientCredentials clientCredentials) + { + throw new InvalidOperationException("Authentication to MSA services is not supported."); + } + + public SecurityTokenResponse AuthenticateCrossRealm(ClientCredentials clientCredentials, string appliesTo, Uri crossRealmSts) + { + return service.AuthenticateCrossRealm(clientCredentials, appliesTo, crossRealmSts); + } + + public SecurityTokenResponse AuthenticateCrossRealm(SecurityToken securityToken, string appliesTo, Uri crossRealmSts) + { + return service.AuthenticateCrossRealm(securityToken, appliesTo, crossRealmSts); + } + + public PolicyConfiguration PolicyConfiguration + { + get { return service.PolicyConfiguration; } + } + + #endregion + + public IdentityProvider GetIdentityProvider(string userPrincipalName) + { + return service.GetIdentityProvider(userPrincipalName); + } + + #region Implementation of IWebAuthentication + + public SecurityTokenResponse Authenticate(ClientCredentials clientCredentials, Uri uri, string keyType) + { + return service.Authenticate(clientCredentials, uri, keyType); + } + + public SecurityTokenResponse Authenticate(SecurityToken securityToken, Uri uri, string keyType) + { + return service.Authenticate(securityToken, uri, keyType); + } + + #endregion + + private bool AdjustServiceEndpoint(Uri serviceUri) + { + // Try to get the non org-based service info and adjust it. + // This is most likely because we are requesting the org-based url in AD mode, in which case, the server won't let us access it. + var newServiceUri = RemoveOrgName(serviceUri); + if (newServiceUri != null) + { + // Don't try to catch the exception this time. Just let it go. + service = new ServiceConfiguration(newServiceUri); + if (service != null && service.ServiceEndpoints != null) + { + foreach (var endpointKey in service.ServiceEndpoints) + { + ServiceMetadataUtility.ReplaceEndpointAddress(endpointKey.Value, serviceUri); + } + + return true; + } + } + + return false; + } + + private static Uri RemoveOrgName(Uri serviceUri) + { + if (!serviceUri.AbsolutePath.StartsWith("/" + XrmServicesRoot, StringComparison.OrdinalIgnoreCase)) + { + // We're accessing the org url. + var pathBuilder = new StringBuilder(); + + for (int i = 2; i < serviceUri.Segments.Length; i++) + { + pathBuilder.Append(serviceUri.Segments[i]); + } + + if (pathBuilder.Length > 0) + { + var builder = new UriBuilder(serviceUri.GetComponents(UriComponents.SchemeAndServer, UriFormat.UriEscaped)); + builder.Path = pathBuilder.ToString(); + serviceUri = builder.Uri; + return serviceUri; + } + } + + return null; + } + + public AuthenticationCredentials Authenticate(AuthenticationCredentials authenticationCredentials) + { + return service.Authenticate(authenticationCredentials); + } + + public bool EndpointAutoSwitchEnabled + { + get { return service.EndpointAutoSwitchEnabled; } + set { service.EndpointAutoSwitchEnabled = value; } + } + + public Uri AlternateEndpoint + { + get { return service.AlternateEndpoint; } + } + + public Uri PrimaryEndpoint + { + get { return service.PrimaryEndpoint; } + } + + public void SwitchEndpoint() + { + service.SwitchEndpoint(); + } + + public event EventHandler EndpointSwitched + { + add { service.EndpointSwitched += value; } + remove { service.EndpointSwitched -= value; } + } + + public event EventHandler EndpointSwitchRequired + { + add { service.EndpointSwitchRequired += value; } + remove { service.EndpointSwitchRequired -= value; } + } + + public bool HandleEndpointSwitch() + { + return service.HandleEndpointSwitch(); + } + + public bool IsPrimaryEndpoint + { + get { return service.IsPrimaryEndpoint; } + } + + public bool CanSwitch(Uri currentUri) + { + return service.CanSwitch(currentUri); + } + } +} diff --git a/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/OrganizationServiceProxyAsync.cs b/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/OrganizationServiceProxyAsync.cs new file mode 100644 index 0000000..88900ed --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/OrganizationServiceProxyAsync.cs @@ -0,0 +1,852 @@ +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Client; +using Microsoft.Xrm.Sdk.Query; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Security.Permissions; +using System.ServiceModel; +using System.ServiceModel.Description; +using System.ServiceModel.Security; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PowerPlatform.Dataverse.Client.Connector.OnPremises +{ + /// + /// helper class that manages a ChannelFactory and serves up channels for sdk client use + /// For internal use only + /// + internal class OrganizationServiceProxyAsync : ServiceProxy, IOrganizationServiceAsync + { + internal bool OfflinePlayback { get; set; } + + public Guid CallerId { get; set; } + + public UserType UserType { get; set; } + + public Guid CallerRegardingObjectId { get; set; } + + internal int LanguageCodeOverride { get; set; } + + public string SyncOperationType { get; set; } + + internal string ClientAppName { get; set; } + + internal string ClientAppVersion { get; set; } + + public string SdkClientVersion { get; set; } + + private static string _xrmSdkAssemblyFileVersion; + + internal OrganizationServiceProxyAsync() + { + } + + public OrganizationServiceProxyAsync(Uri uri, Uri homeRealmUri, ClientCredentials clientCredentials, ClientCredentials deviceCredentials) + : base(uri, homeRealmUri, clientCredentials, deviceCredentials) + { + } + + public OrganizationServiceProxyAsync(IServiceConfiguration serviceConfiguration, SecurityTokenResponse securityTokenResponse) + : base(serviceConfiguration, securityTokenResponse) + { + } + + public OrganizationServiceProxyAsync(IServiceConfiguration serviceConfiguration, ClientCredentials clientCredentials) + : base(serviceConfiguration, clientCredentials) + { + } + + public OrganizationServiceProxyAsync(IServiceManagement serviceManagement, SecurityTokenResponse securityTokenResponse) + : this(serviceManagement as IServiceConfiguration, securityTokenResponse) + { + } + + public OrganizationServiceProxyAsync(IServiceManagement serviceManagement, ClientCredentials clientCredentials) + : this(serviceManagement as IServiceConfiguration, clientCredentials) + { + } + + #region Public Members + + /// + /// This method will enable support for the default strong proxy types. + /// + /// If you are using a shared Service Configuration instance, you must be careful if using + /// + public void EnableProxyTypes() + { + ClientExceptionHelper.ThrowIfNull(this.ServiceConfiguration, "ServiceConfiguration"); + OrganizationServiceConfigurationAsync orgConfig = ServiceConfiguration as OrganizationServiceConfigurationAsync; + ClientExceptionHelper.ThrowIfNull(orgConfig, "orgConfig"); + + orgConfig.EnableProxyTypes(); + } + + /// + /// This method will enable support for the strong proxy types exposed in the passed assembly. + /// The assembly that will provide support for the desired strong types in the proxy. + /// + public void EnableProxyTypes(Assembly assembly) + { + ClientExceptionHelper.ThrowIfNull(assembly, "assembly"); + + ClientExceptionHelper.ThrowIfNull(this.ServiceConfiguration, "ServiceConfiguration"); + OrganizationServiceConfigurationAsync orgConfig = ServiceConfiguration as OrganizationServiceConfigurationAsync; + ClientExceptionHelper.ThrowIfNull(orgConfig, "orgConfig"); + + orgConfig.EnableProxyTypes(assembly); + } + + /// + /// Get's the file version of the Xrm Sdk assembly that is loaded in the current client domain. + /// For Sdk clients called via the OrganizationServiceProxy this is the version of the local Microsoft.Xrm.Sdk dll used by the Client App. + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2143:TransparentMethodsShouldNotDemandFxCopRule")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2141:TransparentMethodsMustNotSatisfyLinkDemandsFxCopRule")] + [PermissionSet(SecurityAction.Demand, Unrestricted = true)] + internal static string GetXrmSdkAssemblyFileVersion() + { + if (string.IsNullOrEmpty(_xrmSdkAssemblyFileVersion)) + { + string[] assembliesToCheck = new string[] { "Microsoft.Xrm.Sdk.dll" }; + Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); + + foreach (var assemblyToCheck in assembliesToCheck) + { + foreach (Assembly assembly in assemblies) + { + if (assembly.ManifestModule.Name.Equals(assemblyToCheck, StringComparison.OrdinalIgnoreCase) && + !string.IsNullOrEmpty(assembly.Location) && + System.IO.File.Exists(assembly.Location)) + { + _xrmSdkAssemblyFileVersion = FileVersionInfo.GetVersionInfo(assembly.Location).FileVersion; + break; + } + } + } + } + + // If the assembly is embedded as resource and loaded from memory, there is no physical file on disk to check for file version + if (string.IsNullOrEmpty(_xrmSdkAssemblyFileVersion)) + { + _xrmSdkAssemblyFileVersion = "9.1.2.3"; + } + + return _xrmSdkAssemblyFileVersion; + } + + #endregion Public Members + + #region Protected Members + protected internal virtual Guid CreateCore(Entity entity) + { + bool? retry = null; + do + { + bool forceCloseChannel = false; + try + { + using (new OrganizationServiceProxyContextAsyncInitializer(this)) + { + return ServiceChannel.Channel.Create(entity); + } + } + catch (MessageSecurityException messageSecurityException) + { + forceCloseChannel = true; + + retry = ShouldRetry(messageSecurityException, retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch (EndpointNotFoundException) + { + forceCloseChannel = true; + retry = HandleFailover(retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch (TimeoutException) + { + forceCloseChannel = true; + retry = HandleFailover(retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch (FaultException fault) + { + forceCloseChannel = true; + retry = HandleFailover(fault.Detail, retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch + { + forceCloseChannel = true; + throw; + } + finally + { + CloseChannel(forceCloseChannel); + } + } + while (retry.HasValue && retry.Value); + return Guid.Empty; + } + + protected internal virtual async Task CreateAsyncCore(Entity entity) + { + return await ExecuteOperation(async () => { await ServiceChannel.Channel.CreateAsync(entity).ConfigureAwait(false); }); + } + + protected internal virtual Entity RetrieveCore(string entityName, Guid id, ColumnSet columnSet) + { + bool? retry = null; + do + { + bool forceCloseChannel = false; + try + { + using (new OrganizationServiceProxyContextAsyncInitializer(this)) + { + return ServiceChannel.Channel.Retrieve(entityName, id, columnSet); + } + } + catch (MessageSecurityException messageSecurityException) + { + forceCloseChannel = true; + + retry = ShouldRetry(messageSecurityException, retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch (EndpointNotFoundException) + { + forceCloseChannel = true; + retry = HandleFailover(retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch (TimeoutException) + { + forceCloseChannel = true; + retry = HandleFailover(retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch (FaultException fault) + { + forceCloseChannel = true; + retry = HandleFailover(fault.Detail, retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch + { + forceCloseChannel = true; + throw; + } + finally + { + CloseChannel(forceCloseChannel); + } + } + while (retry.HasValue && retry.Value); + return null; + } + + protected internal virtual async Task RetrieveAsyncCore(string entityName, Guid id, ColumnSet columnSet) + { + return await ExecuteOperation(async () => { await ServiceChannel.Channel.RetrieveAsync(entityName, id, columnSet).ConfigureAwait(false); }); + } + + protected internal virtual void UpdateCore(Entity entity) + { + bool? retry = null; + do + { + bool forceCloseChannel = false; + try + { + using (new OrganizationServiceProxyContextAsyncInitializer(this)) + { + ServiceChannel.Channel.Update(entity); + } + + return; // CRM SE 33359: Return so retry being true won't cause an infinite loop + } + catch (MessageSecurityException messageSecurityException) + { + forceCloseChannel = true; + + retry = ShouldRetry(messageSecurityException, retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch (EndpointNotFoundException) + { + forceCloseChannel = true; + retry = HandleFailover(retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch (TimeoutException) + { + forceCloseChannel = true; + retry = HandleFailover(retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch (FaultException fault) + { + forceCloseChannel = true; + retry = HandleFailover(fault.Detail, retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch + { + forceCloseChannel = true; + throw; + } + finally + { + CloseChannel(forceCloseChannel); + } + } + while (retry.HasValue && retry.Value); + } + + protected internal virtual async Task UpdateAsyncCore(Entity entity) + { + _ = await ExecuteOperation(async () => { await ServiceChannel.Channel.UpdateAsync(entity).ConfigureAwait(false); }); + } + + protected internal virtual void DeleteCore(string entityName, Guid id) + { + bool? retry = null; + do + { + bool forceCloseChannel = false; + try + { + using (new OrganizationServiceProxyContextAsyncInitializer(this)) + { + ServiceChannel.Channel.Delete(entityName, id); + } + + return; // CRM SE 33359: Return so retry being true won't cause an infinite loop + } + catch (MessageSecurityException messageSecurityException) + { + forceCloseChannel = true; + + retry = ShouldRetry(messageSecurityException, retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch (EndpointNotFoundException) + { + forceCloseChannel = true; + retry = HandleFailover(retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch (TimeoutException) + { + forceCloseChannel = true; + retry = HandleFailover(retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch (FaultException fault) + { + forceCloseChannel = true; + retry = HandleFailover(fault.Detail, retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch + { + forceCloseChannel = true; + throw; + } + finally + { + CloseChannel(forceCloseChannel); + } + } + while (retry.HasValue && retry.Value); + } + + protected internal virtual async Task DeleteAsyncCore(string entityName, Guid id) + { + _ = await ExecuteOperation(async () => { await ServiceChannel.Channel.DeleteAsync(entityName, id).ConfigureAwait(false); }); + } + + protected internal virtual OrganizationResponse ExecuteCore(OrganizationRequest request) + { + bool? retry = null; + do + { + bool forceCloseChannel = false; + try + { + using (new OrganizationServiceProxyContextAsyncInitializer(this)) + { + return ServiceChannel.Channel.Execute(request); + } + } + catch (MessageSecurityException messageSecurityException) + { + forceCloseChannel = true; + + retry = ShouldRetry(messageSecurityException, retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch (EndpointNotFoundException) + { + forceCloseChannel = true; + retry = HandleFailover(retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch (TimeoutException) + { + forceCloseChannel = true; + retry = HandleFailover(retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch (FaultException fault) + { + forceCloseChannel = true; + retry = HandleFailover(fault.Detail, retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch + { + forceCloseChannel = true; + throw; + } + finally + { + CloseChannel(forceCloseChannel); + } + } + while (retry.HasValue && retry.Value); + return null; + } + + protected internal virtual async Task ExecuteAsyncCore(OrganizationRequest request) + { + return await ExecuteOperation(async () => { await ServiceChannel.Channel.ExecuteAsync(request).ConfigureAwait(false); }); + } + + protected internal virtual void AssociateCore(string entityName, Guid entityId, Relationship relationship, EntityReferenceCollection relatedEntities) + { + bool? retry = null; + do + { + bool forceCloseChannel = false; + try + { + using (new OrganizationServiceProxyContextAsyncInitializer(this)) + { + ServiceChannel.Channel.Associate(entityName, entityId, relationship, relatedEntities); + } + + return; // CRM SE 33359: Return so retry being true won't cause an infinite loop + } + catch (MessageSecurityException messageSecurityException) + { + forceCloseChannel = true; + + retry = ShouldRetry(messageSecurityException, retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch (EndpointNotFoundException) + { + forceCloseChannel = true; + retry = HandleFailover(retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch (TimeoutException) + { + forceCloseChannel = true; + retry = HandleFailover(retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch (FaultException fault) + { + forceCloseChannel = true; + retry = HandleFailover(fault.Detail, retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch + { + forceCloseChannel = true; + throw; + } + finally + { + CloseChannel(forceCloseChannel); + } + } + while (retry.HasValue && retry.Value); + } + + protected internal virtual async Task AssociateAsyncCore(string entityName, Guid entityId, Relationship relationship, EntityReferenceCollection relatedEntities) + { + await ExecuteOperation(async () => { await ServiceChannel.Channel.AssociateAsync(entityName, entityId, relationship, relatedEntities).ConfigureAwait(false); }); + } + + protected internal virtual void DisassociateCore(string entityName, Guid entityId, Relationship relationship, EntityReferenceCollection relatedEntities) + { + bool? retry = null; + do + { + bool forceCloseChannel = false; + try + { + using (new OrganizationServiceProxyContextAsyncInitializer(this)) + { + ServiceChannel.Channel.Disassociate(entityName, entityId, relationship, relatedEntities); + } + + return; // CRM SE 33359: Return so retry being true won't cause an infinite loop + } + catch (MessageSecurityException messageSecurityException) + { + forceCloseChannel = true; + + retry = ShouldRetry(messageSecurityException, retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch (EndpointNotFoundException) + { + forceCloseChannel = true; + retry = HandleFailover(retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch (TimeoutException) + { + forceCloseChannel = true; + retry = HandleFailover(retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch (FaultException fault) + { + forceCloseChannel = true; + retry = HandleFailover(fault.Detail, retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch + { + forceCloseChannel = true; + throw; + } + finally + { + CloseChannel(forceCloseChannel); + } + } + while (retry.HasValue && retry.Value); + } + + protected internal virtual async Task DisassociateAsyncCore(string entityName, Guid entityId, Relationship relationship, EntityReferenceCollection relatedEntities) + { + await ExecuteOperation(async () => { await ServiceChannel.Channel.DisassociateAsync(entityName, entityId, relationship, relatedEntities).ConfigureAwait(false); }); + } + + protected internal virtual EntityCollection RetrieveMultipleCore(QueryBase query) + { + bool? retry = null; + do + { + bool forceCloseChannel = false; + try + { + using (new OrganizationServiceProxyContextAsyncInitializer(this)) + { + return ServiceChannel.Channel.RetrieveMultiple(query); + } + } + catch (MessageSecurityException messageSecurityException) + { + forceCloseChannel = true; + + retry = ShouldRetry(messageSecurityException, retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch (EndpointNotFoundException) + { + forceCloseChannel = true; + retry = HandleFailover(retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch (TimeoutException) + { + forceCloseChannel = true; + retry = HandleFailover(retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch (FaultException fault) + { + forceCloseChannel = true; + retry = HandleFailover(fault.Detail, retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch + { + forceCloseChannel = true; + throw; + } + finally + { + CloseChannel(forceCloseChannel); + } + } + while (retry.HasValue && retry.Value); + return null; + } + + protected internal virtual async Task RetrieveMultipleAsyncCore(QueryBase query) + { + return await ExecuteOperation(async () => { await ServiceChannel.Channel.RetrieveMultipleAsync(query).ConfigureAwait(false); }); + } + + protected async internal Task ExecuteOperation(Func asyncAction) + { + bool? retry = null; + do + { + bool forceCloseChannel = false; + try + { + using (new OrganizationServiceProxyContextAsyncInitializer(this)) + { + await asyncAction().ConfigureAwait(continueOnCapturedContext: false); ; + } + } + catch (MessageSecurityException messageSecurityException) + { + forceCloseChannel = true; + + retry = ShouldRetry(messageSecurityException, retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch (EndpointNotFoundException) + { + forceCloseChannel = true; + retry = HandleFailover(retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch (TimeoutException) + { + forceCloseChannel = true; + retry = HandleFailover(retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch (FaultException fault) + { + forceCloseChannel = true; + retry = HandleFailover(fault.Detail, retry); + if (!retry.GetValueOrDefault()) + { + throw; + } + } + catch + { + forceCloseChannel = true; + throw; + } + finally + { + CloseChannel(forceCloseChannel); + } + } + while (retry.HasValue && retry.Value); + return default; + } + + #endregion Protected Members + + #region IOrganizationService implementation + + public Guid Create(Entity entity) + { + return CreateCore(entity); + } + + public async Task CreateAsync(Entity entity) + { + return await CreateAsyncCore(entity); + } + + public Entity Retrieve(string entityName, Guid id, ColumnSet columnSet) + { + return RetrieveCore(entityName, id, columnSet); + } + public async Task RetrieveAsync(string entityName, Guid id, ColumnSet columnSet) + { + return await RetrieveAsyncCore(entityName, id, columnSet); + } + + public void Update(Entity entity) + { + UpdateCore(entity); + } + public async Task UpdateAsync(Entity entity) + { + await UpdateAsyncCore(entity); + } + + public void Delete(string entityName, Guid id) + { + DeleteCore(entityName, id); + } + + public async Task DeleteAsync(string entityName, Guid id) + { + await DeleteAsyncCore(entityName, id); + } + + + public OrganizationResponse Execute(OrganizationRequest request) + { + return ExecuteCore(request); + } + + public async Task ExecuteAsync(OrganizationRequest request) + { + return await ExecuteAsyncCore(request); + } + + public void Associate(string entityName, Guid entityId, Relationship relationship, EntityReferenceCollection relatedEntities) + { + AssociateCore(entityName, entityId, relationship, relatedEntities); + } + + public async Task AssociateAsync(string entityName, Guid entityId, Relationship relationship, EntityReferenceCollection relatedEntities) + { + await AssociateAsyncCore(entityName, entityId, relationship, relatedEntities); + } + + public void Disassociate(string entityName, Guid entityId, Relationship relationship, EntityReferenceCollection relatedEntities) + { + DisassociateCore(entityName, entityId, relationship, relatedEntities); + } + + public async Task DisassociateAsync(string entityName, Guid entityId, Relationship relationship, EntityReferenceCollection relatedEntities) + { + await DisassociateAsyncCore(entityName, entityId, relationship, relatedEntities); + } + + public EntityCollection RetrieveMultiple(QueryBase query) + { + return RetrieveMultipleCore(query); + } + + public async Task RetrieveMultipleAsync(QueryBase query) + { + return await RetrieveMultipleAsyncCore(query); + } + + #endregion + + } +} diff --git a/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/OrganizationServiceProxyContextAsyncInitializer.cs b/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/OrganizationServiceProxyContextAsyncInitializer.cs new file mode 100644 index 0000000..3ff82a2 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/OrganizationServiceProxyContextAsyncInitializer.cs @@ -0,0 +1,100 @@ +using System; +using Microsoft.Xrm.Sdk.Client; +using Microsoft.Xrm.Sdk.XmlNamespaces; +using System.ServiceModel; +using System.ServiceModel.Channels; + +namespace Microsoft.PowerPlatform.Dataverse.Client.Connector.OnPremises +{ + /// + /// Manages context for sdk calls + /// + internal sealed class OrganizationServiceProxyContextAsyncInitializer : ServiceContextInitializer + { + public OrganizationServiceProxyContextAsyncInitializer(OrganizationServiceProxyAsync proxy) + : base(proxy) + { + Initialize(); + } + + private OrganizationServiceProxyAsync OrganizationServiceProxyAsync + { + get { return ServiceProxy as OrganizationServiceProxyAsync; } + } + + private void Initialize() + { + if (OrganizationServiceProxyAsync != null) + { + if (OrganizationServiceProxyAsync.OfflinePlayback) + { + OperationContext.Current.OutgoingMessageHeaders.Add(MessageHeader.CreateHeader(SdkHeaders.IsOfflinePlayback, + V5.Contracts, true)); + } + + if (OrganizationServiceProxyAsync.CallerId != Guid.Empty) + { + OperationContext.Current.OutgoingMessageHeaders.Add(MessageHeader.CreateHeader(SdkHeaders.CallerId, + V5.Contracts, + OrganizationServiceProxyAsync.CallerId)); + } + + if (OrganizationServiceProxyAsync.CallerRegardingObjectId != Guid.Empty) + { + OperationContext.Current.OutgoingMessageHeaders.Add(MessageHeader.CreateHeader(SdkHeaders.CallerRegardingObjectId, + V5.Contracts, + OrganizationServiceProxyAsync.CallerRegardingObjectId)); + } + + if (OrganizationServiceProxyAsync.LanguageCodeOverride != 0) + { + OperationContext.Current.OutgoingMessageHeaders.Add(MessageHeader.CreateHeader(SdkHeaders.LanguageCodeOverride, + V5.Contracts, + OrganizationServiceProxyAsync.LanguageCodeOverride)); + } + + if (OrganizationServiceProxyAsync.SyncOperationType != null) + { + OperationContext.Current.OutgoingMessageHeaders.Add(MessageHeader.CreateHeader(SdkHeaders.OutlookSyncOperationType, + V5.Contracts, + OrganizationServiceProxyAsync.SyncOperationType)); + } + + if (!string.IsNullOrEmpty(OrganizationServiceProxyAsync.ClientAppName)) + { + OperationContext.Current.OutgoingMessageHeaders.Add(MessageHeader.CreateHeader(SdkHeaders.ClientAppName, + V5.Contracts, + OrganizationServiceProxyAsync.ClientAppName)); + } + + if (!string.IsNullOrEmpty(OrganizationServiceProxyAsync.ClientAppVersion)) + { + OperationContext.Current.OutgoingMessageHeaders.Add(MessageHeader.CreateHeader(SdkHeaders.ClientAppVersion, + V5.Contracts, + OrganizationServiceProxyAsync.ClientAppVersion)); + } + + if (!string.IsNullOrEmpty(OrganizationServiceProxyAsync.SdkClientVersion)) + { + OperationContext.Current.OutgoingMessageHeaders.Add(MessageHeader.CreateHeader(SdkHeaders.SdkClientVersion, + V5.Contracts, + OrganizationServiceProxyAsync.SdkClientVersion)); + } + else + { + string fileVersion = OrganizationServiceProxyAsync.GetXrmSdkAssemblyFileVersion(); + if (!string.IsNullOrEmpty(fileVersion)) + { + OperationContext.Current.OutgoingMessageHeaders.Add(MessageHeader.CreateHeader(SdkHeaders.SdkClientVersion, + V5.Contracts, + fileVersion)); + } + } + + OperationContext.Current.OutgoingMessageHeaders.Add(MessageHeader.CreateHeader(SdkHeaders.UserType, + V5.Contracts, + OrganizationServiceProxyAsync.UserType)); + } + } + } +} diff --git a/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/OrganizationServiceProxyExt.cs b/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/OrganizationServiceProxyExt.cs new file mode 100644 index 0000000..195f3a5 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/OrganizationServiceProxyExt.cs @@ -0,0 +1,85 @@ +using System; +using System.ServiceModel; +using System.ServiceModel.Description; +using Microsoft.Xrm.Sdk.Client; + +namespace Microsoft.PowerPlatform.Dataverse.Client.Connector.OnPremises +{ + /// + /// Extension to the Organization Service Proxy to allow for management of reAuthentication + /// Base class borrowed from the Plugin Registration tool, and modified for this class. + /// + internal sealed class ManagedTokenOrganizationServiceProxy : OrganizationServiceProxyAsync + { + /// + /// Support for AD + /// + /// + /// + public ManagedTokenOrganizationServiceProxy(IServiceManagement serviceManagement, ClientCredentials clientCredentials) + : base(serviceManagement, clientCredentials) + { + + } + + /// + /// Support for things other then AD. + /// + /// + /// + /// + public ManagedTokenOrganizationServiceProxy(IServiceManagement serviceManagement, SecurityTokenResponse securityTokenResponse, ClientCredentials clientCredentials) + : base(serviceManagement, clientCredentials) + { + // While this process is odd, it is functional and allows for the onboard ReAuthenticate system to work correctly + this.SecurityTokenResponse = securityTokenResponse; + } + + // + // Called when the device needs to be r + // + // + //protected override SecurityTokenResponse AuthenticateDeviceCore() + //{ + // if (_deviceCredentials == null) + // { + // _deviceCredentials = DeviceIdManager.LoadOrRegisterDevice( + // this.ServiceConfiguration.CurrentIssuer.IssuerAddress.Uri); + // } + // return ServiceConfiguration.AuthenticateDevice(this._deviceCredentials); + //} + + /// + /// Overrides the Authentication core process in the SDK Proxy.. + /// + protected override void AuthenticateCore() + { + base.AuthenticateCore(); + } + + /// + /// Determines if a ReAuthentication is required for this call. + /// + protected override void ValidateAuthentication() + { +#if NETFRAMEWORK + if (SecurityTokenResponse != null && + DateTime.UtcNow >= SecurityTokenResponse.Response.Lifetime.Expires) +#else + if (SecurityTokenResponse != null && + DateTime.UtcNow >= SecurityTokenResponse.Token.ValidTo) +#endif + { + try + { + Authenticate(); + } + catch (CommunicationException) + { + throw; + } + } + base.ValidateAuthentication(); + } + } +} diff --git a/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/ServiceConfiguration.cs b/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/ServiceConfiguration.cs new file mode 100644 index 0000000..497e469 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/ServiceConfiguration.cs @@ -0,0 +1,226 @@ +using System; +using System.IdentityModel.Protocols.WSTrust; +using System.Linq; +using System.ServiceModel.Description; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Client; + + +namespace Microsoft.PowerPlatform.Dataverse.Client.Connector.OnPremises +{ + internal sealed partial class ServiceConfiguration + { + #region IServiceManagement + public AuthenticationCredentials Authenticate(AuthenticationCredentials authenticationCredentials) + { + ClientExceptionHelper.ThrowIfNull(authenticationCredentials, "authenticationCredentials"); + + switch (AuthenticationType) + { + case AuthenticationProviderType.OnlineFederation: + return AuthenticateOnlineFederationInternal(authenticationCredentials); + case AuthenticationProviderType.Federation: + return AuthenticateFederationInternal(authenticationCredentials); + case AuthenticationProviderType.ActiveDirectory: + { + ServiceMetadataUtility.AdjustUserNameForWindows(authenticationCredentials.ClientCredentials); + return authenticationCredentials; + } + + default: + return authenticationCredentials; + } + } + + private AuthenticationCredentials AuthenticateFederationInternal(AuthenticationCredentials authenticationCredentials) + { + if (authenticationCredentials.SecurityTokenResponse != null) + { + return AuthenticateFederationTokenInternal(authenticationCredentials); + } + + if (authenticationCredentials.AppliesTo == null) + { + authenticationCredentials.AppliesTo = CurrentServiceEndpoint.Address.Uri; + } + + authenticationCredentials.EndpointType = GetCredentialsEndpointType(authenticationCredentials.ClientCredentials); + + authenticationCredentials.IssuerEndpoints = authenticationCredentials.HomeRealm != null ? CrossRealmIssuerEndpoints[authenticationCredentials.HomeRealm] : IssuerEndpoints; + authenticationCredentials.SecurityTokenResponse = AuthenticateInternal(authenticationCredentials); + + return authenticationCredentials; + } + + private AuthenticationCredentials AuthenticateFederationTokenInternal(AuthenticationCredentials authenticationCredentials) + { + AuthenticationCredentials returnCredentials = new AuthenticationCredentials(); + returnCredentials.SupportingCredentials = authenticationCredentials; + if (authenticationCredentials.AppliesTo == null) + { + authenticationCredentials.AppliesTo = CurrentServiceEndpoint.Address.Uri; + } + + authenticationCredentials.EndpointType = _tokenEndpointType; + authenticationCredentials.KeyType = string.Empty; + authenticationCredentials.IssuerEndpoints = IssuerEndpoints; + + returnCredentials.SecurityTokenResponse = AuthenticateInternal(authenticationCredentials); + + return returnCredentials; + } + + /// + /// Supported matrix: + /// 1. Security Token Response populated: We will submit the token to Org ID to exchange for a CRM token. + /// 2. Credentials passed. + /// a. The UserPrincipalName MUST be populated if the Username/Windows username is empty AND the Home Realm Uri is null. + /// a. If the Home Realm + /// + /// + /// + private AuthenticationCredentials AuthenticateOnlineFederationInternal(AuthenticationCredentials authenticationCredentials) + { + var onlinePolicy = PolicyConfiguration as OnlinePolicyConfiguration; + ClientExceptionHelper.ThrowIfNull(onlinePolicy, "onlinePolicy"); + + OrgIdentityProviderTrustConfiguration liveTrustConfig = onlinePolicy.OnlineProviders.Values.OfType().FirstOrDefault(); + ClientExceptionHelper.ThrowIfNull(liveTrustConfig, "liveTrustConfig"); + + // Two scenarios: + // 1. Managed Credentials + // 2. Federated Credentials + // 3. A token to submit to OrgID. + if (authenticationCredentials.SecurityTokenResponse != null) + { + return AuthenticateOnlineFederationTokenInternal(liveTrustConfig, authenticationCredentials); + } + + bool authWithOrgId = true; + + if (authenticationCredentials.HomeRealm == null) + { + IdentityProvider identityProvider = !string.IsNullOrEmpty(authenticationCredentials.UserPrincipalName) ? GetIdentityProvider(authenticationCredentials.UserPrincipalName) : GetIdentityProvider(authenticationCredentials.ClientCredentials); + ClientExceptionHelper.ThrowIfNull(identityProvider, "identityProvider"); + authenticationCredentials.HomeRealm = identityProvider.ServiceUrl; + authWithOrgId = identityProvider.IdentityProviderType == IdentityProviderType.OrgId; + if (authWithOrgId) + { + ClientExceptionHelper.Assert(onlinePolicy.OnlineProviders.ContainsKey(authenticationCredentials.HomeRealm), "Online Identity Provider NOT found! {0}", identityProvider.ServiceUrl); + } + } + + authenticationCredentials.AppliesTo = new Uri(liveTrustConfig.AppliesTo); + authenticationCredentials.IssuerEndpoints = this.IssuerEndpoints; + authenticationCredentials.KeyType = KeyTypes.Bearer; + authenticationCredentials.EndpointType = TokenServiceCredentialType.Username; + + if (authWithOrgId) + { + return AuthenticateTokenWithOrgIdForCrm(authenticationCredentials); + } + + // Authenticate with ADFS to retrieve a token for OrgId + AuthenticationCredentials adfsCredentials = AuthenticateWithADFSForOrgId(authenticationCredentials, liveTrustConfig.Identifier); + + return AuthenticateFederatedTokenWithOrgIdForCRM(adfsCredentials); + } + + /// + /// Authenticates a federated token with OrgID to retrieve a token for CRM + /// + /// + private AuthenticationCredentials AuthenticateFederatedTokenWithOrgIdForCRM(AuthenticationCredentials authenticationCredentials) + { + ClientExceptionHelper.ThrowIfNull(authenticationCredentials, "authenticationCredentials"); + + ClientExceptionHelper.ThrowIfNull(authenticationCredentials.SecurityTokenResponse, "authenticationCredentials.SecurityTokenResponse"); + + AuthenticationCredentials returnCredentials = new AuthenticationCredentials(); + returnCredentials.SupportingCredentials = authenticationCredentials; + returnCredentials.AppliesTo = authenticationCredentials.AppliesTo; + returnCredentials.IssuerEndpoints = authenticationCredentials.IssuerEndpoints; + returnCredentials.EndpointType = TokenServiceCredentialType.SymmetricToken; + returnCredentials.SecurityTokenResponse = AuthenticateInternal(returnCredentials); + return returnCredentials; + } + + /// + /// Authenticates with ADFS to retrieve a federated token to exchange with OrgId for CRM + /// + /// + /// + private AuthenticationCredentials AuthenticateWithADFSForOrgId(AuthenticationCredentials authenticationCredentials, Uri identifier) + { + AuthenticationCredentials returnCredentials = new AuthenticationCredentials(); + returnCredentials.AppliesTo = authenticationCredentials.AppliesTo; + returnCredentials.SupportingCredentials = authenticationCredentials; + returnCredentials.AppliesTo = authenticationCredentials.AppliesTo; + returnCredentials.IssuerEndpoints = authenticationCredentials.IssuerEndpoints; + returnCredentials.EndpointType = TokenServiceCredentialType.SymmetricToken; + + // We are authenticating against ADFS with the credentials. + authenticationCredentials.AppliesTo = identifier; + authenticationCredentials.KeyType = KeyTypes.Bearer; + authenticationCredentials.EndpointType = GetCredentialsEndpointType(authenticationCredentials.ClientCredentials); + authenticationCredentials.IssuerEndpoints = CrossRealmIssuerEndpoints[authenticationCredentials.HomeRealm]; + + returnCredentials.SecurityTokenResponse = AuthenticateInternal(authenticationCredentials); + + return returnCredentials; + } + + private AuthenticationCredentials AuthenticateTokenWithOrgIdForCrm(AuthenticationCredentials authenticationCredentials) + { + ClientExceptionHelper.ThrowIfNull(authenticationCredentials, "authenticationCredentials"); + + AuthenticationCredentials returnAcsCredentials = new AuthenticationCredentials(); + returnAcsCredentials.SupportingCredentials = authenticationCredentials; + returnAcsCredentials.AppliesTo = authenticationCredentials.AppliesTo; + returnAcsCredentials.IssuerEndpoints = authenticationCredentials.IssuerEndpoints; + returnAcsCredentials.KeyType = KeyTypes.Bearer; + returnAcsCredentials.EndpointType = TokenServiceCredentialType.Username; + + returnAcsCredentials.SecurityTokenResponse = AuthenticateInternal(authenticationCredentials); + return returnAcsCredentials; + } + + private AuthenticationCredentials AuthenticateOnlineFederationTokenInternal(IdentityProviderTrustConfiguration liveTrustConfig, AuthenticationCredentials authenticationCredentials) + { + AuthenticationCredentials returnCredentials = new AuthenticationCredentials(); + returnCredentials.SupportingCredentials = authenticationCredentials; + + string appliesTo = authenticationCredentials.AppliesTo != null ? authenticationCredentials.AppliesTo.AbsoluteUri : liveTrustConfig.AppliesTo; + Uri tokenEndpoint = authenticationCredentials.HomeRealm ?? liveTrustConfig.Endpoint.GetServiceRoot(); + + returnCredentials.SecurityTokenResponse = AuthenticateCrossRealm(authenticationCredentials.SecurityTokenResponse.Token, appliesTo, tokenEndpoint); + + return returnCredentials; + } + + #endregion IServiceManagement + + internal IdentityProvider GetIdentityProvider(ClientCredentials clientCredentials) + { + string userName = string.Empty; + + if (!string.IsNullOrWhiteSpace(clientCredentials.UserName.UserName)) + { + userName = ExtractUserName(clientCredentials.UserName.UserName); + } + else if (!string.IsNullOrWhiteSpace(clientCredentials.Windows.ClientCredential.UserName)) + { + userName = ExtractUserName(clientCredentials.Windows.ClientCredential.UserName); + } + + ClientExceptionHelper.Assert(!string.IsNullOrEmpty(userName), "clientCredentials.UserName.UserName or clientCredentials.Windows.ClientCredential.UserName MUST be populated!"); + return GetIdentityProvider(userName); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")] + private string ExtractUserName(string userName) + { + return userName.Contains('@') ? userName : string.Empty; + } + } +} diff --git a/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/ServiceConfiguration.partial.EndpointSwitch.cs b/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/ServiceConfiguration.partial.EndpointSwitch.cs new file mode 100644 index 0000000..5ee3d34 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/ServiceConfiguration.partial.EndpointSwitch.cs @@ -0,0 +1,186 @@ +using System; +using System.Globalization; +using System.ServiceModel; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Client; + +namespace Microsoft.PowerPlatform.Dataverse.Client.Connector.OnPremises +{ + internal sealed partial class ServiceConfiguration : IEndpointSwitch + { + public bool EndpointAutoSwitchEnabled { get; set; } + + public string GetAlternateEndpointAddress(string host) + { + var index = host.IndexOf('.'); + return host.Insert(index, "." + AlternateEndpointToken); + } + + public void OnEndpointSwitchRequiredEvent() + { + var tmp = EndpointSwitchRequired; + HandleEndpointEvent(tmp, (CurrentServiceEndpoint.Address.Uri == PrimaryEndpoint) ? AlternateEndpoint : PrimaryEndpoint, CurrentServiceEndpoint.Address.Uri); + } + + public void OnEndpointSwitchedEvent() + { + var tmp = EndpointSwitched; + HandleEndpointEvent(tmp, CurrentServiceEndpoint.Address.Uri, (CurrentServiceEndpoint.Address.Uri == PrimaryEndpoint) ? AlternateEndpoint : PrimaryEndpoint); + } + + private void HandleEndpointEvent(EventHandler tmp, Uri newUrl, Uri previousUrl) + { + if (tmp != null) + { + var args = new EndpointSwitchEventArgs(); + lock (_lockObject) + { + args.NewUrl = newUrl; + args.PreviousUrl = previousUrl; + } + + tmp(this, args); + } + } + + public event EventHandler EndpointSwitched; + + public event EventHandler EndpointSwitchRequired; + + public string AlternateEndpointToken { get; set; } + + public Uri AlternateEndpoint { get; internal set; } + + public Uri PrimaryEndpoint { get; internal set; } + + private void SetEndpointSwitchingBehavior() + { + if (ServiceEndpointMetadata.ServiceUrls == null) + { + return; + } + + PrimaryEndpoint = ServiceEndpointMetadata.ServiceUrls.PrimaryEndpoint; + + bool enableFailover = false; + bool endpointEnabled = true; + if (!ServiceEndpointMetadata.ServiceUrls.GeneratedFromAlternate) + { + var bindingElements = CurrentServiceEndpoint.Binding.CreateBindingElements(); + var xrmPolicy = bindingElements.Find(); + if (xrmPolicy != null) + { + if (xrmPolicy.PolicyElements.ContainsKey(FailoverPolicy.FailoverAvailable)) + { + enableFailover = Convert.ToBoolean(xrmPolicy.PolicyElements[FailoverPolicy.FailoverAvailable], CultureInfo.InvariantCulture); + endpointEnabled = Convert.ToBoolean(xrmPolicy.PolicyElements[FailoverPolicy.EndpointEnabled], CultureInfo.InvariantCulture); + } + } + } + else + { + enableFailover = true; + } + + if (enableFailover) + { + AlternateEndpoint = ServiceEndpointMetadata.ServiceUrls.AlternateEndpoint; + if (!endpointEnabled) + { + SwitchEndpoint(); + } + } + } + + public bool IsPrimaryEndpoint + { + get + { + lock (_lockObject) + { + return AlternateEndpoint == null || CurrentServiceEndpoint.Address.Uri != AlternateEndpoint; + } + } + } + + public bool CanSwitch(Uri currentUri) + { + ClientExceptionHelper.ThrowIfNull(currentUri, "currentUri"); + + lock (_lockObject) + { + return currentUri == CurrentServiceEndpoint.Address.Uri; + } + } + + public bool HandleEndpointSwitch() + { + if (AlternateEndpoint != null) + { + OnEndpointSwitchRequiredEvent(); + if (EndpointAutoSwitchEnabled) + { + SwitchEndpoint(); + return true; + } + } + + return false; + } + + public void SwitchEndpoint() + { + if (AlternateEndpoint == null) + { + return; + } + + lock (_lockObject) + { +#if NETFRAMEWORK + if (CurrentServiceEndpoint.Address.Uri != AlternateEndpoint) + { + // Switch to backup, otherwise do nothing. + CurrentServiceEndpoint.Address = new EndpointAddress(AlternateEndpoint, CurrentServiceEndpoint.Address.Identity, + CurrentServiceEndpoint.Address.Headers); + } + else + { + CurrentServiceEndpoint.Address = new EndpointAddress(PrimaryEndpoint, CurrentServiceEndpoint.Address.Identity, + CurrentServiceEndpoint.Address.Headers); + } +#else + throw new PlatformNotSupportedException("Xrm.Sdk WSDL"); +#endif + + //OnEndpointSwitchedEvent(); + } + } + + internal static ServiceUrls CalculateEndpoints(Uri serviceUri) + { + ServiceUrls endpoints = new ServiceUrls(); + var uBuilder = new UriBuilder(serviceUri); + var segments = uBuilder.Host.Split('.'); + if (segments[0].EndsWith("--s", StringComparison.OrdinalIgnoreCase)) + { + endpoints.AlternateEndpoint = uBuilder.Uri; + + // This is the secondary url + segments[0] = segments[0].Remove(segments[0].Length - 3); + uBuilder.Host = string.Join(".", segments); + endpoints.PrimaryEndpoint = uBuilder.Uri; + endpoints.GeneratedFromAlternate = true; + } + else + { + endpoints.PrimaryEndpoint = uBuilder.Uri; + segments[0] += "--s"; + uBuilder.Host = string.Join(".", segments); + endpoints.AlternateEndpoint = uBuilder.Uri; + } + + return endpoints; + } + } +} diff --git a/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/ServiceConfigurationFactoryAsync.cs b/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/ServiceConfigurationFactoryAsync.cs new file mode 100644 index 0000000..742e177 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/ServiceConfigurationFactoryAsync.cs @@ -0,0 +1,67 @@ +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Client; +using Microsoft.Xrm.Sdk.Discovery; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PowerPlatform.Dataverse.Client.Connector.OnPremises +{ + internal static class ServiceConfigurationFactoryAsync + { + public static IServiceConfiguration CreateConfiguration(Uri serviceUri) + { + return CreateConfiguration(serviceUri, false, null); + } + + public static IServiceConfiguration CreateConfiguration(Uri serviceUri, bool enableProxyTypes, Assembly assembly) + { + if (serviceUri != null) + { + if (typeof(TService) == typeof(Xrm.Sdk.Discovery.IDiscoveryService)) + { + return new DiscoveryServiceConfiguration(serviceUri) as IServiceConfiguration; + } + else if (typeof(TService) == typeof(Xrm.Sdk.IOrganizationService)) + { + return new OrganizationServiceConfiguration(serviceUri, enableProxyTypes, assembly) as IServiceConfiguration; + } + else if (typeof(TService) == typeof(IOrganizationServiceAsync)) + { + return new OrganizationServiceConfigurationAsync(serviceUri, enableProxyTypes, assembly) as IServiceConfiguration; + } + } + + return null; + } + + public static IServiceManagement CreateManagement(Uri serviceUri) + { + return CreateManagement(serviceUri, false, null); + } + + public static IServiceManagement CreateManagement(Uri serviceUri, bool enableProxyTypes, Assembly assembly) + { + if (serviceUri != null) + { + if (typeof(TService) == typeof(IDiscoveryService)) + { + return new DiscoveryServiceConfiguration(serviceUri) as IServiceManagement; + } + else if (typeof(TService) == typeof(IOrganizationService)) + { + return new OrganizationServiceConfiguration(serviceUri, enableProxyTypes, assembly) as IServiceManagement; + } + else if (typeof(TService) == typeof(IOrganizationServiceAsync)) + { + return new OrganizationServiceConfigurationAsync(serviceUri, enableProxyTypes, assembly) as IServiceManagement; + } + } + + return null; + } + } +} diff --git a/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/ServiceContextInit.cs b/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/ServiceContextInit.cs new file mode 100644 index 0000000..4ac765c --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/ServiceContextInit.cs @@ -0,0 +1,62 @@ +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Client; +using System; +using System.ServiceModel; + + +namespace Microsoft.PowerPlatform.Dataverse.Client.Connector.OnPremises +{ + internal abstract class ServiceContextInitializer : IDisposable + where TService : class + { + private OperationContextScope _operationScope; + + /// + /// Initializes a new instance of the class. + /// Constructs a context initializer + /// + /// sdk proxy + protected ServiceContextInitializer(ServiceProxy proxy) + { + ClientExceptionHelper.ThrowIfNull(proxy, "proxy"); + + ServiceProxy = proxy; + + Initialize(proxy); + } + + public ServiceProxy ServiceProxy { get; private set; } + + protected void Initialize(ServiceProxy proxy) + { + // This call initializes operation context scope for the call using the channel + _operationScope = new OperationContextScope((IContextChannel)proxy.ServiceChannel.Channel); + } + + #region IDisposable implementation + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + ~ServiceContextInitializer() + { + Dispose(false); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (_operationScope != null) + { + _operationScope.Dispose(); + } + } + } + + #endregion + } +} diff --git a/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/ServiceInformation.cs b/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/ServiceInformation.cs new file mode 100644 index 0000000..4b0a960 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/ServiceInformation.cs @@ -0,0 +1,881 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IdentityModel.Protocols.WSTrust; +using System.IdentityModel.Tokens; +using System.Linq; +using System.Security; +using System.Security.Permissions; +using System.ServiceModel; +using System.ServiceModel.Description; +using System.ServiceModel.Security; +using System.Text; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Client; +using Microsoft.Xrm.Sdk.Common; + +namespace Microsoft.PowerPlatform.Dataverse.Client.Connector.OnPremises +{ + [SecurityPermission(SecurityAction.Demand, Unrestricted = true)] + [SecuritySafeCritical] + internal sealed partial class ServiceConfiguration + { + private ServiceEndpoint currentServiceEndpoint; + + public PolicyConfiguration PolicyConfiguration { get; set; } + + public ServiceEndpointMetadata ServiceEndpointMetadata { get; private set; } + + private ServiceConfiguration() + { + } + + /// + /// Returns true if the AuthenticationType == Federation or LiveFederation + /// + private bool ClaimsEnabledService + { + get + { + return AuthenticationType == AuthenticationProviderType.Federation || AuthenticationType == AuthenticationProviderType.OnlineFederation; + } + } + + public ServiceConfiguration(Uri serviceUri) + : this(serviceUri, false) + { + } + + internal ServiceConfiguration(Uri serviceUri, bool checkForSecondary) + { + ServiceUri = serviceUri; + + ServiceEndpointMetadata = ServiceMetadataUtility.RetrieveServiceEndpointMetadata(typeof(TService), ServiceUri, checkForSecondary); + + ClientExceptionHelper.ThrowIfNull(ServiceEndpointMetadata, "ServiceEndpointMetadata"); + + if (ServiceEndpointMetadata.ServiceEndpoints.Count == 0) + { + StringBuilder errorBuilder = new StringBuilder(); + if (ServiceEndpointMetadata.MetadataConversionErrors.Count > 0) + { + foreach (MetadataConversionError error in ServiceEndpointMetadata.MetadataConversionErrors) + { + errorBuilder.Append(error.Message); + } + } + + throw new InvalidOperationException(ClientExceptionHelper.FormatMessage(0, "The provided uri did not return any Service Endpoints!\n{0}", errorBuilder.ToString())); + } + + ServiceEndpoints = ServiceEndpointMetadata.ServiceEndpoints; + + if (CurrentServiceEndpoint != null) + { + CrossRealmIssuerEndpoints = new CrossRealmIssuerEndpointCollection(); + + SetAuthenticationConfiguration(); + + if (checkForSecondary) + { + SetEndpointSwitchingBehavior(); + } + else + { + if (CurrentServiceEndpoint.Address.Uri != serviceUri) + { + ServiceMetadataUtility.ReplaceEndpointAddress(CurrentServiceEndpoint, serviceUri); + } + + PrimaryEndpoint = serviceUri; + } + } + } + + /// + /// If there is no binding, there is nothing to do. Otherwise, import the XRM Policy elements and set the issuers if claims. + /// + private void SetAuthenticationConfiguration() + { + if (CurrentServiceEndpoint.Binding == null) + { + return; + } + + var bindingElements = CurrentServiceEndpoint.Binding.CreateBindingElements(); + + var xrmPolicy = bindingElements.Find(); + if (xrmPolicy != null) + { + if (xrmPolicy.PolicyElements.ContainsKey("AuthenticationType")) + { + string type = xrmPolicy.PolicyElements["AuthenticationType"]; + if (!string.IsNullOrEmpty(type)) + { + AuthenticationProviderType authType; + if (Enum.TryParse(type, out authType)) + { + switch (authType) + { + case AuthenticationProviderType.OnlineFederation: + PolicyConfiguration = new OnlineFederationPolicyConfiguration(xrmPolicy); + foreach (var onlineProvider in ((OnlineFederationPolicyConfiguration)PolicyConfiguration).OnlineProviders.Values) + { + IssuerEndpoints = ServiceMetadataUtility.RetrieveLiveIdIssuerEndpoints(onlineProvider); + } + + break; + case AuthenticationProviderType.Federation: + // Set the issuer information + IssuerEndpoints = ServiceMetadataUtility.RetrieveIssuerEndpoints(AuthenticationProviderType.Federation, ServiceEndpoints, true); + PolicyConfiguration = new ClaimsPolicyConfiguration(xrmPolicy); + break; + default: + PolicyConfiguration = new WindowsPolicyConfiguration(xrmPolicy); + break; + } + + return; + } + } + } + } + } + + public Uri ServiceUri { get; internal set; } + + /// + /// This defaults to the first avaialble endpoint in the ServiceEndpoints dictionary if it has not been set. + /// + public ServiceEndpoint CurrentServiceEndpoint + { + get + { + if (currentServiceEndpoint == null) + { + foreach (var endpoint in ServiceEndpoints.Values) + { + if (ServiceUri.Port == endpoint.Address.Uri.Port && + ServiceUri.Scheme == endpoint.Address.Uri.Scheme) + { + currentServiceEndpoint = endpoint; + break; + } + } + } + + return currentServiceEndpoint; + } + + set + { + currentServiceEndpoint = value; + } + } + + /// + /// If there is a CurrentServiceEndpoint and the Service has been configured for claims (Federation,) then this + /// is the endpoint used by the Secure Token Service (STS) to issue the trusted token. + /// + public IssuerEndpoint CurrentIssuer + { + get + { + if (CurrentServiceEndpoint != null) + { + return ServiceMetadataUtility.GetIssuer(CurrentServiceEndpoint.Binding); + } + + return null; + } + + set + { + if (CurrentServiceEndpoint != null) + { + CurrentServiceEndpoint.Binding = ServiceMetadataUtility.SetIssuer(CurrentServiceEndpoint.Binding, value); + } + } + } + + /// + /// Identifies whether the constructed service is using Claims (Federation) authentication or legacy AD/RPS. + /// + public AuthenticationProviderType AuthenticationType + { + get + { + if (PolicyConfiguration is WindowsPolicyConfiguration) + { + return AuthenticationProviderType.ActiveDirectory; + } + + if (PolicyConfiguration is ClaimsPolicyConfiguration) + { + return AuthenticationProviderType.Federation; + } + + if (PolicyConfiguration is LiveIdPolicyConfiguration) + { + return AuthenticationProviderType.LiveId; + } + + if (PolicyConfiguration is OnlineFederationPolicyConfiguration) + { + return AuthenticationProviderType.OnlineFederation; + } + + return AuthenticationProviderType.None; + } + } + + /// + /// Contains the list of urls and binding information required in order to make a call to a WCF service. If the service is configured + /// for On-Premise use only, then the endpoint(s) contained within will NOT require the use of an Issuer Endpoint on the binding. + /// + /// If the service is configured to use Claims (Federation,) then the binding on the service endpoint MUST be configured to use + /// the appropriate Issuer Endpoint, i.e., UserNamePassword, Kerberos, etc. + /// + public ServiceEndpointDictionary ServiceEndpoints { get; internal set; } + + /// + /// The following property contains the urls and binding information required to use a configured Secure Token Service (STS) + /// for issuing a trusted token that the service endpoint will trust for authentication. + /// + /// The available endpoints can vary, depending on how the administrator of the STS has configured the server, but may include + /// the following authentication methods: + /// + /// 1. UserName and Password + /// 2. Kerberos + /// 3. Certificate + /// 4. Asymmetric Token + /// 5. Symmetric Token + /// + public IssuerEndpointDictionary IssuerEndpoints { get; internal set; } + + /// + /// Contains the STS IssuerEndpoints as determined dynamically by calls to AuthenticateCrossRealm. + /// + public CrossRealmIssuerEndpointCollection CrossRealmIssuerEndpoints { get; internal set; } + + private TokenServiceCredentialType _tokenEndpointType = TokenServiceCredentialType.AsymmetricToken; + + [SuppressMessage("Microsoft.Usage", "CA9888:DisposeObjectsCorrectly", Justification = "Value is returned from method and cannot be disposed.")] + public ChannelFactory CreateChannelFactory(TokenServiceCredentialType endpointType) + { + ClientExceptionHelper.ThrowIfNull(CurrentServiceEndpoint, "CurrentServiceEndpoint"); + + if (ClaimsEnabledService) + { + IssuerEndpoint authEndpoint = null; + + authEndpoint = IssuerEndpoints.GetIssuerEndpoint(endpointType); + if (authEndpoint != null) + { + lock (_lockObject) + { + CurrentServiceEndpoint.Binding = ServiceMetadataUtility.SetIssuer(CurrentServiceEndpoint.Binding, authEndpoint); + } + } + } + + var factory = CreateLocalChannelFactory(); + factory.Credentials.SetSupportInteractive(false); + + return factory; + } + + [SuppressMessage("Microsoft.Usage", "CA9888:DisposeObjectsCorrectly", Justification = "Value is returned from method and cannot be disposed.")] + public ChannelFactory CreateChannelFactory(ClientAuthenticationType clientAuthenticationType) + { + ClientExceptionHelper.ThrowIfNull(CurrentServiceEndpoint, "CurrentServiceEndpoint"); + + if (ClaimsEnabledService) + { + IssuerEndpoint authEndpoint = null; + + TokenServiceCredentialType credentialType; + if (clientAuthenticationType == ClientAuthenticationType.SecurityToken) + { + // Use symmetrictoken only for online + credentialType = (AuthenticationType == AuthenticationProviderType.OnlineFederation) ? TokenServiceCredentialType.SymmetricToken : _tokenEndpointType; + } + else + { + credentialType = TokenServiceCredentialType.Kerberos; + } + + authEndpoint = IssuerEndpoints.GetIssuerEndpoint(credentialType); + if (authEndpoint != null) + { + lock (_lockObject) + { + CurrentServiceEndpoint.Binding = ServiceMetadataUtility.SetIssuer(CurrentServiceEndpoint.Binding, authEndpoint); + } + } + } + + var factory = CreateLocalChannelFactory(); + factory.Credentials.SetSupportInteractive(false); + + return factory; + } + + [SuppressMessage("Microsoft.Usage", "CA9888:DisposeObjectsCorrectly", Justification = "Value is returned from method and cannot be disposed.")] + public ChannelFactory CreateChannelFactory(ClientCredentials clientCredentials) + { + ClientExceptionHelper.ThrowIfNull(CurrentServiceEndpoint, "CurrentServiceEndpoint"); + + // We can't check for the client credentials in order not to throw with Legacy RPS + if (ClaimsEnabledService) + { + TokenServiceCredentialType credentialType = GetCredentialsEndpointType(clientCredentials); + + var authEndpoint = IssuerEndpoints.GetIssuerEndpoint(credentialType); + if (authEndpoint != null) + { + lock (_lockObject) + { + CurrentServiceEndpoint.Binding = ServiceMetadataUtility.SetIssuer(CurrentServiceEndpoint.Binding, authEndpoint); + } + } + } + + var factory = CreateLocalChannelFactory(); + + ConfigureCredentials(factory, clientCredentials); + +#if NETFRAMEWORK + factory.Credentials.SetSupportInteractive(clientCredentials != null ? clientCredentials.GetSupportInteractive() : false); +#endif + return factory; + } + +#region Authenticate Cross Realm + + /// + /// Authenticates based on the client credentials passed in. + /// + /// The standard ClientCredentials + /// + /// + /// RequestSecurityTokenResponse + public SecurityTokenResponse AuthenticateCrossRealm(ClientCredentials clientCredentials, string appliesTo, Uri crossRealmSts) + { + if (crossRealmSts != null) + { + AuthenticationCredentials authenticationCredentials = new AuthenticationCredentials(); + authenticationCredentials.AppliesTo = !string.IsNullOrWhiteSpace(appliesTo) ? new Uri(appliesTo) : null; + authenticationCredentials.KeyType = string.Empty; + + authenticationCredentials.ClientCredentials = clientCredentials; + authenticationCredentials.SecurityTokenResponse = null; + IdentityProviderTrustConfiguration idp = TryGetOnlineTrustConfiguration(crossRealmSts); + authenticationCredentials.EndpointType = idp != null ? TokenServiceCredentialType.Username : GetCredentialsEndpointType(clientCredentials); + authenticationCredentials.IssuerEndpoints = CrossRealmIssuerEndpoints[crossRealmSts]; + + if (this.AuthenticationType == AuthenticationProviderType.OnlineFederation && idp == null) + { + authenticationCredentials.KeyType = KeyTypes.Bearer; + } + + return AuthenticateInternal(authenticationCredentials); + } + + return null; + } + + /// + /// Authenticates based on the security token passed in. + /// + /// + /// + /// + /// RequestSecurityTokenResponse + public SecurityTokenResponse AuthenticateCrossRealm(SecurityToken securityToken, string appliesTo, Uri crossRealmSts) + { + if (crossRealmSts != null) + { + AuthenticationCredentials authenticationCredentials = new AuthenticationCredentials(); + authenticationCredentials.AppliesTo = !string.IsNullOrWhiteSpace(appliesTo) ? new Uri(appliesTo) : null; + + authenticationCredentials.KeyType = string.Empty; + + authenticationCredentials.ClientCredentials = null; + authenticationCredentials.SecurityTokenResponse = new SecurityTokenResponse() { Token = securityToken }; + bool useDefaultTokenType = true; + if (AuthenticationType == AuthenticationProviderType.OnlineFederation) + { + IdentityProviderTrustConfiguration idp = TryGetOnlineTrustConfiguration(crossRealmSts); + if (idp != null) + { + if (idp.Endpoint.GetServiceRoot() == crossRealmSts) + { + authenticationCredentials.EndpointType = TokenServiceCredentialType.SymmetricToken; + useDefaultTokenType = false; + } + } + } + + if (useDefaultTokenType) + { + authenticationCredentials.EndpointType = _tokenEndpointType; + } + + authenticationCredentials.IssuerEndpoints = CrossRealmIssuerEndpoints[crossRealmSts]; + return AuthenticateInternal(authenticationCredentials); + } + + return null; + } + +#endregion Authenticate Cross Realm + +#region OrgID + + private IdentityProviderTrustConfiguration TryGetOnlineTrustConfiguration() + { + var liveConfiguration = PolicyConfiguration as OnlinePolicyConfiguration; + if (liveConfiguration == null) + { + return null; + } + + return liveConfiguration.OnlineProviders.Values.OfType().FirstOrDefault(); + } + + private IdentityProviderTrustConfiguration GetLiveTrustConfig() + where T : IdentityProviderTrustConfiguration + { + var liveConfiguration = PolicyConfiguration as OnlinePolicyConfiguration; + ClientExceptionHelper.ThrowIfNull(liveConfiguration, "liveConfiguration"); + + IdentityProviderTrustConfiguration liveTrustConfig = liveConfiguration.OnlineProviders.Values.OfType().FirstOrDefault(); + + ClientExceptionHelper.ThrowIfNull(liveTrustConfig, "liveTrustConfig"); + return liveTrustConfig; + } + + private IdentityProviderTrustConfiguration GetOnlineTrustConfiguration(Uri crossRealmSts) + { + var liveFederationConfiguration = PolicyConfiguration as OnlineFederationPolicyConfiguration; + + ClientExceptionHelper.ThrowIfNull(liveFederationConfiguration, "liveFederationConfiguration"); + + if (liveFederationConfiguration.OnlineProviders.ContainsKey(crossRealmSts)) + { + return liveFederationConfiguration.OnlineProviders[crossRealmSts]; + } + + return null; + } + + private IdentityProviderTrustConfiguration TryGetOnlineTrustConfiguration(Uri crossRealmSts) + { + var liveFederationConfiguration = PolicyConfiguration as OnlineFederationPolicyConfiguration; + + if (liveFederationConfiguration != null && liveFederationConfiguration.OnlineProviders.ContainsKey(crossRealmSts)) + { + return liveFederationConfiguration.OnlineProviders[crossRealmSts]; + } + + return null; + } +#endregion OrgID + +#region Authenticate ClientCredentials + + /// + /// Authenticates based on the client credentials passed in. + /// + /// The standard ClientCredentials + /// RequestSecurityTokenResponse + public SecurityTokenResponse Authenticate(ClientCredentials clientCredentials) + { + if (CurrentServiceEndpoint != null) + { + AuthenticationCredentials authenticationCredentials = new AuthenticationCredentials(); + authenticationCredentials.ClientCredentials = clientCredentials; + var returnCrededentials = Authenticate(authenticationCredentials); + if (returnCrededentials != null && returnCrededentials.SecurityTokenResponse != null) + { + return returnCrededentials.SecurityTokenResponse; + } + } + + return null; + } + + /// + /// Authenticates based on the client credentials passed in. + /// + /// + /// + /// Optional. Can be set to Bearer if bearer token required + /// RequestSecurityTokenResponse + internal SecurityTokenResponse Authenticate(ClientCredentials clientCredentials, Uri uri, string keyType) + { + AuthenticationCredentials authenticationCredentials = new AuthenticationCredentials(); + authenticationCredentials.AppliesTo = uri; + authenticationCredentials.EndpointType = GetCredentialsEndpointType(clientCredentials); + authenticationCredentials.KeyType = keyType; + authenticationCredentials.IssuerEndpoints = IssuerEndpoints; + authenticationCredentials.ClientCredentials = clientCredentials; + authenticationCredentials.SecurityTokenResponse = null; + return AuthenticateInternal(authenticationCredentials); + } +#endregion Authenticate ClientCredentials + +#region Authenticate SecurityToken + + /// + /// Authenticates based on the security token passed in. + /// + /// + /// RequestSecurityTokenResponse + public SecurityTokenResponse Authenticate(SecurityToken securityToken) + { + ClientExceptionHelper.ThrowIfNull(securityToken, "securityToken"); + + if (AuthenticationType == AuthenticationProviderType.OnlineFederation) + { + var liveTrustConfig = TryGetOnlineTrustConfiguration(); + if (liveTrustConfig == null) + { + return null; + } + + return AuthenticateCrossRealm(securityToken, liveTrustConfig.AppliesTo, liveTrustConfig.Endpoint.GetServiceRoot()); + } + + if (CurrentServiceEndpoint != null) + { + AuthenticationCredentials authenticationCredentials = new AuthenticationCredentials(); + authenticationCredentials.AppliesTo = CurrentServiceEndpoint.Address.Uri; + authenticationCredentials.EndpointType = _tokenEndpointType; + authenticationCredentials.KeyType = string.Empty; + authenticationCredentials.IssuerEndpoints = IssuerEndpoints; + authenticationCredentials.ClientCredentials = null; + authenticationCredentials.SecurityTokenResponse = new SecurityTokenResponse() { Token = securityToken }; + + return AuthenticateInternal(authenticationCredentials); + } + + return null; + } + + /// + /// Authenticates based on the security token passed in. + /// + /// + /// + /// Optional. Can be set to Bearer if bearer token required + /// RequestSecurityTokenResponse + internal SecurityTokenResponse Authenticate(SecurityToken securityToken, Uri uri, string keyType) + { + ClientExceptionHelper.ThrowIfNull(securityToken, "securityToken"); + + if (uri != null) + { + AuthenticationCredentials authenticationCredentials = new AuthenticationCredentials(); + authenticationCredentials.AppliesTo = uri.GetServiceRoot(); + authenticationCredentials.EndpointType = _tokenEndpointType; + authenticationCredentials.KeyType = keyType; + authenticationCredentials.IssuerEndpoints = IssuerEndpoints; + authenticationCredentials.ClientCredentials = null; + authenticationCredentials.SecurityTokenResponse = new SecurityTokenResponse() { Token = securityToken }; + return AuthenticateInternal(authenticationCredentials); + } + + return null; + } +#endregion Authenticate SecurityToken + +#region Authenticate LiveId + + /// + /// This will default to LiveID auth when on-line. + /// + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")] + public SecurityTokenResponse AuthenticateDevice(ClientCredentials clientCredentials) + { + ClientExceptionHelper.ThrowIfNull(clientCredentials, "clientCredentials"); + + throw new InvalidOperationException("Authentication to MSA services is not supported."); + } + + /// + /// This will default to LiveID auth when on-line. + /// + /// + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")] + public SecurityTokenResponse Authenticate(ClientCredentials clientCredentials, SecurityTokenResponse deviceTokenResponse) + { + ClientExceptionHelper.ThrowIfNull(clientCredentials, "clientCredentials"); + ClientExceptionHelper.ThrowIfNull(deviceTokenResponse, "deviceTokenResponse"); + + throw new InvalidOperationException("Authentication to MSA services is not supported."); + } + + /// + /// This will default to LiveID auth when on-line. + /// + /// + /// + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")] + public SecurityTokenResponse Authenticate(ClientCredentials clientCredentials, SecurityTokenResponse deviceTokenResponse, string keyType) + { + ClientExceptionHelper.ThrowIfNull(clientCredentials, "clientCredentials"); + ClientExceptionHelper.ThrowIfNull(deviceTokenResponse, "deviceTokenResponse"); + ClientExceptionHelper.ThrowIfNullOrEmpty(keyType, "keyType"); + + throw new InvalidOperationException("Authentication to MSA services is not supported."); + } + + public IdentityProvider GetIdentityProvider(string userPrincipalName) + { + IdentityProviderTrustConfiguration idp = TryGetOnlineTrustConfiguration(); + if (idp == null) + { + return null; + } + + return IdentityProviderLookup.Instance.GetIdentityProvider(idp.Endpoint.GetServiceRoot(), idp.Endpoint.GetServiceRoot(), userPrincipalName); + } + +#endregion Authenticate LiveId + + private SecurityTokenResponse AuthenticateInternal(AuthenticationCredentials authenticationCredentials) + { + ClientExceptionHelper.Assert(this.AuthenticationType == AuthenticationProviderType.Federation || this.AuthenticationType == AuthenticationProviderType.OnlineFederation, "Authenticate is not supported when not in claims mode!"); + + if (ClaimsEnabledService) + { + if (authenticationCredentials.IssuerEndpoint.CredentialType == TokenServiceCredentialType.Kerberos) + { + bool retry = false; + int retryCount = 0; + do + { + try + { + return Issue(authenticationCredentials); + } + catch (SecurityTokenValidationException) + { + retry = false; + + // Fall back to windows integrated. + if (authenticationCredentials.IssuerEndpoints.ContainsKey(TokenServiceCredentialType.Windows.ToString())) + { + authenticationCredentials.EndpointType = TokenServiceCredentialType.Windows; + retry = ++retryCount < 2; + } + + // We don't care, we just want to return null. The reason why we are are catching this one is because in pure Kerberos mode, this + // will throw a very bad exception that will crash VS. + } + catch (SecurityNegotiationException) + { + // This is the exception with Integrated Windows Auth. + // We don't care, we just want to return null. The reason why we are are catching this one is because in pure Kerberos mode, this + // will throw a very bad exception that will crash VS. + retry = ++retryCount < 2; + } + catch (FaultException) + { + // Fall back to windows integrated. + if (authenticationCredentials.IssuerEndpoints.ContainsKey(TokenServiceCredentialType.Windows.ToString())) + { + authenticationCredentials.EndpointType = TokenServiceCredentialType.Windows; + retry = ++retryCount < 2; + } + } + } + while (retry); + } + else + { + return Issue(authenticationCredentials); + } + } + + return null; + } + + /// + /// This is the method that actually creates the trust channel factory and issues the request for the token. + /// + /// + [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "[WSTrustChannelFactory] Pending resolution of bug: https://dev.azure.com/dynamicscrm/OneCRM/_workitems/edit/2493634")] + private SecurityTokenResponse Issue(AuthenticationCredentials authenticationCredentials) + { + ClientExceptionHelper.ThrowIfNull(authenticationCredentials, "authenticationCredentials"); + ClientExceptionHelper.ThrowIfNull(authenticationCredentials.IssuerEndpoint, "authenticationCredentials.IssuerEndpoint"); + ClientExceptionHelper.ThrowIfNull(authenticationCredentials.AppliesTo, "authenticationCredentials.AppliesTo"); + +#if NETFRAMEWORK + WSTrustChannelFactory channelFactory = null; + WSTrustChannel channel = null; + try + { + authenticationCredentials.RequestType = RequestTypes.Issue; + + channelFactory = new WSTrustChannelFactory(authenticationCredentials.IssuerEndpoint.IssuerBinding, authenticationCredentials.IssuerEndpoint.IssuerAddress); + + var supportingToken = (authenticationCredentials.SecurityTokenResponse != null && authenticationCredentials.SecurityTokenResponse.Token != null) ? authenticationCredentials.SecurityTokenResponse.Token : + (authenticationCredentials.SupportingCredentials != null && authenticationCredentials.SupportingCredentials.SecurityTokenResponse != null && authenticationCredentials.SupportingCredentials.SecurityTokenResponse.Token != null) ? authenticationCredentials.SupportingCredentials.SecurityTokenResponse.Token : null; + + if (supportingToken != null) + { + channelFactory.Credentials.SupportInteractive = false; + } + else + { + ConfigureCredentials(channelFactory, authenticationCredentials.ClientCredentials); + } + + channel = supportingToken != null + ? (WSTrustChannel)channelFactory.CreateChannelWithIssuedToken(supportingToken) + : (WSTrustChannel)channelFactory.CreateChannel(); + + if (channel != null) + { + var rst = new RequestSecurityToken(authenticationCredentials.RequestType) + { + AppliesTo = new EndpointReference(authenticationCredentials.AppliesTo.AbsoluteUri) + }; + + if (!string.IsNullOrEmpty(authenticationCredentials.KeyType)) + { + rst.KeyType = authenticationCredentials.KeyType; + } + + RequestSecurityTokenResponse rstr; + var token = channel.Issue(rst, out rstr); + return new SecurityTokenResponse() { Token = token, Response = rstr, EndpointType = authenticationCredentials.EndpointType }; + } + } + finally + { + if (channel != null) + { + channel.Close(true); + } + + channel = null; + if (channelFactory != null) + { + channelFactory.Close(true); + } + + channelFactory = null; + } + + return null; +#else + throw new PlatformNotSupportedException("Xrm.Sdk WSTrust"); +#endif + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")] + private void ConfigureCredentials(ChannelFactory channelFactory, ClientCredentials clientCredentials) + { + if (clientCredentials != null) + { + if (clientCredentials.ClientCertificate != null && clientCredentials.ClientCertificate.Certificate != null) + { + channelFactory.Credentials.ClientCertificate.Certificate = clientCredentials.ClientCertificate.Certificate; + } + else if (clientCredentials.UserName != null && !string.IsNullOrEmpty(clientCredentials.UserName.UserName)) + { + channelFactory.Credentials.UserName.UserName = clientCredentials.UserName.UserName; + channelFactory.Credentials.UserName.Password = clientCredentials.UserName.Password; + } + else if (clientCredentials.Windows != null && (clientCredentials.Windows.ClientCredential != null)) + { + channelFactory.Credentials.Windows.ClientCredential = clientCredentials.Windows.ClientCredential; + channelFactory.Credentials.Windows.AllowedImpersonationLevel = clientCredentials.Windows.AllowedImpersonationLevel; + } + + // We don't want to do anything specific so that the default credential searching can be done. + } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")] + private TokenServiceCredentialType GetCredentialsEndpointType(ClientCredentials clientCredentials) + { + if (clientCredentials != null) + { + if (clientCredentials.UserName != null && !string.IsNullOrEmpty(clientCredentials.UserName.UserName)) + { + return TokenServiceCredentialType.Username; + } + + if (clientCredentials.ClientCertificate != null && clientCredentials.ClientCertificate.Certificate != null) + { + return TokenServiceCredentialType.Certificate; + } + + if (clientCredentials.Windows != null && (clientCredentials.Windows.ClientCredential != null)) + { + return TokenServiceCredentialType.Kerberos; + } + + // We don't want to do anything specific so that the default credential searching can be done. + } + + return TokenServiceCredentialType.Kerberos; + } + + [SuppressMessage("Microsoft.Usage", "CA9888:DisposeObjectsCorrectly", Justification = "Value is returned from method and cannot be disposed.")] + private ChannelFactory CreateLocalChannelFactory() + { +#if NETFRAMEWORK + lock (_lockObject) + { + ServiceEndpoint endpoint = new ServiceEndpoint(CurrentServiceEndpoint.Contract, CurrentServiceEndpoint.Binding, CurrentServiceEndpoint.Address); + foreach (var behavior in CurrentServiceEndpoint.Behaviors) + { + endpoint.Behaviors.Add(behavior); + } + + endpoint.IsSystemEndpoint = CurrentServiceEndpoint.IsSystemEndpoint; + endpoint.ListenUri = CurrentServiceEndpoint.ListenUri; + endpoint.ListenUriMode = CurrentServiceEndpoint.ListenUriMode; + endpoint.Name = CurrentServiceEndpoint.Name; + + var factory = new ChannelFactory(endpoint); + + factory.Credentials.IssuedToken.CacheIssuedTokens = true; + return factory; + } +#else + lock (_lockObject) + { + ServiceEndpoint endpoint = new ServiceEndpoint(CurrentServiceEndpoint.Contract, CurrentServiceEndpoint.Binding, CurrentServiceEndpoint.Address); + foreach (var behavior in CurrentServiceEndpoint.EndpointBehaviors) + { + endpoint.EndpointBehaviors.Add(behavior); + } + endpoint.Name = CurrentServiceEndpoint.Name; + + var factory = new ChannelFactory(endpoint); + return factory; + } + + throw new PlatformNotSupportedException("Xrm.Sdk WSDL"); +#endif + } + + private static object _lockObject = new object(); +#region Constants + internal const string DefaultRequestType = RequestTypes.Issue; +#endregion Constants + } +} diff --git a/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/ServiceMetadataUtility.cs b/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/ServiceMetadataUtility.cs new file mode 100644 index 0000000..a7a73a9 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/ServiceMetadataUtility.cs @@ -0,0 +1,774 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using Microsoft.Extensions.DependencyInjection; +using System.IdentityModel.Tokens; +using System.IO; +using System.Net; +using System.Net.Http; +using System.ServiceModel; +using System.ServiceModel.Channels; +using System.ServiceModel.Description; +using System.ServiceModel.Security; +using System.ServiceModel.Security.Tokens; +using System.Text; +using System.Xml; +using Microsoft.PowerPlatform.Dataverse.Client.Utils; +//using Microsoft.Crm.Protocols.WSTrust.Bindings; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Client; +using Microsoft.Xrm.Sdk.Common; + +namespace Microsoft.PowerPlatform.Dataverse.Client.Connector.OnPremises +{ + /// + /// Handles retrieving/making use of service metadata information. + /// + internal static class ServiceMetadataUtility + { + public static IssuerEndpointDictionary RetrieveIssuerEndpoints(EndpointAddress issuerMetadataAddress) + { +#if NETFRAMEWORK + var alternateIssuers = new IssuerEndpointDictionary(); + + var mcli = CreateMetadataClient(issuerMetadataAddress.Uri.Scheme); + MetadataSet stsMDS = mcli.GetMetadata(issuerMetadataAddress.Uri, MetadataExchangeClientMode.HttpGet); + + if (stsMDS != null) + { + var stsImporter = new WsdlImporter(stsMDS); + var stsEndpoints = stsImporter.ImportAllEndpoints(); + + foreach (var stsEndpoint in stsEndpoints) + { + if (!(stsEndpoint.Binding is NetTcpBinding)) + { + var credentialType = TokenServiceCredentialType.None; + TrustVersion endpointTrustVersion = TrustVersion.Default; + + var wsHttpBinding = stsEndpoint.Binding as WS2007HttpBinding; + if (wsHttpBinding != null) + { + var elements = wsHttpBinding.CreateBindingElements(); + var securityElement = elements.Find(); + if (securityElement != null) + { + endpointTrustVersion = securityElement.MessageSecurityVersion.TrustVersion; + if (endpointTrustVersion == TrustVersion.WSTrust13) + { + if (wsHttpBinding.Security.Message.ClientCredentialType == MessageCredentialType.UserName) + { + credentialType = TokenServiceCredentialType.Username; + } + else if (wsHttpBinding.Security.Message.ClientCredentialType == MessageCredentialType.Certificate) + { + credentialType = TokenServiceCredentialType.Certificate; + } + else if (wsHttpBinding.Security.Message.ClientCredentialType == MessageCredentialType.Windows) + { + credentialType = TokenServiceCredentialType.Windows; + } + } + } + } + else + { + // We need to do a little deeper look to figure out what we need. + var elements = stsEndpoint.Binding.CreateBindingElements(); + var securityElement = elements.Find(); + if (securityElement != null) + { + endpointTrustVersion = securityElement.MessageSecurityVersion.TrustVersion; + + if (endpointTrustVersion == TrustVersion.WSTrust13) + { + var issuedTokenParameters = GetIssuedTokenParameters(securityElement); + if (issuedTokenParameters != null) + { + if (issuedTokenParameters.KeyType == SecurityKeyType.SymmetricKey) + { + credentialType = TokenServiceCredentialType.SymmetricToken; + } + else if (issuedTokenParameters.KeyType == SecurityKeyType.AsymmetricKey) + { + credentialType = TokenServiceCredentialType.AsymmetricToken; + } + else if (issuedTokenParameters.KeyType == SecurityKeyType.BearerKey) + { + credentialType = TokenServiceCredentialType.Bearer; + } + } + else + { + var kerberosTokenParameters = GetKerberosTokenParameters(securityElement); + if (kerberosTokenParameters != null) + { + credentialType = TokenServiceCredentialType.Kerberos; + } + } + } + else + { + // We only support 2005 for MFG + } + } + } + + if (credentialType != TokenServiceCredentialType.None) + { + var endpointKey = credentialType.ToString(); + if (!alternateIssuers.ContainsKey(endpointKey)) + { + alternateIssuers.Add(endpointKey, + new IssuerEndpoint + { + IssuerAddress = stsEndpoint.Address, + IssuerBinding = stsEndpoint.Binding, + IssuerMetadataAddress = issuerMetadataAddress, + CredentialType = credentialType, + TrustVersion = endpointTrustVersion, + }); + } + } + } + } + } + + return alternateIssuers; +#else + throw new PlatformNotSupportedException("Xrm.Sdk WSDL"); +#endif + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2141:TransparentMethodsMustNotSatisfyLinkDemandsFxCopRule")] + public static IssuerEndpointDictionary RetrieveLiveIdIssuerEndpoints(IdentityProviderTrustConfiguration identityProviderTrustConfiguration) + { +#if NETFRAMEWORK + var issuers = new IssuerEndpointDictionary(); + + issuers.Add(TokenServiceCredentialType.Username.ToString(), + new IssuerEndpoint + { + CredentialType = TokenServiceCredentialType.Username, + IssuerAddress = new EndpointAddress(identityProviderTrustConfiguration.Endpoint.AbsoluteUri), + IssuerBinding = identityProviderTrustConfiguration.Binding + }); + var stsBinding = new Microsoft.Crm.Protocols.WSTrust.Bindings.IssuedTokenWSTrustBinding() + { + TrustVersion = identityProviderTrustConfiguration.TrustVersion, + SecurityMode = identityProviderTrustConfiguration.SecurityMode + }; + stsBinding.KeyType = SecurityKeyType.BearerKey; + issuers.Add(TokenServiceCredentialType.SymmetricToken.ToString(), + new IssuerEndpoint + { + CredentialType = TokenServiceCredentialType.SymmetricToken, + IssuerAddress = new EndpointAddress(identityProviderTrustConfiguration.Endpoint.AbsoluteUri), + IssuerBinding = stsBinding + }); + return issuers; +#else + throw new PlatformNotSupportedException("Xrm.Sdk WSDL"); +#endif + } + + public static IssuerEndpointDictionary RetrieveDefaultIssuerEndpoint(AuthenticationProviderType authenticationProviderType, IssuerEndpoint issuer) + { + var issuers = new IssuerEndpointDictionary(); + + if (issuer != null && issuer.IssuerAddress != null) + { + // Default to username + TokenServiceCredentialType credentialType; + + // Go ahead and add the auth endpoint. We'll assume username for now, since we have nothing to go on. + switch (authenticationProviderType) + { + case AuthenticationProviderType.Federation: + credentialType = TokenServiceCredentialType.Kerberos; + break; + case AuthenticationProviderType.OnlineFederation: + credentialType = TokenServiceCredentialType.Username; + break; + default: + credentialType = TokenServiceCredentialType.Kerberos; + break; + } + + issuers.Add(credentialType.ToString(), + new IssuerEndpoint + { + CredentialType = credentialType, + IssuerAddress = issuer.IssuerAddress, + IssuerBinding = issuer.IssuerBinding + }); + } + + return issuers; + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We don't care about the actual exception, just want to ignore it for now.")] + public static IssuerEndpointDictionary RetrieveIssuerEndpoints(AuthenticationProviderType authenticationProviderType, ServiceEndpointDictionary endpoints, bool queryMetadata) + { + foreach (var serviceEndpoint in endpoints.Values) + { + try + { + var issuer = GetIssuer(serviceEndpoint.Binding); + + if (issuer != null) + { + if (queryMetadata && issuer.IssuerMetadataAddress != null) + { + return RetrieveIssuerEndpoints(issuer.IssuerMetadataAddress); + } + + // There is no metadata available. So attempt to add a default one, and allow the calling program to generate missing data. + return RetrieveDefaultIssuerEndpoint(authenticationProviderType, issuer); + } + } + catch (Exception) + { + // We don't care what the exception is at this time. + } + } + + return new IssuerEndpointDictionary(); + } + + public static IssuerEndpoint GetIssuer(Binding binding) + { + if (binding == null) + { + return null; + } + + var elements = binding.CreateBindingElements(); + var securityElement = elements.Find(); + var issuerParams = GetIssuedTokenParameters(securityElement); + if (issuerParams != null) + { +#if NETFRAMEWORK + var endpoint = new IssuerEndpoint(); + endpoint.IssuerAddress = issuerParams.IssuerAddress; + endpoint.IssuerBinding = issuerParams.IssuerBinding; + endpoint.IssuerMetadataAddress = issuerParams.IssuerMetadataAddress; + return endpoint; +#else + throw new PlatformNotSupportedException("Xrm.Sdk WSDL"); +#endif + } + + return null; + } + + private static KerberosSecurityTokenParameters GetKerberosTokenParameters(SecurityBindingElement securityElement) + { + if (securityElement != null) + { +#if NETFRAMEWORK + if (securityElement.EndpointSupportingTokenParameters != null) + { + if (securityElement.EndpointSupportingTokenParameters.Endorsing != null) + { + if (securityElement.EndpointSupportingTokenParameters.Endorsing.Count > 0) + { + return securityElement.EndpointSupportingTokenParameters.Endorsing[0] as KerberosSecurityTokenParameters; + } + } + } +#else + throw new PlatformNotSupportedException("Xrm.Sdk WSTrust"); +#endif + } + + return null; + } + + private static IssuedSecurityTokenParameters GetIssuedTokenParameters(SecurityBindingElement securityElement) + { + if (securityElement != null) + { +#if NETFRAMEWORK + if (securityElement.EndpointSupportingTokenParameters != null) + { + if (securityElement.EndpointSupportingTokenParameters.Endorsing != null && securityElement.EndpointSupportingTokenParameters.Endorsing.Count > 0) + { + var issuedSecurityTokenParameters = securityElement.EndpointSupportingTokenParameters.Endorsing[0] as IssuedSecurityTokenParameters; + if (issuedSecurityTokenParameters != null) + { + return issuedSecurityTokenParameters; + } + + var endorsingParam = securityElement.EndpointSupportingTokenParameters.Endorsing[0] as SecureConversationSecurityTokenParameters; + if (endorsingParam != null) + { + // It is possible that there will be more token parameters or in one of the other collections at some point. For now, we know this is ok. + if (endorsingParam.BootstrapSecurityBindingElement.EndpointSupportingTokenParameters.Endorsing.Count > 0) + { + return endorsingParam.BootstrapSecurityBindingElement.EndpointSupportingTokenParameters.Endorsing[0] as IssuedSecurityTokenParameters; + } + + if (endorsingParam.BootstrapSecurityBindingElement.EndpointSupportingTokenParameters.Signed.Count > 0) + { + return endorsingParam.BootstrapSecurityBindingElement.EndpointSupportingTokenParameters.Signed[0] as IssuedSecurityTokenParameters; + } + } + } + else if (securityElement.EndpointSupportingTokenParameters.Signed != null && securityElement.EndpointSupportingTokenParameters.Signed.Count > 0) + { + // If we have a token parameter here then the server is on-line and behind an NLB. + return securityElement.EndpointSupportingTokenParameters.Signed[0] as IssuedSecurityTokenParameters; + } + } +#else + throw new PlatformNotSupportedException("Xrm.Sdk WSDL"); +#endif + } + + return null; + } + + public static CustomBinding SetIssuer(Binding binding, IssuerEndpoint issuerEndpoint) + { + var elements = binding.CreateBindingElements(); + var securityElement = elements.Find(); + var securityTokenParameters = GetIssuedTokenParameters(securityElement); + if (securityTokenParameters != null) + { +#if NETFRAMEWORK + securityTokenParameters.IssuerAddress = issuerEndpoint.IssuerAddress; + securityTokenParameters.IssuerBinding = issuerEndpoint.IssuerBinding; + if (issuerEndpoint.IssuerMetadataAddress != null) + { + securityTokenParameters.IssuerMetadataAddress = issuerEndpoint.IssuerMetadataAddress; + } +#else + throw new PlatformNotSupportedException("Xrm.Sdk WSTrust"); +#endif + } + + return new CustomBinding(elements); + } + + private static void ParseEndpoints(ServiceEndpointDictionary serviceEndpoints, ServiceEndpointCollection serviceEndpointCollection) + { + serviceEndpoints.Clear(); + + if (serviceEndpointCollection != null) + { +#if NETFRAMEWORK + foreach (var endpoint in serviceEndpointCollection) + { + if (IsEndpointSupported(endpoint)) + { + serviceEndpoints.Add(endpoint.Name, endpoint); + } + } +#else + throw new PlatformNotSupportedException("Xrm.Sdk WSDL"); +#endif + } + } + + private static bool IsEndpointSupported(ServiceEndpoint endpoint) + { + if (endpoint != null) + { + // The web endpoints are currently designed for in-browser JScript usage and not by the rich client. + if (!endpoint.Address.Uri.AbsolutePath.EndsWith(XrmServiceConstants.WebEndpointExtension, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + + internal static ServiceEndpointMetadata RetrieveServiceEndpointMetadata(Type contractType, Uri serviceUri, bool checkForSecondary) + { + ServiceEndpointMetadata serviceEndpointMetadata = new ServiceEndpointMetadata(); + + serviceEndpointMetadata.ServiceUrls = ServiceConfiguration.CalculateEndpoints(serviceUri); + + if (!checkForSecondary) + { + serviceEndpointMetadata.ServiceUrls.AlternateEndpoint = null; + } + +#if !NETFRAMEWORK + // TODO: Waiting on work for updated WCF endpoints collection to be completed. // hard throw here to prevent any futher progress. + throw new PlatformNotSupportedException("Xrm.Sdk WSDL"); +#endif + + // Get version of current assembly which is the version of the SDK +#pragma warning disable CS0162 // Unreachable code detected + Version sdkVersion = GetSDKVersionNumberFromAssembly(); +#pragma warning restore CS0162 // Unreachable code detected + var wsdlUri = new Uri(string.Format(CultureInfo.InvariantCulture, "{0}{1}&sdkversion={2}", serviceUri.AbsoluteUri, "?wsdl", sdkVersion.ToString(2))); + + var mcli = CreateMetadataClient(wsdlUri.Scheme); + if (mcli != null) + { +#if NETFRAMEWORK + try + { + serviceEndpointMetadata.ServiceMetadata = mcli.GetMetadata(wsdlUri, MetadataExchangeClientMode.HttpGet); + } + catch (InvalidOperationException ioexp) + { + bool rethrow = true; + if (checkForSecondary) + { + var wexp = ioexp.InnerException as WebException; + if (wexp != null) + { + if (wexp.Status == WebExceptionStatus.NameResolutionFailure || wexp.Status == WebExceptionStatus.Timeout) + { + if (serviceEndpointMetadata.ServiceUrls != null) + { + if (serviceEndpointMetadata.ServiceUrls.PrimaryEndpoint == serviceUri) + { + rethrow = TryRetrieveMetadata(mcli, new Uri(serviceEndpointMetadata.ServiceUrls.AlternateEndpoint.AbsoluteUri + "?wsdl"), serviceEndpointMetadata); + } + else if (serviceEndpointMetadata.ServiceUrls.AlternateEndpoint == serviceUri) + { + rethrow = TryRetrieveMetadata(mcli, new Uri(serviceEndpointMetadata.ServiceUrls.PrimaryEndpoint.AbsoluteUri + "?wsdl"), serviceEndpointMetadata); + } + } + } + } + } + + if (rethrow) + { + throw; + } + } +#else + throw new PlatformNotSupportedException("Xrm.Sdk WSDL"); +#endif + } + else + { +#if !NETFRAMEWORK + + if (serviceEndpointMetadata.ServiceMetadata == null) + serviceEndpointMetadata.ServiceMetadata = new MetadataSet(); + var MetadataBody = GetMexDocument(wsdlUri); +#else + throw new PlatformNotSupportedException("Xrm.Sdk WSDL"); +#endif + } + + ClientExceptionHelper.ThrowIfNull(serviceEndpointMetadata.ServiceMetadata, "STS Metadata"); + + var contracts = CreateContractCollection(contractType); + + if (contracts != null) + { +#if NETFRAMEWORK + // The following code inserts a custom WsdlImporter without removing the other + // importers already in the collection. + var importer = new WsdlImporter(serviceEndpointMetadata.ServiceMetadata); + var exts = importer.WsdlImportExtensions; + + List policyImportExtensions = AddSecurityBindingToPolicyImporter(importer); + + WsdlImporter stsImporter = new WsdlImporter(serviceEndpointMetadata.ServiceMetadata, policyImportExtensions, exts); + + foreach (ContractDescription description in contracts) + { + stsImporter.KnownContracts.Add(GetPortTypeQName(description), description); + } + + ServiceEndpointCollection endpoints = stsImporter.ImportAllEndpoints(); + + if (stsImporter.Errors.Count > 0) + { + foreach (MetadataConversionError error in stsImporter.Errors) + { + // We can't throw for metadata errors as the initial retrieve will generate a metadata conversion error after querying for the WS-Trust metadata + // in claims mode since we don't require a particular endpoint. Why this is considered a fatal error is unclear. + serviceEndpointMetadata.MetadataConversionErrors.Add(error); + } + } + + ParseEndpoints(serviceEndpointMetadata.ServiceEndpoints, endpoints); +#else + + // Dataverse requires Message Transport security which is not supported in .net core for ActiveDirectory. + + + //AuthenticationPolicy authenticationPolicy = new AuthenticationPolicy(); + //authenticationPolicy.PolicyElements.Add("AuthenticationType", "ActiveDirectory"); // Need to read these from metdata in the future if WCF does not provide support/. + //TextMessageEncodingBindingElement text01 = new TextMessageEncodingBindingElement(); + //HttpsTransportBindingElement http1 = new HttpsTransportBindingElement(); + //http1.ExtendedProtectionPolicy = new System.Security.Authentication.ExtendedProtection.ExtendedProtectionPolicy(System.Security.Authentication.ExtendedProtection.PolicyEnforcement.WhenSupported, System.Security.Authentication.ExtendedProtection.ProtectionScenario.TransportSelected, null); + //CustomBinding bind = new CustomBinding(authenticationPolicy, new TextMessageEncodingBindingElement(), http1); + //bind.Name = "CustomBinding_IOrganizationService"; + //bind.Namespace = "http://schemas.microsoft.com/xrm/2011/Contracts/Services"; + //serviceEndpointMetadata.ServiceEndpoints.Add( + // "CustomBinding_IOrganizationService", + // new ServiceEndpoint(contracts[0], + // bind, + // new EndpointAddress(serviceEndpointMetadata.ServiceUrls.PrimaryEndpoint))); + + + throw new PlatformNotSupportedException("Xrm.Sdk WSDL"); +#endif + } + + return serviceEndpointMetadata; + } + + private static Version GetSDKVersionNumberFromAssembly() + { + string fileVersion = OrganizationServiceProxy.GetXrmSdkAssemblyFileVersion(); + + if (!Version.TryParse(fileVersion, out Version parsedVersion)) + { + // you need to have major.minor version format, else you will get runtime exception + // exception message: Version string portion was too short or too long + parsedVersion = new Version("0.0"); + } + + return parsedVersion; + } + + /// + /// Returns a list of policy import extensions in the importer parameter and adds a SecurityBindingElementImporter if not already present in the list. + /// + /// The WsdlImporter object + /// The list of PolicyImportExtension objects + private static List AddSecurityBindingToPolicyImporter(WsdlImporter importer) + { + List newExts = new List(); +#if NETFRAMEWORK + + KeyedByTypeCollection policyExtensions = importer.PolicyImportExtensions; + SecurityBindingElementImporter securityBindingElementImporter = policyExtensions.Find(); + + if (securityBindingElementImporter != null) + { + policyExtensions.Remove(); + } + else + { + securityBindingElementImporter = new SecurityBindingElementImporter(); + } + + newExts.Add(new AuthenticationPolicyImporter(securityBindingElementImporter)); + newExts.AddRange(policyExtensions); + + return newExts; +#else + + newExts.Add(new AuthenticationPolicyImporter(new SecurityBindingElementImporter())); + return newExts; + //throw new PlatformNotSupportedException("Xrm.Sdk WSDL"); +#endif + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Need to catch any exception here and fail.")] + private static bool TryRetrieveMetadata(MetadataExchangeClient mcli, Uri serviceEndpoint, ServiceEndpointMetadata serviceEndpointMetadata) + { +#if NETFRAMEWORK + bool rethrow = true; + try + { + serviceEndpointMetadata.ServiceMetadata = mcli.GetMetadata(serviceEndpoint, MetadataExchangeClientMode.HttpGet); + serviceEndpointMetadata.ServiceUrls.GeneratedFromAlternate = true; + rethrow = false; + } + catch + { + // We don't care what the exception was at this point. Just let the original be re-thrown. + } + + return rethrow; +#else + throw new PlatformNotSupportedException("Xrm.Sdk WSDL"); +#endif + } + + private static XmlQualifiedName GetPortTypeQName(ContractDescription contract) + { + return new XmlQualifiedName(contract.Name, contract.Namespace); + } + + private static Collection CreateContractCollection(Type contract) + { + return new Collection { ContractDescription.GetContract(contract) }; + } + + private static MetadataExchangeClient CreateMetadataClient(string scheme) + { +#if NETFRAMEWORK + WSHttpBinding mexBinding = null; + + if (string.Compare(scheme, "https", StringComparison.OrdinalIgnoreCase) == 0) + { + mexBinding = MetadataExchangeBindings.CreateMexHttpsBinding() as WSHttpBinding; + } + else + { + mexBinding = MetadataExchangeBindings.CreateMexHttpBinding() as WSHttpBinding; + } + + mexBinding.MaxReceivedMessageSize = int.MaxValue; + + // Set the maximum characters allowed in a table name greater than the default 16384 + mexBinding.ReaderQuotas.MaxNameTableCharCount = int.MaxValue; + var mcli = new MetadataExchangeClient(mexBinding); + + mcli.ResolveMetadataReferences = true; + mcli.MaximumResolvedReferences = 100; + + return mcli; +#else + return null; + //throw new PlatformNotSupportedException("Xrm.Sdk WSDL"); +#endif + } + + public static void ReplaceEndpointAddress(ServiceEndpoint endpoint, Uri adddress) + { + var addressBuilder = new EndpointAddressBuilder(endpoint.Address); + addressBuilder.Uri = adddress; + endpoint.Address = addressBuilder.ToEndpointAddress(); + } + + internal static void AdjustUserNameForWindows(ClientCredentials clientCredentials) + { + ClientExceptionHelper.ThrowIfNull(clientCredentials, "clientCredentials"); + + if (string.IsNullOrWhiteSpace(clientCredentials.UserName.UserName)) + { + return; + } + + NetworkCredential credential = null; + if (clientCredentials.UserName.UserName.IndexOf('@') > -1) + { + var userCreds = clientCredentials.UserName.UserName.Split('@'); + if (userCreds.Length > 1) + { + credential = new NetworkCredential(userCreds[0], clientCredentials.UserName.Password, + userCreds[1]); + } + else + { + credential = new NetworkCredential(userCreds[0], clientCredentials.UserName.Password); + } + } + else if (clientCredentials.UserName.UserName.IndexOf('\\') > -1) + { + var userCreds = clientCredentials.UserName.UserName.Split('\\'); + if (userCreds.Length > 1) + { + credential = new NetworkCredential(userCreds[1], clientCredentials.UserName.Password, + userCreds[0]); + } + else + { + credential = new NetworkCredential(userCreds[0], clientCredentials.UserName.Password); + } + } + else + { + credential = new NetworkCredential(clientCredentials.UserName.UserName, clientCredentials.UserName.Password); + } + + clientCredentials.Windows.ClientCredential = credential; + clientCredentials.UserName.UserName = string.Empty; + clientCredentials.UserName.Password = string.Empty; + } + + private static string GetMexDocument(Uri wsdlUri) + { + string baseMetaDoc = "{0}"; + + StringBuilder sbMetadataBody = new StringBuilder(); + + string intialRequest = GetMexBodyDocument(wsdlUri); + sbMetadataBody = ProcessMexBody(intialRequest, sbMetadataBody); + string resultingBody = string.Format(baseMetaDoc, sbMetadataBody.ToString()); + + sbMetadataBody.Clear(); + // turn into Xml Doc + return resultingBody; + + } + + private static StringBuilder ProcessMexBody(string payload, StringBuilder sbMetadataBody) + { + string metadataSection = "{1}"; + + XmlDocument doc = new XmlDocument(); + doc.LoadXml(payload); + var nsMgr = new XmlNamespaceManager(doc.NameTable); + nsMgr.AddNamespace("wsdl", "http://schemas.xmlsoap.org/wsdl/"); + + var TargetNSForPayload = "http://schemas.microsoft.com/xrm/2011/Contracts"; + var defintionNode = doc.SelectSingleNode(@"wsdl:definitions", nsMgr); + if (defintionNode != null) + { + TargetNSForPayload = defintionNode.Attributes["targetNamespace"]?.Value; + } + sbMetadataBody.AppendFormat(metadataSection, TargetNSForPayload, payload); + var nodes = doc.SelectNodes(@"wsdl:definitions/wsdl:import", nsMgr); + if (nodes.Count > 0) + { + foreach (XmlNode node in nodes) + { + string nextLink = node.Attributes["location"]?.Value; + if (Uri.IsWellFormedUriString(nextLink, UriKind.RelativeOrAbsolute)) + { + // Call Get the body for the next request. + string mexBody = GetMexBodyDocument(new Uri(nextLink)); + if (!string.IsNullOrEmpty(mexBody)) + sbMetadataBody = ProcessMexBody(mexBody, sbMetadataBody); + } + + } + } + + return sbMetadataBody; + + } + + private static string GetMexBodyDocument(Uri wsdlUri) + { + var client = ClientServiceProviders.Instance.GetService().CreateClient("DataverseHttpClientFactory"); + HttpResponseMessage response = null; + try + { + response = client.GetAsync(wsdlUri).ConfigureAwait(false).GetAwaiter().GetResult(); + } + catch (HttpRequestException ex) + { + var errDetails = string.Empty; + if (ex.InnerException is WebException wex) + { + errDetails = $"; details: {wex.Message} ({wex.Status})"; + } + //LogError($"Failed to get response from: {endpoint}; error: {ex.Message}{errDetails}"); + //return details; + } + if (response.StatusCode == HttpStatusCode.NotFound || response.StatusCode == HttpStatusCode.BadRequest) + { + // didn't find endpoint. + //LogError($"Failed to get Authority and Resource error. Attempt to Access Endpoint {endpoint} resulted in {response.StatusCode}."); + //return details; + } + string body = response.Content.ReadAsStringAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + + return body; + + } + + } +} diff --git a/src/GeneralTools/DataverseClient/Client/Connector/OrganizationWebProxyClientAsync.cs b/src/GeneralTools/DataverseClient/Client/Connector/OrganizationWebProxyClientAsync.cs new file mode 100644 index 0000000..654ab46 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Connector/OrganizationWebProxyClientAsync.cs @@ -0,0 +1,241 @@ +namespace Microsoft.PowerPlatform.Dataverse.Client.Connector +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.Net; + using System.Reflection; + using System.ServiceModel.Description; + using System.Threading.Tasks; + using Microsoft.PowerPlatform.Dataverse.Client; + using Microsoft.Xrm.Sdk; + using Microsoft.Xrm.Sdk.Client; + using Microsoft.Xrm.Sdk.Query; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + + internal class OrganizationWebProxyClientAsync : WebProxyClientAsync, IOrganizationServiceAsync + { + public OrganizationWebProxyClientAsync(Uri serviceUrl, bool useStrongTypes) + : base(serviceUrl, useStrongTypes) + { + } + + public OrganizationWebProxyClientAsync(Uri serviceUrl, Assembly strongTypeAssembly) + : base(serviceUrl, strongTypeAssembly) + { + } + + public OrganizationWebProxyClientAsync(Uri serviceUrl, TimeSpan timeout, bool useStrongTypes) + : base(serviceUrl, timeout, useStrongTypes) + { + } + + public OrganizationWebProxyClientAsync(Uri uri, TimeSpan timeout, Assembly strongTypeAssembly) + : base(uri, timeout, strongTypeAssembly) + { + } + + #region Properties + + internal bool OfflinePlayback { get; set; } + + public string SyncOperationType { get; set; } + + public Guid CallerId { get; set; } + + public UserType userType { get; set; } + + public Guid CallerRegardingObjectId { get; set; } + + internal int LanguageCodeOverride { get; set; } + + #endregion + + #region IOrganizationService implementation + + public void Associate(string entityName, Guid entityId, Relationship relationship, + EntityReferenceCollection relatedEntities) + { + AssociateCore(entityName, entityId, relationship, relatedEntities); + } + + public Task AssociateAsync(string entityName, Guid entityId, Relationship relationship, + EntityReferenceCollection relatedEntities) + { + return AssociateAsyncCore(entityName, entityId, relationship, relatedEntities); + } + + public Guid Create(Entity entity) + { + return CreateCore(entity); + } + + public Task CreateAsync(Entity entity) + { + return CreateAsyncCore(entity); + } + + public void Delete(string entityName, Guid id) + { + DeleteCore(entityName, id); + } + + public Task DeleteAsync(string entityName, Guid id) + { + return DeleteAsyncCore(entityName, id); + } + + public void Disassociate(string entityName, Guid entityId, Relationship relationship, + EntityReferenceCollection relatedEntities) + { + DisassociateCore(entityName, entityId, relationship, relatedEntities); + } + + public Task DisassociateAsync(string entityName, Guid entityId, Relationship relationship, + EntityReferenceCollection relatedEntities) + { + return DisassociateAsyncCore(entityName, entityId, relationship, relatedEntities); + } + + public OrganizationResponse Execute(OrganizationRequest request) + { + return ExecuteCore(request); + } + + public Task ExecuteAsync(OrganizationRequest request) + { + return ExecuteAsyncCore(request); + } + + public Entity Retrieve(string entityName, Guid id, ColumnSet columnSet) + { + return RetrieveCore(entityName, id, columnSet); + } + + public Task RetrieveAsync(string entityName, Guid id, ColumnSet columnSet) + { + return RetrieveAsyncCore(entityName, id, columnSet); + } + + public EntityCollection RetrieveMultiple(QueryBase query) + { + return RetrieveMultipleCore(query); + } + + public Task RetrieveMultipleAsync(QueryBase query) + { + return RetrieveMultipleAsyncCore(query); + } + + public void Update(Entity entity) + { + UpdateCore(entity); + } + + public Task UpdateAsync(Entity entity) + { + return UpdateAsyncCore(entity); + } + + #endregion + + #region Protected IOrganizationService CoreMembers + + protected internal virtual Guid CreateCore(Entity entity) + { + return ExecuteAction(() => Channel.Create(entity)); + } + + protected Task CreateAsyncCore(Entity entity) + { + return ExecuteAction(() => Channel.CreateAsync(entity)); + } + + protected internal virtual Entity RetrieveCore(string entityName, Guid id, ColumnSet columnSet) + { + return ExecuteAction(() => Channel.Retrieve(entityName, id, columnSet)); + } + + protected internal virtual Task RetrieveAsyncCore(string entityName, Guid id, ColumnSet columnSet) + { + return ExecuteAction(() => Channel.RetrieveAsync(entityName, id, columnSet)); + } + + protected internal virtual void UpdateCore(Entity entity) + { + ExecuteAction(() => Channel.Update(entity)); + } + + protected internal virtual Task UpdateAsyncCore(Entity entity) + { + return ExecuteAction(() => Channel.UpdateAsync(entity)); + } + + protected internal virtual void DeleteCore(string entityName, Guid id) + { + ExecuteAction(() => Channel.Delete(entityName, id)); + } + + protected internal virtual Task DeleteAsyncCore(string entityName, Guid id) + { + return ExecuteAction(() => Channel.DeleteAsync(entityName, id)); + } + + protected internal virtual OrganizationResponse ExecuteCore(OrganizationRequest request) + { + return ExecuteAction(() => Channel.Execute(request)); + } + + protected internal virtual Task ExecuteAsyncCore(OrganizationRequest request) + { + return ExecuteAction(() => Channel.ExecuteAsync(request)); + } + + protected internal virtual void AssociateCore(string entityName, Guid entityId, Relationship relationship, + EntityReferenceCollection relatedEntities) + { + ExecuteAction(() => Channel.Associate(entityName, entityId, relationship, relatedEntities)); + } + + protected internal virtual Task AssociateAsyncCore(string entityName, Guid entityId, Relationship relationship, + EntityReferenceCollection relatedEntities) + { + return ExecuteAction(() => Channel.AssociateAsync(entityName, entityId, relationship, relatedEntities)); + } + + protected internal virtual void DisassociateCore(string entityName, Guid entityId, Relationship relationship, + EntityReferenceCollection relatedEntities) + { + ExecuteAction(() => Channel.Disassociate(entityName, entityId, relationship, relatedEntities)); + } + + protected internal virtual Task DisassociateAsyncCore(string entityName, Guid entityId, Relationship relationship, + EntityReferenceCollection relatedEntities) + { + return ExecuteAction(() => Channel.DisassociateAsync(entityName, entityId, relationship, relatedEntities)); + } + + protected internal virtual EntityCollection RetrieveMultipleCore(QueryBase query) + { + return ExecuteAction(() => Channel.RetrieveMultiple(query)); + } + + protected internal virtual Task RetrieveMultipleAsyncCore(QueryBase query) + { + return ExecuteAction(() => Channel.RetrieveMultipleAsync(query)); + } + + #endregion Protected Members + + #region Protected Methods + + protected override WebProxyClientContextAsyncInitializer CreateNewInitializer() + { + return new OrganizationWebProxyClientAsyncContextInitializer(this); + } + + #endregion + } +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + +} diff --git a/src/GeneralTools/DataverseClient/Client/Connector/OrganizationWebProxyClientContextInitializer.cs b/src/GeneralTools/DataverseClient/Client/Connector/OrganizationWebProxyClientContextInitializer.cs new file mode 100644 index 0000000..e03c5c6 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Connector/OrganizationWebProxyClientContextInitializer.cs @@ -0,0 +1,88 @@ +namespace Microsoft.PowerPlatform.Dataverse.Client.Connector +{ + using Microsoft.Xrm.Sdk.Client; + using Microsoft.PowerPlatform.Dataverse.Client; + using System; + using System.ServiceModel; + using System.ServiceModel.Channels; + using Microsoft.Xrm.Sdk.XmlNamespaces; + + /// + /// Manages context for sdk calls + /// + internal sealed class OrganizationWebProxyClientAsyncContextInitializer : + WebProxyClientContextAsyncInitializer + { + public OrganizationWebProxyClientAsyncContextInitializer(OrganizationWebProxyClientAsync proxy) + : base(proxy) + { + Initialize(); + } + + #region Properties + + private OrganizationWebProxyClientAsync OrganizationWebProxyClient + { + get { return ServiceProxy as OrganizationWebProxyClientAsync; } + } + + #endregion + + #region Private Methods + + private void Initialize() + { + if (ServiceProxy == null) + { + return; + } + + AddTokenToHeaders(); + + if (ServiceProxy != null) + { + if (OrganizationWebProxyClient.OfflinePlayback) + { + OperationContext.Current.OutgoingMessageHeaders.Add(MessageHeader.CreateHeader(SdkHeaders.IsOfflinePlayback, + V5.Contracts, true)); + } + + if (OrganizationWebProxyClient.CallerId != Guid.Empty) + { + OperationContext.Current.OutgoingMessageHeaders.Add(MessageHeader.CreateHeader(SdkHeaders.CallerId, + V5.Contracts, + OrganizationWebProxyClient.CallerId)); + } + + if (OrganizationWebProxyClient.CallerRegardingObjectId != Guid.Empty) + { + OperationContext.Current.OutgoingMessageHeaders.Add(MessageHeader.CreateHeader(SdkHeaders.CallerRegardingObjectId, + V5.Contracts, + OrganizationWebProxyClient.CallerRegardingObjectId)); + } + + if (OrganizationWebProxyClient.LanguageCodeOverride != 0) + { + OperationContext.Current.OutgoingMessageHeaders.Add(MessageHeader.CreateHeader(SdkHeaders.LanguageCodeOverride, + V5.Contracts, + OrganizationWebProxyClient.LanguageCodeOverride)); + } + + if (OrganizationWebProxyClient.SyncOperationType != null) + { + OperationContext.Current.OutgoingMessageHeaders.Add(MessageHeader.CreateHeader(SdkHeaders.OutlookSyncOperationType, + V5.Contracts, + OrganizationWebProxyClient.SyncOperationType)); + } + + OperationContext.Current.OutgoingMessageHeaders.Add(MessageHeader.CreateHeader(SdkHeaders.UserType, + V5.Contracts, + OrganizationWebProxyClient.userType)); + + AddCommonHeaders(); + } + } + + #endregion + } +} diff --git a/src/GeneralTools/DataverseClient/Client/Connector/WebProxyClient.cs b/src/GeneralTools/DataverseClient/Client/Connector/WebProxyClient.cs new file mode 100644 index 0000000..e912883 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Connector/WebProxyClient.cs @@ -0,0 +1,222 @@ +using System; +using System.Diagnostics; +using System.Reflection; +using System.Security.Permissions; +using System.ServiceModel; +using System.ServiceModel.Channels; +using System.ServiceModel.Description; +using Microsoft.Xrm.Sdk.Client; + +namespace Microsoft.PowerPlatform.Dataverse.Client.Connector +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.CodeQuality.Analyzers", "CA1063: Implement IDisposable correctly", Justification = "FxCop Bankruptcy")] + internal abstract class WebProxyClientAsync : ClientBase, IDisposable + where TService : class + { + #region Fields + + private string _xrmSdkAssemblyFileVersion; + + #endregion + + protected WebProxyClientAsync(Uri serviceUrl, bool useStrongTypes) + : base(CreateServiceEndpoint(serviceUrl, useStrongTypes, Utilites.DefaultTimeout, null)) + { + } + + protected WebProxyClientAsync(Uri serviceUrl, Assembly strongTypeAssembly) + : base(CreateServiceEndpoint(serviceUrl, true, Utilites.DefaultTimeout, strongTypeAssembly)) + { + } + + protected WebProxyClientAsync(Uri serviceUrl, TimeSpan timeout, bool useStrongTypes) + : base(CreateServiceEndpoint(serviceUrl, useStrongTypes, timeout, null)) + { + } + + protected WebProxyClientAsync(Uri serviceUrl, TimeSpan timeout, Assembly strongTypeAssembly) + : base(CreateServiceEndpoint(serviceUrl, true, timeout, strongTypeAssembly)) + { + } + + #region Properties + + public string HeaderToken { get; set; } + + public string SdkClientVersion { get; set; } + + internal string ClientAppName { get; set; } + + internal string ClientAppVersion { get; set; } + + #endregion + + #region Protected Methods + + protected abstract WebProxyClientContextAsyncInitializer CreateNewInitializer(); + + #endregion + + internal void ExecuteAction(Action action) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + using (CreateNewInitializer()) + { + action(); + } + } + + internal TResult ExecuteAction(Func action) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + using (CreateNewInitializer()) + { + return action(); + } + } + + protected static ServiceEndpoint CreateServiceEndpoint(Uri serviceUrl, bool useStrongTypes, TimeSpan timeout, + Assembly strongTypeAssembly) + { + ServiceEndpoint serviceEndpoint = CreateBaseServiceEndpoint(serviceUrl, timeout); + + // Since we have no way of know if the assembly is different, always remove the old one. + + // Reafactored to support both .net full framework and .net core. + if (serviceEndpoint.EndpointBehaviors.Contains(typeof(ProxyTypesBehavior))) + { + var behavior = serviceEndpoint.EndpointBehaviors[typeof(ProxyTypesBehavior)]; + if (behavior != null) + { + serviceEndpoint.EndpointBehaviors.Remove(behavior); + } + } + + if (useStrongTypes) + { + serviceEndpoint.EndpointBehaviors.Add(strongTypeAssembly != null + ? new ProxyTypesBehavior(strongTypeAssembly) + : new ProxyTypesBehavior()); + } + + return serviceEndpoint; + } + + private static ServiceEndpoint CreateBaseServiceEndpoint(Uri serviceUrl, TimeSpan timeout) + { + Binding binding = GetBinding(serviceUrl, timeout); + + var endpointAddress = new EndpointAddress(serviceUrl); + + ContractDescription contract = ContractDescription.GetContract(typeof(TService)); + + var endpoint = new ServiceEndpoint(contract, binding, endpointAddress); + + // Loop through the behaviors for the endpoint and increase the maximum number of objects in the graph + foreach (OperationDescription operation in endpoint.Contract.Operations) + { + // Retrieve the behavior for the operator + var serializerBehavior = operation.Behaviors.Find(); + if (serializerBehavior != null) + { + serializerBehavior.MaxItemsInObjectGraph = int.MaxValue; + } + } + + return endpoint; + } + + protected static Binding GetBinding(Uri serviceUrl, TimeSpan timeout) + { + var binding = new BasicHttpBinding(serviceUrl.Scheme == "https" + ? BasicHttpSecurityMode.Transport + : BasicHttpSecurityMode.TransportCredentialOnly); + + binding.MaxReceivedMessageSize = int.MaxValue; + binding.MaxBufferSize = int.MaxValue; + + binding.SendTimeout = timeout; + binding.ReceiveTimeout = timeout; + binding.OpenTimeout = timeout; + + // Set the properties on the reader quotas + binding.ReaderQuotas.MaxStringContentLength = int.MaxValue; + binding.ReaderQuotas.MaxArrayLength = int.MaxValue; + binding.ReaderQuotas.MaxBytesPerRead = int.MaxValue; + binding.ReaderQuotas.MaxNameTableCharCount = int.MaxValue; + + return binding; + } + + /// + /// Get's the file version of the Xrm Sdk assembly that is loaded in the current client domain. + /// For Sdk clients called via the OrganizationServiceProxy this is the version of the local Microsoft.Xrm.Sdk dll used + /// by the Client App. + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2143:TransparentMethodsShouldNotDemandFxCopRule")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2141:TransparentMethodsMustNotSatisfyLinkDemandsFxCopRule")] + [PermissionSet(SecurityAction.Demand, Unrestricted = true)] + internal string GetXrmSdkAssemblyFileVersion() + { + if (string.IsNullOrEmpty(_xrmSdkAssemblyFileVersion)) + { + string[] assembliesToCheck = { "Microsoft.Xrm.Sdk.dll" }; + Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); + + foreach (string assemblyToCheck in assembliesToCheck) + { + foreach (Assembly assembly in assemblies) + { + if (assembly.ManifestModule.Name.Equals(assemblyToCheck, StringComparison.OrdinalIgnoreCase) && + !string.IsNullOrEmpty(assembly.Location) && + System.IO.File.Exists(assembly.Location)) + { + _xrmSdkAssemblyFileVersion = FileVersionInfo.GetVersionInfo(assembly.Location).FileVersion; + break; + } + } + } + } + + // If the assembly is embedded as resource and loaded from memory, there is no physical file on disk to check for file version + if (string.IsNullOrEmpty(_xrmSdkAssemblyFileVersion)) + { + _xrmSdkAssemblyFileVersion = "9.1.2.3"; + } + + return _xrmSdkAssemblyFileVersion; + } + + #region IDisposable implementation + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + #region Protected Methods + + protected virtual void Dispose(bool disposing) + { + } + + #endregion + + ~WebProxyClientAsync() + { + Dispose(false); + } + + #endregion + } +} diff --git a/src/GeneralTools/DataverseClient/Client/Connector/WebProxyClientContextInitializer.cs b/src/GeneralTools/DataverseClient/Client/Connector/WebProxyClientContextInitializer.cs new file mode 100644 index 0000000..11f0591 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Connector/WebProxyClientContextInitializer.cs @@ -0,0 +1,122 @@ +using System; +using System.ServiceModel; +using System.Net; +using System.ServiceModel.Channels; +using Microsoft.Xrm.Sdk.XmlNamespaces; +using Microsoft.Xrm.Sdk.Client; +using Microsoft.Xrm.Sdk.WebServiceClient; + +namespace Microsoft.PowerPlatform.Dataverse.Client.Connector +{ + /// + /// Manages context for sdk calls + /// + internal abstract class WebProxyClientContextAsyncInitializer : IDisposable + where TService : class + { + #region Fields + + private OperationContextScope _operationScope; + + #endregion + + /// + /// Initializes a new instance of the class. + /// Constructs a context initializer + /// + /// sdk proxy + protected WebProxyClientContextAsyncInitializer(WebProxyClientAsync proxy) + { + ServiceProxy = proxy; + + Initialize(proxy); + } + + #region Properties + + public WebProxyClientAsync ServiceProxy { get; private set; } + + #endregion + + #region Protected Methods + + protected void AddTokenToHeaders() + { + var request = new HttpRequestMessageProperty(); + request.Headers[HttpRequestHeader.Authorization.ToString()] = "Bearer " + ServiceProxy.HeaderToken; + OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = request; + } + + protected void AddCommonHeaders() + { + if (!string.IsNullOrEmpty(ServiceProxy.ClientAppName)) + { + OperationContext.Current.OutgoingMessageHeaders.Add(MessageHeader.CreateHeader(SdkHeaders.ClientAppName, + V5.Contracts, + ServiceProxy.ClientAppName)); + } + + if (!string.IsNullOrEmpty(ServiceProxy.ClientAppVersion)) + { + OperationContext.Current.OutgoingMessageHeaders.Add(MessageHeader.CreateHeader(SdkHeaders.ClientAppVersion, + V5.Contracts, + ServiceProxy.ClientAppVersion)); + } + + if (!string.IsNullOrEmpty(ServiceProxy.SdkClientVersion)) + { + OperationContext.Current.OutgoingMessageHeaders.Add(MessageHeader.CreateHeader(SdkHeaders.SdkClientVersion, + V5.Contracts, + ServiceProxy.SdkClientVersion)); + } + else + { + string fileVersion = ServiceProxy.GetXrmSdkAssemblyFileVersion(); + if (!string.IsNullOrEmpty(fileVersion)) + { + OperationContext.Current.OutgoingMessageHeaders.Add(MessageHeader.CreateHeader(SdkHeaders.SdkClientVersion, + V5.Contracts, + fileVersion)); + } + } + } + + protected void Initialize(ClientBase proxy) + { + // This call initializes operation context scope for the call using the channel + _operationScope = new OperationContextScope(proxy.InnerChannel); + } + + #endregion + + #region IDisposable implementation + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + #region Protected Methods + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (_operationScope != null) + { + _operationScope.Dispose(); + } + } + } + + #endregion + + ~WebProxyClientContextAsyncInitializer() + { + Dispose(false); + } + + #endregion + } +} diff --git a/src/GeneralTools/DataverseClient/Client/DataverseTelemetryBehaviors.cs b/src/GeneralTools/DataverseClient/Client/DataverseTelemetryBehaviors.cs index 2d4a7d4..017485c 100644 --- a/src/GeneralTools/DataverseClient/Client/DataverseTelemetryBehaviors.cs +++ b/src/GeneralTools/DataverseClient/Client/DataverseTelemetryBehaviors.cs @@ -1,3 +1,7 @@ +using Microsoft.Extensions.Options; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.PowerPlatform.Dataverse.Client.Model; +using Microsoft.PowerPlatform.Dataverse.Client.Utils; using System; using System.Collections; using System.Configuration; @@ -21,10 +25,12 @@ internal class DataverseTelemetryBehaviors : IEndpointBehavior, IClientMessageIn private ConnectionService _callerCdsConnectionServiceHandler; private int _maxFaultSize = -1; private int _maxReceivedMessageSize = -1; + private int _maxBufferPoolSize = -1; private string _userAgent; #endregion #region Const + private const int MAXBUFFERPOOLDEFAULT = 524288; private const int MAXFAULTSIZEDEFAULT = 131072; private const int MAXRECVMESSAGESIZEDEFAULT = 2147483647; #endregion @@ -35,6 +41,8 @@ internal class DataverseTelemetryBehaviors : IEndpointBehavior, IClientMessageIn public DataverseTelemetryBehaviors(ConnectionService cli) { _callerCdsConnectionServiceHandler = cli; + IOptions _configuration = ClientServiceProviders.Instance.GetService>(); + // reading overrides from app config if present.. // these values override the values that are set on the client from the server. @@ -50,9 +58,9 @@ public DataverseTelemetryBehaviors(ConnectionService cli) _userAgent = $"{_userAgent} (DataverseSvcClient:{Environs.FileVersion})"; - if (_maxFaultSize == -1 && ConfigurationManager.AppSettings.AllKeys.Contains("MaxFaultSizeOverride")) + if (_maxFaultSize == -1 && !string.IsNullOrEmpty(_configuration.Value.MaxFaultSizeOverride)) { - var maxFaultSz = ConfigurationManager.AppSettings["MaxFaultSizeOverride"]; + var maxFaultSz = _configuration.Value.MaxFaultSizeOverride; if (maxFaultSz is string && !string.IsNullOrWhiteSpace(maxFaultSz)) { int.TryParse(maxFaultSz, out _maxFaultSize); @@ -69,9 +77,9 @@ public DataverseTelemetryBehaviors(ConnectionService cli) logg.Log($"Failed to parse MaxFaultSizeOverride property. Value found: {maxFaultSz}. MaxFaultSizeOverride must be a valid integer.", System.Diagnostics.TraceEventType.Warning); } - if (_maxReceivedMessageSize == -1 && ConfigurationManager.AppSettings.AllKeys.Contains("MaxReceivedMessageSizeOverride")) + if (_maxReceivedMessageSize == -1 && !string.IsNullOrEmpty(_configuration.Value.MaxReceivedMessageSizeOverride)) { - var maxRecvSz = ConfigurationManager.AppSettings["MaxReceivedMessageSizeOverride"]; + var maxRecvSz = _configuration.Value.MaxReceivedMessageSizeOverride; if (maxRecvSz is string && !string.IsNullOrWhiteSpace(maxRecvSz)) { int.TryParse(maxRecvSz, out _maxReceivedMessageSize); @@ -87,10 +95,30 @@ public DataverseTelemetryBehaviors(ConnectionService cli) else logg.Log($"Failed to parse MaxReceivedMessageSizeOverride property. Value found: {maxRecvSz}. MaxReceivedMessageSizeOverride must be a valid integer.", System.Diagnostics.TraceEventType.Warning); } + + if (_maxBufferPoolSize == -1 && !string.IsNullOrEmpty(_configuration.Value.MaxBufferPoolSizeOveride)) + { + var maxBufferPoolSz = _configuration.Value.MaxBufferPoolSizeOveride; + if (maxBufferPoolSz is string && !string.IsNullOrWhiteSpace(maxBufferPoolSz)) + { + int.TryParse(maxBufferPoolSz, out _maxBufferPoolSize); + if (_maxBufferPoolSize != -1) + { + if (_maxBufferPoolSize < MAXBUFFERPOOLDEFAULT) + { + _maxBufferPoolSize = -1; + logg.Log($"Failed to set MaxBufferPoolSizeOveride property. Value found: {maxBufferPoolSz}. Size must be larger then {MAXBUFFERPOOLDEFAULT}.", System.Diagnostics.TraceEventType.Warning); + } + } + } + else + logg.Log($"Failed to parse MaxBufferPoolSizeOveride property. Value found: {maxBufferPoolSz}. MaxReceivedMessageSizeOverride must be a valid integer.", System.Diagnostics.TraceEventType.Warning); + } + } catch (Exception ex) { - logg.Log("Failed to process binding override properties, Only MaxFaultSizeOverride and MaxReceivedMessageSizeOverride are supported and must be integers.", System.Diagnostics.TraceEventType.Warning, ex); + logg.Log("Failed to process binding override properties, Only MaxFaultSizeOverride, MaxReceivedMessageSizeOverride and MaxBufferPoolSizeOveride are supported and must be integers.", System.Diagnostics.TraceEventType.Warning, ex); } finally { @@ -131,6 +159,14 @@ public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRu binding.MaxReceivedMessageSize = _maxReceivedMessageSize; } } + + if (_maxBufferPoolSize != -1) + { + if (endpoint.Binding is BasicHttpBinding binding1) + { + binding1.MaxBufferPoolSize = _maxBufferPoolSize; + } + } } public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) @@ -158,13 +194,13 @@ public void AfterReceiveReply(ref Message reply, object correlationState) string cookieHeader = httpResponseMessage.Headers[Utilities.ResponseHeaders.SETCOOKIE]; if (cookieHeader != null) { - _callerCdsConnectionServiceHandler.CurrentCookieCollection = Utilities.GetAllCookiesFromHeader(httpResponseMessage.Headers[Utilities.ResponseHeaders.SETCOOKIE] , _callerCdsConnectionServiceHandler.CurrentCookieCollection); + _callerCdsConnectionServiceHandler.CurrentCookieCollection = Utilities.GetAllCookiesFromHeader(httpResponseMessage.Headers[Utilities.ResponseHeaders.SETCOOKIE], _callerCdsConnectionServiceHandler.CurrentCookieCollection); } string dregreeofparallelismHint = httpResponseMessage.Headers[Utilities.ResponseHeaders.RECOMMENDEDDEGREESOFPARALLELISM]; if (!string.IsNullOrEmpty(dregreeofparallelismHint)) { - if(int.TryParse(dregreeofparallelismHint, out int idop)) + if (int.TryParse(dregreeofparallelismHint, out int idop)) { if (_callerCdsConnectionServiceHandler != null) { @@ -253,6 +289,8 @@ public object BeforeSendRequest(ref Message request, IClientChannel channel) { if (_callerCdsConnectionServiceHandler.WebClient != null) callerId = _callerCdsConnectionServiceHandler.WebClient.CallerId; + if (_callerCdsConnectionServiceHandler.OnPremClient != null) + callerId = _callerCdsConnectionServiceHandler.OnPremClient.CallerId; } if (callerId == Guid.Empty) // Prefer the Caller ID over the AADObjectID. diff --git a/src/GeneralTools/DataverseClient/Client/DataverseTraceLogger.cs b/src/GeneralTools/DataverseClient/Client/DataverseTraceLogger.cs index 086ae5e..8b73ab3 100644 --- a/src/GeneralTools/DataverseClient/Client/DataverseTraceLogger.cs +++ b/src/GeneralTools/DataverseClient/Client/DataverseTraceLogger.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Microsoft.PowerPlatform.Dataverse.Client.Utils; using Microsoft.Rest; using Microsoft.Xrm.Sdk; @@ -96,7 +96,7 @@ public DataverseTraceLogger(ILogger logger) public override void ResetLastError() { if (base.LastError.Length > 0) - base.LastError.Remove(0, LastError.Length - 1); + base.LastError = base.LastError.Remove(0, LastError.Length - 1); LastException = null; _ActiveExceptionsList.Clear(); } @@ -229,7 +229,7 @@ public void LogRetry(int retryCount, OrganizationRequest req, TimeSpan retryPaus } else { - Log($"Retry No={retryCount} Retry=Started IsThrottle={isThrottled} Delay={retryPauseTimeRunning} for Command {reqName}", TraceEventType.Verbose); + Log($"Retry No={retryCount} Retry=Started IsThrottle={isThrottled} Delay={retryPauseTimeRunning} for Command {reqName}", TraceEventType.Warning); } } @@ -277,8 +277,16 @@ public void LogFailure(OrganizationRequest req, Guid requestTrackingId, Guid? se if (req != null) { Log(string.Format(CultureInfo.InvariantCulture, "{6}Failed to Execute Command - {0}{1} : {5}RequestID={2} {3}: {8} duration={4} ExceptionMessage = {7}", - req.RequestName, disableConnectionLocking ? " : DisableCrossThreadSafeties=true :" : string.Empty, requestTrackingId.ToString(), LockWait == TimeSpan.Zero ? string.Empty : string.Format(": LockWaitDuration={0} ", LockWait.ToString()), logDt.Elapsed.ToString(), - sessionTrackingId.HasValue && sessionTrackingId.Value != Guid.Empty ? $"SessionID={sessionTrackingId} : " : "", isTerminalFailure ? "[TerminalFailure] " : "", ex.Message, errorStringCheck), TraceEventType.Error, ex); + req.RequestName, + disableConnectionLocking ? " : DisableCrossThreadSafeties=true :" : string.Empty, + requestTrackingId.ToString(), + LockWait == TimeSpan.Zero ? string.Empty : string.Format(": LockWaitDuration={0} ", LockWait.ToString()), + logDt.Elapsed.ToString(), + sessionTrackingId.HasValue && sessionTrackingId.Value != Guid.Empty ? $"SessionID={sessionTrackingId} : " : "", + isTerminalFailure ? "[TerminalFailure] " : "", + ex.Message, + errorStringCheck), + TraceEventType.Error, ex); } else if (ex is HttpOperationException httpOperationException) { diff --git a/src/GeneralTools/DataverseClient/Client/DynamicsFileLogTraceListener.cs b/src/GeneralTools/DataverseClient/Client/DynamicsFileLogTraceListener.cs index 59accbd..015ba38 100644 --- a/src/GeneralTools/DataverseClient/Client/DynamicsFileLogTraceListener.cs +++ b/src/GeneralTools/DataverseClient/Client/DynamicsFileLogTraceListener.cs @@ -1,9 +1,9 @@ -using System; +using System; using System.Collections.Generic; using System.Configuration; using System.Linq; using System.Text; -#if NET462 +#if NETFRAMEWORK using Microsoft.VisualBasic.Logging; #endif using System.IO; @@ -14,7 +14,7 @@ namespace Microsoft.PowerPlatform.Dataverse.Client { -#if NET462 // Only available in 4.6.2 right now. +#if NETFRAMEWORK // Only available in 4.6.2 right now. /// /// Extension to the FileLogTraceListner class. /// diff --git a/src/GeneralTools/DataverseClient/Client/Extensions/BatchExtensions.cs b/src/GeneralTools/DataverseClient/Client/Extensions/BatchExtensions.cs new file mode 100644 index 0000000..21561ca --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Extensions/BatchExtensions.cs @@ -0,0 +1,321 @@ +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Messages; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.ServiceModel; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PowerPlatform.Dataverse.Client.Extensions +{ + /// + /// Dataverse Service Client extensions for batch operations. + /// + public static class BatchExtensions + { + #region Batch Interface methods. + /// + /// Create a Batch Request for executing batch operations. This returns an ID that will be used to identify a request as a batch request vs a "normal" request. + /// + /// Name of the Batch + /// Should Results be returned + /// Should the process continue on an error. + /// ServiceClient + /// + public static Guid CreateBatchOperationRequest(this ServiceClient serviceClient, string batchName, bool returnResults = true, bool continueOnError = false) + { + #region PreChecks + serviceClient._logEntry.ResetLastError(); + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); + return Guid.Empty; + } + + if (!serviceClient.IsBatchOperationsAvailable) + { + serviceClient._logEntry.Log("Batch Operations are not available", TraceEventType.Error); + return Guid.Empty; + } + #endregion + + Guid guBatchId = Guid.Empty; + if (serviceClient._batchManager != null) + { + // Try to create a new Batch here. + guBatchId = serviceClient._batchManager.CreateNewBatch(batchName, returnResults, continueOnError); + } + return guBatchId; + } + + /// + /// Returns the batch id for a given batch name. + /// + /// Name of Batch + /// ServiceClient + /// + public static Guid GetBatchOperationIdRequestByName(this ServiceClient serviceClient, string batchName) + { + #region PreChecks + serviceClient._logEntry.ResetLastError(); + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); + return Guid.Empty; + } + + if (!serviceClient.IsBatchOperationsAvailable) + { + serviceClient._logEntry.Log("Batch Operations are not available", TraceEventType.Error); + return Guid.Empty; + } + #endregion + + if (serviceClient._batchManager != null) + { + var b = serviceClient._batchManager.GetRequestBatchByName(batchName); + if (b != null) + return b.BatchId; + } + return Guid.Empty; + } + + /// + /// Returns the organization request at a give position + /// + /// ID of the batch + /// Position + /// ServiceClient + /// + public static OrganizationRequest GetBatchRequestAtPosition(this ServiceClient serviceClient, Guid batchId, int position) + { + #region PreChecks + serviceClient._logEntry.ResetLastError(); + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); + return null; + } + + if (!serviceClient.IsBatchOperationsAvailable) + { + serviceClient._logEntry.Log("Batch Operations are not available", TraceEventType.Error); + return null; + } + #endregion + + RequestBatch b = serviceClient.GetBatchById(batchId); + if (b != null) + { + if (b.BatchItems.Count >= position) + return b.BatchItems[position].Request; + } + return null; + } + + /// + /// Release a batch from the stack + /// Once you have completed using a batch, you must release it from the system. + /// + /// ServiceClient + /// ID of the batch + public static void ReleaseBatchInfoById(this ServiceClient serviceClient, Guid batchId) + { + #region PreChecks + serviceClient._logEntry.ResetLastError(); + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); + return; + } + + if (!serviceClient.IsBatchOperationsAvailable) + { + serviceClient._logEntry.Log("Batch Operations are not available", TraceEventType.Error); + return; + } + #endregion + + if (serviceClient._batchManager != null) + serviceClient._batchManager.RemoveBatch(batchId); + + } + + /// + /// Returns a request batch by BatchID + /// + /// ServiceClient + /// ID of the batch + /// + public static RequestBatch GetBatchById(this ServiceClient serviceClient, Guid batchId) + { + #region PreChecks + serviceClient._logEntry.ResetLastError(); + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); + return null; + } + + if (!serviceClient.IsBatchOperationsAvailable) + { + serviceClient._logEntry.Log("Batch Operations are not available", TraceEventType.Error); + return null; + } + #endregion + + if (serviceClient._batchManager != null) + { + return serviceClient._batchManager.GetRequestBatchById(batchId); + } + return null; + } + + /// + /// Executes the batch command and then parses the retrieved items into a list. + /// If there exists a exception then the LastException would be filled with the first item that has the exception. + /// + /// ServiceClient + /// ID of the batch to run + /// results which is a list of responses(type >> ]]>) in the order of each request or null or complete failure + public static List>> RetrieveBatchResponse(this ServiceClient serviceClient, Guid batchId) + { + ExecuteMultipleResponse results = serviceClient.ExecuteBatch(batchId); + if (results == null) + { + return null; + } + if (results.IsFaulted) + { + foreach (var response in results.Responses) + { + if (response.Fault != null) + { + FaultException ex = new FaultException(response.Fault, new FaultReason(new FaultReasonText(response.Fault.Message))); + + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "Failed to Execute Batch - {0}", batchId), TraceEventType.Verbose); + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ BatchExecution failed - : {0}\n\r{1}", response.Fault.Message, response.Fault.ErrorDetails.ToString()), TraceEventType.Error, ex); + break; + } + } + } + List>> retrieveMultipleResponseList = new List>>(); + foreach (var response in results.Responses) + { + if (response.Response != null) + { + retrieveMultipleResponseList.Add(QueryExtensions.CreateResultDataSet(((RetrieveMultipleResponse)response.Response).EntityCollection)); + } + } + return retrieveMultipleResponseList; + } + + /// + /// Begins running the Batch command. + /// + /// ServiceClient + /// ID of the batch to run + /// true if the batch begins, false if not. + public static ExecuteMultipleResponse ExecuteBatch(this ServiceClient serviceClient, Guid batchId) + { + #region PreChecks + serviceClient._logEntry.ResetLastError(); + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); + return null; + } + + if (!serviceClient.IsBatchOperationsAvailable) + { + serviceClient._logEntry.Log("Batch Operations are not available", TraceEventType.Error); + return null; + } + #endregion + + if (serviceClient._batchManager != null) + { + var b = serviceClient._batchManager.GetRequestBatchById(batchId); + if (b.Status == BatchStatus.Complete || b.Status == BatchStatus.Running) + { + serviceClient._logEntry.Log("Batch is not in the correct state to run", TraceEventType.Error); + return null; + } + + if (!(b.BatchItems.Count > 0)) + { + serviceClient._logEntry.Log("No Items in the batch", TraceEventType.Error); + return null; + } + + // Ready to run the batch. + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "Executing Batch {0}|{1}, Sending {2} events.", b.BatchId, b.BatchName, b.BatchItems.Count), TraceEventType.Verbose); + ExecuteMultipleRequest req = new ExecuteMultipleRequest(); + req.Settings = b.BatchRequestSettings; + OrganizationRequestCollection reqstList = new OrganizationRequestCollection(); + + // Make sure the batch is ordered. + reqstList.AddRange(b.BatchItems.Select(s => s.Request)); + + req.Requests = reqstList; + b.Status = BatchStatus.Running; + ExecuteMultipleResponse resp = (ExecuteMultipleResponse)serviceClient.Command_Execute(req, "Execute Batch Command"); + // Need to add retry logic here to deal with a "server busy" status. + b.Status = BatchStatus.Complete; + if (resp != null) + { + if (resp.IsFaulted) + serviceClient._logEntry.Log("Batch request faulted.", TraceEventType.Warning); + b.BatchResults = resp; + return b.BatchResults; + } + serviceClient._logEntry.Log("Batch request faulted - No Results.", TraceEventType.Warning); + } + return null; + } + + /// + /// Adds a request to a batch with display and handling logic + /// will fail out if batching is not enabled. + /// + /// ID of the batch to add too + /// Organization request to Add + /// Batch Add Text, this is the text that will be reflected when the batch is added - appears in the batch diags + /// Success Added Batch - appears in webSvcActions diag + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// ServiceClient + /// + internal static bool AddRequestToBatch(this ServiceClient serviceClient, Guid batchId, OrganizationRequest req, string batchTagText, string successText, bool bypassPluginExecution) + { + if (batchId != Guid.Empty) + { + // if request should bypass plugin exec. + if (bypassPluginExecution && + Utilities.FeatureVersionMinimums.IsFeatureValidForEnviroment(serviceClient.ConnectedOrgVersion, Utilities.FeatureVersionMinimums.AllowBypassCustomPlugin)) + req.Parameters.Add(Utilities.RequestHeaders.BYPASSCUSTOMPLUGINEXECUTION, true); + + if (serviceClient.IsBatchOperationsAvailable) + { + if (serviceClient._batchManager.AddNewRequestToBatch(batchId, req, batchTagText)) + { + serviceClient._logEntry.Log(successText, TraceEventType.Verbose); + return true; + } + else + serviceClient._logEntry.Log("Unable to add request to batch queue, Executing normally", TraceEventType.Warning); + } + else + { + // Error and fall though. + serviceClient._logEntry.Log("Unable to add request to batch, Batching is not currently available, Executing normally", TraceEventType.Warning); + } + } + return false; + } + + #endregion + } +} diff --git a/src/GeneralTools/DataverseClient/Client/Extensions/CRUDExtensions.cs b/src/GeneralTools/DataverseClient/Client/Extensions/CRUDExtensions.cs new file mode 100644 index 0000000..b9d26d8 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Extensions/CRUDExtensions.cs @@ -0,0 +1,630 @@ +using Microsoft.Crm.Sdk.Messages; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Messages; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; + +namespace Microsoft.PowerPlatform.Dataverse.Client.Extensions +{ + /// + /// Extensions to support more generic record interaction mechanic's + /// + public static class CRUDExtentions + { + /// + /// Uses the dynamic entity patter to create a new entity + /// + /// Name of Entity To create + /// Initial Values + /// Optional: Applies the update with a solution by Unique name + /// Optional: if true, enabled Dataverse onboard duplicate detection + /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// ServiceClient + /// Guid on Success, Guid.Empty on fail + public static Guid CreateNewRecord(this ServiceClient serviceClient, string entityName, Dictionary valueArray, string applyToSolution = "", bool enabledDuplicateDetection = false, Guid batchId = default(Guid), bool bypassPluginExecution = false) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); + return Guid.Empty; + } + + if (string.IsNullOrEmpty(entityName)) + return Guid.Empty; + + if ((valueArray == null) || (valueArray.Count == 0)) + return Guid.Empty; + + + // Create the New Entity Type. + Entity NewEnt = new Entity(); + NewEnt.LogicalName = entityName; + + AttributeCollection propList = new AttributeCollection(); + foreach (KeyValuePair i in valueArray) + { + serviceClient.AddValueToPropertyList(i, propList); + } + + NewEnt.Attributes.AddRange(propList); + + CreateRequest createReq = new CreateRequest(); + createReq.Target = NewEnt; + createReq.Parameters.Add("SuppressDuplicateDetection", !enabledDuplicateDetection); + if (!string.IsNullOrWhiteSpace(applyToSolution)) + createReq.Parameters.Add(Utilities.RequestHeaders.SOLUTIONUNIQUENAME, applyToSolution); + + CreateResponse createResp = null; + + if (serviceClient.AddRequestToBatch(batchId, createReq, entityName, string.Format(CultureInfo.InvariantCulture, "Request for Create on {0} queued", entityName), bypassPluginExecution)) + return Guid.Empty; + + createResp = (CreateResponse)serviceClient.ExecuteOrganizationRequestImpl(createReq, entityName, useWebAPI: true, bypassPluginExecution: bypassPluginExecution); + if (createResp != null) + { + return createResp.id; + } + else + return Guid.Empty; + + } + + /// + /// Generic update entity + /// + /// String version of the entity name + /// Key fieldname of the entity + /// Guid ID of the entity to update + /// Fields to update + /// Optional: Applies the update with a solution by Unique name + /// Optional: if true, enabled Dataverse onboard duplicate detection + /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// ServiceClient + /// true on success, false on fail + public static bool UpdateEntity(this ServiceClient serviceClient, string entityName, string keyFieldName, Guid id, Dictionary fieldList, string applyToSolution = "", bool enabledDuplicateDetection = false, Guid batchId = default(Guid), bool bypassPluginExecution = false) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + if (serviceClient.DataverseService == null || id == Guid.Empty) + { + return false; + } + + if (fieldList == null || fieldList.Count == 0) + return false; + + Entity uEnt = new Entity(); + uEnt.LogicalName = entityName; + + + AttributeCollection PropertyList = new AttributeCollection(); + + #region MapCode + foreach (KeyValuePair field in fieldList) + { + serviceClient.AddValueToPropertyList(field, PropertyList); + } + + // Add the key... + // check to see if the key is in the import set already + if (!fieldList.ContainsKey(keyFieldName)) + PropertyList.Add(new KeyValuePair(keyFieldName, id)); + + #endregion + + uEnt.Attributes.AddRange(PropertyList.ToArray()); + uEnt.Id = id; + + UpdateRequest req = new UpdateRequest(); + req.Target = uEnt; + + req.Parameters.Add("SuppressDuplicateDetection", !enabledDuplicateDetection); + if (!string.IsNullOrWhiteSpace(applyToSolution)) + req.Parameters.Add("SolutionUniqueName", applyToSolution); + + + if (serviceClient.AddRequestToBatch(batchId, req, string.Format(CultureInfo.InvariantCulture, "Updating {0} : {1}", entityName, id.ToString()), string.Format(CultureInfo.InvariantCulture, "Request for update on {0} queued", entityName), bypassPluginExecution)) + return false; + + UpdateResponse resp = (UpdateResponse)serviceClient.ExecuteOrganizationRequestImpl(req, string.Format(CultureInfo.InvariantCulture, "Updating {0} : {1}", entityName, id.ToString()), useWebAPI: true, bypassPluginExecution: bypassPluginExecution); + if (resp == null) + return false; + else + return true; + } + + + /// + /// Updates the State and Status of the Entity passed in. + /// + /// Name of the entity + /// Guid ID of the entity you are updating + /// String version of the new state + /// String Version of the new status + /// Optional : Batch ID to attach this request too. + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// ServiceClient + /// true on success. + public static bool UpdateStateAndStatusForEntity(this ServiceClient serviceClient, string entName, Guid id, string stateCode, string statusCode, Guid batchId = default(Guid), bool bypassPluginExecution = false) + { + return serviceClient.UpdateStateStatusForEntity(entName, id, stateCode, statusCode, batchId: batchId, bypassPluginExecution: bypassPluginExecution); + } + + /// + /// Updates the State and Status of the Entity passed in. + /// + /// Name of the entity + /// Guid ID of the entity you are updating + /// Int version of the new state + /// Int Version of the new status + /// Optional : Batch ID to attach this request too. + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// ServiceClient + /// true on success. + public static bool UpdateStateAndStatusForEntity(this ServiceClient serviceClient, string entName, Guid id, int stateCode, int statusCode, Guid batchId = default(Guid), bool bypassPluginExecution = false) + { + return serviceClient.UpdateStateStatusForEntity(entName, id, string.Empty, string.Empty, stateCode, statusCode, batchId, bypassPluginExecution); + } + + /// + /// Deletes an entity from the Dataverse + /// + /// entity type name + /// entity id + /// Optional : Batch ID to attach this request too. + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// ServiceClient + /// true on success, false on failure + public static bool DeleteEntity(this ServiceClient serviceClient, string entityType, Guid entityId, Guid batchId = default(Guid), bool bypassPluginExecution = false) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); + return false; + } + + DeleteRequest req = new DeleteRequest(); + req.Target = new EntityReference(entityType, entityId); + + if (batchId != Guid.Empty) + { + if (serviceClient.IsBatchOperationsAvailable) + { + if (serviceClient._batchManager.AddNewRequestToBatch(batchId, req, string.Format(CultureInfo.InvariantCulture, "Trying to Delete. Entity = {0}, ID = {1}", entityType, entityId))) + { + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "Request for Delete on {0} queued", entityType), TraceEventType.Verbose); + return false; + } + else + serviceClient._logEntry.Log("Unable to add request to batch queue, Executing normally", TraceEventType.Warning); + } + else + { + // Error and fall though. + serviceClient._logEntry.Log("Unable to add request to batch, Batching is not currently available, Executing normally", TraceEventType.Warning); + } + } + + if (batchId != Guid.Empty) + { + if (serviceClient.IsBatchOperationsAvailable) + { + if (serviceClient._batchManager.AddNewRequestToBatch(batchId, req, string.Format(CultureInfo.InvariantCulture, "Delete Entity = {0}, ID = {1} queued", entityType, entityId))) + { + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "Request for Delete. Entity = {0}, ID = {1} queued", entityType, entityId), TraceEventType.Verbose); + return false; + } + else + serviceClient._logEntry.Log("Unable to add request to batch queue, Executing normally", TraceEventType.Warning); + } + else + { + // Error and fall though. + serviceClient._logEntry.Log("Unable to add request to batch, Batching is not currently available, Executing normally", TraceEventType.Warning); + } + } + + if (serviceClient.AddRequestToBatch(batchId, req, string.Format(CultureInfo.InvariantCulture, "Trying to Delete. Entity = {0}, ID = {1}", entityType, entityId), string.Format(CultureInfo.InvariantCulture, "Request to Delete. Entity = {0}, ID = {1} Queued", entityType, entityId), bypassPluginExecution)) + return false; + + DeleteResponse resp = (DeleteResponse)serviceClient.ExecuteOrganizationRequestImpl(req, string.Format(CultureInfo.InvariantCulture, "Trying to Delete. Entity = {0}, ID = {1}", entityType, entityId), useWebAPI: true, bypassPluginExecution: bypassPluginExecution); + if (resp != null) + { + // Clean out the cache if the account happens to be stored in there. + if ((serviceClient._CachObject != null) && (serviceClient._CachObject.ContainsKey(entityType))) + { + while (serviceClient._CachObject[entityType].ContainsValue(entityId)) + { + foreach (KeyValuePair v in serviceClient._CachObject[entityType].Values) + { + if (v.Value == entityId) + { + serviceClient._CachObject[entityType].Remove(v.Key); + break; + } + } + } + } + return true; + } + return false; + } + + /// + /// This creates a annotation [note] entry, related to a an existing entity + /// Required Properties in the fieldList + /// notetext (string) = Text of the note, + /// subject (string) = this is the title of the note + /// + /// Target Entity TypeID + /// Target Entity ID + /// Fields to populate + /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// ServiceClient + /// + public static Guid CreateAnnotation(this ServiceClient serviceClient, string targetEntityTypeName, Guid targetEntityId, Dictionary fieldList, Guid batchId = default(Guid), bool bypassPluginExecution = false) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + + if (string.IsNullOrEmpty(targetEntityTypeName)) + return Guid.Empty; + + if (targetEntityId == Guid.Empty) + return Guid.Empty; + + if (fieldList == null) + fieldList = new Dictionary(); + + fieldList.Add("objecttypecode", new DataverseDataTypeWrapper(targetEntityTypeName, DataverseFieldType.String)); + fieldList.Add("objectid", new DataverseDataTypeWrapper(targetEntityId, DataverseFieldType.Lookup, targetEntityTypeName)); + fieldList.Add("ownerid", new DataverseDataTypeWrapper(serviceClient.SystemUser.UserId, DataverseFieldType.Lookup, "systemuser")); + + return serviceClient.CreateNewRecord("annotation", fieldList, batchId: batchId, bypassPluginExecution: bypassPluginExecution); + + } + + /// + /// Creates a new activity against the target entity type + /// + /// Type of Activity you would like to create + /// Entity type of the Entity you want to associate with. + /// Subject Line of the Activity + /// Description Text of the Activity + /// ID of the Entity to associate the Activity too + /// User ID that Created the Activity *Calling user must have necessary permissions to assign to another user + /// Additional fields to add as part of the activity creation + /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// ServiceClient + /// Guid of Activity ID or Guid.empty + public static Guid CreateNewActivityEntry(this ServiceClient serviceClient, + string activityEntityTypeName, + string regardingEntityTypeName, + Guid regardingId, + string subject, + string description, + string creatingUserId, + Dictionary fieldList = null, + Guid batchId = default(Guid), + bool bypassPluginExecution = false + ) + { + + #region PreChecks + serviceClient._logEntry.ResetLastError(); // Reset Last Error + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); + return Guid.Empty; + } + if (string.IsNullOrWhiteSpace(activityEntityTypeName)) + { + serviceClient._logEntry.Log("You must specify the activity type name to create", TraceEventType.Error); + return Guid.Empty; + } + if (string.IsNullOrWhiteSpace(subject)) + { + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "A Subject is required to create an activity of type {0}", regardingEntityTypeName), TraceEventType.Error); + return Guid.Empty; + } + #endregion + + Guid activityId = Guid.Empty; + try + { + // reuse the passed in field list if its available, else punt and create a new one. + if (fieldList == null) + fieldList = new Dictionary(); + + fieldList.Add("subject", new DataverseDataTypeWrapper(subject, DataverseFieldType.String)); + if (regardingId != Guid.Empty) + fieldList.Add("regardingobjectid", new DataverseDataTypeWrapper(regardingId, DataverseFieldType.Lookup, regardingEntityTypeName)); + if (!string.IsNullOrWhiteSpace(description)) + fieldList.Add("description", new DataverseDataTypeWrapper(description, DataverseFieldType.String)); + + // Create the base record. + activityId = serviceClient.CreateNewRecord(activityEntityTypeName, fieldList, bypassPluginExecution: bypassPluginExecution); + + // if I have a user ID, try to assign it to that user. + if (!string.IsNullOrWhiteSpace(creatingUserId)) + { + Guid userId = serviceClient.GetLookupValueForEntity("systemuser", creatingUserId); + + if (userId != Guid.Empty) + { + EntityReference newAction = new EntityReference(activityEntityTypeName, activityId); + EntityReference principal = new EntityReference("systemuser", userId); + + AssignRequest arRequest = new AssignRequest(); + arRequest.Assignee = principal; + arRequest.Target = newAction; + if (serviceClient.AddRequestToBatch(batchId, arRequest, string.Format(CultureInfo.InvariantCulture, "Trying to Assign a Record. Entity = {0} , ID = {1}", newAction.LogicalName, principal.LogicalName), + string.Format(CultureInfo.InvariantCulture, "Request to Assign a Record. Entity = {0} , ID = {1} Queued", newAction.LogicalName, principal.LogicalName), bypassPluginExecution)) + return Guid.Empty; + serviceClient.Command_Execute(arRequest, "Assign Activity", bypassPluginExecution); + } + } + } + catch (Exception exp) + { + serviceClient._logEntry.Log(exp); + } + return activityId; + } + + /// + /// Closes the Activity type specified. + /// The Activity Entity type supports fax , letter , and phonecall + /// *Note: This will default to using English names for Status. if you need to use Non-English, you should populate the names for completed for the status and state. + /// + /// Type of Activity you would like to close.. Supports fax, letter, phonecall + /// ID of the Activity you want to close + /// State Code configured on the activity + /// Status code on the activity + /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// ServiceClient + /// true if success false if not. + public static bool CloseActivity(this ServiceClient serviceClient, + string activityEntityType, + Guid activityId, + string stateCode = "completed", + string statusCode = "completed", + Guid batchId = default(Guid), + bool bypassPluginExecution = false + ) + { + return serviceClient.UpdateStateStatusForEntity(activityEntityType, activityId, stateCode, statusCode, batchId: batchId, bypassPluginExecution: bypassPluginExecution); + } + + /// + /// Updates the state of an activity + /// + /// + /// + /// + /// + /// ID for the new State ( Skips metadata lookup ) + /// ID for new Status ( Skips Metadata Lookup) + /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// ServiceClient + /// + private static bool UpdateStateStatusForEntity(this ServiceClient serviceClient, + string entName, + Guid entId, + string newState, + string newStatus, + int newStateid = -1, + int newStatusid = -1, + Guid batchId = default(Guid), + bool bypassPluginExecution = false + ) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + SetStateRequest req = new SetStateRequest(); + req.EntityMoniker = new EntityReference(entName, entId); + + int istatuscode = -1; + int istatecode = -1; + + // Modified to prefer IntID's first... this is in support of multi languages. + + if (newStatusid != -1) + istatuscode = newStatusid; + else + { + if (!String.IsNullOrWhiteSpace(newStatus)) + { + PickListMetaElement picItem = serviceClient.GetPickListElementFromMetadataEntity(entName, "statuscode"); + if (picItem != null) + { + var statusOption = picItem.Items.FirstOrDefault(s => s.DisplayLabel.Equals(newStatus, StringComparison.CurrentCultureIgnoreCase)); + if (statusOption != null) + istatuscode = statusOption.PickListItemId; + } + } + } + + if (newStateid != -1) + istatecode = newStateid; + else + { + if (!string.IsNullOrWhiteSpace(newState)) + { + PickListMetaElement picItem2 = serviceClient.GetPickListElementFromMetadataEntity(entName, "statecode"); + var stateOption = picItem2.Items.FirstOrDefault(s => s.DisplayLabel.Equals(newState, StringComparison.CurrentCultureIgnoreCase)); + if (stateOption != null) + istatecode = stateOption.PickListItemId; + } + } + + if (istatecode == -1 && istatuscode == -1) + { + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "Cannot set status on {0}, State and Status codes not found, State = {1}, Status = {2}", entName, newState, newStatus), TraceEventType.Information); + return false; + } + + if (istatecode != -1) + req.State = new OptionSetValue(istatecode);// "Completed"; + if (istatuscode != -1) + req.Status = new OptionSetValue(istatuscode); //Status = 2; + + + if (serviceClient.AddRequestToBatch(batchId, req, string.Format(CultureInfo.InvariantCulture, "Setting Activity State in Dataverse... {0}", entName), string.Format(CultureInfo.InvariantCulture, "Request for SetState on {0} queued", entName), bypassPluginExecution)) + return false; + + SetStateResponse resp = (SetStateResponse)serviceClient.Command_Execute(req, string.Format(CultureInfo.InvariantCulture, "Setting Activity State in Dataverse... {0}", entName), bypassPluginExecution); + if (resp != null) + return true; + else + return false; + } + + /// + /// Associates one Entity to another where an M2M Relationship Exists. + /// + /// Entity on one side of the relationship + /// The Id of the record on the first side of the relationship + /// Entity on the second side of the relationship + /// The Id of the record on the second side of the relationship + /// Relationship name between the 2 entities + /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// ServiceClient + /// true on success, false on fail + public static bool CreateEntityAssociation(this ServiceClient serviceClient, string entityName1, Guid entity1Id, string entityName2, Guid entity2Id, string relationshipName, Guid batchId = default(Guid), bool bypassPluginExecution = false) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); + return false; + } + + if (string.IsNullOrEmpty(entityName1) || string.IsNullOrEmpty(entityName2) || entity1Id == Guid.Empty || entity2Id == Guid.Empty) + { + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception in CreateEntityAssociation, all parameters must be populated"), TraceEventType.Error); + return false; + } + + AssociateEntitiesRequest req = new AssociateEntitiesRequest(); + req.Moniker1 = new EntityReference(entityName1, entity1Id); + req.Moniker2 = new EntityReference(entityName2, entity2Id); + req.RelationshipName = relationshipName; + + + if (serviceClient.AddRequestToBatch(batchId, req, string.Format(CultureInfo.InvariantCulture, "Creating association between({0}) and {1}", entityName1, entityName2), + string.Format(CultureInfo.InvariantCulture, "Request to Create association between({0}) and {1} Queued", entityName1, entityName2), bypassPluginExecution)) + return true; + + AssociateEntitiesResponse resp = (AssociateEntitiesResponse)serviceClient.Command_Execute(req, "Executing CreateEntityAssociation", bypassPluginExecution); + if (resp != null) + return true; + + return false; + } + + /// + /// Associates multiple entities of the same time to a single entity + /// + /// Entity that things will be related too. + /// ID of entity that things will be related too + /// Entity that you are relating from + /// ID's of the entities you are relating from + /// Name of the relationship between the target and the source entities. + /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately + /// Optional: if set to true, indicates that this is a N:N using a reflexive relationship + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// ServiceClient + /// true on success, false on fail + public static bool CreateMultiEntityAssociation(this ServiceClient serviceClient, string targetEntity, Guid targetEntity1Id, string sourceEntityName, List sourceEntitieIds, string relationshipName, Guid batchId = default(Guid), bool bypassPluginExecution = false, bool isReflexiveRelationship = false) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); + return false; + } + + if (string.IsNullOrEmpty(targetEntity) || string.IsNullOrEmpty(sourceEntityName) || targetEntity1Id == Guid.Empty || sourceEntitieIds == null || sourceEntitieIds.Count == 0) + { + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception in CreateMultiEntityAssociation, all parameters must be populated"), TraceEventType.Error); + return false; + } + + AssociateRequest req = new AssociateRequest(); + req.Relationship = new Relationship(relationshipName); + if (isReflexiveRelationship) // used to determine if the relationship role is reflexive. + req.Relationship.PrimaryEntityRole = EntityRole.Referenced; + req.RelatedEntities = new EntityReferenceCollection(); + foreach (Guid g in sourceEntitieIds) + { + req.RelatedEntities.Add(new EntityReference(sourceEntityName, g)); + } + req.Target = new EntityReference(targetEntity, targetEntity1Id); + + if (serviceClient.AddRequestToBatch(batchId, req, string.Format(CultureInfo.InvariantCulture, "Creating multi association between({0}) and {1}", targetEntity, sourceEntityName), + string.Format(CultureInfo.InvariantCulture, "Request to Create multi association between({0}) and {1} queued", targetEntity, sourceEntityName), bypassPluginExecution)) + return true; + + AssociateResponse resp = (AssociateResponse)serviceClient.Command_Execute(req, "Executing CreateMultiEntityAssociation", bypassPluginExecution); + if (resp != null) + return true; + + return false; + } + + /// + /// Removes the Association between 2 entity items where an M2M Relationship Exists. + /// + /// Entity on one side of the relationship + /// The Id of the record on the first side of the relationship + /// Entity on the second side of the relationship + /// The Id of the record on the second side of the relationship + /// Relationship name between the 2 entities + /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// ServiceClient + /// true on success, false on fail + public static bool DeleteEntityAssociation(this ServiceClient serviceClient, string entityName1, Guid entity1Id, string entityName2, Guid entity2Id, string relationshipName, Guid batchId = default(Guid), bool bypassPluginExecution = false) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); + return false; + } + + if (string.IsNullOrEmpty(entityName1) || string.IsNullOrEmpty(entityName2) || entity1Id == Guid.Empty || entity2Id == Guid.Empty) + { + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception in DeleteEntityAssociation, all parameters must be populated"), TraceEventType.Error); + return false; + } + + DisassociateEntitiesRequest req = new DisassociateEntitiesRequest(); + req.Moniker1 = new EntityReference(entityName1, entity1Id); + req.Moniker2 = new EntityReference(entityName2, entity2Id); + req.RelationshipName = relationshipName; + + if (serviceClient.AddRequestToBatch(batchId, req, string.Format(CultureInfo.InvariantCulture, "Executing DeleteEntityAssociation between ({0}) and {1}", entityName1, entityName2), + string.Format(CultureInfo.InvariantCulture, "Request to Execute DeleteEntityAssociation between ({0}) and {1} Queued", entityName1, entityName2), bypassPluginExecution)) + return true; + + DisassociateEntitiesResponse resp = (DisassociateEntitiesResponse)serviceClient.Command_Execute(req, "Executing DeleteEntityAssociation", bypassPluginExecution); + if (resp != null) + return true; + + return false; + } + + } +} diff --git a/src/GeneralTools/DataverseClient/Client/Extensions/DeploymentExtensions.cs b/src/GeneralTools/DataverseClient/Client/Extensions/DeploymentExtensions.cs new file mode 100644 index 0000000..da12cb7 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Extensions/DeploymentExtensions.cs @@ -0,0 +1,1077 @@ +using Microsoft.Crm.Sdk.Messages; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Messages; +using Microsoft.Xrm.Sdk.Query; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; + +namespace Microsoft.PowerPlatform.Dataverse.Client.Extensions +{ + /// + /// Extensions to support deploying solutions and data to Dataverse. + /// + public static class DeploymentExtensions + { + + + /// + /// Starts an Import request for CDS. + /// Supports a single file per Import request. + /// + /// Delays the import jobs till specified time - Use DateTime.MinValue to Run immediately + /// Import Data Request + /// ServiceClient + /// Guid of the Import Request, or Guid.Empty. If Guid.Empty then request failed. + public static Guid SubmitImportRequest(this ServiceClient serviceClient, ImportRequest importRequest, DateTime delayUntil) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); + return Guid.Empty; + } + + // Error checking + if (importRequest == null) + { + serviceClient._logEntry.Log("************ Exception on SubmitImportRequest, importRequest is required", TraceEventType.Error); + return Guid.Empty; + } + + if (importRequest.Files == null || (importRequest.Files != null && importRequest.Files.Count == 0)) + { + serviceClient._logEntry.Log("************ Exception on SubmitImportRequest, importRequest.Files is required and must have at least one file listed to import.", TraceEventType.Error); + return Guid.Empty; + } + + // Done error checking + if (string.IsNullOrWhiteSpace(importRequest.ImportName)) + importRequest.ImportName = "User Requested Import"; + + + Guid ImportId = Guid.Empty; + Guid ImportMap = Guid.Empty; + Guid ImportFile = Guid.Empty; + List ImportFileIds = new List(); + + // Create Import Object + // The Import Object is the anchor for the Import job in Dataverse. + Dictionary importFields = new Dictionary(); + importFields.Add("name", new DataverseDataTypeWrapper(importRequest.ImportName, DataverseFieldType.String)); + importFields.Add("modecode", new DataverseDataTypeWrapper(importRequest.Mode, DataverseFieldType.Picklist)); // 0 == Create , 1 = Update.. + ImportId = serviceClient.CreateNewRecord("import", importFields); + + if (ImportId == Guid.Empty) + // Error here; + return Guid.Empty; + + #region Determin Map to Use + //Guid guDataMapId = Guid.Empty; + if (string.IsNullOrWhiteSpace(importRequest.DataMapFileName) && importRequest.DataMapFileId == Guid.Empty) + // User Requesting to use System Mapping here. + importRequest.UseSystemMap = true; // Override whatever setting they had here. + else + { + // User providing information on a map to use. + // Query to get the map from the system + List fldList = new List(); + fldList.Add("name"); + fldList.Add("source"); + fldList.Add("importmapid"); + Dictionary MapData = null; + if (importRequest.DataMapFileId != Guid.Empty) + { + // Have the id here... get the map based on the ID. + MapData = serviceClient.GetEntityDataById("importmap", importRequest.DataMapFileId, fldList); + } + else + { + // Search by name... exact match required. + List filters = new List(); + DataverseSearchFilter filter = new DataverseSearchFilter(); + filter.FilterOperator = Microsoft.Xrm.Sdk.Query.LogicalOperator.And; + filter.SearchConditions.Add(new DataverseFilterConditionItem() { FieldName = "name", FieldOperator = Microsoft.Xrm.Sdk.Query.ConditionOperator.Equal, FieldValue = importRequest.DataMapFileName }); + filters.Add(filter); + + // Search by Name.. + Dictionary> rslts = serviceClient.GetEntityDataBySearchParams("importmap", filters, LogicalSearchOperator.None, fldList); + if (rslts != null && rslts.Count > 0) + { + // if there is more then one record returned.. throw an error ( should not happen ) + if (rslts.Count > 1) + { + // log error here. + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception on SubmitImportRequest, More then one mapping file was found for {0}, Specifiy the ID of the Mapfile to use", importRequest.DataMapFileName), TraceEventType.Error); + return Guid.Empty; + } + else + { + // Get my single record and move on.. + MapData = rslts.First().Value; + // Update the Guid for the mapID. + importRequest.DataMapFileId = serviceClient.GetDataByKeyFromResultsSet(MapData, "importmapid"); + } + } + } + ImportMap = importRequest.DataMapFileId; + + + // Now get the entity import mapping info, We need this to get the source entity name from the map XML file. + if (ImportMap != Guid.Empty) + { + // Iterate over the import files and update the entity names. + + fldList.Clear(); + fldList.Add("sourceentityname"); + List filters = new List(); + DataverseSearchFilter filter = new DataverseSearchFilter(); + filter.FilterOperator = Microsoft.Xrm.Sdk.Query.LogicalOperator.And; + filter.SearchConditions.Add(new DataverseFilterConditionItem() { FieldName = "importmapid", FieldOperator = ConditionOperator.Equal, FieldValue = ImportMap }); + filters.Add(filter); + Dictionary> al = serviceClient.GetEntityDataBySearchParams("importentitymapping", filters, LogicalSearchOperator.None, null); + if (al != null && al.Count > 0) + { + foreach (var row in al.Values) + { + importRequest.Files.ForEach(fi => + { + if (fi.TargetEntityName.Equals(serviceClient.GetDataByKeyFromResultsSet(row, "targetentityname"), StringComparison.OrdinalIgnoreCase)) + fi.SourceEntityName = serviceClient.GetDataByKeyFromResultsSet(row, "sourceentityname"); + }); + } + } + else + { + if (ImportId != Guid.Empty) + serviceClient.DeleteEntity("import", ImportId); + + // Failed to find mapping entry error , Map not imported properly + serviceClient._logEntry.Log("************ Exception on SubmitImportRequest, Cannot find mapping file information found MapFile Provided.", TraceEventType.Error); + return Guid.Empty; + } + } + else + { + if (ImportId != Guid.Empty) + serviceClient.DeleteEntity("import", ImportId); + + // Failed to find mapping entry error , Map not imported properly + serviceClient._logEntry.Log("************ Exception on SubmitImportRequest, Cannot find ImportMappingsFile Provided.", TraceEventType.Error); + return Guid.Empty; + } + + } + #endregion + + #region Create Import File for each File in array + bool continueImport = true; + Dictionary importFileFields = new Dictionary(); + foreach (var FileItem in importRequest.Files) + { + // Create the Import File Object - Loop though file objects and create as many as necessary. + // This is the row that has the data being imported as well as the status of the import file. + importFileFields.Add("name", new DataverseDataTypeWrapper(FileItem.FileName, DataverseFieldType.String)); + importFileFields.Add("source", new DataverseDataTypeWrapper(FileItem.FileName, DataverseFieldType.String)); + importFileFields.Add("filetypecode", new DataverseDataTypeWrapper(FileItem.FileType, DataverseFieldType.Picklist)); // File Type is either : 0 = CSV , 1 = XML , 2 = Attachment + importFileFields.Add("content", new DataverseDataTypeWrapper(FileItem.FileContentToImport, DataverseFieldType.String)); + importFileFields.Add("enableduplicatedetection", new DataverseDataTypeWrapper(FileItem.EnableDuplicateDetection, DataverseFieldType.Boolean)); + importFileFields.Add("usesystemmap", new DataverseDataTypeWrapper(importRequest.UseSystemMap, DataverseFieldType.Boolean)); // Use the System Map to get somthing done. + importFileFields.Add("sourceentityname", new DataverseDataTypeWrapper(FileItem.SourceEntityName, DataverseFieldType.String)); + importFileFields.Add("targetentityname", new DataverseDataTypeWrapper(FileItem.TargetEntityName, DataverseFieldType.String)); + importFileFields.Add("datadelimitercode", new DataverseDataTypeWrapper(FileItem.DataDelimiter, DataverseFieldType.Picklist)); // 1 = " | 2 = | 3 = ' + importFileFields.Add("fielddelimitercode", new DataverseDataTypeWrapper(FileItem.FieldDelimiter, DataverseFieldType.Picklist)); // 1 = : | 2 = , | 3 = ' + importFileFields.Add("isfirstrowheader", new DataverseDataTypeWrapper(FileItem.IsFirstRowHeader, DataverseFieldType.Boolean)); + importFileFields.Add("processcode", new DataverseDataTypeWrapper(1, DataverseFieldType.Picklist)); + if (FileItem.IsRecordOwnerATeam) + importFileFields.Add("recordsownerid", new DataverseDataTypeWrapper(FileItem.RecordOwner, DataverseFieldType.Lookup, "team")); + else + importFileFields.Add("recordsownerid", new DataverseDataTypeWrapper(FileItem.RecordOwner, DataverseFieldType.Lookup, "systemuser")); + + importFileFields.Add("importid", new DataverseDataTypeWrapper(ImportId, DataverseFieldType.Lookup, "import")); + if (ImportMap != Guid.Empty) + importFileFields.Add("importmapid", new DataverseDataTypeWrapper(ImportMap, DataverseFieldType.Lookup, "importmap")); + + ImportFile = serviceClient.CreateNewRecord("importfile", importFileFields); + if (ImportFile == Guid.Empty) + { + continueImport = false; + break; + } + ImportFileIds.Add(ImportFile); + importFileFields.Clear(); + } + + #endregion + + + // if We have an Import File... Activate Import. + if (continueImport) + { + ParseImportResponse parseResp = (ParseImportResponse)serviceClient.Command_Execute(new ParseImportRequest() { ImportId = ImportId }, + string.Format(CultureInfo.InvariantCulture, "************ Exception Executing ParseImportRequest for ImportJob ({0})", importRequest.ImportName)); + if (parseResp.AsyncOperationId != Guid.Empty) + { + if (delayUntil != DateTime.MinValue) + { + importFileFields.Clear(); + importFileFields.Add("postponeuntil", new DataverseDataTypeWrapper(delayUntil, DataverseFieldType.DateTime)); + serviceClient.UpdateEntity("asyncoperation", "asyncoperationid", parseResp.AsyncOperationId, importFileFields); + } + + TransformImportResponse transformResp = (TransformImportResponse)serviceClient.Command_Execute(new TransformImportRequest() { ImportId = ImportId }, + string.Format(CultureInfo.InvariantCulture, "************ Exception Executing TransformImportRequest for ImportJob ({0})", importRequest.ImportName)); + if (transformResp != null) + { + if (delayUntil != DateTime.MinValue) + { + importFileFields.Clear(); + importFileFields.Add("postponeuntil", new DataverseDataTypeWrapper(delayUntil.AddSeconds(1), DataverseFieldType.DateTime)); + serviceClient.UpdateEntity("asyncoperation", "asyncoperationid", transformResp.AsyncOperationId, importFileFields); + } + + ImportRecordsImportResponse importResp = (ImportRecordsImportResponse)serviceClient.Command_Execute(new ImportRecordsImportRequest() { ImportId = ImportId }, + string.Format(CultureInfo.InvariantCulture, "************ Exception Executing ImportRecordsImportRequest for ImportJob ({0})", importRequest.ImportName)); + if (importResp != null) + { + if (delayUntil != DateTime.MinValue) + { + importFileFields.Clear(); + importFileFields.Add("postponeuntil", new DataverseDataTypeWrapper(delayUntil.AddSeconds(2), DataverseFieldType.DateTime)); + serviceClient.UpdateEntity("asyncoperation", "asyncoperationid", importResp.AsyncOperationId, importFileFields); + } + + return ImportId; + } + } + } + } + else + { + // Error.. Clean up the other records. + string err = serviceClient.LastError; + Exception ex = serviceClient.LastException; + + if (ImportFileIds.Count > 0) + { + ImportFileIds.ForEach(i => + { + serviceClient.DeleteEntity("importfile", i); + }); + ImportFileIds.Clear(); + } + + if (ImportId != Guid.Empty) + serviceClient.DeleteEntity("import", ImportId); + + // This is done to allow the error to be available to the user after the class cleans things up. + if (ex != null) + serviceClient._logEntry.Log(err, TraceEventType.Error, ex); + else + serviceClient._logEntry.Log(err, TraceEventType.Error); + + return Guid.Empty; + } + return ImportId; + } + + /// + /// Used to upload a data map to the Dataverse + /// + /// XML of the datamap in string form + /// True to have Dataverse replace ID's on inbound data, False to have inbound data retain its ID's + /// if true, dataMapXml is expected to be a File name and path to load. + /// ServiceClient + /// Returns ID of the datamap or Guid.Empty + public static Guid ImportDataMap(this ServiceClient serviceClient, string dataMapXml, bool replaceIds = true, bool dataMapXmlIsFilePath = false) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); + return Guid.Empty; + } + + if (string.IsNullOrWhiteSpace(dataMapXml)) + { + serviceClient._logEntry.Log("************ Exception on ImportDataMap, dataMapXml is required", TraceEventType.Error); + return Guid.Empty; + } + + if (dataMapXmlIsFilePath) + { + // try to load the file from the file system + if (File.Exists(dataMapXml)) + { + try + { + string sContent = ""; + using (var a = File.OpenText(dataMapXml)) + { + sContent = a.ReadToEnd(); + } + + dataMapXml = sContent; + } + #region Exception handlers for files + catch (UnauthorizedAccessException ex) + { + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception on ImportDataMap, Unauthorized Access to file: {0}", dataMapXml), TraceEventType.Error, ex); + return Guid.Empty; + } + catch (ArgumentNullException ex) + { + serviceClient._logEntry.Log("************ Exception on ImportDataMap, File path not specified", TraceEventType.Error, ex); + return Guid.Empty; + } + catch (ArgumentException ex) + { + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception on ImportDataMap, File path is invalid: {0}", dataMapXml), TraceEventType.Error, ex); + return Guid.Empty; + } + catch (PathTooLongException ex) + { + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception on ImportDataMap, File path is too long. Paths must be less than 248 characters, and file names must be less than 260 characters\n{0}", dataMapXml), TraceEventType.Error, ex); + return Guid.Empty; + } + catch (DirectoryNotFoundException ex) + { + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception on ImportDataMap, File path is invalid: {0}", dataMapXml), TraceEventType.Error, ex); + return Guid.Empty; + } + catch (FileNotFoundException ex) + { + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception on ImportDataMap, File Not Found: {0}", dataMapXml), TraceEventType.Error, ex); + return Guid.Empty; + } + catch (NotSupportedException ex) + { + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception on ImportDataMap, File path or name is invalid: {0}", dataMapXml), TraceEventType.Error, ex); + return Guid.Empty; + } + #endregion + } + else + { + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception on ImportDataMap, File path specified in dataMapXml is not found: {0}", dataMapXml), TraceEventType.Error); + return Guid.Empty; + } + + } + + ImportMappingsImportMapResponse resp = (ImportMappingsImportMapResponse)serviceClient.Command_Execute(new ImportMappingsImportMapRequest() { MappingsXml = dataMapXml, ReplaceIds = replaceIds }, + "************ Exception Executing ImportMappingsImportMapResponse for ImportDataMap"); + if (resp != null) + { + if (resp.ImportMapId != Guid.Empty) + { + return resp.ImportMapId; + } + } + + return Guid.Empty; + } + + /// + /// Import Solution Async used Execute Async pattern to run a solution import. + /// + /// Path to the Solution File + /// Activate Plugin's and workflows on the Solution + /// This will populate with the Import ID even if the request failed. + /// You can use this ID to request status on the import via a request to the ImportJob entity. + /// Forces an overwrite of unmanaged customizations of the managed solution you are installing, defaults to false + /// Skips dependency against dependencies flagged as product update, defaults to false + /// Applies only on Dataverse organizations version 7.2 or higher. This imports the Dataverse solution as a holding solution utilizing the “As Holding” capability of ImportSolution + /// Internal Microsoft use only + /// Extra parameters + /// ServiceClient + /// Returns the Async Job ID. To find the status of the job, query the AsyncOperation Entity using GetEntityDataByID using the returned value of this method + public static Guid ImportSolutionAsync(this ServiceClient serviceClient, string solutionPath, out Guid importId, bool activatePlugIns = true, bool overwriteUnManagedCustomizations = false, bool skipDependancyOnProductUpdateCheckOnInstall = false, bool importAsHoldingSolution = false, bool isInternalUpgrade = false, Dictionary extraParameters = null) + { + return serviceClient.ImportSolutionToImpl(solutionPath, out importId, activatePlugIns, overwriteUnManagedCustomizations, skipDependancyOnProductUpdateCheckOnInstall, importAsHoldingSolution, isInternalUpgrade, true, extraParameters); + } + + + /// + /// + /// Imports a Dataverse solution to the Dataverse Server currently connected. + /// *** Note: this is a blocking call and will take time to Import to Dataverse *** + /// + /// + /// Path to the Solution File + /// Activate Plugin's and workflows on the Solution + /// This will populate with the Import ID even if the request failed. + /// You can use this ID to request status on the import via a request to the ImportJob entity. + /// Forces an overwrite of unmanaged customizations of the managed solution you are installing, defaults to false + /// Skips dependency against dependencies flagged as product update, defaults to false + /// Applies only on Dataverse organizations version 7.2 or higher. This imports the Dataverse solution as a holding solution utilizing the “As Holding” capability of ImportSolution + /// Internal Microsoft use only + /// ServiceClient + /// Extra parameters + public static Guid ImportSolution(this ServiceClient serviceClient, string solutionPath, out Guid importId, bool activatePlugIns = true, bool overwriteUnManagedCustomizations = false, bool skipDependancyOnProductUpdateCheckOnInstall = false, bool importAsHoldingSolution = false, bool isInternalUpgrade = false, Dictionary extraParameters = null) + { + return serviceClient.ImportSolutionToImpl(solutionPath, out importId, activatePlugIns, overwriteUnManagedCustomizations, skipDependancyOnProductUpdateCheckOnInstall, importAsHoldingSolution, isInternalUpgrade, false, extraParameters); + } + + /// + /// Executes a Delete and Propmote Request against Dataverse using the Async Pattern. + /// + /// Unique Name of solution to be upgraded + /// ServiceClient + /// Returns the Async Job ID. To find the status of the job, query the AsyncOperation Entity using GetEntityDataByID using the returned value of this method + public static Guid DeleteAndPromoteSolutionAsync(this ServiceClient serviceClient, string uniqueName) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); + return Guid.Empty; + } + // Test for non blank unique name. + if (string.IsNullOrEmpty(uniqueName)) + { + serviceClient._logEntry.Log("Solution UniqueName is required.", TraceEventType.Error); + return Guid.Empty; + } + + DeleteAndPromoteRequest delReq = new DeleteAndPromoteRequest() + { + UniqueName = uniqueName + }; + + // Assign Tracking ID + Guid requestTrackingId = Guid.NewGuid(); + delReq.RequestId = requestTrackingId; + + // Execute Async here + ExecuteAsyncRequest req = new ExecuteAsyncRequest() { Request = delReq }; + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "{1} - Created Async DeleteAndPromoteSolutionRequest : RequestID={0} ", + requestTrackingId.ToString(), uniqueName), TraceEventType.Verbose); + ExecuteAsyncResponse resp = (ExecuteAsyncResponse)serviceClient.Command_Execute(req, "Submitting DeleteAndPromoteSolution Async Request"); + if (resp != null) + { + if (resp.AsyncJobId != Guid.Empty) + { + serviceClient._logEntry.Log(string.Format("{1} - AsyncJobID for DeleteAndPromoteSolution {0}.", resp.AsyncJobId, uniqueName), TraceEventType.Verbose); + return resp.AsyncJobId; + } + } + + serviceClient._logEntry.Log(string.Format("{0} - Failed execute Async Job for DeleteAndPromoteSolution.", uniqueName), TraceEventType.Error); + return Guid.Empty; + } + + /// + /// + /// Request Dataverse to install sample data shipped with Dataverse. Note this is process will take a few moments to execute. + /// This method will return once the request has been submitted. + /// + /// + /// ServiceClient + /// ID of the Async job executing the request + public static Guid InstallSampleData(this ServiceClient serviceClient) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); + return Guid.Empty; + } + + if (ImportStatus.NotImported != serviceClient.IsSampleDataInstalled()) + { + serviceClient._logEntry.Log("************ InstallSampleData failed, sample data is already installed on Dataverse", TraceEventType.Error); + return Guid.Empty; + } + + // Create Request to Install Sample data. + InstallSampleDataRequest loadSampledataRequest = new InstallSampleDataRequest() { RequestId = Guid.NewGuid() }; + InstallSampleDataResponse resp = (InstallSampleDataResponse)serviceClient.Command_Execute(loadSampledataRequest, "Executing InstallSampleDataRequest for InstallSampleData"); + if (resp == null) + return Guid.Empty; + else + return loadSampledataRequest.RequestId.Value; + } + + /// + /// + /// Request Dataverse to remove sample data shipped with Dataverse. Note this is process will take a few moments to execute. + /// This method will return once the request has been submitted. + /// + /// + /// ServiceClient + /// ID of the Async job executing the request + public static Guid UninstallSampleData(this ServiceClient serviceClient) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); + return Guid.Empty; + } + + if (ImportStatus.NotImported == serviceClient.IsSampleDataInstalled()) + { + serviceClient._logEntry.Log("************ UninstallSampleData failed, sample data is not installed on Dataverse", TraceEventType.Error); + return Guid.Empty; + } + + UninstallSampleDataRequest removeSampledataRequest = new UninstallSampleDataRequest() { RequestId = Guid.NewGuid() }; + UninstallSampleDataResponse resp = (UninstallSampleDataResponse)serviceClient.Command_Execute(removeSampledataRequest, "Executing UninstallSampleDataRequest for UninstallSampleData"); + if (resp == null) + return Guid.Empty; + else + return removeSampledataRequest.RequestId.Value; + } + + /// + /// Determines if the Dataverse sample data has been installed + /// + /// ServiceClient + /// True if the sample data is installed, False if not. + public static ImportStatus IsSampleDataInstalled(this ServiceClient serviceClient) + { + try + { + // Query the Org I'm connected to to get the sample data import info. + Dictionary> theOrg = + serviceClient.GetEntityDataBySearchParams("organization", + new Dictionary(), LogicalSearchOperator.None, new List() { "sampledataimportid" }); + + if (theOrg != null && theOrg.Count > 0) + { + var v = theOrg.FirstOrDefault(); + if (v.Value != null && v.Value.Count > 0) + { + if (serviceClient.GetDataByKeyFromResultsSet(v.Value, "sampledataimportid") != Guid.Empty) + { + string sampledataimportid = serviceClient.GetDataByKeyFromResultsSet(v.Value, "sampledataimportid").ToString(); + serviceClient._logEntry.Log(string.Format("sampledataimportid = {0}", sampledataimportid), TraceEventType.Verbose); + Dictionary basicSearch = new Dictionary(); + basicSearch.Add("importid", sampledataimportid); + Dictionary> importSampleData = serviceClient.GetEntityDataBySearchParams("import", basicSearch, LogicalSearchOperator.None, new List() { "statuscode" }); + + if (importSampleData != null && importSampleData.Count > 0) + { + var import = importSampleData.FirstOrDefault(); + if (import.Value != null) + { + OptionSetValue ImportStatusResult = serviceClient.GetDataByKeyFromResultsSet(import.Value, "statuscode"); + if (ImportStatusResult != null) + { + serviceClient._logEntry.Log(string.Format("sampledata import job result = {0}", ImportStatusResult.Value), TraceEventType.Verbose); + //This Switch Case needs to be in Sync with the Dataverse Import StatusCode. + switch (ImportStatusResult.Value) + { + // 4 is the Import Status Code for Complete Import + case 4: return ImportStatus.Completed; + // 5 is the Import Status Code for the Failed Import + case 5: return ImportStatus.Failed; + // Rest (Submitted, Parsing, Transforming, Importing) are different stages of Inprogress Import hence putting them under same case. + default: return ImportStatus.InProgress; + } + } + } + } + } + } + } + } + catch { } + return ImportStatus.NotImported; + //return false; + } + + #region SupportClasses + /// + /// ImportStatus Reasons + /// + public enum ImportStatus + { + /// Not Yet Imported + NotImported = 0, + /// Import is in Progress + InProgress = 1, + /// Import has Completed + Completed = 2, + /// Import has Failed + Failed = 3 + }; + + /// + /// Describes an import request for Dataverse + /// + public sealed class ImportRequest + { + #region Vars + // Import Items.. + /// + /// Name of the Import Request. this Name will appear in Dataverse + /// + public string ImportName { get; set; } + /// + /// Sets or gets the Import Mode. + /// + public ImportMode Mode { get; set; } + + // Import Map Items. + /// + /// ID of the DataMap to use + /// + public Guid DataMapFileId { get; set; } + /// + /// Name of the DataMap File to use + /// ID or Name is required + /// + public string DataMapFileName { get; set; } + + /// + /// if True, infers the map from the type of entity requested.. + /// + public bool UseSystemMap { get; set; } + + /// + /// List of files to import in this job, there must be at least one. + /// + public List Files { get; set; } + + + #endregion + + /// + /// Mode of the Import, Update or Create + /// + public enum ImportMode + { + /// + /// Create a new Import + /// + Create = 0, + /// + /// Update to Imported Items + /// + Update = 1 + } + + /// + /// Default constructor + /// + public ImportRequest() + { + Files = new List(); + } + + } + + /// + /// Describes an Individual Import Item. + /// + public class ImportFileItem + { + /// + /// File Name of Individual file + /// + public string FileName { get; set; } + /// + /// Type of Import file.. XML or CSV + /// + public FileTypeCode FileType { get; set; } + /// + /// This is the CSV file you wish to import, + /// + public string FileContentToImport { get; set; } + /// + /// This enabled duplicate detection rules + /// + public bool EnableDuplicateDetection { get; set; } + /// + /// Name of the entity that Originated the data. + /// + public string SourceEntityName { get; set; } + /// + /// Name of the entity that Target Entity the data. + /// + public string TargetEntityName { get; set; } + /// + /// This is the delimiter for the Data, + /// + public DataDelimiterCode DataDelimiter { get; set; } + /// + /// this is the field separator + /// + public FieldDelimiterCode FieldDelimiter { get; set; } + /// + /// Is the first row of the CSV the RowHeader? + /// + public bool IsFirstRowHeader { get; set; } + /// + /// UserID or Team ID of the Record Owner ( from systemuser ) + /// + public Guid RecordOwner { get; set; } + /// + /// Set true if the Record Owner is a Team + /// + public bool IsRecordOwnerATeam { get; set; } + + /// + /// Key used to delimit data in the import file + /// + public enum DataDelimiterCode + { + /// + /// Specifies " + /// + DoubleQuotes = 1, // " + /// + /// Specifies no delimiter + /// + None = 2, // + /// + /// Specifies ' + /// + SingleQuote = 3 // ' + } + + /// + /// Key used to delimit fields in the import file + /// + public enum FieldDelimiterCode + { + /// + /// Specifies : + /// + Colon = 1, + /// + /// Specifies , + /// + Comma = 2, + /// + /// Specifies ' + /// + SingleQuote = 3 + } + + /// + /// Type if file described in the FileContentToImport + /// + public enum FileTypeCode + { + /// + /// CSV File Type + /// + CSV = 0, + /// + /// XML File type + /// + XML = 1 + } + + } + + #endregion + + + #region Private + + /// + /// + /// Imports a Dataverse solution to the Dataverse Server currently connected. + /// *** Note: this is a blocking call and will take time to Import to Dataverse *** + /// + /// + /// Path to the Solution File + /// Activate Plugin's and workflows on the Solution + /// This will populate with the Import ID even if the request failed. + /// You can use this ID to request status on the import via a request to the ImportJob entity. + /// Forces an overwrite of unmanaged customizations of the managed solution you are installing, defaults to false + /// Skips dependency against dependencies flagged as product update, defaults to false + /// Applies only on Dataverse organizations version 7.2 or higher. This imports the Dataverse solution as a holding solution utilizing the “As Holding” capability of ImportSolution + /// Internal Microsoft use only + /// Requires the use of an Async Job to do the import. + /// ServiceClient + /// Extra parameters + /// Returns the Import Solution Job ID. To find the status of the job, query the ImportJob Entity using GetEntityDataByID using the returned value of this method + internal static Guid ImportSolutionToImpl(this ServiceClient serviceClient, string solutionPath, out Guid importId, bool activatePlugIns, bool overwriteUnManagedCustomizations, bool skipDependancyOnProductUpdateCheckOnInstall, bool importAsHoldingSolution, bool isInternalUpgrade, bool useAsync, Dictionary extraParameters) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + importId = Guid.Empty; + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); + return Guid.Empty; + } + + if (string.IsNullOrWhiteSpace(solutionPath)) + { + serviceClient._logEntry.Log("************ Exception on ImportSolutionToImpl, SolutionPath is required", TraceEventType.Error); + return Guid.Empty; + } + + // determine if the system is connected to OnPrem + bool isConnectedToOnPrem = (serviceClient._connectionSvc.ConnectedOrganizationDetail != null && string.IsNullOrEmpty(serviceClient._connectionSvc.ConnectedOrganizationDetail.Geo)); + + //Extract extra parameters if they exist + string solutionName = string.Empty; + LayerDesiredOrder desiredLayerOrder = null; + bool? asyncRibbonProcessing = null; + EntityCollection componetsToProcess = null; + bool? convertToManaged = null; + bool? isTemplateModeImport = null; + string templateSuffix = null; + + if (extraParameters != null) + { + solutionName = extraParameters.ContainsKey(ImportSolutionProperties.SOLUTIONNAMEPARAM) ? extraParameters[ImportSolutionProperties.SOLUTIONNAMEPARAM].ToString() : string.Empty; + desiredLayerOrder = extraParameters.ContainsKey(ImportSolutionProperties.DESIREDLAYERORDERPARAM) ? extraParameters[ImportSolutionProperties.DESIREDLAYERORDERPARAM] as LayerDesiredOrder : null; + componetsToProcess = extraParameters.ContainsKey(ImportSolutionProperties.COMPONENTPARAMETERSPARAM) ? extraParameters[ImportSolutionProperties.COMPONENTPARAMETERSPARAM] as EntityCollection : null; + convertToManaged = extraParameters.ContainsKey(ImportSolutionProperties.CONVERTTOMANAGED) ? extraParameters[ImportSolutionProperties.CONVERTTOMANAGED] as bool? : null; + isTemplateModeImport = extraParameters.ContainsKey(ImportSolutionProperties.ISTEMPLATEMODE) ? extraParameters[ImportSolutionProperties.ISTEMPLATEMODE] as bool? : null; + templateSuffix = extraParameters.ContainsKey(ImportSolutionProperties.TEMPLATESUFFIX) ? extraParameters[ImportSolutionProperties.TEMPLATESUFFIX].ToString() : string.Empty; + + // Pick up the data from the request, if the request has the AsyncRibbonProcessing flag, pick up the value of it. + asyncRibbonProcessing = extraParameters.ContainsKey(ImportSolutionProperties.ASYNCRIBBONPROCESSING) ? extraParameters[ImportSolutionProperties.ASYNCRIBBONPROCESSING] as bool? : null; + // If the value is populated, and t + if (asyncRibbonProcessing != null && asyncRibbonProcessing.HasValue) + { + if (isConnectedToOnPrem) + { + // Not supported for OnPrem. + // reset the asyncRibbonProcess to Null. + serviceClient._logEntry.Log($"ImportSolution request contains {ImportSolutionProperties.ASYNCRIBBONPROCESSING} property. This is not valid for OnPremise deployments and will be removed", TraceEventType.Warning); + asyncRibbonProcessing = null; + } + else + { + if (!Utilities.FeatureVersionMinimums.IsFeatureValidForEnviroment(serviceClient._connectionSvc?.OrganizationVersion, Utilities.FeatureVersionMinimums.AllowAsyncRibbonProcessing)) + { + // Not supported on this version of Dataverse + serviceClient._logEntry.Log($"ImportSolution request contains {ImportSolutionProperties.ASYNCRIBBONPROCESSING} property. This request Dataverse version {Utilities.FeatureVersionMinimums.AllowAsyncRibbonProcessing.ToString()} or above. Current Dataverse version is {serviceClient._connectionSvc?.OrganizationVersion}. This property will be removed", TraceEventType.Warning); + asyncRibbonProcessing = null; + } + } + } + + if (componetsToProcess != null) + { + if (isConnectedToOnPrem) + { + serviceClient._logEntry.Log($"ImportSolution request contains {ImportSolutionProperties.COMPONENTPARAMETERSPARAM} property. This is not valid for OnPremise deployments and will be removed", TraceEventType.Warning); + componetsToProcess = null; + } + else + { + if (!Utilities.FeatureVersionMinimums.IsFeatureValidForEnviroment(serviceClient._connectionSvc?.OrganizationVersion, Utilities.FeatureVersionMinimums.AllowComponetInfoProcessing)) + { + // Not supported on this version of Dataverse + serviceClient._logEntry.Log($"ImportSolution request contains {ImportSolutionProperties.COMPONENTPARAMETERSPARAM} property. This request Dataverse version {Utilities.FeatureVersionMinimums.AllowComponetInfoProcessing.ToString()} or above. Current Dataverse version is {serviceClient._connectionSvc?.OrganizationVersion}. This property will be removed", TraceEventType.Warning); + componetsToProcess = null; + } + } + } + + if (isTemplateModeImport != null) + { + if (isConnectedToOnPrem) + { + serviceClient._logEntry.Log($"ImportSolution request contains {ImportSolutionProperties.ISTEMPLATEMODE} property. This is not valid for OnPremise deployments and will be removed", TraceEventType.Warning); + isTemplateModeImport = null; + } + else + { + if (!Utilities.FeatureVersionMinimums.IsFeatureValidForEnviroment(serviceClient._connectionSvc?.OrganizationVersion, Utilities.FeatureVersionMinimums.AllowTemplateSolutionImport)) + { + // Not supported on this version of Dataverse + serviceClient._logEntry.Log($"ImportSolution request contains {ImportSolutionProperties.ISTEMPLATEMODE} property. This request Dataverse version {Utilities.FeatureVersionMinimums.AllowTemplateSolutionImport.ToString()} or above. Current Dataverse version is {serviceClient._connectionSvc?.OrganizationVersion}. This property will be removed", TraceEventType.Warning); + isTemplateModeImport = null; + } + } + } + } + + string solutionNameForLogging = string.IsNullOrWhiteSpace(solutionName) ? string.Empty : string.Concat(solutionName, " - "); + + // try to load the file from the file system + if (File.Exists(solutionPath)) + { + try + { + importId = Guid.NewGuid(); + byte[] fileData = File.ReadAllBytes(solutionPath); + ImportSolutionRequest SolutionImportRequest = new ImportSolutionRequest() + { + CustomizationFile = fileData, + PublishWorkflows = activatePlugIns, + ImportJobId = importId, + OverwriteUnmanagedCustomizations = overwriteUnManagedCustomizations + }; + + //If the desiredLayerOrder is null don't add it to the request. This ensures backward compatibility. It makes old packages work on old builds + if (desiredLayerOrder != null) + { + //If package contains the LayerDesiredOrder hint but the server doesn't support the new message, we want the package to fail + //The server will throw - "Unrecognized request parameter: LayerDesiredOrder" - That's the desired behavior + //The hint is only enforced on the first time a solution is added to the org. If we allow it to go, the import will succeed, but the desired state won't be achieved + SolutionImportRequest.LayerDesiredOrder = desiredLayerOrder; + + string solutionsInHint = string.Join(",", desiredLayerOrder.Solutions.Select(n => n.Name).ToArray()); + + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "{0}DesiredLayerOrder clause present: Type: {1}, Solutions: {2}", solutionNameForLogging, desiredLayerOrder.Type, solutionsInHint), TraceEventType.Verbose); + } + + if (asyncRibbonProcessing != null && asyncRibbonProcessing == true) + { + SolutionImportRequest.AsyncRibbonProcessing = true; + SolutionImportRequest.SkipQueueRibbonJob = true; + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "{0} AsyncRibbonProcessing: {1}", solutionNameForLogging, true), TraceEventType.Verbose); + } + + if (componetsToProcess != null) + { + SolutionImportRequest.ComponentParameters = componetsToProcess; + } + + if (convertToManaged != null) + { + SolutionImportRequest.ConvertToManaged = convertToManaged.Value; + } + + if (isTemplateModeImport != null && isTemplateModeImport.Value) + { + SolutionImportRequest.Parameters[ImportSolutionProperties.ISTEMPLATEMODE] = isTemplateModeImport.Value; + SolutionImportRequest.Parameters[ImportSolutionProperties.TEMPLATESUFFIX] = templateSuffix; + } + + if (serviceClient.IsBatchOperationsAvailable) + { + // Support for features added in UR12 + SolutionImportRequest.SkipProductUpdateDependencies = skipDependancyOnProductUpdateCheckOnInstall; + } + + if (importAsHoldingSolution) // If Import as Holding is set.. + { + // Check for Min version of Dataverse for support of Import as Holding solution. + if (Utilities.FeatureVersionMinimums.IsFeatureValidForEnviroment(serviceClient._connectionSvc?.OrganizationVersion, Utilities.FeatureVersionMinimums.ImportHoldingSolution)) + { + // Use Parameters to add the property here to support the underlying Xrm API on the incorrect version. + SolutionImportRequest.Parameters.Add("HoldingSolution", importAsHoldingSolution); + } + } + + // Set IsInternalUpgrade flag on request only for upgrade scenario for V9 only. + if (isInternalUpgrade) + { + if (Utilities.FeatureVersionMinimums.IsFeatureValidForEnviroment(serviceClient._connectionSvc?.OrganizationVersion, Utilities.FeatureVersionMinimums.InternalUpgradeSolution)) + { + SolutionImportRequest.Parameters["IsInternalUpgrade"] = true; + } + } + + if (useAsync) + { + // Assign Tracking ID + Guid requestTrackingId = Guid.NewGuid(); + SolutionImportRequest.RequestId = requestTrackingId; + + if (!isConnectedToOnPrem && Utilities.FeatureVersionMinimums.IsFeatureValidForEnviroment(serviceClient.ConnectedOrgVersion, Utilities.FeatureVersionMinimums.AllowImportSolutionAsyncV2)) + { + // map import request to Async Model + ImportSolutionAsyncRequest asynImportRequest = new ImportSolutionAsyncRequest() + { + AsyncRibbonProcessing = SolutionImportRequest.AsyncRibbonProcessing, + ComponentParameters = SolutionImportRequest.ComponentParameters, + ConvertToManaged = SolutionImportRequest.ConvertToManaged, + CustomizationFile = SolutionImportRequest.CustomizationFile, + HoldingSolution = SolutionImportRequest.HoldingSolution, + LayerDesiredOrder = SolutionImportRequest.LayerDesiredOrder, + OverwriteUnmanagedCustomizations = SolutionImportRequest.OverwriteUnmanagedCustomizations, + Parameters = SolutionImportRequest.Parameters, + PublishWorkflows = SolutionImportRequest.PublishWorkflows, + RequestId = SolutionImportRequest.RequestId, + SkipProductUpdateDependencies = SolutionImportRequest.SkipProductUpdateDependencies, + SkipQueueRibbonJob = SolutionImportRequest.SkipQueueRibbonJob + }; + + // remove unsupported parameter from importsolutionasync request. + if (asynImportRequest.Parameters.ContainsKey("ImportJobId")) + asynImportRequest.Parameters.Remove("ImportJobId"); + + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "{1}Created Async ImportSolutionAsyncRequest : RequestID={0} ", requestTrackingId.ToString(), solutionNameForLogging), TraceEventType.Verbose); + ImportSolutionAsyncResponse asyncResp = (ImportSolutionAsyncResponse)serviceClient.Command_Execute(asynImportRequest, solutionNameForLogging + "Executing Request for ImportSolutionAsyncRequest : "); + if (asyncResp == null) + return Guid.Empty; + else + return asyncResp.AsyncOperationId; + } + else + { + // Creating Async Solution Import request. + ExecuteAsyncRequest req = new ExecuteAsyncRequest() { Request = SolutionImportRequest }; + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "{1}Created Async ImportSolutionRequest : RequestID={0} ", + requestTrackingId.ToString(), solutionNameForLogging), TraceEventType.Verbose); + ExecuteAsyncResponse asyncResp = (ExecuteAsyncResponse)serviceClient.Command_Execute(req, solutionNameForLogging + "Executing Request for ImportSolutionToAsync : "); + if (asyncResp == null) + return Guid.Empty; + else + return asyncResp.AsyncJobId; + } + } + else + { + ImportSolutionResponse resp = (ImportSolutionResponse)serviceClient.Command_Execute(SolutionImportRequest, solutionNameForLogging + "Executing ImportSolutionRequest for ImportSolution"); + if (resp == null) + return Guid.Empty; + else + return importId; + } + } + #region Exception handlers for files + catch (UnauthorizedAccessException ex) + { + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception on ImportSolution, Unauthorized Access to file: {0}", solutionPath), TraceEventType.Error, ex); + return Guid.Empty; + } + catch (ArgumentNullException ex) + { + serviceClient._logEntry.Log("************ Exception on ImportSolutionToCds, File path not specified", TraceEventType.Error, ex); + return Guid.Empty; + } + catch (ArgumentException ex) + { + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception on ImportSolution, File path is invalid: {0}", solutionPath), TraceEventType.Error, ex); + return Guid.Empty; + } + catch (PathTooLongException ex) + { + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception on ImportSolution, File path is too long. Paths must be less than 248 characters, and file names must be less than 260 characters\n{0}", solutionPath), TraceEventType.Error, ex); + return Guid.Empty; + } + catch (DirectoryNotFoundException ex) + { + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception on ImportSolution, File path is invalid: {0}", solutionPath), TraceEventType.Error, ex); + return Guid.Empty; + } + catch (FileNotFoundException ex) + { + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception on ImportSolution, File Not Found: {0}", solutionPath), TraceEventType.Error, ex); + return Guid.Empty; + } + catch (NotSupportedException ex) + { + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception on ImportSolution, File path or name is invalid: {0}", solutionPath), TraceEventType.Error, ex); + return Guid.Empty; + } + #endregion + } + else + { + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception on ImportSolution, File path specified in dataMapXml is not found: {0}", solutionPath), TraceEventType.Error); + return Guid.Empty; + } + } + + #endregion + } +} diff --git a/src/GeneralTools/DataverseClient/Client/Extensions/GeneralExtensions.cs b/src/GeneralTools/DataverseClient/Client/Extensions/GeneralExtensions.cs new file mode 100644 index 0000000..d23c3b6 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Extensions/GeneralExtensions.cs @@ -0,0 +1,244 @@ +using Microsoft.Crm.Sdk.Messages; +using Microsoft.Xrm.Sdk; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; + +namespace Microsoft.PowerPlatform.Dataverse.Client.Extensions +{ + /// + /// General Extensions for the Dataverse ServiceClient + /// + public static class GeneralExtensions + { + #region Dataverse Service Methods + + /// + /// Executes a named workflow on an object. + /// + /// name of the workflow to run + /// ID to exec against + /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// ServiceClient + /// Async Op ID of the WF or Guid.Empty + public static Guid ExecuteWorkflowOnEntity(this ServiceClient serviceClient, string workflowName, Guid id, Guid batchId = default(Guid), bool bypassPluginExecution = false) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); + return Guid.Empty; + } + + if (id == Guid.Empty) + { + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception Executing workflow ({0}) on ID {1} in Dataverse : " + "Target Entity Was not provided", workflowName, id), TraceEventType.Error); + return Guid.Empty; + } + + if (string.IsNullOrEmpty(workflowName)) + { + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception Executing workflow ({0}) on ID {1} in Dataverse : " + "Workflow Name Was not provided", workflowName, id), TraceEventType.Error); + return Guid.Empty; + } + + Dictionary SearchParm = new Dictionary(); + SearchParm.Add("name", workflowName); + + Dictionary> rslts = + serviceClient.GetEntityDataBySearchParams("workflow", SearchParm, LogicalSearchOperator.None, null, bypassPluginExecution: bypassPluginExecution); + + if (rslts != null) + { + if (rslts.Count > 0) + { + foreach (Dictionary row in rslts.Values) + { + if (serviceClient.GetDataByKeyFromResultsSet(row, "parentworkflowid") != Guid.Empty) + continue; + Guid guWorkflowID = serviceClient.GetDataByKeyFromResultsSet(row, "workflowid"); + if (guWorkflowID != Guid.Empty) + { + // Ok try to exec the workflow request + ExecuteWorkflowRequest wfRequest = new ExecuteWorkflowRequest(); + wfRequest.EntityId = id; + wfRequest.WorkflowId = guWorkflowID; + + if (serviceClient.AddRequestToBatch(batchId, wfRequest, string.Format(CultureInfo.InvariantCulture, "Executing workflow ({0}) on ID {1}", workflowName, id), + string.Format(CultureInfo.InvariantCulture, "Request to Execute workflow ({0}) on ID {1} Queued", workflowName, id), bypassPluginExecution)) + return Guid.Empty; + + ExecuteWorkflowResponse wfResponse = (ExecuteWorkflowResponse)serviceClient.Command_Execute(wfRequest, string.Format(CultureInfo.InvariantCulture, "Executing workflow ({0}) on ID {1}", workflowName, id), bypassPluginExecution); + if (wfResponse != null) + return wfResponse.Id; + else + return Guid.Empty; + } + else + { + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception Executing workflow ({0}) on ID {1} in Dataverse : " + "Unable to Find Workflow by ID", workflowName, id), TraceEventType.Error); + } + } + } + else + { + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception Executing workflow ({0}) on ID {1} in Dataverse : " + "Unable to Find Workflow by Name", workflowName, id), TraceEventType.Error); + } + } + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception Executing workflow ({0}) on ID {1} in Dataverse : " + "Unable to Find Workflow by Name Search", workflowName, id), TraceEventType.Error); + return Guid.Empty; + } + + /// + /// Assign an Entity to the specified user ID + /// + /// User ID to assign too + /// Target entity Name + /// Target entity id + /// Batch ID of to use, Optional + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// ServiceClient + /// + public static bool AssignEntityToUser(this ServiceClient serviceClient, Guid userId, string entityName, Guid entityId, Guid batchId = default(Guid), bool bypassPluginExecution = false) + { + + + serviceClient._logEntry.ResetLastError(); // Reset Last Error + if (serviceClient.DataverseService == null || userId == Guid.Empty || entityId == Guid.Empty) + { + return false; + } + + AssignRequest assignRequest = new AssignRequest(); + assignRequest.Assignee = new EntityReference("systemuser", userId); + assignRequest.Target = new EntityReference(entityName, entityId); + + if (serviceClient.AddRequestToBatch(batchId, assignRequest, string.Format(CultureInfo.InvariantCulture, "Assigning entity ({0}) to {1}", entityName, userId.ToString()), + string.Format(CultureInfo.InvariantCulture, "Request to Assign entity ({0}) to {1} Queued", entityName, userId.ToString()), bypassPluginExecution)) + return true; + + AssignResponse arResp = (AssignResponse)serviceClient.Command_Execute(assignRequest, "Assigning Entity to User", bypassPluginExecution); + if (arResp != null) + return true; + + return false; + } + + /// + /// This will route a Entity to a public queue, + /// + /// ID of the Entity to route + /// Name of the Entity that the Id describes + /// Name of the Queue to Route Too + /// ID of the user id to set as the working system user + /// if true Set the worked by when doing the assign + /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// ServiceClient + /// true on success + public static bool AddEntityToQueue(this ServiceClient serviceClient, Guid entityId, string entityName, string queueName, Guid workingUserId, bool setWorkingByUser = false, Guid batchId = default(Guid), bool bypassPluginExecution = false) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + if (serviceClient.DataverseService == null || entityId == Guid.Empty) + { + return false; + } + + Dictionary SearchParams = new Dictionary(); + SearchParams.Add("name", queueName); + + // Get the Target QUeue + Dictionary> rslts = serviceClient.GetEntityDataBySearchParams("queue", SearchParams, LogicalSearchOperator.None, null); + if (rslts != null) + if (rslts.Count > 0) + { + Guid guQueueID = Guid.Empty; + foreach (Dictionary row in rslts.Values) + { + // got something + guQueueID = serviceClient.GetDataByKeyFromResultsSet(row, "queueid"); + break; + } + + if (guQueueID != Guid.Empty) + { + + + AddToQueueRequest req = new AddToQueueRequest(); + req.DestinationQueueId = guQueueID; + req.Target = new EntityReference(entityName, entityId); + + // Set the worked by user if the request includes it. + if (setWorkingByUser) + { + Entity queItm = new Entity("queueitem"); + queItm.Attributes.Add("workerid", new EntityReference("systemuser", workingUserId)); + req.QueueItemProperties = queItm; + } + + if (serviceClient.AddRequestToBatch(batchId, req, string.Format(CultureInfo.InvariantCulture, "Assigning entity to queue ({0}) to {1}", entityName, guQueueID.ToString()), + string.Format(CultureInfo.InvariantCulture, "Request to Assign entity to queue ({0}) to {1} Queued", entityName, guQueueID.ToString()), bypassPluginExecution)) + return true; + + AddToQueueResponse resp = (AddToQueueResponse)serviceClient.Command_Execute(req, string.Format(CultureInfo.InvariantCulture, "Adding a item to queue {0} in CDS", queueName), bypassPluginExecution); + if (resp != null) + return true; + else + return false; + } + } + return false; + } + + /// + /// this will send an Email to the + /// + /// ID of the Email activity + /// Tracking Token or Null + /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// ServiceClient + /// + public static bool SendSingleEmail(this ServiceClient serviceClient, Guid emailid, string token, Guid batchId = default(Guid), bool bypassPluginExecution = false) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + if (serviceClient.DataverseService == null || emailid == Guid.Empty) + { + return false; + } + + if (token == null) + token = string.Empty; + + // Send the mail now. + SendEmailRequest req = new SendEmailRequest(); + req.EmailId = emailid; + req.TrackingToken = token; + req.IssueSend = true; // Send it now. + + if (serviceClient.AddRequestToBatch(batchId, req, string.Format(CultureInfo.InvariantCulture, "Send Direct email ({0}) tracking token {1}", emailid.ToString(), token), + string.Format(CultureInfo.InvariantCulture, "Request to Send Direct email ({0}) tracking token {1} Queued", emailid.ToString(), token), bypassPluginExecution)) + return true; + + SendEmailResponse sendresp = (SendEmailResponse)serviceClient.Command_Execute(req, string.Format(CultureInfo.InvariantCulture, "Sending email ({0}) from Dataverse", emailid), bypassPluginExecution); + if (sendresp != null) + return true; + else + return false; + } + + /// + /// Returns the user ID of the currently logged in user. + /// + /// ServiceClient + /// + public static Guid GetMyUserId(this ServiceClient serviceClient) + { + return serviceClient.SystemUser.UserId; + } + + #endregion + } +} diff --git a/src/GeneralTools/DataverseClient/Client/Extensions/MetadataExtensions.cs b/src/GeneralTools/DataverseClient/Client/Extensions/MetadataExtensions.cs new file mode 100644 index 0000000..ac34b56 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Extensions/MetadataExtensions.cs @@ -0,0 +1,755 @@ +using Microsoft.Crm.Sdk.Messages; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Messages; +using Microsoft.Xrm.Sdk.Metadata; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PowerPlatform.Dataverse.Client.Extensions +{ + /// + /// Extensions for interacting with the Dataverse Metadata system. + /// + public static class MetadataExtensions + { + #region Dataverse MetadataService methods + + + /// + /// Gets a PickList, Status List or StateList from the metadata of an attribute + /// + /// text name of the entity to query + /// name of the attribute to query + /// ServiceClient + /// + public static PickListMetaElement GetPickListElementFromMetadataEntity(this ServiceClient serviceClient, string targetEntity, string attribName) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + if (serviceClient.DataverseService != null) + { + List attribDataList = serviceClient._dynamicAppUtility.GetAttributeDataByEntity(targetEntity, attribName); + if (attribDataList.Count > 0) + { + // have data.. + // need to make sure its really a pick list. + foreach (AttributeData attributeData in attribDataList) + { + switch (attributeData.AttributeType) + { + case AttributeTypeCode.Picklist: + case AttributeTypeCode.Status: + case AttributeTypeCode.State: + PicklistAttributeData pick = (PicklistAttributeData)attributeData; + PickListMetaElement resp = new PickListMetaElement((string)pick.ActualValue, pick.AttributeLabel, pick.DisplayValue); + if (pick.PicklistOptions != null) + { + foreach (OptionMetadata opt in pick.PicklistOptions) + { + PickListItem itm = null; + itm = new PickListItem((string)GetLocalLabel(opt.Label), (int)opt.Value.Value); + resp.Items.Add(itm); + } + } + return resp; + default: + break; + } + } + } + } + return null; + } + + /// + /// Gets a global option set from Dataverse. + /// + /// Name of the Option Set To get + /// ServiceClient + /// OptionSetMetadata or null + public static OptionSetMetadata GetGlobalOptionSetMetadata(this ServiceClient serviceClient, string globalOptionSetName) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); + return null; + } + + try + { + return serviceClient._metadataUtlity.GetGlobalOptionSetMetadata(globalOptionSetName); + } + catch (Exception ex) + { + serviceClient._logEntry.Log("************ Exception getting optionset metadata info from Dataverse : " + ex.Message, TraceEventType.Error); + } + return null; + } + + + /// + /// Returns a list of entities with basic data from Dataverse + /// + /// defaults to true, will only return published information + /// EntityFilter to apply to this request, note that filters other then Default will consume more time. + /// ServiceClient + /// + public static List GetAllEntityMetadata(this ServiceClient serviceClient, bool onlyPublished = true, EntityFilters filter = EntityFilters.Default) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + #region Basic Checks + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); + return null; + } + #endregion + + try + { + return serviceClient._metadataUtlity.GetAllEntityMetadata(onlyPublished, filter); + } + catch (Exception ex) + { + serviceClient._logEntry.Log("************ Exception getting metadata info from CDS : " + ex.Message, TraceEventType.Error); + } + return null; + } + + /// + /// Returns the Metadata for an entity from Dataverse, defaults to basic data only. + /// + /// Logical name of the entity + /// filter to apply to the query, defaults to default entity data. + /// ServiceClient + /// + public static EntityMetadata GetEntityMetadata(this ServiceClient serviceClient, string entityLogicalname, EntityFilters queryFilter = EntityFilters.Default) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + #region Basic Checks + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); + return null; + } + #endregion + + try + { + return serviceClient._metadataUtlity.GetEntityMetadata(queryFilter, entityLogicalname); + } + catch (Exception ex) + { + serviceClient._logEntry.Log("************ Exception getting metadata info from Dataverse : " + ex.Message, TraceEventType.Error); + } + return null; + } + + /// + /// Returns the Form Entity References for a given form type. + /// + /// logical name of the entity you are querying for form data. + /// Form Type you want + /// ServiceClient + /// List of Entity References for the form type requested. + public static List GetEntityFormIdListByType(this ServiceClient serviceClient, string entityLogicalname, FormTypeId formTypeId) + { + serviceClient._logEntry.ResetLastError(); + #region Basic Checks + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); + return null; + } + if (string.IsNullOrWhiteSpace(entityLogicalname)) + { + serviceClient._logEntry.Log("An Entity Name must be supplied", TraceEventType.Error); + return null; + } + #endregion + + try + { + RetrieveFilteredFormsRequest req = new RetrieveFilteredFormsRequest(); + req.EntityLogicalName = entityLogicalname; + req.FormType = new OptionSetValue((int)formTypeId); + RetrieveFilteredFormsResponse resp = (RetrieveFilteredFormsResponse)serviceClient.Command_Execute(req, "GetEntityFormIdListByType"); + if (resp != null) + return resp.SystemForms.ToList(); + else + return null; + } + catch (Exception ex) + { + serviceClient._logEntry.Log("************ Exception getting metadata info from Dataverse : " + ex.Message, TraceEventType.Error); + } + return null; + } + + /// + /// Returns all attributes on a entity + /// + /// returns all attributes on a entity + /// ServiceClient + /// + public static List GetAllAttributesForEntity(this ServiceClient serviceClient, string entityLogicalname) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + #region Basic Checks + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); + return null; + } + if (string.IsNullOrWhiteSpace(entityLogicalname)) + { + serviceClient._logEntry.Log("An Entity Name must be supplied", TraceEventType.Error); + return null; + } + #endregion + + try + { + return serviceClient._metadataUtlity.GetAllAttributesMetadataByEntity(entityLogicalname); + } + catch (Exception ex) + { + serviceClient._logEntry.Log("************ Exception getting metadata info from Dataverse : " + ex.Message, TraceEventType.Error); + } + return null; + } + + /// + /// Gets metadata for a specific entity's attribute. + /// + /// Name of the entity + /// Attribute Name + /// ServiceClient + /// + public static AttributeMetadata GetEntityAttributeMetadataForAttribute(this ServiceClient serviceClient, string entityLogicalname, string attribName) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + #region Basic Checks + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); + return null; + } + if (string.IsNullOrWhiteSpace(entityLogicalname)) + { + serviceClient._logEntry.Log("An Entity Name must be supplied", TraceEventType.Error); + return null; + } + #endregion + + try + { + return serviceClient._metadataUtlity.GetAttributeMetadata(entityLogicalname, attribName); + } + catch (Exception ex) + { + serviceClient._logEntry.Log("************ Exception getting metadata info from Dataverse : " + ex.Message, TraceEventType.Error); + } + return null; + } + + /// + /// Gets an Entity Name by Logical name or Type code. + /// + /// logical name of the entity + /// Type code for the entity + /// ServiceClient + /// Localized name for the entity in the current users language + public static string GetEntityDisplayName(this ServiceClient serviceClient, string entityName, int entityTypeCode = -1) + { + return serviceClient.GetEntityDisplayNameImpl(entityName, entityTypeCode); + } + + /// + /// Gets an Entity Name by Logical name or Type code. + /// + /// logical name of the entity + /// Type code for the entity + /// ServiceClient + /// Localized plural name for the entity in the current users language + public static string GetEntityDisplayNamePlural(this ServiceClient serviceClient, string entityName, int entityTypeCode = -1) + { + return serviceClient.GetEntityDisplayNameImpl(entityName, entityTypeCode, true); + } + + /// + /// This will clear the Metadata cache for either all entities or the specified entity + /// + /// ServiceClient + /// Optional: name of the entity to clear cached info for + public static void ResetLocalMetadataCache(this ServiceClient serviceClient, string entityName = "") + { + if (serviceClient._metadataUtlity != null) + serviceClient._metadataUtlity.ClearCachedEntityMetadata(entityName); + } + + /// + /// Gets the Entity Display Name. + /// + /// + /// + /// + /// ServiceClient + /// + private static string GetEntityDisplayNameImpl(this ServiceClient serviceClient, string entityName, int entityTypeCode = -1, bool getPlural = false) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + #region Basic Checks + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); + return string.Empty; + } + + if (entityTypeCode == -1 && string.IsNullOrWhiteSpace(entityName)) + { + serviceClient._logEntry.Log("Target entity or Type code is required", TraceEventType.Error); + return string.Empty; + } + #endregion + + try + { + // Get the entity by type code if necessary. + if (entityTypeCode != -1) + entityName = serviceClient._metadataUtlity.GetEntityLogicalName(entityTypeCode); + + if (string.IsNullOrWhiteSpace(entityName)) + { + serviceClient._logEntry.Log("Target entity or Type code is required", TraceEventType.Error); + return string.Empty; + } + + // Pull Object type code for this object. + EntityMetadata entData = + serviceClient._metadataUtlity.GetEntityMetadata(EntityFilters.Entity, entityName); + + if (entData != null) + { + if (getPlural) + { + if (entData.DisplayCollectionName != null && entData.DisplayCollectionName.UserLocalizedLabel != null) + return entData.DisplayCollectionName.UserLocalizedLabel.Label; + else + return entityName; // Default to echo the same name back + } + else + { + if (entData.DisplayName != null && entData.DisplayName.UserLocalizedLabel != null) + return entData.DisplayName.UserLocalizedLabel.Label; + else + return entityName; // Default to echo the same name back + } + } + + } + catch (Exception ex) + { + serviceClient._logEntry.Log("************ Exception getting metadata info from Dataverse : " + ex.Message, TraceEventType.Error); + } + return string.Empty; + } + + /// + /// Gets the typecode of an entity by name. + /// + /// name of the entity to get the type code on + /// ServiceClient + /// + public static string GetEntityTypeCode(this ServiceClient serviceClient, string entityName) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + #region Basic Checks + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); + return string.Empty; + } + + if (string.IsNullOrEmpty(entityName)) + { + serviceClient._logEntry.Log("Target entity is required", TraceEventType.Error); + return string.Empty; + } + #endregion + + try + { + + // Pull Object type code for this object. + EntityMetadata entData = + serviceClient._metadataUtlity.GetEntityMetadata(EntityFilters.Entity, entityName); + + if (entData != null) + { + if (entData.ObjectTypeCode != null && entData.ObjectTypeCode.HasValue) + { + return entData.ObjectTypeCode.Value.ToString(CultureInfo.InvariantCulture); + } + } + } + catch (Exception ex) + { + serviceClient._logEntry.Log("************ Exception getting metadata info from Dataverse : " + ex.Message, TraceEventType.Error); + } + return string.Empty; + } + + + /// + /// Returns the Entity name for the given Type code + /// + /// + /// ServiceClient + /// + public static string GetEntityName(this ServiceClient serviceClient, int entityTypeCode) + { + return serviceClient._metadataUtlity.GetEntityLogicalName(entityTypeCode); + } + + + /// + /// Adds an option to a pick list on an entity. + /// + /// Entity Name to Target + /// Attribute Name on the Entity + /// List of Localized Labels + /// integer Value + /// Publishes the Update to the Live system.. note this is a time consuming process.. if you are doing a batch up updates, call PublishEntity Separately when you are finished. + /// ServiceClient + /// true on success, on fail check last error. + public static bool CreateOrUpdatePickListElement(this ServiceClient serviceClient, string targetEntity, string attribName, List locLabelList, int valueData, bool publishOnComplete) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + #region Basic Checks + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); + return false; + } + + if (string.IsNullOrEmpty(targetEntity)) + { + serviceClient._logEntry.Log("Target entity is required", TraceEventType.Error); + return false; + } + + if (string.IsNullOrEmpty(attribName)) + { + serviceClient._logEntry.Log("Target attribute name is required", TraceEventType.Error); + return false; + } + + if (locLabelList == null || locLabelList.Count == 0) + { + serviceClient._logEntry.Log("Target Labels are required", TraceEventType.Error); + return false; + } + + serviceClient.LoadLCIDs(); // Load current languages . + + // Clear out the Metadata for this object. + if (serviceClient._metadataUtlity != null) + serviceClient._metadataUtlity.ClearCachedEntityMetadata(targetEntity); + + EntityMetadata entData = + serviceClient._metadataUtlity.GetEntityMetadata(targetEntity); + + if (!entData.IsCustomEntity.Value) + { + // Only apply this if the entity is not a custom entity + if (valueData <= 199999) + { + serviceClient._logEntry.Log("Option Value must exceed 200000", TraceEventType.Error); + return false; + } + } + #endregion + + // get the values for the requested attribute. + PickListMetaElement listData = serviceClient.GetPickListElementFromMetadataEntity(targetEntity, attribName); + if (listData == null) + { + // error here. + } + + bool isUpdate = false; + if (listData.Items != null && listData.Items.Count != 0) + { + // Check to see if the value we are looking to insert already exists by name or value. + List DisplayLabels = new List(); + foreach (LocalizedLabel loclbl in locLabelList) + { + if (DisplayLabels.Contains(loclbl.Label)) + continue; + else + DisplayLabels.Add(loclbl.Label); + } + + foreach (PickListItem pItem in listData.Items) + { + // check the value by id. + if (pItem.PickListItemId == valueData) + { + if (DisplayLabels.Contains(pItem.DisplayLabel)) + { + DisplayLabels.Clear(); + serviceClient._logEntry.Log("PickList Element exists, No Change required.", TraceEventType.Error); + return false; + } + isUpdate = true; + break; + } + + //// Check the value by name... by putting this hear, we will handle a label update vs a Duplicate label. + if (DisplayLabels.Contains(pItem.DisplayLabel)) + { + // THis is an ERROR State... While Dataverse will allow 2 labels with the same text, it looks weird. + DisplayLabels.Clear(); + serviceClient._logEntry.Log("Label Name exists, Please use a different display name for the label.", TraceEventType.Error); + return false; + } + } + + DisplayLabels.Clear(); + } + + if (isUpdate) + { + // update request + UpdateOptionValueRequest updateReq = new UpdateOptionValueRequest(); + updateReq.AttributeLogicalName = attribName; + updateReq.EntityLogicalName = targetEntity; + updateReq.Label = new Label(); + List lblList = new List(); + foreach (LocalizedLabel loclbl in locLabelList) + { + if (serviceClient._loadedLCIDList.Contains(loclbl.LanguageCode)) + { + LocalizedLabel lbl = new LocalizedLabel() + { + Label = loclbl.Label, + LanguageCode = loclbl.LanguageCode + }; + lblList.Add(lbl); + } + } + updateReq.Label.LocalizedLabels.AddRange(lblList.ToArray()); + updateReq.Value = valueData; + updateReq.MergeLabels = true; + + UpdateOptionValueResponse UpdateResp = (UpdateOptionValueResponse)serviceClient.Command_Execute(updateReq, "Updating a PickList Element in Dataverse"); + if (UpdateResp == null) + return false; + } + else + { + // create request. + // Create a new insert request + InsertOptionValueRequest req = new InsertOptionValueRequest(); + + req.AttributeLogicalName = attribName; + req.EntityLogicalName = targetEntity; + req.Label = new Label(); + List lblList = new List(); + foreach (LocalizedLabel loclbl in locLabelList) + { + if (serviceClient._loadedLCIDList.Contains(loclbl.LanguageCode)) + { + LocalizedLabel lbl = new LocalizedLabel() + { + Label = loclbl.Label, + LanguageCode = loclbl.LanguageCode + }; + lblList.Add(lbl); + } + } + req.Label.LocalizedLabels.AddRange(lblList.ToArray()); + req.Value = valueData; + + + InsertOptionValueResponse resp = (InsertOptionValueResponse)serviceClient.Command_Execute(req, "Creating a PickList Element in Dataverse"); + if (resp == null) + return false; + + } + + // Publish the update if asked to. + if (publishOnComplete) + return serviceClient.PublishEntity(targetEntity); + else + return true; + } + + /// + /// Publishes an entity to the production system, + /// used in conjunction with the Metadata services. + /// + /// Name of the entity to publish + /// ServiceClient + /// True on success + public static bool PublishEntity(this ServiceClient serviceClient, string entityName) + { + // Now Publish the update. + string sPublishUpdateXml = + string.Format(CultureInfo.InvariantCulture, "{0}", + entityName); + + PublishXmlRequest pubReq = new PublishXmlRequest(); + pubReq.ParameterXml = sPublishUpdateXml; + + PublishXmlResponse rsp = (PublishXmlResponse)serviceClient.Command_Execute(pubReq, "Publishing a PickList Element in Dataverse"); + if (rsp != null) + return true; + else + return false; + } + + /// + /// Loads the Currently loaded languages for Dataverse + /// + /// + internal static bool LoadLCIDs(this ServiceClient serviceClient) + { + // Now Publish the update. + // Check to see if the Language ID's are loaded. + if (serviceClient._loadedLCIDList == null) + { + serviceClient._loadedLCIDList = new List(); + + // load the Dataverse Language List. + RetrieveAvailableLanguagesRequest lanReq = new RetrieveAvailableLanguagesRequest(); + RetrieveAvailableLanguagesResponse rsp = (RetrieveAvailableLanguagesResponse)serviceClient.Command_Execute(lanReq, "Reading available languages from Dataverse"); + if (rsp == null) + return false; + if (rsp.LocaleIds != null) + { + foreach (int iLCID in rsp.LocaleIds) + { + if (serviceClient._loadedLCIDList.Contains(iLCID)) + continue; + else + serviceClient._loadedLCIDList.Add(iLCID); + } + } + } + return true; + } + + #endregion + + #region Utilities + + + /// + /// Adds values for an update to a Dataverse propertyList + /// + /// + /// + /// ServiceClient + /// + internal static void AddValueToPropertyList(this ServiceClient serviceClient, KeyValuePair Field, AttributeCollection PropertyList) + { + if (string.IsNullOrEmpty(Field.Key)) + // throw exception + throw new System.ArgumentOutOfRangeException("valueArray", "Missing Dataverse field name"); + + try + { + switch (Field.Value.Type) + { + + case DataverseFieldType.Boolean: + PropertyList.Add(new KeyValuePair(Field.Key, (bool)Field.Value.Value)); + break; + + case DataverseFieldType.DateTime: + PropertyList.Add(new KeyValuePair(Field.Key, (DateTime)Field.Value.Value)); + break; + + case DataverseFieldType.Decimal: + PropertyList.Add(new KeyValuePair(Field.Key, Convert.ToDecimal(Field.Value.Value))); + break; + + case DataverseFieldType.Float: + PropertyList.Add(new KeyValuePair(Field.Key, Convert.ToDouble(Field.Value.Value))); + break; + + case DataverseFieldType.Money: + PropertyList.Add(new KeyValuePair(Field.Key, new Money(Convert.ToDecimal(Field.Value.Value)))); + break; + + case DataverseFieldType.Number: + PropertyList.Add(new KeyValuePair(Field.Key, (int)Field.Value.Value)); + break; + + case DataverseFieldType.Customer: + PropertyList.Add(new KeyValuePair(Field.Key, new EntityReference(Field.Value.ReferencedEntity, (Guid)Field.Value.Value))); + break; + + case DataverseFieldType.Lookup: + PropertyList.Add(new KeyValuePair(Field.Key, new EntityReference(Field.Value.ReferencedEntity, (Guid)Field.Value.Value))); + break; + + case DataverseFieldType.Picklist: + PropertyList.Add(new KeyValuePair(Field.Key, new OptionSetValue((int)Field.Value.Value))); + break; + + case DataverseFieldType.String: + PropertyList.Add(new KeyValuePair(Field.Key, (string)Field.Value.Value)); + break; + + case DataverseFieldType.Raw: + PropertyList.Add(new KeyValuePair(Field.Key, Field.Value.Value)); + break; + + case DataverseFieldType.UniqueIdentifier: + PropertyList.Add(new KeyValuePair(Field.Key, (Guid)Field.Value.Value)); + break; + } + } + catch (InvalidCastException castEx) + { + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "Failed when casting DataverseDataTypeWrapper wrapped objects to the Dataverse Type. Field : {0}", Field.Key), TraceEventType.Error, castEx); + throw; + } + catch (System.Exception ex) + { + serviceClient._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "Failed when casting DataverseDataTypeWrapper wrapped objects to the Dataverse Type. Field : {0}", Field.Key), TraceEventType.Error, ex); + throw; + } + + } + + /// + /// Get the localize label from a Dataverse Label. + /// + /// + /// + private static string GetLocalLabel(Label localLabel) + { + foreach (LocalizedLabel lbl in localLabel.LocalizedLabels) + { + // try to get the current display language code. + if (lbl.LanguageCode == CultureInfo.CurrentUICulture.LCID) + { + return lbl.Label; + } + } + return localLabel.UserLocalizedLabel.Label; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/GeneralTools/DataverseClient/Client/Extensions/QueryExtensions.cs b/src/GeneralTools/DataverseClient/Client/Extensions/QueryExtensions.cs new file mode 100644 index 0000000..57d0799 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Extensions/QueryExtensions.cs @@ -0,0 +1,1199 @@ +using Microsoft.Crm.Sdk.Messages; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Messages; +using Microsoft.Xrm.Sdk.Query; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml; + +namespace Microsoft.PowerPlatform.Dataverse.Client.Extensions +{ + /// + /// Extentions to support query builder and untyped object returns. + /// + public static class QueryExtensions + { + /// + /// Gets a list of accounts based on the search parameters. + /// + /// Dataverse Entity Type Name to search + /// Array of Search Parameters + /// List of fields to retrieve, Null indicates all Fields + /// Logical Search Operator + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately + /// ServiceClient + /// List of matching Entity Types. + + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope = "member")] + public static Dictionary> GetEntityDataBySearchParams(this ServiceClient serviceClient, + string entityName, + Dictionary searchParameters, + LogicalSearchOperator searchOperator, + List fieldList, + Guid batchId = default(Guid), + bool bypassPluginExecution = false) + { + List searchList = new List(); + serviceClient.BuildSearchFilterListFromSearchTerms(searchParameters, searchList); + + string pgCookie = string.Empty; + bool moreRec = false; + return serviceClient.GetEntityDataBySearchParams(entityName, searchList, searchOperator, fieldList, null, -1, -1, string.Empty, out pgCookie, out moreRec, batchId, bypassPluginExecution: bypassPluginExecution); + } + + + /// + /// Gets a list of accounts based on the search parameters. + /// + /// Dataverse Entity Type Name to search + /// Array of Search Parameters + /// List of fields to retrieve, Null indicates all Fields + /// Logical Search Operator + /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// ServiceClient + /// List of matching Entity Types. + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope = "member")] + public static Dictionary> GetEntityDataBySearchParams(this ServiceClient serviceClient, + string entityName, + List searchParameters, + LogicalSearchOperator searchOperator, + List fieldList, Guid batchId = default(Guid), + bool bypassPluginExecution = false) + { + string pgCookie = string.Empty; + bool moreRec = false; + return serviceClient.GetEntityDataBySearchParams(entityName, searchParameters, searchOperator, fieldList, null, -1, -1, string.Empty, out pgCookie, out moreRec, batchId, bypassPluginExecution); + } + + /// + /// Searches for data from an entity based on the search parameters. + /// + /// Name of the entity to search + /// Array of Search Parameters + /// List of fields to retrieve, Null indicates all Fields + /// Logical Search Operator + /// Number records per Page + /// Current Page number + /// inbound place holder cookie + /// outbound place holder cookie + /// is there more records or not + /// Sort order + /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// ServiceClient + /// List of matching Entity Types. + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope = "member")] + public static Dictionary> GetEntityDataBySearchParams(this ServiceClient serviceClient, + string entityName, + List searchParameters, + LogicalSearchOperator searchOperator, + List fieldList, + Dictionary sortParameters, + int pageCount, + int pageNumber, + string pageCookie, + out string outPageCookie, + out bool isMoreRecords, + Guid batchId = default(Guid), + bool bypassPluginExecution = false + ) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + + outPageCookie = string.Empty; + isMoreRecords = false; + + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); + return null; + } + + if (searchParameters == null) + searchParameters = new List(); + + // Build the query here. + QueryExpression query = BuildQueryFilter(entityName, searchParameters, fieldList, searchOperator); + + if (pageCount != -1) + { + PagingInfo pgInfo = new PagingInfo(); + pgInfo.Count = pageCount; + pgInfo.PageNumber = pageNumber; + pgInfo.PagingCookie = pageCookie; + query.PageInfo = pgInfo; + } + + if (sortParameters != null) + if (sortParameters.Count > 0) + { + List qExpressList = new List(); + foreach (KeyValuePair itm in sortParameters) + { + OrderExpression ordBy = new OrderExpression(); + ordBy.AttributeName = itm.Key; + if (itm.Value == LogicalSortOrder.Ascending) + ordBy.OrderType = OrderType.Ascending; + else + ordBy.OrderType = OrderType.Descending; + + qExpressList.Add(ordBy); + } + + query.Orders.AddRange(qExpressList.ToArray()); + } + + + RetrieveMultipleRequest retrieve = new RetrieveMultipleRequest(); + //retrieve.ReturnDynamicEntities = true; + retrieve.Query = query; + + + if (serviceClient.AddRequestToBatch(batchId, retrieve, "Running GetEntityDataBySearchParms", "Request For GetEntityDataBySearchParms Queued", bypassPluginExecution)) + return null; + + + RetrieveMultipleResponse retrieved; + retrieved = (RetrieveMultipleResponse)serviceClient.Command_Execute(retrieve, "GetEntityDataBySearchParms", bypassPluginExecution); + if (retrieved != null) + { + outPageCookie = retrieved.EntityCollection.PagingCookie; + isMoreRecords = retrieved.EntityCollection.MoreRecords; + + return CreateResultDataSet(retrieved.EntityCollection); + } + else + return null; + } + + + /// + /// Searches for data based on a FetchXML query + /// + /// Fetch XML query data. + /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// ServiceClient + /// results or null + public static Dictionary> GetEntityDataByFetchSearch(this ServiceClient serviceClient, string fetchXml, Guid batchId = default(Guid), bool bypassPluginExecution = false) + { + EntityCollection ec = serviceClient.GetEntityDataByFetchSearchEC(fetchXml, batchId, bypassPluginExecution); + if (ec != null) + return CreateResultDataSet(ec); + else + return null; + } + + + /// + /// Searches for data based on a FetchXML query + /// + /// Fetch XML query data. + /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// ServiceClient + /// results as an entity collection or null + public static EntityCollection GetEntityDataByFetchSearchEC(this ServiceClient serviceClient, string fetchXml, Guid batchId = default(Guid), bool bypassPluginExecution = false) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); + return null; + } + + if (string.IsNullOrWhiteSpace(fetchXml)) + return null; + + // This model directly requests the via FetchXML + RetrieveMultipleRequest req = new RetrieveMultipleRequest() { Query = new FetchExpression(fetchXml) }; + RetrieveMultipleResponse retrieved; + + if (serviceClient.AddRequestToBatch(batchId, req, "Running GetEntityDataByFetchSearchEC", "Request For GetEntityDataByFetchSearchEC Queued", bypassPluginExecution)) + return null; + + retrieved = (RetrieveMultipleResponse)serviceClient.Command_Execute(req, "GetEntityDataByFetchSearch - Direct", bypassPluginExecution); + if (retrieved != null) + { + return retrieved.EntityCollection; + } + else + return null; + } + + /// + /// Searches for data based on a FetchXML query + /// + /// Fetch XML query data. + /// Number records per Page + /// Current Page number + /// inbound place holder cookie + /// outbound place holder cookie + /// is there more records or not + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately + /// ServiceClient + /// results or null + public static Dictionary> GetEntityDataByFetchSearch(this ServiceClient serviceClient, + string fetchXml, + int pageCount, + int pageNumber, + string pageCookie, + out string outPageCookie, + out bool isMoreRecords, + Guid batchId = default(Guid), + bool bypassPluginExecution = false) + { + EntityCollection ec = serviceClient.GetEntityDataByFetchSearchEC(fetchXml, pageCount, pageNumber, pageCookie, out outPageCookie, out isMoreRecords, bypassPluginExecution: bypassPluginExecution); + if (ec != null) + return CreateResultDataSet(ec); + else + return null; + } + + /// + /// Searches for data based on a FetchXML query + /// + /// Fetch XML query data. + /// Number records per Page + /// Current Page number + /// inbound place holder cookie + /// outbound place holder cookie + /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// is there more records or not + /// ServiceClient + /// results as an Entity Collection or null + public static EntityCollection GetEntityDataByFetchSearchEC(this ServiceClient serviceClient, + string fetchXml, + int pageCount, + int pageNumber, + string pageCookie, + out string outPageCookie, + out bool isMoreRecords, + Guid batchId = default(Guid), + bool bypassPluginExecution = false) + { + + serviceClient._logEntry.ResetLastError(); // Reset Last Error + + outPageCookie = string.Empty; + isMoreRecords = false; + + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); + return null; + } + + if (string.IsNullOrWhiteSpace(fetchXml)) + return null; + + if (pageCount != -1) + { + // Add paging related parameter to fetch xml. + fetchXml = AddPagingParametersToFetchXml(fetchXml, pageCount, pageNumber, pageCookie); + } + + RetrieveMultipleRequest retrieve = new RetrieveMultipleRequest() { Query = new FetchExpression(fetchXml) }; + RetrieveMultipleResponse retrieved; + + if (serviceClient.AddRequestToBatch(batchId, retrieve, "Running GetEntityDataByFetchSearchEC", "Request For GetEntityDataByFetchSearchEC Queued", bypassPluginExecution)) + return null; + + retrieved = (RetrieveMultipleResponse)serviceClient.Command_Execute(retrieve, "GetEntityDataByFetchSearch", bypassPluginExecution); + if (retrieved != null) + { + outPageCookie = retrieved.EntityCollection.PagingCookie; + isMoreRecords = retrieved.EntityCollection.MoreRecords; + return retrieved.EntityCollection; + } + + return null; + } + + + /// + /// Queries an Object via a M to M Link + /// + /// Name of the entity you want return data from + /// Search Prams for the Return Entity + /// Name of the entity you are linking too + /// Search Prams for the Entity you are linking too + /// Key field on the Entity you are linking too + /// Dataverse Name of the Relationship + /// Key field on the Entity you want to return data from + /// Search Operator to apply + /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// List of Fields from the Returned Entity you want + /// ServiceClient + /// + public static Dictionary> GetEntityDataByLinkedSearch(this ServiceClient serviceClient, + string returnEntityName, + Dictionary primarySearchParameters, + string linkedEntityName, + Dictionary linkedSearchParameters, + string linkedEntityLinkAttribName, + string m2MEntityName, + string returnEntityPrimaryId, + LogicalSearchOperator searchOperator, + List fieldList, + Guid batchId = default(Guid), + bool bypassPluginExecution = false) + { + List primarySearchList = new List(); + serviceClient.BuildSearchFilterListFromSearchTerms(primarySearchParameters, primarySearchList); + + List linkedSearchList = new List(); + serviceClient.BuildSearchFilterListFromSearchTerms(linkedSearchParameters, linkedSearchList); + + return serviceClient.GetEntityDataByLinkedSearch(returnEntityName, primarySearchList, linkedEntityName, linkedSearchList, linkedEntityLinkAttribName, + m2MEntityName, returnEntityPrimaryId, searchOperator, fieldList, bypassPluginExecution: bypassPluginExecution); + + } + + /// + /// Queries an Object via a M to M Link + /// + /// Name of the entity you want return data from + /// Search Prams for the Return Entity + /// Name of the entity you are linking too + /// Search Prams for the Entity you are linking too + /// Key field on the Entity you are linking too + /// Dataverse Name of the Relationship + /// Key field on the Entity you want to return data from + /// Search Operator to apply + /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately + /// List of Fields from the Returned Entity you want + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// If the relationship is defined as Entity:Entity or Account N:N Account, this parameter should be set to true + /// ServiceClient + /// + public static Dictionary> GetEntityDataByLinkedSearch(this ServiceClient serviceClient, + string returnEntityName, + List /*Dictionary*/ primarySearchParameters, + string linkedEntityName, + List /*Dictionary*/ linkedSearchParameters, + string linkedEntityLinkAttribName, + string m2MEntityName, + string returnEntityPrimaryId, + LogicalSearchOperator searchOperator, + List fieldList, + Guid batchId = default(Guid), + bool isReflexiveRelationship = false, + bool bypassPluginExecution = false) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); + return null; + } + + if (primarySearchParameters == null && linkedSearchParameters == null) + return null; + + if (primarySearchParameters == null) + primarySearchParameters = new List(); // new Dictionary(); + + if (linkedSearchParameters == null) + linkedSearchParameters = new List(); //new Dictionary(); + + + + #region Primary QueryFilter and Conditions + + FilterExpression primaryFilter = new FilterExpression(); + primaryFilter.Filters.AddRange(BuildFilterList(primarySearchParameters)); + + #endregion + + #region Secondary QueryFilter and conditions + + FilterExpression linkedEntityFilter = new FilterExpression(); + linkedEntityFilter.Filters.AddRange(BuildFilterList(linkedSearchParameters)); + + #endregion + + // Create Link Object for LinkedEnitty Name and add the filter info + LinkEntity nestedLinkEntity = new LinkEntity(); // this is the Secondary + nestedLinkEntity.LinkToEntityName = linkedEntityName; // what Entity are we linking too... + nestedLinkEntity.LinkToAttributeName = linkedEntityLinkAttribName; // what Attrib are we linking To on that Entity + nestedLinkEntity.LinkFromAttributeName = isReflexiveRelationship ? string.Format("{0}two", linkedEntityLinkAttribName) : linkedEntityLinkAttribName; // what Attrib on the primary object are we linking too. + nestedLinkEntity.LinkCriteria = linkedEntityFilter; // Filtered query + + //Create Link Object for Primary + LinkEntity m2mLinkEntity = new LinkEntity(); + m2mLinkEntity.LinkToEntityName = m2MEntityName; // this is the M2M table + m2mLinkEntity.LinkToAttributeName = isReflexiveRelationship ? string.Format("{0}one", returnEntityPrimaryId) : returnEntityPrimaryId; // this is the name of the other side. + m2mLinkEntity.LinkFromAttributeName = returnEntityPrimaryId; + m2mLinkEntity.LinkEntities.AddRange(new LinkEntity[] { nestedLinkEntity }); + + + // Return Cols + // Create ColumnSet + ColumnSet cols = null; + if (fieldList != null && fieldList.Count > 0) + { + cols = new ColumnSet(); + cols.Columns.AddRange(fieldList.ToArray()); + } + + // Build Query + QueryExpression query = new QueryExpression(); + query.NoLock = false; // Added to remove the Locks. + + query.EntityName = returnEntityName; // Set to the requested entity Type + if (cols != null) + query.ColumnSet = cols; + else + query.ColumnSet = new ColumnSet(true);// new AllColumns(); + + query.Criteria = primaryFilter; + query.LinkEntities.AddRange(new LinkEntity[] { m2mLinkEntity }); + + //Dictionary> Results = new Dictionary>(); + + + RetrieveMultipleRequest req = new RetrieveMultipleRequest(); + req.Query = query; + RetrieveMultipleResponse retrieved; + + + if (serviceClient.AddRequestToBatch(batchId, req, string.Format(CultureInfo.InvariantCulture, "Running Get Linked data, returning {0}", returnEntityName), string.Format(CultureInfo.InvariantCulture, "Request for Get Linked data, returning {0}", returnEntityName), bypassPluginExecution)) + return null; + + retrieved = (RetrieveMultipleResponse)serviceClient.Command_Execute(req, "Search On Linked Data", bypassPluginExecution); + + if (retrieved != null) + { + + return CreateResultDataSet(retrieved.EntityCollection); + } + else + return null; + + } + + /// + /// Gets a List of variables from the account based on the list of field specified in the Fields List + /// + /// The entity to be searched. + /// ID of Entity to query + /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// Populated Array of Key value pairs with the Results of the Search + /// ServiceClient + /// + public static Dictionary GetEntityDataById(this ServiceClient serviceClient, string searchEntity, Guid entityId, List fieldList, Guid batchId = default(Guid), bool bypassPluginExecution = false) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + if (serviceClient.DataverseService == null || entityId == Guid.Empty) + { + return null; + } + + EntityReference re = new EntityReference(searchEntity, entityId); + if (re == null) + return null; + + RetrieveRequest req = new RetrieveRequest(); + + // Create ColumnSet + ColumnSet cols = null; + if (fieldList != null) + { + cols = new ColumnSet(); + cols.Columns.AddRange(fieldList.ToArray()); + } + + if (cols != null) + req.ColumnSet = cols; + else + req.ColumnSet = new ColumnSet(true);// new AllColumns(); + + req.Target = re; //getEnt; + + if (serviceClient.AddRequestToBatch(batchId, req, string.Format(CultureInfo.InvariantCulture, "Trying to Read a Record. Entity = {0} , ID = {1}", searchEntity, entityId.ToString()), + string.Format(CultureInfo.InvariantCulture, "Request to Read a Record. Entity = {0} , ID = {1} queued", searchEntity, entityId.ToString()), bypassPluginExecution)) + return null; + + RetrieveResponse resp = (RetrieveResponse)serviceClient.Command_Execute(req, string.Format(CultureInfo.InvariantCulture, "Trying to Read a Record. Entity = {0} , ID = {1}", searchEntity, entityId.ToString()), bypassPluginExecution); + if (resp == null) + return null; + + if (resp.Entity == null) + return null; + + try + { + // Not really doing an update here... just turning it into something I can walk. + Dictionary resultSet = new Dictionary(); + AddDataToResultSet(ref resultSet, resp.Entity); + return resultSet; + } + catch + { + return null; + } + } + + /// + /// Returns all Activities Related to a given Entity ID. + /// Only Account, Contact and Opportunity entities are supported. + /// + /// Type of Entity to search against + /// ID of the entity to search against. + /// List of Field to return for the entity , null indicates all fields. + /// Search Operator to use + /// Filters responses based on search prams. + /// Sort order + /// Number of Pages + /// Current Page number + /// inbound place holder cookie + /// outbound place holder cookie + /// is there more records or not + /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately + /// ServiceClient + /// Array of Activities + public static Dictionary> GetActivitiesBy(this ServiceClient serviceClient, + string searchEntity, + Guid entityId, + List fieldList, + LogicalSearchOperator searchOperator, + Dictionary searchParameters, + Dictionary sortParameters, + int pageCount, + int pageNumber, + string pageCookie, + out string outPageCookie, + out bool isMoreRecords, + Guid batchId = default(Guid) + ) + { + List searchList = new List(); + serviceClient.BuildSearchFilterListFromSearchTerms(searchParameters, searchList); + + return serviceClient.GetEntityDataByRollup(searchEntity, entityId, "activitypointer", fieldList, searchOperator, searchList, sortParameters, pageCount, pageNumber, pageCookie, out outPageCookie, out isMoreRecords, batchId); + } + + /// + /// Returns all Activities Related to a given Entity ID. + /// Only Account, Contact and Opportunity entities are supported. + /// + /// Type of Entity to search against + /// ID of the entity to search against. + /// List of Field to return for the entity , null indicates all fields. + /// Search Operator to use + /// Filters responses based on search prams. + /// Sort order + /// Number of Pages + /// Current Page number + /// inbound place holder cookie + /// outbound place holder cookie + /// is there more records or not + /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately + /// ServiceClient + /// Array of Activities + public static Dictionary> GetActivitiesBy(this ServiceClient serviceClient, + string searchEntity, + Guid entityId, + List fieldList, + LogicalSearchOperator searchOperator, + List searchParameters, + Dictionary sortParameters, + int pageCount, + int pageNumber, + string pageCookie, + out string outPageCookie, + out bool isMoreRecords, + Guid batchId = default(Guid) + ) + { + return serviceClient.GetEntityDataByRollup(searchEntity, entityId, "activitypointer", fieldList, searchOperator, searchParameters, sortParameters, pageCount, pageNumber, pageCookie, out outPageCookie, out isMoreRecords, batchId: batchId); + } + + /// + /// Returns all Activities Related to a given Entity ID. + /// Only Account, Contact and Opportunity entities are supported. + /// + /// Type of Entity to search against + /// ID of the entity to search against. + /// List of Field to return for the entity , null indicates all fields. + /// + /// Filters responses based on search prams. + /// Array of Activities + /// Sort Order + /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately + /// Entity to Rollup from + /// ServiceClient + public static Dictionary> GetEntityDataByRollup(this ServiceClient serviceClient, + string searchEntity, + Guid entityId, + string rollupfromEntity, + List fieldList, + LogicalSearchOperator searchOperator, + Dictionary searchParameters, + Dictionary sortParameters, + Guid batchId = default(Guid)) + { + + List searchList = new List(); + serviceClient.BuildSearchFilterListFromSearchTerms(searchParameters, searchList); + + + string pgCookie = string.Empty; + bool moreRec = false; + + return serviceClient.GetEntityDataByRollup( + searchEntity, entityId, rollupfromEntity, fieldList, + searchOperator, searchList, sortParameters, -1, -1, string.Empty, + out pgCookie, out moreRec, batchId: batchId); + } + + + /// + /// Returns all Activities Related to a given Entity ID. + /// Only Account, Contact and Opportunity entities are supported. + /// + /// Type of Entity to search against + /// ID of the entity to search against. + /// List of Field to return for the entity , null indicates all fields. + /// Entity to Rollup from + /// Search Operator to user + /// Dataverse Filter list to apply + /// Sort by + /// Number of Pages + /// Current Page number + /// inbound place holder cookie + /// outbound place holder cookie + /// is there more records or not + /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// ServiceClient + /// + public static Dictionary> GetEntityDataByRollup(this ServiceClient serviceClient, + string searchEntity, + Guid entityId, + string rollupfromEntity, + List fieldList, + LogicalSearchOperator searchOperator, + List searchParameters, + Dictionary sortParameters, + int pageCount, + int pageNumber, + string pageCookie, + out string outPageCookie, + out bool isMoreRecords, + Guid batchId = default(Guid), + bool bypassPluginExecution = false + ) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + outPageCookie = string.Empty; + isMoreRecords = false; + + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); + return null; + } + + QueryExpression query = BuildQueryFilter(rollupfromEntity, searchParameters, fieldList, searchOperator); + + if (pageCount != -1) + { + PagingInfo pgInfo = new PagingInfo(); + pgInfo.Count = pageCount; + pgInfo.PageNumber = pageNumber; + pgInfo.PagingCookie = pageCookie; + query.PageInfo = pgInfo; + + } + + if (sortParameters != null) + if (sortParameters.Count > 0) + { + List qExpressList = new List(); + foreach (KeyValuePair itm in sortParameters) + { + OrderExpression ordBy = new OrderExpression(); + ordBy.AttributeName = itm.Key; + if (itm.Value == LogicalSortOrder.Ascending) + ordBy.OrderType = OrderType.Ascending; + else + ordBy.OrderType = OrderType.Descending; + + qExpressList.Add(ordBy); + } + + query.Orders.AddRange(qExpressList.ToArray()); + } + + if (query.Orders == null) + { + OrderExpression ordBy = new OrderExpression(); + ordBy.AttributeName = "createdon"; + ordBy.OrderType = OrderType.Descending; + query.Orders.AddRange(new OrderExpression[] { ordBy }); + } + + EntityReference ro = new EntityReference(searchEntity, entityId); + if (ro == null) + return null; + + RollupRequest req = new RollupRequest(); + req.Query = query; + req.RollupType = RollupType.Related; + req.Target = ro; + + if (serviceClient.AddRequestToBatch(batchId, req, string.Format(CultureInfo.InvariantCulture, "Running Get entitydatabyrollup... {0}", searchEntity), string.Format(CultureInfo.InvariantCulture, "Request for GetEntityDataByRollup on {0} queued", searchEntity), bypassPluginExecution)) + return null; + + RollupResponse resp = (RollupResponse)serviceClient.Command_Execute(req, string.Format(CultureInfo.InvariantCulture, "Locating {0} by ID in Dataverse GetActivitesBy", searchEntity), bypassPluginExecution); + if (resp == null) + return null; + + if ((resp.EntityCollection != null) || + (resp.EntityCollection.Entities != null) || + (resp.EntityCollection.Entities.Count > 0) + ) + { + isMoreRecords = resp.EntityCollection.MoreRecords; + outPageCookie = resp.EntityCollection.PagingCookie; + return CreateResultDataSet(resp.EntityCollection); + } + else + return null; + } + + /// + /// This function gets data from a Dictionary object, where "string" identifies the field name, and Object contains the data, + /// this method then attempts to cast the result to the Type requested, if it cannot be cast an empty object is returned. + /// + /// Results from the query + /// key name you want + /// Type if object to return + /// ServiceClient + /// object + public static T GetDataByKeyFromResultsSet(this ServiceClient serviceClient, Dictionary results, string key) + { + try + { + if (results != null) + { + if (results.ContainsKey(key)) + { + + if ((typeof(T) == typeof(int)) || (typeof(T) == typeof(string))) + { + try + { + string s = (string)results[key]; + if (s.Contains("PICKLIST:")) + { + try + { + //parse the PickList bit for what is asked for + Collection eleList = new Collection(s.Split(':')); + if (typeof(T) == typeof(int)) + { + return (T)(object)Convert.ToInt32(eleList[1], CultureInfo.InvariantCulture); + } + else + return (T)(object)eleList[3]; + } + catch + { + // try to do the basic return + return (T)results[key]; + } + } + } + catch + { + if (results[key] is T) + // try to do the basic return + return (T)results[key]; + } + } + + // MSB :: Added this method in light of new features in CDS 2011.. + if (results[key] is T) + // try to do the basic return + return (T)results[key]; + else + { + if (results != null && results.ContainsKey(key)) // Specific To CDS 2011.. + { + if (results.ContainsKey(key + "_Property")) + { + // Check for the property entry - CDS 2011 Specific + KeyValuePair property = (KeyValuePair)results[key + "_Property"]; + // try to return the casted value. + if (property.Value is T) + return (T)property.Value; + } + } + } + } + } + } + catch (Exception ex) + { + serviceClient._logEntry.Log("Error In GetDataByKeyFromResultsSet (Non-Fatal)", TraceEventType.Verbose, ex); + } + return default(T); + + } + + #region Utilities + + /// + /// Builds the Query expression to use with a Search. + /// + /// + /// + /// + /// + /// + private static QueryExpression BuildQueryFilter(string entityName, List searchParams, List fieldList, LogicalSearchOperator searchOperator) + { + // Create ColumnSet + ColumnSet cols = null; + if (fieldList != null) + { + cols = new ColumnSet(); + cols.Columns.AddRange(fieldList.ToArray()); + } + + List filters = BuildFilterList(searchParams); + + // Link Filter. + FilterExpression Queryfilter = new FilterExpression(); + Queryfilter.Filters.AddRange(filters); + + // Add Logical relationship. + if (searchOperator == LogicalSearchOperator.Or) + Queryfilter.FilterOperator = LogicalOperator.Or; + else + Queryfilter.FilterOperator = LogicalOperator.And; + + + // Build Query + QueryExpression query = new QueryExpression(); + query.EntityName = entityName; // Set to the requested entity Type + if (cols != null) + query.ColumnSet = cols; + else + query.ColumnSet = new ColumnSet(true);// new AllColumns(); + + query.Criteria = Queryfilter; + query.NoLock = true; // Added to remove locking on queries. + return query; + } + + /// + /// Adds paging related parameter to the input fetchXml + /// + /// Input fetch Xml + /// The number of records to be fetched + /// The page number + /// Page cookie + /// + private static String AddPagingParametersToFetchXml(string fetchXml, int pageCount, int pageNum, string pageCookie) + { + if (String.IsNullOrWhiteSpace(fetchXml)) + { + return fetchXml; + } + + XmlDocument fetchdoc = XmlUtil.CreateXmlDocument(fetchXml); + XmlElement fetchroot = fetchdoc.DocumentElement; + + XmlAttribute pageAttribute = fetchdoc.CreateAttribute("page"); + pageAttribute.Value = pageNum.ToString(CultureInfo.InvariantCulture); + + XmlAttribute countAttribute = fetchdoc.CreateAttribute("count"); + countAttribute.Value = pageCount.ToString(CultureInfo.InvariantCulture); + + XmlAttribute pagingCookieAttribute = fetchdoc.CreateAttribute("paging-cookie"); + pagingCookieAttribute.Value = pageCookie; + + fetchroot.Attributes.Append(pageAttribute); + fetchroot.Attributes.Append(countAttribute); + fetchroot.Attributes.Append(pagingCookieAttribute); + + return fetchdoc.DocumentElement.OuterXml; + } + + /// + /// Builds the Query expression to use with a Search. + /// + /// + /// + /// + /// + /// ServiceClient + /// + private static QueryExpression BuildQueryFilter(this ServiceClient serviceClient, string entityName, List searchParams, List fieldList, LogicalSearchOperator searchOperator) + { + // Create ColumnSet + ColumnSet cols = null; + if (fieldList != null) + { + cols = new ColumnSet(); + cols.Columns.AddRange(fieldList.ToArray()); + } + + List filters = BuildFilterList(searchParams); + + // Link Filter. + FilterExpression Queryfilter = new FilterExpression(); + Queryfilter.Filters.AddRange(filters); + + // Add Logical relationship. + if (searchOperator == LogicalSearchOperator.Or) + Queryfilter.FilterOperator = LogicalOperator.Or; + else + Queryfilter.FilterOperator = LogicalOperator.And; + + + // Build Query + QueryExpression query = new QueryExpression(); + query.EntityName = entityName; // Set to the requested entity Type + if (cols != null) + query.ColumnSet = cols; + else + query.ColumnSet = new ColumnSet(true);// new AllColumns(); + + query.Criteria = Queryfilter; + query.NoLock = true; // Added to remove locking on queries. + return query; + } + + /// + /// Creates a SearchFilterList from a Search string Dictionary + /// + /// Inbound Search Strings + /// List that will be populated + /// ServiceClient + private static void BuildSearchFilterListFromSearchTerms(this ServiceClient serviceClient, Dictionary inSearchParams, List outSearchList) + { + if (inSearchParams != null) + { + foreach (var item in inSearchParams) + { + DataverseSearchFilter f = new DataverseSearchFilter(); + f.FilterOperator = LogicalOperator.And; + f.SearchConditions.Add(new DataverseFilterConditionItem() + { + FieldName = item.Key, + FieldValue = item.Value, + FieldOperator = string.IsNullOrWhiteSpace(item.Value) ? ConditionOperator.Null : item.Value.Contains("%") ? ConditionOperator.Like : ConditionOperator.Equal + }); + outSearchList.Add(f); + } + } + } + + /// + /// Builds the filter list for a query + /// + /// + /// + private static List BuildFilterList(List searchParams) + { + List filters = new List(); + // Create Conditions + foreach (DataverseSearchFilter conditionItemList in searchParams) + { + FilterExpression filter = new FilterExpression(); + foreach (DataverseFilterConditionItem conditionItem in conditionItemList.SearchConditions) + { + ConditionExpression condition = new ConditionExpression(); + condition.AttributeName = conditionItem.FieldName; + condition.Operator = conditionItem.FieldOperator; + if (!(condition.Operator == ConditionOperator.NotNull || condition.Operator == ConditionOperator.Null)) + condition.Values.Add(conditionItem.FieldValue); + + filter.AddCondition(condition); + } + if (filter.Conditions.Count > 0) + { + filter.FilterOperator = conditionItemList.FilterOperator; + filters.Add(filter); + } + } + return filters; + } + + /// + /// Creates and Returns a Search Result Set + /// + /// + /// + internal static Dictionary> CreateResultDataSet(EntityCollection resp) + { + Dictionary> Results = new Dictionary>(); + foreach (Entity bEnt in resp.Entities) + { + // Not really doing an update here... just turning it into something I can walk. + Dictionary SearchRstls = new Dictionary(); + AddDataToResultSet(ref SearchRstls, bEnt); + // Add Ent name and ID + SearchRstls.Add("ReturnProperty_EntityName", bEnt.LogicalName); + SearchRstls.Add("ReturnProperty_Id ", bEnt.Id); + Results.Add(Guid.NewGuid().ToString(), SearchRstls); + } + if (Results.Count > 0) + return Results; + else + return null; + } + + /// + /// Adds data from a Entity to result set + /// + /// + /// + private static void AddDataToResultSet(ref Dictionary resultSet, Entity dataEntity) + { + if (dataEntity == null) + return; + if (resultSet == null) + return; + try + { + foreach (var p in dataEntity.Attributes) + { + resultSet.Add(p.Key + "_Property", p); + resultSet.Add(p.Key, dataEntity.FormattedValues.ContainsKey(p.Key) ? dataEntity.FormattedValues[p.Key] : p.Value); + } + + } + catch { } + } + + /// + /// Lookup a entity ID by a single search element. + /// Used for Lookup Lists. + /// + /// Text to search for + /// Entity Type to Search in + /// Field that contains the id + /// Field to Search against + /// ServiceClient + /// Guid of Entity or Empty Guid + internal static Guid LookupEntitiyID(this ServiceClient serviceClient, string SearchValue, string ent, string IDField, string SearchField) + { + try + { + Guid guID = Guid.Empty; + List FieldList = new List(); + FieldList.Add(IDField); + + Dictionary SearchList = new Dictionary(); + SearchList.Add(SearchField, SearchValue); + + Dictionary> rslts = serviceClient.GetEntityDataBySearchParams(ent, SearchList, LogicalSearchOperator.None, FieldList); + + if (rslts != null) + { + foreach (Dictionary rsl in rslts.Values) + { + if (rsl.ContainsKey(IDField)) + { + guID = (Guid)rsl[IDField]; + } + } + } + return guID; + } + catch + { + return Guid.Empty; + } + } + + /// + /// Gets the Lookup Value GUID for any given entity name + /// + /// Entity you are looking for + /// Value you are looking for + /// ServiceClient + /// ID of the lookup value in the entity + internal static Guid GetLookupValueForEntity(this ServiceClient serviceClient, string entName, string Value) + { + // Check for existence of cached list. + if (serviceClient._CachObject == null) + { + object objc = serviceClient._connectionSvc.LocalMemoryCache.Get(serviceClient._cachObjecName); + if (objc is Dictionary> workingObj) + serviceClient._CachObject = workingObj; + + if (serviceClient._CachObject == null) + serviceClient._CachObject = new Dictionary>(); + } + + Guid guResultID = Guid.Empty; + + if ((serviceClient._CachObject.ContainsKey(entName.ToString())) && (serviceClient._CachObject[entName.ToString()].ContainsKey(Value))) + return (Guid)serviceClient._CachObject[entName.ToString()][Value]; + + switch (entName) + { + case "transactioncurrency": + guResultID = serviceClient.LookupEntitiyID(Value, entName, "transactioncurrencyid", "currencyname"); + break; + case "subject": + guResultID = serviceClient.LookupEntitiyID(Value, entName, "subjectid", "title"); //LookupSubjectIDForName(Value); + break; + case "systemuser": + guResultID = serviceClient.LookupEntitiyID(Value, entName, "systemuserid", "domainname"); + break; + case "pricelevel": + guResultID = serviceClient.LookupEntitiyID(Value, entName, "pricelevelid", "name"); + break; + case "product": + guResultID = serviceClient.LookupEntitiyID(Value, entName, "productid", "productnumber"); + break; + case "uom": + guResultID = serviceClient.LookupEntitiyID(Value, entName, "uomid", "name"); + break; + default: + return Guid.Empty; + } + + + // High effort objects that are generally not changed during the live cycle of a connection are cached here. + if (guResultID != Guid.Empty) + { + if (!serviceClient._CachObject.ContainsKey(entName.ToString())) + serviceClient._CachObject.Add(entName.ToString(), new Dictionary()); + serviceClient._CachObject[entName.ToString()].Add(Value, guResultID); + + serviceClient._connectionSvc.LocalMemoryCache.Set(serviceClient._cachObjecName, serviceClient._CachObject, DateTime.Now.AddMinutes(5)); + } + + return guResultID; + + } + + #endregion + + + } +} diff --git a/src/GeneralTools/DataverseClient/Client/Extensions/SupportClasses/DataverseFilterConditionItem.cs b/src/GeneralTools/DataverseClient/Client/Extensions/SupportClasses/DataverseFilterConditionItem.cs new file mode 100644 index 0000000..cfdb29f --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Extensions/SupportClasses/DataverseFilterConditionItem.cs @@ -0,0 +1,24 @@ +using Microsoft.Xrm.Sdk.Query; + +namespace Microsoft.PowerPlatform.Dataverse.Client.Extensions +{ + + /// + /// Dataverse Filter item. + /// + public class DataverseFilterConditionItem + { + /// + /// Dataverse Field name to Filter on + /// + public string FieldName { get; set; } + /// + /// Value to use for the Filter + /// + public object FieldValue { get; set; } + /// + /// Dataverse Operator to apply + /// + public ConditionOperator FieldOperator { get; set; } + } +} diff --git a/src/GeneralTools/DataverseClient/Client/Extensions/SupportClasses/DataverseSearchFilter.cs b/src/GeneralTools/DataverseClient/Client/Extensions/SupportClasses/DataverseSearchFilter.cs new file mode 100644 index 0000000..04c321b --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Extensions/SupportClasses/DataverseSearchFilter.cs @@ -0,0 +1,29 @@ +using System; +using Microsoft.Xrm.Sdk.Query; +using System.Collections.Generic; + +namespace Microsoft.PowerPlatform.Dataverse.Client.Extensions +{ + /// + /// Dataverse Filter class. + /// + public class DataverseSearchFilter + { + /// + /// List of Dataverse Filter conditions + /// + public List SearchConditions { get; set; } + /// + /// Dataverse Filter Operator + /// + public LogicalOperator FilterOperator { get; set; } + + /// + /// Creates an empty Dataverse Search Filter. + /// + public DataverseSearchFilter() + { + SearchConditions = new List(); + } + } +} diff --git a/src/GeneralTools/DataverseClient/Client/Extensions/SupportClasses/FormTypeId.cs b/src/GeneralTools/DataverseClient/Client/Extensions/SupportClasses/FormTypeId.cs new file mode 100644 index 0000000..27eae3a --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Extensions/SupportClasses/FormTypeId.cs @@ -0,0 +1,31 @@ +namespace Microsoft.PowerPlatform.Dataverse.Client.Extensions +{ + /// + /// Used with GetFormIdsForEntity Call + /// + public enum FormTypeId + { + /// + /// Dashboard form + /// + Dashboard = 0, + /// + /// Appointment book, for service requests. + /// + AppointmentBook = 1, + /// + /// Main or default form + /// + Main = 2, + //MiniCampaignBo = 3, // Not used in 2011 + //Preview = 4, // Not used in 2011 + /// + /// Mobile default form + /// + Mobile = 5, + /// + /// User defined forms + /// + Other = 100 + } +} diff --git a/src/GeneralTools/DataverseClient/Client/Extensions/SupportClasses/LogicalSearchOperator.cs b/src/GeneralTools/DataverseClient/Client/Extensions/SupportClasses/LogicalSearchOperator.cs new file mode 100644 index 0000000..73fe4bd --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Extensions/SupportClasses/LogicalSearchOperator.cs @@ -0,0 +1,22 @@ + +namespace Microsoft.PowerPlatform.Dataverse.Client.Extensions +{ + /// + /// Logical Search Pram to apply to over all search. + /// + public enum LogicalSearchOperator + { + /// + /// Do not apply the Search Operator + /// + None = 0, + /// + /// Or Search + /// + Or = 1, + /// + /// And Search + /// + And = 2 + } +} diff --git a/src/GeneralTools/DataverseClient/Client/Extensions/SupportClasses/LogicalSortOrder.cs b/src/GeneralTools/DataverseClient/Client/Extensions/SupportClasses/LogicalSortOrder.cs new file mode 100644 index 0000000..34a34e3 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Extensions/SupportClasses/LogicalSortOrder.cs @@ -0,0 +1,17 @@ +namespace Microsoft.PowerPlatform.Dataverse.Client.Extensions +{ + /// + /// Logical Search Pram to apply to over all search. + /// + public enum LogicalSortOrder + { + /// + /// Sort in Ascending + /// + Ascending = 0, + /// + /// Sort in Descending + /// + Descending = 1, + } +} diff --git a/src/GeneralTools/DataverseClient/Client/Extensions/SupportClasses/PickListItem.cs b/src/GeneralTools/DataverseClient/Client/Extensions/SupportClasses/PickListItem.cs new file mode 100644 index 0000000..47e8eb6 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Extensions/SupportClasses/PickListItem.cs @@ -0,0 +1,35 @@ +namespace Microsoft.PowerPlatform.Dataverse.Client.Extensions +{ + /// + /// PickList Item + /// + public sealed class PickListItem + { + /// + /// Display label for the PickList Item + /// + public string DisplayLabel { get; set; } + /// + /// ID of the picklist item + /// + public int PickListItemId { get; set; } + + /// + /// Default Constructor + /// + public PickListItem() + { + } + + /// + /// Constructor with data. + /// + /// + /// + public PickListItem(string label, int id) + { + DisplayLabel = label; + PickListItemId = id; + } + } +} diff --git a/src/GeneralTools/DataverseClient/Client/Extensions/SupportClasses/PickListMetaElement.cs b/src/GeneralTools/DataverseClient/Client/Extensions/SupportClasses/PickListMetaElement.cs new file mode 100644 index 0000000..4b72e8c --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Extensions/SupportClasses/PickListMetaElement.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PowerPlatform.Dataverse.Client.Extensions +{ + /// + /// PickList data + /// + public sealed class PickListMetaElement + { + /// + /// Current value of the PickList Item + /// + public string ActualValue { get; set; } + /// + /// Displayed Label + /// + public string PickListLabel { get; set; } + /// + /// Displayed value for the PickList + /// + public string DisplayValue { get; set; } + /// + /// Array of Potential Pick List Items. + /// + public List Items { get; set; } + + /// + /// Default Constructor + /// + public PickListMetaElement() + { + Items = new List(); + } + + /// + /// Constructs a PickList item with data. + /// + /// + /// + /// + public PickListMetaElement(string actualValue, string displayValue, string pickListLabel) + { + Items = new List(); + ActualValue = actualValue; + PickListLabel = pickListLabel; + DisplayValue = displayValue; + } + } +} diff --git a/src/GeneralTools/DataverseClient/Client/Interfaces/IOrganizationServiceAsync.cs b/src/GeneralTools/DataverseClient/Client/Interfaces/IOrganizationServiceAsync.cs index b1eea16..38621aa 100644 --- a/src/GeneralTools/DataverseClient/Client/Interfaces/IOrganizationServiceAsync.cs +++ b/src/GeneralTools/DataverseClient/Client/Interfaces/IOrganizationServiceAsync.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ServiceModel; using System.Threading; using System.Threading.Tasks; @@ -7,89 +7,89 @@ namespace Microsoft.PowerPlatform.Dataverse.Client { - /// - /// Interface containing extension methods provided by the DataverseServiceClient for the IOrganizationService Interface. - /// These extensions will only operate from within the client and are not supported server side. - /// - [ServiceContract(Name = "IOrganizationService", Namespace = Xrm.Sdk.XmlNamespaces.V5.Services)] - [KnownAssembly] - public interface IOrganizationServiceAsync: IOrganizationService - { - /// - /// Create an entity and process any related entities - /// - /// entity to create - /// The ID of the created record - [OperationContract] - - Task CreateAsync(Entity entity); - - /// - /// Retrieves instance of an entity - /// - /// Logical name of entity - /// Id of entity - /// Column Set collection to return with the request - /// Selected Entity - [OperationContract] - - Task RetrieveAsync(string entityName, Guid id, ColumnSet columnSet); - - /// - /// Updates an entity and process any related entities - /// - /// entity to update - [OperationContract] - - Task UpdateAsync(Entity entity); - - /// - /// Delete instance of an entity - /// - /// Logical name of entity - /// Id of entity - [OperationContract] - - Task DeleteAsync(string entityName, Guid id); - - /// - /// 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 - [OperationContract] - - Task ExecuteAsync(OrganizationRequest request ); - - /// - /// Associate an entity with a set of entities - /// - /// - /// - /// - /// - [OperationContract] - - Task AssociateAsync(string entityName, Guid entityId, Relationship relationship, EntityReferenceCollection relatedEntities); - - /// - /// Disassociate an entity with a set of entities - /// - /// - /// - /// - /// - [OperationContract] - - Task DisassociateAsync(string entityName, Guid entityId, Relationship relationship, EntityReferenceCollection relatedEntities); - - /// - /// Retrieves a collection of entities - /// - /// - /// Returns an EntityCollection Object containing the results of the query - [OperationContract] - - Task RetrieveMultipleAsync(QueryBase query); - } + /// + /// Interface containing extension methods provided by the DataverseServiceClient for the IOrganizationService Interface. + /// These extensions will only operate from within the client and are not supported server side. + /// + [ServiceContract(Name = "IOrganizationService", Namespace = Xrm.Sdk.XmlNamespaces.V5.Services)] + [KnownAssembly] + public interface IOrganizationServiceAsync : IOrganizationService + { + /// + /// Create an entity and process any related entities + /// + /// entity to create + /// The ID of the created record + [OperationContract] + + Task CreateAsync(Entity entity); + + /// + /// Retrieves instance of an entity + /// + /// Logical name of entity + /// Id of entity + /// Column Set collection to return with the request + /// Selected Entity + [OperationContract] + + Task RetrieveAsync(string entityName, Guid id, ColumnSet columnSet); + + /// + /// Updates an entity and process any related entities + /// + /// entity to update + [OperationContract] + + Task UpdateAsync(Entity entity); + + /// + /// Delete instance of an entity + /// + /// Logical name of entity + /// Id of entity + [OperationContract] + + Task DeleteAsync(string entityName, Guid id); + + /// + /// 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 + [OperationContract] + + Task ExecuteAsync(OrganizationRequest request); + + /// + /// Associate an entity with a set of entities + /// + /// + /// + /// + /// + [OperationContract] + + Task AssociateAsync(string entityName, Guid entityId, Relationship relationship, EntityReferenceCollection relatedEntities); + + /// + /// Disassociate an entity with a set of entities + /// + /// + /// + /// + /// + [OperationContract] + + Task DisassociateAsync(string entityName, Guid entityId, Relationship relationship, EntityReferenceCollection relatedEntities); + + /// + /// Retrieves a collection of entities + /// + /// + /// Returns an EntityCollection Object containing the results of the query + [OperationContract] + + Task RetrieveMultipleAsync(QueryBase query); + } } diff --git a/src/GeneralTools/DataverseClient/Client/Interfaces/IOrganizationServiceAsync2.cs b/src/GeneralTools/DataverseClient/Client/Interfaces/IOrganizationServiceAsync2.cs index 151e4a7..abbea29 100644 --- a/src/GeneralTools/DataverseClient/Client/Interfaces/IOrganizationServiceAsync2.cs +++ b/src/GeneralTools/DataverseClient/Client/Interfaces/IOrganizationServiceAsync2.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ServiceModel; using System.Threading; using System.Threading.Tasks; @@ -7,80 +7,90 @@ namespace Microsoft.PowerPlatform.Dataverse.Client { - /// - /// Interface containing extension methods provided by the DataverseServiceClient for the IOrganizationService Interface. - /// These extensions will only operate from within the client and are not supported server side. - /// - public interface IOrganizationServiceAsync2 : IOrganizationServiceAsync - { - /// - /// Associate an entity with a set of entities - /// - /// - /// - /// - /// - /// Propagates notification that operations should be canceled. - Task AssociateAsync(string entityName, Guid entityId, Relationship relationship, EntityReferenceCollection relatedEntities, CancellationToken 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 - Task CreateAsync(Entity entity, CancellationToken cancellationToken); - /// - /// Create an entity and process any related entities - /// - /// entity to create - /// Propagates notification that operations should be canceled. - /// Returns the newly created record - Task CreateAndReturnAsync(Entity entity, CancellationToken cancellationToken); - /// - /// Delete instance of an entity - /// - /// Logical name of entity - /// Id of entity - /// Propagates notification that operations should be canceled. - Task DeleteAsync(string entityName, Guid id, CancellationToken cancellationToken); - /// - /// Disassociate an entity with a set of entities - /// - /// - /// - /// - /// - /// Propagates notification that operations should be canceled. - Task DisassociateAsync(string entityName, Guid entityId, Relationship relationship, EntityReferenceCollection relatedEntities, CancellationToken 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 - Task ExecuteAsync(OrganizationRequest request, CancellationToken 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 - Task RetrieveAsync(string entityName, Guid id, ColumnSet columnSet, CancellationToken cancellationToken); - /// - /// Retrieves a collection of entities - /// - /// - /// Propagates notification that operations should be canceled. - /// Returns an EntityCollection Object containing the results of the query - Task RetrieveMultipleAsync(QueryBase query, CancellationToken cancellationToken); - /// - /// Updates an entity and process any related entities - /// - /// entity to update - /// Propagates notification that operations should be canceled. - Task UpdateAsync(Entity entity, CancellationToken cancellationToken); - } + /// + /// Interface containing extension methods provided by the DataverseServiceClient for the IOrganizationService Interface. + /// These extensions will only operate from within the client and are not supported server side. + /// + [ServiceContract(Name = "IOrganizationService", Namespace = Xrm.Sdk.XmlNamespaces.V5.Services)] + [KnownAssembly] + public interface IOrganizationServiceAsync2 : IOrganizationServiceAsync + { + /// + /// Associate an entity with a set of entities + /// + /// + /// + /// + /// + /// Propagates notification that operations should be canceled. + [OperationContract] + Task AssociateAsync(string entityName, Guid entityId, Relationship relationship, EntityReferenceCollection relatedEntities, CancellationToken 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 + [OperationContract] + Task CreateAsync(Entity entity, CancellationToken cancellationToken); + /// + /// Create an entity and process any related entities + /// + /// entity to create + /// Propagates notification that operations should be canceled. + /// Returns the newly created record + Task CreateAndReturnAsync(Entity entity, CancellationToken cancellationToken); + /// + /// Delete instance of an entity + /// + /// Logical name of entity + /// Id of entity + /// Propagates notification that operations should be canceled. + [OperationContract] + Task DeleteAsync(string entityName, Guid id, CancellationToken cancellationToken); + /// + /// Disassociate an entity with a set of entities + /// + /// + /// + /// + /// + /// Propagates notification that operations should be canceled. + [OperationContract] + Task DisassociateAsync(string entityName, Guid entityId, Relationship relationship, EntityReferenceCollection relatedEntities, CancellationToken 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 + [OperationContract] + Task ExecuteAsync(OrganizationRequest request, CancellationToken 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 + [OperationContract] + Task RetrieveAsync(string entityName, Guid id, ColumnSet columnSet, CancellationToken cancellationToken); + /// + /// Retrieves a collection of entities + /// + /// + /// Propagates notification that operations should be canceled. + /// Returns an EntityCollection Object containing the results of the query + [OperationContract] + Task RetrieveMultipleAsync(QueryBase query, CancellationToken cancellationToken); + /// + /// Updates an entity and process any related entities + /// + /// entity to update + /// Propagates notification that operations should be canceled. + [OperationContract] + Task UpdateAsync(Entity entity, CancellationToken cancellationToken); + } } diff --git a/src/GeneralTools/DataverseClient/Client/InternalExtensions/BaseTypeExtensions.cs b/src/GeneralTools/DataverseClient/Client/InternalExtensions/BaseTypeExtensions.cs new file mode 100644 index 0000000..2c02e39 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/InternalExtensions/BaseTypeExtensions.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PowerPlatform.Dataverse.Client.InternalExtensions +{ + internal static class BaseTypeExtensions + { + /// + /// Enum extension + /// + /// + /// + /// Enum Value + public static T ToEnum(this string enumName) + { + return (T)((object)Enum.Parse(typeof(T), enumName)); + } + /// + /// Converts a int to a Enum of the requested type (T) + /// + /// Enum Type to translate too + /// Int Value too translate. + /// Enum of Type T + public static T ToEnum(this int enumValue) + { + return enumValue.ToString().ToEnum(); + } + /// + /// Converts a ; separated string into a dictionary + /// + /// String to parse + /// Dictionary of properties from the connection string + public static IDictionary ToDictionary(this string connectionString) + { + try + { + DbConnectionStringBuilder source = new DbConnectionStringBuilder + { + ConnectionString = connectionString + }; + + Dictionary dictionary = source.Cast>(). + ToDictionary((KeyValuePair pair) => pair.Key, + (KeyValuePair pair) => pair.Value != null ? pair.Value.ToString() : string.Empty); + return new Dictionary(dictionary, StringComparer.OrdinalIgnoreCase); + } + catch + { + //ignore + } + return new Dictionary(); + + } + /// + /// Extension to support formating a string + /// + /// Formatting pattern + /// Argument collection + /// Formated String + public static string FormatWith(this string format, params object[] args) + { + return format.FormatWith(CultureInfo.InvariantCulture, args); + } + /// + /// Extension to get the first item in a dictionary if the dictionary contains the key. + /// + /// Type to return + /// Dictionary to search + /// Collection of Keys to find. + /// + public static string FirstNotNullOrEmpty(this IDictionary dictionary, params TKey[] keys) + { + return ( + from key in keys + where dictionary.ContainsKey(key) && !string.IsNullOrEmpty(dictionary[key]) + select dictionary[key]).FirstOrDefault(); + } + } +} diff --git a/src/GeneralTools/DataverseClient/Client/Utils/RequestResponseExtenstions.cs b/src/GeneralTools/DataverseClient/Client/InternalExtensions/RequestResponseExtenstions.cs similarity index 92% rename from src/GeneralTools/DataverseClient/Client/Utils/RequestResponseExtenstions.cs rename to src/GeneralTools/DataverseClient/Client/InternalExtensions/RequestResponseExtenstions.cs index 739f7a3..20c2e9d 100644 --- a/src/GeneralTools/DataverseClient/Client/Utils/RequestResponseExtenstions.cs +++ b/src/GeneralTools/DataverseClient/Client/InternalExtensions/RequestResponseExtenstions.cs @@ -1,4 +1,4 @@ -#region using +#region using using Microsoft.Xrm.Sdk; using System; using System.Collections.Generic; @@ -8,12 +8,12 @@ using System.Threading.Tasks; #endregion -namespace Microsoft.PowerPlatform.Dataverse.Client.Utils +namespace Microsoft.PowerPlatform.Dataverse.Client.InternalExtensions { /// /// Organization request/response extenstions /// - public static class RequestResponseExtenstions + internal static class RequestResponseExtenstions { /// /// Converts OrganizationRequest object to ExpandoObject diff --git a/src/GeneralTools/DataverseClient/Client/InternalExtensions/SecureStringExtensions.cs b/src/GeneralTools/DataverseClient/Client/InternalExtensions/SecureStringExtensions.cs new file mode 100644 index 0000000..3c4e124 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/InternalExtensions/SecureStringExtensions.cs @@ -0,0 +1,36 @@ +using System; +using System.Runtime.InteropServices; +using System.Security; + +namespace Microsoft.PowerPlatform.Dataverse.Client.InternalExtensions +{ + /// + /// Adds a extension to Secure string + /// + internal static class SecureStringExtensions + { + /// + /// DeCrypt a Secure password + /// + /// + /// + public static string ToUnsecureString(this SecureString value) + { + if (null == value) + throw new ArgumentNullException("value"); + + // Get a pointer to the secure string memory data. + IntPtr ptr = Marshal.SecureStringToGlobalAllocUnicode(value); + try + { + // DeCrypt + return Marshal.PtrToStringUni(ptr); + } + finally + { + // release the pointer. + Marshal.ZeroFreeGlobalAllocUnicode(ptr); + } + } + } +} diff --git a/src/GeneralTools/DataverseClient/Client/Microsoft.PowerPlatform.Dataverse.Client.csproj b/src/GeneralTools/DataverseClient/Client/Microsoft.PowerPlatform.Dataverse.Client.csproj index 95d2a3a..a32dd74 100644 --- a/src/GeneralTools/DataverseClient/Client/Microsoft.PowerPlatform.Dataverse.Client.csproj +++ b/src/GeneralTools/DataverseClient/Client/Microsoft.PowerPlatform.Dataverse.Client.csproj @@ -1,4 +1,4 @@ - + Microsoft.PowerPlatform.Dataverse.Client @@ -25,7 +25,7 @@ - + @@ -37,7 +37,9 @@ - + + + diff --git a/src/GeneralTools/DataverseClient/Client/Model/AppSettingsConfiguration.cs b/src/GeneralTools/DataverseClient/Client/Model/ConfigurationOptions.cs similarity index 50% rename from src/GeneralTools/DataverseClient/Client/Model/AppSettingsConfiguration.cs rename to src/GeneralTools/DataverseClient/Client/Model/ConfigurationOptions.cs index 3b55721..b05cf52 100644 --- a/src/GeneralTools/DataverseClient/Client/Model/AppSettingsConfiguration.cs +++ b/src/GeneralTools/DataverseClient/Client/Model/ConfigurationOptions.cs @@ -1,4 +1,4 @@ -#region +#region using Microsoft.Extensions.Configuration; using System; using System.Collections.Generic; @@ -10,9 +10,9 @@ namespace Microsoft.PowerPlatform.Dataverse.Client.Model { /// - /// App settings configuration + /// Client Configuration Options Array. /// - public class AppSettingsConfiguration + public class ConfigurationOptions { #region Dataverse Interaction Settings private int _maxRetryCount = Utils.AppSettingsHelper.GetAppSetting("ApiOperationRetryCountOverride", 10); @@ -59,6 +59,67 @@ public bool UseWebApiLoginFlow set => _useWebApiLoginFlow = value; } + private bool _enableAffinityCookie = Utils.AppSettingsHelper.GetAppSetting("EnableAffinityCookie", true); + /// + /// 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. + /// + public bool EnableAffinityCookie + { + get => _enableAffinityCookie; + set => _enableAffinityCookie = value; + } + + // For future work... + + //private TimeSpan _maxConnectionTimeout = Utils.AppSettingsHelper.GetAppSettingTimeSpan("MaxDataverseConnectionTimeOutMinutes", Utils.AppSettingsHelper.TimeSpanFromKey.Minutes, TimeSpan.FromMinutes(4)); + + ///// + ///// Max connection timeout property + ///// https://docs.microsoft.com/en-us/azure/app-service/faq-availability-performance-application-issues#why-does-my-request-time-out-after-230-seconds + ///// Azure Load Balancer has a default idle timeout setting of four minutes. This is generally a reasonable response time limit for a web request. + ///// + //public TimeSpan MaxConnectionTimeout + //{ + // get => _maxConnectionTimeout; + // set => _maxConnectionTimeout = value; + + //} + + private string _maxFaultSizeOverride = Utils.AppSettingsHelper.GetAppSetting("MaxFaultSizeOverride", null); + /// + /// MaxFaultSize override. - Use under Microsoft Direction only. + /// + public string MaxFaultSizeOverride + { + get => _maxFaultSizeOverride; + set => _maxFaultSizeOverride = value; + } + + private string _maxReceivedMessageSize = Utils.AppSettingsHelper.GetAppSetting("MaxReceivedMessageSizeOverride", null); + /// + /// MaxReceivedMessageSize override. - Use under Microsoft Direction only. + /// + public string MaxReceivedMessageSizeOverride + { + get => _maxReceivedMessageSize; + set => _maxReceivedMessageSize = value; + } + + private string _maxBufferPoolSizeOveride = Utils.AppSettingsHelper.GetAppSetting("MaxBufferPoolSizeOveride", null); + /// + /// MaxBufferPoolSize override. - Use under Microsoft Direction only. + /// + public string MaxBufferPoolSizeOveride + { + get => _maxBufferPoolSizeOveride; + set => _maxBufferPoolSizeOveride = value; + } + + #endregion #region MSAL Settings. diff --git a/src/GeneralTools/DataverseClient/Client/Model/ConnectionOptions.cs b/src/GeneralTools/DataverseClient/Client/Model/ConnectionOptions.cs new file mode 100644 index 0000000..50788a4 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Client/Model/ConnectionOptions.cs @@ -0,0 +1,112 @@ +using Microsoft.Extensions.Logging; +using Microsoft.PowerPlatform.Dataverse.Client.Auth; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PowerPlatform.Dataverse.Client.Model +{ + /// + /// Describes connection Options for the Dataverse ServiceClient + /// + public class ConnectionOptions + { + /// + /// Defines which type of login will be used to connect to Dataverse + /// + public AuthenticationType AuthenticationType { get; set; } = AuthenticationType.OAuth; + + /// + /// URL of the Dataverse Instance to connect too. + /// + public Uri ServiceUri { get; set; } + + /// + /// User Name to use - Used with Interactive Login scenarios + /// + public string UserName { get; set; } + + /// + /// User Password to use - Used with Interactive Login scenarios + /// + public SecureString Password { get; set; } + + /// + /// User Domain to use - Use with Interactive Login for On Premises + /// + public string Domain { get; set; } + + /// + /// Home Realm to use when working with AD Federation. + /// + public Uri HomeRealmUri { get; set; } + + /// + /// Require a unique instance of the Dataverse ServiceClient per Login. + /// + public bool RequireNewInstance { get; set; } = true; + + /// + /// Client \ Application ID to be used when logging into Dataverse. + /// + public string ClientId { get; set; } = DataverseConnectionStringProcessor.sampleClientId; + + /// + /// Client Secret Id to use to login to Dataverse + /// + public string ClientSecret { get; set; } + + /// + /// Redirect Uri to use when connecting to dataverse. Required for OAuth Authentication. + /// + public Uri RedirectUri { get; set; } = new Uri(DataverseConnectionStringProcessor.sampleRedirectUrl); + + /// + /// Path and FileName for MSAL Token Cache. Used only for OAuth - User Interactive flows. + /// + public string TokenCacheStorePath { get; set; } + + /// + /// Type of Login prompt to use. + /// + public PromptBehavior? LoginPrompt { get; set; } + + /// + /// Certificate ThumbPrint to use to lookup machine certificate to use for authentication. + /// + public string CertificateThumbprint { get; set; } + + /// + /// Certificate store name to look up thumbprint. + /// + public System.Security.Cryptography.X509Certificates.StoreName CertificateStoreName { get; set; } + + /// + /// Skip discovery leg when connecting to Dataverse + /// + public bool SkipDiscovery { get; set; } = true; + + /// + /// (Windows Only) If True, Uses the current user of windows to attempt the login with + /// + public bool UseCurrentUserForLogin { get; set; } + + /// + /// ILogger Interface for Dataverse ServiceClient. + /// + public ILogger Logger { get; set; } + + /// + /// Function that Dataverse ServiceClient will call to request an access token for a given connection. + /// + public Func> AccessTokenProviderFunctionAsync { get; set; } + + /// + /// Function that Dataverse ServiceClient will call to request custom headers + /// + public Func>> RequestAdditionalHeadersAsync { get; set; } + } +} diff --git a/src/GeneralTools/DataverseClient/Client/ServiceClient.cs b/src/GeneralTools/DataverseClient/Client/ServiceClient.cs index 3816108..93e9d08 100644 --- a/src/GeneralTools/DataverseClient/Client/ServiceClient.cs +++ b/src/GeneralTools/DataverseClient/Client/ServiceClient.cs @@ -30,6 +30,8 @@ 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; #endregion namespace Microsoft.PowerPlatform.Dataverse.Client @@ -45,17 +47,17 @@ public class ServiceClient : IOrganizationService, IOrganizationServiceAsync2, I /// /// Cached Object collection, used for pick lists and such. /// - private Dictionary> _CachObject; //Cache object. + internal Dictionary> _CachObject; //Cache object. /// /// List of Dataverse Language ID's /// - private List _loadedLCIDList; + internal List _loadedLCIDList; /// /// Name of the cache object. /// - private string _cachObjecName = ".LookupCache"; + internal string _cachObjecName = ".LookupCache"; /// /// Logging object for the Dataverse Interface. @@ -70,17 +72,17 @@ public class ServiceClient : IOrganizationService, IOrganizationServiceAsync2, I /// /// Dynamic app utility /// - private DynamicEntityUtility _dynamicAppUtility = null; + internal DynamicEntityUtility _dynamicAppUtility = null; /// /// Configuration /// - private IOptions _configuration = ClientServiceProviders.Instance.GetService>(); + internal IOptions _configuration = ClientServiceProviders.Instance.GetService>(); /// /// Metadata Utility /// - private MetadataUtility _metadataUtlity = null; + internal MetadataUtility _metadataUtlity = null; /// /// This is an internal Lock object, used to sync communication with Dataverse. @@ -90,7 +92,7 @@ public class ServiceClient : IOrganizationService, IOrganizationServiceAsync2, I /// /// BatchManager for Execute Multiple. /// - private BatchManager _batchManager = null; + internal BatchManager _batchManager = null; ///// ///// To cache the token @@ -120,7 +122,7 @@ public class ServiceClient : IOrganizationService, IOrganizationServiceAsync2, I #region Properties /// - /// Exposed OrganizationWebProxyClient for consumers + /// Internal OnLineClient /// internal OrganizationWebProxyClientAsync OrganizationWebProxyClient { @@ -130,8 +132,6 @@ internal OrganizationWebProxyClientAsync OrganizationWebProxyClient { if (_connectionSvc.WebClient == null) { - if (_logEntry != null) - _logEntry.Log("OrganizationWebProxyClientAsync is null", TraceEventType.Error); return null; } else @@ -139,8 +139,29 @@ internal OrganizationWebProxyClientAsync OrganizationWebProxyClient } else { - if (_logEntry != null) - _logEntry.Log("OrganizationWebProxyClientAsync is null", TraceEventType.Error); + return null; + } + } + } + + /// + /// Internal OnLineClient + /// + internal OrganizationServiceProxyAsync OnPremClient + { + get + { + if (_connectionSvc != null) + { + if (_connectionSvc.OnPremClient == null) + { + return null; + } + else + return _connectionSvc.OnPremClient; + } + else + { return null; } } @@ -315,7 +336,10 @@ internal IOrganizationService DataverseService if (_connectionSvc != null) { - return _connectionSvc.WebClient; + if (_connectionSvc.WebClient != null) + return _connectionSvc.WebClient; + else + return _connectionSvc.OnPremClient; } else return null; } @@ -334,7 +358,10 @@ internal IOrganizationServiceAsync DataverseServiceAsync if (_connectionSvc != null) { - return _connectionSvc.WebClient; + if (_connectionSvc.WebClient != null) + return _connectionSvc.WebClient; + else + return _connectionSvc.OnPremClient; } else return null; } @@ -629,7 +656,7 @@ internal ServiceClient(IOrganizationService orgSvc, HttpClient httpClient, strin LogRetentionDuration = new TimeSpan(0, 10, 0), EnabledInMemoryLogCapture = true }; - _connectionSvc = new ConnectionService(orgSvc, baseConnectUrl , httpClient, logger); + _connectionSvc = new ConnectionService(orgSvc, baseConnectUrl, httpClient, logger); if (targetVersion != null) _connectionSvc.OrganizationVersion = targetVersion; @@ -800,7 +827,6 @@ public ServiceClient(X509Certificate2 certificate, StoreName certificateStoreNam clientId, redirectUri, PromptBehavior.Never, null, certificateThumbPrint, certificateStoreName, certificate, instanceUrl, externalLogger: logger); } - /// /// Log in with Certificate Auth OnLine connections. /// This requires the org API URI. @@ -835,7 +861,6 @@ public ServiceClient(X509Certificate2 certificate, StoreName certificateStoreNam clientId, redirectUri, PromptBehavior.Never, null, certificateThumbPrint, certificateStoreName, certificate, instanceUrl, externalLogger: logger); } - /// /// ClientID \ ClientSecret Based Authentication flow. /// @@ -870,6 +895,16 @@ public ServiceClient(Uri instanceUrl, string clientId, SecureString clientSecret null, clientId, null, PromptBehavior.Never, null, null, instanceUrl: instanceUrl, externalLogger: logger); } + /// + /// Creating the ServiceClient Connection with a Set of ConnectionOptionsObject and a default configuration. + /// + /// + /// + public ServiceClient(ConnectionOptions connectionOptions, IConfigureOptions serviceClientConfiguration) + { + throw new NotImplementedException("PreWork for Builder Support"); + } + /// /// Parse the given connection string /// Connects to Dataverse using CreateWebServiceConnection @@ -895,8 +930,8 @@ internal void ConnectToService(string connectionString, ILogger logger = null) // Orgname is mandatory if skip discovery is not passed throw new ArgumentNullException("Dataverse Instance Name or URL name Required", parsedConnStr.IsOnPremOauth ? - $"Unable to determine instance name to connect to from passed instance Uri, Uri does not match known online deployments." : - $"Unable to determine instance name to connect to from passed instance Uri. Uri does not match specification for OnPrem instances."); + $"Unable to determine instance name to connect to from passed instance Uri. Uri does not match specification for OnPrem instances." : + $"Unable to determine instance name to connect to from passed instance Uri, Uri does not match known online deployments."); string homesRealm = parsedConnStr.HomeRealmUri != null ? parsedConnStr.HomeRealmUri.AbsoluteUri : string.Empty; @@ -963,10 +998,14 @@ internal void ConnectToService(string connectionString, ILogger logger = null) MakeSecureString(parsedConnStr.ClientSecret), string.Empty, onlineRegion, string.Empty, useSsl, parsedConnStr.UseUniqueConnectionInstance, null, clientId, redirectUri, PromptBehavior.Never, null, null, instanceUrl: parsedConnStr.ServiceUri, externalLogger: logger); break; + case AuthenticationType.AD: + CreateServiceConnection(null, parsedConnStr.AuthenticationType, hostname, port, orgName, networkCredentials, userId, + MakeSecureString(password), domainname, string.Empty, string.Empty, useSsl, parsedConnStr.UseUniqueConnectionInstance, null, instanceUrl: parsedConnStr.SkipDiscovery ? parsedConnStr.ServiceUri : null, externalLogger: logger); + + break; } } - /// /// Uses the Organization Web proxy Client provided by the user /// @@ -1092,7 +1131,7 @@ internal void CreateServiceConnection( // if using an user provided connection,. if (externalOrgWebProxyClient != null) { - _connectionSvc = new ConnectionService(externalOrgWebProxyClient, requestedAuthType , _logEntry); + _connectionSvc = new ConnectionService(externalOrgWebProxyClient, requestedAuthType, _logEntry); _connectionSvc.IsAClone = isCloned; if (isCloned && incomingOrgVersion != null) { @@ -1140,6 +1179,14 @@ internal void CreateServiceConnection( else _connectionSvc = new ConnectionService(requestedAuthType, orgName, userId, password, Geo, useUniqueInstance, orgDetail, clientId, redirectUri, promptBehavior, hostName, port, false, instanceToConnectToo: instanceUrl, logSink: _logEntry, useDefaultCreds: useDefaultCreds, tokenCacheStorePath: tokenCacheStorePath); } + else if (requestedAuthType == AuthenticationType.AD) + { + // User is using AD or IFD + if (credential == null) + _connectionSvc = new ConnectionService(requestedAuthType, hostName, port, orgName, System.Net.CredentialCache.DefaultNetworkCredentials, useUniqueInstance, orgDetail, instanceToConnectToo: instanceUrl, logSink: _logEntry); + else + _connectionSvc = new ConnectionService(requestedAuthType, hostName, port, orgName, credential, useUniqueInstance, orgDetail, instanceToConnectToo: instanceUrl, logSink: _logEntry); + } } } @@ -1230,9 +1277,13 @@ public ServiceClient Clone(System.Reflection.Assembly strongTypeAsm, ILogger log if (_connectionSvc == null || IsReady == false) { _logEntry.Log("You must have successfully created a connection to Dataverse before it can be cloned.", TraceEventType.Error); - return null; + throw new DataverseOperationException("You must have successfully created a connection to Dataverse before it can be cloned."); } + // On-Prem Auth flows are not supported for Clone right now. + if (_connectionSvc.AuthenticationTypeInUse == AuthenticationType.AD) + throw new DataverseOperationException("On-Premises Connections are not supported for clone operations at this time.", new NotImplementedException("OnPrem Auth Flow are not implemented for clone operations")); + OrganizationWebProxyClientAsync proxy = null; if (_connectionSvc.ConnectOrgUriActual != null) { @@ -1388,4052 +1439,198 @@ public static async Task DiscoverOnlineOrganizatio #endregion - #region Dataverse Service Methods - - #region Batch Interface methods. - /// - /// Create a Batch Request for executing batch operations. This returns an ID that will be used to identify a request as a batch request vs a "normal" request. - /// - /// Name of the Batch - /// Should Results be returned - /// Should the process continue on an error. - /// - public Guid CreateBatchOperationRequest(string batchName, bool returnResults = true, bool continueOnError = false) - { - #region PreChecks - _logEntry.ResetLastError(); - if (DataverseService == null) - { - _logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); - return Guid.Empty; - } - - if (!IsBatchOperationsAvailable) - { - _logEntry.Log("Batch Operations are not available", TraceEventType.Error); - return Guid.Empty; - } - #endregion + #endregion - Guid guBatchId = Guid.Empty; - if (_batchManager != null) - { - // Try to create a new Batch here. - guBatchId = _batchManager.CreateNewBatch(batchName, returnResults, continueOnError); - } - return guBatchId; - } + #region OAuth Token Cache /// - /// Returns the batch id for a given batch name. + /// Clear the persistent and in-memory store cache /// - /// Name of Batch + /// /// - public Guid GetBatchOperationIdRequestByName(string batchName) + public static bool RemoveOAuthTokenCache(string tokenCachePath = "") { - #region PreChecks - _logEntry.ResetLastError(); - if (DataverseService == null) - { - _logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); - return Guid.Empty; - } - - if (!IsBatchOperationsAvailable) - { - _logEntry.Log("Batch Operations are not available", TraceEventType.Error); - return Guid.Empty; - } - #endregion - - if (_batchManager != null) - { - var b = _batchManager.GetRequestBatchByName(batchName); - if (b != null) - return b.BatchId; - } - return Guid.Empty; + throw new NotImplementedException(); + //If tokenCachePath is not supplied it will take from the constructor of token cache and delete the file. + //if (_CdsServiceClientTokenCache == null) + // _CdsServiceClientTokenCache = new CdsServiceClientTokenCache(tokenCachePath); + //return _CdsServiceClientTokenCache.Clear(tokenCachePath); + //TODO: Update for new Token cache providers. + //return false; } + #endregion + + #region XRM Commands and handlers + #region Public Access to direct commands. /// - /// Returns the organization request at a give position + /// Executes a web request against Xrm WebAPI. /// - /// ID of the batch - /// Position + /// Here you would pass the path and query parameters that you wish to pass onto the WebAPI. + /// The format used here is as follows: + /// {APIURI}/api/data/v{instance version}/querystring. + /// For example, + /// if you wanted to get data back from an account, you would pass the following: + /// accounts(id) + /// which creates: get - https://myinstance.crm.dynamics.com/api/data/v9.0/accounts(id) + /// if you were creating an account, you would pass the following: + /// accounts + /// which creates: post - https://myinstance.crm.dynamics.com/api/data/v9.0/accounts - body contains the data. + /// + /// Method to use for the request + /// Content your passing to the request + /// Headers in addition to the default headers added by for Executing a web request + /// Content Type attach to the request. this defaults to application/json if not set. + /// Cancellation token for the request /// - public OrganizationRequest GetBatchRequestAtPosition(Guid batchId, int position) + public HttpResponseMessage ExecuteWebRequest(HttpMethod method, string queryString, string body, Dictionary> customHeaders, string contentType = default, CancellationToken cancellationToken = default) { - #region PreChecks - _logEntry.ResetLastError(); + _logEntry.ResetLastError(); // Reset Last Error if (DataverseService == null) { _logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); return null; } - if (!IsBatchOperationsAvailable) + if (string.IsNullOrEmpty(queryString) && string.IsNullOrEmpty(body)) { - _logEntry.Log("Batch Operations are not available", TraceEventType.Error); + _logEntry.Log("Execute Web Request failed, queryString and body cannot be null", TraceEventType.Error); return null; } - #endregion - RequestBatch b = GetBatchById(batchId); - if (b != null) + if (Uri.TryCreate(queryString, UriKind.Absolute, out var urlPath)) { - if (b.BatchItems.Count >= position) - return b.BatchItems[position].Request; + // Was able to create a URL here... Need to make sure that we strip out everything up to the last segment. + string baseQueryString = urlPath.Segments.Last(); + if (!string.IsNullOrEmpty(urlPath.Query)) + queryString = baseQueryString + urlPath.Query; + else + queryString = baseQueryString; } - return null; + + var result = _connectionSvc.Command_WebExecuteAsync(queryString, body, method, customHeaders, contentType, string.Empty, CallerId, _disableConnectionLocking, MaxRetryCount, RetryPauseTime, cancellationToken: cancellationToken).Result; + if (result == null) + throw LastException; + else + return result; } /// - /// Release a batch from the stack - /// Once you have completed using a batch, you must release it from the system. + /// Executes a Dataverse Organization Request (thread safe) and returns the organization response object. Also adds metrics for logging support. /// - /// ID of the batch - public void ReleaseBatchInfoById(Guid batchId) + /// Organization Request to run + /// Message identifying what this request in logging. + /// When True, uses the webAPI to execute the organization Request. This works for only Create at this time. + /// Result of request or null. + public OrganizationResponse ExecuteOrganizationRequest(OrganizationRequest req, string logMessageTag = "User Defined", bool useWebAPI = false) { - #region PreChecks - _logEntry.ResetLastError(); - if (DataverseService == null) - { - _logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); - return; - } - - if (!IsBatchOperationsAvailable) - { - _logEntry.Log("Batch Operations are not available", TraceEventType.Error); - return; - } - #endregion - - if (_batchManager != null) - _batchManager.RemoveBatch(batchId); - + return ExecuteOrganizationRequestImpl(req, logMessageTag, useWebAPI, false); } + #endregion - /// - /// TEMP - /// - /// ID of the batch - /// - public RequestBatch GetBatchById(Guid batchId) + #region Internal + + internal OrganizationResponse ExecuteOrganizationRequestImpl(OrganizationRequest req, string logMessageTag = "User Defined", bool useWebAPI = false, bool bypassPluginExecution = false) { - #region PreChecks - _logEntry.ResetLastError(); - if (DataverseService == null) + if (req != null) { - _logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); - return null; + useWebAPI = Utilities.IsRequestValidForTranslationToWebAPI(req); + if (!useWebAPI) + { + return Command_Execute(req, logMessageTag, bypassPluginExecution); + } + else + { + // use Web API. + return _connectionSvc.Command_WebAPIProcess_ExecuteAsync(req, logMessageTag, bypassPluginExecution, _metadataUtlity, CallerId, _disableConnectionLocking, MaxRetryCount, RetryPauseTime, new CancellationToken()).Result; + } } - - if (!IsBatchOperationsAvailable) + else { - _logEntry.Log("Batch Operations are not available", TraceEventType.Error); + _logEntry.Log("Execute Organization Request failed, Organization Request cannot be null", TraceEventType.Error); return null; } - #endregion - - if (_batchManager != null) - { - return _batchManager.GetRequestBatchById(batchId); - } - return null; } - /// - /// Executes the batch command and then parses the retrieved items into a list. - /// If there exists a exception then the LastException would be filled with the first item that has the exception. - /// - /// ID of the batch to run - /// results which is a list of responses(type >> ]]>) in the order of each request or null or complete failure - public List>> RetrieveBatchResponse(Guid batchId) + private async Task ExecuteOrganizationRequestAsyncImpl(OrganizationRequest req, CancellationToken cancellationToken, string logMessageTag = "User Defined", bool useWebAPI = false, bool bypassPluginExecution = false) { - ExecuteMultipleResponse results = ExecuteBatch(batchId); - if (results == null) - { - return null; - } - if (results.IsFaulted) + cancellationToken.ThrowIfCancellationRequested(); + if (req != null) { - foreach (var response in results.Responses) + useWebAPI = Utilities.IsRequestValidForTranslationToWebAPI(req); + if (!useWebAPI) { - if (response.Fault != null) - { - FaultException ex = new FaultException(response.Fault, new FaultReason(new FaultReasonText(response.Fault.Message))); - - _logEntry.Log(string.Format(CultureInfo.InvariantCulture, "Failed to Execute Batch - {0}", batchId), TraceEventType.Verbose); - _logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ BatchExecution failed - : {0}\n\r{1}", response.Fault.Message, response.Fault.ErrorDetails.ToString()), TraceEventType.Error, ex); - break; - } + return await Command_ExecuteAsync(req, logMessageTag, cancellationToken, bypassPluginExecution).ConfigureAwait(false); } - } - List>> retrieveMultipleResponseList = new List>>(); - foreach (var response in results.Responses) - { - if (response.Response != null) + else { - retrieveMultipleResponseList.Add(CreateResultDataSet(((RetrieveMultipleResponse)response.Response).EntityCollection)); + // use Web API. + return await _connectionSvc.Command_WebAPIProcess_ExecuteAsync(req, logMessageTag, bypassPluginExecution, _metadataUtlity, CallerId, _disableConnectionLocking, MaxRetryCount, RetryPauseTime, cancellationToken).ConfigureAwait(false); } } - return retrieveMultipleResponseList; + else + { + _logEntry.Log("Execute Organization Request failed, Organization Request cannot be null", TraceEventType.Error); + return null; + } } - /// - /// Begins running the Batch command. + /// Executes a Dataverse Create Request and returns the organization response object. + /// Uses an Async pattern to allow for the thread to be backgrounded. /// - /// ID of the batch to run - /// true if the batch begins, false if not. - public ExecuteMultipleResponse ExecuteBatch(Guid batchId) + /// Request to run + /// Formatted Error string + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// Propagates notification that operations should be canceled. + /// Result of create request or null. + internal async Task Command_ExecuteAsync(OrganizationRequest req, string errorStringCheck, System.Threading.CancellationToken cancellationToken, bool bypassPluginExecution = false) { - #region PreChecks - _logEntry.ResetLastError(); - if (DataverseService == null) + if (DataverseServiceAsync != null) { - _logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); - return null; + // if created based on Async Client. + return await Command_ExecuteAsyncImpl(req, errorStringCheck, cancellationToken, bypassPluginExecution).ConfigureAwait(false); } - - if (!IsBatchOperationsAvailable) + else { - _logEntry.Log("Batch Operations are not available", TraceEventType.Error); - return null; + // if not use task.run(). + return await Task.Run(() => Command_Execute(req, errorStringCheck, bypassPluginExecution), cancellationToken).ConfigureAwait(false); } - #endregion - - if (_batchManager != null) - { - var b = _batchManager.GetRequestBatchById(batchId); - if (b.Status == BatchStatus.Complete || b.Status == BatchStatus.Running) - { - _logEntry.Log("Batch is not in the correct state to run", TraceEventType.Error); - return null; - } - - if (!(b.BatchItems.Count > 0)) - { - _logEntry.Log("No Items in the batch", TraceEventType.Error); - return null; - } - // Ready to run the batch. - _logEntry.Log(string.Format(CultureInfo.InvariantCulture, "Executing Batch {0}|{1}, Sending {2} events.", b.BatchId, b.BatchName, b.BatchItems.Count), TraceEventType.Verbose); - ExecuteMultipleRequest req = new ExecuteMultipleRequest(); - req.Settings = b.BatchRequestSettings; - OrganizationRequestCollection reqstList = new OrganizationRequestCollection(); - - // Make sure the batch is ordered. - reqstList.AddRange(b.BatchItems.Select(s => s.Request)); - - req.Requests = reqstList; - b.Status = BatchStatus.Running; - ExecuteMultipleResponse resp = (ExecuteMultipleResponse)Command_Execute(req, "Execute Batch Command"); - // Need to add retry logic here to deal with a "server busy" status. - b.Status = BatchStatus.Complete; - if (resp != null) - { - if (resp.IsFaulted) - _logEntry.Log("Batch request faulted.", TraceEventType.Warning); - b.BatchResults = resp; - return b.BatchResults; - } - _logEntry.Log("Batch request faulted - No Results.", TraceEventType.Warning); - } - return null; } - // Need methods here to work with the batch now, - // get items out by id, - // get batch request. - - - #endregion - /// - /// Uses the dynamic entity patter to create a new entity + /// Executes a Dataverse Create Request and returns the organization response object. /// - /// Name of Entity To create - /// Initial Values - /// Optional: Applies the update with a solution by Unique name - /// Optional: if true, enabled Dataverse onboard duplicate detection - /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// Guid on Success, Guid.Empty on fail - public Guid CreateNewRecord(string entityName, Dictionary valueArray, string applyToSolution = "", bool enabledDuplicateDetection = false, Guid batchId = default(Guid), bool bypassPluginExecution = false) + /// Request to run + /// Formatted Error string + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// + /// Result of create request or null. + internal async Task Command_ExecuteAsyncImpl(OrganizationRequest req, string errorStringCheck, System.Threading.CancellationToken cancellationToken, bool bypassPluginExecution = false) { - _logEntry.ResetLastError(); // Reset Last Error + Guid requestTrackingId = Guid.NewGuid(); + OrganizationResponse resp = null; + Stopwatch logDt = new Stopwatch(); + TimeSpan LockWait = TimeSpan.Zero; + int retryCount = 0; + bool retry = false; - if (DataverseService == null) + do { - _logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); - return Guid.Empty; - } - - if (string.IsNullOrEmpty(entityName)) - return Guid.Empty; + try + { + cancellationToken.ThrowIfCancellationRequested(); + _retryPauseTimeRunning = _configuration.Value.RetryPauseTime; + retry = false; + if (!_disableConnectionLocking) + if (_lockObject == null) + _lockObject = new object(); - if ((valueArray == null) || (valueArray.Count == 0)) - return Guid.Empty; - - - // Create the New Entity Type. - Entity NewEnt = new Entity(); - NewEnt.LogicalName = entityName; - - AttributeCollection propList = new AttributeCollection(); - foreach (KeyValuePair i in valueArray) - { - AddValueToPropertyList(i, propList); - } - - NewEnt.Attributes.AddRange(propList); - - CreateRequest createReq = new CreateRequest(); - createReq.Target = NewEnt; - createReq.Parameters.Add("SuppressDuplicateDetection", !enabledDuplicateDetection); - if (!string.IsNullOrWhiteSpace(applyToSolution)) - createReq.Parameters.Add(Utilities.RequestHeaders.SOLUTIONUNIQUENAME, applyToSolution); - - CreateResponse createResp = null; - - if (AddRequestToBatch(batchId, createReq, entityName, string.Format(CultureInfo.InvariantCulture, "Request for Create on {0} queued", entityName), bypassPluginExecution)) - return Guid.Empty; - - createResp = (CreateResponse)ExecuteOrganizationRequestImpl(createReq, entityName, useWebAPI: true, bypassPluginExecution: bypassPluginExecution); - if (createResp != null) - { - return createResp.id; - } - else - return Guid.Empty; - - } - - /// - /// Generic update entity - /// - /// String version of the entity name - /// Key fieldname of the entity - /// Guid ID of the entity to update - /// Fields to update - /// Optional: Applies the update with a solution by Unique name - /// Optional: if true, enabled Dataverse onboard duplicate detection - /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// true on success, false on fail - public bool UpdateEntity(string entityName, string keyFieldName, Guid id, Dictionary fieldList, string applyToSolution = "", bool enabledDuplicateDetection = false, Guid batchId = default(Guid), bool bypassPluginExecution = false) - { - _logEntry.ResetLastError(); // Reset Last Error - if (DataverseService == null || id == Guid.Empty) - { - return false; - } - - if (fieldList == null || fieldList.Count == 0) - return false; - - Entity uEnt = new Entity(); - uEnt.LogicalName = entityName; - - - AttributeCollection PropertyList = new AttributeCollection(); - - #region MapCode - foreach (KeyValuePair field in fieldList) - { - AddValueToPropertyList(field, PropertyList); - } - - // Add the key... - // check to see if the key is in the import set already - if (!fieldList.ContainsKey(keyFieldName)) - PropertyList.Add(new KeyValuePair(keyFieldName, id)); - - #endregion - - uEnt.Attributes.AddRange(PropertyList.ToArray()); - uEnt.Id = id; - - UpdateRequest req = new UpdateRequest(); - req.Target = uEnt; - - req.Parameters.Add("SuppressDuplicateDetection", !enabledDuplicateDetection); - if (!string.IsNullOrWhiteSpace(applyToSolution)) - req.Parameters.Add("SolutionUniqueName", applyToSolution); - - - if (AddRequestToBatch(batchId, req, string.Format(CultureInfo.InvariantCulture, "Updating {0} : {1}", entityName, id.ToString()), string.Format(CultureInfo.InvariantCulture, "Request for update on {0} queued", entityName), bypassPluginExecution)) - return false; - - UpdateResponse resp = (UpdateResponse)ExecuteOrganizationRequestImpl(req, string.Format(CultureInfo.InvariantCulture, "Updating {0} : {1}", entityName, id.ToString()), useWebAPI: true, bypassPluginExecution: bypassPluginExecution); - if (resp == null) - return false; - else - return true; - } - - - /// - /// Updates the State and Status of the Entity passed in. - /// - /// Name of the entity - /// Guid ID of the entity you are updating - /// String version of the new state - /// String Version of the new status - /// Optional : Batch ID to attach this request too. - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// true on success. - public bool UpdateStateAndStatusForEntity(string entName, Guid id, string stateCode, string statusCode, Guid batchId = default(Guid), bool bypassPluginExecution = false) - { - return UpdateStateStatusForEntity(entName, id, stateCode, statusCode, batchId: batchId, bypassPluginExecution: bypassPluginExecution); - } - - /// - /// Updates the State and Status of the Entity passed in. - /// - /// Name of the entity - /// Guid ID of the entity you are updating - /// Int version of the new state - /// Int Version of the new status - /// Optional : Batch ID to attach this request too. - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// true on success. - public bool UpdateStateAndStatusForEntity(string entName, Guid id, int stateCode, int statusCode, Guid batchId = default(Guid), bool bypassPluginExecution = false) - { - return UpdateStateStatusForEntity(entName, id, string.Empty, string.Empty, stateCode, statusCode, batchId, bypassPluginExecution); - } - - /// - /// Deletes an entity from the Dataverse - /// - /// entity type name - /// entity id - /// Optional : Batch ID to attach this request too. - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// true on success, false on failure - public bool DeleteEntity(string entityType, Guid entityId, Guid batchId = default(Guid), bool bypassPluginExecution = false) - { - _logEntry.ResetLastError(); // Reset Last Error - if (DataverseService == null) - { - _logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); - return false; - } - - DeleteRequest req = new DeleteRequest(); - req.Target = new EntityReference(entityType, entityId); - - if (batchId != Guid.Empty) - { - if (IsBatchOperationsAvailable) - { - if (_batchManager.AddNewRequestToBatch(batchId, req, string.Format(CultureInfo.InvariantCulture, "Trying to Delete. Entity = {0}, ID = {1}", entityType, entityId))) - { - _logEntry.Log(string.Format(CultureInfo.InvariantCulture, "Request for Delete on {0} queued", entityType), TraceEventType.Verbose); - return false; - } - else - _logEntry.Log("Unable to add request to batch queue, Executing normally", TraceEventType.Warning); - } - else - { - // Error and fall though. - _logEntry.Log("Unable to add request to batch, Batching is not currently available, Executing normally", TraceEventType.Warning); - } - } - - if (batchId != Guid.Empty) - { - if (IsBatchOperationsAvailable) - { - if (_batchManager.AddNewRequestToBatch(batchId, req, string.Format(CultureInfo.InvariantCulture, "Delete Entity = {0}, ID = {1} queued", entityType, entityId))) - { - _logEntry.Log(string.Format(CultureInfo.InvariantCulture, "Request for Delete. Entity = {0}, ID = {1} queued", entityType, entityId), TraceEventType.Verbose); - return false; - } - else - _logEntry.Log("Unable to add request to batch queue, Executing normally", TraceEventType.Warning); - } - else - { - // Error and fall though. - _logEntry.Log("Unable to add request to batch, Batching is not currently available, Executing normally", TraceEventType.Warning); - } - } - - if (AddRequestToBatch(batchId, req, string.Format(CultureInfo.InvariantCulture, "Trying to Delete. Entity = {0}, ID = {1}", entityType, entityId), string.Format(CultureInfo.InvariantCulture, "Request to Delete. Entity = {0}, ID = {1} Queued", entityType, entityId), bypassPluginExecution)) - return false; - - DeleteResponse resp = (DeleteResponse)ExecuteOrganizationRequestImpl(req, string.Format(CultureInfo.InvariantCulture, "Trying to Delete. Entity = {0}, ID = {1}", entityType, entityId), useWebAPI: true, bypassPluginExecution: bypassPluginExecution); - if (resp != null) - { - // Clean out the cache if the account happens to be stored in there. - if ((_CachObject != null) && (_CachObject.ContainsKey(entityType))) - { - while (_CachObject[entityType].ContainsValue(entityId)) - { - foreach (KeyValuePair v in _CachObject[entityType].Values) - { - if (v.Value == entityId) - { - _CachObject[entityType].Remove(v.Key); - break; - } - } - } - } - return true; - } - return false; - } - - /// - /// Gets a list of accounts based on the search parameters. - /// - /// Dataverse Entity Type Name to search - /// Array of Search Parameters - /// List of fields to retrieve, Null indicates all Fields - /// Logical Search Operator - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately - /// List of matching Entity Types. - - [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope = "member")] - public Dictionary> GetEntityDataBySearchParams(string entityName, - Dictionary searchParameters, - LogicalSearchOperator searchOperator, - List fieldList, - Guid batchId = default(Guid), - bool bypassPluginExecution = false) - { - List searchList = new List(); - BuildSearchFilterListFromSearchTerms(searchParameters, searchList); - - string pgCookie = string.Empty; - bool moreRec = false; - return GetEntityDataBySearchParams(entityName, searchList, searchOperator, fieldList, null, -1, -1, string.Empty, out pgCookie, out moreRec, batchId, bypassPluginExecution: bypassPluginExecution); - } - - - /// - /// Gets a list of accounts based on the search parameters. - /// - /// Dataverse Entity Type Name to search - /// Array of Search Parameters - /// List of fields to retrieve, Null indicates all Fields - /// Logical Search Operator - /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// List of matching Entity Types. - [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope = "member")] - public Dictionary> GetEntityDataBySearchParams(string entityName, - List searchParameters, - LogicalSearchOperator searchOperator, - List fieldList, Guid batchId = default(Guid), - bool bypassPluginExecution = false) - { - string pgCookie = string.Empty; - bool moreRec = false; - return GetEntityDataBySearchParams(entityName, searchParameters, searchOperator, fieldList, null, -1, -1, string.Empty, out pgCookie, out moreRec, batchId, bypassPluginExecution); - } - - /// - /// Searches for data from an entity based on the search parameters. - /// - /// Name of the entity to search - /// Array of Search Parameters - /// List of fields to retrieve, Null indicates all Fields - /// Logical Search Operator - /// Number records per Page - /// Current Page number - /// inbound place holder cookie - /// outbound place holder cookie - /// is there more records or not - /// Sort order - /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// List of matching Entity Types. - [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope = "member")] - public Dictionary> GetEntityDataBySearchParams(string entityName, - List searchParameters, - LogicalSearchOperator searchOperator, - List fieldList, - Dictionary sortParameters, - int pageCount, - int pageNumber, - string pageCookie, - out string outPageCookie, - out bool isMoreRecords, - Guid batchId = default(Guid), - bool bypassPluginExecution = false - ) - { - _logEntry.ResetLastError(); // Reset Last Error - - outPageCookie = string.Empty; - isMoreRecords = false; - - if (DataverseService == null) - { - _logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); - return null; - } - - if (searchParameters == null) - searchParameters = new List(); - - // Build the query here. - QueryExpression query = BuildQueryFilter(entityName, searchParameters, fieldList, searchOperator); - - if (pageCount != -1) - { - PagingInfo pgInfo = new PagingInfo(); - pgInfo.Count = pageCount; - pgInfo.PageNumber = pageNumber; - pgInfo.PagingCookie = pageCookie; - query.PageInfo = pgInfo; - } - - if (sortParameters != null) - if (sortParameters.Count > 0) - { - List qExpressList = new List(); - foreach (KeyValuePair itm in sortParameters) - { - OrderExpression ordBy = new OrderExpression(); - ordBy.AttributeName = itm.Key; - if (itm.Value == LogicalSortOrder.Ascending) - ordBy.OrderType = OrderType.Ascending; - else - ordBy.OrderType = OrderType.Descending; - - qExpressList.Add(ordBy); - } - - query.Orders.AddRange(qExpressList.ToArray()); - } - - - RetrieveMultipleRequest retrieve = new RetrieveMultipleRequest(); - //retrieve.ReturnDynamicEntities = true; - retrieve.Query = query; - - - if (AddRequestToBatch(batchId, retrieve, "Running GetEntityDataBySearchParms", "Request For GetEntityDataBySearchParms Queued", bypassPluginExecution)) - return null; - - - RetrieveMultipleResponse retrieved; - retrieved = (RetrieveMultipleResponse)Command_Execute(retrieve, "GetEntityDataBySearchParms", bypassPluginExecution); - if (retrieved != null) - { - outPageCookie = retrieved.EntityCollection.PagingCookie; - isMoreRecords = retrieved.EntityCollection.MoreRecords; - - return CreateResultDataSet(retrieved.EntityCollection); - } - else - return null; - } - - - /// - /// Searches for data based on a FetchXML query - /// - /// Fetch XML query data. - /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// results or null - public Dictionary> GetEntityDataByFetchSearch(string fetchXml, Guid batchId = default(Guid), bool bypassPluginExecution = false) - { - EntityCollection ec = GetEntityDataByFetchSearchEC(fetchXml, batchId, bypassPluginExecution); - if (ec != null) - return CreateResultDataSet(ec); - else - return null; - } - - - /// - /// Searches for data based on a FetchXML query - /// - /// Fetch XML query data. - /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// results as an entity collection or null - public EntityCollection GetEntityDataByFetchSearchEC(string fetchXml, Guid batchId = default(Guid), bool bypassPluginExecution = false) - { - _logEntry.ResetLastError(); // Reset Last Error - - if (DataverseService == null) - { - _logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); - return null; - } - - if (string.IsNullOrWhiteSpace(fetchXml)) - return null; - - // This model directly requests the via FetchXML - RetrieveMultipleRequest req = new RetrieveMultipleRequest() { Query = new FetchExpression(fetchXml) }; - RetrieveMultipleResponse retrieved; - - if (AddRequestToBatch(batchId, req, "Running GetEntityDataByFetchSearchEC", "Request For GetEntityDataByFetchSearchEC Queued", bypassPluginExecution)) - return null; - - retrieved = (RetrieveMultipleResponse)Command_Execute(req, "GetEntityDataByFetchSearch - Direct", bypassPluginExecution); - if (retrieved != null) - { - return retrieved.EntityCollection; - } - else - return null; - } - - /// - /// Searches for data based on a FetchXML query - /// - /// Fetch XML query data. - /// Number records per Page - /// Current Page number - /// inbound place holder cookie - /// outbound place holder cookie - /// is there more records or not - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately - /// results or null - public Dictionary> GetEntityDataByFetchSearch( - string fetchXml, - int pageCount, - int pageNumber, - string pageCookie, - out string outPageCookie, - out bool isMoreRecords, - Guid batchId = default(Guid), - bool bypassPluginExecution = false) - { - EntityCollection ec = GetEntityDataByFetchSearchEC(fetchXml, pageCount, pageNumber, pageCookie, out outPageCookie, out isMoreRecords, bypassPluginExecution: bypassPluginExecution); - if (ec != null) - return CreateResultDataSet(ec); - else - return null; - } - - /// - /// Searches for data based on a FetchXML query - /// - /// Fetch XML query data. - /// Number records per Page - /// Current Page number - /// inbound place holder cookie - /// outbound place holder cookie - /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// is there more records or not - /// results as an Entity Collection or null - public EntityCollection GetEntityDataByFetchSearchEC( - string fetchXml, - int pageCount, - int pageNumber, - string pageCookie, - out string outPageCookie, - out bool isMoreRecords, - Guid batchId = default(Guid), - bool bypassPluginExecution = false) - { - - _logEntry.ResetLastError(); // Reset Last Error - - outPageCookie = string.Empty; - isMoreRecords = false; - - if (DataverseService == null) - { - _logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); - return null; - } - - if (string.IsNullOrWhiteSpace(fetchXml)) - return null; - - if (pageCount != -1) - { - // Add paging related parameter to fetch xml. - fetchXml = AddPagingParametersToFetchXml(fetchXml, pageCount, pageNumber, pageCookie); - } - - RetrieveMultipleRequest retrieve = new RetrieveMultipleRequest() { Query = new FetchExpression(fetchXml) }; - RetrieveMultipleResponse retrieved; - - if (AddRequestToBatch(batchId, retrieve, "Running GetEntityDataByFetchSearchEC", "Request For GetEntityDataByFetchSearchEC Queued", bypassPluginExecution)) - return null; - - retrieved = (RetrieveMultipleResponse)Command_Execute(retrieve, "GetEntityDataByFetchSearch", bypassPluginExecution); - if (retrieved != null) - { - outPageCookie = retrieved.EntityCollection.PagingCookie; - isMoreRecords = retrieved.EntityCollection.MoreRecords; - return retrieved.EntityCollection; - } - - return null; - } - - - /// - /// Queries an Object via a M to M Link - /// - /// Name of the entity you want return data from - /// Search Prams for the Return Entity - /// Name of the entity you are linking too - /// Search Prams for the Entity you are linking too - /// Key field on the Entity you are linking too - /// Dataverse Name of the Relationship - /// Key field on the Entity you want to return data from - /// Search Operator to apply - /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// List of Fields from the Returned Entity you want - /// - public Dictionary> GetEntityDataByLinkedSearch( - string returnEntityName, - Dictionary primarySearchParameters, - string linkedEntityName, - Dictionary linkedSearchParameters, - string linkedEntityLinkAttribName, - string m2MEntityName, - string returnEntityPrimaryId, - LogicalSearchOperator searchOperator, - List fieldList, - Guid batchId = default(Guid), - bool bypassPluginExecution = false) - { - List primarySearchList = new List(); - BuildSearchFilterListFromSearchTerms(primarySearchParameters, primarySearchList); - - List linkedSearchList = new List(); - BuildSearchFilterListFromSearchTerms(linkedSearchParameters, linkedSearchList); - - return GetEntityDataByLinkedSearch(returnEntityName, primarySearchList, linkedEntityName, linkedSearchList, linkedEntityLinkAttribName, - m2MEntityName, returnEntityPrimaryId, searchOperator, fieldList, bypassPluginExecution: bypassPluginExecution); - - } - - /// - /// Queries an Object via a M to M Link - /// - /// Name of the entity you want return data from - /// Search Prams for the Return Entity - /// Name of the entity you are linking too - /// Search Prams for the Entity you are linking too - /// Key field on the Entity you are linking too - /// Dataverse Name of the Relationship - /// Key field on the Entity you want to return data from - /// Search Operator to apply - /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately - /// List of Fields from the Returned Entity you want - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// If the relationship is defined as Entity:Entity or Account N:N Account, this parameter should be set to true - /// - public Dictionary> GetEntityDataByLinkedSearch( - string returnEntityName, - List /*Dictionary*/ primarySearchParameters, - string linkedEntityName, - List /*Dictionary*/ linkedSearchParameters, - string linkedEntityLinkAttribName, - string m2MEntityName, - string returnEntityPrimaryId, - LogicalSearchOperator searchOperator, - List fieldList, - Guid batchId = default(Guid), - bool isReflexiveRelationship = false, - bool bypassPluginExecution = false) - { - _logEntry.ResetLastError(); // Reset Last Error - if (DataverseService == null) - { - _logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); - return null; - } - - if (primarySearchParameters == null && linkedSearchParameters == null) - return null; - - if (primarySearchParameters == null) - primarySearchParameters = new List(); // new Dictionary(); - - if (linkedSearchParameters == null) - linkedSearchParameters = new List(); //new Dictionary(); - - - - #region Primary QueryFilter and Conditions - - FilterExpression primaryFilter = new FilterExpression(); - primaryFilter.Filters.AddRange(BuildFilterList(primarySearchParameters)); - - #endregion - - #region Secondary QueryFilter and conditions - - FilterExpression linkedEntityFilter = new FilterExpression(); - linkedEntityFilter.Filters.AddRange(BuildFilterList(linkedSearchParameters)); - - #endregion - - // Create Link Object for LinkedEnitty Name and add the filter info - LinkEntity nestedLinkEntity = new LinkEntity(); // this is the Secondary - nestedLinkEntity.LinkToEntityName = linkedEntityName; // what Entity are we linking too... - nestedLinkEntity.LinkToAttributeName = linkedEntityLinkAttribName; // what Attrib are we linking To on that Entity - nestedLinkEntity.LinkFromAttributeName = isReflexiveRelationship ? string.Format("{0}two", linkedEntityLinkAttribName) : linkedEntityLinkAttribName; // what Attrib on the primary object are we linking too. - nestedLinkEntity.LinkCriteria = linkedEntityFilter; // Filtered query - - //Create Link Object for Primary - LinkEntity m2mLinkEntity = new LinkEntity(); - m2mLinkEntity.LinkToEntityName = m2MEntityName; // this is the M2M table - m2mLinkEntity.LinkToAttributeName = isReflexiveRelationship ? string.Format("{0}one", returnEntityPrimaryId) : returnEntityPrimaryId; // this is the name of the other side. - m2mLinkEntity.LinkFromAttributeName = returnEntityPrimaryId; - m2mLinkEntity.LinkEntities.AddRange(new LinkEntity[] { nestedLinkEntity }); - - - // Return Cols - // Create ColumnSet - ColumnSet cols = null; - if (fieldList != null && fieldList.Count > 0) - { - cols = new ColumnSet(); - cols.Columns.AddRange(fieldList.ToArray()); - } - - // Build Query - QueryExpression query = new QueryExpression(); - query.NoLock = false; // Added to remove the Locks. - - query.EntityName = returnEntityName; // Set to the requested entity Type - if (cols != null) - query.ColumnSet = cols; - else - query.ColumnSet = new ColumnSet(true);// new AllColumns(); - - query.Criteria = primaryFilter; - query.LinkEntities.AddRange(new LinkEntity[] { m2mLinkEntity }); - - //Dictionary> Results = new Dictionary>(); - - - RetrieveMultipleRequest req = new RetrieveMultipleRequest(); - req.Query = query; - RetrieveMultipleResponse retrieved; - - - if (AddRequestToBatch(batchId, req, string.Format(CultureInfo.InvariantCulture, "Running Get Linked data, returning {0}", returnEntityName), string.Format(CultureInfo.InvariantCulture, "Request for Get Linked data, returning {0}", returnEntityName), bypassPluginExecution)) - return null; - - retrieved = (RetrieveMultipleResponse)Command_Execute(req, "Search On Linked Data", bypassPluginExecution); - - if (retrieved != null) - { - - return CreateResultDataSet(retrieved.EntityCollection); - } - else - return null; - - } - - /// - /// Gets a List of variables from the account based on the list of field specified in the Fields List - /// - /// The entity to be searched. - /// ID of Entity to query - /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// Populated Array of Key value pairs with the Results of the Search - /// - public Dictionary GetEntityDataById(string searchEntity, Guid entityId, List fieldList, Guid batchId = default(Guid), bool bypassPluginExecution = false) - { - _logEntry.ResetLastError(); // Reset Last Error - if (DataverseService == null || entityId == Guid.Empty) - { - return null; - } - - EntityReference re = new EntityReference(searchEntity, entityId); - if (re == null) - return null; - - RetrieveRequest req = new RetrieveRequest(); - - // Create ColumnSet - ColumnSet cols = null; - if (fieldList != null) - { - cols = new ColumnSet(); - cols.Columns.AddRange(fieldList.ToArray()); - } - - if (cols != null) - req.ColumnSet = cols; - else - req.ColumnSet = new ColumnSet(true);// new AllColumns(); - - req.Target = re; //getEnt; - - if (AddRequestToBatch(batchId, req, string.Format(CultureInfo.InvariantCulture, "Trying to Read a Record. Entity = {0} , ID = {1}", searchEntity, entityId.ToString()), - string.Format(CultureInfo.InvariantCulture, "Request to Read a Record. Entity = {0} , ID = {1} queued", searchEntity, entityId.ToString()), bypassPluginExecution)) - return null; - - RetrieveResponse resp = (RetrieveResponse)Command_Execute(req, string.Format(CultureInfo.InvariantCulture, "Trying to Read a Record. Entity = {0} , ID = {1}", searchEntity, entityId.ToString()), bypassPluginExecution); - if (resp == null) - return null; - - if (resp.Entity == null) - return null; - - try - { - // Not really doing an update here... just turning it into something I can walk. - Dictionary resultSet = new Dictionary(); - AddDataToResultSet(ref resultSet, resp.Entity); - return resultSet; - } - catch - { - return null; - } - } - - /// - /// This creates a annotation [note] entry, related to a an existing entity - /// Required Properties in the fieldList - /// notetext (string) = Text of the note, - /// subject (string) = this is the title of the note - /// - /// Target Entity TypeID - /// Target Entity ID - /// Fields to populate - /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// - public Guid CreateAnnotation(string targetEntityTypeName, Guid targetEntityId, Dictionary fieldList, Guid batchId = default(Guid), bool bypassPluginExecution = false) - { - _logEntry.ResetLastError(); // Reset Last Error - - if (string.IsNullOrEmpty(targetEntityTypeName)) - return Guid.Empty; - - if (targetEntityId == Guid.Empty) - return Guid.Empty; - - if (fieldList == null) - fieldList = new Dictionary(); - - fieldList.Add("objecttypecode", new DataverseDataTypeWrapper(targetEntityTypeName, DataverseFieldType.String)); - fieldList.Add("objectid", new DataverseDataTypeWrapper(targetEntityId, DataverseFieldType.Lookup, targetEntityTypeName)); - fieldList.Add("ownerid", new DataverseDataTypeWrapper(SystemUser.UserId, DataverseFieldType.Lookup, "systemuser")); - - return CreateNewRecord("annotation", fieldList, batchId: batchId, bypassPluginExecution: bypassPluginExecution); - - } - - /// - /// Creates a new activity against the target entity type - /// - /// Type of Activity you would like to create - /// Entity type of the Entity you want to associate with. - /// Subject Line of the Activity - /// Description Text of the Activity - /// ID of the Entity to associate the Activity too - /// User ID that Created the Activity *Calling user must have necessary permissions to assign to another user - /// Additional fields to add as part of the activity creation - /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// Guid of Activity ID or Guid.empty - public Guid CreateNewActivityEntry(string activityEntityTypeName, - string regardingEntityTypeName, - Guid regardingId, - string subject, - string description, - string creatingUserId, - Dictionary fieldList = null, - Guid batchId = default(Guid), - bool bypassPluginExecution = false - ) - { - - #region PreChecks - _logEntry.ResetLastError(); // Reset Last Error - if (DataverseService == null) - { - _logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); - return Guid.Empty; - } - if (string.IsNullOrWhiteSpace(activityEntityTypeName)) - { - _logEntry.Log("You must specify the activity type name to create", TraceEventType.Error); - return Guid.Empty; - } - if (string.IsNullOrWhiteSpace(subject)) - { - _logEntry.Log(string.Format(CultureInfo.InvariantCulture, "A Subject is required to create an activity of type {0}", regardingEntityTypeName), TraceEventType.Error); - return Guid.Empty; - } - #endregion - - Guid activityId = Guid.Empty; - try - { - // reuse the passed in field list if its available, else punt and create a new one. - if (fieldList == null) - fieldList = new Dictionary(); - - fieldList.Add("subject", new DataverseDataTypeWrapper(subject, DataverseFieldType.String)); - if (regardingId != Guid.Empty) - fieldList.Add("regardingobjectid", new DataverseDataTypeWrapper(regardingId, DataverseFieldType.Lookup, regardingEntityTypeName)); - if (!string.IsNullOrWhiteSpace(description)) - fieldList.Add("description", new DataverseDataTypeWrapper(description, DataverseFieldType.String)); - - // Create the base record. - activityId = CreateNewRecord(activityEntityTypeName, fieldList, bypassPluginExecution: bypassPluginExecution); - - // if I have a user ID, try to assign it to that user. - if (!string.IsNullOrWhiteSpace(creatingUserId)) - { - Guid userId = GetLookupValueForEntity("systemuser", creatingUserId); - - if (userId != Guid.Empty) - { - EntityReference newAction = new EntityReference(activityEntityTypeName, activityId); - EntityReference principal = new EntityReference("systemuser", userId); - - AssignRequest arRequest = new AssignRequest(); - arRequest.Assignee = principal; - arRequest.Target = newAction; - if (AddRequestToBatch(batchId, arRequest, string.Format(CultureInfo.InvariantCulture, "Trying to Assign a Record. Entity = {0} , ID = {1}", newAction.LogicalName, principal.LogicalName), - string.Format(CultureInfo.InvariantCulture, "Request to Assign a Record. Entity = {0} , ID = {1} Queued", newAction.LogicalName, principal.LogicalName), bypassPluginExecution)) - return Guid.Empty; - Command_Execute(arRequest, "Assign Activity", bypassPluginExecution); - } - } - } - catch (Exception exp) - { - this._logEntry.Log(exp); - } - return activityId; - } - - /// - /// Closes the Activity type specified. - /// The Activity Entity type supports fax , letter , and phonecall - /// *Note: This will default to using English names for Status. if you need to use Non-English, you should populate the names for completed for the status and state. - /// - /// Type of Activity you would like to close.. Supports fax, letter, phonecall - /// ID of the Activity you want to close - /// State Code configured on the activity - /// Status code on the activity - /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// true if success false if not. - public bool CloseActivity(string activityEntityType, Guid activityId, string stateCode = "completed", string statusCode = "completed", Guid batchId = default(Guid), bool bypassPluginExecution = false) - { - return UpdateStateStatusForEntity(activityEntityType, activityId, stateCode, statusCode, batchId: batchId, bypassPluginExecution: bypassPluginExecution); - } - - /// - /// Updates the state of an activity - /// - /// - /// - /// - /// - /// ID for the new State ( Skips metadata lookup ) - /// ID for new Status ( Skips Metadata Lookup) - /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// - private bool UpdateStateStatusForEntity(string entName, Guid entId, string newState, string newStatus, int newStateid = -1, int newStatusid = -1, Guid batchId = default(Guid), bool bypassPluginExecution = false) - { - _logEntry.ResetLastError(); // Reset Last Error - SetStateRequest req = new SetStateRequest(); - req.EntityMoniker = new EntityReference(entName, entId); - - int istatuscode = -1; - int istatecode = -1; - - // Modified to prefer IntID's first... this is in support of multi languages. - - if (newStatusid != -1) - istatuscode = newStatusid; - else - { - if (!String.IsNullOrWhiteSpace(newStatus)) - { - PickListMetaElement picItem = GetPickListElementFromMetadataEntity(entName, "statuscode"); - if (picItem != null) - { - var statusOption = picItem.Items.FirstOrDefault(s => s.DisplayLabel.Equals(newStatus, StringComparison.CurrentCultureIgnoreCase)); - if (statusOption != null) - istatuscode = statusOption.PickListItemId; - } - } - } - - if (newStateid != -1) - istatecode = newStateid; - else - { - if (!string.IsNullOrWhiteSpace(newState)) - { - PickListMetaElement picItem2 = GetPickListElementFromMetadataEntity(entName, "statecode"); - var stateOption = picItem2.Items.FirstOrDefault(s => s.DisplayLabel.Equals(newState, StringComparison.CurrentCultureIgnoreCase)); - if (stateOption != null) - istatecode = stateOption.PickListItemId; - } - } - - if (istatecode == -1 && istatuscode == -1) - { - _logEntry.Log(string.Format(CultureInfo.InvariantCulture, "Cannot set status on {0}, State and Status codes not found, State = {1}, Status = {2}", entName, newState, newStatus), TraceEventType.Information); - return false; - } - - if (istatecode != -1) - req.State = new OptionSetValue(istatecode);// "Completed"; - if (istatuscode != -1) - req.Status = new OptionSetValue(istatuscode); //Status = 2; - - - if (AddRequestToBatch(batchId, req, string.Format(CultureInfo.InvariantCulture, "Setting Activity State in Dataverse... {0}", entName), string.Format(CultureInfo.InvariantCulture, "Request for SetState on {0} queued", entName), bypassPluginExecution)) - return false; - - SetStateResponse resp = (SetStateResponse)Command_Execute(req, string.Format(CultureInfo.InvariantCulture, "Setting Activity State in Dataverse... {0}", entName), bypassPluginExecution); - if (resp != null) - return true; - else - return false; - } - - /// - /// Returns all Activities Related to a given Entity ID. - /// Only Account, Contact and Opportunity entities are supported. - /// - /// Type of Entity to search against - /// ID of the entity to search against. - /// List of Field to return for the entity , null indicates all fields. - /// Search Operator to use - /// Filters responses based on search prams. - /// Sort order - /// Number of Pages - /// Current Page number - /// inbound place holder cookie - /// outbound place holder cookie - /// is there more records or not - /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately - /// Array of Activities - public Dictionary> GetActivitiesBy( - string searchEntity, - Guid entityId, - List fieldList, - LogicalSearchOperator searchOperator, - Dictionary searchParameters, - Dictionary sortParameters, - int pageCount, - int pageNumber, - string pageCookie, - out string outPageCookie, - out bool isMoreRecords, - Guid batchId = default(Guid) - ) - { - List searchList = new List(); - BuildSearchFilterListFromSearchTerms(searchParameters, searchList); - - return GetEntityDataByRollup(searchEntity, entityId, "activitypointer", fieldList, searchOperator, searchList, sortParameters, pageCount, pageNumber, pageCookie, out outPageCookie, out isMoreRecords, batchId); - } - - /// - /// Returns all Activities Related to a given Entity ID. - /// Only Account, Contact and Opportunity entities are supported. - /// - /// Type of Entity to search against - /// ID of the entity to search against. - /// List of Field to return for the entity , null indicates all fields. - /// Search Operator to use - /// Filters responses based on search prams. - /// Sort order - /// Number of Pages - /// Current Page number - /// inbound place holder cookie - /// outbound place holder cookie - /// is there more records or not - /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately - /// Array of Activities - public Dictionary> GetActivitiesBy( - string searchEntity, - Guid entityId, - List fieldList, - LogicalSearchOperator searchOperator, - List searchParameters, - Dictionary sortParameters, - int pageCount, - int pageNumber, - string pageCookie, - out string outPageCookie, - out bool isMoreRecords, - Guid batchId = default(Guid) - ) - { - return GetEntityDataByRollup(searchEntity, entityId, "activitypointer", fieldList, searchOperator, searchParameters, sortParameters, pageCount, pageNumber, pageCookie, out outPageCookie, out isMoreRecords, batchId: batchId); - } - - /// - /// Returns all Activities Related to a given Entity ID. - /// Only Account, Contact and Opportunity entities are supported. - /// - /// Type of Entity to search against - /// ID of the entity to search against. - /// List of Field to return for the entity , null indicates all fields. - /// - /// Filters responses based on search prams. - /// Array of Activities - /// Sort Order - /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately - /// Entity to Rollup from - public Dictionary> GetEntityDataByRollup( - string searchEntity, - Guid entityId, - string rollupfromEntity, - List fieldList, - LogicalSearchOperator searchOperator, - Dictionary searchParameters, - Dictionary sortParameters, - Guid batchId = default(Guid)) - { - - List searchList = new List(); - BuildSearchFilterListFromSearchTerms(searchParameters, searchList); - - - string pgCookie = string.Empty; - bool moreRec = false; - - return GetEntityDataByRollup( - searchEntity, entityId, rollupfromEntity, fieldList, - searchOperator, searchList, sortParameters, -1, -1, string.Empty, - out pgCookie, out moreRec, batchId: batchId); - } - - - /// - /// Returns all Activities Related to a given Entity ID. - /// Only Account, Contact and Opportunity entities are supported. - /// - /// Type of Entity to search against - /// ID of the entity to search against. - /// List of Field to return for the entity , null indicates all fields. - /// Entity to Rollup from - /// Search Operator to user - /// Dataverse Filter list to apply - /// Sort by - /// Number of Pages - /// Current Page number - /// inbound place holder cookie - /// outbound place holder cookie - /// is there more records or not - /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// - public Dictionary> GetEntityDataByRollup( - string searchEntity, - Guid entityId, - string rollupfromEntity, - List fieldList, - LogicalSearchOperator searchOperator, - List searchParameters, - Dictionary sortParameters, - int pageCount, - int pageNumber, - string pageCookie, - out string outPageCookie, - out bool isMoreRecords, - Guid batchId = default(Guid), - bool bypassPluginExecution = false - ) - { - _logEntry.ResetLastError(); // Reset Last Error - outPageCookie = string.Empty; - isMoreRecords = false; - - if (DataverseService == null) - { - _logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); - return null; - } - - QueryExpression query = BuildQueryFilter(rollupfromEntity, searchParameters, fieldList, searchOperator); - - if (pageCount != -1) - { - PagingInfo pgInfo = new PagingInfo(); - pgInfo.Count = pageCount; - pgInfo.PageNumber = pageNumber; - pgInfo.PagingCookie = pageCookie; - query.PageInfo = pgInfo; - - } - - if (sortParameters != null) - if (sortParameters.Count > 0) - { - List qExpressList = new List(); - foreach (KeyValuePair itm in sortParameters) - { - OrderExpression ordBy = new OrderExpression(); - ordBy.AttributeName = itm.Key; - if (itm.Value == LogicalSortOrder.Ascending) - ordBy.OrderType = OrderType.Ascending; - else - ordBy.OrderType = OrderType.Descending; - - qExpressList.Add(ordBy); - } - - query.Orders.AddRange(qExpressList.ToArray()); - } - - if (query.Orders == null) - { - OrderExpression ordBy = new OrderExpression(); - ordBy.AttributeName = "createdon"; - ordBy.OrderType = OrderType.Descending; - query.Orders.AddRange(new OrderExpression[] { ordBy }); - } - - EntityReference ro = new EntityReference(searchEntity, entityId); - if (ro == null) - return null; - - RollupRequest req = new RollupRequest(); - req.Query = query; - req.RollupType = RollupType.Related; - req.Target = ro; - - if (AddRequestToBatch(batchId, req, string.Format(CultureInfo.InvariantCulture, "Running Get entitydatabyrollup... {0}", searchEntity), string.Format(CultureInfo.InvariantCulture, "Request for GetEntityDataByRollup on {0} queued", searchEntity), bypassPluginExecution)) - return null; - - RollupResponse resp = (RollupResponse)Command_Execute(req, string.Format(CultureInfo.InvariantCulture, "Locating {0} by ID in Dataverse GetActivitesBy", searchEntity), bypassPluginExecution); - if (resp == null) - return null; - - if ((resp.EntityCollection != null) || - (resp.EntityCollection.Entities != null) || - (resp.EntityCollection.Entities.Count > 0) - ) - { - isMoreRecords = resp.EntityCollection.MoreRecords; - outPageCookie = resp.EntityCollection.PagingCookie; - return CreateResultDataSet(resp.EntityCollection); - } - else - return null; - } - - /// - /// This function gets data from a Dictionary object, where "string" identifies the field name, and Object contains the data, - /// this method then attempts to cast the result to the Type requested, if it cannot be cast an empty object is returned. - /// - /// Results from the query - /// key name you want - /// Type if object to return - /// object - public T GetDataByKeyFromResultsSet(Dictionary results, string key) - { - try - { - if (results != null) - { - if (results.ContainsKey(key)) - { - - if ((typeof(T) == typeof(int)) || (typeof(T) == typeof(string))) - { - try - { - string s = (string)results[key]; - if (s.Contains("PICKLIST:")) - { - try - { - //parse the PickList bit for what is asked for - Collection eleList = new Collection(s.Split(':')); - if (typeof(T) == typeof(int)) - { - return (T)(object)Convert.ToInt32(eleList[1], CultureInfo.InvariantCulture); - } - else - return (T)(object)eleList[3]; - } - catch - { - // try to do the basic return - return (T)results[key]; - } - } - } - catch - { - if (results[key] is T) - // try to do the basic return - return (T)results[key]; - } - } - - // MSB :: Added this method in light of new features in CDS 2011.. - if (results[key] is T) - // try to do the basic return - return (T)results[key]; - else - { - if (results != null && results.ContainsKey(key)) // Specific To CDS 2011.. - { - if (results.ContainsKey(key + "_Property")) - { - // Check for the property entry - CDS 2011 Specific - KeyValuePair property = (KeyValuePair)results[key + "_Property"]; - // try to return the casted value. - if (property.Value is T) - return (T)property.Value; - } - } - } - } - } - } - catch (Exception ex) - { - _logEntry.Log("Error In GetDataByKeyFromResultsSet (Non-Fatal)", TraceEventType.Verbose, ex); - } - return default(T); - - } - - /// - /// Executes a named workflow on an object. - /// - /// name of the workflow to run - /// ID to exec against - /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// Async Op ID of the WF or Guid.Empty - public Guid ExecuteWorkflowOnEntity(string workflowName, Guid id, Guid batchId = default(Guid), bool bypassPluginExecution = false) - { - _logEntry.ResetLastError(); // Reset Last Error - if (DataverseService == null) - { - _logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); - return Guid.Empty; - } - - if (id == Guid.Empty) - { - this._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception Executing workflow ({0}) on ID {1} in Dataverse : " + "Target Entity Was not provided", workflowName, id), TraceEventType.Error); - return Guid.Empty; - } - - if (string.IsNullOrEmpty(workflowName)) - { - this._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception Executing workflow ({0}) on ID {1} in Dataverse : " + "Workflow Name Was not provided", workflowName, id), TraceEventType.Error); - return Guid.Empty; - } - - Dictionary SearchParm = new Dictionary(); - SearchParm.Add("name", workflowName); - - Dictionary> rslts = - GetEntityDataBySearchParams("workflow", SearchParm, LogicalSearchOperator.None, null, bypassPluginExecution: bypassPluginExecution); - - if (rslts != null) - { - if (rslts.Count > 0) - { - foreach (Dictionary row in rslts.Values) - { - if (GetDataByKeyFromResultsSet(row, "parentworkflowid") != Guid.Empty) - continue; - Guid guWorkflowID = GetDataByKeyFromResultsSet(row, "workflowid"); - if (guWorkflowID != Guid.Empty) - { - // Ok try to exec the workflow request - ExecuteWorkflowRequest wfRequest = new ExecuteWorkflowRequest(); - wfRequest.EntityId = id; - wfRequest.WorkflowId = guWorkflowID; - - if (AddRequestToBatch(batchId, wfRequest, string.Format(CultureInfo.InvariantCulture, "Executing workflow ({0}) on ID {1}", workflowName, id), - string.Format(CultureInfo.InvariantCulture, "Request to Execute workflow ({0}) on ID {1} Queued", workflowName, id), bypassPluginExecution)) - return Guid.Empty; - - ExecuteWorkflowResponse wfResponse = (ExecuteWorkflowResponse)Command_Execute(wfRequest, string.Format(CultureInfo.InvariantCulture, "Executing workflow ({0}) on ID {1}", workflowName, id), bypassPluginExecution); - if (wfResponse != null) - return wfResponse.Id; - else - return Guid.Empty; - } - else - { - this._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception Executing workflow ({0}) on ID {1} in Dataverse : " + "Unable to Find Workflow by ID", workflowName, id), TraceEventType.Error); - } - } - } - else - { - this._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception Executing workflow ({0}) on ID {1} in Dataverse : " + "Unable to Find Workflow by Name", workflowName, id), TraceEventType.Error); - } - } - this._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception Executing workflow ({0}) on ID {1} in Dataverse : " + "Unable to Find Workflow by Name Search", workflowName, id), TraceEventType.Error); - return Guid.Empty; - } - - #region Solution and Data Import Methods - /// - /// Starts an Import request for CDS. - /// Supports a single file per Import request. - /// - /// Delays the import jobs till specified time - Use DateTime.MinValue to Run immediately - /// Import Data Request - /// Guid of the Import Request, or Guid.Empty. If Guid.Empty then request failed. - public Guid SubmitImportRequest(ImportRequest importRequest, DateTime delayUntil) - { - _logEntry.ResetLastError(); // Reset Last Error - if (DataverseService == null) - { - _logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); - return Guid.Empty; - } - - // Error checking - if (importRequest == null) - { - this._logEntry.Log("************ Exception on SubmitImportRequest, importRequest is required", TraceEventType.Error); - return Guid.Empty; - } - - if (importRequest.Files == null || (importRequest.Files != null && importRequest.Files.Count == 0)) - { - this._logEntry.Log("************ Exception on SubmitImportRequest, importRequest.Files is required and must have at least one file listed to import.", TraceEventType.Error); - return Guid.Empty; - } - - // Done error checking - if (string.IsNullOrWhiteSpace(importRequest.ImportName)) - importRequest.ImportName = "User Requested Import"; - - - Guid ImportId = Guid.Empty; - Guid ImportMap = Guid.Empty; - Guid ImportFile = Guid.Empty; - List ImportFileIds = new List(); - - // Create Import Object - // The Import Object is the anchor for the Import job in Dataverse. - Dictionary importFields = new Dictionary(); - importFields.Add("name", new DataverseDataTypeWrapper(importRequest.ImportName, DataverseFieldType.String)); - importFields.Add("modecode", new DataverseDataTypeWrapper(importRequest.Mode, DataverseFieldType.Picklist)); // 0 == Create , 1 = Update.. - ImportId = CreateNewRecord("import", importFields); - - if (ImportId == Guid.Empty) - // Error here; - return Guid.Empty; - - #region Determin Map to Use - //Guid guDataMapId = Guid.Empty; - if (string.IsNullOrWhiteSpace(importRequest.DataMapFileName) && importRequest.DataMapFileId == Guid.Empty) - // User Requesting to use System Mapping here. - importRequest.UseSystemMap = true; // Override whatever setting they had here. - else - { - // User providing information on a map to use. - // Query to get the map from the system - List fldList = new List(); - fldList.Add("name"); - fldList.Add("source"); - fldList.Add("importmapid"); - Dictionary MapData = null; - if (importRequest.DataMapFileId != Guid.Empty) - { - // Have the id here... get the map based on the ID. - MapData = GetEntityDataById("importmap", importRequest.DataMapFileId, fldList); - } - else - { - // Search by name... exact match required. - List filters = new List(); - DataverseSearchFilter filter = new DataverseSearchFilter(); - filter.FilterOperator = Microsoft.Xrm.Sdk.Query.LogicalOperator.And; - filter.SearchConditions.Add(new DataverseFilterConditionItem() { FieldName = "name", FieldOperator = Microsoft.Xrm.Sdk.Query.ConditionOperator.Equal, FieldValue = importRequest.DataMapFileName }); - filters.Add(filter); - - // Search by Name.. - Dictionary> rslts = GetEntityDataBySearchParams("importmap", filters, LogicalSearchOperator.None, fldList); - if (rslts != null && rslts.Count > 0) - { - // if there is more then one record returned.. throw an error ( should not happen ) - if (rslts.Count > 1) - { - // log error here. - this._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception on SubmitImportRequest, More then one mapping file was found for {0}, Specifiy the ID of the Mapfile to use", importRequest.DataMapFileName), TraceEventType.Error); - return Guid.Empty; - } - else - { - // Get my single record and move on.. - MapData = rslts.First().Value; - // Update the Guid for the mapID. - importRequest.DataMapFileId = GetDataByKeyFromResultsSet(MapData, "importmapid"); - } - } - } - ImportMap = importRequest.DataMapFileId; - - - // Now get the entity import mapping info, We need this to get the source entity name from the map XML file. - if (ImportMap != Guid.Empty) - { - // Iterate over the import files and update the entity names. - - fldList.Clear(); - fldList.Add("sourceentityname"); - List filters = new List(); - DataverseSearchFilter filter = new DataverseSearchFilter(); - filter.FilterOperator = Microsoft.Xrm.Sdk.Query.LogicalOperator.And; - filter.SearchConditions.Add(new DataverseFilterConditionItem() { FieldName = "importmapid", FieldOperator = Microsoft.Xrm.Sdk.Query.ConditionOperator.Equal, FieldValue = ImportMap }); - filters.Add(filter); - Dictionary> al = GetEntityDataBySearchParams("importentitymapping", filters, LogicalSearchOperator.None, null); - if (al != null && al.Count > 0) - { - foreach (var row in al.Values) - { - importRequest.Files.ForEach(fi => - { - if (fi.TargetEntityName.Equals(GetDataByKeyFromResultsSet(row, "targetentityname"), StringComparison.OrdinalIgnoreCase)) - fi.SourceEntityName = GetDataByKeyFromResultsSet(row, "sourceentityname"); - }); - } - } - else - { - if (ImportId != Guid.Empty) - DeleteEntity("import", ImportId); - - // Failed to find mapping entry error , Map not imported properly - this._logEntry.Log("************ Exception on SubmitImportRequest, Cannot find mapping file information found MapFile Provided.", TraceEventType.Error); - return Guid.Empty; - } - } - else - { - if (ImportId != Guid.Empty) - DeleteEntity("import", ImportId); - - // Failed to find mapping entry error , Map not imported properly - this._logEntry.Log("************ Exception on SubmitImportRequest, Cannot find ImportMappingsFile Provided.", TraceEventType.Error); - return Guid.Empty; - } - - } - #endregion - - #region Create Import File for each File in array - bool continueImport = true; - Dictionary importFileFields = new Dictionary(); - foreach (var FileItem in importRequest.Files) - { - // Create the Import File Object - Loop though file objects and create as many as necessary. - // This is the row that has the data being imported as well as the status of the import file. - importFileFields.Add("name", new DataverseDataTypeWrapper(FileItem.FileName, DataverseFieldType.String)); - importFileFields.Add("source", new DataverseDataTypeWrapper(FileItem.FileName, DataverseFieldType.String)); - importFileFields.Add("filetypecode", new DataverseDataTypeWrapper(FileItem.FileType, DataverseFieldType.Picklist)); // File Type is either : 0 = CSV , 1 = XML , 2 = Attachment - importFileFields.Add("content", new DataverseDataTypeWrapper(FileItem.FileContentToImport, DataverseFieldType.String)); - importFileFields.Add("enableduplicatedetection", new DataverseDataTypeWrapper(FileItem.EnableDuplicateDetection, DataverseFieldType.Boolean)); - importFileFields.Add("usesystemmap", new DataverseDataTypeWrapper(importRequest.UseSystemMap, DataverseFieldType.Boolean)); // Use the System Map to get somthing done. - importFileFields.Add("sourceentityname", new DataverseDataTypeWrapper(FileItem.SourceEntityName, DataverseFieldType.String)); - importFileFields.Add("targetentityname", new DataverseDataTypeWrapper(FileItem.TargetEntityName, DataverseFieldType.String)); - importFileFields.Add("datadelimitercode", new DataverseDataTypeWrapper(FileItem.DataDelimiter, DataverseFieldType.Picklist)); // 1 = " | 2 = | 3 = ' - importFileFields.Add("fielddelimitercode", new DataverseDataTypeWrapper(FileItem.FieldDelimiter, DataverseFieldType.Picklist)); // 1 = : | 2 = , | 3 = ' - importFileFields.Add("isfirstrowheader", new DataverseDataTypeWrapper(FileItem.IsFirstRowHeader, DataverseFieldType.Boolean)); - importFileFields.Add("processcode", new DataverseDataTypeWrapper(1, DataverseFieldType.Picklist)); - if (FileItem.IsRecordOwnerATeam) - importFileFields.Add("recordsownerid", new DataverseDataTypeWrapper(FileItem.RecordOwner, DataverseFieldType.Lookup, "team")); - else - importFileFields.Add("recordsownerid", new DataverseDataTypeWrapper(FileItem.RecordOwner, DataverseFieldType.Lookup, "systemuser")); - - importFileFields.Add("importid", new DataverseDataTypeWrapper(ImportId, DataverseFieldType.Lookup, "import")); - if (ImportMap != Guid.Empty) - importFileFields.Add("importmapid", new DataverseDataTypeWrapper(ImportMap, DataverseFieldType.Lookup, "importmap")); - - ImportFile = CreateNewRecord("importfile", importFileFields); - if (ImportFile == Guid.Empty) - { - continueImport = false; - break; - } - ImportFileIds.Add(ImportFile); - importFileFields.Clear(); - } - - #endregion - - - // if We have an Import File... Activate Import. - if (continueImport) - { - ParseImportResponse parseResp = (ParseImportResponse)Command_Execute(new ParseImportRequest() { ImportId = ImportId }, - string.Format(CultureInfo.InvariantCulture, "************ Exception Executing ParseImportRequest for ImportJob ({0})", importRequest.ImportName)); - if (parseResp.AsyncOperationId != Guid.Empty) - { - if (delayUntil != DateTime.MinValue) - { - importFileFields.Clear(); - importFileFields.Add("postponeuntil", new DataverseDataTypeWrapper(delayUntil, DataverseFieldType.DateTime)); - UpdateEntity("asyncoperation", "asyncoperationid", parseResp.AsyncOperationId, importFileFields); - } - - TransformImportResponse transformResp = (TransformImportResponse)Command_Execute(new TransformImportRequest() { ImportId = ImportId }, - string.Format(CultureInfo.InvariantCulture, "************ Exception Executing TransformImportRequest for ImportJob ({0})", importRequest.ImportName)); - if (transformResp != null) - { - if (delayUntil != DateTime.MinValue) - { - importFileFields.Clear(); - importFileFields.Add("postponeuntil", new DataverseDataTypeWrapper(delayUntil.AddSeconds(1), DataverseFieldType.DateTime)); - UpdateEntity("asyncoperation", "asyncoperationid", transformResp.AsyncOperationId, importFileFields); - } - - ImportRecordsImportResponse importResp = (ImportRecordsImportResponse)Command_Execute(new ImportRecordsImportRequest() { ImportId = ImportId }, - string.Format(CultureInfo.InvariantCulture, "************ Exception Executing ImportRecordsImportRequest for ImportJob ({0})", importRequest.ImportName)); - if (importResp != null) - { - if (delayUntil != DateTime.MinValue) - { - importFileFields.Clear(); - importFileFields.Add("postponeuntil", new DataverseDataTypeWrapper(delayUntil.AddSeconds(2), DataverseFieldType.DateTime)); - UpdateEntity("asyncoperation", "asyncoperationid", importResp.AsyncOperationId, importFileFields); - } - - return ImportId; - } - } - } - } - else - { - // Error.. Clean up the other records. - string err = LastError; - Exception ex = LastException; - - if (ImportFileIds.Count > 0) - { - ImportFileIds.ForEach(i => - { - DeleteEntity("importfile", i); - }); - ImportFileIds.Clear(); - } - - if (ImportId != Guid.Empty) - DeleteEntity("import", ImportId); - - // This is done to allow the error to be available to the user after the class cleans things up. - if (ex != null) - _logEntry.Log(err, TraceEventType.Error, ex); - else - _logEntry.Log(err, TraceEventType.Error); - - return Guid.Empty; - } - return ImportId; - } - - /// - /// Used to upload a data map to the Dataverse - /// - /// XML of the datamap in string form - /// True to have Dataverse replace ID's on inbound data, False to have inbound data retain its ID's - /// if true, dataMapXml is expected to be a File name and path to load. - /// Returns ID of the datamap or Guid.Empty - public Guid ImportDataMap(string dataMapXml, bool replaceIds = true, bool dataMapXmlIsFilePath = false) - { - _logEntry.ResetLastError(); // Reset Last Error - if (DataverseService == null) - { - _logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); - return Guid.Empty; - } - - if (string.IsNullOrWhiteSpace(dataMapXml)) - { - this._logEntry.Log("************ Exception on ImportDataMap, dataMapXml is required", TraceEventType.Error); - return Guid.Empty; - } - - if (dataMapXmlIsFilePath) - { - // try to load the file from the file system - if (File.Exists(dataMapXml)) - { - try - { - string sContent = ""; - using (var a = File.OpenText(dataMapXml)) - { - sContent = a.ReadToEnd(); - } - - dataMapXml = sContent; - } - #region Exception handlers for files - catch (UnauthorizedAccessException ex) - { - this._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception on ImportDataMap, Unauthorized Access to file: {0}", dataMapXml), TraceEventType.Error, ex); - return Guid.Empty; - } - catch (ArgumentNullException ex) - { - this._logEntry.Log("************ Exception on ImportDataMap, File path not specified", TraceEventType.Error, ex); - return Guid.Empty; - } - catch (ArgumentException ex) - { - this._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception on ImportDataMap, File path is invalid: {0}", dataMapXml), TraceEventType.Error, ex); - return Guid.Empty; - } - catch (PathTooLongException ex) - { - this._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception on ImportDataMap, File path is too long. Paths must be less than 248 characters, and file names must be less than 260 characters\n{0}", dataMapXml), TraceEventType.Error, ex); - return Guid.Empty; - } - catch (DirectoryNotFoundException ex) - { - this._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception on ImportDataMap, File path is invalid: {0}", dataMapXml), TraceEventType.Error, ex); - return Guid.Empty; - } - catch (FileNotFoundException ex) - { - this._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception on ImportDataMap, File Not Found: {0}", dataMapXml), TraceEventType.Error, ex); - return Guid.Empty; - } - catch (NotSupportedException ex) - { - this._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception on ImportDataMap, File path or name is invalid: {0}", dataMapXml), TraceEventType.Error, ex); - return Guid.Empty; - } - #endregion - } - else - { - this._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception on ImportDataMap, File path specified in dataMapXml is not found: {0}", dataMapXml), TraceEventType.Error); - return Guid.Empty; - } - - } - - ImportMappingsImportMapResponse resp = (ImportMappingsImportMapResponse)Command_Execute(new ImportMappingsImportMapRequest() { MappingsXml = dataMapXml, ReplaceIds = replaceIds }, - "************ Exception Executing ImportMappingsImportMapResponse for ImportDataMap"); - if (resp != null) - { - if (resp.ImportMapId != Guid.Empty) - { - return resp.ImportMapId; - } - } - - return Guid.Empty; - } - - - /// - /// Import Solution Async used Execute Async pattern to run a solution import. - /// - /// Path to the Solution File - /// Activate Plugin's and workflows on the Solution - /// This will populate with the Import ID even if the request failed. - /// You can use this ID to request status on the import via a request to the ImportJob entity. - /// Forces an overwrite of unmanaged customizations of the managed solution you are installing, defaults to false - /// Skips dependency against dependencies flagged as product update, defaults to false - /// Applies only on Dataverse organizations version 7.2 or higher. This imports the Dataverse solution as a holding solution utilizing the “As Holding” capability of ImportSolution - /// Internal Microsoft use only - /// Extra parameters - /// Returns the Async Job ID. To find the status of the job, query the AsyncOperation Entity using GetEntityDataByID using the returned value of this method - public Guid ImportSolutionAsync(string solutionPath, out Guid importId, bool activatePlugIns = true, bool overwriteUnManagedCustomizations = false, bool skipDependancyOnProductUpdateCheckOnInstall = false, bool importAsHoldingSolution = false, bool isInternalUpgrade = false, Dictionary extraParameters = null) - { - return ImportSolutionToImpl(solutionPath, out importId, activatePlugIns, overwriteUnManagedCustomizations, skipDependancyOnProductUpdateCheckOnInstall, importAsHoldingSolution, isInternalUpgrade, true, extraParameters); - } - - - /// - /// - /// Imports a Dataverse solution to the Dataverse Server currently connected. - /// *** Note: this is a blocking call and will take time to Import to Dataverse *** - /// - /// - /// Path to the Solution File - /// Activate Plugin's and workflows on the Solution - /// This will populate with the Import ID even if the request failed. - /// You can use this ID to request status on the import via a request to the ImportJob entity. - /// Forces an overwrite of unmanaged customizations of the managed solution you are installing, defaults to false - /// Skips dependency against dependencies flagged as product update, defaults to false - /// Applies only on Dataverse organizations version 7.2 or higher. This imports the Dataverse solution as a holding solution utilizing the “As Holding” capability of ImportSolution - /// Internal Microsoft use only - /// Extra parameters - public Guid ImportSolution(string solutionPath, out Guid importId, bool activatePlugIns = true, bool overwriteUnManagedCustomizations = false, bool skipDependancyOnProductUpdateCheckOnInstall = false, bool importAsHoldingSolution = false, bool isInternalUpgrade = false, Dictionary extraParameters = null) - { - return ImportSolutionToImpl(solutionPath, out importId, activatePlugIns, overwriteUnManagedCustomizations, skipDependancyOnProductUpdateCheckOnInstall, importAsHoldingSolution, isInternalUpgrade, false, extraParameters); - } - - /// - /// Executes a Delete and Propmote Request against Dataverse using the Async Pattern. - /// - /// Unique Name of solution to be upgraded - /// Returns the Async Job ID. To find the status of the job, query the AsyncOperation Entity using GetEntityDataByID using the returned value of this method - public Guid DeleteAndPromoteSolutionAsync(string uniqueName) - { - _logEntry.ResetLastError(); // Reset Last Error - if (DataverseService == null) - { - _logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); - return Guid.Empty; - } - // Test for non blank unique name. - if (string.IsNullOrEmpty(uniqueName)) - { - _logEntry.Log("Solution UniqueName is required.", TraceEventType.Error); - return Guid.Empty; - } - - DeleteAndPromoteRequest delReq = new DeleteAndPromoteRequest() - { - UniqueName = uniqueName - }; - - // Assign Tracking ID - Guid requestTrackingId = Guid.NewGuid(); - delReq.RequestId = requestTrackingId; - - // Execute Async here - ExecuteAsyncRequest req = new ExecuteAsyncRequest() { Request = delReq }; - _logEntry.Log(string.Format(CultureInfo.InvariantCulture, "{1} - Created Async DeleteAndPromoteSolutionRequest : RequestID={0} ", - requestTrackingId.ToString(), uniqueName), TraceEventType.Verbose); - ExecuteAsyncResponse resp = (ExecuteAsyncResponse)Command_Execute(req, "Submitting DeleteAndPromoteSolution Async Request"); - if (resp != null) - { - if (resp.AsyncJobId != Guid.Empty) - { - _logEntry.Log(string.Format("{1} - AsyncJobID for DeleteAndPromoteSolution {0}.", resp.AsyncJobId, uniqueName), TraceEventType.Verbose); - return resp.AsyncJobId; - } - } - - _logEntry.Log(string.Format("{0} - Failed execute Async Job for DeleteAndPromoteSolution.", uniqueName), TraceEventType.Error); - return Guid.Empty; - } - - /// - /// - /// Request Dataverse to install sample data shipped with Dataverse. Note this is process will take a few moments to execute. - /// This method will return once the request has been submitted. - /// - /// - /// ID of the Async job executing the request - public Guid InstallSampleData() - { - _logEntry.ResetLastError(); // Reset Last Error - if (DataverseService == null) - { - _logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); - return Guid.Empty; - } - - if (ImportStatus.NotImported != IsSampleDataInstalled()) - { - _logEntry.Log("************ InstallSampleData failed, sample data is already installed on Dataverse", TraceEventType.Error); - return Guid.Empty; - } - - // Create Request to Install Sample data. - InstallSampleDataRequest loadSampledataRequest = new InstallSampleDataRequest() { RequestId = Guid.NewGuid() }; - InstallSampleDataResponse resp = (InstallSampleDataResponse)Command_Execute(loadSampledataRequest, "Executing InstallSampleDataRequest for InstallSampleData"); - if (resp == null) - return Guid.Empty; - else - return loadSampledataRequest.RequestId.Value; - } - - /// - /// - /// Request Dataverse to remove sample data shipped with Dataverse. Note this is process will take a few moments to execute. - /// This method will return once the request has been submitted. - /// - /// - /// ID of the Async job executing the request - public Guid UninstallSampleData() - { - _logEntry.ResetLastError(); // Reset Last Error - if (DataverseService == null) - { - _logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); - return Guid.Empty; - } - - if (ImportStatus.NotImported == IsSampleDataInstalled()) - { - _logEntry.Log("************ UninstallSampleData failed, sample data is not installed on Dataverse", TraceEventType.Error); - return Guid.Empty; - } - - UninstallSampleDataRequest removeSampledataRequest = new UninstallSampleDataRequest() { RequestId = Guid.NewGuid() }; - UninstallSampleDataResponse resp = (UninstallSampleDataResponse)Command_Execute(removeSampledataRequest, "Executing UninstallSampleDataRequest for UninstallSampleData"); - if (resp == null) - return Guid.Empty; - else - return removeSampledataRequest.RequestId.Value; - } - - /// - /// Determines if the Dataverse sample data has been installed - /// - /// True if the sample data is installed, False if not. - public ImportStatus IsSampleDataInstalled() - { - try - { - // Query the Org I'm connected to to get the sample data import info. - Dictionary> theOrg = - GetEntityDataBySearchParams("organization", - new Dictionary(), LogicalSearchOperator.None, new List() { "sampledataimportid" }); - - if (theOrg != null && theOrg.Count > 0) - { - var v = theOrg.FirstOrDefault(); - if (v.Value != null && v.Value.Count > 0) - { - if (GetDataByKeyFromResultsSet(v.Value, "sampledataimportid") != Guid.Empty) - { - string sampledataimportid = GetDataByKeyFromResultsSet(v.Value, "sampledataimportid").ToString(); - _logEntry.Log(string.Format("sampledataimportid = {0}", sampledataimportid), TraceEventType.Verbose); - Dictionary basicSearch = new Dictionary(); - basicSearch.Add("importid", sampledataimportid); - Dictionary> importSampleData = GetEntityDataBySearchParams("import", basicSearch, LogicalSearchOperator.None, new List() { "statuscode" }); - - if (importSampleData != null && importSampleData.Count > 0) - { - var import = importSampleData.FirstOrDefault(); - if (import.Value != null) - { - OptionSetValue ImportStatusResult = GetDataByKeyFromResultsSet(import.Value, "statuscode"); - if (ImportStatusResult != null) - { - _logEntry.Log(string.Format("sampledata import job result = {0}", ImportStatusResult.Value), TraceEventType.Verbose); - //This Switch Case needs to be in Sync with the Dataverse Import StatusCode. - switch (ImportStatusResult.Value) - { - // 4 is the Import Status Code for Complete Import - case 4: return ImportStatus.Completed; - // 5 is the Import Status Code for the Failed Import - case 5: return ImportStatus.Failed; - // Rest (Submitted, Parsing, Transforming, Importing) are different stages of Inprogress Import hence putting them under same case. - default: return ImportStatus.InProgress; - } - } - } - } - } - } - } - } - catch { } - return ImportStatus.NotImported; - //return false; - } - - /// - /// ImportStatus Reasons - /// - public enum ImportStatus - { - /// Not Yet Imported - NotImported = 0, - /// Import is in Progress - InProgress = 1, - /// Import has Completed - Completed = 2, - /// Import has Failed - Failed = 3 - }; - - #endregion - - /// - /// Associates one Entity to another where an M2M Relationship Exists. - /// - /// Entity on one side of the relationship - /// The Id of the record on the first side of the relationship - /// Entity on the second side of the relationship - /// The Id of the record on the second side of the relationship - /// Relationship name between the 2 entities - /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// true on success, false on fail - public bool CreateEntityAssociation(string entityName1, Guid entity1Id, string entityName2, Guid entity2Id, string relationshipName, Guid batchId = default(Guid), bool bypassPluginExecution = false) - { - _logEntry.ResetLastError(); // Reset Last Error - if (DataverseService == null) - { - _logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); - return false; - } - - if (string.IsNullOrEmpty(entityName1) || string.IsNullOrEmpty(entityName2) || entity1Id == Guid.Empty || entity2Id == Guid.Empty) - { - _logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception in CreateEntityAssociation, all parameters must be populated"), TraceEventType.Error); - return false; - } - - AssociateEntitiesRequest req = new AssociateEntitiesRequest(); - req.Moniker1 = new EntityReference(entityName1, entity1Id); - req.Moniker2 = new EntityReference(entityName2, entity2Id); - req.RelationshipName = relationshipName; - - - if (AddRequestToBatch(batchId, req, string.Format(CultureInfo.InvariantCulture, "Creating association between({0}) and {1}", entityName1, entityName2), - string.Format(CultureInfo.InvariantCulture, "Request to Create association between({0}) and {1} Queued", entityName1, entityName2), bypassPluginExecution)) - return true; - - AssociateEntitiesResponse resp = (AssociateEntitiesResponse)Command_Execute(req, "Executing CreateEntityAssociation", bypassPluginExecution); - if (resp != null) - return true; - - return false; - } - - /// - /// Associates multiple entities of the same time to a single entity - /// - /// Entity that things will be related too. - /// ID of entity that things will be related too - /// Entity that you are relating from - /// ID's of the entities you are relating from - /// Name of the relationship between the target and the source entities. - /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately - /// Optional: if set to true, indicates that this is a N:N using a reflexive relationship - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// true on success, false on fail - public bool CreateMultiEntityAssociation(string targetEntity, Guid targetEntity1Id, string sourceEntityName, List sourceEntitieIds, string relationshipName, Guid batchId = default(Guid), bool bypassPluginExecution = false, bool isReflexiveRelationship = false) - { - _logEntry.ResetLastError(); // Reset Last Error - if (DataverseService == null) - { - _logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); - return false; - } - - if (string.IsNullOrEmpty(targetEntity) || string.IsNullOrEmpty(sourceEntityName) || targetEntity1Id == Guid.Empty || sourceEntitieIds == null || sourceEntitieIds.Count == 0) - { - _logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception in CreateMultiEntityAssociation, all parameters must be populated"), TraceEventType.Error); - return false; - } - - AssociateRequest req = new AssociateRequest(); - req.Relationship = new Relationship(relationshipName); - if (isReflexiveRelationship) // used to determine if the relationship role is reflexive. - req.Relationship.PrimaryEntityRole = EntityRole.Referenced; - req.RelatedEntities = new EntityReferenceCollection(); - foreach (Guid g in sourceEntitieIds) - { - req.RelatedEntities.Add(new EntityReference(sourceEntityName, g)); - } - req.Target = new EntityReference(targetEntity, targetEntity1Id); - - if (AddRequestToBatch(batchId, req, string.Format(CultureInfo.InvariantCulture, "Creating multi association between({0}) and {1}", targetEntity, sourceEntityName), - string.Format(CultureInfo.InvariantCulture, "Request to Create multi association between({0}) and {1} queued", targetEntity, sourceEntityName), bypassPluginExecution)) - return true; - - AssociateResponse resp = (AssociateResponse)Command_Execute(req, "Executing CreateMultiEntityAssociation", bypassPluginExecution); - if (resp != null) - return true; - - return false; - } - - /// - /// Removes the Association between 2 entity items where an M2M Relationship Exists. - /// - /// Entity on one side of the relationship - /// The Id of the record on the first side of the relationship - /// Entity on the second side of the relationship - /// The Id of the record on the second side of the relationship - /// Relationship name between the 2 entities - /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// true on success, false on fail - public bool DeleteEntityAssociation(string entityName1, Guid entity1Id, string entityName2, Guid entity2Id, string relationshipName, Guid batchId = default(Guid), bool bypassPluginExecution = false) - { - _logEntry.ResetLastError(); // Reset Last Error - if (DataverseService == null) - { - _logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); - return false; - } - - if (string.IsNullOrEmpty(entityName1) || string.IsNullOrEmpty(entityName2) || entity1Id == Guid.Empty || entity2Id == Guid.Empty) - { - _logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception in DeleteEntityAssociation, all parameters must be populated"), TraceEventType.Error); - return false; - } - - DisassociateEntitiesRequest req = new DisassociateEntitiesRequest(); - req.Moniker1 = new EntityReference(entityName1, entity1Id); - req.Moniker2 = new EntityReference(entityName2, entity2Id); - req.RelationshipName = relationshipName; - - if (AddRequestToBatch(batchId, req, string.Format(CultureInfo.InvariantCulture, "Executing DeleteEntityAssociation between ({0}) and {1}", entityName1, entityName2), - string.Format(CultureInfo.InvariantCulture, "Request to Execute DeleteEntityAssociation between ({0}) and {1} Queued", entityName1, entityName2), bypassPluginExecution)) - return true; - - DisassociateEntitiesResponse resp = (DisassociateEntitiesResponse)Command_Execute(req, "Executing DeleteEntityAssociation", bypassPluginExecution); - if (resp != null) - return true; - - return false; - } - - /// - /// Assign an Entity to the specified user ID - /// - /// User ID to assign too - /// Target entity Name - /// Target entity id - /// Batch ID of to use, Optional - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// - public bool AssignEntityToUser(Guid userId, string entityName, Guid entityId, Guid batchId = default(Guid), bool bypassPluginExecution = false) - { - _logEntry.ResetLastError(); // Reset Last Error - if (DataverseService == null || userId == Guid.Empty || entityId == Guid.Empty) - { - return false; - } - - AssignRequest assignRequest = new AssignRequest(); - assignRequest.Assignee = new EntityReference("systemuser", userId); - assignRequest.Target = new EntityReference(entityName, entityId); - - if (AddRequestToBatch(batchId, assignRequest, string.Format(CultureInfo.InvariantCulture, "Assigning entity ({0}) to {1}", entityName, userId.ToString()), - string.Format(CultureInfo.InvariantCulture, "Request to Assign entity ({0}) to {1} Queued", entityName, userId.ToString()), bypassPluginExecution)) - return true; - - AssignResponse arResp = (AssignResponse)Command_Execute(assignRequest, "Assigning Entity to User", bypassPluginExecution); - if (arResp != null) - return true; - - return false; - } - - /// - /// This will route a Entity to a public queue, - /// - /// ID of the Entity to route - /// Name of the Entity that the Id describes - /// Name of the Queue to Route Too - /// ID of the user id to set as the working system user - /// if true Set the worked by when doing the assign - /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// true on success - public bool AddEntityToQueue(Guid entityId, string entityName, string queueName, Guid workingUserId, bool setWorkingByUser = false, Guid batchId = default(Guid), bool bypassPluginExecution = false) - { - _logEntry.ResetLastError(); // Reset Last Error - if (DataverseService == null || entityId == Guid.Empty) - { - return false; - } - - Dictionary SearchParams = new Dictionary(); - SearchParams.Add("name", queueName); - - // Get the Target QUeue - Dictionary> rslts = GetEntityDataBySearchParams("queue", SearchParams, LogicalSearchOperator.None, null); - if (rslts != null) - if (rslts.Count > 0) - { - Guid guQueueID = Guid.Empty; - foreach (Dictionary row in rslts.Values) - { - // got something - guQueueID = GetDataByKeyFromResultsSet(row, "queueid"); - break; - } - - if (guQueueID != Guid.Empty) - { - - - AddToQueueRequest req = new AddToQueueRequest(); - req.DestinationQueueId = guQueueID; - req.Target = new EntityReference(entityName, entityId); - - // Set the worked by user if the request includes it. - if (setWorkingByUser) - { - Entity queItm = new Entity("queueitem"); - queItm.Attributes.Add("workerid", new EntityReference("systemuser", workingUserId)); - req.QueueItemProperties = queItm; - } - - if (AddRequestToBatch(batchId, req, string.Format(CultureInfo.InvariantCulture, "Assigning entity to queue ({0}) to {1}", entityName, guQueueID.ToString()), - string.Format(CultureInfo.InvariantCulture, "Request to Assign entity to queue ({0}) to {1} Queued", entityName, guQueueID.ToString()), bypassPluginExecution)) - return true; - - AddToQueueResponse resp = (AddToQueueResponse)Command_Execute(req, string.Format(CultureInfo.InvariantCulture, "Adding a item to queue {0} in CDS", queueName), bypassPluginExecution); - if (resp != null) - return true; - else - return false; - } - } - return false; - } - - /// - /// this will send an Email to the - /// - /// ID of the Email activity - /// Tracking Token or Null - /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// - public bool SendSingleEmail(Guid emailid, string token, Guid batchId = default(Guid), bool bypassPluginExecution = false) - { - _logEntry.ResetLastError(); // Reset Last Error - if (DataverseService == null || emailid == Guid.Empty) - { - return false; - } - - if (token == null) - token = string.Empty; - - // Send the mail now. - SendEmailRequest req = new SendEmailRequest(); - req.EmailId = emailid; - req.TrackingToken = token; - req.IssueSend = true; // Send it now. - - if (AddRequestToBatch(batchId, req, string.Format(CultureInfo.InvariantCulture, "Send Direct email ({0}) tracking token {1}", emailid.ToString(), token), - string.Format(CultureInfo.InvariantCulture, "Request to Send Direct email ({0}) tracking token {1} Queued", emailid.ToString(), token), bypassPluginExecution)) - return true; - - SendEmailResponse sendresp = (SendEmailResponse)Command_Execute(req, string.Format(CultureInfo.InvariantCulture, "Sending email ({0}) from Dataverse", emailid), bypassPluginExecution); - if (sendresp != null) - return true; - else - return false; - } - - /// - /// Returns the user ID of the currently logged in user. - /// - /// - public Guid GetMyUserId() - { - return SystemUser.UserId; - } - - #endregion - - #region Dataverse MetadataService methods - - - /// - /// Gets a PickList, Status List or StateList from the metadata of an attribute - /// - /// text name of the entity to query - /// name of the attribute to query - /// - public PickListMetaElement GetPickListElementFromMetadataEntity(string targetEntity, string attribName) - { - _logEntry.ResetLastError(); // Reset Last Error - if (DataverseService != null) - { - List attribDataList = _dynamicAppUtility.GetAttributeDataByEntity(targetEntity, attribName); - if (attribDataList.Count > 0) - { - // have data.. - // need to make sure its really a pick list. - foreach (AttributeData attributeData in attribDataList) - { - switch (attributeData.AttributeType) - { - case AttributeTypeCode.Picklist: - case AttributeTypeCode.Status: - case AttributeTypeCode.State: - PicklistAttributeData pick = (PicklistAttributeData)attributeData; - PickListMetaElement resp = new PickListMetaElement((string)pick.ActualValue, pick.AttributeLabel, pick.DisplayValue); - if (pick.PicklistOptions != null) - { - foreach (OptionMetadata opt in pick.PicklistOptions) - { - PickListItem itm = null; - itm = new PickListItem((string)GetLocalLabel(opt.Label), (int)opt.Value.Value); - resp.Items.Add(itm); - } - } - return resp; - default: - break; - } - } - } - } - return null; - } - - /// - /// Gets a global option set from Dataverse. - /// - /// Name of the Option Set To get - /// OptionSetMetadata or null - public OptionSetMetadata GetGlobalOptionSetMetadata(string globalOptionSetName) - { - _logEntry.ResetLastError(); // Reset Last Error - if (DataverseService == null) - { - _logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); - return null; - } - - try - { - return _metadataUtlity.GetGlobalOptionSetMetadata(globalOptionSetName); - } - catch (Exception ex) - { - this._logEntry.Log("************ Exception getting optionset metadata info from Dataverse : " + ex.Message, TraceEventType.Error); - } - return null; - } - - - /// - /// Returns a list of entities with basic data from Dataverse - /// - /// defaults to true, will only return published information - /// EntityFilter to apply to this request, note that filters other then Default will consume more time. - /// - public List GetAllEntityMetadata(bool onlyPublished = true, EntityFilters filter = EntityFilters.Default) - { - _logEntry.ResetLastError(); // Reset Last Error - #region Basic Checks - if (DataverseService == null) - { - _logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); - return null; - } - #endregion - - try - { - return _metadataUtlity.GetAllEntityMetadata(onlyPublished, filter); - } - catch (Exception ex) - { - this._logEntry.Log("************ Exception getting metadata info from CDS : " + ex.Message, TraceEventType.Error); - } - return null; - } - - /// - /// Returns the Metadata for an entity from Dataverse, defaults to basic data only. - /// - /// Logical name of the entity - /// filter to apply to the query, defaults to default entity data. - /// - public EntityMetadata GetEntityMetadata(string entityLogicalname, EntityFilters queryFilter = EntityFilters.Default) - { - _logEntry.ResetLastError(); // Reset Last Error - #region Basic Checks - if (DataverseService == null) - { - _logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); - return null; - } - #endregion - - try - { - return _metadataUtlity.GetEntityMetadata(queryFilter, entityLogicalname); - } - catch (Exception ex) - { - this._logEntry.Log("************ Exception getting metadata info from Dataverse : " + ex.Message, TraceEventType.Error); - } - return null; - } - - /// - /// Returns the Form Entity References for a given form type. - /// - /// logical name of the entity you are querying for form data. - /// Form Type you want - /// List of Entity References for the form type requested. - public List GetEntityFormIdListByType(string entityLogicalname, FormTypeId formTypeId) - { - _logEntry.ResetLastError(); - #region Basic Checks - if (DataverseService == null) - { - _logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); - return null; - } - if (string.IsNullOrWhiteSpace(entityLogicalname)) - { - _logEntry.Log("An Entity Name must be supplied", TraceEventType.Error); - return null; - } - #endregion - - try - { - RetrieveFilteredFormsRequest req = new RetrieveFilteredFormsRequest(); - req.EntityLogicalName = entityLogicalname; - req.FormType = new OptionSetValue((int)formTypeId); - RetrieveFilteredFormsResponse resp = (RetrieveFilteredFormsResponse)Command_Execute(req, "GetEntityFormIdListByType"); - if (resp != null) - return resp.SystemForms.ToList(); - else - return null; - } - catch (Exception ex) - { - this._logEntry.Log("************ Exception getting metadata info from Dataverse : " + ex.Message, TraceEventType.Error); - } - return null; - } - - /// - /// Returns all attributes on a entity - /// - /// returns all attributes on a entity - /// - public List GetAllAttributesForEntity(string entityLogicalname) - { - _logEntry.ResetLastError(); // Reset Last Error - #region Basic Checks - if (DataverseService == null) - { - _logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); - return null; - } - if (string.IsNullOrWhiteSpace(entityLogicalname)) - { - _logEntry.Log("An Entity Name must be supplied", TraceEventType.Error); - return null; - } - #endregion - - try - { - return _metadataUtlity.GetAllAttributesMetadataByEntity(entityLogicalname); - } - catch (Exception ex) - { - this._logEntry.Log("************ Exception getting metadata info from Dataverse : " + ex.Message, TraceEventType.Error); - } - return null; - } - - /// - /// Gets metadata for a specific entity's attribute. - /// - /// Name of the entity - /// Attribute Name - /// - public AttributeMetadata GetEntityAttributeMetadataForAttribute(string entityLogicalname, string attribName) - { - _logEntry.ResetLastError(); // Reset Last Error - #region Basic Checks - if (DataverseService == null) - { - _logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); - return null; - } - if (string.IsNullOrWhiteSpace(entityLogicalname)) - { - _logEntry.Log("An Entity Name must be supplied", TraceEventType.Error); - return null; - } - #endregion - - try - { - return _metadataUtlity.GetAttributeMetadata(entityLogicalname, attribName); - } - catch (Exception ex) - { - this._logEntry.Log("************ Exception getting metadata info from Dataverse : " + ex.Message, TraceEventType.Error); - } - return null; - } - - /// - /// Gets an Entity Name by Logical name or Type code. - /// - /// logical name of the entity - /// Type code for the entity - /// Localized name for the entity in the current users language - public string GetEntityDisplayName(string entityName, int entityTypeCode = -1) - { - return GetEntityDisplayNameImpl(entityName, entityTypeCode); - } - - /// - /// Gets an Entity Name by Logical name or Type code. - /// - /// logical name of the entity - /// Type code for the entity - /// Localized plural name for the entity in the current users language - public string GetEntityDisplayNamePlural(string entityName, int entityTypeCode = -1) - { - return GetEntityDisplayNameImpl(entityName, entityTypeCode, true); - } - - /// - /// This will clear the Metadata cache for either all entities or the specified entity - /// - /// Optional: name of the entity to clear cached info for - public void ResetLocalMetadataCache(string entityName = "") - { - if (_metadataUtlity != null) - _metadataUtlity.ClearCachedEntityMetadata(entityName); - } - - /// - /// Gets the Entity Display Name. - /// - /// - /// - /// - /// - private string GetEntityDisplayNameImpl(string entityName, int entityTypeCode = -1, bool getPlural = false) - { - _logEntry.ResetLastError(); // Reset Last Error - #region Basic Checks - if (DataverseService == null) - { - _logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); - return string.Empty; - } - - if (entityTypeCode == -1 && string.IsNullOrWhiteSpace(entityName)) - { - _logEntry.Log("Target entity or Type code is required", TraceEventType.Error); - return string.Empty; - } - #endregion - - try - { - // Get the entity by type code if necessary. - if (entityTypeCode != -1) - entityName = _metadataUtlity.GetEntityLogicalName(entityTypeCode); - - if (string.IsNullOrWhiteSpace(entityName)) - { - _logEntry.Log("Target entity or Type code is required", TraceEventType.Error); - return string.Empty; - } - - - - // Pull Object type code for this object. - EntityMetadata entData = - _metadataUtlity.GetEntityMetadata(EntityFilters.Entity, entityName); - - if (entData != null) - { - if (getPlural) - { - if (entData.DisplayCollectionName != null && entData.DisplayCollectionName.UserLocalizedLabel != null) - return entData.DisplayCollectionName.UserLocalizedLabel.Label; - else - return entityName; // Default to echo the same name back - } - else - { - if (entData.DisplayName != null && entData.DisplayName.UserLocalizedLabel != null) - return entData.DisplayName.UserLocalizedLabel.Label; - else - return entityName; // Default to echo the same name back - } - } - - } - catch (Exception ex) - { - this._logEntry.Log("************ Exception getting metadata info from Dataverse : " + ex.Message, TraceEventType.Error); - } - return string.Empty; - } - - /// - /// Gets the typecode of an entity by name. - /// - /// name of the entity to get the type code on - /// - public string GetEntityTypeCode(string entityName) - { - _logEntry.ResetLastError(); // Reset Last Error - #region Basic Checks - if (DataverseService == null) - { - _logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); - return string.Empty; - } - - if (string.IsNullOrEmpty(entityName)) - { - _logEntry.Log("Target entity is required", TraceEventType.Error); - return string.Empty; - } - #endregion - - try - { - - // Pull Object type code for this object. - EntityMetadata entData = - _metadataUtlity.GetEntityMetadata(EntityFilters.Entity, entityName); - - if (entData != null) - { - if (entData.ObjectTypeCode != null && entData.ObjectTypeCode.HasValue) - { - return entData.ObjectTypeCode.Value.ToString(CultureInfo.InvariantCulture); - } - } - } - catch (Exception ex) - { - this._logEntry.Log("************ Exception getting metadata info from Dataverse : " + ex.Message, TraceEventType.Error); - } - return string.Empty; - } - - - /// - /// Returns the Entity name for the given Type code - /// - /// - /// - public string GetEntityName(int entityTypeCode) - { - return _metadataUtlity.GetEntityLogicalName(entityTypeCode); - } - - - /// - /// Adds an option to a pick list on an entity. - /// - /// Entity Name to Target - /// Attribute Name on the Entity - /// List of Localized Labels - /// integer Value - /// Publishes the Update to the Live system.. note this is a time consuming process.. if you are doing a batch up updates, call PublishEntity Separately when you are finished. - /// true on success, on fail check last error. - public bool CreateOrUpdatePickListElement(string targetEntity, string attribName, List locLabelList, int valueData, bool publishOnComplete) - { - _logEntry.ResetLastError(); // Reset Last Error - #region Basic Checks - if (DataverseService == null) - { - _logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); - return false; - } - - if (string.IsNullOrEmpty(targetEntity)) - { - _logEntry.Log("Target entity is required", TraceEventType.Error); - return false; - } - - if (string.IsNullOrEmpty(attribName)) - { - _logEntry.Log("Target attribute name is required", TraceEventType.Error); - return false; - } - - if (locLabelList == null || locLabelList.Count == 0) - { - _logEntry.Log("Target Labels are required", TraceEventType.Error); - return false; - } - - LoadLCIDs(); // Load current languages . - - // Clear out the Metadata for this object. - if (_metadataUtlity != null) - _metadataUtlity.ClearCachedEntityMetadata(targetEntity); - - EntityMetadata entData = - _metadataUtlity.GetEntityMetadata(targetEntity); - - if (!entData.IsCustomEntity.Value) - { - // Only apply this if the entity is not a custom entity - if (valueData <= 199999) - { - _logEntry.Log("Option Value must exceed 200000", TraceEventType.Error); - return false; - } - } - - - #endregion - - // get the values for the requested attribute. - PickListMetaElement listData = GetPickListElementFromMetadataEntity(targetEntity, attribName); - if (listData == null) - { - // error here. - } - - bool isUpdate = false; - if (listData.Items != null && listData.Items.Count != 0) - { - // Check to see if the value we are looking to insert already exists by name or value. - List DisplayLabels = new List(); - foreach (LocalizedLabel loclbl in locLabelList) - { - if (DisplayLabels.Contains(loclbl.Label)) - continue; - else - DisplayLabels.Add(loclbl.Label); - } - - foreach (PickListItem pItem in listData.Items) - { - // check the value by id. - if (pItem.PickListItemId == valueData) - { - if (DisplayLabels.Contains(pItem.DisplayLabel)) - { - DisplayLabels.Clear(); - _logEntry.Log("PickList Element exists, No Change required.", TraceEventType.Error); - return false; - } - isUpdate = true; - break; - } - - //// Check the value by name... by putting this hear, we will handle a label update vs a Duplicate label. - if (DisplayLabels.Contains(pItem.DisplayLabel)) - { - // THis is an ERROR State... While Dataverse will allow 2 labels with the same text, it looks weird. - DisplayLabels.Clear(); - _logEntry.Log("Label Name exists, Please use a different display name for the label.", TraceEventType.Error); - return false; - } - } - - DisplayLabels.Clear(); - } - - if (isUpdate) - { - // update request - UpdateOptionValueRequest updateReq = new UpdateOptionValueRequest(); - updateReq.AttributeLogicalName = attribName; - updateReq.EntityLogicalName = targetEntity; - updateReq.Label = new Label(); - List lblList = new List(); - foreach (LocalizedLabel loclbl in locLabelList) - { - if (_loadedLCIDList.Contains(loclbl.LanguageCode)) - { - LocalizedLabel lbl = new LocalizedLabel() - { - Label = loclbl.Label, - LanguageCode = loclbl.LanguageCode - }; - lblList.Add(lbl); - } - } - updateReq.Label.LocalizedLabels.AddRange(lblList.ToArray()); - updateReq.Value = valueData; - updateReq.MergeLabels = true; - - UpdateOptionValueResponse UpdateResp = (UpdateOptionValueResponse)Command_Execute(updateReq, "Updating a PickList Element in Dataverse"); - if (UpdateResp == null) - return false; - } - else - { - // create request. - // Create a new insert request - InsertOptionValueRequest req = new InsertOptionValueRequest(); - - req.AttributeLogicalName = attribName; - req.EntityLogicalName = targetEntity; - req.Label = new Label(); - List lblList = new List(); - foreach (LocalizedLabel loclbl in locLabelList) - { - if (_loadedLCIDList.Contains(loclbl.LanguageCode)) - { - LocalizedLabel lbl = new LocalizedLabel() - { - Label = loclbl.Label, - LanguageCode = loclbl.LanguageCode - }; - lblList.Add(lbl); - } - } - req.Label.LocalizedLabels.AddRange(lblList.ToArray()); - req.Value = valueData; - - - InsertOptionValueResponse resp = (InsertOptionValueResponse)Command_Execute(req, "Creating a PickList Element in Dataverse"); - if (resp == null) - return false; - - } - - // Publish the update if asked to. - if (publishOnComplete) - return PublishEntity(targetEntity); - else - return true; - } - - /// - /// Publishes an entity to the production system, - /// used in conjunction with the Metadata services. - /// - /// Name of the entity to publish - /// True on success - public bool PublishEntity(string entityName) - { - // Now Publish the update. - string sPublishUpdateXml = - string.Format(CultureInfo.InvariantCulture, "{0}", - entityName); - - PublishXmlRequest pubReq = new PublishXmlRequest(); - pubReq.ParameterXml = sPublishUpdateXml; - - PublishXmlResponse rsp = (PublishXmlResponse)Command_Execute(pubReq, "Publishing a PickList Element in Dataverse"); - if (rsp != null) - return true; - else - return false; - } - - /// - /// Loads the Currently loaded languages for Dataverse - /// - /// - private bool LoadLCIDs() - { - // Now Publish the update. - // Check to see if the Language ID's are loaded. - if (_loadedLCIDList == null) - { - _loadedLCIDList = new List(); - - // load the Dataverse Language List. - RetrieveAvailableLanguagesRequest lanReq = new RetrieveAvailableLanguagesRequest(); - RetrieveAvailableLanguagesResponse rsp = (RetrieveAvailableLanguagesResponse)Command_Execute(lanReq, "Reading available languages from Dataverse"); - if (rsp == null) - return false; - if (rsp.LocaleIds != null) - { - foreach (int iLCID in rsp.LocaleIds) - { - if (_loadedLCIDList.Contains(iLCID)) - continue; - else - _loadedLCIDList.Add(iLCID); - } - } - } - return true; - } - - #endregion - - #endregion - - #region OAuth Token Cache - - /// - /// Clear the persistent and in-memory store cache - /// - /// - /// - public static bool RemoveOAuthTokenCache(string tokenCachePath = "") - { - throw new NotImplementedException(); - //If tokenCachePath is not supplied it will take from the constructor of token cache and delete the file. - //if (_CdsServiceClientTokenCache == null) - // _CdsServiceClientTokenCache = new CdsServiceClientTokenCache(tokenCachePath); - //return _CdsServiceClientTokenCache.Clear(tokenCachePath); - //TODO: Update for new Token cache providers. - //return false; - } - - #endregion - - #region DataverseUtilites - - /// - /// Adds paging related parameter to the input fetchXml - /// - /// Input fetch Xml - /// The number of records to be fetched - /// The page number - /// Page cookie - /// - private String AddPagingParametersToFetchXml(string fetchXml, int pageCount, int pageNum, string pageCookie) - { - if (String.IsNullOrWhiteSpace(fetchXml)) - { - return fetchXml; - } - - XmlDocument fetchdoc = XmlUtil.CreateXmlDocument(fetchXml); - XmlElement fetchroot = fetchdoc.DocumentElement; - - XmlAttribute pageAttribute = fetchdoc.CreateAttribute("page"); - pageAttribute.Value = pageNum.ToString(CultureInfo.InvariantCulture); - - XmlAttribute countAttribute = fetchdoc.CreateAttribute("count"); - countAttribute.Value = pageCount.ToString(CultureInfo.InvariantCulture); - - XmlAttribute pagingCookieAttribute = fetchdoc.CreateAttribute("paging-cookie"); - pagingCookieAttribute.Value = pageCookie; - - fetchroot.Attributes.Append(pageAttribute); - fetchroot.Attributes.Append(countAttribute); - fetchroot.Attributes.Append(pagingCookieAttribute); - - return fetchdoc.DocumentElement.OuterXml; - } - - /// - /// Makes a secure string - /// - /// - /// - public static SecureString MakeSecureString(string pass) - { - SecureString _pass = new SecureString(); - if (!string.IsNullOrEmpty(pass)) - { - foreach (char c in pass) - { - _pass.AppendChar(c); - } - _pass.MakeReadOnly(); // Lock it down. - return _pass; - } - return null; - } - - /// - /// Builds the Query expression to use with a Search. - /// - /// - /// - /// - /// - /// - private static QueryExpression BuildQueryFilter(string entityName, List searchParams, List fieldList, LogicalSearchOperator searchOperator) - { - // Create ColumnSet - ColumnSet cols = null; - if (fieldList != null) - { - cols = new ColumnSet(); - cols.Columns.AddRange(fieldList.ToArray()); - } - - List filters = BuildFilterList(searchParams); - - // Link Filter. - FilterExpression Queryfilter = new FilterExpression(); - Queryfilter.Filters.AddRange(filters); - - // Add Logical relationship. - if (searchOperator == LogicalSearchOperator.Or) - Queryfilter.FilterOperator = LogicalOperator.Or; - else - Queryfilter.FilterOperator = LogicalOperator.And; - - - // Build Query - QueryExpression query = new QueryExpression(); - query.EntityName = entityName; // Set to the requested entity Type - if (cols != null) - query.ColumnSet = cols; - else - query.ColumnSet = new ColumnSet(true);// new AllColumns(); - - query.Criteria = Queryfilter; - query.NoLock = true; // Added to remove locking on queries. - return query; - } - - /// - /// Creates a SearchFilterList from a Search string Dictionary - /// - /// Inbound Search Strings - /// List that will be populated - private static void BuildSearchFilterListFromSearchTerms(Dictionary inSearchParams, List outSearchList) - { - if (inSearchParams != null) - { - foreach (var item in inSearchParams) - { - DataverseSearchFilter f = new DataverseSearchFilter(); - f.FilterOperator = LogicalOperator.And; - f.SearchConditions.Add(new DataverseFilterConditionItem() - { - FieldName = item.Key, - FieldValue = item.Value, - FieldOperator = string.IsNullOrWhiteSpace(item.Value) ? ConditionOperator.Null : item.Value.Contains("%") ? ConditionOperator.Like : ConditionOperator.Equal - }); - outSearchList.Add(f); - } - } - } - - /// - /// Builds the filter list for a query - /// - /// - /// - private static List BuildFilterList(List searchParams) - { - List filters = new List(); - // Create Conditions - foreach (DataverseSearchFilter conditionItemList in searchParams) - { - FilterExpression filter = new FilterExpression(); - foreach (DataverseFilterConditionItem conditionItem in conditionItemList.SearchConditions) - { - ConditionExpression condition = new ConditionExpression(); - condition.AttributeName = conditionItem.FieldName; - condition.Operator = conditionItem.FieldOperator; - if (!(condition.Operator == ConditionOperator.NotNull || condition.Operator == ConditionOperator.Null)) - condition.Values.Add(conditionItem.FieldValue); - - filter.AddCondition(condition); - } - if (filter.Conditions.Count > 0) - { - filter.FilterOperator = conditionItemList.FilterOperator; - filters.Add(filter); - } - } - return filters; - } - - /// - /// Get the localize label from a Dataverse Label. - /// - /// - /// - private static string GetLocalLabel(Label localLabel) - { - foreach (LocalizedLabel lbl in localLabel.LocalizedLabels) - { - // try to get the current display langue code. - if (lbl.LanguageCode == CultureInfo.CurrentUICulture.LCID) - { - return lbl.Label; - } - } - return localLabel.UserLocalizedLabel.Label; - } - - /// - /// Adds data from a Entity to result set - /// - /// - /// - private static void AddDataToResultSet(ref Dictionary resultSet, Entity dataEntity) - { - if (dataEntity == null) - return; - if (resultSet == null) - return; - try - { - foreach (var p in dataEntity.Attributes) - { - resultSet.Add(p.Key + "_Property", p); - resultSet.Add(p.Key, dataEntity.FormattedValues.ContainsKey(p.Key) ? dataEntity.FormattedValues[p.Key] : p.Value); - } - - } - catch { } - } - - /// - /// Gets the Lookup Value GUID for any given entity name - /// - /// Entity you are looking for - /// Value you are looking for - /// ID of the lookup value in the entity - private Guid GetLookupValueForEntity(string entName, string Value) - { - // Check for existence of cached list. - if (_CachObject == null) - { - object objc = _connectionSvc.LocalMemoryCache.Get(_cachObjecName); - if (objc is Dictionary> workingObj) - _CachObject = workingObj; - - if (_CachObject == null) - _CachObject = new Dictionary>(); - } - - Guid guResultID = Guid.Empty; - - if ((_CachObject.ContainsKey(entName.ToString())) && (_CachObject[entName.ToString()].ContainsKey(Value))) - return (Guid)_CachObject[entName.ToString()][Value]; - - switch (entName) - { - case "transactioncurrency": - guResultID = LookupEntitiyID(Value, entName, "transactioncurrencyid", "currencyname"); - break; - case "subject": - guResultID = LookupEntitiyID(Value, entName, "subjectid", "title"); //LookupSubjectIDForName(Value); - break; - case "systemuser": - guResultID = LookupEntitiyID(Value, entName, "systemuserid", "domainname"); - break; - case "pricelevel": - guResultID = LookupEntitiyID(Value, entName, "pricelevelid", "name"); - break; - - case "product": - guResultID = LookupEntitiyID(Value, entName, "productid", "productnumber"); - break; - case "uom": - guResultID = LookupEntitiyID(Value, entName, "uomid", "name"); - break; - default: - return Guid.Empty; - } - - - // High effort objects that are generally not changed during the live cycle of a connection are cached here. - if (guResultID != Guid.Empty) - { - if (!_CachObject.ContainsKey(entName.ToString())) - _CachObject.Add(entName.ToString(), new Dictionary()); - _CachObject[entName.ToString()].Add(Value, guResultID); - - _connectionSvc.LocalMemoryCache.Set(_cachObjecName, _CachObject, DateTime.Now.AddMinutes(5)); - } - - return guResultID; - - } - - /// - /// Lookup a entity ID by a single search element. - /// Used for Lookup Lists. - /// - /// Text to search for - /// Entity Type to Search in - /// Field that contains the id - /// Field to Search against - /// Guid of Entity or Empty Guid - private Guid LookupEntitiyID(string SearchValue, string ent, string IDField, string SearchField) - { - try - { - Guid guID = Guid.Empty; - List FieldList = new List(); - FieldList.Add(IDField); - - Dictionary SearchList = new Dictionary(); - SearchList.Add(SearchField, SearchValue); - - Dictionary> rslts = GetEntityDataBySearchParams(ent, SearchList, LogicalSearchOperator.None, FieldList); - - if (rslts != null) - { - foreach (Dictionary rsl in rslts.Values) - { - if (rsl.ContainsKey(IDField)) - { - guID = (Guid)rsl[IDField]; - } - } - } - return guID; - } - catch - { - return Guid.Empty; - } - } - - /// - /// Adds values for an update to a Dataverse propertyList - /// - /// - /// - /// - internal void AddValueToPropertyList(KeyValuePair Field, AttributeCollection PropertyList) - { - if (string.IsNullOrEmpty(Field.Key)) - // throw exception - throw new System.ArgumentOutOfRangeException("valueArray", "Missing Dataverse field name"); - - try - { - switch (Field.Value.Type) - { - - case DataverseFieldType.Boolean: - PropertyList.Add(new KeyValuePair(Field.Key, (bool)Field.Value.Value)); - break; - - case DataverseFieldType.DateTime: - PropertyList.Add(new KeyValuePair(Field.Key, (DateTime)Field.Value.Value)); - break; - - case DataverseFieldType.Decimal: - PropertyList.Add(new KeyValuePair(Field.Key, Convert.ToDecimal(Field.Value.Value))); - break; - - case DataverseFieldType.Float: - PropertyList.Add(new KeyValuePair(Field.Key, Convert.ToDouble(Field.Value.Value))); - break; - - case DataverseFieldType.Money: - PropertyList.Add(new KeyValuePair(Field.Key, new Money(Convert.ToDecimal(Field.Value.Value)))); - break; - - case DataverseFieldType.Number: - PropertyList.Add(new KeyValuePair(Field.Key, (int)Field.Value.Value)); - break; - - case DataverseFieldType.Customer: - PropertyList.Add(new KeyValuePair(Field.Key, new EntityReference(Field.Value.ReferencedEntity, (Guid)Field.Value.Value))); - break; - - case DataverseFieldType.Lookup: - PropertyList.Add(new KeyValuePair(Field.Key, new EntityReference(Field.Value.ReferencedEntity, (Guid)Field.Value.Value))); - break; - - case DataverseFieldType.Picklist: - PropertyList.Add(new KeyValuePair(Field.Key, new OptionSetValue((int)Field.Value.Value))); - break; - - case DataverseFieldType.String: - PropertyList.Add(new KeyValuePair(Field.Key, (string)Field.Value.Value)); - break; - - case DataverseFieldType.Raw: - PropertyList.Add(new KeyValuePair(Field.Key, Field.Value.Value)); - break; - - case DataverseFieldType.UniqueIdentifier: - PropertyList.Add(new KeyValuePair(Field.Key, (Guid)Field.Value.Value)); - break; - } - } - catch (InvalidCastException castEx) - { - _logEntry.Log(string.Format(CultureInfo.InvariantCulture, "Failed when casting DataverseDataTypeWrapper wrapped objects to the Dataverse Type. Field : {0}", Field.Key), TraceEventType.Error, castEx); - throw; - } - catch (System.Exception ex) - { - _logEntry.Log(string.Format(CultureInfo.InvariantCulture, "Failed when casting DataverseDataTypeWrapper wrapped objects to the Dataverse Type. Field : {0}", Field.Key), TraceEventType.Error, ex); - throw; - } - - } - - /// - /// Creates and Returns a Search Result Set - /// - /// - /// - private static Dictionary> CreateResultDataSet(EntityCollection resp) - { - Dictionary> Results = new Dictionary>(); - foreach (Entity bEnt in resp.Entities) - { - // Not really doing an update here... just turning it into something I can walk. - Dictionary SearchRstls = new Dictionary(); - AddDataToResultSet(ref SearchRstls, bEnt); - // Add Ent name and ID - SearchRstls.Add("ReturnProperty_EntityName", bEnt.LogicalName); - SearchRstls.Add("ReturnProperty_Id ", bEnt.Id); - Results.Add(Guid.NewGuid().ToString(), SearchRstls); - } - if (Results.Count > 0) - return Results; - else - return null; - } - - /// - /// Adds a request to a batch with display and handling logic - /// will fail out if batching is not enabled. - /// - /// ID of the batch to add too - /// Organization request to Add - /// Batch Add Text, this is the text that will be reflected when the batch is added - appears in the batch diags - /// Success Added Batch - appears in webSvcActions diag - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// - internal bool AddRequestToBatch(Guid batchId, OrganizationRequest req, string batchTagText, string successText, bool bypassPluginExecution) - { - if (batchId != Guid.Empty) - { - // if request should bypass plugin exec. - if (bypassPluginExecution && - Utilities.FeatureVersionMinimums.IsFeatureValidForEnviroment(ConnectedOrgVersion, Utilities.FeatureVersionMinimums.AllowBypassCustomPlugin)) - req.Parameters.Add(Utilities.RequestHeaders.BYPASSCUSTOMPLUGINEXECUTION, true); - - if (IsBatchOperationsAvailable) - { - if (_batchManager.AddNewRequestToBatch(batchId, req, batchTagText)) - { - _logEntry.Log(successText, TraceEventType.Verbose); - return true; - } - else - _logEntry.Log("Unable to add request to batch queue, Executing normally", TraceEventType.Warning); - } - else - { - // Error and fall though. - _logEntry.Log("Unable to add request to batch, Batching is not currently available, Executing normally", TraceEventType.Warning); - } - } - return false; - } - - #region XRM Commands and handlers - - #region Public Access to direct commands. - - /// - /// Executes a web request against Xrm WebAPI. - /// - /// Here you would pass the path and query parameters that you wish to pass onto the WebAPI. - /// The format used here is as follows: - /// {APIURI}/api/data/v{instance version}/querystring. - /// For example, - /// if you wanted to get data back from an account, you would pass the following: - /// accounts(id) - /// which creates: get - https://myinstance.crm.dynamics.com/api/data/v9.0/accounts(id) - /// if you were creating an account, you would pass the following: - /// accounts - /// which creates: post - https://myinstance.crm.dynamics.com/api/data/v9.0/accounts - body contains the data. - /// - /// Method to use for the request - /// Content your passing to the request - /// Headers in addition to the default headers added by for Executing a web request - /// Content Type attach to the request. this defaults to application/json if not set. - /// Cancellation token for the request - /// - public HttpResponseMessage ExecuteWebRequest(HttpMethod method, string queryString, string body, Dictionary> customHeaders, string contentType = default, CancellationToken cancellationToken = default) - { - _logEntry.ResetLastError(); // Reset Last Error - if (DataverseService == null) - { - _logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); - return null; - } - - if (string.IsNullOrEmpty(queryString) && string.IsNullOrEmpty(body)) - { - _logEntry.Log("Execute Web Request failed, queryString and body cannot be null", TraceEventType.Error); - return null; - } - - if (Uri.TryCreate(queryString, UriKind.Absolute, out var urlPath)) - { - // Was able to create a URL here... Need to make sure that we strip out everything up to the last segment. - string baseQueryString = urlPath.Segments.Last(); - if (!string.IsNullOrEmpty(urlPath.Query)) - queryString = baseQueryString + urlPath.Query; - else - queryString = baseQueryString; - } - - var result = _connectionSvc.Command_WebExecuteAsync(queryString, body, method, customHeaders, contentType, string.Empty, CallerId, _disableConnectionLocking, MaxRetryCount, RetryPauseTime, cancellationToken: cancellationToken).Result; - if (result == null) - throw LastException; - else - return result; - } - - /// - /// Executes a Dataverse Organization Request (thread safe) and returns the organization response object. Also adds metrics for logging support. - /// - /// Organization Request to run - /// Message identifying what this request in logging. - /// When True, uses the webAPI to execute the organization Request. This works for only Create at this time. - /// Result of request or null. - public OrganizationResponse ExecuteOrganizationRequest(OrganizationRequest req, string logMessageTag = "User Defined", bool useWebAPI = false) - { - return ExecuteOrganizationRequestImpl(req, logMessageTag, useWebAPI, false); - } - - private OrganizationResponse ExecuteOrganizationRequestImpl(OrganizationRequest req, string logMessageTag = "User Defined", bool useWebAPI = false, bool bypassPluginExecution = false) - { - if (req != null) - { - useWebAPI = Utilities.IsRequestValidForTranslationToWebAPI(req); - if (!useWebAPI) - { - return Command_Execute(req, logMessageTag, bypassPluginExecution); - } - else - { - // use Web API. - return _connectionSvc.Command_WebAPIProcess_ExecuteAsync(req, logMessageTag, bypassPluginExecution, _metadataUtlity, CallerId, _disableConnectionLocking, MaxRetryCount, RetryPauseTime, new CancellationToken()).Result; - } - } - else - { - _logEntry.Log("Execute Organization Request failed, Organization Request cannot be null", TraceEventType.Error); - return null; - } - } - - private async Task ExecuteOrganizationRequestAsyncImpl(OrganizationRequest req, CancellationToken cancellationToken, string logMessageTag = "User Defined", bool useWebAPI = false, bool bypassPluginExecution = false) - { - cancellationToken.ThrowIfCancellationRequested(); - if (req != null) - { - useWebAPI = Utilities.IsRequestValidForTranslationToWebAPI(req); - if (!useWebAPI) - { - return await Command_ExecuteAsync(req, logMessageTag, cancellationToken, bypassPluginExecution).ConfigureAwait(false); - } - else - { - // use Web API. - return await _connectionSvc.Command_WebAPIProcess_ExecuteAsync(req, logMessageTag, bypassPluginExecution, _metadataUtlity, CallerId, _disableConnectionLocking, MaxRetryCount, RetryPauseTime, cancellationToken).ConfigureAwait(false); - } - } - else - { - _logEntry.Log("Execute Organization Request failed, Organization Request cannot be null", TraceEventType.Error); - return null; - } - } - - /// - /// Executes a row level delete on a Dataverse entity ( thread safe ) and returns true or false. Also adds metrics for logging support. - /// - /// Name of the Entity to delete from - /// ID of the row to delete - /// Message identifying what this request in logging - /// True on success, False on fail. - public bool ExecuteEntityDeleteRequest(string entName, Guid entId, string logMessageTag = "User Defined") - { - if (string.IsNullOrWhiteSpace(entName)) - { - _logEntry.Log("Execute Delete Request failed, Entity Name cannot be null or empty", TraceEventType.Error); - return false; - } - if (entId == Guid.Empty) - { - _logEntry.Log("Execute Delete Request failed, Guid to delete cannot be null or empty", TraceEventType.Error); - return false; - } - - DeleteRequest req = new DeleteRequest(); - req.Target = new EntityReference(entName, entId); - - DeleteResponse resp = (DeleteResponse)Command_Execute(req, string.Format(CultureInfo.InvariantCulture, "Trying to Delete. Entity = {0}, ID = {1}", entName, entId)); - if (resp != null) - { - return true; - } - return false; - } - - #endregion - - - /// - /// - /// Imports a Dataverse solution to the Dataverse Server currently connected. - /// *** Note: this is a blocking call and will take time to Import to Dataverse *** - /// - /// - /// Path to the Solution File - /// Activate Plugin's and workflows on the Solution - /// This will populate with the Import ID even if the request failed. - /// You can use this ID to request status on the import via a request to the ImportJob entity. - /// Forces an overwrite of unmanaged customizations of the managed solution you are installing, defaults to false - /// Skips dependency against dependencies flagged as product update, defaults to false - /// Applies only on Dataverse organizations version 7.2 or higher. This imports the Dataverse solution as a holding solution utilizing the “As Holding” capability of ImportSolution - /// Internal Microsoft use only - /// Requires the use of an Async Job to do the import. - /// Extra parameters - /// Returns the Import Solution Job ID. To find the status of the job, query the ImportJob Entity using GetEntityDataByID using the returned value of this method - internal Guid ImportSolutionToImpl(string solutionPath, out Guid importId, bool activatePlugIns, bool overwriteUnManagedCustomizations, bool skipDependancyOnProductUpdateCheckOnInstall, bool importAsHoldingSolution, bool isInternalUpgrade, bool useAsync, Dictionary extraParameters) - { - _logEntry.ResetLastError(); // Reset Last Error - importId = Guid.Empty; - if (DataverseService == null) - { - _logEntry.Log("Dataverse Service not initialized", TraceEventType.Error); - return Guid.Empty; - } - - if (string.IsNullOrWhiteSpace(solutionPath)) - { - this._logEntry.Log("************ Exception on ImportSolutionToImpl, SolutionPath is required", TraceEventType.Error); - return Guid.Empty; - } - - // determine if the system is connected to OnPrem - bool isConnectedToOnPrem = (_connectionSvc.ConnectedOrganizationDetail != null && string.IsNullOrEmpty(_connectionSvc.ConnectedOrganizationDetail.Geo)); - - //Extract extra parameters if they exist - string solutionName = string.Empty; - LayerDesiredOrder desiredLayerOrder = null; - bool? asyncRibbonProcessing = null; - EntityCollection componetsToProcess = null; - bool? convertToManaged = null; - bool? isTemplateModeImport = null; - string templateSuffix = null; - - if (extraParameters != null) - { - solutionName = extraParameters.ContainsKey(ImportSolutionProperties.SOLUTIONNAMEPARAM) ? extraParameters[ImportSolutionProperties.SOLUTIONNAMEPARAM].ToString() : string.Empty; - desiredLayerOrder = extraParameters.ContainsKey(ImportSolutionProperties.DESIREDLAYERORDERPARAM) ? extraParameters[ImportSolutionProperties.DESIREDLAYERORDERPARAM] as LayerDesiredOrder : null; - componetsToProcess = extraParameters.ContainsKey(ImportSolutionProperties.COMPONENTPARAMETERSPARAM) ? extraParameters[ImportSolutionProperties.COMPONENTPARAMETERSPARAM] as EntityCollection : null; - convertToManaged = extraParameters.ContainsKey(ImportSolutionProperties.CONVERTTOMANAGED) ? extraParameters[ImportSolutionProperties.CONVERTTOMANAGED] as bool? : null; - isTemplateModeImport = extraParameters.ContainsKey(ImportSolutionProperties.ISTEMPLATEMODE) ? extraParameters[ImportSolutionProperties.ISTEMPLATEMODE] as bool? : null; - templateSuffix = extraParameters.ContainsKey(ImportSolutionProperties.TEMPLATESUFFIX) ? extraParameters[ImportSolutionProperties.TEMPLATESUFFIX].ToString() : string.Empty; - - // Pick up the data from the request, if the request has the AsyncRibbonProcessing flag, pick up the value of it. - asyncRibbonProcessing = extraParameters.ContainsKey(ImportSolutionProperties.ASYNCRIBBONPROCESSING) ? extraParameters[ImportSolutionProperties.ASYNCRIBBONPROCESSING] as bool? : null; - // If the value is populated, and t - if (asyncRibbonProcessing != null && asyncRibbonProcessing.HasValue) - { - if (isConnectedToOnPrem) - { - // Not supported for OnPrem. - // reset the asyncRibbonProcess to Null. - this._logEntry.Log($"ImportSolution request contains {ImportSolutionProperties.ASYNCRIBBONPROCESSING} property. This is not valid for OnPremise deployments and will be removed", TraceEventType.Warning); - asyncRibbonProcessing = null; - } - else - { - if (!Utilities.FeatureVersionMinimums.IsFeatureValidForEnviroment(_connectionSvc?.OrganizationVersion, Utilities.FeatureVersionMinimums.AllowAsyncRibbonProcessing)) - { - // Not supported on this version of Dataverse - this._logEntry.Log($"ImportSolution request contains {ImportSolutionProperties.ASYNCRIBBONPROCESSING} property. This request Dataverse version {Utilities.FeatureVersionMinimums.AllowAsyncRibbonProcessing.ToString()} or above. Current Dataverse version is {_connectionSvc?.OrganizationVersion}. This property will be removed", TraceEventType.Warning); - asyncRibbonProcessing = null; - } - } - } - - if (componetsToProcess != null) - { - if (isConnectedToOnPrem) - { - this._logEntry.Log($"ImportSolution request contains {ImportSolutionProperties.COMPONENTPARAMETERSPARAM} property. This is not valid for OnPremise deployments and will be removed", TraceEventType.Warning); - componetsToProcess = null; - } - else - { - if (!Utilities.FeatureVersionMinimums.IsFeatureValidForEnviroment(_connectionSvc?.OrganizationVersion, Utilities.FeatureVersionMinimums.AllowComponetInfoProcessing)) - { - // Not supported on this version of Dataverse - this._logEntry.Log($"ImportSolution request contains {ImportSolutionProperties.COMPONENTPARAMETERSPARAM} property. This request Dataverse version {Utilities.FeatureVersionMinimums.AllowComponetInfoProcessing.ToString()} or above. Current Dataverse version is {_connectionSvc?.OrganizationVersion}. This property will be removed", TraceEventType.Warning); - componetsToProcess = null; - } - } - } - - if (isTemplateModeImport != null) - { - if (isConnectedToOnPrem) - { - this._logEntry.Log($"ImportSolution request contains {ImportSolutionProperties.ISTEMPLATEMODE} property. This is not valid for OnPremise deployments and will be removed", TraceEventType.Warning); - isTemplateModeImport = null; - } - else - { - if (!Utilities.FeatureVersionMinimums.IsFeatureValidForEnviroment(_connectionSvc?.OrganizationVersion, Utilities.FeatureVersionMinimums.AllowTemplateSolutionImport)) - { - // Not supported on this version of Dataverse - this._logEntry.Log($"ImportSolution request contains {ImportSolutionProperties.ISTEMPLATEMODE} property. This request Dataverse version {Utilities.FeatureVersionMinimums.AllowTemplateSolutionImport.ToString()} or above. Current Dataverse version is {_connectionSvc?.OrganizationVersion}. This property will be removed", TraceEventType.Warning); - isTemplateModeImport = null; - } - } - } - } - - string solutionNameForLogging = string.IsNullOrWhiteSpace(solutionName) ? string.Empty : string.Concat(solutionName, " - "); - - // try to load the file from the file system - if (File.Exists(solutionPath)) - { - try - { - importId = Guid.NewGuid(); - byte[] fileData = File.ReadAllBytes(solutionPath); - ImportSolutionRequest SolutionImportRequest = new ImportSolutionRequest() - { - CustomizationFile = fileData, - PublishWorkflows = activatePlugIns, - ImportJobId = importId, - OverwriteUnmanagedCustomizations = overwriteUnManagedCustomizations - }; - - //If the desiredLayerOrder is null don't add it to the request. This ensures backward compatibility. It makes old packages work on old builds - if (desiredLayerOrder != null) - { - //If package contains the LayerDesiredOrder hint but the server doesn't support the new message, we want the package to fail - //The server will throw - "Unrecognized request parameter: LayerDesiredOrder" - That's the desired behavior - //The hint is only enforced on the first time a solution is added to the org. If we allow it to go, the import will succeed, but the desired state won't be achieved - SolutionImportRequest.LayerDesiredOrder = desiredLayerOrder; - - string solutionsInHint = string.Join(",", desiredLayerOrder.Solutions.Select(n => n.Name).ToArray()); - - _logEntry.Log(string.Format(CultureInfo.InvariantCulture, "{0}DesiredLayerOrder clause present: Type: {1}, Solutions: {2}", solutionNameForLogging, desiredLayerOrder.Type, solutionsInHint), TraceEventType.Verbose); - } - - if (asyncRibbonProcessing != null && asyncRibbonProcessing == true) - { - SolutionImportRequest.AsyncRibbonProcessing = true; - SolutionImportRequest.SkipQueueRibbonJob = true; - _logEntry.Log(string.Format(CultureInfo.InvariantCulture, "{0} AsyncRibbonProcessing: {1}", solutionNameForLogging, true), TraceEventType.Verbose); - } - - if (componetsToProcess != null) - { - SolutionImportRequest.ComponentParameters = componetsToProcess; - } - - if (convertToManaged != null) - { - SolutionImportRequest.ConvertToManaged = convertToManaged.Value; - } - - if (isTemplateModeImport != null && isTemplateModeImport.Value) - { - SolutionImportRequest.Parameters[ImportSolutionProperties.ISTEMPLATEMODE] = isTemplateModeImport.Value; - SolutionImportRequest.Parameters[ImportSolutionProperties.TEMPLATESUFFIX] = templateSuffix; - } - - if (IsBatchOperationsAvailable) - { - // Support for features added in UR12 - SolutionImportRequest.SkipProductUpdateDependencies = skipDependancyOnProductUpdateCheckOnInstall; - } - - if (importAsHoldingSolution) // If Import as Holding is set.. - { - // Check for Min version of Dataverse for support of Import as Holding solution. - if (Utilities.FeatureVersionMinimums.IsFeatureValidForEnviroment(_connectionSvc?.OrganizationVersion, Utilities.FeatureVersionMinimums.ImportHoldingSolution)) - { - // Use Parameters to add the property here to support the underlying Xrm API on the incorrect version. - SolutionImportRequest.Parameters.Add("HoldingSolution", importAsHoldingSolution); - } - } - - // Set IsInternalUpgrade flag on request only for upgrade scenario for V9 only. - if (isInternalUpgrade) - { - if (Utilities.FeatureVersionMinimums.IsFeatureValidForEnviroment(_connectionSvc?.OrganizationVersion, Utilities.FeatureVersionMinimums.InternalUpgradeSolution)) - { - SolutionImportRequest.Parameters["IsInternalUpgrade"] = true; - } - } - - if (useAsync) - { - // Assign Tracking ID - Guid requestTrackingId = Guid.NewGuid(); - SolutionImportRequest.RequestId = requestTrackingId; - - if (!isConnectedToOnPrem && Utilities.FeatureVersionMinimums.IsFeatureValidForEnviroment(ConnectedOrgVersion, Utilities.FeatureVersionMinimums.AllowImportSolutionAsyncV2)) - { - // map import request to Async Model - ImportSolutionAsyncRequest asynImportRequest = new ImportSolutionAsyncRequest() - { - AsyncRibbonProcessing = SolutionImportRequest.AsyncRibbonProcessing, - ComponentParameters = SolutionImportRequest.ComponentParameters, - ConvertToManaged = SolutionImportRequest.ConvertToManaged, - CustomizationFile = SolutionImportRequest.CustomizationFile, - HoldingSolution = SolutionImportRequest.HoldingSolution, - LayerDesiredOrder = SolutionImportRequest.LayerDesiredOrder, - OverwriteUnmanagedCustomizations = SolutionImportRequest.OverwriteUnmanagedCustomizations, - Parameters = SolutionImportRequest.Parameters, - PublishWorkflows = SolutionImportRequest.PublishWorkflows, - RequestId = SolutionImportRequest.RequestId, - SkipProductUpdateDependencies = SolutionImportRequest.SkipProductUpdateDependencies, - SkipQueueRibbonJob = SolutionImportRequest.SkipQueueRibbonJob - }; - - // remove unsupported parameter from importsolutionasync request. - if (asynImportRequest.Parameters.ContainsKey("ImportJobId")) - asynImportRequest.Parameters.Remove("ImportJobId"); - - _logEntry.Log(string.Format(CultureInfo.InvariantCulture, "{1}Created Async ImportSolutionAsyncRequest : RequestID={0} ", requestTrackingId.ToString(), solutionNameForLogging), TraceEventType.Verbose); - ImportSolutionAsyncResponse asyncResp = (ImportSolutionAsyncResponse)Command_Execute(asynImportRequest, solutionNameForLogging + "Executing Request for ImportSolutionAsyncRequest : "); - if (asyncResp == null) - return Guid.Empty; - else - return asyncResp.AsyncOperationId; - } - else - { - // Creating Async Solution Import request. - ExecuteAsyncRequest req = new ExecuteAsyncRequest() { Request = SolutionImportRequest }; - _logEntry.Log(string.Format(CultureInfo.InvariantCulture, "{1}Created Async ImportSolutionRequest : RequestID={0} ", - requestTrackingId.ToString(), solutionNameForLogging), TraceEventType.Verbose); - ExecuteAsyncResponse asyncResp = (ExecuteAsyncResponse)Command_Execute(req, solutionNameForLogging + "Executing Request for ImportSolutionToAsync : "); - if (asyncResp == null) - return Guid.Empty; - else - return asyncResp.AsyncJobId; - } - } - else - { - ImportSolutionResponse resp = (ImportSolutionResponse)Command_Execute(SolutionImportRequest, solutionNameForLogging + "Executing ImportSolutionRequest for ImportSolution"); - if (resp == null) - return Guid.Empty; - else - return importId; - } - } - #region Exception handlers for files - catch (UnauthorizedAccessException ex) - { - this._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception on ImportSolution, Unauthorized Access to file: {0}", solutionPath), TraceEventType.Error, ex); - return Guid.Empty; - } - catch (ArgumentNullException ex) - { - this._logEntry.Log("************ Exception on ImportSolutionToCds, File path not specified", TraceEventType.Error, ex); - return Guid.Empty; - } - catch (ArgumentException ex) - { - this._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception on ImportSolution, File path is invalid: {0}", solutionPath), TraceEventType.Error, ex); - return Guid.Empty; - } - catch (PathTooLongException ex) - { - this._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception on ImportSolution, File path is too long. Paths must be less than 248 characters, and file names must be less than 260 characters\n{0}", solutionPath), TraceEventType.Error, ex); - return Guid.Empty; - } - catch (DirectoryNotFoundException ex) - { - this._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception on ImportSolution, File path is invalid: {0}", solutionPath), TraceEventType.Error, ex); - return Guid.Empty; - } - catch (FileNotFoundException ex) - { - this._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception on ImportSolution, File Not Found: {0}", solutionPath), TraceEventType.Error, ex); - return Guid.Empty; - } - catch (NotSupportedException ex) - { - this._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception on ImportSolution, File path or name is invalid: {0}", solutionPath), TraceEventType.Error, ex); - return Guid.Empty; - } - #endregion - } - else - { - this._logEntry.Log(string.Format(CultureInfo.InvariantCulture, "************ Exception on ImportSolution, File path specified in dataMapXml is not found: {0}", solutionPath), TraceEventType.Error); - return Guid.Empty; - } - } - - /// - /// Executes a Dataverse Create Request and returns the organization response object. - /// Uses an Async pattern to allow for the thread to be backgrounded. - /// - /// Request to run - /// Formatted Error string - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// Propagates notification that operations should be canceled. - /// Result of create request or null. - internal async Task Command_ExecuteAsync(OrganizationRequest req, string errorStringCheck, System.Threading.CancellationToken cancellationToken, bool bypassPluginExecution = false) - { - if (DataverseServiceAsync != null) - { - // if created based on Async Client. - return await Command_ExecuteAsyncImpl(req, errorStringCheck, cancellationToken, bypassPluginExecution).ConfigureAwait(false); - } - else - { - // if not use task.run(). - return await Task.Run(() => Command_Execute(req, errorStringCheck, bypassPluginExecution), cancellationToken).ConfigureAwait(false); - } - - } - - /// - /// Executes a Dataverse Create Request and returns the organization response object. - /// - /// Request to run - /// Formatted Error string - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// - /// Result of create request or null. - internal async Task Command_ExecuteAsyncImpl(OrganizationRequest req, string errorStringCheck, System.Threading.CancellationToken cancellationToken, bool bypassPluginExecution = false) - { - Guid requestTrackingId = Guid.NewGuid(); - OrganizationResponse resp = null; - Stopwatch logDt = new Stopwatch(); - TimeSpan LockWait = TimeSpan.Zero; - int retryCount = 0; - bool retry = false; - - do - { - try - { - cancellationToken.ThrowIfCancellationRequested(); - _retryPauseTimeRunning = _configuration.Value.RetryPauseTime; - retry = false; - if (!_disableConnectionLocking) - if (_lockObject == null) - _lockObject = new object(); - - if (_connectionSvc != null && _connectionSvc.AuthenticationTypeInUse == AuthenticationType.OAuth) - _connectionSvc.CalledbyExecuteRequest = true; - OrganizationResponse rsp = null; + if (_connectionSvc != null && _connectionSvc.AuthenticationTypeInUse == AuthenticationType.OAuth) + _connectionSvc.CalledbyExecuteRequest = true; + OrganizationResponse rsp = null; // Check to see if a Tracking ID has allready been assigned, if (!req.RequestId.HasValue || (req.RequestId.HasValue && req.RequestId.Value == Guid.Empty)) @@ -5451,7 +1648,7 @@ internal async Task Command_ExecuteAsyncImpl(OrganizationR if (bypassPluginExecution && Utilities.FeatureVersionMinimums.IsFeatureValidForEnviroment(_connectionSvc?.OrganizationVersion, Utilities.FeatureVersionMinimums.AllowBypassCustomPlugin)) req.Parameters.Add(Utilities.RequestHeaders.BYPASSCUSTOMPLUGINEXECUTION, true); - _logEntry.Log(string.Format(CultureInfo.InvariantCulture, "Execute Command - {0}{1}: RequestID={2} {3}", + _logEntry.Log(string.Format(CultureInfo.InvariantCulture, "Execute Command - {0}{1}: {3}RequestID={2}", req.RequestName, string.IsNullOrEmpty(errorStringCheck) ? "" : $" : {errorStringCheck} ", requestTrackingId.ToString(), @@ -5459,7 +1656,7 @@ internal async Task Command_ExecuteAsyncImpl(OrganizationR ), TraceEventType.Verbose); logDt.Restart(); - rsp = await DataverseServiceAsync.ExecuteAsync(req); + rsp = await DataverseServiceAsync.ExecuteAsync(req).ConfigureAwait(false); logDt.Stop(); _logEntry.Log(string.Format(CultureInfo.InvariantCulture, "Executed Command - {0}{2}: {5}RequestID={3} {4}: duration={1}", @@ -5544,7 +1741,7 @@ internal OrganizationResponse Command_Execute(OrganizationRequest req, string er if (bypassPluginExecution && Utilities.FeatureVersionMinimums.IsFeatureValidForEnviroment(_connectionSvc?.OrganizationVersion, Utilities.FeatureVersionMinimums.AllowBypassCustomPlugin)) req.Parameters.Add(Utilities.RequestHeaders.BYPASSCUSTOMPLUGINEXECUTION, true); - _logEntry.Log(string.Format(CultureInfo.InvariantCulture, "Execute Command - {0}{1}: RequestID={2} {3}", + _logEntry.Log(string.Format(CultureInfo.InvariantCulture, "Execute Command - {0}{1}: {3}RequestID={2}", req.RequestName, string.IsNullOrEmpty(errorStringCheck) ? "" : $" : {errorStringCheck} ", requestTrackingId.ToString(), @@ -5660,366 +1857,28 @@ private bool ShouldRetry(OrganizationRequest req, Exception ex, int retryCount, } #endregion - #endregion - #region Support classes - - /// - /// PickList data - /// - public sealed class PickListMetaElement - { - /// - /// Current value of the PickList Item - /// - public string ActualValue { get; set; } - /// - /// Displayed Label - /// - public string PickListLabel { get; set; } - /// - /// Displayed value for the PickList - /// - public string DisplayValue { get; set; } - /// - /// Array of Potential Pick List Items. - /// - public List Items { get; set; } - - /// - /// Default Constructor - /// - public PickListMetaElement() - { - Items = new List(); - } - - /// - /// Constructs a PickList item with data. - /// - /// - /// - /// - public PickListMetaElement(string actualValue, string displayValue, string pickListLabel) - { - Items = new List(); - ActualValue = actualValue; - PickListLabel = pickListLabel; - DisplayValue = displayValue; - } - } - - /// - /// PickList Item - /// - public sealed class PickListItem - { - /// - /// Display label for the PickList Item - /// - public string DisplayLabel { get; set; } - /// - /// ID of the picklist item - /// - public int PickListItemId { get; set; } - - /// - /// Default Constructor - /// - public PickListItem() - { - } - - /// - /// Constructor with data. - /// - /// - /// - public PickListItem(string label, int id) - { - DisplayLabel = label; - PickListItemId = id; - } - } - - /// - /// Dataverse Filter class. - /// - public sealed class DataverseSearchFilter - { - /// - /// List of Dataverse Filter conditions - /// - public List SearchConditions { get; set; } - /// - /// Dataverse Filter Operator - /// - public LogicalOperator FilterOperator { get; set; } - - /// - /// Creates an empty Dataverse Search Filter. - /// - public DataverseSearchFilter() - { - SearchConditions = new List(); - } - } - - /// - /// Dataverse Filter item. - /// - public sealed class DataverseFilterConditionItem - { - /// - /// Dataverse Field name to Filter on - /// - public string FieldName { get; set; } - /// - /// Value to use for the Filter - /// - public object FieldValue { get; set; } - /// - /// Dataverse Operator to apply - /// - public ConditionOperator FieldOperator { get; set; } - - } - /// - /// Describes an import request for Dataverse - /// - public sealed class ImportRequest - { - #region Vars - // Import Items.. - /// - /// Name of the Import Request. this Name will appear in Dataverse - /// - public string ImportName { get; set; } - /// - /// Sets or gets the Import Mode. - /// - public ImportMode Mode { get; set; } - - // Import Map Items. - /// - /// ID of the DataMap to use - /// - public Guid DataMapFileId { get; set; } - /// - /// Name of the DataMap File to use - /// ID or Name is required - /// - public string DataMapFileName { get; set; } - - /// - /// if True, infers the map from the type of entity requested.. - /// - public bool UseSystemMap { get; set; } - - /// - /// List of files to import in this job, there must be at least one. - /// - public List Files { get; set; } - - - #endregion - - /// - /// Mode of the Import, Update or Create - /// - public enum ImportMode - { - /// - /// Create a new Import - /// - Create = 0, - /// - /// Update to Imported Items - /// - Update = 1 - } - - /// - /// Default constructor - /// - public ImportRequest() - { - Files = new List(); - } - - } - - /// - /// Describes an Individual Import Item. + /// Makes a secure string /// - public class ImportFileItem + /// + /// + public static SecureString MakeSecureString(string pass) { - /// - /// File Name of Individual file - /// - public string FileName { get; set; } - /// - /// Type of Import file.. XML or CSV - /// - public FileTypeCode FileType { get; set; } - /// - /// This is the CSV file you wish to import, - /// - public string FileContentToImport { get; set; } - /// - /// This enabled duplicate detection rules - /// - public bool EnableDuplicateDetection { get; set; } - /// - /// Name of the entity that Originated the data. - /// - public string SourceEntityName { get; set; } - /// - /// Name of the entity that Target Entity the data. - /// - public string TargetEntityName { get; set; } - /// - /// This is the delimiter for the Data, - /// - public DataDelimiterCode DataDelimiter { get; set; } - /// - /// this is the field separator - /// - public FieldDelimiterCode FieldDelimiter { get; set; } - /// - /// Is the first row of the CSV the RowHeader? - /// - public bool IsFirstRowHeader { get; set; } - /// - /// UserID or Team ID of the Record Owner ( from systemuser ) - /// - public Guid RecordOwner { get; set; } - /// - /// Set true if the Record Owner is a Team - /// - public bool IsRecordOwnerATeam { get; set; } - - /// - /// Key used to delimit data in the import file - /// - public enum DataDelimiterCode - { - /// - /// Specifies " - /// - DoubleQuotes = 1, // " - /// - /// Specifies no delimiter - /// - None = 2, // - /// - /// Specifies ' - /// - SingleQuote = 3 // ' - } - - /// - /// Key used to delimit fields in the import file - /// - public enum FieldDelimiterCode - { - /// - /// Specifies : - /// - Colon = 1, - /// - /// Specifies , - /// - Comma = 2, - /// - /// Specifies ' - /// - SingleQuote = 3 - } - - /// - /// Type if file described in the FileContentToImport - /// - public enum FileTypeCode + SecureString _pass = new SecureString(); + if (!string.IsNullOrEmpty(pass)) { - /// - /// CSV File Type - /// - CSV = 0, - /// - /// XML File type - /// - XML = 1 + foreach (char c in pass) + { + _pass.AppendChar(c); + } + _pass.MakeReadOnly(); // Lock it down. + return _pass; } - - } - - /// - /// Logical Search Pram to apply to over all search. - /// - public enum LogicalSearchOperator - { - /// - /// Do not apply the Search Operator - /// - None = 0, - /// - /// Or Search - /// - Or = 1, - /// - /// And Search - /// - And = 2 - } - - /// - /// Logical Search Pram to apply to over all search. - /// - public enum LogicalSortOrder - { - /// - /// Sort in Ascending - /// - Ascending = 0, - /// - /// Sort in Descending - /// - Descending = 1, - } - - /// - /// Used with GetFormIdsForEntity Call - /// - public enum FormTypeId - { - /// - /// Dashboard form - /// - Dashboard = 0, - /// - /// Appointment book, for service requests. - /// - AppointmentBook = 1, - /// - /// Main or default form - /// - Main = 2, - //MiniCampaignBo = 3, // Not used in 2011 - //Preview = 4, // Not used in 2011 - /// - /// Mobile default form - /// - Mobile = 5, - /// - /// User defined forms - /// - Other = 100 + return null; } - - #endregion + #region IOrganzation Service Proxy - Proxy object /// diff --git a/src/GeneralTools/DataverseClient/Client/TraceLoggerBase.cs b/src/GeneralTools/DataverseClient/Client/TraceLoggerBase.cs index c97a2e8..ae3550d 100644 --- a/src/GeneralTools/DataverseClient/Client/TraceLoggerBase.cs +++ b/src/GeneralTools/DataverseClient/Client/TraceLoggerBase.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.Linq; using System.Text; @@ -8,212 +8,212 @@ namespace Microsoft.PowerPlatform.Dataverse.Client { - /// - /// TraceLoggerBase Class. - /// - [LocalizableAttribute(false)] + /// + /// TraceLoggerBase Class. + /// + [LocalizableAttribute(false)] #pragma warning disable CA1063 // Implement IDisposable Correctly - public abstract class TraceLoggerBase : IDisposable + public abstract class TraceLoggerBase : IDisposable #pragma warning restore CA1063 // Implement IDisposable Correctly - { - - #region Private Fields - - /// - /// String Builder Info - /// - private string _lastError = string.Empty; - - /// - /// string _traceSourceName private field - /// - private string _traceSourceName; - - /// - /// Last Exception - /// - private Exception _lastException = null; - - private TraceSource _source; - - #endregion - - #region Protected fields - /// - /// Trace source - /// - protected TraceSource Source - { - get - { - try - { - SourceLevels sourceLevel = _source.Switch.Level; - } - catch(Exception ex) - { - string errMsg = string.Format(CultureInfo.InvariantCulture, - "Logging Provider Exception: {0}\nInnerEx: {1}\nStack:{2}\n", - ex.Message, ex.InnerException != null ? ex.InnerException.Message : string.Empty, ex.StackTrace); - try - { -#if NET462 - //adding event log - System.Diagnostics.EventLog.WriteEntry("application", errMsg, System.Diagnostics.EventLogEntryType.Error); + { + + #region Private Fields + + /// + /// String Builder Info + /// + private string _lastError = string.Empty; + + /// + /// string _traceSourceName private field + /// + private string _traceSourceName; + + /// + /// Last Exception + /// + private Exception _lastException = null; + + private TraceSource _source; + + #endregion + + #region Protected fields + /// + /// Trace source + /// + protected TraceSource Source + { + get + { + try + { + SourceLevels sourceLevel = _source.Switch.Level; + } + catch (Exception ex) + { + string errMsg = string.Format(CultureInfo.InvariantCulture, + "Logging Provider Exception: {0}\nInnerEx: {1}\nStack:{2}\n", + ex.Message, ex.InnerException != null ? ex.InnerException.Message : string.Empty, ex.StackTrace); + try + { +#if NETFRAMEWORK + //adding event log + System.Diagnostics.EventLog.WriteEntry("application", errMsg, System.Diagnostics.EventLogEntryType.Error); #endif - } - catch - { - //error in writing to event log - string log = string.Format("UNABLE TO WRITE TO EVENT LOG FOR: {0}", errMsg); - System.Diagnostics.Trace.WriteLine(log); - } - _source.Switch.Level = SourceLevels.Error; - } - return _source; - } - private set - { - _source = value; - } - } - - /// - /// Trace Name - /// - protected string TraceSourceName - { - get - { - return _traceSourceName; - } - set - { - if (string.IsNullOrEmpty(value)) - { - throw new ArgumentNullException("TraceSourceName"); - } - _traceSourceName = value; - } - } - - #endregion - - #region Properties - /// - /// Last Error from Dataverse - /// - public string LastError - { - get { return _lastError; } - set { _lastError = value; } - } - /// - /// Last Exception from Dataverse - /// - public Exception LastException - { - get { return _lastException; } - set { this._lastException = value; } - } - - /// - /// Current Trace level - /// - public SourceLevels CurrentTraceLevel - { - get { return Source.Switch.Level; } - } - - #endregion - - #region Public Methods - /// - /// default TraceLoggerBase constructor - /// - protected TraceLoggerBase() - { - TraceListenerBroker.RegisterTraceLogger(this); - } - - /// - /// Initialize Trace Source - /// - protected void Initialize() - { - _source = new TraceSource(TraceSourceName); - - if (TraceSourceSettingStore.TraceSourceSettingsCollection.Count > 0) - { - RefreshListeners(TraceSourceSettingStore.TraceSourceSettingsCollection); - } - } - - /// - /// Reset the last Stored Error - /// - public abstract void ResetLastError(); - - /// - /// Log a Message as an Information event. - /// - /// - public abstract void Log(string message); - - /// - /// Log a Trace event - /// - /// - /// - public abstract void Log(string message, TraceEventType eventType); - - /// - /// Log a Trace event - /// - /// - /// - /// - public abstract void Log(string message, TraceEventType eventType, Exception exception); - - /// - /// Logg an error with an Exception - /// - /// - public abstract void Log(Exception exception); - - - /// - /// To refresh listeners - /// - /// - public void RefreshListeners(List traceSourceSettingCollection) - { - if (traceSourceSettingCollection == null) - throw new ArgumentNullException("Input param traceSourceSettingCollection cannot be null."); - - var traceSourceSetting = traceSourceSettingCollection.FirstOrDefault(x => String.Compare(x.SourceName, Source.Name, StringComparison.OrdinalIgnoreCase) == 0); - if (traceSourceSetting != null && traceSourceSetting.TraceListeners.Any()) - { - Source.Listeners.Clear(); - Source.Listeners.AddRange(traceSourceSetting.TraceListeners.Select(x => x.Value).ToArray()); - Source.Switch.Level = traceSourceSetting.TraceLevel; - } - } - - #region IDisposable Support - - /// - /// - /// + } + catch + { + //error in writing to event log + string log = string.Format("UNABLE TO WRITE TO EVENT LOG FOR: {0}", errMsg); + System.Diagnostics.Trace.WriteLine(log); + } + _source.Switch.Level = SourceLevels.Error; + } + return _source; + } + private set + { + _source = value; + } + } + + /// + /// Trace Name + /// + protected string TraceSourceName + { + get + { + return _traceSourceName; + } + set + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentNullException("TraceSourceName"); + } + _traceSourceName = value; + } + } + + #endregion + + #region Properties + /// + /// Last Error from Dataverse + /// + public string LastError + { + get { return _lastError; } + set { _lastError = value; } + } + /// + /// Last Exception from Dataverse + /// + public Exception LastException + { + get { return _lastException; } + set { this._lastException = value; } + } + + /// + /// Current Trace level + /// + public SourceLevels CurrentTraceLevel + { + get { return Source.Switch.Level; } + } + + #endregion + + #region Public Methods + /// + /// default TraceLoggerBase constructor + /// + protected TraceLoggerBase() + { + TraceListenerBroker.RegisterTraceLogger(this); + } + + /// + /// Initialize Trace Source + /// + protected void Initialize() + { + _source = new TraceSource(TraceSourceName); + + if (TraceSourceSettingStore.TraceSourceSettingsCollection.Count > 0) + { + RefreshListeners(TraceSourceSettingStore.TraceSourceSettingsCollection); + } + } + + /// + /// Reset the last Stored Error + /// + public abstract void ResetLastError(); + + /// + /// Log a Message as an Information event. + /// + /// + public abstract void Log(string message); + + /// + /// Log a Trace event + /// + /// + /// + public abstract void Log(string message, TraceEventType eventType); + + /// + /// Log a Trace event + /// + /// + /// + /// + public abstract void Log(string message, TraceEventType eventType, Exception exception); + + /// + /// Logg an error with an Exception + /// + /// + public abstract void Log(Exception exception); + + + /// + /// To refresh listeners + /// + /// + public void RefreshListeners(List traceSourceSettingCollection) + { + if (traceSourceSettingCollection == null) + throw new ArgumentNullException("Input param traceSourceSettingCollection cannot be null."); + + var traceSourceSetting = traceSourceSettingCollection.FirstOrDefault(x => String.Compare(x.SourceName, Source.Name, StringComparison.OrdinalIgnoreCase) == 0); + if (traceSourceSetting != null && traceSourceSetting.TraceListeners.Any()) + { + Source.Listeners.Clear(); + Source.Listeners.AddRange(traceSourceSetting.TraceListeners.Select(x => x.Value).ToArray()); + Source.Switch.Level = traceSourceSetting.TraceLevel; + } + } + + #region IDisposable Support + + /// + /// + /// #pragma warning disable CA1063 // Implement IDisposable Correctly - public void Dispose() + public void Dispose() #pragma warning restore CA1063 // Implement IDisposable Correctly - { - // Always need this to be called. - TraceListenerBroker.UnRegisterTraceLogger(this); - } - #endregion - - #endregion - } + { + // Always need this to be called. + TraceListenerBroker.UnRegisterTraceLogger(this); + } + #endregion + + #endregion + } } \ No newline at end of file diff --git a/src/GeneralTools/DataverseClient/Client/Utils/ADALLoggerCallBack.cs b/src/GeneralTools/DataverseClient/Client/Utils/ADALLoggerCallBack.cs deleted file mode 100644 index 6374791..0000000 --- a/src/GeneralTools/DataverseClient/Client/Utils/ADALLoggerCallBack.cs +++ /dev/null @@ -1,69 +0,0 @@ -using Microsoft.Identity.Client; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.PowerPlatform.Dataverse.Client.Utils -{ - /// - /// This class will be used to support hooking into ADAL 3.x+ Call back logic. - /// - internal static class ADALLoggerCallBack - { - private static DataverseTraceLogger _logEntry; - - /// - /// Enabled PII logging for this connection. - /// if this flag is set, it will override the value from app config. - /// - public static bool? EnabledPIILogging { get; set; } - - /// - /// - /// - /// - /// - /// - static public void Log(LogLevel level, string message, bool containsPii) - { - if (_logEntry == null) - _logEntry = new DataverseTraceLogger("Microsoft.IdentityModel.Clients.ActiveDirectory"); // set up logging client. - - if (!EnabledPIILogging.HasValue) - { - EnabledPIILogging = true;//Utils.AppSettingsHelper.GetAppSetting("LogADALPII", false); - _logEntry.Log($"Setting ADAL PII Logging Feature to {EnabledPIILogging.Value}", System.Diagnostics.TraceEventType.Information); - } - - if (containsPii && !EnabledPIILogging.Value) - { - _logEntry.Log($"ADAL LOG EVENT SKIPPED --> PII Logging Disabled.", System.Diagnostics.TraceEventType.Warning); - return; - } - - // Add (PII) prefix to messages that have PII in them per AAD Message alert. - message = containsPii ? $"(PII){message}" : message; - - switch (level) - { - case LogLevel.Info: - _logEntry.Log(message , System.Diagnostics.TraceEventType.Information); - break; - case LogLevel.Verbose: - _logEntry.Log(message, System.Diagnostics.TraceEventType.Verbose); - break; - case LogLevel.Warning: - _logEntry.Log(message, System.Diagnostics.TraceEventType.Warning); - break; - case LogLevel.Error: - _logEntry.Log(message, System.Diagnostics.TraceEventType.Error); - break; - default: - break; - } - } - - } -} diff --git a/src/GeneralTools/DataverseClient/Client/Utils/DataverseConnectionStringProcessor.cs b/src/GeneralTools/DataverseClient/Client/Utils/DataverseConnectionStringProcessor.cs index 5e68a95..21b4520 100644 --- a/src/GeneralTools/DataverseClient/Client/Utils/DataverseConnectionStringProcessor.cs +++ b/src/GeneralTools/DataverseClient/Client/Utils/DataverseConnectionStringProcessor.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Configuration; using System.Data.Common; @@ -10,6 +10,7 @@ using Microsoft.PowerPlatform.Dataverse.Client.Model; using Microsoft.PowerPlatform.Dataverse.Client.Auth; using Microsoft.Extensions.Logging; +using Microsoft.PowerPlatform.Dataverse.Client.InternalExtensions; namespace Microsoft.PowerPlatform.Dataverse.Client { @@ -227,7 +228,7 @@ private DataverseConnectionStringProcessor(IDictionary connectio private DataverseConnectionStringProcessor(string serviceUri, string userName, string password, string domain, string homeRealmUri, string authType, string requireNewInstance, string clientId, string redirectUri, string tokenCacheStorePath, string loginPrompt, string certStoreName, string certThumbprint, string skipDiscovery, string IntegratedSecurity, string clientSecret, ILogger logger) { - DataverseTraceLogger logEntry = new DataverseTraceLogger(); + DataverseTraceLogger logEntry = new DataverseTraceLogger(logger); Uri _serviceuriName, _realmUri; bool tempbool = false; @@ -288,7 +289,7 @@ private DataverseConnectionStringProcessor(string serviceUri, string userName, s } //if the client Id was not passed, use Sample AppID - if (string.IsNullOrWhiteSpace(ClientId)) + if (authenticationType != AuthenticationType.AD && string.IsNullOrWhiteSpace(ClientId)) { logEntry.Log($"Client ID not supplied, using SDK Sample Client ID for this connection", System.Diagnostics.TraceEventType.Warning); ClientId = sampleClientId;// sample client ID @@ -299,10 +300,16 @@ private DataverseConnectionStringProcessor(string serviceUri, string userName, s if (!string.IsNullOrWhiteSpace(userName) && !string.IsNullOrWhiteSpace(password)) { ClientCredentials clientCredentials = new ClientCredentials(); - clientCredentials.UserName.UserName = userName; - clientCredentials.UserName.Password = password; + if (AuthenticationType == AuthenticationType.AD && !string.IsNullOrWhiteSpace(domain)) + { + clientCredentials.Windows.ClientCredential = new NetworkCredential(userName, password, domain); + } + else + { + clientCredentials.UserName.UserName = userName; + clientCredentials.UserName.Password = password; + } ClientCredentials = clientCredentials; - } logEntry.Dispose(); @@ -347,82 +354,4 @@ public static DataverseConnectionStringProcessor Parse(string connectionString, } } - - /// - /// Extension - /// - public static class Extension - { - /// - /// Enum extension - /// - /// - /// - /// Enum Value - public static T ToEnum(this string enumName) - { - return (T)((object)Enum.Parse(typeof(T), enumName)); - } - /// - /// Converts a int to a Enum of the requested type (T) - /// - /// Enum Type to translate too - /// Int Value too translate. - /// Enum of Type T - public static T ToEnum(this int enumValue) - { - return enumValue.ToString().ToEnum(); - } - /// - /// Converts a ; separated string into a dictionary - /// - /// String to parse - /// Dictionary of properties from the connection string - public static IDictionary ToDictionary(this string connectionString) - { - try - { - DbConnectionStringBuilder source = new DbConnectionStringBuilder - { - ConnectionString = connectionString - }; - - Dictionary dictionary = source.Cast>(). - ToDictionary((KeyValuePair pair) => pair.Key, - (KeyValuePair pair) => pair.Value != null ? pair.Value.ToString() : string.Empty); - return new Dictionary(dictionary, StringComparer.OrdinalIgnoreCase); - } - catch - { - //ignore - } - return new Dictionary(); - - } - /// - /// Extension to support formating a string - /// - /// Formatting pattern - /// Argument collection - /// Formated String - public static string FormatWith(this string format, params object[] args) - { - return format.FormatWith(CultureInfo.InvariantCulture, args); - } - /// - /// Extension to get the first item in a dictionary if the dictionary contains the key. - /// - /// Type to return - /// Dictionary to search - /// Collection of Keys to find. - /// - public static string FirstNotNullOrEmpty(this IDictionary dictionary, params TKey[] keys) - { - return ( - from key in keys - where dictionary.ContainsKey(key) && !string.IsNullOrEmpty(dictionary[key]) - select dictionary[key]).FirstOrDefault(); - } - - } } diff --git a/src/GeneralTools/DataverseClient/Client/Utils/MSALLoggerCallBack.cs b/src/GeneralTools/DataverseClient/Client/Utils/MSALLoggerCallBack.cs index e27b6b4..a4e779f 100644 --- a/src/GeneralTools/DataverseClient/Client/Utils/MSALLoggerCallBack.cs +++ b/src/GeneralTools/DataverseClient/Client/Utils/MSALLoggerCallBack.cs @@ -1,4 +1,4 @@ -using Microsoft.Identity.Client; +using Microsoft.Identity.Client; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Microsoft.PowerPlatform.Dataverse.Client.Model; @@ -27,11 +27,11 @@ internal static class MSALLoggerCallBack static public void Log(LogLevel level, string message, bool containsPii) { if (_logEntry == null) - _logEntry = new DataverseTraceLogger("Microsoft.IdentityModel.Clients.ActiveDirectory"); // set up logging client. + _logEntry = new DataverseTraceLogger(typeof(LogCallback).Assembly.GetName().Name); // set up logging client. if (!EnabledPIILogging.HasValue) { - EnabledPIILogging = ClientServiceProviders.Instance.GetService>().Value.MSALEnabledLogPII; + EnabledPIILogging = ClientServiceProviders.Instance.GetService>().Value.MSALEnabledLogPII; _logEntry.Log($"Setting MSAL PII Logging Feature to {EnabledPIILogging.Value}", System.Diagnostics.TraceEventType.Information); } diff --git a/src/GeneralTools/DataverseClient/Client/Utils/SecureStringExtensions.cs b/src/GeneralTools/DataverseClient/Client/Utils/SecureStringExtensions.cs deleted file mode 100644 index 626dfa6..0000000 --- a/src/GeneralTools/DataverseClient/Client/Utils/SecureStringExtensions.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using System.Security; - -namespace Microsoft.PowerPlatform.Dataverse.Client -{ - /// - /// Adds a extension to Secure string - /// - internal static class SecureStringExtensions - { - /// - /// DeCrypt a Secure password - /// - /// - /// - public static string ToUnsecureString(this SecureString value) - { - if (null == value) - throw new ArgumentNullException("value"); - - // Get a pointer to the secure string memory data. - IntPtr ptr = Marshal.SecureStringToGlobalAllocUnicode(value); - try - { - // DeCrypt - return Marshal.PtrToStringUni(ptr); - } - finally - { - // release the pointer. - Marshal.ZeroFreeGlobalAllocUnicode(ptr); - } - } - } -} diff --git a/src/GeneralTools/DataverseClient/Client/Utils/ServiceProviders.cs b/src/GeneralTools/DataverseClient/Client/Utils/ServiceProviders.cs index 4606240..635a8b4 100644 --- a/src/GeneralTools/DataverseClient/Client/Utils/ServiceProviders.cs +++ b/src/GeneralTools/DataverseClient/Client/Utils/ServiceProviders.cs @@ -36,7 +36,7 @@ private static void BindServiceProviders() var services = new ServiceCollection(); services.AddTransient(); - services.AddOptions(); + services.AddOptions(); services.AddHttpClient("DataverseHttpClientFactory") .ConfigurePrimaryHttpMessageHandler(() => { @@ -49,7 +49,7 @@ private static void BindServiceProviders() }); services.AddHttpClient("MSALClientFactory", (sp, client) => { - client.Timeout = sp.GetService>().Value.MSALRequestTimeout; + client.Timeout = sp.GetService>().Value.MSALRequestTimeout; }) .AddHttpMessageHandler(); // Adding on board retry hander for MSAL. _instance = services.BuildServiceProvider(); diff --git a/src/GeneralTools/DataverseClient/Client/Utils/Utils.cs b/src/GeneralTools/DataverseClient/Client/Utils/Utils.cs index 52a92e2..9598b70 100644 --- a/src/GeneralTools/DataverseClient/Client/Utils/Utils.cs +++ b/src/GeneralTools/DataverseClient/Client/Utils/Utils.cs @@ -141,6 +141,14 @@ public static void GetOrgnameAndOnlineRegionFromServiceUri(Uri serviceUri, out s if (serviceUri.Segments.Count() >= 2) { organizationName = serviceUri.Segments[1].TrimEnd('/'); // Fix for bug 294040 http://vstfmbs:8080/tfs/web/wi.aspx?pcguid=12e6d33f-1461-4da4-b3d9-5517a4567489&id=294040 + }else + { + // IFD style. + var segementsList = serviceUri.DnsSafeHost.Split('.'); + if ( segementsList.Length > 1) + { + organizationName = segementsList[0]; + } } } @@ -298,8 +306,8 @@ public static bool IsValidOnlineHost(Uri hostUri) /// internal static bool IsRequestValidForTranslationToWebAPI(OrganizationRequest req) { - bool useWebApi = ClientServiceProviders.Instance.GetService>().Value.UseWebApi; - bool useWebApiForLogin = ClientServiceProviders.Instance.GetService>().Value.UseWebApiLoginFlow; + bool useWebApi = ClientServiceProviders.Instance.GetService>().Value.UseWebApi; + bool useWebApiForLogin = ClientServiceProviders.Instance.GetService>().Value.UseWebApiLoginFlow; switch (req.RequestName.ToLowerInvariant()) { case "create": diff --git a/src/GeneralTools/DataverseClient/ConnectControl/AdvancedOptions.xaml b/src/GeneralTools/DataverseClient/ConnectControl/AdvancedOptions.xaml new file mode 100644 index 0000000..bf0fce2 --- /dev/null +++ b/src/GeneralTools/DataverseClient/ConnectControl/AdvancedOptions.xaml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/GeneralTools/DataverseClient/ConnectControl/AdvancedOptions.xaml.cs b/src/GeneralTools/DataverseClient/ConnectControl/AdvancedOptions.xaml.cs new file mode 100644 index 0000000..75ab1da --- /dev/null +++ b/src/GeneralTools/DataverseClient/ConnectControl/AdvancedOptions.xaml.cs @@ -0,0 +1,42 @@ +#region using +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; +#endregion + +namespace Microsoft.PowerPlatform.Dataverse.ConnectControl +{ + /// + /// Interaction logic for AdvancedOptions.xaml + /// + public partial class AdvancedOptions : UserControl + { + /// + /// Constructor + /// + public AdvancedOptions() + { + InitializeComponent(); + if (!DesignerProperties.GetIsInDesignMode(new DependencyObject())) + { + } + } + + /// + /// If true domain name textbox is visible + /// + public bool DomainVisible { get; set; } = false; + } +} diff --git a/src/GeneralTools/DataverseClient/ConnectControl/ConnectionManager.cs b/src/GeneralTools/DataverseClient/ConnectControl/ConnectionManager.cs index f2fdae1..fa17fda 100644 --- a/src/GeneralTools/DataverseClient/ConnectControl/ConnectionManager.cs +++ b/src/GeneralTools/DataverseClient/ConnectControl/ConnectionManager.cs @@ -1,10 +1,10 @@ -using Microsoft.Identity.Client; -using Microsoft.IdentityModel.Clients.ActiveDirectory; +using Microsoft.Identity.Client; using Microsoft.PowerPlatform.Dataverse.Client; using Microsoft.PowerPlatform.Dataverse.Client.Model; using Microsoft.PowerPlatform.Dataverse.ConnectControl.Model; using Microsoft.PowerPlatform.Dataverse.ConnectControl.Properties; using Microsoft.PowerPlatform.Dataverse.ConnectControl.Utility; +using Microsoft.PowerPlatform.Dataverse.ConnectControl.InternalExtensions; using Microsoft.Xrm.Sdk.Discovery; using System; using System.Collections.Generic; @@ -140,11 +140,6 @@ public class ConnectionManager : IDisposable /// private string _cachedUserId = null; - /// - /// if true, Skip discovery is in focus - /// - private bool _isSkipDiscovery = false; - /// /// if populated, contains the URL to try a direct connect too. /// @@ -188,11 +183,6 @@ public class ConnectionManager : IDisposable /// public ClaimsHomeRealmOptions HomeRealmServersList { get { if (_homeRealmServersList == null) _homeRealmServersList = new ClaimsHomeRealmOptions(); return _homeRealmServersList; } set { _homeRealmServersList = value; } } - /// - /// User Identifier as a login hint - /// - public UserIdentifier UserId { get; set; } - /// /// ClientId for the client /// @@ -573,9 +563,7 @@ private bool ValidateServerConnection(OrgByServer selectedOrg) if (!ValidateUserSpecifiedData()) return false; // Check to see if its a direct connect. - bool.TryParse(StorageUtils.GetConfigKey(ServerConfigKeys, Dynamics_ConfigFileServerKeys.UseDirectConnection), out _isSkipDiscovery); - if (_isSkipDiscovery) - _directConnectUri = StorageUtils.GetConfigKey(ServerConfigKeys, Dynamics_ConfigFileServerKeys.DirectConnectionUri); + _directConnectUri = StorageUtils.GetConfigKey(ServerConfigKeys, Dynamics_ConfigFileServerKeys.DirectConnectionUri); // Value is not a bool.. check to see if there is a value. string sDeploymentType = StorageUtils.GetConfigKey(ServerConfigKeys, Dynamics_ConfigFileServerKeys.CrmDeploymentType); @@ -808,8 +796,6 @@ private bool ValidateServerConnection(OrgByServer selectedOrg) } else { - if (UserId == null && _cachedUserId != null) - UserId = new UserIdentifier(_cachedUserId, UserIdentifierType.RequiredDisplayableId); discoverOrganizationsResult = ServiceClient.DiscoverOnPremiseOrganizationsAsync(uCrmUrl, _userClientCred, ClientId, RedirectUri, _cachedAuthorityName, _promptBehavior, false, TokenCachePath).ConfigureAwait(false).GetAwaiter().GetResult(); } } @@ -920,7 +906,7 @@ private bool ValidateServerConnection(OrgByServer selectedOrg) // THIS IS FOR CONNECTING TO AN ON-LINE SERVER _bgWorker.ReportProgress(30, new ServerConnectStatusEventArgs(Messages.CRMCONNECT_LOGIN_PROCESS_CONNECT_TO_UII)); - if (_isSkipDiscovery) + if (IsAdvancedCheckEnabled && !string.IsNullOrWhiteSpace(_directConnectUri)) { string _baseWebApiUriFormat = @"{0}/api/data/v{1}/"; string _baseSoapOrgUriFormat = @"{0}/XRMServices/2011/Organization.svc"; @@ -1371,7 +1357,7 @@ private DiscoverOrganizationsResult FindOnlineDiscoveryServer(ClientCredentials { _tracer.Log(string.Format("Trying Discovery Server, ({1}) URI is = {0}", svr.DiscoveryServer.ToString(), svr.DisplayName), TraceEventType.Information); _bgWorker.ReportProgress(25, new ServerConnectStatusEventArgs(string.Format(Messages.CRMCONNECT_LOGIN_PROCESS_GET_ORGS_LIVE, svr.DisplayName))); - discoverResult = QueryOAuthDiscoveryServer(svr.DiscoveryServer, liveCreds, UserId, ClientId, RedirectUri, _promptBehavior, TokenCachePath); + discoverResult = QueryOAuthDiscoveryServer(svr.DiscoveryServer, liveCreds, ClientId, RedirectUri, _promptBehavior, TokenCachePath); } else { @@ -1425,7 +1411,7 @@ private DiscoverOrganizationsResult QueryOnlineServerList(ObservableCollection /// Discovery Service Uri /// Credentials supplied for login - /// User identifier /// Registered Client Id of application trying for OAuth /// Uri to redirect the application /// Prompt behavior defining ADAL login popup /// Token cache path supplied by user for storing bearer tokens /// if true, calls global discovery path /// - private DiscoverOrganizationsResult QueryOAuthDiscoveryServer(Uri discoServer, ClientCredentials liveCreds, UserIdentifier user, string clientId, Uri redirectUri, Client.Auth.PromptBehavior promptBehavior, string tokenCachePath, bool useGlobalDisco = false) + private DiscoverOrganizationsResult QueryOAuthDiscoveryServer(Uri discoServer, ClientCredentials liveCreds, string clientId, Uri redirectUri, Client.Auth.PromptBehavior promptBehavior, string tokenCachePath, bool useGlobalDisco = false) { _tracer.Log($"{nameof(QueryOAuthDiscoveryServer)}", TraceEventType.Start); @@ -1521,8 +1506,6 @@ private DiscoverOrganizationsResult QueryOAuthDiscoveryServer(Uri discoServer, C } else { - if (user == null && _cachedUserId != null) - user = new UserIdentifier(_cachedUserId, UserIdentifierType.RequiredDisplayableId); result = ServiceClient.DiscoverOnlineOrganizationsAsync(discoServer, liveCreds, clientId, redirectUri, false, _cachedAuthorityName, promptBehavior, tokenCacheStorePath:tokenCachePath).ConfigureAwait(false).GetAwaiter().GetResult(); } return result; @@ -1943,6 +1926,7 @@ public Dictionary LoadConfigFromFile(bool SetServerConfigKey(config, Dynamics_ConfigFileServerKeys.AdvancedCheck, bool.FalseString, overrideDefaultSet: readLocalFirst); SetServerConfigKey(config, Dynamics_ConfigFileServerKeys.Authority, overrideDefaultSet: readLocalFirst); SetServerConfigKey(config, Dynamics_ConfigFileServerKeys.UserId, overrideDefaultSet: readLocalFirst); + SetServerConfigKey(config, Dynamics_ConfigFileServerKeys.DirectConnectionUri, overrideDefaultSet: readLocalFirst); if (config.AppSettings.Settings[Dynamics_ConfigFileServerKeys.UseDefaultCreds.ToString()] != null) { @@ -2120,6 +2104,7 @@ public bool SaveConfigToFile(Dictionary c config.AppSettings.Settings.Remove(Dynamics_ConfigFileServerKeys.AuthHomeRealm.ToString()); config.AppSettings.Settings.Remove(Dynamics_ConfigFileServerKeys.AskForOrg.ToString()); config.AppSettings.Settings.Remove(Dynamics_ConfigFileServerKeys.AdvancedCheck.ToString()); + config.AppSettings.Settings.Remove(Dynamics_ConfigFileServerKeys.DirectConnectionUri.ToString()); // Create new data. config.AppSettings.Settings.Add(Dynamics_ConfigFileServerKeys.CrmDeploymentType.ToString(), StorageUtils.GetConfigKey(configToSave, Dynamics_ConfigFileServerKeys.CrmDeploymentType)); @@ -2134,6 +2119,7 @@ public bool SaveConfigToFile(Dictionary c config.AppSettings.Settings.Add(Dynamics_ConfigFileServerKeys.AskForOrg.ToString(), StorageUtils.GetConfigKey(configToSave, Dynamics_ConfigFileServerKeys.AskForOrg)); config.AppSettings.Settings.Add(Dynamics_ConfigFileServerKeys.CrmDomain.ToString(), StorageUtils.GetConfigKey(configToSave, Dynamics_ConfigFileServerKeys.CrmDomain)); config.AppSettings.Settings.Add(Dynamics_ConfigFileServerKeys.AdvancedCheck.ToString(), StorageUtils.GetConfigKey(configToSave, Dynamics_ConfigFileServerKeys.AdvancedCheck)); + config.AppSettings.Settings.Add(Dynamics_ConfigFileServerKeys.DirectConnectionUri.ToString(), StorageUtils.GetConfigKey(configToSave, Dynamics_ConfigFileServerKeys.DirectConnectionUri)); if (ServiceClient != null && ServiceClient.ActiveAuthenticationType == AuthenticationType.OAuth) { diff --git a/src/GeneralTools/DataverseClient/ConnectControl/InstanceUrlCapture.xaml b/src/GeneralTools/DataverseClient/ConnectControl/InstanceUrlCapture.xaml deleted file mode 100644 index c084582..0000000 --- a/src/GeneralTools/DataverseClient/ConnectControl/InstanceUrlCapture.xaml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - private void CancelConnectCheck() { - _connectionManager.CancelConnectToServerCheck(); + ConnectionManager.CancelConnectToServerCheck(); } /// @@ -592,12 +579,12 @@ private void storageAccess_ConnectionCheckComplete(object sender, ServerConnectS /// private void ShowSelectOrgDialog() { - if (_connectionManager != null && !bOnlineMultiOrgFix) + if (ConnectionManager != null && !_bOnlineMultiOrgFix) { MessageGrid.Visibility = Visibility.Collapsed; LoginGrid.Visibility = Visibility.Collapsed; OrgSelectGrid.Visibility = Visibility.Visible; - lvOrgList.ItemsSource = _connectionManager.CrmOrgsFoundForUser.OrgsList; + lvOrgList.ItemsSource = ConnectionManager.CrmOrgsFoundForUser.OrgsList; } } @@ -620,9 +607,9 @@ private void SetSort(GridViewColumnHeader column) String field = column.Tag as String; - if (_CurSortCol != null) + if (_curSortCol != null) { - AdornerLayer.GetAdornerLayer(_CurSortCol).Remove(_CurAdorner); + AdornerLayer.GetAdornerLayer(_curSortCol).Remove(_curAdorner); lvOrgList.Items.SortDescriptions.Clear(); } @@ -630,26 +617,26 @@ private void SetSort(GridViewColumnHeader column) if (_isSortButtonClicked) { //Changing sort direction only on sort button click - if (_CurSortCol == column && _CurAdorner.Direction == newDir) + if (_curSortCol == column && _curAdorner.Direction == newDir) newDir = ListSortDirection.Descending; _isSortButtonClicked = false; } - else if (_CurAdorner != null) + else if (_curAdorner != null) { - newDir = _CurAdorner.Direction; + newDir = _curAdorner.Direction; } - _CurSortCol = column; + _curSortCol = column; - _CurAdorner = new SortAdorner(_CurSortCol, newDir, "ConnectControlSortOrderBrush"); + _curAdorner = new SortAdorner(_curSortCol, newDir, "ConnectControlSortOrderBrush"); lvOrgList.Items.SortDescriptions.Add( new SortDescription(field, newDir)); // Check to see if the adorner - var Layer = AdornerLayer.GetAdornerLayer(_CurSortCol); + var Layer = AdornerLayer.GetAdornerLayer(_curSortCol); if (Layer != null) - Layer.Add(_CurAdorner); + Layer.Add(_curAdorner); } catch (Exception) { @@ -665,9 +652,8 @@ private void SetSort(GridViewColumnHeader column) private void ConnectionCheckComplete(ServerConnectStatusEventArgs e) { // Sync to main UI thread. - - _connectionManager.ConnectionCheckComplete -= new EventHandler(storageAccess_ConnectionCheckComplete); - _connectionManager.ServerConnectionStatusUpdate -= new EventHandler(storageAccess_ServerConnectionStatusUpdate); + ConnectionManager.ConnectionCheckComplete -= new EventHandler(storageAccess_ConnectionCheckComplete); + ConnectionManager.ServerConnectionStatusUpdate -= new EventHandler(storageAccess_ServerConnectionStatusUpdate); if (e.Connected) { @@ -776,7 +762,7 @@ private void HandleError(bool bOrg) /// private void UpdateConnectStatusText(ServerConnectStatusEventArgs e) { - bOnlineMultiOrgFix = false; + _bOnlineMultiOrgFix = false; if (e.exEvent != null) { //// Error here .. @@ -812,18 +798,18 @@ private void UpdateConnectStatusText(ServerConnectStatusEventArgs e) if (MessageGrid.IsVisible) { //Found Single Org - if (_connectionManager != null && !string.IsNullOrEmpty(_connectionManager.ConnectedOrgFriendlyName)) + if (ConnectionManager != null && !string.IsNullOrEmpty(ConnectionManager.ConnectedOrgFriendlyName)) { if (e.StatusMessage.Equals(uiMessages.CRMCONNECT_LOGIN_PROCESS_CONNNECTING, StringComparison.CurrentCultureIgnoreCase)) { if (!bMultiOrg) { - lblCrmOrg.Text = string.Format(uiResources.LOGIN_FRM_RETRIEVE_DEF, _connectionManager.ConnectedOrgFriendlyName); + lblCrmOrg.Text = string.Format(uiResources.LOGIN_FRM_RETRIEVE_DEF, ConnectionManager.ConnectedOrgFriendlyName); } } else { - lblCrmOrg.Text = string.Format(uiMessages.CRMCONNECT_SERVER_CONNECT_GOOD + " - {0}", _connectionManager.ConnectedOrgFriendlyName); + lblCrmOrg.Text = string.Format(uiMessages.CRMCONNECT_SERVER_CONNECT_GOOD + " - {0}", ConnectionManager.ConnectedOrgFriendlyName); ipb.Visibility = Visibility.Collapsed; } } @@ -917,7 +903,7 @@ private void CompleteConnectCheck(bool bSuccess) if (bSuccess) { // Save settings - _connectionManager.SaveConfigToFile(ServerConfigKeys); + ConnectionManager.SaveConfigToFile(ServerConfigKeys); // Force a reload to pick up any special bits. LoadDisplayWithAppSettingsData(); @@ -940,36 +926,16 @@ private void CompleteConnectCheck(bool bSuccess) private void SetAdvancedGridWidth() { //To make sure Inner grid(Advanced Grid) alligmrnt is inline with parent grid(Login Grid) - if (LastBoardWasOnline) - { - //Office365: Reseting width of Advanced Grid - AdvancedGrid.ColumnDefinitions[0].Width = GridLength.Auto; - } - else + if (!LastBoardWasOnline) { // On-Prem: Seting Login Grid width to Advanced Grid GridLength LgnGridColumn0length = new GridLength(LoginGrid.ColumnDefinitions[0].ActualWidth); - AdvancedGrid.ColumnDefinitions[0].Width = LgnGridColumn0length; } } - /// - /// Checks to see if the skip discovery flag is set, if so, requires the user to provide a full URI to connect to the remote server. - /// - /// - private bool IsSkipDiscoverySet() - { - if (ConfigurationManager.AppSettings != null) - if (ConfigurationManager.AppSettings["SkipDiscovery"] != null && ConfigurationManager.AppSettings["SkipDiscovery"].Equals("true", StringComparison.OrdinalIgnoreCase)) - { - return true; - }; - return false; - } - #endregion - #region Events + #region Event handlers /// /// Raised when the Connect to Server Button is Pushed. @@ -981,9 +947,10 @@ private void btn_ConnectToServer(object sender, System.Windows.RoutedEventArgs e { bMultiOrg = false; btnCancel.Visibility = Visibility.Collapsed; - _connectionManager.ServiceClient = null; + ConnectionManager.ServiceClient = null; StartConnectCheck(); } + /// /// Raised when the Default Credentials Check box State changes /// Sets the visible state for using the setting of the Default Credentials control @@ -995,12 +962,10 @@ private void cbUseDefaultCreds_Click(object sender, System.Windows.RoutedEventAr tracer.Log(string.Format("Use Current User Checkbox State = {0}", cbUseDefaultCreds.IsChecked.Value)); // set to the opposite of the initial value - tbUserId.IsEnabled = !cbUseDefaultCreds.IsChecked.Value; - tbPassword.IsEnabled = !cbUseDefaultCreds.IsChecked.Value; - tbDomain.IsEnabled = !cbUseDefaultCreds.IsChecked.Value; - if (rbOn365.IsChecked.Value) - tbDomain.IsEnabled = false; - + AdvancedOptions.tbUserId.IsEnabled = !cbUseDefaultCreds.IsChecked.Value; + AdvancedOptions.tbPassword.IsEnabled = !cbUseDefaultCreds.IsChecked.Value; + AdvancedOptions.tbDomain.IsEnabled = !cbUseDefaultCreds.IsChecked.Value; + AdvancedOptions.DomainVisible = !rbOn365.IsChecked.Value; } /// @@ -1020,35 +985,41 @@ private void btn_CancelSave(object sender, System.Windows.RoutedEventArgs e) /// /// /// - private void UiiServerConnectionCtrl_Loaded(object sender, RoutedEventArgs e) + private void OnLoaded(object sender, RoutedEventArgs e) { if (!DesignerProperties.GetIsInDesignMode(new DependencyObject())) { - iRow3 = LoginGrid.RowDefinitions[3].Height.Value == 0 ? 31.0 : LoginGrid.RowDefinitions[3].Height.Value; - iRow4 = LoginGrid.RowDefinitions[4].Height.Value == 0 ? 31.0 : LoginGrid.RowDefinitions[4].Height.Value; - iRow5 = LoginGrid.RowDefinitions[5].Height.Value == 0 ? 31.0 : LoginGrid.RowDefinitions[5].Height.Value; - iRow6 = LoginGrid.RowDefinitions[6].Height.Value == 0 ? 31.0 : LoginGrid.RowDefinitions[6].Height.Value; - iRow7 = LoginGrid.RowDefinitions[7].Height.Value == 0 ? 31.0 : LoginGrid.RowDefinitions[7].Height.Value; - iRow8 = LoginGrid.RowDefinitions[8].Height.Value == 93 ? 115.0 : LoginGrid.RowDefinitions[8].Height.Value; - iRow9 = LoginGrid.RowDefinitions[9].Height.Value == 0 ? 31.0 : LoginGrid.RowDefinitions[9].Height.Value; - advRow0 = AdvancedGrid.RowDefinitions[0].Height.Value == 0 ? 31.0 : AdvancedGrid.RowDefinitions[0].Height.Value; - advRow3 = AdvancedGrid.RowDefinitions[3].Height.Value == 0 ? 31.0 : AdvancedGrid.RowDefinitions[3].Height.Value; - - LoginGrid.RowDefinitions[4].Height = new GridLength(0); - if (LoginGrid.RowDefinitions[5].Height.Value == 0) - LoginGrid.RowDefinitions[5].Height = new GridLength(iRow5); - //In config file if CrmDeploymentType is O365 need to manualy set the height of iRow8 - if ((iRow8 == 0 || iRow8 == 31) && LastBoardWasOnline) - iRow8 = 115; - - if (iRow5 == 115 && LastBoardWasOnline) - iRow5 = 31; + // This allows control to be dragged by mouse at any point + var parentWindow = Window.GetWindow(this); + parentWindow.MouseDown += delegate (object s, MouseButtonEventArgs mbEventArgs) + { + if (mbEventArgs.ChangedButton == MouseButton.Left) parentWindow.DragMove(); + }; + + // Make sure first control in tab order has keyboard focus + MoveFocus(new TraversalRequest(FocusNavigationDirection.First)); + + _iRow6 = LoginGrid.RowDefinitions[6].Height.Value == 0 ? 31.0 : LoginGrid.RowDefinitions[6].Height.Value; + _iRow7 = LoginGrid.RowDefinitions[7].Height.Value == 0 ? 31.0 : LoginGrid.RowDefinitions[7].Height.Value; + _iRow8 = LoginGrid.RowDefinitions[8].Height.Value == 0 ? 31.0 : LoginGrid.RowDefinitions[8].Height.Value; + _iRow9 = LoginGrid.RowDefinitions[9].Height.Value == 93 ? 115.0 : LoginGrid.RowDefinitions[9].Height.Value; + _iRow10 = LoginGrid.RowDefinitions[10].Height.Value == 0 ? 31.0 : LoginGrid.RowDefinitions[10].Height.Value; + + LoginGrid.RowDefinitions[5].Height = new GridLength(0); + if (LoginGrid.RowDefinitions[6].Height.Value == 0) + LoginGrid.RowDefinitions[6].Height = new GridLength(_iRow6); + // In config file if CrmDeploymentType is O365 need to manualy set the height of iRow8 + if ((_iRow9 == 0 || _iRow9 == 31) && LastBoardWasOnline) + _iRow9 = 115; + + if (_iRow6 == 115 && LastBoardWasOnline) + _iRow6 = 31; // Load Stored settings here. - if (_connectionManager != null) + if (ConnectionManager != null) { LoadHomeRealmData(); // Load HomeRealm Information from Config. - ServerConfigKeys = _connectionManager.LoadConfigFromFile(); + ServerConfigKeys = ConnectionManager.LoadConfigFromFile(); if (ServerConfigKeys != null && ServerConfigKeys.Count > 3) LoadDisplayWithAppSettingsData(); else @@ -1057,42 +1028,35 @@ private void UiiServerConnectionCtrl_Loaded(object sender, RoutedEventArgs e) } } - /// - /// Sets the UI state when either the CRM Online or Prem Radio buttons are checked + /// Sets the UI state when either the Dataverse Online or Prem Radio buttons are checked /// /// /// - private void rbOnlinePrem_Click(object sender, System.Windows.RoutedEventArgs e) + private void rbOnlinePrem_Click(object sender, RoutedEventArgs e) { // Storyboard execution moved out of triggers, to here, to support the not unnecessary replaying a storyboard // Bind to O365 Servers. - ddlCrmOnlineRegions.ItemsSource = _connectionManager.OnlineDiscoveryServerList.OSDPServers; + //ddlCrmOnlineRegions.ItemsSource = _connectionManager.OnlineDiscoveryServerList.OSDPServers; if (rbOnPrem.IsChecked.Value) { LoginGrid.RowDefinitions[3].Height = new GridLength(0); LoginGrid.RowDefinitions[4].Height = new GridLength(31); LoginGrid.RowDefinitions[5].Height = new GridLength(31); - LoginGrid.RowDefinitions[6].Height = new GridLength(iRow6); - LoginGrid.RowDefinitions[7].Height = new GridLength(iRow7); + LoginGrid.RowDefinitions[6].Height = new GridLength(_iRow6); + LoginGrid.RowDefinitions[7].Height = new GridLength(_iRow7); LoginGrid.RowDefinitions[8].Height = new GridLength(93); - LoginGrid.RowDefinitions[9].Height = new GridLength(iRow9); - - AdvancedGrid.RowDefinitions[0].Height = new GridLength(0); - AdvancedGrid.RowDefinitions[3].Height = new GridLength(advRow3); - GbAdvanced.Margin = new Thickness(-6, -20, -6, -20); + LoginGrid.RowDefinitions[9].Height = new GridLength(_iRow9); - tbUserId.IsEnabled = !cbUseDefaultCreds.IsChecked.Value; - tbPassword.IsEnabled = !cbUseDefaultCreds.IsChecked.Value; - tbDomain.IsEnabled = !cbUseDefaultCreds.IsChecked.Value; + AdvancedOptions.tbUserId.IsEnabled = !cbUseDefaultCreds.IsChecked.Value; + AdvancedOptions.tbPassword.IsEnabled = !cbUseDefaultCreds.IsChecked.Value; + AdvancedOptions.tbDomain.IsEnabled = !cbUseDefaultCreds.IsChecked.Value; - GbAdvanced.Visibility = Visibility.Visible; - GbAdvanced.Header = ""; - GbAdvanced.BorderThickness = new Thickness(0); + AdvancedOptions.GbAdvanced.Visibility = Visibility.Visible; - Grid.SetRow(GbAdvanced, 8); + Grid.SetRow(AdvancedOptions, 8); Grid.SetRow(stkOrg, 9); if (goPrem != null) @@ -1104,12 +1068,13 @@ private void rbOnlinePrem_Click(object sender, System.Windows.RoutedEventArgs e) LoginGrid.RowDefinitions[3].Height = new GridLength(22); LoginGrid.RowDefinitions[4].Height = new GridLength(22); LoginGrid.RowDefinitions[5].Height = new GridLength(22); - LoginGrid.RowDefinitions[7].Height = new GridLength(0); - LoginGrid.RowDefinitions[9].Height = new GridLength(0); + LoginGrid.RowDefinitions[6].Height = new GridLength(22); + LoginGrid.RowDefinitions[8].Height = new GridLength(0); + LoginGrid.RowDefinitions[10].Height = new GridLength(0); - Grid.SetRow(stkUseDefaultCreds, 3); - Grid.SetRow(stkOrg, 4); - Grid.SetRow(stkAdvanced, 5); + Grid.SetRow(stkUseDefaultCreds, 4); + Grid.SetRow(stkOrg, 5); + Grid.SetRow(stkAdvanced, 6); // Do UI.. if (!LastBoardWasOnline) @@ -1134,19 +1099,20 @@ private void SetAdvancedGroupBoxVisibility() && ((Model.ClaimsHomeRealmOptionsHomeRealm)ddlAuthSource.SelectedValue).DisplayName.Equals(uiResources.LOGIN_FRM_AUTHTYPE_OAUTH)) { //Disabling Username, Password and Domain textboxes on select of OAuth (On-Prem). - GbAdvanced.IsEnabled = false; - tbUserId.Clear(); - tbPassword.Clear(); - tbDomain.Clear(); + AdvancedOptions.IsEnabled = false; + AdvancedOptions.tbUserId.Clear(); + AdvancedOptions.tbPassword.Clear(); + AdvancedOptions.tbDomain.Clear(); LastSelectionWasOAuth = true; } else if (LastSelectionWasOAuth) { - GbAdvanced.IsEnabled = true; + AdvancedOptions.GbAdvanced.IsEnabled = true; //Reading Username, Password and Domain from config file - tbUserId.Text = StorageUtils.GetConfigKey(ServerConfigKeys, Dynamics_ConfigFileServerKeys.CrmUserName); - tbDomain.Text = StorageUtils.GetConfigKey(ServerConfigKeys, Dynamics_ConfigFileServerKeys.CrmDomain); + AdvancedOptions.tbUserId.Text = StorageUtils.GetConfigKey(ServerConfigKeys, Dynamics_ConfigFileServerKeys.CrmUserName); + AdvancedOptions.tbConnectUrl.Text = StorageUtils.GetConfigKey(ServerConfigKeys, Dynamics_ConfigFileServerKeys.DirectConnectionUri); + AdvancedOptions.tbDomain.Text = StorageUtils.GetConfigKey(ServerConfigKeys, Dynamics_ConfigFileServerKeys.CrmDomain); bool tempBool = true; @@ -1156,10 +1122,10 @@ private void SetAdvancedGroupBoxVisibility() { SecureString password = StorageUtils.GetConfigKey(ServerConfigKeys, Dynamics_ConfigFileServerKeys.CrmPassword); if (password != null) - tbPassword.Password = password.ToUnsecureString(); + AdvancedOptions.tbPassword.Password = password.ToUnsecureString(); } else - tbPassword.Password = string.Empty; + AdvancedOptions.tbPassword.Password = string.Empty; } LastSelectionWasOAuth = false; } @@ -1189,13 +1155,13 @@ private void tbCrmServerPort_KeyDown(object sender, System.Windows.Input.KeyEven e.Key == Key.NumPad9 || e.Key == Key.Tab) return; - else + else + { e.Handled = true; - + SystemSounds.Hand.Play(); + } } - #endregion - private void btnCancel_Click(object sender, RoutedEventArgs e) { CancelConnectCheck(); @@ -1226,6 +1192,20 @@ private void btnConnectOrg_Click(object sender, RoutedEventArgs e) ConnectToSelectedOrg(); } + private void btnCancelOrg_Click(object sender, RoutedEventArgs e) + { + CancelConnectCheck(); + if (UserCancelClicked != null) + UserCancelClicked(this, null); + } + + private void lvOrgList_Loaded(object sender, RoutedEventArgs e) + { + SetSort((GridViewColumnHeader)OrgCol.Header); + } + + #endregion + /// /// To Connect to Selected Org /// @@ -1249,17 +1229,17 @@ private void ConnectToSelectedOrg() ipb.Visibility = Visibility.Visible; btnCancel.Visibility = Visibility.Collapsed; - _connectionManager.ConnectionCheckComplete -= storageAccess_ConnectionCheckComplete; - _connectionManager.ServerConnectionStatusUpdate -= storageAccess_ServerConnectionStatusUpdate; + ConnectionManager.ConnectionCheckComplete -= storageAccess_ConnectionCheckComplete; + ConnectionManager.ServerConnectionStatusUpdate -= storageAccess_ServerConnectionStatusUpdate; - _connectionManager.ConnectionCheckComplete += storageAccess_ConnectionCheckComplete; - _connectionManager.ServerConnectionStatusUpdate += storageAccess_ServerConnectionStatusUpdate; - _connectionManager.ConnectToServerCheck(selectedorg); + ConnectionManager.ConnectionCheckComplete += storageAccess_ConnectionCheckComplete; + ConnectionManager.ServerConnectionStatusUpdate += storageAccess_ServerConnectionStatusUpdate; + ConnectionManager.ConnectToServerCheck(selectedorg); if (stkMessageOrg.IsVisible) stkMessageOrg.Visibility = Visibility.Collapsed; return; } } - if (_connectionManager != null) + if (ConnectionManager != null) { stkMessageOrg.Visibility = Visibility.Visible; tbConnectStatusOrg.Text = uiMessages.CRMCONNECT_NOORG_SEL; @@ -1268,18 +1248,6 @@ private void ConnectToSelectedOrg() } } - private void btnCancelOrg_Click(object sender, RoutedEventArgs e) - { - CancelConnectCheck(); - if (UserCancelClicked != null) - UserCancelClicked(this, null); - } - - private void lvOrgList_Loaded(object sender, RoutedEventArgs e) - { - SetSort((GridViewColumnHeader)OrgCol.Header); - } - /// /// LoginGrid load event to set wid /// @@ -1289,6 +1257,7 @@ private void LoginGrid_Loaded(object sender, RoutedEventArgs e) { SetAdvancedGridWidth(); } + /// /// API Called when Client wants to go back to login and cancel connect. /// @@ -1300,6 +1269,7 @@ public void GoBackToLogin() MessageGrid.Visibility = Visibility.Collapsed; LoginGrid.Visibility = Visibility.Visible; } + /// /// API Called when Client wants to cancel connect. /// @@ -1312,6 +1282,7 @@ public void StartCancelSave() UserCancelClicked(this, null); } } + /// /// Called when the client wants to show the message grid. /// @@ -1328,32 +1299,21 @@ private void cbAdvanced_Checked(object sender, RoutedEventArgs e) if (goAdvancedCheck != null) goAdvancedCheck.Begin(); - GbAdvanced.Visibility = Visibility.Visible; - LoginGrid.RowDefinitions[6].Height = new GridLength(iRow8); - LoginGrid.RowDefinitions[8].Height = new GridLength(iRow5); - AdvancedGrid.RowDefinitions[0].Height = new GridLength(advRow0); - AdvancedGrid.RowDefinitions[3].Height = new GridLength(0); - - GbAdvanced.BorderThickness = new Thickness(1); - GbAdvanced.Header = uiResources.LOGIN_FRM_GB_HEADER; - GbAdvanced.Margin = new Thickness(0, 0, 0, 0); - Grid.SetRow(GbAdvanced, 6); - - tbUserId.IsEnabled = !cbUseDefaultCreds.IsChecked.Value; - tbPassword.IsEnabled = !cbUseDefaultCreds.IsChecked.Value; - tbDomain.IsEnabled = !cbUseDefaultCreds.IsChecked.Value; - if (rbOn365.IsChecked.Value) - tbDomain.IsEnabled = false; + AdvancedOptions.Visibility = Visibility.Visible; + LoginGrid.RowDefinitions[7].Height = new GridLength(_iRow9); + LoginGrid.RowDefinitions[9].Height = new GridLength(_iRow7); + Grid.SetRow(AdvancedOptions, 7); + + AdvancedOptions.tbUserId.IsEnabled = !cbUseDefaultCreds.IsChecked.Value; + AdvancedOptions.tbPassword.IsEnabled = !cbUseDefaultCreds.IsChecked.Value; + AdvancedOptions.tbDomain.IsEnabled = !cbUseDefaultCreds.IsChecked.Value; + AdvancedOptions.DomainVisible = !rbOn365.IsChecked.Value; } else { - GbAdvanced.Header = ""; - GbAdvanced.BorderThickness = new Thickness(0); - AdvancedGrid.RowDefinitions[0].Height = new GridLength(advRow0); - AdvancedGrid.RowDefinitions[3].Height = new GridLength(0); if (goAdvancedUncheck != null) goAdvancedUncheck.Begin(); - GbAdvanced.Visibility = Visibility.Collapsed; + AdvancedOptions.Visibility = Visibility.Collapsed; } } diff --git a/src/GeneralTools/DataverseClient/ConnectControl/Utility/CredentialManager.cs b/src/GeneralTools/DataverseClient/ConnectControl/Utility/CredentialManager.cs index c8155c3..e055f42 100644 --- a/src/GeneralTools/DataverseClient/ConnectControl/Utility/CredentialManager.cs +++ b/src/GeneralTools/DataverseClient/ConnectControl/Utility/CredentialManager.cs @@ -1,4 +1,4 @@ -using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.PowerPlatform.Dataverse.ConnectControl.InternalExtensions; using System; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; diff --git a/src/GeneralTools/DataverseClient/DataverseClient.sln b/src/GeneralTools/DataverseClient/DataverseClient.sln index 5d76a52..8705e17 100644 --- a/src/GeneralTools/DataverseClient/DataverseClient.sln +++ b/src/GeneralTools/DataverseClient/DataverseClient.sln @@ -11,10 +11,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DataverseClient_Core_UnitTe EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerPlatform.Dataverse.Client.Dynamics", "Extensions\DynamicsExtension\Microsoft.PowerPlatform.Dataverse.Client.Dynamics.csproj", "{8CE32D7B-EA3D-4725-A270-9780D366EDB7}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Dynamics.Sdk.Messages.Shell", "Extensions\Microsoft.Dynamics.Sdk.Messages\Microsoft.Dynamics.Sdk.Messages.Shell.csproj", "{503645DD-7711-40ED-8811-F06391F294AB}" -EndProject +#Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Dynamics.Sdk.Messages.Shell", "Extensions\Microsoft.Dynamics.Sdk.Messages\Microsoft.Dynamics.Sdk.Messages.Shell.csproj", "{503645DD-7711-40ED-8811-F06391F294AB}" +#EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LiveTestsConsole", "UnitTests\LiveTestsConsole\LiveTestsConsole.csproj", "{5A1A4FFF-78F5-48A2-9AB0-3E507E938465}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.PowerPlatform.Dataverse.ServiceClientConverter", "Extensions\Microsoft.PowerPlatform.Dataverse.ServiceClientConverter\Microsoft.PowerPlatform.Dataverse.ServiceClientConverter.csproj", "{752E5268-3D99-485D-A31E-FC40AE9C0867}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -41,6 +43,10 @@ Global {5A1A4FFF-78F5-48A2-9AB0-3E507E938465}.Debug|Any CPU.Build.0 = Debug|Any CPU {5A1A4FFF-78F5-48A2-9AB0-3E507E938465}.Release|Any CPU.ActiveCfg = Release|Any CPU {5A1A4FFF-78F5-48A2-9AB0-3E507E938465}.Release|Any CPU.Build.0 = Release|Any CPU + {752E5268-3D99-485D-A31E-FC40AE9C0867}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {752E5268-3D99-485D-A31E-FC40AE9C0867}.Debug|Any CPU.Build.0 = Debug|Any CPU + {752E5268-3D99-485D-A31E-FC40AE9C0867}.Release|Any CPU.ActiveCfg = Release|Any CPU + {752E5268-3D99-485D-A31E-FC40AE9C0867}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/GeneralTools/DataverseClient/DataverseClientWithConnector.sln b/src/GeneralTools/DataverseClient/DataverseClientWithConnector.sln index ee391a0..b19e32c 100644 --- a/src/GeneralTools/DataverseClient/DataverseClientWithConnector.sln +++ b/src/GeneralTools/DataverseClient/DataverseClientWithConnector.sln @@ -11,8 +11,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DataverseClient_Core_UnitTe EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerPlatform.Dataverse.Client.Dynamics", "Extensions\DynamicsExtension\Microsoft.PowerPlatform.Dataverse.Client.Dynamics.csproj", "{8CE32D7B-EA3D-4725-A270-9780D366EDB7}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Dynamics.Sdk.Messages.Shell", "Extensions\Microsoft.Dynamics.Sdk.Messages\Microsoft.Dynamics.Sdk.Messages.Shell.csproj", "{503645DD-7711-40ED-8811-F06391F294AB}" -EndProject +#Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Dynamics.Sdk.Messages.Shell", "Extensions\Microsoft.Dynamics.Sdk.Messages\Microsoft.Dynamics.Sdk.Messages.Shell.csproj", "{503645DD-7711-40ED-8811-F06391F294AB}" +#EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LiveTestsConsole", "UnitTests\LiveTestsConsole\LiveTestsConsole.csproj", "{5A1A4FFF-78F5-48A2-9AB0-3E507E938465}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerPlatform.Dataverse.ConnectControl", "ConnectControl\Microsoft.PowerPlatform.Dataverse.ConnectControl.csproj", "{294C017A-54A0-4582-A512-5B58B9220082}" diff --git a/src/GeneralTools/DataverseClient/Extensions/DynamicsExtension/CdsServiceClientExtensions.cs b/src/GeneralTools/DataverseClient/Extensions/DynamicsExtension/CdsServiceClientExtensions.cs index 2961f90..d9890fa 100644 --- a/src/GeneralTools/DataverseClient/Extensions/DynamicsExtension/CdsServiceClientExtensions.cs +++ b/src/GeneralTools/DataverseClient/Extensions/DynamicsExtension/CdsServiceClientExtensions.cs @@ -1,9 +1,10 @@ -using Microsoft.Crm.Sdk.Messages; using Microsoft.Xrm.Sdk; +using Microsoft.Crm.Sdk.Messages; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using Microsoft.PowerPlatform.Dataverse.Client.Extensions; namespace Microsoft.PowerPlatform.Dataverse.Client.Dynamics { @@ -12,371 +13,371 @@ namespace Microsoft.PowerPlatform.Dataverse.Client.Dynamics /// public static class ServiceClientExtensions { - /// - /// Closes a quote as won or lost, - /// Revise is not supported via this method - /// - /// ID of the quote to close - /// List of fields that need to be updated - /// Status id of the quote, must be greater then 3 but not 7 - /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately - /// Connected Dataverse Service Client - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// - public static Guid CloseQuote(this ServiceClient serviceClient , Guid quoteId, Dictionary fieldList, int quoteStatusCode = 3, Guid batchId = default(Guid), bool bypassPluginExecution = false) - { - serviceClient._logEntry.ResetLastError(); // Reset Last Error - if (serviceClient.DataverseService == null) - { - serviceClient._logEntry.Log("Service not initialized", TraceEventType.Error); - return Guid.Empty; - } - - if (quoteId == Guid.Empty) - return Guid.Empty; - - if (quoteStatusCode < 3) - return Guid.Empty; - - Guid actId = Guid.Empty; - Entity uEnt = new Entity("quoteclose"); - AttributeCollection PropertyList = new AttributeCollection(); - - #region MapCode - if (fieldList != null) - foreach (KeyValuePair field in fieldList) - { - serviceClient.AddValueToPropertyList(field, PropertyList); - } - - // Add the key... - // check to see if the key is in the import set already - if (fieldList != null && !fieldList.ContainsKey("quoteid")) - PropertyList.Add(new KeyValuePair("quoteid", quoteId)); - - if (fieldList != null && fieldList.ContainsKey("activityid")) - actId = (Guid)fieldList["activityid"].Value; - else - { - actId = Guid.NewGuid(); - uEnt.Id = actId; - } - #endregion - uEnt.Attributes.AddRange(PropertyList.ToArray()); - - // 2 types of close supported... Won or Lost. - if (quoteStatusCode == 4) - { - WinQuoteRequest req = new WinQuoteRequest(); - req.QuoteClose = uEnt; - req.Status = new OptionSetValue(quoteStatusCode); - - - if (serviceClient.AddRequestToBatch(batchId, req, "Calling Close Quote as Won", "Request to Close Quote as Won Queued", bypassPluginExecution)) - return Guid.Empty; - - WinQuoteResponse resp = (WinQuoteResponse)serviceClient.Command_Execute(req, "Closing a Quote in CRM as Won", bypassPluginExecution); - if (resp != null) - return actId; - else - return Guid.Empty; - } - else - { - CloseQuoteRequest req = new CloseQuoteRequest(); - req.QuoteClose = uEnt; - req.Status = new OptionSetValue(quoteStatusCode); - - if (serviceClient.AddRequestToBatch(batchId, req, "Calling Close Quote as Lost", "Request to Close Quote as Lost Queued", bypassPluginExecution)) - return Guid.Empty; - - CloseQuoteResponse resp = (CloseQuoteResponse)serviceClient.Command_Execute(req, "Closing a Quote in CRM as Lost", bypassPluginExecution); - if (resp != null) - return actId; - else - return Guid.Empty; - } - } - - - /// - /// This will close an opportunity as either Won or lost in CRM - /// - /// ID of the opportunity to close - /// List of fields for the Opportunity Close Entity - /// Status code of Opportunity, Should be either 1 or 2, defaults to 1 ( won ) - /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately - /// Connected Dataverse Service Client - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// - public static Guid CloseOpportunity(this ServiceClient serviceClient, Guid opportunityId, Dictionary fieldList, int opportunityStatusCode = 3, Guid batchId = default(Guid), bool bypassPluginExecution = false) - { - serviceClient._logEntry.ResetLastError(); // Reset Last Error - if (serviceClient.DataverseService == null) - { - serviceClient._logEntry.Log("Service not initialized", TraceEventType.Error); - return Guid.Empty; - } - - if (opportunityId == Guid.Empty) - return Guid.Empty; - - if (opportunityStatusCode < 3) - return Guid.Empty; - - Guid actId = Guid.Empty; - Entity uEnt = new Entity("opportunityclose"); - AttributeCollection PropertyList = new AttributeCollection(); - - #region MapCode - if (fieldList != null) - foreach (KeyValuePair field in fieldList) - { - serviceClient.AddValueToPropertyList(field, PropertyList); - } - - // Add the key... - // check to see if the key is in the import set allready - if (fieldList != null && !fieldList.ContainsKey("opportunityid")) - PropertyList.Add(new KeyValuePair("opportunityid", opportunityId)); - - if (fieldList != null && fieldList.ContainsKey("activityid")) - actId = (Guid)fieldList["activityid"].Value; - else - { - actId = Guid.NewGuid(); - uEnt.Id = actId; - } - #endregion - uEnt.Attributes.AddRange(PropertyList.ToArray()); - - // 2 types of close supported... Won or Lost. - if (opportunityStatusCode == 3) - { - WinOpportunityRequest req = new WinOpportunityRequest(); - req.OpportunityClose = uEnt; - req.Status = new OptionSetValue(opportunityStatusCode); - - if (serviceClient.AddRequestToBatch(batchId, req, "Calling Close Opportunity as Won", "Request to Close Opportunity as Won Queued", bypassPluginExecution)) - return Guid.Empty; - - WinOpportunityResponse resp = (WinOpportunityResponse)serviceClient.Command_Execute(req, "Closing a Opportunity in CRM as Won", bypassPluginExecution); - if (resp != null) - return actId; - else - return Guid.Empty; - } - else - { - LoseOpportunityRequest req = new LoseOpportunityRequest(); - req.OpportunityClose = uEnt; - req.Status = new OptionSetValue(opportunityStatusCode); - - if (serviceClient.AddRequestToBatch(batchId, req, "Calling Close Opportunity as Lost", "Request to Close Opportunity as Lost Queued",bypassPluginExecution)) - return Guid.Empty; - - LoseOpportunityResponse resp = (LoseOpportunityResponse)serviceClient.Command_Execute(req, "Closing a Opportunity in CRM as Lost",bypassPluginExecution); - if (resp != null) - return actId; - else - return Guid.Empty; - } - } - - /// - /// Closes an Incident request in CRM, - /// this special handling is necessary to support CRM Built In Object. - /// - /// ID of the CRM Incident to close - /// List of data items to add to the request, By default, subject is required. - /// Status code to close the incident with, defaults to resolved - /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately - /// Connected Dataverse Service Client - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// Guid of the Activity. - public static Guid CloseIncident(this ServiceClient serviceClient, Guid incidentId, Dictionary fieldList, int incidentStatusCode = 5, Guid batchId = default(Guid), bool bypassPluginExecution = false) - { - serviceClient._logEntry.ResetLastError(); // Reset Last Error - if (serviceClient.DataverseService == null) - { - serviceClient._logEntry.Log("Service not initialized", TraceEventType.Error); - return Guid.Empty; - } - - if (incidentId == Guid.Empty) - return Guid.Empty; - - Guid actId = Guid.Empty; - Entity uEnt = new Entity("incidentresolution"); - AttributeCollection PropertyList = new AttributeCollection(); - - #region MapCode - if (fieldList != null) - foreach (KeyValuePair field in fieldList) - { - serviceClient.AddValueToPropertyList(field, PropertyList); - } - - // Add the key... - // check to see if the key is in the import set already - if (fieldList != null && !fieldList.ContainsKey("incidentid")) - PropertyList.Add(new KeyValuePair("incidentid", new EntityReference("incident", incidentId))); - - if (fieldList != null && fieldList.ContainsKey("activityid")) - actId = (Guid)fieldList["activityid"].Value; - else - { - actId = Guid.NewGuid(); - uEnt.Id = actId; - } - #endregion - uEnt.Attributes.AddRange(PropertyList.ToArray()); - - - CloseIncidentRequest req4 = new CloseIncidentRequest(); - req4.IncidentResolution = uEnt; - req4.Status = new OptionSetValue(incidentStatusCode); - - if (serviceClient.AddRequestToBatch(batchId, req4, "Calling Close Incident", "Request to Close Incident Queued", bypassPluginExecution)) - return Guid.Empty; - - - CloseIncidentResponse resp4 = (CloseIncidentResponse)serviceClient.Command_Execute(req4, "Closing a incidentId in CRM",bypassPluginExecution); - if (resp4 != null) - return actId; - else - return Guid.Empty; - - - } - - /// - /// Cancel Sales order - /// - /// Sales order id to close - /// List of fields to add - /// Status code of the order - /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately - /// Connected Dataverse Service Client - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// - public static Guid CancelSalesOrder(this ServiceClient serviceClient, Guid salesOrderId, Dictionary fieldList, int orderStatusCode = 4, Guid batchId = default(Guid), bool bypassPluginExecution = false) - { - serviceClient._logEntry.ResetLastError(); // Reset Last Error - if (serviceClient.DataverseService == null) - { - serviceClient._logEntry.Log("Service not initialized", TraceEventType.Error); - return Guid.Empty; - } - - if (salesOrderId == Guid.Empty) - return Guid.Empty; - - if (orderStatusCode < 4) - return Guid.Empty; - - Guid actId = Guid.Empty; - Entity uEnt = new Entity("orderclose"); - AttributeCollection PropertyList = new AttributeCollection(); - - #region MapCode - if (fieldList != null) - foreach (KeyValuePair field in fieldList) - { - serviceClient.AddValueToPropertyList(field, PropertyList); - } - - // Add the key... - // check to see if the key is in the import set allready - if (fieldList != null && !fieldList.ContainsKey("salesorderid")) - PropertyList.Add(new KeyValuePair("salesorderid", salesOrderId)); - - if (fieldList != null && fieldList.ContainsKey("activityid")) - actId = (Guid)fieldList["activityid"].Value; - else - { - actId = Guid.NewGuid(); - uEnt.Id = actId; - } - #endregion - uEnt.Attributes.AddRange(PropertyList.ToArray()); - - CancelSalesOrderRequest req = new CancelSalesOrderRequest(); - req.OrderClose = uEnt; - req.Status = new OptionSetValue(orderStatusCode); - - if (serviceClient.AddRequestToBatch(batchId, req, "Calling Close Sales Order", "Request to Close Sales Order Queued",bypassPluginExecution)) - return Guid.Empty; - - - CancelSalesOrderResponse resp = (CancelSalesOrderResponse)serviceClient.Command_Execute(req, "Closing a Sales Order in CRM as Closed",bypassPluginExecution); - if (resp != null) - return actId; - else - return Guid.Empty; - - } - - - /// - /// Closes a Trouble ticket by ID - /// - /// ID of the Ticket to close - /// Title of the close ticket record - /// Description of the closed ticket - /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately - /// Connected Dataverse Service Client - /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. - /// Returns the ID of the closed ticket - public static Guid CloseTroubleTicket(this ServiceClient serviceClient, Guid ticketId, string subject, string description, Guid batchId = default(Guid), bool bypassPluginExecution = false) - { - // ONE OF THEASE SOULD BE MADE THE MASTER - - serviceClient._logEntry.ResetLastError(); // Reset Last Error - if (serviceClient.DataverseService == null) - { - serviceClient._logEntry.Log("Service not initialized", TraceEventType.Error); - return Guid.Empty; - } - - if (ticketId == Guid.Empty) - return Guid.Empty; - - // Create Incident Resolution Type - - Entity reso = new Entity("incidentresolution"); - - Guid closeTicketId = Guid.NewGuid(); - reso.Attributes.Add("activityid", closeTicketId); - reso.Attributes.Add("incidentid", new EntityReference("incident", ticketId)); - - // NEED TO REWORK THIS WITH METAD DATA> - reso.Attributes.Add("statecode", new OptionSetValue(1)); - reso.Attributes.Add("statuscode", new OptionSetValue(2)); - reso.Attributes.Add("subject", subject); - reso.Attributes.Add("description", description); - - // Set Close Time Stamp - reso.Attributes.Add("actualend", DateTime.Now.ToString()); - - // Get State close for Resolving a case - int defaultStateCodeForResolveCase = 1; - CloseIncidentRequest req4 = new CloseIncidentRequest(); - req4.IncidentResolution = reso; - req4.Status = new OptionSetValue(defaultStateCodeForResolveCase); - - - if (serviceClient.AddRequestToBatch(batchId, req4, "Calling Close Incident", "Request to Close Incident Queued",bypassPluginExecution)) - return Guid.Empty; + /// + /// Closes a quote as won or lost, + /// Revise is not supported via this method + /// + /// ID of the quote to close + /// List of fields that need to be updated + /// Status id of the quote, must be greater then 3 but not 7 + /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately + /// Connected Dataverse Service Client + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// + public static Guid CloseQuote(this ServiceClient serviceClient, Guid quoteId, Dictionary fieldList, int quoteStatusCode = 3, Guid batchId = default(Guid), bool bypassPluginExecution = false) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Service not initialized", TraceEventType.Error); + return Guid.Empty; + } + + if (quoteId == Guid.Empty) + return Guid.Empty; + + if (quoteStatusCode < 3) + return Guid.Empty; + + Guid actId = Guid.Empty; + Entity uEnt = new Entity("quoteclose"); + AttributeCollection PropertyList = new AttributeCollection(); + + #region MapCode + if (fieldList != null) + foreach (KeyValuePair field in fieldList) + { + serviceClient.AddValueToPropertyList(field, PropertyList); + } + + // Add the key... + // check to see if the key is in the import set already + if (fieldList != null && !fieldList.ContainsKey("quoteid")) + PropertyList.Add(new KeyValuePair("quoteid", quoteId)); + + if (fieldList != null && fieldList.ContainsKey("activityid")) + actId = (Guid)fieldList["activityid"].Value; + else + { + actId = Guid.NewGuid(); + uEnt.Id = actId; + } + #endregion + uEnt.Attributes.AddRange(PropertyList.ToArray()); + + // 2 types of close supported... Won or Lost. + if (quoteStatusCode == 4) + { + WinQuoteRequest req = new WinQuoteRequest(); + req.QuoteClose = uEnt; + req.Status = new OptionSetValue(quoteStatusCode); + + + if (serviceClient.AddRequestToBatch(batchId, req, "Calling Close Quote as Won", "Request to Close Quote as Won Queued", bypassPluginExecution)) + return Guid.Empty; + + WinQuoteResponse resp = (WinQuoteResponse)serviceClient.Command_Execute(req, "Closing a Quote in CRM as Won", bypassPluginExecution); + if (resp != null) + return actId; + else + return Guid.Empty; + } + else + { + CloseQuoteRequest req = new CloseQuoteRequest(); + req.QuoteClose = uEnt; + req.Status = new OptionSetValue(quoteStatusCode); + + if (serviceClient.AddRequestToBatch(batchId, req, "Calling Close Quote as Lost", "Request to Close Quote as Lost Queued", bypassPluginExecution)) + return Guid.Empty; + + CloseQuoteResponse resp = (CloseQuoteResponse)serviceClient.Command_Execute(req, "Closing a Quote in CRM as Lost", bypassPluginExecution); + if (resp != null) + return actId; + else + return Guid.Empty; + } + } + + + /// + /// This will close an opportunity as either Won or lost in CRM + /// + /// ID of the opportunity to close + /// List of fields for the Opportunity Close Entity + /// Status code of Opportunity, Should be either 1 or 2, defaults to 1 ( won ) + /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately + /// Connected Dataverse Service Client + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// + public static Guid CloseOpportunity(this ServiceClient serviceClient, Guid opportunityId, Dictionary fieldList, int opportunityStatusCode = 3, Guid batchId = default(Guid), bool bypassPluginExecution = false) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Service not initialized", TraceEventType.Error); + return Guid.Empty; + } + + if (opportunityId == Guid.Empty) + return Guid.Empty; + + if (opportunityStatusCode < 3) + return Guid.Empty; + + Guid actId = Guid.Empty; + Entity uEnt = new Entity("opportunityclose"); + AttributeCollection PropertyList = new AttributeCollection(); + + #region MapCode + if (fieldList != null) + foreach (KeyValuePair field in fieldList) + { + serviceClient.AddValueToPropertyList(field, PropertyList); + } + + // Add the key... + // check to see if the key is in the import set allready + if (fieldList != null && !fieldList.ContainsKey("opportunityid")) + PropertyList.Add(new KeyValuePair("opportunityid", opportunityId)); + + if (fieldList != null && fieldList.ContainsKey("activityid")) + actId = (Guid)fieldList["activityid"].Value; + else + { + actId = Guid.NewGuid(); + uEnt.Id = actId; + } + #endregion + uEnt.Attributes.AddRange(PropertyList.ToArray()); + + // 2 types of close supported... Won or Lost. + if (opportunityStatusCode == 3) + { + WinOpportunityRequest req = new WinOpportunityRequest(); + req.OpportunityClose = uEnt; + req.Status = new OptionSetValue(opportunityStatusCode); + + if (serviceClient.AddRequestToBatch(batchId, req, "Calling Close Opportunity as Won", "Request to Close Opportunity as Won Queued", bypassPluginExecution)) + return Guid.Empty; + + WinOpportunityResponse resp = (WinOpportunityResponse)serviceClient.Command_Execute(req, "Closing a Opportunity in CRM as Won", bypassPluginExecution); + if (resp != null) + return actId; + else + return Guid.Empty; + } + else + { + LoseOpportunityRequest req = new LoseOpportunityRequest(); + req.OpportunityClose = uEnt; + req.Status = new OptionSetValue(opportunityStatusCode); + + if (serviceClient.AddRequestToBatch(batchId, req, "Calling Close Opportunity as Lost", "Request to Close Opportunity as Lost Queued", bypassPluginExecution)) + return Guid.Empty; + + LoseOpportunityResponse resp = (LoseOpportunityResponse)serviceClient.Command_Execute(req, "Closing a Opportunity in CRM as Lost", bypassPluginExecution); + if (resp != null) + return actId; + else + return Guid.Empty; + } + } + + /// + /// Closes an Incident request in CRM, + /// this special handling is necessary to support CRM Built In Object. + /// + /// ID of the CRM Incident to close + /// List of data items to add to the request, By default, subject is required. + /// Status code to close the incident with, defaults to resolved + /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately + /// Connected Dataverse Service Client + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// Guid of the Activity. + public static Guid CloseIncident(this ServiceClient serviceClient, Guid incidentId, Dictionary fieldList, int incidentStatusCode = 5, Guid batchId = default(Guid), bool bypassPluginExecution = false) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Service not initialized", TraceEventType.Error); + return Guid.Empty; + } + + if (incidentId == Guid.Empty) + return Guid.Empty; + + Guid actId = Guid.Empty; + Entity uEnt = new Entity("incidentresolution"); + AttributeCollection PropertyList = new AttributeCollection(); + + #region MapCode + if (fieldList != null) + foreach (KeyValuePair field in fieldList) + { + serviceClient.AddValueToPropertyList(field, PropertyList); + } + + // Add the key... + // check to see if the key is in the import set already + if (fieldList != null && !fieldList.ContainsKey("incidentid")) + PropertyList.Add(new KeyValuePair("incidentid", new EntityReference("incident", incidentId))); + + if (fieldList != null && fieldList.ContainsKey("activityid")) + actId = (Guid)fieldList["activityid"].Value; + else + { + actId = Guid.NewGuid(); + uEnt.Id = actId; + } + #endregion + uEnt.Attributes.AddRange(PropertyList.ToArray()); + + + CloseIncidentRequest req4 = new CloseIncidentRequest(); + req4.IncidentResolution = uEnt; + req4.Status = new OptionSetValue(incidentStatusCode); + + if (serviceClient.AddRequestToBatch(batchId, req4, "Calling Close Incident", "Request to Close Incident Queued", bypassPluginExecution)) + return Guid.Empty; + + + CloseIncidentResponse resp4 = (CloseIncidentResponse)serviceClient.Command_Execute(req4, "Closing a incidentId in CRM", bypassPluginExecution); + if (resp4 != null) + return actId; + else + return Guid.Empty; + + + } + + /// + /// Cancel Sales order + /// + /// Sales order id to close + /// List of fields to add + /// Status code of the order + /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately + /// Connected Dataverse Service Client + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// + public static Guid CancelSalesOrder(this ServiceClient serviceClient, Guid salesOrderId, Dictionary fieldList, int orderStatusCode = 4, Guid batchId = default(Guid), bool bypassPluginExecution = false) + { + serviceClient._logEntry.ResetLastError(); // Reset Last Error + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Service not initialized", TraceEventType.Error); + return Guid.Empty; + } + + if (salesOrderId == Guid.Empty) + return Guid.Empty; + + if (orderStatusCode < 4) + return Guid.Empty; + + Guid actId = Guid.Empty; + Entity uEnt = new Entity("orderclose"); + AttributeCollection PropertyList = new AttributeCollection(); + + #region MapCode + if (fieldList != null) + foreach (KeyValuePair field in fieldList) + { + serviceClient.AddValueToPropertyList(field, PropertyList); + } + + // Add the key... + // check to see if the key is in the import set allready + if (fieldList != null && !fieldList.ContainsKey("salesorderid")) + PropertyList.Add(new KeyValuePair("salesorderid", salesOrderId)); + + if (fieldList != null && fieldList.ContainsKey("activityid")) + actId = (Guid)fieldList["activityid"].Value; + else + { + actId = Guid.NewGuid(); + uEnt.Id = actId; + } + #endregion + uEnt.Attributes.AddRange(PropertyList.ToArray()); + + CancelSalesOrderRequest req = new CancelSalesOrderRequest(); + req.OrderClose = uEnt; + req.Status = new OptionSetValue(orderStatusCode); + + if (serviceClient.AddRequestToBatch(batchId, req, "Calling Close Sales Order", "Request to Close Sales Order Queued", bypassPluginExecution)) + return Guid.Empty; + + + CancelSalesOrderResponse resp = (CancelSalesOrderResponse)serviceClient.Command_Execute(req, "Closing a Sales Order in CRM as Closed", bypassPluginExecution); + if (resp != null) + return actId; + else + return Guid.Empty; + + } + + + /// + /// Closes a Trouble ticket by ID + /// + /// ID of the Ticket to close + /// Title of the close ticket record + /// Description of the closed ticket + /// Optional: if set to a valid GUID, generated by the Create Batch Request Method, will assigned the request to the batch for later execution, on fail, runs the request immediately + /// Connected Dataverse Service Client + /// Adds the bypass plugin behavior to this request. Note: this will only apply if the caller has the prvBypassPlugins permission to bypass plugins. If its attempted without the permission the request will fault. + /// Returns the ID of the closed ticket + public static Guid CloseTroubleTicket(this ServiceClient serviceClient, Guid ticketId, string subject, string description, Guid batchId = default(Guid), bool bypassPluginExecution = false) + { + // ONE OF THEASE SOULD BE MADE THE MASTER + + serviceClient._logEntry.ResetLastError(); // Reset Last Error + if (serviceClient.DataverseService == null) + { + serviceClient._logEntry.Log("Service not initialized", TraceEventType.Error); + return Guid.Empty; + } + + if (ticketId == Guid.Empty) + return Guid.Empty; + + // Create Incident Resolution Type + + Entity reso = new Entity("incidentresolution"); + + Guid closeTicketId = Guid.NewGuid(); + reso.Attributes.Add("activityid", closeTicketId); + reso.Attributes.Add("incidentid", new EntityReference("incident", ticketId)); + + // NEED TO REWORK THIS WITH METAD DATA> + reso.Attributes.Add("statecode", new OptionSetValue(1)); + reso.Attributes.Add("statuscode", new OptionSetValue(2)); + reso.Attributes.Add("subject", subject); + reso.Attributes.Add("description", description); + + // Set Close Time Stamp + reso.Attributes.Add("actualend", DateTime.Now.ToString()); + + // Get State close for Resolving a case + int defaultStateCodeForResolveCase = 1; + CloseIncidentRequest req4 = new CloseIncidentRequest(); + req4.IncidentResolution = reso; + req4.Status = new OptionSetValue(defaultStateCodeForResolveCase); + + + if (serviceClient.AddRequestToBatch(batchId, req4, "Calling Close Incident", "Request to Close Incident Queued", bypassPluginExecution)) + return Guid.Empty; - CloseIncidentResponse resp4 = (CloseIncidentResponse)serviceClient.Command_Execute(req4, "Closing a Case in CRM",bypassPluginExecution); - if (resp4 != null) - return closeTicketId; - else - return Guid.Empty; - } - - - - } + CloseIncidentResponse resp4 = (CloseIncidentResponse)serviceClient.Command_Execute(req4, "Closing a Case in CRM", bypassPluginExecution); + if (resp4 != null) + return closeTicketId; + else + return Guid.Empty; + } + + + + } } 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 6293beb..3ea8778 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 @@ -14,7 +14,7 @@ - + diff --git a/src/GeneralTools/DataverseClient/Extensions/Microsoft.Dynamics.Sdk.Messages/Microsoft.Dynamics.Sdk.Messages.Shell.csproj b/src/GeneralTools/DataverseClient/Extensions/Microsoft.Dynamics.Sdk.Messages/Microsoft.Dynamics.Sdk.Messages.Shell.csproj index c7fac2c..2c76ec0 100644 --- a/src/GeneralTools/DataverseClient/Extensions/Microsoft.Dynamics.Sdk.Messages/Microsoft.Dynamics.Sdk.Messages.Shell.csproj +++ b/src/GeneralTools/DataverseClient/Extensions/Microsoft.Dynamics.Sdk.Messages/Microsoft.Dynamics.Sdk.Messages.Shell.csproj @@ -9,7 +9,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 new file mode 100644 index 0000000..1f700a1 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Extensions/Microsoft.PowerPlatform.Dataverse.ServiceClientConverter/Microsoft.PowerPlatform.Dataverse.ServiceClientConverter.csproj @@ -0,0 +1,15 @@ + + + + Microsoft.PowerPlatform.Dataverse.ServiceClientConverter + DataverseClient-Converter + true + + + + + false + $(OutDir)\Microsoft.PowerPlatform.Dataverse.ServiceClientConverter.xml + + + diff --git a/src/GeneralTools/DataverseClient/Extensions/Microsoft.PowerPlatform.Dataverse.ServiceClientConverter/Properties/AssemblyInfo.cs b/src/GeneralTools/DataverseClient/Extensions/Microsoft.PowerPlatform.Dataverse.ServiceClientConverter/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..d1e29e7 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Extensions/Microsoft.PowerPlatform.Dataverse.ServiceClientConverter/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Microsoft.PowerPlatform.Dataverse.ServiceClientConverter")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Microsoft.PowerPlatform.Dataverse.ServiceClientConverter")] +[assembly: AssemblyCopyright("Copyright © 2022")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("752e5268-3d99-485d-a31e-fc40ae9c0867")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/GeneralTools/DataverseClient/Extensions/Microsoft.PowerPlatform.Dataverse.ServiceClientConverter/ServiceClientConverter.cs b/src/GeneralTools/DataverseClient/Extensions/Microsoft.PowerPlatform.Dataverse.ServiceClientConverter/ServiceClientConverter.cs new file mode 100644 index 0000000..be9b9c7 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Extensions/Microsoft.PowerPlatform.Dataverse.ServiceClientConverter/ServiceClientConverter.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PowerPlatform.Dataverse.ServiceClientConverter +{ + /// + /// + /// + public static class ServiceClientConverter + { + // TBD:// Post next release add converter to this method based on Nuget Packages. + } +} diff --git a/src/GeneralTools/DataverseClient/Testers/LoginControlTester/MainWindow.xaml b/src/GeneralTools/DataverseClient/Testers/LoginControlTester/MainWindow.xaml index ed2dd97..cef7882 100644 --- a/src/GeneralTools/DataverseClient/Testers/LoginControlTester/MainWindow.xaml +++ b/src/GeneralTools/DataverseClient/Testers/LoginControlTester/MainWindow.xaml @@ -1,10 +1,14 @@ - + Icon="/Dataverse.ico" + xmlns:my="clr-namespace:Microsoft.PowerPlatform.Dataverse.ConnectControl;assembly=Microsoft.PowerPlatform.Dataverse.ConnectControl" + Loaded="Window_Loaded" ResizeMode="CanResize" WindowStartupLocation="CenterScreen" WindowStyle="None" AllowDrop="True" AllowsTransparency="True" + BorderThickness="1" BorderBrush="#173561" SizeToContent="Width" + FocusManager.FocusedElement="{Binding ElementName=CrmLoginCtrl}" + > diff --git a/src/GeneralTools/DataverseClient/Testers/LoginControlTester/MainWindow.xaml.cs b/src/GeneralTools/DataverseClient/Testers/LoginControlTester/MainWindow.xaml.cs index 6faeb47..3f52005 100644 --- a/src/GeneralTools/DataverseClient/Testers/LoginControlTester/MainWindow.xaml.cs +++ b/src/GeneralTools/DataverseClient/Testers/LoginControlTester/MainWindow.xaml.cs @@ -1,4 +1,4 @@ -//=============================================================================== +//=============================================================================== // MICROSOFT SAMPLE // Microsoft Dynamics CRM 2010 // Project: Dynamics CRM Connect Control Login Control Tester @@ -22,6 +22,7 @@ using System.Threading; using System.Windows.Threading; using System.Net; +using Microsoft.PowerPlatform.Dataverse.Client.Extensions; namespace LoginControlTester { @@ -112,6 +113,7 @@ private void Window_Loaded(object sender, RoutedEventArgs e) if (MessageBox.Show(LoginControlTester.Resources.Resources.CREDENTIALS_ALREADY_SAVED_IN_CONFIGURATION, LoginControlTester.Resources.Resources.AUTO_LOGIN, MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes) { CrmLoginCtrl.IsEnabled = false; + // When running an auto login, you need to wire and listen to the events from the connection manager. // Run Auto User Login process, Wire events. _connectionManager.ServerConnectionStatusUpdate += new EventHandler(mgr_ServerConnectionStatusUpdate); diff --git a/src/GeneralTools/DataverseClient/Testers/LoginControlTester/Resources/Resources.resx b/src/GeneralTools/DataverseClient/Testers/LoginControlTester/Resources/Resources.resx index 82cf2c9..9d20c4b 100644 --- a/src/GeneralTools/DataverseClient/Testers/LoginControlTester/Resources/Resources.resx +++ b/src/GeneralTools/DataverseClient/Testers/LoginControlTester/Resources/Resources.resx @@ -1,4 +1,4 @@ - + - - - - - + + + + + - - - - + + + - - + + diff --git a/src/GeneralTools/DataverseClient/UIStyles/Microsoft.PowerPlatform.Dataverse.Ui.Styles.csproj b/src/GeneralTools/DataverseClient/UIStyles/Microsoft.PowerPlatform.Dataverse.Ui.Styles.csproj index 5793a2f..ee95b13 100644 --- a/src/GeneralTools/DataverseClient/UIStyles/Microsoft.PowerPlatform.Dataverse.Ui.Styles.csproj +++ b/src/GeneralTools/DataverseClient/UIStyles/Microsoft.PowerPlatform.Dataverse.Ui.Styles.csproj @@ -7,6 +7,7 @@ Microsoft.PowerPlatform.Dataverse.Ui.Styles {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} win + true @@ -29,220 +30,17 @@ - - Designer - MSBuild:Compile - - - Designer - MSBuild:Compile - - - MSBuild:Compile - Designer - - - MSBuild:Compile - Designer - - - MSBuild:Compile - Designer - - - Designer - MSBuild:Compile - - - MSBuild:Compile - Designer - - - MSBuild:Compile - Designer - - - MSBuild:Compile - Designer - - - MSBuild:Compile - Designer - - - MSBuild:Compile - Designer - - - MSBuild:Compile - Designer - - - MSBuild:Compile - Designer - - - MSBuild:Compile - Designer - - - Designer - MSBuild:Compile - - - MSBuild:Compile - Designer - - - MSBuild:Compile - Designer - - - Designer - MSBuild:Compile - - - MSBuild:Compile - Designer - - - MSBuild:Compile - Designer - - - Designer - MSBuild:Compile - - - MSBuild:Compile - Designer - - - MSBuild:Compile - Designer - - - MSBuild:Compile - Designer - - - MSBuild:Compile - Designer - - - MSBuild:Compile - Designer - - - Designer - MSBuild:Compile - - - MSBuild:Compile - Designer - - - MSBuild:Compile - Designer - - - Designer - MSBuild:Compile - - - MSBuild:Compile - Designer - - - MSBuild:Compile - Designer - - - MSBuild:Compile - Designer - - - MSBuild:Compile - Designer - - - MSBuild:Compile - Designer - - - MSBuild:Compile - Designer - - - Designer - MSBuild:Compile - - - MSBuild:Compile - Designer - - - MSBuild:Compile - Designer - - - Designer - MSBuild:Compile - - - MSBuild:Compile - Designer - - - MSBuild:Compile - Designer - - - Designer - MSBuild:Compile - - - MSBuild:Compile - Designer - - - MSBuild:Compile - Designer - - - Designer - MSBuild:Compile - - - MSBuild:Compile - Designer - - - MSBuild:Compile - Designer - - - Designer - MSBuild:Compile - - - MSBuild:Compile - Designer - - - MSBuild:Compile - Designer - - - MSBuild:Compile - Designer - - - MSBuild:Compile - Designer - + + + True + True + Resources.resx + - + + PublicResXFileCodeGenerator + Resources.Designer.cs + \ No newline at end of file diff --git a/src/GeneralTools/DataverseClient/UIStyles/Properties/Resources.Designer.cs b/src/GeneralTools/DataverseClient/UIStyles/Properties/Resources.Designer.cs index 3418066..10fbca3 100644 --- a/src/GeneralTools/DataverseClient/UIStyles/Properties/Resources.Designer.cs +++ b/src/GeneralTools/DataverseClient/UIStyles/Properties/Resources.Designer.cs @@ -19,10 +19,10 @@ namespace Microsoft.PowerPlatform.Dataverse.Ui.Styles.Properties { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { + public class Resources { private static global::System.Resources.ResourceManager resourceMan; @@ -36,7 +36,7 @@ internal Resources() { /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { + public static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.PowerPlatform.Dataverse.Ui.Styles.Properties.Resources", typeof(Resources).Assembly); @@ -51,7 +51,7 @@ internal Resources() { /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { + public static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -63,7 +63,7 @@ internal Resources() { /// /// Looks up a localized string similar to Close. /// - internal static string Close { + public static string Close { get { return ResourceManager.GetString("Close", resourceCulture); } @@ -72,7 +72,7 @@ internal static string Close { /// /// Looks up a localized string similar to Maximize. /// - internal static string Maximize { + public static string Maximize { get { return ResourceManager.GetString("Maximize", resourceCulture); } @@ -81,7 +81,7 @@ internal static string Maximize { /// /// Looks up a localized string similar to Minimize. /// - internal static string Minimize { + public static string Minimize { get { return ResourceManager.GetString("Minimize", resourceCulture); } diff --git a/src/GeneralTools/DataverseClient/UIStyles/Properties/Resources.resx b/src/GeneralTools/DataverseClient/UIStyles/Properties/Resources.resx index 91e1534..0aea0dd 100644 --- a/src/GeneralTools/DataverseClient/UIStyles/Properties/Resources.resx +++ b/src/GeneralTools/DataverseClient/UIStyles/Properties/Resources.resx @@ -119,6 +119,7 @@ Close + Close Maximize diff --git a/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/DynamicsExtensionsTests.cs b/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/DynamicsExtensionsTests.cs index 968b8c7..3dda326 100644 --- a/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/DynamicsExtensionsTests.cs +++ b/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/DynamicsExtensionsTests.cs @@ -1,4 +1,4 @@ -using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.PowerPlatform.Dataverse.Client; using Microsoft.PowerPlatform.Dataverse.Client.Dynamics; using Microsoft.Crm.Sdk.Messages; using Microsoft.Xrm.Sdk; @@ -14,6 +14,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using DataverseClient_Core_UnitTests; +using Microsoft.PowerPlatform.Dataverse.Client.Extensions; namespace Client_Core_UnitTests { diff --git a/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/MoqHttpMessagehander.cs b/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/MoqHttpMessagehander.cs index d8a75aa..48ebb88 100644 --- a/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/MoqHttpMessagehander.cs +++ b/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/MoqHttpMessagehander.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; @@ -16,7 +16,11 @@ public virtual HttpResponseMessage Send(HttpRequestMessage request) protected override Task SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { - return Task.FromResult(Send(request)); + lock (this) + { + return Task.FromResult(Send(request)); + } + } } } diff --git a/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/ServiceClientTests.cs b/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/ServiceClientTests.cs index d9f2da0..75d6438 100644 --- a/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/ServiceClientTests.cs +++ b/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/ServiceClientTests.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerPlatform.Dataverse.Client; using Microsoft.PowerPlatform.Dataverse.Client.Auth; +using Microsoft.PowerPlatform.Dataverse.Client.Extensions; using Microsoft.PowerPlatform.Dataverse.Client.Utils; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Messages; @@ -132,10 +133,7 @@ public void DeleteRequestTests() fakHttpMethodHander.Setup(s => s.Send(It.Is(f => f.Method.ToString().Equals("delete", StringComparison.OrdinalIgnoreCase)))).Returns(new HttpResponseMessage(System.Net.HttpStatusCode.OK)); orgSvc.Setup(f => f.Execute(It.Is(p => p.Target.LogicalName.Equals("account") && p.Target.Id.Equals(testSupport._DefaultId)))).Returns(new DeleteResponse()); - bool rslt = cli.ExecuteEntityDeleteRequest("account", testSupport._DefaultId); - Assert.True(rslt); - - rslt = cli.DeleteEntity("account", testSupport._DefaultId); + bool rslt = cli.DeleteEntity("account", testSupport._DefaultId); Assert.True(rslt); } @@ -660,7 +658,7 @@ public void ResetLocalMetadataCacheTest() //} [Fact] - public void TestResponseHeaderBehavior() + public void TestResponseHeaderWebAPIBehavior() { Mock orgSvc = null; Mock fakHttpMethodHander = null; @@ -670,25 +668,51 @@ public void TestResponseHeaderBehavior() // Setup handlers to deal with both orgRequest and WebAPI request. int baseTestDOP = 10; - int defaultDOP = 5; var httpResp = new HttpResponseMessage(System.Net.HttpStatusCode.OK); httpResp.Headers.Add(Utilities.ResponseHeaders.RECOMMENDEDDEGREESOFPARALLELISM, baseTestDOP.ToString()); - fakHttpMethodHander.Setup(s => s.Send(It.Is(f => f.Method.ToString().Equals("delete", StringComparison.OrdinalIgnoreCase)))).Returns(httpResp); orgSvc.Setup(f => f.Execute(It.Is(p => p.Target.LogicalName.Equals("account") && p.Target.Id.Equals(testSupport._DefaultId)))).Returns(new DeleteResponse()); + fakHttpMethodHander.Setup(s => s.Send(It.Is(f => f.Method.ToString().Equals("delete", StringComparison.OrdinalIgnoreCase)))).Returns(httpResp); // Tests/ - cli.UseWebApi = false; - bool rslt = cli.ExecuteEntityDeleteRequest("account", testSupport._DefaultId); + cli.UseWebApi = true; + bool rslt = cli.DeleteEntity("account", testSupport._DefaultId); Assert.True(rslt); - Assert.Equal(defaultDOP, cli.RecommendedDegreesOfParallelism); + Assert.Equal(baseTestDOP, cli.RecommendedDegreesOfParallelism); - cli.UseWebApi = true; cli.Delete("account", testSupport._DefaultId); Assert.Equal(baseTestDOP, cli.RecommendedDegreesOfParallelism); + } + [Fact] + public void TestResponseHeaderBehavior() + { + Mock orgSvc = null; + Mock fakHttpMethodHander = null; + ServiceClient cli = null; + testSupport.SetupMockAndSupport(out orgSvc, out fakHttpMethodHander, out cli); + + + cli.UseWebApi = false; + // Setup handlers to deal with both orgRequest and WebAPI request. + int defaultDOP = 5; + var httpResp = new HttpResponseMessage(System.Net.HttpStatusCode.OK); + httpResp.Headers.Add(Utilities.ResponseHeaders.RECOMMENDEDDEGREESOFPARALLELISM, defaultDOP.ToString()); + orgSvc.Setup(f => f.Execute(It.Is(p => p.Target.LogicalName.Equals("account") && p.Target.Id.Equals(testSupport._DefaultId)))).Returns(new DeleteResponse()); + fakHttpMethodHander.Setup(s => s.Send(It.Is(f => f.Method.ToString().Equals("delete", StringComparison.OrdinalIgnoreCase)))).Returns(httpResp); + // Tests/ + cli.UseWebApi = false; + + bool rslt = cli.DeleteEntity("account", testSupport._DefaultId); + Assert.True(rslt); + Assert.Equal(defaultDOP, cli.RecommendedDegreesOfParallelism); + + cli.UseWebApi = false; + cli.Delete("account", testSupport._DefaultId); + Assert.Equal(defaultDOP, cli.RecommendedDegreesOfParallelism); } + #region LiveConnectedTests [SkippableConnectionTest] @@ -952,7 +976,7 @@ public void RelatedEntityLiveTest() task1["subject"] = "task1"; task1["description"] = "task1-description"; ec.Entities.Add(task1); - primaryContact.RelatedEntities.Add(new Relationship("contact_tasks"), ec); + primaryContact.RelatedEntities.Add(new Relationship("Contact_Tasks"), ec); // Add the contact to an EntityCollection EntityCollection primaryContactCollection = new EntityCollection(); diff --git a/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/UtilsTests.cs b/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/UtilsTests.cs index a53d9e4..35b8eee 100644 --- a/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/UtilsTests.cs +++ b/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/UtilsTests.cs @@ -1,6 +1,7 @@ -#region using +#region using using FluentAssertions; using Microsoft.Crm.Sdk.Messages; +using Microsoft.PowerPlatform.Dataverse.Client.InternalExtensions; using Microsoft.PowerPlatform.Dataverse.Client.Utils; using System; using System.Collections.Generic; diff --git a/src/GeneralTools/DataverseClient/UnitTests/LivePackageRunUnitTests/LivePackageRunUnitTests.csproj b/src/GeneralTools/DataverseClient/UnitTests/LivePackageRunUnitTests/LivePackageRunUnitTests.csproj index db02224..ed9c80c 100644 --- a/src/GeneralTools/DataverseClient/UnitTests/LivePackageRunUnitTests/LivePackageRunUnitTests.csproj +++ b/src/GeneralTools/DataverseClient/UnitTests/LivePackageRunUnitTests/LivePackageRunUnitTests.csproj @@ -7,12 +7,14 @@ true DataverseClient-Tests-Package false + + + $(MSBuildThisFileDirectory)..\.packages - diff --git a/src/GeneralTools/DataverseClient/UnitTests/LivePackageRunUnitTests/RunTests.cs b/src/GeneralTools/DataverseClient/UnitTests/LivePackageRunUnitTests/RunTests.cs index 6dd13eb..5d87f68 100644 --- a/src/GeneralTools/DataverseClient/UnitTests/LivePackageRunUnitTests/RunTests.cs +++ b/src/GeneralTools/DataverseClient/UnitTests/LivePackageRunUnitTests/RunTests.cs @@ -1,4 +1,6 @@ +using FluentAssertions; using System; +using System.Collections.Generic; using System.IO; using System.Text; using Xunit; @@ -17,22 +19,27 @@ public RunTests(ITestOutputHelper output) [Fact] public void InvokeBasicTest() { - LivePackageTestsConsole.Program.SkipStop = true; LivePackageTestsConsole.Program.Main(new string[] { "BasicFlow" }); } [Fact] public void InvokeReadSolutionsTest() { - LivePackageTestsConsole.Program.SkipStop = true; LivePackageTestsConsole.Program.Main(new string[] { "listsolutions" }); } + [Fact] + public void InvokeStageSolutionTest() + { + LivePackageTestsConsole.Program.Main(new string[] { "stagesolution" }); + //Action act = () => LivePackageTestsConsole.Program.Main(new string[] { "stagesolution" }); + //act.Should() .ThrowExactly("because the assembly Microsoft.Cds.Sdk.Proxy currently does not define StageSolutionRequest, StageSolutionResponse"); + } + [Fact] public void InvokeCUDTestTest() { - LivePackageTestsConsole.Program.SkipStop = true; LivePackageTestsConsole.Program.Main(new string[] { "CUDTest" }); } diff --git a/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/Auth.cs b/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/Auth.cs deleted file mode 100644 index 4b7759e..0000000 --- a/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/Auth.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Microsoft.PowerPlatform.Dataverse.Client; -using Microsoft.PowerPlatform.Dataverse.Client.Auth; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace LivePackageTestsConsole -{ - public class Auth - { - /// - /// Sample / stand-in appID used when replacing O365 Auth - /// - internal static string SampleClientId = "51f81489-12ee-4a9e-aaae-a2591f45987d"; - /// - /// Sample / stand-in redirect URI used when replacing o365 Auth - /// - internal static string SampleRedirectUrl = "app://58145B91-0C36-4500-8554-080854F2AC97"; - - public static ServiceClient CreateClient() - { - var userName = Environment.GetEnvironmentVariable("XUNITCONNTESTUSERID"); - var password = Environment.GetEnvironmentVariable("XUNITCONNTESTPW"); - var connectionUrl = Environment.GetEnvironmentVariable("XUNITCONNTESTURI"); - if (string.IsNullOrWhiteSpace(userName) || string.IsNullOrWhiteSpace(password) || string.IsNullOrWhiteSpace(connectionUrl)) - { - throw new ArgumentNullException("Make sure to set XUNITCONNTESTUSERID, XUNITCONNTESTPW, XUNITCONNTESTURI environment variables"); - } - - return new ServiceClient(userName, ServiceClient.MakeSecureString(password), new Uri(connectionUrl), true, SampleClientId, new Uri(SampleRedirectUrl), PromptBehavior.Never); - } - } -} diff --git a/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/BasicFlow.cs b/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/BasicFlow.cs deleted file mode 100644 index b740de7..0000000 --- a/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/BasicFlow.cs +++ /dev/null @@ -1,42 +0,0 @@ -using FluentAssertions; -using Microsoft.Crm.Sdk.Messages; -using Microsoft.PowerPlatform.Dataverse.Client; -using Microsoft.PowerPlatform.Dataverse.Client.Auth; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace LivePackageTestsConsole -{ - public class BasicFlow - { - - internal void Run() - { - Console.WriteLine("Starting Basic Flow"); - - var client = Auth.CreateClient(); - client.IsReady.Should().BeTrue(); - - Console.WriteLine("\nCalling WhoAmI"); - var whoAmIResponse = client.Execute(new WhoAmIRequest()) as WhoAmIResponse; - whoAmIResponse.Should().NotBeNull(); - Console.WriteLine($"OrganizationId:{whoAmIResponse.OrganizationId} UserId:{whoAmIResponse.UserId}"); - - Console.WriteLine("\nCalling RetrieveCurrentOrganizationRequest"); - var retrieveCurrentOrganizationRequest = new RetrieveCurrentOrganizationRequest(); - var retrieveCurrentOrganizationResponse = client.Execute(retrieveCurrentOrganizationRequest) as RetrieveCurrentOrganizationResponse; - retrieveCurrentOrganizationResponse.Should().NotBeNull(); - Console.WriteLine($"FriendlyName:{retrieveCurrentOrganizationResponse.Detail.FriendlyName} GEO:{retrieveCurrentOrganizationResponse.Detail.Geo}"); - - Console.WriteLine("\nCalling RetrieveVersionRequest"); - var retrieveVersionRequest = new RetrieveVersionRequest(); - var retrieveVersionResponse = client.Execute(retrieveVersionRequest) as RetrieveVersionResponse; - retrieveVersionResponse.Should().NotBeNull(); - Console.WriteLine($"Version:{retrieveVersionResponse.Version}"); - } - } -} diff --git a/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/CUDTest.cs b/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/CUDTest.cs index 6d5acaa..6df859d 100644 --- a/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/CUDTest.cs +++ b/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/CUDTest.cs @@ -6,6 +6,7 @@ using System.Text; using System.Threading.Tasks; using CrmSdk; +using LiveTestsConsole; namespace LivePackageTestsConsole { diff --git a/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/LivePackageTestsConsole.csproj b/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/LivePackageTestsConsole.csproj index 7a48717..1167584 100644 --- a/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/LivePackageTestsConsole.csproj +++ b/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/LivePackageTestsConsole.csproj @@ -9,7 +9,6 @@ LivePackageTestsConsole LivePackageTestsConsole true - @@ -18,6 +17,9 @@ $(RepoRoot)\binSigned\$(Configuration)\packages 0.5.9 + + + $(MSBuildThisFileDirectory)..\.packages @@ -30,9 +32,16 @@ - + + + + + + + + - + PreserveNewest diff --git a/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/Program.cs b/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/Program.cs index fb0fb47..924dacc 100644 --- a/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/Program.cs +++ b/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/Program.cs @@ -1,3 +1,4 @@ +using LiveTestsConsole; using System; namespace LivePackageTestsConsole @@ -7,7 +8,6 @@ namespace LivePackageTestsConsole /// public class Program { - public static bool SkipStop { get; set; } = false; public static void Main(string[] args) { Console.WriteLine("Starting Tests"); @@ -37,6 +37,12 @@ public static void Main(string[] args) tests.ImportSolution(); } + else if (string.Compare(args[0], "StageSolution", StringComparison.OrdinalIgnoreCase) == 0) + { + var tests = new SolutionTests(); + + tests.StageSolution(); + } else if (string.Compare(args[0], "DeleteSolution", StringComparison.OrdinalIgnoreCase) == 0) { var tests = new SolutionTests(); @@ -61,9 +67,6 @@ public static void Main(string[] args) var tests = new BasicFlow(); tests.Run(); } - - if (!SkipStop) - Console.ReadKey(); } } } diff --git a/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/SolutionTests.cs b/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/SolutionTests.cs deleted file mode 100644 index 9c27828..0000000 --- a/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/SolutionTests.cs +++ /dev/null @@ -1,63 +0,0 @@ -using Microsoft.Crm.Sdk.Messages; -using Microsoft.Xrm.Sdk; -using Microsoft.Xrm.Sdk.Messages; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace LivePackageTestsConsole -{ - public class SolutionTests - { - public void ImportSolution() - { - Console.WriteLine("Starting ImportSolution"); - - var client = Auth.CreateClient(); - - client.ImportSolution(Path.Combine("TestData", "TestSolution_1_0_0_1.zip"), out var importId); - Console.WriteLine($"ImportSolution id:{importId}"); - } - - public void ExportSolution() - { - Console.WriteLine("Starting ExportSolution"); - - var client = Auth.CreateClient(); - - var request = new ExportSolutionRequest() - { - SolutionName = "TestSolution" - }; - - var response = client.Execute(request) as ExportSolutionResponse; - Console.WriteLine($"ExportSolutionFile length:{response.ExportSolutionFile.Length}"); - } - - public void ListSolutions() - { - Console.WriteLine("Starting ListSolutions"); - - var client = Auth.CreateClient(); - - var request = new RetrieveOrganizationInfoRequest(); - var response = client.Execute(request) as RetrieveOrganizationInfoResponse; - Console.WriteLine($"Solutions.Count:{response.organizationInfo.Solutions.Count}"); - } - - public void DeleteSolution() - { - Console.WriteLine("Starting DeleteSolution"); - - var client = Auth.CreateClient(); - - var request = new DeleteRequest() { Target = new EntityReference("solution", new Guid("a50ac31a-b3f3-4fd3-b691-20ddc4d494d7")) }; - //var request = new DeleteRequest() { Target = new EntityReference("solutions", "UniqueName", "TestSolution") }; - var response = client.Execute(request) as DeleteResponse; - Console.WriteLine("Done"); - } - } -} diff --git a/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/TestData/TestSolution_1_0_0_1.zip b/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/TestData/TestSolution_1_0_0_1.zip deleted file mode 100644 index 4dd8ba2..0000000 Binary files a/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/TestData/TestSolution_1_0_0_1.zip and /dev/null differ diff --git a/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/TokenRefresh.cs b/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/TokenRefresh.cs deleted file mode 100644 index f20505d..0000000 --- a/src/GeneralTools/DataverseClient/UnitTests/LivePackageTestsConsole/TokenRefresh.cs +++ /dev/null @@ -1,36 +0,0 @@ -using FluentAssertions; -using Microsoft.Crm.Sdk.Messages; -using Microsoft.PowerPlatform.Dataverse.Client; -using Microsoft.PowerPlatform.Dataverse.Client.Auth; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace LivePackageTestsConsole -{ - public class TokenRefresh - { - - internal void Run() - { - Console.WriteLine("Starting TokenRefresh"); - - var client = Auth.CreateClient(); - client.IsReady.Should().BeTrue(); - - Console.WriteLine("Calling WhoAmI"); - var response1 = client.Execute(new WhoAmIRequest()) as WhoAmIResponse; - response1.Should().NotBeNull(); - - Console.WriteLine("Going to sleep for 26 hours until token expires"); - Thread.Sleep(1000 * 60 * 60 * 26); - - Console.WriteLine("Calling WhoAmI after long sleep"); - var response2 = client.Execute(new WhoAmIRequest()) as WhoAmIResponse; - response2.Should().NotBeNull(); - } - } -} diff --git a/src/GeneralTools/DataverseClient/UnitTests/LiveTestsConsole/Program.cs b/src/GeneralTools/DataverseClient/UnitTests/LiveTestsConsole/Program.cs index 1f2f81b..42c69e7 100644 --- a/src/GeneralTools/DataverseClient/UnitTests/LiveTestsConsole/Program.cs +++ b/src/GeneralTools/DataverseClient/UnitTests/LiveTestsConsole/Program.cs @@ -1,58 +1,75 @@ -using System; +using System; namespace LiveTestsConsole { class Program { - static void Main(string[] args) + static int Main(string[] args) { Console.WriteLine("Starting Tests"); - if (0 < args.Length) + try { - if (string.Compare(args[0], "BasicFlow", StringComparison.OrdinalIgnoreCase) == 0) + if (0 < args.Length) { - var tests = new BasicFlow(); - tests.Run(); - } - else if (string.Compare(args[0], "ListSolutions", StringComparison.OrdinalIgnoreCase) == 0) - { - var tests = new SolutionTests(); + if (string.Compare(args[0], "BasicFlow", StringComparison.OrdinalIgnoreCase) == 0) + { + var tests = new BasicFlow(); + tests.Run(); + } + else if (string.Compare(args[0], "ListSolutions", StringComparison.OrdinalIgnoreCase) == 0) + { + var tests = new SolutionTests(); - tests.ListSolutions(); - } - else if (string.Compare(args[0], "ExportSolution", StringComparison.OrdinalIgnoreCase) == 0) - { - var tests = new SolutionTests(); + tests.ListSolutions(); + } + else if (string.Compare(args[0], "ExportSolution", StringComparison.OrdinalIgnoreCase) == 0) + { + var tests = new SolutionTests(); - tests.ExportSolution(); - } - else if (string.Compare(args[0], "ImportSolution", StringComparison.OrdinalIgnoreCase) == 0) - { - var tests = new SolutionTests(); + tests.ExportSolution(); + } + else if (string.Compare(args[0], "ImportSolution", StringComparison.OrdinalIgnoreCase) == 0) + { + var tests = new SolutionTests(); - tests.ImportSolution(); - } - else if (string.Compare(args[0], "DeleteSolution", StringComparison.OrdinalIgnoreCase) == 0) - { - var tests = new SolutionTests(); + tests.ImportSolution(); + } + else if (string.Compare(args[0], "StageSolution", StringComparison.OrdinalIgnoreCase) == 0) + { + var tests = new SolutionTests(); + + tests.StageSolution(); + } + else if (string.Compare(args[0], "DeleteSolution", StringComparison.OrdinalIgnoreCase) == 0) + { + var tests = new SolutionTests(); + + tests.DeleteSolution(); + } + else if (string.Compare(args[0], "TokenRefresh", StringComparison.OrdinalIgnoreCase) == 0) + { + var tests = new TokenRefresh(); - tests.DeleteSolution(); + tests.Run(); + } } - else if (string.Compare(args[0], "TokenRefresh", StringComparison.OrdinalIgnoreCase) == 0) + else { - var tests = new TokenRefresh(); - + var tests = new BasicFlow(); tests.Run(); } } - else + catch (Exception ex) { - var tests = new BasicFlow(); - tests.Run(); + // We catch and write to console here so we don't make requests to umwatson.events.data.microsoft.com due to exe crash + Console.WriteLine($"Unhandled Exception: {ex}"); + return 1; } - Console.ReadKey(); + + Console.WriteLine("Finished executing tests"); + return 0; } } } diff --git a/src/GeneralTools/DataverseClient/UnitTests/LiveTestsConsole/SolutionTests.cs b/src/GeneralTools/DataverseClient/UnitTests/LiveTestsConsole/SolutionTests.cs index caa5b77..a72e2b1 100644 --- a/src/GeneralTools/DataverseClient/UnitTests/LiveTestsConsole/SolutionTests.cs +++ b/src/GeneralTools/DataverseClient/UnitTests/LiveTestsConsole/SolutionTests.cs @@ -1,6 +1,7 @@ -using Microsoft.Crm.Sdk.Messages; +using Microsoft.Crm.Sdk.Messages; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Messages; +using Microsoft.PowerPlatform.Dataverse.Client.Extensions; using System; using System.Collections.Generic; using System.IO; @@ -12,16 +13,62 @@ namespace LiveTestsConsole { public class SolutionTests { + private readonly string _testSolutionPath; + + public SolutionTests() + { + _testSolutionPath = Path.GetFullPath(Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, "TestData", "TestSolution_1_0_0_1.zip")); + if (!File.Exists(_testSolutionPath)) + { + throw new FileNotFoundException($"Could not fine test zip file located at: {_testSolutionPath}"); + } + } + public void ImportSolution() { Console.WriteLine("Starting ImportSolution"); var client = Auth.CreateClient(); - client.ImportSolution(Path.Combine("TestData", "TestSolution_1_0_0_1.zip"), out var importId); + client.ImportSolution(_testSolutionPath, out var importId); + if (importId == Guid.Empty) + { + throw new InvalidOperationException($"Import of solution was unsuccessful. See logs or debug."); + } Console.WriteLine($"ImportSolution id:{importId}"); } + public void StageSolution() + { + Console.WriteLine("Starting StageSolution"); + + var client = Auth.CreateClient(); + + // BUG: The StageSolutionRequest/StageSolutionResponse message types are currently not generated. This will be fixed shortly. +//#if false + // Using strong-typed request/response message classes + var request = new StageSolutionRequest + { + CustomizationFile = File.ReadAllBytes(_testSolutionPath) + }; + var response = (StageSolutionResponse)client.Execute(request); + var results = response.StageSolutionResults; +//#else +// // For now, we'll use an OrganizationRequest/Response +// var request = new OrganizationRequest("StageSolution") +// { +// ["CustomizationFile"] = File.ReadAllBytes(_testSolutionPath) +// }; +// var response = client.Execute(request); +// // BUG: Throws exception "The given key was not present in the dictionary." - Because the message types are missing for the 'StageSolution' operation. +// var results = (StageSolutionResults)response["StageSolutionResults"]; +//#endif + + // Need to make sure we get the right data back + Console.WriteLine("Results:"); + Console.WriteLine($" StageSolutionUploadId: {results.StageSolutionUploadId}"); + } + public void ExportSolution() { Console.WriteLine("Starting ExportSolution"); @@ -33,7 +80,7 @@ public void ExportSolution() SolutionName = "TestSolution" }; - var response = client.Execute(request) as ExportSolutionResponse; + var response = (ExportSolutionResponse)client.Execute(request); Console.WriteLine($"ExportSolutionFile length:{response.ExportSolutionFile.Length}"); } @@ -44,8 +91,16 @@ public void ListSolutions() var client = Auth.CreateClient(); var request = new RetrieveOrganizationInfoRequest(); - var response = client.Execute(request) as RetrieveOrganizationInfoResponse; + var response = (RetrieveOrganizationInfoResponse)client.Execute(request); Console.WriteLine($"Solutions.Count:{response.organizationInfo.Solutions.Count}"); + + Console.WriteLine($"Listing non-1st party solutions:"); + var excludePublishers = new[] { "microsoftfirstparty", "microsoftdynamics", "MicrosoftCorporation", "dynamics365customerengagement" }; + var non1stPartySolutions = response.organizationInfo.Solutions.Where(s => !excludePublishers.Contains(s.PublisherUniqueName, StringComparer.OrdinalIgnoreCase)); + foreach (var solution in non1stPartySolutions) + { + Console.WriteLine($" Id: {solution.Id}, Publisher: {solution.PublisherUniqueName,-10}, Name: {solution.SolutionUniqueName}"); + } } public void DeleteSolution() @@ -54,9 +109,19 @@ public void DeleteSolution() var client = Auth.CreateClient(); - var request = new DeleteRequest() { Target = new EntityReference("solution", new Guid("a50ac31a-b3f3-4fd3-b691-20ddc4d494d7")) }; - //var request = new DeleteRequest() { Target = new EntityReference("solutions", "UniqueName", "TestSolution") }; - var response = client.Execute(request) as DeleteResponse; + // First, get the id of our TestSolution we install via the 'ImportSolution' + var installedSolution = ((RetrieveOrganizationInfoResponse)client.Execute(new RetrieveOrganizationInfoRequest())).organizationInfo.Solutions + .SingleOrDefault(s => s.SolutionUniqueName == "TestSolution"); + if (installedSolution == null) + { + throw new InvalidOperationException($"The org is missing the solution with unique name 'TestSolution'. Be sure to install it by running the 'ImportSolution' test first."); + } + + var request = new DeleteRequest() + { + Target = new EntityReference("solution", installedSolution.Id) + }; + var response = (DeleteResponse)client.Execute(request); Console.WriteLine("Done"); } } diff --git a/src/GeneralTools/DataverseClient/WebResourceUtility/ImageResources.cs b/src/GeneralTools/DataverseClient/WebResourceUtility/ImageResources.cs index 34cf4db..3528e35 100644 --- a/src/GeneralTools/DataverseClient/WebResourceUtility/ImageResources.cs +++ b/src/GeneralTools/DataverseClient/WebResourceUtility/ImageResources.cs @@ -1,4 +1,4 @@ -//=================================================================================== +//=================================================================================== // // eService Accelerator V1.0 // Copyright 2003-2012 Microsoft Corp All rights reserved. @@ -9,130 +9,131 @@ namespace Microsoft.PowerPlatform.Dataverse.WebResourceUtility { - using System; - using System.Collections.Generic; - using System.Linq; - using System.Text; - using System.Windows.Media.Imaging; - using Microsoft.PowerPlatform.Dataverse.Client; - using System.IO; - using System.Diagnostics; - using Microsoft.Xrm.Sdk; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Windows.Media.Imaging; + using Microsoft.PowerPlatform.Dataverse.Client; + using System.IO; + using System.Diagnostics; + using Microsoft.Xrm.Sdk; + using Microsoft.PowerPlatform.Dataverse.Client.Extensions; + + /// + /// Web Resource actions for dealing with Image Resources. + /// + public class ImageResources + { + #region Vars - /// - /// Web Resource actions for dealing with Image Resources. - /// - public class ImageResources - { - #region Vars + /// + /// Dataverse Connection + /// + private ServiceClient _serviceClient; - /// - /// Dataverse Connection - /// - private ServiceClient _serviceClient; + /// + /// Tracer + /// + private TraceLogger _logEntry = null; - /// - /// Tracer - /// - private TraceLogger _logEntry = null; + #endregion - #endregion + /// + /// Constructs a class used to retrieve image resources from CRM.. + /// + /// Initialized copy of a ServiceClient object + public ImageResources(ServiceClient serviceClient) + { + _serviceClient = serviceClient; + _logEntry = new TraceLogger(string.Empty); + } - /// - /// Constructs a class used to retrieve image resources from CRM.. - /// - /// Initialized copy of a ServiceClient object - public ImageResources(ServiceClient serviceClient) - { - _serviceClient = serviceClient; - _logEntry = new TraceLogger(string.Empty); - } + /// + /// Returns BitMap Image Resource from CRM + /// + /// Image Resource Name requested + /// Returns Null if the Image is not found, or the resource type is not an Image. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "This is done to support disposing of the stream and its resource by the calling method")] + public BitmapImage GetImageFromCRMWebResource(string webResourceName) + { + #region PreCheck + _logEntry.ResetLastError(); // Reset Last Error + if (_serviceClient == null || string.IsNullOrWhiteSpace(webResourceName)) + { + return null; + } + #endregion - /// - /// Returns BitMap Image Resource from CRM - /// - /// Image Resource Name requested - /// Returns Null if the Image is not found, or the resource type is not an Image. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "This is done to support disposing of the stream and its resource by the calling method")] - public BitmapImage GetImageFromCRMWebResource(string webResourceName) - { - #region PreCheck - _logEntry.ResetLastError(); // Reset Last Error - if (_serviceClient == null || string.IsNullOrWhiteSpace(webResourceName)) - { - return null; - } - #endregion + BitmapImage outImage = null; + //// CRM Connection + //// Get the Web Resources from CRM + var SearchFilter = new List(); + var filter1 = new DataverseSearchFilter() + { + SearchConditions = new List() + { + new DataverseFilterConditionItem() { FieldName = "name", FieldOperator = Xrm.Sdk.Query.ConditionOperator.Equal , FieldValue=webResourceName } + }, + FilterOperator = Xrm.Sdk.Query.LogicalOperator.And + }; - BitmapImage outImage = null; - //// CRM Connection - //// Get the Web Resources from CRM - var SearchFilter = new List(); - var filter1 = new ServiceClient.DataverseSearchFilter() - { - SearchConditions = new List() - { - new ServiceClient.DataverseFilterConditionItem() { FieldName = "name", FieldOperator = Microsoft.Xrm.Sdk.Query.ConditionOperator.Equal , FieldValue=webResourceName } - }, - FilterOperator = Microsoft.Xrm.Sdk.Query.LogicalOperator.And - }; + SearchFilter.Add(filter1); + var rslts = _serviceClient.GetEntityDataBySearchParams("webresource", SearchFilter, LogicalSearchOperator.None, + new List() { "content", "webresourcetype" }); + if (rslts != null && rslts.Count > 0) + { + // Found it.. Get the first one. + var workingWith = rslts.FirstOrDefault().Value; + // get the resource type. + int rsType = -1; + OptionSetValue rsOsType = _serviceClient.GetDataByKeyFromResultsSet(workingWith, "webresourcetype"); + if (rsOsType != null) + rsType = rsOsType.Value; - SearchFilter.Add(filter1); - var rslts = _serviceClient.GetEntityDataBySearchParams("webresource", SearchFilter, ServiceClient.LogicalSearchOperator.None, - new List() { "content", "webresourcetype" }); - if (rslts != null && rslts.Count > 0) - { - // Found it.. Get the first one. - var workingWith = rslts.FirstOrDefault().Value; - // get the resource type. - int rsType = -1; - OptionSetValue rsOsType = _serviceClient.GetDataByKeyFromResultsSet(workingWith, "webresourcetype"); - if (rsOsType != null) - rsType = rsOsType.Value; - - switch (rsType) - { - case (int)WebResourceWebResourceType.PNGformat: - case (int)WebResourceWebResourceType.GIFformat: - case (int)WebResourceWebResourceType.JPGformat: - case (int)WebResourceWebResourceType.ICOformat: - // Get the content - string sData = _serviceClient.GetDataByKeyFromResultsSet(workingWith, "content"); + switch (rsType) + { + case (int)WebResourceWebResourceType.PNGformat: + case (int)WebResourceWebResourceType.GIFformat: + case (int)WebResourceWebResourceType.JPGformat: + case (int)WebResourceWebResourceType.ICOformat: + // Get the content + string sData = _serviceClient.GetDataByKeyFromResultsSet(workingWith, "content"); - if (string.IsNullOrWhiteSpace(sData)) - return outImage; - try - { - // Convert from Base64 string to byte[] - byte[] imageBytes = Convert.FromBase64String(sData); - //// need to leave the memory stream active an allow the bitmapImage life to control it.. - //// worst case the GC will pick it up. - MemoryStream ms = new MemoryStream(imageBytes, 0, imageBytes.Length); - //// Init the new Image source... - outImage = new BitmapImage(); - outImage.BeginInit(); - outImage.StreamSource = ms; - outImage.EndInit(); - return outImage; - } - catch (Exception ex) - { - _logEntry.Log(ex); - } + if (string.IsNullOrWhiteSpace(sData)) + return outImage; + try + { + // Convert from Base64 string to byte[] + byte[] imageBytes = Convert.FromBase64String(sData); + //// need to leave the memory stream active an allow the bitmapImage life to control it.. + //// worst case the GC will pick it up. + MemoryStream ms = new MemoryStream(imageBytes, 0, imageBytes.Length); + //// Init the new Image source... + outImage = new BitmapImage(); + outImage.BeginInit(); + outImage.StreamSource = ms; + outImage.EndInit(); + return outImage; + } + catch (Exception ex) + { + _logEntry.Log(ex); + } - break; - default: - _logEntry.Log(string.Format("Web Resource is not an Image file, Name: {0} File Type:{1}", webResourceName, rsType), TraceEventType.Error); - return outImage; - } - } - else - { - _logEntry.Log(string.Format("Web Resource Image file not found, Looking for : {0}", webResourceName), TraceEventType.Error); - } + break; + default: + _logEntry.Log(string.Format("Web Resource is not an Image file, Name: {0} File Type:{1}", webResourceName, rsType), TraceEventType.Error); + return outImage; + } + } + else + { + _logEntry.Log(string.Format("Web Resource Image file not found, Looking for : {0}", webResourceName), TraceEventType.Error); + } - return outImage; - } - } + return outImage; + } + } } diff --git a/src/GeneralTools/DataverseClient/WebResourceUtility/XmlResources.cs b/src/GeneralTools/DataverseClient/WebResourceUtility/XmlResources.cs index 72e87a5..a21cd68 100644 --- a/src/GeneralTools/DataverseClient/WebResourceUtility/XmlResources.cs +++ b/src/GeneralTools/DataverseClient/WebResourceUtility/XmlResources.cs @@ -1,4 +1,4 @@ -//=================================================================================== +//=================================================================================== // Microsoft – subject to the terms of the Microsoft EULA and other agreements // Microsoft.PowerPlatform.Dataverse.WebResourceUtility // copyright 2003-2012 Microsoft Corp. @@ -12,79 +12,80 @@ using System.Text; using Microsoft.PowerPlatform.Dataverse.Client; using System.Diagnostics; +using Microsoft.PowerPlatform.Dataverse.Client.Extensions; namespace Microsoft.PowerPlatform.Dataverse.WebResourceUtility { - /// - /// This class is used to access and retrieve web - /// - public class XmlResources - { - #region Vars + /// + /// This class is used to access and retrieve web + /// + public class XmlResources + { + #region Vars - /// - /// Dataverse Connection - /// - private ServiceClient _serviceClient; + /// + /// Dataverse Connection + /// + private ServiceClient _serviceClient; - /// - /// Tracer - /// - private TraceLogger _logEntry; + /// + /// Tracer + /// + private TraceLogger _logEntry; - #endregion + #endregion - /// - /// Constructs a class used to retrieve an XML resources from Dataverse.. - /// - /// Initialized copy of a ServiceClient object - public XmlResources(ServiceClient serviceClient) - { - _serviceClient = serviceClient; - _logEntry = new TraceLogger(string.Empty); - } + /// + /// Constructs a class used to retrieve an XML resources from Dataverse.. + /// + /// Initialized copy of a ServiceClient object + public XmlResources(ServiceClient serviceClient) + { + _serviceClient = serviceClient; + _logEntry = new TraceLogger(string.Empty); + } - /// - /// Returns Xml Resource from Dataverse - /// - /// Xml Resource Name requested - /// Returns Null if the Xml is not found, or Xml document as text. - public string GetXmlFromCRMWebResource(string webResourceName) - { - #region PreCheck - _logEntry.ResetLastError(); // Reset Last Error - if (_serviceClient == null || string.IsNullOrWhiteSpace(webResourceName)) - { - return null; - } - #endregion + /// + /// Returns Xml Resource from Dataverse + /// + /// Xml Resource Name requested + /// Returns Null if the Xml is not found, or Xml document as text. + public string GetXmlFromCRMWebResource(string webResourceName) + { + #region PreCheck + _logEntry.ResetLastError(); // Reset Last Error + if (_serviceClient == null || string.IsNullOrWhiteSpace(webResourceName)) + { + return null; + } + #endregion - string outData = null; - // Get the Web Resources from CRM - var SearchFilter = new List(); - var filter1 = new ServiceClient.DataverseSearchFilter() - { - SearchConditions = new List() - { - new ServiceClient.DataverseFilterConditionItem() { FieldName = "name", FieldOperator = Microsoft.Xrm.Sdk.Query.ConditionOperator.Equal, FieldValue=webResourceName }, - new ServiceClient.DataverseFilterConditionItem() { FieldName = "webresourcetype", FieldOperator = Microsoft.Xrm.Sdk.Query.ConditionOperator.Equal, FieldValue=4 } - }, - FilterOperator = Microsoft.Xrm.Sdk.Query.LogicalOperator.And - }; + string outData = null; + // Get the Web Resources from CRM + var SearchFilter = new List(); + var filter1 = new DataverseSearchFilter() + { + SearchConditions = new List() + { + new DataverseFilterConditionItem() { FieldName = "name", FieldOperator = Xrm.Sdk.Query.ConditionOperator.Equal, FieldValue=webResourceName }, + new DataverseFilterConditionItem() { FieldName = "webresourcetype", FieldOperator = Xrm.Sdk.Query.ConditionOperator.Equal, FieldValue=4 } + }, + FilterOperator = Microsoft.Xrm.Sdk.Query.LogicalOperator.And + }; - SearchFilter.Add(filter1); - var rslts = _serviceClient.GetEntityDataBySearchParams("webresource", SearchFilter, ServiceClient.LogicalSearchOperator.None, - new List() { "content", "webresourcetype" }); - if (rslts != null && rslts.Count > 0) - { - // Found it.. Get the first one. - var workingWith = rslts.FirstOrDefault().Value; - return _serviceClient.GetDataByKeyFromResultsSet(workingWith, "content"); - } - else - _logEntry.Log(string.Format("Web Resource Xml file not found, Looking for : {0}", webResourceName), TraceEventType.Error); - return outData; - } + SearchFilter.Add(filter1); + var rslts = _serviceClient.GetEntityDataBySearchParams("webresource", SearchFilter, LogicalSearchOperator.None, + new List() { "content", "webresourcetype" }); + if (rslts != null && rslts.Count > 0) + { + // Found it.. Get the first one. + var workingWith = rslts.FirstOrDefault().Value; + return _serviceClient.GetDataByKeyFromResultsSet(workingWith, "content"); + } + else + _logEntry.Log(string.Format("Web Resource Xml file not found, Looking for : {0}", webResourceName), TraceEventType.Error); + return outData; + } - } + } } diff --git a/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.Dynamics.nuspec b/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.Dynamics.nuspec index 1782f12..cc6cb28 100644 --- a/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.Dynamics.nuspec +++ b/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.Dynamics.nuspec @@ -23,9 +23,6 @@ - - - @@ -47,8 +44,6 @@ - - diff --git a/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.ReleaseNotes.txt b/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.ReleaseNotes.txt index 748f8e7..965aa78 100644 --- a/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.ReleaseNotes.txt +++ b/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.ReleaseNotes.txt @@ -1,13 +1,40 @@ Notice: This package is in a public preview release. - This package is intended to work with .net full framework 4.6.2, 4.7.2 and 4.8, .net core 3.0, 3.1 and 5.0 + This package is intended to work with .net full framework 4.6.2, 4.7.2 and 4.8, .net core 3.1, 5.0 and 6.0 General Documentation is the same as CrmServiceClient and can be found here: https://docs.microsoft.com/en-us/dotnet/api/microsoft.xrm.tooling.connector.crmserviceclient?view=dynamics-xrmtooling-ce-9 Connection String Docs can be found here: https://docs.microsoft.com/en-us/powerapps/developer/common-data-service/xrm-tooling/use-connection-strings-xrm-tooling-connect - Note: that only OAuth, Certificate, ClientSecret Authentication types are supported at this time. + Note: that only AD on FullFramework, OAuth, Certificate, ClientSecret Authentication types are supported at this time. ++CURRENTRELEASEID++ +Minor Bump to 0.6.x - *Should* be last bump prior to 1.0. + +Added initial support for AD auth for OnPremise. This will work for full framework only. +Added new constructor that will accept a ConnectionOptions class and a ConfigurationOptions Options Object. This constructor is currently not fully implemented. + This is prep work for a builder process and intended to unify the connection flow to a single constructor at some point in the future. + The goal of this work will be to resolve the issues caused by lazy loading hosts. +Fixed a bug in Logger when working with Connection Strings where log events would not be correctly generated. +Speculative fix for GIT issue #219 and #220 +Fixed a bug where LastError was not properly reset, fixed Git #229 +Fixed Error message issue between onPrem and onLine , Fixed Git #232 +Added configuration support to allow for better memory management of very large return sets, Git #207 + New AppSetting is called "MaxBufferPoolSizeOveride" - there is currently not a way to set this via code at runtime. + Support for configuration of this will be added as part of the new Constructor for .net core, .net framework can use it by setting the appsetting value now. +Fixed exceptions not being thrown from several paths during login flows. + +-- WARNING -- +.net 3.0 Target Removed from Nuget packages. +REFACTORED helpers and utilities for the DataverseServiceClient into the Microsoft.PowerPlatform.Dataverse.Client.Extensions Namespace of the DataverseClient. +Dependency changes: + Removed Microsoft.Dynamics.Messages.Sdk.nuspec for Dynamics messages at this time. This will return in the future in some form. + Removed Microsoft.Cds.Sdk.Proxy.dll from Package and replaced with Microsoft.Crm.Sdk.Proxy.dll in package. + System.Text.Json moved to 6.0.2 min dependency. + -- Note: As of this writing, when using Azure functions you cannot use Microsoft.Net.Sdk.Functions.3.0.12 or .3.0.13 due to blocks on System.Text.Json dependencies. You need to either use .3.0.11 or move up to 4.x + System.ServiceModel.Http moved to 4.9.0 due to internal dependencies on Microsoft.Xrm.Sdk + + +0.5.17: Accepted fix requested here: https://github.com/microsoft/PowerPlatform-DataverseServiceClient/issues/205 fixing delete by alternate key request in client. Fixed dependency issue for System.Security.Premisions (git #203) diff --git a/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.nuspec b/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.nuspec index 302a365..7f5c5e6 100644 --- a/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.nuspec +++ b/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.nuspec @@ -23,7 +23,7 @@ - + @@ -34,7 +34,7 @@ - + @@ -45,37 +45,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -89,10 +59,10 @@ - - - - + + + + @@ -127,23 +97,19 @@ - - - - - + - + - + - + diff --git a/src/nuspecs/Microsoft.PowerPlatform.Dataverse.ConnectControl.ReleaseNotes.txt b/src/nuspecs/Microsoft.PowerPlatform.Dataverse.ConnectControl.ReleaseNotes.txt new file mode 100644 index 0000000..74f964e --- /dev/null +++ b/src/nuspecs/Microsoft.PowerPlatform.Dataverse.ConnectControl.ReleaseNotes.txt @@ -0,0 +1,6 @@ +Notice: + This package is in a public preview release. + This package is intended to work with .net full framework 4.6.2, 4.7.2 and 4.8 + +++CURRENTRELEASEID++ +Initial release diff --git a/src/nuspecs/Microsoft.PowerPlatform.Dataverse.ConnectControl.nuspec b/src/nuspecs/Microsoft.PowerPlatform.Dataverse.ConnectControl.nuspec new file mode 100644 index 0000000..ec533cd --- /dev/null +++ b/src/nuspecs/Microsoft.PowerPlatform.Dataverse.ConnectControl.nuspec @@ -0,0 +1,66 @@ + + + + Microsoft.PowerPlatform.Dataverse.ConnectControl + 1.0.0 + Microsoft + crmsdk,Microsoft + https://go.microsoft.com/fwlink/?linkid=2108407 + https://github.com/microsoft/PowerPlatform-DataverseServiceClient + images\Dataverse.128x128.png + true + + This package contains the following official Microsoft assemblies and has been authored by the Microsoft Dataverse SDK team. + - Microsoft.PowerPlatform.Dataverse.ConnectControl + - Microsoft.PowerPlatform.Dataverse.Ui.Styles + - Microsoft.PowerPlatform.Dataverse.WebResourceUtility + + Assemblies required to add Dataverse custom controls to Windows Presentation Foundation managed code applications. + © Microsoft Corporation. All rights reserved. + Dynamics CommonDataService CDS PowerApps PowerPlatform Dataverse + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +