Saturday, July 03, 2010 2:45 PM by roger

In my last post I promised to tell you more about the performance counters. So here we are: today I'm going to tell you about how to use the performance counter infrastructure offered by Windows in a .net application. On top of this I'm providing you with a basic framework that you can use to author performance counters through XML and then get the code to read/write the counters generated for you at build time. So let's get started.

Some Basics

Before I go off to the code, here's a little overview on performance counters in Windows. I'm sure if you're reading this, you already know about Perfmon.exe, a nice little tool to look at various performance aspects of Windows machines. When plottig performance counters, you're typically adding the counters you're interested in. These counters are grouped in categories, for instance Processor or PhsyicalDisk. Each category can contain multiple counters, for instance Disk Read Bytes/sec or Disk Write Bytes/sec. And finally, each counter can have multiple instances, for instance in multi-processor machines, you'll find one instance of the % Idle Time counter per processor.

What tools like Perfmon.exe do is to grab the values of the counters you selected every second (by default, but that can typically be changed) and record/plot the values. Your job here is to build performance counters that measure certain aspects of your application so these tools can help you in analyzing your performance. And that's why you're here, right?

Step 1: The XML Schema

In order to know what we talk about in the XML I announced, let me start with the XML schema. I use this mainly to make sure that what I have in the XML declaring the perf counters makes some sense and is legal input. And of course because Visual Studio tells you that something's wrong when the schema is not adhered to.

 1: <?xml version="1.0" encoding="utf-8"?>
 2: <xs:schema targetNamespace="urn:Cymbeline.Diagnostics.PerfCounters"
 3:            xmlns="urn:Cymbeline.Diagnostics.PerfCounters"
 4:            xmlns:tns="urn:Cymbeline.Diagnostics.PerfCounters"
 5:            xmlns:xs="http://www.w3.org/2001/XMLSchema"
 6:            attributeFormDefault="unqualified"
 7:            elementFormDefault="qualified">
 8:   <xs:element name="PerfCounters">
 9:     <xs:complexType>
10:       <xs:sequence>
11:         <xs:element name="Category" maxOccurs="unbounded">
12:           <xs:complexType>
13:             <xs:sequence>
14:               <xs:element name="Counter" maxOccurs="unbounded">
15:                 <xs:complexType>
16:                   <xs:attribute name="Name" type="xs:string" use="required" />
17:                   <xs:attribute name="Symbol" type="tns:Symbol" use="required" />
18:                   <xs:attribute name="Type" use="required">
19:                     <xs:simpleType>
20:                       <xs:restriction base="xs:string">
21:                         <xs:enumeration value="NumberOfItems32" />
22:                         <xs:enumeration value="RateOfCountsPerSecond32" />
23:                         <xs:enumeration value="RawFraction" />
24:                       </xs:restriction>
25:                     </xs:simpleType>
26:                   </xs:attribute>
27:                   <xs:attribute name="Help" type="xs:string" use="required" />
28:                 </xs:complexType>
29:               </xs:element>
30:             </xs:sequence>
31:             <xs:attribute name="Name" type="xs:string" use="required" />
32:             <xs:attribute name="Symbol" type="tns:Symbol" use="required" />
33:             <xs:attribute name="Help" type="xs:string" use="required" />
34:           </xs:complexType>
35:         </xs:element>
36:       </xs:sequence>
37:     </xs:complexType>
38:   </xs:element>
39:   <xs:simpleType name="Symbol">
40:     <xs:restriction base="xs:token">
41:       <xs:pattern value="[a-zA-Z_][\w_]*" />
42:     </xs:restriction>
43:   </xs:simpleType>
44: </xs:schema>

On lines 21 to 23 you find some enumeration values. These map to the values defined in the PerformanceCounterType Enumeration. This is also where you can add support for more performance counter types when you need it. The other elements and attributes are used to describe the category for the performance counters and the performance counters themselves, including the help text that will show up in Perfmon.exe and also including the symbol that you can use in the code to access the counter.

Step 2: Declaring the Performance Counters

Now let's use the schema we built above. To give you an idea of the context, I'm providing here some of the XML I use to build the perf counters for my SMTP server.

 1: <?xml version="1.0" encoding="utf-8" ?>
 2: <PerfCounters xmlns="urn:Cymbeline.Diagnostics.PerfCounters">
 3:   <Category Name="CymbeMail" Symbol="CymbeMail" Help="CymbeMail SMTP Server v1">
 4:     <Counter Name="# Total Connections"
 5:          Symbol="TotalConnections"
 6:          Type="NumberOfItems32"
 7:          Help="The total number of connections since the server was started." />
 8:     <Counter Name="# Total Connections Refused"
 9:          Symbol="TotalRefusedConnections"
10:          Type="NumberOfItems32"
11:          Help="The total number of connections refused since the server was started." />
12:     <Counter Name="# Total Connections with Errors"
13:          Symbol="TotalErroneousConnections"
14:          Type="NumberOfItems32"
15:          Help="The total number of connections which reported errors since the server was started." />
16:     <Counter Name="# Active Connections"
17:          Symbol="ActiveConnections"
18:          Type="NumberOfItems32"
19:          Help="The number of currently active connections." />
20:     <Counter Name="# Authorization Records"
21:          Symbol="AuthorizationRecords"
22:          Type="NumberOfItems32"
23:          Help="The number of authorization records currently kept in the server." />
24:     <Counter Name="# Connections/sec"
25:          Symbol="ConnectionsPerSec"
26:          Type="RateOfCountsPerSecond32"
27:          Help="The number of connections per second." />
28:     <Counter Name="# Refused Connections/sec"
29:          Symbol="RefusedConnectionsPerSec"
30:          Type="RateOfCountsPerSecond32"
31:          Help="The number of connections refused per second." />
32:   </Category>
33: </PerfCounters>

As you can see, I create seven perf counters, most of them ordinary counters that count the number of occurrences of a certain event (like when a client makes a connection to the server). I actually also use the NumberOfItems32 type to count the number of active connections — I basically increment the counter when a connection was established and decrement it when the connection is terminated. And I have a couple of counters which count the number of occurrences per second (the RateOfCountsPerSecond32 counter type). The good thing about these counters is that you don't need to provide your own counter base (check out MSDN for more info on this, starting with the above mentioned PerformanceCounterType Enumeration).

Step 3: Building the XSLT to Generate Code

We already have our perf counters declared, so let's generate some code from that XML. Using XSL stylesheets makes this very easy: We'll use the XML as input and get c# code as output that we can simply include into our project. I won't show the full XSLT here (it's about 240 lines).

  1: <?xml version="1.0" encoding="utf-8"?>
  2: <xsl:stylesheet version="1.0"
  3:         xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  4:         xmlns:msxsl="urn:schemas-microsoft-com:xslt"
  5:         xmlns:tns="urn:Cymbeline.Diagnostics.PerfCounters"
  6:         exclude-result-prefixes="msxsl">
  7:   <xsl:output method="text" indent="no"/>
  8:  
  9:   <xsl:param name="TargetNamespace" />
 10:   <xsl:param name="TargetClassName" select="'PerfCounters'" />
 11:   <xsl:param name="AccessModifier" select="'public'" />
 12:  
 13:   <xsl:template match="tns:PerfCounters">
 14:     <xsl:if test="$TargetNamespace=''">
 15:       <xsl:message terminate="yes">
 16:         The Parameter 'TargetNamespace' is undefined.
 17:       </xsl:message>
 18:     </xsl:if>
 19:     <!-- ... -->
 20:   </xsl:template>
 21:  
 22:   <xsl:template match="tns:Counter" mode="Counter">
 23:     <xsl:text>
 24:       private static PerformanceCounter _</xsl:text><xsl:value-of select="@Symbol"/><xsl:text>;
 25:  
 26:       </xsl:text><xsl:value-of select="$AccessModifier"/><xsl:text> static PerformanceCounter </xsl:text><xsl:value-of select="@Symbol"/><xsl:text>
 27:       {
 28:         get
 29:         {
 30:           MakeSureCountersAreInitialized();
 31:           return _</xsl:text><xsl:value-of select="@Symbol"/><xsl:text>;
 32:         }
 33:       }
 34: </xsl:text>
 35:   </xsl:template>
 36:  
 37:   <xsl:template match="tns:Counter[@Type='RawFraction']" mode="Counter">
 38:     <xsl:text>
 39:       private static PerformanceCounter _</xsl:text><xsl:value-of select="@Symbol"/><xsl:text>;
 40:  
 41:       </xsl:text><xsl:value-of select="$AccessModifier"/><xsl:text> static PerformanceCounter </xsl:text><xsl:value-of select="@Symbol"/><xsl:text>
 42:       {
 43:         get
 44:         {
 45:           MakeSureCountersAreInitialized();
 46:           return _</xsl:text><xsl:value-of select="@Symbol"/><xsl:text>;
 47:         }
 48:       }
 49:  
 50:       private static PerformanceCounter _</xsl:text><xsl:value-of select="@Symbol"/><xsl:text>Base;
 51:  
 52:       </xsl:text><xsl:value-of select="$AccessModifier"/><xsl:text> static PerformanceCounter </xsl:text><xsl:value-of select="@Symbol"/><xsl:text>Base
 53:       {
 54:         get
 55:         {
 56:           MakeSureCountersAreInitialized();
 57:           return _</xsl:text><xsl:value-of select="@Symbol"/><xsl:text>Base;
 58:         }
 59:       }
 60: </xsl:text>
 61:   </xsl:template>
 62:  
 63:   <xsl:template match="tns:Counter" mode="InitCounter">
 64:     <xsl:text>
 65:         _</xsl:text><xsl:value-of select="@Symbol"/><xsl:text> = new PerformanceCounter(
 66:           CategoryName,
 67:           "</xsl:text><xsl:value-of select="@Name"/><xsl:text>",
 68:           false);
 69: </xsl:text>
 70:   </xsl:template>
 71:  
 72:   <xsl:template match="tns:Counter[@Type='RawFraction']" mode="InitCounter">
 73:     <xsl:text>
 74:         _</xsl:text><xsl:value-of select="@Symbol"/><xsl:text> = new PerformanceCounter(
 75:           CategoryName,
 76:           "</xsl:text><xsl:value-of select="@Name"/><xsl:text>",
 77:           false);
 78:  
 79:         _</xsl:text><xsl:value-of select="@Symbol"/><xsl:text>Base = new PerformanceCounter(
 80:           CategoryName,
 81:           "</xsl:text><xsl:value-of select="@Name"/><xsl:text> Base",
 82:           false);
 83: </xsl:text>
 84:   </xsl:template>
 85:  
 86:   <xsl:template match="tns:Counter" mode="CreateCounter">
 87:       <xsl:text>
 88:       {
 89:         CounterCreationData counter = new CounterCreationData();
 90:  
 91:         counter.CounterName = "</xsl:text><xsl:value-of select="@Name" /><xsl:text>";
 92:         counter.CounterHelp = "</xsl:text><xsl:value-of select="@Help" /><xsl:text>";
 93:         counter.CounterType = PerformanceCounterType.</xsl:text><xsl:value-of select="@Type" /><xsl:text>;
 94:  
 95:         counters.Add(counter);
 96:       }
 97: </xsl:text>
 98:   </xsl:template>
 99:  
100:   <xsl:template match="tns:Counter[@Type='RawFraction']" mode="CreateCounter">
101:       <xsl:text>
102:       {
103:         CounterCreationData counter = new CounterCreationData();
104:  
105:         counter.CounterName = "</xsl:text><xsl:value-of select="@Name" /><xsl:text>";
106:         counter.CounterHelp = "</xsl:text><xsl:value-of select="@Help" /><xsl:text>";
107:         counter.CounterType = PerformanceCounterType.</xsl:text><xsl:value-of select="@Type" /><xsl:text>;
108:  
109:         CounterCreationData counterBase = new CounterCreationData();
110:  
111:         counterBase.CounterName = "</xsl:text><xsl:value-of select="@Name" /><xsl:text> Base";
112:         counterBase.CounterHelp = "</xsl:text><xsl:value-of select="@Help" /><xsl:text>";
113:         counterBase.CounterType = PerformanceCounterType.RawBase;
114:  
115:         counters.AddRange(new CounterCreationData[]{counter, counterBase});
116:       }
117: </xsl:text>
118:   </xsl:template>
119:  
120:   <!-- ... -->
121: </xsl:stylesheet>

On lines 9 to 11 you'll find the declaration for the parameters which you can use to change the namespace, the class name and the access modifiers (public vs internal really) for the generated classes.

Starting on lines 22 and 37 respectively, you find different templates for general counters and the RawFraction type counters. I use this to automatically generate the base counter so you won't have to worry about it. You can do similar things for other counters which need a base counter. Then, starting on lines 63 and 72 respecctively, I initialize the counters, with specialization again for the RawFraction counter types. And starting on lines 86 and 100 respectively you'll find the code to create and set the CounterCreationData which is used to register perf counters.

You can find the full code in the ZIP file linked to at the end of this post. With this we basically have all the tools we need to actually generate the code as part of the build. We just need to bring the pieces together.

Step 4: Bringing it Together aka Updating the Project

Well yes, I have silently assumed that you have a c# project (.csproj or any other project that is supported by MSBuild.exe) that you're working on to update. All we need to do in the .csproj is to add the files we authored, then run the XSLT transformation and add the output .cs file to the list of source code files as well. And here's how you can do that.

 1: <?xml version="1.0" encoding="utf-8"?>
 2: <Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Build">
 3:   <!-- ... -->
 4:   <ItemGroup>
 5:     <PerfCounterXml Include="Diag\PerfCounters.xml">
 6:       <OutputCs>Diag\PerfCounters.Generated.cs</OutputCs>
 7:       <Parameters>
 8:         <Parameter Name="TargetNamespace" Value="$(RootNamespace).Diag" />
 9:       </Parameters>
10:       <SubType>Designer</SubType>
11:     </PerfCounterXml>
12:     <PerfCounterXslt Include="Diag\PerfCounters.xslt" />
13:     <Compile Include="Diag\PerfCounters.Generated.cs">
14:       <PerfCounters>true</PerfCounters>
15:       <AutoGen>true</AutoGen>
16:       <DependentUpon>PerfCounters.xml</DependentUpon>
17:     </Compile>
18:   </ItemGroup>
19:   <ItemGroup>
20:     <None Include="Diag\PerfCounters.xsd">
21:       <SubType>Designer</SubType>
22:     </None>
23:   </ItemGroup>
24:   <!-- XSL Transform for Perf Counters -->
25:   <Target Name="GeneratePerfCounters" BeforeTargets="BeforeBuild"
26:       Inputs="@(PerfCounterXml);@(PerfCounterXslt)"
27:       Outputs="@(PerfCounterXml->'%(OutputCs)')">
28:     <XslTransformation XmlInputPaths="@(PerfCounterXml)"
29:       XslInputPath="@(PerfCounterXslt)"
30:       OutputPaths="@(PerfCounterXml->'%(OutputCs)')"
31:       Parameters="@(PerfCounterXml->'%(Parameters)')" />
32:   </Target>
33: </Project>

Add lines 4 to 32 to the end of the project file and make sure the paths you're using point to the places where you actually stored the files. On line 8 you can see that I'm passing the target namespace parameter (called out in step 3) by concatenating the assemblies default root namespace and ".Diag". This way you can actually put this stuff into a .targets file that you can include in all of the projects which take advantage of this framework.

Starting on line 25 I defined the actual target which runs the XslTransformation task that comes with .net 4.0. I also declared the inputs as being the XML file and the XSLT file — this way, while experimenting with the XSLT, the code gets regenerated also if the XML file hasn't changed, but the XSLT has.

Using the Generated Classes

So now you have pretty much all the code you need to use these performance counters of yours. All that's left is actually writing the code which

  1. Installs the Performance Counters
  2. Updates the Performance Counters
  3. Uninstalls the Performance Counters

#1 and #3 would probably be used by your installer and #2 by your application at runtime. Incrementing and decrementing the counters is as easy as

1: PerfCounters.CymbeMail.ActiveConnections.Increment();
2: //...
3: PerfCounters.CymbeMail.ActiveConnections.Decrement();

Where PerfCounters is the name of the generated class which contains all the generated perf counters, CymbeMail is the name of the class for the category (the Symbol attribute, remember?) and ActiveConnections is of course the symbol name for the counter we're modifying. Isn't this simple?

As for setting things up (or removing them), the relevant piece of code gets generated for you, too. So all you need to do is actually call it, potentially from a small application that you run — as I mentioned before — from your installer.

1: // Setup the Performace Counters
2: PerfCounters.Setup();
3:  
4: // And for the Uninstaller:
5: // Remove the Performance Counters
6: PerfCounters.Remove();

Of course you'll have to make sure that the version of the generated code that's run by the uninstaller is the same version as the installer did run — else you may end up with stale categories / counters on the machine. But then again, if you're installing through an MSI package, you get that for free ... when done properly.

Summary

In just a few steps I have shown how you can build a framework to use performance counters in .Net applications. On top of it's simplicity, it's also quite easy to modify the framework and adapt it to your own needs or extend it to allow more performance counter types.

And finally, as promised, here's the ZIP file which contains the relevant pieces. Please forgive me for not adding the .csproj file — that actually contains other relevant data that I didn't want to share. Instead I added the generated c# code file for reference.

PerfCounters.zip (4.08 kb)

Monday, March 29, 2010 8:43 PM by roger

This is surely no surprise for many people: you can actually debug managed code with WinDbg (or CDB, if you prefer that one) by using the SOS extension (that ships with the .Net framework) in the debugger. What's new is that today, Microsoft released Psscor2 which is pretty much an enhanced version of SOS. Now I'm sure that you'll soon find much information on using Psscor2 on Tom's blog. I'm much in favor of Psscor2 myself and therefore I'll try to post useful scripts here as well (if I can come up with some ;-)).

Sunday, October 11, 2009 1:36 PM by roger

Let's assume you have a web site which is exposed to the internet (i.e. to the public) but the site itself works with windows accounts internally. If you have problems visualizing this scenario, think about a web mail interface for a mail server system which offers mail services for user accounts in active directory. Users inside your corporate network can use integrated Windows authentication to access this site, so they don't really have a problem. But when they want to access the interface from outside the corporate network, or from a machine/device which doesn't understand integrated Windows authentication, you'll realize that it doesn't just work like that.

So you'll want to do authentication based on the credentials the user enters in a simple form on a web page. The challenge now is that the rest of the site (i.e. apart from the one form which does the initial authentication) will likely still work with the HttpContext.Current.User property to determine which user is currently authenticated and provide actions and data based on that identity, because you don't want to re-implement the entire logic. The good news is, you can do that! The bad news is, you don't just get it for free. But that's why you came here, and that's why I'll try to help you with this.

Let's first arrange a few things on the web site facing the public — you don't necessarily need to change this on your internal site. First, the login page will need to be accessible for anyone, i.e. anonymous access must be turned on on the site. Second, from ASP.net's point of view, all the pages which require the user to be authenticated should be in/under the same directory. If you don't want to do this, you'll simply have to add a <location> element for all pages requiring authentication in your web.config or do the opposite and add all pages which don't require authentication in the same way. With the directories however, the basic structure can be as simple as WebSiteRoot/
+--Default.aspx        Could redirect to /ActualSite/Default.aspx
+--ActualSite/
   +--Default.aspx     The actual home page with all its logic
+--Auth/
   +--Login.aspx       The login page
So all the logic you had in the site's root directly would be under ActualSite or whatever name you chose.

Updating Web.config

Now for the above scenario with different directories, the web.config in the site's root could look as follows. Please note that for the pages under ActualSite we are simply disabling anonymous access, while for everything else, we allow it. Also, in the authentication element we're setting the mode to None because we're not going to use any predefined authentication mechanism as is.

 1: <configuration>
 2:     <location path="ActualSite">
 3:         <system.web>
 4:             <authorization>
 5:                 <deny users="?"/>
 6:             </authorization>
 7:         </system.web>
 8:     </location>
 9:  
10:     <system.web>
11:         <authentication mode="None">
12:             <forms loginUrl="~/Auth/Login.aspx" defaultUrl="~/ActualSite/Default.aspx" />
13:         </authentication>
14:  
15:         <authorization>
16:             <allow users="*" />
17:         </authorization>
18:     </system.web>
19: </configuration>
But of course that's not all yet. You'll see that if now you wanted to look at the actual site, you'll get a 401 because you're not authenticated. So let's take a look at that.

Validating credentials

Next, let's create the login page which the user will use to enter his credentials. Fortunately, ASP.net offers the Login control which does almost everything we need. So add that one to your login page plus add an event handler for the OnAuthenticate event of that control. Alternatively, you can also derive your own control from the Login control and override the OnAuthenticate method. This event handler is where we'll do our custom logic to check that the credentials really map to an existing Windows user. Below's the code which does that.

 1: protected override void OnAuthenticate(AuthenticateEventArgs args)
 2: {
 3:     string[] parts = UserName.Split('\\');
 4:     string password = Password;
 5:     string username;
 6:     string domain = null;
 7:  
 8:     args.Authenticated = false;
 9:  
10:     if (parts.Length == 1)
11:     {
12:         username = parts[0];
13:     }
14:     else if (parts.Length == 2)
15:     {
16:         domain = parts[0];
17:         username = parts[1];
18:     }
19:     else
20:     {
21:         return;
22:     }
23:  
24:     if (WebAuthenticationModule.AuthenticateUser(username, domain, password))
25:     {
26:         string userData = String.Format("{0}\n{1}\n{2}",
27:             username, domain, password);
28:  
29:         FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
30:             2                           /* version */,
31:             username,
32:             DateTime.Now                /* issueDate */,
33:             DateTime.Now.AddMinutes(30) /* expiration */,
34:             true                        /* isPersistent */,
35:             userData,
36:             FormsAuthentication.FormsCookiePath);
37:  
38:         HttpCookie ticketCookie = new HttpCookie(
39:             FormsAuthentication.FormsCookieName,
40:             FormsAuthentication.Encrypt(ticket));
41:  
42:         Context.Response.Cookies.Add(ticketCookie);
43:  
44:         Context.Response.Redirect(
45:             FormsAuthentication.GetRedirectUrl(username, false), false);
46:     }
47: }
The AuthenticateUser method from WebAuthenticationModule is a wrapper on the LogonUser function from the Win32 API which will return true if the user could be logged on. So if the credentials are valid, we're going to pass them in to the FormsAuthenticationTicket in the UserData property so later on, we'll be able to use them again. At least, we don't want the consumers of the site have to enter credentials for every request their making, right? Also, we're encrypting the entire ticket because we're going to send it over the wire. The Encrypt method from the FormsAuthentication class does this. However, you'll have to make sure that the protection attribute of the forms element in the web.config is set to All which actually is the default (but it can be inherited, so watch out!).

What you see is that we're heavily using the functionality offered by the FormsAuthentication class and related classes to handle the tickets, encryption, settings, etc. This is not mandatory but it helps a lot. Plus it's better anyway than coming with your own ticketing and encryption mechanisms; unless you have a degree in maths and/or cryptography, chances are that that's not so secure as you think it is.

Authenticating users

Then, we need to authenticate the user for all the requests he makes after providing the credentials. Thus, we need to add some custom logic to the AuthenticateRequest event of the HttpApplication. There's multiple ways to do that:

  • Add a file called global.asax to your site's root
  • Create and register a new HttpModule by implementing the System.Web.IHttpModule interface and adding an entry in the httpModules section in your root's web.config
Personally, I like the approach with the custom module more, but adding this stuff to the global.asax can be done a little bit faster. In either case, make sure you can handle the AuthenticateRequest event. My code proposal for the handler is given below. I'm intentionally omitting most error handling code here.
 1: private static void OnAuthenticateRequest(object sender, EventArgs args)
 2: {
 3:     HttpApplication application = sender as HttpApplication;
 4:  
 5:     HttpContext context = application.Context;
 6:     HttpRequest request = context.Request;
 7:     HttpResponse response = context.Response;
 8:  
 9:     if (!request.IsAuthenticated &&
10:         !context.SkipAuthorization)
11:     {
12:         if (request.CurrentExecutionFilePath.Equals(FormsAuthentication.LoginUrl,
13:                                                     StringComparison.OrdinalIgnoreCase) ||
14:             request.CurrentExecutionFilePath.EndsWith(".axd"))
15:         {
16:             context.SkipAuthorization = true;
17:         }
18:         else
19:         {
20:             HttpCookie cookie = request.Cookies.Get(FormsAuthentication.FormsCookieName);
21:             FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(cookie.Value);
22:  
23:             if (!ticket.Expired)
24:             {
25:                 IntPtr hToken = LogonUserFromTicket(ticket);
26:  
27:                 WindowsIdentity identity = new WindowsIdentity(
28:                     hToken, "Win/Forms", WindowsAccountType.Normal, true);
29:  
30:                 context.User = new WindowsPrincipal(identity);
31:  
32:                 if (FormsAuthentication.SlidingExpiration)
33:                 {
34:                     ticket = FormsAuthentication.RenewTicketIfOld(ticket);
35:                     cookie.Value = FormsAuthentication.Encrypt(ticket);
36:                     response.Cookies.Set(cookie);
37:                 }
38:  
39:                 return;
40:             }
41:  
42:             FormsAuthentication.RedirectToLoginPage();
43:         }
44:     }
45: }
So let's go through the important things here. First we check (lines 9/10) if the request is already authenticated or authorization is to be skipped completely. If either of those is true, we're out of the picture already. Else it's our job to do the authentication. Lines 12-17 are used to prevent authentication on the login page as well as the *.axd handlers, which are typically used to return resources like scripts for ASP.net components. Lines 20-42 do the actual authentication: we first retrieve the cookie from the previous credential validation and decrypt it to get the ticket. If the ticket has not expired, we log the user on (the call to LogonUserFromTicket is again only a wrapper for the LogonUser function from the Win32 API; it uses the data from the UserData property of the ticket) to get the logon token which we'll pass in to the WindowsIdentity constructor to get the WindowsIdentity object all the code in ActualSite will use to determine which user is making the request. Then of course we need to update the context with the new identity. If sliding expiration is turned on in the web.config, we renew the ticket. And if the ticket has expired, of course we don't authenticate the user but instead we redirect him to the login page.

Cleanup

Finally, we still have a tiny problem here. We have used the LogonUser function, but according to its documentation, we should call the CloseHandle function from the Win32 API. For the method AuthenticateUser I have already done that; there we don't need the token/handle anymore when we have verified the credentials. But what about the authentication we're doing in AuthenticateRequest? We're setting the newly created WindowsIdentity (which is here used to represent the user token) to the current HttpContext because we'll need it there to ultimately handle the request. But once the request handler is done, we don't need it anymore. Luckily, there's also an event for this purpose. It's the last event that there is and it's called EndRequest. So let's add the following code in the handler for that event.

 1: private static void OnEndRequest(object sender, EventArgs args)
 2: {
 3:     HttpApplication application = sender as HttpApplication;
 4:  
 5:     HttpContext context = application.Context;
 6:  
 7:     if (null != context.User)
 8:     {
 9:         WindowsIdentity identity = context.User.Identity as WindowsIdentity;
10:  
11:         if (null != identity)
12:         {
13:             NativeAuth.CloseHandle(identity.Token);
14:         }
15:     }
16: }
Basically all it does, if the request was given a WindowsIdentity, it'll call the CloseHandle function from the Win32 API on that identity's token handle. That should do the trick and we shouldn't leak handles anymore.

Summary

I have shown here how you can make use of the forms authentication mechanisms which come with ASP.net to do Windows authentication behind the scenes. This can be very useful in cases when not all users have access to systems which know how to do Windows authentication.

Please note that I do not claim that this solution is going to work for every challenge you may be facing. The solution shown here is neither claimed to be complete nor suitable for every web application.

Thursday, August 06, 2009 4:59 PM by roger

I recently had to migrate a common ASP.NET web service over to WCF, making sure that clients of the former would still be able to use the latter. There were a couple of things I stumbled across, so I am blogging about the minimal steps I had to perform to get clients of the old ASP.NET web service running with the new WCF one. Let's use the following simple ASP.NET web service for this tiny tutorial.

 1: [WebService(Namespace = "http://foo.bar.com/Service/Math")]
 2: public class MathAddService : WebService
 3: {
 4:     [WebMethod]
 5:     public int Add(int x, int y)
 6:     {
 7:         // Let's ignore overflows here ;-)
 8:         return x + y;
 9:     }
10: }

The first thing we need to do is create a new interface which offers the same methods as the web service did and mark it as a service contract. This is required because the WCF endpoints are contract based, i.e. they need such an interface. So we extract the public web service interface of the MathAddService class and decorate it with the WCF attributes:

1: [ServiceContract(Namespace = "http://foo.bar.com/Service/Math")]
2: [XmlSerializerFormat]
3: public interface IMathAddService
4: {
5:     [OperationContract(Action = "http://foo.bar.com/Service/Math/Add")]
6:     int Add(int x, int y);
7: }

The ServiceContract attribute tells WCF to use the same namespace for the web service as ASP.NET did. If you don't do this, your clients will not be able to use the migrated service because the namespaces don't match. The XmlSerializerFormat attribute is used to make sure that WCF uses the standard SOAP format for messages. If you don't specify this, your clients will likely see strange error messages of mismatching operations / messages. Then, for each method you exposed in the former web service, you need to add the exact same signature here, plus make sure that the OperationContract attribute for each method has the Action property set to <Namespace> '/' <MethodName>. Without this, you'll get another set of exceptions like 'operation not defined'.

Now the next step is to implement this interface in a class, but we basically already have this in the former MathAddService class. So we just adapt the class' definition as follows.

 1: [WebService(Namespace = "http://foo.bar.com/Service/Math")]
 2: [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
 3: [ServiceBehavior(Namespace = "http://foo.bar.com/Service/Math")]
 4: public class MathAddService : WebService, IMathAddService
 5: {
 6:     [WebMethod]
 7:     public int Add(int x, int y)
 8:     {
 9:         // Let's ignore overflows here ;-)
10:         return x + y;
11:     }
12: }

As you can see, we're also adding two new attributes. AspNetCompatibilityRequirements are used to make sure that the new WCF service is really capable of serving old clients. The ServiceBehavior attribute is used to make sure that the WCF hosted service really uses the correct namespace, i.e. the same as the old ASP.NET service used. By the way, you should find all the additional attributes in the System.ServiceModel and System.ServiceModel.Activation namespaces (from the System.ServiceModel assembly).

Now lets get to the configuration of endpoints and bindings for the web service. The following block shows you the new sections in the web.config file for the virtual directory which hosts the WCF service.

 1: <configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
 2:     <system.web>
 3:         <!-- ... -->
 4:     </system.web>
 5:     <system.serviceModel>
 6:         <serviceHostingEnvironment aspNetCompatibilityEnabled="true" />
 7:         <services>
 8:             <service name="MathAddService" behaviorConfiguration="MathAddServiceBehavior">
 9:                 <endpoint address=""
10:                           binding="basicHttpBinding"
11:                           bindingConfiguration="httpsIwa"
12:                           bindingNamespace="http://foo.bar.com/Service/Math"
13:                           contract="IMathAddService"/>
14:             </service>
15:         </services>
16:         <bindings>
17:             <basicHttpBinding>
18:                 <binding name="httpsIwa">
19:                     <security mode="Transport">
20:                         <transport clientCredentialType="Windows" />
21:                     </security>
22:                 </binding>
23:             </basicHttpBinding>
24:         </bindings>
25:         <behaviors>
26:             <serviceBehaviors>
27:                 <behavior name="MathAddServiceBehavior">
28:                     <serviceMetadata httpsGetEnabled="true" />
29:                     <serviceDebug httpsHelpPageEnabled="true" includeExceptionDetailInFaults="true" />
30:                 </behavior>
31:             </serviceBehaviors>
32:         </behaviors>
33:     </system.serviceModel>
34: </configuration>

As you can see on lines 19 and 20, we are using HTTPS and IWA for this particular binding, but you should of course make it the same as you had for your ASP.NET service. If you served all requests without HTTP based authentication and without SSL/TLS, then you should stick to that so you don't break your clients :). You have to make sure that you are offering at least one basicHttpBinding, because that's what closest matches the ASP.NET SOAP interface.

Finally, we add a new file called 'MathAddService.svc' in the virtual directory on IIS with the following contents.

1: <%@ ServiceHost Service="MathAddService" %>

This will use the implementation of the MathAddService class to serve the request for the IMathAddService interface. Of course your clients will have to be updated to use the new URL now (or you can try a 302 redirect but depending on the client's policies, this may fail). In case your requests to the new SVC file produce strange results (or send you back the above contents of the file), in the IIS administrative tools make sure that the .svc extension is mapped properly. If it isn't, you can run the aspnet_regiis.exe tool from the .NET framework to get that done.

Thursday, July 09, 2009 9:06 AM by roger

If you are like me working on server development you may run into the situation that a service fails early during startup, i.e. within the first couple of seconds. You'll soon realize that manually attaching a debugger doesn't work well, if at all. Even if you'll be running

windbg -pn MyService.exe
you may not actually be fast enough. Or there are multiple instances of that image running (e.g. SvcHost.exe) and the above command becomes kind of useless. Now if you have been reading the 'Debugging Tools for Windows' documentation (or have debugged services before) you'll already know what I am about to tell you here.

As I mentioned above, the first problem you're fighting with is that the failure happens very early. Another problem you may have is that your service is running e.g. as 'NT AUTHORITY\NETWORK SERVICE' and thus may not be able to interact with your desktop. But Windows' right here to help you out with 'Image File Execution Options'. This basically allows you to execute a command when a particular image is being executed.

You start by creating a key for the image file in the registry:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\MyService.exe
There you create a new string value of the name 'Debugger' and set its value to the path of the debugger you want to invoke, e.g. something like 'C:\Debuggers\cdb.exe'. To mitigate the problem of the non-interactive service, you'll probably want to use the debugger remotely, so you'll create a debugging server: 'C:\Debuggers\cdb.exe -server npipe:pipe=MyDebuggingSession'. What you have now is a debugger which is attached as soon as the service starts and which is accessible remotely. Thus, you can either on the server itself or another machine which has access run for instance

windbg -remote npipe:server=my-machine,pipe=MyDebuggingSession
and there you go: you are now debugging the service. Use the various command line options for the debuggers to

  • ignore the initial break point (-g)
  • run commands right after the debugger is attached (-c)
  • create a script which sets some useful breakpoints and run it when the debugger is attached (e.g. -c "$<MyScript.txt")

You may actually have to adjust the service control timeout. To do so, add a DWORD called 'ServicesPipeTimeout' to the registry key

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control
and set its value to the number of milliseconds you want the service to wait before timing out.

You'll find pretty much all of this information also in the help file for 'Debugging Tools for Windows' under 'Debugging a Service Application'. Enjoy!

Sunday, March 22, 2009 12:23 PM by roger

I've already described how you would create your own client using the RGS Agent WebService. Now what if you want to keep the tab in OC but make it look more fancy? Silverlight of course is your friend here. This post describes how you can create your own Silverlight based Agent tab, which will look like following. Btw, I am using Visual Studio 2008 to build this entire solution.

All the code for this can be found in AgentTabSl.zip (45.3 KB).

Getting Started

First I created a new Silverlight Application project and named it 'AgentTabSl'. The project template already comes with the basic structure which we need for this Silverlight control. I added another Silverlight user control which I use to show one Agent Group, named 'Group.xaml'. Its XAML looks like following.

 1: <UserControl x:Class="AgentTabSl.Group"
 2:              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 3:              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
 4:     <Grid x:Name="LayoutRoot" Background="White">
 5:         <Grid.ColumnDefinitions>
 6:             <ColumnDefinition Width="Auto" />
 7:             <ColumnDefinition Width="*" />
 8:             <ColumnDefinition Width="Auto" />
 9:         </Grid.ColumnDefinitions>
10:         <Grid.RowDefinitions>
11:             <RowDefinition />
12:             <RowDefinition />
13:         </Grid.RowDefinitions>
14:  
15:         <Rectangle Width="Auto" Height="Auto" Stroke="Black" RadiusX="4" RadiusY="4"
16:                    Grid.ColumnSpan="3" Grid.RowSpan="2" Name="Back" />
17:  
18:         <CheckBox Grid.RowSpan="2" Content="" VerticalAlignment="Center" Name="SignedIn"
19:                   Margin="2" Cursor="Hand" Checked="OnSignedInChanged" Unchecked="OnSignedInChanged" />
20:  
21:         <TextBlock Grid.Column="1" Grid.ColumnSpan="2" Name="GroupName" Margin="0,2"
22:                    FontWeight="Bold" />
23:  
24:         <TextBlock Grid.Column="1" Grid.Row="1" Margin="0,2">Number of Agents</TextBlock>
25:  
26:         <TextBlock Grid.Column="2" Grid.Row="1" TextAlignment="Right" Name="AgentCount"
27:                    Margin="2,2,4,2" />
28:     </Grid>
29: </UserControl>

I also added some code to the Group class to allow setting the agent group name, whether or not it's a formal agent group, if the user is currently signed in and the number of agents. On top of this, I added a static method which return a new instance of Group created based on the required parameters.

Retrieving the data from the Web Service

Unfortunately, because of the limitations of the Silverlight runtime, we cannot use the client proxy generated from wsdl.exe here. But you can still add a reference to the web service, e.g. through the 'Add Service Reference ...' command in Visual Studio. Simply enter the url to a deployed RGS Agent WebService (e.g. https://ocs-pool-01.contoso.com/Rgs/Clients/ProxyService.asmx) and everything else will be done for you. It is worthwhile to note that all methods of the web service will be asynchronous, but that's not a problem. I've added an event handler for the Loaded event of the Silverlight application:

 1: private void OnLoaded(object sender, RoutedEventArgs e)
 2: {
 3:     _service = new ProxyServiceSoapClient();
 4:  
 5:     _service.Endpoint.Address = new EndpointAddress(
 6:         "https://ocs-pool-01.contoso.com/Rgs/Clients/ProxyService.asmx");
 7:  
 8:     _service.GetGroupsCompleted += OnGetGroupsCompleted;
 9:     _service.GetAgentCompleted += OnGetAgentCompleted;
10:     _service.SignInCompleted += OnSignInCompleted;
11:     _service.SignOutCompleted += OnSignOutCompleted;
12:  
13:     _service.GetAgentAsync();
14:  
15:     _timer = new Timer(OnTimer, null, 0, 60 * 1000);
16: }

Timer is from System.Threading and I use it to periodically (every 60 seconds) refresh the data from the web service. This is required because else the Silverlight application won't figure out if groups have been added / removed / modified. You should not make it refresh in less than 60 seconds, but in the end, it is your decision (and your servers).

Please keep in mind that if you want to deploy this Silverlight application on a web server different than the OCS WebComponents, you'll need to allow Silverlight to access to that server. Please read the HTTP Communication and Security with Silverlight topic on MSDN for more information.

Retrieving the agent group memberships

The following code snippet shows the event handler for the GetGroupsCompleted event generated for us through the service reference. The first thing I am doing is to check whether or not we're on the UI thread. If we're not, we have to use the Dispatcher to invoke the method on the UI thread. The actual code to add the groups is then as simple as going through all groups and creating a new instance of the Group control which we'll add to the Children collection of the StackPanel which holds the groups.

 1: private void OnGetGroupsCompleted(object sender, GetGroupsCompletedEventArgs e)
 2: {
 3:     if (!Dispatcher.CheckAccess())
 4:     {
 5:         Dispatcher.BeginInvoke(() => OnGetGroupsCompleted(sender, e));
 6:     }
 7:     else
 8:     {
 9:         IEnumerable<AcdGroup> groups = e.Result.OrderBy(group => group.Name);
10:  
11:         Groups.Children.Clear();
12:  
13:         foreach (AcdGroup group in groups)
14:         {
15:             Group ctrl = Group.Create(group.Id, group.CanSignIn, group.IsSignedIn,
16:                                       group.Name, group.NumberOfAgents);
17:  
18:             ctrl.SignedInChanged += OnSignedInChanged;
19:  
20:             Groups.Children.Add(ctrl);
21:         }
22:  
23:         SetStatus("Ready.");
24:     }
25: }

Sign in / Sign out

Now what's left is basically to let the user sign in / out throug a click on the checkbox for the corresponding group. As you can see in the code (when you downloaded it), I also added an event to the Group control which is fired when the sign-in state for the group is changed. In the code above you see that we're subscribing to that event on line 18. So all we need to do is to actually call the corresponding method on the web service when the event is fired.

 1: private void OnSignedInChanged(object sender, EventArgs e)
 2: {
 3:     Group group = sender as Group;
 4:  
 5:     SetStatus("Please wait while the operation is performed ...");
 6:  
 7:     if (group.IsSignedIn)
 8:     {
 9:         _service.SignInAsync(group.Id);
10:     }
11:     else
12:     {
13:         _service.SignOutAsync(group.Id);
14:     }
15: }

What's missing?

If you look at the interface of the web service again, you see that it also allows you to sign in / out with multiple groups at the same time. I'll leave it to you to implement that in this Silverlight application as well as it does not really involve anything which I haven't discussed here. Also, you may want to highlight groups which have just been added by using a different gradient for the box.

If you're planning on actually deploying a Silverlight Agent OC Tab, you most likely also want to make sure that the users get the static strings in their preferred language. The article Deployment and Localization on MSDN should give you all the information required to do that.

Thursday, March 19, 2009 11:25 PM by roger

Your Office Communications Server 2007 R2, Response Group Service deployment comes with a tiny but nice little addition: the Agent WebService. It basically offers exactly the same data and functionality as the Agent OC tab but does this through a SOAP interface. If you have RGS deployed on a pool with the FQDN 'ocs-pool-01.contoso.com', you'll find the Agent OC tab at https://ocs-pool-01.contoso.com/Rgs/Clients/Tab.aspx. The Agent WebService is then located at https://ocs-pool-01.contoso.com/Rgs/Clients/ProxyService.asmx. If you are running the OCS WebComponents on other machines than the front ends, then the host name is the FQDN of the WebComponents machine/farm. So here's how you can write your own client to sign in/out with RGS Agent Groups. The TechNet article Deploying Response Group Clients gives more information about deploying RGS Clients, with focus on the Agent OC Tab.

Generating the Client Proxy

The first thing you typically want to do is to actually generate the proxy code which you will compile into your own client. You do so by calling

wsdl.exe /namespace:RgsAgentService /language:cs /out:RgsAgentService.cs https://ocs-pool-01.contoso.com/Rgs/Clients/ProxyService.asmx?wsdl

This will generate the RgsAgentService.cs file which you can include into your project and use right away. Web Services Description Language Tool (Wsdl.exe) on MSDN has more info on wsdl.exe if needed.

Using the Web Service

Now you're already good to go and use the web service. The code below shows a sample console application which does the following.

  • Create a new instance of ProxyService (the generated class from the step above) which points to the service on the pool you're interested in.
  • Query the web service if the current user is an agent or not. This requires you to authenticate with the user's credentials.
  • If the current user is an agent
    • Determine some basic information (e.g. the name and SIP address of the agent).
    • Retrieve the list of agent groups the agent is a member of in the connected pool.
    • If there are formal agent groups to which the agent is not signed in, the user is asked if he wants to sign in.
      • If he choses to sign in, tries to sign the agent in to those formal groups.

  1: using System;
  2: using System.Collections.Generic;
  3:  
  4: using RgsAgentService;
  5:  
  6: namespace RgsClient
  7: {
  8:     class Program
  9:     {
 10:         static void Main(string[] args)
 11:         {
 12:             string poolFqdn = "ocs-pool-01.contoso.com";
 13:  
 14:             if (args.Length > 0)
 15:             {
 16:                 poolFqdn = args[0];
 17:             }
 18:  
 19:             ProxyService service = ConnectToPool(poolFqdn);
 20:  
 21:             // First, figure out if the current user is an Agent.
 22:             if (!service.IsAgent())
 23:             {
 24:                 Console.WriteLine("You are not an agent in Pool '{0}'.", poolFqdn);
 25:                 return;
 26:             }
 27:  
 28:             // Now get some information about the Agent (i.e. the current User).
 29:             AcdAgent self = service.GetAgent();
 30:  
 31:             Console.WriteLine("You were authenticated as agent '{0}' ('{1}') in pool '{2}'.",
 32:                 self.DisplayName, self.SipAddress, poolFqdn);
 33:             Console.WriteLine();
 34:  
 35:             // Finally, determine which Agent Groups this Agent belongs to.
 36:             AcdGroup[] agentGroups = service.GetGroups();
 37:  
 38:             Console.WriteLine("Agent Group Name                  Formal?   Signed In?  # Agents");
 39:             Console.WriteLine("----------------------------------------------------------------");
 40:  
 41:             Dictionary<string, Guid> agentGroupIdsForSignIn = new Dictionary<string, Guid>();
 42:  
 43:             for (int i = 0; i < agentGroups.Length; i++)
 44:             {
 45:                 Console.WriteLine("{0,-32}  {1,-8}  {2,-10}  {3,8}",
 46:                     agentGroups[i].Name, agentGroups[i].CanSignIn,
 47:                     agentGroups[i].IsSignedIn, agentGroups[i].NumberOfAgents);
 48:  
 49:                 if (agentGroups[i].CanSignIn &&
 50:                     !agentGroups[i].IsSignedIn)
 51:                 {
 52:                     agentGroupIdsForSignIn.Add(agentGroups[i].Name, agentGroups[i].Id);
 53:                 }
 54:             }
 55:  
 56:             // If the Agent is not signed in to all his formal groups, then offer
 57:             // him to do so now.
 58:             if (agentGroupIdsForSignIn.Count > 0)
 59:             {
 60:                 Console.WriteLine();
 61:                 Console.WriteLine("You are not currently signed in to {0} agent group(s):",
 62:                     agentGroupIdsForSignIn.Count);
 63:  
 64:                 foreach (string agentGroupName in agentGroupIdsForSignIn.Keys)
 65:                 {
 66:                     Console.WriteLine("    {0}", agentGroupName);
 67:                 }
 68:  
 69:                 Console.WriteLine();
 70:                 Console.Write("Do you want to sign in to these groups now? [y/n] ");
 71:  
 72:                 ConsoleKeyInfo key = Console.ReadKey();
 73:                 while (key.KeyChar != 'y' && key.KeyChar != 'n')
 74:                 {
 75:                     key = Console.ReadKey();
 76:                 }
 77:  
 78:                 if (key.KeyChar == 'n')
 79:                 {
 80:                     return;
 81:                 }
 82:  
 83:                 Console.WriteLine();
 84:  
 85:                 if (service.SignInMultiple(agentGroupIdsForSignIn.Values.ToArray()))
 86:                 {
 87:                     Console.WriteLine("You have successfully signed in.");
 88:                 }
 89:                 else
 90:                 {
 91:                     Console.WriteLine("Sign-in to at leat one agent group has failed.");
 92:                 }
 93:             }
 94:         }
 95:  
 96:         private static ProxyService ConnectToPool(string poolFqdn)
 97:         {
 98:             ProxyService service = new ProxyService();
 99:  
100:             service.Url = String.Format("https://{0}/Rgs/Clients/ProxyService.asmx", poolFqdn);
101:             service.UseDefaultCredentials = true;
102:  
103:             return service;
104:         }
105:     }
106: }

Running this program will yield something similar to the following.

You were authenticated as agent 'Bob' ('bob@contoso.com') in pool 'rgs-pool-01.contoso.com'.

Agent Group Name                  Formal?   Signed In?  # Agents
----------------------------------------------------------------
Payroll Questions                 False     True               5
General HR Questions              True      False              8

You are not currently signed in to 1 agent group(s):
    General HR Questions

Do you want to sign in to these groups now? [y/n] y
You have successfully signed in.

More Methods

The WebService has a few more methods. These are in particular

  • SignIn(Guid groupId) - Tries to sign the current agent in to the agent group with the given ID
  • SignOut(Guid groupId) - Tries to sign the current agent out of the agent group with the given ID
  • SignOutMultiple(Guid[] groupIds) - Tries to sign the current agent out of all the groups identified with their respective IDs

These methods all return a boolean which indicates success (true) or failure (false).

Summary

You can integrate the RGS Agent Services into your own application by using the corresponding web service which is installed with the Response Group Service. This allows you to offer the same information and functionality as the RGS Agent OC Tab in a customizable manner.

Tuesday, February 10, 2009 11:16 PM by roger

I stumbled upon the System.DirectoryServices.AccountManagement namespace this week. It was introduced with .Net 3.5 and offers functionality to perform queries on AD objects like users, groups and computers in a more comfortable way than through the DirectorySearcher class from the System.DirectoryServices namespace. To illustrate the ease of using these classes, I came up with a tiny example which lists all users whose account name (the samAccountName attribute in AD) starts with an 'a'. On top of this, using LINQ it is quite simple to convert the resulting PrincipalSearchResult<Principal> collection into an IEnumerable<UserPrincipal>.

 1: using System;
 2: using System.Collections.Generic;
 3: using System.DirectoryServices.AccountManagement;
 4: using System.Linq;
 5:
 6: namespace UserSearch
 7: {
 8:     class Program
 9:     {
10:         static void Main(string[] args)
11:         {
12:             PrincipalContext context = new PrincipalContext(ContextType.Domain, "contoso.com");
13:
14:             UserPrincipal searchFilter = new UserPrincipal(context);
15:             searchFilter.SamAccountName = "a*";
16:
17:             PrincipalSearcher ps = new PrincipalSearcher(searchFilter);
18:
19:             IEnumerable<UserPrincipal> results = from principal in ps.FindAll()
20:                                                  where principal is UserPrincipal
21:                                                  select principal as UserPrincipal;
22:
23:             foreach (UserPrincipal user in results)
24:             {
25:                 Console.WriteLine("User '{0}' ({1}) Info:", user.SamAccountName, user.Name);
26:                 Console.WriteLine("    Password Set On  {0}", user.LastPasswordSet);
27:                 Console.WriteLine("    Last Log On      {0}", user.LastLogon);
28:                 Console.WriteLine();
29:             }
30:         }
31:     }
32: }