Everyone that needed to develop an application that uses System.DirectoryServices to connect to Microsoft’s Active Directory in a multithreaded application surely would agree with me when I say that it was a living nightmare. You always had to use ‘using’ or ‘Monitor.Enter/Monitor.Exit’ to keep all the threads synchronized which obviously slowed down your application so much…
This is probably why Microsoft developed ADWS (Active Directory Web Services) as part of Windows 2008 R2. The ADWS generates LDAP queries internally just as you would but the trick is that it uses a connection pooling mechanisms to make it respond as quickly as possible. However, the documentation of those services is sometimes missing the needed information for the average .Net developer and sometimes they’re simply inacurate.
This is the main reason that brought me to write this post after struggling so hard with it to develop a simple ASMX webservice that allows the client application to retrieve information from the Active Directory and update it when needed. I never could have anticipated how difficult it’s going to be.
Let’s take a close look at the ADWS: If you’d open the services control panel applet on a Windows 2008 R2 domain controller you’ll see a service named “Active Directory Web Services”, this service hosts all the ADWS services. If it isn’t started already – Start it now. Well, technically the ADWS isn’t a webservice, instead – it’s a collection of WCF services accesible on TCP port 9389 that includes the following WCF services: Resource, ResourceFactory, Enumeration, AccountManagement and TopologyManagement. Each one of those services can be used with integrated Windows authentication as well as with cleartext username and password. Each service has two URLs, one for every type of access, here are the URLs for each service: All the URLs are based on the same base path, like so: Windows Autentication: net.tcp://dc_server_name:9389/ActiveDirectoryWebServices/Windows/ User/Password Authentication: net.tcp://dc_server_name:9389/ActiveDirectoryWebServices/UserName/ At the end of these base URLs you should add the service name as listed before so, for example the service URL for the account management service with integrated windows authentication would look like this: net.tcp://dc_server_name:9389/ActiveDirectoryWebServices/Windows/AccountManagement The WCF service metadata can be obtained here: net.tcp://dc_server_name:9389/ActiveDirectoryWebServices/mex Now, how can all this works when you need to access the ADWS from a .Net appication? Well, to make a long story short, I think the best way to answer this would be by providing an example on how it can be done: First, create a .Net project (it can be any project type you’d like – ASMX WebService, ASP.Net page, Winforms Application, Console Application, DLL, really anything), then add a service reference (by right clicking references and clicking on “Add service reference” and enter the mex URL as specified above). The next thing would be to write your code. Here’s how it should look like (For the clearity of this tutorial I named my WCF proxy namespace ‘ADWS_NS’ but you can choose any other name you like):
[cce lang="csharp"] using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Data; using System.Text; using System.ServiceModel.Channels; using System.ServiceModel; using System.Net; using System.Xml; using System.Runtime.Serialization.Formatters; using System.Runtime.Remoting.Messaging; namespace MyAdwsSampleAppNS { public class MyAdwsSampleApp { public static void Main(string[] args) { try { NetTcpBinding bnd = new NetTcpBinding(); bnd.MaxReceivedMessageSize = 1048576; ADWS_NS.ResourceFactoryClient rfc = new ADWS_NS.ResourceFactoryClient(bnd, new EndpointAddress("net.tcp://serverName:9389/ActiveDirectoryWebServices/Windows/ResourceFactory")); ADWS_NS.AccountManagementClient amc = new ADWS_NS.AccountManagementClient(bnd, new EndpointAddress("net.tcp://serverName:9389/ActiveDirectoryWebServices/Windows/AccountManagement")); ADWS_NS.TopologyManagementClient tmc = new ADWS_NS.TopologyManagementClient(bnd, new EndpointAddress("net.tcp://serverName:9389/ActiveDirectoryWebServices/Windows/TopologyManagement")); ADWS_NS.SearchClient sc = new ADWS_NS.SearchClient(bnd, new EndpointAddress("net.tcp://serverName:9389/ActiveDirectoryWebServices/Windows/Enumeration")); rfc.ClientCredentials.Windows.AllowedImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Impersonation; amc.ClientCredentials.Windows.AllowedImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Impersonation; tmc.ClientCredentials.Windows.AllowedImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Impersonation; sc.ClientCredentials.Windows.AllowedImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Impersonation; //Topology management info - BEGIN (Use topoplogy management to get information about the Active Directory itself and about the ADWS itself, such as roles holders, replication topology, forest and domains ) int vMinor; string vString; dtStart = DateTime.Now; Console.WriteLine("v" + tmc.GetVersion(out vMinor, out vString) + "." + vMinor + " (" + vString + ") (" + DateTime.Now.Subtract(dtStart).TotalMilliseconds + "ms taken)"); Console.WriteLine(); dtStart = DateTime.Now; ADWS_NS.ActiveDirectoryForest frst = tmc.GetADForest("ldap:389"); Console.WriteLine("Forest domain naming master: " + frst.DomainNamingMaster + " (" + DateTime.Now.Subtract(dtStart).TotalMilliseconds + "ms taken)"); dtStart = DateTime.Now; ADWS_NS.ActiveDirectoryDomain dom = tmc.GetADDomain("ldap:389"); Console.WriteLine("PDC emulator: " + dom.PDCEmulator + " (" + DateTime.Now.Subtract(dtStart).TotalMilliseconds + "ms taken)"); Console.WriteLine(); //Topology management info - END //Account management - BEGIN (Use account management to set or change user's password, retrieve user's groups membership information and to retrieve group members information) Console.Write("Please enter a DN of a valid group in the Active Directory: "); string groupDn = Console.ReadLine(); dtStart = DateTime.Now; ADWS_NS.ActiveDirectoryPrincipal[] members = amc.GetADGroupMember("ldap:389", groupDn, dom.DistinguishedName, true); foreach (ADWS_NS.ActiveDirectoryPrincipal member in members) { Console.WriteLine("Group member: " + member.DistinguishedName); } Console.WriteLine("Completed. (" + DateTime.Now.Subtract(dtStart).TotalMilliseconds + "ms taken)"); //Account management - END //Enumeration - BEGIN (Use enumeration to locate Active Directory objects by using LDAP queries) //NOTE: This service reqires that you manually create the WCF request message and analyze the response message //Enumeration - END } catch(Exception ex) { Console.WriteLine("ERROR: The following exception has occurred: " + ex.Message + " (" + ex.GetType().ToString() + ")\n" + ex.StackTrace); } } private static string GetEnumerationContext(ADWS_NS.SearchClient sc, string ldapQuery, string[] fields, string baseObjId) { StringBuilder sbEnumReq = new StringBuilder(); sbEnumReq.Append( "" + "" + "" + "" + WebUtility.HtmlEncode(ldapQuery) + "" + "" + baseObjId + "" + "subtree" + "" + "" + ""); foreach (string field in fields) { sbEnumReq.Append("" + "addata:" + field + ""); } sbEnumReq.Append( "" + "" + "addata:givenName" + "" + ""); StringReader srEnumReq = new StringReader(sbEnumReq.ToString()); XmlTextReader xmlrEnumReq = new XmlTextReader(srEnumReq); Message msgEnumReq = Message.CreateMessage(MessageVersion.Soap12WSAddressing10, "http://schemas.xmlsoap.org/ws/2004/09/enumeration/Enumerate", xmlrEnumReq); msgEnumReq.Headers.Add(MessageHeader.CreateHeader("instance", "http://schemas.microsoft.com/2008/1/ActiveDirectory", "ldap:389", false)); msgEnumReq.Headers.MessageId = new UniqueId(); msgEnumReq.Headers.ReplyTo = new EndpointAddress("http://www.w3.org/2005/08/addressing/anonymous"); msgEnumReq.Headers.To = new Uri("net.tcp://" + ADWS_ServerName + ":9389/ActiveDirectoryWebServices/Windows/Enumeration"); Message msgEnumResp = sc.Enumerate(msgEnumReq); StringBuilder sbEnumResp = new StringBuilder(); StringWriter swEnumResp = new StringWriter(sbEnumResp); msgEnumResp.WriteBody(new XmlTextWriter(swEnumResp)); sbEnumResp.Replace("<wsen:", "<wsen-"); sbEnumResp.Replace("</wsen:", "</wsen-"); XmlDocument xdocEnumResp = new XmlDocument(); xdocEnumResp.InnerXml = sbEnumResp.ToString(); string guidEnumCtxt = SafelySelectNodeText(xdocEnumResp, "//wsen-EnumerationContext"); return guidEnumCtxt; } private static string SafelySelectMultiValuedNodeText(XmlNode xdoc, string xpath, string delimiter) { StringBuilder resString = new StringBuilder(); XmlNodeList res = xdoc.SelectNodes(xpath); if (res != null && res[0] != null) { for (int i = 0; i < res[0].ChildNodes.Count; i++) { XmlNode node = res[0].ChildNodes[i]; resString.Append(node.InnerText); if (i + 1 < res[0].ChildNodes.Count) { resString.Append(delimiter); } } } return resString.ToString(); } private static string SafelySelectNodeText(XmlNode xdoc, string xpath) { XmlNode res = xdoc.SelectSingleNode(xpath); return ((res == null) ? "" : res.InnerText); } private XmlDocument PullEnumeratedInfo(ADWS_NS.SearchClient sc, string guidEnumCtxt) { StringReader srPullReq = new StringReader( "" + "" + guidEnumCtxt.ToString() + "" + "PT10S" + "2000" + ""); XmlTextReader xmlrPullReq = new XmlTextReader(srPullReq); Message msgPullReq = Message.CreateMessage(MessageVersion.Soap12WSAddressing10, "http://schemas.xmlsoap.org/ws/2004/09/enumeration/Pull", xmlrPullReq); msgPullReq.Headers.Add(MessageHeader.CreateHeader("instance", "http://schemas.microsoft.com/2008/1/ActiveDirectory", "ldap:389", false)); msgPullReq.Headers.MessageId = new UniqueId(); msgPullReq.Headers.ReplyTo = new EndpointAddress("http://www.w3.org/2005/08/addressing/anonymous"); msgPullReq.Headers.To = new Uri("net.tcp://" + ADWS_ServerName + ":9389/ActiveDirectoryWebServices/Windows/Enumeration"); Message msgPullResp = sc.Pull(msgPullReq); StringBuilder sbPullResp = new StringBuilder(); StringWriter swPullResp = new StringWriter(sbPullResp); msgPullResp.WriteBody(new XmlTextWriter(swPullResp)); sbPullResp.Replace("<addata:", "<addata-"); sbPullResp.Replace("</addata:", "</addata-"); sbPullResp.Replace("<ad:", "<ad-"); sbPullResp.Replace("</ad:", "</ad-"); XmlDocument xdoc = new XmlDocument(); xdoc.InnerXml = sbPullResp.ToString(); return xdoc; } } } [/cce]
Hello there!
This would be what I’m looking for, but there’s a missing part in your code at line 118-119. Could you please fix this?
Many thanks
Hi,
That post is extremely old… Are you sure that this is what you need? I published that post almost 4 years ago…
Maybe if you’ll provide me with some insights on what are you trying to do I can offer a better solution?
Anyway – I just looked at that code and fixed it…
Thanks for your comment (-:
Fine way of explaining, and nice post to take data concerning my presentation subject
matter, which i am going to present in university.
Thanks a lot for your comment!
Hi Team,
Can i get the the details on retrieving the User attributes by passing samaccount name?