Custom routing for ASP .NET MVC

May 26th, 2008

Custom routing for ASP .NET MVC

Those familiar with the MVC framework for ASP .NET will know that one of its primary features is the mapping of URLs to methods on controllers. For example, /Products/Find will cause the ProductsController to be created and have its Find method invoked. It is also possible to pass arguments to methods, for instance /Products/Load/53 would call the Load method of the ProductsController, supplying 53 as the argument.

Organising controllers

Whilst this allows the developer to structure their code better, keeping presentational logic in the view and application logic in the controller, it isn’t ideal. To continue the example, as the project grows it will provide an increasing amount of features related to products, all of which will be delivered by the ProductsController. As a result the code for searching for products will end up in the same class as that used to edit products, and so on.

Everyone has their own take on the MVC pattern and in the past I have tended to use one controller per use case. The use cases in question here are Find Product and Edit Product and as such their functionality would be provided by the FindProductController and EditProductController, rather than living together in a single ProductsController.

A simple way to implement this pattern is to keep the ProductsController and have it delegate all its work. For example, the Load method would simply create an instance of EditProductController and call its Load method, passing any arguments as well. Whilst this is feasible it is more of a workaround than a genuine solution. It would be far better to cut out the ProductsController altogether, and have methods on the two controllers be called directly. The routing engine in ASP .NET MVC is very flexible and, by developing a custom route, it is possible to do this.

Creating a custom route

It is the job of a route to take a URL and call the appropriate method of a controller. The default MVC route has a format of {controller}/{action}/{id}, however our use case route will use {useCaseNoun}/{useCaseVerb}/{action}/{id}. The key difference is that the controller token has been replaced with two new tokens, noun and verb. This will allow us to provide the following routes

URL Controller Action Behaviour
/Product/Find/Search FindProductController Search Execute a search for products and display the results
/Product/Find/Clear FindProductController Clear Reset all the fields of the search page
/Product/Find FindProductController [default] Execute the default controller method (more on this shortly)
/Product/Edit/Load/17 EditProductController Load(17) Load the product with ID 17 and display its data for editing
/Product/Edit/Save/23 EditProductController Save(23) Save the supplied data against the product with ID 23
/Product/Edit/Clear EditProductController Clear Clear out the edit page ready for entering a new product

A fringe benefit of adopting this strategy is that both controllers can have a method of the same name, e.g. Clear, but have the method perform a completely different task. With a single ProductsController there could only be one Clear method.

In order to register the custom route with the MVC framework, some changes need to be made to the Global.asax file. Its Application_Start method calls RegisterRoutes which, using the default MVC project template, will already set up the default route format of {controller}/{action}/{id}. To this method we need to add the following

routes.Add(new Route(“{useCaseNoun}/{useCaseVerb}/{action}/{id}”, new MvcRouteHandler())
{
Defaults = new RouteValueDictionary(new { action = “Index”, id = “” }),
});

Note that the default action is Index so, in the case of the /Product/Find URL in the table above, this would map to the Index method of the FindProductController.

At this point we can use Phil Haack’s Url Routing Debugger to test that our URLs are being correctly routed. To do so we get a reference to Phil’s RouteDebug.dll and add the following code after RegisterRoutes is called

RouteDebug.RouteDebugger.RewriteRoutesForTesting(RouteTable.Routes);

It is then possible to enter each of URLs into a browser and see which route they match. Check out Phil’s post for further details.

Creating a handler for the route

The format of our route is such that the {controller} token is no longer present. As a result the MvcRouteHandler that is associated with the route will not be able to identify which controller to use. Typically it just extracts the value of the controller token, appends “Controller” to it, and instantiates an object of that type. To resolve this issue we need to replace MvcRouteHandler with a route handler of our own.

Fredrik Normén produced an excellent blog post, Create your own IRouteHandler, which describes how to do this. For our route, we need to create two new classes, the first of which implements IRouteHandler, as shown below

public class UseCaseRouteHandler : IRouteHandler
{
    public IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        return new UseCaseMvcHandler(requestContext);
    }
}

This class, UseCaseRouteHandler, is used in place of MvcRouteHandler, and simply creates a new IHttpHandler which will do the real work. The implementation of IHttpHandler is actually our second class, UseCaseMvcHandler. This inherits from MvcHandler and overrides the ProcessRequest method, during which the correct controller is identified and then created. It is this behaviour that we need to redefine.

To determine how our ProcessRequest should work, I downloaded the source code of the MVC framework itself, which is available from CodePlex. A quick inspection of MvcHandler’s ProcessRequest shows that the GetRequiredString method is used to extract the values of the route’s tokens. For the default routing this is just a case of getting the controller name, whereas our custom route needs to grab both the {useCaseNoun} and {useCaseVerb} tokens. I moved this logic into a separate function, GetControllerName, which is shown below

private string GetControllerName()
{
    string noun = this.RequestContext.RouteData.GetRequiredString(“useCaseNoun”);
    string verb = this.RequestContext.RouteData.GetRequiredString(“useCaseVerb”);
    return verb + noun;
}

So, if the URL is /Product/Find/Search, this method will extract a noun of “Product”, a verb of “Find” and return the value “FindProduct”.

I then copied MvcHandler’s ProcessRequest code into UseCaseMvcHandler and replaced the line extracting the controller token value with a call to the GetControllerName function. Simple. Well, almost! Unfortunately the resource strings are not available to inheriting classes, and neither is the ControllerBuilder property. I replaced the former with a hard-wired string, whilst the latter is accessible via the ControllerBuilder class’ static Current property.

At this point the code is almost ready to run. We just need to adjust the code in RegisterRoutes so that our route uses the new UseCaseRouteHandler class. This is done as follows

routes.Add(new Route(“{useCaseNoun}/{useCaseVerb}/{action}/{id}”, new UseCaseRouteHandler())
{
Defaults = new RouteValueDictionary(new { action = “Index”, id = “” }),
});

Identifying which view to show

Having commented out the call to the routing debugger, I then browsed to /Product/Edit/Load/17 and…BANG! An exception with the message,

The RouteData must contain an item named ‘controller’ with a non-empty string value

was shown. After some digging through the MVC source, it seems that the code responsible for identifying which view to create (the ViewEngine class does this) was also trying to find a controller token in the URL, in order to work out which subfolder of Views to look in. The Load method of EditProductController calls RenderView, passing “Edit” as the viewName argument. By altering this to “~/Views/Product/Edit.aspx” I was able to work around this issue.

This was a far from satisfactory solution however. Fully-qualifying all of the view names is a potential maintenance problem in the future, if views are moved or folders renamed. To combat this I introduced a UseCaseControllerBase class, from which EditProductController and FindProductController now inherit. This class overrides RenderView and works out the full path to the view. The following code shows how

public abstract class UseCaseControllerBase : Controller
{
    protected override void RenderView(string viewName, string masterName, object viewData)
    {
        string noun = this.RouteData.GetRequiredString(“useCaseNoun”);
        string fullViewName = string.Format(“~/Views/{0}/{1}.aspx”, noun, viewName);
        base.RenderView(fullViewName, masterName, viewData);
    }
}

The ideal resolution would be to customise the behaviour of the ViewEngine, however that is beyond the scope of this article.

This post demonstrates the flexibility of the routing subsystem provided by ASP .NET. It also shows how to improve the separation of functionality between controllers. If you are interested the sample code is available from CodePlex.

Technorati tags:

How to publish a web site with MSBuild

May 18th, 2008

How to publish a web site with MSBuild

In part two of my MSBuild tutorial I needed to find a way to call Visual Studio’s Publish Web Site feature from MSBuild. Much trawling of the interweb failed to find anything of use, so in the end I had to produce my own target which copied the relevant files into the output folder.

The problem with this approach is that it only works for files of type dll, aspx or config. It is a simple task to add an extension, for example png, however on larger projects this becomes impractical. Developers would have to check the build script each time they added or removed a file just in case the Publish target needed to be updated. These are just the sort of jobs that get forgotten, which can lead to invalid builds later on.

Fortunately I came across a post on Mirosław Jedkynak’s blog showing how to use MSBuild to publish a web site. As some of you may be aware, a project file (vbproj or, in the case of my demo, csproj) contains targets of its own, as well as importing targets from other files that come as part of a Visual Studio installation. One such file is Microsoft.WebApplication.targets. This file provides the _CopyWebApplication target which will effectively replace my home-brewed Publish target.

In order to make use of this target we need to pass it two properties, WebProjectOutputDir and OutDir, which will ensure that the files get published into the correct folder. Here is an example

<Target Name=”Publish”>
  <RemoveDir Directories=”$(OutputFolder)”
             ContinueOnError=”true” />
  <MSBuild Projects=”BuildDemoSite.csproj”
           Targets=”ResolveReferences;_CopyWebApplication”
           Properties=”WebProjectOutputDir=$(OutputFolder);
           OutDir=$(WebProjectOutputDir)\” />
</Target>

As you can see the ResolveReferences target is also called, this ensures that any third party dependencies are copied over as well.

Integrating this into my demo build script was simple, however I noticed that some files were being copied over that I didn’t want. These were the build script itself, and the environment-specific config files. This is because their build action was set to Content. Once I had switched it to None and ran the script again, everything was fine. The build action can be set by right-clicking on a file in the Solution Explorer and selecting Properties. Build Action is the first item in the list.

I have posted a new version of the build script to CodePlex for those interested in taking a look.

Technorati tags:

A custom MSBuild task for merging config files

May 11th, 2008

A custom MSBuild task for merging config files

In the last part of my MSBuild tutorial I mentioned that the target for merging config files was less than ideal. Although we were able to use the XmlRead and XmlUpdate tasks to make life easier, the list of settings to merge still needed to be maintained in the build script.

If a developer were to add a setting to, or remove a setting from, the web.config file, they would need to mirror this change in the build script. This is exactly the type of job which is easily forgotten, leading to problems further down the line.

Ideally the list of settings to merge would be self-maintaining, i.e. any settings found in the environment-specific config file would simply be enumerated and copied into web.config, rather than being explicitly listed in the build script. I decided to create my own MSBuild task for doing this. The spec for it is as follows

  • Copy over the value of the debug attribute under the compilation node
  • Copy over the value of the mode attribute under the customErrors node
  • Enumerate each of the settings under the appSettings node and copy them over too

It has been developed to copy a setting over only if it exists in both the source and target files. As such it will not add settings missing from the target file or remove settings that are not in the source file.

It is very simple to create your own MSBuild task, just follow these steps

  • Create a class which inherits from Task
  • Add properties for storing the arguments the task uses. Apply the Required attribute to make the argument mandatory
  • Use the LogMessage method to show progress and any errors in the command window
  • Override the Execute method, returning a flag to indicate success or failure

There are plenty of examples to follow in the Community Tasks source code and a more detailed article can be found on MSDN. It is also possible to debug a task, check out the MSBuild Team Blog for an explanation.

Consuming the custom task from a build script

Having produced the task, called MergeConfig, the next step was to integrate it with the sample solution created back in part one of the tutorial. This is done by adding this statement to the build script

<UsingTask TaskName=”MergeConfig” AssemblyFile=”MergeConfigTask.dll” />

This piece of XML tells MSBuild that there is task called MergeConfig in the MergeConfigTask.dll. Note that the path to the dll is relative to the build script’s directory. Having done this we can add a target which will then call the new MergeConfig task, like so

<Target Name=”MergeConfig”>
  <MergeConfig SourceConfigFilename=”$(Environment).config”
               TargetConfigFilename=”$(OutputFolder)\Web.config” />
</Target>

In addition to achieving the main goal, which was to have a self-maintaining list of settings to be merged, the use of this custom task has reduced the size of the build script, by around thirty lines. If you would like to make use of this task in your own script it can be downloaded from CodePlex. I have also updated the sample project to illustrate how the task is used.

Technorati tags: ,

Automating the build with MSBuild (part three)

May 5th, 2008

Automating the build with MSBuild

Welcome to part three of the MSBuild tutorial; please take a look at the previous parts if you haven’t already.

At this point there are a couple of minor issues with our build script that need to be resolved before pressing on with any new targets.

Usability

The first of these is usability. Right now there is no easy way to run all of the targets in one go. This is not very helpful if someone wants to run the full build process from start to finish. To remedy this, I’ve introduced a new target called Run, below

<Target Name=”Run”>
  <CallTarget Targets=”Compile” />
  <CallTarget Targets=”Publish” />
  <CallTarget Targets=”SetConfig” />
</Target>

This simply uses the CallTarget task to hand the work off to the targets we created in the last two posts. Notice that it isn’t necessary to call Clean and GetConfig as they are dependencies of Compile and SetConfig respectively. MSBuild will automatically run them, in the correct order.

The finishing touch is applied by adding the DefaultTargets attribute to the root Project node, with a value of Run. This tells MSBuild to call the Run target if no targets are explicitly passed in from the command line. Having added the attribute, the whole script can be executed with this simple command

msbuild Build.xml

As you can see the /t switch has gone. Much nicer!

DRYing out the XML

Our second issue involves refactoring. It is important to treat our build script just like any other code and apply good practices to it. Some repetition has crept in, which violates the DRY principle, specifically

  • Each XPath to a config setting occurs twice
  • The Output folder is referred to numerous times

The best solution is to extract these strings into properties and then use the $(PropertyName) syntax to refer to them. It doesn’t take long and makes the script much easier to maintain in future. You can see the end result here.

Deploying the web site

Now the housekeeping is out of the way, we can start work on the last target, which is to deploy the web site to the correct location and configure IIS accordingly. This means copying the contents of the Output folder to another computer and either adding or updating a virtual directory to run the site.

The first part is pretty simple, and just a case of using the RemoveDir and Copy tasks in a similar way to the Publish target in part two. The following XML illustrates this

<Target Name=”Deploy”>
  <RemoveDir Directories=”$(DeploymentFolder)”
             ContinueOnError=”true” />
  <ItemGroup>
    <DeploymentFiles Include=”$(OutputFolder)\**\*.*” />
  </ItemGroup>
  <Copy SourceFiles=”@(DeploymentFiles)”
        DestinationFolder=”$(DeploymentFolder)\%(RecursiveDir)” />
</Target>

Nothing new to see there, so we’ll move on quickly to configuring the IIS virtual directory. There is no built-in task to do this, however the MSBuild Community Tasks Project comes to the rescue again with the WebDirectoryDelete and WebDirectoryCreate tasks. It is important to recreate the virtual directory each time to ensure we have a clean deployment. This is accomplished by adding the following XML to our Deploy target

<WebDirectoryDelete VirtualDirectoryName=”$(VirtualDirectory)”
                    ContinueOnError=”true” />
<WebDirectoryCreate VirtualDirectoryName=”$(VirtualDirectory)”
                    VirtualDirectoryPhysicalPath=”$(DeploymentFolder)” />

Something worth noting at this point is that this only works for the local machine. If you are deploying to another box, which is highly likely, then you would need to supply the ServerName, Username and Password attributes as well.

The final piece of the puzzle is to amend the Run target to ensure it calls our new Deploy target at the end. At this point it is possible to run the entire script and get a newly built and deployed web site in a matter of seconds. Very useful indeed.

Environment-specific deployment

At this point we need to revisit the Environment property which was defined back in part two. This is still hard-wired to Test, as are the new DeploymentFolder and VirtualDirectory properties. As a consequence, users of the build script will have to edit it if they want to build for, say, a live environment.

To prevent this we can introduce a PropertyGroup for each of the environments, and move the DeploymentFolder and VirtualDirectory properties into each of them. Then, by adding the Condition attribute to each group, it is possible to test which environment is being used and bring its PropertyGroup into play. The following snippet shows how to do so

<PropertyGroup Condition=”$(Environment) == ‘Test’”>
  <DeploymentFolder>C:\Temp\BuildDemoSite\Test</DeploymentFolder>
  <VirtualDirectory>BuildDemoTest</VirtualDirectory>
</PropertyGroup>

<PropertyGroup Condition=”$(Environment) == ‘Live’”>
  <DeploymentFolder>C:\Temp\BuildDemoSite\Live</DeploymentFolder>
  <VirtualDirectory>BuildDemoLive</VirtualDirectory>
</PropertyGroup>

As you can see, the value of the Condition attribute is an expression that evaluates to true or false. In our case, we test the value of the Environment setting. In future, if further deployment environments are required, it is simple to add a new PropertyGroup.

Despite these changes, we still have that pesky Environment property - let’s delete it. Instead, it can specified via the command line when the script is run, as follows

msbuild Build.xml /p:Environment=Live

By using the /p switch it is possible to specify a value for the property, in a similar way to calling a function and passing in arguments. However, there is always a chance that someone may forget to do this, in which case you may wish to restore the Environment property and have it act as a default.

Summary

That’s the end of the tutorial. Although the final script (available on CodePlex) achieves all of the aims set out in part one there are still a couple of areas for improvement, specifically

  • There is no MSBuild task which provides the same behaviour as Visual Studio’s Publish feature
  • A certain amount of repetition remains when merging config settings. Ideally all settings would be merged automatically, perhaps via a custom MSBuild task (something for another post perhaps)

Overall however the end result is still very useful, and only scratches the surface of what MSBuild can do. If you are interested in learning more, the following links may be of use

All of the source code for the tutorial is available from CodePlex. I hope you’ve found it useful and welcome any feedback.

Technorati tags:

Automating the build with MSBuild (part two)

April 20th, 2008

Automating the build with MSBuild

If you missed part one please check it out and then come back here. You can also download the sample code from CodePlex.

Our next job is to get MSBuild to copy the files needed for deployment into a new directory. This is effectively the same as using the Publish Web Site option in Visual Studio. However, I couldn’t find any way to do that via MSBuild - if anyone knows how, please leave a comment. UPDATE I found a way to do this in the end, check out the post How to publish a web site with MSBuild.

Instead I’ve used the Copy task, and defined a couple of ItemGroups, one for the aspx pages and another for the contents of the bin folder. The XML below shows this.

<Target Name=”Publish”>
  <RemoveDir Directories=”Output” ContinueOnError=”true” />
  <ItemGroup>
    <Pages Include=”**\*.aspx” />
    <Binaries Include=”bin\*.dll” />
  </ItemGroup>
  <Copy SourceFiles =”@(Pages)” DestinationFolder=”Output\%(RecursiveDir)” />
  <Copy SourceFiles =”@(Binaries)” DestinationFolder=”Output\bin” />
  <Copy SourceFiles =”Web.config” DestinationFolder=”Output” />
</Target>

Along with the web.config file, these files are all that are needed for our simple website to run.

This new target introduces some more features of MSBuild. First off the RemoveDir task runs with the ContinueOnError attribute set to true. The effect of this is that, should we try to remove a directory that does not exist, the resulting error will not stop the script. This attribute is actually available for all tasks and defaults to false.

When specifying the Pages ItemGroup, using “**\*.aspx” allows us to pick up all aspx files in the project, rather than just those in the root. This is important as our web site has an Admin folder containing an aspx.

Having defined the ItemGroups it is quite simple to use the Copy task to place them in the Output folder. We do not need to explicitly create the folder either - the act of copying into it will do so.

In order to preserve the directory structure of the web site, the pages are copied using the %(RecursiveDir) syntax. Had this not been used then all pages would have been placed in the root of the Output folder.

To give the new target a quick spin, execute the following command

msbuild Build.xml /t:Publish

and you should find the Output folder populated with the binaries, aspx pages and the web.config. The site is almost ready to be deployed, however we still need to copy the environment-specific settings over. Let’s tackle that now.

As you may recall from part one, in addition to the web.config file, there are live.config and test.config files. Whilst web.config contains all settings for the web site, live.config and test.config contain only those settings that are different for that environment. It is then the job of the build script to copy the correct values over.

To achieve our goal, the general plan is to read settings out of one file, using an XPath query, and then write them back to another file, using the same XPath. A quick scan of the built-in tasks didn’t find anything useful, however the MSBuild Community Tasks Project came to the rescue. Having downloaded and installed the extra tasks they provide, we now have access to XmlRead and XmlUpdate tasks. Excellent!

Before diving in and using them, make sure the following XML is inserted below the root Project node

<Import Project=”C:\Program Files\MSBuild\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets”/>

This ensures that MSBuild knows about the extra tasks. We also need some intermediary variables to store the settings between reading and writing. MSBuild allows a script to declare a PropertyGroup, which can then contain a bunch of properties. The following XML illustrates this

<PropertyGroup>
  <Environment>Test</Environment>
  <CompilationDebug />
  <CustomErrorsMode />
  <ContentEditorsEmail />
  <AdministratorsEmail />
</PropertyGroup>

Note that a property has also been added for indicating which environment we are working with. At the moment this is hard-wired as Test but that will change later on.

The first step is to get those properties populated correctly, using the XmlRead task. This is pretty simple as can be seen below

<Target Name=”GetConfig”>
  <XmlRead XPath=”configuration/system.web/compilation/@debug”
           XmlFileName=”$(Environment).config”>
    <Output TaskParameter=”Value” PropertyName=”CompilationDebug” />
  </XmlRead>
</Target>

I’ve only shown one instance of XmlRead here, to avoid repetition. The full script contains all four. The points to make here are

  • A GetConfig target has been declared to keep all our XmlRead tasks together
  • The XmlFileName attribute has a dynamic value. Using MSBuild’s $(PropertyName) syntax it is easy to point at the correct config file dependant on the value of the Environment property
  • The Output tag is used in order to store the return value of XmlRead in the CompilationDebug property

We’re almost there now. As you may have guessed, the syntax for XmlUpdate is very similar. Again, it makes sense to group all four updates into a SetConfig target, as follows

<Target Name=”SetConfig” DependsOnTargets=”GetConfig”>
  <XmlUpdate XPath=”configuration/system.web/compilation/@debug”
             XmlFileName=”Output\web.config”
             Value=”$(CompilationDebug)” />
</Target>

The same XPath is used to find the target node in the web.config file and the Value attribute is set using the $(PropertyName) substitution technique. I also added DependsOnTargets=”GetConfig” to enforce that settings are read before they are written.

Phew! OK, we can now run our shiny new target by switching back to the command window and executing this statement

msbuild Build.xml /t:SetConfig

To test everything has worked, open up the web.config file in the Output folder and you will find that its values match those in Test.config.

At this point the script is in need of a little refactoring. It is also awkward to run all of the targets in one go. In the next part we will resolve these issues and create the deployment target. Until then please feel free to provide feedback in the comments. You can also download the script in its current state from CodePlex.

Technorati tags:

Automating the build with MSBuild

April 15th, 2008

Automating the build with MSBuild

One of the less well known tools that comes with the .NET framework is MSBuild. This command line utility runs XML scripts which can automate the build of a software project.

The question is, “why would I want to automate my build?”. Well, all but the most trivial of projects are likely to contain a number of build steps, for example

  • Incrementing the version number
  • Compiling source code
  • Choosing the correct settings for the deployment environment (e.g. test, live)
  • Including third party dependencies in the install package

As such, producing the build by hand can be a time-consuming and error-prone process. The initial effort invested in creating a build script will be more than offset by that gained from automation. This is especially true if the manual process often went wrong and either had to be repeated or fixed.

OK, at this point hopefully you are sold on the benefits of build automation and are champing at the bit to write your first script. Let’s take a look at our sample project, which is an ASP .NET web site.

Project structure

Notice that, in addition to the web.config file, there are live.config and test.config files. The idea here is that whilst web.config contains all settings for the web site, live.config and test.config contain only those settings that are different for that deployment environment. This avoids repeating settings in each file, which can turn into a maintenance headache. However it does present a challenge in that we will need to merge the environment-specific settings into the web.config when performing the build. More on that later.

Before we start, here are the steps that constitute the build for this project

  • Clean the bin directory
  • Compile the project
  • Copy all files required for deployment into a new build output directory
  • Merge config settings
  • Deploy the build output to the correct environment

Each project will have its own build process, however for this purposes of this demo ours is fairly basic.

Right then, let’s get started with the tutorial. If you would like to follow along, the sample project is available from CodePlex. The first step is to add a new XML file to the root of the site, called Build.xml. All MSBuild scripts should contain a root Project node at the top. Having added that, our next job is to clean the bin directory, ready for another compilation to be performed. That leaves our script looking like this

<Project xmlns=”http://schemas.microsoft.com/developer/msbuild/2003″>

  <Target Name=”Clean”>
    <ItemGroup>
      <BinFiles Include=”bin\*.*” />
    </ItemGroup>
    <Delete Files=”@(BinFiles)” />
  </Target>

</Project>

There are a number of points to make here

  • Including a namespace allows Visual Studio to provide IntelliSense for all MSBuild features
  • MSBuild scripts are largely made up of targets, which contain one or more tasks. In this case our target is called Clean and it uses the Delete task
  • ItemGroups, surprisingly, are used to define a group of items. In our case this is a list of all files in the bin folder
  • All paths, e.g. bin\*.*, are relative to the build script itself. It is a good idea to use relative paths in case you try to run the script from a different location in the future

I have to admit that I was initially unaware of the need for ItemGroup, instead expecting to simply pass “bin\*.*” to the Delete task. A post on the MSBuild Team Blog explains why.

OK, having coded our first target, let’s run it. Open a command prompt and switch directories into the folder containing your build script. Then execute this statement

msbuild Build.xml /t:Clean

If you receive an error stating that MSBuild could not be found, you will have to either fully qualify the path to it (in my case C:\WINDOWS\Microsoft.NET\Framework\v3.5), or add its location to your PATH environment variable. I recommend the latter, instructions on which can be found here.

Having run the command the bin folder should be empty. Make sure something was actually in there in the first place before giving yourself a pat on the back! As you can see, the syntax for running MSBuild is to provide the name of the build script, then the t switch with the name of the target to run.

Moving swiftly on, our next step is to compile the project. To do so we create a target called Compile, and use the MSBuild task to perform the compilation, passing it the web site’s project filename, as follows

<Target Name=”Compile” DependsOnTargets=”Clean”>
  <MSBuild Projects=”BuildDemoSite.csproj” />
</Target>

It seems a little confusing that Microsoft decided to have a task called “MSBuild”, but there you go. I’ve introduced the DependsOnTargets attribute here, which forces Clean to run before Compile does. This ensures we can’t forget to to clean the bin folder, and in fact removes the need to run Clean separately. Instead we can just call Compile, as follows

msbuild Build.xml /t:Compile

Reviewing the messages output to the command window confirms that Clean ran before Compile did.

OK, that’s it for this post. So far we have

  • Established what a build consists of
  • Decided to automate it
  • Created targets for cleaning the bin folder and compiling the project

You can download the script in its current state from CodePlex.

In part 2 we’ll create the remaining targets and refactor our script. Stay tuned and, in the meantime, please leave feedback in the comments.

Technorati tags: