A member in the book's forum mentioned some code I had originally posted here in the blog for asynchronous, paged searches in System.DirectoryServices.Protocols (SDS.P). He questioned whether or not it was thread safe. I honestly don't know - it might not be as I didn't test it extensively.
Regardless, I had actually moved on from that code and started using anonymous delegates for callbacks instead of events. I liked this pattern a bit better because it also got rid of the shared resources.
After reading Stephen Toub's article on asynchronous stream processing, I learned about the AsyncOperationManager which was something I was missing in my implementation. I have been doing a lot lately with .NET 3.5, LINQ, and lambda expressions, so I also decided to rewrite the anonymous delegates to lambda expressions. That is not as big a change, but it is more concise.
I actively investigated using async iterators, but ultimately I decided closures seemed to be more intuitive for me. I might revisit this at some time and change my mind. Here is my outcome:
public class AsyncSearcher { LdapConnection _connect; public AsyncSearcher(LdapConnection connection) { this._connect = connection; this._connect.AutoBind = true; //will bind on first search } public void BeginPagedSearch( string baseDN, string filter, string[] attribs, int pageSize, Action<SearchResponse> page, Action<Exception> completed ) { if (page == null) throw new ArgumentNullException("page"); AsyncOperation asyncOp = AsyncOperationManager.CreateOperation(null); Action<Exception> done = e => { if (completed != null) asyncOp.Post(delegate { completed(e); }, null); }; SearchRequest request = new SearchRequest( baseDN, filter, System.DirectoryServices.Protocols.SearchScope.Subtree, attribs ); PageResultRequestControl prc = new PageResultRequestControl(pageSize); //add the paging control request.Controls.Add(prc); AsyncCallback rc = null; rc = readResult => { try { var response = (SearchResponse)_connect.EndSendRequest(readResult); //let current thread handle results asyncOp.Post(delegate { page(response); }, null); var cookie = response.Controls .Where(c => c is PageResultResponseControl) .Select(s => ((PageResultResponseControl)s).Cookie) .Single(); if (cookie != null && cookie.Length != 0) { prc.Cookie = cookie; _connect.BeginSendRequest( request, PartialResultProcessing.NoPartialResultSupport, rc, null ); } else done(null); //signal complete } catch (Exception ex) { done(ex); } }; //kick off async try { _connect.BeginSendRequest( request, PartialResultProcessing.NoPartialResultSupport, rc, null ); } catch (Exception ex) { done(ex); } } }
It can be consumed very easily using something like this:
class Program { static ManualResetEvent _resetEvent = new ManualResetEvent(false); static void Main(string[] args) { //set these to your environment string servername = "server.yourdomain.com"; string baseDN = "dc=yourdomain,dc=com"; using (LdapConnection connection = CreateConnection(servername)) { AsyncSearcher searcher = new AsyncSearcher(connection); searcher.BeginPagedSearch( baseDN, "(sn=Dunn)", null, 100, f => //runs per page { foreach (var item in f.Entries) { var entry = item as SearchResultEntry; if (entry != null) { Console.WriteLine(entry.DistinguishedName); } } }, c => //runs on error or when done { if (c != null) Console.WriteLine(c.ToString()); Console.WriteLine("Done"); _resetEvent.Set(); } ); _resetEvent.WaitOne(); } Console.WriteLine(); Console.WriteLine("Finished.... Press Enter to Continue."); Console.ReadLine(); } static LdapConnection CreateConnection(string server) { LdapConnection connect = new LdapConnection( new LdapDirectoryIdentifier(server), null, AuthType.Negotiate ); connect.SessionOptions.ProtocolVersion = 3; connect.SessionOptions.ReferralChasing = ReferralChasingOptions.None; connect.SessionOptions.Sealing = true; connect.SessionOptions.Signing = true; return connect; } }
The important thing to note is that because everything is running asynchronously, it is totally possible for the end delegate to be invoked before the paging delegate has a chance to finish processing results (depending on how complicated your code is). You would need to compensate for this yourself.
This client is a console application, so I am using a ManualResetEvent just to prevent it from closing before finishing. You wouldn't need to do this in a WinForms or WPF app.
I am sure there are other optimizations you could make to pass in parameters or even other directory controls. However, the general pattern should apply.
Comments [0] June 5, 2008 Trackback
This is the personal site of Ryan Dunn, co-author of the The .NET Developers Guide to Directory Services Programming.
Ryan currently works for Microsoft and is the Technical Evangelist for SQL Server Data Services (SSDS)
Buy the Book
Contact Ryan