Thursday, June 5, 2008
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.