{"id":22118,"date":"2013-09-22T14:35:05","date_gmt":"2013-09-22T21:35:05","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/aspnet\/?p=22118"},"modified":"2013-09-22T14:35:05","modified_gmt":"2013-09-22T21:35:05","slug":"web-publish-how-to-automate-multi-project-publish-with-file-system","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/dotnet\/web-publish-how-to-automate-multi-project-publish-with-file-system\/","title":{"rendered":"Web Publish how to automate multi-project publish with file system"},"content":{"rendered":"<p>The other day I received an email from a customer with a question which I\u2019ve summarized as below.<\/p>\n<blockquote>\n<p>I have a solution containing multiple web projects. One of the projects, _RootSite, is the top level website which I want to publish. It\u2019s an MVC project. I also have other web projects in the same solution. These other projects are apps themselves but being published under _RootSite as an area. In some cases I\u2019d like to publish my entire site, in other cases just the areas. Sometimes when I publish an area I need to refresh the assembly for _RootSite. How can I automate this with the most flexibility?<\/p>\n<p>I am publishing by using file copy over a network share.<\/p>\n<\/blockquote>\n<p>To help visualize the issue take a look at the screenshot below which is from the samples that I created to accompany this post (<em>download links at the bottom of the post here<\/em>).<\/p>\n<p><a href=\"https:\/\/msdnshared.blob.core.windows.net\/media\/MSDNBlogsFS\/prod.evol.blogs.msdn.com\/CommunityServer.Blogs.Components.WeblogFiles\/00\/00\/00\/63\/56\/metablogapi\/0572.image_578DADBD.png\"><img decoding=\"async\" title=\"image\" style=\"border-width: 0px\" border=\"0\" alt=\"image\" src=\"https:\/\/msdnshared.blob.core.windows.net\/media\/MSDNBlogsFS\/prod.evol.blogs.msdn.com\/CommunityServer.Blogs.Components.WeblogFiles\/00\/00\/00\/63\/56\/metablogapi\/8715.image_thumb_2529DA3E.png\" width=\"357\" height=\"150\" \/><\/a> <\/p>\n<p>In this case both AdminArea and HrArea will be published to _RootSite\\Areas under their own folders.<\/p>\n<p>The way that I\u2019d solve this is to break it down into two steps using MSBuild and PowerShell to automate all steps. The first step is to build and publish all projects to the local file system. You can think of this an \u201cintermediate publish folder\u201d. From there you can construct copy commands to publish each part. <\/p>\n<p>In the samples you\u2019ll find publish.proj. This file is used to build and publish each (or all) components. This file is composed of two sections; a build section and a publish section. The build section takes care of building and publishing to that intermediate publish folder. The publish related targets perform the actual publish operations to prod.<\/p>\n<p>Let\u2019s take a look at the build related elements in that file. In this post I\u2019ll use the <a href=\"http:\/\/sedodream.com\/2013\/09\/21\/HowToExtendTheWebPublishProcessWithoutModifyingProjectContents.aspx\">publish injection technique<\/a> which I\u2019ve recently blogged. If you haven\u2019t read that you may want to take a look to fully understand what is happening here.<\/p>\n<p><strong><u>Build related elements from publish.proj<\/u><\/strong><\/p>\n<pre class=\"brush: xml;\">&lt;PropertyGroup&gt;\n  &lt;VisualStudioVersion Condition=&quot; '$(VisualStudioVersion)'=='' &quot;&gt;11.0&lt;\/VisualStudioVersion&gt;\n  &lt;Configuration Condition=&quot; '$(Configuration)'=='' &quot;&gt;Release&lt;\/Configuration&gt;\n  &lt;SourceRoot Condition=&quot; '$(SourceRoot)'=='' &quot;&gt;$(MSBuildThisFileDirectory)..\\&lt;\/SourceRoot&gt;\n  &lt;!-- Location for build output of the project --&gt;\n  &lt;OutputRoot Condition=&quot; '$(OutputRoot)'=='' &quot;&gt;$(MSBuildThisFileDirectory)..\\BuildOutput\\&lt;\/OutputRoot&gt;\n  &lt;!-- Root for the publish output --&gt;\n  &lt;PublishFolder Condition=&quot; '$(PublishFolder)'==''&quot;&gt;C:\\temp\\Publish\\MySite\\&lt;\/PublishFolder&gt;\n  &lt;DeployOnBuild Condition=&quot; '$(DeployOnBuild)'=='' &quot;&gt;true&lt;\/DeployOnBuild&gt;\n  &lt;BuildBeforePublish Condition=&quot; '$(BuildBeforePublish)'=='' &quot;&gt;true&lt;\/BuildBeforePublish&gt;\n&lt;\/PropertyGroup&gt;\n  \n&lt;ItemGroup&gt;\n  &lt;ProjectsToBuild Include=&quot;$(SourceRoot)RootSite\\_RootSite.csproj&quot;&gt;\n    &lt;AdditionalProperties&gt;publishUrl=$(PublishFolder);&lt;\/AdditionalProperties&gt;\n  &lt;\/ProjectsToBuild&gt;\n\n  &lt;ProjectsToBuild Include=&quot;$(SourceRoot)AdminArea\\AdminArea.csproj&quot;&gt;\n    &lt;AdditionalProperties&gt;publishUrl=$(PublishFolder)Areas\\Admin\\;&lt;\/AdditionalProperties&gt;\n  &lt;\/ProjectsToBuild&gt;\n\n  &lt;ProjectsToBuild Include=&quot;$(SourceRoot)HrArea\\HrArea.csproj&quot;&gt;\n    &lt;AdditionalProperties&gt;publishUrl=$(PublishFolder)Areas\\HR\\;&lt;\/AdditionalProperties&gt;\n  &lt;\/ProjectsToBuild&gt;\n&lt;\/ItemGroup&gt;\n\n&lt;PropertyGroup&gt;\n  &lt;BuildAndPublishDependsOn&gt;\n    $(BuildAndPublishDependsOn);\n    CoreBuildAndPublish;\n    Publish\n  &lt;\/BuildAndPublishDependsOn&gt;\n&lt;\/PropertyGroup&gt;\n&lt;Target Name=&quot;BuildAndPublish&quot; DependsOnTargets=&quot;$(BuildAndPublishDependsOn)&quot; \/&gt;\n  \n&lt;Target Name=&quot;CoreBuildAndPublish&quot;&gt;\n  &lt;Message Text=&quot;Building and publishing all projects in ProjectsToBuild [@(ProjectsToBuild)]&quot;\n            Condition=&quot; '$(DeployOnBuild)'=='true' &quot;\/&gt;\n  &lt;Message Text=&quot;Building all projects in ProjectsToBuild [@(ProjectsToBuild)]&quot;\n            Condition=&quot; '$(DeployOnBuild)'!='true' &quot;\/&gt;\n    \n  &lt;MSBuild Projects=&quot;@(ProjectsToBuild)&quot;\n            Properties=&quot;\n              VisualStudioVersion=$(VisualStudioVersion);\n              Configuration=$(Configuration);\n              OutputPath=$(OutputRoot);\n              WebPublishMethod=FileSystem;\n              DeployOnBuild=$(DeployOnBuild);\n              DeployTarget=WebPublish;\n              PublishProfile=$(MSBuildThisFileFullPath)&quot; \/&gt;\n&lt;\/Target&gt;<\/pre>\n<p>Here you can see that we have defined the main build related target; BuildAndPublish. This is a publish to the intermediate publish folder. In this snippet I define some MSBuild parameters which are required when building the projects. I define the projects which I want to build in the ProjectsToBuild item list. Along with that I declare all the MSBuild properties which are specific to that project in the AdditionalProperties metadata. <em>FYI for more info on Properties vs. AdditionalProperties take a look at <\/em><a href=\"http:\/\/sedodream.com\/2009\/04\/29\/MSBuildPropertiesAndAdditionalPropertiesKnownMetadata.aspx\"><em>my previous post<\/em><\/a>. In this case the only parameter which varies from project to project is the <em>publishUrl<\/em>. This defines where the project will be published to.<\/p>\n<p>For the _RootSite project publishUrl is $(PublishFolder), the intermediate publish folder. In my case this is <em>c:\\temp\\Publish\\MySite\\<\/em>. For AdminArea publishUrl is $(PublishFolder)Areas\\Admin and similarly for HrArea. This will ensure the contents are published to the correct location.<\/p>\n<p>Inside CoreBuildAndPublish I invoke the <a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/vstudio\/z7f65y0d.aspx\">MSBuild task<\/a> to build each project and publish to the local intermediate publish folder.&#160; After invoking that all projects will be built and the $(PublishFolder) will be populated.<\/p>\n<p>Now let\u2019s take a look at the publish related elements in publish.proj.<\/p>\n<p><strong><u>Publish elements from publish.proj<\/u><\/strong><\/p>\n<pre class=\"brush: xml;\">&lt;PropertyGroup&gt;\n  &lt;ProdPublishFolder Condition=&quot; '$(ProdPublishFolder)'=='' &quot;&gt;C:\\temp\\Publish\\MySite-Prod\\&lt;\/ProdPublishFolder&gt;\n\n  &lt;!-- If \/p:BuildBeforPublish is true then invoke build before any publish operations. --&gt;\n  &lt;PublishComputeItemsDependsOn Condition=&quot; '$(BuildBeforePublish)'=='true' &quot;&gt;$(PublishComputeItemsDependsOn);BuildAndPublish&lt;\/PublishComputeItemsDependsOn&gt;\n    \n  &lt;PublishComputeItemsDependsOn&gt;$(PublishComputeItemsDependsOn);&lt;\/PublishComputeItemsDependsOn&gt;\n  &lt;PublishMySiteBinDependsOn&gt;$(PublishMySiteBinDependsOn);PublishComputeItems&lt;\/PublishMySiteBinDependsOn&gt;\n  &lt;PublishMySiteAllDependsOn&gt;$(PublishMySiteAllDependsOn);PublishComputeItems&lt;\/PublishMySiteAllDependsOn&gt;\n  &lt;PublishAdminAreaDependsOn&gt;$(PublishAdminAreaDependsOn);PublishComputeItems&lt;\/PublishAdminAreaDependsOn&gt;\n  &lt;PublishHrAreaDependsOn&gt;$(PublishHrAreaDependsOn);PublishComputeItems&lt;\/PublishHrAreaDependsOn&gt;  \n&lt;\/PropertyGroup&gt;\n\n  \n&lt;!-- \nThis target populates the publish items so that they can be\nused in the Publish* targets.\n--&gt;\n&lt;Target Name=&quot;PublishComputeItems&quot; DependsOnTargets=&quot;$(PublishComputeItemsDependsOn)&quot;&gt;\n  &lt;ItemGroup&gt;\n    &lt;PubMySiteBinFiles Include=&quot;$(PublishFolder)bin\\**\\*&quot;\/&gt;\n\n    &lt;PubMySiteAll Include=&quot;$(PublishFolder)**\\*&quot;\/&gt;\n\n    &lt;PubMySiteAdminArea Include=&quot;$(PublishFolder)Areas\\Admin\\**\\*&quot;\/&gt;\n      \n    &lt;PubMySiteHrArea Include=&quot;$(PublishFolder)Areas\\HR\\**\\*&quot;\/&gt;\n  &lt;\/ItemGroup&gt;\n&lt;\/Target&gt;\n\n&lt;!-- This is just a placeholder target so that we can specify \/p:PublishTargets from the cmd line --&gt;\n&lt;Target Name=&quot;Publish&quot; DependsOnTargets=&quot;$(PublishTargets.Split('|'))&quot;&gt;&lt;\/Target&gt;\n  \n&lt;Target Name=&quot;PublishMySiteBin&quot; \n        DependsOnTargets=&quot;$(PublishMySiteBinDependsOn)&quot;\n        Inputs=&quot;@(PubMySiteBinFiles)&quot; \n        Outputs=&quot;@(PubMySiteBinFiles-&gt;'$(ProdPublishFolder)bin\\%(RecursiveDir)%(FileName)%(Extension)')&quot;&gt;\n  &lt;Copy SourceFiles=&quot;@(PubMySiteBinFiles)&quot; \n        DestinationFiles=&quot;@(PubMySiteBinFiles-&gt;'$(ProdPublishFolder)bin\\%(RecursiveDir)%(FileName)%(Extension)')&quot;\/&gt;\n&lt;\/Target&gt;\n  \n&lt;Target Name=&quot;PublishMySiteAll&quot; \n        DependsOnTargets=&quot;$(PublishMySiteAllDependsOn)&quot;\n        Inputs=&quot;@(PubMySiteAll)&quot;\n        Outputs=&quot;@(PubMySiteAll-&gt;'$(ProdPublishFolder)%(RecursiveDir)%(FileName)%(Extension)')&quot;&gt;\n  &lt;Copy SourceFiles=&quot;@(PubMySiteAll)&quot;\n        DestinationFiles=&quot;@(PubMySiteAll-&gt;'$(ProdPublishFolder)%(RecursiveDir)%(FileName)%(Extension)')&quot;\/&gt;\n&lt;\/Target&gt;\n\n&lt;Target Name=&quot;PublishAdminArea&quot; \n        DependsOnTargets=&quot;$(PublishAdminAreaDependsOn)&quot;\n        Inputs=&quot;@(PubMySiteAdminArea)&quot;\n        Outputs=&quot;@(PubMySiteAdminArea-&gt;'$(ProdPublishFolder)Areas\\Admin\\%(RecursiveDir)%(FileName)%(Extension)')&quot;&gt;\n  &lt;Copy SourceFiles=&quot;@(PubMySiteAdminArea)&quot;\n        DestinationFiles=&quot;@(PubMySiteAdminArea-&gt;'$(ProdPublishFolder)Areas\\Admin\\%(RecursiveDir)%(FileName)%(Extension)')&quot;\/&gt;\n&lt;\/Target&gt;\n  \n&lt;Target Name=&quot;PublishHrArea&quot; \n        DependsOnTargets=&quot;$(PublishHrAreaDependsOn)&quot;\n        Inputs=&quot;@(PubMySiteHrArea)&quot;\n        Outputs=&quot;@(PubMySiteHrArea-&gt;'$(ProdPublishFolder)Areas\\Hr\\%(RecursiveDir)%(FileName)%(Extension)')&quot;&gt;\n  &lt;Copy SourceFiles=&quot;@(PubMySiteHrArea)&quot;\n        DestinationFiles=&quot;@(PubMySiteHrArea-&gt;'$(ProdPublishFolder)Areas\\Hr\\%(RecursiveDir)%(FileName)%(Extension)')&quot;\/&gt;\n&lt;\/Target&gt;<\/pre>\n<p>Here we define leaf targets like, PublishMySiteAll, PublishMySiteBin, etc. These targets contain a simple Copy command. All of these targets have Inputs and Outputs so that these are all incremental. Up to date files will be skipped by MSBuild automatically by using Inputs and Outputs. All of these targets depend on the PublishComputeItems target. This target is responsible for populating the item lists used by other publish targets. So if I want to publish the entire site I can execute the PublishMySiteAll target. If I just want to publish the Hr area I can invoke the PublishHrArea target and so forth. Pretty straight forward stuff here.<\/p>\n<p><strong><u>Now let\u2019s tie it all together with PowerShell.<\/u><\/strong><\/p>\n<p>In the samples you\u2019ll find publish.ps1. This is the file that will be used to simplify the user experience and drive the entire process. The full contents are below.<\/p>\n<pre class=\"brush: csharp;\">param(\n    [switch]\n    $BuildProjects,\n\n    [switch]\n    $PublishAll,\n\n    [switch]\n    $PublishMySiteBin,\n\n    [switch]\n    $PublishAdminArea,\n\n    [switch]\n    $PublishHrArea\n)\n\nfunction Get-ScriptDirectory\n{\n    $Invocation = (Get-Variable MyInvocation -Scope 1).Value\n    $path = Split-Path $Invocation.MyCommand.Path\n    return (&quot;{0}\\&quot; -f $path)\n}\n\n# we need to build the msbuild command\nfunction Get-MSBuildCommandArgs(){\n    param(\n        [Parameter(Mandatory=$true)]\n        [string]\n        $logsDir\n    )\n\n    $cmdArgs = @()\n    $cmdArgs += (&quot;{0}publish.proj&quot; -f $scriptDir)\n    $cmdArgs += '\/p:Configuration=release'\n    $cmdArgs += '\/p:VisualStudioVersion=11.0'\n\n    if($BuildProjects){\n        $cmdArgs += &quot;\/p:BuildBeforePublish=true&quot;\n    }<br \/>    else { <br \/>        $cmdArgs += &quot;\/p:BuildBeforePublish=false&quot; <br \/>    }<\/pre>\n<pre class=\"brush: csharp;\">    foreach($target in (Get-PublishTargetsFromParams)){\n        $cmdArgs += (&quot;\/t:{0}&quot; -f $target)\n    }\n\n    $logFileBaseName = (&quot;publish-$(get-date -f yyyy.MM.dd.ss.ff)&quot;)\n\n    # add logging parameters here\n    # \/flp1:&quot;v=d;logfile=logs\\%~n0.d.log&quot; \/flp2:&quot;v=diag;logfile=logs\\%~n0.diag.log&quot;\n    $cmdArgs += ('\/flp1:&quot;v=d;logfile={0}{1}.d.log&quot; ' -f $logsDir, $logFileBaseName)\n    $cmdArgs += ('\/flp2:&quot;v=diag;logfile={0}{1}.diag.log&quot; ' -f $logsDir, $logFileBaseName)\n\n    return $cmdArgs\n}\n\nfunction Get-PublishTargetsFromParams(){\n    $pubTargets = @()\n        \n    if($PublishAll){\n        # if PublishAll is passed we do not need to add any additional targets\n        # for any other publish targets\n        $pubTargets += &quot;PublishMySiteAll&quot;\n    }\n    else{\n        if($PublishMySiteBin){\n            $pubTargets += &quot;PublishMySiteBin&quot;\n        }\n        if($PublishAdminArea){\n            $pubTargets += &quot;PublishAdminArea&quot;\n        }\n        if($PublishHrArea){\n            $pubTargets += &quot;PublishHrArea&quot;\n        }\n    }\n\n    return $pubTargets\n}\nfunction Get-MSBuildPath(){\n    return &quot;$env:windir\\Microsoft.NET\\Framework\\v4.0.30319\\MSBuild.exe&quot;\n}\n\n# begin script\n\n$scriptDir = (Get-ScriptDirectory)\n$logsDir = (Join-Path -Path $scriptDir -ChildPath &quot;logs\\&quot;)\nif(!(Test-Path $logsDir)){\n    mkdir $logsDir\n}\n$msbuildCmd = (Get-MSBuildPath)\n$allArgs  = (Get-MSBuildCommandArgs -logsDir $logsDir)\n&quot;Calling msbuild.exe with the following parameters. [$msbuildCmd] [$allArgs] &quot; | Write-Host\n&amp; $msbuildCmd $allArgs<\/pre>\n<p>&#160;<\/p>\n<p>This script will build up the msbuild command based on the switches above and then invoke the correct targets based on the inputs. This is pretty straight forward PowerShell as well. It just interprets the switches that are passed in and builds the correct set of properties\/targets to be passed to msbuild.exe.<\/p>\n<p><strong>.\\publish.ps1 -BuildProjects -PublishAll<\/strong><\/p>\n<blockquote>\n<p>This will build all projects and publish them.<\/p>\n<\/blockquote>\n<p><strong>.\\publish.ps1 -BuildProjects -PublishHrArea<\/strong><\/p>\n<blockquote>\n<p>This will build all projects and only publish the HR area<\/p>\n<\/blockquote>\n<p><strong>.\\publish.ps1 -BuildProjects -PublishAdminArea -PublishMySiteBin<\/strong> <\/p>\n<blockquote>\n<p>This will build all projects and publish the bin\\ folder of MySite as well as the Admin area<\/p>\n<\/blockquote>\n<p><strong>.\\publish.ps1 -PublishAdminArea -PublishMySiteBin<\/strong><\/p>\n<blockquote>\n<p>This will skip the build and pub to intermediate publish folder (<em>must have been performed earlier at some point<\/em>) and publish the Admin area and the bin\\ folder of MySite.<\/p>\n<\/blockquote>\n<p>&#160;<\/p>\n<p>You can find all the samples at <a title=\"https:\/\/dl.dropboxusercontent.com\/u\/40134810\/blog\/SubappPublish.zip\" href=\"https:\/\/dl.dropboxusercontent.com\/u\/40134810\/blog\/SubappPublish.zip\">https:\/\/dl.dropboxusercontent.com\/u\/40134810\/blog\/SubappPublish.zip<\/a>. You can find the latest version in my <a href=\"https:\/\/github.com\/sayedihashimi\/publish-samples\">publish-samples<\/a> repository at <a href=\"https:\/\/github.com\/sayedihashimi\/publish-samples\/tree\/master\/SubappPublish\">SubappPublish<\/a>.<\/p>\n<p>If you have any comments on this please let us know by replying below.<\/p>\n<p>&#160;<\/p>\n<p>Sayed Ibrahim Hashimi | <a href=\"http:\/\/msbuildbook.com\">http:\/\/msbuildbook.com<\/a> | <a href=\"https:\/\/twitter.com\/sayedihashimi\">@SayedIHashimi<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>The other day I received an email from a customer with a question which I\u2019ve summarized as below. I have a solution containing multiple web projects. One of the projects, _RootSite, is the top level website which I want to publish. It\u2019s an MVC project. I also have other web projects in the same solution. [&hellip;]<\/p>\n","protected":false},"author":357,"featured_media":58792,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[197],"tags":[4,7343,147,7412,7436,7268],"class_list":["post-22118","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-aspnet","tag-net","tag-publish","tag-visual-studio","tag-visual-studio-2012","tag-visual-studio-2013","tag-web"],"acf":[],"blog_post_summary":"<p>The other day I received an email from a customer with a question which I\u2019ve summarized as below. I have a solution containing multiple web projects. One of the projects, _RootSite, is the top level website which I want to publish. It\u2019s an MVC project. I also have other web projects in the same solution. [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/22118","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/users\/357"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/comments?post=22118"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/22118\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media\/58792"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media?parent=22118"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/categories?post=22118"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/tags?post=22118"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}