A long time ago a reader sent me a build script and asked for my thoughts on it. This is my response. In this entry I have marked up his build script with my comments inside of tags like:
<!-- ****************************************
My comments inside of these
********************************************* -->
I thought that you guys might be interested in this too. Here it is.
< Project DefaultTargets = " Build " xmlns = " http://schemas.microsoft.com/developer/msbuild/2003 " >
<!-- ****************************************
You can pull these out into a seperate file, i.e., CompanyName.BuildTasks.tasks
********************************************* -->
< UsingTask TaskName = " BuildTasks.MoveUpBuildNumber " AssemblyFile = " BuildTasks\bin\Debug\BuildTasks.dll " />
< UsingTask TaskName = " BuildTasks.ReplaceInFile " AssemblyFile = " BuildTasks\bin\Debug\BuildTasks.dll " />
< UsingTask TaskName = " BuildTasks.CheckInIntoVSS " AssemblyFile = " BuildTasks\bin\Debug\BuildTasks.dll " />
< UsingTask TaskName = " BuildTasks.CheckOutFromVSS " AssemblyFile = " BuildTasks\bin\Debug\BuildTasks.dll " />
< UsingTask TaskName = " BuildTasks.UndoCheckOutFromVSS " AssemblyFile = " BuildTasks\bin\Debug\BuildTasks.dll " />
< UsingTask TaskName = " BuildTasks.GetLatestFromVSS " AssemblyFile = " BuildTasks\bin\Debug\BuildTasks.dll " />
< UsingTask TaskName = " BuildTasks.LabelInVSS " AssemblyFile = " BuildTasks\bin\Debug\BuildTasks.dll " />
< UsingTask TaskName = " BuildTasks.ShowMessageBox " AssemblyFile = " BuildTasks\bin\Debug\BuildTasks.dll " />
<!-- ****************************************
In order to create more extensible MSBuild files you should place conditions on Properties.
For example:
<PropertyGroup Condition="'$(VSSDatabasePath)'==''">
<VSSDatabasePath>\\rataserv\vss\srcsafe.ini</VSSDatabasePath>
</PropertyGroup>
When you do this users can create another file, i.e., MyCustomBuild.proj which imports your
file and just overrides a few values.
********************************************* -->
< PropertyGroup >
< VSSDatabasePath > \\some\path\here\srcsafe.ini </ VSSDatabasePath >
</ PropertyGroup >
<!-- ======================================================================================== -->
<!-- ****************************************
Also for extensibility your DependsOnTargets should always be pulled from a property
which pre-prendes its values to the property itself. So that is:
<PropertyGroup>
<BuildDependsOn>
PrebuildAndRun;
CheckInDeliverables;
$(BuildDependsOn);
</BuildDependsOn>
</PropertyGroup>
In this manner external files can extend the behavior of the Build target. Without this it is going
to be difficult for people to effectively customize the build process.
********************************************* -->
< Target Name = " Build " DependsOnTargets = " PrebuildAndRun;CheckInDeliverables " >
</ Target >
<!-- ****************************************
Be careful with names. I would recommend using a naming convention that will ensure
that your Targets/Properties/Items do not collide with each other.
For example if I create re-usable .targets files and many of them have a "Build" target
then I cannot user more than 1 at a time via an <Import ..>. Which is how I like for
reusable .targets files to be used. For example using a prefix such as in my case SedoConfig or SedoDB.
Do I need to expand on this?
**************************************** -->
<!-- ****************************************
All "Important" targets should have Before and After targets which are on
the DependsOnTargets property. So that would be:
<PropertyGroup>
<BuildDependsOn>
BeforeBuild;
PrebuildAndRun;
CheckInDeliverables;
AfterBuild;
$(BuildDependsOn);
</BuildDependsOn>
</PropertyGroup>
<Target Name="Build" DependsOnTargets ="$(BuildDependsOn)"/>
<Target Name="BeforeBuild"/>
<Target Name="AfterBuild"/>
**************************************** -->
<!-- ****************************************
All the files that you need to checkout can be placed inside of an item and then
you can batch the usage of the BuildTasks.CheckOutFromVSS task. The item would
have to have custom metadata of DatabasePath, FilePathInVss and WorkingDirectory.
So that would be:
<ItemGroup>
<FilesToCheckOut Include="BuildNumberLP.txt">
<DatabasePath>$(VSSDatabasePath)</DatabasePath>
<FilePathInVSS>$/Source/src/LP/BuildNumberLP.txt</FilePathInVSS>
<WorkingDirectory>LP</WorkingDirectory>
</FilesToCheckOut>
<FilesToCheckOut Include="DatabasePackages.sql">
<DatabasePath>$(VSSDatabasePath)</DatabasePath>
<FilePathInVSS>$/Source/src/LP/DatabaseScripts/DatabasePackages.sql</FilePathInVSS>
<WorkingDirectory>LP/DatabaseScripts</WorkingDirectory>
</FilesToCheckOut>
</ItemGroup>
<Target Name="PrebuildAndRun">
<BuildTasks.CheckOutFromVSS
DatabasePath="%(FilesToCheckOut.DatabasePath)"
FilePathInVSS="%(FilesToCheckOut.FilePathInVSS)"
WorkingDirectory="%(FilesToCheckOut.WorkingDirectory)"
/>
</Target>
**************************************** -->
<!-- ****************************************
The value for DatabasePath probably could just be taken from $(VSSDatabasePath)
still. It depends on if you might pull files from more than one repository.
**************************************** -->
< Target Name = " PrebuildAndRun " >
< Message Text = " building $(MSBuildProjectFile) " Importance = " high " />
< Message Text = " ------ PATCHING FILES WITH BUILD NUMBER " Importance = " high " />
<!-- check out file that stores the version incremented on each release build -->
< BuildTasks.CheckOutFromVSS
DatabasePath = " $(VSSDatabasePath) "
FilePathInVSS = " $/Some/Path/Here/BuildNumberLP.txt "
WorkingDirectory = " LP "
/>
<!-- check out source files we gonna patch -->
< BuildTasks.CheckOutFromVSS
DatabasePath = " $(VSSDatabasePath) "
FilePathInVSS = " $/Some/Path/Here/DatabaseScripts/DatabasePackages.sql "
WorkingDirectory = " LP/DatabaseScripts "
/>
< BuildTasks.CheckOutFromVSS
DatabasePath = " $(VSSDatabasePath) "
FilePathInVSS = " $/Some/Path/Here/AssemblyInfo.cs "
WorkingDirectory = " LP "
/>
< BuildTasks.CheckOutFromVSS
DatabasePath = " $(VSSDatabasePath) "
FilePathInVSS = " $/Some/Path/Here/Properties/AssemblyInfo.cs "
WorkingDirectory = " LPinstaller/Properties "
/>
<!-- check out deliverables -->
< BuildTasks.CheckOutFromVSS
DatabasePath = " $(VSSDatabasePath) "
FilePathInVSS = " $/Some/Path/Here/Release/LPSetup.msi "
WorkingDirectory = " LPSetup/Release "
/>
< BuildTasks.CheckOutFromVSS
DatabasePath = " $(VSSDatabasePath) "
FilePathInVSS = " $/Some/Path/Here/Release/setup.exe "
WorkingDirectory = " LPSetup/Release "
/>
<!-- ****************************************
The value passed to file could be taken from a property
so that it can be overridden by another user.
**************************************** -->
<!-- PATCH! -->
< BuildTasks.MoveUpBuildNumber
File = " LP/BuildNumberLP.txt " >
< Output TaskParameter = " BuildNumber " PropertyName = " BuildNumber " />
</ BuildTasks.MoveUpBuildNumber >
<!-- <Message Text="Build number for the new build will be:$(BuildNumber)" Importance="high" /> -->
< BuildTasks.ReplaceInFile
SearchString = " PACKAGE_REVISION "
ReplaceString = " $(BuildNumber) "
FileName = " LP/DatabaseScripts/DatabasePackages.sql "
/>
< BuildTasks.ReplaceInFile
SearchString = " 27857 "
ReplaceString = " $(BuildNumber) "
FileName = " LP/AssemblyInfo.cs "
/>
< BuildTasks.ReplaceInFile
SearchString = " 27857 "
ReplaceString = " $(BuildNumber) "
FileName = " LPinstaller/Properties/AssemblyInfo.cs "
/>
< Message Text = " ------ PATCHING FILES WITH BUILD NUMBER... DONE " Importance = " high " />
<!-- ****************************************
To get to devenv you could also use:
$(VS80COMNTOOLS)..\IDE\devenv.com</Devenv>
**************************************** -->
<!-- ====================================== -->
<!-- now build the entire solution including the setup packaging in release mode -->
<!-- <MSBuild Projects="LP/LP.SLN" Properties="Configuration=Release"/> -->
< Exec Command = " " C:\Program Files\Microsoft Visual Studio 8\Common7\IDE/devenv.com " " .\LP\LP.SLN
" /build " Release " /project ..\LPSetup\LPSetup.vdproj /projectconfig " Release " " />
<!-- ====================================== -->
< OnError ExecuteTargets = " RecoverFromError " />
</ Target >
<!-- ======================================================================================== -->
< Target Name = " CheckInDeliverables " >
<!-- ****************************************
You could use the same item as you used in the BuildTasks.CheckOutFromVSS
step here.
**************************************** -->
< Message Text = " ------ CHECKING IN CHANGED FILES " Importance = " high " />
<!-- check the file with new version back into VSS -->
< BuildTasks.CheckInIntoVSS
DatabasePath = " $(VSSDatabasePath) "
FilePathInVSS = " $/Some/Path/Here/LP/BuildNumberLP.txt "
WorkingDirectory = " LP "
/>
<!-- ****************************************
Since you are performing two different actions CheckIn and UndoCheckout
you would need another piece of metadata on the item lets say, i.e. CheckInAfterEdit,
then on your CheckInIntoVSS task usage you would place the condition
Condition="'%(FilesToCheckOut.CheckInAfterEdit)'=='true'"
that way you would create a batch of files to check in and pass it to the task.
On your UndoCheckOut you would jus inver the == to !=.
**************************************** -->
<!-- revese the assemblies and script files to the "search marker" that
gets updated on patching by the build version -->
< BuildTasks.UndoCheckOutFromVSS
DatabasePath = " $(VSSDatabasePath) "
FilePathInVSS = " $/Some/Path/Here/DatabaseScripts/DatabasePackages.sql "
WorkingDirectory = " LP/DatabaseScripts "
/>
< BuildTasks.UndoCheckOutFromVSS
DatabasePath = " $(VSSDatabasePath) "
FilePathInVSS = " $/Some/Path/Here/LP/AssemblyInfo.cs "
WorkingDirectory = " LP "
/>
< BuildTasks.UndoCheckOutFromVSS
DatabasePath = " $(VSSDatabasePath) "
FilePathInVSS = " $/Some/Path/Here/LPinstaller/Properties/AssemblyInfo.cs "
WorkingDirectory = " LPinstaller/Properties "
/>
<!-- checkin deliverables we just built -->
< BuildTasks.CheckInIntoVSS
DatabasePath = " $(VSSDatabasePath) "
FilePathInVSS = " $/Some/Path/Here/Release/LPSetup.msi "
WorkingDirectory = " LPSetup/Release "
/>
< BuildTasks.CheckInIntoVSS
DatabasePath = " $(VSSDatabasePath) "
FilePathInVSS = " $/Some/Path/Here/Release/setup.exe "
WorkingDirectory = " LPSetup/Release "
/>
<!-- Label as the new version -->
< BuildTasks.LabelInVSS
DatabasePath = " $(VSSDatabasePath) "
FilePathInVSS = " $ "
Label = " Revision $(BuildNumber) "
/>
< Message Text = " ------ CHECKING IN CHANGED FILES... DONE " Importance = " high " />
< OnError ExecuteTargets = " ErrorOnCheckIn " />
</ Target >
<!-- ======================================================================================== -->
< Target Name = " RecoverFromError " >
< Message Text = " An error has occurred, reversing checkouts " />
< BuildTasks.UndoCheckOutFromVSS
DatabasePath = " $(VSSDatabasePath) " IgnoreError = " true "
FilePathInVSS = " $/Some/Path/Here/LP/AssemblyInfo.cs "
WorkingDirectory = " LP "
/>
< BuildTasks.UndoCheckOutFromVSS
DatabasePath = " $(VSSDatabasePath) " IgnoreError = " true "
FilePathInVSS = " $/Some/Path/Here/LP/BuildNumberLP.txt "
WorkingDirectory = " LP "
/>
< BuildTasks.UndoCheckOutFromVSS
DatabasePath = " $(VSSDatabasePath) " IgnoreError = " true "
FilePathInVSS = " $/Some/Path/Here/DatabaseScripts/DatabasePackages.sql "
WorkingDirectory = " LP/DatabaseScripts "
/>
< BuildTasks.UndoCheckOutFromVSS
DatabasePath = " $(VSSDatabasePath) " IgnoreError = " true "
FilePathInVSS = " $/Some/Path/Here/LPinstaller/Properties/AssemblyInfo.cs "
WorkingDirectory = " LPinstaller/Properties "
/>
< BuildTasks.UndoCheckOutFromVSS
DatabasePath = " $(VSSDatabasePath) " IgnoreError = " true "
FilePathInVSS = " $/Some/Path/Here/Release/LPSetup.msi "
WorkingDirectory = " LPSetup/Release "
/>
< BuildTasks.UndoCheckOutFromVSS
DatabasePath = " $(VSSDatabasePath) " IgnoreError = " true "
FilePathInVSS = " $/Some/Path/Here/Release/setup.exe "
WorkingDirectory = " LPSetup/Release "
/>
</ Target >
<!-- ======================================================================================== -->
< Target Name = " ErrorOnCheckIn " >
<!-- ****************************************
Does this actually show a message box? If so you should place a condition on this task
usage here to make sure that this can be disabled so that the builds can be automated.
**************************************** -->
< BuildTasks.ShowMessageBox Message =
" Build process failed to check in the files for the new build or reverse checkouts.
Make sure that all files are checked in and retry the build. " />
</ Target >
</ Project >
Sayed Ibrahim Hashimi
msbuild |
review
Wednesday, June 03, 2009 4:25:21 AM (GMT Daylight Time, UTC+01:00)

|
If you are using the
ASP.NET MVC
DefaultModelBinder to manage the creating complex types from your views’ form data, as well as the validation summary helper provided with ASP.NET MVC then you may have run into the situation I did. I created a simple form, in a dummy app, to create a new contact. The page is named AddContactClassic.aspx, the contents are shown below.
<%
@
Page
Title
=""
Language
="C#"
MasterPageFile
="~/Views/Shared/Site.Master"
Inherits
="System.Web.Mvc.ViewPage"
%>
<%
@
Import
Namespace
="Sedodream.Web.Common.Contact"
%>
<
asp
:
Content
ID
="Content1"
ContentPlaceHolderID
="TitleContent"
runat
="server">
Add Contact Classic
</
asp
:
Content
>
<
asp
:
Content
ID
="Content2"
ContentPlaceHolderID
="MainContent"
runat
="server">
<
h2
>
Add Contact Classic
</
h2
>
<%
=
Html.ValidationSummary(
"Errors exist"
) %>
<
ol
><
li
><
span
class
="success-message">
<%
=
ViewData[
"SuccessMessage"
]%>
</
span
></
li
></
ol
>
<%
using
(Html.BeginForm())
{ %>
<
fieldset
>
<
legend
>
Account Information
</
legend
>
<
ol
>
<
li
>
<
label
for
="FirstName">
First name
</
label
>
<%
=
Html.TextBox(
"FirstName"
) %>
<%
=
Html.ValidationMessage(
"FirstName"
,
"*"
) %>
</
li
>
<
li
>
<
label
for
="LastName">
Last name
</
label
>
<%
=
Html.TextBox(
"LastName"
) %>
<%
=
Html.ValidationMessage(
"LastName"
,
"*"
) %>
</
li
>
<
li
>
<
label
for
="Email">
Email
</
label
>
<%
=
Html.TextBox(
"Email"
)%>
<%
=
Html.ValidationMessage(
"Email"
,
"*"
)%>
</
li
>
<
li
>
<
label
for
="Phone">
Phone
</
label
>
<%
=
Html.TextBox(
"Phone"
)%>
<%
=
Html.ValidationMessage(
"Phone"
,
"*"
)%>
</
li
>
<
li
>
<
div
class
="option-group"
id
="Gender">
<%
=
Html.RadioButton(
"Gender"
,
Gender
.Male.ToString())%>
<
span
>
<%
=
Gender
.Male.ToString() %>
</
span
>
<%
=
Html.RadioButton(
"Gender"
,
Gender
.Female.ToString())%>
<
span
>
<%
=
Gender
.Female.ToString() %>
</
span
>
</
div
>
</
li
>
<
li
>
<
input
type
="submit"
value
="Add contact"
/>
</
li
>
</
ol
>
</
fieldset
>
<% } %>
</
asp
:
Content
>
In my
ContactController
class the following methods are defined.
public
ActionResult
AddContactClassic()
{
return
View();
}
[
AcceptVerbs
(
HttpVerbs
.Post)]
public
ActionResult
AddContactClassic(
Contact
contact)
{
if
(contact ==
null
) {
throw
new
ArgumentNullException
(
"contact"
); }
InternalAddContact(contact);
return
View();
}
When I ran the app and filled in all the values on the AddContactClassic page I was a bit surprised to see an error simply stating “A value is required.” Here is a screen shot.
So I assumed that I must have misspelled one of the names of the fields that were passed to the
Html.TextBox
method. Obviously this was not the issue, so then I remembered that the Contact class that I defined had another property, Id, which I was not contained in a field on the form. This is the case because this page is supposed to create a new Contact, so its Id will not be set. I changed the AddContactClassic(Contact) method to ignore the Id property when binding was occurring. Here is the new method.
[
AcceptVerbs
(
HttpVerbs
.Post)]
public
ActionResult
AddContactClassic(
[
Bind
(Exclude=
"Id"
)]
Contact
contact)
{
if
(contact ==
null
) {
throw
new
ArgumentNullException
(
"contact"
); }
InternalAddContact(contact);
return
View();
}
By using the
Bind attribute
I was able to let the
DefaultModelBinder
know that I was not interested in getting the value for that field. Once I made this change everything worked fine.
In the Contact class I had incorrectly defined the Id property to be
Guid
instead of
Guid?
which is better. If I had correctly declared that then the
DefaultModelBinder
would have known that it was not a required field and it would not have complained. But if you are using the
Entity Framework
, you may still have this issue anyway because it does not always create nullable fields for all nullable columns in my experience. Even after changing the Contact class to use Guid? I have purposefully left the
Bind(Exclude=”Id”)
on the method in case something changes in the implementation of the Contact class.
Sayed Ibrahim Hashimi
Monday, May 18, 2009 4:25:27 AM (GMT Daylight Time, UTC+01:00)

|
With MSBuild 3.5 two new known metadata names were introduced for items passed to the MSBuild Task. Those known metadata names are Properties and AdditionalProperties. The idea is that you can now specify properties on the item itself instead of having to pass all the properties using the Properties attribute on the MSBuild Task. For example you could define an item, Projects, to be:
<ItemGroup>
<Projects
Include="Project01.proj">
<Properties>Configuration=Release</Properties>
</Projects>
</ItemGroup>
Then you can build the values contained in the Projects item using the MSBuild task:
<Target
Name="BuildProjects">
<MSBuild
Projects="@(Projects)"/>
</Target>
Like I said previously there are two new ways to pass properties in item metadata, Properties and AdditionalProperties. The difference can be confusing and very problematic if used incorrectly. Admittedly I didn't know the difference until about 6 months ago (but soon enough to include in my book
J ). The difference is that if you specify properties using the Properties metadata then any properties defined using the Properties attribute on the MSBuild Task will be ignored. In contrast to that if you use the AdditionalProperties metadata then both values will be used, with a preference going to the AdditionalProperties values.
Now let's take a look at an example to make this clear. The following MSBuild file is named Properties.proj and is shown below.
<Project
xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
ToolsVersion="3.5">
<PropertyGroup>
<ProjectPath>Project01.proj</ProjectPath>
</PropertyGroup>
<ItemGroup>
<_ProjectToBuild_Properties
Include="$(ProjectPath)">
<Properties>Configuration=Debug</Properties>
</_ProjectToBuild_Properties>
</ItemGroup>
<ItemGroup>
<_ProjectToBuild_AdditionalProperties
Include="$(ProjectPath)">
<AdditionalProperties>Configuration=Debug</AdditionalProperties>
</_ProjectToBuild_AdditionalProperties>
</ItemGroup>
<Target
Name="Build_Properties">
<Message
Text="Building project with Properties metadata"
Importance="high"/>
<MSBuild
Projects="@(_ProjectToBuild_Properties)"
Properties="DebugSymbols=true" />
</Target>
<Target
Name="Build_AProperties">
<Message
Text="Building project with AdditionalProperties metadata"
Importance="high"/>
<MSBuild
Projects="@(_ProjectToBuild_AdditionalProperties)"
Properties="DebugSymbols=true" />
</Target>
</Project>
I've highlighted a few important elements contained in this file. Firstly if you do not specify your ToolsVersion to be 3.5 then none of this functionality will work. Make sure to always provide this value, or you may be baffled by the un-expected behavior.
Inside of both targets you can see that I and providing the value Properties="DebugSymbols=true". Below is the definition of the file that it is building, the Project01.proj file.
<Project
xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
ToolsVersion="3.5">
<Target
Name="Build">
<Message
Text="Building $(MSBuildProjectFile)"
Importance="high"/>
<Message
Text="Configuration: $(Configuration)"/>
<Message
Text="DebugSymbols: $(DebugSymbols)"/>
<Message
Text="OutputPath: $(OutputPath)"/>
</Target>
</Project>
This file just prints out the values for a couple of properties that are not defined inside the file itself. Now let's see the behavior when we execute both of these targets. The result is shown in the image below.
As I've highlighted here the result of executing the Build_Properties target results in the attribute Properties="DebugSymbols=true" being completely ignored. The result from the Build_AProperties target takes the Properties attribute into consideration as well as the AdditionalProperties metadata. Don't use both of these together!
I personally prefer using the AdditionalProperties because I think that ignoring the Properties attribute can be confusing and hard to diagnose. This is why I tend to use that approach instead of the Properties metadata unless I have a specific reason.
Sayed Ibrahim Hashimi