Friday, September 04, 2009

This weekend I will be speaking at the Tallahassee Code Camp on Saturday September 5, 2009. I will be presenting two sessions which are:

ASP.NET Custom View Helpers

If you have been using ASP.NET MVC then you certainly have been using some of the built in
view helper methods that are available, you know those expressions like
Html.TextBox("textBoxName")
and Html.ValidationMessage("Required").
View helpers are nothing more than extension methods which create HTML that is injected into
your views based on the method and its parameters. Creating your own view helpers is very
simple and can be extremely beneficial. By writing your own custom view helpers you will
benefit in at least the following ways

  • Simplifies Your Views
  • Easies Rehydrating HTML Elements with ModelState Values
  • Standardizes the Creation of Common HTML Components
  • Helps you Implement the DRY (Don't Repeat Yourself) Principal

We will take an in depth look at how you can easily and effectively create your own view helpers. We will also discuss how the default view helpers were created and the benefits that they provide.

Utilizing Web Deployment Projects

In this session we will take a look at how Web Deployment Projects can be used to assist in the deployment of web sites and web applications; including ASP.NET Web Applications and ASP.NET MVC Web Applications. We will give an overview of what Web Deployment Projects are and the functionality that is available out of the box. A Web Deployment Project is a wrapper for the aspnet_compiler.exe tool in the form of an MSBuild project and adds value to using the tool itself. Because they are MSBuild files we are able to customize and extend the process. We will discuss how we can customize the process to perform common steps such as

  1. Creating Virtual Directories
  2. Updating values in the web.config file
  3. Encrypting the web.config file
  4. Minimizing JavaScript files
  5. Versioning the Assemblies

 

If you are in the area and interested in these topics make sure to drop in!

Sayed Ibrahim Hashimi

Friday, September 04, 2009 5:40:20 AM (GMT Daylight Time, UTC+01:00)  #    Comments [0]  | 
Wednesday, August 26, 2009

On Friday August 28 and Saturday August 29 there will be a conference in St. Louis called St. Louis Day of .NET that I will be speaking at. Here is the outline

Simplify build and deployment of ASP.NET sites with Web Deployment Projects

When you are creating ASP.NET sites, (either ASP.NET web site / ASP.NET Web Project / ASP.NET MVC Project / etc) you will need to deploy the site to machines which will host them. Classically there were two options; xcopy the actual source to the live server and let them be compiled on demand or you could use the aspnet_compiler.exe and aspnet_merge.exe tools to pre-compile the website for you. The little known, yet extremely useful, Web Deployment Projects (an add on for Visual Studio) can greatly simplify the process of build and deployment. Web Deployment Projects will take care of the complexities of the aspnet_compiler.exe and the aspnet_merge.exe tool for you by way of a tight UI integration into Visual Studio itself. Aslo Web Deployment Projects are MSBuild files so you can extend and customize the process to suit your exact needs. In this session we will introduce Web Deployment Project, show that you can perform very powerful actions without writing a single line of code (or even text)! We will also dive into the MSBuild file that is the Web Deployment Project and show how to customize the process.

This session talks about Web Deployment Projects (2005 version) and how they can help in the build and deployment process. If you are going to be attending and interested in build and deployment of ASP.NET web sites and projects then you don't want to miss this session.

Sayed Ibrahim Hashimi

Wednesday, August 26, 2009 4:19:16 AM (GMT Daylight Time, UTC+01:00)  #    Comments [1]  | 
Friday, August 21, 2009

The other day I had a reader ask me about the difference between 32 and 64 bit builds. Here is his email to me.

I have your Using MSBuild book and I have been learning much with it. One aspect that I cannot seem to find online or in the book is that of the build server environment. Specifically how the choice of Server OS affects your builds and subsequent testing environments. The aspects of most of our build environments are TFS2008 and MSbuild servers running on Server 2003 32bit Standard some have VSTS Team Test edition 2008.

Here are some questions I am looking for answers on.

If my dev team is writing an app for run on 64 bit servers and they will be running VS Team test 2008 on the same build server then do they want VS 2008 64-bit installed as their build server? What could the pitfalls be if they are running builds and tests or just build for that matter on Server 2008 32-bit or 2003 32-bit? Or even just running builds?

If a team is developing an app for 32 bit server 2003 but your build machine is Server2008 64-bit, what could go wrong there? Also in many of the many combinations here are there any false hopes, such as building 32bit on a 64 machine may work but…

Additionally what is to be expected of WOW in this context 64-bit will run Windows on Windows on some apps I think but is this a best practice?

There are so many combinations of the way that an environment could be setup. A blog post exploring these different setups would be great if you could manage it. I just want to make sure my teams are getting what they need and are getting the expected results. Thanks much for your time.

I don't have too much knowledge in this area myself so I asked a couple other guys (William Bartholomew, JB Brown, Jim Lamb, and Grant Holliday) and here is some related info.

Assuming this is a .NET application the bitness of the build machine has no effect on the bitness of the produced application, this is controlled entirely by the project's compilation settings. Basically:

1. If the assembly is configured for Any CPU then it will run as x64 on a 64-bit machine and as x86 on a 32-bit machine.

2. If the assembly is configured for x86 then it will run as WOW64 on a 64-bit machine (i.e. a 32-bit process) and as x86 on a 32-bit machine.

3. If the assembly is configured for x64 then it will run as x64 on a 64-bit machine and will fail to run on a 32-bit machine. For some related info on this see Visual Studio: Why is there no 64 bit version? (yet)

The bitness of a process is determined by the compilation settings for the entry point (i.e. the executable)... You need to ensure that dependencies are compiled with an appropriate bitness for their host process. For example, devenv.exe always runs as a 32-bit process so if you create a Visual Studio add-in that's compiled for 64-bit only then it will never be loadable by Visual Studio. Basically it comes down to that a process can always load a dependency that's compiled as Any CPU or the same bitness as the process.

A few extra points about deployment and runtime.

4 - Extra considerations need to be made if you are dealing with multiple web sites under IIS 6. Since IIS 6 is a single process it is the entry point and determines the 64 bitness of the process it can only be in 1 "bitness" - so all sites under IIS have to be compatible with that bitness choice - IE - no running a 32 bit website and a 64 bit website under the same IIS 6 process. For more info on this see How to switch between the 32-bit versions of ASP.NET 1.1 and the 64-bit version of ASP.NET 2.0 on a 64-bit version of Windows.

5. While a 64 bit entry point/process may load a dependency that is compiled in Any CPU or x64 - it doesn't guarantee that those dependencies are run-time 64 bit process compatible. For example they may use COM-interop which will pass all compile time checks but fail at runtime. You'll need to do your due-diligence here on COM interop and 3rd party assemblies being used.

6. If your assemblies need regsrv32'd and run as a 32 bit WOW process remember that you need to use the 32 bit version of regsvr32.exe to get it registered into the right hive on that 64 bit OS. 

7. There is a similar thing to #6 for installutil.exe - a 64 bit version and a 32 bit WOW version

 

 

Sayed Ibrahim Hashimi

Friday, August 21, 2009 3:58:27 AM (GMT Daylight Time, UTC+01:00)  #    Comments [1]  | 
Friday, August 14, 2009

I am a pretty big fan of Web Deployment Projects (2005 version) I try and promote their use whenever I get a chance. The other day I had a customer tell me that he added a WDP to his solution and now all the devs were complaining because the build was taking much longer than it was previously. The reason behind this is pretty simple, when you build in Visual Studio it will build the WDP will call the aspnet_compiler.exe and the aspnet_merge.exe tool on your site(s) which can be a lengthy process. When you add a WDP to your solution by default it will be configured to build for each defined configuration. To alleviate this headache you should go to the configuration manager and disable these from building.

Once you open the configuration manager you will be presented a dialog like the following.

Here what you should do is make sure that the Build check box is un-checked for all WDPs. You need to do this for each configuration/platform combination. The idea here is that you want the developers to build inside of Visual Studio as they normally would without any hassles. If developers have to wait for a longer time for a build to complete, not only will it waste their time (which if you add those minutes up becomes a lot of time) but also breeds contempt towards WDPs. Basically they will associate longer build times with WDPs and then they will dislike them. This is the opposite of what you want. You want to encourage them to use WDPs. If the developer needs to build the WDP they can always right click on the WDP and then click build. But better would be to create a build script that can be run on you CI server as well as locally on the developers machine to perform a full build. I am not a fan of using solution files for CI builds, so I would say to create a "master" MSBuild file which uses the MSBuild task to build each project in the correct order and for every desired configuration. If you take this approach then you will never need a WDP to build in your solution. But if you want to use a solution file to build on your CI server then you have at least 2 options.

  • Create a new solution configuration, i.e. Deploy, which will build the WDPs and have the CI build that configuration
  • Have your CI build the solution file and when it completes, just build the WDP

From these two options I would pick 1, but best would be to create a build which didn't rely on solution files.

 

Sayed Ibrahim Hashimi

Friday, August 14, 2009 4:04:31 AM (GMT Daylight Time, UTC+01:00)  #    Comments [1]  | 
Thursday, August 13, 2009

I have seen a couple blog entries about executing MSTest unit tests from MSBuild. Most recently I saw the entry by Scott A. Lawrence. So I decided to share how I execute MSTest unit tests from MSBuild

I created a file named Build.Common.UnitTest.targets which contains all the behavior that will execute the test cases. This file can then be imported into whatever scripts that need to execute test cases. The entire file is shown below. We will discuss afterwards.

Build.Common.UnitTest.targets

<?xml version="1.0" encoding="utf-8"?>

<Project ToolsVersion="3.5" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

  <!-- =======================================================================

    TESTING

  ======================================================================= -->

 

  <!-- Default build settings go here -->

  <PropertyGroup>

    <RunMSTest Condition="'$(RunMSTest)'==''">true</RunMSTest>

    <BuildInParallel Condition="'$(BuildInParallel)'==''">true</BuildInParallel>

  </PropertyGroup>

 

  <Target Name="MSTestValidateSettings">

    <!-- Cleare out these items -->

    <ItemGroup>

      <_RequiredProperties Remove="@(_RequiredProperties)"/>

      <_RequiredItems Remove="@(_RequiredItems)"/>

    </ItemGroup>

   

    <ItemGroup>

      <_RequiredProperties Include="BuildContribRoot">

        <Value>$(BuildContribRoot)</Value>

      </_RequiredProperties>

      <_RequiredProperties Include="OutputRoot">

        <Value>$(OutputRoot)</Value>

      </_RequiredProperties>

 

      <_RequiredItems Include="MSTestProjects">

        <RequiredValue>@(MSTestProjects)</RequiredValue>

        <RequiredFilePath>%(MSTestProjects.FullPath)</RequiredFilePath>

      </_RequiredItems>

      <_RequiredItems Include="AllConfigurations">

        <RequiredValue>@(AllConfigurations)</RequiredValue>

      </_RequiredItems>

      <_RequiredItems Include="AllConfigurations.Configuration">

        <RequiredValue>%(AllConfigurations.Configuration)</RequiredValue>

      </_RequiredItems>

    </ItemGroup>

   

    <!-- Raise an error if any value in _RequiredProperties is missing -->

    <Error Condition="'%(_RequiredProperties.Value)'==''"

           Text="Missing required property [%(_RequiredProperties.Identity)]"/>

 

    <!-- Raise an error if any value in _RequiredItems is empty -->

    <Error Condition="'%(_RequiredItems.RequiredValue)'==''"

           Text="Missing required item value [%(_RequiredItems.Identity)]" />

 

    <!-- Validate any file/directory that should exist -->

    <Error Condition="'%(_RequiredItems.RequiredFilePath)' != '' and !Exists('%(_RequiredItems.RequiredFilePath)')"

           Text="Unable to find expeceted path [%(_RequiredItems.RequiredFilePath)] on item [%(_RequiredItems.Identity)]" />

  </Target>

 

  <UsingTask

      TaskName="TestToolsTask"

      AssemblyFile="$(BuildContribRoot)TestToolsTask-1.3\Microsoft.VisualStudio.QualityTools.MSBuildTasks.dll"/>

 

  <!-- TODO: Create a ValidateTestSettings target and put it on this list -->

  <PropertyGroup>

    <MSTestDependsOn>

      BuildMSTestProjects;

      BeforeMSTest;

      CoreMSTest;

      AfterMSTest

      $(MSTestDependsOn);

    </MSTestDependsOn>

  </PropertyGroup>

  <Target Name="MSTest" DependsOnTargets="$(MSTestDependsOn)"/>

  <Target Name="BeforeMSTest"/>

  <Target Name="AfterMSTest"/>

  <Target Name="CoreMSTest" Outputs="%(MSTestProjects.Identity)">

    <Message Text="Running MSTest for project [%(MSTestProjects.Identity)]"/>

    <Message Text="MSTestProjects.Directory: %(MSTestProjects.RootDir)%(MSTestProjects.Directory)" />

 

    <PropertyGroup>

      <_CurrentConfig>Debug</_CurrentConfig>

    </PropertyGroup>

 

    <PropertyGroup>

      <_SearchPath>$(OutputRoot)$(_CurrentConfig)\%(MSTestProjects.Filename)\</_SearchPath>

      <_TestContainers></_TestContainers>

    </PropertyGroup>

 

    <TestToolsTask

      SearchPathRoot="%(MSTestProjects.RootDir)%(MSTestProjects.Directory)"

      TestContainers="$(OutputRoot)$(_CurrentConfig)\%(MSTestProjects.Filename)\bin\%(MSTestProjects.Filename).dll"/>

 

    <!-- TODO: Read in the results of the tests and get all failures and report them here -->

  </Target>

 

  <PropertyGroup>

    <BuildMSTestProjectsDependsOn>

      BeforeBuildMSTestProjects;

      CoreBuildMSTestProjects;

      AfterBuildMSTestProjects;

      $(BuildMSTestProjectsDependsOn);

    </BuildMSTestProjectsDependsOn>

  </PropertyGroup>

  <Target Name="BuildMSTestProjects" DependsOnTargets="$(BuildMSTestProjectsDependsOn)"/>

  <Target Name="BeforeBuildMSTestProjects"/>

  <Target Name="AfterBuildMSTestProjects"/>

  <Target Name="CoreBuildMSTestProjects" Outputs="%(AllConfigurations.Configuration)"

          DependsOnTargets="CleanMSTest">   

    <!-- Make sure to do clean build in case test cases were added -->

   

    <PropertyGroup>

      <_CurrentConfig>%(AllConfigurations.Configuration)</_CurrentConfig>

    </PropertyGroup>

 

    <Message Text="Building (MSTestProjects.Identity) %(MSTestProjects.Identity)" Importance="high"/>

   

    <!-- Build the projects here. -->

    <MSBuild Projects="%(MSTestProjects.Identity)"

             Properties="Configuration=$(_CurrentConfig);

                          OutputPath=$(OutputRoot)$(_CurrentConfig)\%(MSTestProjects.Filename)\bin\;

                          BaseIntermediateOutputPath=$(OutputRoot)$(_CurrentConfig)\%(MSTestProjects.Filename)\obj\;

                          GenerateResourceNeverLockTypeAssemblies=true;

                          %(ProjectsToBuild.Properties);

                          $(AllProjectProperties);"

             BuildInParallel="$(BuildInParallel)"

             >

    </MSBuild>

  </Target>

 

 

  <Target Name="CleanMSTest">

 

    <MSBuild Projects="@(MSTestProjects)"

         Targets="Clean"

         Properties="Configuration=$(_CurrentConfig);

                          OutputPath=$(OutputRoot)$(_CurrentConfig)\%(MSTestProjects.Filename)\bin\;

                          BaseIntermediateOutputPath=$(OutputRoot)$(_CurrentConfig)\%(MSTestProjects.Filename)\obj\;

                          GenerateResourceNeverLockTypeAssemblies=true;"

         BuildInParallel="$(BuildInParallel)"

             />

   

  </Target>

 

</Project>

There are five important targets which are described below.

Name

Description

MSTestValidateSettings

This validates that the file was provided the needed data values to perform its task, to run the unit tests. For more info on this technique see my previous entry Elements of Reusable MSBuild Scripts: Validation.

MSTest

This is the target that you would execute to run the test cases. The target itself is empty but it sets up the chain of dependent targets.

CoreMSTest

This is the target which executes the test cases. This is preformed using the TestToolsTask.

BuildMSTestProjects

This target is responsible for building (i.e. compiling) the projects which contain the test cases. You don't have to call this it is called automagically.

CleanMSTest

This target will execute the Clean target for all the test projects defined.

 

If you take a look at the CoreBuildMSTestProjects target you can see that I am batching (Target batching to be specific) it on each defined configuration. This is achieved with the attribute Outputs="%(AllConfigurations.Configuration)". If you are not familiar with batching, see the links at the end of this post for more details, and you can always grab my book for even more detailed info J. Then inside that target I build each project by batching (Task batching) the MSBuild task on each project defined in the MSTestProjects item list.

Then inside the CoreMSTest target I execute the test cases. This target is batched for every value in the MSTestProjects item. As I'm writing this I have noticed that I've hard-coded the value for the configuration used in that target to be Debug with the statement

<PropertyGroup>

  <_CurrentConfig>Debug</_CurrentConfig>

</PropertyGroup>

This shouldn't be hard coded, but passed in. I will leave it as is for now though. Then the TestToolsTask is invoked to execute the test cases.

Now that we have written the re-usable .targets file to execute the test cases we need to create a file which will "feed" it the necessary data values and let it do its magic. I created a sample solution, which you can download at the end of this post, which demonstrates its usage. The solution is named MSTestExample and you can see the files it contains in the screen shot below.

Here I've highlighted the two MSTest projects as well as a couple build files. I've already shown the contents of the Build.Common.UnitTest.targets file. Here is the contents of the Build.MSTestExample.proj file.

Build.MSTestExample.proj

<?xml version="1.0" encoding="utf-8"?>

<Project ToolsVersion="3.5" DefaultTargets="MSTest" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

 

  <!--

  Some properties you may be interested in setting:

    Name              Description

    *****************************************************

    RunMSTest         true/false to run unit tests or not

  -->

 

  <PropertyGroup>

    <Root Condition="'$(Root)'==''">$(MSBuildProjectDirectory)\..\</Root>

 

    <BuildRoot Condition="'$(BuildRoot)'==''">$(Root)Build\</BuildRoot>

    <BuildContribRoot Condition="'$(BuildContribRoot)'==''">$(BuildRoot)Contrib\</BuildContribRoot>

 

    <SourceRoot Condition="'$(SourceRoot)'==''">$(Root)</SourceRoot>

    <BuildArtifactsRoot Condition="'$(BuildArtifactsRoot)'==''">$(BuildRoot)BuildAftifacts\</BuildArtifactsRoot>

    <OutputRoot Condition="'$(OutputRoot)'==''">$(BuildArtifactsRoot)Output\</OutputRoot>

    <TreatWarningsAsErrors Condition=" '$(TreatWarningsAsErrors)'=='' ">true</TreatWarningsAsErrors>

  </PropertyGroup>

 

  <PropertyGroup>

    <CodeAnalysisTreatWarningsAsErrors Condition="'$(CodeAnalysisTreatWarningsAsErrors)'==''">true</CodeAnalysisTreatWarningsAsErrors>

    <BuildInParallel Condition="'$(BuildInParallel)'==''">true</BuildInParallel>

   

    <RunCodeAnalysis Condition="'$(RunCodeAnalysis)'==''">true</RunCodeAnalysis>

    <RunMSTest Condition="'$(RunMSTest)'==''">false</RunMSTest>

    <RunStyleCop Condition="''=='$(RunStyleCop)'">true</RunStyleCop>

  </PropertyGroup>

 

  <!-- Configurations that we want to build for -->

  <ItemGroup>

    <AllConfigurations Include="Debug">

      <Configuration>Debug</Configuration>

    </AllConfigurations>

  </ItemGroup>

 

  <!-- =======================================================================

    MSTest

  ======================================================================= -->

 

  <ItemGroup>

    <MSTestProjects Include="$(SourceRoot)TestProject1\TestProject1.csproj">

    </MSTestProjects>

    <MSTestProjects Include="$(SourceRoot)TestProject2\TestProject2.csproj">

    </MSTestProjects>

  </ItemGroup>

 

  <Import Project="$(BuildRoot)Build.Common.UnitTest.targets"/>

 

  <!-- Inject the MSTest targets into the build -->

  <!--<PropertyGroup Condition="'$(RunMSTest)'=='true'">

    <BuildDependsOn>

      $(BuildDependsOn);

      MSTest;

    </BuildDependsOn>

    <BuildMSTestProjectsDependsOn>

      CoreBuild;

      $(BuildMSTestProjectsDependsOn)

    </BuildMSTestProjectsDependsOn>

  </PropertyGroup>-->

 

</Project>

This file is pretty simple. It just creates some properties, and items and then just imports the Build.Common.UnitTest.targets file to do all the heavy lifting. You will probably notice that there are some properties defined that don't make sense here, like CodeAnalysisTreatWarningsAsErrors, this is because this was taken from a build script which does some other tasks. You can ignore those. To see what properties/items are required for the MSTest just look at the MSTestValidateSettings target. Also in the previous code snippet I showed how you could inject the MSTest target into the build process but it is commented out since there is no real build process in this case.

One thing about this approach that may not be ideal is that it will execute the test cases in one assembly at a time and if there is a failure it will not go on to the other assemblies. In my case this is OK because this is for local builds, public builds are executing test cases by Team Build, but this can be looked at.

Questions? Comments?

 

MSTestExamples.zip

MSBuild Batching Links


Sayed Ibrahim Hashim

Thursday, August 13, 2009 5:11:52 AM (GMT Daylight Time, UTC+01:00)  #    Comments [1]  | 
Wednesday, August 05, 2009

A few weeks ago Carl Franklin interviewed me for his site dnrtv.com on the topic of MSBuild. I'm on the home page right now but the permalink is http://www.dnrtv.com/default.aspx?showNum=145. Check it out. I think we may do a couple other shows to cover a bit more detail; we just got started with MSBuild in that video.

Sayed Ibrahim Hashimi

dnrTV | msbuild | Video
Wednesday, August 05, 2009 4:27:35 AM (GMT Daylight Time, UTC+01:00)  #    Comments [2]  | 
Tuesday, July 07, 2009

I am now offering FREE MSBuild training / Team Build training. Here is the deal. For those who do not know me I am the lead author for Inside the Microsoft Build Engine: Using MSBuild and Team Foundation Build, co-author for Deploying .NET Applications: Learning MSBuild and ClickOnce and have written several related articles.

I am beginning a new program where I will personally come out to your site and provide your team (from as little as 1 or as many as 10) 8 hours of free MSBuild / Team Build training. Here are the conditions; the training will be performed on a Saturday and you will have to cover the cost of airfare, hotel and meals for the weekend. You will also need to purchase X number of copies of my book where X is the number of people being trained. The book will be used throughout the session. Also you would have to provide the facilities where the training would take place. This will be a personalized training session. Ahead of time you tell me what topics you want to be trained on, I will create the specific training materials and then train you on that. If your team is 100% new to MSBuild I can save them countless hours of learning and pain on their own. If you already have a knowledgeable group then I can take them to the next level by covering advanced topics and best practices. I want to be clear on this, you will not be paying me any fee for the training and I will personally be delivering the training.

I am making a very limited number of slots available for this extremely rare special training, so if you are interested you should contact me very soon, because this will not be available for long. Send me an email [sayed DOT hashimi AT gmail DOT com] stating a bit about your organization, where the training would take place, what weekends are good (at least 4), how many people you want to be trained, and the specific topics that you would like to cover.

Looking forward to hearing from you guys!

Sayed Ibrahim Hashimi

Tuesday, July 07, 2009 4:49:08 AM (GMT Daylight Time, UTC+01:00)  #    Comments [2]  | 
Monday, July 06, 2009

The past few days I was having some issues with my blogs hosting provider, everything is squared away and back to normal.

Sayed Ibrahim Hashimi

Monday, July 06, 2009 6:01:25 PM (GMT Daylight Time, UTC+01:00)  #    Comments [1]  | 
Tuesday, June 30, 2009

If you picked up a copy of my book then in Chapter 7 External Tools you will find my rules for Creating Reusable Build Elements. In case you don’t have it here are the rules that I’ve outlined.

1.        Needs to be self-contained

2.        Process needs to be transparent and extensible by the consumer

3.        Overridable behavior

4.        A contract should be defined and validated

I personally think that this section is one of the most important sections in the book. The ideas there are not just how to use MSBuild but more like best practice guidance on creating uber-reusable build scripts. It took me a while to come up with those particular rules and I am continuing to evolve them. I’m not going to cover these topics here because it would take up too much space and that information is already available to you. I have evolved my validation technique and would like to discuss that here. If you have a copy of the book then you know that I am a big fan of the pattern of splitting up data and behavior in build scripts. To expand on this consider the project files that are created by Visual Studio. Those project files just define a bunch of properties and items, i.e. data. Then another file is imported, in the case of C# projects Microsoft.CSharp.targets, which contains all the targets, i.e. behavior. By doing this you can re-use the logic contained in the .targets files. This is the best way that I’ve found to create good build scripts. I prefer this over using the MSBuild task to just build another project file. There are a lot of strange problems with that technique and it can be very confusing to debug, especially if you are building out a bunch of different files. I think it’s better to build up one build script (via the Import element ) and go with that. Anywayz, this is a topic for another day.

Today we will talk about how we can have an MSBuild script validate itself. From the samples provided with the book I deliver the nunit.targets file which contains a target, ValidateNUnitSettings, which is shown below. You can download these files at the very end of this post.

< Target Name = " ValidateNUnitSettings " >

  <!-- Validate assumptions that are contracted  -->

  < Message Text = " NUnitAssemblies: @(NUnitAssemblies) " Importance = " low " />

 

  < Error Condition = " '$(NUnitOutputDir)'=='' "

    Text = " NUnitOutputDir property not defined " />

 

  < Error Condition = " '@(NUnitAssemblies)'=='' "

    Text = " NUnitAssemblies not defined " />

  < Error Condition = " '%(NUnitAssemblies.ProjectName)'=='' "

    Text = " Atleast 1 item in NuitAssemblies doesn't have metadata 'ProjectName' defined. " />

  < Error Condition = " !Exists('%(NUnitAssemblies.FullPath)') "

    Text = " Couldn't locate assembly at: %(NUnitAssemblies.FullPath) " />   

</ Target >

So we can see that this target expects there to be one property, NUnitOutputDir, and an item, NUnitAssemblies, to be defined. If not then an error is raised. This target is placed on the dependency list for the UnitTest target so we know that it will be executed before that target. Since I wrote that section in the book I noticed myself “copying and pasting” these various validate targets and just changing the property and item names. Being a fan of the DRY principal this didn’t sit right with me. So I began to explore better options. This is what I’ve come up with. Imagine that you have a shared .targets file, Build.Common.targets, which just takes a bunch of project and runs the same build process on it. This is the file which contains the targets (behavior) and then you have another file, in this example YourProject.proj, which is the driver for the build process. This file mostly contains properties and item (data). Below is the YourProject.proj file.

YourProject.proj

<? xml version = " 1.0 " encoding = " utf-8 " ?>

< Project ToolsVersion = " 3.5 " DefaultTargets = " Build " xmlns = " http://schemas.microsoft.com/developer/msbuild/2003 " >

 

  < PropertyGroup >

    < Root Condition = " '$(Root)'=='' " > .\ </ Root >

 

    < BuildInstallRoot Condition = " '$(BuildInstallRoot)'=='' " > $(Root)\Build\ </ BuildInstallRoot >

 

    < SourceRoot Condition = " '$(SourceRoot)'=='' " > $(Root) </ SourceRoot >

    < OutputRoot Condition = " '$(OutputRoot)'=='' " ></ OutputRoot >

  </ PropertyGroup >

 

  <!-- Configurations that we want to build -->

  < ItemGroup >

    < AllConfigurations Include = " Debug " >

      < Configuration > Debug </ Configuration >

    </ AllConfigurations >

    < AllConfigurations Include = " Release " >

      < Configuration > Release </ Configuration >

    </ AllConfigurations >

  </ ItemGroup >

 

  <!-- Projects that we want to build -->

  < ItemGroup >

    < ProjectsToBuild Include = " $(SourceRoot)Sedo.ProjectOne.csproj " />

    < ProjectsToBuild Include = " $(SourceRoot)Sedo.ProjectTwo.csproj " />

  </ ItemGroup >

 

  < Import Project = " Build.Common.targets " />

 

</ Project >

Here are the contents of a very crude and simple Build.Common.targets.

Build.Common.targets

<? xml version = " 1.0 " encoding = " utf-8 " ?>

< Project ToolsVersion = " 3.5 " DefaultTargets = " Build " xmlns = " http://schemas.microsoft.com/developer/msbuild/2003 " >

  < Target Name = " ValidateBuildSettings " >

    < ItemGroup >

      < _RequiredProperties Include = " Root " >

        < Value > $(Root) </ Value >

      </ _RequiredProperties >

      < _RequiredProperties Include = " BuildInstallRoot " >

        < Value > $(BuildInstallRoot) </ Value >

      </ _RequiredProperties >

      < _RequiredProperties Include = " SourceRoot " >

        < Value > $(SourceRoot) </ Value >

      </ _RequiredProperties >

 

      <!--

      _RequiredItems is the item where required items should be placed.

      The following metadata is significant:

        REQUIRED METADATA:

        Identity          = This will basically be used to identify the specific required item

        RequiredValue     = This is the specific value that will be validated to exist

       

        OPTIONAL METADATA

        RequiredFilePath  = Populate this with a path that should exists, if it is not empty

                              then it will be checked to exist on disk.

      -->

 

      < _RequiredItems Include = " AllConfigurations " >

        < RequiredValue > @(AllConfigurations) </ RequiredValue >

      </ _RequiredItems >

      < _RequiredItems Include = " AllConfigurations.Configuration " >

        < RequiredValue > %(AllConfigurations.Configuration) </ RequiredValue >

      </ _RequiredItems >

      < _RequiredItems Include = " ProjectsToBuild " >

        < RequiredValue > %(ProjectsToBuild.Identity) </ RequiredValue >

        < RequiredFilePath > %(ProjectsToBuild.Identity) </ RequiredFilePath >

      </ _RequiredItems >

    </ ItemGroup >

 

 

    <!-- Raise an error if any value in _RequiredProperties is missing -->

    < Error Condition = " '%(_RequiredProperties.Value)'=='' "

           Text = " Missing required property [%(_RequiredProperties.Identity)] " />

 

    <!-- Raise an error if any value in _RequiredItems is empty -->

    < Error Condition = " '%(_RequiredItems.RequiredValue)'=='' "

           Text = " Missing required item value [%(_RequiredItems.Identity)] " />

 

    <!-- Validate any file/directory that should exist -->

    < Error Condition = " '%(_RequiredItems.RequiredFilePath)' != '' and !Exists('%(_RequiredItems.RequiredFilePath)') "

           Text = " Unable to find expeceted path [%(_RequiredItems.RequiredFilePath)] on item [%(_RequiredItems.Identity)] " />

  </ Target >

 

  < PropertyGroup >

    < BuildDependsOn >

      ValidateBuildSettings;

      BeforeBuild;

      CoreBuild;

      AfterBuild;

      $(BuildDependsOn)

    </ BuildDependsOn >

  </ PropertyGroup >

  < Target Name = " Build " DependsOnTargets = " $(BuildDependsOn) " />

  < Target Name = " BeforeBuild " />

  < Target Name = " AfterBuild " />

  < Target Name = " CoreBuild " Outputs = " %(AllConfigurations.Configuration) " >

    <!--

    Create a temporary property that contains the lone configuration. This is needed because we

    don't want to batch MSBuild task on both ProjectsToBuild and AllConfigurations at the same

    time. Anywayz since this target is batched we are guaranteed that

    it contains a single value within the scope of this target.

    -->

    < PropertyGroup >

      < CurrentConfig > %(AllConfigurations.Configuration) </ CurrentConfig >

    </ PropertyGroup >

   

    <!-- Build the projects here, for this example we just print a message -->

    < Message Text = " Building project [%(ProjectsToBuild.Identity)] for configuration [$(CurrentConfig)] " Importance = " high " />

   

  </ Target >

 

</ Project >

Focus your attention on the ValidateBuildSettings target. Instead of manually validating each property and item, I create an item _RequiredProperties for property validation and an item _RequiredItems for item validation. Notice the leading _ which says “Don’t touch me I’m private!” The target populates both of those items with the values that are required for the build script to be able to execute. Then using batching those assumptions are validated. The property validation is pretty easy, if any value for _RequiredProperties.Value is empty then raises an error. For items I wanted to be able to not only be able to assert the following

1.        The item was defined

2.        Certain metadata values were defined

3.        The file exists on disk

So I came up with the concept of having three metadata values on that item

·          Identity

·          RequiredValue

·          RequiredFilePath

Identity, this is just the contents of the Include attribute on the item itself. The RequiredValue is the metadata that will be checked to ensure that a value exists. So if you want to make sure that an item was simply declared then you would do

< _RequiredItems Include = " AllConfigurations " >

  < RequiredValue > @(AllConfigurations) </ RequiredValue >

</ _RequiredItems >

If the AllConfigurations item wasn’t declared then @(AllConfigurations) would evaluate to empty and an error would be raised. You can also do this to assert an items metadata like %(AllConfigurations.Configuration) . And for RequiredFilePath, if that metadata value was defined then the validation target will make sure that the file is located on disk as expected.

You have to keep in mind that this is not just about validation. It’s also about usability. You may be wondering when I say that. But think about it, in one target I have been able to express to you (the person consuming the .targets file) everything that you need to define in great detail. And if you get it wrong then the process will stop itself, instead of potentially continuing in an erroneous scenario.

Here is what the result would look like if I had forgotten to define the AllConfigurations item.

And here it is after I inserted it.

This was one of my longer posts in a while, but I think that there is much more to this topic. I think this goes pretty deep, but if you understand these ideas then you are well on your way to writing some sweet reusable build scripts.

 

Build.Common.targets

YourProject.proj

Sayed Ibrahim Hashimi

Tuesday, June 30, 2009 4:33:13 AM (GMT Daylight Time, UTC+01:00)  #    Comments [0]  | 
Monday, June 29, 2009

The other day Dan Moseley from the MSBuild team wrote up a review on the books Amazon page. Here are the contents of the review

I'm a developer on MSBuild; Sayed wrote this book with our encouragement, and we reviewed it for accuracy and completeness, so I can recommend it. The documentation for MSBuild in 2.0 and 3.5 was not great; I consider this something like the missing manual. Unfortunately there aren't many other MSBuild books; fortunately Sayed did a good job on this one.

We're fixing a lot of what's "missing" in MSBuild in the upcoming version 4.0 -- I hope Sayed can do a 2nd edition when that comes out. Plus, our docs should be better then :-)

I'm glad to say that this review was posted as 5 out of 5 and that is the 9th review (out of 9) which has been given 5 stars. When we wrote the book I knew that we had put something together that would really meet a specific need. I'm happy to see that the book has been accepted soo well by everyone and I hope that we are able to write a second edition as Dan mentioned.

Sayed Ibrahim Hashimi

book | msbuild | review
Monday, June 29, 2009 4:31:19 AM (GMT Daylight Time, UTC+01:00)  #    Comments [0]  | 

Theme design by Jelle Druyts