Introduction to System.DirectoryServices.ActiveDirectory (S.DS.AD)

 

Ethan Wilansky

January 2007

Summary: Microsoft directory services programming has come a long way since the days when you either had to script directory management via Active Directory Service Interfaces (ADSI) or use C++ to perform more advanced directory management tasks using non-automation interfaces or the LDAP API. .NET 2.0 introduces improvements to System.DirectoryServices and two new namespaces, System.DirectoryServices.ActiveDirectory (S.DS.AD) and System.DirectoryServices.Protocols (S.DS.P). This first paper in this series on directory services programming introduces you to using S.DS.AD to perform Active Directory and ADAM management tasks. (50 printed pages)

Contents

Introduction
   What to Expect
   How to Prepare for Running the Code
   What Not to Expect
System.DirectoryServices.ActiveDirectory Architecture
Common S.DS.AD Tasks
   GettingContext
   Retrieving ADAM and Active Directory Information
   Managing the Schema
   Managing ADAM and Active Directory Topology
   Managing Active Directory Replication
   Managing Active Directory Trusts
References
Conclusion

Introduction

Microsoft directory services programming has come a long way since the days when you either had to script directory management via Active Directory Service Interfaces (ADSI) or use C++ to perform more advanced directory management tasks using non-automation interfaces or the LDAP API. The System.DirectoryServices (S.DS) namespace in .NET 1.1 was a great start because it gave developers the opportunity to write managed code to leverage ADSI. With .NET 2.0, the improvements were just as significant, both in enhancements to System.DirectoryServices and with the introduction of two new namespaces, System.DirectoryServices.ActiveDirectory (S.DS.AD) and System.DirectoryServices.Protocols (S.DS.P).

For a primer on some of the major improvements in S.DS, see the article I wrote for MSDN Magazine in the December 2005 issue at: https://msdn.microsoft.com/msdnmag/issues/05/12/DirectoryServices/default.aspx. For a deep exploration of S.DS, I highly recommend The.NET Developer's Guide to Directory Services Programming by Joe Kaplan and Ryan Dunn. This is an invaluable reference with important directory services programming patterns and real-world guidance that you won't find anywhere else. This book also goes into detail about some of the concepts presented here. Where necessary, Joe and Ryan explore both S.DS.AD and S.DS.P to extend their exploration of S.DS. I have included additional directory services programming and architecture references throughout this white paper.

This paper provides you with an introduction to S.DS.AD by describing common tasks and how you code those tasks with this new namespace. Microsoft created S.DS.AD to simplify many directory programming tasks that administrators are clamoring for — for example, extending an Active Directory Application Mode (ADAM) schema programmatically, or initiating a forced, forest-wide Active Directory replication. While these types of programming tasks are possible to complete with S.DS alone, Microsoft has significantly simplified the programming effort with the classes in this new namespace. The next paper in this series introduces you to programming with S.DS.P.

The example companies, organizations, products, domain names, e-mail addresses, logos, people, places, and events depicted herein are fictitious. No association with any real company, organization, product, domain name, email address, logo, person, places, or events is intended or should be inferred.

What to Expect

This white paper includes copious code examples and an associated code download so that you can test everything explored here. The examples are intentionally simple so that you can quickly grasp their purpose and begin to detect patterns that repeat throughout the exploration. For example, you'll have an excellent understanding of how to get context and bind to many types of directory objects from forests to attribute schema objects by the time you complete this introduction.

The code download is a console application solution containing the DirectoryServices.ActiveDirectory project. The code examples include some error handling. However, for brevity, I've left out a lot of error handling that I consider essential for production code. Use the error handling examples in the code as a starting point. Also, refer to each member of a class that you use in your code to see what exception types it exposes. Code examples are similar to the snippets here, but many of them allow you to supply parameters for items that appear hard-coded in the simple examples with this text.

How to Prepare for Running the Code

To run these examples, have at least one Active Directory forest available; some samples require two forests for establishing a trust relationship. Most examples can be completed with as small as a single DC hosting a forest. Also, have an ADAM instance available and know the designated port numbers applied to the instance. After compiling the solution, your output will be the DS.AD program. Typing the program name at the command line will return a list of available commands and their parameters, as shown here:

  • Forest, domain, and ADAM reporting tasks
    GetDomainData
    GetForestData
    GetGcData
    GetDcData
    GetSchemaData
    GetSchemaClassData className

    GetSchemaPropertyData propertyName
    GetAdamPartitions
  • Schema reporting and management tasks
    GetAdamSchemaData
    GetAdamSchemaClassData
    GetAdamSchemaPropertyData
    AddSchemaClasstoAdam
    AddSchemaAttributetoAdam
  • Topology reporting and management tasks
    GetTopologyData forestName
    **CreateAdSite newSiteName
    CreateAdamSite targetName newSiteName
    CreateSubnet newSubnet siteName
    CreateSiteLink siteName newLinkName
    AddSiteToSiteLink siteName siteLinkName
    RemoveSiteFromSiteLink siteName siteLinkName
    MoveDcToSite sourceDC targetSite
    DeleteAdSite siteName
    DeleteAdamSite targetName siteName
    DeleteLink linkName
    **Replication reporting and management tasks
    GetReplicationStateData
    ReplicateFromSource partitionDN sourceServer targetServer
    ReplicateFromNeighbors targetServer partitionDN
    SyncAllServers partitionDN
    CreateNewConnection sourceServer targetServer connectionName
    SetReplicationConnection server connectionName
    DeleteReplicationConnection server connectionName
  • Trust reporting and management tasks
    GetCurrentForestTrusts
    GetCurrentDomainTrusts
    GetTrustWithTargetForest
    GetTrustWithTargetDomain
    CreateCrossForestTrust targetForest userNameTargetForest password
    SetForestTrustAttributes targetForestName
    ChangeForestTrustToOutbound targetForestName userNameTargetForest password
    AddExcludedDomain targetForestName targetDomainName
    DisableDomainNetbiosName targetForestName targetDomainName
    RepairTrust targetForestName userNameTargetForest password
    RemoveForestTrust targetForestName userNameTargetForest password
    RemoveDomainTrust targetDomainName userNameTargetDomain password

Be sure that the .NET Framework 2.0 is installed wherever you are going to compile and run the sample and also be sure to reference the System.DirectoryServices assembly. The S.DS.AD namespace is within the System.DirectoryServices.dll assembly.

If you're not already familiar with ADAM, visit the ADAM Resource Site at https://www.microsoft.com/adam and read the introductory reviews on this technology.

What Not to Expect

S.DS.AD is a robust namespace and, while I attempt to provide you with a lot of useful introductory examples, it is not a complete survey of the namespace. It also does not provide guidance on best practices for DS programming or guidance on best practices for configuration settings in ADAM or Active Directory. The .NET Developer's Guide to Directory Services Programming will provide the directory programming best practices not included in this introduction, and I'll reference a number of good online resources for configuring Active Directory. Finally, all the examples are in C#. Even if C# isn't your language of choice, I think you will find the examples simple enough to rewrite/convert into your preferred managed code language.

System.DirectoryServices.ActiveDirectory Architecture

Before exploring the code samples, it's useful to take a little time to describe exactly where S.DS.AD fits in the grander scheme of directory services programming. S.DS.AD contains a rich set of strongly typed classes that simplify the process of programming common directory services administrative tasks. For example, the Forest class allows you to get information about a forest and create trust relationships between forests. You can view the classes as providing access to and management of directory partitions, schemas, trust relationships, and directory replication.

Within these four areas of management, S.DS.AD simplifies the process of finding objects in a domain through helper methods such as the FindByName method that allows you to find and bind to all sorts of directory objects, such as a class schema object or a site object, without having to perform a complex query operation.

S.DS.AD relies on the classes in System.DirectoryServices, like the DirectoryEntry class for binding to objects in the directory and, wherever necessary, it uses functions in low-level APIs to perform tasks that provide complete coverage of directory administration tasks. For example, to get all of the trusted domains, S.DS.AD calls the DsEnumerateDomainTrusts function in the Net Win32 API (Netapi32) library. The following figure shows the relationship between S.DS.AD and the components it relies on to perform directory management tasks:

Figure 1. S.DS.AD architectural block diagram

Common S.DS.AD Tasks

Before reading or writing to a directory, such as Active Directory or ADAM, you must complete two tasks. First, you have to establish an initial connection to a directory, which is referred to as getting context, and then you must either bind to an object in the directory when working with S.DS or S.DS.AD. Regardless of what you're binding to, the bind operation serves to authenticate a user using the provided context. Any code operations you complete from that point forward use the security token provided from the authenticated/bound connection.

There are two ways that you can bind to a directory, either by binding using your current context or providing credentials. Credential options include both user names and passwords or certificate-based authentication. While I'll be demonstrating a variety of binding techniques, in most cases the best approach is to bind using your current context unless elevated permissions are required to perform some privileged operation. As I walk through code examples demonstrating how to use S.DS.AD, I will be sure to call-out exactly how I'm completing context and bind operations.

Getting Context

Getting context means to define a connection that will later be used for binding to an object in a directory. To emphasize a point I made earlier, the act of establishing context does not pass authentication information to the target directory, it simply initializes a connection between the running code and the target directory. It's not until a bind operation occurs that credentials are passed over the wire to the directory.

The two types of context relevant to S.DS.AD are ADAM context and Active Directory context. To establish ADAM context, you need to know not only the name of the server hosting a specific instance of ADAM, but a numeric port assigned to the ADAM instance. Establishing Active Directory context is a bit easier because of the way that S.DS.AD takes advantage of the domain controller locator (often called the Locator).

Getting ADAM Context

When using S.DS.AD to work with ADAM, you begin by getting context. To accomplish this task, you create a DirectoryContext object, specify a context type from the DirectoryContextType enumeration and pass the DirectoryContext object a server and port value for the connection. The port value identifies to which instance of ADAM you want to connect. Here's a typical code example for getting context to an ADAM instance:

DirectoryContext adamContext = new DirectoryContext(
DirectoryContextType.DirectoryServer, 
"localhost:389");

In this example, you are getting context to an ADAM instance on the local machine using the standard port assigned to ADAM on a computer not acting as an Active Directory domain controller.

Within the DirectoryContextType enumeration, there are only a few options relevant to ADAM, namely, DirectoryServer or ConfigurationSet. It's likely that you'll use the ConfigurationSet value a lot less than the DirectoryServer value. Configuration set defines a set of ADAM instances that share and replicate a common schema partition and a common configuration partition. If you use the ConfigurationSet value of the DirectoryContextType enumeration, you provide a configuration set name to the DirectoryContext.

Once you have gotten context, you can use the ConfigurationSet class to find information about the ADAM configuration set, like the AdamInstances within the set. While this is not in scope for this paper, you can learn more about ADAM configuration sets and all things ADAM admin. on Server 2003 R2 by visiting http://technet2.microsoft.com/WindowsServer/en/Library/db9893df-3209-4b66-8a68-a17d9bbbd56d1033.mspx?mfr=true. For ADAM pre-Windows Server 2003 R2, visit https://www.microsoft.com/windowsserver2003/adam/default.mspx.

Let's return to the example of getting context using the DirectoryServer value of the DirectoryContextType enumeration. In this case, you provide a host name and port value for your connection. If you have worked with ADAM before, server naming and port assignment (the second parameter required by the DirectoryContext object) will be familiar to you. If not, this explanation should prove helpful. The server naming and port assignment value is the most complicated part of specifying parameters for creating a DirectoryContext object that connects to ADAM. For the server name, you can use an IP address, host name, or fully qualified domain name (FQDN). If you are not connecting from a local instance of ADAM, the loopback address 127.0.0.1 and localhost are not options. Ideally, use an FQDN to make your connection to an ADAM instance. When you install ADAM, the default LDAP port number on a non-domain controller is 389 and over an SSL port it is 636. On a domain controller, the defaults are 50000 and 50001 respectively. You must not use 389 and 636 on a domain controller as Active Directory requires these ports. The LDAP and SSL port numbers uniquely identify the instance of ADAM running on a computer. Because you can have multiple instances of ADAM running on a single machine, it's not necessary to use these defaults. In fact, you can use any unused port from 1025 to 65535 for your designated port numbers. The following example of getting context connects to a domain controller with an FQDN of sea-dc-02.fabrikam.com running an instance of ADAM on ports 50000 and 50001. In this case, the connection uses port 50000:

DirectoryContext adamContext = new DirectoryContext(
DirectoryContextType.DirectoryServer, 
"sea-dc-02.fabrikam.com:50000");

You might notice that no credentials are specified in the last two examples. In both cases, the directory context object uses the credentials of the currently logged on user. This is specifically a Windows user account with access to the specified ADAM instance. There is an overload of the DirectoryContext object that takes a user name and password as two additional parameters. If you use this overload, the credentials you provide must be those of a Windows user. You cannot use an ADAM user account to establish context to an ADAM instance using the DirectoryContext object.

Getting Active Directory Context

In most cases you will also want to get context using the DirectoryContext class before completing tasks using S.DS.AD to manage Active Directory. However, unlike ADAM, you have some additional DirectoryContextTypes at your disposal to simplify creation of a DirectoryContext object. In addition, there are methods that both establish context for you and bind to Active Directory objects without having to code with the DirectoryContext class at all.

Let's first take a look at establishing forest and domain context using the DirectoryContext class. This first example demonstrates how you would go about getting a forest context using the DirectoryContext class and the Forest option of the DirectoryContextType enumeration:

DirectoryContext forestContext =  new DirectoryContext(DirectoryContextType.Forest);

and for a domain:

DirectoryContext domainContext =  new 
DirectoryContext(DirectoryContextType.Domain);

Notice in both examples I didn't have to specify a server name or port value. In both cases, the code relies on the domain controller locator service (a.k.a. Locator) to find an appropriate DC for establishing context. You can specify a connection string and credentials, but in the two examples I've shown, you connect to the current forest or domain using the Locator to find an appropriate server in your site for your connection and you connect with your current credentials.

You can also bypass calling the DirectoryContext class from your code by calling methods of the Forest or Domain class that both get context and bind to a forest or domain. For example, when you call the GetCurrentDomain or GetCurrentForest methods of the Domain and Forest class respectively, the method creates a Domain or Forest context type object and then binds to the domain or forest for you. Here's how you can establish a domain context and bind to the current domain using the Domain class:

Domain domain = Domain.GetCurrentDomain();

Here's how you can establish a forest context and bind to the current forest using the Forest class:

Forest forest = Forest.GetCurrentForest();

Note   There are other classes that rely on the Locator to get context and bind to directory objects, for example, the GetDomainController method of the DomainController class.

If you don't use one of these helper methods that both get context and bind, you can achieve similar results in two steps. In this code snippet, I show how you can bind to a forest by passing the GetForest method to get a forest context:

DirectoryContext context = new DirectoryContext(DirectoryContextType.Forest);
Forest aForest = Forest.GetForest(context);

You can also combine this into a single line of code, as shown:

Forest aForest = Forest.GetForest(new DirectoryContext(DirectoryContextType.Forest));

I personally prefer getting context in one line of code and then binding to an object in a directory using a second line of code, but this shorthand is something you will inevitably see used elsewhere.

Now that you have a good sense of what getting context and binding means, it's time to look at some examples of what you can do after getting context and binding to directory objects. The next section explores S.DS.AD by showing you some examples of how you can use this namespace to work with ADAM and Active Directory. The examples contain the context for connecting to ADAM in the adamContext variable and in either the domainContext or forestContext variables for the Active Directory examples.

Retrieving ADAM and Active Directory Information

One of the simplest operations you will complete when managing directories is retrieving basic information about them. Both ADAM and Active Directory are treated as first-class citizens with S.DS.AD. To retrieve information from either directory, you start by binding to either an instance of ADAM or to some high-level object in Active Directory, such as a forest, domain, or domain controller.

ADAM Instance Data

Once you have determined the appropriate context for establishing a connection to an ADAM instance, S.DS.AD provides the AdamInstance class for managing the instance. You use the GetAdamInstance method of an AdamInstance object for binding to and working with an ADAM instance. The following example demonstrates how to inspect an ADAM instance and return information about it:

  1. After getting context to an ADAM instance, call the GetAdamInstance method and pass it the context.
  2. Call the Roles property of the ADAM instance to return an AdamRoleCollection object, and then iterate this collection to return information about the various roles that the ADAM instance serves.
  3. Call the Partitions property of the ADAM instance to return a ReadOnlyStringCollection object, and then iterate this collection to return information about the partitions hosted on the ADAM instance.

Example 1. Getting ADAMInstance information

AdamInstance adamInstance;

try
{
    adamInstance = AdamInstance.GetAdamInstance(adamContext);
}

//This catch block runs if the ADAM connection string is 
//invalid or the server can't be reached
catch (ActiveDirectoryObjectNotFoundException e)
{
    Console.WriteLine(e.Message);
    return;
}

Console.WriteLine("<--ADAM Instance Information-->\n");

Console.WriteLine("Configuration set {0}", adamInstance.ConfigurationSet);

//get the roles of this ADAM instance
AdamRoleCollection roles = adamInstance.Roles;

Console.WriteLine("\nADAM Roles\n");
foreach (AdamRole role in roles)
{
    Console.WriteLine("\t{0}", role.ToString());
}

Console.WriteLine("\nADAM Partitions\n");

//get the partitions of this ADAM instance
ReadOnlyStringCollection partitions = adamInstance.Partitions;

foreach (string partition in partitions)
{
    Console.WriteLine("\t{0}", partition.ToString());
}
Active Directory Forest Data

An ADAM instance is the parent of an ADAM repository, just as an Active Directory forest is the parent of an Active Directory instance. In Active Directory, you can easily establish context and bind to the forest by relying on the Locator. This is accomplished by calling the GetCurrentForest method of the Forest class. This method gets the forest context and binds to the forest using the credentials of the currently logged-on user. Alternatively, you can use the GetForest method to pass a customized DirectoryContext and bind to the specified forest. The following example demonstrates how to use the GetCurrentForest method to return information about the current forest:

  1. Get context and bind to the forest by calling the GetCurrentForest method.

  2. Call the Domains property of the forest object to return a DomainCollection object, and then iterate this collection to return information about the domains in the forest.

    As you'll see in later examples, the Domain class is rich with many other properties and methods.

  3. Call the GlobalCatalogs property of the forest object to return a GlobalCatalogCollection object, and then iterate this collection to return information about the global catalogs in the forest.

    Like the Domain object returned by the Domains property in the previous step, the GlobalCatalog object exposes a rich set of properties and methods for viewing information about and managing global catalog servers.

  4. Call the NamingRoleOwner property of the forest object to return the name of the server holding the Flexible Single Master Operations (FSMO) role in the forest.

  5. Call the ApplicationPartitions property of the forest object to return an ApplicationPartitionsCollection object, and then iterate this collection to return information about all of the partitions in the forest.

    Note   The ApplicationPartitions property of a forest returns an ApplicationPartitionsCollection object, which provides a rich set of capabilities for reporting on and managing partitions in a forest. If you remember, in the previous example of getting ADAM partitions (Example 1), the Partitions property returned a ReadOnlyStringCollection object.

Example 2. Getting Active Directory forest information

Console.WriteLine("<--FOREST INFO-->\n");

// bind to the current forest
Forest forest;

try
{
    forest = Forest.GetCurrentForest();
}
catch (ActiveDirectoryObjectNotFoundException e)
{
    // current context is not associated with domain/forest
    Console.WriteLine(e.Message);
    return;
}

Console.WriteLine("Current forest: {0}", forest.Name);
Console.WriteLine();

// all domains within the forest
Console.WriteLine("\nDomains in the forest:");
foreach (Domain domain in forest.Domains)
{
    Console.WriteLine("\t{0}", domain.Name);
}

// all global catalogs within the forest
Console.WriteLine("\nGlobal catalogs in the forest:");
foreach (GlobalCatalog gc in forest.GlobalCatalogs)
{
    Console.WriteLine("\t{0}", gc.Name);
}
Console.WriteLine();

// role owners in the forest
Console.WriteLine("\nRole owners:");
Console.WriteLine("\tNamingRole: {0}", forest.NamingRoleOwner);
Console.WriteLine("\tSchemaRole: {0}", forest.SchemaRoleOwner);
Console.WriteLine();

Console.WriteLine("\nApplication partitions in the forest:");
//use the Forest class to obtain partition information
ApplicationPartitionCollection appPartitions = 
    forest.ApplicationPartitions;

foreach (ApplicationPartition appPartition in appPartitions)
{
    Console.WriteLine("\t{0}", appPartition.Name);
}

Active Directory Global Catalog Data

Sometimes you might be after something more than just general forest information. For example, you might want to find all global catalog servers to see what other roles each server might be serving in the forest or you might want to find a single global catalog server to begin an optimized, forest-wide search operation.

Using the forest context and the GlobalCatalog class, you can get information about all global catalog servers in the forest or just one. The GlobalCatalog class contains the FindAll method for finding all global catalogs in a forest. This method returns a read-only GlobalCatalogCollection object that you can enumerate to find information about all global catalog servers. Alternatively, you can use the GetGlobalCatalog method to bind to an instance of a specific global catalog server or you can use the FindOne method to find any instance of a global catalog server in the forest and bind to it.

The following code demonstrates how to use the FindOne method with a forest context to bind to a global catalog server and return information about the server in the forest:

  1. Get a forest context by creating a DirectoryContext object and passing it a DirectoryContextType of forest.

    This operation connects to the current forest context.

  2. Call the FindOne method of a GlobalCatalog object and pass it the current forest context to bind to any available global catalog server.

  3. Return the name of the global catalog server and the value of the SiteName property to display the site to which this global catalog server is a member.

  4. Call the Roles property of the GlobalCatalog object to return an ActiveDirectoryRoleCollection object, and then iterate this object to return a list of roles that this global catalog server holds.

  5. Call the Partitions property of the GlobalCatalog object to return a ReadOnlyStringCollection of the partitions on the global catalog server.

Note   Like the ADAM partitions property, this partitions property returns a read-only string collection. If you need manage Active Directory partitions, use the Partitions property of the Forest object instead.

Example 3. Getting Active Directory global catalog information

Console.WriteLine("<--GLOBAL CATALOG CONFIGURATION DATA-->\n");

// Get a forest context
DirectoryContext forestContext =
                   new DirectoryContext(DirectoryContextType.Forest);

// Find one global catalog within the current forest
GlobalCatalog gc;

try
{
    gc = GlobalCatalog.FindOne(forestContext);
    Console.WriteLine("Finding one global catalog "+
                                        "in the current forest:");
    Console.WriteLine("Name: {0}", gc);
    Console.WriteLine("Site: {0}", gc.SiteName);

    // roles held by the GC
    Console.WriteLine("\nRoles:");
    foreach (ActiveDirectoryRole role in gc.Roles)
    {
        Console.WriteLine(role);
    }

    // partitions hosted by the GC
    Console.WriteLine("\nPartitions hosted by this global catalog:");
    foreach (string partition in gc.Partitions)
    {
        Console.WriteLine(partition);
    }
}
catch (ActiveDirectoryObjectNotFoundException e)
{
    // gc not found
    Console.WriteLine(e.Message);
}

Active Directory Domain Data

In Getting Active Directory Context, I mentioned ways in which S.DS.AD leverages the Locator to establish context. Then, in Active Directory Forest Data I demonstrated how to get forest context via the Locator and the Forest class. The following code example demonstrates how to use the Domain class and the GetCurrentDomain method to bind to the current domain and return data about it. Note that I've simplified the steps describing the code since I have provided some detail on previous code examples that retrieve information about high-level objects in Active Directory:

  1. Bind to the current domain using the Domain class and the GetCurrentDomain method.
  2. Return information about the domain, including the distinguished name of the domain, the parent domain (if any), any child domains, any domain controllers in the domain, and the PDC role owner.

Example 4. Getting domain information

Console.WriteLine("<--DOMAIN INFORMATION-->\n");

// get the current domain
Domain domain;

try
{
    domain = Domain.GetCurrentDomain();
}
catch (ActiveDirectoryObjectNotFoundException e)
{
    // current context is not associated with a domain/forest
    Console.WriteLine(e.Message);
    return;
}

Console.WriteLine("Current domain: {0}\n", domain.Name);


// get the dn of the current domain by using GetDirectoryEntry
string domainDn = domain.GetDirectoryEntry()
                        .Properties["distinguishedName"]
                        .Value.ToString();

Console.WriteLine("The distinguishedName of the current domain is: {0}",
                    domainDn);


// get its parent domain
Console.Write("\nParent domain: ");
Domain parentDomain = domain.Parent;
if (parentDomain == null)
{
    Console.WriteLine("The current domain is the root of the domain tree.");
}
else
{
    Console.WriteLine(domain.Parent);
}

// all child domains
Console.Write("\nChild domains:");
foreach (Domain childDomain in domain.Children)
{
    Console.WriteLine(childDomain.Name);
}

// all domain controllers within the domain
Console.WriteLine("\nDomain controllers in the domain:");
foreach (DomainController dc in domain.DomainControllers)
{
    Console.WriteLine(dc.Name);
}

// Find the PDC
Console.WriteLine("\nPDC: {0}", domain.PdcRoleOwner);

Notice that I call the GetDirectoryEntry method of the Domain object to return a DirectoryEntry object from S.DS. Here's the snippet of code where this happens:

// get the dn of the current domain by using GetDirectoryEntry
string domainDn = domain.GetDirectoryEntry()
                        .Properties["distinguishedName"]
                        .Value.ToString();

Once I've got the DirectoryEntry object, I can retrieve attributes of the underlying object including, in this case, the distinguished name of the domain. There is not a distinguished name property for the domain available directly from the Domain class. Calling the GetDirectoryEntry method demonstrates a way to retrieve additional information about an S.DS.AD object.

Domain Controller Data

S.DS.AD also comes equipped with a DomainController class. While the Forest, Domain, and GlobalCatalog classes return information about their respective objects in the directory, the DomainController class returns information about one or more domain controllers.

You can bind to a domain controller using the DomainController object by calling either the FindOne or GetDomainController methods. You can also call the FindAll method with a directory context to return a DomainControllerCollection object. All three methods contain an overload for specifying the Active Directory site in which you want to bind. If you don't specify a site, the Locator provides the closest site to the client running the code. Not specifying a site provides the most flexibility as the code leverages the directory services infrastructure to find the nearest site. However, if you need to perform a site-specific operation, you can pass any of these methods a site name.

The FindOne method contains overloads that allow you to also specify one or more of the values contained in the LocatorOptions enumeration. The following values available in LocatorOptions are designed to make your code either more resilient, more secure or both:

  • ForceRediscovery   This causes the cached domain controller to be ignored. This is useful when you want the running code to locate another domain controller if the previously cached domain controller is unavailable.
  • KdcRequired   This ensures that the returned domain controller is running the Kerberos Key Distribution Center service and thus can use the Kerberos security protocol to authenticate the client rather than using NTLM authentication.
  • TimerServerRequired   This ensures that the returned domain controller is running the Windows Time Service. This is important if you're code is performing an operation where the returned time is critical to the accuracy of the operation.
  • AvoidSelf   This ensures that the responding domain controller is not the local machine. Use this when you are running your code on a local domain controller but you want to test results being returned from another domain controller in the domain.

The following code demonstrates getting domain controller information. The code is broken into the following demonstrations using the FindOne method:

  1. Call the FindOne method with only the directory context specified.
  2. Call the FindOne method by specifying the directory context and a site name.
  3. Call the FindOne method and specify both a directory context and a locator option.

Example 5. Using the FindOne method to get domain controller configuration data

Console.WriteLine("<--DOMAIN CONTROLLER CONFIGURATION DATA-->\n");

DirectoryContext domainContext =
   new DirectoryContext(DirectoryContextType.Domain);


DomainController dc;

// Find one domain controller within the current domain
try
{

    dc = DomainController.FindOne(domainContext);
    Console.WriteLine("Domain controller in the current domain: {0}", dc);
    Console.WriteLine("Site: {0}", dc.SiteName);
    Console.WriteLine("Is global catalog: {0}", dc.IsGlobalCatalog());
    Console.WriteLine("Current Time: {0}", dc.CurrentTime.ToLocalTime());
    Console.WriteLine("IP Address: {0}\n", dc.IPAddress);
}
catch (ActiveDirectoryObjectNotFoundException e)
{
    // dc not found
    Console.WriteLine(e.Message);
}

// finding DC in a specific site
// site name used to find a DC in a specific site
// string siteName = "<replaceWithaSiteName>";
string siteName = "Default-First-Site-Name";

try
{
    dc = DomainController.FindOne(domainContext, siteName);
    Console.WriteLine("Domain controller in the current" +
                             " domain and site \"{0}\":", siteName);
    Console.WriteLine(dc);
    Console.WriteLine("Site: {0}", dc.SiteName);
    Console.WriteLine("Is global catalog: {0}\n", dc.IsGlobalCatalog());
}
catch (ActiveDirectoryObjectNotFoundException)
{
    // dc not found
    Console.WriteLine("Domain controller in site \"{0}\" not found.",
                                                          siteName);
}

// finding KDC
try
{
    dc = DomainController.FindOne(domainContext,
                                    LocatorOptions.KdcRequired |
                                    LocatorOptions.AvoidSelf);
    Console.WriteLine("KDC in the current domain:");
    Console.WriteLine(dc);
    Console.WriteLine("Site: {0}", dc.SiteName);
    Console.WriteLine("Is global catalog: {0}", dc.IsGlobalCatalog());
}
catch (ActiveDirectoryObjectNotFoundException)
{
    // kdc not found or no other dc answered
    Console.WriteLine(
        "KDC not found in current domain or no other dc responded.");
}

Notice in the third example using the FindOne method that two locator options are specified. Both a key distribution center server and another domain controller other than a local domain controller (assuming that the code is being run from a domain controller) must respond. If both conditions aren't met, the code will error with an ActiveDirectoryObjectNotFoundException.

Managing the Schema

As you can see, returning basic information about an instance of ADAM or a high-level Active Directory object is straightforward. Often, you'll want to dig deeper to retrieve data within those partitions. In particular getting schema information is critical to better understanding aspects of both repositories, such as the replication behavior of attributes.

Retrieving ADAM and Active Directory Schema Data

Using S.DS.AD, you can get overall ADAM schema information and information about specific schema classes and schema attribute objects. The three classes in S.DS.AD that make this possible are:

  • ActiveDirectorySchema   Use this class and the GetSchema method to enumerate ADAM or Active Directory schema data and retrieve information about each object in the schema container.
  • ActiveDirectorySchemaClass   Use this class and the FindByName method for inspecting an individual class schema object.
  • ActiveDirectorySchemaProperty   Use this class and the FindByName method for inspecting an individual attribute schema object.

Note   There isn't a specific set of .NET ADAM schema classes for accessing the ADAM schema from managed code. Instead, both ADAM and Active Directory rely on the same ActiveDirectorySchema, ActiveDirectorySchemaClass, and ActiveDirectorySchemaProperty classes to work with schema data.

ADAM Schema Data

The GetSchema method of the ActiveDirectorySchema object has a single constructor that takes a DirectoryContext for binding to a schema. Once bound to the ADAM schema, you use the FindAllClasses method to retrieve each ActiveDirectorySchemaClass object. If you call the FindAllClasses method using the default constructor, you can return information about all class schema objects, as the following code snippet demonstrates:

ActiveDirectorySchema schema =
ActiveDirectorySchema.GetSchema(adamContext);

foreach (ActiveDirectorySchemaClass schemaClass in
schema.FindAllClasses())
 {
    Console.WriteLine("Common name: {0}\n\tlDAPDisplayName: {1}",
                schemaClass.CommonName, schemaClass.Name);
 }

To limit the schema class objects returned by FindAllClasses, you can specify the schema class type you want the method to return using the other available constructor for the method. This constructor takes a value from the SchemaClassType enumeration, which provides access to four types: Abstract, Auxiliary, Structural, or Type88. Behind the scenes, these four types are identified by reading the value of the objectClassCategory attribute in each schema class object.

A quick tangent about Type88 classes: these classes behave like structural classes in that they can be instantiated into directory objects. Active Directory complies with the X.500 standard and, as such, must maintain this class category as defined in the X.500 (1993) specification. Any classes defined before the 1993 specification comply with the X.500 (1988) specification, which does not contain a categorization requirement. Thus, the 88 category includes all classes defined before there was a categorization requirement. So, long story short, don't use this type designation for class schema objects you are creating.

For more information about the three values you might use for the objectClassCategory attribute (all but Type88), see https://msdn.microsoft.com/library/default.asp?url=/library/en-us/ad/ad/structural_abstract_and_auxiliary_classes.asp.

Other attributes of the schema class objects are exposed as members of the ActiveDirectorySchema class. For example, you can return all defunct classes using the FindAllDefunctClasses method. This retrieves all classes (as AttributeSchemaClass objects) with the isDefunct attribute set to True. Similarly, you use the FindAllDefunctProperties method to return attributes (as AttributeSchemaProperty objects) with the isDefunct attribute set to True. Yes, schema attribute objects contain attributes. Often, they are referred to as properties as the method and return type names imply.

So why would you care to have a list of defunct schema objects? Because you cannot instantiate a defunct class or attribute, it's good to know which objects have the isDefunct attribute set to True. Since you cannot delete a schema class or attribute, setting the isDefunct attribute to True is the next best thing.

A powerful method for getting other information about attribute objects is FindAllProperties. Like the FindAllClasses method, this method contains two constructors. The default constructor returns all attribute schema objects while the other method returns a subset of all attributes, as defined by the PropertyTypes enumeration, which contains two possible values: Indexed and InGlobalCatalog. The Indexed value means that the first bit in the searchFlags attribute is enabled for a particular attribute and the InGlobalCatalog value means that the isMemberOfPartialAttributeSet attribute is set to True. For an ADAM instance, the InGlobalCatalog property is irrelevant as ADAM does not support the infrastructure necessary for a global catalog server. In the Active Directory sample later in this article, I'll demonstrate how to return both the Indexed and the InGlobalCatalog property values.

The following code example demonstrates how to return information about all class and attribute schema objects in the ADAM schema:

  1. Bind to an ADAM schema by calling the GetSchema method and passing it an ADAM context.
  2. Call the FindAllClasses method to return a ReadOnlyActiveDirectorySchemaClassCollection object, then iterate through this collection to return the common name (cn) and lDAPDisplayName of each class.
  3. Call the FindAllClasses method again but this time pass it the Abstract value of the SchemaClassType enumeration to limit the display to all schema class objects of type abstract.
  4. Call the FindAllDefunctClasses method to return a ReadOnlyActiveDirectorySchemaClassCollection object that contains a list of all classes with the isDefunct attribute set to True.
  5. Call the FindAllDefunctProperties method to return a ReadOnlyActiveDirectorySchemaPropertyCollection object that contains a list of all attributes with the isDefunct attribute enabled.
  6. Call the FindAllProperties method to also return a ReadOnlyActiveDirectorySchemaPropertyCollection and pass it the Indexed value of the PropertyTypes enumeration to limit the display of attributes to where the first bit in the searchFlags attribute is enabled.

Example 6. Getting ADAM Schema information

Console.WriteLine("<--SCHEMA Information-->\n");

// get the schema associated with the current ADAM instance
ActiveDirectorySchema schema;

try
{
    schema = ActiveDirectorySchema.GetSchema(adamContext);
}
catch (ActiveDirectoryObjectNotFoundException e)
{
    // current context could not be obtained
    Console.WriteLine(e.Message);
    return;
}

Console.WriteLine("Current schema: {0}", schema);

//get all class names
Console.WriteLine("\nAll schema classes:");
foreach (ActiveDirectorySchemaClass schemaClass in
                    schema.FindAllClasses())
{
    Console.WriteLine("Common name: {0}\n\tlDAPDisplayName: {1}",
        schemaClass.CommonName, schemaClass.Name);
}

// get all the abstract classes in the schema
Console.WriteLine("\nAll abstract schema classes:");
foreach (ActiveDirectorySchemaClass schemaClass in
                    schema.FindAllClasses(SchemaClassType.Abstract))
{
    Console.WriteLine(schemaClass);
}

// get all the defunct classes in the schema
// By default, an ADAM instance doesn't contain any defunct classes.
Console.WriteLine("\nAll defunct schema classes:");
foreach (ActiveDirectorySchemaClass schemaClass in
                       schema.FindAllDefunctClasses())
{
    Console.WriteLine(schemaClass);
}

Console.WriteLine("\nAll defunct schema attributes:");

foreach (ActiveDirectorySchemaProperty schemaProperty in
                    schema.FindAllDefunctProperties())
{
    Console.WriteLine(schemaProperty);
}

Console.WriteLine("\nIndexed attributes:");

foreach (ActiveDirectorySchemaProperty schemaProperty in
                    schema.FindAllProperties(
                                     PropertyTypes.Indexed))
{
    Console.WriteLine(schemaProperty);
}

ADAM Class Schema Data

The FindByName method of the ActiveDirectorySchemaClass object has a single constructor that takes two arguments. Just like the GetSchema method of an ActiveDirectorySchema object, the ActiveDirectorySchemaClass object's first parameter is the DirectoryContext for binding to a schema. The second argument is the ldapDisplayName of the class you want to inspect. If you're not sure what the lDAPDisplayName is for a particular class in the schema, you can use tools like ADAM ADSI Edit, ADAM Schema or the previous example (AdamData.GetSchemaData in the code download with this article) to obtain the lDAPDisplayNames of class schema objects you are interested in inspecting further. For example, given a context as provided by the adamContext variable, this code snippet gets some basic information about the Top class schema object:

ActiveDirectorySchemaClass schemaClass =
ActiveDirectorySchemaClass.FindByName(adamContext, "Top");

Console.WriteLine("Name: {0}\nOid: {1}\nDescription: {2}", 
schemaClass.Name, schemaClass.Oid, schemaClass.Description);

The previous code snippet provides basic information without digging into the attributes contained in this class schema object. However, the ActiveDirectorySchemaClass object contains lots of useful information about each attribute. Two properties, MandatoryProperties and OptionalProperties of the ActiveDirectorySchemaClass object return ActiveDirectorySchemaPropertyCollection objects. The MandatoryProperties property contains a list of attributes stored in the systemMustContain and mayContain attributes while the OptionalProperties property contains a list of attributes stored in the systemMightContain and mightContain multi-valued attributes of a class. The following code snippet demonstrates how you can retrieve this information:

Console.WriteLine("MandatoryProperties:");
foreach (ActiveDirectorySchemaProperty schemaProperty in
                                                schemaClass.MandatoryProperties)
{
Console.WriteLine(schemaProperty);
}

Console.WriteLine("OptionalProperties:");
foreach (ActiveDirectorySchemaProperty schemaProperty in
                                    schemaClass.OptionalProperties)
{
    Console.WriteLine(schemaProperty);
}

Other aspects of the class, such as its relationship to other classes (for example, the parent class, possible superiors, and any auxiliary classes) are exposed by the ActiveDirectorySchemaClass object.

The following code example demonstrates how to return information about a class schema object in the ADAM schema:

  1. Get an ADAM context then call the FindByName method and pass it the context and lDAPDisplayName of the class to bind to the class.
  2. Return basic information about the class schema object — name, OID, description, and schema GUID.
  3. Call the MandatoryProperties property of the schema class object to return an ActiveDirectorySchemaClassCollection object, and then iterate this collection to return a list of mandatory attributes.
  4. Perform the same procedure described in step 3, but call the OptionalProperties property and then the PossibleSuperiors property to return optional attributes and possible superiors of the schema class object.
  5. Call the SubClassOf property to read the parent class of the current class schema object.

Example 7. Getting ADAM Schema class information

Console.WriteLine("<--ADAM SCHEMA CLASS-->\n");

string className = "user";

// Find a schema class object in the current ADAM instance 
// and display its properties
ActiveDirectorySchemaClass schemaClass;

try
{
    schemaClass = ActiveDirectorySchemaClass.FindByName(adamContext,
                                                        className);
}
catch (ArgumentException e)
{
    // this exception could be thrown if the current context 
    // is not associated to an instance of ADAM
    Console.WriteLine(e.Message);
    return;
}
catch (ActiveDirectoryObjectNotFoundException)
{
    Console.WriteLine("Schema class \"{0}\" could not be found", className);
    return;
}

Console.WriteLine("Name: {0}", schemaClass.Name);
Console.WriteLine("Oid: {0}", schemaClass.Oid);
Console.WriteLine("Description: {0}", schemaClass.Description);
Console.WriteLine("SchemaGuid: {0}", schemaClass.SchemaGuid);
Console.WriteLine("\nMandatoryProperties:");

foreach (ActiveDirectorySchemaProperty schemaProperty in
                                    schemaClass.MandatoryProperties)
{
    Console.WriteLine(schemaProperty);
}

Console.WriteLine("\nOptionalProperties:");
foreach (ActiveDirectorySchemaProperty schemaProperty in
                                    schemaClass.OptionalProperties)
{
    Console.WriteLine(schemaProperty);
}

Console.WriteLine("\nPossible Superiors:");
foreach (ActiveDirectorySchemaClass supClass in
                                      schemaClass.PossibleSuperiors)
{
    Console.WriteLine(supClass);
}
Console.WriteLine("\nSubClassOf: {0}", schemaClass.SubClassOf);

ADAM Schema Attribute Data

If what you're really after is information about a specific attribute in the schema, you use a similar pattern as I demonstrated with the FindByName method of an ActiveDirectorySchemaClass object. However, this time, you use the FindByName method of the ActiveDirectorySchemaProperty object and pass it the appropriate DirectoryContext and the name of an attribute to inspect. For example, given a context as provided by the adamContext variable, this code snippet gets some basic information about the description attribute:

ActiveDirectorySchemaProperty schemaProperty = 
ActiveDirectorySchemaProperty.FindByName(adamContext, "description");
 
Console.WriteLine("Name: {0}\nOid: {1}\nDescription: {2}", 
schemaProperty.Name, schemaProperty.Oid, schemaProperty.Description);

The code demonstrates how to retrieve the following information about an attribute schema object:

  1. Return basic information — name, OID, description, schema GUID, and whether an attribute is indexed and is single or multi-valued.

  2. Return the lower and upper range of an attribute, if any.

    Note the use of the ActiveDirectoryObjectNotFoundException exception. I've used it here to emphasize that if an attribute isn't mandatory, you should be sure to catch this exception in your code.

Example 8. Getting ADAM Schema attribute information

Console.WriteLine("<--ADAM SCHEMA ATTRIBUTE-->\n");

// Find the schema property in the current forest 
// and display it's properties
ActiveDirectorySchemaProperty schemaProperty;

string propertyName = "description";
try
{
    schemaProperty = ActiveDirectorySchemaProperty.FindByName(
                                                      adamContext,
                                                      propertyName);
}
catch (ArgumentException e)
{
    Console.WriteLine(e.Message);
    return;
}
catch (ActiveDirectoryObjectNotFoundException)
{
    Console.WriteLine("Schema property \"{0}\" could not be found",
                        propertyName);
    return;
}

Console.WriteLine("Name: {0}", schemaProperty.Name);
Console.WriteLine("Oid: {0}", schemaProperty.Oid);
Console.WriteLine("Description: {0}", schemaProperty.Description);
Console.WriteLine("Schema GUID: {0}", schemaProperty.SchemaGuid);
Console.WriteLine("IsIndexed: {0}", schemaProperty.IsIndexed);
Console.WriteLine("IsSingleValued: {0}",
                                  schemaProperty.IsSingleValued);

Console.WriteLine("\nUpper and lower value/length:");
try
{
    Console.WriteLine("RangeLower: {0}", schemaProperty.RangeLower);
    Console.WriteLine("RangeUpper: {0}", schemaProperty.RangeUpper);
}
catch (ArgumentNullException)
{
    // if these properties are not defined for the schema property, 
    // ArgumentNullException is thrown
    Console.WriteLine("RangeLower/RangeUpper not defined.");
}

Retrieving Data about the Active Directory Schema

As you might expect, accessing the Active Directory schema or objects within the schema is almost identical to accessing ADAM schema data. The only difference is that you have the option of directly binding to the current Active Directory schema to access the data within it. The binding operation to get context and bind to the current schema is:

ActiveDirectorySchema schema = ActiveDirectorySchema.GetCurrentSchema();

To get information about a specific class or attribute, you use the FindByName methods that you saw in Examples 7 and 8. The only difference is that you establish context to the forest rather than to an ADAM instance. As a reminder, here's how you establish a forest context:

DirectoryContext context = new DirectoryContext(DirectoryContextType.Forest);  

The code download included with this article contains methods in the SchemaData class for returning Active Directory schema data.

Extending the Schema

Up to now all examples demonstrated how to read data from ADAM and Active Directory. This is a good start, but seeing how to make changes to data is a critical aspect of managing directories. While we are on the subject of schema data, let's start by exploring how to extend the schema. The upcoming examples are for extending ADAM, but they also work for Active Directory. However, unless you are building a commercial product, building some internal enterprise application that requires Active Directory schema extensions or even working with a test instance of Active Directory, you are better off extending a schema in a test ADAM instance. This is because when you extend a schema, you are not able to delete the schema object (class or attribute) that you create. As I mentioned earlier, you can set the isDefunct attribute on the object to True to avoid class or attribute instantiation, but you can't delete the object. Obviously, you could restore an earlier instance of Active Directory, but this is a lot of effort that you can easily avoid by targeting an ADAM test instance instead.

Creating an Attribute

Creating a new attribute schema object programmatically involves, at minimum, specifying key identifying values for a new attribute, connecting to a schema, instantiating an ActiveDirectorySchemaProperty object, assigning the key identifying attribute values, and saving the attribute schema object to the schema repository. You can also assign additional attributes to the object, as you'll see in the upcoming code example.

The key identifying values for the following attributes are necessary in order to create attribute schema objects:

  • common name (cn)
  • ldap display name (lDAPDisplayName)
  • object identifier (OID)
  • syntax

Note   This is by no means a complete list of identifying attributes. For a more complete list of key attributes of an attribute schema object, see Characteristics of Attributes.

Before I get to creating an attribute schema object from code, it's important to understand a little about each of the key identifying values for an attribute schema object. The common name is a mandatory attribute of any schema object. The cn moniker with a name (for example, cn=New-Attribute) is referred to as the relative distinguished name (RDN) of an object. There are other monikers that are used in building an object hierarchy in a directory but for the purpose of creating schema objects, you will always use the cn moniker. The lDAPDisplayName is the name that you use most when identifying an object within a directory programming tasks, such as adding an attribute to a class or adding a user account to a group. This is typically similar in name to the RDN. The naming convention used in the Active Directory schema for most attribute schema objects Microsoft created RDNs using Pascal case with a dash separating parts of the name and it shows the lDAPDisplayName using camel case without any dashes (for example, newAttribute). This is simply a naming convention. There is no hard and fast rule saying you have to follow it. For consistency, your organization should come up with a naming standard for new attribute and class schema objects and stick with it.

So what's the story with the OID? This unique value is assigned to each schema object (attribute or class). Active Directory and ADAM uses this ID to uniquely identify any directory object created from the attribute or class. For more information about OID notation, see http://windowssdk.msdn.microsoft.com/en-us/library/ms677614.aspx. Depending on your needs and where you are located, you can currently register for a unique OID range with Microsoft (http://windowssdk.msdn.microsoft.com/en-us/library/ms677620.aspx), ANSI (www.ansi.org), IANA (www.iana.org), or BSI (www.bsi.org). If your goal is simply for testing, a more expedient approach is to use the oidgen tool that was part of the Windows 2000 Server Resource Kit to generate unique IDs within the Microsoft address space. Please be cautious with this tool. It is only for testing and the OID values generated should never be used for a production system. Possibly because some people misused this tool, Microsoft did not make it a part of the Windows Server 2003 Resource Kit. However, it runs perfectly fine on Windows Server 2003. You simply run oidgen from the command line to generate unique attribute and class base OIDs. Here's an example of the output:

Attribute Base OID: 1.2.840.113556.1.4.7000.233.28688.28684.8.145234.1728134.2034934.1932637
Class Base OID: 1.2.840.113556.1.5.7000.111.28688.28684.8.240397.1734810.1181742.544876

Once you have these values, you can create attribute and class schema objects by appending to these base values using dot notation and a number. For example, to the end of the Attribute Base OID you can start with <attribute base OID>.1 and make the next attribute OID <attribute base OID>.2 and so on.

If you're a TechNet subscriber, you can find the resource kit files on the Resourced Kits DVD. The oidgen.exe utility is contained in the netmgmt.cab file. Open this compressed file and copy the utility to a local disk and run the tool.

Except for syntax, you define each (common name, OID, lDAPDisplayName) value as a string. For the syntax value, S.DS.AD provides the ActiveDirectorySyntax enumeration. Using this enumeration, you can define any of the 23 available syntax names. To view a list of syntax names, see the ActiveDirectorySyntax enumeration in the .NET Framework class library. The following example shows how you would define a newPropertySyntax variable as a Boolean syntax type:

ActiveDirectorySyntax newPropertySyntax = ActiveDirectorySyntax.Bool; 

Now that you have an understanding of the cn, lDAPDisplayName, OID, and syntax values, let's get to the code example. The following code example demonstrates how to extend an ADAM schema instance with a new attribute schema object:

  1. Specify a common name, LDAP display name, OID, and syntax for the new attribute schema object.
  2. Create an ActiveDirectorySchemaProperty object using a pre-defined ADAM directory context and the defined LDAP display name.
  3. Set the CommonName, OID, IsSingleValued OID, and Syntax properties of the ActiveDirectorySchemaProperty object.
  4. Call the Save method of the ActiveDirectorySchemaProperty object to create the attribute schema object in the ADAM schema.

Example 9. Adding a new attribute schema object to an ADAM Schema

// specify a common name
string newAttributeCommonName = "New-Attribute";

// specify an lDAPDisplaName
string newAttributeLdapDisplayName = "newAttribute";

// specify an OID value. The root of this value was generated by oidgen.exe
string newAttributeOid =       "1.2.840.113556.1.4.7000.233.28688.28684.8.145234.1728134.2034934.1932637.1";

// specify a syntax
ActiveDirectorySyntax syntax = ActiveDirectorySyntax.CaseIgnoreString;
            
// create a schema property object
ActiveDirectorySchemaProperty newAttribute = 
                                    new ActiveDirectorySchemaProperty(
                                            adamContext, 
                                            newAttributeLdapDisplayName);

// set attributes for this schema attribute object
newAttribute.CommonName = newAttributeCommonName;
newAttribute.Oid = newAttributeOid;
newAttribute.IsSingleValued = true;
newAttribute.Syntax = syntax;

// save the new attribute schema object to the schema
try
{
    newAttribute.Save();
}
catch (ActiveDirectoryObjectExistsException e)
{
    // an object by this name already exists in the schema
    Console.WriteLine("The schema object \"{0}\" was not created. {1}", 
        newAttributeLdapDisplayName, e.Message);
    return;
}

catch (ActiveDirectoryOperationException e)
{
    // a call to the underlying directory was rejected
    Console.WriteLine("The schema object \"{0}\" was not created. {0}",
                newAttributeLdapDisplayName, e.Message);
    return;

}

Console.WriteLine("Attribute schema object \"{0}\" created successfully.", 
                                        newAttributeLdapDisplayName);

In the previous code example, the object's GUID (schemaIDGUID attribute) is set automatically. As Joe Kaplan and Ryan Dunn suggest in The .NET Developer's Guide to Directory Services Programming, if you are working toward programming a set of production level schema extensions, it's a best practice to explicitly set the SchemaGuid property value of the ActiveDirectorySchemaProperty object rather than letting Active Directory or ADAM automatically set the schemaIDGUID attribute value in the directory. In addition, if you don't specify Boolean values for properties of the ActiveDirectorySchemaProperty object, the default target attribute is set to False. For example, if the previous code snippet did not show explicitly setting the IsSingleValued property to True, the default isSingleValued attribute would be set to False. You should try to be explicit about configurable attribute settings in your code.

Tip   If you create a schema object and it doesn't appear in the ADAM Schema snap-in, right-click the ADAM Schema node for the ADAM instance, and then click Reload Schema. To see the objects in the ADAM ADSI Edit MMC snap-in, refresh the schema DN below the Schema sub-node of the ADAM ADSI Edit MMC snap-in.

Creating a class

The beauty of S.DS.AD and directory services programming in general is the consistency inherent in related operations. I'm sure you've noticed this consistency as I've moved through the various examples. In this case, creating a schema class object is similar to creating an attribute schema object. The exceptions really have to do with how you use a class schema object versus how you use an attribute schema object. A class schema object is a container for attribute schema objects and it potentially has relationships with other class schema objects and exists somewhere within the object hierarchy. You can find additional information about the characteristics of a class schema object by visiting Characteristics of Object Classes.

Just like creating an attribute schema object, to create a class schema object you must define some attribute values for the object, create a .NET object that provides an in-memory representation of the class schema object (in this case an ActiveDirectorySchemaClass object), set various properties of the object, and then save the class to a target schema.

The key difference between creating an attribute schema object and a class schema object is what properties you set on the .NET object to create a useable schema object. For a class schema object, you should set, at minimum, the following properties of an ActiveDirectorySchemaClass object:

  • lDAPDisplayName   Like an attribute schema object, this is the name you will typically use to read or write to the class schema object.

    Unlike the other items in this bullet list, you specify this value when you call the ActiveDirectorySchemaClass constructor.

  • CommonName   Also like an attribute schema object, this is the relative distinguished name of the object as it appears in the schema.

  • OID   This value is stored in the governsID attribute of the class schema object and operates similarly to the attributeID value of an attribute schema object.

  • SubClassOf   This defines the parent class of the class you are creating.

    There are some fine details about what class can be a parent of a class you are creating. See Characteristics of Object Classes!href(https://msdn2.microsoft.com/en-us/library/ms675579.aspx) for some details on this.

  • Type   This is the type of class you are creating.

    The SchemaClassType enumeration contains the four types of classes you can define: Abstract, Auxiliary, Structural, or Type88. If you aren't familiar with class schema types, refer to the previous reference I provided.

  • PossibleSuperiors   This populates the possSuperiors attribute that defines the structural classes to which this class can be a child.

    For example, you can add an organizationalUnit structural class to the possSuperiors attribute of a new class schema object you are creating. You can then create an object based on the new class in an existing OU.

  • MandatoryProperties and OptionalProperties   These two properties can be populated with attributes that the class schema object must or may contain respectively.

    The MandatoryProperties property populates the mustContain attribute and the OptionalProperties property populates the mayContain attribute.

The following code demonstrates how to extend the schema with a new class schema object:

  1. Specify a common name, LDAP display name, OID, parent class, possible superior, and optional attribute for the new class schema object.

  2. Create an ActiveDirectorySchemaClass object using an ADAM directory context and the defined LDAP display name.

  3. Set the CommonName, OID, and Type properties of the ActiveDirectorySchemaProperty object.

  4. Add an ActiveDirectorySchemaProperty object to the OptionalProperties property of the class schema object.

    The OptionalProperties property is an ActiveDirectorySchemaPropertyCollection object to which you add one or more attribute schema objects represented by ActiveDirectorySchemaProperty object. Use the FindByName method of the ActiveDirectorySchemaProperty class to locate an attribute schema class object to add. The FindByName method takes a directory context and the lDAPDisplayName value of an attribute you want to add. Follow the same pattern for adding attributes to the MandatoryProperties property.

  5. Add an ActiveDirectorySchemaClass object to the SubClassOf and PossibleSuperiors properties of the class schema object.

    The SubClassOf and PossibleSuperiors properties are ActiveDirectorySchemaClass objects to which you add one more class schema objects represented by the ActiveDirectorySchemaClass object. Like the ActiveDirectorySchemaProperty, you use the FindByName method of the ActiveDirectorySchemaClass to locate class schema objects to add.

  6. Call the Save method to create the class schema object.

Example 10. Adding a new class schema object to an ADAM Schema

// specify a common name
string newClassCommonName = "new-Class";

// specify an lDAPDisplayName 
string newClassLdapDisplayName = "newClass";

// specify an OID value. The root name was generated by oidgen.exe
string newClassOid =    "1.2.840.113556.1.5.7000.111.28688.28684.8.240397.1734810.1181742.544876.1";

string subClassOf = "top";

string possibleSuperior = "organizationalUnit";

// add an optional attribute to the new schema class object 
// This example adds the new attribute created in the CreateNewAttribute method
string newClassOptionalAttribute = "attribute1";


// create a new class object
ActiveDirectorySchemaClass newClass =
                                new ActiveDirectorySchemaClass(
                                    adamContext,
                                    newClassLdapDisplayName);

// set the attribute values for this schema class object
newClass.CommonName = newClassCommonName;
newClass.Oid = newClassOid;

newClass.Type = SchemaClassType.Structural;

// assign the parent class
newClass.SubClassOf = ActiveDirectorySchemaClass.FindByName(adamContext,
                                                subClassOf);

// add the previously created attribute as an optional attribute
newClass.OptionalProperties.Add(
    ActiveDirectorySchemaProperty.FindByName(adamContext,
                                  newClassOptionalAttribute));

//add an OU as a possible superior so that this class can be 
//instantiated in an OU
newClass.PossibleSuperiors.Add(
    ActiveDirectorySchemaClass.FindByName(adamContext,
                                possibleSuperior));

// save the new class to the schema
try
{
    newClass.Save();
}

catch (ActiveDirectoryObjectExistsException e)
{
    // an schema object by this name already exists in the schema
    Console.WriteLine("The schema object \"{0}\" was not created. {0}",
                                newClassLdapDisplayName, e.Message);
    return;
}

catch (ActiveDirectoryOperationException e)
{
    // a call to the underlying directory was rejected
    Console.WriteLine("The schema object \"{0}\" was not created. {0}",
                newClassLdapDisplayName, e.Message);
    return;

}


Console.WriteLine("Class \"{0}\" created successfully.",
                                      newClassLdapDisplayName);

Managing ADAM and Active Directory Topology

A common task in an enterprise involves managing the directory services topology. S.DS.AD includes classes for managing both the topology of Active Directory and ADAM. In this section I demonstrate various ways to retrieve topology data, such as a list of site subnets and preferred bridge head servers. Later, I explore how you use S.DS.AD to create sites (ADAM and Active Directory), subnets, and site links and also how to add a site to a site link. Finally, to complete the lifecycle, I demonstrate how to delete ADAM and Active Directory sites and site links.

Retrieving AD Topology Data

A good place to begin is with retrieval tasks. To get the overall topology of an Active Directory implementation, you start at the forest level by getting a forest context. While you can certainly begin by getting context to the current forest by simply specifying the Forest value for the DirectoryContextType enumeration, as shown:

DirectoryContext context = new DirectoryContext(DirectoryContextType.Forest);

It's a little more interesting to specify a DirectoryContext overload that requires a value for the Name parameter. This allows you to target a forest other than your current forest context. What that Name parameter means varies based on the context. For example, when establishing a directory context to a forest, the Name parameter represents a target forest name. To establish a forest context to the adatum.com forest using your current credentials, you can use this code:

DirectoryContext context = new DirectoryContext(DirectoryContextType.Forest, "adatum.com");

Note   Another overload for the DirectoryContext constructor allows you to specify alternative credentials as well as the forest name.

The meaning of the Name parameter (or target format as it's called in the .NET class library) varies based on the DirectoryContextType for the context you are trying to establish. As you've seen, the target format for a forest directory context type is a forest name. Conversely, if your directory context type is to a directory server, the target format represents the host name of a server and a port value if you are connecting to an ADAM instance. See DirectoryContext Class in the .NET Framework Class Library for more information.

Most of the common settings that make up an ADAM or Active Directory topology are contained in the Sites container below the forest's configuration container. For example, the fabrikam.com forest topology settings are located at the following DN: cn=sites,cn=configuration,dc=fabrikam,dc=com. Microsoft provides the Active Directory Sites and Services MMC for a getting a graphical view of this data and the various settings contained in the sites container.

S.DS.AD provides a number of key classes for viewing this information, namely:

  • ActiveDirectoryInterSiteTransport for managing an inter-site transport object, either an Rpc (IP) or SMTP transport type. Once you have context, this is the object you bind to, to retrieve basic topology data.
  • ActiveDirectorySiteLink for managing site links within a transport type.
  • ActiveDirectorySiteLinkBridge for managing site link bridges within a transport type.
  • ActiveDirectorySite for managing sites within a forest of sites. This includes managing the domains, servers and subnets within a site. Once you have an ActiveDirectorySite object from the collection of sites in the forest, you can access Domain, DirectoryServer, and ActiveDirectorySubnet classes for managing the site.
  • ActiveDirectorySubnet for managing subnets within a site.

There are additional classes available for performing site tasks such as replication management and viewing information about bridgehead servers in the site. I'll explore replication in more detail later in this article.

Using the .NET classes just described, the following code example demonstrates how to retrieve Active Directory topology data:

  1. Get a forest context and specify a target forest name for the second parameter of the DirectoryContext object.

    Just as a reminder, you don't have to specify a forest name if your goal is to get context to the current domain.

  2. Bind to an inter-site transport object using the FindByTransportType method to view details of site links within a particular transport type.

    The ActiveDirectoryTransportType enumeration contains one of two values, either Rpc or Smtp. These two enumerations represent the two types of replication transports available in Active Directory.

  3. Display information about whether or not the transport object is configured to ignore replication schedules and to bridge all site links.

    These two properties, BridgeAllSiteLinks and IgnoreReplicationSchedule are stored in the options attribute of each interSiteTranport object. You can view these settings by accessing the properties of an Inter-Site Transports container (IP or SMTP) in the Active Directory Sites and Services MMC.

  4. Display the links and site link bridges in the selected transport type.

    The ActiveDirectoryInterSiteTransport object contains two read only collections, one containing site links and the other containing site link bridges. Each link is represented by an ActiveDirectorySiteLink object and each site link bridge is represented by an ActiveDirectorySiteLinkBridge object.

  5. Bind to the forest and iterate through the Forest object's Sites collection.

    Active Directory sites contain a wealth of information about a site's topology. Therefore, it's not surprising that each site in the Sites collection is represented by an ActiveDirectorySite object that contains a large set of properties. The code demonstrates how to display information about the domains, directory servers, subnets, bridgehead servers, preferred bridgehead servers and adjacent sites within each site. In addition, it displays information about the server acting as the inter-site topology generator for a site. This server role (one per site) is responsible for managing the inbound replication connection objects for all bridgehead servers in a site.

Example 11. Retrieving Active Directory topology information

string targetForestName = "adatum.com";

// get context
DirectoryContext context = new DirectoryContext(
                                    DirectoryContextType.Forest,
                                    targetForestName);

// Bind to an inter-site transport object using the FindByTransportType
// method to view details of site links within a particular transport 
// type, either RPC over IP or SMTP. 
ActiveDirectoryInterSiteTransport transport =
        ActiveDirectoryInterSiteTransport.FindByTransportType(
        context,
        ActiveDirectoryTransportType.Rpc);

Console.WriteLine("\tBridge all site links {0}",
                                transport.BridgeAllSiteLinks);

Console.WriteLine("\tIgnore replication schedule" +
  "for this inter-site transport? {0}",
                            transport.IgnoreReplicationSchedule);

// get all the site links within a particular transport type
Console.WriteLine("\nSite links:\n");
foreach (ActiveDirectorySiteLink link in transport.SiteLinks)
{
    Console.WriteLine("\tSitelink \"{0}\"", link);
}

// get all the site link bridges within a particular transport type
Console.WriteLine("\nSite link bridges:\n");
foreach (ActiveDirectorySiteLinkBridge bridge in
                                       transport.SiteLinkBridges)
{
    Console.WriteLine("\tSitelinkBridge \"{0}\"", bridge);
}

// bind to a forest object using the GetForest method
Forest forest = Forest.GetForest(context);

// get all the sites in the forest
foreach (ActiveDirectorySite site in forest.Sites)
{
    Console.WriteLine("\nSite \"{0}\"", site.Name);
    Console.WriteLine("\tcontains the following domain(s):");
    foreach (Domain domain in site.Domains)
    {
        Console.WriteLine("\t\t{0}", domain.Name);
    }
    Console.WriteLine("\tcontains the following server(s):");
    foreach (DirectoryServer server in site.Servers)
    {
        Console.WriteLine("\t\t{0}", server.Name);
    }
    Console.WriteLine("\tcontains the following subnet(s):");
    foreach (ActiveDirectorySubnet subnet in site.Subnets)
    {
        Console.WriteLine("\t\tSubnet: {0} location: {1}",
                                        subnet.Name,
                                        subnet.Location);
    }
    Console.WriteLine("\nInterSiteTopologyGenerator is {0}",
                                        site.InterSiteTopologyGenerator);

    Console.WriteLine("\nBridgehead servers:");
    foreach (DirectoryServer server in site.BridgeheadServers)
    {
        Console.WriteLine("\t\t{0}", server.Name);
    }

    Console.WriteLine("\nPreferred Bridgehead servers:");
    foreach (DirectoryServer server in site.PreferredRpcBridgeheadServers)
    {
        Console.WriteLine("\t\t{0}", server.Name);
    }

    Console.WriteLine("\nAdjacent sites are:");
    foreach (ActiveDirectorySite adjSite in site.AdjacentSites)
    {
        Console.WriteLine("\t\t{0}", adjSite.Name);
    }
}

Note   If you run the accompanying code to read an Active Directory topology of a large forest, iterating the site details can take some time. In addition, you might want to pipe the output to a file rather than displaying it in a console window.

Managing Directory Topology

Retrieving information about the current ActiveDirectory topology is important, but it's only part of the directory topology management landscape. S.DS.AD enables you to configure the topology as well. You can use the same classes just explored to create and delete sites in Active Directory and ADAM, create and delete Active Directory subnets and site links, and add and remove sites from site links. In one instance, I'll introduce a new class, the ActiveDirectorySchedule class to simplify the configuration of a replication schedule.

To keep these topology management examples as simple as possible, I do not check the directory after an object is created. You can either use graphical tools, directory services command line tools, or the code sample included with this article and the ADTopology.GetData method, which is similar to the code snippet appearing in Example 11.

Creating an Active Directory Site

You can create an Active Directory site from a forest context or from a domain and domain controller context. As sites are contained within the forest, it's a bit more intuitive to create them from a forest context and also requires less coding. The following code snippets show how to define a site object named site1 from a domain and domain controller context and then from a forest context:

string siteName = "site1";

Get the current domain context
DirectoryContext domainContext = new DirectoryContext(
                                     DirectoryContextType.Domain);

Bind to a domain controller from the domain context
DomainController dc = DomainController.FindOne(domainContext);

Get a directory server (domain controller) context from the found (and bound) DC
DirectoryContext dcContext = new DirectoryContext(
                                       DirectoryContextType.DirectoryServer, 
                                       dc.Name);

Get a site from the domain controller context
ActiveDirectorySite site = new ActiveDirectorySite(dcContext,
                                                 "site1");

Conversely, here's how you define a new site to create by using the current forest context:

DirectoryContext forestContext = new DirectoryContext(
                        DirectoryContextType.Forest);
   
// create new site
ActiveDirectorySite site = new ActiveDirectorySite(forestContext,
                                                   "site1");

Admittedly, you can skip getting a domain context and simply bind to a domain controller by using the GetDomainController method of the DomainController object. However, this makes this portion of your code less dynamic as you will have to be sure to specify an available domain controller to complete this operation. Yet another option is to call the GetCurrentDomainController method to get context and bind to the current domain controller. Even so, I think it's still more intuitive to define sites to create in the context of the current forest.

As you will see in the upcoming code example, there are some settings of a site object that require you to get a domain controller context even when you start with a forest context. However, defining the site to create using the forest context does not require a domain controller context, as the previous code snippet demonstrates.

After getting a forest context, you specify an ActiveDirectorySite object that points to the appropriate forest context and contains the new site name you want to create. Next, you can set various properties of the ActiveDirectorySite object, such as the Options property and the InterSiteTopologyGenerator property. Finally, you save the new site to the directory using the Save method of the ActiveDirectorySite object.

The following example shows how to create a new site in the forest, set site properties, and save the site to the directory.

  1. Get a forest context.

  2. Create an ActiveDirectorySite object using the forest context and the site name you want to assign.

  3. Using the site Options property and the ActiveDirectorySiteOptions enumeration, enable universal group membership caching for the site using the cache of the default site.

    The ActiveDirectorySiteOptions enumeration contains many values to enable and disable various topology related behaviors of an Active Directory site, for example, controlling inter-site topology generation, and allowing the knowledge consistency checker (KCC) to randomly choose a bridgehead server when creating a connection for replication. For more information on these settings, see the ActiveDirectorySiteOptions Enumeration in the .NET Framework Class Library.

  4. Set the Inter-site topology generator for the site.

    The inter-site topology generator is a domain controller in the forest. To configure this setting, you need to get a domain controller context and then call the GetDomainController method of a DomainController object and pass the method the domain controller context. You then assign the DomainController object to the InterSiteToplogyGenerator property of the site. As a result, the domain controller and the site that contains the server is assigned to the Inter-Site Topology Generator settings of the ActiveDirectorySite object. While this operation requires that you supply the name of a specific domain controller, this is important because you don't want to assign just any domain controller to this role. The Intersite Topology Generator (ISTG) enables replication across site links, by selecting one or more bridgehead servers to perform site-to-site replication.

  5. Once you have configured the ActiveDirectorySite object, you call the Save method of the object to save the site to the directory.

Example 12. Creating an Active Directory site

string siteName = "New-Site";
string topologyGeneratorDc = "sea-dc-02.fabrikam.com";

// using forest context to create a site
DirectoryContext forestContext = new DirectoryContext(
                            DirectoryContextType.Forest);

// create new site
ActiveDirectorySite site = new ActiveDirectorySite(forestContext,
                                                   siteName);
// set site options
site.Options = ActiveDirectorySiteOptions.GroupMembershipCachingEnabled;

// set other settings, such as the inter-site topology generator
// to do this, get context to an existing domain controller
DirectoryContext dcContext = new DirectoryContext(
                                 DirectoryContextType.DirectoryServer,
                                 topologyGeneratorDc);

// get a domain controller object
DomainController dc = DomainController.GetDomainController(dcContext);

// assign that object to the InterSiteTopologyGenerator property of the site.
// this automatically adds the site name of the site containing the domain controller
site.InterSiteTopologyGenerator = dc;

//commit the site to the directory
site.Save();

Console.WriteLine("\nSite \"{0}\" was created successfully", site);

Tip   If you are checking for the creation of a new site using the Active Directory Sites and Services MMC and the console running the snap-in is already open, be sure to click Refresh on the Sites container context menu.

Note that the code example accompanying this article does not include assigning an inter-site topology generator. However, Example 12 demonstrates how you can assign this value.

Creating an ADAM site

Creating an ADAM site is similar to creating an Active Directory site. However, ADAM doesn't provide the level of robust infrastructure services that Active Directory does, so some of the site settings are not relevant. However, ADAM does support replication via configuration sets.

One important difference from the previous example is that you must get an ADAM context to a directory server to complete an ADAM site creation operation. For example, this code snippet gets context to an instance of ADAM running on port 50000 on sea-dc-02.fabrikam.com, and then creates an ActiveDirectorySite named "site2" using the established directory server context and the new site name to create.

string adamConnectionString = "sea-dc-02.fabrikam.com:50000";
string newSiteName = "site2";

DirectoryContext adamContext = new DirectoryContext(
                                             DirectoryContextType.DirectoryServer, 
                                             adamConnectionString); 

ActiveDirectorySite site = new ActiveDirectorySite(adamContext, 
                                                      newSiteName);

Creating an Active Directory Subnet (MngTopology.CreateSubnet)

Creating an Active Directory subnet follows a very similar pattern as I demonstrated for creating a site. You typically begin by getting a forest context, and then you create an ActiveDirectorySite object. However, rather than specify a site you plan on creating, you need to choose an existing site for the subnet. You can complete this task by calling the FindByName method of the ActiveDirectorySite object, which binds to the object. Next, you need to create an ActiveDirectorySubnet object and provide it with the proper forest context and a subnet name. The subnet name includes both the IP address and subnet mask in the format <IP address>/<length of network mask>, for example, 10.1.1.0/24. Finally, you can set properties of the ActiveDirectorySubnet object and use the Save method of the object to save it to the directory.

In the upcoming example, I go one step further. After I call the Save method, I then call the GetDirectoryEntry method of the ActiveDirectorySubnet object. Once I have the DirectoryEntry object, I set the description attribute of the object and save the change back to the directory. Why am I doing this? Because, although S.DS.AD classes are extremely useful, they don't expose every attribute of a directory object. One of the many capabilities of the DirectoryEntry object (derived from a class in the S.DS namespace) is retrieving an existing object from the directory and allowing you to set attributes on that object. Note that I must set the attribute of the object after I call the Save method of the ActiveDirectorySubnet object since the corresponding subnet object doesn't exist in the directory until after I call the Save method. Finally, I call the CommitChanges method of the DirectoryEntry object to save the object with its modification back to the directory.

Tip   If you look in the Active Directory Sites and Services MMC, you will see that the Description field (which corresponds to the description attribute) appears on the General tab.

The following code example demonstrates how to create a new subnet, assign a site, and set both the subnet location and the description attribute.

  1. Create a forest context.
  2. Call the FindByName method of the ActiveDirectorySite object and pass it the forest context and existing site name to create an ActiveDirectorySite object.
  3. Create a ActiveDirectorySubnet object by passing the object the current context and a new subnet name, which includes both an IP address and a network mask.
  4. Set the Location and Site properties of the ActiveDirectorySubnet object, then call the Save method of the object to save the subnet to the directory.
  5. Call the GetDirectoryEntry method of the ActiveDirectorySubnet object to retrieve the subnet object from the directory and create a DirectoryEntry object locally.
  6. Set a value for the description attribute and then call the CommitChanges method of the DirectoryEntry object to save the subnet object back to the directory.

Example 13. Creating an Active Directory subnet

string subNetName = "10.12.3.0/24";
string subNetLocation = "Building 20";
// this value must represent an existing site
string siteName = "site1";

DirectoryContext forestContext = new DirectoryContext(
                            DirectoryContextType.Forest);

// get a site. The subnet will be assigned to it later.
ActiveDirectorySite site = ActiveDirectorySite.FindByName(
                                forestContext,
                                siteName);

// get a subnet using the specified directory context 
// and an IP with length of network mask written as: x.x.x.x/x
// for example, 10.1.1.0/24)
ActiveDirectorySubnet subnet = new ActiveDirectorySubnet(
                                                forestContext,
                                                subNetName);

// set the location of this subnet
subnet.Location = subNetLocation;



// set the site to which this subnet is a member
subnet.Site = site;

// save the subnet to the directory
subnet.Save();
Console.WriteLine("\nSubnet \"{0}\" was created successfully", subnet);

// get the subnet from the directory 
DirectoryEntry de = subnet.GetDirectoryEntry();

// set the description. Currently, this is not exposed as a property of the
// ActiveDirectorySubnet object.
de.Properties["description"].Value = subNetLocation +
    " (" + subNetName + ") in " + siteName;

// save the change back to the directory
de.CommitChanges();

Moving a Domain Controller to Another Site

Another common task after creating a site is to move domain controllers into a new site. To accomplish this task, you connect and bind to the server you want to move and then call the MoveToAnotherSite method and pass this method the name of the site to which you want the server moved.

  1. Get a directory server context
  2. Call the GetDomainController method and pass it the established context for binding to the target domain controller.
  3. Call the MoveToAnotherSite method and pass it the name of the target site for the move operation.

Example 14. Moving a domain controller to a site

string sourceDC = "sea-dc-01.fabrikam.com";
string targetSite = "site1";

// get a dc to move
DirectoryContext dcContext = new DirectoryContext(
                            DirectoryContextType.DirectoryServer, sourceDC);

DomainController dc = DomainController.GetDomainController(dcContext);

dc.MoveToAnotherSite(targetSite);

Console.WriteLine("{0} succesfully moved to {1}", sourceDC, targetSite);

Creating an Active Directory Site Link

Creating an Active Directory SiteLink object using S.DS.AD really helps to demonstrate how the classes in S.DS.AD simplify programmatic creation of Active Directory objects. An Active Directory SiteLink object is a bit more complex than most Active Directory objects (security descriptors aside) because it contains a schedule attribute that is stored as an octet string. Using the ActiveDirectorySiteLink object to create a site link and the ActiveDirectorySchedule object to set the schedule attribute of the site link, significantly simplifies the task of creating and configuring a site link, as the following code example demonstrates:

  1. Get a forest context.

  2. Use the FindByName method to get an existing site and create an ActiveDIrectorySite object.

  3. Create an ActiveDirectoryTransportType variable and use the ActiveDirectoryTransportType enumeration to specify an Rpc transport type.

    While Rpc is the default transport type, it's a good idea to be explicit in your code.

  4. Create an ActiveDirectorySiteLink object by passing it the forest context, the new site link name, and the transport type.

    The ActiveDirectorySiteLink object contains several overloads. This default constructor takes a directory context and a new site link name. In this case, the default Rpc transport is assigned to the site link. The code example uses the overload that also requires that you explicitly specify the transport type.

  5. Set various properties on the ActiveDirectorySiteLink object.

    There are several complex properties being set in the code. The InterSiteReplicationSchedule property takes an ActiveDirectorySchedule object. The ActiveDirectorySchedule object variable linkSchedule contains a SetDailySchedule method, as shown in the code. This method adds a range of values by using the HourOfDay and MinuteOfHour enumeration that are also new to the .NET Framework 2.0. You specify the starting time in hours and minutes and the ending time. In the code example, replication occurs between 5:30 A.M. and 6:30 A.M. and between 5:30 P.M. and 6:30 P.M. every day. Note that in the Active Directory Sites and Services snap-in, the Schedule dialog for a site link shows only one-hour increments so if you set small time increments (for example, MinuteOfHour), the time value will be rounded down to the nearest hour in the display.

    If you need to set a range of times for each day, you can use the SetSchedule method instead of the SetDailySchedule method. I demonstrate how to use this method following this code example.

  6. Add the link to the ActiveDirectorySiteLink object by calling the Add method of the ActiveDirectorySites collection.

    You must add at least one site to the ActiveDirectorySiteLink object or you will not be able to create the site link in the directory. Active Directory will return an ActiveDirectoryOperationException because the siteList attribute must contain at least one assigned site.

  7. Call the ActiveDirectorySiteLink object's Save method to save the link to the directory.

Example 15. Creating an Active Directory site link

string siteName = "site1";
string siteLinkName = "link1"

DirectoryContext forestContext = new DirectoryContext(
                            DirectoryContextType.Forest);

//get a site by name
ActiveDirectorySite site = ActiveDirectorySite.FindByName(
                                                forestContext,
                                                siteName);

// rpc is the default transport type (smtp is the other, 
// this is how you can create a transport type variable to 
// later assign to the link
ActiveDirectoryTransportType adTpt = 
                            ActiveDirectoryTransportType.Rpc;

// using the overload requiring the transport type to  
// demonstrate how to assign a transport type to this link.
ActiveDirectorySiteLink link = new ActiveDirectorySiteLink(
                                    forestContext, 
                                    siteLinkName, 
                                    adTpt);

// configure the ActiveDirectorySiteLink object 
link.Cost = 50;
link.DataCompressionEnabled = true;

// create an AD schedule object for setting intersite replication
ActiveDirectorySchedule linkSchedule = 
                                new ActiveDirectorySchedule();

// set the schedule for 5:30 am to 6:30 am
linkSchedule.SetDailySchedule(HourOfDay.Five,
                              MinuteOfHour.Thirty,
                              HourOfDay.Six,
                              MinuteOfHour.Thirty);

// set the schedule for 5:30 pm to 6:30 pm
linkSchedule.SetDailySchedule(HourOfDay.Seventeen,
                              MinuteOfHour.Thirty,
                              HourOfDay.Eighteen,
                              MinuteOfHour.Thirty);

// apply the replication schedule to the link
link.InterSiteReplicationSchedule = linkSchedule;

// enable inter-site change notification. Typically used for fast, 
// uncongested links between sites 
link.NotificationEnabled = true;


// replicate every twelve hours
TimeSpan linkTimeSpan = new TimeSpan(12, 0, 0);

// assign the TimeSpan object to the ReplicationInterval property
link.ReplicationInterval = linkTimeSpan;

// configure the link so there is no reciprocal replication
link.ReciprocalReplicationEnabled = false;

// assign a site to this site link. 
link.Sites.Add(site);

// commit the link to the directory
link.Save();

Console.WriteLine("\nLink \"{0}\" was created successfully", link.Name);

The SetSchedule method has two overloads. One overload lets you set a range of time for a specific day of the week by using three enumerations: DayOfWeek, HourOfDay and MinuteOfHour. Just like in the SetDailySchedule method, you provide a from and to period of time for the hour and minute enumerations and you specify a single day to which this schedule applies. If you need to set multiple days with different schedules, SetSchedule also allows you to create an array of days to which a particular range of time applies. For example, you could add the following to the code appearing in Example 15:

linkSchedule.SetSchedule(new DayOfWeek[] {DayOfWeek.Saturday, DayOfWeek.Sunday},
                                   HourOfDay.Sixteen, 
                                   MinuteOfHour.Zero, 
                                               HourOfDay.TwentyThree, 
                                               MinuteOfHour.Thirty);

This will set the replication schedule on Saturday and Sunday so that replication can occur between 4 P.M. and midnight as well as during the 4:30 A.M and 5:30 A.M. time window as configured by the SetDailySchedule method shown in Example 15.

Adding a Site to a Site Link

There are times when you might create a site and want to add it to an existing site link. In this case, you get a forest context, and use the FindByName method for both binding to and creating an ActiveDirectorySite object, as shown in Example 15, and for creating an ActiveDirectorySiteLink object. You then call the Add method of the ActiveDirectorySiteCollection to assign the new site to the site link. This is both similar to the previous code example and is part of the code download. While this is a simple procedure, I show it here to connect this with the site link removal task.

The following code example demonstrates how to add a site to an existing site link:

  1. Get a forest context.
  2. Create an ActiveDirectorySite and ActiveDirectorySiteLink using the FindByName method.
  3. Call the ActiveDirectorySiteCollection's Add method to add the site to the link.
  4. Call the ActiveDirectorySiteLink object's Save method to save the link to the directory.

Example 16. Adding a Site to an Existing Site Link.

string siteName = "site1";
string siteLinkName = "link1"

// get the forest context
DirectoryContext forestContext = new DirectoryContext(
                        DirectoryContextType.Forest);

// get a site by name
ActiveDirectorySite site = ActiveDirectorySite.FindByName(
                                                    forestContext,
                                                    siteName);

// get a link by name
ActiveDirectorySiteLink link = ActiveDirectorySiteLink.FindByName(
                                                        forestContext,
                                                        siteLinkName);


Console.WriteLine("\nAdd site \"{0}\" to site link \"{1}\"", site.Name,
                                                             link.Name);
link.Sites.Add(site);
link.Save();

Console.WriteLine("\nSiteLink \"{0}\" now contains: ", link);

foreach (ActiveDirectorySite s in link.Sites)
{
    Console.WriteLine("\tSite \"{0}\"", s);
}

Removing a Site from a Site Link

Removing a site from a site link involves the same steps that appear before calling the Add method in Example 16. Then, you call the Remove method of the ActiveDirectorySiteCollection object to remove the site from the link and finally call the ActiveDirectorySiteLink object's Save method to save the link to the directory. An example of this appears in the code download.

Deleting an Active Directory Site and Its Subnets

Deleting a site simply involves getting a forest context, binding to a site using the FindByName method and then calling the Delete method on the site. You might also want to remove the subnets that are assigned to a site. By doing so, the subnet is deleted from the Subnets container. This is a convenient approach for clean-up because the ActiveDirectorySite object contains a ActiveDirectorySubnetCollection object that you can iterate and then call the Delete method on each ActiveDirectorySubnet object returned.

Note   You don't need to call the Save method when you call the Delete method. The Delete method both deletes the associated object and commits the change to the directory. This is a common behavior of the underlying ADSI layer.

You should be reasonably familiar with the emerging code patterns involving site and subnet management, so instead of providing a step-by-step explanation of the code, I'll simply show the example:

Example 17. Deleting subnets in a site and then deleting a site

string siteName = "site1";

//get a forest context based on the name of the forest
DirectoryContext forestContext = new DirectoryContext(
                DirectoryContextType.Forest);

//Get a site using FindByName and the forest context
ActiveDirectorySite site = ActiveDirectorySite.FindByName(forestContext, siteName);

// iterate the subnets in the site object and call
// the Delete method on each subnet. 
foreach (ActiveDirectorySubnet subnet in site.Subnets)
{
    subnet.Delete();
}

//delete the site 
site.Delete();

Console.WriteLine("\nSite and subnets were deleted successfully\n");

If you don't remove the subnets when you remove a site, they will remain available for reassignment later.

Deleting an ADAM Site

Deleting an ADAM site also involves the ActiveDirectorySite class. However, instead of getting a forest context as you do in Active Directory, you simply get an ADAM instance by passing the DirectoryContext object the DirectoryServer value in the DirectoryContextType enumeration and an ADAM connection string. The following example is similar to how I previously demonstrated deleting an Active Directory site (see Example 17).

The following code example demonstrates how to delete an ADAM site:

Example 18. Deleting an ADAM site

//assemble the connection string using the host name and 
//the port assigned to ADAM
string adamConnectionString = "sea-dc-02.fabrikam.com:50000";
string siteName = "site1";

//get an ADAM context by connecting to the server running ADAM
DirectoryContext adamSrvrContext = new DirectoryContext(
                                        DirectoryContextType.DirectoryServer, 
                                        adamConnectionString);

ActiveDirectorySite site = 
        ActiveDirectorySite.FindByName(adamSrvrContext, siteName);

//delete the site
site.Delete();

Deleting an Active Directory Link

As you can see, the examples and the associated text describing them are getting progressively shorter. In part, it's because deletion operations are much simpler to complete then creation operations. However, I've also had to spend much less time explaining the code examples because clear patterns have emerged. That's the beauty of S.DS.AD. Once you begin to understand the classes in this namespace and how you instantiate them, set properties and call their methods, you can extrapolate that learning into a variety of related tasks. The final code example in this section demonstrates how to delete a link:

Example 19. Deleting an Active Directory link

string linkName = "link1";

//get a forest context based on the name of the forest
DirectoryContext forestContext = new DirectoryContext(
                DirectoryContextType.Forest);

//Get a site using FindByName and the forest context
ActiveDirectorySiteLink link = ActiveDirectorySiteLink.FindByName(
                                                        forestContext, 
                                                        linkName);

//delete the link 
link.Delete();

Managing Active Directory Replication

The purpose of Active Directory replication is to keep all attribute values up-to-date across all domain controllers that store replicas of that data. Replication occurs between a source server (domain controller) and a destination server (domain controller).

Note   The Directory Services SDK refers to a target server as a replication destination.

The granularity of any single attribute replication depends on the forest functional level. If you are running in a Windows 2000 forest functional level, granularity stops at the attribute level. If you are running in Windows Server 2003 interim forest functional level or above, a single value inside a multi-valued attribute is the smallest unit of replication.

By default, for intra-site replication (replication within a site) a source server notifies a target server that it has updates, then the target server pulls replication state data from a source server; for inter-site replication (replication between sites), a target server polls for changes when a replication interval expires. Replication occurs at the partition level. Therefore, when an attribute changes in one partition replica, the source server reports this change so that the changes within a partition can be updated.

This is an oversimplified view of replication, but it should suffice for understanding the code samples in this section. For a significantly more complete explanation of Active Directory replication, read How the Active Directory Replication Model Works.

Retrieving Replication State Data

There are a number of key objects for tracking Active Directory replication. Because partitions containing replication configuration and status data are stored on domain controllers, you use either a Domain or DirectoryServer context and pass that context to a DomainController object. Then, you use the following objects to return replication status and configuration data:

  • ReplicationCursorCollection and ReplicationCursor to retrieve partition by partition replication status for the current domain controller.

    The current domain controller is the server referenced by the DomainController object.

  • ReplicationNeighborCollection and ReplicationNeighbor to retrieve partition by partition replication status about other servers that replicate with the current server.

  • ReplicationConnectionCollection and ReplicationConnection to retrieve inbound and outbound configuration information about the connections that either initiate replication to the current server or connections that the current server initiates to other servers.

  • ActiveDirectoryReplicationMetadata and AttributeMetadata to return replication status data about a particular object in the directory.

The following code example demonstrates how to use these objects to return both replication configuration and status information:

  1. Get a domain context.

  2. Call the FindOne method of the DomainController class to bind to a domain controller.

    If your intent is to check a specific server for replication status, you can choose a target domain controller by using the GetDomainController method of the DomainController object. The DirectoryContextType you pass to this method is DirectoryServer. Here's a code snippet that demonstrates this alternative. In this case, the target server is sea-dc-02.fabrikam.com:

    DirectoryContext context = new DirectoryContext(
                                                DirectoryContextType.DirectoryServer, 
                                                "sea-dc-02.fabrikam.com");
    
    DomainController dc = DomainController.GetDomainController(context);
    
  3. Get the partitions of the DomainController object and iterate through these partitions to locate replication information about them.

    Each DomainController object contains a collection of read-only partitions. Directory partitions are the unit of replication between domain controllers containing partition replicas.

  4. Create a ReplicationCursor object by calling the GetReplicationCursors method of the DomainController object and passing it a partition. Then display replication state information contained in the cursor.

    A replication cursor displays information about the source server of a replication event. Each partition on a domain controller contains a replication cursor. The cursor contains state information about replication. For example, the last time a successful replication occurred, the source of the replication event, and the update sequence number (USN) representing the last update that the destination server has accepted from the source server.

    You'll notice in the code sample appearing below this text returns the SourceInvocationID property as a GUID string. I also demonstrate how to use the BuildFilterOctetString method to return the data as an octet string. Why do I bother showing this? Because the SourceInvocationID property derives its value from the invocationID attribute stored in the nTDSDSA object assigned to each server within a site. If you use ADSIEdit to inspect the value of the invocationID attribute, you'll notice that it's stored using octet GetReplicationNeighbors string syntax. The BuildFilterOctetString method demonstrates how to return the SourceInvocationID in this format so that you can compare the values returned by your application with the value that appears in ADSIEdit.

    Note   The BuildFilterOctetString method is from Listing 4.2 in The .NET Developer's Guide to Directory Services Programming by Joe Kaplan and Ryan Dunn. While Joe tells me this function is all over the Internet, I want to give credit where credit is due.

  5. Create a ReplicationNeighbor object by calling the method of the DomainController object and passing it a partition. Then, display replication neighbor state information contained in the neighbor.

    A replication neighbor displays information about the target server of a replication event. Like replication cursor information, the target for replication is scoped at the partition level. The ReplicationNeighborOption property returned by the GetReplicationNeighbors method deserves some additional explanation. This option is a flag exposing various replication attributes of a particular partition. For example, Writeable, SyncOnStartup, ScheduledSync mean that the local copy of the partition can be written to, it will synchronize the replica from a source server when it is restarted and replication will be performed on a schedule. For more information about these and other flags, read about dwReplicaFlags in the Directory Services SDK.

    The one value that might not be self-explanatory is the value returned by ReplicationScheduleOwnedByUser property. This value will be False if the KCC created the connection and True if you manually create a nTDSConnection object. The next section demonstrates how to use code to create and configure a connection object. This type of connection is considered manually created because the KCC doesn't create it for you.

  6. Iterate the InboundConnections property of the DomainController object. This property is a ReplicationConnectionCollection object. Next, return replication configuration data about each ReplicationConnection object.

    Each domain controller in a site contains an nTDSDSA object that serves as a container for nTDSConnection objects. The information displayed by each ReplicationConnection object is contained in a corresponding Active Directory nTDSConnection object. Inbound connections are simply a listing of all domain controllers that initiate replication to the current domain controller. The current domain controller is the server that the code bound to using the FindOne method appearing earlier in this code sample.

  7. Iterate the OutboundConnections property of the Domain Controller object. This property is also a ReplicationConnectionCollection object. Next, return replication configuration data about each ReplicationConnection object.

    This completes the same task as step 6, but in the opposite direction. Outbound connections list all domain controllers to which the current domain controller initiates replication.

  8. Create an ActiveDirectoryReplicationMetadata object by calling the GetReplicationMetadata method of the Domain Controller object and passing it a specific object path in distinguished name format. Then, iterate through the attribute names associated with the object.

    While viewing replication partition information is useful, sometimes you might want to view the replication status of a specific directory object. The ActiveDirectoryReplicationMetadata allows you to iterate through attributes associated with a target object.

  9. Assign an AttributeMetadata object to the replication metadata of an attribute by passing an ActiveDirectoryReplicationMetadata object the ldapDisplayName of an attribute to examine, then display replication status information about each attribute.

    This code demonstrates how you can get more granular (down to the attribute level) by inspecting replication data about an Active Directory object. The code demonstrates how to see exactly where an attribute update was made, the source and target USNs and the time that the attribute changed.

Example 20. Getting replication data

// get a domain context
DirectoryContext context = new DirectoryContext(
                                    DirectoryContextType.Domain);

// find a domain controller in the domain. The context for the FineOne
// method must be domain
DomainController dc = DomainController.FindOne(context);

// retrieve replication cursor information for each replicated partition
Console.WriteLine("\nReplication cursor data for each partition\n");
foreach (string partition in dc.Partitions)
{
    Console.WriteLine("\tPartition {0}", partition);
    foreach (ReplicationCursor cursor in dc.GetReplicationCursors(partition))
    {
        Console.WriteLine("\t\tSourceServer: {0}\n" +
                          "\t\tLastSuccessfulSyncTime: {1}\n" +
                          "\t\tSourceInvocationId: {2}\n" +
                          "\t\t octet string format: {3}\n" +
                          "\t\tusn: {4}\n",
                          cursor.SourceServer,
                          cursor.LastSuccessfulSyncTime,
                          cursor.SourceInvocationId,
                          BuildFilterOctetString(cursor.SourceInvocationId.ToByteArray()),
                          cursor.UpToDatenessUsn
                          );
    }
}

// retrieve replication neighbor information
Console.WriteLine("\nReplication neighbor data\n");
foreach (string partition in dc.Partitions)
{
    Console.WriteLine("\tPartition: {0}", partition);
    foreach (ReplicationNeighbor neighbor in
                                  dc.GetReplicationNeighbors(partition))
    {
        Console.WriteLine("\t\tSourceServer: {0}\n" +
                          "\t\tReplicationNeighborFlag: {1}\n" +
                          "\t\tUsnAttributeFilter: {2}\n" +
                          "\t\tLastSuccessfulSync: {3}\n",
                          neighbor.SourceServer,
                          neighbor.ReplicationNeighborOption,
                          neighbor.UsnAttributeFilter,
                          neighbor.LastSuccessfulSync);
    }
}

// retrieve the inbound replication connections
// other servers initiate replication to this server
Console.WriteLine("\nInbound replication connection data\n");

foreach (ReplicationConnection con in dc.InboundConnections)
{

        Console.WriteLine("\tReplication connection name (cn): {0}\n " +
                          "\tSourceServer: {1}\n " +
                          "\tDestinationServer: {2}\n" +
                          "\tTransport type: {3}\n" +
                          "\tConnection owned by user: {4}\n",
                          con.Name,
                          con.SourceServer,
                          con.DestinationServer,
                          con.TransportType,
                          con.ReplicationScheduleOwnedByUser);
}

// retrieve the outbound replication connections
// this server initiates replication to the following servers
Console.WriteLine("\nOutbound replication connection data\n");

foreach (ReplicationConnection con in dc.OutboundConnections)
{

    Console.WriteLine("\tReplication connection (cn): {0}\n" +
                      "\tSourceServer: {1}\n" +
                      "\tDestinationServer: {2}\n" +
                      "\tTransport type: {3}\n",
                      con.Name,
                      con.SourceServer,
                      con.DestinationServer,
                      con.TransportType
                      );
}

// A simple approach using S.DS to get to the dn of the current domain
DirectoryEntry de = new DirectoryEntry();
string targetDomainDN = de.Properties["distinguishedName"].Value.ToString();

// UPDATE THIS VALUE WITH A VALID RDN OF AN OBJECT IN YOUR DOMAIN. 
// If you want to evaluate a child object of an object that is a child of 
// the domain, then include it's path up to the domain level:
// for example, "cn=user1,ou=techwriters"
string AdObjectRdn = "ou=techwriters";

string objectPath = AdObjectRdn + "," + targetDomainDN;

// retrieve the replication metadata information for a specific object
// in this case, return information about replication occurring
Console.WriteLine("\nReplication metadata data for object path {0}\n", objectPath);

ActiveDirectoryReplicationMetadata metadata = null;

try
{
    metadata = dc.GetReplicationMetadata(objectPath);
}
catch (ArgumentException e)
{
    Console.WriteLine("{0}.\nPlease specify an existing " +
                       "object for the ADObjectRdn variable",
                       e.Message);
    return;
}

// iterate through the attributes associated with this object
foreach (string attribute in metadata.AttributeNames)
{
    Console.WriteLine("\tAttribute lDAPDisplayName: {0}\n", attribute);

    // for each attribute, assign an AttributeMetadata object by 
    // passing the ActiveDirectoryReplicationMetadata object the 
    // lDapDisplayName of the attribute to inspect. The 
    // ActiveDirectoryReplicationMetadata object gets the attribute
    // to inspect.
    AttributeMetadata replicationData = metadata[attribute];

    Console.WriteLine("\t\tOriginatingServer: {0}\n" +
                      "\t\tOriginatingChangeUsn: {1}\n" +
                      "\t\tLocalChangeUsn: {2}\n" +
                      "\t\tLastOriginatingChangeTime: {3}\n",
                      replicationData.OriginatingServer,
                      replicationData.OriginatingChangeUsn,
                      replicationData.LocalChangeUsn,
                      replicationData.LastOriginatingChangeTime);
}

// convert a hex value to an octet string
// to compare the hex value returned by the InvocationID properties
// with the value as it appears in ADSI Edit
// This method is from Listing 4.2 in The .NET Developer's Guide
// to Directory Services Programming by Joe Kaplan and Ryan Dunn.
static string BuildFilterOctetString(byte[] bytes)
{
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < bytes.Length; i++)
    {
        sb.AppendFormat("0x{0} ", bytes[i].ToString("X2"));
    }
    return sb.ToString();
}

Managing Replication

Forcing replication to synchronize partitions as well as creating, configuring and deleting replication connections (nTDSConnection objects) are replication management tasks available through the S.DS.AD namespace. Identical to getting replication data, as shown in Example 20, you begin all replication management operations by binding to a domain controller. The context passed to the binding operation varies depending on what you are trying to achieve. I'll specifically call out the context in this section's code examples for each task. Next, you use various methods of the domain controller object to initiate partition synchronization events. The method you use depends on the scope of the synchronization activity. For example, if you want to target a single domain controller for a synchronization operation from all of its neighbors, you call the TriggerSyncReplicaFromNeighbors method.

Note   You should perform manual synchronizations with discretion. Microsoft designed the Active Directory replication topology to replicate critical changes when necessary. Performing many manual updates could potentially preempt other scheduled synchronization events and potentially overuse the network at times of high usage.

The ReplicationConnection class provides methods to create, configure and delete connections. For configuring a replication connection, you use the ActiveDirectorySchedule class that I demonstrated earlier in this paper for configuring an Active Directory site link (Example 15). I'll revisit this class in the upcoming code examples.

Triggering Replication of a Partition From a Target Source

If you know the domain controller and the partition in which an attribute change was made and your goal is to force replication between one domain controller and another, you can call the SyncReplicaFromServer method of the DomainController class.

As a best practice, it's a good idea to call the CheckReplicationConsistency method of the DomainController class. This method verifies that the replication topology can support the synchronization operation. CheckReplicationConsistency invokes the Knowledge Consistency Checker (KCC) to check that a replication link exists on the target domain controller against which the method was run.

The following code example demonstrates how you can check replication topology on a target domain controller and how to synchronize a partition replica from a source domain controller to target domain controller.

  1. Initialize variables for the target and source domain controllers and the partition to replicate.

  2. Create a DirectoryContext object for the target domain controller using the DirectoryServer value of the DirectoryContextType enumeration and the name of the targetServer.

    Although you could get a domain context and then use the FindOne method to locate any domain controller to serve as the target server for replication, it doesn't make a lot of sense to do so in this case. This is because the typical purpose for using the SyncReplicaFromServer method is to synchronize a replica from a specific source server to a specific target server.

  3. Call the CheckReplicationConsistency method of the target DomainController object to invoke the KCC to verify that the replication topology is consistent.

  4. Call the SyncReplicaFromServer method of the target DomainController object and pass it the partition to replicate and the name of the domain controller that is the source of the replication.

Example 21. Checking replication topology and synchronizing a replica from a target source

string targetServer = "sea-dc-02.fabrikam.com";
string sourceServer = "sea-dc-01.fabrikam.com";
string partitionName = "dc=fabrikam,dc=com";

// set a directory server context for the target server
DirectoryContext targetContext = new DirectoryContext(
                                    DirectoryContextType.DirectoryServer,
                                    targetServer);

// Bind to a specific dc to serve as the replication source
DomainController targetDc =
                    DomainController.GetDomainController(targetContext);


// invoke the kcc to check the replication topology of the target dc
targetDc.CheckReplicationConsistency();
Console.WriteLine("\nReplication topology is consistent.\n");


// trigger a synchronization of a replica from a source dc
// to the target dc
targetDc.SyncReplicaFromServer(partitionName, sourceServer);

Console.WriteLine("\nSynchronize partition \"{0}\" " +
                    "from server {1} to {2} succeed",
                    partitionName,
                    sourceServer,
                    targetServer);

Triggering Replication of a Partition from All Neighbors

At times, you might be more interested in triggering replication with all domain controllers in which an inbound connection is defined on a target domain controller. The code to accomplish this task is similar, but simpler than triggering replication from a source server to a target server because there is no need to identify a source of replication. The target domain controller contains definitions for each inbound connection. You trigger replication with neighbors by getting a domain controller context and calling the TriggerSyncReplicaFromNeighbors method of the target domain controller object. This method takes the name of the partition you want to synchronize. The following code snippet demonstrates calling the method and passing it the name of the domain partition for fabrikam.com:

string partitionName = "fabrikam.com";
// sync replica from all neighbors
targetDc.TriggerSyncReplicaFromNeighbors(partitionName);

The code download with the article includes an example of how to accomplish this type of synchronization task.

Triggering Site-Wide or Forest Wide Replication

S.DS.AD provides a facility for forcing a more global synchronization of replicas within a site or across an entire forest. Synchronization within and across sites is accomplished using the built-in replication mechanisms, such as transitive replication and replication schedules. Forcing replication could possibly preempt scheduled replication events. Therefore, even though I mentioned this earlier I'll say it again, use caution when forcing replication on a production network and especially the programmatic replication I demonstrate here because it can be used to trigger a forest-wide replication.

This type of forced replication is accomplished with the following components in S.DS.AD:

  • The SyncUpdateCallback delegate for receiving event notifications during a synchronization event and reporting those events during the synchronization request process.
  • The SyncFromAllServersCallback property for getting or setting the SyncUpdateCallback delegate on the target domain controller object.
  • The SyncReplicaFromAllServers method for targeting a partition for replication and for setting replication options using the SyncFromAllServersOptions enumeration.
  • The SyncFromAllServersOptions enumeration for specifying various options that control replication behavior.

Coding this type of replication is similar to the other replication methods explored in this section, but it also involves defining and calling a delegate method. Also, while you can call a specific directory server for the context type as I demonstrated in Example 21, I demonstrate here how you can use a domain context and the FindOne method to bind to any domain controller in the current domain context.

  1. Get a domain context and then pass it to the FindOne method of a DomainController object for binding to an available domain controller.

  2. Call the CheckReplicationConsistency method to invoke the KCC to verify the target domain controller's replication topology.

    Calling this method isn't necessary for performing the type of replication demonstrated in this section. However, it's not a bad idea to verify this topology prior to forcing a replication so that you ensure that the target domain controller is properly configured for replicating with other servers.

  3. Set the SyncFromAllServersCallback property of the domain controller object with the name of the SyncUpdateCallback delegate.

  4. Call the SyncReplicaFromAllServers method of the domain controller object and pass it the name of the partition to replicate and any synchronization control options contained in the SyncFromAllServersOptions enumeration.

    In this code example, the AbortIfServerUnavailable and the CrossSite values of the SyncFromAllServersOptions enumeration is set. The first option reports an error if any server can't be reached and the second option causes synchronization to be triggered across site boundaries to any server connected via RPC.

The SyncUpdateCallback delegate follows this code example:

Example 22. Triggering a cross-site replication

// get a domain context
DirectoryContext context = new DirectoryContext(
                                DirectoryContextType.Domain);

// bind to an available domain controller in the domain. 
// method must be domain
DomainController dc = DomainController.FindOne(context);

// invoke the kcc to check the replication topology for the 
// current domain controller
dc.CheckReplicationConsistency();
Console.WriteLine("\nCheck replication consistency succeed\n");

// Set the synchronization delegate for this dc
dc.SyncFromAllServersCallback = SyncFromAllServersCallbackDelegate;

Console.WriteLine("\nStart sync with all servers:");

// Call the synch. method and set synch. options 
dc.SyncReplicaFromAllServers(partitionName,
                    SyncFromAllServersOptions.AbortIfServerUnavailable
                    | SyncFromAllServersOptions.CrossSite
                    );

Console.WriteLine("\nSynchronize partition \"{0}\" " +
                  "with all servers succeeded", partitionName);

The SyncUpdateCallback delegate receives event notifications during synchronization. This delegate returns True until synchronization completes. This delegate takes four parameters:

  1. The SyncFromAllServersEvent enumeration that returns one of four possible events: Error, Finished, SyncCompleted, or SyncStarted.
  2. A string parameter to specify the target server for synchronization.
  3. A string parameter to specify the source server for synchronization.
  4. A SyncFromAllServersOperationException exception to capture any failures in the request to synchronize with all servers.

There will be event types returned to the SyncUpdateCallback delegate when the target server, source server, or exception is null. The following code example demonstrates how you can handle this in your code.

Example 23. Creating a SyncUpdateCallback delegate to report on synchronization events

// This SyncUpdateCallback delegate receives event
// notifications during replica synchronization
private static bool SyncFromAllServersCallbackDelegate(
                        SyncFromAllServersEvent eventType,
                        string targetServer,
                        string sourceServer,
                        SyncFromAllServersOperationException e
                    )
{
    // return the type of synchronization event that occurred
    Console.WriteLine("\neventType is {0}", eventType);

    // return the DN of the target nTDSDSA object for replication
    // this will be null when the eventType reports finished
    if (targetServer != null)
        Console.WriteLine("target is {0}", targetServer);

    // return the DN of the source nTDSDSA object for replication
    // this will be null when the eventType reports finished
    if (sourceServer != null)
        Console.WriteLine("source is {0}", sourceServer);

    // return any sync. operation exception
    // this will be null if there is no exception to report
    if (e != null)
        Console.WriteLine("exception is {0}", e);

    // return true to instruct the calling method to
    // continue its operation. The SyncUpdateCallback 
    // delegate returns false when the eventType is finished
    return true;
}

Note   This and other replication methods are defined in the Active Directory DC and Replication Management Functions in the Directory Services Platform SDK. The set of S.DS.AD properties, methods and delegates explored here are defined in the DsReplicaSyncAll replication management and SyncUpdateProc functions.

Creating and Configuring a new Replication Connection

Outside of forcing replication, another replication management task involves creating manual replication connection objects (nTDSConnection objects) from one server to another. Each domain controller contains a child NTDS settings container (nTDSDSA object) that holds inbound connection objects for replicating with other domain controllers. The server containing the nTDSConnection connection object is the target server (a.k.a. destination server) for replication.

The KCC automatically creates nTDSConnection connection objects for each connected domain controller. If you create a manual connection object for replication, the automatic connection object created by the KCC will be converted to a manual connection and the KCC will no longer be involved in managing the connection. Therefore, be sure you understand the implications associated with created manual connection objects in a production domain. For more information on when this is appropriate, see How to optimize Active Directory replication in a large network.

To create a manual connection object in Active Directory, you must create a ReplicationConnection object. The default constructor for the ReplicationConnection class requires a source server, a target server context, and a connection name. You get the source server by binding to it with the GetDomainController method of the DomainController object. You can give the connection object any name that is unique within the NTDS settings container. Other constructors for the ReplicationConnection object let you configure connection settings, such as a replication schedule or transport type.

Once you have your connection object named and configured, you call the Save method of the ReplicationConnection object to save the connection object to the directory. As a result, a manual connection object will appear in the NTDS settings container of the target server.

  1. Get a source and target context using the DirectoryServer value of the DirectoryContextType enumeration to create a DirectoryContext object.

  2. Bind to the source domain controller using the GetDomainController method of the DomainController object.

  3. Create a ReplicationConnection object by passing it the target directory server context, a connection name and a source directory server for replication.

  4. Configure various settings on the ReplicationConnection object.

    This code example shows how to configure:

    • Change notification   The IntraSiteOnly value of the NotificationStatus enumeration configures the connection so that changes to the directory are only reported between domain controllers in the same site. This process notifies other severs of changes to the directory rather than waiting for the target server to poll a source server for changes during its scheduled replication period. Intrasite notification is the default behavior. If your sites are connected to each other using a high-bandwidth uncongested backbone, you might want to use the NotificationStatus NotificationAlways value for the ChangeNotificationStatus property of the connection.
    • Replication schedule   The ReplicationSchedule property takes an ActiveDirectorySchedule object. In this code example, the ActiveDirectorySchedule object is configured using the SetDailySchedule and SetSchedule methods. In an earlier code example (Example 15), I demonstrated how to use the SetSchedule and SetDailySchedule methods. In addition, I followed the example by demonstrating how you use the SetSchedule method to set several days of the week to the same start and end time.
    • Replication schedule ownership   The ReplicationScheduleOwnedByUser value is set to False by default and is irrelevant for manually created connections because a manually created connection's replication schedule is always owned by a user. You should set this to True for connections automatically created by the KCC if you don't want the KCC to own the schedule. Setting this value to True flips the sixth position in the options attribute (decimal value: 32) to on.

Example 24. Creating a new nTDSConnection object on a destination server

string sourceServer = "sea-dc-01.fabrikam.com";
string targetServer = "sea-dc-02.fabrikam.com";
string connectionName = "connection01";

// set a directory server context for the source server
DirectoryContext sourceContext = new DirectoryContext(
                                    DirectoryContextType.DirectoryServer,
                                    sourceServer);

// set a directory server context for the target server
DirectoryContext targetContext = new DirectoryContext(
                                    DirectoryContextType.DirectoryServer,
                                    targetServer);


// Get a specific domain controller to serve as the 
// source of a replication connection
DomainController sourceDc =
                    DomainController.GetDomainController(sourceContext);

ReplicationConnection connection = new ReplicationConnection(
                                        targetContext,
                                        connectionName,
                                        sourceDc);

// set change notification status
connection.ChangeNotificationStatus = NotificationStatus.IntraSiteOnly;


// create a customized replication schedule
ActiveDirectorySchedule schedule = new ActiveDirectorySchedule();
schedule.SetDailySchedule(HourOfDay.Twelve,
                          MinuteOfHour.Zero,
                          HourOfDay.Fifteen,
                          MinuteOfHour.Zero);

schedule.SetSchedule(DayOfWeek.Sunday,
                     HourOfDay.Eight,
                     MinuteOfHour.Zero,
                     HourOfDay.Eleven,
                     MinuteOfHour.Zero);

schedule.SetSchedule(DayOfWeek.Saturday,
                     HourOfDay.Seven,
                     MinuteOfHour.Zero,
                     HourOfDay.Ten,
                     MinuteOfHour.Zero);

connection.ReplicationSchedule = schedule;
connection.ReplicationScheduleOwnedByUser = true;
connection.Save();
Console.WriteLine("\nNew replication connection created successfully\n" +
  "from server {0} to {1}.\n The connection appears in the NTDS " +
  "settings of {1}", sourceServer, targetServer);

Deleting a Replication Connection

To delete a nTDSConnection object, you get a DirectoryContext using the DirectoryServer value of the DirectoryContextType enumeration to get context to a domain controller. Then, you call the FindByName method of the ReplicationConnection object and pass it the directory context and the name of the connection you want to delete. Finally, you call the Delete method of the ReplicationConnection object to remove the connection. The following code example demonstrates how you complete this task:

Example 25. Deleting an nTDSConnection object

string connectionName = "connection1";

DirectoryContext context = new DirectoryContext(
                                DirectoryContextType.DirectoryServer,
                                server);

ReplicationConnection connection =
                            ReplicationConnection.FindByName(
                                                     context,
                                                     connectionName);

// delete the replication connection
connection.Delete();
Console.WriteLine("\nReplication connection {0} deleted", connectionName);

It's a good idea to check the replication topology after deleting a connection. By doing so, the KCC will check the NTDS settings container (nTDSDSA object) to see whether any replication connections need to be automatically generated. As mentioned earlier, you can do this programmatically by calling the CheckReplicationConsistency method of the DomainController object. As a reminder, you can get a DomainController object by calling the GetDomainController method and passing it the context, as shown:

// bind to a specific dc
DomainController targetDc =
                    DomainController.GetDomainController(context);

Then you can call the CheckReplicationConsistency method, as shown:

// invoke the kcc to check the replication topology of the target dc
targetDc.CheckReplicationConsistency();

Managing Active Directory Trusts

This final section of tasks demonstrates how to retrieve and configure trust relationships. If you're familiar with the NetDOM command-line tool or the Active Directory Domains and Trusts snap-in, you should be able to make connections to the trust management tasks you complete with those tools as you read through this material.

Using S.DS.AD you can get the current domain and forest trust relationships and get more granular by checking relationships between a specific target forest or target domain. S.DS.AD also allows you to perform many trust management tasks. I'll demonstrate how to create trust relationships, set trust attributes, change trust direction, add excluded domains to a trust, repair broken trusts and remove trust relationships.

Retrieving Forest Trust Data

As you might expect, to get forest trust data from the current forest, you begin by getting forest context and binding to the forest. The simplest way to achieve this is calling the GetCurrentForest method of the Forest class. I introduced this method in the Getting Active Directory Context section earlier in this paper. You can also call the GetForest method instead if you want to bind to a different forest, but you must pass this method a DirectoryContext object that is either a Forest or DirectoryServer directory context type, as these code snippets show:

// get a forest context type
DirectoryContext context = new DirectoryContext(
    DirectoryContextType.Forest);

// get a directory server context type to the 
// sea-dc-02.fabrikam.com domain controller
DirectoryContext context = new DirectoryContext(
    DirectoryContextType.DirectoryServer, "sea-dc-02.fabrikam.com");

After you get either one of these contexts, you can then pass the context to the GetForest method as shown:

Forest currentForest = Forest.GetForest(context);

The following code example demonstrates how you use the GetCurrentForest method to bind to the current forest and then return information about the forest.

  1. Bind to the current forest by calling the GetCurrentForest method of the Forest class.
  2. Call the GetAllTrustRelationships method of the Forest class to return a collection of trust relationships, then, for each returned relationship (TrustRelationShipInformation object) display the following properties:
    • The SourceName and TargetName properties of each forest trust.

    • The TrustDirection property of the forest trust relationship, either Outbound, InBound or Bidirectional.

      Each Outbound trust shows a forest trusted by the current forest. Each Inbound trust shows a forest that trusts the current forest. A Bidirectional trust is both outbound and inbound.

    • The TrustType property of the relationship.

      In this case, the value will be Forest because it represents the relationship between two forest root domains. Review the TrustType enumeration for a list of other trust types.

    • The GetSelectiveAuthenticationStatus property to check if it is True or False between the current forest (trusting forest) and the target forest (trusted forest).

      Notice that you pass the trusted forest to the GetSelectiveAuthenticationStatus property by using the TargetName property of the ForestTrustRelationshipInformation object.

      This property displays a selective authentication status security setting that, when enabled, restricts users from other forests (trusted forests) from accessing resources in the current forest (the trusting forest) unless the user from the trusted forest has been explicitly given the Allowed to Authenticate permission to specific computer resources within the trusting forest. By default Forest-wide authentication is enabled for each forest trust relationship.

    • The GetSidFilteringStatus property to check if it is True or False between the current forest (trusting forest) and the target forest (trusted forest).

      Like the GetSelectiveAuthenticationStatus property, you pass the trusted forest to the GetSelectiveAuthenticationStatus property by using the TargetName property of the ForestTrustRelationshipInformation object.

      This property displays a security setting that, when enabled, causes the trusting domain to inspect all incoming authorization requests and remove any SIDs that do not properly identify the user or security group defined in the trusted domain. This avoids a privilege elevation attack originating from an external trust. SID filtering is enabled by default in Windows Server 2003. See https://www.microsoft.com/technet/security/Bulletin/MS02-001.mspx for more information.

    • The TopLevelNames property returns a collection of TopLevelName objects.

      Each of these objects contain name and status information about the top-level domain in a forest trust relationship.

      The Status property returns one of the values in the TopLevelNameStatus enumeration. The top-level domain trust relationship can be disabled via administrative action, by the system due to forest trust conflicts or upon trust creation. The AdminDisabled value of the enumeration is returned if you disable name suffix routing for the top level domain name suffix. For more information on routing name suffixes (including reasons for forest trust conflicts), see http://technet2.microsoft.com/WindowsServer/en/library/ec5c297e-7f48-4db3-aae7-655b4b3c186f1033.mspx?mfr=true.

    • The ExcludedTopLevelNames property also returns a collection, but this collection returns a string collection object which contains the name of any domains that are specifically excluded from the forest trust relationship.

      Excluded domains are any child domain where routing has been disabled. For example, if you establish a forest trust relationship between fabrikam.com and adatum.com and then disable the *.corp.adataum.com name suffix, then corp.adatum.com will be returned as an excluded top level domain name.

    • The TrustedDomainInformation property returns a collection of ForestTrustInformation objects.

      Each of these objects contains information about all domains involved in the forest trust relationship, their SIDs and their status.

      The Status property returns one of the values in the ForestTrustDomainStatus enumeration. This enumeration returns status on whether a domain NetBIOS name or SID is enabled or disabled for the specified domain. NetBIOS name and SIDs can be disabled administratively (using command line tools or programmatically) or by the system as a result of a detected DNS name, NetBIOS name or domain SID conflict. Later in this section, I will demonstrate how to change the status value programmatically.

Example26. Get forest trust information from the current forest

// get the current forest - get context and bind to the forest
Forest currentForest = Forest.GetCurrentForest();

// Retrieve all the forest trusts
Console.WriteLine("\retrieve all the forest trusts " +
                  "with current forest:\n");

foreach (ForestTrustRelationshipInformation forestTrust in
                               currentForest.GetAllTrustRelationships())
{
    // for each forest trust relationship, get its properties
    Console.WriteLine("\nForest trust: {0} - {1}\n" +
                    "Trust direction: {2}\nTrust type: {3}",
                    forestTrust.SourceName.ToUpper(),
                    forestTrust.TargetName.ToUpper(),
                    forestTrust.TrustDirection,
                    forestTrust.TrustType);

    // display selective authentication status of a forest trust
    Console.WriteLine("SelectiveAuthenticationStatus of the trust: {0}",
        currentForest.GetSelectiveAuthenticationStatus(
                                    forestTrust.TargetName));

    // display Sid filtering status of a forest trust
    Console.WriteLine("SidFilteringStatus of the trust: {0}",
        currentForest.GetSidFilteringStatus(
                                    forestTrust.TargetName));

    Console.WriteLine("\nTopLevelNames:");
    foreach (TopLevelName top in forestTrust.TopLevelNames)
    {
        Console.WriteLine("\t{0}, status: {1}", top.Name, top.Status);
    }
    Console.WriteLine("\nExcludedTopLevelNames:");
    foreach (string excluded in forestTrust.ExcludedTopLevelNames)
    {
        Console.WriteLine("\t{0}\n", excluded);
    }
    Console.WriteLine("\nForest Trust Domain Information:");
    foreach (ForestTrustDomainInformation domainInfo in
                                       forestTrust.TrustedDomainInformation)
    {
        Console.WriteLine("\n\tDNS name: {0}\n\tNetBIOS name: {1}\n" +
                          "\tdomain sid: {2}\n\tstatus: {3}",
                          domainInfo.DnsName,
                          domainInfo.NetBiosName,
                          domainInfo.DomainSid,
                          domainInfo.Status
                          );
    }

}

If you're interested in retrieving a trust relationship between the current forest and a target forest, you still use the GetCurrentForest method to bind to the current forest. Then, instead of calling the GetAllTrustRelationships method of the Forest object, call the GetTrustRelationship method and pass it the name of a target forest. You can then display the same property data shown in Example 26. There is a code sample included with the code download that shows you how to use the GetTrustRelationship method to return a subset of the forest trust data appearing in the previous figure.

Retrieving Domain Trust Data

To get domain trust data, you follow a similar pattern that you saw in getting forest trust data. Therefore, I'll be brief here and point-out code similarities with obtaining forest trust data. Rather than binding to a forest, you start by binding to a domain. The simplest way to do this for the current domain is to call the GetCurrentDomain method of a Domain object. Next, you call the GetAllTrustRelationships method of the Domain object to return a collection of TrustRelationshipInformation objects. This is the same method you called when you retrieved forest trust data except that, in that case, you called this method from the Forest object instead. Finally, as you saw in the last code example, you enumerate each returned object to obtain trust data, as the following code sample demonstrates:

Example 27. Getting domain trust information from the current domain

// get a domain context and bind to the current domain
Domain currentDomain = Domain.GetCurrentDomain();

// Retrieve all the domain trusts
Console.WriteLine("\nRetrieve all domain trusts with the current domain:\n");
foreach (TrustRelationshipInformation trust in
                               currentDomain.GetAllTrustRelationships())
{
    // for each domain trust relationship, get its properties
    Console.WriteLine("\nDomain trust: {0} - {1}" +
                      "\ntrust direction: {2}" +
                      "\ntrust type: {3}",
                      trust.SourceName.ToUpper(),
                      trust.TargetName.ToUpper(),
                      trust.TrustDirection,
                      trust.TrustType);

    // display selective authentication status of the domain trust
    Console.WriteLine("SelectiveAuthenticationStatus of the trust: {0}",
        currentDomain.GetSelectiveAuthenticationStatus(trust.TargetName));

    // display Sid filtering status of the domain trust
    Console.WriteLine("SidFilteringStatus of the trust: {0}/n",
        currentDomain.GetSidFilteringStatus(trust.TargetName));

}

Getting domain trust data returns all trust relationships, including implicit trust within a forest hierarchy and explicit domain trust relationships created between two forests.

To get a specific trust between the current domain and another domain, you bind to the current domain, as shown in Example 27, then call the GetTrustRelationship method of the Domain object and pass it the name of the target domain. The code download with this article includes an example of how to use this method to return trust data.

Managing a Forest Trust

Before you can create or manage a forest trust, the domain and forest must be running the Windows Server 2003 domain and forest functional levels respectively. In addition, you must properly configure a forwarder in DNS so that the forests are visible to each other. See https://www.microsoft.com/technet/prodtechnol/windowsserver2003/technologies/directory/activedirectory/fedffin2.mspx for details on preparing for cross forest trusts.

Creating a Cross Forest Trust

To establish both sides of a forest trust relationship, you need to bind to both forests. If your goal is to establish a forest trust relationship with the current forest, you can use the GetCurrentForest method of the Forest object to bind to the current forest. Next, to bind to the target forest, use the GetForest method and pass it a forest directory context.

When establishing the forest directory context for this operation, you provide the name of the target forest, and user name password combination of a user with sufficient permissions to establish the remote side of the trust relationship. While the default constructor for creating a DirectoryContext object does not require a user name and password, it's likely that you will need to provide it unless both forests contain an identical highly privileged user account and password combination. Obviously, this is an unlikely scenario.

Now that you have both Forest objects, you call the CreateTrustRelationship method of the source forest and pass it the targetForest Forest object and a value from the TrustDirection enumeration (Bidirectional, Inbound or Outbound).

Note   I don't recommend ever hard-coding passwords into code but to be consistent, I'm including a string variable for a password value in several of the upcoming code examples. The code download does not take this approach. Instead, you pass any required parameters into the method from the command line.

The following code example demonstrates how to create both sides of a bidirectional cross forest trust.

  1. Bind to the current forest (sourceForest) and the target forest (targetForest).

  2. Call the CreateTrustRelationship method of the sourceForest object and pass it the targetForest object and the Bidirectional value of the TrustDirection enumeration.

    If this method fails, the code will raise an exception and the next code line that reads, "Cross forest trust created." will not be reached.

  3. Call the VerifyTrustRelationship method of the sourceForest to validate the trust relationship and pass it the targetForest object and the appropriate trust direction to verify.

    VerifyTrustRelationship first verifies that the trust is syntactically correct, then it checks that the trust relationship actually exists. Finally, it calls the NetLogon API status function (I_NetLogonControl2 function) to complete trust validation.

    If you cannot bind to the target forest, there is also the VerifyOutboundTrustRelationship method that allows you to simply verify the outbound side of the trust relationship. This method requires the name of the target forest, not the target forest object.

Example 28. Creating a bi-directional cross-forest trust

string targetForestName = "fabrikam.com"
string userNameTargetForest = "enterpriseAdminUser";
string password = "some password value";


// Get the source and target forest contexts
Forest sourceForest = Forest.GetCurrentForest();

DirectoryContext targetContext = new DirectoryContext(
                                        DirectoryContextType.Forest,
                                        targetForestName,
                                        userNameTargetForest,
                                        password);

Forest targetForest = Forest.GetForest(targetContext);

// Create a bidirectional trust between the source and target forest
sourceForest.CreateTrustRelationship(targetForest,
                                     TrustDirection.Bidirectional);


Console.WriteLine("\nCross forest trust created.");


// verify the trust relationship
sourceForest.VerifyTrustRelationship(targetForest,
                                     TrustDirection.Bidirectional);

Console.WriteLine("\nThe forest trust has been successfully validated.");

}

If you're creating just one side of a trust relationship, you can use the CreateLocalSideOfTrustRelationship method instead of the CreateTrustRelationship method. In this case you don't establish any context with the target forest. Instead, you simply pass this method the name of the target forest, the direction of the trust relationship and a password that the administrator on the remote side of the trust relationship will use to establish the far side (from your perspective) of the trust relationship.

You will see other methods in this exploration of trust management, each with a corresponding local version of the method so that you can complete the near side of a trust management operation. Once that's done, an administrator on the other side of the trust can run the same operation to complete the trust management task.

Configuring Forest Trust Properties

The Forest object contains the SetSelectiveAuthenticationStatus and SetSidFilteringStatus methods that allow you to configure the selective authentication and SID filtering properties of a trust relationship. See the Retrieving Forest Trust Data section earlier in this paper for details on these two settings.

The following code example shows how you can set these two properties of a trust relationship using the current forest context:

  1. Bind to a forest. In this case, the code binds to the current forest.
  2. Call the SetSelectiveAuthenticationStatus method and then the SetSidFilteringStatus method and pass each method the name of the target forest and a Boolean True or False value to enable or disable these properties.

Example 29. Configuring properties of a trust relationship

string targetForestName = "adatum.com";

// Bind to the current forest
Forest sourceForest = Forest.GetCurrentForest();

// change forest trust attributes
sourceForest.SetSelectiveAuthenticationStatus(targetForestName, true);
sourceForest.SetSidFilteringStatus(targetForestName, false);

Changing Trust Direction

Changing a trust direction is simply a matter of calling the UpdateTrustRelationship method of the a source forest object and passing a target forest object and the new direction from the TrustDirection enumeration.

Like you saw with creating a trust relationship, there is also a similar method for updating one side of a trust relationship at a time. In this case, the method is UpdateLocalSideOfTrustRelationship. You pass this method the name of the target forest and a password that an administrator on the other side will use in order to complete the trust management task.

The following code example demonstrates how you convert a forest trust to outbound. As a result, the far side of the trust is set to inbound:

  1. Bind to the current and target forest.
  2. Call the UpdateTrustRelationship method of the sourceForest object and pass it the targetForest object and the Outbound value of the TrustDirection enumeration.
  3. Verify the outbound trust relationship by calling the VerifyOutboundTrustRelationship method and pass it the name of the target forest.

Example 30. Updating a trust to outbound

string targetForestName = "adatum.com"
string userNameTargetForest = "enterpriseAdminUser";
string password = "some password value";

// Bind to the current forest 
Forest sourceForest = Forest.GetCurrentForest();

// Get context to the target forest
DirectoryContext targetContext = new DirectoryContext(
                                        DirectoryContextType.Forest,
                                        targetForestName,
                                        userNameTargetForest,
                                        password);

// Bind to the target forest
Forest targetForest = Forest.GetForest(targetContext);


// update the trust direction
sourceForest.UpdateTrustRelationship(targetForest,
                                     TrustDirection.Outbound);

Console.WriteLine("\nUpdateTrustRelationship succeeded");

// verify outbound side of the trust relationship. 
sourceForest.VerifyOutboundTrustRelationship(targetForestName);
Console.WriteLine("\nVerifyOutboundTrustRelationship succeeded\n");

Adding an Excluded Domain to a Forest Trust

Forest name suffix routing controls whether authentication requests can be sent across a forest trust. By adding an excluded domain to a forest trust, you are disabling routing across that trust of an authentication attempt that uses that domain name suffix. Once a domain in a trusted forest (commonly called the account domain) is excluded within the trusting forest (a forest containing resource domains), a user from the account domain cannot authenticate to resources in the trusting forest.

To add an excluded domain, you must first call the GetTrustRelationship method of a source forest and pass it the name of the target forest. This returns a ForestTrustRelationshipInformation object. You can then use the object to call the Add method of the ExcludedTopLevelNames property of the forest trust. The ExcludedTopLevelNames property is a string collection which takes the name of the top-level domain to add. After adding a domain to the collection, you call the Save method of the forest trust. The following code example demonstrates how to add an excluded domain:

  1. Bind to the current forest.
  2. Call the GetTrustRelationship method of the sourceForest object and pass it the name of the target forest to create a ForestTrustRelationshipInformation object named forestTrust.
  3. Call the Add method from the ExcludedTopLevelNames property of the forestTrust object and pass it the name of the domain to exclude.
  4. Call the Save method of the forestTrust object to save the change to the directory.

Example31. Disabling name suffix routing to a domain in a forest trust relationship

string targetForestName = "adatum.com";
string targetDomainName = "corp.adatum.com";

// Bind to the current forest
Forest sourceForest = Forest.GetCurrentForest();
 
//get the trust relationship
ForestTrustRelationshipInformation forestTrust =
    sourceForest.GetTrustRelationship(targetForestName);

// add a top level name
forestTrust.ExcludedTopLevelNames.Add(targetDomainName);
forestTrust.Save();

Assuming that the current forest containing resource domains is fabrikam.com, the code sample (Example 31) performs the same action as the following NetDOM command:

netdom trust fabrikam.com /domain:adatum.com /addtlnex:corp.adatum.com

The prior examples disables both the standard account name (for example, corp\user1) and user principal name user account formats (for example, user1@corp.adatum.com). To prevent just the standard account name from being routed across the forest trust, get a ForestTrustDomainInformation object from the TrustedDomainInformation property of a forest trust object. Next, set the Status value of the object to either NetBiosNameAdminDisabled or SidAdminDisabled from the ForestTrustDomainStatus information enumeration. The following code snippet demonstrates how to set the status value to NetBiosNameAdminDisabled:

Example 32. Disabling a domain NetBIOS name in a forest trust relationship

string targetForestName = "adatum.com";
string targetDomainName = "corp.adatum.com";

// Bind to the current forest
Forest sourceForest = Forest.GetCurrentForest();

//get the trust relationship
ForestTrustRelationshipInformation forestTrust =
    sourceForest.GetTrustRelationship(targetForestName);

foreach (ForestTrustDomainInformation domainInfo in
    forestTrust.TrustedDomainInformation)
{
    if (domainInfo.DnsName == targetDomainName)
    {
        domainInfo.Status = 
                ForestTrustDomainStatus.NetBiosNameAdminDisabled;
        Console.WriteLine("\nNetBIOS Domain Name routing for {0}\n" +
                                                "is now set to {1}",
                                                targetDomainName, 
                                                domainInfo.Status);
    }
}

forestTrust.Save();

Assuming that the current forest containing resource domains is fabrikam.com, and the NetBIOS name for corp.adatum.com was the third name in the list of namesuffixes, the code sample (Example 32) performs the same action as the following NetDOM command:

netdom trust fabrikam.com /namesuffixes:adatum.com /togglesuffix:3

Repairing a Forest Trust

To repair a trust between two forests, you call the RepairTrustRelationship of a source forest object and pass it the target forest. To accomplish a repair, you'll have to bind to both forests in the relationship. I've shown many examples of binding to forests and calling methods. The code download with this article includes a method called RepairTrust that shows how to call a repair operation.

Removing a Cross Forest Trust

To remove a forest trust relationship, you can bind to both forests to remove both sides of the relationship. After completing the binding operation, you call the DeleteTrustRelationship method from one forest and pass the method the other forest object. If you can only bind to one of the forests, you can call the DeleteLocalSideOfTrustRelationship method instead. This method requires the name of the remote forest. The code download includes an example of how to remove both sides of a forest relationship.

Removing a Cross Domain Trust

The code for removing a domain trust relationship is almost identical to the code for removing a forest trust except that you bind to domains rather than forests. There is also an equivalent DeleteLocalSideOfTrustRelationship method for removing just one side of a domain trust. The code download includes an example of how to remove both sides of a domain relationship.

References

This is a short list of references that I've found valuable outside of the references that appear in this whitepaper:

Conclusion

At this point, you should have a good feel for how to use S.DS.AD to manage ADAM and Active Directory. Obviously there is a lot more you can do with this namespace that I wasn't able to cover here. However, with this introduction and the associated code download, you should be well on your way to leveraging the power of this namespace.

About the author

Ethan Wilansky is a contributing editor for Windows IT Pro, an enterprise architect for EDS in its Innovation Engineering practice, and a Microsoft MVP. He has authored or coauthored more than a dozen books for Microsoft and more than 70 articles.