The project that I’m currently on has a test environment in which many configurations were made to the user profile properties settings. I just began the process of building out their production environment and was faced with a minor issue – how do I get all the settings that have been applied to the test environments profile properties migrated to the production environment in a reliable and repeatable way? I took a look around and remembered that I already had a command to dump out the profile property settings into an XML file – gl-enumprofileproperties – so now I just needed another command that could take the output from my previous command and use it to import those settings into another farm.

Fortunately this turned out to be very easy (with one minor caveat). I called this new command gl-importprofileproperties. First lets take a look at a sample of the output generated by the gl-enumprofileproperties command:

 1<Properties>
 2  <Property>
 3    <Name>UserProfile_GUID</Name>
 4    <AllowPolicyOverride>False</AllowPolicyOverride>
 5    <ChoiceList />
 6    <ChoiceType>Off</ChoiceType>
 7    <DefaultPrivacy>Public</DefaultPrivacy>
 8    <Description />
 9    <DisplayName>Id</DisplayName>
10    <DisplayOrder>1</DisplayOrder>
11    <IsAdminEditable>False</IsAdminEditable>
12    <IsAlias>False</IsAlias>
13    <IsColleagueEventLog>False</IsColleagueEventLog>
14    <IsImported>False</IsImported>
15    <IsMultivalued>False</IsMultivalued>
16    <IsReplicable>False</IsReplicable>
17    <IsRequired>True</IsRequired>
18    <IsSearchable>True</IsSearchable>
19    <IsSection>False</IsSection>
20    <IsSystem>True</IsSystem>
21    <IsUpgrade>False</IsUpgrade>
22    <IsUpgradePrivate>False</IsUpgradePrivate>
23    <IsUserEditable>False</IsUserEditable>
24    <IsVisibleOnEditor>False</IsVisibleOnEditor>
25    <IsVisibleOnViewer>False</IsVisibleOnViewer>
26    <Length>0</Length>
27    <ManagedPropertyName>UserProfile_GUID</ManagedPropertyName>
28    <MaximumShown>10</MaximumShown>
29    <PrivacyPolicy>Mandatory</PrivacyPolicy>
30    <Separator>Unknown</Separator>
31    <Type>unique identifier</Type>
32    <URI>urn:schemas-microsoft-com:sharepoint:portal:profile:UserProfile_GUID</URI>
33    <UserOverridePrivacy>False</UserOverridePrivacy>
34    <ImportMapping />
35  </Property>
36  <Property>
37    <Name>SID</Name>
38    <AllowPolicyOverride>False</AllowPolicyOverride>
39    <ChoiceList />
40    <ChoiceType>Off</ChoiceType>
41    <DefaultPrivacy>Public</DefaultPrivacy>
42    <Description />
43    <DisplayName>SID</DisplayName>
44    <DisplayOrder>2</DisplayOrder>
45    <IsAdminEditable>False</IsAdminEditable>
46    <IsAlias>False</IsAlias>
47    <IsColleagueEventLog>False</IsColleagueEventLog>
48    <IsImported>True</IsImported>
49    <IsMultivalued>False</IsMultivalued>
50    <IsReplicable>False</IsReplicable>
51    <IsRequired>False</IsRequired>
52    <IsSearchable>False</IsSearchable>
53    <IsSection>False</IsSection>
54    <IsSystem>True</IsSystem>
55    <IsUpgrade>False</IsUpgrade>
56    <IsUpgradePrivate>False</IsUpgradePrivate>
57    <IsUserEditable>False</IsUserEditable>
58    <IsVisibleOnEditor>False</IsVisibleOnEditor>
59    <IsVisibleOnViewer>False</IsVisibleOnViewer>
60    <Length>512</Length>
61    <ManagedPropertyName>SID</ManagedPropertyName>
62    <MaximumShown>10</MaximumShown>
63    <PrivacyPolicy>OptIn</PrivacyPolicy>
64    <Separator>Unknown</Separator>
65    <Type>binary</Type>
66    <URI>urn:schemas-microsoft-com:sharepoint:portal:profile:SID</URI>
67    <UserOverridePrivacy>False</UserOverridePrivacy>
68    <ImportMapping>
69      <DSPropName>objectSID</DSPropName>
70      <ConnectionName />
71      <AssociationName />
72    </ImportMapping>
73  </Property>
74  ...
75</Properties>

As you can see from the above XML there’s really not much to – it’s just a dump of all the properties (including read-only properties) that are found on the Microsoft.Office.Server.UserProfiles.Property object. So, importing these settings back in is a simple matter of looping through the Property XML elements shown above, finding the right Property object, and then set each property value. Here’s the core code that accomplishes this:

  1/// <summary>
  2/// Imports the user profile properties using the provided input file.
  3/// </summary>
  4/// <param name="sspName">Name of the SSP.</param>
  5/// <param name="input">The input.</param>
  6/// <param name="removeMissing">if set to <c>true</c> [remove missing].</param>
  7public static void Import(string sspName, string input, bool removeMissing)
  8{
  9    ServerContext serverContext;
 10    if (string.IsNullOrEmpty(sspName))
 11        serverContext = ServerContext.Default;
 12    else
 13        serverContext = ServerContext.GetContext(sspName);
 14 
 15    UserProfileConfigManager manager = new UserProfileConfigManager(serverContext);
 16    PropertyCollection properties = manager.GetProperties();
 17 
 18    XmlDocument xmlDoc = new XmlDocument();
 19    xmlDoc.Load(input);
 20 
 21    Log("Import started at {0}", DateTime.Now.ToString());
 22 
 23    try
 24    {
 25        bool displayOrderChanged = false;
 26        foreach (XmlElement propElement in xmlDoc.SelectNodes("//Property"))
 27        {
 28            string propName = propElement.SelectSingleNode("Name").InnerText;
 29            Property prop = properties.GetPropertyByName(propName);
 30            bool isNew = false;
 31            if (prop == null)
 32            {
 33                Log("Progress: Creating property '{0}'.", propName);
 34                // We couldn't find a matching property so go ahead and create it
 35                prop = properties.Create(bool.Parse(propElement.SelectSingleNode("IsSection").InnerText));
 36                prop.Name = propName;
 37                isNew = true;
 38                SetProperties(prop, propElement, isNew, manager);
 39                properties.Add(prop);
 40            }
 41            Log("Progress: Setting properties for '{0}'.", propName);
 42            try
 43            {
 44                if (!isNew)
 45                    SetProperties(prop, propElement, isNew, manager);
 46                prop.Commit();
 47 
 48                SetDataMapping(propElement, prop, manager);
 49 
 50                Log("Progress: Property '{0}' imported.", propName);
 51            }
 52            catch (Exception ex)
 53            {
 54                Log("ERROR: {0}\r\n{1}", EventLogEntryType.Error, ex.Message, ex.StackTrace);
 55            }
 56        }
 57 
 58        if (removeMissing)
 59        {
 60            Log("Progress: Removing properties not included in the import...");
 61            PropertyCollection workColl = manager.GetProperties();
 62            foreach (Property prop in properties)
 63            {
 64                if (xmlDoc.SelectSingleNode(string.Format("//Property[Name='{0}']", prop.Name)) == null)
 65                {
 66                    Log("Progress: Removing property '{0}'.", prop.Name);
 67                    workColl.RemovePropertyByName(prop.Name);
 68                }
 69            }
 70        }
 71 
 72        Log("Progress: Setting display order...");
 73        properties = manager.GetProperties();
 74        foreach (XmlElement propElement in xmlDoc.SelectNodes("//Property"))
 75        {
 76            string propName = propElement.SelectSingleNode("Name").InnerText;
 77            Property prop = properties.GetPropertyByName(propName);
 78 
 79            if (!string.IsNullOrEmpty(propElement.SelectSingleNode("DisplayOrder").InnerText))
 80            {
 81                int displayOrder = int.Parse(propElement.SelectSingleNode("DisplayOrder").InnerText);
 82                if (displayOrder != prop.DisplayOrder)
 83                {
 84                    Log("Progress: Setting display order for '{0}' to '{1}'.", prop.Name, displayOrder.ToString());
 85                    properties.SetDisplayOrderByPropertyName(prop.Name, displayOrder);
 86                    displayOrderChanged = true;
 87                }
 88            }
 89            
 90        }
 91        if (displayOrderChanged)
 92        {
 93            Log("Progress: Committing display order.");
 94            properties.CommitDisplayOrder();
 95        }
 96    }
 97    catch (Exception ex)
 98    {
 99        Log("ERROR: {0}\r\n{1}", EventLogEntryType.Error, ex.Message, ex.StackTrace);
100    }
101    finally
102    {
103        Log("Import finished at {0}", DateTime.Now.ToString());
104    }
105}
106 
107/// <summary>
108/// Sets the properties.
109/// </summary>
110/// <param name="prop">The prop.</param>
111/// <param name="propXml">The prop XML.</param>
112/// <param name="isNew">if set to <c>true</c> [is new].</param>
113/// <param name="manager">The manager.</param>
114private static void SetProperties(Property prop, XmlElement propXml, bool isNew, UserProfileConfigManager manager)
115{
116    string displayName = propXml.SelectSingleNode("DisplayName").InnerText;
117    if (isNew)
118    {
119        prop.DisplayName = displayName;
120 
121        bool isMultivalued = bool.Parse(propXml.SelectSingleNode("IsMultivalued").InnerText);
122        if (isMultivalued != prop.IsMultivalued)
123            prop.IsMultivalued = isMultivalued;
124 
125        int length = int.Parse(propXml.SelectSingleNode("Length").InnerText);
126        if (prop.Length != length)
127            prop.Length = length;
128 
129        string type = propXml.SelectSingleNode("Type").InnerText;
130        if (type != prop.Type)
131            prop.Type = type;
132    }
133 
134    //prop.AllowPolicyOverride = bool.Parse(propXml.SelectSingleNode("AllowPolicyOverride").InnerText);
135    //prop.DisplayOrder = propXml.SelectSingleNode("DisplayOrder").InnerText;
136    //prop.IsAdminEditable = bool.Parse(propXml.SelectSingleNode("IsAdminEditable").InnerText);
137    //prop.IsImported = bool.Parse(propXml.SelectSingleNode("IsImported").InnerText);
138    //prop.IsRequired = bool.Parse(propXml.SelectSingleNode("IsRequired").InnerText);
139    //prop.IsSection = bool.Parse(propXml.SelectSingleNode("IsSection").InnerText);
140    //prop.ManagedPropertyName = propXml.SelectSingleNode("ManagedPropertyName").InnerText;
141    //prop.URI = propXml.SelectSingleNode("URI").InnerText;
142    
143    if (displayName != prop.DisplayName)
144        prop.DisplayName = displayName;
145    
146    Privacy defaultPrivacy = (Privacy) Enum.Parse(typeof (Privacy), propXml.SelectSingleNode("DefaultPrivacy").InnerText, true);
147    if (defaultPrivacy != prop.DefaultPrivacy)
148        prop.DefaultPrivacy = defaultPrivacy;
149 
150    string desc = propXml.SelectSingleNode("Description").InnerText;
151    if (desc != prop.Description)
152        prop.Description = desc;
153 
154    bool isAlias = bool.Parse(propXml.SelectSingleNode("IsAlias").InnerText);
155    if (isAlias != prop.IsAlias)
156        prop.IsAlias = isAlias;
157 
158    bool isColleagueEventLog = bool.Parse(propXml.SelectSingleNode("IsColleagueEventLog").InnerText);
159    if (isColleagueEventLog != prop.IsColleagueEventLog)
160        prop.IsColleagueEventLog = isColleagueEventLog;
161 
162    bool isReplicable = bool.Parse(propXml.SelectSingleNode("IsReplicable").InnerText);
163    if (isReplicable != prop.IsReplicable)
164        prop.IsReplicable = isReplicable;
165 
166    bool isSearchable = bool.Parse(propXml.SelectSingleNode("IsSearchable").InnerText);
167    if (isSearchable != prop.IsSearchable)
168        prop.IsSearchable = isSearchable;
169 
170    bool isUpgrade = bool.Parse(propXml.SelectSingleNode("IsUpgrade").InnerText);
171    if (isUpgrade != prop.IsUpgrade)
172        prop.IsUpgrade = isUpgrade;
173 
174    bool isUpgradePrivate = bool.Parse(propXml.SelectSingleNode("IsUpgradePrivate").InnerText);
175    if (isUpgradePrivate != prop.IsUpgradePrivate)
176        prop.IsUpgradePrivate = isUpgradePrivate;
177 
178    bool isUserEditable = bool.Parse(propXml.SelectSingleNode("IsUserEditable").InnerText);
179    if (isUserEditable != prop.IsUserEditable)
180        prop.IsUserEditable = isUserEditable;
181 
182    bool isVisibleOnEditor = bool.Parse(propXml.SelectSingleNode("IsVisibleOnEditor").InnerText);
183    if (isVisibleOnEditor != prop.IsVisibleOnEditor)
184        prop.IsVisibleOnEditor = isVisibleOnEditor;
185 
186    bool isVisibleOnViewer = bool.Parse(propXml.SelectSingleNode("IsVisibleOnViewer").InnerText);
187    if (isVisibleOnViewer != prop.IsVisibleOnViewer)
188        prop.IsVisibleOnViewer = isVisibleOnViewer;
189 
190    int maxShown = int.Parse(propXml.SelectSingleNode("MaximumShown").InnerText);
191    if (maxShown != prop.MaximumShown)
192        prop.MaximumShown = maxShown;
193 
194    PrivacyPolicy privacyPolicy = (PrivacyPolicy)Enum.Parse(typeof(PrivacyPolicy), propXml.SelectSingleNode("PrivacyPolicy").InnerText, true);
195    if (privacyPolicy != prop.PrivacyPolicy)
196        prop.PrivacyPolicy = privacyPolicy;
197 
198    MultiValueSeparator separator = (MultiValueSeparator)Enum.Parse(typeof(MultiValueSeparator), propXml.SelectSingleNode("Separator").InnerText, true);
199    if (separator != prop.Separator)
200        prop.Separator = separator;
201 
202    bool userOverridePrivacy = bool.Parse(propXml.SelectSingleNode("UserOverridePrivacy").InnerText);
203    if (userOverridePrivacy != prop.UserOverridePrivacy)
204        prop.UserOverridePrivacy = userOverridePrivacy;
205 
206    ChoiceTypes choiceType = (ChoiceTypes)Enum.Parse(typeof(ChoiceTypes), propXml.SelectSingleNode("ChoiceType").InnerText, true);
207    if (choiceType != prop.ChoiceType)
208        prop.ChoiceType = choiceType;
209 
210 
211    foreach (XmlElement choiceXml in propXml.SelectNodes("ChoiceList/Choice"))
212    {
213        if (prop.ChoiceList.FindTerms(choiceXml.InnerText, ChoiceListSearchOption.ExactMatch).Length == 0)
214        {
215            prop.ChoiceList.Add(choiceXml.InnerText);
216            // Settng choice list values doesn't mark the property as dirty so we have to manually mark it using reflection.
217            Utilities.SetFieldValue(prop, typeof(Property), "m_fIsChanged", true);
218        }
219    }
220}
221 
222/// <summary>
223/// Sets the data mapping.
224/// </summary>
225/// <param name="propXml">The prop XML.</param>
226/// <param name="prop">The prop.</param>
227/// <param name="manager">The manager.</param>
228private static void SetDataMapping(XmlElement propXml, Property prop, UserProfileConfigManager manager)
229{
230    PropertyMapCollection propertyMapping = manager.GetDataSource().PropertyMapping;
231    if (propXml.SelectSingleNode("ImportMapping") != null && propXml.SelectSingleNode("ImportMapping").ChildNodes.Count > 0)
232    {
233        PropertyMap map = propertyMapping[prop.Name];
234        
235        string dsPropName = propXml.SelectSingleNode("ImportMapping/DSPropName").InnerText;
236        string connectionName = propXml.SelectSingleNode("ImportMapping/ConnectionName").InnerText;
237        string associationName = propXml.SelectSingleNode("ImportMapping/AssociationName").InnerText;
238 
239        // Remove any mappings that are assigned to the current mapping.
240        if (!string.IsNullOrEmpty(dsPropName))
241        {
242            foreach (PropertyMap tempMap in propertyMapping)
243            {
244                if (tempMap.DSPropName == dsPropName && 
245                    tempMap.ConnectionName == connectionName && 
246                    tempMap.AssociationName == associationName && 
247                    tempMap.PropName != prop.Name)
248                {
249                    Log("Progress: Removing mapping associated with '{0}' so that it may be assigned to '{1}'.",
250                        tempMap.PropName, prop.Name);
251                    propertyMapping.Remove(tempMap.PropName);
252                    propertyMapping = manager.GetDataSource().PropertyMapping;
253                    break;
254                }
255            }
256        }
257 
258        if (map == null)
259        {
260            Log("Progress: Adding import mapping to '{0}'.", prop.Name);
261            propertyMapping.Add(prop.Name, dsPropName, connectionName, associationName);
262        }
263        else
264        {
265            Log("Progress: Updating import mapping for '{0}'.", prop.Name);
266            if (map.DSPropName != dsPropName)
267                map.DSPropName = dsPropName;
268 
269            if (map.ConnectionName != connectionName)
270                map.ConnectionName = connectionName;
271 
272            if (map.AssociationName != associationName)
273                map.AssociationName = associationName;
274 
275            map.Commit();
276        }
277    }
278    else if (propertyMapping[prop.Name] != null)
279    {
280        Log("Progress: Removing import mapping for '{0}'.", prop.Name);
281        propertyMapping.Remove(prop.Name);
282    }
283}

So I mentioned that there was one caveat that I discovered – turns out that it’s more of a bug. If you do an export and then immediately import using this command you will likely get an exception when it comes to importing the SPS-ProxyAddresses field. Here’s the specific exception you will see:

Progress: Setting properties for 'SPS-ProxyAddresses'.
Progress: Updating import mapping for 'SPS-ProxyAddresses'.
ERROR: The character length specified is invalid.
   at Microsoft.Office.Server.UserProfiles.Property._TypeValidate(DBAction action)
   at Microsoft.Office.Server.UserProfiles.Property._TypeValidate(IEnumerable enumAddPropertyList, IEnumerable enumUpdatePropertyList)
   at Microsoft.Office.Server.UserProfiles.Property._Update(SRPSite site, IEnumerable enumAddPropertyList, IEnumerable enumUpdatePropertyList, IEnumerable enumRemovePropertyList)
   at Microsoft.Office.Server.UserProfiles.Property.Commit()
   at Lapointe.SharePoint.STSADM.Commands.UserProfiles.ImportProfileProperties.Execute(String command, StringDictionary keyValues, String& output)

So what’s the deal with this? Simple – it’s a bug. Here’s another way you can expose the bug – via the browser go to your user profile properties page and then click to edit the SPS-ProxyAddresses field. Then, on the edit screen, simply click the “OK” button without making any changes – you’ll get the following error:

The problem is that this particular field is a multi-value field which can only have a maximum length of 400 but the field length is set to 2048 and because it’s a built-in system field we can’t change the length to fix it and therefore we can’t change anything about the property.

So, to deal with this I made it so that my code simply dumps out errors like this and moves on to the next item – but you will see this error every time you attempt an import – there’s no way around it (you can’t change the length of an existing field so you can’t fix the problem to prevent the error).

The help for the command is shown below:

C:\>stsadm -help gl-importprofileproperties

stsadm -o gl-importprofileproperties


Imports user profile properties using the output of gl-enumprofileproperties.

Parameters:
        -inputfile <file to import results from>
        [-sspname <name of the SSP>]
        [-removemissing (removes properties missing from the import)

The following table summarizes the command and its various parameters:

Command NameAvailabilityBuild Date
gl-importprofilepropertiesMOSS 2007Released: 8/14/2008, Updated: 8/22/2008
Parameter NameShort FormRequiredDescriptionExample Usage
sspnamesspNoThe name of the SSP that the profile properties are associated with. If omitted the default SSP will be used.-sspname SSP1, -ssp SSP1
inputfileinputYesThe path to the XML file to use for import. The XML must correspond to the XML generated by the gl-enumprofileproperties command.-inputfile c:\properties.xml, -input c:\properties.xml
removemissingrmNoDeletes any properties that were not defined in the import file. It’s recommended that if you use this flag that you first backup the existing properties using the gl-enumprofileproperties command.-removemissing, -rm

The following is an example of how to import user profile properties:

stsadm -o gl-importprofileproperties -inputfile c:\properties.xml

Update 8/22/2008: I fixed a few bugs that were preventing the importing of new properties. I also fixed it so that data mappings can be reassigned without having to run multiple times. I also added support for setting the display order and for removing properties that do not exist in the import by passing in the -removemissing parameter.