diff --git a/src/GeneralTools/DataverseClient/Client/Builder/AbstractClientRequestBuilder.cs b/src/GeneralTools/DataverseClient/Client/Builder/AbstractClientRequestBuilder.cs index 082c6be..427c52c 100644 --- a/src/GeneralTools/DataverseClient/Client/Builder/AbstractClientRequestBuilder.cs +++ b/src/GeneralTools/DataverseClient/Client/Builder/AbstractClientRequestBuilder.cs @@ -67,7 +67,14 @@ public T WithCorrelationId(Guid correlationId) /// public T WithHeader(string key, string value) { - _headers.Add(key, value); + if ( _headers.ContainsKey(key)) + { + _headers[key] = value; + } + else + { + _headers.Add(key, value); + } return (T)this; } @@ -79,7 +86,16 @@ public T WithHeader(string key, string value) public T WithHeaders(IDictionary headers) { foreach (var itm in headers) - _headers.Add(itm.Key, itm.Value); + { + if( _headers.ContainsKey(itm.Key)) + { + _headers[itm.Key] = itm.Value; + } + else + { + _headers.Add(itm.Key, itm.Value); + } + } return (T)this; } diff --git a/src/GeneralTools/DataverseClient/Client/ConnectionService.cs b/src/GeneralTools/DataverseClient/Client/ConnectionService.cs index 38c2edd..1151ba3 100644 --- a/src/GeneralTools/DataverseClient/Client/ConnectionService.cs +++ b/src/GeneralTools/DataverseClient/Client/ConnectionService.cs @@ -394,7 +394,7 @@ internal System.Net.NetworkCredential DataverseServiceAccessCredential /// /// Type of protocol to use /// - internal string InternetProtocalToUse { get { return _InternetProtocalToUse; } set { _InternetProtocalToUse = value; } } + internal string InternetProtocolToUse { get { return _InternetProtocalToUse; } set { _InternetProtocalToUse = value; } } /// /// returns the connected organization detail object. @@ -2337,6 +2337,8 @@ internal async Task Command_WebExecuteAsync(string queryStr string requestIdLogSegement = logEntry.GetFormatedRequestSessionIdString(requestTrackingId, SessionTrackingId); do { + retry = false; // Set intial state. + // Add authorization header. - Here to catch the situation where a token expires during retry. if (!customHeaders.ContainsKey(Utilities.RequestHeaders.AUTHORIZATION_HEADER)) customHeaders.Add(Utilities.RequestHeaders.AUTHORIZATION_HEADER, new List() { string.Format("Bearer {0}", await RefreshClientTokenAsync().ConfigureAwait(false)) }); diff --git a/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/ServiceInformation.cs b/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/ServiceInformation.cs index c53c3f4..16c1f37 100644 --- a/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/ServiceInformation.cs +++ b/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/ServiceInformation.cs @@ -4,11 +4,11 @@ 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; @@ -52,6 +52,7 @@ internal ServiceConfiguration(Uri serviceUri, bool checkForSecondary) ClientExceptionHelper.ThrowIfNull(ServiceEndpointMetadata, "ServiceEndpointMetadata"); +#if NETFRAMEWORK if (ServiceEndpointMetadata.ServiceEndpoints.Count == 0) { StringBuilder errorBuilder = new StringBuilder(); @@ -65,6 +66,7 @@ internal ServiceConfiguration(Uri serviceUri, bool checkForSecondary) throw new InvalidOperationException(ClientExceptionHelper.FormatMessage(0, "The provided uri did not return any Service Endpoints!\n{0}", errorBuilder.ToString())); } +#endif ServiceEndpoints = ServiceEndpointMetadata.ServiceEndpoints; @@ -665,20 +667,20 @@ private SecurityTokenResponse AuthenticateInternal(AuthenticationCredentials aut { 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 (SecurityTokenValidationException) // Removed due to a type conflict with DV Server + //{ + // 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. diff --git a/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/ServiceMetadataUtility.cs b/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/ServiceMetadataUtility.cs index a7a73a9..1519eec 100644 --- a/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/ServiceMetadataUtility.cs +++ b/src/GeneralTools/DataverseClient/Client/Connector/OnPremises/ServiceMetadataUtility.cs @@ -16,7 +16,6 @@ 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; @@ -262,13 +261,13 @@ public static IssuerEndpoint GetIssuer(Binding binding) } return null; - } + } +#if NETFRAMEWORK private static KerberosSecurityTokenParameters GetKerberosTokenParameters(SecurityBindingElement securityElement) { if (securityElement != null) { -#if NETFRAMEWORK if (securityElement.EndpointSupportingTokenParameters != null) { if (securityElement.EndpointSupportingTokenParameters.Endorsing != null) @@ -279,15 +278,12 @@ private static KerberosSecurityTokenParameters GetKerberosTokenParameters(Securi } } } -#else - throw new PlatformNotSupportedException("Xrm.Sdk WSTrust"); -#endif } - return null; } +#endif - private static IssuedSecurityTokenParameters GetIssuedTokenParameters(SecurityBindingElement securityElement) + private static IssuedSecurityTokenParameters GetIssuedTokenParameters(SecurityBindingElement securityElement) { if (securityElement != null) { @@ -351,15 +347,15 @@ public static CustomBinding SetIssuer(Binding binding, IssuerEndpoint issuerEndp } return new CustomBinding(elements); - } + } +#if NETFRAMEWORK private static void ParseEndpoints(ServiceEndpointDictionary serviceEndpoints, ServiceEndpointCollection serviceEndpointCollection) { serviceEndpoints.Clear(); if (serviceEndpointCollection != null) { -#if NETFRAMEWORK foreach (var endpoint in serviceEndpointCollection) { if (IsEndpointSupported(endpoint)) @@ -367,13 +363,11 @@ private static void ParseEndpoints(ServiceEndpointDictionary serviceEndpoints, S serviceEndpoints.Add(endpoint.Name, endpoint); } } -#else - throw new PlatformNotSupportedException("Xrm.Sdk WSDL"); -#endif } } +#endif - private static bool IsEndpointSupported(ServiceEndpoint endpoint) + private static bool IsEndpointSupported(ServiceEndpoint endpoint) { if (endpoint != null) { @@ -389,6 +383,7 @@ private static bool IsEndpointSupported(ServiceEndpoint endpoint) internal static ServiceEndpointMetadata RetrieveServiceEndpointMetadata(Type contractType, Uri serviceUri, bool checkForSecondary) { +#if NETFRAMEWORK // WebInfra; MetadataSet and CreateMetadataClient are NETFRAMEWORK-ONLY ServiceEndpointMetadata serviceEndpointMetadata = new ServiceEndpointMetadata(); serviceEndpointMetadata.ServiceUrls = ServiceConfiguration.CalculateEndpoints(serviceUri); @@ -398,25 +393,17 @@ internal static ServiceEndpointMetadata RetrieveServiceEndpointMetadata(Type con 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; @@ -447,29 +434,14 @@ internal static ServiceEndpointMetadata RetrieveServiceEndpointMetadata(Type con 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"); + 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); @@ -497,34 +469,15 @@ internal static ServiceEndpointMetadata RetrieveServiceEndpointMetadata(Type con } 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; +#else + throw new NotImplementedException("ServiceModel metadata support is limited for this target framework"); +#endif } - private static Version GetSDKVersionNumberFromAssembly() + private static Version GetSDKVersionNumberFromAssembly() { string fileVersion = OrganizationServiceProxy.GetXrmSdkAssemblyFileVersion(); @@ -536,8 +489,9 @@ private static Version GetSDKVersionNumberFromAssembly() } return parsedVersion; - } + } +#if NETFRAMEWORK /// /// Returns a list of policy import extensions in the importer parameter and adds a SecurityBindingElementImporter if not already present in the list. /// @@ -546,7 +500,7 @@ private static Version GetSDKVersionNumberFromAssembly() private static List AddSecurityBindingToPolicyImporter(WsdlImporter importer) { List newExts = new List(); -#if NETFRAMEWORK + KeyedByTypeCollection policyExtensions = importer.PolicyImportExtensions; SecurityBindingElementImporter securityBindingElementImporter = policyExtensions.Find(); @@ -564,18 +518,14 @@ private static List AddSecurityBindingToPolicyImporter(W newExts.AddRange(policyExtensions); return newExts; -#else - - newExts.Add(new AuthenticationPolicyImporter(new SecurityBindingElementImporter())); - return newExts; - //throw new PlatformNotSupportedException("Xrm.Sdk WSDL"); -#endif } +#endif - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Need to catch any exception here and fail.")] +#if NETFRAMEWORK + [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 { @@ -589,12 +539,10 @@ private static bool TryRetrieveMetadata(MetadataExchangeClient mcli, Uri service } return rethrow; -#else - throw new PlatformNotSupportedException("Xrm.Sdk WSDL"); -#endif } +#endif - private static XmlQualifiedName GetPortTypeQName(ContractDescription contract) + private static XmlQualifiedName GetPortTypeQName(ContractDescription contract) { return new XmlQualifiedName(contract.Name, contract.Namespace); } @@ -602,11 +550,11 @@ private static XmlQualifiedName GetPortTypeQName(ContractDescription contract) private static Collection CreateContractCollection(Type contract) { return new Collection { ContractDescription.GetContract(contract) }; - } + } +#if NETFRAMEWORK private static MetadataExchangeClient CreateMetadataClient(string scheme) { -#if NETFRAMEWORK WSHttpBinding mexBinding = null; if (string.Compare(scheme, "https", StringComparison.OrdinalIgnoreCase) == 0) @@ -628,13 +576,10 @@ private static MetadataExchangeClient CreateMetadataClient(string scheme) mcli.MaximumResolvedReferences = 100; return mcli; -#else - return null; - //throw new PlatformNotSupportedException("Xrm.Sdk WSDL"); -#endif } +#endif - public static void ReplaceEndpointAddress(ServiceEndpoint endpoint, Uri adddress) + public static void ReplaceEndpointAddress(ServiceEndpoint endpoint, Uri adddress) { var addressBuilder = new EndpointAddressBuilder(endpoint.Address); addressBuilder.Uri = adddress; diff --git a/src/GeneralTools/DataverseClient/Client/DataverseTelemetryBehaviors.cs b/src/GeneralTools/DataverseClient/Client/DataverseTelemetryBehaviors.cs index 7264d1a..22bb328 100644 --- a/src/GeneralTools/DataverseClient/Client/DataverseTelemetryBehaviors.cs +++ b/src/GeneralTools/DataverseClient/Client/DataverseTelemetryBehaviors.cs @@ -369,8 +369,8 @@ public object BeforeSendRequest(ref Message request, IClientChannel channel) { using (OperationContextScope scope = new OperationContextScope((IContextChannel)channel)) { - var ForceConsistencytHeader = new MessageHeader("Strong").GetUntypedHeader(Utilities.RequestHeaders.FORCE_CONSISTENCY, "http://schemas.microsoft.com/xrm/2011/Contracts"); - request.Headers.Add(ForceConsistencytHeader); + var ForceConsistencyHeader = new MessageHeader("Strong").GetUntypedHeader(Utilities.RequestHeaders.FORCE_CONSISTENCY, "http://schemas.microsoft.com/xrm/2011/Contracts"); + request.Headers.Add(ForceConsistencyHeader); } } return null; diff --git a/src/GeneralTools/DataverseClient/Client/Microsoft.PowerPlatform.Dataverse.Client.csproj b/src/GeneralTools/DataverseClient/Client/Microsoft.PowerPlatform.Dataverse.Client.csproj index eebfc46..1dbd599 100644 --- a/src/GeneralTools/DataverseClient/Client/Microsoft.PowerPlatform.Dataverse.Client.csproj +++ b/src/GeneralTools/DataverseClient/Client/Microsoft.PowerPlatform.Dataverse.Client.csproj @@ -34,22 +34,31 @@ - - - - - + + + + + + + + + + + + + + + + diff --git a/src/GeneralTools/DataverseClient/Client/ServiceClient.cs b/src/GeneralTools/DataverseClient/Client/ServiceClient.cs index d241467..b0200ec 100644 --- a/src/GeneralTools/DataverseClient/Client/ServiceClient.cs +++ b/src/GeneralTools/DataverseClient/Client/ServiceClient.cs @@ -578,6 +578,12 @@ public bool ForceServerMetadataCacheConsistency } set { + if (ConnectedOrgVersion == Version.Parse("9.0.0.0")) // Default setting found as this is a version number that is hard set during setup of connection. it is not possible to actually have an environment with this version number + { + //force update version + _logEntry.Log($"Requested current version from Dataverse, found: {OrganizationDetail.OrganizationVersion}"); + } + if (_connectionSvc != null && Utilities.FeatureVersionMinimums.IsFeatureValidForEnviroment(_connectionSvc?.OrganizationVersion, Utilities.FeatureVersionMinimums.ForceConsistencySupported)) _connectionSvc.ForceServerCacheConsistency = value; else @@ -1269,7 +1275,7 @@ internal void CreateServiceConnection( _connectionSvc.RequestAdditionalHeadersAsync = GetCustomHeaders; // Assign the log entry host to the ConnectionService engine ConnectionService tempConnectService = null; - _connectionSvc.InternetProtocalToUse = useSsl ? "https" : "http"; + _connectionSvc.InternetProtocolToUse = useSsl ? "https" : "http"; if (!_connectionSvc.DoLogin(out tempConnectService)) { _logEntry.Log("Unable to Login to Dataverse", TraceEventType.Error); diff --git a/src/GeneralTools/DataverseClient/DataverseClient.sln b/src/GeneralTools/DataverseClient/DataverseClient.sln index dc1e6e0..a427964 100644 --- a/src/GeneralTools/DataverseClient/DataverseClient.sln +++ b/src/GeneralTools/DataverseClient/DataverseClient.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29609.76 +# Visual Studio Version 17 +VisualStudioVersion = 17.10.35027.167 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerPlatform.Dataverse.Client", "Client\Microsoft.PowerPlatform.Dataverse.Client.csproj", "{7303AAC5-BCEF-4BDB-B7D8-303490D506C6}" EndProject @@ -11,12 +11,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DataverseClient_Core_UnitTe EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerPlatform.Dataverse.Client.Dynamics", "Extensions\DynamicsExtension\Microsoft.PowerPlatform.Dataverse.Client.Dynamics.csproj", "{8CE32D7B-EA3D-4725-A270-9780D366EDB7}" EndProject -#Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Dynamics.Sdk.Messages.Shell", "Extensions\Microsoft.Dynamics.Sdk.Messages\Microsoft.Dynamics.Sdk.Messages.Shell.csproj", "{503645DD-7711-40ED-8811-F06391F294AB}" -#EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LiveTestsConsole", "UnitTests\LiveTestsConsole\LiveTestsConsole.csproj", "{5A1A4FFF-78F5-48A2-9AB0-3E507E938465}" EndProject -#Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.PowerPlatform.Dataverse.ServiceClientConverter", "Extensions\Microsoft.PowerPlatform.Dataverse.ServiceClientConverter\Microsoft.PowerPlatform.Dataverse.ServiceClientConverter.csproj", "{752E5268-3D99-485D-A31E-FC40AE9C0867}" -#EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzDevOps_ServiceConnection_Test", "UnitTests\AzDevOps_ServiceConnection_Test\AzDevOps_ServiceConnection_Test.csproj", "{581F17CF-C7DE-4147-8764-E6B4328C07E7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerPlatform.Dataverse.Client.AzAuth", "Extensions\Microsoft.PowerPlatform.Dataverse.Client.AzAuth\Microsoft.PowerPlatform.Dataverse.Client.AzAuth.csproj", "{6A891168-EACF-4AC1-B081-ABA2FFF0DA7D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerPlatform.Dataverse.Client.PowerShell", "PowerShell\Microsoft.PowerPlatform.Dataverse.Client.PowerShell\Microsoft.PowerPlatform.Dataverse.Client.PowerShell.csproj", "{849A9DED-F4C6-4CFE-BA25-696F6E100198}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -35,18 +37,22 @@ Global {8CE32D7B-EA3D-4725-A270-9780D366EDB7}.Debug|Any CPU.Build.0 = Debug|Any CPU {8CE32D7B-EA3D-4725-A270-9780D366EDB7}.Release|Any CPU.ActiveCfg = Release|Any CPU {8CE32D7B-EA3D-4725-A270-9780D366EDB7}.Release|Any CPU.Build.0 = Release|Any CPU - {503645DD-7711-40ED-8811-F06391F294AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {503645DD-7711-40ED-8811-F06391F294AB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {503645DD-7711-40ED-8811-F06391F294AB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {503645DD-7711-40ED-8811-F06391F294AB}.Release|Any CPU.Build.0 = Release|Any CPU {5A1A4FFF-78F5-48A2-9AB0-3E507E938465}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5A1A4FFF-78F5-48A2-9AB0-3E507E938465}.Debug|Any CPU.Build.0 = Debug|Any CPU {5A1A4FFF-78F5-48A2-9AB0-3E507E938465}.Release|Any CPU.ActiveCfg = Release|Any CPU {5A1A4FFF-78F5-48A2-9AB0-3E507E938465}.Release|Any CPU.Build.0 = Release|Any CPU - {752E5268-3D99-485D-A31E-FC40AE9C0867}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {752E5268-3D99-485D-A31E-FC40AE9C0867}.Debug|Any CPU.Build.0 = Debug|Any CPU - {752E5268-3D99-485D-A31E-FC40AE9C0867}.Release|Any CPU.ActiveCfg = Release|Any CPU - {752E5268-3D99-485D-A31E-FC40AE9C0867}.Release|Any CPU.Build.0 = Release|Any CPU + {581F17CF-C7DE-4147-8764-E6B4328C07E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {581F17CF-C7DE-4147-8764-E6B4328C07E7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {581F17CF-C7DE-4147-8764-E6B4328C07E7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {581F17CF-C7DE-4147-8764-E6B4328C07E7}.Release|Any CPU.Build.0 = Release|Any CPU + {6A891168-EACF-4AC1-B081-ABA2FFF0DA7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6A891168-EACF-4AC1-B081-ABA2FFF0DA7D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6A891168-EACF-4AC1-B081-ABA2FFF0DA7D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6A891168-EACF-4AC1-B081-ABA2FFF0DA7D}.Release|Any CPU.Build.0 = Release|Any CPU + {849A9DED-F4C6-4CFE-BA25-696F6E100198}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {849A9DED-F4C6-4CFE-BA25-696F6E100198}.Debug|Any CPU.Build.0 = Debug|Any CPU + {849A9DED-F4C6-4CFE-BA25-696F6E100198}.Release|Any CPU.ActiveCfg = Release|Any CPU + {849A9DED-F4C6-4CFE-BA25-696F6E100198}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -54,6 +60,7 @@ Global GlobalSection(NestedProjects) = preSolution {F85F6B93-D17D-4CAE-9B2F-D293BE73C700} = {CD32538C-E929-42F9-A936-5550427E2CD5} {5A1A4FFF-78F5-48A2-9AB0-3E507E938465} = {CD32538C-E929-42F9-A936-5550427E2CD5} + {581F17CF-C7DE-4147-8764-E6B4328C07E7} = {CD32538C-E929-42F9-A936-5550427E2CD5} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {EA6D26FC-C065-4849-959E-1EF7FC013AF2} diff --git a/src/GeneralTools/DataverseClient/DataverseClientWithConnector.sln b/src/GeneralTools/DataverseClient/DataverseClientWithConnector.sln index 0c60704..6b19603 100644 --- a/src/GeneralTools/DataverseClient/DataverseClientWithConnector.sln +++ b/src/GeneralTools/DataverseClient/DataverseClientWithConnector.sln @@ -23,7 +23,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerPlatform.Dat EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerPlatform.Dataverse.WebResourceUtility", "WebResourceUtility\Microsoft.PowerPlatform.Dataverse.WebResourceUtility.csproj", "{FDFD6B7F-A925-40EE-98DC-2E06C1D1E3B6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.PowerPlatform.Dataverse.Client.AzAuth", "Extensions\Microsoft.PowerPlatform.Dataverse.Client.AzAuth\Microsoft.PowerPlatform.Dataverse.Client.AzAuth.csproj", "{618D52B7-4CE7-402F-972B-E381C1C28299}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerPlatform.Dataverse.Client.AzAuth", "Extensions\Microsoft.PowerPlatform.Dataverse.Client.AzAuth\Microsoft.PowerPlatform.Dataverse.Client.AzAuth.csproj", "{618D52B7-4CE7-402F-972B-E381C1C28299}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzDevOps_ServiceConnection_Test", "UnitTests\AzDevOps_ServiceConnection_Test\AzDevOps_ServiceConnection_Test.csproj", "{581F17CF-C7DE-4147-8764-E6B4328C07E7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerPlatform.Dataverse.Client.PowerShell", "PowerShell\Microsoft.PowerPlatform.Dataverse.Client.PowerShell\Microsoft.PowerPlatform.Dataverse.Client.PowerShell.csproj", "{8FD12028-BBEF-448B-BD43-E352386149DD}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -143,6 +147,30 @@ Global {618D52B7-4CE7-402F-972B-E381C1C28299}.Release|Any CPU.Build.0 = Release|Any CPU {618D52B7-4CE7-402F-972B-E381C1C28299}.Release|x64.ActiveCfg = Release|Any CPU {618D52B7-4CE7-402F-972B-E381C1C28299}.Release|x64.Build.0 = Release|Any CPU + {581F17CF-C7DE-4147-8764-E6B4328C07E7}.CRMINTERNAL|Any CPU.ActiveCfg = Debug|Any CPU + {581F17CF-C7DE-4147-8764-E6B4328C07E7}.CRMINTERNAL|Any CPU.Build.0 = Debug|Any CPU + {581F17CF-C7DE-4147-8764-E6B4328C07E7}.CRMINTERNAL|x64.ActiveCfg = Debug|Any CPU + {581F17CF-C7DE-4147-8764-E6B4328C07E7}.CRMINTERNAL|x64.Build.0 = Debug|Any CPU + {581F17CF-C7DE-4147-8764-E6B4328C07E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {581F17CF-C7DE-4147-8764-E6B4328C07E7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {581F17CF-C7DE-4147-8764-E6B4328C07E7}.Debug|x64.ActiveCfg = Debug|Any CPU + {581F17CF-C7DE-4147-8764-E6B4328C07E7}.Debug|x64.Build.0 = Debug|Any CPU + {581F17CF-C7DE-4147-8764-E6B4328C07E7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {581F17CF-C7DE-4147-8764-E6B4328C07E7}.Release|Any CPU.Build.0 = Release|Any CPU + {581F17CF-C7DE-4147-8764-E6B4328C07E7}.Release|x64.ActiveCfg = Release|Any CPU + {581F17CF-C7DE-4147-8764-E6B4328C07E7}.Release|x64.Build.0 = Release|Any CPU + {8FD12028-BBEF-448B-BD43-E352386149DD}.CRMINTERNAL|Any CPU.ActiveCfg = Debug|Any CPU + {8FD12028-BBEF-448B-BD43-E352386149DD}.CRMINTERNAL|Any CPU.Build.0 = Debug|Any CPU + {8FD12028-BBEF-448B-BD43-E352386149DD}.CRMINTERNAL|x64.ActiveCfg = Debug|Any CPU + {8FD12028-BBEF-448B-BD43-E352386149DD}.CRMINTERNAL|x64.Build.0 = Debug|Any CPU + {8FD12028-BBEF-448B-BD43-E352386149DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8FD12028-BBEF-448B-BD43-E352386149DD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8FD12028-BBEF-448B-BD43-E352386149DD}.Debug|x64.ActiveCfg = Debug|Any CPU + {8FD12028-BBEF-448B-BD43-E352386149DD}.Debug|x64.Build.0 = Debug|Any CPU + {8FD12028-BBEF-448B-BD43-E352386149DD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8FD12028-BBEF-448B-BD43-E352386149DD}.Release|Any CPU.Build.0 = Release|Any CPU + {8FD12028-BBEF-448B-BD43-E352386149DD}.Release|x64.ActiveCfg = Release|Any CPU + {8FD12028-BBEF-448B-BD43-E352386149DD}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -151,6 +179,7 @@ Global {F85F6B93-D17D-4CAE-9B2F-D293BE73C700} = {CD32538C-E929-42F9-A936-5550427E2CD5} {5A1A4FFF-78F5-48A2-9AB0-3E507E938465} = {CD32538C-E929-42F9-A936-5550427E2CD5} {E24CC0FA-4686-448E-A9AC-7A2B58D07FF6} = {01915F6D-79FF-4118-9EAD-2470351C8035} + {581F17CF-C7DE-4147-8764-E6B4328C07E7} = {CD32538C-E929-42F9-A936-5550427E2CD5} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {EA6D26FC-C065-4849-959E-1EF7FC013AF2} diff --git a/src/GeneralTools/DataverseClient/Extensions/Microsoft.PowerPlatform.Dataverse.Client.AzAuth/AzAuth.cs b/src/GeneralTools/DataverseClient/Extensions/Microsoft.PowerPlatform.Dataverse.Client.AzAuth/AzAuth.cs index ff72d4b..72f3e17 100644 --- a/src/GeneralTools/DataverseClient/Extensions/Microsoft.PowerPlatform.Dataverse.Client.AzAuth/AzAuth.cs +++ b/src/GeneralTools/DataverseClient/Extensions/Microsoft.PowerPlatform.Dataverse.Client.AzAuth/AzAuth.cs @@ -108,8 +108,11 @@ public async Task GetAccessToken(string instanceUrl) if (_cacheList.ContainsKey(instanceUri)) { accessToken = _cacheList[instanceUri]; - if (accessToken.HasValue && accessToken.Value.ExpiresOn < DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(30))) + if (accessToken.HasValue && accessToken.Value.ExpiresOn < DateTimeOffset.UtcNow.Subtract(TimeSpan.FromSeconds(30))) + { accessToken = null; // flush the access token if it is about to expire. + _cacheList.Remove(instanceUri); + } } if ( accessToken == null) @@ -137,22 +140,25 @@ public async Task GetAccessToken(string instanceUrl) return accessToken.Value.Token; } - private string[] ResolveScopesList(Uri instanceUrl , Uri resource = null) + /// + /// gets or creates the scope list for the current instance. + /// + /// + /// + /// + /// + private string[] ResolveScopesList(Uri instanceUrl, Uri resource = null) { _scopesList ??= new Dictionary>(); - if ( _scopesList.ContainsKey(instanceUrl)) - { - return _scopesList[instanceUrl].ToArray(); - } + + if (_scopesList.TryGetValue(instanceUrl, out List foundList)) + return foundList.ToArray(); + if (resource == null) - { throw new ArgumentNullException("Resource URI is required"); - } - else - { - _scopesList.Add(instanceUrl, new List { $"{resource}.default" }); - return _scopesList[instanceUrl].ToArray(); - } + + _scopesList.Add(instanceUrl, new List { $"{resource}.default" }); + return _scopesList[instanceUrl].ToArray(); } /// diff --git a/src/GeneralTools/DataverseClient/Extensions/Microsoft.PowerPlatform.Dataverse.Client.AzAuth/AzPipelineFederatedIdentityAuth.cs b/src/GeneralTools/DataverseClient/Extensions/Microsoft.PowerPlatform.Dataverse.Client.AzAuth/AzPipelineFederatedIdentityAuth.cs new file mode 100644 index 0000000..69eaf0a --- /dev/null +++ b/src/GeneralTools/DataverseClient/Extensions/Microsoft.PowerPlatform.Dataverse.Client.AzAuth/AzPipelineFederatedIdentityAuth.cs @@ -0,0 +1,189 @@ +using Azure.Core; +using Azure.Identity; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using Microsoft.PowerPlatform.Dataverse.Client.Model; +using System.Diagnostics; +using System.Threading.Tasks; + +namespace Microsoft.PowerPlatform.Dataverse.Client +{ + /// + /// Auth class that will create a Workload Identity based authentication for Dataverse Service Client using the specified Azure DevOps Service Connection. + /// + public class AzPipelineFederatedIdentityAuth + { + private AzurePipelinesCredential _pipelineCredential; + private AzurePipelinesCredentialOptions _credentialOptions; + private readonly bool _autoResolveAuthorityAndTenant; + private Dictionary> _scopesList; + private Dictionary _cacheList; + private ILogger _logger; + private string _tenantId; + private string _clientId; + private string _serviceConnectionId; + private string _systemAccessTokenEnvVarName; + + /// + /// Creates a new instance of the ServiceClient class using the AzDevOps Service Connection + /// + /// TenantId for the service connection + /// ClientId for the service connection + /// Service Connection Id of AzDevOps ServiceConnection configured for workload identity + /// Dataverse ServiceClient Connection Options + /// Dataverse ServiceClient Configuration Options. Default = null + /// Environment Variable that has the current AzDevOps System Access Token. Default=SYSTEM_ACCESSTOKEN + /// + /// + public static ServiceClient CreateServiceClient( + string tenantId, + string clientId, + string serviceConnectionId, + ConnectionOptions connectionOptions, + ConfigurationOptions configurationOptions = null, + string systemAccessTokenEnvVarName = "SYSTEM_ACCESSTOKEN") + { + if (connectionOptions == null) + { + throw new ArgumentException("ConnectionOptions are required"); + } + if (connectionOptions.ServiceUri == null) + { + throw new ArgumentException("ConnectionOptions.ServiceUri is required"); + } + connectionOptions.AuthenticationType = AuthenticationType.ExternalTokenManagement; // force the authentication type to be external token management. + + AzPipelineFederatedIdentityAuth azAuth = new AzPipelineFederatedIdentityAuth( tenantId, clientId, serviceConnectionId, systemAccessTokenEnvVarName, true, connectionOptions.Logger); + connectionOptions.AccessTokenProviderFunctionAsync = azAuth.GetAccessToken; + return new ServiceClient(connectionOptions, false, configurationOptions); + } + + /// + /// Creates an instance of the AzPipelineFederatedIdentityAuth class + /// + /// Should resolve Dataverse authority and resource from url. + /// Service Connection Id of AzDevOps ServiceConnection configured for workload identity + /// ClientId for the service connection + /// TenantId for the service connection + /// Environment Variable that has the current AzDevOps System Access Token. Default=SYSTEM_ACCESSTOKEN + /// ILogger instance + public AzPipelineFederatedIdentityAuth(string tenantId, string clientId, string serviceConnectionId, string SystemAccessTokenEnvVarName, bool autoResolveAuthorityAndTenant, ILogger logger = null) + { + _tenantId = tenantId; + _clientId = clientId; + _serviceConnectionId = serviceConnectionId; + _systemAccessTokenEnvVarName = SystemAccessTokenEnvVarName; + _autoResolveAuthorityAndTenant = autoResolveAuthorityAndTenant; + _logger = logger; + } + + /// + /// Returns the current access token for the connected ServiceClient instance + /// + /// + /// + public async Task GetAccessToken(string instanceUrl) + { + if (!Uri.IsWellFormedUriString(instanceUrl, UriKind.RelativeOrAbsolute)) + { + throw new ArgumentException("Invalid instance URL"); + } + AccessToken? accessToken = null; + Uri instanceUri = new Uri(instanceUrl); + if (_pipelineCredential == null) + { + Uri resourceUri = await InitializeCredentials(instanceUri).ConfigureAwait(false); + ResolveScopesList(instanceUri, resourceUri); + } + + // Get or create existing token. + _cacheList ??= new Dictionary(); + if (_cacheList.ContainsKey(instanceUri)) + { + accessToken = _cacheList[instanceUri]; + if (accessToken.HasValue && accessToken.Value.ExpiresOn < DateTimeOffset.UtcNow.Subtract(TimeSpan.FromSeconds(30))) + { + accessToken = null; // flush the access token if it is about to expire. + _cacheList.Remove(instanceUri); + } + } + + if (accessToken == null) + { + Stopwatch sw = Stopwatch.StartNew(); + _logger.LogDebug("Getting new access token for {0}", instanceUri); + accessToken = await _pipelineCredential.GetTokenAsync(new TokenRequestContext(ResolveScopesList(instanceUri)), System.Threading.CancellationToken.None).ConfigureAwait(false); + _logger.LogDebug("Access token retrieved in {0}ms", sw.ElapsedMilliseconds); + sw.Stop(); + if (_cacheList.ContainsKey(instanceUri)) + { + _cacheList[instanceUri] = accessToken; + } + else + { + _cacheList.Add(instanceUri, accessToken); + } + } + + if (accessToken == null) + { + throw new Exception("Failed to retrieve access token"); + } + + return accessToken.Value.Token; + } + + private string[] ResolveScopesList(Uri instanceUrl, Uri resource = null) + { + _scopesList ??= new Dictionary>(); + + if (_scopesList.TryGetValue(instanceUrl, out List foundList)) + return foundList.ToArray(); + + if (resource == null) + throw new ArgumentNullException("Resource URI is required"); + + _scopesList.Add(instanceUrl, new List { $"{resource}.default" }); + return _scopesList[instanceUrl].ToArray(); + } + + /// + /// Initialize the credentials for the current instance + /// + /// + /// + private async Task InitializeCredentials(Uri instanceUrl) + { + _logger.LogDebug("Initializing credentials for {0}", instanceUrl); + Stopwatch sw = Stopwatch.StartNew(); + + Uri resourceUri = null; + _credentialOptions ??= new AzurePipelinesCredentialOptions(); + + if (_autoResolveAuthorityAndTenant) + { + _logger.LogDebug("Resolving authority and tenant for {0}", instanceUrl); + using var httpClient = new System.Net.Http.HttpClient(); + Auth.AuthorityResolver authorityResolver = new Auth.AuthorityResolver(httpClient); + var authDetails = await authorityResolver.ProbeForExpectedAuthentication(instanceUrl).ConfigureAwait(false); + resourceUri = authDetails.Resource; + _credentialOptions.AuthorityHost = authDetails.Authority; + //_credentialOptions.TenantId = authDetails.Authority.Segments[1].Replace("/", ""); + + _logger.LogDebug("Authority and tenant resolved in {0}ms", sw.ElapsedMilliseconds); + _logger.LogDebug("Initialize Creds - found authority with name " + (string.IsNullOrEmpty(authDetails.Authority.ToString()) ? "" : authDetails.Authority.ToString())); + _logger.LogDebug("Initialize Creds - found resource with name " + (string.IsNullOrEmpty(authDetails.Resource.ToString()) ? "" : authDetails.Resource.ToString())); + //_logger.LogDebug("Initialize Creds - found tenantId " + (string.IsNullOrEmpty(_credentialOptions.TenantId) ? "" : _credentialOptions.TenantId)); + } + + _pipelineCredential = new AzurePipelinesCredential(_tenantId, _clientId, _serviceConnectionId, Environment.GetEnvironmentVariable(_systemAccessTokenEnvVarName), _credentialOptions); + + _logger.LogDebug("Credentials initialized in {0}ms", sw.ElapsedMilliseconds); + sw.Start(); + + return resourceUri; + } + + } +} diff --git a/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/BuildDrop.ps1 b/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/BuildDrop.ps1 new file mode 100644 index 0000000..851b3ca --- /dev/null +++ b/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/BuildDrop.ps1 @@ -0,0 +1,120 @@ +#Build Drop Script +#Drops the files in the drop folder for the build. +[CmdletBinding(PositionalBinding=$true)] +param( + [string] $BuildSourcesDirectory , + [string] $BuildConfiguration, + [string] $StagingDirectory, + [string] $ProjectRootDirectory, + [string] $SolutionName, + [bool] $RunFromVSBuild = $false, + [bool] $EnableDebug = $false + + ) + +#BuildDrop for Microsoft.Xrm.OnlineManagementAPI solution +Write-Host ">>> ========================= Invoking BuildDrop.ps1 for $SolutionName =======================" +Write-Host ">>> BuildSourcesDirectory = $BuildSourcesDirectory" +Write-Host ">>> ProjectRootDirectory = $ProjectRootDirectory" +Write-Host ">>> SolutionName = $SolutionName" +Write-Host ">>> BuildConfiguration = $BuildConfiguration" +Write-Host ">>> StagingDirectory = $StagingDirectory" +Write-Host ">>> RunFromVSBuild = $RunFromVSBuild" +Write-Host ">>> Write Debug Info = $EnableDebug" + + +if ( [System.String]::IsNullOrEmpty($StagingDirectory) -eq $true) +{ + #Running local build + $StagingDirectory = $BuildSourcesDirectory +} + +if ( $RunFromVSBuild -eq $false ) +{ + $dropFolderName = "Drop" +} +else +{ + $dropFolderName = "Drop" +} + +$SolutionName = "Microsoft.PowerPlatform.Dataverse.Client.PowerShell"; +#Create path for drop directory +#format for Local Build: Root/Drop/Buildconfig/SolutionName/Bins. +#format for Server Build: Root/Buildconfig/SolutionName/Bins. +if($RunFromVSBuild -eq $false) +{ + #$dropPath = [System.IO.Path]::Combine($dropFolderName , $SolutionName ) + $dropPath = [System.IO.Path]::Combine($dropFolderName ) +} +else +{ + $dropPath = [System.IO.Path]::Combine($StagingDirectory , $dropFolderName , $BuildConfiguration) +} +Write-Host ">>> Output path is $dropPath" + +## Assembly Out directory +if($RunFromVSBuild -eq $false) +{ + $BinsDirectory = $StagingDirectory #[System.IO.Path]::Combine($ProjectRootDirectory , $SolutionName , "bin" , $BuildConfiguration ) +} +else +{ + $BinsDirectory = [System.IO.Path]::Combine($BuildSourcesDirectory , "bin" , $BuildConfiguration, "DataverseClient" , "net6.0" ) +} +## Copying PowerShell Module out only. +Write-Host ">>> BINS path is $BinsDirectory" + +# Setup Module Drop Directory Key +if($RunFromVSBuild -eq $false) +{ + $PowerShellModuleFilesDirectory = [System.IO.Path]::Combine($dropPath , $SolutionName) +} +else { + $PowerShellModuleFilesDirectory = [System.IO.Path]::Combine($dropPath , $SolutionName) +} +Write-Host ">>> Module Drop path is $PowerShellModuleFilesDirectory" + + +## ############## Project or Solution COPY code here. ############ ## +#create the Root Drop directory +New-Item -ItemType directory -Force $dropPath +##create subfolder for Management Powershell +New-Item -ItemType Directory -Force $PowerShellModuleFilesDirectory + +if ( [System.IO.Directory]::Exists($PowerShellModuleFilesDirectory) -eq $true ) +{ + #copy launcher. + #Copy-Item -Path "$BinsDirectory\*" -Destination $dropFolderName -Include 'RegisterXrmTooling.ps1' -Force + Robocopy $BinsDirectory $dropPath 'RegisterServiceClient.ps1' /XX + if ($lastexitcode -le 7) { + Write-Host ">>> ExitCode = " $lastexitcode + $lastexitcode = 0 + } + + # remove anything from Target so as to not upset robocopy + + #copy modules. + Robocopy ([System.IO.Path]::Combine($BinsDirectory , 'Microsoft.PowerPlatform.Dataverse.Client.PowerShell')) $PowerShellModuleFilesDirectory *.* /XX + if ($lastexitcode -le 7) { + Write-Host ">>> ExitCode = " $lastexitcode + $lastexitcode = 0 + } + #copy DLL's + Robocopy $BinsDirectory $PowerShellModuleFilesDirectory *.dll + if ($lastexitcode -le 7) + { + Write-Host ">>> ExitCode1 = " $lastexitcode + $lastexitcode = 0 + } + #copy Help + Robocopy $BinsDirectory $PowerShellModuleFilesDirectory *.dll-help.xml + if ($lastexitcode -le 7) + { + Write-Host ">>> ExitCode1 = " $lastexitcode + $lastexitcode = 0 + Exit 0 + } + + +} diff --git a/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Commands/BaseCmdlet.cs b/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Commands/BaseCmdlet.cs new file mode 100644 index 0000000..d20acb2 --- /dev/null +++ b/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Commands/BaseCmdlet.cs @@ -0,0 +1,159 @@ +// Ignore Spelling: Dataverse + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using System.Management.Automation; +using System.Reflection; + +namespace Microsoft.PowerPlatform.Dataverse.Client.PowerShell.Commands +{ + public class BaseCmdLet : PSCmdlet + { + #region Vars + ///// + ///// file Writer Link + ///// + //private Microsoft.Xrm.Tooling.Connector.DynamicsFileLogTraceListener commonFileWriter = null; + + /// + /// when present and populated, this will write the logs to the directory specified. loges are written only when -verbose is chosen. + /// + [Parameter(Mandatory = false, ValueFromPipelineByPropertyName = true)] + public string LogWriteDirectory { get; set; } = string.Empty; + +//#if DEBUG +// private System.Diagnostics.DefaultTraceListener commonConsoleListener = null; +//#endif + + #endregion + + internal ILogger CreateILogger() + { + var ConfigFileLocation = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? "", "appsettings.json"); + IConfiguration config = new ConfigurationBuilder() + .AddJsonFile(ConfigFileLocation, optional: true, reloadOnChange: true) + .Build(); + + ILoggerFactory loggerFactory = LoggerFactory.Create(builder => + builder.AddConsole(options => + { +#pragma warning disable CS0618 // Type or member is obsolete + options.IncludeScopes = true; + options.TimestampFormat = "hh:mm:ss "; +#pragma warning restore CS0618 // Type or member is obsolete + }) + .AddConfiguration(config.GetSection("Logging"))); + + return loggerFactory.CreateLogger(); + } + + /// + /// Determines if its necessary to enable tracing. + /// + internal void SetDiagnosticsMode() + { + if (this.MyInvocation.BoundParameters.ContainsKey("verbose") && (SwitchParameter)this.MyInvocation.BoundParameters["verbose"]) + { +// CrmConnectControl.Utility.TraceControlSettings.TraceLevel = System.Diagnostics.SourceLevels.Verbose; +// Connector.TraceControlSettings.TraceLevel = System.Diagnostics.SourceLevels.Verbose; + +// if (CrmConnectControl.Utility.TraceControlSettings.TraceLevel != System.Diagnostics.SourceLevels.Off) +// { +// // Create a common TraceFile Writer. +// if (commonFileWriter == null) +// { +// if (!string.IsNullOrEmpty(LogWriteDirectory) && System.IO.Directory.Exists(LogWriteDirectory)) +// { +// commonFileWriter = new Connector.DynamicsFileLogTraceListener() +// { +// BaseFileName = "Microsoft.PowerPlatform.Dataverse.Client.PowerShell", +// Location = VisualBasic.Logging.LogFileLocation.Custom, +// CustomLocation = LogWriteDirectory +// }; +// } +// else +// { +// commonFileWriter = new Connector.DynamicsFileLogTraceListener() +// { +// BaseFileName = "Microsoft.PowerPlatform.Dataverse.Client.PowerShell", +// Location = VisualBasic.Logging.LogFileLocation.LocalUserApplicationDirectory +// }; + +// } + +// this.WriteVerbose(string.Format("Verbose output log file: '{0}'", commonFileWriter.FullLogFileName)); + +// CrmConnectControl.Utility.TraceControlSettings.AddTraceListener(commonFileWriter); +// Connector.TraceControlSettings.AddTraceListener(commonFileWriter); +// } + +//#if DEBUG +// if ( commonConsoleListener == null ) +// { +// commonConsoleListener = new System.Diagnostics.DefaultTraceListener(); +// CrmConnectControl.Utility.TraceControlSettings.AddTraceListener(commonFileWriter); +// Connector.TraceControlSettings.AddTraceListener(commonFileWriter); +// } +//#endif + +// } + } + else + if (this.MyInvocation.BoundParameters.ContainsKey("verbose") && !(SwitchParameter)this.MyInvocation.BoundParameters["verbose"]) + { + //// forces it off. + //CrmConnectControl.Utility.TraceControlSettings.TraceLevel = System.Diagnostics.SourceLevels.Off; + //Connector.TraceControlSettings.TraceLevel = System.Diagnostics.SourceLevels.Off; + } + } + + /// + /// Cleans up open TraceWriters + /// + internal void CleanUpDiagnosticsMode() + { + //CrmConnectControl.Utility.TraceControlSettings.CloseListeners(); + //Connector.TraceControlSettings.CloseListeners(); + } + + } + + #region Threading Support Class + /// + /// Type of write to use. + /// + internal enum WriteInfoType + { + Warning = 0, + Verbose, + Debug + } + + /// + /// holder class to signal handling to the cmdlet adapter writer. + /// + internal class GeneralWriteInfo + { + /// + /// Warning message to write. + /// + public string Message { get; set; } + + /// + /// Type of Message in this class. + /// + public WriteInfoType MessageType { get; set; } + + /// + /// Warning Message to write. + /// + /// + public GeneralWriteInfo(string warningMessage, WriteInfoType messageType) + { + Message = warningMessage; + MessageType = messageType; + } + } + + #endregion +} diff --git a/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Commands/CommonAuth.cs b/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Commands/CommonAuth.cs new file mode 100644 index 0000000..7348601 --- /dev/null +++ b/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Commands/CommonAuth.cs @@ -0,0 +1,224 @@ +// Ignore Spelling: Dataverse Auth queryfor Orgs + +using System.Management.Automation; +using System.Globalization; +using Microsoft.Extensions.Logging; +using Microsoft.PowerPlatform.Dataverse.Client.Model; + +namespace Microsoft.PowerPlatform.Dataverse.Client.PowerShell.Commands +{ + public class CommonAuth : BaseCmdLet + { + #region Vars + /// + /// Connection timeout setting for CRM connection. + /// + private int maxcrmconnectiontimeoutminutes = -1; + /// + /// Connection string to use when connecting to CRM + /// + internal string connectionString = string.Empty; + + /// + /// Url of organization to connect too. + /// + internal Uri? orgUrlToUse; + /// + /// Tenant ID for FIC + /// + internal Guid tenantId; + /// + /// FIC Client Id + /// + internal Guid clientId; + /// + /// Service Connection Id + /// + internal Guid serviceConnectionId; + /// + /// Environment AccessToken KeyName. + /// + internal string accessTokenEnvKeyName = string.Empty; + + /// + /// CrmSvcConnection + /// + protected ServiceClient? serviceClient = null; + + ///// + ///// Progress Record. + ///// + //private ProgressRecord? connProgress = null; + + /// + /// Error Record if any. + /// + private ErrorRecord? errorRecord = null; + + /// + /// set when querying for orgs. + /// + public bool queryforOrgs = false; + + /// + /// PowerShell MultiThreaded Adapter. + /// + //private PowerShellAdapter? adapter = null; + + /// + /// Percentage completed. + /// + //private int percentCmp = 0; + + + #endregion + + #region Properties. + + /// + /// The following is the definition of the input parameter "MaxConnectionTimeOutMinutes". + /// User credential used to login to CRM + /// + [Parameter(Position = 20, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + public int MaxConnectionTimeOutMinutes + { + get { return this.maxcrmconnectiontimeoutminutes; } + set { this.maxcrmconnectiontimeoutminutes = value; } + } + + #endregion + + public CommonAuth() + { + // Auto Add TLS 1.2 support which is required. + System.Net.ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Tls12; + } + + /// + /// Get Orgs. + /// + public void ExecuteGetOrgs() + { + ExecuteAuth(); + if (errorRecord != null) + WriteError(errorRecord); + } + + /// + /// Run Auth. + /// + public void ExecuteAuth() + { + RunAuth(); + if (!queryforOrgs && errorRecord != null) + WriteError(errorRecord); + } + + /// + /// Authenticate with Dataverse. + /// + private void RunAuth() + { + if (serviceConnectionId != null && serviceConnectionId != Guid.Empty) + { + serviceClient = AzPipelineFederatedIdentityAuth.CreateServiceClient( + tenantId.ToString(), + clientId.ToString(), + serviceConnectionId.ToString(), + new ConnectionOptions() + { + ServiceUri = orgUrlToUse, + Logger = CreateILogger() + } + ); + } + else if (!string.IsNullOrEmpty(connectionString)) + { + // Connection string is present. + ServiceClient localServiceClient = new ServiceClient(connectionString, CreateILogger()); + if (localServiceClient != null && localServiceClient.IsReady) + serviceClient = localServiceClient; + else + { + if (!string.IsNullOrEmpty(localServiceClient?.LastError) || localServiceClient?.LastException != null) + { + errorRecord = new ErrorRecord(new Exception(string.Format(CultureInfo.InvariantCulture, "Failed to connect to Dataverse: {0}", localServiceClient.LastError), localServiceClient.LastException), "-10", ErrorCategory.PermissionDenied, null); + WriteError(errorRecord); + serviceClient = null; + } + } + } + else + { + // No connection string. + errorRecord = new ErrorRecord(new Exception("Connection string is required to connect to Dataverse"), "-1", ErrorCategory.InvalidArgument, null); + WriteError(errorRecord); + serviceClient = null; + } + + // Set connection timeout if required. + if (serviceClient != null && MaxConnectionTimeOutMinutes != -1) + { + WriteVerbose(string.Format(CultureInfo.InstalledUICulture, "Dataverse Connection Timeout set to {0} Minutes", MaxConnectionTimeOutMinutes)); + SetConnectionTimeoutValues(new TimeSpan(0, MaxConnectionTimeOutMinutes, 0)); + } + else + if (serviceClient != null) + WriteVerbose(string.Format(CultureInfo.InstalledUICulture, "Dataverse Connection Timeout is set to {0} Minutes", GetConnectionTimeoutValues().Minutes)); + } + + #region Private classes. + + /// + /// Updates the timeout value to extend the amount of item that a request will wait. + /// + public void SetConnectionTimeoutValues(TimeSpan TimeOutToSet) + { + ServiceClient.MaxConnectionTimeout = TimeOutToSet; + } + + /// + /// Gets the current connection time out value. + /// + /// + private TimeSpan GetConnectionTimeoutValues() + { + return ServiceClient.MaxConnectionTimeout; + } + + + #endregion + + } + #region Extension Methods for SecureString + ///// + ///// Adds a extension to Secure string + ///// + //internal static class SecureStringExtensions + //{ + // /// + // /// DeCrypt a Secure password + // /// + // /// + // /// + // public static string ToUnsecureString(this SecureString value) + // { + // if (null == value) + // throw new ArgumentNullException("value"); + + // // Get a pointer to the secure string memory data. + // IntPtr ptr = Marshal.SecureStringToGlobalAllocUnicode(value); + // try + // { + // // DeCrypt + // return Marshal.PtrToStringUni(ptr); + // } + // finally + // { + // // release the pointer. + // Marshal.ZeroFreeGlobalAllocUnicode(ptr); + // } + // } + //} + #endregion +} diff --git a/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Commands/GetCrmConnection.cs b/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Commands/GetCrmConnection.cs new file mode 100644 index 0000000..5b20404 --- /dev/null +++ b/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Commands/GetCrmConnection.cs @@ -0,0 +1,134 @@ +// Ignore Spelling: Dataverse + +using System.Management.Automation; +using System.Reflection; + +namespace Microsoft.PowerPlatform.Dataverse.Client.PowerShell.Commands +{ + /// + /// this will establish a CRM connection + /// + [Cmdlet(VerbsCommon.Get, "PowerPlatformConnection")] + public class GetPowerPlatformConnection : CommonAuth + { + + /// + /// Tenant Id of the FIC + /// + [Parameter(Mandatory = true, Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "FIC")] + public Guid TenantId + { + get { return base.tenantId; } + set { base.tenantId = value; } + } + + /// + /// Client Id of the FIC + /// + [Parameter(Mandatory = true, Position = 2, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "FIC")] + public Guid ClientId + { + get { return base.clientId; } + set { base.clientId = value; } + } + + /// + /// Service Connection Id of the FIC + /// + [Parameter(Mandatory = true, Position = 3, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "FIC")] + public Guid ServiceConnectionId + { + get { return base.serviceConnectionId; } + set { base.serviceConnectionId = value; } + } + + /// + /// OrganizationUrl of the FIC + /// + [Parameter(Mandatory = true, Position = 4, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "FIC")] + public Uri? OrganizationUrl + { + get { return base.orgUrlToUse; } + set { base.orgUrlToUse = value; } + } + + /// + /// Environment AccessToken KeyName. + /// + [Parameter(Mandatory = false , Position = 4, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "FIC")] + public string AccessTokenEnvKeyName + { + get { return base.accessTokenEnvKeyName; } + set { base.accessTokenEnvKeyName = value; } + } + + /// + /// Used to set the connection string to connect to crm. all other connection elements are ignored when this string appears. + /// + [Parameter(Mandatory = true, Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "ConnectionStringOnly")] + public string ConnectionString + { + get { return base.connectionString; } + set { base.connectionString = value; } + } + + /// + /// PreInitializion + /// + protected override void BeginProcessing() + { + base.queryforOrgs = false; + + //string thisAssemblyPath = string.Empty; + //var AsmList = AppDomain.CurrentDomain.GetAssemblies(); + //bool found = false; + //// Look in the assemblies for the resources file. + //foreach (Assembly asm in AsmList) + //{ + // if (asm.FullName.ToLower().Contains(XamlResourceName)) + // { + // found = true; + // break; + // } + //} + + //if (System.IO.File.Exists(Assembly.GetExecutingAssembly().Location)) + // thisAssemblyPath = Assembly.GetExecutingAssembly().Location; + + //if (!found) + //{ + // // Get the Direction Info object + // System.IO.DirectoryInfo d = new System.IO.DirectoryInfo(thisAssemblyPath); + // // Remove file name + // thisAssemblyPath = System.IO.Path.GetDirectoryName(thisAssemblyPath); + + // // Load the assembly. + // if (System.IO.File.Exists(System.IO.Path.Combine(thisAssemblyPath, string.Format("{0}.dll", XamlResourceName)))) + // Assembly.LoadFile(System.IO.Path.Combine(thisAssemblyPath, string.Format("{0}.dll", XamlResourceName))); + //} + + base.BeginProcessing(); + } + + /// + /// ProcessRecord method. + /// + protected override void ProcessRecord() + { + try + { + base.SetDiagnosticsMode(); + base.ExecuteAuth(); + if (serviceClient != null) + WriteObject(serviceClient); + base.CleanUpDiagnosticsMode(); + } + catch (Exception generalEx) + { + // General error write for something we don't understand going wrong. + WriteError(new ErrorRecord(generalEx, "-9", ErrorCategory.SyntaxError, null)); + } + } + + }//End Class +}//End namespace diff --git a/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Commands/PowerShellAdapter.cs b/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Commands/PowerShellAdapter.cs new file mode 100644 index 0000000..d40d8b8 --- /dev/null +++ b/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Commands/PowerShellAdapter.cs @@ -0,0 +1,110 @@ +// Ignore Spelling: Dataverse cmdlet + +using System.Management.Automation; + +namespace Microsoft.PowerPlatform.Dataverse.Client.PowerShell.Commands +{ + /// + /// Adapter Class + /// + internal class PowerShellAdapter + { + private ManualResetEvent queueEvent = new ManualResetEvent(false); + + #region Parameters + private Cmdlet cmdlet { get; set; } + private Queue queue { get; set; } + private object LockToken { get; set; } + public State state; + public ErrorRecord? fatalErrorRecord; + #endregion + + /// + /// Ctor + /// + /// cmdlet + public PowerShellAdapter(Cmdlet cmdlet) + { + this.cmdlet = cmdlet; + this.LockToken = new object(); + this.queue = new Queue(); + this.state = State.Running; + fatalErrorRecord = null; + } + + #region Methods + /// + /// Listener Method + /// + /// Time after which Operation will declared as Timed Out + public void Listen(TimeSpan timeout) + { + DateTime dateTime = DateTime.UtcNow; + dateTime = dateTime.Add(timeout); + while (state == State.Running || queue.Count > 0) + { + queueEvent.WaitOne(timeout); + while (queue.Count > 0) + { + var obj = queue.Dequeue(); + + if (obj is ErrorRecord) + { + cmdlet.WriteError((ErrorRecord)obj); + } + else if (obj is ProgressRecord) + { + cmdlet.WriteProgress((ProgressRecord)obj); + } + else if (obj is GeneralWriteInfo) + { + // General write info to the UX. + switch (((GeneralWriteInfo)obj).MessageType) + { + case WriteInfoType.Warning: + cmdlet.WriteWarning(((GeneralWriteInfo)obj).Message); + break; + case WriteInfoType.Verbose: + cmdlet.WriteVerbose(((GeneralWriteInfo)obj).Message); + break; + default: + cmdlet.WriteDebug(((GeneralWriteInfo)obj).Message); + break; + } + } + } + if (DateTime.UtcNow > dateTime) + { + cmdlet.ThrowTerminatingError(new ErrorRecord(new Exception("TIMED_OUT"), "1", ErrorCategory.OperationTimeout, this)); + } + Thread.Sleep(100); + } + + if (state == State.FinishedWithError) + { + cmdlet.ThrowTerminatingError(fatalErrorRecord); + } + } + + public void Write(object obj) + { + lock (LockToken) + { + queue.Enqueue(obj); + queueEvent.Set(); + } + } + #endregion + } + + /// + /// Determines the State of the Running Import + /// + internal enum State + { + Running, + FinishedWithError, + FinishedWithSuccess, + TimedOut + }; +} diff --git a/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/GenerateCatlogFile.ps1 b/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/GenerateCatlogFile.ps1 new file mode 100644 index 0000000..596c7fb --- /dev/null +++ b/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/GenerateCatlogFile.ps1 @@ -0,0 +1,50 @@ +<# +This script will create the catalog file from the signed assembly in a PowerShell command shell and drops the files in the drop folder for the signing. . +#> + +[CmdletBinding(PositionalBinding=$true)] +param( + [string] $BuildSourcesDirectory , + [string] $BuildConfiguration, + [string] $StagingDirectory, + [bool] $RunFromVSBuild = $false, + [bool] $EnableDebug = $false + ) + +$SolutionName = "Microsoft.PowerPlatform.Dataverse.Client.PowerShell" +Write-Host ">>> ========================= Invoking GenerateCatalogFile.ps1 for $SolutionName =======================" +Write-Host ">>> BuildSourcesDirectory = $BuildSourcesDirectory" +Write-Host ">>> SolutionName = $SolutionName" +Write-Host ">>> BuildConfiguration = $BuildConfiguration" +Write-Host ">>> StagingDirectory = $StagingDirectory" +Write-Host ">>> RunFromVSBuild = $RunFromVSBuild" +Write-Host ">>> Write Debug Info = $EnableDebug" + +Write-Host ">>> VERSION INFO:" +$PSVersionTable + +Write-Host ">>> LOADING Microsoft.Powershell.Security" +Import-Module Microsoft.Powershell.Security -Verbose + +if ( [System.String]::IsNullOrEmpty($StagingDirectory) -eq $true) +{ + #Running local build + $StagingDirectory = $BuildSourcesDirectory +} + +#Create path for drop directory +$dropFolderName = "Drop" +$dropPath = [System.IO.Path]::Combine($StagingDirectory , $dropFolderName, $SolutionName) +Write-Host ">>> Output path is $dropPath" + +$catalogFilePath = "$dropPath\Microsoft.PowerPlatform.Dataverse.Client.PowerShell.cat" + +Write-Host ">>> CatalogFile path is $catalogFilePath" + +$isExists = Get-Item -LiteralPath "$catalogFilePath" -ErrorAction SilentlyContinue +if ($null -ne $isExists) +{ + Remove-Item $catalogFilePath -Force +} + +New-FileCatalog -Path "$dropPath" -CatalogFilePath $catalogFilePath -CatalogVersion 1.0 \ No newline at end of file diff --git a/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell.csproj b/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell.csproj new file mode 100644 index 0000000..449aabe --- /dev/null +++ b/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell.csproj @@ -0,0 +1,62 @@ + + + + latest + DataverseClient + true + + + + + $(MSBuildProjectDirectory)\ + true + net6.0 + enable + enable + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + Always + + + + + pwsh -ExecutionPolicy RemoteSigned -Command "& '$(ProjectDir)BuildDrop.ps1' -EnableDebug 0 -BuildSourcesDirectory '$(ProjectDir)' -BuildConfiguration '$(Configuration)' -StagingDirectory '$(OutDir)' -ProjectRootDirectory '$(SolutionDir)' -SolutionName '$(SolutionName)' " +pwsh -ExecutionPolicy RemoteSigned -Command "& '$(ProjectDir)GenerateCatlogFile.ps1' -EnableDebug 0 -BuildSourcesDirectory '$(ProjectDir)' -BuildConfiguration '$(Configuration)' -StagingDirectory '$(OutDir)' " + + + diff --git a/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Microsoft.PowerPlatform.Dataverse.Client.Connect.psm1 b/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Microsoft.PowerPlatform.Dataverse.Client.Connect.psm1 new file mode 100644 index 0000000..682450d --- /dev/null +++ b/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Microsoft.PowerPlatform.Dataverse.Client.Connect.psm1 @@ -0,0 +1,144 @@ +function Connect-PowerPlatformDataverse { + # .ExternalHelp Microsoft.Xrm.Data.PowerShell.Help.xml + [CmdletBinding()] + PARAM( + [parameter(Position = 1, Mandatory = $true, ParameterSetName = "connectionstring")] + [string]$ConnectionString, + [parameter(Position = 1, Mandatory = $true, ParameterSetName = "FIC")] + [ValidatePattern('([\w-]+).crm([0-9]*).(microsoftdynamics|dynamics|crm[\w-]*).(com|de|us|cn)')] + [string]$ServerUrl, + [parameter(Position = 2, Mandatory = $true, ParameterSetName = "FIC")] + [ValidateScript({ +             try { +                 [System.Guid]::Parse($_) | Out-Null +                 $true +             } + catch { +                 $false +             } +         })] + [string]$TenantId, + [parameter(Position = 3, Mandatory = $true, ParameterSetName = "FIC")] + [ValidateScript({ +             try { +                 [System.Guid]::Parse($_) | Out-Null +                 $true +             } + catch { +                 $false +             } +         })] + [string]$ClientId, + [parameter(Position = 4, Mandatory = $true, ParameterSetName = "FIC")] + [ValidateScript({ +             try { +                 [System.Guid]::Parse($_) | Out-Null +                 $true +             } + catch { +                 $false +             } +         })] + [string]$ServiceConnectionId, + [parameter(Position = 4, Mandatory = $false, ParameterSetName = "FIC")] + [string]$AccessTokenEnvironmentKeyName, + + [int]$ConnectionTimeoutInSeconds, +# [string]$LogWriteDirectory, + [switch]$BypassTokenCache + ) + AddTls12Support #make sure tls12 is enabled + + if ($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true) { + # Enable-CrmConnectorVerboseLogging + } + + if (-not [string]::IsNullOrEmpty($ServerUrl) -and $ServerUrl.StartsWith("https://", "CurrentCultureIgnoreCase") -ne $true) { + Write-Verbose "ServerUrl is missing https, fixing URL: https://$ServerUrl" + $ServerUrl = "https://" + $ServerUrl + } + + #starting default connection string with require new instance and server url + $cs = ";Url=$ServerUrl" + if ($BypassTokenCache) { + $cs += ";TokenCacheStorePath=" + } + + if ($ConnectionTimeoutInSeconds -and $ConnectionTimeoutInSeconds -gt 0) { + $newTimeout = New-Object System.TimeSpan -ArgumentList 0, 0, $ConnectionTimeoutInSeconds + Write-Verbose "Setting new connection timeout of $newTimeout" + #set the timeout on the MaxConnectionTimeout static + [Microsoft.PowerPlatform.Dataverse.Client.ServiceClient]::MaxConnectionTimeout = $newTimeout + } + + if ($ConnectionString) { + if (!$ConnectionString -or $ConnectionString.Length -eq 0) { + throw "Cannot create the ServiceClient, the connection string is null" + } + Write-Verbose "ConnectionString provided - skipping all helpers/known parameters" + + $global:conn = Get-PowerPlatformConnection -ConnectionString $ConnectionString + if ($global:conn) { + ApplyServiceClientObjectTemplate($global:conn) #applyObjectTemplateFormat + } + return $global:conn + } + elseif ($ServiceConnectionId) { + try { + + $global:conn = Get-PowerPlatformConnection -ServiceConnectionId $ServiceConnectionId -TenantId $TenantId -ClientId $ClientId -AccessTokenEnvKeyName $AccessTokenEnvironmentKeyName -OrganizationUrl $ServerUrl + + ApplyServiceClientObjectTemplate($global:conn) #applyObjectTemplateFormat + $global:conn + return + } + catch { + throw $_ + } + } +} + +function AddTls12Support { + #by default PowerShell will show Ssl3, Tls - since SSL3 is not desirable we will drop it and use Tls + Tls12 + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls -bor [System.Net.SecurityProtocolType]::Tls12 +} + +function ApplyServiceClientObjectTemplate { + [CmdletBinding()] + PARAM( + [parameter(Mandatory = $true)] + [Microsoft.PowerPlatform.Dataverse.Client.ServiceClient]$conn + ) + try { + $defaultPropsServiceClient = @( + 'IsReady', + 'IsBatchOperationsAvailable', + 'MaxRetryCount', + 'RetryPauseTime', + 'Authority', + 'ActiveAuthenticationType', + 'OAuthUserId', + 'TenantId', + 'EnvironmentId', + 'ConnectedOrgId', + 'ConnectedOrgUriActual', + 'ConnectedOrgFriendlyName', + 'ConnectedOrgUniqueName', + 'ConnectedOrgVersion', + 'SdkVersionProperty', + 'CallerId', + 'CallerAADObjectId', + 'DisableCrossThreadSafeties', + 'SessionTrackingId', + 'ForceServerMetadataCacheConsistency', + 'RecommendedDegreesOfParallelism', + 'LastError' + ) + $defaultPropsSetServiceClient = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet', [string[]]$defaultPropsServiceClient) + $PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]@($defaultPropsSetServiceClient) + $conn | Add-Member MemberSet PSStandardMembers $PSStandardMembers -Force + } + Catch { + Write-Verbose "Failed to set a new PSStandardMember on connection object" + } +} \ No newline at end of file diff --git a/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Microsoft.PowerPlatform.Dataverse.Client.Operations.psm1 b/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Microsoft.PowerPlatform.Dataverse.Client.Operations.psm1 new file mode 100644 index 0000000..fc5b6f8 --- /dev/null +++ b/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Microsoft.PowerPlatform.Dataverse.Client.Operations.psm1 @@ -0,0 +1,1346 @@ +Import-Module (Join-Path (Split-Path $script:MyInvocation.MyCommand.Path) "UtilityFunctions.psm1") -NoClobber #-Force + + +function New-DataverseRecord { + # .ExternalHelp Microsoft.Xrm.Data.PowerShell.Help.xml + [CmdletBinding()] + PARAM( + [parameter(Mandatory = $false)] + [Microsoft.PowerPlatform.Dataverse.Client.ServiceClient]$conn, + [parameter(Mandatory = $true, Position = 1, ParameterSetName = "NameAndFields")] + [string]$EntityLogicalName, + [parameter(Mandatory = $true, Position = 2, ParameterSetName = "NameAndFields")] + [hashtable]$Fields, + [parameter(Mandatory = $true, Position = 1, ParameterSetName = "DataverseRecord")] + [PSObject]$DataverseRecord, + [parameter(Mandatory = $false, Position = 2, ParameterSetName = "DataverseRecord")] + [switch]$PreserveDataverseRecordId + ) + + $conn = VerifyConnectionParam -conn $conn -pipelineValue ($PSBoundParameters.ContainsKey('conn')) + + $newfields = New-Object 'System.Collections.Generic.Dictionary[[String], [Microsoft.PowerPlatform.Dataverse.Client.DataverseDataTypeWrapper]]' + + if ($DataverseRecord -ne $null) { + $EntityLogicalName = $DataverseRecord.ReturnProperty_EntityName + $atts = Get-DataverseEntityAttributes -conn $conn -EntityLogicalName $EntityLogicalName + foreach ($DvFieldKey in ($DataverseRecord | Get-Member -MemberType NoteProperty).Name) { + if ($DvFieldKey.EndsWith("_Property")) { + if ($DataverseRecord.ReturnProperty_Id -eq $DataverseRecord.$DvFieldKey.Value -and !$PreserveDataverseRecordId) { + continue; + } + elseif (($atts | ? logicalname -eq $DataverseRecord.$DvFieldKey.Key).IsValidForCreate) { + # Some fields cannot be created even though it is set as IsValidForCreate + if ($DataverseRecord.$DvFieldKey.Key.Contains("addressid")) { + continue; + } + else { + $newfield = New-Object -TypeName 'Microsoft.PowerPlatform.Dataverse.Client.DataverseDataTypeWrapper' + + $newfield.Type = MapFieldTypeByFieldValue -Value $DataverseRecord.$DvFieldKey.Value + $newfield.Value = $DataverseRecord.$DvFieldKey.Value + $newfields.Add($DataverseRecord.$DvFieldKey.Key, $newfield) + } + } + } + } + } + else { + foreach ($field in $Fields.GetEnumerator()) { + $newfield = New-Object -TypeName 'Microsoft.PowerPlatform.Dataverse.Client.DataverseDataTypeWrapper' + + $newfield.Type = MapFieldTypeByFieldValue -Value $field.Value + + $newfield.Value = $field.Value + $newfields.Add($field.Key, $newfield) + } + } + try { + $result = [Microsoft.PowerPlatform.Dataverse.Client.Extensions.CRUDExtentions]::CreateNewRecord($conn, $EntityLogicalName, $newfields, $null, $false, [Guid]::Empty, $false) + if (!$result -or $result -eq [System.Guid]::Empty) { + throw LastConnectorException($conn) + } + } + catch { + Write-Error LastConnectorException($conn) + } + + return $result +} + +function Get-DataverseRecord { + # .ExternalHelp Microsoft.Xrm.Data.PowerShell.Help.xml + [CmdletBinding()] + PARAM( + [parameter(Mandatory = $false)] + [Microsoft.PowerPlatform.Dataverse.Client.ServiceClient]$conn, + [parameter(Mandatory = $true, Position = 1)] + [string]$EntityLogicalName, + [parameter(Mandatory = $true, Position = 2)] + [guid]$Id, + [parameter(Mandatory = $true, Position = 3)] + [string[]]$Fields, + [parameter(Mandatory = $false, Position = 4)] + [switch]$IncludeNullValue + ) + + $conn = VerifyConnectionParam -conn $conn -pipelineValue ($PSBoundParameters.ContainsKey('conn')) + + if ($Fields -eq "*") { + [Collections.Generic.List[String]]$x = $null + } + else { + [Collections.Generic.List[String]]$x = $Fields + } + + try { + $record = [Microsoft.PowerPlatform.Dataverse.Client.Extensions.QueryExtensions]::GetEntityDataById($conn, $EntityLogicalName, $Id, $x, [Guid]::Empty) + } + catch { + throw LastConnectorException($conn) + } + + if ($record -eq $null) { + throw LastConnectorException($conn) + } + + $psobj = @{ } + $meta = Get-DataverseEntityMetadata -conn $conn -EntityLogicalName $EntityLogicalName -EntityFilters Attributes + if ($IncludeNullValue) { + if ($Fields -eq "*") { + # Add all fields first + foreach ($attName in $meta.Attributes) { + if (-not $attName.IsValidForRead) { continue } + $psobj[$attName.LogicalName] = $null + $psobj["$($attName.LogicalName)_Property"] = $null + } + } + else { + foreach ($attName in $Fields) { + $psobj[$attName] = $null + $psobj["$($attName)_Property"] = $null + } + } + } + + foreach ($att in $record.GetEnumerator()) { + if ($att.Value -is [Microsoft.Xrm.Sdk.EntityReference]) { + $psobj[$att.Key] = $att.Value.Name + } + elseif ($att.Value -is [Microsoft.Xrm.Sdk.AliasedValue]) { + $psobj[$att.Key] = $att.Value.Value + } + else { + $psobj[$att.Key] = $att.Value + } + } + $psobj += @{ + original = $record + logicalname = $EntityLogicalName + EntityReference = New-DataverseEntityReference -EntityLogicalName $EntityLogicalName -Id $Id + ReturnProperty_EntityName = $EntityLogicalName + ReturnProperty_Id = $record.($meta.PrimaryIdAttribute) + } + + [PSCustomObject]$psobj +} + +function Set-DataverseRecord { + # .ExternalHelp Microsoft.Xrm.Data.PowerShell.Help.xml + + [CmdletBinding()] + PARAM( + [parameter(Mandatory = $false)] + [Microsoft.PowerPlatform.Dataverse.Client.ServiceClient]$conn, + [parameter(Mandatory = $true, Position = 1, ParameterSetName = "DataverseRecord")] + [PSObject]$DataverseRecord, + [parameter(Mandatory = $true, Position = 1, ParameterSetName = "Fields")] + [string]$EntityLogicalName, + [parameter(Mandatory = $true, Position = 2, ParameterSetName = "Fields")] + [guid]$Id, + [parameter(Mandatory = $true, Position = 3, ParameterSetName = "Fields")] + [hashtable]$Fields, + [parameter(Mandatory = $false)] + [switch]$Upsert, + [parameter(Mandatory = $false)] + [AllowNull()] + [AllowEmptyString()] + [string]$PrimaryKeyField + ) + + $conn = VerifyConnectionParam -conn $conn -pipelineValue ($PSBoundParameters.ContainsKey('conn')) + + if ($DataverseRecord -ne $null) { + $entityLogicalName = $DataverseRecord.logicalname + } + else { + $entityLogicalName = $EntityLogicalName + } + + # 'PrimaryKeyField' is an options parameter and is used for custom activity entities + if (-not [string]::IsNullOrEmpty($PrimaryKeyField)) { + $primaryKeyField = $PrimaryKeyField + } + else { + $primaryKeyField = GuessPrimaryKeyField -EntityLogicalName $entityLogicalName + } + + # If upsert specified + if ($Upsert) { + $retrieveFields = New-Object System.Collections.Generic.List[string] + if ($DataverseRecord -ne $null) { + # when DataverseRecord passed, assume this comes from other system. + $id = $DataverseRecord.$primaryKeyField + foreach ($DvFieldKey in ($DataverseRecord | Get-Member -MemberType NoteProperty).Name) { + if ($DvFieldKey.EndsWith("_Property")) { + $retrieveFields.Add(($DataverseRecord.$DvFieldKey).Key) + } + elseif (($DvFieldKey -eq "original") -or ($DvFieldKey -eq "logicalname") -or ($DvFieldKey -eq "EntityReference")` + -or ($DvFieldKey -like "ReturnProperty_*")) { + continue + } + else { + # to have original value, rather than formatted value, replace the value from original record. + $DataverseRecord.$DvFieldKey = $DataverseRecord.original[$DvFieldKey + "_Property"].Value + } + } + } + else { + foreach ($DvFieldKey in $Fields.Keys) { + $retrieveFields.Add($DvFieldKey) + } + } + + $existingRecord = Get-DataverseRecord -conn $conn -EntityLogicalName $entityLogicalName -Id $id -Fields $retrieveFields.ToArray() -ErrorAction SilentlyContinue + + if ($existingRecord.original -eq $null) { + if ($DataverseRecord -ne $null) { + $Fields = @{} + foreach ($DvFieldKey in ($DataverseRecord | Get-Member -MemberType NoteProperty).Name) { + if ($DvFieldKey.EndsWith("_Property")) { + $Fields.Add(($DataverseRecord.$DvFieldKey).Key, ($DataverseRecord.$DvFieldKey).Value) + } + } + } + + if ($Fields[$primaryKeyField] -eq $null) { + $Fields.Add($primaryKeyField, $Id) + } + # if no record exists, then create new + $result = New-DataverseRecord -conn $conn -EntityLogicalName $entityLogicalName -Fields $Fields + + return $result + } + else { + if ($DataverseRecord -ne $null) { + # if record exists, then swap original record so that we can compare updated fields + $DataverseRecord.original = $existingRecord.original + } + } + } + + $newfields = New-Object 'System.Collections.Generic.Dictionary[[String], [Microsoft.PowerPlatform.Dataverse.Client.DataverseDataTypeWrapper]]' + + if ($DataverseRecord -ne $null) { + $originalRecord = $DataverseRecord.original + $Id = $originalRecord[$primaryKeyField] + + foreach ($DvFieldKey in ($DataverseRecord | Get-Member -MemberType NoteProperty).Name) { + $DvFieldValue = $DataverseRecord.($DvFieldKey) + if (($DvFieldKey -eq "original") -or ($DvFieldKey -eq "logicalname") -or ($DvFieldKey -eq "EntityReference")` + -or ($DvFieldKey -like "*_Property") -or ($DvFieldKey -like "ReturnProperty_*")) { + continue + } + elseif ($originalRecord[$DvFieldKey + "_Property"].Value -is [bool]) { + if ($DvFieldValue -is [Int32]) { + if (($originalRecord[$DvFieldKey + "_Property"].Value -and $DvFieldValue -eq 1) -or ` + (!$originalRecord[$DvFieldKey + "_Property"].Value -and $DvFieldValue -eq 0)) { + continue + } + } + elseif ($DvFieldValue -is [bool]) { + if ($DvFieldValue -eq $originalRecord[$DvFieldKey + "_Property"].Value) { + continue + } + } + elseif ($DvFieldValue -eq $originalRecord[$DvFieldKey]) { + continue + } + } + elseif ($originalRecord[$DvFieldKey + "_Property"].Value -is [Microsoft.Xrm.Sdk.OptionSetValue]) { + if ($DvFieldValue -is [Microsoft.Xrm.Sdk.OptionSetValue]) { + if ($DvFieldValue.Value -eq $originalRecord[$DvFieldKey + "_Property"].Value.Value) { + continue + } + } + elseif ($DvFieldValue -is [Int32]) { + if ($DvFieldValue -eq $originalRecord[$DvFieldKey + "_Property"].Value.Value) { + continue + } + } + elseif ($DvFieldValue -eq $originalRecord[$DvFieldKey]) { + continue + } + } + elseif ($originalRecord[$DvFieldKey + "_Property"].Value -is [Microsoft.Xrm.Sdk.Money]) { + if ($DvFieldValue -is [Microsoft.Xrm.Sdk.Money]) { + if ($DvFieldValue.Value -eq $originalRecord[$DvFieldKey + "_Property"].Value.Value) { + continue + } + } + elseif ($DvFieldValue -is [decimal] -or $DvFieldValue -is [Int32]) { + if ($DvFieldValue -eq $originalRecord[$DvFieldKey + "_Property"].Value.Value) { + continue + } + } + elseif ($DvFieldValue -eq $originalRecord[$DvFieldKey]) { + continue + } + } + elseif ($originalRecord[$DvFieldKey + "_Property"].Value -is [Microsoft.Xrm.Sdk.EntityReference]) { + if (($DvFieldValue -is [Microsoft.Xrm.Sdk.EntityReference]) -and ($DvFieldValue.Name -eq $originalRecord[$DvFieldKey].Name)) { + continue + } + elseif ($DvFieldValue -eq $originalRecord[$DvFieldKey]) { + continue + } + } + elseif ($DvFieldValue -eq $originalRecord[$DvFieldKey]) { + continue + } + + $newfield = New-Object -TypeName 'Microsoft.PowerPlatform.Dataverse.Client.DataverseDataTypeWrapper' + $value = New-Object psobject + + # When value set to null, then just use raw type and set value to $null + if ($DvFieldValue -eq $null) { + $newfield.Type = [Microsoft.PowerPlatform.Dataverse.Client.DataverseFieldType]::Raw + $value = $null + } + else { + if ($DataverseRecord.($DvFieldKey + "_Property") -ne $null) { + $type = $DataverseRecord.($DvFieldKey + "_Property").Value.GetType().Name + } + else { + $type = $DvFieldValue.GetType().Name + } + switch ($type) { + "Boolean" { + $newfield.Type = [Microsoft.PowerPlatform.Dataverse.Client.DataverseFieldType]::Boolean + if ($DvFieldValue -is [Boolean]) { + $value = $DvFieldValue + } + else { + $value = [Int32]::Parse($DvFieldValue) + } + break + } + "DateTime" { + $newfield.Type = [Microsoft.PowerPlatform.Dataverse.Client.DataverseFieldType]::DateTime + if ($DvFieldValue -is [DateTime]) { + $value = $DvFieldValue + } + else { + $value = [DateTime]::Parse($DvFieldValue) + } + break + } + "Decimal" { + $newfield.Type = [Microsoft.PowerPlatform.Dataverse.Client.DataverseFieldType]::Decimal + if ($DvFieldValue -is [Decimal]) { + $value = $DvFieldValue + } + else { + $value = [Decimal]::Parse($DvFieldValue) + } + break + } + "Single" { + $newfield.Type = [Microsoft.PowerPlatform.Dataverse.Client.DataverseFieldType]::Float + if ($DvFieldValue -is [Single]) { + $value = $DvFieldValue + } + else { + $value = [Single]::Parse($DvFieldValue) + } + break + } + "Money" { + $newfield.Type = [Microsoft.PowerPlatform.Dataverse.Client.DataverseFieldType]::Raw + if ($DvFieldValue -is [Microsoft.Xrm.Sdk.Money]) { + $value = $DvFieldValue + } + else { + $value = New-Object -TypeName 'Microsoft.Xrm.Sdk.Money' + $value.Value = $DvFieldValue + } + break + } + "Int32" { + $newfield.Type = [Microsoft.PowerPlatform.Dataverse.Client.DataverseFieldType]::Number + if ($DvFieldValue -is [Int32]) { + $value = $DvFieldValue + } + else { + $value = [Int32]::Parse($DvFieldValue) + } + break + } + "EntityReference" { + $newfield.Type = [Microsoft.PowerPlatform.Dataverse.Client.DataverseFieldType]::Raw + $value = $DvFieldValue + break + } + "OptionSetValue" { + $newfield.Type = [Microsoft.PowerPlatform.Dataverse.Client.DataverseFieldType]::Raw + if ($DvFieldValue -is [Microsoft.Xrm.Sdk.OptionSetValue]) { + $value = $DvFieldValue + } + else { + $value = New-Object -TypeName 'Microsoft.Xrm.Sdk.OptionSetValue' + $value.Value = [Int32]::Parse($DvFieldValue) + } + break + } + "String" { + $newfield.Type = [Microsoft.PowerPlatform.Dataverse.Client.DataverseFieldType]::String + $value = $DvFieldValue + break + } + default { + $newfield.Type = [Microsoft.PowerPlatform.Dataverse.Client.DataverseFieldType]::Raw + $value = $DvFieldValue + break + } + } + } + $newfield.Value = $value + $newfields.Add($DvFieldKey, $newfield) + } + } + else { + foreach ($field in $Fields.GetEnumerator()) { + $newfield = New-Object -TypeName 'Microsoft.PowerPlatform.Dataverse.Client.DataverseDataTypeWrapper' + if ($field.value -eq $null) { + $newfield.Type = [Microsoft.PowerPlatform.Dataverse.Client.DataverseFieldType]::Raw + } + else { + $newfield.Type = MapFieldTypeByFieldValue -Value $field.Value + } + $newfield.Value = $field.Value + $newfields.Add($field.Key, $newfield) + } + } + try { + # if no field has new value, then do nothing. + if ($newfields.Count -eq 0) { + return + } + $result = [Microsoft.PowerPlatform.Dataverse.Client.Extensions.CRUDExtentions]::UpdateEntity($conn, $entityLogicalName, $primaryKeyField, $Id, $newfields, $null, $false, [Guid]::Empty) + if (!$result) { + throw LastConnectorException($conn) + } + } + catch { + #TODO: Throw Exceptions back to user + throw LastConnectorException($conn) + } +} + +#DeleteEntity +function Remove-DataverseRecord { + # .ExternalHelp Microsoft.Xrm.Data.PowerShell.Help.xml + [CmdletBinding()] + PARAM( + [parameter(Mandatory = $false)] + [Microsoft.PowerPlatform.Dataverse.Client.ServiceClient]$conn, + [parameter(Mandatory = $true, Position = 1, ParameterSetName = "DataverseRecord", ValueFromPipeline = $True)] + [PSObject]$DataverseRecord, + [parameter(Mandatory = $true, Position = 1, ParameterSetName = "Fields")] + [string]$EntityLogicalName, + [parameter(Mandatory = $true, Position = 2, ParameterSetName = "Fields")] + [guid]$Id + ) + + begin { + $conn = VerifyConnectionParam -conn $conn -pipelineValue ($PSBoundParameters.ContainsKey('conn')) + } + process { + if ($DataverseRecord -ne $null) { + $EntityLogicalName = $DataverseRecord.logicalname + $Id = $DataverseRecord.($EntityLogicalName + "id") + } + + try { + $result = [Microsoft.PowerPlatform.Dataverse.Client.Extensions.CRUDExtentions]::DeleteEntity($conn, $EntityLogicalName, $Id, [Guid]::Empty) + if (!$result) { + throw LastConnectorException($conn) + } + } + catch { + throw LastConnectorException($conn) + } + } +} + +function Get-DataverseEntityMetadata { + # .ExternalHelp Microsoft.Xrm.Data.PowerShell.Help.xml + [CmdletBinding()] + PARAM( + [parameter(Mandatory = $false)] + [Microsoft.PowerPlatform.Dataverse.Client.ServiceClient]$conn, + [parameter(Mandatory = $true, Position = 1)] + [string]$EntityLogicalName, + [parameter(Mandatory = $false, Position = 2)] + [string]$EntityFilters + ) + + $conn = VerifyConnectionParam -conn $conn -pipelineValue ($PSBoundParameters.ContainsKey('conn')) + + switch ($EntityFilters.ToLower()) { + "all" { + $filter = [Microsoft.Xrm.Sdk.Metadata.EntityFilters]::All + break + } + "attributes" { + $filter = [Microsoft.Xrm.Sdk.Metadata.EntityFilters]::Attributes + break + } + "entity" { + $filter = [Microsoft.Xrm.Sdk.Metadata.EntityFilters]::Entity + break + } + "privileges" { + $filter = [Microsoft.Xrm.Sdk.Metadata.EntityFilters]::Privileges + break + } + "relationships" { + $filter = [Microsoft.Xrm.Sdk.Metadata.EntityFilters]::Relationships + break + } + default { + $filter = [Microsoft.Xrm.Sdk.Metadata.EntityFilters]::Default + break + } + } + + try { + $result = [Microsoft.PowerPlatform.Dataverse.Client.Extensions.MetadataExtensions]::GetEntityMetadata($conn, $EntityLogicalName, $filter) + if ($result -eq $null) { + throw LastConnectorException($conn) + } + } + catch { + throw LastConnectorException($conn) + } + + return $result +} + +function Get-DataverseEntityAttributes { + # .ExternalHelp Microsoft.Xrm.Data.PowerShell.Help.xml + [CmdletBinding()] + PARAM( + [parameter(Mandatory = $false)] + [Microsoft.PowerPlatform.Dataverse.Client.ServiceClient]$conn, + [parameter(Mandatory = $true, Position = 1)] + [string]$EntityLogicalName + ) + + $conn = VerifyConnectionParam -conn $conn -pipelineValue ($PSBoundParameters.ContainsKey('conn')) + + try { + $result = [Microsoft.PowerPlatform.Dataverse.Client.Extensions.MetadataExtensions]::GetAllAttributesForEntity($conn, $EntityLogicalName) + if ($result -eq $null) { + throw LastConnectorException($conn) + } + } + catch { + throw LastConnectorException($conn) + } + + return $result +} + +function New-DataverseEntityReference{ + # .ExternalHelp Microsoft.Xrm.Data.PowerShell.Help.xml + [CmdletBinding()] + PARAM( + [parameter(Mandatory=$true, Position=0)] + [string]$EntityLogicalName, + [parameter(Mandatory=$true, Position=1)] + [guid]$Id + ) + $DataverseEntityReference = [Microsoft.Xrm.Sdk.EntityReference]::new() + $DataverseEntityReference.LogicalName = $EntityLogicalName + $DataverseEntityReference.Id = $Id + $DataverseEntityReference + return + } + +#GetMyUserId +function Get-DataverseMyUserId{ +# .ExternalHelp Microsoft.Xrm.Data.PowerShell.Help.xml + [CmdletBinding()] + PARAM( + [parameter(Mandatory=$false)] + [Microsoft.PowerPlatform.Dataverse.Client.ServiceClient]$conn + ) + + $conn = VerifyConnectionParam -conn $conn -pipelineValue ($PSBoundParameters.ContainsKey('conn')) + + try + { + $result = [Microsoft.PowerPlatform.Dataverse.Client.Extensions.GeneralExtensions]::GetMyUserId($conn) + if($result -eq $null) + { + throw LastConnectorException($conn) + } + } + catch + { + throw LastConnectorException($conn) + } + + return $result +} + +#GetEntityDataByFetchSearch +function Get-DataverseRecordsByFetch{ + # .ExternalHelp Microsoft.Xrm.Data.PowerShell.Help.xml + [CmdletBinding()] + PARAM( + [parameter(Mandatory=$false)] + [Microsoft.PowerPlatform.Dataverse.Client.ServiceClient]$conn, + [parameter(Mandatory=$true, Position=1)] + [string]$Fetch, + [parameter(Mandatory=$false, Position=2)] + [int]$TopCount, + [parameter(Mandatory=$false, Position=3)] + [int]$PageNumber, + [parameter(Mandatory=$false, Position=4)] + [string]$PageCookie, + [parameter(Mandatory=$false, Position=5)] + [switch]$AllRows + ) + $conn = VerifyConnectionParam -conn $conn -pipelineValue ($PSBoundParameters.ContainsKey('conn')) + #default page number to 1 if not supplied + if($PageNumber -eq 0) + { + $PageNumber = 1 + } + $PagingCookie = "" + $NextPage = $false + if($PageCookie -eq "") + { + $PageCookie = $null + } + + $recordslist = New-Object "System.Collections.Generic.List[System.Management.Automation.PSObject]" + $fetchQueryTime = New-TimeSpan -Seconds 0 + $crmFetchTimer = [System.Diagnostics.Stopwatch]::StartNew() + try + { + $xml = [xml]$Fetch + if($xml.fetch.count -ne 0 -and $TopCount -eq 0) + { + $TopCount = $xml.fetch.count + } + + $logicalName = $xml.SelectSingleNode("/fetch/entity").Name + + do { + Write-Debug "Fetching Page $PageNumber" + $crmFetchTimer.Restart() + $records = [Microsoft.PowerPlatform.Dataverse.Client.Extensions.QueryExtensions]::GetEntityDataByFetchSearch($conn,$Fetch, $TopCount, $PageNumber, $PageCookie, [ref]$PagingCookie, [ref]$NextPage, [Guid]::Empty) + $fetchQueryTime += $crmFetchTimer.Elapsed + + if($conn.LastException) + { + throw LastConnectorException($conn) + } + + $recordsList.AddRange([System.Collections.Generic.List[System.Management.Automation.PSObject]](parseRecordsPage -records $records -logicalname $logicalName -xml $xml -Verbose)) + + $PageNumber = $PageNumber + 1 + } while ($NextPage -and $AllRows) + } + catch + { + Write-Error $_.Exception + throw LastConnectorException($conn) + } + + $resultSet = New-Object 'System.Collections.Generic.Dictionary[[System.String],[System.Management.Automation.PSObject]]' + $resultSet.Add("Records", $recordslist) + $resultSet.Add("Count", $recordslist.Count) + $resultSet.Add("PagingCookie",$PagingCookie) + $resultSet.Add("NextPage",$NextPage) + $resultSet.Add("FetchXml", $Fetch) + $resultSet.Add("FetchQueryTime", $fetchQueryTime) + Write-Verbose "FetchQueryTime:$fetchQueryTime" + $resultSet +} + +function Get-DataverseOrgDbOrgSettings{ +# .ExternalHelp Microsoft.Xrm.Data.PowerShell.Help.xml + + [CmdletBinding()] + PARAM( + [parameter(Mandatory=$false)] + [Microsoft.PowerPlatform.Dataverse.Client.ServiceClient]$conn + ) + + $conn = VerifyConnectionParam -conn $conn -pipelineValue ($PSBoundParameters.ContainsKey('conn')) + + $fetch = @" + + + + + +"@ + $result = Get-DataverseRecordsByFetch -conn $conn -Fetch $fetch + $record = $result.Records[0] + + if($record.orgdborgsettings -eq $null) + { + Write-Warning 'No settings found.' + } + else + { + $xml = [xml]$record.orgdborgsettings + return $xml.SelectSingleNode("/OrgSettings") + } +} + +function Invoke-DataverseAction { +# .ExternalHelp Microsoft.Xrm.Data.PowerShell.Help.xml + [OutputType([hashtable])] + [OutputType([Microsoft.Xrm.Sdk.OrganizationResponse], ParameterSetName="Raw")] + param ( + [Microsoft.PowerPlatform.Dataverse.Client.ServiceClient] + $conn, + + [Parameter( + Position=1, + Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string] + $Name, + + [Parameter(Position=2)] + [hashtable] + $Parameters, + + [Parameter(ValueFromPipeline, Position=3)] + [ValidateNotNullOrEmpty()] + [Microsoft.Xrm.Sdk.EntityReference] + $Target, + + [Parameter(ParameterSetName="Raw")] + [switch] + $Raw + ) + begin + { + $conn = VerifyConnectionParam -conn $conn -pipelineValue ($PSBoundParameters.ContainsKey('conn')) + } + process + { + $request = new-object Microsoft.Xrm.Sdk.OrganizationRequest + $request.RequestName = $Name + if($Target) { + $request.Parameters.Add("Target", $Target) + } + + if($Parameters) { + foreach($parameter in $Parameters.GetEnumerator()) { + $request.Parameters.Add($parameter.Name, $parameter.Value) + } + } + + try { + $response = $conn.Execute($request) + + if($Raw) { + Write-Output $response + } elseif ($response.Results -and $response.Results.Count -gt 0) { + $outputArguments = @{} + foreach($outputArgument in $response.Results) { + $outputArguments.Add($outputArgument.Key, $outputArgument.Value) + } + Write-Output $outputArguments + } else { + Write-Output $null + } + } + catch { + Write-Error $_ + } + } +} + +function Get-DataverseSystemSettings{ +# .ExternalHelp Microsoft.Xrm.Data.PowerShell.Help.xml + + [CmdletBinding()] + PARAM( + [parameter(Mandatory=$false)] + [Microsoft.PowerPlatform.Dataverse.Client.ServiceClient]$conn, + [parameter(Mandatory=$false)] + [switch]$ShowDisplayName + ) + + $conn = VerifyConnectionParam -conn $conn -pipelineValue ($PSBoundParameters.ContainsKey('conn')) + + $fetch = @" + + + + + +"@ + + $record = (Get-DataverseRecordsByFetch -conn $conn -Fetch $fetch).Records[0] + + $attributes = Get-DataverseEntityAttributes -conn $conn -EntityLogicalName organization + + $psobj = New-Object -TypeName System.Management.Automation.PSObject + + foreach($att in $record.original.GetEnumerator()) + { + if(($att.Key.Contains("Property")) -or ($att.Key -eq "organizationid") -or ($att.Key.StartsWith("ReturnProperty_")) -or ($att.Key -eq "logicalname") -or ($att.Key -eq "original")) + { + continue + } + if($att.Key -eq "defaultemailsettings") + { + if($ShowDisplayName) + { + $name = ($attributes | where {$_.LogicalName -eq $att.Key}).Displayname.UserLocalizedLabel.Label + ":" +((Get-DataverseEntityOptionSet -conn $conn mailbox incomingemaildeliverymethod).DisplayValue) + } + else + { + $name = "defaultemailsettings:incomingemaildeliverymethod" + } + Add-Member -InputObject $psobj -MemberType NoteProperty -Name $name -Value ((Get-DataverseEntityOptionSet -conn $conn mailbox incomingemaildeliverymethod).Items.DisplayLabel)[([xml]$att.Value).FirstChild.IncomingEmailDeliveryMethod] + if($ShowDisplayName) + { + $name = ($attributes | where {$_.LogicalName -eq $att.Key}).Displayname.UserLocalizedLabel.Label + ":" +((Get-DataverseEntityOptionSet -conn $conn mailbox outgoingemaildeliverymethod).DisplayValue) + } + else + { + $name = "defaultemailsettings:outgoingemaildeliverymethod" + } + Add-Member -InputObject $psobj -MemberType NoteProperty -Name $name -Value ((Get-DataverseEntityOptionSet -conn $conn mailbox outgoingemaildeliverymethod).Items.DisplayLabel)[([xml]$att.Value).FirstChild.OutgoingEmailDeliveryMethod] + if($ShowDisplayName) + { + $name = ($attributes | where {$_.LogicalName -eq $att.Key}).Displayname.UserLocalizedLabel.Label + ":" +((Get-DataverseEntityOptionSet -conn $conn mailbox actdeliverymethod).DisplayValue) + } + else + { + $name = "defaultemailsettings:actdeliverymethod" + } + Add-Member -InputObject $psobj -MemberType NoteProperty -Name $name -Value ((Get-DataverseEntityOptionSet -conn $conn mailbox actdeliverymethod).Items.DisplayLabel)[([xml]$att.Value).FirstChild.ACTDeliveryMethod] + continue + } + + if($ShowDisplayName) + { + $name = ($attributes | where {$_.LogicalName -eq $att.Key}).Displayname.UserLocalizedLabel.Label + if($name -eq $null) + { + $name = ($attributes | where {$_.LogicalName -eq $att.Key}).SchemaName + } + } + else + { + $name = ($attributes | where {$_.LogicalName -eq $att.Key}).SchemaName + } + + if($name -eq $null){ + Write-Warning "SKIPPING Property: $($att.Key)" + } + else{ + Add-Member -InputObject $psobj -MemberType NoteProperty -Name $name -Value $record.($att.Key) -Force + } + } + + return $psobj +} + +function Set-DataverseSystemSettings { +# .ExternalHelp Microsoft.Xrm.Data.PowerShell.Help.xml + [CmdletBinding()] + PARAM( + [parameter(Mandatory=$false)] + [Microsoft.PowerPlatform.Dataverse.Client.ServiceClient]$conn, + [parameter(Mandatory=$false)] + [guid]$AcknowledgementTemplateId, + [parameter(Mandatory=$false)] + [int]$ACTDeliveryMethod, + [parameter(Mandatory=$false)] + [bool]$AllowAddressBookSyncs, + [parameter(Mandatory=$false)] + [bool]$AllowAutoResponseCreation, + [parameter(Mandatory=$false)] + [bool]$AllowAutoUnsubscribe, + [parameter(Mandatory=$false)] + [bool]$AllowAutoUnsubscribeAcknowledgement, + [parameter(Mandatory=$false)] + [bool]$AllowClientMessageBarAd, + [parameter(Mandatory=$false)] + [bool]$AllowEntityOnlyAudit, + [parameter(Mandatory=$false)] + [bool]$AllowMarketingEmailExecution, + [parameter(Mandatory=$false)] + [bool]$AllowOfflineScheduledSyncs, + [parameter(Mandatory=$false)] + [bool]$AllowOutlookScheduledSyncs, + [parameter(Mandatory=$false)] + [bool]$AllowUnresolvedPartiesOnEmailSend, + [parameter(Mandatory=$false)] + [bool]$AllowUserFormModePreference, + [parameter(Mandatory=$false)] + [bool]$AllowUsersSeeAppdownloadMessage, + [parameter(Mandatory=$false)] + [bool]$AllowWebExcelExport, + [parameter(Mandatory=$false)] + [string]$AMDesignator, + [parameter(Mandatory=$false)] + [bool]$AutoApplyDefaultonCaseCreate, + [parameter(Mandatory=$false)] + [bool]$AutoApplyDefaultonCaseUpdate, + [parameter(Mandatory=$false)] + [bool]$AutoApplySLA, + [parameter(Mandatory=$false)] + [string]$BingMapsApiKey, + [parameter(Mandatory=$false)] + [string]$BlockedAttachments, + [parameter(Mandatory=$false)] + [guid]$BusinessClosureCalendarId, + [parameter(Mandatory=$false)] + [string]$CampaignPrefix, + [parameter(Mandatory=$false)] + [bool]$CascadeStatusUpdate, + [parameter(Mandatory=$false)] + [string]$CasePrefix, + [parameter(Mandatory=$false)] + [string]$ContractPrefix, + [parameter(Mandatory=$false)] + [bool]$CortanaProactiveExperienceEnabled, + [parameter(Mandatory=$false)] + [bool]$CreateProductsWithoutParentInActiveState, + [parameter(Mandatory=$false)] + [int]$CurrencyDecimalPrecision, + [parameter(Mandatory=$false)] + [int]$CurrencyDisplayOption, + [parameter(Mandatory=$false)] + [int]$CurrentCampaignNumber, + [parameter(Mandatory=$false)] + [int]$CurrentCaseNumber, + [parameter(Mandatory=$false)] + [int]$CurrentContractNumber, + [parameter(Mandatory=$false)] + [int]$CurrentInvoiceNumber, + [parameter(Mandatory=$false)] + [int]$CurrentKbNumber, + [parameter(Mandatory=$false)] + [int]$CurrentOrderNumber, + [parameter(Mandatory=$false)] + [int]$CurrentQuoteNumber, + [parameter(Mandatory=$false)] + [ValidatePattern('\+{1}\d{1,}')] + [string]$DefaultCountryCode, + [parameter(Mandatory=$false)] + [guid]$DefaultEmailServerProfileId, + [parameter(Mandatory=$false)] + [bool]$DisableSocialCare, + [parameter(Mandatory=$false)] + [bool]$DisplayNavigationTour, + [parameter(Mandatory=$false)] + [int]$EmailConnectionChannel, + [parameter(Mandatory=$false)] + [int]$EmailCorrelationEnabled, + [parameter(Mandatory=$false)] + [bool]$EnableBingMapsIntegration, + [parameter(Mandatory=$false)] + [bool]$EnableSmartMatching, + [parameter(Mandatory=$false)] + [int]$FullNameConventionCode, + [parameter(Mandatory=$false)] + [bool]$GenerateAlertsForErrors, + [parameter(Mandatory=$false)] + [bool]$GenerateAlertsForWarnings, + [parameter(Mandatory=$false)] + [bool]$GenerateAlertsForInformation, + [parameter(Mandatory=$false)] + [bool]$GlobalAppendUrlParametersEnabled, + [parameter(Mandatory=$false)] + #[ValidatePattern('http(s)?://([\w-]+\.)+[\w-]+(/[\w- ./?%&=]*)?')] + [string]$GlobalHelpUrl, + [parameter(Mandatory=$false)] + [bool]$GlobalHelpUrlEnabled, + [parameter(Mandatory=$false)] + [int]$HashDeltaSubjectCount, + [parameter(Mandatory=$false)] + [string]$HashFilterKeywords, + [parameter(Mandatory=$false)] + [int]$HashMaxCount, + [parameter(Mandatory=$false)] + [int]$HashMinAddressCount, + [parameter(Mandatory=$false)] + [bool]$IgnoreInternalEmail, + [parameter(Mandatory=$false)] + [int]$IncomingEmailDeliveryMethod, + [parameter(Mandatory=$false)] + [string]$InvoicePrefix, + [parameter(Mandatory=$false)] + [bool]$IsAutoSaveEnabled, + [parameter(Mandatory=$false)] + [bool]$IsDefaultCountryCodeCheckEnabled, + [parameter(Mandatory=$false)] + [bool]$IsDuplicateDetectionEnabled, + [parameter(Mandatory=$false)] + [bool]$IsDuplicateDetectionEnabledForImport, + [parameter(Mandatory=$false)] + [bool]$IsDuplicateDetectionEnabledForOfflineSync, + [parameter(Mandatory=$false)] + [bool]$IsDuplicateDetectionEnabledForOnlineCreateUpdate, + [parameter(Mandatory=$false)] + [bool]$isenabledforallroles, + [parameter(Mandatory=$false)] + [bool]$IsFolderBasedTrackingEnabled, + [parameter(Mandatory=$false)] + [bool]$IsFullTextSearchEnabled, + [parameter(Mandatory=$false)] + [bool]$IsHierarchicalSecurityModelEnabled, + [parameter(Mandatory=$false)] + [bool]$IsPresenceEnabled, + [parameter(Mandatory=$false)] + [bool]$IsUserAccessAuditEnabled, + [parameter(Mandatory=$false)] + [string]$KbPrefix, + [parameter(Mandatory=$false)] + [int]$MaxAppointmentDurationDays, + [parameter(Mandatory=$false)] + [int]$MaxDepthForHierarchicalSecurityModel, + [parameter(Mandatory=$false)] + [int]$MaximumActiveBusinessProcessFlowsAllowedPerEntity, + [parameter(Mandatory=$false)] + [int]$MaximumDynamicPropertiesAllowed, + [parameter(Mandatory=$false)] + [int]$MaximumTrackingNumber, + [parameter(Mandatory=$false)] + [int]$MaxProductsInBundle, + [parameter(Mandatory=$false)] + [int]$MaxRecordsForExportToExcel, + [parameter(Mandatory=$false)] + [int]$MaxRecordsForLookupFilters, + [parameter(Mandatory=$false)] + [int]$MaxUploadFileSize, + [parameter(Mandatory=$false)] + [int]$MinAddressBookSyncInterval, + [parameter(Mandatory=$false)] + [int]$MinOfflineSyncInterval, + [parameter(Mandatory=$false)] + [int]$MinOutlookSyncInterval, + [parameter(Mandatory=$false)] + [bool]$NotifyMailboxOwnerOfEmailServerLevelAlerts, + [parameter(Mandatory=$false)] + [string]$OrderPrefix, + [parameter(Mandatory=$false)] + [int]$OutgoingEmailDeliveryMethod, + [parameter(Mandatory=$false)] + [ValidateSet(0,1,2)] + [int]$PluginTraceLogSetting, + [parameter(Mandatory=$false)] + [ValidateSet(0,1,2,3,4)] + [int]$PricingDecimalPrecision, + [parameter(Mandatory=$false)] + [bool]$QuickFindRecordLimitEnabled, + [parameter(Mandatory=$false)] + [string]$QuotePrefix, + [parameter(Mandatory=$false)] + [bool]$RequireApprovalForUserEmail, + [parameter(Mandatory=$false)] + [bool]$RequireApprovalForQueueEmail, + [parameter(Mandatory=$false)] + [bool]$ShareToPreviousOwnerOnAssign, + [parameter(Mandatory=$false)] + [string]$TrackingPrefix, + [parameter(Mandatory=$false)] + [int]$TrackingTokenIdBase, + [parameter(Mandatory=$false)] + [int]$TrackingTokenIdDigits, + [parameter(Mandatory=$false)] + [int]$UniqueSpecifierLength, + [parameter(Mandatory=$false)] + [bool]$UseLegacyRendering, + [parameter(Mandatory=$false)] + [bool]$UsePositionHierarchy, + [parameter(Mandatory=$false)] + [bool]$UseSkypeProtocol, + [parameter(Mandatory=$false)] + [bool]$UseAllowUsersSeeAppdownloadMessage, + [parameter(Mandatory=$false)] + [string]$DefaultCrmCustomName, + [parameter(Mandatory=$false)] + [bool]$SuppressSLA, + [parameter(Mandatory=$false)] + [bool]$IsAuditEnabled, + [parameter(Mandatory=$false)] + [bool]$AllowLegacyClientExperience + ) + + $conn = VerifyConnectionParam -conn $conn -pipelineValue ($PSBoundParameters.ContainsKey('conn')) + + $updateFields = @{} + + $attributesMetadata = Get-DataverseEntityAttributes -conn $conn -EntityLogicalName organization + + $defaultEmailSettings = @{} + + foreach($parameter in $MyInvocation.BoundParameters.GetEnumerator()) + { + $attributeMetadata = $attributesMetadata | ? {$_.SchemaName -eq $parameter.Key} + + if($parameter.Key -in ("IncomingEmailDeliveryMethod","OutgoingEmailDeliveryMethod","ACTDeliveryMethod")) + { + $defaultEmailSettings.Add($parameter.Key,$parameter.Value) + } + elseif($attributeMetadata -eq $null) + { + continue + } + elseif($attributeMetadata.AttributeType -eq "Picklist") + { + $updateFields.Add($parameter.Key.ToLower(), (New-DataverseOptionSetValue $parameter.Value)) + } + elseif($attributeMetadata.AttributeType -eq "Lookup") + { + $updateFields.Add($parameter.Key.ToLower(), (New-DataverseEntityReference emailserverprofile $parameter.Value)) + } + else + { + $updateFields.Add($parameter.Key.ToLower(), $parameter.Value) + } + } + + $fetch = @" + + + + + + +"@ + + $systemSettings = (Get-DataverseRecordsByFetch -conn $conn -Fetch $fetch).Records[0] + $recordid = $systemSettings.organizationid + + if($defaultEmailSettings.Count -ne 0) + { + $emailSettings = [xml]$systemSettings.defaultemailsettings + if($defaultEmailSettings.ContainsKey("IncomingEmailDeliveryMethod")) + { + $emailSettings.EmailSettings.IncomingEmailDeliveryMethod = [string]$defaultEmailSettings["IncomingEmailDeliveryMethod"] + } + if($defaultEmailSettings.ContainsKey("OutgoingEmailDeliveryMethod")) + { + $emailSettings.EmailSettings.OutgoingEmailDeliveryMethod = [string]$defaultEmailSettings["OutgoingEmailDeliveryMethod"] + } + if($defaultEmailSettings.ContainsKey("ACTDeliveryMethod")) + { + $emailSettings.EmailSettings.ACTDeliveryMethod = [string]$defaultEmailSettings["ACTDeliveryMethod"] + } + + $updateFields.Add("defaultemailsettings",$emailSettings.OuterXml) + } + + Set-DataverseRecord -conn $conn -EntityLogicalName organization -Id $recordid -Fields $updateFields +} + +#GetPickListElementFromMetadataEntity +function Get-DataverseEntityOptionSet{ +# .ExternalHelp Microsoft.Xrm.Data.PowerShell.Help.xml + [CmdletBinding()] + PARAM( + [parameter(Mandatory=$false)] + [Microsoft.PowerPlatform.Dataverse.Client.ServiceClient]$conn, + [parameter(Mandatory=$true, Position=1)] + [string]$EntityLogicalName, + [parameter(Mandatory=$true, Position=2)] + [string]$FieldLogicalName + ) + $conn = VerifyConnectionParam -conn $conn -pipelineValue ($PSBoundParameters.ContainsKey('conn')) + try + { + $result = [Microsoft.PowerPlatform.Dataverse.Client.Extensions.MetadataExtensions]::GetPickListElementFromMetadataEntity($conn, $EntityLogicalName, $FieldLogicalName) + if($result -eq $null) + { + throw LastConnectorException($conn) + } + } + catch + { + throw LastConnectorException($conn) + } + + return $result +} + + +#UtilityFunctions +function MapFieldTypeByFieldValue { + PARAM( + [Parameter(Mandatory = $true)] + [object]$Value + ) + + $valueTypeToDvTypeMapping = @{ + "Boolean" = [Microsoft.PowerPlatform.Dataverse.Client.DataverseFieldType]::Boolean; + "DateTime" = [Microsoft.PowerPlatform.Dataverse.Client.DataverseFieldType]::DateTime; + "Decimal" = [Microsoft.PowerPlatform.Dataverse.Client.DataverseFieldType]::Decimal; + "Single" = [Microsoft.PowerPlatform.Dataverse.Client.DataverseFieldType]::Float; + "Money" = [Microsoft.PowerPlatform.Dataverse.Client.DataverseFieldType]::Raw; + "Int32" = [Microsoft.PowerPlatform.Dataverse.Client.DataverseFieldType]::Number; + "EntityReference" = [Microsoft.PowerPlatform.Dataverse.Client.DataverseFieldType]::Raw; + "OptionSetValue" = [Microsoft.PowerPlatform.Dataverse.Client.DataverseFieldType]::Raw; + "String" = [Microsoft.PowerPlatform.Dataverse.Client.DataverseFieldType]::String; + "Guid" = [Microsoft.PowerPlatform.Dataverse.Client.DataverseFieldType]::UniqueIdentifier; + } + + # default is RAW + $DvDataType = [Microsoft.PowerPlatform.Dataverse.Client.DataverseFieldType]::Raw + + if ($Value -ne $null) { + + $valueType = $Value.GetType().Name + + if ($valueTypeToDvTypeMapping.ContainsKey($valueType)) { + $DvDataType = $valueTypeToDvTypeMapping[$valueType] + } + } + + return $DvDatatype +} + +function New-DataverseOptionSetValue{ +# .ExternalHelp Microsoft.Xrm.Data.PowerShell.Help.xml + [CmdletBinding()] + PARAM( + [parameter(Mandatory=$true, Position=0)] + [int]$Value + ) + $crmOptionSetValue = [Microsoft.Xrm.Sdk.OptionSetValue]::new() + $crmOptionSetValue.Value = $Value + $crmOptionSetValue + return +} + +function GuessPrimaryKeyField() { + PARAM( + [Parameter(Mandatory = $true)] + [object]$EntityLogicalName + ) + + $standardActivityEntities = @( + "opportunityclose", + "socialactivity", + "campaignresponse", + "letter", "orderclose", + "appointment", + "recurringappointmentmaster", + "fax", + "email", + "activitypointer", + "incidentresolution", + "bulkoperation", + "quoteclose", + "task", + "campaignactivity", + "serviceappointment", + "phonecall" + ) + # Some Entity has different pattern for id name. + if ($EntityLogicalName -eq "usersettings") { + $primaryKeyField = "systemuserid" + } + elseif ($EntityLogicalName -eq "systemform") { + $primaryKeyField = "formid" + } + elseif ($EntityLogicalName -in $standardActivityEntities) { + $primaryKeyField = "activityid" + } + else { + # default + $primaryKeyField = $EntityLogicalName + "id" + } + + $primaryKeyField +} + +function parseRecordsPage { + PARAM( + [parameter(Mandatory=$true)] + [object]$records, + [parameter(Mandatory=$true)] + [string] $logicalname, + [parameter(Mandatory=$true)] + [xml] $xml + ) + $recordslist = New-Object 'System.Collections.Generic.List[System.Management.Automation.PSObject]' + foreach($record in $records.Values){ + $null = $record.Add("original",$record) + $null = $record.Add("logicalname",$logicalname) + if($record.ContainsKey("ReturnProperty_Id ")) + { + $null = $record.Add("ReturnProperty_Id",$record.'ReturnProperty_Id ') + $null = $record.Remove("ReturnProperty_Id ") + } + #add entityReferences values as values + ForEach($attribute in $record.Keys|Select) + { + if(-not $attribute.EndsWith("_Property")) { continue } + + #if aliased value BUT if it's an EntityRef... then ignore it + if($record[$attribute].Value -is [Microsoft.Xrm.Sdk.AliasedValue]) + { + if($record[$attribute].Value.Value -isnot [Microsoft.Xrm.Sdk.EntityReference]) + { + $attName = $attribute.Replace("_Property","") + $record[$attName] = $record[$attribute].Value.Value + } + } + + if($record[$attribute].Value -is [Microsoft.Xrm.Sdk.EntityReference]) + { + $attName = $attribute.Replace("_Property","") + $record[$attName] = $record[$attribute].Value.Name + } + } + + $hashtable = $record -as [Hashtable] + + #adding Dynamic EntityReference + if ($hashtable.ReturnProperty_Id -and $hashtable.ReturnProperty_EntityName) { + $hashtable.EntityReference = New-DataverseEntityReference -EntityLogicalName $hashtable.ReturnProperty_EntityName -Id $hashtable.ReturnProperty_Id + } + + $recordslist.Add([pscustomobject]$hashtable) + } + $recordslist +} + +function Coalesce { + foreach($i in $args){ + if($i -ne $null){ + return $i + } + } +} + + + diff --git a/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell.psd1 b/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell.psd1 new file mode 100644 index 0000000..9d094e4 --- /dev/null +++ b/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell.psd1 @@ -0,0 +1,136 @@ +# +# Module manifest for module 'Microsoft.PowerPlatform.Dataverse.Client.PowerShell' +# +# Generated by: Microsoft Power Platform Team +# + +@{ + + # Script module or binary module file associated with this manifest. + RootModule = 'Microsoft.PowerPlatform.Dataverse.Client.PowerShell.psm1' + + # Version number of this module. + ModuleVersion = '1.1.0.0' + + # ID used to uniquely identify this module + GUID = '996701E3-B7C8-4A91-AC14-BB27918350DA' + + # Author of this module + Author = 'Microsoft Power Platform Team' + + # Company or vendor of this module + CompanyName = 'Microsoft' + + # Copyright statement for this module + Copyright = 'Copyright 2023. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'PowerShell Module for Connecting to Power Platform Dataverse' + + # Minimum version of the Windows PowerShell engine required by this module + PowerShellVersion = '7.0' + + # Supported PSEditions: 'Desktop', 'Core' + CompatiblePSEditions = @('Core') + + # Modules that must be imported into the global environment prior to importing this module + #RequiredModules = + + # Assemblies that must be loaded prior to importing this module + RequiredAssemblies = @( + 'Microsoft.PowerPlatform.Dataverse.Client.dll', + 'Microsoft.PowerPlatform.Dataverse.Client.PowerShell.dll', + 'Microsoft.Xrm.Sdk.dll', + 'Microsoft.Crm.Sdk.Proxy.dll', + 'Newtonsoft.Json.dll' + ) + + # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess + NestedModules = if ($PSEdition -eq 'Core') { + @( + 'Microsoft.PowerPlatform.Dataverse.Client.PowerShell.dll', + 'UtilityFunctions.psm1', + 'Microsoft.PowerPlatform.Dataverse.Client.Operations', + 'Microsoft.PowerPlatform.Dataverse.Client.Connect' + ) + } + else { # Desktop + @( + #null + ) + } + + # Functions to export from this module + FunctionsToExport = @( + 'New-DataverseRecord', + 'Get-DataverseRecord', + 'Get-DataverseEntityMetadata', + 'Get-MyUserId', + 'Get-DataverseRecordsByFetch', + 'Get-DataverseOrgDbOrgSettings', + 'Get-DataverseEntityOptionSet', + 'Get-DataverseEntityAttributes', + 'Get-DataverseSystemSettings', + 'Set-DataverseRecord', + 'Set-DataverseSystemSettings', + 'Invoke-DataverseAction', + 'Remove-DataverseRecord', + 'Connect-PowerPlatformDataverse' + ) + + # Cmdlets to export from this module + CmdletsToExport = @( + 'Get-PowerPlatformConnection' + ) + + # Variables to export from this module + VariablesToExport = @( + ) + + # Aliases to export from this module + AliasesToExport = @( + ) + + # List of all modules packaged with this module. + ModuleList = @( + 'Microsoft.PowerPlatform.Dataverse.Client.PowerShell' + ) + + # List of all files packaged with this module + #FileList = @() + + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + # Tags = @() + + # A URL to the license for this module. + LicenseUri = 'http://download.microsoft.com/download/E/1/8/E18C0FAD-FEC8-44CD-9A16-98EDC4DAC7A2/LicenseTerms.docx' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/microsoft/PowerPlatform-DataverseServiceClient' + # A URL to an icon representing this module. + IconUri = 'https://connectoricons-prod.azureedge.net/powerappsforappmakers/icon_1.0.1056.1255.png' + + # ReleaseNotes of this module + ReleaseNotes = ' +Current Release: + +>>CURRENTRELEASEID<< + +' + + } # End of PSData hashtable + + } # End of PrivateData hashtable + + # HelpInfo URI of this module + #HelpInfoURI = '' + + # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. + #DefaultCommandPrefix = '' + +} \ No newline at end of file diff --git a/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell.psm1 b/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell.psm1 new file mode 100644 index 0000000..95c8ec1 --- /dev/null +++ b/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell.psm1 @@ -0,0 +1,11 @@ +Push-Location $PSScriptRoot + +$PackageRoot = $PSScriptRoot + +$LoadingModule = $true + +dir *.ps1 | % Name | Resolve-Path | Import-Module + +$LoadingModule = $false + +Pop-Location diff --git a/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/UtilityFunctions.psm1 b/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/UtilityFunctions.psm1 new file mode 100644 index 0000000..1a5240e --- /dev/null +++ b/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/UtilityFunctions.psm1 @@ -0,0 +1,43 @@ +function Coalesce { + foreach ($i in $args) { + if ($i -ne $null) { + return $i + } + } +} + +function LastConnectorException { + [CmdletBinding()] + PARAM( + [parameter(Mandatory = $true)] + [Microsoft.PowerPlatform.Dataverse.Client.ServiceClient]$conn + ) + + return (Coalesce $conn.LastError $conn.LastException) +} + +function VerifyConnectionParam { + [CmdletBinding()] + PARAM( + [parameter(Mandatory=$false)] + [Microsoft.PowerPlatform.Dataverse.Client.ServiceClient]$conn, + [parameter(Mandatory=$false)] + [bool]$pipelineValue + ) + #we have a $conn value and we were not given a $conn value so we should try to find one + if($conn -eq $null -and $pipelineValue -eq $false) + { + $connobj = Get-Variable conn -Scope global -ErrorAction SilentlyContinue + if($connobj.Value -eq $null) + { + throw 'A connection to Dataverse is required, use Get-PowerPlatformConnection or one of the other connection functions to connect.' + } + else + { + $conn = $connobj.Value + } + }elseif($conn -eq $null -and $pipelineValue -eq $true){ + throw "Connection object provided is null" + } + return $conn +} \ No newline at end of file diff --git a/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/RegisterForSession.ps1 b/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/RegisterForSession.ps1 new file mode 100644 index 0000000..8d20174 --- /dev/null +++ b/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/RegisterForSession.ps1 @@ -0,0 +1,8 @@ +<# +This script will load in a PowerShell command shell and import the module developed in the project. To clean up, exit this shell. +#> + +# Load the module. +$env:PSModulePath = (Resolve-Path .).Path + ";" + $env:PSModulePath +Import-Module 'Microsoft.PowerPlatform.Dataverse.Client.PowerShell' -Verbose + diff --git a/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/RegisterServiceClient.ps1 b/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/RegisterServiceClient.ps1 new file mode 100644 index 0000000..9cf8087 --- /dev/null +++ b/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/RegisterServiceClient.ps1 @@ -0,0 +1,9 @@ +<# +This script will load in a PowerShell command shell and import the module developed in the project. To clean up, exit this shell. +#> + +# Load the module. +$env:PSModulePath = (Resolve-Path .).Path + ";" + $env:PSModulePath +Import-Module 'Microsoft.PowerPlatform.Dataverse.Client.PowerShell' -Verbose + + diff --git a/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Start-Debug.ps1 b/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Start-Debug.ps1 new file mode 100644 index 0000000..a968d19 --- /dev/null +++ b/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/Start-Debug.ps1 @@ -0,0 +1,21 @@ +<# +This script will run on debug. +It will load in a PowerShell command shell and import the module developed in the project. To end debug, exit this shell. +#> + +cd Drop + +# Write a reminder on how to end debugging. +$message = "| Exit this shell to end the debug session! |" +$line = "-" * $message.Length +$color = "Cyan" +Write-Host -ForegroundColor $color $line +Write-Host -ForegroundColor $color $message +Write-Host -ForegroundColor $color $line +Write-Host + +invoke-expression -command ".\RegisterServiceClient.ps1" + + +# Happy debugging :-) + diff --git a/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/appsettings.json b/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/appsettings.json new file mode 100644 index 0000000..a16a7cb --- /dev/null +++ b/src/GeneralTools/DataverseClient/PowerShell/Microsoft.PowerPlatform.Dataverse.Client.PowerShell/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.PowerPlatform.Dataverse.Client.ServiceClient": "Trace", + "dvscClient.Program": "Trace" + } + } +} \ No newline at end of file diff --git a/src/GeneralTools/DataverseClient/UnitTests/AzDevOps_ServiceConnection_Test/AzDevOps_ServiceConnection_Test.csproj b/src/GeneralTools/DataverseClient/UnitTests/AzDevOps_ServiceConnection_Test/AzDevOps_ServiceConnection_Test.csproj new file mode 100644 index 0000000..17a0140 --- /dev/null +++ b/src/GeneralTools/DataverseClient/UnitTests/AzDevOps_ServiceConnection_Test/AzDevOps_ServiceConnection_Test.csproj @@ -0,0 +1,25 @@ + + + + true + Exe + net462;net6.0 + false + DataverseClient-Tests + false + + + + + + + + + + + + + + + + diff --git a/src/GeneralTools/DataverseClient/UnitTests/AzDevOps_ServiceConnection_Test/Operations/ConnectionTest.cs b/src/GeneralTools/DataverseClient/UnitTests/AzDevOps_ServiceConnection_Test/Operations/ConnectionTest.cs new file mode 100644 index 0000000..d1ae9af --- /dev/null +++ b/src/GeneralTools/DataverseClient/UnitTests/AzDevOps_ServiceConnection_Test/Operations/ConnectionTest.cs @@ -0,0 +1,49 @@ +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.PowerPlatform.Dataverse.Client.Model; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Crm.Sdk.Messages; + +namespace AzDevOps_ServiceConnection_Test.Operations +{ + internal class ConnectionTest + { + internal static async Task Run(Uri dvUrlToUse, Guid tenantId, Guid clientId, Guid serviceConnectionId, string systemAccessTokenEnvironmentId) + { + IConfiguration config = new ConfigurationBuilder() + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .Build(); + + ILoggerFactory loggerFactory = LoggerFactory.Create(builder => + builder.AddConsole(options => + { + options.IncludeScopes = true; + options.TimestampFormat = "hh:mm:ss "; + }) + .AddConfiguration(config.GetSection("Logging"))); + + var logger = loggerFactory.CreateLogger(); + + + ServiceClient client = AzPipelineFederatedIdentityAuth.CreateServiceClient( + tenantId.ToString(), + clientId.ToString(), + serviceConnectionId.ToString(), + new ConnectionOptions() + { + ServiceUri = dvUrlToUse, + Logger = logger + } + ); + + var response = (WhoAmIResponse)await client.ExecuteAsync(new WhoAmIRequest()).ConfigureAwait(false); + logger.LogInformation($"Response: {response.UserId} - {response.BusinessUnitId} - {response.OrganizationId}"); + } + } +} diff --git a/src/GeneralTools/DataverseClient/UnitTests/AzDevOps_ServiceConnection_Test/Program.cs b/src/GeneralTools/DataverseClient/UnitTests/AzDevOps_ServiceConnection_Test/Program.cs new file mode 100644 index 0000000..549af06 --- /dev/null +++ b/src/GeneralTools/DataverseClient/UnitTests/AzDevOps_ServiceConnection_Test/Program.cs @@ -0,0 +1,78 @@ +using System; +using System.CommandLine; +using System.Threading.Tasks; +using AzDevOps_ServiceConnection_Test.Operations; + +namespace AzDevOps_ServiceConnection_Test +{ + internal class Program + { + static async Task Main(string[] args) + { + Console.WriteLine("TEST Creating Service Connection to Dataverse"); + + var rootCommand = new RootCommand("Test harness for connecting to Dataverse from DevOps"); + + var DvUrlToUse = GenerateOptionItem( + "--DataverseUrl", + "URL of the Dataverse Server that you want to connect too.\nMake sure to remove any trailing / from the URL! You will see an auth error if present", + "-url", + true); + + var TenantId = GenerateOptionItem( + "--tenantid", + "tenantID (Guid)", + "-t", + true); + + var ClientId = GenerateOptionItem( + "--ClientId", + "ClientId (Guid)", + "-c", + true); + + var ServiceConnectionId = GenerateOptionItem( + "--ServiceConnectionId", + "ServiceConnectionId (Guid)", + "-s", + true); + + + var SystemAccessTokenEnvironmentId = GenerateOptionItem( + "--SystemAccessTokenName", + "Environment Variable name for the System Access Token", + "-en", + false); + + var testCommand = new Command("test", "Get Connection to Dataverse and Test it") + { + DvUrlToUse, + TenantId, + ClientId, + ServiceConnectionId, + SystemAccessTokenEnvironmentId + }; + + testCommand.SetHandler(async (dvUrlToUse, tenantId, clientId, serviceConnectionId, systemAccessTokenEnvironmentId) => + { + await ConnectionTest.Run(dvUrlToUse, tenantId, clientId, serviceConnectionId, systemAccessTokenEnvironmentId).ConfigureAwait(false); + }, DvUrlToUse, TenantId, ClientId, ServiceConnectionId, SystemAccessTokenEnvironmentId); + + rootCommand.Add(testCommand); + + return await rootCommand.InvokeAsync(args).ConfigureAwait(false); + + } + + + private static Option GenerateOptionItem(string name, string description, string alias, bool isRequired = false) + { + var option = new Option(name: $"--{name}", description: description); + option.AddAlias($"-{alias}"); + option.IsRequired = isRequired; + return option; + } + + + } +} diff --git a/src/GeneralTools/DataverseClient/UnitTests/AzDevOps_ServiceConnection_Test/appsettings.json b/src/GeneralTools/DataverseClient/UnitTests/AzDevOps_ServiceConnection_Test/appsettings.json new file mode 100644 index 0000000..c74eea1 --- /dev/null +++ b/src/GeneralTools/DataverseClient/UnitTests/AzDevOps_ServiceConnection_Test/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "None", + "AzDevOps_ServiceConnection_Test.Program": "None", + "Microsoft.PowerPlatform.Dataverse.Client.ServiceClient": "Trace" + } + } +} diff --git a/src/Packages.props b/src/Packages.props index 9692765..d405fd3 100644 --- a/src/Packages.props +++ b/src/Packages.props @@ -3,8 +3,8 @@ 3.19.8 4.61.3 - 9.2.24044.9795-master - 9.2.24044.9795-master + 9.2.24073.11611-master + 9.2.24073.11611-master 13.0.1 2.3.24 9.0.2.55 @@ -17,10 +17,11 @@ 7.0.3 7.0.0 4.5.5 - 4.10.3 6.0.0 6.0.0 1.12.0 + 4.10.3 + 6.2.0 17.5.0 diff --git a/src/SDK-IntelliSense/V9/PublishedXML/CE/Microsoft.PowerPlatform.Dataverse.Client.xml b/src/SDK-IntelliSense/V9/PublishedXML/CE/Microsoft.PowerPlatform.Dataverse.Client.xml index bc6845b..ea57d51 100644 --- a/src/SDK-IntelliSense/V9/PublishedXML/CE/Microsoft.PowerPlatform.Dataverse.Client.xml +++ b/src/SDK-IntelliSense/V9/PublishedXML/CE/Microsoft.PowerPlatform.Dataverse.Client.xml @@ -148,6 +148,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Contains a variable definition. @@ -157,18 +291,18 @@ Create a new Data Type Default Constructor - + Create a new Data Type Data to Set Type of Data to Set + Name of the related entity, applies to the Field Types: Customer and Lookup - + Create a new Data Type Data to Set Type of Data to Set - Name of the related entity, applies to the Field Types: Customer and Lookup @@ -1961,22 +2095,22 @@ Relationship Name Entities to associate - + Associate an entity with a set of entities + Propagates notification that operations should be canceled. - + Associate an entity with a set of entities - Propagates notification that operations should be canceled. @@ -2004,50 +2138,51 @@ Entity to create ID of newly created entity - + Create an entity and process any related entities entity to create + Propagates notification that operations should be canceled. Returns the newly created record - + Create an entity and process any related entities entity to create - Propagates notification that operations should be canceled. Returns the newly created record - + Create an entity and process any related entities entity to create + Propagates notification that operations should be canceled. The ID of the created record - + Create an entity and process any related entities entity to create - Propagates notification that operations should be canceled. The ID of the created record + Issues a Delete request to Dataverse Entity name to delete ID if entity to delete - + Delete instance of an entity Logical name of entity Id of entity + Propagates notification that operations should be canceled. - + Delete instance of an entity Logical name of entity Id of entity - Propagates notification that operations should be canceled. @@ -2057,22 +2192,29 @@ Relationship Name Entities to disassociate - + Disassociate an entity with a set of entities + Propagates notification that operations should be canceled. - + Disassociate an entity with a set of entities - Propagates notification that operations should be canceled. + + + + + + + @@ -2083,13 +2225,6 @@ Logging provider - - - - - - - Discovers Organizations Using the global discovery service. @@ -2147,17 +2282,17 @@ Request object Response object - + Perform an action in an organization specified by the request. Refer to SDK documentation for list of messages that can be used. + Propagates notification that operations should be canceled. Results from processing the request - + Perform an action in an organization specified by the request. Refer to SDK documentation for list of messages that can be used. - Propagates notification that operations should be canceled. Results from processing the request @@ -2228,21 +2363,21 @@ ColumnSet to request Entity object - + Retrieves instance of an entity Logical name of entity Id of entity Column Set collection to return with the request + Propagates notification that operations should be canceled. Selected Entity - + Retrieves instance of an entity Logical name of entity Id of entity Column Set collection to return with the request - Propagates notification that operations should be canceled. Selected Entity @@ -2251,17 +2386,17 @@ Query to Request EntityCollection Result - + Retrieves a collection of entities + Propagates notification that operations should be canceled. Returns an EntityCollection Object containing the results of the query - + Retrieves a collection of entities - Propagates notification that operations should be canceled. Returns an EntityCollection Object containing the results of the query @@ -2269,16 +2404,16 @@ Issues an update to Dataverse. Entity to update into Dataverse - + Updates an entity and process any related entities entity to update + Propagates notification that operations should be canceled. - + Updates an entity and process any related entities entity to update - Propagates notification that operations should be canceled. @@ -2512,10 +2647,12 @@ Logg an error with an Exception - + - Log a Message as an Information event. + Log a Trace event + + @@ -2523,12 +2660,10 @@ - + - Log a Trace event + Log a Message as an Information event. - - @@ -2607,11 +2742,6 @@ - - - Creates a CdsService Client Exception - Error Message - Creates a CdsService Client Exception @@ -2625,6 +2755,11 @@ + + + Creates a CdsService Client Exception + Error Message + @@ -2638,11 +2773,6 @@ - - - Creates a CdsService Client Exception - Error Message - Creates a CdsService Client Exception @@ -2658,6 +2788,11 @@ Data Properties + + + Creates a CdsService Client Exception + Error Message + Creates a CdsService Client Exception from a httpOperationResult. @@ -2683,20 +2818,20 @@ The XML stream to load. the new XmlDocument object - + Creates an XmlDocument object with secure default property values. - Loads the given XML into the XmlDocument. + Loads the given XML into the XmlDocument. + This overload is useful when a whitespace only element value is valid content. The XML to load. + Whether the whitespaces are to be preserved or not. the new XmlDocument object - + Creates an XmlDocument object with secure default property values. - Loads the given XML into the XmlDocument. - This overload is useful when a whitespace only element value is valid content. + Loads the given XML into the XmlDocument. The XML to load. - Whether the whitespaces are to be preserved or not. the new XmlDocument object @@ -2711,17 +2846,17 @@ Xml stream. The new XmlReader object. - + - Creates an XmlReader object with secure default property values. + Creates an XmlReader object with secure default property values and given whitespace setting. The string to get the data from. + Whether the whitespaces are to be preserved or not. the new XmlReader object - + - Creates an XmlReader object with secure default property values and given whitespace setting. + Creates an XmlReader object with secure default property values. The string to get the data from. - Whether the whitespaces are to be preserved or not. the new XmlReader object diff --git a/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.AzAuth.nuspec b/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.AzAuth.nuspec index 274ac43..ba9b207 100644 --- a/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.AzAuth.nuspec +++ b/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.AzAuth.nuspec @@ -15,32 +15,32 @@ Dynamics CommonDataService CDS PowerApps PowerPlatform ServiceClient Dataverse - - + + - - + + - - + + - - + + - - - - - - - - + + + + + + + + diff --git a/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.ReleaseNotes.txt b/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.ReleaseNotes.txt index 75bb23c..d8a2833 100644 --- a/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.ReleaseNotes.txt +++ b/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.ReleaseNotes.txt @@ -7,9 +7,12 @@ Notice: Note: Only AD on FullFramework, OAuth, Certificate, ClientSecret Authentication types are supported at this time. ++CURRENTRELEASEID++ +Fix for endless retry loop issue in WebAPI calls when specific error states are encountered. Fix for Logging MSAL telemetry when using ILogger Previously, Logs for MSAL were not written to the configured ILogger, they would only go to Trace Source and InMemory Logs. Fix for RequestBuilder to properly honor CrmUserId and AADOid in request builder requests. +Fix for ForceServerMetadataCacheConsistency not being effective until an operation to retrieve current organization version has been executed. + If this us set to a value before any organization detail related information is retrieved, it will now cause the organization info to be retrieved from Dataverse. This is done only once. Updated ServiceClient retry logic to use the server specified RetryAfter for Time and Concurrency throttling fault codes, in addition to Burst. Updated ConnectionService retry logic to parse RetryAfter header as seconds instead of hours. Dependency Changes: diff --git a/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.nuspec b/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.nuspec index f0c0196..8adbec8 100644 --- a/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.nuspec +++ b/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.nuspec @@ -24,6 +24,9 @@ + + + @@ -35,6 +38,8 @@ + + @@ -46,6 +51,8 @@ + + @@ -59,8 +66,8 @@ - - + +