2008/04/26

Scott Hanselman talks MultiCore MSBuilding

Use that extra core or three you have sitting idle in your box.

Also see Scott's companion article on how to get your build server to stop being limited to a single core on the companion post http://www.hanselman.com/blog/FasterBuildsWithMSBuildUsingParallelBuildsAndMulticoreCPUs.aspx

Great info as usual Scott.

2008/04/23

Using CTE instead of CURSOR

Today, I came upon a need to change a Scalar Value Function in SQL 2005 that we are using to generate confirmation numbers based on an algorithm much like the following

Characters 1-5 will be the number of days since 1900.01.01; zero-padded if needed

Characters 6-10 will be the number of seconds since Today 00:00:00; zero-padded if needed

If this number is already in use, increment by 1 until reaching an unsed number


So I had two options to ensure that I properly checked for the numerically lowest available value.


  1. Create a cursor incrementing a variable that stored the generated confirmation number until I ran out of matches.

  2. Use a Common Table Expression and have it recursivly loop through to get the max sequential number starting at the generated confirmation number


As a disclaimer, I don't really like using cursors, I prefer to do as much as I can as a batch process. Half the time, I have to look up the syntax to ensure I am properly building up and deallocating the cursor. In short, I took option #2. CTEs are supposed to be easier resource-wise, maybe, possibly. Will map the execution plan at a later time.


In short, the following was the solution chosen. Comments are inline but if you have any questions, comments, concerns, feel free to leave feedback.


-- =============================================
--
Author: Paul Montgomery
-- Create date: 2008.04.23
-- Description: Gets a
time-based confirmation number consisting of number of days
-- Blog:
http://betterlivingthroughcoding.blogspot.com/2008/04/using-cte-instead-of-cursor.html
--
=============================================
CREATE FUNCTION
GetMeAConfirmationNumberForNow()
RETURNS
NVARCHAR(50)
AS
BEGIN
DECLARE @days VARCHAR(5),
@seconds
VARCHAR(5)
SET @days = RIGHT('00000' + CAST(DATEDIFF(d, 0, GetDate()) AS
VARCHAR), 5)
SET @seconds = RIGHT('00000' + CAST(DATEDIFF(s,
CONVERT(varchar(10), GETDATE(), 101), GetDate()) AS VARCHAR), 5)
--Get what
would be this seconds confirmation number
DECLARE @ConfirmationNumber
nvarchar(50)
SET @ConfirmationNumber = @days + @seconds
SET
@ConfirmationNumber = '3955979792'
/*
-- If we were completely sure there
would NEVER be a "future" confirmation
-- number being mistakenly put in the
table, we could just do this.
IF EXISTS (SELECT ConfirmationNumber
FROM
SomeTable
WHERE ConfirmationNumber <> 'SomeBadData' --weed out any
invalid data
AND CAST(ConfirmationNumber AS BIGINT) >
CAST(@ConfirmationNumber AS BIGINT)
BEGIN
SELECT @ConfirmationNumber =
CAST(MAX(CAST(ConfirmationNumber AS BIGINT)) + 1 AS NVARCHAR(50))
FROM
SomeTable
END
-- But since we had some crazy data, I wanted to be sure
that I was going
-- to pull the smallest unused number that was equal to or
greater than the
-- confirmation number genereated via our
algorithm
*/
/*
Select all the confirmation numbers from our table that
are numerically greater than @ConfirmationNumber
*/
DECLARE @TempTable
TABLE(
ConfirmationNumber nvarchar(50))
INSERT INTO
@TempTable(ConfirmationNumber)
SELECT ConfirmationNumber
FROM
SomeTable
WHERE ConfirmationNumber <> 'SomeBadData' --had to remove
some bogus rows
AND CAST(ConfirmationNumber AS BIGINT) >
CAST(@ConfirmationNumber AS BIGINT)
GROUP BY ConfirmationNumber --Yep had
some dupes posted, lukily only in dev/test
--PlaceHolder
DECLARE
@MaxConfirmationNumber nvarchar(50);
--Time for CTE to find my sequential
values
WITH confirmation_numbers (ConfirmationNumber)
AS
(
SELECT
ConfirmationNumber
FROM @TempTable
WHERE CAST(ConfirmationNumber AS
BIGINT) = CAST(@ConfirmationNumber AS BIGINT) + 1
UNION ALL
--This is
where the magic happens
SELECT tt.ConfirmationNumber
FROM @TempTable
tt
INNER JOIN confirmation_numbers
ON CAST(tt.ConfirmationNumber AS
BIGINT) = CAST(confirmation_numbers.ConfirmationNumber AS BIGINT) +1
)
--
We need to increment the max by 1 and get it back to an nvarchar
SELECT
@MaxConfirmationNumber = CAST(MAX(CAST(ConfirmationNumber AS BIGINT)) + 1 AS
NVARCHAR(50))
FROM confirmation_numbers
--Check that we have a this
confirmation or sequential ones higher
IF (@ConfirmationNumber <= @MaxConfirmationNumber) SET @ConfirmationNumber = @MaxConfirmationNumber --SELECT @ConfirmationNumber RETURN @ConfirmationNumber END GO

2008/04/21

Delay signing Click One application

Along the lines of build one; deploy many, I have been working on setting up a ClickOnce application that would be compiled and published once. This published set of files would then be configured for particular environment, signed, and set for public consumption. The problem is that when a ClickOnce application is published, the application and deployment manifests get signed and any changes to the files mean that the checksum values no longer line up. So how do you publish once and then make changes without breaking the newly signed manifests? Below is a subset of the proj file we are using. The csproj is called with OutputPath=$(DropLocation)\Setup\Raw\ and Targets=PublishOnly


<?xml version="1.0" encoding="utf-8" ?><project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" defaulttargets="Publish">
<import project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets">
<propertygroup>
<droplocation condition="'$(DropLocation)'==''">C:\VSDumpingGround\OurApp</droplocation>
<magepath condition="'$(MagePath)'==''">C:\Program Files\Microsoft SDKs\Windows\v6.0A\Bin\mage.exe</magepath>
<certfile condition="'$(CertFile)'==''">$(DropLocation)\OurApp.Key.pfx</certfile>
<signingcertpassword condition="'$(SigningCertPassword)'==''">password</signingcertpassword>
<publishername condition="'$(PublisherName)'==''">Our Company</publishername>
<buildnumberfile>C:\VSDumpingGround\BuildNumber.txt</buildnumberfile>
</propertygroup>
<propertygroup>
<publishdependson>
MageOurApp;
RemoveRawFiles
</publishdependson>
</propertygroup>
<target name="Publish" dependsontargets="$(PublishDependsOn)">
</target>
<target name="VersionOurApp">
<!-- MSBuild Community Task get and increment the versin number stored in text file-->
<version revisiontype="Increment" versionfile="$(BuildNumberFile)">
<output propertyname="Major" taskparameter="Major">
<output propertyname="Minor" taskparameter="Minor">
<output propertyname="Build" taskparameter="Build">
<output propertyname="Revision" taskparameter="Revision">
</version>
<version major="$(Major)" minor="$(Minor)" build="$(Build)" revision="$(Revision)">
</version>
</target>
<target name="MoveApplicationFiles" dependsontargets="VersionOurApp">
<createproperty value="$(DropLocation)\Setup\OurApp.application">
<output propertyname="ApplicationFile" taskparameter="Value">
</createproperty>
<!-- Move all files in the app.publish directory up to the setup folder-->
<createitem include="$(DropLocation)\Setup\Raw\app.publish\*" exclude="$(ApplicationFile)">
<output itemname="MoveSetup" taskparameter="Include">
</createitem>
<Copy SourceFiles="@(MoveSetup)"
DestinationFolder="$(DropLocation)\Setup\"/>
<!-- Remove any duplicate files from the list-->
<createitem include="$(DropLocation)\Setup\Raw\app.publish\Application Files\OurApp*\*">
<output itemname="AppFilesRoot" taskparameter="Include">
</createitem>
<removeduplicates inputs=""> '%(rootdir)%(directory)')">
<output itemname="FilteredAppFilesRoot" taskparameter="Filtered">
</removeduplicates>
<createitem include="%(FilteredAppFilesRoot.Identity)**\*">
<output itemname="MoveAppFiles" taskparameter="Include">
</createitem>
<!-- Copy the deploy files to the Application Files\OurApp_X_X_X_X directory -->
<Copy SourceFiles="@(MoveAppFiles)"
DestinationFolder="$(DropLocation)\Setup\Application Files\OurApp_$(Major)_$(Minor)_$(Build)_$(Revision)\%(RecursiveDir)"/>
<removedir directories="$(DropLocation)\Setup\Raw\">
</target>
<propertygroup>
<prepourappdependson>
MoveApplicationFiles;
VersionOurApp;
ConfigureOurApp
</prepourappdependson>
</propertygroup>
<target name="PrepOurApp" dependsontargets="$(PrepOurAppDependsOn)">
<createproperty value="$(DropLocation)\Setup\Application Files\OurApp_$(Major)_$(Minor)_$(Build)_$(Revision)">
<output taskparameter="Value" propertyname="ApplicationDirectory">
</createproperty>
<createproperty value="$(DropLocation)\Setup\Application Files\OurApp_$(Major)_$(Minor)_$(Build)_$(Revision)\OurApp.exe.manifest">
<output propertyname="ManifestFile" taskparameter="Value">
</createproperty>
<createproperty value="$(DropLocation)\Setup\Raw">
<output propertyname="RawDirectory" taskparameter="Value">
</createproperty>
<!-- Remove the manifest file if it exists-->
<delete files="$(ManifestFile)" continueonerror="true">
<!-- copy files to a new Raw directory-->
<CreateItem Include="$(ApplicationDirectory)\**\*"
Exclude="$(ManifestFile)">
<Output ItemName="DeployFiles"
TaskParameter="Include"/>
</createitem>
<!-- This removes the .deploy from the files as you copy them to a new Raw directory-->
<Copy SourceFiles="@(DeployFiles)"
DestinationFiles="@(DeployFiles -> '$(DropLocation)\Setup\Raw\%(RecursiveDir)\%(FileName)')"/>
</target>
<propertygroup>
<configureourappdependson>
VersionOurApp;
MoveApplicationFiles
</configureourappdependson>
</propertygroup>
<target name="ConfigureOurApp" dependsontargets="$(ConfigureOurAppDependsOn)">
<!-- See my blog about editing XML config files
You would use the files in the newly created $(DropLocation)\Setup\Raw\... location(s)-->
</target>
<propertygroup>
<mageourappdependson>
ConfigureOurApp;
VersionOurApp;
PrepOurApp
</mageourappdependson>
</propertygroup>
<target name="MageOurApp" dependsontargets="$(MageOurAppDependsOn)">
<!-- Generate new application manifest-->
<GenerateApplicationManifest
AssemblyName="OurApp.exe"
AssemblyVersion="$(Major).$(Minor).$(Build).$(Revision)"
EntryPoint="$(RawDirectory)\OurApp.exe"
OutputManifest="$(ManifestFile)"/>
<!-- Sign the application manifest-->
<!-- %22 takes the place of "-->
<!-- This signs the newly created manifest-->
<exec command="%22$(MagePath)%22 -Update %22$(ManifestFile)%22 -fd %22$(RawDirectory)%22 -cf %22$(CertFile)%22 -pwd $(SigningCertPassword)">
<!-- Generate new deployment manifest-->
<GenerateDeploymentManifest
AssemblyName="OurApp.application"
AssemblyVersion="$(Major).$(Minor).$(Build).$(Revision)"
DeploymentUrl="http://somecompany.com/setup/OurApp.application"
DisallowUrlActivation="false"
EntryPoint="$(ManifestFile)"
Install="true"
MapFileExtensions="true"
MinimumRequiredVersion ="$(Major).$(Minor).$(Build).$(Revision)"
OutputManifest="$(ApplicationFile)"
Product="Our Product"
Publisher="$(PublisherName)"
UpdateEnabled="true"
UpdateMode="Foreground">
</generatedeploymentmanifest>
<!-- Sign the deployment manifest-->
<!-- %22 takes the place of "-->
<!-- We had issues with Publisher and Product not properly propogating to the .application file. We are on framework 3.0 so we can't use UseApplicationTrust, Publisher, Product on the GenerateApplicationManifest task-->
<!-- This signs the newly created manifest and forces in Publisher/Product. Marks the deployment manifest as used for trust-->
<exec command="%22$(MagePath)%22 -Update %22$(ApplicationFile)%22 -cf %22$(CertFile)%22 -providerurl %22http://somecompany.com/setup/OurApp.application%22 -Tofile %22$(ApplicationFile)%22 -appm %22$(ManifestFile)%22 -pwd $(SigningCertPassword) -pub %22$(PublisherName)%22 -UseManifestForTrust true">
</target>
<target name="RemoveRawFiles" dependsontargets="MageOurApp">
<removedir directories="$(RawDirectory)">
</target>
</project>

XML editing via MSBuild

Every environment I have been in has strived for a single build that can be deployed to multiple environments. This means one compilation and a single set of binaries. Different settings per environment are to be set in config/xml files. No big surprise. And yes, I know that the MSBuild Community Tasks have an xml editing task. I prefer mine. In part to the config files having a single naespace and if Microsoft desides to change the namespace declaration, I won't have to revist my proj files. Also, I have options to replace full or partial values for attributes or InnerText. Also, you can recursively search a directory for files matching the search pattern.

Bringing on the class




using System;
using System.Collections.Generic;
using System.Xml;
using System.Text;
using Microsoft.Build.Utilities;
using Microsoft.Build.BuildEngine;
using Microsoft.Build.Framework;
using System.IO;
namespace PaulMontgommery.Custom.Tasks
{
///<summary>
/// Updates a XML document using a XPath.
/// </summary>
/// <example>Update a XML element.
/// <code><![CDATA[
/// <xmledit document="C:\VSProjects\MyProject\*.config" xpath="//configuration/appSettings/add[@key='SMTPPort']" value="26">
/// <xmledit document="*.config" folder="C:\VSProjects\MyProject\" xpath="//configuration/appSettings/add[@key='SMTPPort']" value="26">
/// <xmledit document="*.config" folder="C:\VSProjects\MyProject\" xpath="//p:configuration/p:appSettings/p:add[@key='SMTPPort']" value="26" prefix="p">
/// <xmledit document="*.config" folder="C:\VSProjects\MyProject\" xpath="//p:configuration/p:appSettings/p:add[@key='SMTPPort']" value="26" prefix="p" attribute="value">
/// ]]></code>
/// </example>
/// <remarks>
/// The XML node being updated must exist before using the XmlUpdate task.
/// </remarks>
public class EditXml : Task
{
#region Class Variables
string _document = string.Empty;
string _folder = string.Empty;
string _xPath = string.Empty;
string _value = string.Empty;
string _replacedText = string.Empty;
string _attribute = string.Empty;
string _prefix = string.Empty;
bool _recursive = false;
bool _continueOnError = false;
bool _condition = true;
#endregion
#region Public Properties
/// <summary>
/// Required. Document to perform edit on. Wildcards are allowed.
/// </summary>
[Required]
public string Document
{
get { return _document; }
set { _document = value; }
}
/// <summary>
/// Optional. Folder to begin searching.
/// </summary>
public string Folder
{
get { return _folder; }
set { _folder = value; }
}
/// <summary>
/// Required. XPath statement to find value to edit.
/// </summary>
[Required]
public string XPath
{
get { return _xPath; }
set { _xPath = value; }
}
/// <summary>
/// Optional. Namespace prefix for XPath statement.
/// </summary>
public string Prefix
{
get { return _prefix; }
set { _prefix = value; }
}
/// <summary>
/// Required. Value to be placed into document as InnerText or as "value" attribute if innertext is null.
/// </summary>
[Required]
public string Value
{
get { return _value; }
set { _value = value; }
}
/// <summary>
/// Optional. Value to be replaced with <see cref="Value">.
/// </summary>
public string ReplacedText
{
get { return _replacedText; }
set { _replacedText = value; }
}
/// <summary>
/// Optional name of attribute to perform edit on.
/// </summary>
public string Attribute
{
get { return _attribute; }
set { _attribute = value; }
}
/// <summary>
/// Optional. Specifies whether subfolders of <see cref="Folder">should be searched.
/// Default is false.
/// </summary>
public bool Recursive
{
get { return _recursive; }
set { _recursive = value; }
}
/// <summary>
/// Optional. Specifies whether process should continue if an exception is thrown. Default is false.
/// </summary>
public bool ContinueOnError
{
get { return _continueOnError; }
set { _continueOnError = value; }
}
/// <summary>
/// Optional. A Run-time check to see if this process should execute. Default is true.
/// </summary>
public bool Condition
{
get { return _condition; }
set { _condition = value; }
}
#endregion
#region Public Methods
public override bool Execute()
{
// System.Diagnostics.Debugger.Launch();
if (!_condition)
return true;
bool success = false;
try
{
//We must have a folder or document to edit
if (string.IsNullOrEmpty(_folder) && _recursive)
throw new NullReferenceException("Folder must be specified for recursive searches.");
//We must have an XPath statement or ReplacedText to edit
if (string.IsNullOrEmpty(_xPath) && string.IsNullOrEmpty(_replacedText))
throw new NullReferenceException("XPath or ReplacedText must be specified.");
List<string> files = getFiles();
if (files == null)
throw new NullReferenceException(string.Format("Could not find a part of the path '{0}\\{1}'", _folder, _document));
foreach (string file in files)
{
editXml(file);
}
success = true;
}
catch (Exception ex)
{
// System.Diagnostics.Debugger.Launch();
Log.LogErrorFromException(ex);
success = _continueOnError;
}
return (success _continueOnError);
}
#endregion
#region Private Methods
#region editXml
private void editXml(string file)
{
//load file into XmlDocument
Log.LogMessage(MessageImportance.Normal, string.Format("Loading file '{0}'.", file));
XmlDocument doc = new XmlDocument();
doc.Load(file);
//Get namespace manager
XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable);
if (!string.IsNullOrEmpty(doc.DocumentElement.NamespaceURI))
{
if (string.IsNullOrEmpty(Prefix))
{
nsmgr.AddNamespace("ns", doc.DocumentElement.NamespaceURI);
//Inject the namespace into the xpath query
XPath = XPath.Replace("/", "/ns:").Replace("/ns:/ns:", "//ns:");
}
else
{
nsmgr.AddNamespace(Prefix.EndsWith(":") ? Prefix.Replace(":", "") : Prefix, doc.DocumentElement.NamespaceURI);
}
}
int i = 0;
foreach (XmlNode node in doc.SelectNodes(XPath, nsmgr))
{
Log.LogMessage(MessageImportance.Normal, string.Format(" Found node '{0}'.", XPath));
if (string.IsNullOrEmpty(Attribute))
{
//Value setting
if (string.IsNullOrEmpty(ReplacedText))
{
Log.LogMessage(MessageImportance.Normal, string.Format("\t Setting Value to '{0}'.", Value));
if (node.Value == null && string.IsNullOrEmpty(node.InnerText))
node.Attributes["value"].Value = Value;
else
node.InnerText = Value;
}
else
{
//Value replacement
string oldvalue;
if (node.Value == null && string.IsNullOrEmpty(node.InnerText))
{
oldvalue = node.Attributes["value"].Value;
node.Attributes["value"].Value = node.Attributes["value"].Value.Replace(ReplacedText, Value);
Log.LogMessage(MessageImportance.Normal, string.Format("\t Replaced '{0}' with '{1}'.", oldvalue, node.Attributes["value"].Value));
}
else
{
oldvalue = node.InnerText;
node.InnerText = node.InnerText.Replace(ReplacedText, Value);
Log.LogMessage(MessageImportance.Normal, string.Format("\t Replaced '{0}' with '{1}'.", oldvalue, node.InnerText));
}
}
}
else
{
if (string.IsNullOrEmpty(ReplacedText))
{
//Attribute setting
Log.LogMessage(MessageImportance.Normal, string.Format("\t Setting Attribute '{0}' to '{1}'.", Attribute, Value));
node.Attributes[Attribute].Value = Value;
}
else
{
//Attribute replacement
string oldvalue = node.Attributes[Attribute].Value;
node.Attributes[Attribute].Value = node.Attributes[Attribute].Value.Replace(ReplacedText, Value);
Log.LogMessage(MessageImportance.Normal, string.Format("\t Replaced value of Attribute '{0}' from '{1}' to '{2}'.", Attribute, oldvalue, node.Attributes[Attribute].Value));
}
}
i++;
//end of foreach
}
if (i == 0)
Log.LogWarning("Unable to locate node '{0}'.", XPath);
Log.LogMessage(MessageImportance.Normal, string.Format("Document completed with {0} change(s).", i));
doc.Save(file);
}
#endregion
#region getFiles
private List<string> getFiles()
{
if (Document.Contains("\\") && !string.IsNullOrEmpty(Folder))
throw new NotSupportedException("Document cannot have path information when Folder is specified.");
if (!string.IsNullOrEmpty(Folder))
return getFiles(Folder, Document);
else
return getFiles(Document);
}
private List<string> getFiles(string Document)
{
// Split folder from filename
return getFiles(Document.Substring(0, Document.LastIndexOf("\\")),
Document.Substring(Document.LastIndexOf("\\") + 1));
}
private List<string> getFiles(string Folder, string Document)
{
List<string> files = new List<string>();
// Add item for each file matching the search criteria
foreach (string file in Directory.GetFiles(Folder, Document))
files.Add(file);
//Check sub directories for additional files.
if (Recursive)
{
//Call getFiles with each subdirecotry and the Document.
foreach (string directory in Directory.GetDirectories(Folder))
files.AddRange(getFiles(directory, Document));
}
return files;
}
#endregion
#endregion
}
}




Now, how to use these roughly 300 lines of code. As you can see in the XML documentation prior to the class declarations, there are the following examples:


<xmledit document="C:\VSProjects\MyProject\*.config" xpath="//configuration/appSettings/add[@key='SMTPPort']" value="26">
<xmledit document="*.config" folder="C:\VSProjects\MyProject\" xpath="//configuration/appSettings/add[@key='SMTPPort']" value="26">
<xmledit document="*.config" folder="C:\VSProjects\MyProject\" xpath="//p:configuration/p:appSettings/p:add[@key='SMTPPort']" value="26" prefix="p">
<xmledit document="*.config" folder="C:\VSProjects\MyProject\" xpath="//p:configuration/p:appSettings/p:add[@key='SMTPPort']" value="26" prefix="p" attribute="value">

If you have any suggestions, let me know.

Comments