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 Windows Azure
Buy the Book
Contact Ryan