My current CI setup

I was asked by email what my current CI setup is, and did I have a blog post about it.  Um, actually, no.  Oops.  So, here it is.  As always, it's a work in process, and there are lots of unfinished rough edges.  It's also got some phenomenally cool stuff too  Thus, without further ado, my current setup for Continuous Integration:

CruiseControl.NET which runs on SVN commit that runs an NAnt build script which runs:

- MSBuild on one or more solutions

- aspnet_compiler.exe on all the websites (to validate the code in the markup)

- Use YUI Compressor to compress CSS files and compress and combine JavaScript files (yes, the irony isn't lost here)

- NUnit tests including:

       - Fire up Cassini on each site and insure a carefully selected page doesn't blow chunks.  (e.g. no configuration or initialization errors on each site.)

       - Database / code integrity checks like do the enum values match the lookup table content(I realize they're mostly integration tests, and cheesy at that, but it's a far cry better than the previous state of zero tests and "hope it works out" deployment.)

- Deploy content to test server(s), calling iisreset and stopping / restarting services as necessary.

- Label the CI build via svnrevisionlabeler (so the build number in CCTray matches the SVN version number).

- Email out to those who want the spam how the build did.  (Personally I prefer data pull mechanisms like CCTray.)

There's also an SVN commit trigger that generates a commit email and sends it out.

That's what I've got now.

What I want to add to this (given another 257 hours in the day):

- JSLint validation of .js files and hopefully script tags in html

- CSS validation of .css files and hopefully style tags in html

- HTML validation to match the page's doctype of .aspx and .html pages

- SEO evaluation of .aspx and .html pages by crawling the site

- Database migration via Tarantino or RedGate's Sql Compare Pro & Sql Data Compare Pro

Once I've got these in place, I'll be confident that the code functions and is of descent quality before I deploy it to the test servers.  Granted, I haven't validated that it functions correctly, only that it functions completely.  The next step will be to look to Selenium Grid to validate JS works cross-browser and that various pages function as expected.  I hope by then I can also kick-start the the idea that writing unit tests to validate the code functions as expected is also a good idea.

Add a bit of duct tape, a sprinkle of insanity, and that's my CI setup.  Cheers.

Rob

.IsNullOrEmpty() for List and Dictionary

string.IsNullOrEmpty() in C# for strings is awesome.  I pass in a string, it tells me if it was null or blank.  Pre-trim it with something like this :

string.IsNullOrEmpty( ( var ?? "" ).Trim() )

and I know if I'm toast or not before the null reference exception or blank screen.

Well, what if I have a List<T>?  Or a Dictionary<T,U>?  Here's extension methods I wrote for checking blank-ness:

    public static bool IsNullOrEmpty<T>( this IList<T> List ) {
        return ( List == null || List.Count < 1 );
    }

    public static bool IsNullOrEmpty<T,U>( this IDictionary<T,U> Dictionary ) {
        return ( Dictionary == null || Dictionary.Count < 1 );
    }

The added benefit of this is I can say:

    myDict.IsNullOrEmpty()

which is usually more like the thought I had when I started writing the code.  So I also add this method:

    public static bool IsNullOrEmpty( this string String ) {
        return string.IsNullOrEmpty( ( String ?? "" ).Trim() );
    }

so I can call it like this:

    myString.IsNullOrEmpty()

but since I'm coalescing to empty string before trimming, I can just as easily say:

    public static bool IsNullOrEmpty( this string String ) {
        return ( (String ?? "").Trim() != "" );
    }

And for good measure, here's a similar JavaScript function I wrote to check for blank-ness:

    function isNullOrEmpty(val) {
        var empty = true,
            name = null;

        if ( typeof(val) === 'undefined' || val === null ) {
            return true; // It's null or undefined
        }
        if ( typeof(val) === 'string' ) {
            return (val === ''); // It's a string that may or may not be blank
        }
        if ( typeof(val) === 'object' ) {
            if (value.constructor === Array && val.length === 0) {
                return true; // It's an empty array
            }
            for ( name in val ) {
                if ( val.hasOwnProperty(name) ) {
                    empty = false;
                    break;
                }
            }
            return empty; // It's an object that has or doesn't have data in it
        }
        // It's not null or empty
        return false;
    }

And that, as we say, is null ... or empty.  :D

Validating web content in CI

If I had another 257 hours in the day, I'd love to build the ultimate web content validator into the continuous integration process I now have.  After a successful build, I'd start by kicking off a WebDev.WebServer instance of the site, then fire the SEO toolkit by wrapping it into a .net library.  Then extend it with custom tasks run on each page download: like validating the HTML and CSS via W3C, validating the JavaScript via JSLint, and for html content, I'd regex out script and style tag content, padding the top of a temp file with whitespace to keep the line numbers right, then validate that as CSS and JavaScript as well.  (I'd rather find an offline way to do HTML validation that doesn't involve Cygwin, as I have enough emulation going on here.)  Perhaps I'd wrap it all in an NAnt task, or just an NUnit test suite that either reflected through the solution for web.configs or took in a list of projects via TestCase or the project's AppSettings.  (I'd like to be able to authenticate certain requests too, so I can validate the user profile content.  WebCrawler.Settings exposes a Credentials property, though I've had more success setting an Authorization header than using HttpWebRequest.Credentials.  Neither gets me through the forms authentication cookie though as UrlDownloader.WebRequestCreate() has no settings.Cookies.  I'd love a per-url dictionary of "use credentials or don't", though I realize that's totally overkill for the stock use of the SEO toolkit, and more than likely I can just rescan with a StartUrl inside the profile, and an ExternalLinkCriteria of SameFolderAndDeeper.  If all else fails, I'd Reflector out the WebCrawler, or inject an override into UrlDownloader.OnGetContent() in the same dll.)  More than likely, the report from each of the validators is too big for the build log, so I'd save off each report, named by download url and module, and build an index page dynamically to navigate through them all.  After the report was run, I'd RoboCopy the report tree to a deployment url or find a way to include it into CC.NET's document list.  Wrap all that up in a bow, and we've got the uber-web validator CI engine.  Now where'd I put that other 257 hours in the day?  And will condensing a few weeks worth of random thoughts and links this tightly get me into the Google Dungeon?

Working with both VS 2010 and 2008 on the team

With any software version upgrade, there are those that upgrade on day 1, and can't wait to get the beta bits of v.next, and there are those who wait-n-see, wait for a plugin to get upgraded, or just generally need some time to get comfortable with the change.  Upgrading to Visual Studio 2010 is no exception.

When you've got a team of people and certain people fall into each category, things can get interesting.  In this case, there are some specific steps we can take to facilitate both groups of users simultaneously working on the same code base.

Note that upgrading a solution from .net 3.5 to .net 4.0 is also a step to take, but you can't use VS 2008 with .net 4.0, so you'll need to wait for the entire team to upgrade for that.

Towards the end of using .net 3.5 simultaneouslyin both VS 2008 and VS 2010, the steps are pretty straight-forward.  Much like the adjustments for VS 2008 / VS 2005 compatibility, this is a pretty straight shot.  Here's the steps I took.  I give it the official "works on my machine" seal of approval.

1. Check in any changes to source control.  Make a backup.  Archive the machine.  Get comfortable with it.  If this fails, you'll need it.  :)

2. Fire up Visual Studio 2010, and run the project conversion wizard, insuring you keep the solution in .net 3.5.  It'll add tons of gunk to each .csproj file and to the .sln file, and that's fine.  Getting VS 2010 comfortable with the solution is the biggest leap, and it's also usually the most automatic.

With those done, the rest unfortunately is manual.

3. The first major difference is at the top of the sln file.  A VS 2008 file has these 2 lines at the top:

Microsoft Visual Studio Solution File, Format Version 10.00
# Visual Studio 2008

A VS 2010 sln file has these 2 lines at the top:

Microsoft Visual Studio Solution File, Format Version 11.00
# Visual Studio 2010

That's the only difference.  Unfortunately, you can't exactly switch it back and forth locally.  Well, I guess you could, but someone someday will check it in the wrong way, and the build will go boom.  Bad news.

Instead, my chosen solution is to copy the sln file and name the new one with a catchy suffix.  For example, if I have "mysln.sln", I'll name the new one "mysln-10.sln".  Since we ran the project conversion wizard previously, we can copy the sln file to it's new name, add that to source control, and revert the changes to the original (VS 2008-compatable) sln file.

Periodically diff the files and/or invite the team to add / remove projects from both.  This happens so rarely that it doesn't seem an issue.

4. The next major difference is in Web Application Project .csproj files.  At the bottom, in the <Import ...> section, a VS 2008 project has this line:

<Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v9.0\WebApplications\Microsoft.WebApplication.targets" />

A VS 2010 project has this line:

<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" />

MSBuild's Condition statement comes in really handy here.  Change the line to these lines and it'll work nicely for both:

<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" Condition="'$(Solutions.VSVersion)' == '9.0'" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" Condition="'$(Solutions.VSVersion)' == '10.0'" />

5. The C++ project file is drastically different.  VS 2008 uses .vcproj files, VS 2010 uses MSBuild compatible .vcxproj files.  Unfortunately I find no good way to correlate the differences easily.  Here's my solution of choice:

- In the VS 2008 sln file, reference the .vcproj file.  In the VS 2010 sln file, reference the .vcxproj file.

- When changing the project (adding/removing files), you gotta update both.  Yeah, I hate it too.

- Managed code referencing C++ projects gets weird.  Inside the IDE, all you see is the project name, but in the .csproj file is the name of the C++ project file.  Therefore, if VS 2010 notices the vcproj reference, it'll convert it, and when the build server sees the .vcxproj reference, it'll die noting it doesn't support that project type.  To get around it, I use conditionals here too:

<ProjectReference Condition="'$(MSBuildToolsVersion)' == '3.5'" Include="path\to\project.vcproj">
...
</ProjectReference>
<ProjectReference Condition="'$(MSBuildToolsVersion)' == '4.0'" Include="path\to\project.vcxproj">
...
</ProjectReference>

On the up-side, if you don't have any native code, you can completely skip this step.

6. Source control: We'll see some new files appear in our solution when working with VS 2010.  Here's how I handled them:

- Commit the new VS 2010 .sln file, and the new VS 2010 .vcxproj files (if any)

- Review the additions to .csproj files and web.config files.  You may deem some of the adjustments it made unnecessary (e.g. the big section about "previous tools version").  After removing them, fire the solution back up in both VS 2010 and VS 2008 to see if either gripes or fires up the conversion wizard.  If it does, sadly, ya gotta leave the gunk in.

- The .ncb and .openncb files are the C++ intelisense database.  Exclude them from the repository.  It's like the .user files -- it'll change or get recreated as soon as you open the IDE.

That's it.  Just a few simple steps and you can simultaneously use VS 2008 and VS 2010 on the same code base.  If you don't have Web Application Projects or you don't have C++ projects, it's even easier.  And now each member of the team can use the tool that works best for them.

Unfortunately, those in VS 2010 can't use the C# 4 conventions yet though.  Push the VS 2008 stragglers to upgrade, switch the solution to .net 4.0, and you'll get a whole new love from your IDE.

[Update]: As a matter of "avoiding unnecessary commits to source control", I removed all the additional tags VS 2010 added, including FileUpgradeFlags.  Thanks to Zuhaib (comment below) who pointed out this is the way to get VS 2010 to not keep doing an upgrade loop.  He noted that he had to put the V10.0 path before the V9.0 path as well, which I didn't need to do.

[Update]: I've found more success matching the MSBuild version rather than the VSVersion in random spots.

Presenting jQuery

I had a great honor of presenting both an intro and advanced jQuery class at successive JavaScript Users' Groups.  We had quite a lot of fun discussing both what makes jQuery cool and how one can leverage it for really powerful uses.  Here's the slides for the intro, and here's the project for the advanced one.

NOTE: Internet Explorer users will note that the download fails because the files are corrupt.  This is because my server felt the need to gzip the zipped file, and IE doesn't know how to handle that.  The fault is completely mine, but the files are not corrupt.  Download them with Firefox, and you'll do just fine.

In the intro class, the primary topics included:
  • including in the page and browser compatibility
  • jQuery selectors (CSS queries)
  • basic jQuery functions (.css() is a great example)
  • script loading paradigms and the $(document).ready() function
  • node traversal: .parent(), .children(), .find(), etc
  • a brief look at AJAX calls, though we didn't execute any of them
  • a brief walk through animation (.fadeTo() and .show() / .hide())
Interspersed in this we discussed advanced topics (mostly answers to questions):
  • put CSS in the head, put scripts at the end for best performance
  • avoid redundant trips to the dom by caching selections or chaining
  • catch and handle HTTP errors when making AJAX calls
  • avoid global JavaScript variables
In the advanced class, we talked about:
  • a quick review of the intro class
  • AJAX with $.get(), $.post(), $.getJSON, and $.ajax()
  • AJAX error handling in jQuery
  • built-in jQuery animation, and how to layer effects
  • a lap through our favorite plugins
  • building a plugin: both a $(...).dosomethingwitheach() and a $.dosomethingwhencalled() plugin
  • passing dom ids from server-side frameworks into client-side code
  • using jQuery and jQuery UI from Google's CDN
We also had fun discussing random off-topic ideas:
  • building markup using arrays vs. dom snippits
  • mechanisms for passing page data into library files
  • a quick lap around compression
  • jQuery UI theming
  • tools to use when developing -- to that end, the resources list inside the advanced download is quite helpful
All in all, both days were a barrel of fun, and I was quite honored to be a part of it.

iPhone: it's not really a digital convergence device

It finally gelled in my head what bugged me about the iPhone's lack of simultaneous processing.  It's not truly a digital convergence device.  It's a sequential task device.

A digital convergence device is a device that does lots of things: MP3/FM music player, alarm clock, cell phone, web browser, calendar, address book, GPS-enabled map, pedometer, etc.  The purpose of such a device is to do all of these things, not each of these things.  If I can't do them all simultaneously, it isn't a convergence device, it's a sequential device.  The iPhone is exactly that: a sequential task device.

If I've decided I'm going to IM, I can't do anything else with the phone -- I can't browse the web, can't check on news feeds, can't check email.  The device is useless to me until I get an IM.  If I wanted an IM-only device, I'd have bought one.  I bought a digital convergence device.  The same could be said of any app that receives data: Skype, Google Voice, Email, RSS feeds, Facebook sync, texting services, RTMPGs, etc, as well as anything that monitors anything like Google Latitude, a pedometer app, navigation app, etc.  Anything I expect to alert me to something or to keep track of something either needs to "push notification" me, or I forfeit all other features of this device as I use it for that task.  If it goes the push notification route, the logic must be in the cloud, not on my device, can't be peer-to-peer, and can't get real-time status call-backs -- e.g. it can't auto-detect where I am and update my progress.  As an extreme example, I can't count the number of times I was playing some random game and walked around for a while looking for a clock so I didn't need to kill my game.  My convergence device was in "single task" mode.

The notable exceptions to the single-use mode are all Apple apps: phone, iTunes, and alert apps such as calendar and text alerts.  I can get a call which immediately and irrevocably halts anything I'm doing (including upgrading OS versions), and I can listen to iTunes music while I do a few other things.  But what if my music player of choice is Pandora?

I think you see where I'm going with this.  The iPhone clearly isn't a digital convergence device.  Neither is the iPad.  It is clearly a sequential task device, a data snacker device.  Pick the function you want it to do now, and it'll do great.  Want to do 2 things at once?  Well of course, buy two of them.  Want to get alerted when something happens?  Buy a specialty device too.  Um, I think I'll pass.

iPhone: the rise and fall of the latest Apple gadget

The iPhone.  It's a wonderful device.  It was a game changer.  3 years ago when it was released, it revolutionized the phone landscape.  For the first time, people had a portable digital convergence device.  (Ok, maybe it wasn't the first, maybe it wasn't the best, maybe it was just targeted at regular people instead of corporate users.)  For the first time, we could walk around with a web browser, a calendar, a music player, an address book, and really manage our lives on the go.  (Ok, I'll grant that BlackBerry defined that niche for business users, but the iPhone made it cool and made it work for consumers.)

I'm hesitant to sit on the bleeding edge.  I waited until Service Pack 1 to get my iPhone 3G.  It was nearly 2 years ago.  It was a game changer for me too.  At random times in random places, I could check email, surf the web to answer the nagging question, create calendar entries, or just horse around.  I could pull up a map of where I am and where I wanted to go, so the preparation for travel wasn't as urgent.  I could schedule stuff on my calendar, so no more wads of post-its hoping I guessed right.  And reading email in the 2 minutes standing in line or on the walk home from dropping kids off at school is awesome.  It truly revolutionized my world for the better.

Fast forward 3 years for Apple, nearly 2 years for me.  I waited with baited breath at every Apple announcement for the thing that'd keep the iPhone cool or for them to switch to a new carrier.  (The tag-line for the iPhone I've heard not a few times is, "The iPhone is so cool it almost makes up for being stuck on AT&T.")  In that time, they've started to back-fill some missing features, and made subtle improvements, but they've really never leaped out and grabbed me again.

The following features were instantly missing from my experience, and with few exceptions, Apple really never delivered:
  • OTA Syncing
  • contact and calendar categories
  • turn-by-turn voice navigation
  • copy & paste
  • an IM and VOIP client that's always on (e.g. I'll still get new IMs or receive VOIP calls if I switched over to read my email)
  • 3rd party data on maps (e.g. "map all my contacts" or "where is the closest gas station on my route" or "does Acme, Inc. have an office near me?"  I ranted about this previously -- that I want to shove a kml file into maps.)
  • OpenVPN / RDP client
  • disable auto-rotation based on the accelerometer -- so I can read in bed
Ok, I'll grant that some of these things have come with third-party apps.  Some have come as jailbreak apps.  Some are just not there.

And I get that I'm hardly a typical user.  Most users looking for "IM client" on a phone would point me at text messaging.  (Yeah, but I can't text into an MSN IM contact.  And if a Skype contact IMs me back but I'm sending an email, I never get it.  Yeah, I know Trillian Astra is "almost there".  I've been waiting on them for as long too.)


Meanwhile, over the last 3 years since the iPhone changed the landscape, Android and most recently Windows Mobile 7 have come into view.  Neither is as polished as the iPhone was at launch.  (I've not seen a Win 7 phone, but I've seen a good deal of Android phones.)  Android only recently got pinch-to-zoom, and really only on Nexus One, and the Droid's map application.  But I see a lot more potential in these devices than in the iPhone.

For instance, me and my family were on vacation in California.  We were caravaning with friends on an unfamiliar stretch of highway.  We popped open the Droid, put in our destination in the voice guidance app, and went back to the home screen.  When something of interest popped up in a friends' cars, they'd text us or tweet.  As the perfect example, my wife was on the web looking up something, the navigation was telling me where to turn, and a text came in.  It all happened simultaneously.  It just worked.

I've played with the Droid pretty extensively since.  I can push the search button, and pull up a web page or a contact or directions.  The back button is just truly awesome -- getting me back a web page or back into the previous app or back a page in the app.   It just works.  The syncing is awesome because it just works.  I'm very good at forgetting day-to-day stuff, and so I often find I haven't sync'd my calendar in a while, and when I do, I double-booked myself.  There's no android sync procedure -- it just does it.

And don't get me started on the iPhone carrier lock-in.  I probably would've gotten a 3GS if my carrier of choice was announced last July.  As it stands, I'm the only member of my circle of mobiles that can't use free mobile-to-mobile minutes ... because I'm stuck in AT&T -- say nothing of the service.


When Android finally got pinch-to-zoom, it was a done deal.  I'm sold.  As soon as my AT&T contract is up, I'm sailing away on Android.  It'll let me use it the way I want.

Rob

VS's Cassini (Web Server) + Fiddler = Remote testing

From time to time, I have a need to debug a web site from a browser not installed on my current machine.  Safari/Mac or phone browsers are the quintessential example.  Granted, if it happens server-side, it happens for all browsers, and if it happens client-side, debugging server-side may not help.  But there's that one random time every now and again when this becomes quite important: debug server, client isn't on localhost.  Perhaps it's easier to replay the request from the browser where the problem happened rather than figure out how to simulate it locally.  Perhaps it's a weird combination of race conditions that make it happen.  Perhaps it's how the cookies built up that led to cruft not easily reproducible elsewhere.  Alas, being able to debug the server from a non-local client comes in mighty handy now-and-again.

As we all know, Visual Studio's built in web server (originally based on Cassini) is hard-coded to only bind to loopback.  If I had my way that wouldn't be the case, but here we are.  Traditionally, we'd need to install our site in IIS, attach to the w3svc service, and debug that way.  (Even if you're not on a 64-bit box, you can empathize with how intense that is.)

Fiddler is an awesome tool for watching network traffic flow between browser and web server.  In the end, everything you send between server and browser is either HTML, CSS, or JavaScript (or binary content like images and plug-ins), so watching it happen is incredibly insiteful.  We'll not use Fiddler for that here, but I highly recommend it for that work as well.  (There's times when watching the traffic yields insights quite difficult to see any other way.)  Alas, today we'll just mis-use it as a proxy server that can forward traffic back and forth from a LAN address to a loopback address.

Here's the steps:

1. Download and install Fiddler from www.fiddlertool.com on the same machine as Visual Studio.

2. Tools menu -> Fiddler Options -> Connections tab: click on "Allow remote computers to connect", and push ok.  (While I'm in here, I usually turn off IPv6 in the General tab as that's another no-no in VS's version of Cassini.)

3.  Rules menu -> Customize Rules.  For me, it opened up CustomRules.js in notepad.

4. Scroll down to the function labeled OnBeforeRequest and add a line kinda like this:

if (oSession.host.toLowerCase() == "192.168.1.5:8888") oSession.host = "localhost:45678";

Replace the first IP with the address of your machine (ipconfig /all from a cmd will give you this), and replace the port at the end with what ever you set / VS assigned to your project.  (Inside Visual Studio, in the project properties window, in the web tab, I always set it to "Specific Port" so I don't have to come in here and futz with it every time VS wants to "help".)

This line says "if the inbound request says it's going to this ip and port (that fiddler is listening to), reroute it to this other ip and port (that Visual Studio's web debugger is listening to).

5. Scroll down a bit further in CustomRules.js to the OnBeforeResponse function and add a line kinda like this:

oSession.utilReplaceInResponse("localhost:45678","192.168.1.5:8888");

You'll also need to change the IP and port here to match the changes above as well, but note they're reversed.

This line says "if the body of the outbound message includes text that routes to Visual Studio's debugger, rewrite it to point to Fiddler."

6. Save this file, restart Fiddler, and try it out.

(You may also have to adjust firewall rules on your development machine to allow inbound TCP connections on port 8888.)

And that's it!  Now you're remote debugging your project from a non-local address.

One final note: ideally, we'd create another rule in OnBeforeResponse that would rewrite headers as well.  I haven't been very successful in doing that, so I haven't included all the ways that don't work.  But if you're sending back a redirect to a full url without this rule, your client will happily time out looking for content on itself.  Why itself?  Because you redirected to localhost.  :D

Rob

Logging and Rewriting Service Calls

I love web services.  I love how easy it is to wire up a JavaScript client either through jQuery or through a ServiceProxy.  I love how I can point a .net client at a service, and I get strongly typed command parameters.  And my latest love of asmx services in C# is the SoapExtension.  A SoapExtension is a class that allows you to inspect outgoing or incoming services to get at the data involved.  I use them for logging service calls (frequency, duration, errors), and for rewriting output (fixed a "double html encoded" bug, remove whitespace, rewrite SoapFaults, etc).

The technique is incredibly awesome, and I can't claim original ownership, but I can claim excellent results.  A few simple steps:

1. Create a class that derives from SoapExtension.

2. Override ProcessMessage, handling the SoapMessage's Stage that you're interested in.

3. Add either a web.config entry (to handle all inbound and outbound service calls), or put an attribute on each WebMethod you want to track.

Voila!  You've got a soap extension.

Here's my ExampleSoapExtension (business logic replaced with TODO to keep it simple):

namespace ExampleNamespace {

    #region using
    using System;
    using System.Data.Linq;
    using System.IO;
    using System.Linq;
    using System.Text.RegularExpressions;
    using System.Threading;
    using System.Web;
    using System.Web.Services.Protocols;
    #endregion

    public class ExampleSoapExtension : SoapExtension {

        private Stream originalStream; // The original stream
        private Stream internalStream; // Our new stream we chained into place

        public ExampleSoapExtension() {
        }

        // Called the first time the Web service is used if configured with a config file
        public override object GetInitializer( Type t ) {
            return typeof( ExampleSoapExtension );
        }

        // Called the first time the Web service is used if configured with an attribute
        public override object GetInitializer( LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute ) {
            return attribute;
        }

        // Called each time the Web service is used and gets passed the data from GetInitializer() method
        public override void Initialize( object initializer ) {
            // Get the attribute here and pull whatever you need off it.
            //ExampleSoapExtensionAttribute attr = initializer as ExampleSoapExtensionAttribute;
        }

        // The ChainStream() method gives us a chance to grab the SOAP messages as they go by
        public override Stream ChainStream( Stream stream ) {
            // Save the original stream
            originalStream = stream;
            // Create and return our own in its place
            internalStream = new MemoryStream();

            return internalStream;
        }

        // The ProcessMessage() method is called between each step in the server-side process
        public override void ProcessMessage( SoapMessage message ) {

            switch ( message.Stage ) {

                case SoapMessageStage.BeforeDeserialize:
                    // About to handle incoming SOAP request

                    string requestContent = StreamToText( this.originalStream, false );
                    WriteIncomingToLog( message, requestContent );
                    TextToStream( requestContent, this.internalStream, true );
                    break;

                case SoapMessageStage.AfterDeserialize:
                    // incoming SOAP request has been deserialized

                    WriteInputToLog( message );
                    break;

                case SoapMessageStage.BeforeSerialize:
                    // About to prepare outgoing SOAP Response

                    WriteOutputToLog( message );
                    break;

                case SoapMessageStage.AfterSerialize:
                    // outgoing SOAP response is ready to go

                    string responseContent = null;

                    if ( message.Exception != null ) {
                        // Rewrite soap fault as valid soap message with internal error code
                        // Though it might be nice to do this before serialization, we can't inject our own return value easily
                        responseContent = RewriteError( message );
                    } else {
                        responseContent = StreamToText( this.internalStream, true );
                    }

                    WriteOutgoingToLog( message, responseContent );
                    TextToStream( responseContent, this.originalStream, false );
                    break;

                default:
                    throw new Exception( "invalid stage" );
            }
        }

        private static string StreamToText( Stream stream, bool rewindBefore ) {
            if ( rewindBefore ) {
                stream.Position = 0;
            }
            TextReader reader = new StreamReader( stream );
            string content = reader.ReadToEnd();
            return content;
        }

        private static void TextToStream( String text, Stream stream, bool rewindAfter ) {
            TextWriter writer = new StreamWriter( stream );
            writer.Write( text );
            writer.Flush();
            if ( rewindAfter ) {
                stream.Position = 0;
            }
        }

        #region WriteIncomingToLog
        /// <summary>
        /// Write the incoming stream content to the logs
        /// </summary>
        private void WriteIncomingToLog( SoapMessage Message, string Content ) {

            HttpRequest req = HttpContext.Current.Request;

            // TODO: Log various parameters here including the incoming xml and start date

        }
        #endregion

        #region WriteInputToLog
        private void WriteInputToLog( SoapMessage Message ) {

            ServiceInputObject input = null;
            if ( Message.MethodInfo.InParameters.Length > 0 ) {
                try {
                    input = Message.GetInParameterValue( 0 ) as ServiceInputObject;
                } catch ( Exception /*ex*/ ) {
                    input = null;
                }
            }
            if ( input != null ) {
           
                // TODO: Log the message details
               
            }

        }
        #endregion

        #region WriteOutputToLog
        private void WriteOutputToLog( SoapMessage Message ) {

            if ( Message.Exception != null ) {
                // The return object doesn't exist because an exception was thrown
                // RewriteError will create one
                return;
            }

            ServiceOutputObject output = null;
            if ( Message.MethodInfo != null && Message.MethodInfo.ReturnType != null && Message.MethodInfo.ReturnType.FullName != "System.Void" ) {
                output = Message.GetReturnValue() as ServiceOutputObject;
            } else {
                output = null;
            }
            if ( output != null ) {
           
                // TODO: Log the output details
               
            }

        }
        #endregion

        #region WriteOutgoingToLog
        /// <summary>
        /// Write the message in the stream to the log4net log
        /// </summary>
        private void WriteOutgoingToLog( SoapMessage Message, string Content ) {

            string outboundData = GetStreamText( StreamToLog );

            // TODO: Log the outgoing xml string and end date

        }
        #endregion

        #region CaptureError
        /// <summary>
        /// Insure the exception is logged
        /// </summary>
        private Stream CaptureError( SoapMessage Message, Stream stream ) {

            // Log the exception
            Exception ex = Message.Exception;
            while ( ex != null && ex.InnerException != null && ex.GetType() == typeof( SoapException ) ) {
                ex = ex.InnerException;
            }
           
            // TODO: Log the exception
           
            // TODO: Form the output reply and serialize it
            string soapMessage = null;

            // Now send this as the out stream instead of the <soap:Fault> stream we had before
            MemoryStream outStream = new MemoryStream();
            TextWriter writer = new StreamWriter( outStream );
            writer.WriteLine( soapMessage );
            writer.Flush();
            outStream.Position = 0;

            // And send the 200 status instead of 400
            //HttpContext.Current.Response.StatusCode = 200;

            // Here's the content to send instead of what ever you had
            return outStream;
        }
        #endregion

    }

    [AttributeUsage( AttributeTargets.Method )]
    public class ExampleSoapExtensionAttribute : SoapExtensionAttribute {

        public ExampleSoapExtensionAttribute() {
            this.Priority = 1;
        }

        public override int Priority { get; set; }

        // Specifies the class of the SOAP Extension to use with this method
        public override Type ExtensionType {
            get { return typeof( ExampleSoapExtension ); }
        }

    }

}

My current quest: how do I do this in WCF?  Is there such a thing as a "message watcher" or "handler" or "pipeline interceptor"?  All these terms on Google come up blank.  With WCF's new configuration-less services and MS's desire to move on past asmx, WCF seems a foregone conclusion.  For me, this is the last hurdle.

Rob

Desert Code Camp and Desert Code Camp Jr

In the past few years, I've been privileged to learn and network at Desert Code Camp.  When I jumped the fence to teach, it was an honor to learn from y'all as I shared the skills you'd taught me previously.  Many years ago, I began by teaching an intro to CSS and a team-taught SVN class.  Last code camp, I taught Thinking in JavaScript, Intro to jQuery, and Advanced jQuery including building plugins and animations.  I also got to learn about ASP.NET MVC, programming usb gadgets, and unit testing JavaScript -- to name a few.  It was a blast.

For anyone who's not taught the skills and talents you have, take the opportunity to do so.  It is well worth the effort and the skills you develop are invaluable.  Imagine how confident you'll be in the next job interview when you notice the same stage fright that quickly passes when teaching a room full of peers can also dissipate just as fast when you're getting grilled by a potential colleague.  And imagine the awesome resume tick and skill justification when you can document that not only do you have x years doing it, but that you taught it to a crowded room of your peers.

But I digress.  This year I'm taking a different path with Desert Code Camp.  My son often comes with me to Users' Group meetings -- mostly for the 1-on-1 time with me as we drive and the free pizza and stickers.  (At the conclusion of an evening 6 months or so ago, with his collection of treasured swag in hand, he said to me, "Dad, there's a lot of stickers in programming."  :D)  Each of the recruiters also engage him a bit each time, and I'm sure if he played his hand right, he could find an awesome job in 5 or 10 years.  Today, he does pretty well in DreamWeaver if I jump in with hard stuff and "how do you spell ...?" from time to time.  His site is robrich.org/mark.html.  He created the concept when he was 4 as a way to keep track of his favorite sites without the need to ask me where the letter H was on his keyboard every time.  For a 4-year-old, the problem's solution was brilliant!  Lately he's been asking to learn more about the guts of how it works -- hardware, software, marketing, and finance.  An eager mind like his is such a treasure.

Thus, this year, I'm not attending Desert Code Camp as a learning and teaching and networking opportunity.  This year, it will all be about my son.  Desert Code Camp Jr is a trek slated for kids "ages 4 to 18".  (In my opinion, kids over  13 or 15 or so will probably want to take -- or teach -- the other classes though.)  We'll learn about MIT's Scratch -- drag-drop programming for kids, programming Lego Mindstorms, and we'll learn about circuits.  Lunch is generously provided by GangPlank.  Mark explained to me he wants to give me some time to go to a grown-up class too, though I'll defer to him once the excitement kicks in.  The way they've structured it with a small instruction period up front and then a generous experimentation and play time afterward for each topic will be perfect for developing skill and creativity in these bright young minds.

Thus, if you were looking for the Continuous Integration class or the Unit Testing with TypeMock and Ivonna class I hyped, or an encore performance of the Advanced jQuery class, you'll have to wait until next year.  This year, I get to be Dad -- the greatest honor I could imagine.

Rob