I was recently tasked with developing an application to easily add, remove and edit local standard user accounts in Windows. My goal was to replicate much of the functionality in the Local Account Management MMC snap-in, but in a more user-friendly and informative way. Imagine these screens combined, with other properties visible, such as bad logon count, last password set, password expiration date, etc.:
I started playing with the usual
DirectoryEntry objects in the System.DirectoryServices namespace, but luckily I stumbled onto this excellent post before I got too far down that path. I know this is old news, but it turns out .NET 3.5 introduced the new System.DirectoryServices.AccountManagement namespace to make managing user accounts and groups on a local machine and in Active Directory much simpler and faster. That namespace, along with the API usage examples from the aforementioned post, gave me a nice jump start into my project.
Everything was going peachy as I started implementing my own AccountManagement API. However, I was noticing some major delays in the execution of my methods to search for the Group and User principals that I am after. I didn’t want to hard-code the user or group names, so I decided to try searching by SID rather than by user or group names. Unfortunately, this didn’t improve the speed at all. Here are the methods I was using:
On XP machines (don’t ask!), each of the
FindByIdentity calls above take 2-3 seconds to complete–long enough I didn’t like it locking up my GUI thread. But the same calls in Windows 10 were taking over 20 seconds! Note that the required context for my calls is local machine (note that I found a post1 which indicated the delay may not be so bad if searching an Active Directory domain). It didn’t take long to discover a plethora of recommendations to use a
PrincipalSearcher rather than
FindByIdentity for better performance.
For example, the following code2 executes well under a second (near instantaneous in the Debugger) in both XP and 10:
Notice that in the code above I am back to searching for the
SamAccountName. Unfortunately, the
Sid member of the
User/GroupPrincipal objects is readonly, thus that member can’t be assigned as a query parameter–as others noted3. Fortunately for us, the
PrincipalSearcher class includes a
FindAll method which can be used to return all User or Group principals in the provided context. Thus, with a bit of help from LINQ (and this answer), we can easily do our own query to get the object associated with the desired SID. Here are my final implementations:
There, much better. And you can call them like this:
Again, this code executes fast enough that it’s tempting to leave it on my GUI thread (I won’t though!). Not sure what the deal is with
FindByIdentity (at least on a local machine), but these calls should work for both local machine and on a domain.
As a side note, I ran into a several gotchas when calling members of the
UserPrincipal objects. Thus, I still had to drop down to
DirectoryEntry and even native
IADsUser objects to get my final solution working right. But in the end, I could replace all of the functionality from the screenshot above and more. Maybe I’ll throw my API on GitHub someday if there’s any interest.