Since no solution has worked, I decided to see what could be done from a coding perspective. The answer turned out to be straightforward. NOTE: Please make sure that you check the solutions provided below prior to the coding approach, as Azure DevOps Server is supposed to be refreshing identities automatically.
First, I found a Stack Overflow article about finding users by name:
TFS get user by name
This can be used to fetch a user or a group by its display name, among other attributes, using the ReadIdentity method.
This same IIDentityServiceProvider also has a method on it called RefreshIdentity. This method, when called with the IdentityDescriptor of the user, forces the identity to be immediately refreshed from its provider. See the documentation here:
https://docs.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2013/ff734945(v=vs.120)?redirectedfrom=MSDN
This method returns true if the refresh was successful, or false if the refresh failed. it is also possible for the refresh to throw an exception. For example, the Azure DevOps identity named "Project Collection Build Service" is listed as a user when retrieved, but this identity throws an exception when refreshed.
Using these methods, a complete tool was able to be constructed to repair the identities of individual users, or to scan through all users in the group "Project Collection Valid Users" and refresh the entire system. Using this tool, we were able to fix our synchronization issues between Azure DevOps Server and Active Directory.
Here's some sample code showing how to use these methods:
string rootSourceControlUrl = "TODO: Root URL of Azure DevOps";
string projectCollection = "TODO: Individual project collection within Azure DevOps";
TfsTeamProjectCollection tfsCollection = TfsTeamProjectCollectionFactory.GetTeamProjectCollection(new Uri($"{rootSourceControlUrl}/{projectCollection}"));
IIdentityManagementService ims = (IIdentityManagementService)tfsCollection.GetService(typeof(IIdentityManagementService));
TeamFoundationIdentity foundUser = ims.ReadIdentity(IdentitySearchFactor.DisplayName,
"TODO: Display name of user", MembershipQuery.Direct,
ReadIdentityOptions.ExtendedProperties);
if(foundUser != null)
{
try
{
if (ims.RefreshIdentity(foundUser.Descriptor))
{
// Find the user by its original IdentityDescriptor, which shouldn't change during the refresh
TeamFoundationIdentity refreshedUser = ims.ReadIdentity(foundUser.Descriptor,
MembershipQuery.Direct, ReadIdentityOptions.ExtendedProperties);
// TODO : Display changes from foundUser to refreshedUser, using individual properties
// and the method foundUser.GetProperties(), which returns an
// IEnumerable<KeyValuePair<string, object>> collection.
}
else
{
// TODO : Notify that user failed to refresh
}
}
catch(Exception exc)
{
// TODO : Notify that exception occurred
}
}
else
{
// TODO : Notify that user was not found
}