Thursday, November 19, 2009

Handling Extensionless URIs in a WebForms Application

[Edit: I wrote this just a few days before finding out that WebForms 4 does routing out of the box]

ASP.Net MVC is all the rage, but I just needed to add extensionless functionality to a WebForms application. I'm going to host this application in IIS7, so that's how I'm describing it; there is more work involved for IIS6.

This all seems obvious now, but it took me awhile to figure out.

I started by adding a class that supported IHttpHandler. I need to have a handler (rather than a module) because I want to set a value in session, and that's not yet available within a module (at that phase in the IIS pipeline).


public class OnDemandHandler : IHttpHandler, System.Web.SessionState.IRequiresSessionState
{
public void ProcessRequest( System.Web.HttpContext context )
{
; // add something to session
}

public bool IsReusable { get { return true; } }
}

<httphandlers>
<add verb="GET" path="*" type="MyNamespace.OnDemandHandler, MyDll" validate="false">



That works fine, but it handles everything. How do I handle aspx pages, or images, or anything else? Copy in all the base handlers from the web.config at the machine level to my web.config? Move my httpHandler config element from my web.config to the machine web.config so it can fall last in order there and only run after all the base handlers have run? Use a module instead of a handler? I was having a hard time figuring out what to do until I decided to go check out a config of an MVC project. After all, they are doing what I want to do:


<httphandlers>
<add verb="*" path="*.mvc" validate="false" type="System.Web.Mvc.MvcHttpHandler, System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
</httphandlers>
<httpmodules>
<add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule, System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
</httpmodules>



Again, it seems obvious now; use a module that checks for extensionless URIs, tack on my own pre-determined extension (just like they're using *.mvc), and send it off to my handler. For URIs with extensions, let them pass on normally.

So here's how it looks in the end:


public class OnDemandModule: IHttpModule
{

#region IHttpModule Members
public void Dispose()
{
}

public void Init( HttpApplication app )
{
app.BeginRequest += new EventHandler( OnBeginRequest );
}
#endregion

private void OnBeginRequest( object src, EventArgs e )
{
HttpContext context = ( src as HttpApplication ).Context;
var uriPath = context.Request.Path;
if ( UriIsExtensionless( uriPath ) )
{
/// Add the .inst extension so our handler will process it
context.Response.Redirect( context.Response.ApplyAppPathModifier( uriPath + ".inst" + context.Request.Url.Query));
}
}

/// <param name="uriPath">like /blah or /something.aspx. Does not contain possible query params</param>
/// <returns></returns>
private bool UriIsExtensionless( string uriPath )
{
var result = false;
if ( uriPath[0].Equals( '/' ) )
{
var restOfURI = uriPath.Substring( 1 );
if ( restOfURI.Length > 0 )
{
return !restOfURI.Contains( '.' );
}
}
return result;
}

}

public class OnDemandHandler : IHttpHandler, System.Web.SessionState.IRequiresSessionState
{
public void ProcessRequest( System.Web.HttpContext context )
{
context.Response.Write("OnDemandHandler fired");
context.Response.End();
}

public bool IsReusable { get { return true; } }
}

<!-- for cassini -->
<system.web>
<httphandlers>
<add verb="GET">
path="*.inst"
type="MyNamespace.OnDemandHandler, MyAssembly"
validate="false" />
</httphandlers>
<httpmodules>
<add name="OnDemandModule" type="MyNamespace.OnDemandModule, MyAssembly">
</httpmodules>
</system.web>

<!-- for IIS7 -->
<system.webserver>
<modules runallmanagedmodulesforallrequests="true">
<add name="OnDemandModule">
type="MyNamespace.OnDemandModule, MyAssembly" />
</modules>
<handlers>
<add name="OnDemandSurvey">
verb="GET"
path="*.inst"
preCondition="integratedMode"
type="MyNamespace.OnDemandModule, MyAssembly" />
</handlers>
</system.webServer>

Monday, November 16, 2009

Getting the SQL (and other goodness) from a running transaction

I had run a bunch of SQL scripts against a SQL Server database during a deployment. All tests after the updates checked out. I went to close SSMS and got the message, "There are uncommitted transactions. Do you wish to commit these transactions before closing the window"?

WTF? How could I have failed to commit a transaction yet testing all checked out? Well that's a mystery I haven't yet figured out, but I did find a good query (that I had to alter somewhat to make work) for getting the text of the command that had not yet been committed:


SELECT
  st.Session_id, req.Blocking_Session_ID [Blocker], req.Wait_Type,
  req.Wait_Time [WaitTimeMS], req.Wait_Resource, req.open_transaction_count,
  req.percent_complete, dt.transaction_id, dt.database_transaction_begin_time,
  case when dt.database_transaction_type = 1 then 'RW'
    when dt.database_transaction_type = 2 then 'R'
    when dt.database_transaction_type = 3 then 'Sys'
    else 'Unknown'
end [TranType],
  case when dt.database_transaction_state = 1 then 'Not Initialized'
    when dt.database_transaction_state = 3 then 'Initialized, but no logs'
    when dt.database_transaction_state = 4 then 'Generated logs'
  when dt.database_transaction_state = 5 then 'Prepared'
    when dt.database_transaction_state = 10 then 'Committed'
    when dt.database_transaction_state = 11 then 'Rolled Back'
    when dt.database_transaction_state = 12 then 'In process of committing'
  else 'Unknown'
end [TranState],
  req.Status, req.Command, stxt.objectid [ExecObjID],
(SUBSTRING(stxt.text, req.statement_start_offset/2,( CASE WHEN req.statement_end_offset = -1 then LEN(CONVERT(nvarchar(max), stxt. text)) * 2
        ELSE req.statement_end_offset
      end -req.statement_start_offset)/2)) [SubText],
  stxt.text, req.statement_start_offset
FROM
  sys.dm_tran_database_transactions dt (nolock)
  inner join sys.dm_tran_session_transactions st (nolock) on dt.transaction_id = st.transaction_id
  inner join sys.dm_exec_requests req (nolock) on st.transaction_id = req.transaction_id
  CROSS APPLY sys.dm_exec_sql_text(req.sql_handle) [stxt]
where
  dt.database_id = db_id() and st.is_user_transaction = 1


This query wouldn't work in my situation (would barf "Incorrect syntax near '.'" at the second to last dot), but it will work on a database with compatibility level 90 or better.