Andornot Consulting Inc.
Home Page
Home Page
 |  | 

Monday, January 28, 2008

Avoid Application Pool Conflicts

In IIS 6.0, different versions of the .NET framework can co-exist on the same website but must use separate application pools. Once a .NET process "grabs" the app pool, other .NET processes are denied its use and report generic server errors in the browser. Which .NET process gets an app pool first depends on which is first requested after an application pool recycle. For example the .NET 2.0 process might get the application pool first, and all .NET 1.1 applications that rely on the application pool fail to run; end users see generic server errors that do not report what is really going on.

Installing Inmagic Webpublisher or Genie

Although Webpublisher inmagicbrowse and Genie inmagicgenie virtual directories are correctly set to use .NET 2.0, the installer lets the parent website determine which application pool to use. A typical Windows 2003 Server might have .NET 1.1 and DefaultAppPool as the default on new websites. If there are any .NET 1.1 applications already on the server that rely on DefaultAppPool, then inmagicbrowse and inmagicgenie are setting the server up for an application pool conflict.

Separate Application Pools

Set up a separate application pool just for Genie, and one for Webpublisher. This way you avoid any conflict with applications currently on the server, but also allow for changes in .NET dependency in future: when Genie starts using .NET 3.5 you won't have to worry about re-organizing application pools. It's also good practice to isolate applications like this so that when one does go down, it doesn't take other applications with it.

Labels: , , ,

Thursday, November 22, 2007

Short Persistent URLs for Database Queries

Inmagic Webpublisher canned query URLs can be very very long, so I wrote a .NET HttpHandler that shortens them and bolsters their persistence into the bargain.

Here's an example of a very long Webpublisher query string that displays a single record from the sample cars database.

http://localhost/dbtw-wpd/exec/dbtwpub.dll?AC=GET_RECORD&XC=/dbtw-wpd/exec/dbtwpub.dll&BU=&TN=cars&SN=AUTO29781&SE=267&RN=0&MR=0&TR=0&TX=1000&ES=0&CS=1&XP=&RF=&EF=&DF=&RL=0&EL=0&DL=0&NP=1&ID=&MF=&MQ=&TI=0&DT=&ST=0&IR=1&NR=0&NB=0&SV=0&SS=0&BG=&FG=&QS=&OEX=ISO-8859-1&OEH=ISO-8859-1
 

See? Looooooooong. Not at all memorable, and difficult to display or pass around. But with the handler in play, I can now shorten it:

Both get the same result:

Porsche

Porsche

A classic best-seller, the Porsche 911's anodized aluminum tub chassis has a strong front brace for extra support. A steel mount holds the engine snugly in place. Mounted on 25-degree caster blocks, the A-arms are longer than most and can be adjusted for low-speed steering or short tracks. The upper links have coated turnbuckles which can also be adjusted for different terrains. The rear suspension uses extra-long arms and variable shocks. The engine is side mounted and sits lower than many models. The clutch, which happens to double as the brake drum, is mounted on the crankshaft.

Configuration

In the case of the short URL, the original query remains valid, but is held in the application's web.config:

<PermanentUrlSettings>
    <queries>
        <add name="mycarquery" uniqueIDField="Product-Number" queryParameters="/dbtw-wpd/exec/dbtwpub.dll?AC=qbe_query&amp;TN=cars" />
    </queries>
</PermanentUrlSettings>

The original query is called upon with an alias: qn=mycarquery. The id parameter is appended to focus the query to a single record, or, if no id parameter, the base query is run as-is.

The short URL path is /shorturl.ashx, but this is completely imaginary. And configurable in web.config:

<httpHandlers>
    <add verb="GET" path="shorturl.ashx" type="Andornot.Web.PermanentUrlHandler"/>
    <add verb="GET" path="/shorturl" type="Andornot.Web.PermanentUrlHandler"/>
    <add verb="GET" path="whatever/" type="Andornot.Web.PermanentUrlHandler"/>
</httpHandlers>

Using the examples above, any one of the following would be valid.

http://localhost/shorturl.ashx?qn=mycarquery

http://localhost/shorturl?qn=mycarquery

http://localhost/whatever/?qn=mycarquery

None of the paths exist on disk, which is what an HttpHandler is all about. The handler hijacks the request to any path bound to it, whether the path exists on disk or not.

Advantages

Mapping entire queries to simple aliases has some immediate and obvious advantages:

  • Short URLs.
  • URLs are friendlier, more memorable, and hackable. (If you are trying to make your information available this is a *good* thing.)
  • You can map almost any URL path you want to the handler, in order to organize query paths into pleasing hierarchies of your own devising:
    • e.g. /catalog/queries?qn=mycatalogquery
    • /archives/photos?qn=myphotoquery
    • etc.
  • You can define and update queries in one central location.
  • URLs become more persistent. Modifications to queries will not break URLs already in the wild:
    • Switch from Dbtext to Content Server without breaking any canned queries
    • Switch display forms or any parameter
    • Switch hostname, even

Labels: , , ,

Friday, October 05, 2007

ASP.NET AJAX Funky Exceptions Part II

Back in the summer I described an issue with ASP.NET AJAX. The session solution dealt with things well enough (and note the update to simply disable session in a page-by-page basis for all but the pages requiring reading or writing to the session), but I'm still getting the occasional SystemWeb.HttpException where there's a problem with the RoleManager module:

Server cannot modify cookies after HTTP headers have been sent.

Stack trace: at System.Web.HttpCookieCollection.Add(HttpCookie cookie) at System.Web.Security.RoleManagerModule.OnLeave(Object source, EventArgs eventArgs)

Of course, because the user doesn't get what's going on (the user just gets the javascript alert box mentioned previously as I haven't intercepted the ASP.NET AJAX thrown error in this application - for this reason, I will be in all future applications), this exception is usually thrown multiple times until the user navigates to another page and then returns to try again. Some users are quite persistent (I recall one being close to 10 times) which would be humorous if it wasn't just so cruel and bad.

It turns out I've had to disable role caching as per the article I mentioned in the last post until either the ASP.NET Role Manager is fixed or I decide to write/find a custom role module. What's interesting is that the roles are not ever being changed in the pages throwing the exception! Harumph. Back to looking at jQuery from here on in.

Has anyone come up with a solution for this so that the roles data store is not being hit on every page load?

UPDATE: As per the comments below, apparently this has been fixed in ASP.NET 3.5, but there are no plans to back port the fix to 2.0.

Labels: ,

Wednesday, July 18, 2007

Testing websites that use SMTP email on Vista

Many of the ASP.NET web applications we build use SMTP to send email for one reason or another. Order confirmations, mostly, or selected search results.

Testing code that sends email has always been a pain. We have servers with SMTP service that I could point at from my development workstation, but their various restrictions have been, well, restricting. Nor did I ever like the idea of letting SMTP run openly on my local machine in XP. Not that I had much choice, because a) I had to ensure that the code I wrote followed through with the email send, and b) I wanted to view the email as email to ensure it looked the way it ought.

So along comes Vista with IIS 7. And SMTP is not included. It is included with Longhorn Server 2008 with IIS 7 (apparently), but not Vista. I now no longer have the choice of running SMTP locally. Disaster!

Well, as it happens, I'm fine. I'm better than fine, because I stumbled across a better solution all around. I use a pickup directory location.

Using a pickup directory location lets me specify a folder on my local machine for the email generated by System.Net.Mail. It's not sent anywhere, it's just dumped in that location as a *.eml file which can be viewed by Vista's built-in Windows Mail.

The best thing is, I don't have to change my code in any way to make this happen. I only need to add a snippet to the web.config as follows, identifying the (absolute) directory for pickup:

<system.net>
  <mailSettings>
    <smtp deliveryMethod="SpecifiedPickupDirectory">
      <specifiedPickupDirectory 
pickupDirectoryLocation="v:\inetpub\mailroot\pickup"/>
    </smtp>
  </mailSettings>
</system.net>

I did have to make sure that the ASP.NET worker account, NETWORK SERVICE, had read/write access to that location.
And here's the result. One double-click and I get to see it as it would appear in my inbox:
CropperCapture[17]
Probably other developers are like, well *duh*, but this was a new and very pleasant discovery for me.

Labels: , , , ,

Monday, July 09, 2007

ASP.NET AJAX and Sys.Webforms.PageRequestManagerServerErrorException

Using ASP.NET AJAX extensively in my latest project I've been sporadically running into the Sys.WebForms.PageRequestManagerParserErrorException. It got to the point that I was contemplating ripping out ASP.NET AJAX completely until this known issue had been ironed out. The various causes for this error are mentioned many different places, but for some samples, go here, here, and here.

Quoting from Eilon Lipton's blog posting, this particular exception is very common and can be caused by any one of the following:

  1. Calls to Response.Write():
    By calling Response.Write() directly you are bypassing the normal rendering mechanism of ASP.NET controls. The bits you write are going straight out to the client without further processing (well, mostly...). This means that UpdatePanel can't encode the data in its special format.
  2. Response filters:
    Similar to Response.Write(), response filters can change the rendering in such a way that the UpdatePanel won't know.
  3. HttpModules:
    Again, the same deal as Response.Write() and response filters.
  4. Server trace is enabled:
    If I were going to implement trace again, I'd do it differently. Trace is effectively written out using Response.Write(), and as such messes up the special format that we use for UpdatePanel.
  5. Calls to Server.Transfer():
    Unfortunately, there's no way to detect that Server.Transfer() was called. This means that UpdatePanel can't do anything intelligent when someone calls Server.Transfer(). The response sent back to the client is the HTML markup from the page to which you transferred. Since its HTML and not the special format, it can't be parsed, and you get the error.

The problem was I wasn't doing any of the above (who uses Response.Write in an ASP.NET application these days?) and I was still sporadically encountering the error - a show stopping error I might add. An error that is popped up in a javascript warning box completely undecipherable to the end user leaving an empty/useless/castrated UpdatePanel in its wake. This of course leaves the end user feeling likewise empty/useless/castrated (to say nothing of the developer).

This post here indicates that there is a problem with the RoleMangerModule or any time you set a cookie to the response in an AJAX callback, which can only be solved by doing one of the following:

  1. Disable caching of cookies in the RoleManager. (yuck)
  2. Handle the Application's Error event to clear the error from the Context selectively (eek).
  3. Disable EventValidation in the web form.
    <%@ Page Language="C#" EnableEventValidation="false" %>
    (gulp)

None of the above are entirely reasonable solutions (especially the last two), and the worst part was that my test page was just a simple contact page that did not change/set roles or cookies, or response.write, or set anything in the session, and wasn't receiving any Unicode character input, or even requiring a user to be logged in, or writing anything to the trace, or anything beyond the basics. And still it blew up. But only occasionally.

In order to faithfully reproduce the error, I finally determined that it must have something to do with sessions as it would only occur if the app pool had recycled and all browser windows had been closed. So, based on one of the comments in one of the above posts, even though I'm not touching session on one of the problem pages, I tried a hack in one of the problem page's Page_Load:

Session["FixAJAXSysBug"] = true;

And lo and behold, we're good to go! So even though I am not using Session on the problem page it must be attempting to set the initial session cookie using the Update Panel callback. So the solution is to make sure the initial session is set before any Update Panel callback takes place. How this got through into production is beyond me.

So if you're sporadically encountering the Sys.Webforms.PageRequestManagerServerErrorException, it could be for any of the above reasons or the fact that your dog/cat/stuffed teddy bear is sitting too close to your monitor. But give the last one a try in every page utilizing AJAX if you're using sessions in your application.

UPDATE: If the problem pages aren't even using session, just turn session off for the page:

<%@ Page EnableSessionState="false" ... %>

Or better yet, set it in your base class to always be off, and turn it on for the pages where you need it on.

UPDATE II: Further developments.

Labels: , ,

Thursday, June 28, 2007

ResolveUrl vs. ResolveClientUrl

The .NET Control class methods ResolveUrl and ResolveClientUrl both take a relative URL as a parameter and return relative URLs for client browser use. So what's the difference between them?

In simple terms, ResolveClientUrl returns a path relative to the current page, and ResolveUrl returns a path relative to the site root. Both methods are particularly useful when passing in a relative URL prefaced with the tilde (~) to indicate the application root.

Let's say we have an image in a layout directory, and a page which needs to pass a usable relative URL for the image to the client browser.

image http://www.andornot.com/layout/images/andornotLogo.gif

page http://www.andornot.com/Products/Default.aspx.

Here is what we would get if we called the two methods from our Default.aspx page.

Page.ResolveUrl("~/layout/images/andornotLogo.gif")
"/layout/images/andornotLogo.gif"
Page.ResolveClientUrl("~/layout/images/andornotLogo.gif")
"../layout/images/andornotLogo.gif"
There is more to it, of course, particularly since these are Control methods, not exclusively Page methods. It's pretty straightforward to see how paths relative to a page resolve, but not necessarily so obvious when the control is being instantiated in a UserControl or MasterPage.

ResolveUrl uses the Control.TemplateSourceDirectory property to do its job, and that property value is the virtual directory of the Page or UserControl that contains the current server control.

ResolveClientUrl returns a URL relative to the folder containing the source file in which the control is instantiated.

Labels: ,

Public Hotfix Patch Available for Debugging ASP.NET on IIS7

Mentioned first a couple posts back, there is now a public hotfix patch available for debugging ASP.NET on IIS7 that you can download here. The reason for this post is that Scott Guthrie just wrote a post that gives a much better explanation of the problem and fix.

Labels: , , ,

Wednesday, June 27, 2007

Type-safe access to the current page's URL

Virtually every web project you work on requires that you get the current page's complete URL. Years ago, I got tired of using non-type-safe ways of munging together various Request.ServerVariables to get it (anyone remember stuff like "If Request.ServerVariables("HTTPS") = "on" Then..."?) and with .NET there had to be a better way. Due to a dearth of non-Request.ServerVariables examples out there back then, it took trusty trial and error Response.Write tests and the MSDN help to settle on the following:

Request.Url.Scheme + Request.Url.SchemeDelimiter + Request.Url.Authority + Request.Url.PathAndQuery

While I never understood why I still had to parse these together (and I sporadically investigated alternatives), it nevertheless felt so much better than parsing together strings like "://" with icky stuff like Request.ServerVariables("SERVER_NAME").

But yes, there had to be an even better way: a way that resisted forgetfulness and the ensuing battle with more than vague intellisense descriptions. Since I'm slow, it was only today that I found it. My my, sometimes I do miss the obvious:

Request.Url.ToString()
returns the canonically unescaped form of the URI (i.e. "http://www.example.com:80//thick and thin.htm")

and/or

Request.Url.AbsoluteUri
Returns the canonically escaped form of the URI (i.e. http://www.example.com:80//thick%20and%20thin.htm)

And yes, I had tried out AbsoluteUri before (I must have, right?), but for some reason, I still missed it.

Labels: ,

Monday, January 08, 2007

ASP.NET 2.0 Profile with Web Application Project Gotcha

When using Web Application Project with Visual Studio 2005 instead of Website Project, the profile personalization feature new to ASP.NET 2.0 doesn't "just work" the way all the articles describing Membership and Profile say it does. The strongly-typed ProfileCommon class is not auto-generated and Intellisense tells you it has no idea what you're talking about when you attempt to access HttpContext.Profile from code.

ScottGu reveals (full article):

VS 2005 Web Application Projects don't automatically support generating a strongly-typed Profile class proxy. However, you can use this free download to automatically keep your own proxy class in sync with the profile configuration.

I got quite frustrated, enough to bang the desk, but fortunately not enough to emit a Primal Yodel. As usual, Super Ted came to the rescue and pointed me to the above download.

UPDATE June 25, 2007 - The gotdotnet site in the "free download" link above has shut its doors, possibly forever, so here is a copy of the Web Profile Generator for your downloading pleasure: Web Profile Generator download.

UPDATE II from Ted, July 4, 2007 - As of yesterday, the Web Profile Generator lives again on CodePlex: http://www.codeplex.com/WebProfile

Labels: , ,