Is there any easy way to see the underlying MSBuild command when building in VS2010? Want to see the MSDeploy params. @wdeploy?
This is actually pretty easy, but wouldn’t fit into 140 characters, so I decided to blog it.
One thing to know is that when you publish from Visual Studio, by default we use the MSDeploy (AKA Web Deployment Tool) Object Model in order to perform the deployment. We do this for performance and other reasons. Because of this there is no real msdeploy.exe command that is being issued. You can however change that behavior. This is controlled by an MSBuild property UseMSDeployExe which is false by default. In this case since Troy wants to see the command we will need to set that property to false. There are 2 ways in which you can do this. You can set it in the project file itself, or you can define it in a .wpp.targets file. I would recommend the second approach. What you need to do is to create a file with the name {ProjectName}.wpp.targets in the same directory as the project where {ProjectName} is the name of the Web Application Project (WAP). When you do this, during a build or publish the file is automatically imported into the build process. In my example I have a WAP named WebApplication1.csproj, so I created the file WebApplication1.wpp.targets and its contents are shown below.
In the snippet above you can see that I defined the property to true. Now there is one more thing to do, publish the project. Once you publish the project, in the output window you will see the MSDeploy command which is being used. In my case I published the project to localhost to the Default Web Site/Test01 application path. You may have to copy the text from the output window into Notepad and search for msdeploy.exe. The command that was issued in my case is shown below (with formatting changes for readability).
"C:\Program Files (x86)\IIS\Microsoft Web Deploy\msdeploy.exe"
-source:manifest='C:\temp\_NET\ThrowAway\WebApplication3\WebApplication1\obj\Debug\Package\WebApplication1.SourceManifest.xml'
-dest:auto,IncludeAcls='False',AuthType='NTLM'
-verb:sync
-enableRule:DoNotDeleteRule
-disableLink:AppPoolExtension
-disableLink:ContentExtension
-disableLink:CertificateExtension
-setParam:kind='ProviderPath',
scope='IisApp',match='^C:\\temp\\_NET\\ThrowAway\\WebApplication3\\WebApplication1\\obj\\Debug\\Package\\PackageTmp$',
value='Default Web Site/Test01'
-setParam:kind='ProviderPath',
scope='setAcl',
match='^C:\\temp\\_NET\\ThrowAway\\WebApplication3\\WebApplication1\\obj\\Debug\\Package\\PackageTmp$',
value='Default Web Site/Test01'
-retryAttempts=2
So that’s it, pretty simple.
FYI, if you want more detail you can increase the MSBuild Output Window verbosity by going to Tools->Options->Projects and Solutions->Build and Run then specifying a different value for MSBuild project build output verbosity.
One of the most commonly asked questions that I get is “How can I extend the build process for my solution?” In fact I was asked this soo much that 4 years ago (wow I’ve been dealing with MSBuild for far too long:) ) I wrote a blog post on Use MSBuild to build Solution files (no longer recommended technique) and that post to this day is one of the most frequently visited ones. Well in the 4+ years that have passed I’m glad to say that things have changed. The problem is that almost no one knows about it. Hopefully this blog post will be the start to that changing.
What’s under the covers
In all versions of Visual Studio, the solution file is not an MSBuild file. Which sucks for many of us. Essentially the solution build was like a black box.
Even in VS2010 it still kinda is, but there is a hook for us to extend it built in. Let me explain what happens when you build a solution using msbuild.exe. When you build a solution using msbuild.exe the solution file is converted to an MSBuild file in memory then that MSBuild file is used to build the solution itself. When you build from the command line here is the rough outline of the MSBuild project that gets built.
OK here I have left out the targets and properties because they are irrelevant to this discussion. Take a look at these import statements. There are two pairs of import statements ones that are imported before the build is defined and the imports which are after the build definition. Instead of looking at the first import statement lets look at the second one, because I think its more compelling and its easier to demo.
This statement is pretty simple, if a file with the name of before.ExtendSolutionBuild.sln.targets exists in the same directory as the ExtendSolutionBuild.sln file then it will be imported in at the top of the build file. Also there is a corresponding entry for after.ExtendSolutionBuild.sln.targets. In stead of getting into all the nitty gritty of why two exists let me just boil it down. You should place properties/items inside of the before targets file, and inside of the after .targets file you should place targets but its OK to put properties/items here as well. The most important thing to keep a note of is that you cannot place anything inside of the before file which relies on any properties/items defined in the build because they will not be available. Because of this most times you will just need an after targets file. So in my case I will create a file named after.ExtendSolutionBuild.sln.targets, this follows the pattern after.{SolutionFile}.sln.targets where {SolutionFile} is the name of the solution file mine happens to be ExtendSolutionBuild. Every solution has 4 targets that will be defined.
4 Targets on Every Solution file
Build
Rebuild
Clean
Publish
So let’s say that I want to extend the build file and inject two targets; GenerateCode and RunCodeAnalysis. Obviously I want the GenerateCode target to run before anything is built and the RunCodeAnalysis to be run after everything is built. So in my after.ExtendSolutionBuild.sln.targets file I place the following content.
You can see here that I am using BeforeTargets=”Build” to make sure that the GenerateCode target is run before the Bulid target and also the RunCodeAnalysis target uses AfterTargets=”Build”. So now let’s see what happens when I build my solution from the command line using msbuild.exe.
From the figure above you can see that the GenerateCode target was executed before the build process started and then the RunCodeAnalysis target was executed after the build. With MSBuild 3.5 and earlier this was simply not possible, and you would have had to write another build file to do these kinds of things for you. If you are building solution files from the command then you might be interested in the technique. I would be interested to hear you feedback and to see how you guys would use this feature.
ImportBefore/ImportAfter
Now let’s talk briefly about the two that we skipped. If you want an MSBuild file to be imported for every solution that is build using msbuild.exe for a given machine then you can place that file in one of the two folders below.
So if you place any file in either of those folders (which don’t exist by default) then they will automatically be imported for every solution file which builds on that machine using msbuild.exe. Typically you do not run into many times where you want to do this, but the times that you do then it is critical and really hard to work around without something like this. This is useful if you have a build machine and you want to perform a set of actions for every build.
Warning: What you see below feels hacky to me, but if you find it useful then use it
I have heard a lot of questions and confusion regarding web.debug.config and web.release.config. For example here is just one question on StackOverflow. The question states:
Hello, I want to use the web.config transformation that works fine for publish also for debugging.
When i publish a web app, visual studio automatically transforms the web.config based on my
current build configuration. How can i tell visual studio
to do the same when i start debugging. On debug start it simply
uses the default web.config without transformation.
Any idea?
First let me explain, as I did to that question, the purpose of the files: web.config/web.debug.config/web.release.config.
web.config
This is the config file which developers should use locally. Ideally you should get this to be standardized. For instance you could use localhost for DB strings, and what not. You should strive for this to work on dev machines without changes.
web.debug.config
This is the transform that is applied when you publish your application to the development staging environment. This would make changes to the web.config which are required for the target environment.
web.release.config
This is the transform that is applied when you publish your application to the "production" environment. Obviously you'll have to be careful with passwords depending on your application/team.
The problem with transforming the web.config that you are currently running is that a transform can perform destructive actions to the web.config. For example it may delete a attributes, delete elements, etc.
Resolution
Ok, with that out the way not let’s see how we can enable what the question asker wants to do. To recap, when he builds on a particular configuration he wants a specific transform to be applied to web.config. So obviously you do not want to maintain a web.config file, because it is going to be overwritten. So what we need to do is to create a new file web.template.config, which is just a copy of web.config. Then just delete web.config by using Windows Explorer (don’t delete using Visual Studio because we do not want to delete it from the project). Note: If you are using a source control provider which is integrated into Visual Studio then you probably want to delete web.config from source control. Also with this we do not want to use web.debug.config or web.release.config because these already have a well defined role in the Web Publishing Pipeline so we do not want to disturb that. So instead we will create two new files, in the same folder as the project and web.template.config, web.dev.debug.config and web.dev.release.config. The ideas is that these will be the transforms applied when you debug, or run, your application from Visual Studio. Now we need to hook into the build/package/publish process to get this all wired up. With Web Application Projects (WAP) there is an extensibility point that you can create a project file in the same folder with the name {ProjectName}.wpp.targets where {ProjectName} is the name of the project. If this file is on disk in the same folder as the WAP then it will automatically be imported into the project file. So I have created this file. And I have placed the following content:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- Make sure web.config will be there even for package/publish -->
<Target Name="CopyWebTemplateConfig" BeforeTargets="Build">
<Copy SourceFiles="web.template.config"
DestinationFiles="web.config"/>
</Target>
<PropertyGroup>
<PrepareForRunDependsOn>
$(PrepareForRunDependsOn);
UpdateWebConfigBeforeRun;
</PrepareForRunDependsOn>
</PropertyGroup>
<!-- This target will run right before you run your app in Visual Studio -->
<Target Name="UpdateWebConfigBeforeRun">
<Message Text="Configuration: $(Configuration): web.dev.$(Configuration).config"/>
<TransformXml Source="web.template.config"
Transform="web.dev.$(Configuration).config"
Destination="web.config" />
</Target>
<!-- Exclude the config template files from the created package -->
<Target Name="ExcludeCustomConfigTransformFiles" BeforeTargets="ExcludeFilesFromPackage">
<ItemGroup>
<ExcludeFromPackageFiles Include="web.template.config;web.dev.*.config"/>
</ItemGroup>
<Message Text="ExcludeFromPackageFiles: @(ExcludeFromPackageFiles)" Importance="high"/>
</Target>
</Project>
Let me explain this a bit. I have created the CopyWebTemplateConfig target which will always copy web.template.config to web.config on build, even if you are not debugging your application in Visual Studio. This is needed because we still need to support the package/publish process of Visual Studio. Then I extended the property PrepareForRunDependsOn to include the UpdateWebConfigBeforeRun target. This property is used to identify the list of targets which needs to be executed before any managed project is run from Visual Studio. In this target I am using the TransformXml task to transform web.template.config, using the correct web.dev.***.config file. After that your app starts up using the correct web.config based on your build configuration.
After that I have another target ExcludeCustomConfigTransformsFiles, which I inject into the package/publish process via the attribute BeforeTargets=”ExcludeFilesFromPackage”. This is needed because we do not want these files to be included when the application is packaged or published.
So that is really all there is to it. To explain the package/publish process a bit more for this scenario. When you package/publish web.debug.config or web.release.config, depending on build configuration, will still be used. But ultimately the file that it is transforming is web.template.config, so you may have to adjust depending on what you have in that file. Questions/Comments?
@MikeFourie MSBuild Q: given I have @(ProjectOutputs) how can I get a collection of all *.Unit.dll files?
About a year ago I blogged about inline tasks including a FilterList task. So for his case the FilterList would work, but then I was thinking that he should be able to do this with some of the goodness that was released wtih MSBuild 4.0. Specifically with Property Function and Item Functions. So I thought about it and whipped up a quick example.
Take a look at the file below.
In this file I have created a dummy item named TargetOutputs and populated it with a bunch of fake assembly names. Then inside the Demo target, I print out the items in that list and construct another item list, UnitTestAssemblies. In this declaration I am including %(TargetOutputs.Identity) with the condition '@(TargetOutputs->Contains("unit"))'=='True'. Let me break this down for you a bit. With Item Functions there is a specific set of functions that you can call like DirectoryName/Metadata/Distinct/etc (see below for the reference link for full list), but you can also execute any string instance method. What you need to understand is that it will execute the method for all items in the list then return a new list with the invocation result. Essentially these are a new form of Item Transformations. I didn’t want to handle these in bulk, I need to apply the filter to each individual item value. In order to do that I use the %(TargetOutputs.Identity) which kicks batching in. Since I can see that the values in the include value are unique I know that the Identity metadata is unique. So what that means is When I do something like the message task will be invoked once per item value in TargetOutputs. The same goes for the item declaration. So in the UnitTestAssemblies item group declaration I can use a condition of Condition='@(TargetOutputs->Contains("unit"))'=='True' , because we are know that @(TargetOutputs) will only have 1 value per invocation. With that condition I only added files that have unit in the name. When you execute the Demo target the result is shown below.
Pretty cool huh? The best part, I managed to fit my reply in 140 characters in this tweet.
MSBuild supports a concept known as Incremental Building which you may already be familiar with but I will cover it here briefly but that is not the focus of this post. Incremental building is the concept that you should only build what is out of date. To support this MSBuild has the attributes, inputs and outputs on the Target element. With these attributes you can specify the files that go into a target (via inputs attribute), and the files that you are expecting to come out of a target (via outputs attribute). Once you do this MSBuild will compare the timestamp of the inputs to the outputs and if all outputs are up-to-date (i.e. the inputs are older) then the target will be skipped. Take a look at the very simple project file below.
dest\
In this project file we have two targets; CopyFiles and DeleteTwoFiles. Ignore DeleteTwoFiles for now. Also take a note that the directory where I’m executing this build has a folder, src, with the files listed in the Files item. On the CopyFiles target I have specified the inputs and outputs. The inputs is just @(Files), this are the files that the target is acting upon. The outputs contains the expression @(Files->'$(Dest)%(Filename)%(Extension)'). Which is the same expression from the Copy statement. If the Dest folder is empty and I execute the CopyFiles target the result is shown below.
So just as expected the files were copied over, so its all good. Now what happens if I execute it again? The output is shown below.
So as you can see the target was skipped, the message statement “CopyFiles” was not executed nor was the copy as a result. So this, in a nutshell, is incremental building.
Now, with the dest folder containing all files, what do you think would happen I execute the command msbuild.exe PartialBuilding01.proj /t:DeleteTwoFiles;CopyFiles? This command will first delete two files from the output directory and then call the CopyFiles target again. Let’s see the result below.
When the CopyFiles target was executed you see that statement “Building target ‘CopyFiles’ partially, …”. When the time came to execute the target MSBuild examined the inputs and outputs, it determined that the files 01.txt & 02.txt were out of date (because they didn’t exist in the target) but 03.txt, 04.txt and 05.txt were up to date. So MSBuild feed the CopyFiles target a value for the Files item that only contained the 01.txt and 02.txt and let it do its thing. So if you are performing builds which are taking a lot of time then incremental (and therefore partial) building is your friend!
Sayed Ibrahim Hashimi
msbuild
Thursday, September 23, 2010 7:05:59 AM (GMT Daylight Time, UTC+01:00)
Comments [0]
|
In his question he asks (summarizing) can we replace portions of attribute values instead of this entire attribute? So for instance you have the following in your web.config. Below is two sets of appSettings one from Dev and the other from Prod (taken from the original question).
In the above we just want to replace dev with prod and ma1-lab.lab1.domain with ws.ServiceName2.domain. For those wondering currently we have the following transformations out of the box.
Replace – Replaces the entire element
Remove – Removes the entire element
RemoveAll – Removes all matching elements
Insert – Inserts an element
SetAttributes – Sets the value of the specified attributes
RemoveAttributes – Removes attributes
InsertAfter – Inserts an element after another
InsertBefore – Inserts an element before another
At the end of this article I’ve linked to another blog which has more info about these transformations. So it sounds like SetAttributes is almost what we want, but not quite what there. A little known fact is that you can create your own config transformations and use those. In fact all of the out of the box transformations follow the same patterns that custom transformations would. To solve this issue we need to create our own config transformation, AttributeRegexReplace. This transformation will take an attribute value and do a regular expression replace on its value. In order to create a new transformation you first reference the Microsoft.Web.Publishing.Tasks.dll which can be found in the %Program Files (x86)%MSBuild\Microsoft\VisualStudio\v10.0\Web folder. If you are working with a team it is best if you copy that assembly, place it in a shared folder in source control, and make the reference from that location. After you create the reference to that assembly you will need to create a class which extends the Transform class. The class diagram for this abstract class is shown below.
The only thing that you will need to implement is the Apply method. You don’t even need to fully understand all of the properties and methods just the portions that you are interested in. Here we will not cover all the details of this class, or other related classes which exist, but there will be future posts which will shed more light on this area.
In the sample class library that I created, I called the project CustomTransformType. Inside of that project I created the class AttributeRegexReplace. The entire contents of that class are shown below, we will go over the details after that.
namespace CustomTransformType
{
using System;
using System.Text.RegularExpressions;
using System.Xml;
using Microsoft.Web.Publishing.Tasks;
public class AttributeRegexReplace : Transform
{
private string pattern;
private string replacement;
private string attributeName;
protected string AttributeName
{
get
{
if (this.attributeName == null)
{
this.attributeName = this.GetArgumentValue("Attribute");
}
return this.attributeName;
}
}
protected string Pattern
{
get
{
if (this.pattern == null)
{
this.pattern = this.GetArgumentValue("Pattern");
}
return pattern;
}
}
protected string Replacement
{
get
{
if (this.replacement == null)
{
this.replacement = this.GetArgumentValue("Replacement");
}
return replacement;
}
}
protected string GetArgumentValue(string name)
{
// this extracts a value from the arguments provided
if (string.IsNullOrWhiteSpace(name))
{ throw new ArgumentNullException("name"); }
string result = null;
if (this.Arguments != null && this.Arguments.Count > 0)
{
foreach (string arg in this.Arguments)
{
if (!string.IsNullOrWhiteSpace(arg))
{
string trimmedArg = arg.Trim();
if (trimmedArg.ToUpperInvariant().StartsWith(name.ToUpperInvariant()))
{
int start = arg.IndexOf('\'');
int last = arg.LastIndexOf('\'');
if (start <= 0 || last <= 0 || last <= 0)
{
throw new ArgumentException("Expected two ['] characters");
}
string value = trimmedArg.Substring(start, last - start);
if (value != null)
{
// remove any leading or trailing '
value = value.Trim().TrimStart('\'').TrimStart('\'');
}
result = value;
}
}
}
}
return result;
}
protected override void Apply()
{
foreach (XmlAttribute att in this.TargetNode.Attributes)
{
if (string.Compare(att.Name, this.AttributeName, StringComparison.InvariantCultureIgnoreCase) == 0)
{
// get current value, perform the Regex
att.Value = Regex.Replace(att.Value, this.Pattern, this.Replacement);
}
}
}
}
}
In this class we have 3 properties; Pattern, Replacement, and AttributeName. All of these values will be provided via an argument in the config transformation. For example take a look at the element below which contains a transform attribute may look like the following.
In this example I declare that I am using AttributeRegexReplace and then specify the values for the attributes within the (). In the class above I have a method, GetArgumentValue, which is used to parse values from that argument string. When your transform is invoked the string inside of () is passed in as the ArgumentString value. If you are using a , as the argument separator, as I am, then you can use the Arguments list. Which will split up the arguments by the , character. Surprisingly in the 101 lines of code in the sample there are only a few interesting lines. Those are what’s contained inside the Apply method. Inside that method I search the TargetNode’s attributes (TargetNode is the node which was matched in the xml file being transformed) for an attribute with the same name as the one specified in the AttributeName property. Once I find it I just make a call to Regex.Replace to get the new value, and assign it. Pretty simple! Now lets see how can we use this.
Let’s say you have the following very simple web.config
If we want to be able to use our own transform then we will have to use the xdt:Import element. You can place that element inside the xml document anywhere immediately under the root element. This element will allow us to utilize our own transform class. It only has 3 possible attributes.
namespace – This is the namespace which the transform is contained in
path – This is the full path to the assembly
assembly – This is the assembly name which contains the transform
You can only use one of the two; path and assembly. Basically it boils down to how the assembly is loaded. If you use path the assembly will be loaded with Assembly.LoadFrom and if you chose to use assembly passing in the AssemblyName, for instance if the assembly in in the GAC, then it will be loaded using Assembly.Load.
I chose to use path, because I just placed the file inside of the MSBuild Extensions directory (%Program Files (x86)%MSBuild) in a folder named Custom. Then I created my config transform file to be the following.
This file uses the TransformXml task as I outlined in a previous post Config transformations outside of web app builds. Once you execute the Demo target with the command msbuild PerformTransform.proj /t:Demo you will see the file web.tranzed.config with the following contents.
So you can see that the replacement did occur as we intended. Below you will find the download link for the samples as well as another blog entry for more info on the out of the box transformations.
A while back I posted an entry on How to build a package including extra files or exclude files a reader posted a question to StackOverflow.com asking how to exclude files from the created package based on the configuration for the project. He asked me to take a look at it so I figured it would be a good blog post.
From the previous post we can see that the way to exclude files from packaging is by declaring an item as follows.
So we need to extend this to only exclude files if the config is a certain value. Since MSBuild supports conditions on almost every element this is going to be a breeze. As an example I have created a sample web project with a scripts directory that has the following files.
In that folder there I there are two files which have ‘debug’ in the name of the file. We only want those to be included if the configuration is set to Debug, or another way of putting it is we want to exclude those files if the configuration is not Debug. So we need to create to add files to the ExcludeFromPackageFiles and guard it with the condition that the configuration is not debug. Here is that.
You can see the item group defined above which does what we want. Please note that I put this inside of a target, CustomExcludeFiles, I will discuss why in a bit but let’s stay on topic now. So this is pretty straight forward when the item group is evaluated all files under scripts which have debug in the file name will be excluded if the configuration is not set to Debug. Let’s see if it works, I will build the deployment package once in both debug & release then examine the contents of the Package folder.
So we can see that the files were excluded from the Release package. Now back to why I declared the item group in a target instead of directly in the project file itself. I noticed that if I declare that item in the project file there are some visual issues with the representation in the Solution Explorer. To be specific the files show up as dups, see image below.
I have reported this to the right people, but for now this is a harmless issue with an easy workaround.
You cannot detect this out of the box, but you can by creating a custom task. Better would be to create an inline task so that you don’t have to deal with the headache of maintaining a .dll for something like this, unless you are using it on many different project files. Basically the task that we will create will need two parameters; the Item itself (single value) and MetadataName (metadata name to check for). It will have one output parameter, MetadataDefined, which we can check to see if the metadata value was defined or not.
This is a pretty easy task to create because we just look at the MetadataNames property on the ITaskItem interface. The task as well as a sample target is shown below.
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<UsingTask TaskFactory="CodeTaskFactory"
TaskName="MetadataExists"
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<ParameterGroup>
<MetadataName Required="true"/>
<Item ParameterType="Microsoft.Build.Framework.ITaskItem"/>
<MetadataDefined ParameterType="System.Boolean" Output="true" />
</ParameterGroup>
<Task>
<Code>
<![CDATA[
this.MetadataDefined = false;
if (this.Item != null)
{
foreach (string name in this.Item.MetadataNames)
{
if (string.Compare(this.MetadataName, name, StringComparison.OrdinalIgnoreCase) == 0)
{
this.MetadataDefined = true;
break;
}
}
}
]]>
</Code>
</Task>
</UsingTask>
<Target Name="CheckForMetadata">
<ItemGroup>
<Content Include="Sample-DefinedEmpty.sdf">
<SubPath></SubPath>
</Content>
<Content Include="Sample-NotDefined.sdf">
</Content>
</ItemGroup>
<Message Text="Starting - Content"/>
<!-- Create an Item which has exactly 1 value to pass to the task -->
<ItemGroup>
<_Content Remove="@(_Content)"/>
<_Content Include="@(Content)" Condition=" '%(Content.Identity)' == 'Sample-DefinedEmpty.sdf' "/>
</ItemGroup>
<MetadataExists MetadataName="SubPath" Item="@(_Content)">
<Output PropertyName="existsResult" TaskParameter="MetadataDefined"/>
</MetadataExists>
<Message Text="existsResult: $(existsResult)" />
<Message Text="Starting - Content2"/>
<!-- Create an Item which has exactly 1 value to pass to the task -->
<ItemGroup>
<_Content Remove="@(_Content)"/>
<_Content Include="@(Content)" Condition=" '%(Content.Identity)' == 'Sample-NotDefined.sdf' "/>
</ItemGroup>
<MetadataExists MetadataName="SubPath" Item="@(_Content)">
<Output PropertyName="existsResult" TaskParameter="MetadataDefined"/>
</MetadataExists>
<Message Text="existsResult: $(existsResult)" />
</Target>
</Project>
Here you can take a look at the MetadataExists task and its usage. The only thing that really needs to be pointed out here is that since this task accepts a single item value we will have to take the item group Content and pick from it a specific value which is passed to the task. That is what I am doing when I create the temp item _Content. If you execute the CheckForMetadata target with the command msbuild CheckMetadata01.proj /t:CheckForMetadata the result will be what is shown below.
So from the output you can see that we were able to tell the difference!
BTW, if you were wondering if you can do the same with properties, the answer is no.
In the previous examples I have use C# as my language of choice. C# is not your only choice you can also use VB.Net but you can also use JavaScript! When MSBuild creates the class for the inline task it uses CodeDom to do so, and JavaScript is one of its supported languages. Perhaps its actually called JScript. In any case take a look at the project file below which shows this in action.
Here you can see that I created a new inline task called, Jsex01 and its written in Javascript. Then inside of the PrintValues target this task is called. If you execute the PrintValues target the result will be what you see below.
So if you prefer JavaScript to C# or VB.Net then you should try this out!
Do you have a great idea for the next version of Visual Studio and you want to get your voice heard? Now is your chance! Go to twitter and post a message stating your request with the hash tag #vswish and we will take a look at it. We are listening to that channel so please do submit your feedback.