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