Archive for category Web Development

SubSonic 2.2 – RESTHandler (Add ProviderName Support)

I’ve been using SubSonic’s built-in RESTHandler for some basic AJAX features in my portal at work. This is a really nice extra feature, and provides a quick way to pull xml/json/rss from your DAL. I noticed that a few features based on this handler had stopped working a few weeks ago. I found that SubSonic’s RESTHandler had no way to pass in the provider name as part of the url. I currently have 5 different providers (databases) setup for SubSonic, so I needed a way to point to the various providers when calling a URL.

Edit the following file “SubSonic/HttpHandlers/RESTfullUrl.cs”

1. Add new property to access “ProviderName”

private string _providerName = String.Empty;
public string ProviderName
{
get { return _providerName; }
set { _providerName = value; }
}

2. Modify ParseURL()

private void ParseUrl()
{
//work backwards up the URL
//first thing, check for a query string and strip it
string workingUrl = _rawUrl;

if(workingUrl.Contains("?"))
{
//strip off the query string - it'll be used later
workingUrl = Strings.Chop(workingUrl, "?");

}

string[] bits = workingUrl.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries);
//the URL should be split out into
//protocol ("http");
//domain ("foo.com");
//service directory ("services");
//provider name ("northwinds")
//table or sp ("products")
//command.format ("list.xml")

//the index of each item within the URL
int commandIndex = bits.Length - 1;
int tableSPIndex = bits.Length - 2;
int providerIndex = bits.Length - 3;

//now check to see if the last item on the URL is a key, or
if(IsNonKeyCommand(workingUrl))
{
//the last item is the command - list/show/etc
//set it
//the last item is a key, set it, and then the command
_key = Path.GetFileNameWithoutExtension(bits[bits.Length - 1]);
commandIndex--;
tableSPIndex--;
providerIndex--;
}

//the command is the next item up the chain
_restCommand = DecideCommand(Path.GetFileNameWithoutExtension(bits[commandIndex]));

//evaluate the command
if(_restCommand == RESTCommand.Exec)
_spName = bits[tableSPIndex];
else
_tableName = bits[tableSPIndex];

_providerName = bits[providerIndex];

ParseQueryString();
}

Edit the following file “SubSonic/HttpHandlers/RESTHandler.cs”

3. Update GenerateReturnSet(), line #286.

q = new Query(_url.TableName);  //before
q = new Query(_url.TableName,_url.ProviderName);  //after

Here is an example using the new syntax that supports provider name.

http://localhost/services/northwinds/customers/list.xml

jQuery AssignEnterKey Plug-in

A few days ago, I was trying to figure out what to do about a user control that had multiple fieldsets that needed different default buttons triggered when the enter key was pressed on the page… Too bad the first button on the page is the default, by default! Since there was ~4 different forms that each had their own button, I came up with a hard-coded solution that solved the problem. Somebody suggested I make a jQuery Plug-In, so I tweaked the code a bit more and came up with the following plug-in below (I don’t think this qualifies as anything amazing, but it for the simpleton like me, it has a place).

A few examples on how to set the default to a field/form/etc…

1. You have 2+ fieldsets with their own button to perform an action: $(“FIELDSET”).AssignEnterKey();

2. A div called Search that provides site wide search: $(“#Search”).AssignEnterKey();

3. Multiple look ups on one page that have the class name Lookup: $(“.Lookup”).AssignEnterKey();

/*
* jQuery AssignEnterKey Plug-in *
* Summary This plug-in will attach a click/blur event to all input elements
 inside a parent container, the events will add and remove a class to the
 parent container that is used to associate the enter key with the a
 button or submit button.

* * Notes Using the options, you can target a different controls for both
 triggering the focus and the target to be executed.
* * Example $("FIELDSET").AssignEnterKey();
* * Developed by Zachary.Hunter@gmail.com * Date 2009-05-29 23:05
*/

(function($) {
   $.fn.AssignEnterKey = function(options) {
      var defaults = {triggerExp : ":input", targetExp : ":submit,:button" };
      var options = $.extend(defaults, options);

      $(document).keypress(function(e) {
         if ((e.which && e.which == 13) || (e.keyCode && e.keyCode == 13)) {
            $(".EnterKeyAssigned").find(options.targetExp).click(); return false;
            }
         });

       return this.each(function() {
         var obj = $(this);
         obj.find(options.triggerExp)
             .click(function(){ obj.addClass("EnterKeyAssigned"); })
             .blur(function(){ obj.removeClass("EnterKeyAssigned"); });
         }
      );
   };
})(jQuery);

I think this came out much more usable than my original code. I setup two parameters you can over-ride so it can look for specific trigger or target controls.

Overall, this is very basic plug-in and I can already see a few things that should be addressed (What to do when nothing is selected? What to do when you have multiple buttons in a container?). I think I’ll add a few more defaults options to address these. I’m very happy with what I got, since this was my first attempt at developing a jQuery plug-in…

Feedback is always appreciated!!!

[EDIT 5/19] by Zach : I have submitted this to the jQuery website as a plug-in. Hopefully somebody else finds this useful…

Using jQuery Autocomplete Plugin with ASMX & XML

I’m really surprised that the jQuery auto complete plug-in does not support XML by default. Almost all web services have the option to transmit XML, that it should be a requirement that every Ajax plug-in or widget consumes XML. If your using ASP.NET 2.0 and you want to use the jQuery auto-complete plug-in, you’ll notice this isn’t an easy task. Since the default plug-in requires a flat file with a list of values separated by new row “\r”. To support this natively, you can use a ASHX handler, but I wanted to use my existing web services. To get around the problem, I created a “parseXML” function and added an if statement in the $ajax results function.

The change really came down to adding an if statement for dataType = “xml” and added a new property datakey to support the parsing of the xml file. This way, I can parse ASMX 2.0 Web Service calls or static XML files.

[UPDATE] There is a catch to “STATIC” xml data, the default plug-in is based on the assumption that your results (aka: XML data) will only contain filtered results. If your data contains all possible results, you’ll need to filter the data… See comments for instructions! The XML parsing is done using jQuery built-in XML parser, so you don’t have to worry about dealing with browser specific parsers.

Here is the modified request function and the new parseXML function that I added to the “jQuery.autocomplete.js” plug-in:

        function request(term, success, failure) {
            if (!options.matchCase)
                term = term.toLowerCase();
            var data = cache.load(term);
            // recieve the cached data
            if (data && data.length) {
                success(term, data);
                // if an AJAX url has been supplied, try loading the data now
            } else if ((typeof options.url == "string") && (options.url.length > 0)) {

                var extraParams = {
                    timestamp: +new Date()
                };
                $.each(options.extraParams, function(key, param) {
                    extraParams[key] = typeof param == "function" ? param() : param;
                });

                $.ajax({
                    // try to leverage ajaxQueue plugin to abort previous requests
                    mode: "abort",
                    // limit abortion to this input
                    // port: "autocomplete" + input.name,
                    dataType: options.dataType,
                    url: options.url,
                    data: $.extend({
                        q: lastWord(term),
                        limit: options.max
                    }, extraParams),
                    success: function(data) {
                        var parsed = [];

                        // Added Logic by Zach
                        // If dataType = "XML" use the parseXML fuction
                        if (options.dataType == "xml") {
                            parsed = parseXML(data);
                        }
                        else {
                            parsed = options.parse && options.parse(data) || parse(data);
                        }
                        cache.add(term, parsed);
                        success(term, parsed);
                    }
                });
            } else {
                // if we have a failure, we need to empty the list -- this prevents the the [TAB] key from selecting the last successful match
                select.emptyList();
                failure(term);
            }
        };

        // Added Logic by Zach
        // Added XML Parse Function
        function parseXML(xml) {
            var parsed = [];

            $(xml).find(options.datakey).each(function() {
                parsed[parsed.length] = {
                    data: [$(this).text()],
                    value: $(this).text(),
                    result: options.formatResult && options.formatResult($(this).text(), [$(this).text()]) || [$(this).text()]
                };
            });
            return parsed;
        }

In order to use this new functionality, you’ll need to add the following options when you bind to a XML data source.

    $(document).ready(function() {
        $("#customerCode").autocomplete("Customers.xml", { dataType: "xml", datakey: "customer" });
    });

Here is the sample XML file used in the auto complete instance above.

<?xml version="1.0" encoding="utf-8" ?>
<CUSTOMERS>
    <CUSTOMER>Albert</CUSTOMER>
    <CUSTOMER>Allen</CUSTOMER>
    <CUSTOMER>Brandy</CUSTOMER>
    <CUSTOMER>Brink</CUSTOMER>
    <CUSTOMER>Cathy</CUSTOMER>
    <CUSTOMER>Candy</CUSTOMER>
    <CUSTOMER>Crystal</CUSTOMER>
    <CUSTOMER>Deon</CUSTOMER>
    <CUSTOMER>Dirk</CUSTOMER>
    <CUSTOMER>Erie</CUSTOMER>
    <CUSTOMER>Eve</CUSTOMER>
    <CUSTOMER>Francis</CUSTOMER>
    <CUSTOMER>Gina</CUSTOMER>
    <CUSTOMER>Harry</CUSTOMER>
    <CUSTOMER>Ingrid</CUSTOMER>
    <CUSTOMER>Janice</CUSTOMER>
    <CUSTOMER>Kelly</CUSTOMER>
    <CUSTOMER>Kylee</CUSTOMER>
    <CUSTOMER>Nathan</CUSTOMER>
    <CUSTOMER>Zachary</CUSTOMER>
</CUSTOMERS>

The only requirement for your ASMX web service, is your method signature must accept a string parameter called “q”. You can add additional parameters on your method, by default the auto-complete sends (q,limit,timestamp) but only “q” is required.

Here is a link to the official jQuery autocomplete plug-in.

Here is a complete ASP.NET 2.0 Web Application sample with the modified auto complete plug-in jQueryAutocompleteASMX.zip

[EDIT 5/14] by Zach :

        $(document).ready(function() {
            $("#textboxCustomer").autocomplete("/Autocomplete.asmx/GetCustomers", {
                parse: function(data) {
                    var parsed = [];

                    $(data).find("string").each(function() {
                        parsed[parsed.length] = {
                            data: [$(this).text()],
                            value: $(this).text(),
                            result: [$(this).text()]
                        };
                    });
                    return parsed;
                },
                dataType: "xml", datakey: "string", max: 5
            });
        });

Using the function above, you can replace the built-in auto-complete parse function with a dynamic parser. The only requirement is returning string array, which is the same object returned by the default function. I think the original example is useful if you are using the jQuery plug-in with ASP.NET and .NET Web Services, but if I was only going to use the plug-in a few times… I’d use this solution. -Zach

[EDIT 5/19] by Zach:
Don’t forget the following lines should be added to your web.config under “<system.web>” to support GET & POST requests for your web services.

        <!-- Added WebServices Section -->
        <webServices>
            <protocols>
                <add name="HttpGet"/>
                <add name="HttpPost"/>
            </protocols>
        </webServices>

jQuery REST Demo Application

Over the past few weeks I’ve been trying to learn how WCF can be used for REST. I found lots of information, but I also found lots of broken and incomplete demonstrations. Being new to WCF/REST, I really needed a working demo to see what I was doing wrong in my setup. After using the mighty Google for a few days and reading a ton of blog posts, I was able to piece together a working solution. I then played for a few days to make sure I could consistently reproduce working code and decided to build a working sample to show how to use WCF/ASMX services with ASP.NET 3.5 SP1 and jQuery. This demo was designed to give people the basic instructions on what to setup to be able to send/receiving data from the browser using jQuery with the server.

Demo 1-4 shows you the ASMX and the WCF equivalents for doing the basics (Get String, Get Array, Send String, Send Array), and Demo 5 shows you how to use a WCF Data Contract with a Wrapped/Bare message format. To make things easier to see, I log all JSON requests/results to the web page for inspection. This also allows you to see how requests are non-blocking and can be received out of order depending on processing time.

Demo Screen Shot

jQuery & JSON using ASP.NET

Download Demo Applicaiton

How to add a ASMX Web Service (ASP.NET Web Application w/.NET 3.5 SP1 Framework)

  1. Add New Item – Web Service
  2. Un-comment “[ScriptService]”

That’s it, your done. To access your web service via jQuery, use the following syntax.

    $.ajax({
        type: "POST",
        url: "WebService1.ASMX/HelloWorld",
        contentType: "application/json; charset=utf-8",
        dataType: "json",
        data: "{}",
        success: function(res) {
        // Do your work here.
        // Remember, the results for a ASMX Web Service are wrapped
        // within the key "d" by default. e.g. {"d" : "Hello World"}
        }
    });

How to add a WCF Service (ASP.NET Web Applicaiton w/.NET 3.5 SP1 Framework)

  1. Add New Item – WCF Service
  2. Right-click on the service and choose “View Markup”.
  3. Add the following line to the ServiceHost declaration: Factory=”System.ServiceModel.Activation.WebServiceHostFactory”
  4. Add a reference to the project for “System.ServiceModel.Web”.
  5. Open the interface associated with the service (e.g. IService1.cs) and add the following using statement “using System.ServiceModel.Web;”.
  6. Inside the interface, set the attributes on the method to support JSON & POST or GET request types.
  7. [OperationContract]
    [WebInvoke( Method = "POST", ResponseFormat = WebMessageFormat.Json)]
    
  8. Open the web.config and comment out the following lines:
  9.   <!--<behaviors>
       <endpointBehaviors>
        <behavior name="Web.Service1AspNetAjaxBehavior">
         <enableWebScript />
        </behavior>
       </endpointBehaviors>
      </behaviors>
      <serviceHostingEnvironment aspNetCompatibilityEnabled="true" />
      <services>
       <service name="Web.Service1">
        <endpoint address="" behaviorConfiguration="Web.Service1AspNetAjaxBehavior"
         binding="webHttpBinding" contract="Web.Service1" />
       </service>
      </services>-->
    

That’s it, your done. To access your web service via jQuery, use the following syntax.

    $.ajax({
        type: "POST",
        url: "Service1.svc/DoWork",
        contentType: "application/json",
        dataType: "json",
        success: function(res) {
        // Do you work here.
        }
    });

I found a lot of blog posts pointing to WCF Service Factory projects, which require a completely different setup. The sample above is for adding a WCF Service to a Web Application.

While debugging this, I used Firebug for Firefox and Fiddler to see the ajax & JSON data fired between the client and server. When you use fiddler to debug a local website, you need to add a “.” after the domain name to cause fiddler to catch the traffic (e.g. http://localhost.:1234/WebSite1). Hopefully the demo compiles and runs without problems, so you can see a quick 1-2-3 on how to get WCF up and running quickly!

I’m by far no expert on these technologies, so if anybody has suggestions on what I could be to make this demo better… Let me know! Hopefully seeing a working demo will help somebody else get a jump start on using jQuery to consume REST services.

Tracking User Statistics with ColdFusion

I love to dabble in new things, about 3 months ago I started helping a friend fix up their Cold Fusion 7 website. I noticed a bunch of things I consider important missing from the site and was curious about the user demographics/stats (who are they, what browser are they using, what is the max resolution we can use on the site, etc…). After talking a bit, I decided to implement the following items to gather the data:

  1. Implement AWSTAT to monitor the website logs.
  2. Implement a custom internal stat tracker to gather info about users.

Here are the fields we required:

  1. User Name
  2. Browser Type & Version (e.g. IE5, IE6, IE7, FF3, Chrome ???)
  3. Operating System (e.g. Linux, Max, Windows)
  4. Default Language Supported (e.g. en, jp )
  5. Screen Resolution (e.g. monitor resolution 1024×768, 1280×1024, or 800×600)
  6. Screen Colors (e.g. 16bit, 24bit, 32bit)
  7. IP Address
  8. URL (with only the first query string parameter, since he uses FuseBox)
  9. Date
  10. Time

Some of the variables in our list are not available in Cold Fusion, they are browser based data that the DOM has available via JavaScript. To get the data to the server I created a a page called “count.cfm” to accept a query string parameters with the data from the client. This is just JS 101, as far as I know there is no other solution to getting client side information about the user to the server. There are a lot more stats you can obtain from the client, to find them look at the JavaScript “screen” and “navigator” objects . I started to stub out a little extra in my code below (e.g. Navigator.javaEnabled ), if you want to use this data you’ll need to add the logic to add the data in a GET request to “count.cfm”.

    var file = '/fusebox/count.cfm';
    w = screen.width;
    h = screen.height;
    v = navigator.appName;
    j = navigator.javaEnabled();

    if (v != 'Netscape') {
        c = screen.colorDepth;
    }
    else {
        c = screen.pixelDepth;
    }

    info = 'res=' + w + 'x' + h + '&amp;js=' + j + '&amp;col=' + c;

    document.write('<img style="display: none;" src="http://www.zachhunter.net/blog/' + file + '?' + info + '" alt="" />');

After the data is received in the “count.cfm” page, I normalize the data by replace some of the data with foreign keys. This keeps the stat log normalized and decreases the table size.

function LogStats()
{
if(isDefined("session.userid"))
{
    UserId = session.userid;
    UserName = session.username;
    AuthorityLevel = session.authoritylevel;
}
else
{
    UserName = "Anonymous";
    UserId = "";
    AuthorityLevel = "";
}

OS = getOs(CGI.HTTP_USER_AGENT);
BrowserType = getBrowser(CGI.HTTP_USER_AGENT);

if(isDefined("CGI.HTTP_ACCEPT_LANGUAGE"))
{
    Language = CGI.HTTP_ACCEPT_LANGUAGE;

    if(Language.IndexOf(";") GT 0)
    {
        Language = Mid(Source, 1, Source.IndexOf(";"));
    }
}
else
{
    Language = "(Unknown)";
}

IP = CGI.REMOTE_ADDR;
LogDate = DateFormat(Now(),"mm/dd/yyyy");
LogTime = TimeFormat(Now(), "hh:mm");

if(isDefined("CGI.HTTP_REFERER"))
{
    Source = CGI.HTTP_REFERER;

    if(Source.IndexOf("&amp;") GT 0)
    {
        Source = Mid(Source, 1, Source.IndexOf("&amp;"));
    }
}
else
{
    Source = "(Home Page)";
}

if(isDefined("CGI.HTTP_REFERER"))
{
    Referrer = CGI.HTTP_REFERER;
}
else
{
    Referrer = "";
}

if(isDefined("url.res"))
{
    Resolution = url.res;
}
else
{
    Resolution = "(Unknown)";
}

if(isDefined("url.js"))
{
    JavaScriptEnabled = url.js;
}
else
{
    JavaScriptEnabled = "(Unknown)";
}

if(isDefined("url.col"))
{
    Colors = url.col;
}
else
{
    Colors = "(Unknown)";
}

IdOs = GetIdOS(OS);
IdColors = GetIdColors(Colors);
IdRes = GetIdRes(Resolution);
IdBrowser = GetIdBrowser(BrowserType);
IdLang = GetIdLang(Language);
IdPath =  GetIdPath(Source);

Insert = "SET NOCOUNT ON;
          INSERT INTO tbl_Stats_Stats
          (username, [date], [time], ip, osid, colorid, browserid, resid, pathid,languageid)
          VALUES
          ('#UserName#', '#LogDate#', '#LogTime#', '#IP#', #IdOs#, #IdColors#, #IdBrowser#, #IdRes#, #IdPath#, #IdLang#);
          SELECT @@identity as LanguageId from tbl_stats_Languages";
InsertResults = cfquery(dsn="",sqlstring=Insert);
return InsertResults.LanguageId;
}

This probably took about 2 hours to throw together. I’m sure there is more than can be done to optimize the code but this way my first time programming with Cold Fusion. Additionally, since my script is targeted for an intranet application, I didn’t implement parameter validation which is a must for external websites! The largest amount of work was building the logic to parse the USER_AGENT so I could determine the browser and OS. These are very important, since our goal was to determine how we could redesign without negatively impacting the users.

This was deployed by putting the JS into the footer template (footer.cfm). The database script is included in the source code download, along with a data reset script.

Hopefully the logic or code can help you solve your problem.

Cold Fusion Web User Statistics

ASP.NET Mixing Forms & Windows Authentication

This logic can be used to give you a SINGLE SIGN-ON / AUTOLOGIN solution with ASP.NET 2.0 or higher using a combination of authentication providers on IIS 5.1 (Windows 2000). The demo is using C# and does require a network administrator account to make the magic work. I’ve tried using other accounts, but was only able to get this to work with a “Domain Administrator” account.

I tried so many solutions (application sub folder using Windows auth, web service, every possible IIS 5.1 settings, WMI query) and nothing worked. After trying to figure this out for a few weeks, I realized that I had a single sign-on solution being used for our SonicWall content filtering system appliances. I looked into how this was being done and I decided to put the same logic in the page_load event of my log in control.

After an hour of playing around, I had a working ASP.NET single sign-on solution. Using impersonation, I create a thread that uses a domain administrator account (this is a requirement, which is also required by the SonicWall solution) to perform a remote call to the client workstation to obtain the currently logged on user(s). The magic is done by using “netapi32.dll”, by passing the NetWkstaUserEnum method a hostname your able to obtain an array of users logged into the computer.

protected void Page_Load(object sender, EventArgs e)
{
     if (!IsPostBack) { // Attempt Autologin
         string account = String.Empty;
         string domain = String.Empty;
         string user = String.Empty;
         string email = String.Empty;

         try
         {

             ImpersonateUser iu = new ImpersonateUser();
             if (iu.impersonateValidUser("", "", ""))
             {

                 NetWorkstationUserEnum nws = new NetWorkstationUserEnum();
                 string host = nws.DNSLookup(Request.UserHostAddress);

                 string[] users = nws.ScanHost(host);

                 if (nws.ScanHost(host).Length &gt; 0)
                 {
                     account = users[users.Length - 1];
                     domain = account.Substring(0, account.IndexOf("\\"));
                     user = account.Substring(account.IndexOf("\\") + 1, account.Length - account.IndexOf("\\") - 1);
                 }

                 iu.undoImpersonation();
             }
         }
         catch (Exception logex)
         {
             Log.Debug("Autologin Failure: " + logex.Message);
         }

         if (!String.IsNullOrEmpty(account))
         {
             Log.Info("Account: " + account);

             if (domain.ToUpper() == "")
             {
                 email = user.ToLower() + "@";
                 String strRole = AssignRoles(email);

                 if (!String.IsNullOrEmpty(strRole))
                 {
                     FormsAuthentication.Initialize();

                     //The AddMinutes determines how long the user will be logged in after leaving
                     //the site if he doesn't log off.
                     FormsAuthenticationTicket fat = new FormsAuthenticationTicket(1,
                         email, DateTime.Now,
                         DateTime.Now.AddDays(7), true, strRole,
                         FormsAuthentication.FormsCookiePath);

                     HttpCookie ck;
                     ck = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(fat));
                     ck.Expires = fat.Expiration;
                     ck.Path = FormsAuthentication.FormsCookiePath;

                     Response.Cookies.Add(ck);
                     Response.Redirect(FormsAuthentication.GetRedirectUrl(email, true));
                 }
                 else
                 {
                     Log.Info("Unable to create FAT, user does not have any roles assigned.");
                 }
             }
         }

     }
}

I always take the last user in the list, since the machine account “domain\machinename$” is always first. I’ve not had it return the wrong person yet. Even if a user is not automatically logged in, they can still manually enter their log in/password to access the site.

** On my development machine, when I checked the array results I would see a bunch of accounts that were generated each time I ran VS. Each time I ran my demo application, I’d see a different account like (ASPNET) as the last user. I think this has something to do with the thread VS users for the built-in web server.

Too keep my original Forms Authentication solution in place, I parse the account (e.g. DOMAIN\User => User) and I check the Forms authentication database for the user’s name, skipping the password check. If they exist, I creates a FormsAuthenticationTicket and the process is complete.

** This code is part of a C#, ASP.NET Website Project. I’ve tested this code on a few websites all using C# and this does work. The cavaet of requiring a high-security account is a pain but it works. If anybody knows of another way to pull this off, let me know…

By default, the log in control is only displayed when a user is not authenticated. This is because the auto log in logic is hooked onto the modules load event.

This solution is currently working on Windows 2000 with IIS 5.1, for a portal developed with ASP.NET 2.0 and C#. The portal uses forms authentication but existing on an internal domain. The site has a a few hundred users, some are remote agents that connect via VPN and are not part of the domain.

This has made a huge difference, since we now set the company default web page to the site and key that they’ll be able to see everything since we’ve automatically logged them in. The first day I made this change, I got 3 emails about the some major changes users saw done to the site. I found this comment really funny, since we didn’t make any changes to the content… it just so happened that this was the first time they had ever logged in!

Good References for the solution:

http://www.codeproject.com/KB/IP/LoggedOnUsersPart2.aspx (had a good note about security)

Other Notes:

I found another solution saying that a WMI Query should be able to obtain the same data, but when I tried this using impersonation it did not work. I read that WMI queries, even when run via Impersonation, are limited by the ASPNET WP thread. I can’t explain the details but I did find two posts about this and took them at face value.

You can download the C# ASP.NET code here. This is an excerpt from my project but it’s working code. If anybody else has a working solution that does not require a domain admin account, let me know. Having a Single Sign-On (“SSO”) solution for an ASP.NET intranet site is awesome!

FormsWindowsMixxed_Source.zip