Why bother learning about the .NET Framework? After all, many AD administrators have been scripting happily for years with VBScript, PowerShell, ADSI, WMI, and a pile of command-line tools. We have been getting along just fine. This .NET stuff is really for the enterprise developers writing line-of-business applications, is it not?
First, let’s be clear that .NET may not be for everyone. There are many techniques available for programming the directory, and you may not need to move away from the tools and techniques you already use. However, there are some compelling reasons to consider it:
Powerful features that were once only available to C++ developers are now being exposed in the .NET Framework. This makes it easier for the vast majority of us who do not program in C++ to get to these features.
Microsoft has a powerful web development platform called ASP.NET that vastly improves how we can build applications for the Web. Many AD development tasks lend themselves to web-based deployment.
Microsoft’s 2008, hardly new! PowerShell command shell, based on the .NET Framework, completely changes the equation for how shell programmers and scripters can program and administer Windows.
.NET is a programming environment that can be accessed from many different languages. In fact, the runtime environment for .NET (or “managed”) code is called the Common Language Runtime, or CLR. The runtime is fundamentally object-oriented, but is otherwise neutral to the language syntax used to program it. At the time of this writing, there are dozens of different languages that can be used to write .NET code, from the time-honored COBOL to experimental languages like F#.
Microsoft itself ships tools for several of these languages, including C#, Visual Basic.NET (or VB.NET), C++, and JScript.NET. It is fair to say that most .NET developers use either C# or VB.NET, with Microsoft itself using C# for the vast majority of its own work. C# as a language looks a bit like C++, but a lot more like Java. However, for many of us, our background is in writing VBScript code, so for this book we will present all of the examples in VB.NET.
This decision will likely make a few people grumpy, but it is the best compromise. For those who are put off by this decision, please take consolation in the fact that the thing that really matters is learning the framework itself. The language details are not usually that important. In fact, the languages are often so similar that there are many free tools that can translate code from one language to another with no loss at all.
Unfortunately, there is no way we can explain the basics of programming .NET or any specific languages feature’s in the space we have remaining. Those sorts of primers already exist on the Internet and in bookstores. Instead, we will take the approach of keeping things simple enough to not require a lot of background and additional depth.
Since the last version of this book was published, the landscape for .NET development tools has expanded as much as the framework itself. Today, you have a lot of choices for how you can write .NET code. The primary decision to make is whether or not you want to use an integrated development environment (IDE) for your development tasks. IDEs simplify development by making it easier to organize your code files and build them into executables.
Back in the days of Visual Studio.NET 2002, Microsoft was basically the only game in town for .NET IDEs. However, that story has changed significantly. Not only does Visual Studio 2012 come in a variety of packages, some of them completely free, but there are other non-Microsoft products available to choose from, such as SharpDevelop from IC# Code.
The point here is that there are many options, and quite a few of them are free. It is not necessary to buy an expensive tool to write .NET code. Although we do not endorse a particular tool, we will say that most .NET developers use Microsoft’s Visual Studio, and the free Express versions are suitable for our needs, so that is a reasonable place to start.
Although an IDE can be nice, we do not actually need one to write code for the .NET Framework. In fact, Microsoft provides a free downloadable SDK with all versions of the framework that includes all the tools we need to build .NET code. Though it is rarely done, it is certainly possible to dive right in with Notepad and a command prompt! There are also a variety of tools available, such as Jeff Key’s Snippet Compiler, that provide an experience similar to scripting, but with compiled code. In addition, all of the examples in this appendix can be accomplished in PowerShell since Windows PowerShell is built on top of the .Net Framework.
At the time of this writing, Microsoft has released versions 1.0, 1.1, 2.0, 3.0, 3.5, 4.0, and 4.5 of the .NET Framework. It can be confusing to understand which .NET features are available in which releases, which release came bundled with which operating system release, and how code written for one version will work on another version. Let’s unravel all that.
First, a few general notes on .NET Framework versions:
It is not a problem to install multiple versions of the framework on the same machine. They happily coexist when installed side by side. In fact, both .NET 3.0 and 3.5 require .NET 2.0 to be installed, because they are not actually standalone releases. NET 3.5 also requires Service Pack 1 for .NET 2.0 to be installed.
By default, code compiled against a specific version of the framework will run against that same version of the framework if it is installed on the machine on which the code is run. However, if that version of the framework is not installed where the code is running but a newer version is installed, the code will run against the newer version.
Generally speaking, code written against an earlier version of the framework will run with no problems against a later version of the framework. There are a few backward-compatibility problems, but they are rare overall.
Make sure you install the latest service packs as they are released.
Depending on the operating system (OS) of the machine where you want to run .NET code, a version of the framework may already be included. Table A-1 summarizes which OS has what. An article at microsoft.com has an excellent diagram that shows the iterative versions of the framework and what version of the underlying CLR is used.
Operating system | Included framework versions |
Windows 2000 | None |
Windows XP | None |
Windows Server 2003 | 1.1 |
Windows Server 2003 R2 | 1.1, 2.0 |
Windows Vista | 2.0, 3.0 |
Windows Server 2008 | 2.0, 3.0 |
Windows 7 | 2.0, 3.0, 3.5 SP1 |
Windows Server 2008 R2 | 2.0, 3.0, 3.5 SP1 |
Windows 8 | 4.5 |
Windows Server 2012 | 4.5 |
You can optionally install .NET 3.5 (along with .Net 2.0 and .Net 3.0) on Windows 8 and Windows Server 2012.
Unfortunately, once we include service packs and other factors, the matrix of possible options for .NET Framework installation across all of the different OS versions can get a little hairy. We will not attempt to document this completely and will instead defer to Microsoft’s own deployment guides for the details:
.NET 1.0: http://msdn.microsoft.com/en-us/library/ms994336.aspx
.NET 1.1: http://msdn.microsoft.com/en-us/library/ms994339.aspx
.NET 2.0: http://msdn.microsoft.com/en-us/library/aa480237.aspx
.NET 3.0: http://msdn.microsoft.com/en-us/library/aa480173.aspx
.NET 3.5: http://msdn.microsoft.com/en-us/library/cc160717.aspx
.NET 4.0: http://msdn.microsoft.com/en-us/library/ee390831(VS.100).aspx
.NET 4.5: http://msdn.microsoft.com/en-us/library/ee390831.aspx
If you’re using .NET 3.5 with a forest that’s at the Windows Server 2008 R2 functional level, make sure you’ve reviewed the hotfix described in Microsoft Knowledge Base article 2260240 to see if it applies to your application.
The .NET Framework includes a huge number of classes and other
programming types. In order to keep things organized, these types are
arranged in hierarchical groupings called
namespaces. The core features of the framework are
arranged under the System
namespace.
There are now four main namespaces specifically used for directory services programming:
System.DirectoryServices
(SDS)
System.DirectoryServices.ActiveDirectory
(SDS.AD)
System.DirectoryServices.Protocols
(SDS.P)
System.DirectoryServices.AccountManagement
(SDS.AM)
For brevity, we will refer to the namespaces by the above abbreviations throughout the text.
This does not include all of the other types we might need to access to perform additional programming tasks and also ignores access to WMI (something that .NET can do, but not something we will cover at all in this book).
In order to keep the framework modular, major pieces of functionality are compiled into separate components called assemblies. An assembly is essentially just a DLL. An assembly can contain types in multiple namespaces, and namespaces can span different assemblies. Do not let this confuse you, though.
There are three important things to know here:
In order to use a type in a specific assembly, our code must reference that assembly.
The directory services programming features are bundled into separate assemblies from the rest of the .NET Framework so that code that does not need these features does not need to load the types contained in these assemblies.
By default, none of the various projects types in Visual Studio reference any of the directory services programming assemblies, so we must remember to add these references. This will likely apply to whatever tool you end up using for development.
Table A-2 summarizes which namespaces are available in which .NET Framework releases.
Namespace | Assembly | Version |
| System.DirectoryServices.dll | 1.0+ |
| System.DirectoryServices.dll | 2.0+ |
| System.DirectoryServices.Protocols.dll | 2.0+ |
| System.DirectoryServices.AccountManagement.dll | 3.5+ |
The bottom line is that .NET 2.0 contains the majority of the available features, but you need .NET 3.5 to get all of them. Looking at Table A-1, this also implies that you may need to get one of the newer versions of .NET installed where you want to run your code.
Our recommendation is that you use at least .NET 3.5, and we will assume .NET 3.5 unless otherwise noted. Use the latest service packs as well.
First, let’s take a high-level view of how all of the various pieces fit together.
As you might imagine, the .NET Framework does not attempt to implement the entire operating system from the ground up, but instead uses existing operating system services to some extent or another and repackages them in the more consistent .NET programming model. All of the .NET directory services features leverage existing operating system functionality, some of which we have already covered in this book.
Figure A-1 depicts how the various pieces fit together.
As with most software architectures, the model is built up in layers from low-level components to high-level ones. At the top, we see how the various .NET components fit together and depend on each other.
Right at the center of the diagram, we see ADSI and how it depends on various lower-level operating system components. You will find it reassuring that much of the investment you have already made in learning ADSI from this book and other sources will directly carry over to your .NET programming activities.
Now, let’s dig into the high-level details of each namespace in turn.
The System.DirectoryServices
namespace (SDS
) is the primary
namespace we use for programming directory services in .NET. It is the
oldest namespace, having been included with .NET since the very first
1.0 release.
SDS
is basically just a simple
interoperability layer over the existing ADSI programming model. Most of
what you already know about ADSI will apply directly to SDS
. SDS
is
in many ways like a stripped-down version of ADSI, but in other ways it
is more powerful. In all cases, it is ADSI that does the bulk of the
work, with the underlying components it depends on doing the actual
heavy lifting.
By “stripped down,” we mean that SDS
supports the core interfaces in ADSI,
such as IADs
and IADsContainer
, but does not directly support
any of the so-called “persistent object interfaces,” such as IADsGroup
and IADsUser
. This does not mean that we cannot
use those interfaces, but that we may need to jump through some
additional hoops to get to them.
Even though there are quite a few types in the namespace, almost
everything in SDS
revolves around two
core classes:
DirectoryEntry
DirectorySearcher
DirectoryEntry
is essentially a
wrapper around the ADSI IADs
interface and is most similar to programming with the familiar IADsOpenDSObject.OpenDSObject
and GetObject
methods. In fact, DirectoryEntry
allows us to “drop down” into
ADSI whenever we need to by exposing a NativeObject
property that provides direct
access to the underlying ADSI COM object.
DirectoryEntry
is used to
connect to the directory, refer to specific objects, read and write
their attributes, create new objects, delete objects, and move objects.
Everything starts with a DirectoryEntry
object.
DirectorySearcher
is the more
interesting part of SDS
. As its name
implies, it allows us to search the directory via LDAP queries. In the
ADSI scripting world, we use ActiveX Data Objects (ADO) for querying the
directory via the ADSI OLE DB provider, while we use GetObject
to access specific objects and
perform modifications. This approach made it easier for developers
accustomed to querying SQL databases to learn LDAP, but the two models
were never well integrated, and ADO gets a little clunky when special
features need to be accessed. Additionally, many of the powerful
advanced LDAP query features supported by Active Directory never got
exposed to ADO, so developers using this technology were basically out
of luck.
DirectorySearcher
takes a
different approach. Instead of using a similar pattern and having .NET
developers use the classes in System.Data
to execute LDAP searches, the
designers of the framework gave us a component purpose-built for doing
LDAP queries. The main advantages are:
It offers tight integration with DirectoryEntry
and the other supporting
classes in SDS
.
It works in terms of LDAP-specific search concepts such as search base, query scope, and filters instead of using SQL terminology that may not apply.
Special features such as paged searches and timeout settings are cleanly supported via strongly typed properties.
All of the advanced Active Directory search features exposed by ADSI are supported.
You’ll see how some of this looks when we show some examples in the upcoming sections.
The .NET Framework data access features in System.Data
still support OLE DB data
sources. This means that the OLE DB provider we used with ADO in VB
and VBScript can still be used with .NET as well. This may seem like
the natural thing to do for programmers coming from that background.
Don’t do it!
Besides having all of the same clumsiness issues that ADO always
suffered from, it is not as fast and is completely unnecessary now
that we have DirectorySearcher
.
While we are issuing warnings, VB.NET also supports a GetObject
method for backward-compatibility
reasons. Don’t use this for accessing Active Directory objects either.
Use DirectoryEntry
instead.
Under the hood, DirectorySearcher
accomplishes its magic by
interoperating with a low-level ADSI interface called IDirectorySearch
. IDirectorySearch
has been in ADSI for a long
time, but VB and VBScript developers could not call it directly as it
was designed only for C++ developers. The ADSI OLE DB provider we use in
VBScript is actually another wrapper around IDirectorySearch
that adapts IDirectorySearch
to the ADO programming model.
With DirectorySearcher
, we get a
feature designed for LDAP programming and with all the special features
exposed.
The .NET Framework has first-class support for Windows security
objects such as security descriptors, DACLs, ACEs, and SIDs.
Consequently, SDS
follows suit and
provides support for the full complement of Active Directory
security-descriptor-manipulation functions. The classes that support
this are much faster and more powerful than the ADSI-based interfaces
we used to program them before, such as IADsSecurityDescriptor
.
We usually do not spend much time writing code that manipulates security descriptors, but full support for these features is welcome in the cases when we do.
SDS
is the primary
namespace we use for .NET directory services programming.
DirectoryEntry
and
DirectorySearcher
are the two
main classes.
SDS
is based on ADSI, so
much of our ADSI knowledge applies directly.
SDS
is a little
stripped-down compared to full ADSI, but it works well in
general.
DirectorySearcher
is a
better query interface than what we had before.
As its name implies, SDS.AD
provides support for performing operations on Active Directory
specifically. When we say Active Directory, we include both AD Domain
Services and AD Lightweight Directory Services (AD LDS).
SDS.AD
provides access to
functions that previously were difficult or impossible for developers
using languages other than C++ to access. The functions can be grouped
into the following categories:
Active Directory infrastructure (forests, domains, global catalogs, domain controllers, sites, etc.)
Replication
Trusts
Directory schema
SDS.AD
accomplishes this goal
by using a clever design to marry the ADSI programming model to a set of
non-LDAP RPC interfaces with function names like DsGetDCName
. As we will see, SDS.AD
is integrated tightly with SDS
, making it easy to move between the two
namespaces as needed.
You may be looking at the list from the previous section and
thinking that the number of times you’ve needed or wanted to be able
to write code to create a trust could be counted on one hand (or
less), and you might be right. In fact, much of the functionality
exposed by SDS.AD
is stuff that
most of us will never need to automate. The tasks involving SDS.AD
classes are infrequent, and other
tools are often more appropriate to accomplish those tasks.
The sweet spot for SDS.AD
is
in the first point, which is the access to Active Directory
infrastructure components. The ability to do things like enumerate
domains and domain controllers is helpful and very easy to use here.
Additionally, SDS.AD
provides
access to the full power of the DC locator component of Windows
(discussed in Chapter 8) and allows
us to access all of its advanced features, such as finding domain
controllers in specific sites and forcing rediscovery. This was never
available to us at all via ADSI or SDS
before.
With this in mind, we will proceed to ignore all of the other,
more obscure features of SDS.AD
and
instead concentrate on the infrastructure features in our upcoming
examples.
SDS.AD
provides many powerful features
that were not previously available to most programmers.
It is tightly integrated with SDS
.
Some of the functions supported are a bit obscure, but the infrastructure features are always useful.
Unlike SDS.AD
, which layers new
capabilities on top of the existing ADSI-based SDS
, SDS.P
is a completely different animal. Referring back to Figure A-1, we see that SDS.P
does not layer on top of ADSI at all,
but instead sits directly on top of the core Windows LDAP library,
wldap32.dll. We also see that SDS.P
does not overlap with SDS
at all.
In a nutshell, SDS.P
gives us a
completely different model for programming LDAP. Instead of using the
“object-centric” metaphor of ADSI, where you create programmatic objects
that point to specific objects in the directory in order to manipulate
them, SDS.P
applies the
“connection-centric” metaphor inherent in the standards-based LDAP API
design. For example, in SDS.P
we
create a DirectoryConnection
object
to connect to the underlying directory, and then we perform operations
on that connection by sending it messages such as search requests and
receiving messages such as search responses in reply.
SDS.P
implements just about
every feature of the underlying LDAP API; it brings the full power of
LDAP to the programmer, including many features not even supported by
ADSI and thus not possible in SDS
. It
also provides the opportunity to get the best possible performance from
LDAP in scenarios where performance trumps productivity (i.e., not
usually scripting).
The downside of this is that SDS.P
is more complicated and demands that you
learn all of the intimate details of LDAP and AD programming. Basically,
it is provided to support the needs of systems programmers rather than
administrators or even normal enterprise developers.
For most of us, the answer is that we will probably never need to. This namespace is not for us, so we can safely ignore it.
However, there are definitely specific scenarios where SDS.P
really is needed or desirable, and we
can at least list a few of them:
You need access to obscure AD LDAP features not supported by ADSI, such as the phantom root or stats controls.
You are programming against a non-Microsoft LDAP directory and ADSI does not mesh well with it.
You are writing a highly scalable multithreaded server application and need full support for asynchronous queries and maximum performance (not available with ADSI).
You use SSL with your LDAP programming and need to override the SSL certificate verification policy so that certain error conditions that would cause an ADSI connection to fail can be ignored.
You need to query a directory using the Directory Services Markup Language (DSML) protocol instead of the LDAP protocol.
At the same time, the difference in complexity between writing
AD code in VBScript and LDAP API code in C++ is much larger than the
difference between programming SDS
and SDS.P
in VB.NET. If you are
inclined to get your hands dirty and are pretty comfortable with LDAP,
feel free to check it out. We will provide one simple sample and leave
it at that.
SDS.P
provides a powerful, low-level
library for programming LDAP directly (no ADSI).
It is not intended for use by typical administrators and enterprise developers; it is intended for systems programmers.
In specific situations, it may be the only way to accomplish a particular goal, but most of these scenarios are uncommon.
It is probably safe for most of us to ignore it.
Since its introduction in NET 1.0, SDS
has often been criticized for being too
difficult to use for performing common user- and groups-management
tasks. It was seen as a step down from the approach provided by ADSI
before it. Microsoft did not really do anything to address this
shortcoming in .NET 2.0, but the 3.5 release changed this
significantly.
In .NET 3.5, Microsoft introduced a new namespace and assembly
called SDS.AM
. Also called “the
principal API,” SDS.AM
significantly
simplifies the tasks associated with managing directory security
principals such as users, computers, and groups.
These are the design goals of SDS.AM
:
Provide a unified design for managing security principals that works the same across all three of Microsoft’s primary directory platforms: AD Directory Services, AD Lightweight Directory Services, and the local machine Security Accounts Manager (SAM) database.
Reduce the need to understand LDAP programming or the underlying details of the store being accessed.
Make the design flexible so that it can be extended to provide
custom functionality and “drop down” into SDS
whenever more power is needed.
Referring back to Figure A-1, we see that SDS.AM
layers on top of SDS
, with a bit of overlap with SDS.P
as well. For the most part, SDS.AM
is based on SDS
and thus ADSI. In a way, we could say that
SDS.AM
reintroduces features ADSI
always had with the IADsUser
,
IADsComputer
, and IADsGroup
interfaces, but in actuality it does
ADSI one better. Because SDS.AM
is
much newer, it allowed the designers to purposefully include support for
AD LDS and to take advantage of many .NET programming features that
simplify development tasks. We will see this when we dive into the
examples.
Referring to Figure A-2, we see that there
are just a few primary classes within SDS.AM
. On the left, we see a class called
Principal
with several other classes
underneath it. The lines here are significant because they indicate an
inheritance relationship. UserPrincipal
derives from AuthenticablePrincipal
, which derives from
Principal
, etc. Principal
is essentially the center of the
universe in SDS.AM
. We can also
derive our own classes from any of these classes as a way to extend
SDS.AM
to our own directory
customizations.
PrincipalContext
is a helper
class used to create different types of Principal
objects. Its primary job is to
specify which type of directory we are accessing.
PrincipalSearcher
and the
classes underneath it are used for finding Principal
objects in the directory and work in
conjunction with PrincipalContext
and
Principal
.
Unlike SDS.P
, SDS.AM
really is intended to be used by most
of us. It significantly simplifies many common tasks and is much
easier to use than any of the other directory services programming
namespaces. It provides such a high degree of abstraction that it
makes it possible for a programmer to know almost nothing about LDAP,
ADSI, or the underlying directory model to accomplish common tasks. It
is by far the most productive API we have been given to date. It also
contains a comparatively small set of types in the namespace, so it
requires little effort to get started.
SDS.AM
is a namespace designed
specifically for managing security principals in Microsoft
directories.
It requires less knowledge about LDAP and the underlying directories than any of the other namespaces.
It provides a highly productive developer experience.
Now, let’s switch focus and dive into some code. Unfortunately, there is no possible way we can do much more here than scratch the surface of all of the types of things we can do with .NET directory services programming. This topic could easily fill an entire book (and has a few times already). If you need a deeper reference, The .NET Developer’s Guide to Directory Services Programming by Joe Kaplan and Ryan Dunn (Addison-Wesley) is an excellent option.
Here are the conventions for the code samples:
For brevity, we will skip error handling and object cleanup code that we really should be using in most “real” work.
Assume that we are using a simple console application type for our samples, but do not let that imply that you can only build console applications. All of these samples may be used in any .NET application type.
Assume we have an assembly reference to
System.DirectoryServices.dll and have imported
namespaces for SDS
and SDS.AD
via the following block of
code:
Imports
System.DirectoryServices
Imports
System.DirectoryServices.ActiveDirectory
Like VBScript and VB6, VB.NET supports a programming technique called “late binding.” Late binding allows us to refer to properties or methods on an object that may not be explicitly available on the current class. This is mostly helpful when working with COM interfaces in .NET.
However, C# does not have such a feature and requires complex
“reflection” code to accomplish the same goal. For that reason, we will
avoid using any late binding in our samples. SDS
provides other ways to accomplish the same
tasks anyway, so late binding is not really necessary. You can add the
Option Strict
and Option Explicit
directives in your code to
disable late binding if you wish.
Additionally, VB.NET is not case-sensitive but C# is, so we’ll use proper case in all of our examples.
There are two basic options available for connecting to the directory:
Use DirectorySearcher
.
Use one of the infrastructure classes in SDS.AD
.
Let’s look at some examples of each in turn. As we discussed earlier in the book, LDAP directories support a special object called RootDSE that allows the programmer to query the directory for information such as what partitions it contains, so we will start there:
'connect to rootDSE using "serverless binding"
Dim
rootDSE
As
New
DirectoryEntry
(
"LDAP://RootDSE"
)
'this uses the full constructor to do the same thing
Dim
rootDSE2
As
New
DirectoryEntry
(
_
"LDAP://RootDSE"
,
_
Nothing
,
_
Nothing
,
_
AuthenticationTypes
.
Secure
_
)
'same as first but specifying a specific domain
Dim
rootDSE3
As
New
DirectoryEntry
(
_
"LDAP://mycorp.com/RootDSE"
)
'now we are using explicit credentials and
'Secure (negotiate) authentication
Dim
rootDSE4
As
New
DirectoryEntry
(
_
"LDAP://rootDSE"
,
_
"[email protected]"
,
_
"Password1"
,
_
AuthenticationTypes
.
Secure
_
)
'bind to LDS rootDSE as an AD LDS user using
'simple bind and SSL to protect the credentials
'on the network
Dim
ldsRootDSE
As
New
DirectoryEntry
(
_
"LDAP://adldsserver.com/RootDSE"
,
_
"otheruser@adlds"
,
_
"Password1"
,
_
AuthenticationTypes
.
SecureSocketsLayer
_
)
The first thing to notice is that this looks quite a bit like
GetObject
or OpenDSObject
in ADSI. This is no accident. The
same technology is being used and the same rules apply. In .NET, we use
a feature called “method overloading” to specify multiple constructors
for the same object. This makes it easy to either use the defaults or
specify explicit credentials and other options whenever we need to. We
also show an example of connecting to AD LDS at the end. Again, the same
rules apply, except that we cannot use “serverless binding” with AD LDS
and must specify a server name.
In the AD LDS example, we connect using SSL and use LDAP simple bind instead of Windows secure bind.
There is no flag to specify a simple bind. Instead, we supply
non-null credentials and specify an AuthenticationType
other than Secure
. It is a little confusing at first,
but it actually works the same way in ADSI. If we still want to do a
secure bind with a Windows user and also use SSL, we simply combine
the flags, like this:
AuthenticationTypes
.
Secure
Or
AuthenticationTypes
.
SecureSocketsLayer
There are many other flags available, but we simply do not have
room to explain them all. They are the same flags available in ADSI in
the ADS_AUTHENTICATION_ENUM
, just
with slightly different names that use .NET standards. Reference
microsoft.com
for a listing of possible AuthenticationTypes
.
Now, let’s read some attributes and connect to the domain root object:
'get the defaultNamingContext for this domain
Dim
ncName
As
string
=
DirectCast
(
_
rootDSE
.
Properties
(
"defaultNamingContext"
).
Value
,
_
String
)
Dim
ncRoot
As
New
DirectoryEntry
(
"LDAP://"
+
ncName
)
'now, let's use SDS.AD to do the same thing
'in one line of code!
Dim
ncRoot2
As
DirectoryEntry
=
_
Domain
.
GetCurrentDomain
().
GetDirectoryEntry
()
In this example, we show how to read LDAP attributes from the
DirectoryEntry
we created previously.
Instead of using Get
and GetEx
like we do in ADSI, .NET implements a
PropertyCollection
that contains
PropertyValueCollection
objects. The
Value
property on PropertyValueCollection
is a helper that
returns:
A null reference (Nothing
)
if the attribute is not set or not available
A single value as Object
if
the attribute has a single value
An array of Object
values
if the attribute is multivalued
This is handy, so we will use it frequently. Since attributes in
AD can be of many different data types and .NET converts them to
specific data types the same way that ADSI does, they are returned as
Object
. This means we must convert
them back into the type they really are.
After that, we use the defaultNamingContext
attribute value from
RootDSE
to build another DirectoryEntry
, this time pointing to the
domain partition root object.
Next, we use an alternate approach to accomplish the same goal in
a single line of code using the Domain
class. Obviously, this is less work and
quite handy. Here are some other examples with the Domain
class:
'create a Domain-type DirectoryContext pointing to
'a specific domain with default credentials
Dim
context1
As
New
DirectoryContext
(
_
DirectoryContextType
.
Domain
,
_
"othercorpdomain.com"
_
)
'build a Domain object with the context
Dim
domain1
As
Domain
=
Domain
.
GetDomain
(
context1
)
'create a Domain-type DirectoryContext pointing to
'a specific domain with alternate plaintext credentials
Dim
context2
As
New
DirectoryContext
(
_
DirectoryContextType
.
Domain
,
_
"othercorpdomain.com"
,
_
"[email protected]"
,
_
"Password1"
_
)
Dim
domain2
As
Domain
=
Domain
.
GetDomain
(
context2
)
With SDS.AD
, we use an object
called DirectoryContext
as a way to
build all sorts of different types of objects, such as domains, forests,
specific servers, and even AD LDS configuration sets or AD application
partitions. We see this pattern repeated in SDS.AM
with the PrincipalContext
class.
We do not set AuthenticationTypes
with DirectoryContext
in the same way we can when
building a DirectoryEntry
. The
underlying code sets all the right defaults for us, including Signing
and Sealing
, although there is no option to use
either SSL or simple bind. SDS.AD
only supports Windows authentication for both AD DS and LDS.
As we said before, SDS.AD
provides a vast array of AD management features, and we do not have room
here to even scratch the surface. Some of our favorite features include
the ability to expand the complete domain tree and locate Global Catalog
servers, from which you can directly construct a DirectorySearcher
for searching the GC. We’ll
end with one final example showing how to enumerate domain controllers
in a specific domain, since nearly everyone has to do this from time to
time:
Dim
currentDomain
As
Domain
=
Domain
.
GetCurrentDomain
()
For
Each
dc
As
DomainController
_
In
currentDomain
.
FindAllDomainControllers
()
'we can do whatever we want now that we have all the DCs...
Console
.
WriteLine
(
dc
.
Name
)
Next
The example is trivially easy, and that is really the whole point.
SDS.AD
takes tasks that can range
from annoying to nearly impossible and makes them easy to
accomplish.
One of the primary reasons we have a directory in the first place
is to look up information in it, so it stands to reason that searching
the directory is one of the things we’ll do most often. Let’s look at
some basic examples of searching with DirectorySearcher
:
Dim
root
As
DirectoryEntry
=
_
Domain
.
GetCurrentDomain
().
GetDirectoryEntry
()
Dim
searcher
As
New
DirectorySearcher
(
root
)
searcher
.
Filter
=
"(&(objectCategory=person)(objectClass=user))"
searcher
.
SearchScope
=
SearchScope
.
SubTree
'we only want this attribute returned, so specify it
searcher
.
PropertiesToLoad
.
Add
(
"sAMAccountName"
)
'enable paging by setting a nonzero page size
searcher
.
PageSize
=
1000
Dim
src
As
SearchResultCollection
Using
(
src
)
src
=
searcher
.
FindAll
()
For
Each
result
As
SearchResult
In
src
Console
.
WriteLine
(
result
.
Properties
(
"sAMAccountName"
)(
0
))
Next
End
Using
A DirectorySearcher
object
always needs a DirectoryEntry
to use
as a way of establishing the connection to the directory and determining
which object will be the base of the search. In this example, we use our
previous shortcut with SDS.AD
to
build a DirectoryEntry
pointing to
the domain partition root object for this purpose. Note that you can
build a DirectorySearcher
without
specifying this, and SDS
will try to
do this for you, but it is nearly always better to be explicit about
your intent in your code.
We use a standard filter for finding user objects and set the
scope to SubTree
, even though that is
not technically necessary since it is the default. We also add the
sAMAccountName
attribute to the list
of PropertiesToLoad
to specify that
we want only that attribute returned instead of the default of “all,” as
per normal LDAP rules.
From a performance perspective, it is important to supply values
to PropertiesToLoad
as a normal
procedure. If we do not, the directory will return Active Directory
default attribute values for each matched object. This results in a
significant waste of network bandwidth, especially for searches that
return many results. It is also worth noting that if we wish to return
operational or constructed attributes such as canonicalName
or allowedAttributesEffective
, we must specify
them in PropertiesToLoad
as they
are never returned in a default search.
We also set the PageSize
to
1000
to enable paged searches. As
with other ADSI-based searches, we do not explicitly loop through the
pages of results returned by the server under the hood. ADSI does this
for us. Generally speaking, it is always a good idea to enable paged
searches as we usually want to get all of the results, even if there are
more than 1,000. If there are less than 1,000, the paging request does
not hurt anything.
To enumerate the results, we use a For
Each
loop against the SearchResultCollection
object returned by the
call to FindAll
. Note that this
approach provides better performance than using a For
loop based on the Count
property of the SearchResultCollection
. This has to do with
the fact that in order to get the Count
, the entire search must be run under the
hood anyway. Using For Each
instead
fetches the results as they are returned.
Notice that like DirectoryEntry
, the SearchResult
class also has a Properties
property that provides access to
the attribute values returned by the search. They behave similarly,
except that SearchResult
objects are
strictly read-only and the ResultPropertyValueCollection
does not have a
handy Value
property like the
PropertyValueCollection
, so we must
access the first value returned by indexing into the array. Make sure
that if you access an attribute that might be null, you first check to
see if the SearchResult
contains that
attribute using the Contains
method.
Otherwise, indexing into the array will result in an error:
If
result
.
Properties
.
Contains
(
"displayName"
)
Then
Console
.
WriteLine
(
result
.
Properties
(
"displayName"
)(
0
))
End
If
The SearchResult
class
contains a method called GetDirectoryEntry
that will return the
DirectoryEntry
object for any given
result using the same security context and connection information
supplied to the original DirectoryEntry
used to build the SearchRoot
for the DirectorySearcher
. While it may seem
tempting to use this method to access the property cache to read
attributes from the search, this is a bad idea from a performance
perspective. The reason is that creating a DirectoryEntry
object results in at least
one and usually two additional searches of the directory to fill the
property cache for each object. Since the SearchResult
object already contains the
results we asked for in our search, it makes no sense to access this
data again in a less efficient way. Always use the SearchResult
when you need to read attribute
values from a search.
The appropriate case to call the GetDirectoryEntry
method is when you need to
perform modifications on the returned object. SearchResult
objects are read-only, so we
must have a DirectoryEntry
if we
need to perform a modification.
DirectorySearcher
is also the
place where many of the special features in AD LDAP are lurking. For
example, DirectorySearcher
provides
access to things like attribute scope queries, directory
synchronization, virtual list views, deleted object searches, and
Extended DN Queries. Once again, we are pressed for
space in this book, but check out some of the other resources available
to find out how to use these advanced features if you find yourself in
need of them.
Now that we have some of the basics covered, let’s turn our attention to modifying the directory. After all, searching is only useful if someone puts some objects in the directory first.
We have a few options when it comes to modifying the directory.
SDS
allows us to create, modify, or
delete just about any type of object in the directory that we can think
of, as long as we know how. It is our workhorse for modifications, so we
will start with it.
On the other hand, much of an administrator’s life tends to
revolve around modifying security principals in the directory, such as
users, computers, and groups. As we’ve already learned, the namespace in
introduced .NET 3.5, SDS.AM
, gives us
a simple, powerful way to
manipulate those types of objects specifically. In the next section, we
will show some samples for performing those tasks, using both namespaces
for contrast.
Finally, SDS.AD
again provides
us with a rich set of features for modifying AD infrastructure objects
such as sites, trusts, and the schema (which is almost always a bad idea
to modify in code, but this is sometimes necessary). This essentially
allows you to build your own versions of the AD Domains and Trusts,
Sites and Services, and Schema Management MMC snap-ins, if you so
desire. While we won’t look at these types of scenarios in depth, we do
want you to know where to look if the need arises.
You cannot perform modification operations on a read-only domain controller. If you attempt to do so, the DC will issue a write referral to a writable DC. This may or may not work.
The other problem with writes to RODCs is that if the write referral did work, the change would go to a different DC than the DC you were trying to access, so the original RODC would not have the new data until replication completes. This can cause chaos in an application that does not anticipate this.
By default, ADSI asks for writable DCs, so this may or may not
actually be a problem. The best solution is to be explicit in your
intentions and ask for a writable DC. Fortunately, .NET provides a
clean way to do this. The LocatorOptions
enumeration now provides a
WriteableRequired
value that can be
specified, so make sure you use the overloaded versions of methods for
locating DCs (such as Domain.FindDomainController
) that allow you
to specify LocatorOptions.WriteableRequired
when
needed.
In order for ADSI on machines running Windows XP or Windows Server 2003 to contact an RODC, you will need to install the compatibility package available from http://support.microsoft.com/kb/944043.
Let’s get started with a simple example, adding a new OU under the root of the domain:
Dim
root
As
DirectoryEntry
=
_
Domain
.
GetCurrentDomain
().
GetDirectoryEntry
()
Dim
newOU
As
DirectoryEntry
=
_
root
.
Children
.
Add
(
"OU=New OU"
,
"organizationalUnit"
)
newOU
.
Properties
(
"description"
).
Value
=
"A new OU"
newOU
.
CommitChanges
()
Adding a new object to the directory using DirectoryEntry
revolves around using the
Add
method on the Children
property of the DirectoryEntry
object that will be the
parent of the newly added object. The first parameter of the Add
method takes the relative distinguished
name (RDN) of the new object. For a default Active Directory forest,
this will always be CN=
xxx
, UID=
xxx
, OU=
xxx
, or
DC=xxx
, as those are the four
default RDN attribute IDs. With AD LDS, there is more flexibility. The
second parameter indicates the objectClass
of the new object.
The second thing to notice is that the Add
method returns a new DirectoryEntry
object. This object is not
yet persisted in the directory, but instead exists in memory until we
actually call the CommitChanges
method. This allows us to set additional attributes on the object
before it is first saved; it is important because some objects define
mandatory attributes that must be set on the object at creation time,
or optional attributes that can only be set at creation time (schema
objects are an example here). Here, we show adding a description
attribute to demonstrate how
this works, although description
is
not mandatory and could be added after the fact.
Removing objects is simple. As a quick example, let’s remove the OU we just created. We will reuse the same variables we already initialized:
root
.
Children
.
Remove
(
newOU
)
Once again, we use the Children
property on DirectoryEntry
, this time calling its
Remove
method. The Remove
method takes another DirectoryEntry
object as its parameter,
which indicates the object to be removed. As long as the object passed
in is a child of the parent and we have the necessary permissions, the
deletion happens immediately.
If we instead want to delete an object and all of its descendants, we must use a slightly different approach. For simplicity, let’s pretend “New OU” from our creation example now has a bunch of child objects, including other containers with children:
newOU
.
DeleteTree
()
Here, we call the DeleteTree
method on the object that we wish to delete along with all of its
descendants. This is a little different than the previous example,
where we removed a single object via the Remove
method on the parent object’s
Children
property.
The overall point here is that we must know which type of deletion we want to perform and use the appropriate technique. In both cases, the deletion is immediate. Hopefully it goes without saying that we must be extremely careful when deleting objects in general!
In ADSI, we have a multipurpose method called MoveHere
that we use for doing object moves,
object renames, or both operations at the same time. SDS
attempts to simplify this model by
providing separate MoveTo
and
Rename
methods instead.
MoveTo
provides two
overloads. One allows us to simply move the object, while the second
overload allows us to move and rename it at the same time. Let’s take
a look. In this example, we have two DirectoryEntry
objects pointing to OUs in
our domain, and we wish to move one OU under the other:
Dim
firstOU
As
New
DirectoryEntry
(
_
"LDAP://OU=First OU,DC=mycorp,DC=com"
)
Dim
newParent
as
New
DirectoryEntry
(
_
"LDAP://OU=Second OU,DC=mycorp,DC=com"
)
'choose one or the other!!!
'this one moves without renaming
firstOU
.
MoveTo
(
newParent
)
'this one moves and renames
firstOU
.
MoveTo
(
newParent
,
"OU=Child OU"
)
In the last line, where we rename the object while moving it, we
supply the new name in RDN format as we did in the creation sample. We
also use this same approach when using the Rename
method to change an object’s name
without moving it. Let’s rename our second OU from the previous
sample:
Dim
toBeRenamed
As
New
DirectoryEntry
(
_
"LDAP://OU=Second OU,DC=mycorp,DC=com"
)
toBeRenamed
.
Rename
(
"OU=A different name"
)
The final aspect of modification basics is changing attribute
values on existing objects. For the most part, this works exactly the
same way we saw in our creation example, where we added an attribute
value between the time we initially created the object and when we
saved it to the directory. We set the desired attribute to the value
we need and call CommitChanges
when
we are done:
Dim
entry
as
New
DirectoryEntry
(
_
"LDAP://CN=test,OU=People,DC=mycorp,DC=com"
)
entry
.
Properties
(
"displayName"
).
Value
=
"new name"
entry
.
Properties
(
"description"
).
Value
=
"new description"
entry
.
CommitChanges
()
Using the Value
property
replaces the existing value with the new value or sets the attribute
if it was not set previously.
If the attribute is multivalued and we wish to modify
incrementally instead of replacing the whole thing, we should instead
use the Add
and Remove
methods to add and remove specific
values. We can also replace an entire multivalued attribute using the
Value
property again. To remove all
attribute values, we call the Clear
method.
The other main thing to keep in mind is that we must supply data in the appropriate type for the attribute value being set. Most directory attributes are strings, but some take numeric, date, or binary data, so check the schema reference documentation to know for sure.
Now that we have the basics of directory modifications mastered, let’s look at a few user management examples. As we have learned by now, effective user management comes down to knowing many of the picky little details about how data is stored in the directory and how to change it to get the results we need. Let’s start with a simple user creation sample:
Dim
parent
As
DirectoryEntry
=
New
DirectoryEntry
(
_
"LDAP://OU=people,DC=mycorp,DC=com"
)
Dim
user
As
DirectoryEntry
=
_
parent
.
Children
.
Add
(
"CN=test.user"
,
"user"
)
user
.
Properties
(
"sAMAccountName"
).
Value
=
"test.user"
user
.
Properties
(
"userPrincipalName"
).
Value
=
"[email protected]"
user
.
CommitChanges
()
This looks fairly similar to our previous example of creating an OU, except that we set some different attributes for the user object. The problem with this is that this user will not have a password and will be disabled by default, so it is not very useful yet. Additionally, if we have a password policy in place requiring passwords, we cannot enable the user until after we have set a password. Let’s revise our sample:
Dim
parent
As
DirectoryEntry
=
New
DirectoryEntry
(
_
"LDAP://OU=people,DC=mycorp,DC=com"
)
Dim
user
As
DirectoryEntry
=
_
parent
.
Children
.
Add
(
"CN=test.user"
,
"user"
)
user
.
Properties
(
"sAMAccountName"
).
Value
=
"test.user"
user
.
Properties
(
"userPrincipalName"
).
Value
=
"[email protected]"
user
.
CommitChanges
()
'this is how we call an IADsUser method
user
.
Invoke
(
"SetPassword"
,
New
Object
()
{
"Password1"
})
'this is how we call an IADsUser property method
user
.
InvokeSet
(
"AccountDisabled"
,
New
Object
()
{
False
})
'or we could do this, which is faster:
user
.
Properties
(
"userAccountControl"
).
Value
=
512
'normal
'force password change at next logon
user
.
Properties
(
"pwdLastSet"
).
Value
=
0
user
.
CommitChanges
()
Suddenly, our sample is getting a little complex. First, notice
that we use a method on DirectoryEntry
called Invoke
to call methods on underlying ADSI
interfaces. In this case, we call IADsUser.SetPassword
. Invoke
takes an array of objects as its other
argument (although in this case it really just wanted a single string),
because the method we could be calling under the hood might take any
number of different arguments of any type and we need a generic
mechanism to make this work.
Next, we see a call to InvokeSet
. While Invoke
is used for calling ADSI methods,
InvokeGet
and InvokeSet
are used specifically for calling
the set and get versions of ADSI properties—in this case, IADsUser.AccountDisabled
. We also show how you
could set userAccountControl
directly
instead of using the ADSI property, although we cheat a little by
setting a direct numeric value instead of changing the single bit we
need to flip in order to remove the disabled flag. In general, setting
this attribute to an arbitrary value is bad practice. Instead, we should
manipulate the individual bit flags to avoid overwriting other settings
and make our intentions in our code more clear.
Finally, we set the pwdLastSet
attribute to 0
to force the user to
change her password at the next login and call the CommitChanges
method to update the object in
the directory.
We also see that we have to do this whole thing in three steps. We
cannot call SetPassword
on an object
that has not been created yet, and we cannot enable an object that has
no password, so all three steps are needed.
Now, let’s pretend we need to add a user to AD LDS. In this case,
we use a totally different method to enable the user. Instead of using
userAccountControl
, AD LDS uses the
msds-userAccountDisabled
Boolean
attribute (set to False
, as you might
guess). While this is certainly easier to deal with than flipping
individual bits on userAccountControl
, the problem is that we
need different code for different stores and have to keep track of all
these details.
Once we throw in all of the rest of the user management functions, such as setting the home directory, account expiration, and so on, it gets messy quickly.
Now, let’s take a look at how SDS.AM
attempts to simplify this by creating
the same user again:
Dim
principalContext
As
new
PrincipalContext
(
_
ContextType
.
Domain
,
_
"MyCorp"
,
_
"OU=People,DC=mycorp,DC=com"
)
Dim
newUser
As
New
UserPrincipal
(
_
principalContext
,
"test.user"
,
"Password1"
,
True
)
Obviously, this is much simpler. The fourth parameter tells
SDS.AM
that we would like the user
enabled during the creation. In fact, the only real LDAP thing we need
to know here is the distinguished name of the container we want to put
the user in.
Where it gets even nicer is the fact that we can create a user
in either the local SAM database or AD LDS simply by varying how we
build the initial PrincipalContext
object. The second line of code stays the same:
Dim
samContext
As
New
PrincipalContext
(
ContextType
.
Machine
)
Dim
ldsContext
As
New
PrincipalContext
(
_
ContextType
.
ApplicationDirectory
,
_
"localhost:50000"
,
"OU=Users,O=Demo"
)
Unlike with SDS
, where we use
the Invoke
and InvokeSet
methods to call into the
underlying ADSI interface members, SDS.AM
provides strongly typed .NET
properties and methods that allow us to manipulate users, groups, and
computers in a friendlier way. For example, we now have methods such
as SetPassword
, ExpirePasswordNow
, IsAccountLockedOut
, and UnlockAccount
, and properties such as
LastPasswordSet
and UserCannotChangePassword
, to make all of
these operations that were once difficult (or at least annoying) both
simple and easy to understand.
The productivity gains with SDS.AM
do not stop there. SDS.AM
also provides:
The full set of user and computer provisioning functions, including read, update, move, and delete functions
A comprehensive set of group management functions, including provisioning of groups and expansion of group membership for both groups and users
A set of functions for finding principals in the directory that require little, if any, LDAP knowledge
An extensibility model that allows us to create our own
Principal
classes that support
our own schema modifications or other relevant AD schema that are
not directly supported in the “out-of-the-box” Principal
classes
A high-performance bind authentication feature for performing LDAP authentication in a scalable, dependable way
Again, we can only scratch the surface here. To dig a little deeper, check out the MSDN magazine article “Managing Directory Security Principals in the .NET Framework 3.5” by Ethan Wilansky and Joe Kaplan, from the January 2008 issue.
As promised, we have up to this point totally avoided any examples
having to do with SDS.P
, suggesting
that it really is not intended for most programmers and not worth
attempting to cover in a high-level introduction such as this one. We
also promised one simple sample to show you what it looks like. This
sample is also relevant because it is something you cannot do in ADSI at
all.
The scenario is that our code needs to connect to an LDAP directory—possibly AD DS or LDS, but also maybe a non-Microsoft LDAP directory—and we need to use SSL. Unfortunately, there is a problem with the server’s SSL certificate. Perhaps the subject name on the certificate does not match the DNS name we must use to access the server, or perhaps we do not trust the certificate’s issuer on our client, or the certificate has expired. ADSI does support SSL/LDAP operations, but it will fail with a “Server Not Operational” error if there is anything wrong with the server’s certificate. This behavior cannot be changed, so with ADSI we are out of luck.
However, the Windows LDAP API does provide
more options here (ADSI simply does not expose access to this advanced
feature). Since SDS.P
provides nearly
the entire scope of Windows LDAP functionality, we also have access to
this feature in SDS.P
and can
override the SSL verification logic:
Public
Module
MyModule
Sub
Main
()
Dim
con
As
New
LdapConnection
(
_
new
LdapDirectoryIdentifier
(
_
"badserver.com:636"
,
True
,
False
))
con
.
SessionOptions
.
SecureSocketLayer
=
True
con
.
SessionOptions
.
VerifyServerCertificate
=
_
AddressOf
ServerCallback
con
.
Credential
=
New
NetworkCredential
(
""
,
""
)
con
.
SessionOptions
.
SecureSocketLayer
=
True
con
.
AuthType
=
AuthType
.
Anonymous
con
.
Bind
()
'do a RootDSE search and get the currentTime attribute
Dim
search
As
New
SearchRequest
(
_
""
,
_
"(objectClass=*)"
,
_
SearchScope
.
Base
,
_
New
String
(){
"currentTime"
}
_
)
Dim
response
As
SearchResponse
=
_
DirectCast
(
con
.
SendRequest
(
search
),
SearchResponse
)
For
Each
entry
As
SearchResultEntry
In
response
.
Entries
Console
.
WriteLine
(
entry
.
Attributes
(
"currentTime"
)(
0
))
Next
End
Sub
Function
ServerCallback
(
_
ByVal
connection
As
LdapConnection
,
_
ByVal
certificate
As
X509Certificate
_
)
As
Boolean
'ignore errors; do not even check the certificate
'just return true
Return
True
End
Function
End
Module
Even though we have tried to scare you with the complexity of
SDS.P
, the code turns out to be
fairly simple.
The main trick here is that we define a method called ServerCallback
that implements a method
signature of a specific delegate (basically a .NET callback function)
defined in SDS.P
called VerifyServerCertificateCallback
. We tell our
LdapConnection
to call our callback
function during SSL certificate verification by using the AddressOf
keyword on the SessionOptions.VerifyServerCertificate
member
to point to our method. ServerCallback
simply returns True
, instructing the underlying code to
accept the certificate presented by the server and ignore any errors. We
could write something more intelligent by looking at the data in the
certificate that is passed to us if we wished.
We are not recommending that you ignore SSL errors as a general practice. This is usually a bad idea, especially in cases where the infrastructure is not something you directly control. SSL is there to help protect us. It is better to try to fix the problem with the server’s certificate instead, but using the correct hostname or adding required trusted root certificates.
The rest of the code just sets up the connection to use SSL and
anonymous authentication and connects to the directory using the
Bind
method. After that, we show a
simple RootDSE
search, a base scope
query with a null search base specified, and return the currentTime
attribute to demonstrate a simple
search operation. However, the point here is not to demonstrate the
search, but to show the SSL verification. We would not even get past the
Bind
method without our SSL
verification override if there was a problem with the server’s
certificate.
.NET, now in version 4.5, is here to stay, and Microsoft continues to invest in these resources for developers while the options in native code APIs have been largely unchanged for years now. Not only does .NET allow us to do the same things we are used to doing in our other tools, but it also provides access to features that were previously only available to C++ developers and new approaches that make development easier.
.NET is also an essential tool to learn for tackling other new technologies, such as ASP.NET web development and Windows PowerShell.
In this appendix, we took a quick tour of the now-expansive landscape of the directory services development namespace in .NET, including the following:
System.DirectoryServices
System.DirectoryServices.ActiveDirectory
System.DirectoryServices.AccountManagement
System.DirectoryServices.Protocols
We’ve only scratched the surface of what we can do, but hopefully we have given you enough to get your feet wet and generate some excitement about the possibilities.