2008/04/26
Scott Hanselman talks MultiCore MSBuilding
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
Characters 1-5 will be the number of days since 1900.01.01; zero-padded if neededCharacters 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.
- Create a cursor incrementing a variable that stored the generated confirmation number until I ran out of matches.
- 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
<?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
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.