Using the KeywordQuery Class to Get a List of Departments

On a recent side project of mine I was writing some SharePoint UserProfile heavy code in order to offer different schemes of showing user data. One of the common methods that I was using was collection building of arbitrary pieces of related user and business structure data, such as building lists of relevant departments assimilated by different data display. While there exist several mechanisms in order to support such a generic call, I have found the Microsoft.Office.Server.Search.Query.KeywordQuery class to be most effective and from a programmatic standpoint exceptionally easy to use (and it’s not just because I think the EnableUrlSmashing property has a nifty name). At a very high level, the KeywordQuery class simply provides the mechanisms in order to execute keyword syntax queries in the same manner as MOSS, returning a DataTable object.

The SDK IMHO adequately covers the innards of the particular classes, so let’s just get down to the code :)

[csharp]

public static DataTable RetrieveDepartmentNames(string url, int rowLimit, string department)
{
using (var table = new DataTable())
{
using (var site = new SPSite(url))
{
using (var query = new KeywordQuery(site))
{
query.ResultTypes = ResultType.RelevantResults;
query.EnableStemming = true;
query.TrimDuplicates = true;
query.StartRow = 0;
query.RowLimit = rowLimit;
query.QueryText = string.Format(“scope:\”{0}\””, “people”);
query.SortList.Add(“Rank”, SortDirection.Ascending);
query.SelectProperties.Add(“Department”);
using (ResultTable reader = query.Execute()[ResultType.RelevantResults])
{
table.Load(reader, LoadOption.OverwriteChanges);
}
if (((table.Rows != null)) && (table.Rows.Count > 0))
{
using (var view = new DataView(table) {Sort = department, RowFilter = (string.Format(“NOT Isnull({0},”) = ” AND NOT {0} = ””, department))})
{
return view.ToTable(“Department”, true, new[] { department });
}
}
}
}
}
return null;
}

[/csharp]

Important to note is that DataTable objects implement IDisposable since it inherits from MarshalByRefComponent, and even while most consider it unnecessary it is proper to place the references in using statements!

Share

The Active Directory Membership Provider and SharePoint

It is relatively common with a corporate environment to use Active Directory as the method of network user management, for authentication to a variety of applications, one of which is typically SharePoint. With the new version of SharePoint, there is the ActiveDirectoryMembershipProvider, which provides all the features that are exploited through the use of custom as well as the standard SQL provider that is shipped with Microsoft Office SharePoint Server (MOSS). While using the Active Directory membership provider, one can use either the full version of Active Directory or the lighter product Active Directory Application Mode (ADAM), and although the provider is quite similar to the default SQL provider that is shipped with MOSS, it clearly has some distinctions because of the way that it must interact with the directory server. It is important to keep in mind that although AD and ADAM appear to provide the same functionality, ADAM is a much lighter version of AD, and an architect must plan accordingly for the extra features that Active Directory will introduce into an environment. If you domain environment is rather complex, and a large forest with a large amount of trees, it is best to use a test environment when firstly setting up the ActiveDirectoryMembershipProvider, paying particular study and analysis to the domain that you plan on leveraging the provider in before implementing and binding it to your SharePoint environment.

LDAP and The Active Directory Membership Provider

The provider AD provider is also called the LDAP (Lightweight Directory Access Protocol provider because the AD/ADAM provider will in essence communicate with the directory using LDAP commands, this is how the provider is structured. It is quite different from the SQL membership provider in this because it will never return a tangible login token, the provider simply builds the structure by which SharePoint can make LDAP calls to the directory server and marshal back those returns back to SharePoint, there is never any security context that is bound to the actual thread.

Directory Containers

Operationally, the provider works with the directory containers. When examining the Active Directory provider, there is a separation yet common architectural consideration between AD and ADAM at this point since the two technologies are dissimilar, yet alike, in regards to extendibility and manageability. When using the provider against a full blown AD environment, it is important to realize that the provider will point to a singular domain, and within ADAM, the provider needs to be pointed at a single application partition, which is essentially a separate domain. It is however, possible to use the provider within an environment that has multiple domains, however the provider must be instantiated however many domains are going to be bound to the AD provider, i.e. if you have 4 domains you are going to have 4 provider instances.

Global Catalog and Connection Strings

Although the Active Directory provider may seem like a new way to interact with Active Directory at a variety of levels, there are some consideration in terms of what can be read and written when using the provider. Active Directory at its heart use the global catalog to manage all the queries that are done against it, particularly when there is a large domain forest with multiple domains that exist under it, it is the core portion that lets an administrator manage various types of user objects. There are however several assets that exist in the global catalog that are considered read only, and therefore are not good for the provider to interact with, such as modifying user information from the provider when prefixing the connection string with the global catalog conventions. Similar to how the SQL membership provider will work, so does the Active Directory membership provider, in that it also uses the concept of connection strings to know where it should point to. The provider connection strings are quite similar in structure as well:

[xml]

< connectionStrings >

< add name=adconnection connectionString=LDAP://SharePointDomain.dns.name/ >

< /connectionStrings >

[/xml]

The exact connection string will vary however between Active Directory and ADAM, for example, you might find this connection string for a full AD environment:

LDAP://sharepointdc.mysharepoint.com/OU=SharePointOU,DC=mySharePoint,DC=com

Caveat Of Using ADAM

However when using ADAM, since it must be instead have the specific container information for it, it will looks slightly different. This differs from Active Directory because the provider when working with SharePoint can instead just be pointed to the at the domain, in which case the AD provider will use the user containers since it is the most common within an enterprise (this is the default option). This is however configurable, and the exact container that will be used can be set within the configuration in the connection string (similar to pointing the container for ADAM). If you require a more extendable option, one in which there are several containers, it is best that the containers are structured in the a hierarchal environment, in which there are parent containers with children ones. For most operations that the provider will use, this will prove to be an adequate architecture since it will search through the children containers when performing queries. This although may result in a complex container architecture, but is a powerful option to leverage if you must leverage several containers to use with the provider, and typically much easier to set up than a more complex multiple instance situation with the provider. It is however important to realize that certain actions within this type of environment are not legal, and will fail unless you do setup those separate instances. If the users aren’t pre-existing, and you require operations for deleting existing users, this is a poor architecture to use, since these operations will not be successful because these operations cannot transverse through several levels. All users that are created or deleted must exist at a specific location in order for user creation and deletion to be successful. Nesting containers, regardless of methods lost when integrating containers that exist outside the one specifically named, is still a powerful mechanism for the SharePoint architect to use. The methods that are being lost typically will fail because as these types of methods are executed against the directory server, it requires that the user object firstly be acquired, in which case a larger search scope cannot be applied in order to gain access to the user object. It is important to realize the child and parent relationship as well, the search query will allows being at the parent and then crawl down into the child OU’s, unless you explicitly name the child container in the connection string, however this would negate the idea of nested containers since there would be no down crawl into the children OU’s.

With regards to connection strings, they will also vary heavily with ADAM since we are pointing a specific container, as opposed to an entire domain that will be default use the User container, this looks like the below:

LDAP://sharepointadam.mysharepoint.com/OU=OU,O=organization,DC=mysharepoint,DC=com

Security Attributes When Using the Active Directory Membership Provider

There are as well some security attributes that are associated when connecting using the Active Directory provider to whatever SharePoint AD or ADAM instance you are leveraging. This is a very simple attribute, connectionProtection, and luckily, it isn’t very complicated to implement because it is a Boolean value, either you want it on or you don’t. This attribute just provides a method by which there can be a secure connection that exists between your directory server and the Active Directory membership provider, and by default it will take the attribute value of Secure, as opposed to None. It is a best practice to use Secure in all environment besides development environments where production level data wouldn’t risk being exposed. In a production environment it is poor practice to leave the connectionProtection attribute set to None since various operations require that a secure connection be established, however for ADAM sometimes it will use the None attribute because it requires setting up SSL certificates. The last attribute that deserves attention is it is also possible for one to add the username and password that they would like to use when connecting to the directory server, otherwise it will connect through the credentials that are bound to the worker process.

Share

Building A String Array Of Local User Names

At some point, you may want to build a string array that contains all your relevant machine local user account names. I came up against this problem this evening while I was working on some code for the MSPress OBA book. I thought it might save someone else some time if I demonstrated the solution.

I needed to implement this because it is helpful to use this type of approach when you are offering your users a customized interaction with Windows Services setup which can harvest the available account names on the machine, and allow the user something like a WinForm drop-down control of all the available accounts that are local. There are, of course, properties that you can build within the relevant Windows Services classes to put these types of credentials into the Windows Services without any method of user interaction. To be honest, I find that when developing something that a client might at a later date move from machine to machine in disparate environments, it is best to build the simplest interfaces possible in order for them to hit the ground running.

It is best to put this type of functionality in an internal sealed class, so that the class cannot be inherited (can’t be used as a base class throughout other sections of the application).

Let’s get the easy stuff over first. When we are working with something like the machine usernames, it is helpful to have a formatting method already out there and ready to go, just a small helper method. This is obviously just going to take the name that needs to be formatted as a parameter to the method, and is just going to use a simple if loop to test whether the username does actually require formatting.

[csharp]

internal static string FormatUserName(string name)
{
if (name.IndexOf(@”\”) >= 0)
{
return name;
}
return string.Format(@”{0}\{1}”, Environment.MachineName, name);
}

[/csharp]

This is really nothing that is exceptionally fancy :-), however it does get the job done, and can be called later if, and when it is necessary to format returned user names appropriately.

Moving along, within our class, there are some DLLimports that have to be done in order to invoke some functionality. The DLLimport is going to allow us to call the relevant unmanaged code that is neccesary.

The primary assembly we require a reference to that contains the relevant unmanaged code is netapi32.dll, also known as the Microsoft Net API Library, is responsible for processing networking objects for Microsoft LAN Manager.

[csharp]

[DllImport(“netapi32.dll”)]
internal static extern uint NetUserEnum(IntPtr serverName, uint level, uint filter, ref IntPtr buffer, uint prefMaxLen, ref uint entriesRead, ref uint totalEntries, IntPtr resumeHandle);

[/csharp]

If you would like, you can decorate this call much more throughly as well, if you have certain marshaling code standards that require adherence to.
[csharp]

[DllImportAttribute(“netapi32.dll”, EntryPoint = “NetUserEnum”, CharSet = CharSet.None, ExactSpelling = false, SetLastError = false, PreserveSig = true, CallingConvention = CallingConvention.Winapi, BestFitMapping = false, ThrowOnUnmappableChar = false)]
[PreserveSigAttribute()]
internal static extern uint NetUserEnum (IntPtr serverName, uint level, uint filter, ref IntPtr buffer, uint prefMaxLen, ref uint entriesRead, ref uint totalEntries, IntPtr resumeHandle);

[/csharp]

We are also going to use this assembly again, in order to test the state of the buffer.
[csharp]

[DllImport(“netapi32.dll”)]
internal static extern void NetApiBufferFree(IntPtr bufptr);

[/csharp]

As before, you can decorate this call much more exhaustively as well.

[csharp]

[DllImportAttribute(“netapi32.dll”, EntryPoint = “NetApiBufferFree”, CharSet = CharSet.None, ExactSpelling = false, SetLastError = false, PreserveSig = true, CallingConvention = CallingConvention.Winapi, BestFitMapping = false, ThrowOnUnmappableChar = false)]
[PreserveSigAttribute()]
internal static extern void NetApiBufferFree (IntPtr bufptr);
[/csharp]

Lastly, there have to be a small struct, a nested type for the username.
[csharp]

[System.Runtime.InteropServices.StructLayout(LayoutKind.Sequential, Pack = 8, CharSet = CharSet.Auto)]
internal struct USER_INFO_0

{
public IntPtr lpszUserName;
}
[/csharp]

Lastly now, you can actually harvest the local user names, which was the subject of this post in the first place.

[csharp]

internal static string[] LocalUserNames
{
get
{
USER_INFO_0_SIZE = sizeof(USER_INFO_0);
IntPtr buffer = IntPtr.Zero;
uint level = 0;
uint filter = 0;
uint entriesRead = 0;
uint totalEntries = 0;
uint prefMaxLen = uint.MaxValue;
if (NetUserEnum(IntPtr.Zero, level, filter, ref buffer, prefMaxLen, ref entriesRead, ref totalEntries, IntPtr.Zero) != 0)
{
return new string[0];
}
string[] textArray = new string[totalEntries];
for (int i = 0; i < totalEntries; i++) { int cCount = buffer.ToInt32() + (USER_INFO_0_SIZE * i); USER_INFO_0 user_info_ = (USER_INFO_0) Marshal.PtrToStructure(new IntPtr(cCount), typeof(USER_INFO_0)); textArray[i] = Marshal.PtrToStringAuto(user_info_.lpszUserName); } NetApiBufferFree(buffer); return textArray; } } [/csharp] And you're done. Now you can make your Windows Service installers not suck so much, and a lot more intuitive to users than just guessing what account they should running under, or assuming the installing user knows how to use the services MMC snap-in.

Share