Debugging with WinDBG

Last week I had the opportunity to talk about debugging and WinDBG at Sogeti. We recorded the session as a Live Meeting and my colleague David Giard was kind enough post the video for me. I have a previous blog post linking to the tools I mentioned during the talk.


Posted: Sunday, September 27, 2009 10:05:00 PM (Eastern Standard Time, UTC-05:00)  #    Comments - Trackback
ASP.NET | Speaking | Tools

Why Windows Azure

As a consultant, I am always trying to see where new tools may fit so that I can help my clients. So I really need to know where the cloud fits into application development recommendations. Really, it’s just another platform that expands the options for a client. This is my 10,000 foot view of criteria for when Azure applies to solution (given what I know today):

Advantages

  • Lower cost of maintenance. You don’t own the hardware, you don’t fix it, update it, etc.
  • Pay as you go. A short term app (say for a marketing campaign) is very cheap to run.
  • The app is abstracted from the infrastructure. You don’t have access to it, so now you ignore it.
  • Scalability is easy. Apps that are seasonal or have know traffic spikes benefit from this greatly. Also apps that become more or less popular are easier to scale up or down in a very short period of time.
  • Cost. See the Azure Pricing
    • The base cost for Azure is about ~ $1000/yr just to have one active app instance ($0.12/hr). Bandwidth and disk space are extra charges on top of the base cost, but seem inexpensive to me.
    • Since bandwidth and storage are cheap in Azure, so if you have a high need for either Azure is a good idea.
  • Reliability. Azure has Service Level Agreements (three nines or better) with penalties for Microsoft if they fail.
  • There is alerting/API integration with SCOM, so if you want to monitor an Azure app with your in-house tools you can.
  • Geo-location. You can have multiple instances of your Azure app in geographically disparate locations. Of course there are issues with data sharing, authentication, etc. But it’s significantly easier with Azure than other options available today.
  • You can use Azure as a storage-only option and split your app between self hosting and Azure, or vice-versa.

Disadvantages

  • Having an app in the cloud may not be compliant with some regulatory guidelines
  • Cloud applications are web apps only. Not all solutions use web apps.
  • Identity and authentication are a challenge. The app is no longer in your domain, so you will need to look at federation or other options.
  • Most out-of-the-box apps are not hosted on Azure. Think CRM, ERP, financial, etc.
  • Apps that need access to the infrastructure aren’t going to work or need to be re-designed for the new paradigm.
  • It’s a new platform, even if it seems just like ASP.NET. This means you design differently, you develop a little differently as well. So there is a learning curve.
  • You are still responsible for data backups, I’m still not clear on how this works for the non-SQL storage options.

Overall, I think this is a significant new option for all sizes of companies. I’m not sure many companies are ready to give up the security of having mission-critical apps in-house, but I think for smaller companies Azure is a huge step forward. If you are already paying for hosting an app of some kind, give Azure a look.

Posted: Friday, September 18, 2009 4:00:00 PM (Eastern Standard Time, UTC-05:00)  #    Comments - Trackback
ASP.NET | Azure | Cloud

Adding A Relationship to the User Profile in Commerce Server 2009

For this post I am assuming you are familiar with Commerce Server 2009 and operation sequences, which in my opinion are the main reason to use the upgraded Commerce Server product. You can find some background on operation sequences on MSDN.

 

The problem we are trying to solve here is the addition of new properties to the Commerce Server Profile system, but we want a one-to-many relationship between the user profile and the new property. As an example, we will be adding a number of Tests to the user’s profile. In this case the user needs to participate in a number of tests that we can track, because these tests determine what products a user will be able to purchase. The more tests and test categories the user participates in, the more products they are eligible for.

 

The basic steps for this are:

  • Create the underlying SQL Server data store
  • Map the store to the user profile in Commerce Server manager
  • Create further mappings for Commerce Entities in the MetadataDefinitions.xml file
  • Create C# classes
  • Add the classes to the operation sequences in ChannelConfiguration.config

See How to Use the Profiles Schema Manager on MSDN for documentation on the Profiles editor in Commerce Server Manager.

  1. Create the underlying data table and fields in SQL Server in the profile database where the data will be stored. This is very important to do first, even though Commerce Server will let you create definitions before the underlying data store exists. (I’m assuming you are familiar with SQL Server). In my example I am going to add a number of tests per each person in the profile. The tests have and ID, name, and category.
  2. Assuming that we are adding our relationship to the User Profile, add a new field to the UserObject table that is nvarchar(255) and named after your custom profile object. Commerce Server uses this field to store the IDs (primary key) of the related items. You may need to make the field size larger if your key is large like a GUID or you expect to have many child records. 255 is the default number from the Commerce Server SDK examples.
  3. Save the database change scripts (for source control)
  4. Open Commerce Server Manager, expand the tree:
    Commerce Server Manager –> Global Resources –> Profiles –>Profile Catalog –> Data Sources –> SQL Data source (name varies) –> Data objects
  5. Create a new object: Right click the Data Objects node and choose New Data Object. Choose the database table name from the list box, the Display Name and Members will fill out automatically, even though the members are not propagated further.
  6. Add Members to your new object: For each field you want to add, right click the new node and choose New Data Member.
    1. Choose the Member Name by clicking the database field name in the list.
    2. Click the Add button.
    3. Repeat for other new members
    4. Click the Finish button.
  7. Move to the Profile Definitions node in Commerce Server Manager.
  8. Right-click the Profile Definitions node and choose New Profile Definition.
  9. Give the new profile definition a name and display name. Click the Finish button, ignoring the Custom Attributes section.
  10. Update the new node by first clicking the node and allowing the editor to load.
  11. Create a new Group by clicking the Add button and selecting Add a New Group.
    1. Name the first group “GeneralInfo” with a display name of “General Information”. This is a common Commerce Server pattern.
    2. Click the Apply button.
  12. For each property you want to add, click the Add button and choose to add a new property
  13. Give the property a name and display name. Case matters later, so pay attention to the details here.
  14. Choose the correct type
    1. If you are adding a relationship to a profile definition type:
      1. Choose Profile.
      2. Choose the definition type for the Type Reference field.
    2. If you are adding a new field
      1. Choose the appropriate data type
  15. Expand the advanced attributes node
    1. Choose the Map To Data and use the Data Source Picker to map the database field
    2. Click the Apply button, or the change won’t update the Properties panel.
  16. Be sure to define the primary key
  17. Save the Profile
  18. Now we will build the relationship between the User Object and the new Profile Item we created.
    Click the User Object node and allow the editor to load.
  19. Expand the General Information group
  20. Add a new property to the General Information group
    1. Name the property after your relationship
    2. Choose Profile as the Type
    3. In the Type Reference field navigate to your new Profile type
    4. In the Map To data field choose the database field you created in the UserObject table in step 2.
    5. Click the Apply button.
  21. Save the Profile
  22. Export the schema as an XML file (to put under source control)
  23. Now we need to map the changes we made in Commerce Server Manager into a CS2009 definition. In your web project in Visual Studio, edit MetadataDefinitions.xml
    1. Find he <CommerceEntities> node under the <DefaultChannel> node
    2. Create a new <CommerceEntity> node for the new item, Test in my example.

      <CommerceEntity name="Test">

          <DisplayName value="Test"/>

          <EntityMappings>

              <EntityMapping

                csType="Microsoft.CommerceServer.Runtime.Profiles.Profile"

                csAssembly="Microsoft.CommerceServer.Runtime, Version=6.0.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"

                csDefinitionName="Test"

                csArea="Profiles">

                  <PropertyMappings>

                      <PropertyMapping property="TestId" csProperty="GeneralInfo.TestId" />

                      <PropertyMapping property="TestName" csProperty="GeneralInfo.TestName" />

                      <PropertyMapping property="TestCategory" csProperty="GeneralInfo.TestCategory" />

                  </PropertyMappings>

              </EntityMapping>

          </EntityMappings>

          <Properties>

              <Property name="TestId" dataType="String" />

              <Property name="TestName" dataType="String" />

              <Property name="TestCategory" dataType="String" />

          </Properties>

          <Relationships/>

      </CommerceEntity>
       

    3. Find the <CommerceEntity> node named UserProfile.
    4. Add a new PropertyMapping node in the ProperyMappings section.
    5. Add a property attribute to name the property after the name defined in step 20.
    6. Add a csProperty attribute that is the name you chose in step 20a. Case matters. The format is PropertyGroup.PropertyName (ex: GeneralInfo.Gender)
    7. Find the <relationships> node under UserProfile.
      1. Add a relationship:
        ex:

        <Relationships>

            <Relationship name="Tests" type="Relationship" modelName="Test" isMultipleItems="true">

                <DisplayName value="Tests" />

                <Description value="User's Tests" />

            </Relationship>

        </Relationships>

  24. Create the following types of classes for Commerce Server:
    1. A class to hold your new data


      namespace
      CommerceDemo

      {

          [Serializable]

          public class Test

          {

              public string TestId { get; set; }

       

              public string TestName { get; set; }

       

              public string TestCategory { get; set; }

          }

      }

    2. A processor class that inherits RelatedProfileProcessorBase

      namespace CommerceDemo

      {

          public class UserProfileTestProcessor : RelatedProfileProcessorBase

          {

              // Methods

              protected override bool CanDeleteRelatedItem(string relatedItemId)

              {

                  return true;

              }

       

              // Properties

              protected override string CommercePreferredIdKeyName

              {

                  get

                  {

                      return "GeneralInfo.Test";

                  }

              }

       

              protected override string CommerceRelatedIdsKeyName

              {

                  get

                  {

                      return "GeneralInfo.Test";

                  }

              }

       

              protected override string ParentProfileModelName

              {

                  get

                  {

                      return "UserProfile";

                  }

              }

       

              protected override string PreferredItemRelationshipName

              {

                  get

                  {

                      return "TestId";

                  }

              }

       

              protected override string ProfileModelName

              {

                  get

                  {

                      return "Test";

                  }

              }

       

              protected override string RelationshipName

              {

                  get

                  {

                      return "Tests";

                  }

              }

       

          }

      }

    3. A committer class that inherits ProfileCommitterBase

      namespace CommerceDemo

      {

          class UserProfileTestCommitter: ProfileCommitterBase

          {

              protected override string ProfileModelName

              {

                  get { return "Test"; }

              }

          }

      }

    4. A response builder class that inherits RelatedTestResponseBuilderBase

      namespace CommerceDemo

      {

           class TestResponseBuilder : ProfileResponseBuilderBase

          {

       

              protected override string ProfileModelName

              {

                  get

                  {

                      return "Test";

                  }

              }

          }

      }


  25. Open ChannelConfiguration.config
    1. Find the <MessageHandler> node for “CommerceQueryOperation_UserProfile”
      1. Add <Component> nodes for the processor and response builder classes created above:

        <MessageHandler name="CommerceQueryOperation_UserProfile" responseType="Microsoft.Commerce.Contracts.Messages.CommerceQueryOperationResponse, Microsoft.Commerce.Contracts, Version=1.0.0.0, Culture=neutral,PublicKeyToken=31bf3856ad364e35">

            <OperationSequence>

                <Component name="User Profile Loader" type="Microsoft.Commerce.Providers.Components.UserProfileLoader, Microsoft.Commerce.Providers, Version=1.0.0.0, Culture=neutral,PublicKeyToken=31bf3856ad364e35"/>

                <Component name="User Profile Test Processor" type="CommerceDemo.UserProfileTestProcessor, CommerceDemo, Version=1.0.0.0, Culture=neutral,PublicKeyToken=fbeb08efdb2d0b73"/>

                <Component name="User Profile Response Builder" type="Microsoft.Commerce.Providers.Components.UserProfileResponseBuilder, Microsoft.Commerce.Providers, Version=1.0.0.0, Culture=neutral,PublicKeyToken=31bf3856ad364e35" />

                <Component name="User Profile Test Response Builder" type="CommerceDemo.UserProfileTestResponseBuilder, CommerceDemo, Version=1.0.0.0, Culture=neutral,PublicKeyToken=fbeb08efdb2d0b73" />

            </OperationSequence>

        </MessageHandler>

    2. Find the <MessageHandler> node for “CommerceCreateOperation_UserProfile”
      1. Add <Component> nodes for the processor, Committer, and response builder classes created above, similar to the previous step.
    3. Find the <MessageHandler> node for “CommerceDeleteOperation_UserProfile”
      1. Add <Component> nodes for the processor, Committer, and response builder classes created above, similar to step 25a1.
  26. Create any converter classes which converts the collection of Commerce Server properties from a CommerceEntity into a typed C# object.

            public static Test ConvertTest(CommerceEntity testEntity)

            {

                Test result = new Test();

     

                result.Id = testEntity.Id;

                result.TestName = testEntity.Properties[“TestName”].ToString();

                result.TestCategory = testEntity.Properties[“TestCategory”].ToString();

     

                return result;

            }

  27. Redeploy the project
  28. Reset IIS so the Commerce Server cache is emptied.

So how does this look when wired up? That’s a complex question to be sure, but here is a simplified example, assuming we have a User object class with simple common properties:

public User GetUser(string userId)

{

    User user = null;

    OperationServiceAgent svc= new OperationServiceAgent();

 

    CommerceQuery<CommerceEntity> query = new CommerceQuery<CommerceEntity>("UserProfile");

    query.SearchCriteria.Model.Properties["Email"] = userId;

 

    CommercePropertyCollection properties = new CommercePropertyCollection();

    properties.Add("Email");

    properties.Add("FirstName");

    properties.Add("LastName");

    query.Model.Properties = properties;

 

    CommerceQueryRelatedItem<CommerceEntity> test= new CommerceQueryRelatedItem<CommerceEntity>("Tests", "Test");

    test.Model.Properties.Add("TestId");

    test.Model.Properties.Add("TestNumber");

    test.Model.Properties.Add("TestCategory");

    query.RelatedOperations.Add(test);

 

 

    CommerceResponse response = svc.ProcessRequest(GetRequestContext(), query.ToRequest());

   

    CommerceQueryOperationResponse  queryResponse = response.OperationResponses[0] as ommerceQueryOperationResponse;

    if (queryResponse !=null && queryResponse.CommerceEntities.Count > 0)

    {

        Convert(queryResponse.CommerceEntities[0]);

    }

    return user;

}

 

public static User Convert(CommerceEntity userEntity)

{

    User result = new User();

    result.Id = userEntity.Properties["Id"] as string;

    result.Email = userEntity.Properties["Email"] as string;

    result.LastName = userEntity.Properties["FirstName"] as string;

    result.FirstName = userEntity.Properties["LastName"] as string;

 

    CommerceRelationshipList relationships = userEntity.Properties["Tests"] as CommerceRelationshipList;

    result.Tests = ConvertTest(relationships); //Function above in post

 

     return output;

}

Posted: Monday, August 10, 2009 8:48:00 PM (Eastern Standard Time, UTC-05:00)  #    Comments - Trackback
ASP.NET | Commerce Server

Useful LogParser Queries

If you deal with production servers and you don’t use LogParser, you should. It gives SQL-like abilities to query web server logs an other log types (like Event Logs). Coding Horror has a great article about the tool as well, and a link to a good article about forensic log parsing. Here are my most used web queries:

Find Pages with 500 errors
logparser "SELECT cs-uri-stem as Url, sc-status as code, COUNT(cs-uri-stem) AS Hits FROM C:\ProdLogFiles\ex*.log WHERE (sc-status >= 500) GROUP BY cs-uri-stem, code ORDER BY Hits DESC" -o:CSV >> C:\Data\ErrorPages.csv

Find 404 Requests
logparser "SELECT cs-uri-stem as Url, sc-status as code, COUNT(cs-uri-stem) AS Hits FROM C:\ProdLogFiles\ex*.log WHERE (sc-status = 404) GROUP BY cs-uri-stem, code ORDER BY Hits DESC" -o:CSV >> C:\Data\404Pages.csv

Find the Slowest Pages
logparser "SELECT TOP 100 cs-uri-stem AS Url, MIN(time-taken) as [Min], AVG(time-taken) AS [Avg], max(time-taken) AS [Max], count(time-taken) AS Hits FROM C:\ProdLogFiles\ex*.log GROUP BY Url ORDER BY [Avg] DESC" -o:CSV >> C:\Data\SlowPages.csv

Posted: Thursday, August 06, 2009 7:41:00 PM (Eastern Standard Time, UTC-05:00)  #    Comments - Trackback
ASP.NET | IIS | Performance | Tools

Another “Why didn’t someone tell me this earlier” Post

90% of the projects I work on need to send emails, and most clients don’t allow developer SMTP servers floating around their network (for good reason). So a big headache ensues trying to get access to an Exchange or SMTP server somewhere. I have been doing this same dance for years. But today I find a blog post about sending emails without an SMTP server. The .Net calls to SMTP just write the emails out on the file system. It’s just configuration even. So much to know, so little time.

Posted: Wednesday, July 29, 2009 7:12:00 PM (Eastern Standard Time, UTC-05:00)  #    Comments - Trackback
.Net 2.0 | .Net 3.5 | ASP.NET

Anti-Simplicity

Josh Holmes recently posted his presentation “The Lost Art of Simplicity”. As a consultant, I am frequently striving for simplicity for one main reason: I leave behind the code I create. Most of the time I leave the code behind to be maintained by some other developer that works for the client. So I am right there with what Josh is advocating. I wouldn’t want to be the guy trying to figure out some overly-complicated code 6 months down the line (I have been that guy before).

<RANT>

So here on my current project is anti-simplicity in action. The app is an ASP.NET app. There is a “toolbar” of sorts specific for the page. One of the toolbar “buttons” is simply a link to find more products. The “toolbar” is hosted in a user control. The developer coded this link as an ASP Linkbutton. The linkbutton has a server-side event handler to capture the click, which it then propagates outside the user control so the container control can get the click event. The hosting control then captures the event and does a redirect to a hard-coded URL. There is zero conditional logic in all the code. What happened to an HTML <A> tag? There is absolutely no reason for any server processing on this action at all! I can only fathom the reasons the developer did this:

  • There are other buttons that act that way, so all buttons should act that way
  • He’s proud of complex code and solutions
  • Because he can
  • He thinks it’s more secure, hiding the url?

In the end, the developer has forgotten why he is on the project. We are creating software for the client. This includes leaving them with something that has a lower maintenance cost by achieving simplicity.

On a related note, the UI/designers on this project are 100% anti-tables. I understand using CSS for layouts and not tables, I’m all on board with that. But using UL lists stacked next to one another to display tabular data is just wrong. Tables are part of the HTML spec for a reason, mainly tabular data. Because developers/designers perverted the use of tables in the past does not preclude their ultimate use in a solution. Zealotry is another anti-simplicity pattern in my world.

</RANT>

Posted: Wednesday, May 06, 2009 2:13:00 AM (Eastern Standard Time, UTC-05:00)  #    Comments - Trackback
ASP.NET

Invalid postback or callback argument - AJAX Control Toolkit

I was working to add some Ajax interactivity to a page that needed some asynchronous workings. Unfortunately, after displaying a ModalPopupExtender and the user clicking the Close button on the “popup”, the page would throw the following exception:

System.ArgumentException occurred
  Message="Invalid postback or callback argument.  Event validation is enabled using <pages enableEventValidation=\"true\"/> in configuration or <%@ Page EnableEventValidation=\"true\" %> in a page.  For security purposes, this feature verifies that arguments to postback or callback events originate from the server control that originally rendered them.  If the data is valid and expected, use the ClientScriptManager.RegisterForEventValidation method in order to register the postback or callback data for validation."
  Source="System.Web"
  StackTrace:
       at System.Web.UI.ClientScriptManager.ValidateEvent(String uniqueId, String argument)
  InnerException:

There are lots of explanations out there for this kind of problem, but none of the common answers really fit. The most common answer was to disable event validation. That wasn’t a good answer for me, I don’t think disabling an important security feature because you have complex code is a good solution. The other common answer was to override the render event to register the control posting back with the ScriptManager. This did not work at all for me.

After finding an reading through this post and the comments, I realized the problem was indeed in the code. After the call to ModalPopupExtender.Show(), the original code was re-binding a grid on the page. This order of events was causing the problem. I changed the binding to occur before the call to the ModalPopupExtender by changing which event containing the rebinding call, and the error went away.

Posted: Monday, March 23, 2009 8:46:00 PM (Eastern Standard Time, UTC-05:00)  #    Comments - Trackback
.Net 3.0 | .Net 3.5 | ASP.NET | JavaScript/AJAX

Linq Foray

I’m working on a website with lots of user controls interacting with one another using ASP.NET Ajax using events. So I end up needing to find other controls often, especially within repeaters. Nested controls all over the place. I found this wonderful post about using an extension method to the Controls Collection that flattens the tree and allows for much simpler Linq queries. The extension method adds and All() function which is seriously useful.

Posted: Sunday, March 22, 2009 9:27:00 PM (Eastern Standard Time, UTC-05:00)  #    Comments - Trackback
.Net 3.5 | ASP.NET | JavaScript/AJAX | Linq

Don’t Do This

In ASP.NET, the DataList control renders as a table. Opening a new table in the header and trying to get rows to render just won’t end well. Lots of strange browser behavior should be the tip off that this isn't right.

Posted: Sunday, March 22, 2009 7:48:00 PM (Eastern Standard Time, UTC-05:00)  #    Comments - Trackback
.Net 2.0 | .Net 3.5 | ASP.NET

A Cool IE HTML/CSS Trick - Conditional Comments

I am continually humbled by the amount of stuff I don't know in the areas where I generally feel good about my level of knowledge....

Today I learned about conditional comments. This is an IE specific trick, but in my case I'm trying to have different CSS for a certain class so it behaves properly in IE6 and standards compliant browsers. The problem is using .png files that have transparency. IE6 totally botches .png files. But it turns out there is an IE-specific filter (progid:DXImageTransform) that causes IE6 to render a .png properly. An older article at A List Apart describes how to use it, along with conditional comments.

Conditional comments allow IE to display the content between the comments based on an expression. In this case, the expression is testing for the browser version:

<!--[if IE 6]>
<link  rel="stylesheet" type="text/css"   href="ie6.css" />
<![endif]-->

So this code snipped would cause IE 6 to load this custom stylesheet. IE7 interprets the comment correctly and doesn't load the stylesheet. Other browsers interpret this as a comment and ignore it. Very nice.

Posted: Thursday, August 07, 2008 3:49:16 AM (Eastern Standard Time, UTC-05:00)  #    Comments - Trackback
ASP.NET

ASP.NET 2.0 and Global.asax: What not to do

I deployed a site today that has some code in the Global.asax event handlers. I let Visual Studio 2008 add the file to my project when I created it, and it put the code directly in the file inside <script runat="server"> tags. I went with it. So when I deployed the file, none of the events fired. Ever. The lesson is: Don't put your code in the global.asax file. Apparently this problem is by design. There is a vague KB Article on this problem, but the solutions aren't all that helpful, I didn't want to pre-compile, and the first solution made no sense at all. A little searching and I found one good solution: put a class that inherits HttpApplication in the App_Code folder as described here. What I don't understand is why Visual Studio adds the file that way if it isn't going to work on an xcopy deployment. Microsoft seems to go out of their way to protect us from ourselves so often that I am surprised the IDE does something intentionally that won't work.

Posted: Wednesday, February 06, 2008 3:57:21 PM (Eastern Standard Time, UTC-05:00)  #    Comments - Trackback
ASP.NET | Visual Studio

ASP.NET Web Project Build Events

I have a web project (the original 2005 web project type, not a web application project) and had a problem getting files copied to the bin directory. Essentially, one of the library projects referenced by the web project has an XML file in the project output, but when the solution is built, the XML file in the bin directory of the library project is not pulled into to the bin directory of the web project. Of course, a post-build event seemed like the thing to do, but web projects don't have support for that.

A little digging and I found this post by Scott Guthrie that describes a "Build Helper Project". You simply add an empty class library project to your solution. You then use the build events in the empty project use to add build events to your web project. You just make sure the project build order is correct so the events get called when you need them.

Posted: Monday, October 08, 2007 4:28:16 PM (Eastern Standard Time, UTC-05:00)  #    Comments - Trackback
.Net 2.0 | ASP.NET

ASP.NET 404 Errors on the Default.aspx Page

On a new server install, you can copy over the files for your web app and mysteriously get 404 errors. It's a simple configuration in IIS. By default the server is configured not to allow ASP.NET. You simply need to enable this in the IIS management console:

Posted: Wednesday, August 29, 2007 11:36:10 PM (Eastern Standard Time, UTC-05:00)  #    Comments - Trackback
ASP.NET | IIS

Web Services Authentication Gotcha

We had code in an ASP.NET page trying to call the Commerce Server Profiles web service that resides on the same physical box. The credentials we used were appropriately configured for Commerce Server using AzMan. For some reason, the code was failing with a 401: Unauthorized error. No matter what credentials we used, no luck. But if you ran the code from another box, it worked fine. Same credentials pointing to the service on that box, no errors.

Turns out the hosts file had an entry for the DNS name we were using, and mapped that name to 127.0.0.1, the loopback address. This was the gotcha. Apparently there is a loopback security feature that causes this behavior. There is a support article describing the effect. Essentially it is a security check to keep certain kinds of attacks at bay. The article suggests registry changes to disable it, but we took a different route.  In the short term, if the calling code accessed the web service via IP address (NOT 127.0.0.1) instead of DNS name the problem was circumvented. Meanwhile the network guru is working to get the actual DNS resolution to work.

Posted: Tuesday, July 03, 2007 11:45:21 PM (Eastern Standard Time, UTC-05:00)  #    Comments - Trackback
ASP.NET | Commerce Server | Security

Design with Users In Mind

We all spend lots of time delving into the technical aspects of our work, but don't forget who we are creating all these wonderful apps for. You need to design your apps to work for the users, not create apps that the users need to work to use.

Required Reading: Four Modes of Seeking Information and How to Design for Them

Posted: Wednesday, January 31, 2007 11:09:29 PM (Eastern Standard Time, UTC-05:00)  #    Comments - Trackback
ASP.NET | Programming | Winforms

ASP.Net Single Sign On - Same Domain

This post in the ASP.Net forums has been the best method for me to make a single-sign on work across different sites in the same domain.

You have to set the machine key in the web.config of all sites even on the same physical box.

Posted: Wednesday, October 11, 2006 6:11:18 PM (Eastern Standard Time, UTC-05:00)  #    Comments - Trackback
ASP.NET | Security

ASP.Net Windows Authentication and 401.2 Errors

I am working on an ASP.Net app that usesWindows authentication for users. I have a certain section of the app, the "Administration" set of pages that I want to exclude from certain roles of users. This is easy using a web.config file, but the unauthorized users get an ugly default 401.2 error page. I would like to have a custom page for that, and surpisingly there was not a ton of information out there on how to do it. In fact, more often than not the answer was "It can't be done."

I did find an acceptable answer in the forums at aspfree.com. Essentially the solution is to handle the Application_EndRequest event in the global.asax and check the status code and authentication of the user. Here is my version:

void Application_EndRequest(object sender, EventArgs e)
{
    if (Response.StatusCode == 401 && Request.IsAuthenticated && Request.Url.AbsoluteUri.Contains("Administration"))
    {
        Response.ClearContent();
        Server.Execute("../NoAccess.aspx?id=Administration");
    }
}

 

I don't believe this method will work with Forms Authentication, I ran across plenty of posts saying that it works differently.

Posted: Tuesday, April 04, 2006 7:28:29 PM (Eastern Standard Time, UTC-05:00)  #    Comments - Trackback
ASP.NET | Security

Securing the Connection String with the Machine Key

d.code asks about securing connection strings. If you are willing to deal with a little unmanaged code, you can use the machine key or a user key via Data Protection API (DPAPI) functions instead of hiding your key somewhere. I first heard about the machine key at a Microsoft Security Summit that was touring around the country last spring. I based my work on this example, and a relevant MSDN article. The upside is that you do not need to maintain your own key somewhere, the downside is that any application run on the computer could decrypt your string, and that an encrypted string cannot be passed between computers (or users if you go that route), so you have to create the encrypted strings during deployment and have access to run an executeable on the production computer during deployment.

I created a small Winforms program that I run during deployment to encrypt my connection strings using the machine key. It just has two text boxes and two buttons for encrypting and decrypting strings. Once the deployment is done the exe is removed from the production machine. If someone compromised the box, they would have to know you used DPAPI (which they could only determine by spending the time to decompile your assembly) and have their own exe ready or create one to decrypt the strings. A little obfuscation on your code would contribute to the security in this situation.

Posted: Friday, November 12, 2004 1:26:46 PM (Eastern Standard Time, UTC-05:00)  #    Comments - Trackback
ASP.NET | Security | Winforms

Using XMLHTTP in Javascript

I attended a terrific meeting tonight of GANG, the Great Lakes Area .Net user Group, based in the Detroit-area. The main presenter was Jason Beres who gave an excellent presentation on ASP.Net tricks, many of them utilizing Javascript. One of my favorites came up in discussion and I agreed to post the code snippet here.

I have used this techniqe mainly on classic ASP pages as a way to populate secondary dropdown lists before ASP.Net and control-based postbacks came along. It still works with ASP.Net... BUT...the secondary listbox is not in the Viewstate so it's value isn't set properly when accessing the control in server code on the postback where you handle the data. The secondary list's selected value is still in the Request.Form collection though. It is still a great technique to use to fetch data from the server without reloading the page.

The technique is IE-only as it uses an ActiveX object (XMLHTTP) in the Javascript. It also uses the XML DOM to parse the response from the server. In this example the server is an ASP page, but it could easily be a web service as well with the addition of one a little more code. This code is related to a previous post where I used VBA to enable Excel to be a web service client.

Here is the Javascript function:

/*  Purpose: Fill secondary list boxes with content
    Arguments:  oItem - the primary dropdownlist object
                sFieldName - The name of the field filling the secondary list
                oDestination - the secondary list object
    Returns: None, fills list box using MSXML*/
 
function fillSecondary(oItem, sFieldName, oDestination)
{
  var nValue = oItem[oItem.selectedIndex].value;
 
  var xmlHTTP = new ActiveXObject("Microsoft.XMLHTTP");
  xmlHTTP.open("POST", "./listboxesXML.asp", false);
  xmlHTTP.send('<' + sFieldName + '>' + nValue + '    
  var xmlDOM = new ActiveXObject("Microsoft.XMLDOM");

  xmlDOM.loadXML(xmlHTTP.ResponseText);

  if (xmlDOM.parseError != 0)
  {
   alert("Error occurred: " + xmlDOM.parseError.reason);
   return false;
  }

  var oNode = xmlDOM.documentElement.firstChild;
  var n=0;
  if(oNode != null)
  {
   //Clear out the secondary list box, it might already have items
   oDestination.length = 0;
   while (oNode != null)
   {
    oDestination[n] = new Option(oNode.text, oNode.attributes(0).text);
    n++;
    oNode = oNode.nextSibling;
   }
   if(n==1)
    oDestination.selectedIndex = 0;
  }
}

In the HTML on the page, add an OnChange handler to the primary dropdown list to call the Javascript when the user changes the value:

<FORM id=“Form1“>
<SELECT id=“Primary“ onchange=“fillSecondary(this, “Primary“, document.form1.secondary)“><OPTION value=“1“ selected>A list Item</OPTION></SELECT>
<SELECT id=“Secondary“ name=“Secondary“></SELECT>
</FORM>

The classic ASP page (listboxesXML.asp) called by the Javascript looks like this:

Dim xmlDOM   'XML DOM object
Dim oRS    'Recordset for child records
Dim sXML   'XML String returned to browser
Dim oNode   'DOM node containing data for searching
Dim sType   'The type/name of the unknown child node

set xmlDOM = Server.CreateObject("MSXML2.DOMDocument")
xmlDOM.async = false
xmlDOM.Load Request
xmlDOM.setProperty "SelectionLanguage", "XPath"
if xmlDOM.parseError = 0 then
 'Select the request node
    set oParent = xmlDOM.selectSingleNode("request")
    'Get the child of the request node
    set oNode = oParent.firstChild
    'Store the child's value
    sSearchValue = oNode.Text
    'Store the child's name
    sType = oNode.nodeName

    set oRS = Server.CreateObject("ADODB.recordset")

    'Fetch the appropriate data into the recordset using sType to tell which
    '  secondary list we should be fetching, in this case it is called “secondarytype“

    'ADO Code should be here, removed for brevity

 if not oRS.EOF then
  'Build a response XML string
  sXML = ""
  while not oRS.EOF
   sXML = sXML & "<" & sType & " id=""" & oRS.Fields(0).Value & """>"
   sXML = sXML & Server.HTMLEncode(ors.Fields(1).Value) & "   oRS.MoveNext
  wend
  sXML = sXML & "
"
  oRS.Close
 else
  sXML = ""
  sXML = sXML & "<" & sType & " id=""0"">"
  sXML = sXML & "None listed  sXML = sXML & "
"
  oRS.Close
 end if
 set oRS = nothing
Response.Write sXML
else
Err.Raise 1, "ParseError", "There was a parse error in the request."
end if

This can be very tricky to debug if you have any problems. Try looking at the xmlHTTP.ResponseText in an alert in the Javascript function to see any 500 errors generated by the server.

Posted: Thursday, June 17, 2004 2:43:28 AM (Eastern Standard Time, UTC-05:00)  #    Comments - Trackback
ASP.NET | JavaScript/AJAX

Viewstate Error and Macintoshes?

The public site I maintain is gathering 500 errors of the dreaded “The viewstate is invalid for this page and might be corrupted” type. Strangely enough, the only users seeing this error are Macintosh users, using IE! The problem is not isloated to a single page, it seems to be happening on any page, but not necessarily every page for that user. In some cases it is clear that the user was getting around fine (they were logged in as members to the site), and later the problem occurred.

I cannot find anything on the web except a solitary Usenet post of a person describing the exact same problem.

Here are the agent strings of the browsers recorded when the error occurred:

Mozilla/4.0 (compatible; MSIE 5.0; Mac_PowerPC)
Mozilla/4.0 (compatible; MSIE 5.17; Mac_PowerPC)
Mozilla/4.0 (compatible; MSIE 5.15; Mac_PowerPC)
Mozilla/4.0 (compatible; MSIE 5.16; Mac_PowerPC)

According to the log files, MacPPC users are just under 1% of the site users, so this problem is clearly not a showstopper, but I hate for any users to have a problem. I am hoping to get a Mac set up to test the problem locally, but the agent string doesn't give me any Mac OS versions....

Posted: Tuesday, April 13, 2004 1:33:07 PM (Eastern Standard Time, UTC-05:00)  #    Comments - Trackback
.Net 1.1 | ASP.NET

Burned by Trace

I found out the hard way that when using the trace on an ASP.Net page, the interaction of the trace observer is not totally separate from the page. I am working on a page where performance is a big issue, so I have been using Trace extensively to find the weak spots and shore them up. Essentially the page is searching both the Indexing Service and an Oracle database and finding where the two datasets intersect. Because the results are a grid of links, the user can click the links and then use the Back button to return to the results. Of course I was getting the dreaded “Warning: Page has Expired” message after using the back button. The page with the grid is the result of an HTTP post, so the post has expired. Clicking the Refresh button was the only way to get the data back initially. This is bad, as the page already was having performance problems, so rendering it again was not really an option. So I tried setting the page caching options to this:

<%@ OutputCache Duration="120" VaryByParam="None" Location="Client"%>

This should cause the page to cache on the client. No dice. No matter what I set, the http_cache_control header always reported “no-cache” in the trace results. I tried making the header changes in code. Again, no change. Still reporting “no-cache”. The simple solution to the problem is to turn off tracing on the page. Well, it turns out that with Trace=”true” in the @Page directive, the http_cache_control header is set to “no-cache” no matter what. When the Tracing is off, the “Warning: Page has Expired” message disappears and the page is back in control of setting the headers.

Also, a related tidbit I discovered when trying to solve this is that the page timeout is essentially set to infinity when compiling in debug mode.

Posted: Monday, March 29, 2004 1:07:41 PM (Eastern Standard Time, UTC-05:00)  #    Comments - Trackback
.Net 1.1 | ASP.NET | Indexing Service

Two Good Solutions for Radio Buttons in a Repeater

Basically, the repeater ignores the GroupName attribute and gives each radiobutton in each row a unique name so they no longer operate as a group. This is an acknowledged bug by Microsoft.

A short, elegant solution posted on the Usenet.

A free control to solve the same problem.

I posted about a similar problem previously with checkboxes and the Repeater, but it was a very different solution than the two I found here.

Posted: Thursday, March 25, 2004 7:17:33 PM (Eastern Standard Time, UTC-05:00)  #    Comments - Trackback
.Net 1.1 | ASP.NET

Query the Indexing Service with Ixsso and ASP.Net

As I am continuing to migrate our ASP app to ASP.Net, it has finally come time to address the Indexing Service search. It's a big feature in the application, and the transition of this piece needs to be seamless. We want to stick with Ixsso for the Indexing Service as opposed to using the Oledb driver. Ixsso is considered to be the faster of the two technologies, even using COM interop (See various of Hilary Cotter's comments in microsoft.public.inetserver.indexserver). Code snippets for using Ixsso with ASP.Net are pretty sparse compared to using Oledb, so I figured I should post mine.  First, I used the IDE to create a reference to the ixsso Control Library dll and let the IDE make the .Net wrapper for the COM object (christened Cisso by the IDE).

Imports Cisso
Imports System.Security.Principal
Imports System.Data.OleDb

 

Private Function GetIndexResults(ByVal Query As String) As DataTable Dim Q As New CissoQueryClass Dim util As CissoUtilClass Dim da As New OleDbDataAdapter Dim ds As New DataSet("IndexServerResults")
Q.Query = Query Q.SortBy = "rank[d]" Q.Columns = "filename, rank, write" Q.Catalog = "query://DocumentServer/Resumes" Q.MaxRecords = 1000 util.AddScopeToQuery(Q, "\", "deep") Q.LocaleID = util.ISOToLocaleID("EN-US") Dim impContext As WindowsImpersonationContext = impersonateAnonymous() da.Fill(ds, Q.CreateRecordset("nonsequential"), "IndexServerResults")
Q = Nothing util = Nothing impContext.Undo()
Return myDS.Tables("IndexServerResults")
End Function

The impersonateAnonymous function is described in a previous post of mine. In our case the anonymous user on the machine has appropriate privledges to query the remote Indexing Service, but the ASP.Net worker process does not so impersonation is in order for the function. That part is probably optional depending on the situation. The rest of it is not very tricky. I tried to fill the DataTable directly without the DataAdapter, but that didn't work. The CreateRecordset function of the CissoQueryClass returns an ADO recordset and I couldn't find a cast that worked. The DataAdapter seems to be doing the casting work during the call to Fill.

Posted: Wednesday, March 03, 2004 8:12:51 PM (Eastern Standard Time, UTC-05:00)  #    Comments - Trackback
.Net 1.1 | ADO.Net | ASP.NET | Indexing Service

Sharing Timeout Management with ASP.Net and ASP

I am in a situation where my application is mixed classic ASP and ASP.Net. I am currently migrating the classic ASP app page-by-page into ASPX as I am developing new functionality. When I have to touch an old page, I convert it.

In an attempt to make them appear to be one app to the user, I need to manage the timeout of the two applications to appear as one. The classic ASP app was using Session variables for this, but obviously that would not work easily in a mixed environment. Luckily, the ASP app is not heavily dependent on Session variables; it only uses a few that are set during login, and a couple on particular pages. So after logging in the login redirects to a classic ASP page to set the few necessary Session variables, and that page redirects to the home page.

  1. Login using the ASPX login page
  2. Create a cookie containing context
  3. Redirect to asp page setting session variables
  4. Redirect to ASPX home page

Initially, I thought it would be easy to manage the timeouts, just use the cookie generated by the ASPX login page. The classic ASP pages can check the cookie and update its expiration if needed. Of course, it was not that simple. The cookie that is created by the ASPX pages contains the authentication ticket generated by the current http context. Its expiration is essentially the same as the cookie, based on the time set in web.config. The problem lies with the classic ASP pages. They can update the cookie’s expiration, but not the ticket. If a user spends too much time using classic ASP pages, the ticket in the cookie will be expired next time the user visits an ASPX page, even if the cookie is good. The ASPX pages consider a user timed out if either the cookie has expired or the ticket has expired. They will need to login again without actually timing out simply due to using classic ASP pages for a little too long.

My solution results from the ratio of classic ASP to ASPX pages. Right now it is about 5 classic ASP per ASPX page. Using this ratio I changed the authentication ticket timeout to be 5 times as long as the cookie timeout. In order to make this work the ASPX application needs to manage the cookie created by ASPX pages instead of allowing it to occur manually. At the login page, I allow the ASPX cookie to be created as normal using my own CreateAuthCookie.

Public Shared Function CreateAuthCookie(ByVal UserID As String) As HttpCookie
    Const TicketLife As Double = 4
    Dim Timeout As String = ConfigurationSettings.AppSettings("Timeout").ToString
Dim principalText As String Dim buffer As New IO.MemoryStream Dim formatter As New Runtime.Serialization.Formatters.Binary.BinaryFormatter
    formatter.Serialize(buffer, HttpContext.Current.User)
    buffer.Position = 0
    principalText = Convert.ToBase64String(buffer.GetBuffer())
    'create a forms ticket
Dim ticket As New FormsAuthenticationTicket(1, HttpContext.Current.User.Identity.Name, _
DateTime.Now, DateTime.Now.AddMinutes(Double.Parse(Timeout) * TicketLife), _
False, principalText)

    'Encrypt the ticket
    Dim encTicket As String = FormsAuthentication.Encrypt(ticket)
    'This is the cookie used by both ASP and ASP.Net, ASP never retrieves the value
    Dim TheCookie As New HttpCookie(FormsAuthentication.FormsCookieName)
    TheCookie.Path = FormsAuthentication.FormsCookiePath
    TheCookie.Value = encTicket
    TheCookie.Expires =    DateTime.Now.AddMinutes(Double.Parse(Timeout))

    Return TheCookie
End Function

In the Global.asax, I added code to take the cookie, get the ticket from it and use the ticket to set the HttpContext.Current.User and update its expiration if necessary. If the cookie or the ticket has expired here it will automatically redirect to the login page as a function of ASP.Net.

Private Sub Global_AcquireRequestState(ByVal sender As Object, ByVal e As EventArgs) _
Handles MyBase.AcquireRequestState Dim cookie As HttpCookie = Request.Cookies.Get(FormsAuthentication.FormsCookieName) If Not cookie Is Nothing Then 'Cookie found, decrypt the value and recreate the authentication ticket
' and user context for the page
Dim ticket As FormsAuthenticationTicket Try If cookie.Value = "" Then
'For some reason the cookie exists but there is no value,
' remove the cookie try again
cookie.Expires = DateTime.Now.AddMinutes(-15) Response.Redirect("../login/login.aspx", False) Else ticket = FormsAuthentication.Decrypt(cookie.Value) End If FormsAuthentication.RenewTicketIfOld(ticket) Dim buffer As New IO.MemoryStream(Convert.FromBase64String(ticket.UserData)) Dim formatter As New Runtime.Serialization.Formatters.Binary.BinaryFormatter HttpContext.Current.User = CType(formatter.Deserialize(buffer), IPrincipal) Catch ex As Exception
'Could not set the ticket, remove the cookie cookie.Expires = DateTime.Now.AddMinutes(-15) Response.Redirect("../login/login.aspx", False) End Try End If End Sub

All my ASPX pages inherit from my own base page instead of System.Web.UI.Page. In the PreRender event for my base page my CreateAuthCookie gets set again and added to the Response. My base page has already taken the UserID from the HttpContext and set it as a page property.

Private Sub Page_PreRender(ByVal sender As System.Object, ByVal e As System.EventArgs) _
Handles MyBase.PreRender Dim AuthCookie As HttpCookie = CreateAuthCookie(Me.User_ID.ToString) Response.Cookies.Add(AuthCookie) End Sub

The classic ASP pages use an include file that just increments the cookie timeout. It also checks the status of the ASP session and resets it if the cookie is still good.


Dim xQString if Request.QueryString <> "" then 
xQString = "?" & Request.QueryString
end if
If
Request.Cookies("EnCoreUser") = "" then
'No cookie, session expired
Session.Abandon
Response.Cookies("EnCoreUser").Expires = DateAdd("n", -15, Now())
response.redirect "../login/login.aspx?ReturnUrl=" & _
Server.URLEncode(Request.ServerVariables("URL") & xQString)
else
'Got aspx cookie, check asp session
if Session("SessionId") <> Session.SessionID then
if Request.Cookies("asp") = "" then
response.redirect "../login/login.aspx?ReturnUrl=" & _
Server.URLEncode(Request.ServerVariables("URL") & xQString)
else
'Cookie is good but classic ASP session ended. Recreate the
' session without forcing another login.
Response.Redirect "../login/verify.asp?userid=" & user_id & "&url=" & _
Server.URLEncode(Request.ServerVariables("URL") & xQString)
end if
else
'All is well, so reset the cookie
Dim sTempx sTempx = CStr(Request.Cookies("EnCoreUser"))
Response.Cookies("EnCoreUser").Path = "/"
Response.Cookies("EnCoreUser").Expires = DateAdd("n", 35, Now())
Response.Cookies("EnCoreUser") = sTempx
end if
end if

As the ratio classic ASP pages to ASPX pages changes, all I need to do is reduce the multiplier to shorten the ticket length accordingly. Eventually, of course, all this work will get tossed out when there are no more classic ASP pages.

Posted: Wednesday, January 28, 2004 5:21:58 PM (Eastern Standard Time, UTC-05:00)  #    Comments - Trackback
.Net 1.1 | ASP.NET

Forms Authentication Problem

An ASP.Net site we created is having infrequent problems with logins using forms authentication. Essentially what happens is that the user attempts to login and is successful, but then is redirected back to the login page immediately. So it looks like an infinite loop of logins. We have been able to deduce that the cookie is related to the problem. If the user deletes their cookies in IE the problem goes away. The problem is very intermittent, so it is very difficult to reproduce. It is not generating 500 errors or errors in the logs. From extensive Googling, the best I can come up with is the fact that we allowed the cookie to persist across sessions, and the problem is related to that. So I changed the createPersistentCookie parameter to false:

FormsAuthentication.SetAuthCookie(nResult.ToString, False)

Of course, solving the problem is only a wait-and-see in this case, since I can't reproduce the problem directly. I thought our login code was pretty straightforward, letting ASP.Net do as much of the work as possible.


Imports System.Web.Security.FormsAuthentication
....
.... 'txtEmail, txtPassword are textboxes on the form, lblMessage is a label control Public Sub Login_Click(ByVal snd As System.Object, ByVal e As System.EventArgs) _
Handles LoginButton.Click
Dim NotRegistered As String = " is not a registered email address. “ & _
“Please use the Create A Profile link to register." Dim nResult As Integer
If Page.IsValid Then Dim sPassword As String





sPassword = HashPasswordForStoringInConfigFile(txtPassword.Text, "sha1") nResult = LoginResult(txtEmail.Text, sPassword) 'Validate against the database If nResult = -1 Then 'Not a registered user, display error message lblMessage.Text = txtEmail.Text & NotRegistered ElseIf nResult = -2 Then 'Bad password, set error message lblMessage.Text = "The password for " & txtEmail.Text & _
" is incorrect" ElseIf nResult > 0 Then 'Registered user, nResult is their ID number If Request.QueryString("ReturnUrl") <> "" Then 'Redirect to requsted page RedirectFromLoginPage(nResult.ToString, False) Else 'Go to My Jobs by default SetAuthCookie(nResult.ToString, False) Response.Redirect("../MyJobs/My_Jobs.aspx") End If
End if End If End Sub
Posted: Wednesday, January 07, 2004 1:36:54 PM (Eastern Standard Time, UTC-05:00)  #    Comments - Trackback
.Net 1.1 | ASP.NET | Security | Strange Problems

Checkboxes and the Repeater

I used a repeater for the first time in my app, and in this instance it is essentially a list of people with checkboxes to the left of their names so the user can select one or more of them and perform an operation (like delete, etc.). When using a datagrid, this is really easy because you can bind the person's ID as a hidden column, making it very easy to retrieve the primay key. This is not the case with the repeater or datalist as far as I can tell. To top that off, the asp:checkbox control does not have a value attribute where I could store the primary key, like the HTML version. But I needed the asp:checkbox functionality, not the HTML checkbox.

Again, this seemed like a common problem so I decided to perform a Google search. No good solution jumped out at me, until I read this thread from microsoft.public.dotnet.framework.aspnet.webcontrols. It is basically a guy griping about the lack of a value property for the asp:checkbox and a MS Rep apologizing for it. At this point I realized neither of these guys in the thread were “thinking in .Net” (and neither was I when I started my search). Everything is now object-oriented. If a class does not have a feature you need, add it! So that is what I did, and it works great:



Public Class RepeaterCheckBox
    Inherits CheckBox
    Property Value() As String
        Get
            Return CType(ViewState("value"), String)
        End Get

        Set(ByVal Input As String)
            ViewState("value") = Input
        End Set
    End Property
End Class


The important thing to remember here is to make sure the value is going into viewstate so it persists.

Posted: Wednesday, December 10, 2003 3:02:25 PM (Eastern Standard Time, UTC-05:00)  #    Comments - Trackback
.Net 1.1 | ASP.NET

One computer - Multiple Versions of IE

Thanks to Lockergnome, the Nov. 18th edition of their IT Professionals newsletter pointed out these two links:

The latter article explains how and why the technique works, and the former link has all the files you need zipped up and ready to install.

I have tried this out, and it seems to work. The different browsers are definitely behaving differently for me. I tried IE v4.01 and some complex client-side scripts did not work as I expected. I also experienced some wierdness with the Address bar, but all is OK now. This could not come at a better time as the test lab in the next aisle is going away as we are moving to a new location. Kudos again to Lockergnome, I consistently get some gems from their newsletters.

Posted: Wednesday, November 19, 2003 12:27:07 PM (Eastern Standard Time, UTC-05:00)  #    Comments - Trackback
ASP.NET

Multiple Select Results in ASP.Net

In our new ASP.Net app, we are using one list box that allows multiple-select. Recently, choosing one of the items in the list produced no results. But items would not be in the list if there were no results, so there must be a problem in the SQL statement related to the multiple-select list or the data.  In classic ASP, you would access the multiple items like this:

    Request.Form(i)(j)

where i is the form item and j is the result of the multiple-select. Unfortunately, that does not work in ASP.Net. If you access the form item directly in ASP.Net, like this:

    Request.Form(i)

it provides a comma-delimited list of the multiple form items. It's pretty easy to then split the list on the commas, and that was what we were doing. Ahh, the bad item in the list had a comma embedded in its text. So it was splitting inappropriately and creating a bad SQL statement for fetching results. This was the source of the problem here. There must be a way to access the items individually, but it was not obvious to me. I must have stared at the MSDN docs forever trying to find the answer. I had to try three or four different keyword searches on Google before I found an excellent explanation here.

The results of a mulitple-select list are also a NameValueCollection, just like the Request.Form. Once I found that, it was easy to get at the individual items. The entire collection is accessed like this:

    Request.Form.GetValues(i)

and the individual items are accessed like this:

    Request.Form.GetValues(i)(j)

I was really surprised at how difficult it was to find this information. Both of my favorite ASP.Net books, Essential ASP.Net by Fritz Onion and Programming ASP.Net by Dino Esposito both had no mention of the embedded NameValueCollection. This seems like it would be a common problem for developers using forms, but I did not find the answer to be as obvious.

Posted: Friday, November 14, 2003 1:22:40 PM (Eastern Standard Time, UTC-05:00)  #    Comments - Trackback
.Net 1.1 | ASP.NET

Impersonation

All right, now that I have done a bunch of talking, how about some code? I stumbled across this recently while pouring through MSDN. The code lets you impersonate any other user, provided you know the credentials. In our case we needed to become the IIS user so we could access files on a remote file server. In our situation, there are multiple web applications within our domain, so the network admin has set up the IIS sites to all use a common domain-wide anonymous user, so it is easier for him to manage permissions. As we are the first .Net project, the ASPNet user has no rights whatsoever on the network. We talked with the admin and he was not interested in giving the ASPNet users from a bunch of different web servers rights to other network resources. We looked at changing the ASPNet user credentials Machine.config, but this broke debugging locally immediately. So I looked into impersonation and found out how to impersonate the IIS user in code:

Imports System.Security.Principal

Function impersonateAnonymous() As WindowsImpersonationContext

     'Grab the current Http context
    
Dim context As HttpContext = HttpContext.Current

    'Set up a Service Provider based on this context
     Dim
iServiceProvider As iServiceProvider = CType(context, iServiceProvider)

     'Create a type which represents an HTTPContext
     Dim
httpWorkerRequestType As Type = GetType(HttpWorkerRequest)

     'Get the HttpWorkerRequest service from the service provider
     Dim
workerRequest As HttpWorkerRequest = _
         
CType(iServiceProvider.GetService(httpWorkerRequestType), HttpWorkerRequest)

     'Get the token passed by IIS from the workerRequest service
     Dim
ptrUserToken As IntPtr = workerRequest.GetUserToken()

     'Create a Windows Identity from the token
     Dim
winIdentity As New WindowsIdentity(ptrUserToken)

    'Send back the IIS identity
     Return
winIdentity.Impersonate

End Function

To use the function, simply call it like so before the code that needs proper permissions:

Dim impContext As WindowsImpersonationContext = impersonateAnonymous()

Now the subsequent lines of code operate in the context of the user assigned to IIS. And then when you are done impersonating:

impContext.Undo()

I based this function on some C# code I found in a Patterns & Practices document on MSDN: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnnetsec/html/thcmch10.asp

Posted: Tuesday, September 02, 2003 6:00:54 PM (Eastern Standard Time, UTC-05:00)  #    Comments - Trackback
.Net 1.1 | ASP.NET | Security

Migrating our ASP app to .Net

We are migrating an ASP app to ASP.Net, but not all at one time as my business customers within the company won't pay for that kind of time all at once. So we are migrating piece-by-piece as we develop new functions within the app. Our plan is to develop all new pages using .Net, and convert classic ASP pages as we have to touch them due to new development. To get the remaining pages converted, we are just going to squeeze them in as often as possible along with the new development, mostly on a one or two at a time basis. So probably of the next 6-12 months the ASP and ASP.Net applications will coexist and appear to the users to be one application.

Right now, we have done what we considered the basic conversion necessary to implement our plan:

  • Convert the login process to .Net. We are using Forms authentication since users are checked from our own database. We may migrate this to LDAP sometime in the future. My company uses Novell Netware for network login and GroupWise for email. I would love to hear from anyone who has a web app that can create appointments in GroupWise!!
  • Move frequently used Session variables to cookies. Luckily, we were not using lots of Session variables in the classic ASP. We encrypt all the data written to the cookies so spoofing is harder. We are currently not a web farm, but that is also in our future so the Session variables need to go anyway.
  • Create a common time-out scheme between the applications using a cookie. The ASP app times out after 35 minutes of inactivity for what the HR folks call “security“, as the app contains lots of personal information about both employees and non-employees.
  • Convert common functions to .Net (like checking user roles, encryption, database access, search engine, etc.)
  • Convert ASP includes for page structure into .ascx files.

Of course, none of this has gone into production yet (sigh...). It is on the test sever and hopefully will roll out by the end of the month.

Posted: Tuesday, September 02, 2003 5:22:45 PM (Eastern Standard Time, UTC-05:00)  #    Comments - Trackback
.Net 1.1 | ASP.NET | Novell | Oracle