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):


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
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;
        }
    }
}

4 Comments

    1. 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 (-:

      admin

Leave a Reply

Your email address will not be published. Required fields are marked *