Slow AD properties retrieval in C#
Hi all I'm porting over my VBScript over to C#. And I ran into a problem that Active Directory properties retrieval is much slower in C#.
This is my incomplete C# code
foreach(string s in dictLast.Keys)
{
if(s.Contains("/"))
str = s.Insert(s.IndexOf('/'), "\\");
else
str = s;
dEntry = new DirectoryEntry("LDAP://" + str);
strUAC = dEntry.Properties["userAccountControl"].Value.ToString();
cmd.CommandText = "INSERT INTO [NOW](readTime) VALUES(\"" + test.Elapsed.Milliseconds.ToString() + "\")";
cmd.ExecuteNonQuery();
test.Reset();
test.Start();
}
If I comment out this line. strUAC = dEntry.Properties["userAccountControl"].Value.ToString();
It runs at 11 secs. But if I don't, it runs at 2mins 35 secs. The number of records are 3700. On average each record runs at 50 secs. I'm using the Stopwatch Class.
My VBscript runs at only 39 secs (Using difference of Time). With each record either a 0 or 15 milliseconds. I'm using the difference of Timer().
Here's my VBscript
strAttributes = "displayName, pwdLastSet, whenCreated, whenChanged, userAccountControl"
For Each strUser In objList.Keys
prevTime = Timer()
strFilter = "(sAMAccountName=" & strUser & ")"
strQuery = strBase & ";" & strFilter & ";" & strAttributes & ";subtree"
adoCommand.CommandText = strQuery
Set adoRecordset = adoCommand.Execute
On Error Resume Next
If (adoRecordset.Fields("displayName") = null) Then
strCN = "-"
Else
strCN = adoRecordset.Fields("displayName")
End If
If (Err.Number <> 0) Then
MsgBox(strUser)
End If
strCr8 = DateAdd("h", 8, adoRecordset.Fields("whenCreated"))
strUAC = adoRecordset.Fields("userAccountControl")
If (strUAC AND ADS_UF_DONT_EXPIRE_PASSWD) Then
strPW = "Never expires"
Else
If (TypeName(adoRecordset.Fields("pwdLastSet").Value) = "Object") Then
Set objDate = adoRecordset.Fields("pwdLastSet").Value
dtmPwdLastSet = Integer8Date(objDate, lngBias)
Else
dtmPwdLastSet = #1/1/1601#
End If
If (dtmPwdLastSet = #1/1/1601#) Then
strPW = "Must Change at Next Logon"
Else
strPW = DateAdd("d", sngMaxPwdAge, dtmPwdLastSet)
End If
End If
retTime = Timer() - prevTime
If (objList.Item(strUser) = #1/1/1601#) Then
Wscript.Echo strCN & ";" & strUser & ";" & strPW & ";" & strCr8 & ";" & ObjChange.Item(strUser) & ";0;" & strUAC & ";" & retTime
Else
Wscript.Echo strCN & ";" & strUser & ";" & strPW & ";" & strCr8 & ";" & ObjChange.Item(strUser) & ";" & objList.Item(strUser) & ";" & strUAC & ";" & retTime
End If
Next
Any ideas what's the prob开发者_运维百科lem? Please tell me if I'm not giving enough information. Thank you.
DirectorySearcher way. 1 min 8 secs.
dEntry = new DirectoryEntry("LDAP://" + strDNSDomain);
string[] strAttr = {"userAccountControl"};
foreach(string s in dictLast.Keys)
{
if(s.Contains("/"))
str = s.Insert(s.IndexOf('/'), "\\");
else
str = s;
ds = new DirectorySearcher(de, "(sAMAccountName=" + s + ")", strAttr, SearchScope.Subtree);
ds.PropertiesToLoad.Add("userAccountControl");
SearchResult rs = ds.FindOne();
strUAC = rs.Properties["userAccountControl"][0].ToString();
cmd.CommandText = "INSERT INTO [NOW](readTime) VALUES(\"" + test.Elapsed.Milliseconds.ToString() + "\")";
cmd.ExecuteNonQuery();
test.Reset();
test.Start();
}
where strDNSDomain is the defaultNamingContext. I've tried with domain name but it runs worse, at 3 mins 30 secs.
Looking at someone else's code. Would there be a difference if we omit the domain part?
using (var LDAPConnection = new DirectoryEntry("LDAP://domain/dc=domain,dc=com", "username", "password"))
And just use "LDAP://dc=domain,dc=com" instead.
Work around. Instead of binding each user and getting the properties. I stored all the properties in the first search for lastLogon instead. And output using StreamWriter.
Both DirectoryEntry
and ADOConnection
use ADSI underlying. There shouldn't be any performance difference.
The only reason for the performance difference is that you are trying to retrieve two different sets of data.
In your VBScript, you are setting ""displayName, pwdLastSet, whenCreated, whenChanged, userAccountControl" to strAttributes
. ADSI is going to load these five attributes back from AD only.
In your C# code, you didn't call the RefreshCache method to specify what attributes that you like to load. So, when you access DirectoryEntry.Properties
, it automatically calls a RefreshCache()
for you without passing in any attributes for you. By default, ADSI will return all non-constructed attributes to you (pretty much all attributes) if you don't specify what attributes to load.
Another problem is that in your VBscript, you run only one LDAP query while in your C# code, you are running many LDAP queries. Each of the DirectoryEntry.RefreshCache() is going to translate to one single LDAP query. So, if you are trying to access 1000 objects, you are going to run 1000 different LDAP queries.
Take a relational database analogy, in VBscript, you are running
SELECT * FROM USER_TABLE
In C# code, you are running multiple times of the following queries
SELECT * FROM USER_TABLE WHERE id = @id
Of course, the C# code will be slower.
To do similar thing in C# code, you should use DirectorySearcher instead of DirectoryEntry.
Similarly, you need to remember to specify DirectorySearcher.PropertiesToLoad in order to specify what attributes to return from a LDAP query. If you don't specify, it will return all non-constructed attributes to you again.
Here are couple of things you can do
Enable Audit log in the LDAP server and see how your requests are going through. Audit logs will show you how much time it is taking for each request from your application and how many connections are opened etc.
Use System.DirectoryServices.Protocols which can make Asynchronous calls to LDAP. Check this sample post. Another advantage of using this name space is that you can specify attributes.
Close connection properly.
use DirectorySearcher.PropertiesToLoad to load only required properties instead of all properties.
what i see from vbscript following is a bit closer .. copying from some project, kindly test
DirectoryEntry de = new DirectoryEntry("ldap://domainname");
DirectorySearcher deSearch = new DirectorySearcher();
deSearch.SearchRoot = de;
deSearch.Filter = "(&(ObjectCategory=user)(sAMAccountName="+strUser+"))";
deSearch.PropertiesToLoad.Add("displayName");
deSearch.PropertiesToLoad.Add("pwdLastSet");
deSearch.PropertiesToLoad.Add("whenCreated");
deSearch.PropertiesToLoad.Add("whenChanged");
deSearch.PropertiesToLoad.Add("userAccountControl);
deSearch.SearchScope = SearchScope.Subtree;
SearchResult sr = deSearch.FindOne();
This is the correct way to read the property:
If searchResult.Properties.Contains(PropertyName) Then
Return searchResult.Properties(PropertyName)(0).ToString()
Else
Return String.Empty
End If
精彩评论