Skip to content
Mar 4 10

Filtering sqlmetal output using XSLT

by Alex Peck

I needed to generate LINQ to SQL O/RM classes for a subset of the objects in a large database. Unfortunately, sqlmetal doesn’t provide a mechanism to filter it’s output, so I made my own using XSLT, a batch file and a custom build target. This post explains how.

Overview

When you invoke a build, a custom “BeforeBuild” build target runs to do the code generation. This code generation is driven by a batch file, this is what it does:

  1. Run sqlmetal to output a dbml file for the entire database schema
  2. Using an XML configuration file and an XSLT transform, generate a test .cs file containing a reference to each expected table. When included in our project, this gives us a compile time test that we generated the expected classes (we make this internal).
  3. Run a second XSLT transform based on the configuration file and the output a dbml file from step 1. In this step we prune the xml according to the config file. The result is a dbml file containing only the tables we specify in our configuration.
  4. Run sqlmetal again using the result of step 3 as input. This time sqlmetal outputs our CSharp code for our Linq to Sql classes.

The XSLT transforms are run from the command line using MSXSL.exe, which is available here.

The generated CSharp files are part of my Visual Studio project. After the BeforeBuild target is run (which does the code generation), the generated code is compiled into an Assembly.

Custom build target

These are the pertinent parts of my project file. You can see that inside my project directory, I created a directory called Prebuild where the work happens. This is where the Configuration.xml, DbmlPruner.xslt, GenerateTestTypes.xslt and generate.bat files live.

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="3.5" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <!-- most of the project file omitted for brevity -->
  <Target Name="BeforeBuild" DependsOnTargets="GenerateDbClasses;"> </Target>
  <Target Name="GenerateDbClasses" Inputs="Prebuild\Configuration.xml;Prebuild\DbmlPruner.xslt;Prebuild\GenerateTestTypes.xslt" Outputs="MasterDb.Generated.cs;TestMasterDbTypes.cs">
    <Exec Command="$(ProjectDir)Prebuild\generate.bat $(ProjectDir) $(TargetName) master.dbml prunedmaster.dbml TestMasterDbTypes.cs MasterDb Configuration.xml master" />
  </Target>
</Project>

Configuration.xml

<?xml version="1.0" encoding="utf-8"?>
<!-- This configuration is used to specify which tables should generate linq to SQL classes, 
  and to generate a sanity check class which verifies all the expected types exist in the 
  generated code.
  -->
<Configuration Name="Master Tables" SourceXml="master.dbml" Namespace="Master.Data.Linq" TestClassName="TestMasterTypes">
  <Table SqlName="dbo.TestTable" DataContextPropertyName="TestTable" ClassName="TestTableRow"/>
</Configuration>

GenerateTestTypes.xslt

<?xml version="1.0" encoding="utf-8"?>
<!-- ===========================================================
  Generate a C# class with members corresponding to all the linq
  to SQL tables specified in the input file.
================================================================ -->
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:output method="text" />
  <xsl:template match="/">
//-----------------------------------------------------------------------
// <![CDATA[<auto-generated>]]>
//     This code was generated by a tool.
//
//     Changes to this file may cause incorrect behavior and will be lost 
//     if the code is regenerated.
// <![CDATA[</auto-generated>]]>
//-----------------------------------------------------------------------
 
// Disable warning CS0169: The private field 'foo' is never used. This is 
// by design.
#pragma warning disable 0169
 
namespace <xsl:value-of select="/Configuration/@Namespace" />
{
    /// <![CDATA[<summary>]]>
    /// This class is provided as a compile time test for the linq to SQL
    /// classes specified in Prebuild/configuration.xml. It will fail to
    /// compile if one of the dependent classes is not generated (or not
    /// generated with the expected name). This is by design.
    /// <![CDATA[</summary>]]>
    internal class <xsl:value-of select="/Configuration/@TestClassName" />
    {      
<xsl:for-each select="/Configuration/Table"><xsl:text>&#9;</xsl:text><xsl:text>&#9;</xsl:text>private <xsl:value-of select="@ClassName" /> <xsl:value-of select="' '" /> <xsl:value-of select="concat(@ClassName, 'Member')" />;<xsl:text>&#xa;</xsl:text></xsl:for-each><xsl:text>&#9;</xsl:text>}
}
 
#pragma warning restore 0169
  </xsl:template>
</xsl:stylesheet>

DbmlPruner.xslt

<?xml version="1.0" encoding="utf-8"?>
<!-- ===========================================================
  Replicate a .dbml file based on the tables specified in a 
  configuration.
================================================================ -->
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
	 xmlns:sql="http://schemas.microsoft.com/linqtosql/dbml/2007"
     version="1.0" >
  	<xsl:output method="xml" version="1.0" encoding="utf-8" indent="yes"
  	  omit-xml-declaration = "no"/>
 
	<xsl:template match="Configuration">
 
		<!-- load sql metal output as $database -->
		<xsl:variable name="database" select="document(@SourceXml)"/>
    <xsl:variable name="namespace" select="namespace-uri($database/sql:Database)"/>
 
		<xsl:comment> =====================================================================================
  <xsl:value-of select="@Name" /> generated from database <xsl:value-of select="$database/sql:Database/@Name" /> (<xsl:value-of select="@SourceXml" />)
========================================================================================== </xsl:comment>
 
		<!-- Output a tree which replicates @SourceXml but contains only the table nodes in the configuration -->
		<xsl:element name="Database" namespace="{$namespace}">
			<xsl:attribute name="Name">
        		<xsl:value-of select="$database/sql:Database/@Name" />
      		</xsl:attribute>
 
			<xsl:for-each select="/Configuration/Table">
 
				<xsl:variable name="sqlName" select="@SqlName"/>
 
				<!-- only output a table element when the source table exists -->
				<xsl:if test="$database/sql:Database/sql:Table[@Name=$sqlName]">
 
					<!-- Output the table substituting Member and Type for the ClassName in the configuration -->
					<xsl:element name="Table" namespace="{$namespace}">
						<xsl:attribute name="Name">
							<xsl:value-of select="@SqlName"/>
						</xsl:attribute>
						<xsl:attribute name="Member">
							<xsl:value-of select="@DataContextPropertyName"/>
						</xsl:attribute>
 
						<xsl:element name="Type" namespace="{$namespace}">
							<xsl:attribute name="Name">
								<xsl:value-of select="@ClassName"/>
							</xsl:attribute>
 
							<!-- Copy the children (Columns etc) -->
							<xsl:copy-of select="$database/sql:Database/sql:Table[@Name=$sqlName]/sql:Type/*"/>
						</xsl:element>
 
					</xsl:element>
 
				</xsl:if>
			</xsl:for-each>
		</xsl:element>
	</xsl:template>
 
</xsl:stylesheet>

generate.bat

This is my entire generate.bat file, you can glean the input arguments from the custom build target above.

::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: Generate LINQ to SQL classes based on tables defined in an xml config
::
 
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 
setlocal 
 
set SQLMETAL=<put your path here!>\sdk\Win2008\Bin\sqlmetal.exe
set MSXSL=<put your path here!>\msxsl.exe
 
set PRJDIR=%1%
set PREDIR=%PRJDIR%Prebuild\
 
set NAMESPACE=%2
 
set METALOUT=%PREDIR%%3
set METALIN=%PREDIR%%4
 
set COMPILETESTCLASS=%PRJDIR%%5
set CONTEXTCLASS=%6
set CONTEXTCLASSFILE=%PRJDIR%%CONTEXTCLASS%.Generated.cs
 
set CONFIGPATH=%PREDIR%%7
set DATABASE=%8
 
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 
echo ================ Generation of LINQ to SQL classes started ================
echo Using %CONFIGPATH%
 
echo 1. Building %METALOUT% for entire %DATABASE% database 
%SQLMETAL% /conn:"server=localhost; database=%DATABASE%;Integrated Security=SSPI" /dbml:%METALOUT%
if errorlevel 1 goto :Failed
 
echo 2. Generating %COMPILETESTCLASS% to test linq to sql types were generated correctly at compile time
%MSXSL% %CONFIGPATH% %PREDIR%GenerateTestTypes.xslt -o %COMPILETESTCLASS%
if errorlevel 1 goto :Failed
 
echo 3. Building %METALIN% from %CONFIGPATH%
%MSXSL% %CONFIGPATH% %PREDIR%DbmlPruner.xslt -o %METALIN%
if errorlevel 1 goto :Failed
 
echo 4. Generating %CONTEXTCLASSFILE% using %METALIN%
%SQLMETAL% %METALIN% /code:%CONTEXTCLASSFILE% /language:csharp /context:%CONTEXTCLASS% /namespace:%NAMESPACE% /serialization:Unidirectional
if errorlevel 1 goto :Failed
 
echo ======== Generation of LINQ to SQL classes completed successfully =========
endlocal
goto :EOF
 
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:Failed
 
echo Configuration %CONFIG% FAILED
endlocal
exit /B 1
Mar 1 10

Folding plug

by Alex Peck

This is a really nice design. The voiceover is considerably less inspiring.

Feb 11 10

XSLT Reference Links

by Alex Peck

Over the last couple of days I started using XSLT for the first time. I found these useful links which were enough to get me going.

Feb 8 10

SqueezeServer WinForms Client

by Alex Peck

A couple of days ago I finally got fed up with the SqueezeCenter web interface. It’s not that it’s bad, it’s just that I open a lot of tabs in my browser, then can’t find the SqueezeCenter page when I need it.

A simple winforms Squeeze client.

I thought it would be nice to be able to control SqueezeCenter from something sitting in the taskbar, so over the last couple of days I made a simple .NET client. I’ve got 90% of what I need running, so as a proof of concept it has served its purpose.

Most of my effort was spent on data access and marshalling threads into the UI. Once I finish under the hood I might make a WPF UI layer, which would make it much more presentable.

Feb 6 10

Heavy Rain Demo

by Alex Peck

This is somewhat hidden, but if you visit Precinct 52 you can obtain a code to get the demo.

Once you register, select the following pieces of evidence (for some of them, you need to drill in to get the individual items):

1. Tyre tracks (117h)
2. Short cigarette (117b)
3. Origami bird
4. Coffee shop C (“diner”)

Once submitted you get a code which you can redeem in the Playstation store (there is a little button next to your basket icon at the top which took me an embarrassingly long time to find).

I really enjoyed the sleazy place demo, despite the clunky camera. This is a classic problem: you can let the player control the camera, or provide canned control sequences which are more cinematically gratifying. Although the shots might be framed better, you blunder about like a fool. This tends to break the suspension of disbelief, and is exacerbated by an animation engine which fails to adapt well enough to recover from most blunders.

This sort of criticism is missing the point I think, and failed to put a dent in my enjoyment. Quick time events have evolved slightly since Shenmue, and I like the way button sequences physically reflect the onscreen action and are embedded in the game world.

Watching this I realised that when I played it parts of some scenes ended differently (it clearly depends which questions you ask).

Jan 21 10

Virtual Machine Manager service crashes mysteriously

by Alex Peck

A few days ago Virtual Machine Manager started to crash, and was writing entries similar to this in the event log:

Fault bucket 736400234, type 5
Event Name: VMM20
Response: None
Cab Id: 0

Problem signature:
P1: vmmservice
P2: 2.0.4271.0
P3: Engine.TaskRepository
P4: 2.0.4271.0
P5: M.V.D.T.Task.GetAllTasks
P6: S.C.Generic.KeyNotFoundException
P7: cdb2
P8:
P9:
P10: 

Attached files:
C:\Windows\Temp\SCVMM.f5c52a6e-8f74-450c-bc57-d0ede6234a3b\report.txt

These files may be available here:
C:\ProgramData\Microsoft\Windows\WER\ReportArchive\Report069118df

The logs were empty, and a quick search based on this error didn’t yield any useful results. Eventually, I tried running a DBCC CHECKDB in SQL Server and found some corrupted page links like this:

Table error: Object ID x index ID x, partition ID x alloc unit ID type In-row data). Page x is missing a reference from previous page Possible chain linkage problem.

No wonder I got no hits by searching, it’s quite unlikely that somebody else’s SQL Server suffered the same corruption. I had to repair with data loss, but was able to recover practically everything (my automated backup hadn’t been running for some time).

ALTER DATABASE VirtualManagerDB SET SINGLE_USER WITH NO_WAIT
DBCC CHECKDB (VirtualManagerDB, REPAIR_ALLOW_DATA_LOSS)
ALTER DATABASE VirtualManagerDB SET MULTI_USER WITH NO_WAIT

Good old DBCC CHECKDB did the job.

Repair: The Nonclustered index successfully rebuilt for the object "dbo.tbl_TR_SubtaskTrail, PK_tbl_TR_SubtaskTrail" in database "VirtualManagerDB".
Msg 8945, Level 16, State 1, Line 3
Table error: Object ID 2137058649, index ID 1 will be rebuilt.
        The error has been repaired.
Msg 8978, Level 16, State 1, Line 3
Table error: Object ID 2137058649, index ID 1, partition ID 72057594039173120, alloc unit ID 72057594040418304 (type In-row data). Page (1:12338) is missing a reference from previous page (1:50295). Possible chain linkage problem.
        The error has been repaired.
Msg 8981, Level 16, State 1, Line 3
Table error: Object ID 2137058649, index ID 1, partition ID 72057594039173120, alloc unit ID 72057594040418304 (type In-row data). The next pointer of (1:50295) refers to page (1:34253). Neither (1:34253) nor its parent were encountered. Possible bad chain linkage.
        The error has been repaired.
Msg 8945, Level 16, State 1, Line 3
Table error: Object ID 2137058649, index ID 2 will be rebuilt.
        The error has been repaired.

The moral of this tale: make sure your automated backups are working properly.

Jan 15 10

Robozzle

by Alex Peck

Robozzle combines three things I like: games, programming and robots. It’s surprisingly addictive.

The source code is available here. If you get stuck, try this.

Jan 6 10

Travis Pastrana jumps 269 feet

by Alex Peck

This breaks the previous record for a ramp-to-ramp rally car jump by 98 feet.

Jan 5 10

Electromyography for the masses

by Alex Peck

Not content with Natal, Microsoft are filing a patent describing how to use EMG as an input medium. The following video demonstrates a few applications.

Ultimately, I’m sure they’re planning to miniaturise and embed this into people.

Dec 25 09

Win7 God Mode

by Alex Peck

Windows 7 provides a hidden shortcut to all control panels. Simply create a new folder and rename it as:

GodMode.{ED7BA470-8E54-465E-825C-99712043E01C}

It’s not quite TweakUI, but it is quite handy.