From a70b006ce6eb888e2dcd3221a9bce2c5f8dfe236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=B6nert?= Date: Mon, 18 Mar 2019 10:10:00 +0100 Subject: [PATCH 1/2] Migrating to XRM Tooling --- .../Dynamics.CRM.SolutionExchanger.fsproj | 27 +++++ .../Dynamics.CRM.SolutionExchanger/Program.fs | 78 +++--------- .../SolutionExchanger.fs | 111 +++++------------- .../packages.config | 5 + 4 files changed, 79 insertions(+), 142 deletions(-) diff --git a/src/app/Dynamics.CRM.SolutionExchanger/Dynamics.CRM.SolutionExchanger.fsproj b/src/app/Dynamics.CRM.SolutionExchanger/Dynamics.CRM.SolutionExchanger.fsproj index a5cba2d..ef0acca 100644 --- a/src/app/Dynamics.CRM.SolutionExchanger/Dynamics.CRM.SolutionExchanger.fsproj +++ b/src/app/Dynamics.CRM.SolutionExchanger/Dynamics.CRM.SolutionExchanger.fsproj @@ -62,14 +62,38 @@ ..\..\..\packages\Microsoft.IdentityModel.6.1.7600.16394\lib\net35\Microsoft.IdentityModel.dll True + + ..\..\..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.2.22.302111727\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.dll + + + ..\..\..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.2.22.302111727\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.WindowsForms.dll + + + ..\..\..\packages\Microsoft.CrmSdk.XrmTooling.CoreAssembly.9.0.2.12\lib\net462\Microsoft.Rest.ClientRuntime.dll + ..\..\..\packages\Microsoft.CrmSdk.CoreAssemblies.9.0.2.12\lib\net462\Microsoft.Xrm.Sdk.dll + + ..\..\..\packages\Microsoft.CrmSdk.Deployment.9.0.2.9\lib\net462\Microsoft.Xrm.Sdk.Deployment.dll + + + ..\..\..\packages\Microsoft.CrmSdk.Workflow.9.0.2.9\lib\net462\Microsoft.Xrm.Sdk.Workflow.dll + + + ..\..\..\packages\Microsoft.CrmSdk.XrmTooling.CoreAssembly.9.0.2.12\lib\net462\Microsoft.Xrm.Tooling.Connector.dll + True + + ..\..\..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll + + + + @@ -81,6 +105,9 @@ + + + diff --git a/src/app/Dynamics.CRM.SolutionExchanger/Program.fs b/src/app/Dynamics.CRM.SolutionExchanger/Program.fs index 9cadea7..f04e87b 100644 --- a/src/app/Dynamics.CRM.SolutionExchanger/Program.fs +++ b/src/app/Dynamics.CRM.SolutionExchanger/Program.fs @@ -26,12 +26,10 @@ let OptionToString (opt : option<'T>)= | Some value -> "Present" | None -> "Missing" -let ExportAllSolutions password user (url:string option) (managed:bool option) (workingDir:string option) timeOut = +let ExportAllSolutions connectionString (managed:bool option) (workingDir:string option) timeOut = let solutions = ExportAllSolutions (fun cred -> { cred with - Password = password - Username = user - Url = url.Value + ConnectionString = connectionString TimeOut = timeOut }) managed.Value @@ -45,34 +43,10 @@ let ExportAllSolutions password user (url:string option) (managed:bool option) ( printf "Skipping solution %s, since it has no value.\n" uniqueName) 0 -let ExportAllOrganizations password user (url:string option) (managed:bool option) (workingDir: string option) timeOut = - let solutions = ExportAllOrganizations (fun cred -> - { cred with - Password = password - Username = user - Url = url.Value - TimeOut = timeOut - }) managed.Value - - let dir = if workingDir.IsNone then @".\" else workingDir.Value - - // Write all solutions into a directory called like the organization - solutions - |> Seq.iter(fun (friendlyName, solutions) -> - solutions - |> Seq.iter(fun (solution, uniqueName) -> - if solution.IsSome then - WriteSolutionToFile (uniqueName + ".zip") solution.Value (Path.Combine(dir, friendlyName)) - else - printf "Skipping solution %s, since it has no value.\n" uniqueName)) - 0 - -let ExportSolution password user (url:string option) (sol:string option) (filename:string option) (managed:bool option) (workingDir: string option) timeOut = +let ExportSolution connectionString (sol:string option) (filename:string option) (managed:bool option) (workingDir: string option) timeOut = let solution = ExportSolution (fun cred -> { cred with - Password = password - Username = user - Url = url.Value + ConnectionString = connectionString TimeOut = timeOut }) sol.Value managed.Value @@ -112,76 +86,62 @@ let parseInt (input:string option) referenceName = /// Used for exporting solution from CRM and saving it to file let Export args = - let url = FindOption "/url:" args - let user = FindOption "/user:" args - let password = FindOption "/password:" args + let connectionString = FindOption "/connectionString:" args let solution = FindOption "/solution:" args let managedText = FindOption "/managed:" args let workingDir = FindOption "/workingdir:" args let filename = FindOption "/filename:" args let allSolutionsText = FindOption "/allSolutions:" args - let allOrganizationsText = FindOption "/allOrganizations:" args let timeOutText = FindOption "/timeout:" args let allSolutions = parseBool allSolutionsText "allSolutions" let managed = parseBool managedText "managed" - let allOrganizations = parseBool allOrganizationsText "allOrganizations" let timeOut = parseInt timeOutText "timeout" - if url.IsNone || (solution.IsNone && (allSolutions.IsNone || not allSolutions.Value) && (allOrganizations.IsNone || not allOrganizations.Value)) || managed.IsNone then - printf "Values missing: Needed /url (%s), (/solution (%s) or /allSolutions:true or allOrganizations:true) and /managed (%s)\n" (OptionToString url) (OptionToString solution) (OptionToString managed) + if connectionString.IsNone || (solution.IsNone && (allSolutions.IsNone || not allSolutions.Value)) || managed.IsNone then + printf "Values missing: Needed /connectionString (%s), (/solution (%s) or /allSolutions:true or allOrganizations:true) and /managed (%s)\n" (OptionToString connectionString) (OptionToString solution) (OptionToString managed) 1 else if allSolutions.IsSome && allSolutions.Value then - ExportAllSolutions password user url managed workingDir timeOut - else if allOrganizations.IsSome && allOrganizations.Value then - ExportAllOrganizations password user url managed workingDir timeOut + ExportAllSolutions connectionString.Value managed workingDir timeOut else - ExportSolution password user url solution filename managed workingDir timeOut + ExportSolution connectionString.Value solution filename managed workingDir timeOut /// Used for importing solution to CRM let Import args = - let url = FindOption "/url:" args - let user = FindOption "/user:" args - let password = FindOption "/password:" args + let connectionString = FindOption "/connectionString:" args let filename = FindOption "/filename:" args let timeOutText = FindOption "/timeout:" args let timeOut = parseInt timeOutText "timeout" - if url.IsNone || filename.IsNone then - printf "Values missing: Needed /url (%s) and /filename (%s)\n" (OptionToString url) (OptionToString filename) + if connectionString.IsNone || filename.IsNone then + printf "Values missing: Needed /connectionString (%s) and /filename (%s)\n" (OptionToString connectionString) (OptionToString filename) 1 else ImportSolution (fun cred -> { cred with - Password = password - Username = user - Url = url.Value + ConnectionString = connectionString.Value TimeOut = timeOut }) filename.Value 0 /// Used for publishing all customizations in CRM let Publish args = - let url = FindOption "/url:" args - let user = FindOption "/user:" args - let password = FindOption "/password:" args - + let connectionString = FindOption "/connectionString:" args + let timeOutText = FindOption "/timeout:" args let timeOut = parseInt timeOutText "timeout" - if url.IsNone then - printf "Values missing: Needed /url (%s)\n" (OptionToString url) + if connectionString.IsNone then + printf "Values missing: Needed /url (%s)\n" (OptionToString connectionString) 1 else PublishAll (fun cred -> { cred with - Password = password - Username = user - Url = url.Value + ConnectionString = connectionString.Value TimeOut = timeOut }) 0 @@ -191,7 +151,7 @@ let main argv = if argv.Length < 1 then printf "%s%s%s%s%s%s%s%s%s%s%s" "Usage SolutionExchanger.exe [Export | Import | Publish] [/user: | /password: | /url: | /solution: | /managed: | /filename: | /workingdir: | /allSolutions: | /allOrganizations:]\n" - "/user - Username for authenticating with CRM endpoint. If no user and password are given, fallback to default credentials\n" + "/connectionString - Connection string for connecting to CRM. Always required.\n" "/password - Password for authenticating with CRM endpoint. If no user and password are given, fallback to default credentials\n" "/url - Required. Url of CRM endpoint\n" "/solution - Required if exporting. Unique name of solution to export. Can be replaced by allSolutions to get all unmanaged solutions in organization\n" diff --git a/src/app/Dynamics.CRM.SolutionExchanger/SolutionExchanger.fs b/src/app/Dynamics.CRM.SolutionExchanger/SolutionExchanger.fs index 1272290..e30f370 100644 --- a/src/app/Dynamics.CRM.SolutionExchanger/SolutionExchanger.fs +++ b/src/app/Dynamics.CRM.SolutionExchanger/SolutionExchanger.fs @@ -9,36 +9,23 @@ open Microsoft.Xrm.Sdk.Query open Microsoft.Xrm.Sdk.Client open Microsoft.Xrm.Sdk.Discovery open Microsoft.Crm.Sdk.Messages +open Microsoft.Xrm.Tooling.Connector +open System.Web.Services.Description let internal _timeOutDefaults = new TimeSpan(0, 10, 0) type CrmEndpointParams = { - Url : string - Username : string option - Password : string option + ConnectionString: string TimeOut: int option } let CrmEndpointDefaults = { - Url = "" - Username = None - Password = None + ConnectionString = "" TimeOut = Some 10 } -/// Sets given credentials or if not present default credentials for user -let internal GetCredentials (username : string option) (password : string option) = - let credentials = new ClientCredentials() - - if username.IsSome && not (String.IsNullOrEmpty username.Value) && password.IsSome && not (String.IsNullOrEmpty password.Value) then - credentials.UserName.UserName <- username.Value - credentials.UserName.Password <- password.Value - else - credentials.Windows.ClientCredential <- (CredentialCache.DefaultCredentials :?> NetworkCredential) - credentials - /// Query all solutions in system let internal RetrieveAllSolutions (service : IOrganizationService) = let query = @@ -60,40 +47,26 @@ let internal RetrieveAllSolutions (service : IOrganizationService) = with | ex -> printf "Encountered exception during retrieval of solutions: %s" ex.Message None - -let internal DiscoverOrganizations crmEndpoint = - let serviceParams = crmEndpoint CrmEndpointDefaults - let credentials = GetCredentials serviceParams.Username serviceParams.Password - - let discoveryService = new DiscoveryServiceProxy(new Uri(serviceParams.Url), null, credentials, null) - let discoveryRequest = new RetrieveOrganizationsRequest(AccessType = EndpointAccessType.Default, Release = OrganizationRelease.Current) - let discoveryResponse = discoveryService.Execute(discoveryRequest) :?> RetrieveOrganizationsResponse - discoveryResponse.Details - + /// Creates Organization Service for communicating with Dynamics CRM /// ## Parameters /// /// - `username` - Username for authentication /// - `password` - Password for authentication /// - `url` - URL that is used to connect to CRM -let private CreateOrganizationService username password url (timeout:int option) = - printfn "Creating Organization Service: %A" url +let private CreateOrganizationService (serviceParams: CrmEndpointParams) = + printfn "Creating Organization Service: %A" serviceParams.ConnectionString try - let credentials = GetCredentials username password - let serviceUri = new Uri(url) - let proxy = new OrganizationServiceProxy(serviceUri, null, credentials, null) - proxy.EnableProxyTypes() + let conn = new CrmServiceClient(serviceParams.ConnectionString); - if timeout.IsSome then - printfn "Setting timeout for service to %i Minutes\n" timeout.Value - proxy.Timeout <- new TimeSpan(0, timeout.Value, 0) - else - printfn "No timeout set, falling back to default of %f minutes\n" _timeOutDefaults.TotalMinutes - proxy.Timeout <- _timeOutDefaults + if not conn.IsReady then + failwith (sprintf "Could not connect, Error: %s" conn.LastCrmError) - printfn "Successfully created organization service" - Some(proxy :> IOrganizationService) + if conn.OrganizationWebProxyClient <> null then + conn.OrganizationWebProxyClient :> IOrganizationService; + else + conn.OrganizationServiceProxy :> IOrganizationService; with | ex -> failwith (sprintf "Error while creating organization service: %A" ex.Message) @@ -104,11 +77,10 @@ let private CreateOrganizationService username password url (timeout:int option) let PublishAll crmEndpoint = printfn "Publishing all" let serviceParams = crmEndpoint CrmEndpointDefaults - let organizationService = CreateOrganizationService serviceParams.Username serviceParams.Password serviceParams.Url serviceParams.TimeOut - if organizationService.IsNone then - failwith "Could not create connection to CRM, check your endpoint config" + let organizationService = CreateOrganizationService serviceParams + let publishRequest = new PublishAllXmlRequest() - let response = organizationService.Value.Execute(publishRequest) + let response = organizationService.Execute(publishRequest) printfn "Successfully published all" /// Writes solution byte[] to file. If a file with the same name is present in given path, it is being overridden. @@ -137,15 +109,12 @@ let ExportSolution crmEndpoint solutionName (managed : bool) = printfn "Exporting solution %A" (solutionName + ": " + if managed then "Managed" else "Unmanaged") let serviceParams = crmEndpoint CrmEndpointDefaults - let organizationService = CreateOrganizationService serviceParams.Username serviceParams.Password serviceParams.Url serviceParams.TimeOut - - if organizationService.IsNone then - failwith "Could not create connection to CRM, check your endpoint config" - + let organizationService = CreateOrganizationService serviceParams + let exportSolutionRequest = new ExportSolutionRequest( Managed = managed, SolutionName = solutionName ) try - let response = organizationService.Value.Execute(exportSolutionRequest) :?> ExportSolutionResponse + let response = organizationService.Execute(exportSolutionRequest) :?> ExportSolutionResponse printfn "Successfully exported solution" Some(response.ExportSolutionFile) with @@ -158,15 +127,12 @@ let ExportAllSolutions crmEndpoint managed = printfn "Retrieving all unmanaged solutions in Organization" let serviceParams = crmEndpoint CrmEndpointDefaults - let organizationService = CreateOrganizationService serviceParams.Username serviceParams.Password serviceParams.Url serviceParams.TimeOut - - if organizationService.IsNone then - failwith "Could not create connection to CRM, check your endpoint config" - - let solutions = (RetrieveAllSolutions organizationService.Value) + let organizationService = CreateOrganizationService serviceParams + + let solutions = (RetrieveAllSolutions organizationService) if solutions.IsNone then - failwith (sprintf "Failed to retrieve solutions for organization %s\n" serviceParams.Url) + failwith (sprintf "Failed to retrieve solutions for organization %s\n" serviceParams.ConnectionString) solutions.Value.Entities |> Seq.map (fun solution -> @@ -179,26 +145,6 @@ let ExportAllSolutions crmEndpoint managed = |> Seq.filter(fun solution -> solution.IsSome) |> Seq.map(fun solution -> solution.Value) -/// Comment -let ExportAllOrganizations crmEndpoint managed = - let serviceParams = crmEndpoint CrmEndpointDefaults - - let organizations = DiscoverOrganizations crmEndpoint - organizations - |> Seq.map(fun organization -> - try - Some (organization.FriendlyName, ExportAllSolutions (fun endpoint -> { endpoint with - // Retrieve endpoint at index 1, because this is the organization service - Url = Seq.nth 1 (organization.Endpoints.Values) - Username = serviceParams.Username - Password = serviceParams.Password - TimeOut = serviceParams.TimeOut}) managed) - with - | ex -> printf "Encountered exception: %s\n" ex.Message - None) - |> Seq.filter(fun solution -> solution.IsSome) - |> Seq.map(fun solution -> solution.Value) - /// Imports zipped solution file to Dynamics CRM /// ## Parameters /// @@ -207,12 +153,11 @@ let ExportAllOrganizations crmEndpoint managed = let ImportSolution crmEndpoint path = printfn "Importing solution %A" path let serviceParams = crmEndpoint CrmEndpointDefaults - let organizationService = CreateOrganizationService serviceParams.Username serviceParams.Password serviceParams.Url serviceParams.TimeOut - if organizationService.IsNone then - failwith "Could not create connection to CRM, check your endpoint config" + let organizationService = CreateOrganizationService serviceParams + if not (File.Exists(path)) then - failwith "File at path %A does not exist!" path + failwith (sprintf "File at path %A does not exist!" path) let file = File.ReadAllBytes(path) let importSolutionRequest = new ImportSolutionRequest( CustomizationFile = file, PublishWorkflows = true ) - let response = organizationService.Value.Execute(importSolutionRequest) :?> ImportSolutionResponse + let response = organizationService.Execute(importSolutionRequest) :?> ImportSolutionResponse printfn "Successfully imported solution" diff --git a/src/app/Dynamics.CRM.SolutionExchanger/packages.config b/src/app/Dynamics.CRM.SolutionExchanger/packages.config index ac37e41..7f651a1 100644 --- a/src/app/Dynamics.CRM.SolutionExchanger/packages.config +++ b/src/app/Dynamics.CRM.SolutionExchanger/packages.config @@ -2,5 +2,10 @@ + + + + + \ No newline at end of file From f48aa2438d47c1442098bdc8c3715fe7eeeb8528 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=B6nert?= Date: Mon, 18 Mar 2019 10:12:45 +0100 Subject: [PATCH 2/2] Fixed help text --- src/app/Dynamics.CRM.SolutionExchanger/Program.fs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/app/Dynamics.CRM.SolutionExchanger/Program.fs b/src/app/Dynamics.CRM.SolutionExchanger/Program.fs index f04e87b..d79a67d 100644 --- a/src/app/Dynamics.CRM.SolutionExchanger/Program.fs +++ b/src/app/Dynamics.CRM.SolutionExchanger/Program.fs @@ -149,14 +149,11 @@ let Publish args = [] let main argv = if argv.Length < 1 then - printf "%s%s%s%s%s%s%s%s%s%s%s" - "Usage SolutionExchanger.exe [Export | Import | Publish] [/user: | /password: | /url: | /solution: | /managed: | /filename: | /workingdir: | /allSolutions: | /allOrganizations:]\n" + printf "%s%s%s%s%s%s%s%s" + "Usage SolutionExchanger.exe [Export | Import | Publish] [/connectionString: | /solution: | /managed: | /filename: | /workingdir: | /allSolutions: | /allOrganizations:]\n" "/connectionString - Connection string for connecting to CRM. Always required.\n" - "/password - Password for authenticating with CRM endpoint. If no user and password are given, fallback to default credentials\n" - "/url - Required. Url of CRM endpoint\n" "/solution - Required if exporting. Unique name of solution to export. Can be replaced by allSolutions to get all unmanaged solutions in organization\n" "/allSolutions - Pass like /allSolutions:true to Export all unmanaged solutions in organization\n" - "/allOrganizations - Pass like /allOrganizations:true to Export all unmanaged solutions in all organizations\n" "/managed - Required if exporting. Pass 'true' for exporting managed, false for unmanaged\n" "/filename - Required if importing. Pass full path to solution. If Exporting sets name of exported solution file\n" "/workingdir - Sets working directory for writing exported solution to file\n"