Mit ASP.NET 4.0 kam ein neues Feature namens "Web.config Tranformations”. In meinen vergangenen MSBuild Posts habe ich gezeigt, wie man über pures MSBuild z.B. eine Solution mit einem Web-Projekt baut. Im Standardfall wird die web.config Transformation nicht getriggert und die WebApp läuft im Debug Mode (was schlecht ist!). Daher müssen wir es manuell im MSBuild antriggern.
[Update] Mit Visual Studio 2012 wird alles besser:
Der Aufruf von TransformXml war zu Visual Studio 2010 Zeiten ziemlich haarig, da es der Task ein Lock auf die Files behalten hatte. Mit VS 2012 sollte dieser Bug behoben werden. D.h. ein mühsames kopieren und renaming ist nicht mehr nötig.
Wie der Output bis jetzt aussah
Wenn man sich an diesen Post hält, sollte ein _PublishedWebsites Ordner erstellt werden und dort findet man entsprechend die WebApp. Allerdings auch die Transformations-Files:
Das große Problem dabei: Die Transformation der eigentlich Web.config fand nicht statt - die Anwendung läuft unter debug:
<compilation debug="true" targetFramework="4.0">
XmlTransfrom
Es gibt allerdings ein Task für MSBuild, dazu muss man diese Targets importieren:
<Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" />
Und dann kann der Aufruf wie folgt aussehen:
<TransformXml Source="Web.config" Transform="Web.Release.config" Destination="Web.transformed.config" />
ABER: So ists es ja auch doof, oder?
Am Ende möchte ich ja dass die transformierte Datei wieder "Web.config” heisst. Das Problem: TransformXml ist buggy. Während des Bauens hält der Task einen lock auf die 3 Files. Zudem: Wir wollen ja unser eigentliches Web.config File, welches in unserer Solution nicht abändern. Daher legen wir eine vollständige Kopie unserer Sourcen an und verändern diese.
Schema:
CopySource:
Wir kopieren unsere gesamten Sourcen an einen anderen Ort, z.B. einen Ordner höher in ein eigenes Verzeichnis. Ich hab es "ClientTemp” genannt.
BeforeCompile:
Hier in dem BeforeCompile Target könnte ich nun an den Kopien, welche im ClientTemp sind, Manipulationen machen z.B. beim Aufruf die AssemblyVersion verändern. An der Stelle ruf ich aber auch den TransformXml Task auf:
<TransformXml Source="..\MsBuildSample.WebApp\Web.config" Transform="..\MsBuildSample.WebApp\Web.$(Configuration).config" Destination="$(BuildDirFullName)MsBuildSample.WebApp\Web.config" />
Source & Transform kommen aus dem Originalen Zweig - daher stört mich dort der lock nicht mehr. Nur die Destination liegt nun in unserem "geklonten” Ordner. Danach ist die "Web.config” tranformiert.
Build:
Hier wird nun der eigentliche Buildvorgang aufgerufen das Standardverhalten setzt ein und baut die Solution und verschiebt die WebApp in das _PublishedWebsites OutDir.
...
Hier danach können noch irgendwelche anderen Aktionen erfolgen. Ich entfern aus dem OutDir noch die nicht benutzen Transformationsdatein (web.release.config / web.debug.config).
Fazit
Es ist ein klein wenig komplizierter geworden, allerdings hat diese Struktur den Vorteil, dass ich während des Buildvorganges (und vor dem Compile) nun munter am Code rumbasteln kann und meinen eigentlichen Source Code daher nicht verändere. Es erinnert mich ein wenig (ganz, ganz grob) an die Funktionsweise eines TFS ;)
Komplettes Buildscript:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Run"> <Import Project="$(MSBuildStartupDirectory)\Lib\MSBuild.Community.Tasks.Targets"/> <Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" /> <PropertyGroup> <!-- After Compile: Result will be saved in OutDir --> <OutDir>$(MSBuildStartupDirectory)\..\..\ClientTemp\OutDir\</OutDir> <!-- Name of the BuildDir with the whole source code before the compile begins --> <BuildDirName>BuildDir</BuildDirName> <!-- Relativ part of the BuildDir --> <BuildDirRelativePart>..\..\ClientTemp\$(BuildDirName)</BuildDirRelativePart> <!-- Absolute part of the BuildDir--> <BuildDirFullName>$(MSBuildStartupDirectory)\$(BuildDirRelativePart)\</BuildDirFullName> <!-- Configuration --> <Configuration>Release</Configuration> <!-- Solutionproperties--> <SolutionProperties> OutDir=$(OutDir); Platform=Any CPU; Configuration=$(Configuration) </SolutionProperties> </PropertyGroup> <ItemGroup> <Solution Include="$(BuildDirFullName)MsBuildSample.sln"> <Properties> $(SolutionProperties) </Properties> </Solution> </ItemGroup> <Target Name="Run"> <Message Text="Run called." /> <CallTarget Targets="CopyToBuildDir" /> <CallTarget Targets="BeforeBuild" /> <CallTarget Targets="Build" /> <CallTarget Targets="Cleanup" /> <CallTarget Targets="Zip" /> </Target> <Target Name="CopyToBuildDir"> <Message Text="CopyToBuildDir called." /> <Exec Command="robocopy .. $(BuildDirRelativePart) /s /z /purge /a-:r" ContinueOnError="true" /> </Target> <Target Name="BeforeBuild"> <Message Text="BeforeBuild called." /> <Message Text="Transform Xml" /> <TransformXml Source="..\MsBuildSample.WebApp\Web.config" Transform="..\MsBuildSample.WebApp\Web.$(Configuration).config" Destination="$(BuildDirFullName)MsBuildSample.WebApp\Web.config" /> </Target> <Target Name="Build"> <Message Text="Build called." /> <MSBuild Projects="@(Solution)"/> </Target> <Target Name="Cleanup"> <Delete Files="$(OutDir)_PublishedWebsites\MsBuildSample.WebApp\Web.Release.config" /> <Delete Files="$(OutDir)_PublishedWebsites\MsBuildSample.WebApp\Web.Debug.config" /> </Target> <ItemGroup> <ZipFiles Include="$(OutDir)_PublishedWebsites\**\*.*" /> </ItemGroup> <Target Name="Zip"> <Zip Files="@(ZipFiles)" WorkingDirectory="$(OutDir)_PublishedWebsites\" ZipFileName="$(OutDir)Package.zip"/> </Target> </Project>
Democode
In meiner Demosolution arbeite ich mit ein paar Hilfsvariablen im MSBuild - davon nicht abschrecken lassen. Wenn ihr den SampleCode auf:
D:\Samples\MsBuildSample ablegt findet Ihr das "BuildVerzeichnis” in D:\Samples\ClientTemp