C# Active Directory calls very slow - c#

I have an application that needs to have a list of names and email addresses of users in a specific security group. I am currently doing this with the code below. When I run on VPN it comes back right away within a second or two usually but when I run on either on ethernet or wireless (both on the domain), it takes about 40 seconds for this to come back. Is there any way I can improve the time of this method on ethernet or wireless?
...
DirectoryEntry entry = new DirectoryEntry(ldap);
DirectorySearcher mySearcher = new DirectorySearcher(entry);
mySearcher.Filter = "(&(objectCategory=group)(objectClass=group)(groupType:1.2.840.113556.1.4.803:=2147483648))";
mySearcher.PropertiesToLoad.Add("member");
SearchResultCollection results = mySearcher.FindAll();
foreach (SearchResult result in results)
{
foreach (string distinguishedMember in result.Properties["member"])
{
string memberPath = "LDAP://" + distinguishedMember;
DirectoryEntry member = new DirectoryEntry(memberPath);
DirectorySearcher Searcher = new DirectorySearcher(member);
Searcher.Filter = "(&(objectCategory=user))";
Searcher.PropertiesToLoad.Add("mail");
Searcher.PropertiesToLoad.Add("name");
SearchResult memberFound = Searcher.FindOne();
if (memberFound != null)
{
String memberEmail = memberFound.Properties["mail"][0].ToString();
String memberName = memberFound.Properties["name"][0].ToString();
users.Add(new KeyValuePair<String, String>(memberName, memberEmail));
}
}
}

Maybe it would help to get all of the users in one go, instead of fetching them one by one*:
Searcher.Filter = "(&(objectCategory=user)(memberOf=" + myGroupsDistinguishedName + "))"
Searcher.PropertiesToLoad.Add("mail");
Searcher.PropertiesToLoad.Add("name");
var allMembers = Searcher.FindAll();
var users = allMembers.Cast<SearchResult>().ToDictionary(sr=>sr.Properties["name"].ToString(), sr=>sr.Properties["mail"].ToString());
*This doesn't handle scenarios with over 1000 users.

Do not fetch all members at one time. Instead, I recommend using the pagesize property of the DirectorySearcher class:
mySearcher.PageSize = 10;

Related

How can I improve the performance for a method

Ive wrote a function to get all Groups with the Members from AD:
public static void getGroupsWithUsers() {
String currentDomain = Domain.GetCurrentDomain().ToString();
using (PrincipalContext context = new PrincipalContext(ContextType.Domain, currentDomain))
{
using (PrincipalSearcher searcher = new PrincipalSearcher(new UserPrincipal(context)))
{
foreach (var result in searcher.FindAll())
{
DirectoryEntry de = result.GetUnderlyingObject() as DirectoryEntry;
var SID = WindowsIdentity.GetCurrent().User.ToString();
// find a user
UserPrincipal user = UserPrincipal.FindByIdentity(context, de.Properties["samAccountName"].Value.ToString());
if (user != null)
{
// get the user's groups
var groups = user.GetAuthorizationGroups();
foreach (GroupPrincipal group in groups)
{
Console.WriteLine("User: " + user + " is in Group: " + group);
}
}
}
}
}
}
The execution time is around 1,5 seconds for a small amount of data.
Are there any improvements I could make to get the method faster?
I ask because if I execute the function for 1 million users or groups that it will take forever.
The bulk of your problem is what you're doing in the loop. You're taking the search result, converting it to a DirectoryEntry, then going back out to the network to find that account that you already just found.
The PrincipalSearcher.FindAll method returns a collection of Principal objects that you can cast to the type you know they are. In this case, you know it will only return users, so you can cast the result to UserPrincipal in your loop.
You're also assigning a SID variable that you're not using (although this is likely not taking any significant time).
A side note: You don't need that currentDomain variable here, since if you create a PrincipalContext with ContextType.Domain and no domain name, it will automatically use the current domain.
So your code can be simplified to this:
using (PrincipalContext context = new PrincipalContext(ContextType.Domain))
using (PrincipalSearcher searcher = new PrincipalSearcher(new UserPrincipal(context)))
{
foreach (UserPrincipal user in searcher.FindAll())
{
// get the user's groups
var groups = user.GetAuthorizationGroups();
foreach (GroupPrincipal group in groups)
{
Console.WriteLine("User: " + user + " is in Group: " + group);
}
}
}
That will likely cut the time quite a bit. But if you really want to find the best performance possible, you need to use DirectorySearcher and DirectoryEntry directly. The PrincipalSearcher/UserPrincipal classes use those in the background anyway, but you lose a lot of control over what is happening in the background, and that's the key to performance. For maximum performance, you need to cut down on:
the number of network requests, and
how much data is being sent back.
I wrote a couple of articles on this subject that can help you with that, if you'd like to go down that route:
Active Directory: Better performance
Finding all of a user’s groups
Try to split operations: first - get info to string builder, second - print it.
Like this
public static void getGroupsWithUsers()
{
String currentDomain = Domain.GetCurrentDomain().ToString();
var stringBuilder = new StringBuilder();
using (PrincipalContext context = new PrincipalContext(ContextType.Domain, currentDomain))
{
using (PrincipalSearcher searcher = new PrincipalSearcher(new UserPrincipal(context)))
{
foreach (var result in searcher.FindAll())
{
DirectoryEntry de = result.GetUnderlyingObject() as DirectoryEntry;
var SID = WindowsIdentity.GetCurrent().User.ToString();
// find a user
UserPrincipal user = UserPrincipal.FindByIdentity(context, de.Properties["samAccountName"].Value.ToString());
if (user != null)
{
// get the user's groups
var groups = user.GetAuthorizationGroups();
foreach (GroupPrincipal group in groups)
{
stringBuilder.AppendLine($"User: {user} is in Group: {group}");
}
}
}
}
}
string result = stringBuilder.Length > 0
? stringBuilder.ToString()
: "No users were found";
Console.WriteLine(result);
}

How to search a user that has a letter (or a few) in his display name

I'll try to explain myself better.
I'm using C# to build a mini-program to myself. I want to search a user within my active directory, but I want to search a user without a full display name. Let me explain myself.
For example, my display name can be: "David Holonka\Jeramy".
Is there a way to search the letters "lonka" and it'll find me all the users that has these combinations of letters within their display name?
My current code:
using (var pc = new PrinicpalContext(ContextType.Domain, "MyDomain"))
{
UserPrincipal user = new UserPrinicpal(pc);
User.DisplayName = "Holonka";
PrinicpalSearcher scrh = new PrinicpalSearcher(user);
Prinicpal found = scrh.FindOne();
}
}
Right now it doesn't find anything becasue there isn't a user that its display name is only "Holonka", but I want it to find the user that i've mentioned before
Thank you very much!
You can use query string in the PrinicpalSearcher:
UserPrincipal user = new UserPrinicpal(pc);
User.DisplayName = "*Holonka*";
PrinicpalSearcher searcher = new PrinicpalSearcher(user);
var results = searcher.FindAll();
You can also using PrincipalSearcher to find users with “or” parameters like in the example below:
List<UserPrincipal> searchPrinciples = new List<UserPrincipal>();
searchPrinciples.Add(new UserPrincipal(context) { DisplayName="*Holonka*"});
searchPrinciples.Add(new UserPrincipal(context) { SamAccountName = "*Holonka*" });
searchPrinciples.Add(new UserPrincipal(context) { MiddleName = "*Holonka*" });
searchPrinciples.Add(new UserPrincipal(context) { GivenName = "*Holonka*" });
List<Principal> results = new List<Principal>();
foreach (var item in searchPrinciples)
{
var searcher = new PrincipalSearcher(item);
// Results may contains duplicate values because of separate searchers can handle the same user
results.AddRange(searcher.FindAll());
}
You can also try using Ambiguous Name Resolution, which is a special AD query that looks for partial matches in several attributes (the list of those attributes is in that article).
You can't use PrincipalSearcher do use ANR though. You have to use DirectorySearcher (which is what PrincipalSearcher uses in the background anyway).
Here's an example:
var searchRoot = new DirectoryEntry("LDAP://domain.com");
var searcher = new DirectorySearcher(searchRoot) {
Filter = "(anr=Holonka)"
};
searcher.PropertiesToLoad.Add("displayName");
using (var results in = searcher.FindAll()) {
foreach (SearchResult result in results) {
if (result.Properties.Contains("displayName")) {
var displayName = (string) result.Properties["displayName"][0];
//do something else
}
}
}
When using DirectorySearcher, it's wise to use PropertiesToLoad. If you don't, it will return every attribute that has a value, which is likely way more data than you need. It's a waste of network traffic and time.
The using statement is also wise, since SearchResultCollection can't be completely cleaned up by the garbage collector.
I wrote some more about that in an article I wrote: Active Directory: Better Performance

Trying to pull 'ManagedBy' property from Computer object in AD

I've found some examples here and there, but I can't seem to find one that addresses retrieving the managedby property from a computer record. (I don't know the username, but it's stored in the managedby)
DirectoryEntry searchRoot = new DirectoryEntry("LDAP://"+lblDomain.Text);
DirectorySearcher search = new DirectorySearcher(searchRoot);
search.Filter = ("(objectClass=computer)");
search.PropertiesToLoad.Add("managedBy");
search.PropertiesToLoad.Add("distinguishedName");
search.PropertiesToLoad.Add("cn");
SearchResultCollection groups = search.FindAll();
foreach (SearchResult sr in groups)
{
if (sr.Properties.Contains("managedby"))
{
lblManagedBy.Text=(sr.Properties["managedBy"][0].ToString());
}
else
{
lblManagedBy.Text = "No owner specified in ManagedBy";
}
}
Your example works fine for me, so a few things to check.
1) Is the domain name correct in lblDomain.Text? Could you provide an example of what you are using for it? Fake names are fine.
2) Do you actually have computer accounts with the Managed By set? It is empty by default.
Other than that, I used you exact code and got back the DN of the user I assigned to a computer account, so it's probably in how you are connecting.
search.Filter = "(&(objectClass=computer)(cn=pcname))";
DirectoryEntry searchRoot = new DirectoryEntry("LDAP://"+lblDomain.Text);
DirectorySearcher search = new DirectorySearcher(searchRoot);
search.Filter = "(&(objectClass=computer)(name=" + host + "))";
search.PropertiesToLoad.Add("managedBy");
search.PropertiesToLoad.Add("distinguishedName");
search.PropertiesToLoad.Add("cn");
SearchResultCollection groups = search.FindAll();
foreach (SearchResult sr in groups)
{
if (sr.Properties["managedBy"].Count > 0)
{
lblManagedBy.Text=(sr.Properties["managedBy"][0].ToString());
}
else
{
lblManagedBy.Text = "No owner specified in ManagedBy";
}
}

Active Directory List OU's

I have this code currently,
string defaultNamingContext;
DirectoryEntry rootDSE = new DirectoryEntry("LDAP://RootDSE");
defaultNamingContext = rootDSE.Properties["defaultNamingContext"].Value.ToString();
rootDSE = new DirectoryEntry("LDAP://" + defaultNamingContext);
//DirectoryEntry domain = new DirectoryEntry((string)"LDAP://" + defaultNamingContext);
DirectorySearcher ouSearch = new DirectorySearcher(rootDSE,"(objectCategory=Organizational-Unit)",
null, SearchScope.Subtree);
MessageBox.Show(rootDSE.Path.ToString());
try
{
SearchResultCollection collectedResult = ouSearch.FindAll();
foreach (SearchResult temp in collectedResult)
{
comboBox1.Items.Add(temp.Properties["name"][0]);
DirectoryEntry ou = temp.GetDirectoryEntry();
}
}
When i use the debugger i can see that rootDSE.Path is infact pointing to the right place, in this case DC=g-t-p,DC=Local but the directory searcher doesn't find any results. Can anyone help?
Stephen - my bad - for some reason, the search using objectCategory doesn't work.
Even though the objectCategory is displayed as CN=Organizational-Unit, for searching, you still need to use the same value as for the objectClass:
So try to use the filter (objectCategory=organizationalUnit) - that definitely works for me!
UPDATE: in order to get some properties in your search result (in order to display them in the combo box), you need to include those when you create the DirectorySearcher:
DirectorySearcher ouSearch = new DirectorySearcher(rootDSE);
ouSearch.Filter = "(objectCategory=Organizational-Unit)";
ouSearch.SearchScope = SearchScope.Subtree;
ouSearch.PropertiesToLoad.Add("name");
// add more properties if you want to ...
With this, you should definitely be able to grab the temp.Properties["name"][0] and stick it into the combobox's list of items.
I don't really see what you need the line
DirectoryEntry ou = temp.GetDirectoryEntry();
after grabbing the name property .....

Correct method to search for AD user by email address from .NET

I'm having some issues with code that is intended to find a user in Active Directory by searching on their email address. I have tried 2 methods but I'm sometimes finding that the FindOne() method will not return any results on some occasions. If I look up the user in the GAL in Outlook I see the SMTP email address listed.
My end goal is to confirm that the user exists in AD. I only have the email address as search criteria, so no way to use first or last name.
Method 1: Using mail property:
DirectorySearcher search = new DirectorySearcher(entry);
search.Filter = "(mail=" + email + ")";
search.PropertiesToLoad.Add("mail");
SearchResult result = search.FindOne();
Method 2: proxyAddresses property:
DirectorySearcher search = new DirectorySearcher(entry);
search.Filter = "(proxyAddresses=SMTP:" + email + ")"; // I've also tried with =smtp:
search.PropertiesToLoad.Add("mail");
SearchResult result = search.FindOne();
I've tried changing the case of the email address input but it still does not return a result. Is there a problem here with case sensitivity? If so, what is the best way to resolve it?
If you are using Exchange Server, proxyAddresses is the most reliable means of getting their email address. The primary smtp address is indicated by all caps "SMTP:" and additional email addresses will be prefixed with lowercase "smtp:". The attribute "mail" does not necessarily have to be the primary SMTP address, though usually it is.
Here is a variation of some code I used:
public static SearchResult FindAccountByEmail(string email)
{
string filter = string.Format("(proxyaddresses=SMTP:{0})", email);
using (DirectoryEntry gc = new DirectoryEntry("GC:"))
{
foreach (DirectoryEntry z in gc.Children)
{
using (DirectoryEntry root = z)
{
using (DirectorySearcher searcher = new DirectorySearcher(root, filter, new string[] { "proxyAddresses", "objectGuid", "displayName", "distinguishedName" }))
{
searcher.ReferralChasing = ReferralChasingOption.All;
SearchResult result = searcher.FindOne();
return result;
}
}
break;
}
}
return null;
}
static void Main(string[] args)
{
SearchResult result = FindAccountByEmail("someone#somewhere.com");
string distinguishedName = result.Properties["distinguishedName"][0] as string;
string name = result.Properties["displayName"] != null
? result.Properties["displayName"][0] as string
: string.Empty;
Guid adGuid = new Guid((byte[]) (result.Properties["objectGUID"][0]));
string emailAddress;
var emailAddresses = (from string z in result.Properties["proxyAddresses"]
where z.StartsWith("SMTP")
select z);
emailAddress = emailAddresses.Count() > 0 ? emailAddresses.First().Remove(0, 5) : string.Empty;
Console.WriteLine(string.Format("{1}{0}\t{2}{0}\t{3}{0}\t{4}",
Environment.NewLine,
name,
distinguishedName,
adGuid,
emailAddress));
}
I've found that using SysInternals ADExplorer is great for testing out/debugging Active Directory queries. As you can build the queries and run them against Active Directory you can see the results as well as easily view objects and see all their properties...
I've never had any issues with case sensitivity searching for users' email addresses - what happens if you search for the address, exactly as it appears in ADSIEDIT? Does it find the address when it is cased correctly?
BTW, I've always used the "mail" property, as it returns the user's single default outgoing email address, even if there are multiple addresses attached to the account. The "proxyAddresses" property is actually a multi-value property, and you're just searching for the value that starts with "smtp:" (it's lowercase in the property). However, a user could have multiple SMTP addresses on their AD account (we do), so between the two, the "mail" property may be what you're looking for.
var ds = new DirectorySearcher(new DirectoryEntry(strLDAPDomain));
ds.Filter = "(&(|(objectClass=User)(objectCategory=Person))(anr=" + user.userId + "))";// "(Name=*" + search + "*)";
ds.PropertiesToLoad.Add("mail");
var results = ds.FindOne();
if (results.Properties["mail"].Count > 0)
{
objUserInfo.UserEmail = results.Properties["mail"][0].ToString();
}
else
{
objUserInfo.UserEmail = user.useridID + "#gmail.com";
}

Categories

Resources