﻿<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/">
  <channel>
    <title>Crafty Code</title>
    <description>The craft of writing code.  The outcomes from being crafty with code.  Crafty Code is tales from the coding bench.</description>
    <link>http://www.ifinity.com.au/Blog/BlogId/2</link>
    <language>en-US</language>
    <managingEditor>bchapman@ifinity.com.au</managingEditor>
    <webMaster>bchapman@ifinity.com.au</webMaster>
    <pubDate>Tue, 09 Feb 2010 17:31:40 GMT</pubDate>
    <lastBuildDate>Tue, 09 Feb 2010 17:31:40 GMT</lastBuildDate>
    <docs>http://backend.userland.com/rss</docs>
    <generator>Blog RSS Generator Version 3.5.0.19041</generator>
    <item>
      <title>Creating a simple Sql Server Express Database Backup</title>
      <description>&lt;p&gt;A lot of people use Sql Server express for the reason of it’s generous licensing.  One of the drawbacks of Sql Server express is that it comes without the Sql Agent – which is the batch-running part of Sql Server.  Sql Agent is used to run jobs to do all sorts of things with Sql Server, and one of the most common jobs is to run a backup schedule.  And, as everyone knows, you need to have a daily backup schedule on your database, or one day, you’re going to pay for it.&lt;/p&gt;  &lt;p&gt;This blog post will cover how to setup a backup procedure which will backup databases, zip them up, copy them to a location, ftp them to a backup server for long term storage, and clean up after itself.   I’ve done this without purchasing a single piece of backup software, and relied on in-built tools or shareware.&lt;/p&gt;  &lt;p&gt;It’s targeted towards people who are running their own web server, whether it’s co-located, hosted or a VPS.  If the thought of command line utilities, scheduled tasks and file permissions stresses you, then I’d suggest the purchase of a nice backup system to ease your troubles.&lt;/p&gt;  &lt;p&gt;If you’re on a shared hosting plan, all you need is a signature in stone that your host can provide you with last nights backup at short notice.  If you are on a hosting plan, it’s worthwhile testing them out randomly by asking for a database backup, just to check out if they can lay their hands on one.  The time you need it is not the time to hear ‘sorry, don’t have one’.&lt;/p&gt;  &lt;p&gt;This setup covers a pretty common scenario where the web server doesn’t have a lot of disk space (common with VPS machines) and is not connected by an internal network to any other computers (hence the FTP part). The first part of the script runs on the server, and the second part runs on a different machine. The second machine could be another server, it could be your workstation, as long as it will be running when the script is scheduled to run. All of the various scripts are included in the attached zip file, but you’ll need to modify them, as I have taken out all the private information.&lt;/p&gt;  &lt;p&gt;Disclaimer : by all means copy what I have done, but you’re on your own when it comes to setting this up and installing it.  I don’t make any promises that it will work for you, or that it is the most secure, or most robust, or most perfect way.  It’s a simple setup and not suitable for mission critical apps.  In other words : if you lose data or get hacked, &lt;em&gt;the responsibility is yours&lt;/em&gt;.&lt;/p&gt;  &lt;p&gt;First, I’ll cover what each piece does, then I’ll cover how to install it.&lt;/p&gt;  &lt;h2&gt;Step 1 : Setup&lt;/h2&gt;  &lt;p&gt;Download the code : &lt;a title="http://www.ifinity.com.auhttp://www.ifinity.com.au/portals/0/downloads/database_backup_scripts.zip" href="http://www.ifinity.com.au/portals/0/downloads/database_backup_scripts.zip"&gt;Database_Backup_Scripts.zip&lt;/a&gt;&lt;/p&gt;  &lt;h3&gt;Background&lt;/h3&gt;  &lt;p&gt;The Sql Server backups will be done by running a Sql query against the database to do the backups.  This is actually what the native Sql Agent in the ‘Full’ Sql Server does anyway, only you don’t have a Guid or pretty Wizard to help you.  The rest of the actions are done by batch commands run against either tools or utilities on your server.&lt;/p&gt;  &lt;h3&gt;How To Install&lt;/h3&gt;  &lt;p&gt;On your server with Sql Server, create a directory called c:\db\backups and c:\db\scripts.  This server must have FTP enabled on it, or be able to copy to a server with FTP enabled (a common scenario with a web server also masquerading as a database server, or web server and sql server in the same DMZ).  Create a directory which has permissions for an FTP user to read from : C:\inetpub\ftp.&lt;/p&gt;  &lt;p&gt;On your server used to store the backups, create a directory called d:\db\ (note, this can be c:\db, but I’ve made it d:\db to make it easy to tell apart)&lt;/p&gt;  &lt;p&gt;Note down, and test the FTP user/password that you will use to copy the database backups from the server.&lt;/p&gt;  &lt;p&gt;Extract the contents of the zip file somewhere where you can easily obtain them.&lt;/p&gt;  &lt;h2&gt;Step 2 : Running a database backup&lt;/h2&gt;  &lt;h3&gt;Background&lt;/h3&gt;  &lt;p&gt;All Sql Server databases can be backed up by running a Sql script, which calls the in-built stored procedures to backup the files. &lt;/p&gt;  &lt;p&gt;I also created a specific user called ‘dbbackup’ on the server to run the backups.  With this user, I went through and granted the ‘db_backupoperator’ role membership through the ‘User Mapping’ screen.  This can’t be scripted because it depends on the individual databases.&lt;/p&gt;  &lt;p&gt;I found a neat little stored procedure script from this page : &lt;a title="http://www.sqlmag.com/Articles/ArticleID/46560/46560.html?Ad=1" href="http://www.sqlmag.com/Articles/ArticleID/46560/46560.html?Ad=1" target="_blank"&gt;Using T-Sql to Backup and restore Sql Server databases&lt;/a&gt;&lt;/p&gt;  &lt;p&gt;I added some details to this script, which includes the creation of a specific user to run the scripts.  &lt;/p&gt;  &lt;h3&gt;How to Install&lt;/h3&gt;  &lt;p&gt;First, either create your database backup user on your Sql Server, or select a user and grant them the ‘db_backupoperator’ role on each database you’d like to backup.&lt;/p&gt;  &lt;p&gt;Open up a Sql query window in your target server, and select the ‘master’ database.  Open the ‘Create_Backup_Proc.sql’ file from the zip package. If you have a different user, change the ‘dbbackup’ user permission to your chosen user (use find/replace).&lt;/p&gt;  &lt;p&gt;Execute the script, which will create the stored procedure.  Test out the procedure to see if it works, by running this script in a Query window:   &lt;br /&gt;&lt;font face="Courier New"&gt;exec sp_backup_databases 'c:\db\backups' &lt;/font&gt;&lt;/p&gt;  &lt;p&gt;Note that this backs up to the aforementioned c:\db\backups location.  I prefer a simple backup location, rather than the convoluted default that Sql server provides.  The backup location for a Sql Server database must always be on the local server.  You can’t back up to a remote machine.  &lt;/p&gt;  &lt;p&gt;Check your c:\db\backups location : you should have a databasename.bak file for each database in your list.  If not, troubleshoot this until your script is working.&lt;/p&gt;  &lt;h2&gt;Step 3: Setting up your backup, database copy and zip into a batch file&lt;/h2&gt;  &lt;h3&gt;Background&lt;/h3&gt;  &lt;p&gt;As the process is designed to use FTP to transfer the backup(s) to another computer, the smaller the file, the better.  One of my database backups runs to over 400 mb, and this would take too long and use up too much bandwidth to be useful.  Therefore, you need to compress the backups into a single file for easy transfer.&lt;/p&gt;  &lt;p&gt;For this, I’m using the PkZip command line utility.  Now, this is an old shareware application that dates back over 10 years, and I already had a copy.  You’ll have to source your own (I can’t distribute it) and make sure that you comply with the licensing terms.  If it helps, the install for mine is called ‘pkzc400s.msi’.  You could also use any other command-line compression utility if you have one.&lt;/p&gt;  &lt;p&gt;Once the backup zip file is created, it will need the FTP user read permissions in order to be downloaded.  This is achieved by running the cacls utility to grant the required permission.&lt;/p&gt;  &lt;p&gt;All of the steps are run through a batch file, which runs the Sql to do the backups, runs the zip utility to put the backups into one zip file, then sets the permissions on the file and copies it to the FTP folder.&lt;/p&gt;  &lt;h3&gt;How to Install&lt;/h3&gt;  &lt;p&gt;Copy the ‘Run_Backups.cmd’ file to the C:\db\script location on your database server.  Open the file in notepad and set the server name (replace dbserver), username (replace dbbackup if different user) and password (replace password) .&lt;/p&gt;  &lt;p&gt;If you have a different command line zip utility, change the command for the Pkzipc call.  It should still create a zip file called c:\db\backups\db_backups.zip.&lt;/p&gt;  &lt;p&gt;Update the FTP folder location to where your FTP logon can read the file.&lt;/p&gt;  &lt;p&gt;Update the FTP user in the cacls command (search for ftp_user).  You may need to prefix the server name or domain name if applicable.&lt;/p&gt;  &lt;p&gt;Set up a scheduled task to run the batch file on a daily basis.  You can use this command line, or go through the scheduled task application on your server.&lt;/p&gt;  &lt;p&gt;&lt;font face="Courier new"&gt;schtasks /create /sc Daily /st 00:00:00 /tn DbBackups /tr "c:\db\scripts\run_backups.cmd"&lt;/font&gt;&lt;/p&gt;  &lt;p&gt;Note the above command line creates a daily task which runs at midnight, and runs the run_backups.cmd file.&lt;/p&gt;  &lt;p&gt;Test out your backup by brining up a command line window can calling : c:\db\scripts\run_backups.cmd.  Check the .txt log files created to see if all steps work correctly, and do an inspection on the created zip file to make sure it contains the database backups.  Try using an FTP program to download the file to make sure you have both the location and permission right for the FTP step.&lt;/p&gt;  &lt;h2&gt;Step 4 : Setting up your backup server&lt;/h2&gt;  &lt;h3&gt;Background&lt;/h3&gt;  &lt;p&gt;In my case, I used an internet connected machine which has an external Terabyte disk on it.  I use this for backups of system drives, data, email archives, and for backups of live websites.  This machine is used to contact the database server and download the backups via FTP.&lt;/p&gt;  &lt;p&gt;Again, this is achieved through a scheduled task which runs the individual tasks, including copying the file via FTP, and cleaning up the directory of backups older than a certain date.&lt;/p&gt;  &lt;h3&gt;How to Install&lt;/h3&gt;  &lt;p&gt;Copy the contents of the Backup_server_Scripts to your d:\db\ directory.  First, open up the ‘Archive_Backups.cmd’ file and change the Z:\backup\database directory.  This is the local path where you want to archive your database backups.  This might be an external drive, a Network Attached Storage device, or anywhere with lots of disk space and low risk of it getting deleted or lost.&lt;/p&gt;  &lt;p&gt;Also, review how long you would like to keep the file for.  The last task in the batch file runs the delfiles.vbs script, which deletes any files older than a specified number of days.  By default it is set to 10 days, to allow for ‘oops’ moments.  You can change the value to whatever number of days you like.  This should be calculated as a function of disk space, backup size, and likely time period before you realise a mistake has been made.&lt;/p&gt;  &lt;p&gt;Open the Ftp_Backups.txt file.  This contains a series of batched commands to provide to the native Windows FTP program.  The edits are (line by line)&lt;/p&gt;  &lt;p&gt;ftp.yoursite.com –&gt; replace with your ftp server address&lt;/p&gt;  &lt;p&gt;ftp_user –&gt; replace with your ftp username&lt;/p&gt;  &lt;p&gt;password –&gt; replace with your password for the ftp user&lt;/p&gt;  &lt;p&gt;cd ftp –&gt; replace ftp with the directory where your file gets copied to.  If it’s in the root ftp directory, delete this line altogether.  If it’s more than one level down, adjust as necessary.&lt;/p&gt;  &lt;p&gt;I got a lot of help from this resource: &lt;a title="http://www.ericphelps.com/batch/samples/ftp.script.txt" href="http://www.ericphelps.com/batch/samples/ftp.script.txt"&gt;Ftp Script Examples&lt;/a&gt;&lt;/p&gt;  &lt;p&gt;Save the changes to both files, and then test out the process by running the file in a command line window by typing in ‘d:\db\archive_backups.cmd’.  You should see the results of this run, and your database backups copied to your archiving location.  For an extra test, copy an old file into that location and see if the cleanup script deletes it.  Note, that your windows firewall (or other firewall) may block the FTP port – you may have to adjust this.  Check the log files created to ascertain if this is the case.&lt;/p&gt;  &lt;p&gt;You will note that the archive task does not overwrite the same file every day, but rather copies it with the date and time, like this : 2010-01-22-Fri_db_Backups.zip.  I use the YYYY-MM-DD format because it is easy to order the files by filename and have them in chronological order.   I got help with the batch file date renaming from this article on creating a  &lt;a title="http://www.zorbathegeek.com/153/batch-file-to-append-date-to-file-name.html" href="http://www.zorbathegeek.com/153/batch-file-to-append-date-to-file-name.html"&gt;Batch file to append date to file name&lt;/a&gt;.  Note that this code may be dependent on local date/time format settings- you might have to modify it to get the result you want.&lt;/p&gt;  &lt;p&gt;Lastly, setup a scheduled task on your workstation to run the archive_backups.cmd file.  This can be done with:&lt;/p&gt;  &lt;p&gt;&lt;font face="Courier new"&gt;schtasks /create /sc Daily /st 01:00:00 /tn DbBackupsArchive /tr "d:\db\archive_backups.cmd"&lt;/font&gt;&lt;/p&gt;  &lt;p&gt;Note that this runs 1 hour later than the database, backup, to leave ample time for the first backup to run and finish. &lt;/p&gt;  &lt;h2&gt;Modifying for your own purposes.&lt;/h2&gt;  &lt;p&gt;There are many ways you can expand this basic setup.  The first, most obvious one is to save bandwidth by only running the ‘archive’ run once a week, so you only get the latest backup.  This gives you a two-step backup – if you just have a local deletion/corruption of the database, then you’ve always got the last night’s backup on the server (in the zip file) to restore from.  If you have a total server loss, the most you’re going to lose is 7 days worth of data.  You can adjust this as you like, for example, running every second day.  It’s a tradeoff between potential data loss and bandwidth, and depends on the individual site involved. For many sites, the bandwidth will be small and it’s worth it to do every day.  If you have a relatively static site, consider your needs there.  If you have an active site with forum posts and user updates happening every day, you may need to do a daily/half daily backup.&lt;/p&gt;  &lt;p&gt;Note that you can also combine other user data into this process – it doesn’t just have to be database files (though the database is most important for a CMS application like DotNetNuke).  If you have other user-created files (uploaded images, etc) you could combine the zip process in a similar way.&lt;/p&gt;  &lt;h3&gt;Results?&lt;/h3&gt;  &lt;p&gt;Please let me know via the comments if you find faults with the attached code, or see glaring flaws, or have suggestions on making the process better.  It’s my intention for this to provide a starting point for people putting together their own customised backup process and getting a better understanding of the tools available to achieve this.&lt;/p&gt;</description>
      <link>http://www.ifinity.com.au/Blog/Technical_Blog/EntryId/80/Creating-a-simple-Sql-Server-Express-Database-Backup</link>
      <author>bchapman@ifinity.com.au</author>
      <comments>http://www.ifinity.com.au/Blog/Technical_Blog/EntryId/80/Creating-a-simple-Sql-Server-Express-Database-Backup#Comments</comments>
      <guid isPermaLink="true">http://www.ifinity.com.au/Blog/Technical_Blog/EntryId/80/Creating-a-simple-Sql-Server-Express-Database-Backup</guid>
      <pubDate>Fri, 22 Jan 2010 06:36:00 GMT</pubDate>
      <slash:comments>3</slash:comments>
      <trackback:ping>http://www.ifinity.com.au/DesktopModules/Blog/Trackback.aspx?id=80</trackback:ping>
    </item>
    <item>
      <title>Thoughts on Debugging when you have no debugger</title>
      <description>&lt;p&gt;This is a philosophical post targeted at anyone who messes about with code, and from time to time comes across intractable problems that they just can’t get around.&lt;/p&gt;  &lt;p&gt;When I first started writing code for a living (as opposed to messing about) I had a mentor who got placed into the role by simple fact that she sat at the desk behind me in my very first job.   As the newbie on the team, I was pretty much less than useless, because not only could I not produce anything worthwhile, I actively took up peoples time trying to figure out why I couldn’t produce anything worthwhile.  I knew this for a fact when I was assigned to ‘tidy up the team bookshelf’ – then a mess of programming and system manuals in those natty clip-open binders, so you could plug in the latest updates.  Remember those?&lt;/p&gt;  &lt;p&gt;Anyway, my self-appointed mentor (shamefully I can’t remember her name, though I’d run with Christina) basically got to the point where she told me “you can solve the problem, just keep tracing it until it presents itself”.  I’m paraphrasing here, I’m pretty sure it was along the lines of “go away, you annoying person, and figure out your own problems.  Why don’t you try using the debugger?”.   However, in those days, point-and-click debuggers with nice little yellow highlighters and instant variable values were features not available to me.  It was code, complile, and check to see if you got it right.  That, and piling in lots of little logging statements to spit out what the program was doing.  Eventually this paid off, because my manager figured she’d put me onto an esoteric bug which had so far eluded all the actual developers on the team.  I had nothing else to do, so I mercilessly tracked this bug until my desk was covered in dot-matrix printouts of programs with little annotations on the side, flow charts – probably even an ER diagram or two.  The one day, I actually found the bug and worked out a fix.  It was a revelation.  I added value to the team.  A manager came by to thank me.  I walked out of the building and thought ‘yeah, I’m a &lt;em&gt;programmer&lt;/em&gt; now’.&lt;/p&gt;  &lt;p&gt;Quite aside from my reminiscing of times long gone by, this early lesson taught me something that has stuck with me the whole time.  No matter how frustrating the bug, no matter how elusive, &lt;em&gt;all bugs can be tracked, found and fixed&lt;/em&gt;.  It’s just a matter of persistence, tools and lots of thinking.  My first bug was solved not by my genius, but because I just kept plugging away at it.  Sadly, not all of us are freshly-minted graduates with days of spare time to fill, so sometimes bugs go into the ‘I’ll never sort that out’ pile.  But you can’t pretend they are solved even if the official status is ‘unable to reproduce’ – eventually that bug is going to re-appear somewhere along the line.&lt;/p&gt;  &lt;h2&gt;Tracking Down a PayPal IPN Bug&lt;/h2&gt;  &lt;p&gt;If you read my prior post on this blog, you’ll know I’ve been doing some work with PayPal IPN listeners.  An IPN listener waits for PayPal to send a Http Post to a Url on your site, called an ‘Instant Payment Notification’.  Your listener must respond to this post, and tell PayPal that it received it OK.  You must send the entire contents of the post back to PayPal, so that PayPal knows it sent the post (and not some nefarious scammer).  If you respond correctly, you get a ‘VERIFIED’ response.&lt;/p&gt;  &lt;p&gt;If you’re writing your IPN listener, in C#, there’s a handy code sample to use at &lt;a title="https://cms.paypal.com/cms_content/US/en_US/files/developer/IPN_ASP_NET_C.txt" href="https://cms.paypal.com/cms_content/US/en_US/files/developer/IPN_ASP_NET_C.txt"&gt;https://cms.paypal.com/cms_content/US/en_US/files/developer/IPN_ASP_NET_C.txt&lt;/a&gt;&lt;/p&gt;  &lt;p&gt;How this relates to this debugging themed post? I took the PayPal example, implemented it ; all working OK.  I then added bits and pieces to the sample code, refactored it for my own purposes, generally started to build my listener.  That’s when I hit the date problem in the previous post.  The problem with debugging programs like this, is that when you’re developing an IPN listener, you’ve got to have a valid Url on the internet for PayPal to send the IPN to.  You can’t host it on ‘localhost’, because the PayPal server can’t send the IPN to that.  You have to host it on the internet somewhere.  And that means no handy debugger.  No stepping through code, no breakpoints, none of that.  Back to where I started ; code listings, in-line logging and some head scratching and figuring things out.*&lt;/p&gt;  &lt;p&gt;But the date problem isn’t what prompted this post : after I figured out the difference between the Sandbox and Live dates, I uploaded my new listener to the server, and consistently got responses of ‘INVALID’.  This means that PayPal didn’t like the response I was sending back to them to verify what they sent.  But the code, as much as I could tell, was much like their sample.   Here’s where I would like to find a handy intern or graduate, assign them the task of tracking down why it isn’t working, and wait for a ‘huzzah’ to come from their cubicle.  But there certainly is no graduate or intern around here.    Equally, there was no possibility of endless days of debugging, with so many other more important things to do.&lt;/p&gt;  &lt;p&gt;This is where a principle I now subscribe to frequently comes in : scrap it and start again.  Whenever debugging something that doesn’t seem to work, it often takes less time to start again, and incrementally add the changes back, this time carefully testing each iteration.&lt;/p&gt;  &lt;p&gt;So while I was paddling around in the Pacific Ocean this morning (marvelling the fact that &lt;a title="http://www.coastalwatch.com/swell/forecastMain.aspx?page=sfforecasts&amp;location=10" href="http://www.coastalwatch.com/swell/forecastMain.aspx?page=sfforecasts&amp;location=10"&gt;mid-summer ocean temperatures&lt;/a&gt; in my part of the world are a balmy 25 degrees Celsius) - why keep trying to figure out where the problem is, when it would just take 20 minutes to build a whole new version.  I would build a new one based on the PayPal example, and then incrementally add the other changes back in and check that it was working between each iteration.  That’s just what I did, and it worked perfectly.  I’ve now junked the old code.  I don’t know what was wrong with it, and I never will.  Total time to solve the problem was less than 30 minutes.&lt;/p&gt;  &lt;p&gt;The moral of this story are the two point I’ve made here:&lt;/p&gt;  &lt;p&gt;1) Never assume a bug is a one-off.  There are very few one-off bugs, and chances are your bug isn’t one of those.  It will re-appear.&lt;/p&gt;  &lt;p&gt;2) All bugs can be tracked down with the right tools, approach and persistence&lt;/p&gt;  &lt;p&gt;3) It’s usually quicker to take a different approach and start again, rather than try and work out where your code is flawed&lt;/p&gt;  &lt;p&gt;4) Blog posts are a great way to procrastinate against further bug fixing.&lt;/p&gt;  &lt;p&gt;That last statement is there because I realise this whole post is probably preaching to the converted, but I’m hoping someday one person will read it, find the energy to tackle an unsolved bug, and solve it with a new approach.&lt;/p&gt;  &lt;p&gt;*I know you can do remote debugging of servers.  You also have to open security holes everywhere on your server in order to make it happen.  You just about have to sign a disclaimer in binary before Microsoft will let you do that.  I figured the server would be full of bots and viruses faster than I could possibly fix it, and the performance would be horrible.&lt;/p&gt;</description>
      <link>http://www.ifinity.com.au/Blog/Technical_Blog/EntryId/79/Thoughts-on-Debugging-when-you-have-no-debugger</link>
      <author>bchapman@ifinity.com.au</author>
      <comments>http://www.ifinity.com.au/Blog/Technical_Blog/EntryId/79/Thoughts-on-Debugging-when-you-have-no-debugger#Comments</comments>
      <guid isPermaLink="true">http://www.ifinity.com.au/Blog/Technical_Blog/EntryId/79/Thoughts-on-Debugging-when-you-have-no-debugger</guid>
      <pubDate>Tue, 12 Jan 2010 03:24:14 GMT</pubDate>
      <slash:comments>0</slash:comments>
      <trackback:ping>http://www.ifinity.com.au/DesktopModules/Blog/Trackback.aspx?id=79</trackback:ping>
    </item>
    <item>
      <title>Keeping Querystring values out of the Friendly Url Path in DotNetNuke with Url Master</title>
      <description>&lt;p&gt;This blog post is going to cover a pretty small subset of problems that people have with auto-generated Urls using Friendly Urls in DotNetNuke.    All Friendly Url solutions in DNN, regardless of what module used to generate them, approach the problem of making Urls better by converting some of the querystring values into the actual Url path.&lt;/p&gt;  &lt;p&gt;Explained simply, a url which looks like this :&lt;/p&gt;  &lt;p&gt;/default.aspx?tabid=64&amp;key=value&amp;text=description&lt;/p&gt;  &lt;p&gt;will be converted to a url which looks like this (assume tabid 64 is ‘DNNPage’)&lt;/p&gt;  &lt;p&gt;/Tabid/64/DNNPage/key/value/text/description/default.aspx or /DNNPage/key/value/text/description.aspx&lt;/p&gt;  &lt;p&gt;The differences between the DNN Friendly Url Provider and the Url Master module is that the DNN provider will keep the tabid and the /default.aspx on the end, whereas the Url Master disposes of these parts of the Url.  However, the main similarity remains : the querystring items are converted to be part of the Url path.&lt;/p&gt;  &lt;p&gt;In most cases this is advantageous, it provides a simpler, cleaner Url that is easier for humans to understand, and search engines have no trouble parsing.  It has to be said, though, that the previous problems with search engines not being able to index querystrings are a thing of the past – so the original reasons for doing this have moved on.&lt;/p&gt;  &lt;h3&gt;Keeping Querystring Items in the Querystring&lt;/h3&gt;  &lt;p&gt;However, in some cases, the querystring needs to stay on the querystring.  This is a common occurrence with the inclusion of tracking parameters, such as Google Analytics campaign tracking parameters (&amp;utm=xyz) and other affiliate and advertising linked tracking.   The other problem is that analytics packages like Google Analytics and others ignore the querystring, but separate out Urls for tracking purposes by path.  &lt;/p&gt;  &lt;p&gt;To illustrate this problem, let’s assume I have setup an advertising campaign, and I want to track the effectiveness of it in Google Analytics.  To do so, I will tag the Url that I am going to send to an email list with special terms in the querystring.  The Url will look like this:&lt;/p&gt;  &lt;p&gt;http://mysite.com/Buy-this-thing.aspx?utm_medium=email&amp;utm_source=subscribers&lt;/p&gt;  &lt;p&gt;When users click on this link in an email I send out to them, the ‘email’ and ‘subscribers’ tracking values will be traceable within Google Analytics, and I’ll be able to tell how successful this emailing campaign was.&lt;/p&gt;  &lt;p&gt;However, if I’m running Url Master on my site, there’s a chance that the Url could end up being:&lt;/p&gt;  &lt;p&gt;http://mysite.com/Buy-this-thing/utm_medium/email/utm_source/subscribers.aspx&lt;/p&gt;  &lt;p&gt;Now, normally if the Url is valid, the Url Master module won’t convert the requested querystring into a friendly url path, but if there is a redirect triggered then the undesired Url can be generated.   The redirect reason could be : lower case change, page relocated, incorrect subdomain, explicit redirect, etc – most redirects would trigger this behaviour.  While the new Url will work just as well as before, an Analytics package will treat this new url as a completely different Url from the ‘original’ Url.  And that means the tracking data will be messed up and split across different Urls.  This is because the tracking data works off the actual Url entered in the browser – it’s a client-side (javascript) technology, so can’t see the server-side (rewritten) Url, which is what the DNN platform uses.&lt;/p&gt;  &lt;h3&gt;New Regex Value for Maintaining Querystrings&lt;/h3&gt;  &lt;p&gt;The solution for this problem has been introduced into the Url Master module from version 1.15 onwards.  This takes the form of a regex value to match any items in the querystring that should not be removed from the querystring.   Note that this works for any Url, anywhere in DNN, and not just for the narrow problem I have described.&lt;/p&gt;  &lt;p&gt;This new Regex field appears in the ‘Advanced Regex Settings’ section of the ‘Friendly Url Settings’ page.  When a Regex value is entered into this string, it compares the components of a querystring that is to be converted into a Friendly Url path, and if a match is found, that portion of the match is excluded from the Friendly Url Path, and is left as a querystring.  This flexibility allows picking and choosing which parts of a querystring to exclude, and which parts to leave in.&lt;/p&gt;  &lt;p&gt;Now, Regex isn’t the easiest of things to wrap ones head around without some prior exposure and practice.  But fortunately the problem is pretty rare and usually relates to key/value pairs in the querystring used for analytics tracking.   So it shouldn’t take too long before a good catalogue of solutions is published on common problems.&lt;/p&gt;  &lt;p&gt;For the example above, to keep the ‘utm=medium&amp;utm-source=subscribers’ in the querystring, I’ll need to create a Regex pattern that will match the full querystring.   It’s tempting to think you can just enter ‘utm’ and leave it at that – however, this would leave the ‘utm’ parts in the querystring, and leave behind the rest of it, resulting in a Url that looked like this:&lt;/p&gt;  &lt;p&gt;_medium/email/_source/subscribers.aspx?utm&amp;utm&lt;/p&gt;  &lt;p&gt;That’s even worse than what we started with, because the Url would no longer work at all.  You’ll note that the ‘utm’ match has been left in the querystring, and everything else has been shuffled into the path.&lt;/p&gt;  &lt;p&gt;Instead, what you need to do is expand the match to locate and find all parts to keep in the querystring.&lt;/p&gt;  &lt;p&gt;Here’s a version that works as we want:&lt;/p&gt;  &lt;p&gt;/utm_[^/]+/[^/]+&lt;/p&gt;  &lt;p&gt;The difference is that this pattern expands to match any querystring parameter starting with utm_, and matches the path of it as well.&lt;/p&gt;  &lt;p&gt;There’s a couple of things to note here:&lt;/p&gt;  &lt;p&gt;1.  Because it’s a non specific match, it repeats and captures both the utm_medium and utm_source values.  You don’t have to explicitly match each if you can do a partial match (utm_) that works.&lt;/p&gt;  &lt;p&gt;2.  The match is actually done on the friendly url path – not the original querystring.  In other words, the key/value pairs are split with path separators (/) rather than the &amp; = of a Url querystring.   You need to write a regex that matches what the incorrect friendly url path is, not what the original querystring is.&lt;/p&gt;  &lt;p&gt;If you’re interested in how the regex match works, it’s pretty simple.  The first part matches a part of a path that starts with /_utm – which is what we want.  The next part matches whatever comes between _utm  and the next / character.  This is done by repeating a character match, as long as that character isn’t a /.  This is the [^/]+ part of the match.  It means – match at least one character (+) as long as it’s not the / [^/] – the ^ is a ‘not’ character match.  The next part of the expression says match the next part of the path, up to the next ‘/’ character.  This is the /[^/]+ part of the expression – it is the same as the prior part, but just looks for a string of characters of any type, as long as they aren’t ‘/’.   By understanding this expression, it should be easy to develop an expression that matches the part of the Url.&lt;/p&gt;  &lt;h3&gt;Setting up the Expression in Url Master&lt;/h3&gt;  &lt;p&gt;Simply go to the ‘Friendly Url Settings’ page, and enter the regex expression in the textbox preceeded by the label:&lt;/p&gt;  &lt;p&gt;&lt;strong&gt;Do not include matching items in friendly url path - force to be in the querystring (doNotIncludeInPathRegex)&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt;You can then test it out on the page (before saving the changes) by going to the ‘Test Friendly Url Settings’ box above the regex fields.  Select a page from a site, and, in the ‘Add on the querystring’ section, type in the raw querystring value you would expect to see on the end of the ‘normal’ DNN Url.  In the example shown in this post, this would be &lt;strong&gt;&amp;utm_medium=email&amp;utm_source=subscribers&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt;Once you have done this, click on the ‘Show Friendly Url Example’ button.  If you have set it up correctly, you should see the Url generated below the box.  In this Url, you should see that the part of the querystring that you’re trying to match in the correct place in the generated Url.&lt;/p&gt;  &lt;p&gt;Once you’re done, click on the ‘Apply Changes’ button, and test the Urls in your browser, ensuring that the Querystring items don’t get transformed into the Friendly Url path.&lt;/p&gt;</description>
      <link>http://www.ifinity.com.au/Blog/Technical_Blog/EntryId/78/Keeping-Querystring-values-out-of-the-Friendly-Url-Path-in-DotNetNuke-with-Url-Master</link>
      <author>bchapman@ifinity.com.au</author>
      <comments>http://www.ifinity.com.au/Blog/Technical_Blog/EntryId/78/Keeping-Querystring-values-out-of-the-Friendly-Url-Path-in-DotNetNuke-with-Url-Master#Comments</comments>
      <guid isPermaLink="true">http://www.ifinity.com.au/Blog/Technical_Blog/EntryId/78/Keeping-Querystring-values-out-of-the-Friendly-Url-Path-in-DotNetNuke-with-Url-Master</guid>
      <pubDate>Fri, 08 Jan 2010 01:03:00 GMT</pubDate>
      <slash:comments>2</slash:comments>
      <trackback:ping>http://www.ifinity.com.au/DesktopModules/Blog/Trackback.aspx?id=78</trackback:ping>
    </item>
    <item>
      <title>Converting PayPal Dates to .Net DateTime using Regex</title>
      <description>&lt;p&gt;I recently had a problem writing a PayPal Instant Payment Notification (IPN) listener.  The problem was that the PayPal Sandbox environment (now puzzlingly called x.com) would process my IPN requests OK, but any ‘live’ request that came through my site ended in error.&lt;/p&gt;  &lt;p&gt;Now the PayPal support people assured me that this wasn’t possible, but after dumping most of the actual code out of my module, and replacing it with highly-sensitive ‘gotcha’ error capturing code, I worked out the problem was due to a slight difference in the date formats between the Sandbox and Live environments.&lt;/p&gt;  &lt;p&gt;The Sandbox date looked like this:&lt;/p&gt;  &lt;p&gt;22:40:29 Dec. 16, 2009 PST&lt;/p&gt;  &lt;p&gt;The live date looked like this:&lt;/p&gt;  &lt;p&gt;13:02:26 Dec 21, 2009 PST&lt;/p&gt;  &lt;p&gt;Eagle eyed readers will detect the difference straight away.  It was a bit more difficult for me, as I was looking through raw logs and all I could see was 22%3A40%3A29+Dec.+16%2C+2009+PST as the values are encoded.  It was a surprise for me, but I guess I should have been more prepared for wide and varied date formats – they’re always the problem child of data handling and validation.  At least they have a text month to differentiate from dd/mm and mm/dd formats – they’re &lt;em&gt;always&lt;/em&gt; fun.&lt;/p&gt;  &lt;h3&gt;Converting a PayPal Date to a .NET DateTime value&lt;/h3&gt;  &lt;p&gt;The format of the PayPal date isn’t one of the ones that you can easily just convert to a .NET DateTime type by doing a DateTime.TryParse call.  I looked into writing a customised date parser to supply with the DateTime.Parse routine, but it all just looked too hard for something that was (supposed) to be coming in with a standard format.&lt;/p&gt;  &lt;p&gt;What I originally wrote (and which caused all the problems between Sandbox and Live environments) was a date parsing routine that worked out which bit of the date was which, all done by splitting up the string into substrings – one for the time, date and timezone.  This worked on the sandbox because it didn’t have the ‘.’ at the end of the month.  But move it into the live environment, and it didn’t work at all, because it was out by one character.  The resulting error wasn’t handled well (my fault) and so the whole thing mysteriously failed.&lt;/p&gt;  &lt;h2&gt;A better .NET / PayPal Date Parsing routine&lt;/h2&gt;  &lt;p&gt;Instead of fiddling around with brittle substring functions, I decided to turn to my old friend Regex, which thrives on this sort of fixed-format string parsing.  I quickly wrote this regex expression, which worked almost straight away.  The routine splits out the time, date and timezone, and within those individual groups, split out the hours, minutes, days, months and years.  Turns out I didn’t need the individual pieces, but by then the expression was written anyway.&lt;/p&gt;  &lt;p&gt;Here’s the expression:&lt;/p&gt;  &lt;p&gt;(?&lt;time&gt;\d{1,2}:\d{2}:\d{2})\s(?&lt;date&gt;(?&lt;Mmm&gt;[A-Za-z\.]{3,5})\s(?&lt;dd&gt;\d{1,2}),?\s(?&lt;yyyy&gt;\d{4}))\s(?&lt;tz&gt;[A-Z]{0,3})&lt;/p&gt;  &lt;h3&gt;A routine to convert a PayPal date to a .NET DateTime value using Regex&lt;/h3&gt;  &lt;p&gt;Here’s the full routine in C# to convert a PayPal date into a .NET DateTime value&lt;/p&gt;  &lt;pre&gt;/// &lt;summary&gt;
/// Converts a PayPal datestring into a valid .net datetime value
/// &lt;/summary&gt;
/// &lt;param name="dateValue"&gt;a string containing a PayPal date&lt;/param&gt;
/// &lt;param name="localUtcOffset"&gt;the number of hours from UTC/GMT the local 
/// time is (ie, the timezone where the computer is)&lt;/param&gt;
/// &lt;returns&gt;Valid DateTime value if successful, DateTime.MinDate if not&lt;/returns&gt;
private static DateTime ConvertFromPayPalDate(string rawPayPalDate, int localUtcOffset)
{
    /* regex pattern splits paypal date into
     * time : hh:mm:ss
     * date : Mmm dd yyyy
     * timezone : PST/PDT
     */
     const string payPalDateRegex = @"(?&lt;time&gt;\d{1,2}:\d{2}:\d{2})\s(?&lt;date&gt;(?&lt;
Mmm&gt;[A-Za-z\.]{3,5})\s(?&lt;dd&gt;\d{1,2}),?\s(?&lt;yyyy&gt;\d{4}))\s(?&lt;tz&gt;[A-Z]{0,3})";  
    //!important : above line broken over two lines for formatting - rejoin in code editor
    //example 05:49:56 Oct. 18, 2009 PDT
    //        20:48:22 Dec 25, 2009 PST
    Match dateMatch = Regex.Match(rawPayPalDate, payPalDateRegex, RegexOptions.IgnoreCase);
    DateTime time, date = DateTime.MinValue;
    //check to see if the regex pattern matched the supplied string
    if (dateMatch.Success)
    {
        //extract the relevant parts of the date from regex match groups
        string rawDate = dateMatch.Groups["date"].Value;
        string rawTime = dateMatch.Groups["time"].Value;
        string tz = dateMatch.Groups["tz"].Value;

        //create date and time values
        if (DateTime.TryParse(rawTime, out time) &amp;&amp; DateTime.TryParse(rawDate, out date))
        {
            //add the time to the date value to get the datetime value
            date = date.Add(new TimeSpan(time.Hour, time.Minute, time.Second));
            //adjust for the pdt timezone.  Pass 0 to localUtcOffset to get UTC/GMT
            int offset = localUtcOffset + 7; //pdt = utc-7, pst = utc-8
            if (tz == "PDT")//pacific daylight time
                date = date.AddHours(offset);
            else  //pacific standard time
                date = date.AddHours(offset + 1);
        }
    }
    return date;
}&lt;/pre&gt;

&lt;p&gt;You can use this routine quite simply by extracting a field which contains a PayPal date, and transforming it into a .NET DateTime value, like this:&lt;/p&gt;

&lt;pre&gt;    //Get the payment date of the request.  Note 0 hours to return UTC time of payment
    DateTime paymentDate = ConvertFromPayPalDate(Request.Form["payment_date"], 0);&lt;/pre&gt;

&lt;p&gt;Feel free to use the code as-is, if you find any problems let me know via the comments.&lt;/p&gt;</description>
      <link>http://www.ifinity.com.au/Blog/Technical_Blog/EntryId/77/Converting-PayPal-Dates-to-Net-DateTime-using-Regex</link>
      <author>bchapman@ifinity.com.au</author>
      <comments>http://www.ifinity.com.au/Blog/Technical_Blog/EntryId/77/Converting-PayPal-Dates-to-Net-DateTime-using-Regex#Comments</comments>
      <guid isPermaLink="true">http://www.ifinity.com.au/Blog/Technical_Blog/EntryId/77/Converting-PayPal-Dates-to-Net-DateTime-using-Regex</guid>
      <pubDate>Wed, 06 Jan 2010 08:27:40 GMT</pubDate>
      <slash:comments>2</slash:comments>
      <trackback:ping>http://www.ifinity.com.au/DesktopModules/Blog/Trackback.aspx?id=77</trackback:ping>
    </item>
    <item>
      <title>Unit Testing DotNetNuke 5 Modules in Visual Studio with a Mock ASP.NET Http Context</title>
      <description>&lt;p&gt;When I was doing the rounds at the recent DotNetNuke OpenForce conference, I was surprised at the number of people who wanted to talk to me about the DNN Unit Testing framework I put together a few years back.   I use this on an almost daily basis, but it’s become part of the plumbing for all projects so I don’t really think about it as much as I used to.   I was recently contacted and asked to update it all for DNN 5, and I thought I would spend some time bringing it up to latest standards and generally re-visit the topic again.  It would seem there is a lot of interest in doing Test Driven Development with DNN, so this post will revisit all of that.&lt;/p&gt;  &lt;h3&gt;Should you be doing Test Driven Development with DotNetNuke?&lt;/h3&gt;  &lt;p&gt;Test Driven Development (TDD) is a worthy goal of any software project that aspires to good quality and the flexibility to change over time.  If this is true of your DotNetNuke module, then the answer is yes.  All non-trivial iFinity modules have Unit Test projects that are run before releases and after changes to check for breaking changes.  This has caught a number of errors over the years, and prevented them from showing up in an actually released software (sadly the unit test coverage is not 100%, hence some bugs do get out, rotten little things!)&lt;/p&gt;  &lt;h3&gt;What will you need?&lt;/h3&gt;  &lt;p&gt;All of the information presented in this post covers DotNetNuke 5.1.2 and later, uses Visual Studio 2008 Team Edition and was done on a Windows 7 machine.  None of this is really necessary : you don’t have to use the in-built unit testing of Visual Studio (nUnit will do) and you can really use any version of Visual Studio you like, as long you’ve hooked up a unit testing framework.  I will suggest, however, that you have loads of fast RAM and a blazing fast processor, or you will soon bore of the constant compiling, running and copying that is part of unit testing.  I’m not going to pretend that the process of setting up unit testing with DNN is straightforward, so any amount of frustration you can remove by throwing hardware at the problem will be justified (see, print that bit out and go ask your boss for a new computer).&lt;/p&gt;  &lt;p&gt;Note, if you aren’t using the Visual Studio test tools, then you’ll have to do some refactoring to the code to get it to work.  If you do manage to, say, get it working with nUnit, let me know via the contents and I can make your work available (or write a blog post somewhere and I’ll reference it).&lt;/p&gt;  &lt;h2&gt;Contents of the Download&lt;/h2&gt;  &lt;div style="border-bottom: blue 1px solid; text-align: center; border-left: blue 1px solid; padding-bottom: 10px; margin: 10px; padding-left: 10px; padding-right: 10px; background: #efefef; float: right; border-top: blue 1px solid; border-right: blue 1px solid; padding-top: 10px"&gt;&lt;a href="http://www.ifinity.com.au/portals/0/downloads/iFinity.DNN5.Basic.UnitTesting.Example.zip"&gt;Download the code&lt;/a&gt;     &lt;p&gt;Zip file containing example test projects&lt;/p&gt; &lt;/div&gt;  &lt;p&gt;In my previous posts on this topic, I provided some DLLs and general instructions on setting up some procedures.  While this was useful, I decided to go a bit further and actually provide a complete example that you can download and setup, and (hopefully) press the magic ‘go’ button and see some tests running.  This is probably a very lofty goal given the territory, but everything should be there.&lt;/p&gt;  &lt;h3&gt;Solution Contents&lt;/h3&gt;  &lt;p&gt;When you download the solution, you’ll see the following projects:&lt;/p&gt;  &lt;div style="margin: 15px; padding-right: 15px; float: left"&gt;&lt;img style="border-right-width: 0px; display: inline; border-top-width: 0px; border-bottom-width: 0px; margin-left: 0px; border-left-width: 0px; margin-right: 0px" title="image" border="0" alt="image" src="/Portals/0/Blog/Files/2/76/WLW-1e2101bc4b41_117A6-image_thumb_1.png" width="127" height="244" /&gt; &lt;/div&gt;  &lt;ul&gt;   &lt;li&gt;Solution items : the testSetupScript.cmd file, which prepares the test output for running a mock DNN environment, the ‘testrunconfig’ file, which references items to be deployed with the test, and the .vsmdi file, which contains a list of unit tests in the solution &lt;/li&gt;    &lt;li&gt;iFinity.Basic.UnitTest.Example project.  This is the unit test project.  It contains a single test class file, as well as the various config files required to make it work with DNN &lt;/li&gt;    &lt;li&gt;iFinity.DNN.Utilities : This is the Unit Testing framework I built to work with DNN.  It contains the code to create the Mock DNN environment in which to run unit tests &lt;/li&gt;    &lt;li&gt;iFinity.ExampleModule, iFInity.ExampleModule.Data : these two projects are pretend module DLLs.  This article is all about unit testing Private Assembly modules, and this setup is the standard way to construct a DNN PA module.  Note there is no UI code in these projects, just a class, a module controller and a data provider.  The idea is just to test the parts of the code that use the DNN framework (namely, reading/writing to the database) &lt;/li&gt; &lt;/ul&gt;  &lt;p style="clear: both"&gt; &lt;/p&gt;  &lt;h3&gt;How to use the download&lt;/h3&gt;  &lt;p&gt;1) Download the code, extract the zip file, open it up in your Visual Studio (note, you’ll need a version which contains the Unit Testing framework, this won’t work in the ‘free’ editions).  &lt;/p&gt;  &lt;p&gt;2) Update the connection string in the app.config file to point at a DNN database that you have lying about somewhere.  If you don’t have a local test DNN database, then create a new instance of DNN just for unit testing purposes.  Actually, that’s not a bad idea anyway.  You can either use a .mdf file in the ‘user instance’ mode, or connect to a sql server database somewhere.  It depends on your objectives with testing and what you’ve got available.&lt;/p&gt;  &lt;p&gt;3) Find the 01.00.00.SqlDataProvider file in the ‘iFinity.ExampleModule.Data’ project, under the ‘SqlDataProvider’ directory.  Then run this through the ‘host-&gt;sql’ page of the DNN install that belongs to your DNN database you’re going to use for testing.  If you can’t do that, then run the Sql in a normal sql query editor, but remember to find/replace the ‘{objectQualifier}’ and ‘{databaseOwner}’ tokens first.&lt;/p&gt;  &lt;p&gt;4) Start the Unit tests with alt-shift-x, or find and click the ‘run tests’ button in Visual Studio (hint, it’s not the green “play” button that starts debugging)&lt;/p&gt;  &lt;p&gt;5) Check the test results to see if you got it to work OK.&lt;/p&gt;  &lt;p&gt;If you have any custom modules in your DNN install that need configuration changes, you might need to change the app.config file in your test project to match the web.config file of the associated DNN install.  It’s very important to realise that you are running a mock DNN application, and that mock application is using the app.config file of the test project, not the web.config file of the DNN project.&lt;/p&gt;  &lt;h3&gt;Test Setup&lt;/h3&gt;  &lt;p&gt;When you hit the magic ‘test it all for me’ button, there’s several things that go on.  In the background, the Unit Testing framework is compiling the versions of your components, and copying them to a special directory, created underneath the ‘TestResults’ directory.  By default this is in the solution folder.  However, this is just a flat directory with a pile of DLL’s in it : not unlike the \bin directory of a standard DNN install.  Our aim here is to make the DNN code run in a test context, and to do that, some parts of the DNN framework have to be re-created.  &lt;/p&gt;  &lt;p&gt;This is achieved by the ‘TestSetupScript.cmd’ file.   This file simply creates a couple of bare-bones directories in the OUT directory of the test, to make it look a bit more like a DNN install to the DotNetNuke code that will be running in the test. Note that the portal is hard-coded in this script file, to match the entries in the ‘dnnUnitTest’ section.  This file needs to be hooked up to the ‘TestRunConfig’ file in the solution containing the Test project.  This tells the Test framework to run the file before testing starts.  &lt;/p&gt;  &lt;p&gt;If you don’t set up this file, the code in the DNNUnitTest class coughs up a helpful error message, which will look something like this:&lt;/p&gt;  &lt;p&gt;Unable to create instance of class iFinity.DNN.Modules.ExampleModule.Test.ExampleRecordInfoDBTest. Error:  System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values.    &lt;br /&gt;Parameter name: Supplied HostMapPath [..\iFinity.UnitTesting.Examples\TestResults\Bruce_WOMBAT 2009-12-02 14_35_39\Out\Website\portals\0\] is not valid. Specify a valid path on the target machine (Did you forget to run the testSetupScript.cmd file to create the portals directory??)&lt;/p&gt;  &lt;p&gt;This message is supposed to be a slap to the forehead “Hey, wake up and include the TestSetupScript.cmd file!”&lt;/p&gt;  &lt;p&gt;There are other files that DotNetNuke needs to run, as well.  These include the DotNetNuke.config file, and the SiteUrls.config file.  Both of these files will be searched for by the DNN framework as it is loaded.  So, these two files are also specified as required items in the .testrunconfig file, in the ‘Deployment’ section.  This ensures that they will be copied to the OUTPUT directory before testing starts.&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;div style="border-bottom: #efefef 1px solid; border-left: #efefef 1px solid; padding-bottom: 5px; background-color: #ededed; margin: 5px; padding-left: 5px; padding-right: 5px; font-size: smaller; border-top: #efefef 1px solid; border-right: #efefef 1px solid; padding-top: 5px"&gt;   &lt;h4&gt;A note on project referencing and Test setup&lt;/h4&gt;    &lt;p&gt;You’ll see that the UnitTest project contains a reference to the ‘Data’ project : namely the iFinity.ExampleModule.Data project (and assembly).  Strictly speaking the Unit Test project shouldn’t have a reference to this project, the database calls should go through the ModuleController class.  However, referencing the project gives two shortcuts.  The first is that when a module is referenced by the test project, the compiled assembly is automatically copied to the OUT directory for testing.  The second is that it allows us to run cleanup database code directly against the database by utilising the connectionString reference that the SqlDataProvider object contains.  If this all makes you feel uncomfortable in a ‘impure architecture’ way, feel free to concoct your own solution.  But I’m here to tell you that getting the assembly manually copied is inelegant as well, and opening the app.config file to look for the connection string is ‘wordy’ as well.&lt;/p&gt; &lt;/div&gt;  &lt;h2&gt;How it Works&lt;/h2&gt;  &lt;p&gt;This framework works by creating a Mock (or fake) ASP.NET runtime context to run your unit tests within.  Tests normally run in the Visual Studio Test context – and if you do this, you can’t access server objects like HttpContext : which are vital for a web-based application like DotNetNuke.&lt;/p&gt;  &lt;p&gt;This fits together because the TestClass inherits from the DNNUnitTest class contained within the ‘iFinity.DNN.Utilities’ project.  When the TestClass is instantiated to run the tests contained within, the constructor code in the DNNUnitTest class kicks off the type of initialisation normally run within the DNN startup code.  It also constructs a mock HttpContext object, and stores some of the key DNN global objects (like PortalSettings and HostSettings) in the HttpContext items.   It also loads some of the Providers used by DNN for key tasks such as database access, data caching and others.&lt;/p&gt;  &lt;p&gt;Thus when your code runs and makes calls to items within the DotNetNuke namespace, the actual objects are there to take the calls and return the results.  This allows you to create unit tests to run pieces of code within your module that would otherwise fail because there was no DotNetNuke object there to receive and process them.  &lt;/p&gt;  &lt;h2&gt;Issues to Understand&lt;/h2&gt;  &lt;h3&gt;Windows Vista/7/2008 Security&lt;/h3&gt;  &lt;p&gt;If you’re like me and have upgraded to the latest Windows offerings, you’ll soon run aground on the fact that Unit Testing is considered a highly risky business by the born-again security converts on the Windows team.  You may get an error like this one:&lt;/p&gt;  &lt;p&gt;&lt;em&gt;Test Run deployment issue: The location of the file or directory 'D:\DotNetNuke\Custom\Common\iFinity.UnitTesting.Examples\dnn510\DotNetNuke.dll' is not trusted.&lt;/em&gt;&lt;/p&gt;  &lt;p&gt;This is because the DLL’s weren’t compiled on the machine you’re testing on, so you’re guilty of running code that someone else wrote.&lt;/p&gt;  &lt;p&gt;The generally accepted solution is to ‘unblock’ the file(s) that cause the problem.  You should do this in the location they are referenced from rather than the location noted in the error message.  This is because when you debug and test, the new copies will be taken from the reference location and copied into the destination location.  To unblock the file, right click on the offending file, select ‘Properties’ and go to the ‘General’ tab.  At the bottom you’ll see a message looking something like this:&lt;/p&gt;  &lt;p&gt;&lt;a href="http://www.ifinity.com.au/Portals/0/Blog/Files/2/76/WLW-1e2101bc4b41_117A6-image_2.png"&gt;&lt;img style="border-right-width: 0px; display: inline; border-top-width: 0px; border-bottom-width: 0px; border-left-width: 0px" title="image" border="0" alt="image" src="/Portals/0/Blog/Files/2/76/WLW-1e2101bc4b41_117A6-image_thumb.png" width="180" height="244" /&gt;&lt;/a&gt; &lt;/p&gt;  &lt;p&gt;Click ‘unblock’ at the bottom, and you should be OK.  You might have to restart Visual Studio to fix this problem, so I’d suggest unblocking all of the referenced files at once, then doing a restart of VS – just to be sure.  A big thanks to whomever at Microsoft decided this was the way to go.  &lt;/p&gt;  &lt;p&gt;If that doesn’t solve the problem for you, take a look at this &lt;a href="http://stackoverflow.com/questions/201327/mstest-run-fails-because-source-assembly-is-not-trusted"&gt;StackOverflow topic&lt;/a&gt;, and see if you can glean some help.  You might also try recompiling the DNN solution on your local computer, and using the output from that.  This also has the benefit of giving up some up-to-date .pdb files to help with any debugging.&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;h3&gt;Changes for DNN 5 : the ‘Cannot register or retrieve components until ComponentFactory.Container is set’ error&lt;/h3&gt;  &lt;p&gt;As previously mentioned, this blog post came about as a result of people asking me about a DNN 5 compatible version of this code.  The big change comes about through the use of the ‘ComponentFactory’ component used to handle the loading of providers and components in DNN 5.  This completely changes the way that 4.x DNN used to load components on startup.&lt;/p&gt;  &lt;p&gt;I was helped immensely by Charles Nurse, who has an excellent series on creating testable modules on his blog : &lt;a title="http://www.charlesnurse.com/post/Creating-Testable-Modules-Part-1.aspx" href="http://www.charlesnurse.com/post/Creating-Testable-Modules-Part-1.aspx"&gt;http://www.charlesnurse.com/post/Creating-Testable-Modules-Part-1.aspx&lt;/a&gt;&lt;/p&gt;  &lt;p&gt;Interestingly enough, he mainly advocates a completely different approach than what I do (in terms of the mock objects and environment, not the methodology) which is definitely worth reading and understanding.  One of the largest differences is that he is discussing the MVP (Model View Presenter) pattern, whereas this article discusses more the traditional pattern of DNN module development.&lt;/p&gt;  &lt;p&gt;However, the key item that I took away from this blog series was the changes in the way that DNN 5 loads up providers and other components.  This is actually all done in the ‘global.asax.vb’ file (which runs at application start).  This is one of the reasons many people had DNN 5 upgrade problems (and encountered the ‘componentFactory.Container’ error) – some modules and developers replace the global.asax.vb file with one of their own making – or fail to copy in the new one.  This removes the important instantiation of the Component Factory Container, and results in lots of bad errors all over the place.  &lt;em&gt;Don’t modify the core code!&lt;/em&gt;&lt;/p&gt;  &lt;p&gt;The end result for the iFinity.DNN.Utilities is that the startup code had to change.  This was done by copying the relevant startup code from the global.asax.vb file, which creates the container and loads the components for the various types of providers supported in DNN.  However, not all the various components are loaded : some are commented out in the ‘InstallComponents’ list.  These can be re-installed, but you’ll also need to reference the matching DLL from the DotNetNuke \bin directory.  For example, if you enable the ‘friendlyUrl’ provider, you’ll need to copy in and reference the matching Friendly Url Provider, as configured in your app.config ‘friendlyUrl’ section.&lt;/p&gt;  &lt;p&gt;However, as an upshot of this, the previous problems I had encountered with the &lt;a href="http://www.ifinity.com.au/Blog/EntryId/28/Bludgeoning-DotNetNuke-Unit-Testing-into-life"&gt;BuildManager.GetType() calls in DNN 4.8.0&lt;/a&gt; have disappeared.  So this code has been removed from the DNNUnitTest class, and replaced with the aforementioned DNN 5 method.&lt;/p&gt;  &lt;h3&gt;Visual Studio 2008 Web Context / Host Adapter&lt;/h3&gt;  &lt;p&gt;Within Visual Studio 2008 Unit Testing, there is a new feature for running Unit Tests in the context of an ASP.NET website.  This solution doesn’t take advantage of this : primarily because it’s an evolution of my work done in Visual Studio 2005, which didn’t have this feature in the Unit Testing.   Instead, this creates a mock HttpContext to host the DNN application.  The end result is similar, although I suspect the Visual Studio version would probably be superior.  If time and inclination permits, I may investigate this in the future and modify the DNNUnitTest code to match.&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;h2&gt;Recommendations for Testing Module Code&lt;/h2&gt;  &lt;p&gt;Hopefully you have managed to setup the example test project and have got the code running.  You can step through the individual tests and investigate how it all works.  About now is the time you should start thinking about how to adapt this to your projects.  If you develop your modules in a similar way, then you can just remove the ‘iFinity.Example’ module, and replace with your own project.  Or, more realistically, either include the iFinity.DNN.Utilities module in with your existing project solution, or just build and reference the DLL once you have it working.  You can then add a test project to your solution, copy across the various components of the example test, and start writing your tests.&lt;/p&gt;  &lt;p&gt;In the example module tests, you’ll see there is a test for each property set/get.  You may think this is overkill, but you’d be surprised how many times this will catch a small but lethal bug from a mistyped property accessor.  Also, in this example module, you’ll see that a flag is kept over whether the data in the object has changed since the last check.  The logic determines if this code is running correctly.&lt;/p&gt;  &lt;p&gt;You’ll also see that the module CRUD code is tested against the database.  This is an obvious choice, but don’t forget all the other bits and pieces your module may do, such as reviewing saving/retrieving module settings, generating Urls, creating emails : you should test as much as you can.&lt;/p&gt;</description>
      <link>http://www.ifinity.com.au/Blog/Technical_Blog/EntryId/76/Unit-Testing-DotNetNuke-5-Modules-in-Visual-Studio-with-a-Mock-ASP-NET-Http-Context</link>
      <author>bchapman@ifinity.com.au</author>
      <comments>http://www.ifinity.com.au/Blog/Technical_Blog/EntryId/76/Unit-Testing-DotNetNuke-5-Modules-in-Visual-Studio-with-a-Mock-ASP-NET-Http-Context#Comments</comments>
      <guid isPermaLink="true">http://www.ifinity.com.au/Blog/Technical_Blog/EntryId/76/Unit-Testing-DotNetNuke-5-Modules-in-Visual-Studio-with-a-Mock-ASP-NET-Http-Context</guid>
      <pubDate>Thu, 03 Dec 2009 03:29:00 GMT</pubDate>
      <slash:comments>6</slash:comments>
      <trackback:ping>http://www.ifinity.com.au/DesktopModules/Blog/Trackback.aspx?id=76</trackback:ping>
    </item>
    <item>
      <title>Include jQuery in a DotNetNuke Module with version independent code</title>
      <description>&lt;p&gt;Count me amongst the legions of jQuery fans.  I might be a bit later to the party than others, but to my defence I really like writing server based code, so I do as much of that as possible.   It’s only when faced with the need to ship code that the UI starts getting the attention it deserves.&lt;/p&gt;  &lt;p&gt;Adding jQuery into DNN is an obvious choice, although made somewhat problematic with the standard Prototype library inclusion, and the fact that DNN modules are built with ASP User Controls rather than pages.   You’ll see plenty of advice on the internet to either use the built-in DNN 5 jQuery inclusion method, or to just add the includes into the page where your jQuery code is.   The first is problematic if you’re building for a broad range of DNN versions, the second is problematic if you want to distribute your code without having a long list of post-install steps to get it working. Another issue is that DNN modules can (and will) be copied to the page more than once, so your code has to be careful of including some sort of duplication check, so that you don't end up with 5 references to the jQuery library next time someone gets a little click-happy with the 'Add Module' button.&lt;/p&gt;  &lt;p&gt;With that background, I set about developing a small routine which can be used with any DNN Version, which would provide version-independent, one-line inclusion of the jQuery libaries.&lt;/p&gt;  &lt;h3&gt;Programmatically Including JavaScript and CSS files in ASP.NET&lt;/h3&gt;  &lt;p&gt;The first thing to solve in this task is a simple inclusion of javascript includes in the header of the page.  This is one of those things that you would think is really easy in ASP.NET, but unfortunately it can be a bit of a trick for new players.  Most people start looking around the ClientScriptManager object, but in my opinion it's overblown and messy.  You'll often see people using the ClientScriptManager to do the duplication check by adding in an empty bit of javascript - a brutal way to save effort. The easiest (and cleanest) way to do this is to create a specific html tag, and then actually add that to the header file, like this:&lt;/p&gt;  &lt;pre&gt;      HtmlGenericControl scriptInclude = (HtmlGenericControl)page.Header.FindControl(id);
      if (scriptInclude == null)
      {
           scriptInclude = new HtmlGenericControl("script");
           scriptInclude.Attributes.Add("src", src);
           scriptInclude.Attributes.Add("type", "text/javascript");
           scriptInclude.ID = id;
           page.Header.Controls.Add(scriptInclude);
      }&lt;/pre&gt;

&lt;p&gt;This code looks for a specific html script include tag in the page header, and if not found, creates the tag and adds it in. Couldn't be simpler really - and no messy, empty &lt;script/&gt; tags lying around in your Html.&lt;/p&gt;

&lt;h3&gt;Determining the DotNetNuke Version&lt;/h3&gt;

&lt;p&gt;Why determine the DotNetNuke version? Well, because if you're using DNN 5 and better, then DNN has a built in function for including the jQuery libraries. No need to reinvent the wheel, so if this function is around, might as well use it. However, if you're like me and building modules for a massive variety in DNN versions, there's no telling if the DNN 5 method will exist or not. So you need to determine the DotNetNuke version.&lt;/p&gt;

&lt;p&gt;You might head for the object browser and start looking for the version method or property. That's where more young players will dash themselves upon the rocky shores of DNN history. You see, somewhere along the line (about 4.9, I believe) the location of the version property actually moved within the object hierarchy. Thus, in order to determine the version, you first have to know the version to know where to look for it. Trying to use this circular logic will only lead to tears and torn-out tufts of hair.&lt;/p&gt;

&lt;p&gt;The only avenue left is to bypass all the built-in methods and just go straight for the assembly version. That's always in the right spot, and thankfully, always correct. Here's my version-independent version checking code:&lt;/p&gt;

&lt;pre&gt;     System.Version ver = System.Reflection.Assembly.GetAssembly(typeof(DotNetNuke.Common.Globals))
			.GetName().Version;
     if (ver != null)
     {
         major = ver.Major;
         minor = ver.Minor;
         build = ver.Build;
         revision = ver.Revision;
         return true;
     }
     else
     {
         major = 0; minor = 0; build = 0; revision = 0;
         return false;
     }&lt;/pre&gt;

&lt;p&gt;Incidentally, that method is so version-safe it can be used on &lt;em&gt;any&lt;/em&gt; asp.net project, not just DNN. Incidentally, just because you've determined the version, you're not out of the woods yet. Because you can't just compile against a method in a later version library and expect it to all work with an earlier version. No, instead we must fall back to the all-purpose tool of the .NET developers toolbox : reflection:&lt;/p&gt;

&lt;pre&gt;     Type jQueryType = Type.GetType("DotNetNuke.Framework.jQuery, DotNetNuke");
     if (jQueryType != null)
     {
         //run the DNN 5.0 specific jQuery registration code
        jQueryType.InvokeMember("RequestRegistration", 
                     System.Reflection.BindingFlags.Static, null, jQueryType, null);
     }&lt;/pre&gt;

&lt;p&gt;This code will run the DNN jQuery 'RequestRegistration' method if it's available (ie, installed in a DNN 5 environment), but will still compile against earlier versions of DNN libraries. For more information on this call, see &lt;a href="http://blog.theaccidentalgeek.com/post/2008/10/20/Using-jQuery-in-DotNetNuke-50.aspx"&gt;Joe Brinkman's informative post on jQuery and DotNetNuke 5&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;Including the jQuery Libaries&lt;/h3&gt;

&lt;p&gt;So, equipped with code to work out what version we're using, and armed with code to insert in a javascript include file, there's nothing left to do but stitch it all together. Just one last thing : Google, in their infinite bandwidth and hey-we'll-get-your-data generosity, provide a hosted version of the jQuery libraries. I've long ago given up on trying to hide anything from the big G, so I'm happy to just mooch off their site and use the hosted version. Thus the example code contains references to the Google libraries. If you're of a more suspicious and distrusting nature, then by all means include the jQuery library with your app and change the code.&lt;/p&gt;

&lt;h3&gt;Full Code Listing&lt;/h3&gt;

&lt;pre&gt;	/// &lt;summary&gt;
        /// Includes the jQuery libraries onto the page
        /// &lt;/summary&gt;
        /// &lt;param name="page" /&gt;Page object from calling page/control&lt;/param&gt;
        /// &lt;param name="includejQueryUI" /&gt;if true, includes the jQuery UI libraries&lt;/param&gt;
        /// &lt;param name="uncompressed" /&gt;if true, includes the uncompressed libraries&lt;/param&gt;
	/// &lt;param name="includeNoConflict" /&gt;if true, includes the uncompressed libraries&lt;/param&gt;
        internal static void InjectjQueryLibary(System.Web.UI.Page page, bool includejQueryUI, 
						bool uncompressed, bool includeNoConflict)
        {
            int major, minor, build, revision;
            bool injectLib = false;
            if (SafeDNNVersion(out major, out minor, out revision, out build))
            {
                switch (major)
                {
                    case 5: 
                        //todo: all versions of 5?
                        injectLib = false;
                        break;
                    default:
                        injectLib = true;
                        break;
                }
            }
            else
                injectLib = true;

            if (injectLib)
            {
                //no in-built jQuery libraries into the framework, so include the google version
                string lib = null;
                if (uncompressed)
                    lib = "http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.js";
                else
                    lib = "http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js";

                if (page.Header.FindControl("jquery") == null)
                {
                    System.Web.UI.HtmlControls.HtmlGenericControl jQueryLib 
				= new System.Web.UI.HtmlControls.HtmlGenericControl("script");
                    jQueryLib.Attributes.Add("src", lib);
                    jQueryLib.Attributes.Add("type", "text/javascript");
                    jQueryLib.ID = "jquery";
                    page.Header.Controls.Add(jQueryLib);

                    if (includeNoConflict)
                    {
                        // use the noConflict (stops use of $) due to the use of prototype 
			// with a standard DNN distro
                        System.Web.UI.HtmlControls.HtmlGenericControl noConflictScript = 
				new System.Web.UI.HtmlControls.HtmlGenericControl("script");
                        noConflictScript.InnerText = " jQuery.noConflict(); ";
                        noConflictScript.Attributes.Add("type", "text/javascript");
                        page.Header.Controls.Add(noConflictScript);
                    }
                }
            }
            else
            {
                //call DotNetNuke.Framework.jQuery.RequestRegistration();
                Type jQueryType = Type.GetType("DotNetNuke.Framework.jQuery, DotNetNuke");
                if (jQueryType != null)
                {
                    //run the DNN 5.0 specific jQuery registration code
                    jQueryType.InvokeMember("RequestRegistration", 
                            System.Reflection.BindingFlags.Static, null, jQueryType, null);
                }
            }

            //include the UI libraries??
            if (includejQueryUI)
            {
                string lib = null;
                if (uncompressed)
                    lib = "http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/jquery-ui.js";
                    
                else
                    lib = "http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/jquery-ui.min.js";
                page.ClientScript.RegisterClientScriptInclude("jqueryUI", lib);
            }
        }
        /// &lt;summary&gt;
        /// Helper function to insert a given javascript file into the page header
        /// &lt;/summary&gt;
        /// &lt;param name="page" /&gt;Current Page object&lt;/param&gt;
        /// &lt;param name="id" /&gt;Unique (for the page) id of the file&lt;/param&gt;
        /// &lt;param name="src" /&gt;Qualified location of the script file&lt;/param&gt;
        internal static void IncludeScriptFile(Page page, string id, string src)
        {
            HtmlGenericControl scriptInclude = (HtmlGenericControl)page.Header.FindControl(id);
            if (scriptInclude == null)
            {
                scriptInclude = new HtmlGenericControl("script");
                scriptInclude.Attributes.Add("src", src);
                scriptInclude.Attributes.Add("type", "text/javascript");
                scriptInclude.ID = id;
                page.Header.Controls.Add(scriptInclude);
            }
        }
        /// &lt;summary&gt;
        /// Helper function to insert a given CSS file into the page header
        /// &lt;/summary&gt;
        /// &lt;param name="page" /&gt;Current Page Object&lt;/param&gt;
        /// &lt;param name="id" /&gt;Unique (for the page) id of the file&lt;/param&gt;
        /// &lt;param name="href" /&gt;Qualified location of the CSS file&lt;/param&gt;
        internal static void IncludeCSSFile(Page page, string id,string href)
        {
            HtmlLink cssLink = (HtmlLink)page.Header.FindControl(id);
            if (cssLink == null)
            {
                cssLink = new HtmlLink();
                cssLink.Href = href;
                cssLink.Attributes.Add("rel", "stylesheet");
                cssLink.Attributes.Add("type", "text/css");
                cssLink.ID = id;
                page.Header.Controls.Add(cssLink);
            }
        }
        /// &lt;summary&gt;
        /// Returns a version-safe set of version numbers for DNN
        /// &lt;/summary&gt;
        /// &lt;param name="major" /&gt;out value of major version (ie, 4,5 etc)&lt;/param&gt;
        /// &lt;param name="minor" /&gt;out value of minor version (ie 1 in 5.1 etc)&lt;/param&gt;
        /// &lt;param name="revision" /&gt;out value of revision (ie 3 in 5.1.3)&lt;/param&gt;
        /// &lt;param name="build" /&gt;out value of build number&lt;/param&gt;
        /// &lt;remarks&gt;Dnn moved the version number during about the 4.9 version, 
        /// which to me was a bit frustrating and caused the need for this reflection method call&lt;/remarks&gt;
        /// &lt;returns&gt;true if successful, false if not&lt;/returns&gt;
        internal static bool SafeDNNVersion(out int major, out int minor, out int revision, out int build)
        {
            System.Version ver = System.Reflection.Assembly.GetAssembly(
			typeof(DotNetNuke.Common.Globals)).GetName().Version;
            if (ver != null)
            {
                major = ver.Major;
                minor = ver.Minor;
                build = ver.Build;
                revision = ver.Revision;
                return true;
            }
            else
            {
                major = 0; minor = 0; build = 0; revision = 0;
                return false;
            }
        }&lt;/pre&gt;

&lt;h4&gt;Code Licensing on this snippet&lt;/h4&gt;

&lt;p&gt;You're free to take the code on this page and use it how you will, commercial or non-commercial, attributed or not. By all means link back and admit where you pinched it from, or you can try and be sneaky and pass it off as your own work to appear productive to your boss. It's not going to worry me. Just don't try and get me to fix your site if something is wrong!  Oh, and as always, please don’t ask for a VB version : there are oodles of converters out there.&lt;/p&gt;

&lt;h3&gt;Using the jQuery Include Code in a DNN Module&lt;/h3&gt;

&lt;p&gt;You’ll see that I have included a couple of options in the main method.  These are:&lt;/p&gt;

&lt;p&gt;- includejQueryUI : this is for including the standard jQueryUI library from google.  Note that this occurs even if you’re using DNN 5 or better.&lt;/p&gt;

&lt;p&gt;- uncompressed: when set to true, the jQuery code will be downloaded in the uncompressed format.  If ‘false’, then the jQuery code will be downloaded in ‘minified’ format.  Minified decreases the download payload and speeds up page load time, but you wouldn’t try and debug it.&lt;/p&gt;

&lt;p&gt;- includeNoConflict : when set to true, the jQuery.noConflict()  call will be included.  This can be used to stop the ‘$’ namespace clash between the prototype code library in DNN and jQuery.   Personally, I just write all my jQuery code using jQuery(‘’) instead of $(‘’)&lt;/p&gt;

&lt;p&gt;Here’s how you would use this code.  Let’s assume you keep it in a static class called ‘IncludeFunctions’.  In the Page_Load event of your DNN Module User Control, this is the code to include:&lt;/p&gt;

&lt;pre&gt;IncludeFunctions.InjectjQueryLibary(Page, false, false, false);&lt;/pre&gt;

&lt;p&gt;That’s it!  The code will automagically do all the rest for you.&lt;/p&gt;

&lt;p&gt;A further recommendation is to use a conditional compile so that your debug code gets the un-minified version of the jQuery library (while ensuring your RELEASE version uses the minified version:&lt;/p&gt;

&lt;pre&gt;#if (DEBUG)
  IncludeFunctions.InjectjQueryLibary(Page, false, true, false);
#else
  IncludeFunctions.InjectjQueryLibary(Page, false, false, false);
#endif&lt;/pre&gt;

&lt;h3&gt;Bonus Code : Including CSS Files&lt;/h3&gt;

&lt;p&gt;If you're an eagle-eyed reader and actually looked through the code before copy/pasting with abandon, you'll notice that there is a method called 'IncludeCSSFile' in the code. This isn't actually used anywhere in the code to include the jQuery library, but generally with any bit of jQuery code you might care to include with your module, there's generally a couple of CSS files along for the ride. This method allows to to use the same 'no duplicate' principle and include these css files in the Html header, right where they are meant to go. Just use the code like this (again, assuming in the Page_Load event of a DNN Module User Control): &lt;/p&gt;

&lt;pre&gt;   IncludeFunctions.IncludeCSSFile(this.Page, "my-css-file", this.ModulePath + "css/my-css-file.css");&lt;/pre&gt;

&lt;p&gt;The above code will include the 'my-css-file.css' file (located in the /desktopModules/your-module-name/css path) into the page header.&lt;/p&gt;

&lt;p&gt;Further, if you're writing jQuery, chances are you're following the design principle of unobtrusive javascript, so aren't going to include a big chunk of messy script code in the middle of your html. &lt;em&gt;Are You?&lt;/em&gt; So you can use the include script function in the code in the same way:&lt;/p&gt;

&lt;pre&gt;   IncludeFunctions.IncludeScriptFile(this.Page, "my-js-file", this.ModulePath + "js/my-js-file.js");&lt;/pre&gt;

&lt;p&gt;This works in the same way and gives a nice, clean way of including the script files into your code&lt;/p&gt;

&lt;p&gt;Note that you might have to remove some line breaks in the main code listing to get the code to work.  &lt;/p&gt;

&lt;p&gt;If you’ve had success, or find a bug, please let me know via the comments.&lt;/p&gt;</description>
      <link>http://www.ifinity.com.au/Blog/EntryId/75/Include-jQuery-in-a-DotNetNuke-Module-with-version-independent-code</link>
      <author>bchapman@ifinity.com.au</author>
      <comments>http://www.ifinity.com.au/Blog/EntryId/75/Include-jQuery-in-a-DotNetNuke-Module-with-version-independent-code#Comments</comments>
      <guid isPermaLink="true">http://www.ifinity.com.au/Blog/EntryId/75/Include-jQuery-in-a-DotNetNuke-Module-with-version-independent-code</guid>
      <pubDate>Sun, 25 Oct 2009 05:33:32 GMT</pubDate>
      <slash:comments>6</slash:comments>
      <trackback:ping>http://www.ifinity.com.au/DesktopModules/Blog/Trackback.aspx?id=75</trackback:ping>
    </item>
    <item>
      <title>Human and SEO Friendly Urls in DotNetNuke 5.1.2</title>
      <description>&lt;p&gt;I’ve had some feedback lately which usually goes something like this:&lt;/p&gt;  &lt;blockquote&gt;   &lt;p&gt;“Now that DotNetNuke 5.1.2 has SEO Friendly Urls by default…”&lt;/p&gt; &lt;/blockquote&gt;  &lt;p&gt;The rest of the sentence is irrelevant – what I’d like to focus on is the assumption that the latest version of DNN has SEO Friendly Urls.&lt;/p&gt;  &lt;p&gt;Now, anyone who regularly reads my blog will know that posts generally fall into either a self-interest post (telling you something I have for you) or an information post (telling you something I know).  This post is going to fall into both categories.  I’m going to tell you things that I know, and in the process of doing that, tell you how I’ve solved that problem with something I have.&lt;/p&gt;  &lt;p&gt;But first, a recap.&lt;/p&gt;  &lt;h2&gt;What is an SEO Friendly Url?&lt;/h2&gt;  &lt;p&gt;I’d like to offer a definition on this one:&lt;/p&gt;  &lt;blockquote&gt;   &lt;p&gt;“An SEO Friendly Url is a globally unique Url that communicates to both humans and search engines what is the most important content on that page, within the standards and conventions of the web, and in the simplest way possible”&lt;/p&gt; &lt;/blockquote&gt;  &lt;p&gt;Note that I include humans on purpose : the entire reason for building web pages is so that humans can read and understand the content.  &lt;/p&gt;  &lt;p&gt;The original Urls for DNN looked like this : mysite.com/default.aspx?tabid=36&lt;/p&gt;  &lt;p&gt;That in no way explained what was on the page, or anything about it.  About the only thing it did was communicate to developers that there was a database table behind the query, and that it contained probably at least 36 rows of data, and it runs on an ASP.NET platform. None of this information is remotely useful to ordinary humans.&lt;/p&gt;  &lt;p&gt;The next improvement for DNN was the ‘Friendly Url Provider’, which transformed the Urls in DNN to look something like this: mysite.com/home/tabid/36/default.aspx.  This improved the context of the Url, because it showed that Tabid 36 was the Home page of the site, so we could expect to find, well, home page content.   However, it still fails my definition because it isn’t &lt;em&gt;“the simplest way possible”.&lt;/em&gt;&lt;/p&gt;  &lt;p&gt;From about DNN 4.6 (using memory here) onwards, there existed an option called ‘urlFormat=”HumanFriendly”’ which could be used in the web.config settings for the FriendlyUrlProvider.  This little ‘hack’ was passed around amongst the forums like a secret handshake, because it transformed your urls into something like this: mysite.com/home.aspx, leaving other DNN users to wonder how you got rid of the infamous /tabid/36 from your Urls.   This Url format is definitely more SEO friendly, because it passes the definition for ‘simplest way possible’.&lt;/p&gt;  &lt;p&gt;And now, in DotNetNuke 5.1.2, the ‘humanFriendly’ option is standard upon installation.   Which brings me back to my original statement, which is ‘&lt;em&gt;Now that DotNetNuke 5.1.2 has SEO Friendly Urls by default…&lt;/em&gt;’&lt;/p&gt;  &lt;h2&gt;Assessing the SEO Attributes of the DotNetNuke Urls&lt;/h2&gt;  &lt;p&gt;Now, don’t get me wrong, because you might think I’m about to criticise the DotNetNuke project.  I’m not.  What I’m going to do is point out where, if performing SEO on your site is important, the standard framework doesn’t meet all of your requirements.  It’s not a criticism at all, no more than saying ‘DotNetNuke does not have a built in Social Networking suite’.  The DotNetNuke platform is used to build specific sites, and as such can’t be all things to all people.  It does an excellent job of being an adaptable, extensible platform, which you can then use to build Search Optimised sites, or Social Network sites, if that’s your thing.  There are many, many reasons why you wouldn’t bother with any SEO at all on your site.  Not all sites are built to be found.&lt;/p&gt;  &lt;p&gt;Going back to my original definition, there’s a couple of things I’m going to higlight:&lt;/p&gt;  &lt;blockquote&gt;   &lt;p&gt;“An SEO Friendly Url is a &lt;strong&gt;&lt;em&gt;globally unique&lt;/em&gt;&lt;/strong&gt; Url that communicates to both humans and search engines what is the most important content on that page, within the &lt;strong&gt;&lt;em&gt;standards and conventions of the web&lt;/em&gt;&lt;/strong&gt;, and in the &lt;strong&gt;&lt;em&gt;simplest way possible&lt;/em&gt;&lt;/strong&gt;”&lt;/p&gt; &lt;/blockquote&gt;  &lt;p&gt;I’ll expand on those three highlighted points with regard to DotNetNuke Urls:&lt;/p&gt;  &lt;p&gt;1. Globally Unique Url :  One of the big problems with DotNetNuke is a feature of it’s evolution.  DotNetNuke has evolved through the three stages of Urls as outlined above, and for backwards compatibility reasons, all three formats still work.  Try it yourself : on your site, you will be able to request /default.aspx?tabid=xx, /tabid/xx/default.aspx and /pagename/tabid/xx/default.aspx.  In fact you can put in /my-site-has-duplicate-content-oh-no/tabid/xx/default.aspx and &lt;em&gt;all these urls will work&lt;/em&gt;.  These are not globally unique Urls.  The older your site, the more trouble you have, because you’ll have been indexed in one of the old formats, people will have linked to your old pages, and you’ll just generally have link mayhem.  To make it worse, your home page is available on as many as 5 different urls – all duplicate content, and all to the most-linked page on your site.&lt;/p&gt;  &lt;p&gt;2. Standards and Conventions of the web : Now, I’m not for one minute going to try and convince you that DNN urls aren’t within the W3C standards – of course they are – it’s one of the few areas where browsers and servers actually adhere strictly to the rules, so you have to use legal Urls.  But, from (1.) above, it won’t tell browsers and search engines that you have changed the Url for your content, and that they should update their bookmarks and indexes.  Strictly speaking, if you change a page location, you should forward the user onto the new address and let them know to always look at this new address for the content.  Of course, this is known as a ‘301 Redirect’.   &lt;/p&gt;  &lt;p&gt;The other part of the definitions is ‘conventions’, and I snuck that one in there on purpose.  Really what I mean is ‘within the current trends’.  One of these trends is putting hyphens in between terms in your url.  In reality, there is no difference in pageone and page-one in terms of the W3C standards, but it’s better for humans and better for search engines if they can easily parse the url and grasp that it is two words, and not some new term coined by a marketeer.  Another trend is to remove encoded characters and simplify down to the ASCII set that is valid for Urls.  And DotNetNuke can’t do either of these : two-word pages have the values appended together.  Pages with accented or unicode only characters are transformed into encoded values.&lt;/p&gt;  &lt;p&gt;3. Simplest Way Possible : this just means, in as few characters as you can, and as little superfluous information as you can.  That means no tabids, no page extensions, no database id’s.   Now, as previously mentioned, the DotNetNuke ‘humanFriendly’ url option gives you nice simple Urls  - but only until you add a content module.  Most content modules use the base page to display content, and add extra parameters to the Url to provide all the other content.  For example, the blog module uses /entryid/xx/blog-entry-name as the url.  And when you go to that page, the DNN Friendly Url Provider reverts to the old /blog/tabid/yy/entryid/xx/blog-entry-name format.  For important pages in blogs, forums, ecommerce and events modules, this is not what you want.&lt;/p&gt;  &lt;h2&gt;Getting True SEO Friendly Urls in DotNetNuke&lt;/h2&gt;  &lt;p&gt;This is where the self interest part of the post comes in.  I’ve solved all of these issues and others I haven’t even touched on.  I’ve literally spent well over a thousand hours plugging away working through all of the issues that come with the standard DotNetNuke Friendly Url Provider and coming up with solutions.  It’s been fun – a genuine technical challenge, but I don’t continue to work on this stuff just for grins alone – I’ve done it so you don’t have to.&lt;/p&gt;  &lt;p&gt;So let me say it clearly : DotNetNuke 5.1.2 does not have SEO Friendly Urls any more than DotNetNuke 4.6 had.  While I think the default human friendly Urls are a good idea and make a standard installation look better, most of the SEO issues are the same as they have been all along.  True, it’s the most immediately obvious part and partial answer to the question ‘why are the Urls so ugly?’ – but you need to solve many more issues than just the most obvious.&lt;/p&gt;  &lt;p&gt;I don’t normally overtly push products in this blog, but here’s my call to action : &lt;a title="Url Master Product Page" href="http://www.ifinity.com.au/Products/Url_Master_DNN_SEO_Urls"&gt;download and trial the Url Master module today&lt;/a&gt;.  It’s a quick and painless install and will not only solve all of your Url-related SEO issues for your DNN site, but it will also give you much greater control over the Urls as well.&lt;/p&gt;</description>
      <link>http://www.ifinity.com.au/Blog/EntryId/74/Human-and-SEO-Friendly-Urls-in-DotNetNuke-5-1-2</link>
      <author>bchapman@ifinity.com.au</author>
      <comments>http://www.ifinity.com.au/Blog/EntryId/74/Human-and-SEO-Friendly-Urls-in-DotNetNuke-5-1-2#Comments</comments>
      <guid isPermaLink="true">http://www.ifinity.com.au/Blog/EntryId/74/Human-and-SEO-Friendly-Urls-in-DotNetNuke-5-1-2</guid>
      <pubDate>Tue, 29 Sep 2009 23:46:00 GMT</pubDate>
      <slash:comments>5</slash:comments>
      <trackback:ping>http://www.ifinity.com.au/DesktopModules/Blog/Trackback.aspx?id=74</trackback:ping>
    </item>
    <item>
      <title>The New SEO Spam Threat : Human Spam Bots</title>
      <description>&lt;h3&gt;In the beginning…&lt;/h3&gt;  &lt;p&gt;Ever since Sergey and Larry invented the PageRank algorithm, published it and started Google, people have been looking for ways to build lots of external links to their sites.  Up to a little while ago, this was done through sending out email spam looking for link exchanges, and having ‘link’ pages.  While there is still benefit in trading links with relevant people, I suspect most people delete link exchange requests immediately without even reading them, just like I do.&lt;/p&gt;  &lt;h3&gt;Along comes ‘web 2.0’&lt;/h3&gt;  &lt;p&gt;While I’m not a fan of the term, one of the features of ‘Web 2.0’ was the level of interaction people could have with websites.  While forums are nothing new, being able to create and upload content on wiki style sites, as well as leave comments on blogs without creating accounts was the next big thing in SEO spammers.   The early blog software was pretty basic, and it didn’t take much effort to write an automated bot to go and automatically post your links all over the web.    The response to this was the ‘Captcha’ field : pretty much a feature of most blogs these days.  I hate the things : a programmer solution to a user problem if I ever saw one.  You’ll note I don’t use one with this blog.   I just manually delete the spam comments as they come in.&lt;/p&gt;  &lt;p&gt;The rel=nofollow era&lt;/p&gt;  &lt;p&gt;Next along was a suggestion from Google that people start using a new tag, called ‘rel=nofollow’ on links.  This would instruct search engines not to pass any rank onto a link tagged with this bit of html.   While this still works, and is effective in preventing losing pagerank for commenters, it’s a blanket approach.  I don’t mind passing on pagerank from a blog page to a useful link posted in a comments field.  In the linking world, what goes around comes around.  If a page is that important for ranking, you wouldn’t have any user-contributed content on it anyway.  I certainly would hope that if I contributed a useful comment to another blog, a bit of link love might come back my way.&lt;/p&gt;  &lt;h3&gt;The Army of a Thousand Monkeys&lt;/h3&gt;  &lt;p&gt;With captchas and nofollow common in the blog comments world, the Spammers were slowed down a bit.  You could easily stop automated bots from posting to comment fields, and if you didn’t use a captcha, you didn’t lose anything because of the nofollow links anyway.&lt;/p&gt;  &lt;p&gt;But the prize is still there : all it takes is a couple of hundred links ‘out there’ and a page can start ranking for a given keyword/keyphrase.  So SEO Spammers have turned to a new trick : the ‘mechanical turk’ approach.  No disrespect to Amazon but the term existed before they started using it.&lt;/p&gt;  &lt;p&gt;What I’ve noticed lately (and discussions with other site owners confirms this) is that there appears to be an army of workers who will dilligently create an account and start posting links all over your site.   You can’t stop them with a Captcha because it is a real human working the controls.  You can’t stop them posting links because you need to let your legitimate users post links.  You can only realise that it’s a spam post after the fact, when your powerful head-mounted computer instantly recognises the fact.&lt;/p&gt;  &lt;p&gt;I’ve found that these spammers like forums the best.  Just a nice, empty page, usually freedom to enter all sorts of html and the ability to make lots of different posts once an account has been created.&lt;/p&gt;  &lt;p&gt;I’ve no idea how people get an ROI on this activity, but I suppose a determined poster can rack up a couple hundred links in an hour if they set out to do it.&lt;/p&gt;  &lt;h3&gt;How to stop the Tide&lt;/h3&gt;  &lt;p&gt;I’ve been mulling over this question a lot, lately.  Specifically because I have seen the problem affect DotNetNuke sites badly : there isn’t a lot of Spam protection (apart from a [somewhat troublesome] captcha installation).  Things like the forum software doesn’t have captcha built in, and besides, we’ve already established that Captcha doesn’t work with human SEO bots.&lt;/p&gt;  &lt;p&gt;Some time back I switched to a bayesian spam filter for my email, after getting frustrated with other [lame] solutions which rely on black lists and keyword lists and other ultimately doomed technology.  I say ultimately doomed because it is defence by whack-a-mole.  It’s always reactive and takes a huge effort to be successful.  It also creates too many false positives : that is, normal email that gets trapped in the spam folder, never to be seen.  That’s because, like adults, occasionally your friends use the word ‘sex’ in emails as well.  &lt;/p&gt;  &lt;p&gt;The reason I got onto this was through Paul Graham’s excellent book &lt;a href="http://www.paulgraham.com/hackpaint.html"&gt;Hackers and Painters&lt;/a&gt;, in which a chapter is devoted to his &lt;a href="http://www.paulgraham.com/better.html"&gt;Bayesian Spam Filter&lt;/a&gt; software.   By the time I got around to reading it, there was already an implementation of the &lt;a href="http://spambayes.sourceforge.net/index.html"&gt;bayesian algorithm for Outlook called SpamBayes&lt;/a&gt;, which I promptly downloaded and installed. It’s even open source.  And I’ve have been happy with it, ever since.&lt;/p&gt;  &lt;h3&gt;Experimenting with DotNetNuke&lt;/h3&gt;  &lt;p&gt;I decided that the way forwards was to try and implement the same algorithm in a one-size-fits-all implementation for DotNetNuke.  The idea would be that you could point it to any module where visitors are allowed to create content.  If it suspected an entry in a page was Spam, it would either send back a 404, redirect the user to somewhere else, or ‘silently fail’ : meaning the content would be created but deleted later, or perhaps modified in such a way that it was a pointless post for the user (for example, replace the offending text with ‘This message was detected as Spam and deleted’.&lt;/p&gt;  &lt;p&gt;I’ve been experimenting with this approach and have had some success, although it needs a lot more trials to figure it out better.  The beauty with the statistical based approach is that it is tailored to each and every site, though you do need to ‘teach’ your particular version what you consider to be spam.   I’ve come to a fork in the road, though, and before I spend much more time, I though I better get some early feedback to see if I’m on the right track.&lt;/p&gt;  &lt;p&gt;This post is really to put it ‘out there’ and see what people think.   Are human spam bots creating content on your site, bloating your blog comments and filling up your forums?   Have you already found a successful solution?  Is a generic solution for any type of DNN module the right way, or do people want specific solutions for specific content modules (ie, blog, forum, etc).  Let me know via the comments - no spam please :)   That means you, &lt;a href="http://www.ifinity.com.aumailto:charlie2342435@yahoo.com"&gt;charlie2342435@yahoo.com&lt;/a&gt;!&lt;/p&gt;</description>
      <link>http://www.ifinity.com.au/Blog/EntryId/73/The-New-SEO-Spam-Threat-Human-Spam-Bots</link>
      <author>bchapman@ifinity.com.au</author>
      <comments>http://www.ifinity.com.au/Blog/EntryId/73/The-New-SEO-Spam-Threat-Human-Spam-Bots#Comments</comments>
      <guid isPermaLink="true">http://www.ifinity.com.au/Blog/EntryId/73/The-New-SEO-Spam-Threat-Human-Spam-Bots</guid>
      <pubDate>Wed, 23 Sep 2009 10:11:18 GMT</pubDate>
      <slash:comments>3</slash:comments>
      <trackback:ping>http://www.ifinity.com.au/DesktopModules/Blog/Trackback.aspx?id=73</trackback:ping>
    </item>
    <item>
      <title>Google Confirms Meta Keywords Tag Not used for Rankings</title>
      <description>&lt;p&gt;Google recently posted advice on it’s Google Webmaster blog advice that the &lt;a href="http://googlewebmastercentral.blogspot.com/2009/09/google-does-not-use-keywords-meta-tag.html"&gt;Meta Keyword tag in Html pages does not count towards ranking&lt;/a&gt;.  This is not really news to anyone who follows trends in SEO and does research into what works well and what doesn’t, but it’s an interesting move by Google.  &lt;/p&gt;  &lt;h3&gt;A Short History of the Meta Keyword Tag&lt;/h3&gt;  &lt;p&gt;If you’re the sort who gets confused with your metas, tags, links and scripts, the Meta Keyword is a particular Html tag that is embedded in the page, but not visible to the end user.   You can see this if you view the source of a web page - the syntax is as follows:&lt;/p&gt;  &lt;p&gt;&lt;meta name="KEYWORDS" content="iFinity Blog" /&gt;&lt;/p&gt;  &lt;p&gt;You can also see this information in FireFox under the Tools-&gt;Page Info menu item.&lt;/p&gt;  &lt;p&gt;This is contained in the Html &lt;head&gt; section of a standard web page.  The tag is as old as Html itself, and reached it’s zenith in the mid 1990’s with the early search engines using this value to determine rank for pages.  That was back when people were relatively honest about what was on their page.  It was also back in the days where you didn’t get spam emails in your inbox, either.  It didn’t take long for people to work out that you could ‘stuff’ your Meta Keywords tag and rank your page for all sorts of things.  &lt;/p&gt;  &lt;p&gt;With the arrival of Google in the late 90’s, search engines began their current migration away from on-page factors like meta keywords and towards off-page factors such as external links.  This was because search engines became virtually useless relying on factors that could be easily manipulated, as search result quality depended on how badly a webmaster wanted to be at the top of a page, rather than what was the best result for a given query.  You could argue will still aren’t there yet, but for someone who has been submitting search queries for over 15 years, the improvements are immense.&lt;/p&gt;  &lt;h3&gt;So what does it mean?&lt;/h3&gt;  &lt;p&gt;You might be wondering ‘how does this affect me?’.  The answer is : it doesn’t.  This is just an official announcement of what has been widely known for years.  The keywords Meta Tag doesn’t influence your ranking &lt;em&gt;at all&lt;/em&gt;.  You could probably even stuff your keywords meta tag with known spammy terms and it shouldn’t make any difference.  I wouldn’t recommend doing that, though.&lt;/p&gt;  &lt;p&gt;About the most important thing this announcement does is to stop any arguments and discussions speculating whether the keywords tag does affect your ranking in Google.&lt;/p&gt;  &lt;h3&gt;So I should delete my keywords Meta Tag, right?&lt;/h3&gt;  &lt;p&gt;In a short answer, no.  For a second, Google have stated they have no effect on ranking.  So you can still put them in if you want.  There’s a couple of reasons behind my thinking:&lt;/p&gt;  &lt;p&gt;1) You don’t know what search technologies are coming down the line.  Leave in your keywords in case they become important again (although I concede this is unlikely).&lt;/p&gt;  &lt;p&gt;2) Only Google has made this announcement.  We don’t know about Yahoo, Bing et al.  They may still use them to sort out ranking for obscure terms or long-tail type searches.   Besides, taking the time to think about the correct keywords in a page helps you to concentrate on the in-page factors for the page (headers, images, bold keywords etc).  It takes 10 seconds to put them in, so what’s the harm.  Just don’t keyword stuff and expect it to help you.&lt;/p&gt;  &lt;p&gt;However, if you’ve got the one-eyed Google love going and hate the Keyword Meta Tag, by all means expunge them from your site.  It’s not going to matter much either way.&lt;/p&gt;  &lt;h3&gt;What about the Meta Description?&lt;/h3&gt;  &lt;p&gt;The other common ‘Meta’ tag is the Description.  This is a short description of the contents of the page.  As the Google webmaster post says &lt;/p&gt;  &lt;blockquote&gt;   &lt;p&gt;“For example, we do sometimes use the "description" meta tag as the text for our search results snippets.&lt;/p&gt;    &lt;p&gt;Even though we sometimes use the description meta tag for the snippets we show, we still don't use the description meta tag in our ranking.”&lt;/p&gt; &lt;/blockquote&gt;  &lt;p&gt;So you absolutely do want to include a short, useful description in your Meta Description field.  Try and keep it to about 150 characters, and Google may pick this up in it’s entirety and use it as the description for their search result on the page.  Even better, they highlight the matching keywords in bold.  You need to take the time to create this entry carefully.  This should be crafted as carefully as the two lines of text that appear in a Google Adwords advertisement.  You need to get the maximum clickthrough from the results page to your website, or all your ranking efforts will be for nothing.&lt;/p&gt;  &lt;h3&gt;Actions to take today&lt;/h3&gt;  &lt;p&gt;If you have spammy keyword tags, you may as well clean them up to be short and succinct.  Aim for just a handful of words to summarise the page.&lt;/p&gt;  &lt;p&gt;If you don’t have any tags, don’t worry.&lt;/p&gt;  &lt;p&gt;If you do have a couple of short tags, don’t worry.&lt;/p&gt;  &lt;p&gt;If you’ve got a content module that generates these tags for you automatically, don’t worry.&lt;/p&gt;  &lt;p&gt;In other words : don’t really worry about it.  But do take the time to review your Meta Description tags to make sure they are nice, short and snappy, and aren’t keyword spammed.  Because that just looks, well, &lt;em&gt;so 1995&lt;/em&gt;.&lt;/p&gt;</description>
      <link>http://www.ifinity.com.au/Blog/EntryId/72/Google-Confirms-Meta-Keywords-Tag-Not-used-for-Rankings</link>
      <author>bchapman@ifinity.com.au</author>
      <comments>http://www.ifinity.com.au/Blog/EntryId/72/Google-Confirms-Meta-Keywords-Tag-Not-used-for-Rankings#Comments</comments>
      <guid isPermaLink="true">http://www.ifinity.com.au/Blog/EntryId/72/Google-Confirms-Meta-Keywords-Tag-Not-used-for-Rankings</guid>
      <pubDate>Tue, 22 Sep 2009 05:29:40 GMT</pubDate>
      <slash:comments>2</slash:comments>
      <trackback:ping>http://www.ifinity.com.au/DesktopModules/Blog/Trackback.aspx?id=72</trackback:ping>
    </item>
    <item>
      <title>The day my server died</title>
      <description>&lt;p&gt;This post is a little out of the usual for my blog, but it’s a tale worth telling, particularly if you’ve got more than one computer and data you just can’t lose.&lt;/p&gt;  &lt;p&gt;Just over a month ago I started a new project of moving my office.  This involved commissioning and building a new office out of a previously unused corner, and fitting it out exactly as I wanted it to be.  A clean sheet design, if you will (within budget and space constraints, of course)  This new design would incorporate tidy wiring, a server ‘cupboard’ (really, a mini server room), everything up off the floor (so my Roomba can work efficiently) and, well, would be a calm private place of productivity.&lt;/p&gt;  &lt;h3&gt;The Move &lt;/h3&gt;  &lt;p&gt;Things always take longer than planned, especially if the plan is on the back of an envelope.  This was bad enough, because I had to set things up temporarily and work with stuff absolutely &lt;em&gt;everywhere&lt;/em&gt; for a couple of weeks while the final touches were made.  If anyone was waiting on me during this period they probably received an email that said something like ‘please bear with me I’m in the middle of moving’.&lt;/p&gt;  &lt;p&gt;Finally, though, everything was in place.  My brand-new development machine was up and running with Windows 7 and twin 23” monitors and I started to regain productivity : slowly at first, but I was soon in the swing of things.&lt;/p&gt;  &lt;h3&gt;The Death&lt;/h3&gt;  &lt;p&gt;However, there was one thing I hadn’t counted on : a dead server.  It happens so innocuously : you make a request to a server, and it is offline.  It happened on a Saturday afternoon, so I just ignored with it, thinking “I’ll deal with that Monday'”.&lt;/p&gt;  &lt;p&gt;But it was dead.  The culprit was a failed main system drive : on my Primary Domain Controller.  The only domain controller, as it turns out – who needed a backup for small setup? It was also the email server, file server and remote access server.  Heat was probably the issue : my newly designed server storage cupboard was running hot : air temperature over 34 degrees (celsius) – the heat soak on the server rack like a car on a sunny day.   I opened all the cases on the computers to give more ventilation and checked all the other drives : too hot to touch.    Another computer spontaneously turned itself off : some type of CPU heat shutdown I expect.&lt;/p&gt;  &lt;h3&gt;The Backup&lt;/h3&gt;  &lt;p&gt;About this time I cheerily turned to my backups, and thought I’d have the whole thing running again a in a jiffy : how wrong I was.  The problem is that with Active Directory is that even if you have all the NTDS database backed up, if you can’t recover the system registry, you’re hosed.  And when you have no active directory, you’ve got no domain.  When you’ve got no domain, you’ve got no exchange server.  No Exchange server, no emails, tasks, calendar appointments.  Your other computers will work on cached mode, but when they start looking for network resources with no primary domain controller to work with, that’s all going to fail as well.&lt;/p&gt;  &lt;h3&gt;The Rebuild&lt;/h3&gt;  &lt;p&gt;I spent a day trying to resurrect what was left of the disk drive onto a newly purchased system disk.  This was frustratingly close to working, yet, as these things go, so very far from actually working correctly.  After several attempts to restore, I cut my losses and started from afresh, formatting the hard drive and building an entirely new domain from scratch.&lt;/p&gt;  &lt;p&gt;Of course, all of the setup had been years in the making : new share here, an extension there.  I had to put it all together.  Fortunately I called upon my brother to help me sort it out : he does it for a living so easily solves problems that would have me melting down a Google node with queries.&lt;/p&gt;  &lt;p&gt;The other big problem is that all of my install media is out-of-date in terms of service packs.  In some cases I had to download gigabytes worth of new installs or service packs to get everything back up to date.  This not only takes a long time, but I also blew my download limit and got ‘shaped’ as a result.  With Windows update these days, the installs are done silently and you don’t realise how different your systems are from their original install disks.&lt;/p&gt;  &lt;h3&gt;The Restore&lt;/h3&gt;  &lt;p&gt;Once the new domain was up and running, it was a case of getting back the exchange backups and setting up the email / appointments / tasks as they were.  Only problem with that was all that was set up for a different domain, and the new exchange server in the new domain didn’t want to have anything to do with the backups from the old domain/exchange server.  So I had to go another route and get some software to convert the cached Outlook data back into a Personal Mail Folders file (.pst) and re-import it back to exchange.  This also loses a lot of the ‘meta’ information like when you replied to emails, what was read and what wasn’t.  It does get you 90% of the way back, but it’s not the best way.  &lt;/p&gt;  &lt;p&gt;But finally everything was back to working as normal, and only a few emails and a few calendar entries and to-do items are gone and lost forever (sorry if you’re waiting on me to turn up for something, or I’ve missed your birthday!)&lt;/p&gt;  &lt;p&gt;However, once you’ve destroyed an old domain and created a new one, then there are a lot of little things that drag you down.  All of your favourites, profiles, cookies, even your wallpaper is all hooked up to your old domain profile.  You start again with all that.  Yes I used the files and settings transfer program that Microsoft provide : this just frustratingly transfers across half your stuff.  Yes, you might get the right desktop background, but not your authentication cookies or all of your favourites.&lt;/p&gt;  &lt;p&gt;Team Foundation Server : always a flighty mistress at the best of times – it’s very sensitive to things like authentication changes.  So a complete piece-by-piece pick through of the TFS server finding all the little services, shares, intranet sites and bits and pieces and re-updating the user permissions and everything else.&lt;/p&gt;  &lt;h3&gt;The Results&lt;/h3&gt;  &lt;p&gt;I wrote this both as a cathartic : “get it all out” and the classic  precautionary tale “don’t do this at home”.  You see, with my RAID drives, scheduled backups, offsite storage and more, I figured I was pretty safe from data loss.  And I was/am.  I didn’t lose any vital work, source code, documentation or anything like that.  What I did lose was a lot of time, which affects service, projects underway, client perception and most of all : my sanity after watching more setup progress bars than any sane person should.  It would have been much more fun re-coding a lost project than messing about trying to get an Active Directory installation back up and running.&lt;/p&gt;  &lt;p&gt;Oh, and I installed a larger extractor fan to the ‘server cupboard’ – it keeps the computers are a nice running temperature all the time.&lt;/p&gt;  &lt;p&gt;Here’s what I would recommend everyone stop and think about:&lt;/p&gt;  &lt;p&gt;- if you lost a disk drive today, would you be able to get back all of the important data on it?  Remember disk drives are like pets : in the end they all die and we’re likely to outlive them.&lt;/p&gt;  &lt;p&gt;- if you’ve got a domain setup , do you have backup domain controllers?  Do you have a backup of your NTDS database? Are you servers replicating properly – an out-of-date domain controller is pretty much the same as no domain controller at all.&lt;/p&gt;  &lt;p&gt;- if you lost your email, do you have a backup solution?  In my case I could carry on using web-based mail, but it is a very inferior solution to a full-featured email system like Outlook.&lt;/p&gt;  &lt;p&gt;- have you checked your backups recently?  Are the jobs working?  Are you out of disk space?  Have you tried to do a test restore and make sure the data is OK?&lt;/p&gt;  &lt;p&gt;- do you download and save your service packs or just let them run automatically?  It’s better to save (and burn) the bigger service packs, because always relying on the download takes time and bandwidth.  One thing you can count on with Microsoft is that eventually a service pack will be larger than the software it is patching.&lt;/p&gt;  &lt;p&gt;When computers are your life, and how you make your living, it’s easy to forget how dependant we all are on them.  And yet they fail from time to time : it’s part of the job and part of the technology.  So spend some time making sure everything is OK.&lt;/p&gt;</description>
      <link>http://www.ifinity.com.au/Blog/EntryId/71/The-day-my-server-died</link>
      <author>bchapman@ifinity.com.au</author>
      <comments>http://www.ifinity.com.au/Blog/EntryId/71/The-day-my-server-died#Comments</comments>
      <guid isPermaLink="true">http://www.ifinity.com.au/Blog/EntryId/71/The-day-my-server-died</guid>
      <pubDate>Fri, 21 Aug 2009 07:58:19 GMT</pubDate>
      <slash:comments>5</slash:comments>
      <trackback:ping>http://www.ifinity.com.au/DesktopModules/Blog/Trackback.aspx?id=71</trackback:ping>
    </item>
  </channel>
</rss>