dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.49k stars 10.04k forks source link

Web.config transformations not working when deploying Blazor WebAssembly application to Production and Staging Environments #35584

Open BenjaminCharlton opened 3 years ago

BenjaminCharlton commented 3 years ago

Describe the bug

I'm not 100% sure if this is a problem with Visual Studio's publishing wizard, or a problem with Blazor, or just something that I did wrong.

I asked on StackOverflow and learned some useful things from the community but we have not solved the problem so far.

Summary I want to apply a web.config transformation when publishing my Blazor WebAssembly application to my Staging and Production environments via FTP using the Visual Studio Publish Wizard, as described here

I want the transformation to add a custom header containing the blazor-environment variable as documented here

Naturally, I don't want the addition of a web.config file to break my local development environment either (I've noticed that's exactly what happens if you just paste the auto-generated web.config from the production/staging environment into the project root).

No matter what I try, my web.config transformations don't get applied when I publish the site, and publishing takes an hour each time, so progress is slow!

To Reproduce

You can view a minimal reproducible sample here, or follow the steps below. 1) Create a new Blazor WebAssembly project. 2) Edit Index.razor so it looks like this:

@page "/"
@inject Microsoft.AspNetCore.Components.WebAssembly.Hosting.IWebAssemblyHostEnvironment Environment

<h1>Hello, world!</h1>

<p>Environment: @Environment.Environment</p>

3) In configuration manager: i) Rename "Debug" to "Development" ii) Rename "Release" to "Production" ii) Add a new configuration called "Staging", based on "Production" 4) Right click on your project in Solution Explorer > Add new item > Add a web.config file. The default one is mostly blank, except for the XML declaration, a comment, a pair of configuration tags. Leave it alone because, in my experience, adding anything to it breaks the project when debugging locally. Check that your project still builds and runs locally. It should. 5) Right click on your project and select "Publish". 6) Add a new FTP publish profile called "Staging" and save some details of an FTP server you can use as a staging environment. Specify the configuration "Staging" for this profile. Don't publish yet; just save the profile. Mine looks like this:

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <EnvironmentName>Staging</EnvironmentName>
    <WebPublishMethod>FTP</WebPublishMethod>
    <LaunchSiteAfterPublish>True</LaunchSiteAfterPublish>
    <LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
    <LastUsedPlatform>Any CPU</LastUsedPlatform>
    <SiteUrlToLaunchAfterPublish>temporaryurl1.myhost.net</SiteUrlToLaunchAfterPublish>
    <ExcludeApp_Data>False</ExcludeApp_Data>
    <ProjectGuid>12c14c2e-4d13-4e23-bf64-8e92faf909e9</ProjectGuid>
    <publishUrl>ftp.myhost.net</publishUrl>
    <DeleteExistingFiles>True</DeleteExistingFiles>
    <FtpPassiveMode>True</FtpPassiveMode>
    <FtpSitePath>myproject-stag</FtpSitePath>
    <UserName>user</UserName>
    <_SavePWD>True</_SavePWD>
    <TargetFramework>net5.0</TargetFramework>
    <SelfContained>false</SelfContained>
  </PropertyGroup>
</Project>

7) Add a new FTP publish profile called "Production" and save some details of an FTP server you can use as a production environment. Specify the configuration "Production" for this profile. Don't publish yet; just save the profile. Mine looks like this:

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <EnvironmentName>Production</EnvironmentName>
    <WebPublishMethod>FTP</WebPublishMethod>
    <LaunchSiteAfterPublish>True</LaunchSiteAfterPublish>
    <LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
    <LastUsedPlatform>Any CPU</LastUsedPlatform>
    <SiteUrlToLaunchAfterPublish>temporaryurl2.myhost.net</SiteUrlToLaunchAfterPublish>
    <ExcludeApp_Data>False</ExcludeApp_Data>
    <ProjectGuid>12c14c2e-4d13-4e23-bf64-8e92faf909e9</ProjectGuid>
    <publishUrl>ftp.myhost.net</publishUrl>
    <DeleteExistingFiles>True</DeleteExistingFiles>
    <FtpPassiveMode>True</FtpPassiveMode>
    <FtpSitePath>myproject-stag</FtpSitePath>
    <UserName>user</UserName>
    <_SavePWD>True</_SavePWD>
    <TargetFramework>net5.0</TargetFramework>
    <SelfContained>false</SelfContained>
  </PropertyGroup>
</Project>

8) Right click on each of the new .pubxml profiles in turn and choose "Add config transform". You should now have a web.Staging.config and a web.Production.config with some boilerplate code. 9) Paste this into web.Staging.config:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
    <system.webServer xdt:Transform="InsertIfMissing">
        <httpProtocol xdt:Transform="InsertIfMissing">
            <customHeaders xdt:Transform="InsertIfMissing">
                <add name="blazor-environment"
                     value="Staging"
                     xdt:Locator="Match(name)"
                     xdt:Transform="SetAttributes" />
            </customHeaders>
        </httpProtocol>
    </system.webServer>
</configuration>

10) Paste this into web.Production.config:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
    <system.webServer xdt:Transform="InsertIfMissing">
        <httpProtocol xdt:Transform="InsertIfMissing">
            <customHeaders xdt:Transform="InsertIfMissing">
                <add name="blazor-environment"
                     value="Production"
                     xdt:Locator="Match(name)"
                     xdt:Transform="SetAttributes" />
            </customHeaders>
        </httpProtocol>
    </system.webServer>
</configuration>

11) Publish the Blazor application using each of the profiles "Production" and "Staging".

What I hoped would happen

What actually happened

Something else I tried that didn't work

I have also tried the above steps but with a complete web.config file in the project root (I got the content off a Blazor WASM application that was published to IIS. This prevents the project running at all in the development environment but it does at least run in Production and Staging. Still, though, no transformations got applied to web.config so the home page says "Environment: Production" in both Production and Staging environments.

pranavkm commented 3 years ago

Thanks for your feedback. Blazor WebAssembly currently does not support web.config transform. We'll consider this an enhancement for the backlog. For the time being, we would recommend using a custom web.config. The default contents for the web.config that Blazor adds (using the output you get from publishing the app).

ghost commented 3 years ago

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

dsoltesz commented 3 years ago

Were you ever able to get a web.config that allows you to still run locally?

BenjaminCharlton commented 3 years ago

For a stand-alone Blazor WASM project, you should be able to do this at build time (rather than at publish time) if you use the SlowCheetah library as described on the accepted answer on my StackOverflow question.

But, if your project uses Blazor WASM with pre-compilation on the server (as described here), as mine now does, then I still haven't yet found the solution, and would love to know.

ghost commented 3 years ago

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

MizardX commented 2 years ago

As a workaround for adding the blazor-environment header. You can add a file called Directory.build.targets in the blazor project, with the content:

<Project>
  <UsingTask
    TaskName="AddBlazorEnvironment"
    TaskFactory="RoslynCodeTaskFactory"
    AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll" >
    <ParameterGroup>
      <Template ParameterType="System.String" Required="true" />
      <Target ParameterType="System.String" Required="true" />
      <Environment ParameterType="System.String" Required="true" />
    </ParameterGroup>
    <Task>
      <Reference Include="$(OutputPath)System.Xml.XDocument.dll" />
      <Using Namespace="System.Xml.Linq" />
      <Code Type="Fragment" Language="CS">
        <![CDATA[
        XElement GetOrAdd(XElement elem, string name) {
          var child = elem.Element(name);
          if (child == null) {
            elem.Add(child = new XElement(name));
          }
          return child;
        }

        var doc = XDocument.Load(Template);

        var elem = doc.Root;
        elem = GetOrAdd(elem, "system.webServer");
        elem = GetOrAdd(elem, "httpProtocol");
        elem = GetOrAdd(elem, "customHeaders");

        const string HEADER_NAME = "blazor-environment";
        var add = elem
          .Elements("add")
          .FirstOrDefault(e => e.Attribute("name")?.Value == HEADER_NAME);
        if (add == null) {
          elem.Add(add = new XElement("add",
            new XAttribute("name", HEADER_NAME)
          ));
        }
        add.SetAttributeValue("value", Environment);

        doc.Save(Target);
        ]]>
      </Code>
    </Task>
  </UsingTask>

  <Target Name="TrasformBlazorWebConfigFile"
          AfterTargets="_AddBlazorWebConfigFile">
    <ItemGroup>
      <BlazorTemplateWebConfig Include="@(ResolvedFileToPublish)" Condition=" '%(Filename)%(Extension)' == 'BlazorWasm.web.config' " />
      <BlazorTransformedWebConfig Include="@(BlazorTemplateWebConfig->'$(IntermediateOutputPath)web.config')" />
    </ItemGroup>

    <AddBlazorEnvironment
      Condition="'@(BlazorTemplateWebConfig)' != ''"
      Template="@(BlazorTemplateWebConfig)"
      Target="@(BlazorTransformedWebConfig)"
      Environment="$(EnvironmentName)"
    />

    <ItemGroup Condition="'@(BlazorTemplateWebConfig)' != ''">
      <ResolvedFileToPublish Remove="@(BlazorTemplateWebConfig)" />
      <ResolvedFileToPublish
        Include="@(BlazorTransformedWebConfig)"
        ExcludeFromSingleFile="true"
        CopyToPublishDirectory="PreserveNewest"
        RelativePath="web.config" />
    </ItemGroup>
  </Target>
</Project>

It assumes you don't have an existing web.config file, and intercepts the creation of the default one.

BenjaminCharlton commented 2 years ago

Holy moley, Markus, this looks perfect! I haven't yet had time to test it out but I shall give it a try as soon as I can.

Thank you very much for thinking of me!

ajamrozek commented 1 year ago

@MizardX I tried this solution but not sure how to inject that Environment param The "AddBlazorEnvironment" task was not given a value for the required parameter "Environment". Is there any movement on the transformation file approach?

MizardX commented 1 year ago

@ajamrozek

@MizardX I tried this solution but not sure how to inject that Environment param The "AddBlazorEnvironment" task was not given a value for the required parameter "Environment". Is there any movement on the transformation file approach?

You can add the parameter -p:EnvironmentName=Development to the arguments when building or publishing. When building in Visual Studio, it should get added automatically.

Tailslide commented 11 months ago

There should be some kind of publish error if this is not supported. Huge time waster.

ezelargo commented 9 months ago

It is unfortunate that it is not compatible, year 2024 and it still does not work....