要开发Active Directory程序,必须导入System.DirectoryServices命名空间。还必须引用System.DirectoryServices程序集。使用这个程序集中的类可以查询对象、查看和更新属性,搜索对象,把对象移动到其他容器对象中等。在下面的代码段中,简单的C#控制台应用程序说明了如何使用System.DirectoryServices命名空间中的类。 本节将介绍: ● System.DirectoryServices命名空间中的类 ● 连接Active Directory的处理方式:绑定 ● 获取目录项,创建新对象,并更新已有的项目 ● 搜索Active Directory 24.4.1 System.DirectoryServices命名空间中的类 表24-1列出了System.DirectoryServices命名空间中的主要类。 表 24-1 类 24.4.2 绑定 LDAP://dc01.athenaproject.com/OU=Development, DC= AthenaProject, DC=Com 在绑定过程中,可以指定下述内容: ● 指定提供程序使用的协议(protocol)。 ● 域控制器的服务器名(server name)。 ● 服务器过程的端口号(port number)。 ● 对象的显名(distingunshed name),以标识要访问的对象。 ● 如果需要访问Active Directory的用户不是运行当前进程的账户,则提供用户名和密码。 ● 如果需要加密,应指定authentication类型。 下面详细介绍这些内容。 1. 协议 2. 服务器名 无服务器的绑定如下所示: LDAP://OU=Sales,DC=AthenaProject,DC=Local 表 24-2 协 议 3. 端口号 4. 显名 例如有一个显名: CN=Christian Nagel, OU=Consultants, DC= AthenaProject, DC=local 这个显名指定域AthenaProject.local中域组件(Domain Component,DC) AthenaProject的组织单元(Organizational Unit,OU) Consultants的公共名称(Common Name,CN) Christian Nagel。最右边的部分是域的根对象。该名称必须符合对象树中的分层方式。 显名的字符串表示的LDAP规范在 RFC 2253( http://www./rfc/rfc2253.txt)上。 (1) 相对显名 相对显名(RDN)用于引用容器对象中的对象。使用RDN时,不需要指定OU和DC,有一个公共名称就足够了。CN=Christian Nagel就是组织单元中的一个相对显名。如果已经引用了一个容器对象,要访问其子对象,就可以使用相对显名。 (2) 默认的命名环境 如果在路径中没有指定显名,绑定过程就会使用默认的命名环境(default naming context)。使用rootDSE可以读取默认命名环境。LDAP 3.0把rootDSE定义为目录服务器中目录树的根。例如 或: 通过列举rootDSE的所有属性,将获得没有指定名称时可以使用的defaultNamingContext信息。schemaNamingContext 和 configurationNamingContext指定了用于访问模式所需要的名称和Active Directory库中的配置。 通过下面的代码可获得rootDSE的所有属性: using (DirectoryEntry de = new DirectoryEntry()) { de.Path = "LDAP://platinum/rootDSE"; de.Username = @" platinum\christian"; de.Password = "password"; PropertyCollection props = de.Properties; foreach (string prop in props.PropertyNames) { PropertyValueCollection values = props[prop]; foreach (string val in values) { Console.Write(prop + ": "); Console.WriteLine(val); } } } 这个程序显示了默认的命名环境(defaultNamingContext DC=eichkogelstrasse、 DC=local),用于访问模式的环境(CN=Schema、 CN=Configuration、 DC=eichkogelstrasse、 DC=local)和配置的命名环境(CN=Configuration、 DC=eichkogelstrasse、 DC=local),如图24-9所示。 图 24-9 (3) 对象标识符 每个对象都有一个全局惟一的标识符GUID。GUID是一个惟一的128位数字,您可能已经在COM开发中了解了它。可以使用GUID绑定一个对象。这样,即使对象移动到另一个容器中,也可以得到该对象。GUID在创建对象时生成,且总是保持不变。 使用DirectoryEntry.NativeGuid可以得到GUID的字符串表示。这个字符串表示就可以用于绑定对象。 下面的示例显示了一个无服务器绑定的路径名称,它绑定到GUID代表的一个特定对象上: LDAP://<GUID=14abbd652aae1a47abc60782dcfc78ea> (4) Windows NT域中的对象名 WinNT提供程序不允许在绑定字符串的名称部分使用LDAP语法。在这个提供程序中,对象用ObjectName、ClassName指定。Windows NT域的有效绑定字符串如下: WinNT: WinNT://DomainName WinNT://DomainName/UserName, user WinNT://DomainName/ServerName/MyGroup, group user和group后缀指定可以访问类型为user或group的对象。 5. 用户名 (1) Downlevel登录 使用downlevel登录,用户名可以用Windows 2000以前的域名来指定: domain\username (2) 显名 也可以用user对象的显名来指定用户,例如: CN=Administrator, CN=Users,DC=athenaproject,DC=local (3) User Principal Name (UPN) 对象的UPN用userPrincipalName属性来定义。系统管理员可以在Active Directory Users and Computers工具中User属性的Account选项卡上,用登录信息来指定UPN,注意这不是用户的电子邮件地址。 这些信息也惟一地标识了用户,可以用于登录: Nagel@ athenaproject.local 6. 身份验证 7. 用DirectoryEntry类绑定 DirectoryEntry de = new DirectoryEntry(); de.Path = "LDAP://platinum/DC=athenaproject, DC=local"; de.Username = "nagel@ athenaproject.local"; de.Password = "password"; // use the current user credentials DirectoryEntry de2 = new DirectoryEntry( "LDAP://DC= athenaproject, DC=local"); 即使成功地构造了DirectoryEntry对象,也并不意味着绑定成功了。在第一次读取属性时进行绑定,可以避免不必要的网络流通量。对象是否存在,或者指定的用户证书是否正确,都可以在第一次访问该对象时确定。 24.4.3 获取目录项 DirectoryEntry类的一些属性可以提供对象的信息,即Name、Guid和 SchemaClassName属性。第一次访问DirectoryEntry对象的属性时,会执行绑定操作,并填充底层ADSI对象的缓存。后面将详细讨论这些。其他属性可以从缓存中读取,同一对象的数据不需要通过与服务器的通信来获得。 在本例中,用组织单元Wrox Press中的公共名称Christian Nagel访问一个用户对象: using (DirectoryEntry de = new DirectoryEntry()) { de.Path = "LDAP://platinum/CN=Christian Nagel, " + "OU=Wrox Press, DC=athenaproject, DC=local"; Console.WriteLine("Name: " + de.Name); Console.WriteLine("GUID: " + de.Guid); Console.WriteLine("Type: " + de.SchemaClassName); Console.WriteLine(); //... } Active Directory对象包含许多信息,这些信息取决于对象的类型。属性Properties将返回一个PropertyCollection。每个属性本身就是一个集合,因为一个属性可以有多个值,例如,user对象可以有多个电话号码。在本例中,用一个内部foreach循环查看这些值。从properties[name]返回的集合是一个object数组。属性值可以是字符串、数字或其他类型。使用ToString()方法就可以显示这些值: Console.WriteLine("Properties: "); PropertyCollection properties = de.Properties; foreach (string name in properties.PropertyNames) { foreach (object o in properties[name]) { Console.WriteLine(name + ": " + o.ToString()); } } 在得到的结果中,包含了user对象Christian Nagel的所有属性,如图24-10所示。OtherTelephone是一个多值属性,它包含许多电话号码。一些属性值只显示System._ComObject对象的类型,例如,lastLogoff、lastLogon和nTSecurityDescriptor。要得到这些属性的值,必须直接使用System.DirectoryServices命名空间的类中的ADSI COM接口。 图 24-10 注意: 第28章介绍了如何使用COM对象和接口。 2. 直接通过名称访问属性 foreach (string homePage in de.Properties["wWWHomePage"]) Console.WriteLine("Home page: " + homePage); 24.4.4 对象集合 用户对象没有子对象,所以下面的示例使用一个组织单元,如图24-11所示。非容器对象用Children属性返回一个空集合。下面在域athenaproject.local的组织单元Wrox Press中获得其所有的用户对象。Children属性返回一个DirectoryEntries集合,其中包含DirectoryEntry对象。迭代所有的DirectoryEntry对象,显示子对象的名称。 using (DirectoryEntry de = new DirectoryEntry()) { de.Path = "LDAP://platinum/OU=Wrox Press, " + "DC=athenaproject, DC=local"; Console.WriteLine("Children of " + de.Name); foreach (DirectoryEntry obj in de.Children) { Console.WriteLine(obj.Name); } } 图 24-11 本例显示了组织单元中的所有对象:users、contacts、printers、shares和其他组织单元。如果只需要查看某些对象类型,可以使用DirectoryEntries类的SchemaFilter属性。SchemaFilter属性返回一个SchemaNameCollection。有了这个SchemaNameCollection,就可以使用Add()方法定义要查看的对象类型。在本例中,因为只需要查看user对象,所以把user添加到这个集合中: using (DirectoryEntry de = new DirectoryEntry()) { de.Path = "LDAP:// platinum /OU=Wrox Press, " + "DC= athenaproject, DC=local"; Console.WriteLine("Children of " + de.Name); de.Children.SchemaFilter.Add("user"); foreach (DirectoryEntry obj in de.Children) { Console.WriteLine(obj.Name); } } 结果,只显示组织单元中的user对象,如图24-12所示。 24.4.5 缓存 写入对象的改变,只会改变已缓存的对象。设置属性不会产生网络流通量。必须使用DirectoryEntry.CommitChanges()才能刷新缓存,把所有已改变的数据传送回服务器。要再次从目录库中获取新写入的数据,可以使用DirectoryEntry.RefreshCache()读取属性。当然,如果没有调用CommitChanges() 和RefreshCache()就修改了一些属性,所有的改变都会丢失,因为我们将再次使用RefreshCache()读取目录服务中的值。 把属性DirectoryEntry.UsePropertyCache设置为false,就可以关闭这个属性cache。但除非在进行调试,否则最好不要关闭它,因为这会与服务器间产生许多不必要的往返通信。 24.4.6 创建新对象 要给目录添加新对象,首先必须绑定一个容器对象,例如组织单元,可以在其中插入一个新对象。不包含其他对象的对象是不能使用的。下面使用一个容器对象,其显名为CN=Users,DC= athenaproject,DC=local: DirectoryEntry de = new DirectoryEntry(); de.Path = "LDAP://platinum/CN=Users, DC= athenaproject, DC=local"; 使用DirectoryEntry的Children属性,可以得到一个DirectoryEntries对象: DirectoryEntries users = de.Children; 使用DirectoryEntries,可以添加、删除和查找集合中的对象。下面创建一个新的user对象。使用Add()方法时,需要一个对象名和一个类型名。使用ADSI Edit很容易获得类型名: DirectoryEntry user = users.Add("CN=John Doe", "user"); 对象现在有了默认属性值。要指定特定的属性值,可以使用Properties属性的Add()方法添加属性。当然,所有的属性都必须存在于user对象的模式中。如果指定的属性不存在,就得到一个COMException“指定的目录服务属性或值不存在”: user.Properties["company"].Add("Some Company"); user.Properties["department"].Add("Sales"); user.Properties["employeeID"].Add("4711"); user.Properties["samAccountName"].Add("JDoe"); user.Properties["userPrincipalName"].Add("JDoe@ athenaproject.local"); user.Properties["givenName"].Add("John"); user.Properties["sn"].Add("Doe"); user.Properties["userPassword"].Add("someSecret"); 此时,为了把数据写入Active Directory,必须刷新缓存中的数据: user.CommitChanges(); 24.4.7 更新目录项 要修改一个值,可以把这个值设置为指定的值。下面的代码将通过PropertyValueCollection的一个索引符把移动电话号码设置为一个新值。使用该索引符,只能改变已存在的值。因此,应使用DirectoryEntry.Properties.Contains()来确定属性是否可用。 using (DirectoryEntry de = new DirectoryEntry()) { de.Path = "LDAP://platinum /CN=Christian Nagel, " + "OU=Wrox Press, DC= athenaproject, DC=local"; if (de.Properties.Contains("mobile")) { de.Properties["mobile"][0] = "+43(664)3434343434"; } else { de.Properties["mobile"].Add("+43(664)3434343434"); } de.CommitChanges(); } 在本例的else部分,如果移动电话号码不存在新属性,就用方法PropertyValue Collection.Add()添加它。如果使用Add()方法和已有的属性,得到的结果将取决于属性的类型:单值属性或多值属性。使用Add()方法和已有的单值属性,会得到一个COMException:A constraint violation occurred。使用Add()方法和已有的多值属性则会成功地把另一个值添加到属性中。 User对象的属性mobile定义为单值属性,所以不能添加其他移动电话的号码。但是用户可以有多个移动电话的号码。对于多个移动电话的号码,可以使用属性otherMobile,它是一个多值属性,允许设置多个移动电话号码,所以可以调用Add()多次。多值属性有一个重要的检查:即检查其唯一性。如果把第二个电话号码添加到同一个user对象上,也会得到一个COMException:指定的目录服务属性或值已经存在。 提示: 在创建或更新新的目录对象后,应调用DirectoryEntry.CommitChanges()。否则只能更新缓存中的信息,改变的信息不会发送到目录服务上。 24.4.8 访问内部的ADSI对象 如本章前面所述,System.DirectoryServices命名空间的类使用底层的ADSI COM对象。DirectoryEntry支持直接使用Invoke()方法调用底层对象的方法。 Invoke()的第一个参数是应在ADSI对象中调用的方法名,第二个参数的params关键字允许把数量可变的其他参数传送给ADSI方法: public object Invoke(string methodName, params object[] args); 在ADSI文档中介绍了可以用Invoke()方法调用的方法。域中的每个对象都支持IADS接口的方法。前面创建的User对象也支持IADsUser接口的方法。 在下面的代码示例中,使用方法IADsUser.SetPassword()改变前面创建的user对象的密码: using (DirectoryEntry de = new DirectoryEntry()) { de.Path = "LDAP://platinum /CN=John Doe, " + "CN=Users, DC= athenaproject, DC=local"; de.Invoke("SetPassword", "anotherSecret"); de.CommitChanges(); } 如果不使用Invoke(),还可以直接使用底层的ADSI对象。要使用这些对象,必须使用Project | Add Reference添加对Active DS Type Library的引用,如图24-13所示。这会创建一个包装器类,在该类中可以访问ActiveDS命名空间中的这些对象。 图 24-13 内部对象可以使用DirectoryEntry类的NativeObject属性来访问。在下面的示例中,对象de是一个user对象,所以可以把它强制转换为ActiveDs.IADsUser。SetPassword()是在IADsUser接口中说明的方法,所以可以直接调用它,而不是使用Invoke()方法来调用。把IADsUser的AccountDisabled属性设置为false,就可以激活账户。与前面的示例一样,调用CommitChanges() 和DirectoryEntry对象,把改变的内容写到目录服务中: ActiveDs.IADsUser user = (ActiveDs.IADsUser)de.NativeObject; user.SetPassword("someSecret"); user.AccountDisabled = false; de.CommitChanges(); 24.4.9 在Active Directory中搜索 注意: DirectorySearcher只能和LDAP提供程序一起使用。它不能和NDS或IIS提供程序一起使用。 在类DirectorySearcher的构造函数中,可以定义搜索的4个重要部分。也可以使用默认构造函数,用属性定义搜索选项。 1. SearchRoot 2. 过滤器 表达式中可以使用关系运算符,如<=、 =、 >=。(objectClass=contact)会搜索所有类型为contact的对象,(lastName>=Nagel) 会按字母表的顺序搜索lastName属性等于或大于Nagel的所有对象。 表达式可以和前缀运算符 & 和 |组合使用。例如,(&(objectClass=user)(description=Auth*))搜索类型为User,其属性description以字符串Auth开头的所有对象。因为 & 和 | 运算符在表达式的开头,所以可以把多个表达式用一个前缀运算符组合在一起。 默认过滤器是(objectClass=*),所以将选择所有的对象。 注意: 过滤器语法在RFC 2254中定义为“LDAP搜索过滤器的字符串表示”。这个RFC在http://www./rfc/rfc2254.txt中。 3. PropertiesToLoad 4. SearchScope ● SearchScope.Base只搜索对象中的属性,至多可以得到一个对象。 ● SearchScope.OneLevel表示在基对象的子集合中继续搜索。基对象本身是不搜索的。 ● SearchScope.Subtree定义了在整个树中搜索。 SearchScope属性的默认值是SearchScope.Subtree。 5. 搜索的限制 表 24-3 属 性 ReferalChasingOption.None表示不能在不同的服务器上继续搜索 ReferalChasingOption.Subordinate指定继续在子域中搜索,搜索从DC=Wrox、DC=COM开始时,服务器可以返回一个结果集,例如DC=France、DC=Wrox、DC=COM引用。客户机可以继续在子域中搜索 ReferalChasingOption.External表示服务器可以让客户机搜索不在子域中的另一个服务器,这是默认选项 ReferalChasingOption.All表示返回外部和从属的引用 在搜索示例中,要搜索组织单元Wrox Press中description属性值为Author的所有user 对象。 首先,绑定组织单元Wrox Press,在该组织单元中开始搜索。创建一个DirectorySearcher对象,在其中设置SearchRoot。过滤器定义为(&(objectClass=user) (description=Auth*)),这样就可以搜索所有类型为User、description属性以Auth开头的对象。搜索的范围应在一个子树中,即在Wrox Press的子组织单元中搜索: using (DirectoryEntry de = new DirectoryEntry("LDAP://OU=Wrox Press, DC= athenaproject, DC=local")) using (DirectorySearcher searcher = new DirectorySearcher()) { searcher.SearchRoot = de; searcher.Filter = "(&(objectClass=user)(description=Auth*))"; searcher.SearchScope = SearchScope.Subtree; 要在搜索结果中包含的属性有name、description、givenName和WWWHomePage。 searcher.PropertiesToLoad.Add("name"); searcher.PropertiesToLoad.Add("description"); searcher.PropertiesToLoad.Add("givenName"); searcher.PropertiesToLoad.Add("wWWHomePage"); 下面准备开始搜索。还应对结果进行排序。DirectorySearcher有一个属性Sort,它可以设置SortOption。SortOption构造函数的第一个参数定义了要排序的属性,第二个参数定义了排序的方向。SortDirection枚举的值是Ascending 和 Descending。 要开始搜索,可以使用FindOne()方法查找第一个对象,或者使用FindAll()。FindOne()返回一个简单的SearchResult,FindAll()返回一个SearchResultCollection。要得到所有的作者对象,就应使用FindAll(): searcher.Sort = new SortOption("givenName", SortDirection.Ascending); SearchResultCollection results = searcher.FindAll(); 使用一个foreach循环,可以访问SearchResultCollection中的每个SearchResult。一个SearchResult表示搜索缓存中的一个对象。Properties属性返回一个ResultPropertyCollection,使用属性名和索引符可以访问该集合中所有的属性: SearchResultCollection results = searcher.FindAll(); foreach (SearchResult result in results) { ResultPropertyCollection props = result.Properties; foreach (string propName in props.PropertyNames) { Console.Write(propName + ": "); Console.WriteLine(props[propName][0]); } Console.WriteLine(); } } 可以在搜索之后获得完整的对象:SearchResult有一个方法GetDirectoryEntry(),它返回查找到的对象的相应DirectoryEntry。 得到的结果应显示《Professional C#》(即《C#高级编程》)的作者列表,这些作者都有我们指定的属性,如图24-14所示。 24.5 搜索用户对象 24.5.1 用户界面 图 24-15 ● 在第一步中,输入用户名、密码和域控制器。所有这些信息都是可选的。如果没有输入域控制器,就要使用无服务器绑定进行连接。如果没有输入用户名,就使用当前用户的安全环境。 ● 使用一个按钮,就可以把User对象的所有属性名动态加载到listBoxProperties 列表 框中。 ● 在加载了属性名后,就可以选择要显示的属性。列表框的SelectionMode设置为MultiSimple。 ● 可以输入限制搜索的过滤器,在该对话框中设置的默认值是搜索所有的user对象:(objectClass=user)。 ● 现在开始搜索。 24.5.2 获取模式命名环境 在处理程序方法buttonLoadProperties_Click()中,使用SetLogonInformation()从对话框中读取用户名、密码和主机名,并存储在类的成员中。接着,SetNamingContext()方法设置模式的LDAP名称和默认环境的LDAP名称。这个模式LDAP名称用于调用中,以设置列表框中的属性SetUserProperties()。 private void buttonLoadProperties_Click(object sender, System.EventArgs e) { try { SetLogonInformation(); SetNamingContext(); SetUserProperties(schemaNamingContext); } catch (Exception ex) { MessageBox.Show("Check your inputs! " + ex.Message); } } protected void SetLogonInformation() { username = (textBoxUsername.Text == "" ? null : textBoxUsername.Text); password = (textBoxPassword.Text == "" ? null : textBoxPassword.Text); hostname = textBoxHostname.Text; if (hostname != "") hostname += "/"; } 在帮助方法SetNamingContext()中,使用目录树的根来获得服务器的属性。这里只考虑两个属性的值:schemaNamingContext 和 defaultNamingContext。 protected string SetNamingContext() { using (DirectoryEntry de = new DirectoryEntry()) { string path = "LDAP://" + hostname + "rootDSE"; de.Username = username; de.Password = password; de.Path = path; schemaNamingContext = de.Properties["schemaNamingContext"][0].ToString(); defaultNamingContext = de.Properties["defaultNamingContext"][0].ToString(); } } 24.5.3 获取User类的属性名 protected void SetUserProperties(string schemaNamingContext) { StringCollection properties = new StringCollection(); string[] data = GetSchemaProperties(schemaNamingContext, "User"); properties.AddRange(GetSchemaProperties(schemaNamingContext, "Organizational-Person")); properties.AddRange(GetSchemaProperties(schemaNamingContext, "Person")); properties.AddRange(GetSchemaProperties(schemaNamingContext, "Top")); listBoxProperties.Items.Clear(); foreach (string s in properties) { listBoxProperties.Items.Add(s); } } 在GetSchemaProperties()中,再次访问Active Directory服务。这次不使用rootDSE,而使用前面介绍的模式的LDAP名称。属性systemMayContain包含objectType类中的所有属性的一个集合: protected string[] GetSchemaProperties(string schemaNamingContext, string objectType) { string[] data; using (DirectoryEntry de = new DirectoryEntry()) { de.Username = username; de.Password = password; de.Path = "LDAP://" + hostname + "CN=" + objectType + "," + schemaNamingContext; DS.PropertyCollection properties = de.Properties; DS.PropertyValueCollection values = properties["systemMayContain"]; data = new String[values.Count]; values.CopyTo(data, 0); } return data; } 注意上述代码中的DS.PropertyCollection,这是因为在Windows Forms应用程序中,System.DirectoryServices命名空间中的PropertyCollection类与System.Data.PropertyCollection的名称相冲突。为了避免使用像System.DirectoryServices.PropertyCollection这么长的名称,可以使用下面的代码来缩短命名空间的名称: using DS = System.DirectoryServices; 这样就完成了应用程序的第二步。Listbox控件包含User对象的所有属性名。 24.5.4 搜索用户对象 private void buttonSearch_Click(object sender, System.EventArgs e) { try { FillResult(); } catch (Exception ex) { MessageBox.Show("Check your input: " + ex.Message); } } 在FillResult()中,在整个Active Directory 域中进行与前面一样的正常搜索。SearchScope设置为Subtree, Filter设置为TextBox中的字符串,应加载到缓存中的属性由用户在列表框中选择的值来设置。方法GetProperties()用于把属性数组传送给方法searcher。PropertiesTo Load.AddRange()是一个帮助方法,它从列表框中把选中的属性度到一个数组中。在设置了DirectorySearcher对象的属性后,就调用SearchAll()方法搜索属性。SearchResultCollection中的搜索结果用于生成写到文本框textBoxResults中的汇总信息。 protected void FillResult() { using (DirectoryEntry root = new DirectoryEntry()) { root.Username = username; root.Password = password; root.Path = "LDAP://" + hostname + defaultNamingContext; using (DirectorySearcher searcher = new DirectorySearcher()) { searcher.SearchRoot = root; searcher.SearchScope = SearchScope.Subtree; searcher.Filter = textBoxFilter.Text; searcher.PropertiesToLoad.AddRange(GetProperties()); SearchResultCollection results = searcher.FindAll(); StringBuilder summary = new StringBuilder(); foreach (SearchResult result in results) { foreach (string propName in result.Properties.PropertyNames) { foreach (string s in result.Properties[propName]) { summary.Append(" " + propName + ": " + s + "\r\n"); } } summary.Append("\r\n"); } textBoxResults.Text = summary.ToString(); } } } 启动该应用程序,将列出所有由过滤器选择的有效对象,如图24-16所示。 24.6 小结 使用System.DirectoryServices命名空间中的类,可以很容易地访问封装到ADSI提供程序中的Active Directory服务。DirectoryEntry类可以直接读写数据库中的对象。 使用DirectorySearcher类可以进行复杂的搜索,定义过滤器、超时、加载的属性和范围等。使用全局目录,可以加快对整个企业中的对象的搜索,因为它在森林中存储了所有对象的只读版本。 |
|