nil4 / dotnet-transform-xdt

Modern .NET tools and library for XDT (Xml Document Transformation)
Apache License 2.0
118 stars 12 forks source link

Duplicate <rule> in the <rewrite><rules> section. #35

Closed cosmoKenney closed 5 years ago

cosmoKenney commented 5 years ago

I've been using this tool for a while now with no issues. But out of the blue it has started adding duplicate <rule> nodes to the <rewrite><rules> section. I haven't even updated the xdt package version.

My web.config in the project looks like this:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <handlers>
      <remove name="aspNetCore" />
      <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" />
    </handlers>
    <aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="true" stdoutLogFile=".\logs\stdout">
      <environmentVariables>
        <environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Development" />
        <environmentVariable name="ASPNETCORE_HTTPS_PORT" value="44350" />
      </environmentVariables>
    </aspNetCore>
    <rewrite>
      <rules></rules>
    </rewrite>
  </system.webServer>
</configuration>

And my web.Staging.config in the project looks like this:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
    <system.webServer>

        <aspNetCore>
            <environmentVariables>
                <environmentVariable name="ASPNETCORE_ENVIRONMENT"
                                     value="Staging"
                                     xdt:Transform="SetAttributes"
                                     xdt:Locator="Match(name)"/>
            </environmentVariables>
        </aspNetCore>

        <rewrite>
            <rules>
                <rule name="HTTP to HTTPS redirect -- Staging"
                      stopProcessing="true"
                      xdt:Transform="Insert">
                    <match url="(.*)" />
                    <conditions>
                        <add input="{HTTPS}"
                             pattern="off"
                             ignoreCase="true" />
                    </conditions>
                    <action type="Redirect"
                            redirectType="Permanent"
                            url="https://{HTTP_HOST}/{R:1}" />
                </rule>
            </rules>
        </rewrite>

    </system.webServer>
</configuration>

And the deployed web.config that get published to our staging server via AzDevOps pipeline looks like this:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <handlers>
      <remove name="aspNetCore" />
      <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" />
    </handlers>
    <aspNetCore processPath="dotnet" arguments=".\MyCompany.MyProject.dll" stdoutLogEnabled="true" stdoutLogFile=".\logs\stdout">
      <environmentVariables>
        <environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Staging" />
        <environmentVariable name="ASPNETCORE_HTTPS_PORT" value="44350" />
      </environmentVariables>
    </aspNetCore>
    <rewrite>
      <rules>

        <rule name="HTTP to HTTPS redirect -- Staging" stopProcessing="true">
          <match url="(.*)"/>
          <conditions>
            <add input="{HTTPS}" pattern="off" ignoreCase="true"/>
          </conditions>
          <action type="Redirect" redirectType="Permanent" url="https://{HTTP_HOST}/{R:1}"/>
        </rule>

        <rule name="HTTP to HTTPS redirect -- Staging" stopProcessing="true">
          <match url="(.*)"/>
          <conditions>
            <add input="{HTTPS}" pattern="off" ignoreCase="true"/>
          </conditions>
          <action type="Redirect" redirectType="Permanent" url="https://{HTTP_HOST}/{R:1}"/>
        </rule>

      </rules>
    </rewrite>
  </system.webServer>
</configuration>
<!--ProjectGuid: 546334BB-0661-42B6-844E-31DC3677C6C1-->

The MyCompany.MyProject.csproj file has this:

    <ItemGroup>
        <DotNetCliToolReference Include="Microsoft.DotNet.Xdt.Tools" Version="2.0.0" />
    </ItemGroup>

    <Target Name="ApplyXdtConfigTransform" BeforeTargets="_TransformWebConfig">
        <PropertyGroup>
            <_SourceWebConfig>$(MSBuildThisFileDirectory)Web.config</_SourceWebConfig>
            <_XdtTransform>$(MSBuildThisFileDirectory)Web.$(Configuration).config</_XdtTransform>
            <_TargetWebConfig>$(PublishDir)Web.config</_TargetWebConfig>
        </PropertyGroup>
        <Exec Command="dotnet transform-xdt --xml &quot;$(_SourceWebConfig)&quot; --transform &quot;$(_XdtTransform)&quot; --output &quot;$(_TargetWebConfig)&quot;" Condition="Exists('$(_XdtTransform)')" />
    </Target>

Is there anything that stands out about this setup?

cosmoKenney commented 5 years ago

Update: I just changed the xdt:Transform="Insert" to xdt:Transform="Replace" And all I get is the original <rule> from web.config:

    <rewrite>
      <rules></rules>
    </rewrite>
nil4 commented 5 years ago

@cosmoKenney that's certainly unexpected! You mentioned this worked before, so I'm wondering if it may be related to ASP.NET Core 2.2 changes, akin to https://github.com/aspnet/AspNetCore/issues/4364.

I think SDK version 2.2 started applying web.config transforms itself (while earlier versions did not), and the transforms may end up applied twice; once by the SDK and once by dotnet-transform-xdt. To confirm, try adding the following property to your .csproj file:

<IsWebConfigTransformDisabled>true</IsWebConfigTransformDisabled>

If that does not help, and you still the same behavior, could you publish a small repro project somewhere on GitHub that I can look at?

Another thing that might help is to run dotnet build -v diag >build.log, which will output a very detailed log to the build.log file. You can search for references to web.config, or web.staging.config in the log file to check if transforms were applied more than once.

geraparra commented 5 years ago

I have the same problem, any solution?

netcore 2.1

nil4 commented 5 years ago

@geraparra the only way we're going to get closer to a solution is if someone finds (and shares) a minimal repro that shows the issue is with dotnet-transform-xdt.

@cosmoKenney the XDT you shared above includes this rule:

<rule name="HTTP to HTTPS redirect -- Staging"
                      stopProcessing="true"
                      xdt:Transform="Insert">

And it looks like the transform ran twice, because the <rule> block appears twice in the output.

Why that happens is unclear, and it's impossible to figure out if the problem is related to dotnet-transform-xdt, or the .NET SDK, or ASP.NET SDK, or Azure DevOps, or a combination of these.

We need a minimal, complete, verifiable example involving just dotnet-transform-xdt.

cosmoKenney commented 5 years ago

@nil4 thanks for the reply. I'm going to try the IsWebConfigTransformDisabled entry in my csproj right now. And you are right that I think during the publish step in both a WebDeploy via visual studio or via an Azure DevOps pipeline, the project is built twice for some reason. And that is not isolated to this one project. I have three .net core projects that do the same thing. @geraparra monitor this thread as I'll update with my findings in about 10 minutes.

cosmoKenney commented 5 years ago

@nil4, @geraparra, Neither IsWebConfigTransformDisabled or IsTransformWebConfigDisabled worked. It only added one entry to the <rules>, but caused the site to throw a 502.5 with error code 0x80070002. I gather the dotnet-xdt-transform isn't required for the web.config anymore. So I may try to eliminate it tomorrow. I'll update with the results. ... time to walk the dogs and eat dinner. ;-)

nil4 commented 5 years ago

@cosmoKenney thanks for the update! I'm going to mark this issue external (tentatively) since it looks like SDK or AzDevOps changes are the most likely root cause.

nil4 commented 5 years ago

@cosmoKenney I hope you were able to track down the cause of the duplicate transform. I'm going to close this issue now because, as far as I see, there's nothing to fix in dotnet-transform-xdt. If you have data that shows otherwise, let me know and I'll reopen.

cosmoKenney commented 5 years ago

@nil4 in case anyone comes here to find the solution, simply removing the xdt:Transform="Insert" from the node that is being transformed seems to have fixed the issue.

cosmoKenney commented 5 years ago

@nil4 I thought I had this fixed in azure dev ops which is how I publish to qa after a commit. But when I publish to production I use visual studio publish via WebDeploy and the publish doesn't seem to run the built-in transforms on web.config. So taking the dot-net-transform-xdt out of the .cspoj to fix for azure devops breaks webdeploy. So I'm a little stumped.

nil4 commented 5 years ago

@cosmoKenney I'm sorry to hear that, but unfortunately I don't have much experience with Azure and the way it publishes apps. The one thing I can suggest is to enable verbose MSBuild and dotnet-transform-xdt logs during publish, which may help figure out what is going on.

I don't know exactly how it's done in Azure dev ops, but if you were using plain dotnet or MSBuild to publish, you'd need to pass on the command line something like /verbosity:d for detailed, or /verbosity:diag for diagnosis (fair warning: diag will produce very large logs).

You should then be able to search the logs for keywords like xdt or web.release.config (the name of your transform file) to hopefully get some clue as to what is the difference between qa and production.

Similarly, you can add --verbose to the invocation of dotnet-transform-xdt in your project file. For example, in the xdt-samples repo, changing this line:

- <Exec Command="dotnet transform-xdt --xml &quot;$(_SourceWebConfig)&quot; --transform &quot;$(_XdtTransform)&quot; --output &quot;$(_TargetWebConfig)&quot;" Condition="Exists('$(_XdtTransform)')" />
+ <Exec Command="dotnet transform-xdt --verbose --xml &quot;$(_SourceWebConfig)&quot; --transform &quot;$(_XdtTransform)&quot; --output &quot;$(_TargetWebConfig)&quot;" Condition="Exists('$(_XdtTransform)')" />

During publishing, when dotnet-transform-xdt is invoked, this will produce output similar to:

  [XDT] Transforming 'Web.config' using 'Web.release.config' into 'bin\release\netcoreapp2.0\publish\Web.config'
  [XDT] Verbose: Start Executing Insert (transform line 4, 19)
  [XDT] Verbose: on /configuration/system.webServer/security
  [XDT] Verbose: Applying to 'system.webServer' element (source line 3, 4)
  [XDT] Verbose: Inserted 'security' element
  [XDT] Verbose: End Done executing Insert
  [XDT] Verbose: Start Executing Insert (transform line 10, 23)
  [XDT] Verbose: on /configuration/system.webServer/httpProtocol
  [XDT] Verbose: Applying to 'system.webServer' element (source line 3, 4)
  [XDT] Verbose: Inserted 'httpProtocol' element
  [XDT] Verbose: End Done executing Insert
  [XDT] Verbose: Start Executing Insert (transform line 16, 18)
  [XDT] Verbose: on /configuration/system.webServer/rewrite
  [XDT] Verbose: Applying to 'system.webServer' element (source line 3, 4)
  [XDT] Verbose: Inserted 'rewrite' element
  [XDT] Verbose: End Done executing Insert

The lines prefixed with [XDT] are logged by dotnet-transform-xdt, describing which transformations are applied by this tool, and what it does exactly, in minute detail.

I wish I could offer more relevant advice, but unless we have an standalone repro that I can look at, involving just dotnet-transform-xdt, there's simply no way for me to figure out what may happening at your end. There are too many variables and tools involved to try to guess what might be the cause.

nil4 commented 5 years ago

@cosmoKenney another idea, can it be that the .NET SDK version is different between qa and production? Can you try inserting a call to dotnet --version in your project file, near the place where dotnet-transform-xdt is called, and compare the values from each environment?

As you previously discovered that the SDK changed, and started applying web.config transforms, it may be the case that on qa you have a different/newer version (which applies transforms itself), vs. an older on production (which does not).

cosmoKenney commented 5 years ago

@nil4 Okay, I'll will look into creating a repo for repro. But in the mean time, is it possible to qualify the xdt transform tool to only run during a production/release build within the csproj file?

BTW, I read some info about placement of the transform attributes and was hoping you could help. One source recommended not applying transform to individual rules, but rather the entire rules node.

So in web.release.config or web.staging.confg, the rules section would look something like this:

<system.webServer>
    <rewrite>
        <rules> <!-- do I place xdt:Transform here? -->
            <rule name="SpecificRewrite" stopProcessing="true"> <!-- or do I place xtd:Transform here? -->

See the comments in the above snippet. I.e. where is the xdt:Transform supported?

nil4 commented 5 years ago

@cosmoKenney yes, it is possible to qualify the transform to run, based on MSBuild variables available in your project. One simple example is applying a transform only when a Release configuration is built:

<Exec Command="dotnet transform-xdt [...]" Condition=" '$(Configuration)' == 'Release' " />

If you run dotnet publish -c Debug, the transform won't be applied. But when dotnet publish -c Release runs, it will.

You can use any other property in conditions, including ones you pass explicitly during a build. Here's one example:

<Exec Command="dotnet transform-xdt [..options1..]" Condition=" '$(MyVar)' == 'Option1' " />
<Exec Command="dotnet transform-xdt [..options2..]" Condition=" '$(MyVar)' == 'Option2' " />

Now, when you run dotnet publish /p:MyVar=Option1, the first transform runs. If you run dotnet publish /p:MyVar=Option2, the second runs. Otherwise, none runs, because the conditions do not match either.

The placement of the xdt:Transform attributes depends a lot on the scenario, and what the value of the attribute is. When you're using xdt:Transform="Replace", for example to replace all rewrite rules for an environment, the attribute should be placed on the node to be replaced.

A different use case is when, for example, you don't want to replace all rules, but perhaps to insert/replace just one specific rule. In that case it's usually required to also use the xdt:Locator attribute to specify which of the rules is to be replaced.

Say the base Web.config looks like:

<rewrite>
  <rules>
    <rule name="rule1">[...]</rule>
    <rule name="rule2">[...]</rule>
  </rules>
</rewrite>

If you want to modify just rule2, leaving everything else as-is, you would use a transform like:

<rewrite>
  <rules>
    <rule name="rule2" xdt:Locator="Match(name)" xdt:Transform="Replace">[...]</rule>
  </rules>
<rewrite>
</rewrite>

The Match(name) locator attribute makes this transform match just the rule named rule2 in the base Web.config file, and none other.

I would encourage you to install the SlowCheetah VS extension which allows you to preview the result of an XDT transform in Visual Studio. It is an excellent tool that shows a diff between the base file, and the file with the transform applied. Here's a quick guide on how it works: https://github.com/Microsoft/slow-cheetah/blob/master/doc/transforming_files.md#getting-started