Friday, 19 February 2010

URL Routing Niggles

I've been playing with URL re-routing in Web Forms recently. Although originally supplied as part of the MVC framework the classes can now be used in standard web forms so you can have nice, neat, copy-and-paste friendly, search engine optimised URLs in your web forms application.

The basics of how to do this are well documented, so I won't bore you with them. If you don't know how it's done, I'll let 4-Guys bore you instead.

Trying to implement it I ran into a couple of small hurdles though, which I thought I'd document for reference. It's a little embarrassing really because both of these things are quite clear if you read the documentation properly instead of skimming and getting down to trying it out.

Optional & Extensible Parameters

The first is how to make parameters optional. Let's say for example that I have a search function with two parameters: firstname and surname. I might want the url for that search function to look like this:
/people/search/(searched text for firstname)/(searched text for surname)/
Let's further assume that I have various other pages I want to map inside the people folder, so things like:
/people/summary/
/people/management/

So I define my virtual URL like this:

Dim urlPattern As String = "people/{action}/{firstname}/{surname}"

And as soon as I do that, /people/summary/ fails. It's expecting a firstname and a surname as part of the URL and doesn't recognise the address.

To solve thisWe add a dictionary to the route value, like so:

reportRoute = New Route(urlPattern, New ReportRouteHandler)
reportRoute.Defaults = New RouteValueDictionary(New With {.firstname = "", .surname = ""})

This instructs the route that the firstname and surname parameters can be replaced with a default - in this case an empty string - if they're missing. And of course this being a dictionary you can add as many key/value pairs as you need.

But there's still one more thing. What if we might want to include some additional information in that URL, or even just make sure that longer URLs fail gracefully? At the moment /people/search/matt/j/thrower/ will fail. To solve this, add an asterisk to the final part of your virtual URL:

Dim urlPattern As String = "people/{action}/{firstname}/{*surname}"

And voilĂ ! The * indicates that the final parameter is extensible and can be of any length.

Getting Values from Parameters in Target Pages

The other thing that you'll probably want to do with this is to have a target page read the items in the url parameter collection. Right now you can access the values in {action}, {firstname}, {lastname} and so on inside route handling functions, but the target web form won't know about them.

My first approach to this was to change the parameters into querystrings and include them in the redirect part of the route handler:

Dim handlerUrl As String
Dim hand As Web.IHttpHandler
Select Case CStr(routeData.Values("action")).ToLower()
Case "search"
handlerUrl = String.Format("/people/search.aspx?firstname={0}&surname={1}", routeData.Values("firstname"), routeData.Values("surname"))
End Case

hand = DirectCast(System.Web.Compilation.BuildManager.CreateInstanceFromVirtualPath(handlerUrl, GetType(Web.UI.Page)), Web.IHttpHandler)

Which blows up in your face because the querystring isn't a valid virtual path.

The way round this is to utilise a handy collection in HttpContext called Items, which allows you to store key/value pairs of data:

Dim handlerUrl As String
Dim hand As Web.IHttpHandler
Select Case CStr(routeData.Values("action")).ToLower()
Case "search"
handlerUrl = "/people/search.aspx"
For Each urlParm In requestContext.RouteData.Values
requestContext.HttpContext.Items(urlParm.Key) = urlParm.Value
Next
End Case

hand = DirectCast(System.Web.Compilation.BuildManager.CreateInstanceFromVirtualPath(handlerUrl, GetType(Web.UI.Page)), Web.IHttpHandler)

And you can pick these up in your target page like so:

Dim firstname As String = HttpContext.Current.Items("firstname").ToString
Dim surname As String = HttpContext.Current.Items("surname").ToString

Friday, 12 February 2010

Quey across a join in nHibernate

My god, I don't think I've ever come across any framework with quite such a poor popularity-to-documentation framework as Fluent nHibernate. Everyone is using it, but no one writes down how.

Stupid nHibernate.

So, after spending ages trying to get something as simple as a rowcount, today I have spent an equally frustrating period trying to get a results set which does a logic OR operation across a joined table. Something as petty as this, in SQL terms:


SELECT * from product p
inner join sku s on s.id = p.id
WHERE p.Name like '%widget%'
OR s.SkuCode like '%wd_%'


The issue arises because even though my mappings are correct, nHibernate throws a wobbler at you if you start with Product and try and get it to recognise that it should be able to filter on a field in the joined sku table. This, for example, compiles but fails horribly even though to my mind it's the intuitive way of presenting the above query in nHibernate:


Dim results As ArrayList = session.CreateCriteria(Of DataTransferObjects.Product) _
.Add(Expression.Or( _
Expression.Like("Name", "widget"), _
Expression.Like("SkuCode", "wd_"))) _
.List()


No, what you have to do is set up a CreateAlias to the table and use that:

Dim results As ArrayList = session.CreateCriteria(Of DataTransferObjects.Product) _
.CreateAlias("Skus", "sku") _
.Add(Expression.Or( _
Expression.Like("Name", "widget"), _
Expression.Like("sku.SkuCode", "wd_"))) _
.List()


Certainly not the way I'd expect a "highly intuitive", productivity-enhancing framework to function. Not by a long shot.

I could have done the required work by creating a couple of custom data handling objects and accompanying T-SQL stored procedures in about ten minutes. To do the same thing in nHibernate has taken two days. What's that about enhancing productivity?

nHibernate Projections in VB.net

Well, this has lead me a merry dance, I can assure you. You'd think it was pretty straightforward to do something as simple as a row count in nHibernate, wouldn't you? Well, in point of fact it actually is pretty simple but it's just not well documented anywhere. Particularly perplexing is the fact that most of the examples seem to have been ripped straight from the Java hibernate implementation and I'm not entirely sure this will even port to C# and work, let alone translate to VB (I haven't tried in C#).

Anyway, just in case some poor soul has the same problem and gets stuck, this is how you do it:


Dim pResults As IList = session.CreateCriteria(GetType(dto.Product)) _
.SetProjection(Projections.RowCount()).List()


Of course "dto.Product" is a custom class I'm using that's specific to what I'm trying to do, so stick your own in there and you should be good to go.

Tuesday, 31 March 2009

Date Conversions in SQL

Does what it says on the tin. I find this a particularly handy trick to know when trying to copy & paste SQL data from Query Analyser into Excel, because the default date/time format causes Excel to scramble the dates into nonsense.

Stupid Excel.

Anyway, on with the show.


CONVERT(CHAR(19),GETDATE()) -- gives 31 March 2003 11:19
CONVERT(CHAR(11),GETDATE(),106) -- gives 31 March 2003

Thursday, 12 March 2009

A SQL Split function

This is so useful it's beyond me why it isn't adopted as standard functionality. Maybe it is in SQL Server 2008. In the meantime, here's how I do it.


CREATE FUNCTION fnSplit(@Data nvarchar(4000),
@Delimiter varchar(10) = ',')
RETURNS @tblSplit TABLE

(ItemId int IDENTITY(1, 1) NOT NULL PRIMARY KEY,
Item nvarchar(4000) NULL)

AS

BEGIN

DECLARE @Delimiter2 varchar(12),
@item nvarchar(4000),
@iPos int,
@DelimWidth int

--had to do this cuz if they send in a > 9 char delimiter I could not pre and post append the % wildcards
SET @Delimiter2 = @Delimiter
SET @Delimiter2 = ISNULL(@Delimiter2, ',')
SET @DelimWidth = LEN(@Delimiter2)

IF RIGHT(RTRIM(@Data), 1) <> @Delimiter2
SELECT @Data = RTRIM(@Data) + @Delimiter2

IF LEFT(@Delimiter2, 1) <> '%'
SET @Delimiter2 = '%' + @Delimiter2

IF RIGHT(@Delimiter2, 1) <> '%'
SET @Delimiter2 = @Delimiter2 + '%'

SELECT @iPos = PATINDEX(@Delimiter2, @Data)

WHILE @iPos > 0
BEGIN
SELECT @item = LTRIM(RTRIM(LEFT(@Data, @iPos - 1)))
IF @@ERROR <> 0 BREAK
SELECT @Data = RIGHT(@Data, LEN(@Data) - (LEN(@item) + @DelimWidth))
IF @@ERROR <> 0 BREAK
INSERT INTO @tblSplit VALUES(@item)
IF @@ERROR <> 0 BREAK
SELECT @iPos = PATINDEX(@Delimiter2, @Data)
IF @@ERROR <> 0 BREAK
END

RETURN
END



You call it like this:


select item from [reference]..fnSplit( @variable, ',' )


Where @variable is a varchar containing your comma-delineated string. Useful for sticking directly into IN clauses.

Tuesday, 10 March 2009

Reading XML with XPath

There's more than one way to read an XML file in dot net, but the XPath model doesn't seem to be terribly widely documented. Which is a shame because although it's a little unintuitive, it offers some functionality that doesn't come with other tools.


using System.Xml.XPath;

XPathDocument xDoc = new XPathDocument(“filepath”);
XPathNavigator xNav = xDoc.CreateNavigator();

Xnav.MoveToRoot();
Xnav.MoveToFirstChild();

do
{
//do stuff .. like xNav.GetAttribute(""AttributeName", "");
}
while (Xnav.MoveToNext());


So, load your xml file by replacing "filepath" with the path of the - ahem - file you want to load. MoveToRoot and MoveToFirstChild do exactly what you'd expect them to, and you can obviously vary these commands depending on where you want to start reading. Then the do-while loop should pass through each node in turn, allowing you to access its attributes and such.

Monday, 9 March 2009

SQL Cursors

SQL cursors, for those who don't know, are effectively a way of introducing a loop into a chunk of SQL code. Quite why the people who invented SQL couldn't call them "loops" I don't know - DBAs always have to be different, I suppose. Anyway it's a handy thing to know.

Beware though that SQL cursors are resource-intensive. If efficiency at the database end is important to you, it might be better to return the data and loop through it in the handling code. However, there are times when you need to get data, loop through it and then do something else at the DB end with the results of the loop. In those cases using a cursor is likely to be less intensive than the round trips involved in two separate back-and-forth trips to the database server.


Declare c1 cursor read_only
For
Select from
-- select statement picks up
--rows to sort through

Declare @trackingID int
-- declare a variable to hold the primary key
-- of the rows you’re returning

Open c1

Fetch next from c1 into @trackingID

While @@fetch_status = 0
Begin
--do something with the tracking variable,
--such as using it to select some
--data or do an update or somesuch

Fetch next from c1 into @trackingID
End

Close c1
Deallocate c1