diff --git a/src/libraries/System.DirectoryServices/src/System/DirectoryServices/SearchResultCollection.cs b/src/libraries/System.DirectoryServices/src/System/DirectoryServices/SearchResultCollection.cs index 32a0666bdb4e90..6369b190988025 100644 --- a/src/libraries/System.DirectoryServices/src/System/DirectoryServices/SearchResultCollection.cs +++ b/src/libraries/System.DirectoryServices/src/System/DirectoryServices/SearchResultCollection.cs @@ -47,7 +47,7 @@ private ArrayList InnerList { if (_innerList == null) { - _innerList = new ArrayList(); + var eagerList = new ArrayList(); var enumerator = new ResultsEnumerator( this, _rootEntry.GetUsername(), @@ -55,7 +55,9 @@ private ArrayList InnerList _rootEntry.AuthenticationType); while (enumerator.MoveNext()) - _innerList.Add(enumerator.Current); + eagerList.Add(enumerator.Current); + + _innerList = eagerList; } return _innerList; @@ -188,12 +190,10 @@ protected virtual void Dispose(bool disposing) public IEnumerator GetEnumerator() { - // Two ResultsEnumerators can't exist at the same time over the - // same object. Need to get a new handle, which means re-querying. - return new ResultsEnumerator(this, - _rootEntry.GetUsername(), - _rootEntry.GetPassword(), - _rootEntry.AuthenticationType); + if (_innerList != null) + return new AlreadyReadResultsEnumerator(_innerList); + else + return new ResultsEnumerator(this, _rootEntry.GetUsername(), _rootEntry.GetPassword(), _rootEntry.AuthenticationType); } public bool Contains(SearchResult result) => InnerList.Contains(result); @@ -214,6 +214,49 @@ void ICollection.CopyTo(Array array, int index) InnerList.CopyTo(array, index); } + /// + /// Supports a simple type-specific wrapper for the underlying cached list + /// + private sealed class AlreadyReadResultsEnumerator : IEnumerator + { + private readonly IEnumerator _innerEnumerator; + + internal AlreadyReadResultsEnumerator(ArrayList innerList) + { + _innerEnumerator = innerList.GetEnumerator(); + } + + /// + /// Gets the current element in the collection. + /// + public SearchResult Current + { + get + { + return (SearchResult)(_innerEnumerator.Current); + } + } + + /// + /// Advances the enumerator to the next element of the collection + /// and returns a Boolean value indicating whether a valid element is available. + /// + public bool MoveNext() + { + return _innerEnumerator.MoveNext(); + } + + /// + /// Resets the enumerator back to its initial position before the first element in the collection. + /// + public void Reset() + { + _innerEnumerator.Reset(); + } + + object IEnumerator.Current => Current; + } + /// /// Supports a simple ForEach-style iteration over a collection. /// @@ -310,6 +353,7 @@ public bool MoveNext() return false; _currentResult = null; + if (!_initialized) { int hr = _results.SearchObject.GetFirstRow(_results.Handle); diff --git a/src/libraries/System.DirectoryServices/tests/System.DirectoryServices.Tests.csproj b/src/libraries/System.DirectoryServices/tests/System.DirectoryServices.Tests.csproj index b553252d53f604..4dd407cbed0e47 100644 --- a/src/libraries/System.DirectoryServices/tests/System.DirectoryServices.Tests.csproj +++ b/src/libraries/System.DirectoryServices/tests/System.DirectoryServices.Tests.csproj @@ -9,6 +9,7 @@ + @@ -20,8 +21,7 @@ - + diff --git a/src/libraries/System.DirectoryServices/tests/System/DirectoryServices/DirectorySearcherTests.cs b/src/libraries/System.DirectoryServices/tests/System/DirectoryServices/DirectorySearcherTests.cs new file mode 100644 index 00000000000000..f78ab9b11344d5 --- /dev/null +++ b/src/libraries/System.DirectoryServices/tests/System/DirectoryServices/DirectorySearcherTests.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using System.Collections.Generic; +using System.Collections; +using Xunit; +using Xunit.Sdk; +using System.Reflection; + +namespace System.DirectoryServices.Tests +{ + public partial class DirectorySearcherTests + { + internal static bool IsLdapConfigurationExist => LdapConfiguration.Configuration != null; + internal static bool IsActiveDirectoryServer => IsLdapConfigurationExist && LdapConfiguration.Configuration.IsActiveDirectoryServer; + + private const int ADS_SYSTEMFLAG_CR_NTDS_NC = 0x1; + private const int ADS_SYSTEMFLAG_CR_NTDS_DOMAIN = 0x2; + + [ConditionalFact(nameof(IsLdapConfigurationExist))] + public void DirectorySearch_IteratesCorrectly_SimpleEnumeration() + { + var e = GetDomains(); + Assert.NotNull(e); + + foreach (var result in e) + { + Assert.NotNull(result); + } + } + + [ConditionalFact(nameof(IsLdapConfigurationExist))] + public void DirectorySearch_IteratesCorrectly_AfterCount() + { + var e = GetDomains(); + Assert.NotNull(e); + Assert.NotEqual(0, e.Count); + + foreach (var result in e) + { + Assert.NotNull(result); + } + } + + + [ConditionalFact(nameof(IsLdapConfigurationExist))] + public void DirectorySearch_IteratesCorrectly_MixedCount() + { + var e = GetDomains(); + Assert.NotNull(e); + Assert.NotEqual(0, e.Count); + + foreach (var result in e) + { + Assert.NotEqual(0, e.Count); + Assert.NotNull(result); + } + } + + private static SearchResultCollection GetDomains() + { + using var entry = new DirectoryEntry("LDAP://rootDSE"); + var namingContext = entry.Properties["configurationNamingContext"][0]!.ToString(); + using var searchRoot = new DirectoryEntry($"LDAP://CN=Partitions,{namingContext}"); + using var ds = new DirectorySearcher(searchRoot) + { + PageSize = 1000, + CacheResults = false + }; + ds.SearchScope = SearchScope.OneLevel; + ds.PropertiesToLoad.Add("distinguishedName"); + ds.PropertiesToLoad.Add("nETBIOSName"); + ds.PropertiesToLoad.Add("nCName"); + ds.PropertiesToLoad.Add("dnsRoot"); + ds.PropertiesToLoad.Add("trustParent"); + ds.PropertiesToLoad.Add("objectSid"); + ds.Filter = string.Format("(&(objectCategory=crossRef)(systemFlags={0}))", ADS_SYSTEMFLAG_CR_NTDS_DOMAIN | ADS_SYSTEMFLAG_CR_NTDS_NC); + + return ds.FindAll(); + } + } +}