MS CRM 2015: Bulk update data using Powershell

Updating existing data in bulk in a Microsoft Dynamics CRM 2015 environment is a non-trivial task. I’ll show how you can use MS CRM’s webservice in combination with Powershell to perform the updates, with the data to be updated coming from a csv file.

Introduction

The other day, a colleague asked me for help with updating data in bulk in a Microsoft Dynamics CRM 2015 environment. As it turns out, CRM is really good at importing new records (just use the “Import data” wizard in combination with a regular Excel or .csv file), but this option cannot be used to update existing records with new values. Why not guys?

ExportImportDataApparently, the way to do a bulk update is to first export the existing data with the “Export Data to Excel” action, and opt to “Make this data available for re-importing by including required column headings”. This will generate an Excel workbook with the required identifying information for each record hidden in the first 3 columns. When this file is imported by the Data Import wizard, it will update the records for you.

ExportDataToExcelObviously the downside to this is that you can’t just use any Excel export that a user gives you, the modifications have to be done in such a specially generated file. Also, as you can tell from the screenshot, this special export format can only be selected for the records on the current page (probably because the generated Excel workbook is full of validation rules which makes it s.l.o.w…). Hence, unless you can get the records that you want to modify to fit on the same page, you can’t use this method.

Luckily, MS CRM 2015 provides an excellent API through its webservice, and I’ll show how to leverage it in combination with Powershell to perform the bulk data update.

I’m using Microsoft Dynamics CRM 2015 (v7.0) in an Internet Facing Deployment (aka IFD), although on-premise works exactly the same, and Powershell 4.

Connecting to MS CRM

We’re going to use the webservice proxy that MS CRM provides as part of its SDK. Copy the Microsoft.Crm.Sdk.Proxy.dll and Microsoft.Xrm.Sdk.dll assemblies to your current working folder, and load them into your Powershell session, along with System.ServiceModel for the authorization classes:

[void][System.Reflection.Assembly]::LoadFile("$pwdmicrosoft.xrm.sdk.dll")
[void][System.Reflection.Assembly]::LoadFile("$pwdmicrosoft.crm.sdk.proxy.dll")
[void][System.Reflection.Assembly]::LoadWithPartialName("system.servicemodel")

To determine where we should connect to, in CRM navigate to Settings, Customizations, Developer resources and copy its Organization Service SOAP endpoint url:

OrganizationServiceUrlFor an IFD scenario you should specify your username and password in the ClientCredentials, whereas in an on-premise scenario you don’t need to because it will default to your Windows credentials.
This is all that’s needed for an OrganizationServiceProxy to connect to CRM:

$crmServiceUrl = "https://myOrg.myCrmProvider.net/XRMServices/2011/Organization.svc"
$clientCredentials = new-object System.ServiceModel.Description.ClientCredentials
$clientCredentials.UserName.UserName = 'Leon@myCrmProvider.net'
$clientCredentials.UserName.Password = 'Secret123'
$service = new-object Microsoft.Xrm.Sdk.Client.OrganizationServiceProxy($crmServiceUrl, $null, $clientCredentials, $null)
$service.Timeout = new-object System.Timespan(0, 10, 0)

To test the connection, try asking who you are:

$request = new-object Microsoft.Crm.Sdk.Messages.WhoAmIRequest
$service.Execute($request)

It should respond with something like this:

UserId         : 0dc96a70-7d2f-e511-80d0-001dd8b721fe
BusinessUnitId : a6099f28-772f-e511-80d0-001dd8b721fe
OrganizationId : 0d67a31e-772f-e511-80d0-001dd8b721fe
ResponseName   : WhoAmI
Results        : {[UserId, 0dc96a70-7d2f-e511-80d0-001dd8b721fe], [BusinessUnitId, a6099f28-772f-e511-80d0-001dd8b721fe], [OrganizationId, 0d67a31e-772f-e511-80d0-001dd8b721fe]}
ExtensionData  : System.Runtime.Serialization.ExtensionDataObject

Loading the data to update from a .csv file

Because both Excel and Powershell can handle comma-separated (.csv) files it makes sense to provide the data to be updated in this way. Suppose that we have several existing Accounts in CRM that we want to update, so our accounts.csv looks like this:

accountid;name;address1_city;telephone1;websiteurl;preferredcontactmethodcode
7e6e0424-0d32-e511-80d1-001dd8b721fe;Alfa;Veenendaal;(555) 123456;http://localhost/alfa;Email
1838263f-0d32-e511-80d1-001dd8b721fe;Beta;Arnhem;(555) 234567;http://localhost/beta;Phone
59a45311-0e32-e511-80d1-001dd8b721fe;Delta;Amsterdam;(555) 345678;http://localhost/delta;

Importing it with Import-Csv produces a PSCustomObject for each line beyond the header, with a string property for each column:

$accounts = Import-Csv "Accounts.csv" -Delimiter ";"
$accounts | Format-Table

Produces:

accountid                                 name       address1_city  telephone1     websiteurl              preferredcontactmethodcode
---------                                 ----       -------------  ----------     ----------              --------------------------
7e6e0424-0d32-e511-80d1-001dd8b721fe      Alfa       Veenendaal     (555) 123456   http://localhost/alfa   Email
1838263f-0d32-e511-80d1-001dd8b721fe      Beta       Arnhem         (555) 234567   http://localhost/beta   Phone
59a45311-0e32-e511-80d1-001dd8b721fe      Delta      Amsterdam      (555) 345678   http://localhost/delta

Performing the bulk update

Updating a record is simply a matter of preparing a new Entity with the Id of the record to be updated. Only the Attributes that we assign a value will be updated in CRM. You can view the Entity definition (via Settings, Customizations, Customize the system) or use the EntityMetadata.xlsx (part of the CRM SDK) to determine which attribute names are available for the Account entity. Finally, $service.Update() takes care of updating the data in CRM:

$accounts | Foreach-Object {
    $entity = New-Object Microsoft.Xrm.Sdk.Entity("account")
    $entity.Id = [Guid]::Parse($_.accountid)
    $entity.Attributes["address1_city"] = $_.address1_city
    $entity.Attributes["telephone1"] = $_.telephone1
    $entity.Attributes["websiteurl"] = $_.websiteurl

    Write-Output ('Updating "{0}" (Id = {1})...' -f $_.name, $entity.Id)
    $service.Update($entity)
}

Note that there also exists an ExecuteMultipleRequest that can be used to send multiple updates in one call.

Working without a known AccountId

In the previous example our .csv file included the accountid of the record to be updated. However, users rarely provide this column, also because there’s not really a way to include the accountid when exporting a view to Excel. But there’s a way to use other information, such as the Account’s name and city, to identify a record (assuming the combination is unique), and to look up the corresponding accountid in CRM. First, we’ll query for all existing Accounts:

$query = new-object Microsoft.Xrm.Sdk.Query.QueryExpression("account")
$query.ColumnSet = new-object Microsoft.Xrm.Sdk.Query.ColumnSet("name", "address1_city")

# RetrieveMultiple returns a maximum of 5000 records by default. 
# If you need more, use the response's PagingCookie.
$response = $service.RetrieveMultiple($query)

Next, we’ll use it to create a (case-insensitive) dictionary that maps each Account’s (name, city) combination to its corresponding accountid:

$idmap = @{ }
$response.Entities | ForEach-Object {
    $key = ("{0}|{1}" -f @($_.Attributes["name"], $_.Attributes["address1_city"]))
    $idmap.Add($key, $_.Id)
}

Once we have this $idmap, performing the bulk update without knowing the accountid becomes:

$accounts | Foreach-Object {
    # Lookup the accountid based on the Account's name and city.
    $key = ("{0}|{1}" -f @($_.name, $_.address1_city))
    $id = $idmap[$key]
    
    if($id)
    {
        $entity = New-Object Microsoft.Xrm.Sdk.Entity("account")
        $entity.Id = $id
        $entity.Attributes["telephone1"] = $_.telephone1
        $entity.Attributes["websiteurl"] = $_.websiteurl

        Write-Output ('Updating "{0}" (Id = {1})...' -f $_.name, $entity.Id)
        $service.Update($entity)
    }
    else
    {
        Write-Output ('Couldn''t determine id for "{0}" in "{1}", skipping it...' -f $_.name, $_.address1_city)
    }
}

Updating OptionSet attributes

As you might have noticed, although the Account.csv includes a preferredcontactmethodcode column, we haven’t used it in our update. This is because this attribute is defined as an “option set” (or “picklist”), which is basically CRM’s version of an enumeration. And you can’t just assign an attribute of type OptionSet a string value, because CRM will then complain about “Incorrect attribute value type System.String”.

Hence, we’ll have to map each string value to its corresponding numerical id ourselves. Luckily, this mapping can be requested from CRM using the RetrieveAttributeRequest, and its result just needs a little massaging to be practical for Powershell:

# For the requested OptionSet attribute, returns a dictionary that maps each string 
# value to its corresponding numerical id.
function GetOptionSet([string]$entityName, [string]$attributeName)
{
    # Request the attribute metadata
    $metadataRequest = New-Object Microsoft.Xrm.Sdk.Messages.RetrieveAttributeRequest
    $metadataRequest.EntityLogicalName = $entityName
    $metadataRequest.LogicalName = $attributeName
    $metadataRequest.RetrieveAsIfPublished = $true
    $metadataResponse = $service.Execute($metadataRequest)

    # If this was indeed a picklist, $metadataResponse.AttributeMetadata is of type 
    # PicklistAttributeMetadata which contains all available options.
    $dict = @{ };
    $metadataResponse.AttributeMetadata.OptionSet.Options | ForEach-Object {
        $label = $_.Label.UserLocalizedLabel.Label
        $value = $_.Value
        $dict.Add($label, $value)
    }

    # Add an empty string that maps to the default value.
    $dict.Add("", $metadataResponse.AttributeMetadata.DefaultFormValue)

    return $dict
}

If we use this function to retrieve the OptionSet values for preferredcontactmethodcode

$contactmethods = GetOptionSet "account" "preferredcontactmethodcode"
$contactmethods

…it produces:

Name               Value
----               -----
Any                1    
Email              2    
Mail               5    
Fax                4    
Phone              3    
                   1    

With these $contactMethods, we can also update the Account’s preferredcontactmethodcode field by specifying it as an OptionSetValue:

# ...
$entity = New-Object Microsoft.Xrm.Sdk.Entity("account")
$entity.Id = $id
$entity.Attributes["telephone1"] = $_.telephone1
$entity.Attributes["websiteurl"] = $_.websiteurl

# Map the code (e.g. "Mail") to its id (5) and create an OptionSetValue for it
$contactMethodId = $contactMethods[$_.preferredcontactmethodcode]
$contactMethodOption = new-object Microsoft.Xrm.Sdk.OptionSetValue($contactMethodId)

# Note that we need the cast - because we're assigning to a dictionary of 
# String to Object, by default Powershell will want to store the 
# $contactMethodOption as a PSObject which the CRM webservice won't be able 
# to deserialize.
$entity.Attributes["preferredcontactmethodcode"] = [Microsoft.Xrm.Sdk.OptionSetValue]$contactMethodOption

Write-Output ('Updating "{0}" (Id = {1})...' -f $_.name, $entity.Id)
$service.Update($entity)

And indeed, running the combined script will update the records in CRM:

UpdatedAccountData
Summary

Until MS CRM provides a better way, using Powershell for a bulk update isn’t too difficult. Obviously what I’ve shown here are just the basics; you can use the MS CRM webservice from Powershell to do pretty much anything (see the list of available messages).

The sky’s the limit 🙂