如何使用C#从Windows活动目录有效地获取用户列表

时间:2017-05-02 09:48:48

标签: c# .net windows active-directory

我遵循了此问题的解决方案How can I get a list of users from active directory?,并且能够从AD获得用户列表。我遇到的问题是加载所有记录需要35秒。

必须有一种更有效的方法一次性查询所有数据,而不必等待35秒才能返回700多条记录。我写了一个方法来返回用户列表。我已经添加了一些额外的代码来尝试过滤掉任何不属于人类的用户。

public List<ActiveUser> GetActiveDirectoryUsers()
{
    List<ActiveUser> response = new List<ActiveUser>();
    using (var context = new PrincipalContext(ContextType.Domain, "mydomain"))
    {
        using (var searcher = new PrincipalSearcher(new UserPrincipal(context)))
        {
            foreach (var result in searcher.FindAll())
            {
                DirectoryEntry de = result.GetUnderlyingObject() as DirectoryEntry;
                if (de.NativeGuid != null && !Convert.ToBoolean((int)de.Properties["userAccountControl"].Value & 0x0002) &&
                    de.Properties["department"].Value != null && de.Properties["sn"].Value != null) response.Add(new ActiveUser(de));
            }
        }
    }
    return response.OrderBy(x => x.DisplayName).ToList();
}

ActiveUser的构造函数只需要输入entry.property [“whataver”]并将其分配给该类的属性。

的开销似乎就在上面
DirectoryEntry de = result.GetUnderlyingObject() as DirectoryEntry;

我可以将用户列表缓存到一个文件中,但是对于一个列表来说仍然需要超过30秒的加载时间。必须有一种更快的方法来做到这一点。

2 个答案:

答案 0 :(得分:2)

作为一种学习经历,我使用了许多不同的方法来解决这个问题。

我发现自己的所有方法都可以很快地列出一组adspath值,但是在迭代中引入Console.WriteLine会导致性能发生巨大变化。

我有限的C#知识让我尝试了各种方法,比如IEnumerator直接使用DirectoryEntry,PrincipleSearcher使用上下文,但这两种方法都很慢,并且根据具体情况而有很大差异完成信息

最后,这就是我最终的结果。它是最快的,并且在增加解析选项时不会引起任何明显的性能损失。

注意:这实际上是该类的完整复制/粘贴 powershell 包装器,因为我目前不在使用Visual Studio的VM附近。

$Source = @"
// " "  <-- this just makes the code highlighter work
// Syntax:  [soexample.search]::Get("LDAP Path", "property1", "property2", "etc...")
// Example: [soexample.search]::Get("LDAP://CN=Users,DC=mydomain,DC=com","givenname","sn","samaccountname","distinguishedname")

namespace soexample
{
    using System;
    using System.DirectoryServices;

    public static class search
    {
        public static string Get(string ldapPath, params string[] propertiesToLoad)
        {
            DirectoryEntry entry = new DirectoryEntry(ldapPath);
            DirectorySearcher searcher = new DirectorySearcher(entry);
            searcher.SearchScope = SearchScope.OneLevel;
            foreach (string p in propertiesToLoad) { searcher.PropertiesToLoad.Add(p); }
            searcher.PageSize = 100;
            searcher.SearchRoot = entry;
            searcher.CacheResults = true;
            searcher.Filter = "(sAMAccountType=805306368)";
            SearchResultCollection results = searcher.FindAll();

            foreach (SearchResult result in results)
            {
                foreach (string propertyName in propertiesToLoad)
                {
                    foreach (object propertyValue in result.Properties[propertyName])
                    {
                        Console.WriteLine(string.Format("{0} : {1}", propertyName, propertyValue));
                    }
                }
                Console.WriteLine("");

            }
            return "";
        }
    }
}
"@
$Asem = ('System.DirectoryServices','System')
Add-Type -TypeDefinition $Source -Language CSharp -ReferencedAssemblies $Asem

我在一个拥有160个用户的特定域上运行此操作,这是结果;

使用代码注释中的example命令:

PS > Measure-Command { [soexample.search]::Get(args as above..) }

输出:

givenname : John
sn : Surname
samaccountname : john.surname
distinguishedname : CN=John Surname,CN=Users,DC=mydomain,DC=com 

etc ... 159 more ...

Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 0
Milliseconds      : 431
Ticks             : 4317575
TotalDays         : 4.99719328703704E-06
TotalHours        : 0.000119932638888889
TotalMinutes      : 0.00719595833333333
TotalSeconds      : 0.4317575
TotalMilliseconds : 431.7575 

给出的每个附加字符串参数似乎都会使总处理时间增加大约100毫秒。

仅使用 samaccountname运行它只需0.1秒即可列出160个用户,并已解析到控制台中。

使用Microsoft的示例here,并将其修改为仅列出一个属性,花了3秒多,每个额外的属性花费大约一秒钟。

一对夫妇注意到:

  • (sAMAccountType=805306368)效率高于(&(objectClass=user)(objectCategory=person))(参见https://stackoverflow.com/a/10053397/3544399)和许多其他示例

  • searcher.CacheResults = true;似乎没有任何区别(无论如何,在我的域名中)是真的还是明显错误。

  • searcher.PageSize = 100;产生了可衡量的差异。我相信2012R2 DC上的默认MaxPageSize为1000(https://technet.microsoft.com/en-us/library/cc770976(v=ws.11).aspx

  • 属性不区分大小写(即在result.Properties.PropertyNames中返回给搜索者的任何内容,因此foreach循环只是迭代那些propertiesToLoad 1}})

  • 乍一看这三个foreach循环似乎没必要,但每次成功删除循环最终都会导致转换转换和运行方法扩展时花费更多的开销。

可能还有更好的方法,我已经看过一些精心设计的线程和结果缓存示例,我不知道该怎么做,但调整后的DirectorySearcher似乎确实如此最灵活的,这里的代码只需要SystemSystem.DirectoryServices名称空间。

不确定你对你的"//do stuff"究竟做了什么,这是否有帮助,但我确实发现这是一个有趣的练习,因为我不知道有这么多做这样的事情的方法。

答案 1 :(得分:0)

为了给出更新,我找到了部分解决方法。它比上面的方法更快,但它缺少一些额外的数据。根据我的阅读,问题中的方法相当于做

之类的事情
SELECT id FROM sometable
foreach row in table
SELECT * FROM sometable where id = ?

所以很清楚为什么它很慢。以下方法在一秒钟内执行,并提供我需要的所有属性。需要单独调用目录条目以获取该数据,但这很容易实现,因为如果您提供一些搜索参数,可以只抓取一个用户。

这是一种更有效的更新方法。

DirectoryEntry de = new DirectoryEntry("ldap://mydomain");
using (DirectorySearcher search = new DirectorySearcher())
{
    search.Filter = "(&(objectClass=user)(objectCategory=person))";
    search.PropertiesToLoad.Add("userAccountControl");
    search.PropertiesToLoad.Add("sn");
    search.PropertiesToLoad.Add("department");
    search.PropertiesToLoad.Add("l");
    search.PropertiesToLoad.Add("title");
    search.PropertiesToLoad.Add("givenname");
    search.PropertiesToLoad.Add("co");
    search.PropertiesToLoad.Add("displayName");
    search.PropertiesToLoad.Add("distinguishedName");
    foreach (SearchResult searchrecord in search.FindAll())
    {
        //do stuff
    }
}