开发者

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

  1. 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.

  2. 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.

  3. 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
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜