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

时间:2018-01-12 15:40:12

标签: deployment msbuild web-config webdeploy

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

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

  1. 使用Web Deploy参数化功能在部署时更改配置文件。这将取代我们希望避免的web.config转换。
  2. 每个项目配置/ web.config转换运行一次MSBuild,以生成包含每个环境的转换web.config的包。这对我们的软件包增加构建时间和存储要求有不利影响。
  3. 同时使用Web Deploy参数化和web.config转换。这允许开发人员继续使用web.configs来调试其他环境并避免创建多个包,但要求我们在多个位置维护配置设置。
  4. 在构建时,使用web.config转换生成多个配置文件,但只生成一个包,并在部署时使用脚本将正确的配置插入包中的正确位置。这说起来容易做起来难,因为它不是Web Deploy的工作方式,而且我们的初始评估看起来很难实现。
  5. 我们还没有考虑过其他选择吗?有没有办法让我们继续使用web.configs,但只生成一个Web Deploy包?

2 个答案:

答案 0 :(得分:2)

在.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(仅在同一服务器上选择的名称将使用相同的默认值)环境变量设置为Development,{{1会看到ConfigurationManager.AppSettings["Foo"]而不是FooValue

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

答案 1 :(得分:0)

我不知道它是否比上面的选项4复杂,但我们要解决的问题是在运行MSBuild之前立即运行PowerShell脚本,该脚本解析web.config转换并生成或扩充参数。 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("'","&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')