I’m working on an upgrade (database attach) for a client of mine and I ran into something somewhat unexpected with some User Profile properties – properties that used a choice field in 2007 (fields that allowed the administrator to define a list of values that the user could pick from) were migrated to Managed Term Store based fields when upgraded. At first glance this was pretty cool; the only problem was that, though the field was converted to a Managed Term Store based field (essentially a Taxonomy field), the Terms themselves were not migrated and the user-specific values were lost. Now, I may have done something wrong during the upgrade process and if someone knows what I did wrong please let me know – that said, I decided to just throw together a couple quick scripts that I could use to export the field information (along with the users’ values for those fields) and then import the information in the new Farm.

To accomplish this I created two separate scripts, one for the 2007 Farm and one for the new 2010 Farm. The 2007 script loops through all the User Profile properties and, if the property has choices defined then it creates an XML structure that defines the property name and the associated choices; I put this in a function called Get-SPChoiceProperties. It then loops through all User Profiles and, for each identified property, creates another XML structure which defines the property name and associated values along with the account name; I put this in a function called Get-SPUserProperties. I then merge these two XML structures to create a single structure which is saved to disk.

Here’s the complete code which I store in a file called Export-UserProfileProperties.ps1 (note that the function names and script name are not really important and the code is by no means perfect – I threw this whole thing together *very* quickly so that I could just get this task done and move on to the next task):

 1[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint") | Out-Null
 2[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Server") | Out-Null
 3[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Server.UserProfiles") | Out-Null
 4function Get-SPChoiceProperties() {
 5    $context = [Microsoft.Office.Server.ServerContext]::Default
 6    $upm = New-Object Microsoft.Office.Server.UserProfiles.UserProfileConfigManager $context
 7    $props = $upm.GetProperties()
 8    [xml]$xml = "<Properties></Properties>"
 9    foreach ($prop in $props) {
10        if ($prop.ChoiceList -eq $null) { continue }
11        
12        $propXml = $xml.CreateElement("Property")
13        $xml.DocumentElement.AppendChild($propXml) | out-null
14        $propXml.SetAttribute("Name", $prop.Name)
15        $choicesXml = $xml.CreateElement("ChoiceList")
16        $propXml.AppendChild($choicesXml) | out-null
17        foreach ($choice in $Prop.ChoiceList.GetAllTerms($true)) {
18            $choiceXml = $xml.CreateElement("Choice")
19            $choicesXml.AppendChild($choiceXml) | out-null
20            $choiceXml.InnerText = $choice
21        }
22    }
23    $xml
24}
25function Get-SPUserProperties([string[]]$propertyNames) {
26    $context = [Microsoft.Office.Server.ServerContext]::Default
27    $upm = New-Object Microsoft.Office.Server.UserProfiles.UserProfileManager $context
28    [xml]$xml = "<Users></Users>"
29    foreach ($profile in $upm) {
30        $userXml = $xml.CreateElement("User")
31        $xml.DocumentElement.AppendChild($userXml) | out-null
32        $userXml.SetAttribute("Account", $profile["AccountName"].Value)
33        $propertyNames | % { 
34            $val = ($profile[$_] -as [string[]]) -join ";"
35            $userXml.SetAttribute($_, $val) 
36        }
37    }
38    $xml
39}
40$propertyXml = Get-SPChoiceProperties
41[string[]]$properties = $propertyXml.Properties.Property | % {$_.Name}
42$userXml = Get-SPUserProperties $properties
43
44[xml]$xml = "<ProfileData>$($propertyXml.OuterXml)$($userXml.OuterXml)</ProfileData>"
45$xml.Save("c:\userdata.xml")

The next thing I needed to do was to create the script for the 2010 Farm. This script had three steps for which I created a separate function for each step:

  1. Create the Term Store Group if not present and, for each User Profile field identified in the exported XML create the Term Set and associated Terms. I store the field name and associated Term Set in a hash table that I can then use for the next steps. The function I created for this is called Create-SPProfileTermSets.
  2. For each User Profile property and associated Term Set returned by step 1, update the property with the new Term Set. The function I created for this is called Set-SPUserProfileProperties.
  3. Loop through all the users defined in the exported XML and, for each user, update the User Profile property with the appropriate Term value. The function I created for this is called Set-SPUserProfileValues.

I then load the exported XML and call these three functions in order, passing in the appropriate information as required. Here’s the completed code (again, it’s rough but it works – well, it met my needs):

 1function Create-SPProfileTermSets($session, [string]$groupName, [xml]$data) {
 2    
 3    $group = $session.DefaultSiteCollectionTermStore.Groups[$groupName]
 4    if ($group -eq $null) {
 5        $group = $ts.DefaultSiteCollectionTermStore.CreateGroup($groupName)
 6        $group.TermStore.CommitAll()
 7    }
 8    $termSets = @{}
 9    $data.ProfileData.Properties.Property | % {
10        $name = $_.Name
11        $termSet = $group.TermSets[$name]
12        if ($termSet -eq $null) {
13            $termSet = $group.CreateTermSet($name)
14        }
15        $termSets += @{$name=$termSet}
16        $_.ChoiceList.Choice | % {
17            $termValue = $_.Replace("&", "&")
18            $term = $termSet.Terms[$termValue]
19            if ($term -eq $null) {
20                Write-Host "Adding $termValue"
21                $termSet.CreateTerm($termValue, 1033) | Out-Null
22            }
23        }
24        $group.TermStore.CommitAll()        
25    }
26    $termSets
27}
28function Set-SPUserProfileProperties($termSets) {
29    $sa = Get-SPServiceApplication | ?{$_.TypeName -eq "User Profile Service Application"}
30    $context = [Microsoft.SharePoint.SPServiceContext]::GetContext($sa.ServiceApplicationProxyGroup, [Microsoft.SharePoint.SPSiteSubscriptionIdentifier]::Default)
31    $upm = New-Object Microsoft.Office.Server.UserProfiles.UserProfileConfigManager $context
32    $properties = $upm.ProfilePropertyManager.GetCoreProperties()
33    foreach ($key in $termSets.Keys) {
34        $prop = $properties.GetPropertyByName($key)
35        $prop.TermSet = $termSets[$key]
36        $prop.Commit()
37    }
38}
39function Set-SPUserProfileValues($termSets, [xml]$data) {
40    $sa = Get-SPServiceApplication | ?{$_.TypeName -eq "User Profile Service Application"}
41    $context = [Microsoft.SharePoint.SPServiceContext]::GetContext($sa.ServiceApplicationProxyGroup, [Microsoft.SharePoint.SPSiteSubscriptionIdentifier]::Default)
42    $upm = New-Object Microsoft.Office.Server.UserProfiles.UserProfileManager $context
43    
44    $data.ProfileData.Users.User | % {
45        if ($upm.UserExists($_.Account)) {
46            $up = $upm.GetUserProfile($_.Account)
47            Write-Host "Evaluating $($_.Account)..."
48            foreach ($key in $termSets.Keys) {
49                Write-Host "    Setting $key..."
50                $prop = $up[$key]
51                $prop.Clear()
52                [string[]]$term = $_.GetAttribute($key).Split(';')
53                $term | % {
54                    if (![string]::IsNullOrEmpty($_)) {
55                        $prop.Add($termSets[$key].Terms[$_].Name)
56                    }
57                }
58            }
59            $up.Commit()
60        }
61    }
62}
63
64$ts = New-Object Microsoft.SharePoint.Taxonomy.TaxonomySession (Get-SPSite "http://<ENTER YOUR SITE URL HERE>"),$true
65[xml]$xml = Get-Content C:\userdata.xml
66$termSets = Create-SPProfileTermSets $ts "Profile Properties" $xml
67Set-SPUserProfileProperties $termSets
68Set-SPUserProfileValues $termSets $xml

One note about the Taxonomy Session – notice that I don’t use the Get-SPTaxonomySession cmdlet – this is because I can’t stand how this cmdlet works; when you create a TaxonomySession object using the API you have the ability to force a reload from the database, thereby ignoring locally cached data, but when you use the cmdlet you don’t have this option. This can cause all kinds of issues when you are running scripts/functions like this repeatedly as what is cached may not reflect what is current and this will inevitably cause you headaches. (If you decide to use this code don’t forget to set the URL for the Taxonomy Session).

So that was my adventure for the day – as I said in the beginning, if there’s a way to do the upgrade that negates the need to do any of this please let me know; otherwise, perhaps others will benefit from today’s upgrade headaches :).