使用 Web Deploy 和 .NET Web.configs 构建一次,随处部署

Chr*_*ris 5 deployment msbuild web-config webdeploy

我正在努力建立一个持续的构建和部署系统,该系统将管理我们的 .NET 应用程序到多个环境的构建和部署。我们希望这样做,以便我们构建并将该构建部署到我们的开发环境,然后可以选择使用不同的配置文件设置将相同的构建部署到我们的测试环境。目前,我们的开发人员习惯于使用 web.config 转换来管理每个环境的配置值,他们更愿意继续这样做。最后,我们希望使用 MS Web Deploy 3.6 及其包部署选项进行部署。

在做了一些研究之后,我们发现并考虑了以下选项:

  1. 使用 Web 部署参数化功能在部署时更改配置文件。这将取代我们希望避免的 web.config 转换。
  2. 每个项目配置/web.config 转换运行一次 MSBuild,以生成一个包,其中包含每个环境的转换后的 web.config。这会增加我们的包的构建时间和存储要求。
  3. 使用 Web Deploy 参数化和 web.config 转换。这允许开发人员继续使用 web.configs 来调试其他环境,避免创建多个包,但需要我们在多个地方维护配置设置。
  4. 在构建时,使用 web.config 转换生成多个配置文件但只有一个包,在部署时使用脚本将正确的配置插入包中的正确位置。这似乎说起来容易做起来难,因为它不是 Web Deploy 设计的工作方式,而且我们的初始评估实施起来似乎很复杂。

还有其他我们没有考虑过的选择吗?有没有办法做到这一点,让我们继续使用 web.configs,但只生成一个 Web 部署包?

Chr*_*ris 1

我不知道它是否比上面的选项 4 简单,但我们要采用的解决方案是在运行 MSBuild 之前立即运行 PowerShell 脚本,该脚本会解析 web.config 转换并生成或增强parameters.xml 文件。这使我们能够灵活地使用参数化,并且能够修改 web.config 之外的配置文件,同时保留 web.config 转换的 100% 当前功能。这是我们目前正在使用的脚本,以方便未来的寻求者:

function Convert-XmlElementToString
{
    [CmdletBinding()]
    param([Parameter(Mandatory=$true)] $xml, [String[]] $attributesToExclude)

    $attributesToRemove = @()
    foreach($attr in $xml.Attributes) {
        if($attr.Name.Contains('xdt') -or $attr.Name.Contains('xmlns') -or $attributesToExclude -contains $attr.Name) {
            $attributesToRemove += $attr
        }
    }
    foreach($attr in $attributesToRemove) { $removedAttr = $xml.Attributes.Remove($attr) }

    $sw = New-Object System.IO.StringWriter
    $xmlSettings = New-Object System.Xml.XmlWriterSettings
    $xmlSettings.ConformanceLevel = [System.Xml.ConformanceLevel]::Fragment
    $xmlSettings.Indent = $true
    $xw = [System.Xml.XmlWriter]::Create($sw, $xmlSettings)
    $xml.WriteTo($xw)
    $xw.Close()
    return $sw.ToString()
}

function BuildParameterXml ($name, $match, $env, $value, $parameterXmlDocument) 
{
    $existingNode = $parameterXmlDocument.selectNodes("//parameter[@name='$name']")
    $value = $value.Replace("'","'") #Need to make sure any single quotes in the value don't break XPath

    if($existingNode.Count -eq 0){
        #no existing parameter for this transformation
        $newParamter = [xml]("<parameter name=`"" + $name + "`">" +
                    "<parameterEntry kind=`"XmlFile`" scope=`"\\web.config$`" match=`"" + $match + "`" />" +
                    "<parameterValue env=`"" + $env + "`" value=`"`" />" +
                    "</parameter>")
        $newParamter.selectNodes('//parameter/parameterValue').ItemOf(0).SetAttribute('value', $value)
        $imported=$parameterXmlDocument.ImportNode($newParamter.DocumentElement, $true)
        $appendedNode = $parameterXmlDocument.selectNodes('//parameters').ItemOf(0).AppendChild($imported)

    } else {
        #parameter exists but entry is different from an existing entry
        $entryXPath = "//parameter[@name=`"$name`"]/parameterEntry[@kind=`"XmlFile`" and @scope=`"\\web.config$`" and @match=`"$match`"]"
        $existingEntry = $parameterXmlDocument.selectNodes($entryXPath)
        if($existingEntry.Count -eq 0) { throw "There is web.config transformation ($name) that conflicts with an existing parameters.xml entry" }

        #parameter exists but environment value is different from an existing environment value
        $envValueXPath = "//parameter[@name='$name']/parameterValue[@env='$env' and @value='$value']"
        $existingEnvValue = $parameterXmlDocument.selectNodes($envValueXPath)
        $existingEnv = $parameterXmlDocument.selectNodes("//parameter[@name=`"$name`"]/parameterValue[@env=`"$env`"]")

        if($existingEnvValue.Count -eq 0 -and $existingEnv.Count -gt 0) { 
            throw "There is web.config transformation ($name) for this environment ($env) that conflicts with an existing parameters.xml value"
        } elseif ($existingEnvValue.Count -eq 0  -and $existingEnv.Count -eq 0) {
            $newParamter = [xml]("<parameterValue env=`"" + $env + "`" value=`"`" />")
            $newParamter.selectNodes('//parameterValue').ItemOf(0).SetAttribute('value', $value)
            $imported=$parameterXmlDocument.ImportNode($newParamter.DocumentElement, $true)
            $appendedNode = $existingNode.ItemOf(0).AppendChild($imported)
        }
    }
}

function UpdateSetParams ($node, $originalXml, $path, $env, $parametersXml) 
{
    foreach ($childNode in $node.ChildNodes) 
    {
        $xdtValue = ""
        $name = ""
        $match = ($path + $childNode.toString())

        if($childNode.Attributes -and $childNode.Attributes.GetNamedItem('xdt:Locator').Value) {
            $hasMatch = $childNode.Attributes.GetNamedItem('xdt:Locator').Value -match ".?\((.*?)\).*"
            $name = $childNode.Attributes.GetNamedItem($matches[1]).Value
            $match = $match + "[@" + $matches[1] + "=`'" + $name + "`']"
        }

        if($childNode.Attributes -and $childNode.Attributes.GetNamedItem('xdt:Transform')) {
            $xdtValue = $childNode.Attributes.GetNamedItem('xdt:Transform').Value
        }

        if($xdtValue -eq 'Replace') {
            if($childNode.Attributes.GetNamedItem('xdt:Locator').Value) {
                $hasMatch = $childNode.Attributes.GetNamedItem('xdt:Locator').Value -match ".?\((.*?)\).*"
                $name = $childNode.Attributes.GetNamedItem($matches[1]).Value
            } else {
                $name = $childNode.toString()
            }
            $nodeString = Convert-XmlElementToString $childNode.PsObject.Copy()

            BuildParameterXml $name $match $env $nodeString $parametersXml

        } elseif ($xdtValue.Contains('RemoveAttributes')) {

            if($originalXml.selectNodes($match).Count -gt 0) {
                $hasMatch = $xdtValue -match ".?\((.*?)\).*"
                $nodeString = Convert-XmlElementToString $originalXml.selectNodes($match).ItemOf(0).PsObject.Copy() $matches[1].Split(',')

                $newParamter = BuildParameterXml $childNode.toString() $match $env $nodeString $parametersXml

                $newParamters += $newParamter
            }
        } elseif ($xdtValue.Contains('SetAttributes')) { 
            if($originalXml.selectNodes($match).Count -gt 0) {
                $nodeCopy = $originalXml.selectNodes($match).ItemOf(0).PsObject.Copy()
                $hasMatch = $xdtValue -match ".?\((.*?)\).*"
                foreach($attr in $matches[1].Split(',')){
                    $nodeCopy.SetAttribute($attr, $childNode.Attributes.GetNamedItem($attr).Value)
                }
                $nodeString = Convert-XmlElementToString $nodeCopy

                BuildParameterXml $childNode.toString() "($match)[1]" $env $nodeString $parametersXml
            }
        } elseif ($xdtValue) {
            throw "Yikes! the script doesn't know how to handle this transformation!"
        }
        #Recurse into this node to check if it has transformations on its children
        if($childNode) {
            UpdateSetParams $childNode $originalXml ($match + "/") $env $parametersXml
        }
    }
}

function TransformConfigsIntoParamters ($webConfigPath, $webConfigTransformPath, $parametersXml) 
{
    #Parse out the environment names
    $hasMatch = $webConfigTransformPath -match ".?web\.(.*?)\.config.*"
    [xml]$transformXml = Get-Content $webConfigTransformPath
    [xml]$webConfigXml = Get-Content $webConfigPath
    UpdateSetParams $transformXml $webConfigXml '//' $matches[1] $parametersXml
}

$applicationRoot = $ENV:WORKSPACE

if(Test-Path ($applicationRoot + '\parameters.xml')) {
    [xml]$parametersXml = Get-Content ($applicationRoot + '\parameters.xml')
    $parametersNode = $parametersXml.selectNodes('//parameters').ItemOf(0)
} else {
    [System.XML.XMLDocument]$parametersXml=New-Object System.XML.XMLDocument
    [System.XML.XMLElement]$parametersNode=$parametersXml.CreateElement("parameters")
    $appendedNode = $parametersXml.appendChild($parametersNode)
}

TransformConfigsIntoParamters ($applicationRoot + '\web.config') ($applicationRoot + '\web.Development.config') $parametersXml
TransformConfigsIntoParamters ($applicationRoot + '\web.config') ($applicationRoot + '\web.SystemTest.config') $parametersXml
TransformConfigsIntoParamters ($applicationRoot + '\web.config') ($applicationRoot + '\web.Production.config') $parametersXml

$parametersXml.Save($applicationRoot + '\parameters.xml')
Run Code Online (Sandbox Code Playgroud)