【问题标题】:Build Once, Deploy Everywhere with Web Deploy and .NET Web.configs使用 Web Deploy 和 .NET Web.configs 构建一次,随处部署
【发布时间】:2018-01-12 15:40:11
【问题描述】:

我正在建立一个持续构建和部署系统,该系统将管理我们的 .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 Deploy 包?

【问题讨论】:

    标签: deployment msbuild web-config webdeploy


    【解决方案1】:

    在 .NET 4.7.1 中,可以使用另一个选项:使用 ConfigurationBuilder

    这个想法是自定义类有机会在将 web.config 中包含的值传递给应用程序之前对其进行操作。这允许插入其他配置系统。

    例如:使用与 ASP.NET Core 类似的配置方法,它包含的 NuGet 包可以在 .NET Framework 上独立使用,也可以加载 json 和覆盖 json 文件。然后可以使用环境变量(或任何其他值,如 IIS 应用程序池 ID、机器名称等)来确定要使用的覆盖 json 文件。

    例如:如果有一个appsettings.json 类似的文件

    {
      "appSettings": { "Foo": "FooValue", "Bar": "BarValue" }
    }
    

    和一个appsettings.Production.json 文件包含

    {
      "appSettings": { "Foo": "ProductionFooValue" }
    }
    

    可以写一个像这样的配置构建器

    public class AppSettingsConfigurationBuilder : ConfigurationBuilder
    {
        public override ConfigurationSection ProcessConfigurationSection(ConfigurationSection configSection)
        {
            if(configSection is AppSettingsSection appSettingsSection)
            {
                var appSettings = appSettingsSection.Settings;
    
                var environmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production";
                var appConfig = new ConfigurationBuilder()
                  .AddJsonFile("appsettings.json", optional: false)
                  .AddJsonFile($"appsettings.{environmentName}.json", optional: true)
                  .Build();
    
                appSettings.Add("Foo", appConfig["appSettings:Foo"]);
                appSettings.Add("Bar", appConfig["appSettings:Bar"]);
    
            }
    
            return configSection;
        }
    }
    

    然后在Web.config 中连接配置生成器:

    <configSections>
      <section name="configBuilders" type="System.Configuration.ConfigurationBuildersSection, System.Configuration, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" restartOnExternalChanges="false" requirePermission="false"/>
    </configSections>
    
    <configBuilders>
      <builders>
        <add name="AppSettingsConfigurationBuilder" type="My.Project.AppSettingsConfigurationBuilder, My.Project"/>
      </builders>
    </configBuilders>
    
    <appSettings configBuilders="AppSettingsConfigurationBuilder" />
    

    如果您随后在开发机器上将 ASPNETCORE_ENVIRONMENT(仅选择名称,以便同一服务器上的 ASP.NET Core 应用程序使用相同的默认值)环境变量设置为 DevelopmentConfigurationManager.AppSettings["Foo"] 将看到 @987654334 @ 而不是 FooProductionValue

    您还可以使用APP_POOL_ID 对环境名称进行硬编码,或使用IIS 10 功能对set environment variables on app pools 进行编码。这样,您可以真正构建一次并将相同的输出复制到不同的服务器,甚至复制到同一服务器上的多个目录,并且仍然为不同的服务器使用不同的配置。

    【讨论】:

    • 这是个好主意,尤其是对于我们可以进行代码更改的新应用程序。不幸的是,我们需要为我们正在维护的许多应用程序创建构建版本,并且我们不希望在执行此操作时更改任何代码。
    【解决方案2】:

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

    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("'","&apos;") #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')
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2017-10-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-06-04
      • 2021-05-28
      相关资源
      最近更新 更多