Identities and Work Item Tracking in TFS 2015
In Team Foundation Server 2015 we introduced identity fields which made significant changes to how the Work Item Tracking (WIT) system handles identity values. These changes help modernize the underlying system and provide support for upcoming features . However, the changes result in API and data format differences that TFS users (especially those writing third-party tools with ClientOM or REST) need to be aware of.
Historically, the WIT system didn’t handle identities well. Before TFS 2015, all fields that dealt with identities treated identities as just plain strings that represented a user’s display name. This resulted in some odd situations especially around ambiguous identities. If you attempted to assign a work item to “Jamal Hartnett”, and two users named “Jamal Hartnett” existed in the system, the system would not ask you which one you meant. Instead, it would create a third “virtual” Jamal Hartnett and assign the work item to him. This inability to disambiguate similarly named accounts presents a problem for the WIT system with repercussions in query, charting, assignment, and capacity.
The TFS 2015 WIT system now distinguishes identity fields separate from simply plain text fields, however, they come with some new behaviors and restrictions.
- Identity fields are intended to uniquely identify identities.
- They still support non-identity values (like “Not Yet Assigned”) but these are not recommended.
- Identity fields are determined by examining what rules are set on a field (except for the core identity fields like AssignedTo, ChangedBy,CreatedBy which are always considered identity fields regardless of rules).
In order to support existing process templates, we use an inference approach to determine identity fields. We consider a field an identity field if it contains any of the following:
- A valid user rule
- A group scoping allowed or prohibited value rule
- An individual user allowed or prohibited value rule
For example, the following field definition defines an identity field because of the ValidUser rule:
<FIELD name="My Identity" refname="My.Identity" type="String" syncnamechanges="true" > <br /> <ALLOWEXISTINGVALUE /> <br /> <VALIDUSER /> <br /></FIELD>
This one makes use of an Allowed Values rule to specify an identity:
<FIELD name="Another Identity" refname="Another.Identity" type="String" syncnamechanges="true" ><br /> <ALLOWEDVALUES> <br /> <LISTITEM value="[global]Project Collection Valid Users" /> <br /> </ALLOWEDVALUES><br /> </FIELD>
Both of the examples show the syncnamechanges property set to true. It is important to set this to true for identity fields to ensure they work correctly when a user changes their name. If you have an identity field with this property not set you can change it (as of TFS 2015 Update 1) using witadmin.
Note: Identity fields are scoped to the entire team project collection. If one work item type indicates that a field is an identity, this field becomes an identity field for all types in the collection.
Once the system marks a field as an identity field several changes occur: it gets a new UI control and the REST API and ClientOM change what values they accept and return.
All identity fields get a new picker which treats identities as first class values. In the web portal, this picker contains an MRU (most recently used) + the ability to search for all other identities.
- Consistent with how majority of users are using identities
- Improved Performance
- Support for Azure Active Directory (AAD)
The data returned and accepted by WIT REST API’s for identity fields has changed. Reading a value for an identity now returns a combo-string that contains the user’s display name followed by their unique name in brackets. For example, the following GET response CreatedBy field contains the value “Jamal Hartnett
When posting an update to a work item or querying work items, the REST API’s now accept that same combo-string in order to unambiguously refer to an identity.
For query you can post WIQL like the following to find all work items assigned to “Jamal Hartnett
By passing the combo-string, only the identities assigned to this particular identity are returned. However, if you were to omit the email/alias portion of the string and just pass the display name, it would still work as expected except if the name was ambiguous. If there were two Jamal Hartnett’s, then the following query would return items from both. This makes sense because we are not uniquely identifying which one we want:
Updating an identity field using the REST API is similar. It is best to pass the combo-string to unambiguously refer to an identity. However, if you pass just the display name it would still work correctly unless that name is ambiguous. If you post the following update to set System.AssignedTo to Jamal Hartnett and he was an ambiguous identity, you would get an error message explaining that.
"message": "The value 'Jamal Hartnett' for field 'Assigned To' is ambiguous with 'Jamal Hartnett ;Jamal Hartnett '. Provide a unique name for this field." <br />"typeName": "Microsoft.TeamFoundation.WorkItemTracking.Server.WorkItemFieldInvalidException, Microsoft.TeamFoundation.WorkItemTracking.Server, Version=126.96.36.199, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" <br />"typeKey": "WorkItemFieldInvalidException"
If you pass the combo-string in this case it would resolve the issue and save correctly.
Identity fields in the TFS 2015 version of the WIT ClientOM have changed to ensure that identity values are always read/written uniquely. Because of how the ClientOM is architected, embedding the flexibility of multiple data formats without breaking existing applications presented a challenge. For this reason, the identity strings in the ClientOM behave differently than in the RESTAPI.
When you read from an identity field and that user’s display name is unique you will get the display name returned:
var workItemStore = teamProjectCollection.GetService();<br /> var workItem = workItemStore.GetWorkItem(1);<br /> Console.WriteLine(workItem.Fields["System.AssignedTo"].Value); // Jamal Hartnett
However, if the system has more than one Jamal Hartnett the field returns the combo-string:
var workItemStore = teamProjectCollection.GetService();<br /> var workItem = workItemStore.GetWorkItem(1);<br /> Console.WriteLine(workItem.Fields["System.AssignedTo"].Value); // Jamal Hartnett <email@example.com>
The ClientOM tries to only show the combo-string when needed. However, it also requires the correct value when assigning an identity field. So, if you’re assigning a value to an identity field, you need to make sure you pass the proper value. Fortunately, there is a helper function to make this easy called GetValidDisplayName. This method has two overloads, one takes a TeamFoundationIdentity object and the other takes a DisplayName and UniqueName.
Before assigning to an identity field you should call this method to ensure you are getting the right value to assign to.
var identityService = teamProjectCollection.GetService(); <br /> var user = identityService.ReadIdentity(IdentitySearchFactor.AccountName, "firstname.lastname@example.org", MembershipQuery.None, ReadIdentityOptions.None); <br /> workItem.Fields["System.AssignedTo"].Value = workItemStore.GetValidDisplayName(user);
workItem.Fields["System.AssignedTo"].Value = workItemStore.GetValidDisplayName("Jamal Hartnett", "email@example.com");