Subscribe Now

ABC, 123, Ruby, C#, SAS, SQL, TDD, VB.NET, XYZ

Thursday, November 22, 2007

The Value of Cross-Training

It was back when Java 1.1 was a hot ticket item that I decided to tinker with this cool, new language. I learned the basics and made a number of fun applets. One silly one I recall involved a cat chasing a mouse around on a rectangular surface. The hardware mouse moved the software mouse and the cat head followed the mouse wherever it went automatically. At the time I was in a primarily Microsoft shop, so I never got to use my Java knowledge at work, but when C# came out, it was very easy to pick up for me due to my Java experience.

A while back I also learned a fair bit about writing Flash applications using Macromedia Flash MX. I wonder how much this is going to help when I dig into Silverlight one of these days.

More recently, I spent a weekend studying Ruby on Rails and then BAM! - a few months later Microsoft's ASP.NET MVC Framework becomes a hot topic. Due to my experience with RoR, understanding the architecture and coding of MS MVC has been very easy. I may never code a production RoR app, but by studying it, I've given myself a valuable education.

Cynics may grumble about "M$ copying others" (I'm won't touch that one!), but there have definitely been other instances of helpful cross-training not involving Microsoft products. An obvious example is all that IBM Mainframe JCL I got to write a decade or so ago...that prepared me quite well for…for…for, well, um, Soap Headers! (And punch cards prepared me for voting in Palm Beach county.) LOL. Ok, maybe that's a bit of a stretch, but I am certain that my work with Perl helped with understanding some Ruby syntax and of course when SAS started supporting regular expressions, the experience with Perl regex paid off. And my explorations into the Matisse object-oriented database definitely helped grokking NNHibernate easier. Procedural programming in QBASIC certainly prepped me for classic VB. And let's not forget BASIC with its fun and frequent GOTO statements and the not infrequent use of same in VB/VBA error handling. On Error Goto Err_Handler. Good times.

My point is that when the urge strikes you to explore that language you've been curious about, go for it. It might just turn out to be the next cool thing or cross-train you for it.

Changing gears a bit, this reminds of the cross-training encouraged by the Integral Life Practice starter kit. By exercising your body, mind, spirit, and shadow (unconscious) in different ways, each realm of human experience is enhanced. For example, the benefits of meditation (spirit) extend well beyond the spiritual realm, into the physical (a more relaxed state), mental (more clarity), and shadow (unconscious material is more accessible to productive processing).

Happy Thanksgiving! :)

Wednesday, November 21, 2007

Visual Studio.NET 2008 Arrives

No more beta blues! Visual Studio.NET 2008 has been released to manufacturing!

90-day trial editions are available right now at this location on MSDN.

Here is the express products web install page for Visual Basic 2008 Express Edition, Visual C# 2008 Express Edition, Visual C++ 2008 Express Edition, & Visual Web Developer 2008 Express Edition. Free to download immediately. (You'll want a fast connection.)

Here's a nice blog post about new features courtesy of Doug Stewart.

Sunday, November 18, 2007

Futzing with FUTS - Part II

In this second look at FUTS (Framework for Unit Testing SAS® programs), I will walk through an example of how to convert a regular piece of SAS code into a testable piece of SAS code with a unit test for testing it. The example consists of proc sql code to calculate age group based on age at an index date.

proc sql;
create table Calc as
select *, (case
when int((IndexDt-BDate)/365.25) between 0 and 17 then 'A: 0-17'
when int((IndexDt-BDate)/365.25) between 18 and 34 then 'B: 18-34'
when int((IndexDt-BDate)/365.25) between 35 and 49 then 'C: 35-49'
when int((IndexDt-BDate)/365.25) between 50 and 64 then 'D: 50-64'
when int((IndexDt-BDate)/365.25) > 64 then 'E: 65+'
else '?' end) as AgeGroup
from sample;
quit;
As I mentioned in Part I, the trick to making this code testable is to place it in a macro. Editing this code to fit in a macro results in something like this. The macro creates the calculated age group variable in an output dataset (outfile) given an input dataset (infile) containing a birthdate variable (bdatevar) and index date variable (indexdtvar).
%macro CalcAgeGroup(outfile,infile,bdatevar,indexdtvar);
proc sql;
create table &outfile as
select *, (case
when int((&indexdtvar-&bdatevar)/365.25) between 0 and 17 then 'A: 0-17'
when int((&indexdtvar-&bdatevar)/365.25) between 18 and 34 then 'B: 18-34'
when int((&indexdtvar-&bdatevar)/365.25) between 35 and 49 then 'C: 35-49'
when int((&indexdtvar-&bdatevar)/365.25) between 50 and 64 then 'D: 50-64'
when int((&indexdtvar-&bdatevar)/365.25) > 64 then 'E: 65+'
else '?' end) as AgeGroup
from &infile;
quit;
%mend CalcAgeGroup;

The production code that calls this macro to achieve the same goal as the first bit of code looks like this. A simple one liner.

%CalcAgeGroup(Calc,sample,BDate,IndexDt);

To unit test the macro, we need at least a minimal set of test data like this CSV. In this sample data file, you can see there's an ID var, BDATE, INDEXDT, and EXPECTED_AGEGROUP based on a calculation done by hand.

Test Data CSV

Given the CSV, the unit test code looks like this. (The macro is assume to be saved in a file called CalcAgeGroup.sas.)


*Import the CSV file into a SAS dataset called TestData;
proc import datafile='test_data.csv' out=TestData dbms=csv replace; getnames=yes; run;

data input; *Prep input dataset;
set TestData (drop=Expected_AgeGroup);
run;

data expected; *Prep expected output dataset;
length AgeGroup $8.; *Make AgeGroup the right size;
set TestData (rename=(Expected_AgeGroup=AgeGroup));
format AgeGroup; informat AgeGroup; *Drop format/informat;
run;

%include 'CalcAgeGroup.sas';
%CalcAgeGroup(Calc,input,bdate,indexdt); *Run test data through macro;

%assert_compare_equal(base=expected,compare=Calc); *Compare macro output & expected output;

Here we see the use of the FUTS macro %assert_compare_equal which compares SAS two datasets (attributes and data values) and throws an error into the log if the base and compare files are at all different. The rest of the code is fairly self-explanatory and involves preparing an input dataset for the macro and an expected output file to compare with the actual output file.

Friday, November 16, 2007

The Other Mother of Invention: Impatience

On 11/8, I blogged about Unit Testing an Email Method. Things have happened since then...



Bad Sleep

You may have heard the saying about necessity or laziness being the mother of invention, but I say impatience is a good mother, too. I say this based on the fact that I quickly got sick of waiting for my email unit tests to run when I was running the whole batch of unit tests in NUnit. Pretty soon I had them [Ignore]'d out. I was distressed about the yellow instead of the green, but my impatience was stronger. One property of a good unit test is that it runs rapidly.



Eventually (doh!) it dawned on me that I could both have and eat my cake. How? I modified my email unit test to query my Outlook Inbox every two seconds to see if the target email had arrived and exit early if so (with success). Otherwise, it would sleep for two more seconds and check the inbox again -- up to a maximum of eight repetitions (i.e., 16 seconds).



This refactoring made me quite the happy camper. Imagine my joy as I ran all of my unit tests quickly without the manditory 10-second waits for the tests involving email. I, like many NUnit users I am sure, just love to see that green bar -- and as soon as possible!



I took all of the Outlook-related goo and put it in a separate class called OutlookInbox with a single public (static) method called EmailReceived. I just pass in the subject line of the email I am expecting to receive and the method works its magic and I get back a boolean value telling me if the email arrived in time or not. The responsibility for doing the 2-second chunks of waiting is delegated to this method and the work of actually interfacing with Outlook is delegated to a helper method called checkInbox.



using System;
using System.Runtime.InteropServices;
using Outlook = Microsoft.Office.Interop.Outlook;

namespace TestHarness
{
public class OutlookInbox
{
private OutlookInbox() { }

public static bool EmailReceived(string subjectline)
{
const int MAXTRIES = 8;
int tries = 0;
bool emailReceived = false;
do
{
tries++;
System.Threading.Thread.Sleep(2000);
emailReceived = checkInbox(subjectline);
} while (emailReceived == false && tries < MAXTRIES);
return emailReceived;
}

private static bool checkInbox(string subjectline)
{
//This webpage (http://www.developerfusion.co.uk/show/4667/) was helpful in
//developing this code.
Outlook.ApplicationClass outlookApp = null;
Outlook.NameSpace outlookNS = null;
bool emailArrived = false;
try
{
outlookApp = new Outlook.ApplicationClass();
outlookNS = outlookApp.GetNamespace("MAPI");
outlookNS.Session.Logon("outlook", "", false, true);
Outlook.MAPIFolder inbox = outlookNS.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox);
foreach (Outlook.MailItem email in inbox.Items)
{
if (email.Subject == subjectline)
{
emailArrived = true;
email.Delete();
}
}
}
finally
{
if (outlookNS != null)
outlookNS.Logoff();
}
return emailArrived;
}
}
}


Now my unit test code looks like this. This is testing a web service that sends email. The business about ServiceConnections.WebServiceBaseUrl() manages whether we are pointing at the dev or prod service.



using System;
using NUnit.Framework;
using IndependentContractorApp.BusinessLayer;

namespace TestHarness
{
[TestFixture]
public class EmailServiceTestSuite
{
[Test]
public void SendRegularText()
{
string asmx = "EmailService.asmx";
string url = ServiceConnections.WebServiceBaseUrl() + asmx;
EmailWebService.EmailService svc = new EmailWebService.EmailService();
svc.Url = url;
svc.Credentials = System.Net.CredentialCache.DefaultCredentials;
DateTime now = DateTime.Now;
svc.SendRegularText("me@mycompany.org", "SendRegularText Test Email - " + now, "Body Text");
Assert.IsTrue(OutlookInbox.EmailReceived("SendRegularText Test Email - " + now),
"Email failed to arrive");
}
}
}


NUnit green bar of success

Ahh, a thing of beauty. :)


Sunday, November 11, 2007

Futzing with FUTS - Part I

Unit testing is a well accepted practice in the software development community. There are many tools and articles devoted it. Google 'unit testing' if you have any doubts.



What about those of us working in the SAS realm? Given that SAS is basically a data-oriented scripting language, is it feasible to even think of unit test SAS code? I would say, "of course it is, it's code afterall!" If there is code, we can test it. There's CUnit for heaven's sake! I've even had the pleasure of using it (and then opted to roll my own C unit tests). :)



A typical SAS program moves data around or analyzes it and consists of data steps (cursor style data manipulation) and/or procedures ("procs"). Also thrown in are miscellaneous statements to make it all work how it should (e.g., libname statements). Here is SAS program that creates a text file with 100 random numbers between 1 and 10.



data _NULL_;
file "c:\my_folder\random.txt";
do i = 1 to 100;
r = 1 + int(10*ranuni(-1));
put r;
end;
run;


Naturally, SAS is capable of WAY more powerful things, but we must start simple.



What if I want to test my random number generator to make sure that it always and only generates numbers between 1 and 10? How would I do such a thing in SAS? Let's change gears for a minute and consider what we would do in C#. In C# we would have solution containing three projects: a class library called RandomNumberGenerator, a console application called RandomClient, and a class library called TestHarness.



RandomNumberGenerator



using System;

namespace UtilityLib
{
public class RandomNumberGenerator
{
private static Random generator = new Random();

public static int Ranuni()
{
return generator.Next(1, 11);
}
}
}


RandomClient



using System;
using System.IO;
using UtilityLib;

namespace RandomClient
{
class Program
{
static void Main(string[] args)
{
using (StreamWriter writer = new StreamWriter(@"c:\my_folder\random.txt"))
{
for(int i=0; i<100; ++i)
writer.WriteLine(RandomNumberGenerator.Ranuni());
writer.Flush();
writer.Close();
}
}
}
}


TestHarness



using System;
using UtilityLib;
using NUnit.Framework;

namespace TestHarness
{
[TestFixture]
public class RandomNumberGeneratorTestSuite
{
[Test]
public void Ranuni_TestBounds()
{
for (int i = 0; i < 100; ++i)
{
int r = RandomNumberGenerator.Ranuni();
Assert.IsTrue(r >= 1 && r <= 10);
}
}

[Test]
public void Ranuni_TestFullRange()
{
int[] counts = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
for (int i = 0; i < 100; ++i)
counts[RandomNumberGenerator.Ranuni() - 1] += 1;
for (int i = 0; i < 10; ++i)
Assert.IsTrue(counts[i] >= 1);
}
}
}


NUnit Success

Now let's get back to SAS. What can we similarly do to test random number generation in SAS? The trick to unit testing in SAS is to place the code you want to test (your black box, if you will), into a macro. So the random number generation code becomes a SAS macro like this.



RandomNumberGenerator



%macro RandomNumberGenerator;
1 + int(10*ranuni(-1))
%mend;


Now my client code looks like this. It just calls the RandomNumberGenerator macro 100 times to create the output file.



RandomClient



data _NULL_;
file "c:\my_folder\random.txt";
do i = 1 to 100;
r = %RandomNumberGenerator;
put r;
end;
run;


Now what about a test harness for RandomNumberGenerator? It is finally time for FUTS (Framework for Unit Testing SAS® programs) to make its appearance. FUTS, a free product from Thotwave, is a wonderful set of easy to use assert SAS macros that test for various conditions - similar to the set of NUnit asserts. Unlike NUnit, FUTS doesn't have a slick GUI, and instead FUTS throws errors into the SAS log when an assert fails and writes nothing to the log in case of success. To run your tests, run the test harness code and check the SAS log for errors.



To test the RandomNumberGenerator macro, I first create a temporary dataset called test1 that contains 100 random numbers. To do a lower and upper bounds check (i.e., all random numbers are between 1 and 10), I select the max(r) and min(r) into macro variables and use the FUTS macro %assert_sym_compare to test (a) minr is greater than or equal to (GE) 1 and (b) maxr is less than or equal to (LE) 10. This is equivalent to Assert.IsTrue(r >= 1 && r <= 10); in the C# test Ranuni_TestBounds() above. The second test, making sure that each number from 1 to 10 is generated at least once, is accomplished by first performing a proc freq (count how many times each value appears), then getting the count for each number (1, 2, ..., 10) into a macro variable and testing, using %assert_sym_compare again, that each count is GE 1.



TestHarness



data test1;
do i = 1 to 100;
r = %RandomNumberGenerator;
output;
end; drop i;
run;

proc sql noprint;
select min(r), max(r) into :minr, :maxr from test1;
quit;
%assert_sym_compare(&minr, 1, type=COMPARISON, operator=GE);
%assert_sym_compare(&maxr, 10, type=COMPARISON, operator=LE);

proc freq data=test1;
tables r / noprint out=freqout;
run;
proc sql noprint;
select count into :count1 from freqout where r=1;
select count into :count2 from freqout where r=2;
select count into :count3 from freqout where r=3;
select count into :count4 from freqout where r=4;
select count into :count5 from freqout where r=5;
select count into :count6 from freqout where r=6;
select count into :count7 from freqout where r=7;
select count into :count8 from freqout where r=8;
select count into :count9 from freqout where r=9;
select count into :count10 from freqout where r=10;
quit;
%assert_sym_compare(&count1, 1, type=COMPARISON, operator=GE);
%assert_sym_compare(&count2, 1, type=COMPARISON, operator=GE);
%assert_sym_compare(&count3, 1, type=COMPARISON, operator=GE);
%assert_sym_compare(&count4, 1, type=COMPARISON, operator=GE);
%assert_sym_compare(&count5, 1, type=COMPARISON, operator=GE);
%assert_sym_compare(&count6, 1, type=COMPARISON, operator=GE);
%assert_sym_compare(&count7, 1, type=COMPARISON, operator=GE);
%assert_sym_compare(&count8, 1, type=COMPARISON, operator=GE);
%assert_sym_compare(&count9, 1, type=COMPARISON, operator=GE);
%assert_sym_compare(&count10, 1, type=COMPARISON, operator=GE);


This last bit of code is repetitive and would ideally be "macro-ized".

Friday, November 9, 2007

Fun with XMethods.com

It's fun to occasionally take a stroll through xmethods.com see what kinds of web services folks are creating.



Here are some of the ones I found interesting in my look today.



Convert Text to Braille



Here is a neat and easy to use service for converting text to Braille. The BrailleText conversion method takes two parameters, the text you want to convert (string) and the font size for the output (float). It returns a byte array that is easily converted into a JPG and displayed on a WinForm.



private void button1_Click(object sender, EventArgs e)
{
net.webservicex.www.Braille svc = new TextToBraille1.net.webservicex.www.Braille();
byte[] response = svc.BrailleText(this.textBox1.Text, 20);
using (FileStream fs = new FileStream("braille.jpg", FileMode.Create, FileAccess.Write))
{
foreach(byte b in response)
{
fs.WriteByte(b);
}
}
pictureBox1.Load("braille.jpg");
}


Text to Braille winform

How to convert the image to actual Braille that the blind can read...hmmm...looks like there are many possibilities there.




Dates of U.S. Holidays



This web service offers methods for discovering the dates of U.S. holidays for the year of your choice. For example, the GetThanksgivingDay method requires that you pass it a year value (integer) and it returns a DateTime value. I wired this service up to a simple ASP.NET page containing a calendar control and show this year's and next year's holidays, disabling selection of those dates as well.



Imports System.Collections.Generic

Public Class Holiday
Public Sub New(ByVal d As DateTime, ByVal nm As String)
Me.Date = d
Me.Name = nm
End Sub
Public [Date] As DateTime
Public Name As String
End Class

Partial Class _Default
Inherits System.Web.UI.Page

Private holidays As New List(Of Holiday)

Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
Dim svc As New com.holidaywebservice.www.USHolidayDates()
For yr As Integer = DateTime.Now.Year To DateTime.Now.Year + 1
holidays.Add(New Holiday(svc.GetChristmasDay(yr), "Christmas"))
holidays.Add(New Holiday(svc.GetIndependenceDay(yr), "Independence Day"))
holidays.Add(New Holiday(svc.GetLaborDay(yr), "Labor Day"))
holidays.Add(New Holiday(svc.GetMartinLutherKingDay(yr), "MLK Day"))
holidays.Add(New Holiday(svc.GetMemorialDay(yr), "Memorial Day"))
holidays.Add(New Holiday(svc.GetNewYear(yr), "New Year's Day"))
holidays.Add(New Holiday(svc.GetPresidentsDay(yr), "President's Day"))
holidays.Add(New Holiday(svc.GetThanksgivingDay(yr), "Thanksgiving"))
Next
End Sub

Protected Sub Calendar1_DayRender(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.DayRenderEventArgs) Handles Calendar1.DayRender
For Each h As Holiday In holidays
If e.Day.Date.Year = h.Date.Year AndAlso e.Day.Date.DayOfYear = h.Date.DayOfYear Then
e.Cell.Text = h.Name
e.Cell.Enabled = False
End If
Next
End Sub
End Class


U.S. Holidays on a Calendar

While the intent of this web service is quite nice, sadly it provides incorrect results as you can see in the image above (Christmas on the 24th? New Year's Day on Dec 31st?). This just illustrates that there are dangers in using black box web services that live "out there in the wild."



Daily Dilbert Cartoon



And last, but not least, this delightful web service serves up the URL of the

Dilbert Cartoon of the day (or the bytes of the JPG). I used the GetDailyDilbertImagePath method (no arguments) to return the URL (string) of the cartoon JPG location and plug that into a picturebox on a WinForm. Very easy.



Public Class Form1
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
Dim svc As New com.esynaps.www.DailyDilbert()
Dim url As String = svc.DailyDilbertImagePath()
Me.PictureBox1.Load(url)
Me.Text = url
End Sub
End Class


WinForm displaying Dilbert cartoon of the day

Thursday, November 8, 2007

Unit Testing an Email Method

Inbox

I recently ran into the problem of needing to unit test a web method in an app that sends email. Searching the web, I finally found this which helped me develop my test. Many thanks to Simon Soanes for his post. Here’s the C# 2.0 test harness code.



using System;
using NUnit.Framework;
using MyApp.WebLayer;
using System.Runtime.InteropServices;
using Outlook = Microsoft.Office.Interop.Outlook; //Refs Microsoft Outlook 11.0 Object Library

namespace TestHarness
{
[TestFixture]
public class EmailServiceTestSuite
{
[Test]
public void SendRegularText()
{
EmailWebService.EmailService svc = new EmailWebService.EmailService();
DateTime now = DateTime.Now;
svc.SendRegularText("me@myorg.org", "SendRegularText Test Email - " + now, "");

System.Threading.Thread.Sleep(10000); //Wait 10 seconds for email to arrive...sometimes not enough

Outlook.ApplicationClass outlookApp = null;
Outlook.NameSpace outlookNS = null;
bool emailArrived = false;
try
{
outlookApp = new Outlook.ApplicationClass();
outlookNS = outlookApp.GetNamespace("MAPI");
outlookNS.Session.Logon("outlook", "", false, true);
Outlook.MAPIFolder inbox = outlookNS.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox);
foreach (Outlook.MailItem email in inbox.Items)
{
if (email.Subject == "SendRegularText Test Email - " + now)
{
emailArrived = true;
email.Delete();
}
}
}
finally
{
if (outlookNS != null)
outlookNS.Logoff();
}
Assert.IsTrue(emailArrived, "Email failed to arrive");
}
}
}


For testing purposes, I just send a test email to myself with a guaranteed unique subject line, wait for 10 seconds which is usually enough time for the email to show up in my Outlook Inbox, then assert that the email is present. The SendRegularText web method accepts three parameters: email recipient, subject line, and body text (left blank in the test).

Wednesday, November 7, 2007

VB.NET / C# Language Conversion Links

I find it's really helpful to have at least a basic understanding of VB.NET and C#.



C#


using System;

public class MyClass
{
static void Main()
{
Console.WriteLine("Hi, from C#!");
}
}


VB.NET



Imports System

Public Class MyClass
Shared Sub Main()
Console.WriteLine("Howdy, from VB.NET!")
End Sub
End Class


Here are links to some miscellaneous online resources.



Tuesday, November 6, 2007

Test Driving TallPDF.NET 3.0

I am a big fan of PDF documents. The only thing about them that is a drag is creating them. I usually create them from a source document of some kind using CutePDF Writer. As the website says, "FREE for personal and commercial use! No watermarks! No Popup Web Ads!" What could be better!? It installs as a printer driver, so you can create a PDF out of just about any kind of file.



For example, I can take this carefully drawn self-portrait in mspaint.exe and make a PDF using CutePDF Writer in no time flat.



self portrait in mspaint

Hit the File | Print... menu...



CutePDF Writer

Select CutePDF Writer, press Print, et voila! It's a PDF...



PDF of self portrait

What could be better than that? Well, I'll tell you...programmatically creating PDFs!



This is where TallPDF.NET 3.0 comes in.



After downloading this .NET component (I really just need the DLL, although the documentation that comes with it is nice, too), I just reference it in any old .NET application. I will use a C# console application.



Here's all I need in the way of code and references.



Visual Studio C# console program to programmatically create a PDF

I actually don't need System.Data or System.Xml for this example, but System.Drawing, System.Web, and of course TallComponents.PDF.Layout are all required. As you can see from the using statements, TallPDF is organized into multiple namespaces. Here I only reference the three required by this example. The object model is pretty self-explanatory: we have a document object, with one or more section objects, each containing paragraph objects which may be text or shapes or images. At the end of the code, we're just pumping PDF bits through a file stream using the Document.Write method.



The PDF produced looks like this.



self portrait PDF

Here's a slightly more complex example.



using System;
using System.Drawing;
using System.IO;
using TallComponents.PDF.Layout;
using F = TallComponents.PDF.Layout.Fonts;
using TallComponents.PDF.Layout.Paragraphs;
using TallComponents.PDF.Layout.Navigation;
using TallComponents.PDF.Layout.Shapes;
using B = TallComponents.PDF.Layout.Brushes;
using P = TallComponents.PDF.Layout.Pens;
using FLD = TallComponents.PDF.Layout.Shapes.Fields;

namespace TestTallPdf2
{
class Program
{
static void Main(string[] args)
{
string lorem1 = "Lorem ipsum dolor sit amet, consectetuer adipiscing elit...";
string lorem2 = "Cras suscipit. Aliquam hendrerit. Vivamus aliquam. Vestibulum...";
Document doc = new Document();
Section sec = new Section();
doc.Sections.Add(sec);

Header hdr = new Header();
TextParagraph phdr = new TextParagraph();
hdr.Paragraphs.Add(phdr);
phdr.Fragments.Add(new Fragment("My Lorem Ipsum Document", F.Font.HelveticaBoldOblique, 12));
hdr.TopMargin = new Unit(0.5, UnitType.Inch);
sec.Header = hdr;

Heading hd1 = new Heading(0);
hd1.SpacingBefore = 10;
hd1.SpacingAfter = 5;
hd1.Fragments.Add(new Fragment("Introduction", F.Font.Helvetica, 11));
sec.Paragraphs.Add(hd1);

TextParagraph psec = new TextParagraph();
psec.Fragments.Add(new Fragment(lorem1, F.Font.TimesRoman, 10));
psec.SpacingAfter = 10;
psec.LineSpacing = 3;
sec.Paragraphs.Add(psec);

Heading hd2 = new Heading(0);
hd2.SpacingBefore = 10;
hd2.SpacingAfter = 5;
hd2.Fragments.Add(new Fragment("About the Author", F.Font.Helvetica, 11));
sec.Paragraphs.Add(hd2);

TextParagraph psec2 = new TextParagraph();
psec2.Justified = true;
psec2.Fragments.Add(new Fragment(lorem2, F.Font.TimesRoman, 10));
psec2.LineSpacing = 3;
sec.Paragraphs.Add(psec2);

Heading hd3 = new Heading(0);
hd3.SpacingBefore = 10;
hd3.SpacingAfter = 5;
hd3.Fragments.Add(new Fragment("1/4 of a Pie", F.Font.Helvetica, 11));
sec.Paragraphs.Add(hd3);

Drawing drawing = new Drawing(60, 60);
PieShape pie = new PieShape();
pie.Start = 0;
pie.Sweep = 90;
pie.Pen = new P.Pen(System.Drawing.Color.Red, 2);
pie.Brush = new B.SolidBrush(System.Drawing.Color.Blue);
drawing.Shapes.Add(pie);
sec.Paragraphs.Add(drawing);

Heading hd4 = new Heading(0);
hd4.SpacingBefore = 10;
hd4.SpacingAfter = 5;
hd4.Fragments.Add(new Fragment("Lorem Ipsum Bracelet", F.Font.Helvetica, 11));
sec.Paragraphs.Add(hd4);

Drawing drawing2 = new Drawing(180, 180);
ImageShape img = new ImageShape(@"C:\my_folder\bracelet.jpg");
drawing2.Shapes.Add(img);
sec.Paragraphs.Add(drawing2);

Heading hd5 = new Heading(0);
hd5.SpacingBefore = 10;
hd5.SpacingAfter = 5;
hd5.Fragments.Add(new Fragment("Enter your name", F.Font.Helvetica, 11));
sec.Paragraphs.Add(hd5);

Drawing drawing3 = new Drawing(200, 30);
FLD.TextFieldShape field = new FLD.TextFieldShape(200, 30);
field.FullName = "txtName";
drawing3.Shapes.Add(field);
sec.Paragraphs.Add(drawing3);

ViewerPreferences vp = new ViewerPreferences();
vp.ZoomFactor = 0.75; //75%
doc.ViewerPreferences = vp;
using (FileStream fs = new FileStream(@"C:\my_folder\lorem.pdf",
FileMode.Create, FileAccess.Write))
{
doc.Write(fs);
}
}
}
}


This produces a PDF looking like this.



Lorem Ipsum PDF

Pretty nice, eh!



It works in ASP.NET, too.



using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using TallComponents.PDF.Layout;
using TallComponents.PDF.Layout.Paragraphs;

public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
Document doc = new Document();
Section sec = new Section();
doc.Sections.Add(sec);
TextParagraph par = new TextParagraph();
par.Fragments.Add(new Fragment("Hello from ASP.NET!"));
sec.Paragraphs.Add(par);
doc.Write(Response);
}
}


ASP.NET example

Saturday, November 3, 2007

SubSonic for Database Versioning

If you're a regular SubSonic user, then you are probably well aware of its database versioning feature. Even if you're into other CRUD glue like data adapters, LINQ, or NHibernate, I think you will find this feature
a reason to check out SubSonic. This is not about source control (there are plenty of other tools for that). This is about taking a snapshot of your database objects (including the data in the tables) at a point in time and being able to recreate an exact copy of that snapshot.



With SubSonic installed, open Visual Studio and go into the Tools | External Tools... menu and configure a new tool as shown. In this case, I'm calling the new tool SubSonic DB Versioner.



SubSonic tool setup in Visual Studio

Click ok, then add an app.config/web.config to your project. I'll explain this using the app.config of a Winforms app.



To set up SubSonic for database versioning, here is the minimum you will need to specify in the config file. This is boilerplate code you can easily reuse by just changing the provider/connection string.



SubSonic app.config settings

I recommend creating a new folder in your project called something like DatabaseSnapshots. You are now ready to make a database snapshot using the versioning tool. All you have to do is go back into the Tools menu and select SubSonic DB Versioner. A window will pop up asking you if you want to accept the default command line arguments (because we checked Prompt for arguments). For the purposes of this demonstration, we need to change the Arguments from "version /out App_Code\DB" to "version /out DatabaseSnapshots". Then let 'er rip, keeping an eye on the Output window to see the progress messages.



When it finishes, you will end up with two new .sql scripts in your DatabaseSnapshots folder (e.g., SqlDataProvider_Schema_2007_10_30.sql and SqlDataProvider_Data_2007_10_30.sql). The first script, the one with Schema in the name, contains code to recreate all tables, views, stored procedures, etc.



Here's just a tiny portion of the schema script.



/****** Object:  Table [dbo].[CustomerDemographics]    Script Date: 10/30/2007 13:52:13 ******/
SET ANSI_NULLS ON
SET QUOTED_IDENTIFIER ON
IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[CustomerDemographics]') AND OBJECTPROPERTY(id, N'IsUserTable') = 1)
BEGIN
CREATE TABLE [dbo].[CustomerDemographics](
[CustomerTypeID] [nchar](10) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
[CustomerDesc] [ntext] COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
CONSTRAINT [PK_CustomerDemographics] PRIMARY KEY NONCLUSTERED
(
[CustomerTypeID] ASC
) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
END
/****** Object: Table [dbo].[Region] Script Date: 10/30/2007 13:52:14 ******/
SET ANSI_NULLS ON
SET QUOTED_IDENTIFIER ON
IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[Region]') AND OBJECTPROPERTY(id, N'IsUserTable') = 1)
BEGIN
CREATE TABLE [dbo].[Region](
[RegionID] [int] NOT NULL,
[RegionDescription] [nchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
CONSTRAINT [PK_Region] PRIMARY KEY NONCLUSTERED
(
[RegionID] ASC
) ON [PRIMARY]
) ON [PRIMARY]
END
/****** Object: Table [dbo].[Employees] Script Date: 10/30/2007 13:52:14 ******/
SET ANSI_NULLS ON
SET QUOTED_IDENTIFIER ON
IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[Employees]') AND OBJECTPROPERTY(id, N'IsUserTable') = 1)
BEGIN
CREATE TABLE [dbo].[Employees](
[EmployeeID] [int] IDENTITY(1,1) NOT NULL,
[LastName] [nvarchar](20) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
[FirstName] [nvarchar](10) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
[Title] [nvarchar](30) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[TitleOfCourtesy] [nvarchar](25) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[BirthDate] [datetime] NULL,
[HireDate] [datetime] NULL,
[Address] [nvarchar](60) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[City] [nvarchar](15) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[Region] [nvarchar](15) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[PostalCode] [nvarchar](10) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[Country] [nvarchar](15) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[HomePhone] [nvarchar](24) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[Extension] [nvarchar](4) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[Photo] [image] NULL,
[Notes] [ntext] COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[ReportsTo] [int] NULL,
[PhotoPath] [nvarchar](255) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
CONSTRAINT [PK_Employees] PRIMARY KEY CLUSTERED
(
[EmployeeID] ASC
) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
END


That's pretty neat, but the second script is the coolest. It contains code to populate the database tables with your data. Here's a snippet of the data script.



ALTER TABLE [Region] NOCHECK CONSTRAINT ALL
GO

PRINT 'Begin inserting data in Region'
INSERT INTO [Region] ([RegionID], [RegionDescription])
VALUES(1, 'Eastern ')
INSERT INTO [Region] ([RegionID], [RegionDescription])
VALUES(2, 'Western ')
INSERT INTO [Region] ([RegionID], [RegionDescription])
VALUES(3, 'Northern ')
INSERT INTO [Region] ([RegionID], [RegionDescription])
VALUES(4, 'Southern ')
ALTER TABLE [Region] CHECK CONSTRAINT ALL
GO



ALTER TABLE [Shippers] NOCHECK CONSTRAINT ALL
GO

SET IDENTITY_INSERT [Shippers] ON
PRINT 'Begin inserting data in Shippers'
INSERT INTO [Shippers] ([ShipperID], [CompanyName], [Phone])
VALUES(1, 'Speedy Express', '(503) 555-9831')
INSERT INTO [Shippers] ([ShipperID], [CompanyName], [Phone])
VALUES(2, 'United Package', '(503) 555-3199')
INSERT INTO [Shippers] ([ShipperID], [CompanyName], [Phone])
VALUES(3, 'Federal Shipping', '(503) 555-9931')
SET IDENTITY_INSERT [Shippers] OFF
ALTER TABLE [Shippers] CHECK CONSTRAINT ALL
GO



ALTER TABLE [Suppliers] NOCHECK CONSTRAINT ALL
GO

SET IDENTITY_INSERT [Suppliers] ON
PRINT 'Begin inserting data in Suppliers'
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(1, 'Exotic Liquids', 'Charlotte Cooper', 'Purchasing Manager', '49 Gilbert St.', 'London', NULL, 'EC1 4SD', 'UK', '(171) 555-2222', NULL, NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(2, 'New Orleans Cajun Delights', 'Shelley Burke', 'Order Administrator', 'P.O. Box 78934', 'New Orleans', 'LA', '70117', 'USA', '(100) 555-4822', NULL, '#CAJUN.HTM#')
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(3, 'Grandma Kelly''s Homestead', 'Regina Murphy', 'Sales Representative', '707 Oxford Rd.', 'Ann Arbor', 'MI', '48104', 'USA', '(313) 555-5735', '(313) 555-3349', NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(4, 'Tokyo Traders', 'Yoshi Nagase', 'Marketing Manager', '9-8 Sekimai Musashino-shi', 'Tokyo', NULL, '100', 'Japan', '(03) 3555-5011', NULL, NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(5, 'Cooperativa de Quesos ''Las Cabras''', 'Antonio del Valle Saavedra', 'Export Administrator', 'Calle del Rosal 4', 'Oviedo', 'Asturias', '33007', 'Spain', '(98) 598 76 54', NULL, NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(6, 'Mayumi''s', 'Mayumi Ohno', 'Marketing Representative', '92 Setsuko Chuo-ku', 'Osaka', NULL, '545', 'Japan', '(06) 431-7877', NULL, 'Mayumi''s (on the World Wide Web)#http://www.microsoft.com/accessdev/sampleapps/mayumi.htm#')
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(7, 'Pavlova, Ltd.', 'Ian Devling', 'Marketing Manager', '74 Rose St. Moonie Ponds', 'Melbourne', 'Victoria', '3058', 'Australia', '(03) 444-2343', '(03) 444-6588', NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(8, 'Specialty Biscuits, Ltd.', 'Peter Wilson', 'Sales Representative', '29 King''s Way', 'Manchester', NULL, 'M14 GSD', 'UK', '(161) 555-4448', NULL, NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(9, 'PB Knäckebröd AB', 'Lars Peterson', 'Sales Agent', 'Kaloadagatan 13', 'Göteborg', NULL, 'S-345 67', 'Sweden', '031-987 65 43', '031-987 65 91', NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(10, 'Refrescos Americanas LTDA', 'Carlos Diaz', 'Marketing Manager', 'Av. das Americanas 12.890', 'Sao Paulo', NULL, '5442', 'Brazil', '(11) 555 4640', NULL, NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(11, 'Heli Süßwaren GmbH & Co. KG', 'Petra Winkler', 'Sales Manager', 'Tiergartenstraße 5', 'Berlin', NULL, '10785', 'Germany', '(010) 9984510', NULL, NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(12, 'Plutzer Lebensmittelgroßmärkte AG', 'Martin Bein', 'International Marketing Mgr.', 'Bogenallee 51', 'Frankfurt', NULL, '60439', 'Germany', '(069) 992755', NULL, 'Plutzer (on the World Wide Web)#http://www.microsoft.com/accessdev/sampleapps/plutzer.htm#')
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(13, 'Nord-Ost-Fisch Handelsgesellschaft mbH', 'Sven Petersen', 'Coordinator Foreign Markets', 'Frahmredder 112a', 'Cuxhaven', NULL, '27478', 'Germany', '(04721) 8713', '(04721) 8714', NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(14, 'Formaggi Fortini s.r.l.', 'Elio Rossi', 'Sales Representative', 'Viale Dante, 75', 'Ravenna', NULL, '48100', 'Italy', '(0544) 60323', '(0544) 60603', '#FORMAGGI.HTM#')
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(15, 'Norske Meierier', 'Beate Vileid', 'Marketing Manager', 'Hatlevegen 5', 'Sandvika', NULL, '1320', 'Norway', '(0)2-953010', NULL, NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(16, 'Bigfoot Breweries', 'Cheryl Saylor', 'Regional Account Rep.', '3400 - 8th Avenue Suite 210', 'Bend', 'OR', '97101', 'USA', '(503) 555-9931', NULL, NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(17, 'Svensk Sjöföda AB', 'Michael Björn', 'Sales Representative', 'Brovallavägen 231', 'Stockholm', NULL, 'S-123 45', 'Sweden', '08-123 45 67', NULL, NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(18, 'Aux joyeux ecclésiastiques', 'Guylène Nodier', 'Sales Manager', '203, Rue des Francs-Bourgeois', 'Paris', NULL, '75004', 'France', '(1) 03.83.00.68', '(1) 03.83.00.62', NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(19, 'New England Seafood Cannery', 'Robb Merchant', 'Wholesale Account Agent', 'Order Processing Dept. 2100 Paul Revere Blvd.', 'Boston', 'MA', '02134', 'USA', '(617) 555-3267', '(617) 555-3389', NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(20, 'Leka Trading', 'Chandra Leka', 'Owner', '471 Serangoon Loop, Suite #402', 'Singapore', NULL, '0512', 'Singapore', '555-8787', NULL, NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(21, 'Lyngbysild', 'Niels Petersen', 'Sales Manager', 'Lyngbysild Fiskebakken 10', 'Lyngby', NULL, '2800', 'Denmark', '43844108', '43844115', NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(22, 'Zaanse Snoepfabriek', 'Dirk Luchte', 'Accounting Manager', 'Verkoop Rijnweg 22', 'Zaandam', NULL, '9999 ZZ', 'Netherlands', '(12345) 1212', '(12345) 1210', NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(23, 'Karkki Oy', 'Anne Heikkonen', 'Product Manager', 'Valtakatu 12', 'Lappeenranta', NULL, '53120', 'Finland', '(953) 10956', NULL, NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(24, 'G''day, Mate', 'Wendy Mackenzie', 'Sales Representative', '170 Prince Edward Parade Hunter''s Hill', 'Sydney', 'NSW', '2042', 'Australia', '(02) 555-5914', '(02) 555-4873', 'G''day Mate (on the World Wide Web)#http://www.microsoft.com/accessdev/sampleapps/gdaymate.htm#')
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(25, 'Ma Maison', 'Jean-Guy Lauzon', 'Marketing Manager', '2960 Rue St. Laurent', 'Montréal', 'Québec', 'H1J 1C3', 'Canada', '(514) 555-9022', NULL, NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(26, 'Pasta Buttini s.r.l.', 'Giovanni Giudici', 'Order Administrator', 'Via dei Gelsomini, 153', 'Salerno', NULL, '84100', 'Italy', '(089) 6547665', '(089) 6547667', NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(27, 'Escargots Nouveaux', 'Marie Delamare', 'Sales Manager', '22, rue H. Voiron', 'Montceau', NULL, '71300', 'France', '85.57.00.07', NULL, NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(28, 'Gai pâturage', 'Eliane Noz', 'Sales Representative', 'Bat. B 3, rue des Alpes', 'Annecy', NULL, '74000', 'France', '38.76.98.06', '38.76.98.58', NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(29, 'Forêts d''érables', 'Chantal Goulet', 'Accounting Manager', '148 rue Chasseur', 'Ste-Hyacinthe', 'Québec', 'J2S 7S8', 'Canada', '(514) 555-2955', '(514) 555-2921', NULL)
SET IDENTITY_INSERT [Suppliers] OFF
ALTER TABLE [Suppliers] CHECK CONSTRAINT ALL
GO


I was so excited the first time I saw this, I nearly choked on my Diet Coke.



There's a nice introduction to SubSonic here. The latest news (here and here) is that Rob Conery (a.k.a. Mr. SubSonic) has joined Microsoft and MS will be paying him to continue developing SS. Rock on.