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 Name | Availability | Build Date |
---|---|---|
gl-importprofileproperties | MOSS 2007 | Released: 8/14/2008, Updated: 8/22/2008 |
Parameter Name | Short Form | Required | Description | Example Usage |
---|---|---|---|---|
sspname | ssp | No | The name of the SSP that the profile properties are associated with. If omitted the default SSP will be used. | -sspname SSP1 , -ssp SSP1 |
inputfile | input | Yes | The 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 |
removemissing | rm | No | Deletes 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.