Skip to content

Commit 96bee50

Browse files
authored
Merge pull request #33 from FrendsPlatform/issue-32
Pagination support for querying objects
2 parents bd0e5ce + 186162a commit 96bee50

File tree

5 files changed

+141
-34
lines changed

5 files changed

+141
-34
lines changed

Frends.LDAP.SearchObjects/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Changelog
22

3+
## [4.0.0] - 2025-04-04
4+
### Added
5+
- [Breaking] - Parameter for PageSize to control how many entries are fetched per page during an LDAP search.
6+
- Default value for the new parameter:
7+
- PageSize: 500
8+
- If you want the Task to work exactly as before (non-paged search), set PageSize = 0.
9+
- Default paging behavior (PageSize = 500) may improve performance on large result sets,
10+
but changes how results are retrieved from the server.
11+
312
## [3.1.0] - 2025-04-01
413
### Added
514
- Added example values for new parameters.

Frends.LDAP.SearchObjects/Frends.LDAP.SearchObjects.Tests/UnitTests.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,7 @@ public void Search_Filter_Test()
367367
SearchDereference = SearchDereference.DerefNever,
368368
MaxResults = default,
369369
BatchSize = default,
370+
PageSize = 500,
370371
TypesOnly = default,
371372
Attributes = null,
372373
};
@@ -557,4 +558,58 @@ public void CreateTestUsers()
557558

558559
conn.Disconnect();
559560
}
561+
562+
[TestMethod]
563+
public void Search_MaxResults_LessThanAvailable_ReturnsLimitedResults()
564+
{
565+
input = new()
566+
{
567+
SearchBase = _path,
568+
Scope = Scopes.ScopeSub,
569+
Filter = null,
570+
MaxResults = 2,
571+
PageSize = 3,
572+
};
573+
574+
var result = LDAP.SearchObjects(input, connection, default);
575+
576+
Assert.IsTrue(result.Success, "Search failed.");
577+
Assert.AreEqual(2, result.SearchResult.Count, "Should return only 2 results.");
578+
}
579+
580+
[TestMethod]
581+
public void Search_PageSizeSmallerThanTotal_ShouldReturnAll()
582+
{
583+
input = new()
584+
{
585+
SearchBase = _path,
586+
Scope = Scopes.ScopeSub,
587+
Filter = null,
588+
MaxResults = 5,
589+
PageSize = 2,
590+
};
591+
592+
var result = LDAP.SearchObjects(input, connection, default);
593+
594+
Assert.IsTrue(result.Success, "Search failed.");
595+
Assert.AreEqual(5, result.SearchResult.Count, "Should return all 5 results using paging.");
596+
}
597+
598+
[TestMethod]
599+
public void Search_PageSizeZero_DisablesPaging()
600+
{
601+
input = new()
602+
{
603+
SearchBase = _path,
604+
Scope = Scopes.ScopeSub,
605+
Filter = null,
606+
MaxResults = 5,
607+
PageSize = 0, // No paging
608+
};
609+
610+
var result = LDAP.SearchObjects(input, connection, default);
611+
612+
Assert.IsTrue(result.Success, "Search failed.");
613+
Assert.AreEqual(5, result.SearchResult.Count, "Should return all 5 results without paging.");
614+
}
560615
}

Frends.LDAP.SearchObjects/Frends.LDAP.SearchObjects/Definitions/Input.cs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,19 +51,30 @@ public class Input
5151
public SearchDereference SearchDereference { get; set; }
5252

5353
/// <summary>
54-
/// The maximum number of search results to return for a search request.
54+
/// The maximum number of search results to return.
55+
/// This acts as a hard limit—regardless of page size or batch size, no more than this number of results will be returned.
56+
/// Set to 0 for no limit.
5557
/// </summary>
5658
/// <example>1000</example>
5759
[DefaultValue(1000)]
5860
public int MaxResults { get; set; }
5961

6062
/// <summary>
61-
/// The number of results to return in a batch. Specifying 0 means to block until all results are received. Specifying 1 means to return results one result at a time.
63+
/// This parameter controls the chunk size in which data will be read from the server. This does not affect the output result amount.
6264
/// </summary>
6365
/// <example>1</example>
64-
[DefaultValue(1)]
66+
[DefaultValue(100)]
6567
public int BatchSize { get; set; }
6668

69+
/// <summary>
70+
/// Controls how many entries are requested from the server in a single page during a paged LDAP search.
71+
/// This directly affects how results are fetched from the server.
72+
/// If set to 0, paging is disabled and all entries are requested in a single operation (not recommended for large directories).
73+
/// </summary>
74+
/// <example>1</example>
75+
[DefaultValue(500)]
76+
public int PageSize { get; set; }
77+
6778
/// <summary>
6879
/// If true, returns the names but not the values of the attributes found.
6980
/// If false, returns the names and values for attributes found.

Frends.LDAP.SearchObjects/Frends.LDAP.SearchObjects/Frends.LDAP.SearchObjects.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<TargetFrameworks>net6.0</TargetFrameworks>
5-
<Version>3.1.0</Version>
5+
<Version>4.0.0</Version>
66
<Authors>Frends</Authors>
77
<Copyright>Frends</Copyright>
88
<Company>Frends</Company>

Frends.LDAP.SearchObjects/Frends.LDAP.SearchObjects/SearchObjects.cs

Lines changed: 62 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Frends.LDAP.SearchObjects.Definitions;
22
using System.ComponentModel;
33
using Novell.Directory.Ldap;
4+
using Novell.Directory.Ldap.Controls;
45
using System;
56
using System.Collections.Generic;
67
using System.Threading;
@@ -41,10 +42,10 @@ public static Result SearchObjects([PropertyTab] Input input, [PropertyTab] Conn
4142
if (connection.IgnoreCertificates)
4243
ldco.ConfigureRemoteCertificateValidationCallback((sender, certificate, chain, errors) => true);
4344

44-
LdapConnection conn = new LdapConnection(ldco);
4545
var defaultPort = connection.SecureSocketLayer ? 636 : 389;
4646
var atr = new List<string>();
4747
var searchResults = new List<SearchResult>();
48+
4849
var searchConstraints = new LdapSearchConstraints(
4950
input.MsLimit,
5051
input.ServerTimeLimit,
@@ -53,7 +54,8 @@ public static Result SearchObjects([PropertyTab] Input input, [PropertyTab] Conn
5354
false,
5455
input.BatchSize,
5556
null,
56-
0);
57+
0
58+
);
5759

5860
if (input.Attributes != null && input.SearchOnlySpecifiedAttributes)
5961
foreach (var i in input.Attributes)
@@ -75,8 +77,10 @@ public static Result SearchObjects([PropertyTab] Input input, [PropertyTab] Conn
7577

7678
try
7779
{
80+
using var conn = new LdapConnection(ldco);
7881
conn.SecureSocketLayer = connection.SecureSocketLayer;
7982
conn.Connect(connection.Host, connection.Port == 0 ? defaultPort : connection.Port);
83+
8084
if (connection.TLS)
8185
conn.StartTls();
8286

@@ -85,35 +89,28 @@ public static Result SearchObjects([PropertyTab] Input input, [PropertyTab] Conn
8589
else
8690
conn.Bind(version: ldapVersion, connection.User, connection.Password);
8791

88-
LdapSearchQueue queue = conn.Search(
89-
input.SearchBase,
90-
SetScope(input),
91-
string.IsNullOrEmpty(input.Filter) ? null : input.Filter,
92-
atr.ToArray(),
93-
input.TypesOnly,
94-
null,
95-
searchConstraints);
92+
byte[] cookie = null;
9693

97-
LdapMessage message;
98-
while ((message = queue.GetResponse()) != null)
94+
do
9995
{
100-
if (message is LdapSearchResult ldapSearchResult)
96+
if (input.PageSize > 0)
10197
{
102-
var entry = ldapSearchResult.Entry;
103-
var attributeDict = new Dictionary<string, dynamic>();
104-
var getAttributeSet = entry.GetAttributeSet();
105-
var ienum = getAttributeSet.GetEnumerator();
98+
var pagedResultsControl = new SimplePagedResultsControl(input.PageSize, cookie);
99+
searchConstraints.SetControls(pagedResultsControl);
100+
}
106101

107-
while (ienum.MoveNext())
108-
{
109-
cancellationToken.ThrowIfCancellationRequested();
110-
LdapAttribute attribute = ienum.Current;
111-
AddAttributeValueToList(input, attribute, attributeDict, encoding, cancellationToken);
112-
}
102+
LdapSearchQueue queue = conn.Search(
103+
input.SearchBase,
104+
SetScope(input),
105+
string.IsNullOrEmpty(input.Filter) ? null : input.Filter,
106+
atr.ToArray(),
107+
input.TypesOnly,
108+
null,
109+
searchConstraints);
110+
111+
ProcessSearchResults(queue, searchResults, cancellationToken, encoding, input, ref cookie);
112+
} while (cookie != null && cookie.Length > 0);
113113

114-
searchResults.Add(new SearchResult() { DistinguishedName = entry.Dn, AttributeSet = attributeDict });
115-
}
116-
}
117114
return new Result(true, null, searchResults);
118115
}
119116
catch (LdapException ex)
@@ -124,12 +121,47 @@ public static Result SearchObjects([PropertyTab] Input input, [PropertyTab] Conn
124121
}
125122
catch (Exception ex)
126123
{
127-
throw new Exception($"SearchObjects error: {ex}");
124+
throw new Exception("SearchObjects error", ex);
128125
}
129-
finally
126+
}
127+
128+
private static void ProcessSearchResults(LdapSearchQueue queue, List<SearchResult> searchResults, CancellationToken cancellationToken, Encoding encoding, Input input, ref byte[] cookie)
129+
{
130+
LdapMessage message;
131+
while ((message = queue.GetResponse()) != null)
130132
{
131-
if (connection.TLS) conn.StopTls();
132-
conn.Disconnect();
133+
cancellationToken.ThrowIfCancellationRequested();
134+
135+
if (message is LdapSearchResult ldapSearchResult)
136+
{
137+
var entry = ldapSearchResult.Entry;
138+
var attributeDict = new Dictionary<string, dynamic>();
139+
var getAttributeSet = entry.GetAttributeSet();
140+
var ienum = getAttributeSet.GetEnumerator();
141+
142+
while (ienum.MoveNext())
143+
{
144+
cancellationToken.ThrowIfCancellationRequested();
145+
LdapAttribute attribute = ienum.Current;
146+
AddAttributeValueToList(input, attribute, attributeDict, encoding, cancellationToken);
147+
}
148+
149+
searchResults.Add(new SearchResult() { DistinguishedName = entry.Dn, AttributeSet = attributeDict });
150+
}
151+
else if (message is LdapResponse ldapResponse)
152+
{
153+
var controls = ldapResponse.Controls;
154+
if (controls != null)
155+
{
156+
foreach (var control in controls)
157+
{
158+
if (control is SimplePagedResultsControl pagedResultsResponse)
159+
{
160+
cookie = pagedResultsResponse.Cookie;
161+
}
162+
}
163+
}
164+
}
133165
}
134166
}
135167

0 commit comments

Comments
 (0)