File: WebConfigTransform.cs
Web Access
Project: ..\..\..\src\WebSdk\Publish\Tasks\Microsoft.NET.Sdk.Publish.Tasks.csproj (Microsoft.NET.Sdk.Publish.Tasks)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.NET.Sdk.Publish.Tasks.Properties;
 
namespace Microsoft.NET.Sdk.Publish.Tasks
{
    public static class WebConfigTransform
    {
        public static XDocument Transform(XDocument? webConfig, string appName, bool configureForAzure, bool useAppHost, string? extension, string? aspNetCoreModuleName, string? aspNetCoreHostingModel, string? environmentName, string? projectFullPath)
        {
            const string HandlersElementName = "handlers";
            const string aspNetCoreElementName = "aspNetCore";
            const string envVariablesElementName = "environmentVariables";
 
            if (webConfig?.Root == null || webConfig.Root.Name.LocalName != "configuration")
            {
                webConfig = XDocument.Parse(WebConfigTemplate.Template);
            }
 
            XElement? rootElement = null;
 
            // Find the first aspNetCore element. If it is null use the default logic. Else use the root containing the aspNetCore element.
            var firstAspNetCoreElement = webConfig.Root?.Descendants(aspNetCoreElementName).FirstOrDefault();
            if (firstAspNetCoreElement == null)
            {
                rootElement = webConfig.Root?.Element("location") == null ? webConfig.Root : webConfig.Root.Element("location");
            }
            else
            {
                rootElement = firstAspNetCoreElement.Ancestors("location").FirstOrDefault() == null ? webConfig.Root : webConfig.Root?.Element("location");
            }
 
            var webServerSection = GetOrCreateChild(rootElement, "system.webServer");
 
            var handlerSection = GetOrCreateChild(webServerSection, HandlersElementName);
            TransformHandlers(handlerSection, aspNetCoreModuleName);
 
            // aspNetCoreModuleName might not get set if the web.config already has a different module name defined.
            string? aspNetCoreModuleNameFinalValue =
                    (string?)handlerSection?.Elements("add")
                   .FirstOrDefault(e => string.Equals((string?)e.Attribute("name"), "aspnetcore", StringComparison.OrdinalIgnoreCase))?
                   .Attribute("modules");
 
            var aspNetCoreSection = GetOrCreateChild(webServerSection, aspNetCoreElementName);
            TransformAspNetCore(aspNetCoreSection, appName, configureForAzure, useAppHost, extension, aspNetCoreModuleNameFinalValue, aspNetCoreHostingModel, projectFullPath);
            if (!string.IsNullOrEmpty(environmentName))
            {
                TransformEnvironmentVariables(GetOrCreateChild(aspNetCoreSection, envVariablesElementName), environmentName);
            }
 
            // make sure that the aspNetCore element is after handlers element
            var aspNetCoreElement = webServerSection?.Element(HandlersElementName)?
                .ElementsBeforeSelf(aspNetCoreElementName).SingleOrDefault();
            if (aspNetCoreElement != null)
            {
                aspNetCoreElement.Remove();
                webServerSection?.Element(HandlersElementName)?.AddAfterSelf(aspNetCoreElement);
            }
 
            return webConfig;
        }
 
        private static void TransformHandlers(XElement handlersElement, string? aspNetCoreModuleName)
        {
            var aspNetCoreElement =
                handlersElement.Elements("add")
                    .FirstOrDefault(e => string.Equals((string?)e.Attribute("name"), "aspnetcore", StringComparison.OrdinalIgnoreCase));
 
            if (aspNetCoreElement == null)
            {
                aspNetCoreElement = new XElement("add");
                handlersElement.Add(aspNetCoreElement);
            }
 
            if (string.IsNullOrEmpty(aspNetCoreModuleName))
            {
                // This is the default ASP.NET core module.
                aspNetCoreModuleName = "AspNetCoreModule";
            }
 
            aspNetCoreElement.SetAttributeValue("name", "aspNetCore");
            SetAttributeValueIfEmpty(aspNetCoreElement, "path", "*");
            SetAttributeValueIfEmpty(aspNetCoreElement, "verb", "*");
            SetAttributeValueIfEmpty(aspNetCoreElement, "modules", aspNetCoreModuleName);
            SetAttributeValueIfEmpty(aspNetCoreElement, "resourceType", "Unspecified");
        }
 
        private static void TransformAspNetCore(XElement aspNetCoreElement, string appName, bool configureForAzure, bool useAppHost, string? extension, string? aspNetCoreModuleName, string? aspNetCoreHostingModelValue, string? projectFullPath)
        {
            // Forward slashes currently work neither in AspNetCoreModule nor in dotnet so they need to be
            // replaced with backwards slashes when the application is published on a non-Windows machine
            var appPath = Path.Combine(".", appName).Replace("/", "\\");
            RemoveLauncherArgs(aspNetCoreElement);
 
            if (useAppHost)
            {
                appPath = Path.ChangeExtension(appPath, !string.IsNullOrWhiteSpace(extension) ? extension : null);
                aspNetCoreElement.SetAttributeValue("processPath", appPath);
            }
            // For Apps targeting .NET Framework, the extension is always exe. RID is not set for .NETFramework apps with PlatformType set to AnyCPU.
            else if (string.Equals(Path.GetExtension(appPath), ".exe", StringComparison.OrdinalIgnoreCase))
            {
                appPath = Path.ChangeExtension(appPath, ".exe");
                aspNetCoreElement.SetAttributeValue("processPath", appPath);
            }
            else
            {
                aspNetCoreElement.SetAttributeValue("processPath", "dotnet");
 
                // In Xml the order of attributes does not matter but it is nice to have
                // the `arguments` attribute next to the `processPath` attribute
                var argumentsAttribute = aspNetCoreElement.Attribute("arguments");
                argumentsAttribute?.Remove();
                var attributes = aspNetCoreElement.Attributes().ToList();
                var processPathIndex = attributes.FindIndex(a => a.Name.LocalName == "processPath");
                // if the app path is already there in the web.config, don't do anything.
                if (string.Equals(appPath, (string?)argumentsAttribute, StringComparison.OrdinalIgnoreCase))
                {
                    appPath = string.Empty;
                }
                attributes.Insert(processPathIndex + 1,
                    new XAttribute("arguments", (appPath + " " + (string?)argumentsAttribute).Trim()));
 
                aspNetCoreElement.Attributes().Remove();
                aspNetCoreElement.Add(attributes);
            }
 
            SetAttributeValueIfEmpty(aspNetCoreElement, "stdoutLogEnabled", "false");
 
            var logPath = Path.Combine(configureForAzure ? @"\\?\%home%\LogFiles" : @".\logs", "stdout").Replace("/", "\\");
            if (configureForAzure)
            {
                // When publishing for Azure we want to always overwrite path - the folder we set the path to
                // will exist, the path is not easy to customize and stdoutLogPath should be only used for
                // diagnostic purposes
                aspNetCoreElement.SetAttributeValue("stdoutLogFile", logPath);
            }
            else
            {
                SetAttributeValueIfEmpty(aspNetCoreElement, "stdoutLogFile", logPath);
            }
 
            var hostingModelAttributeValue = aspNetCoreElement.Attribute("hostingModel");
 
            string? projectWebConfigPath = null;
            if (!string.IsNullOrEmpty(projectFullPath))
            {
                string? projectFolder = Path.GetDirectoryName(projectFullPath);
                if (projectFolder is not null)
                {
                    projectWebConfigPath = Path.Combine(projectFolder, "web.config");
                }
            }
 
            if (File.Exists(projectWebConfigPath))
            {
                // Set the hostingmodel attribute only if it not already set in the project's web.config.
                if (hostingModelAttributeValue == null)
                {
                    SetAspNetCoreHostingModel(aspNetCoreHostingModelValue, aspNetCoreModuleName, aspNetCoreElement);
                }
            }
            else
            {
                SetAspNetCoreHostingModel(aspNetCoreHostingModelValue, aspNetCoreModuleName, aspNetCoreElement);
            }
        }
 
 
        private static void SetAspNetCoreHostingModel(string? aspNetCoreHostingModelValue, string? aspNetCoreModuleName, XElement aspNetCoreElement)
        {
            if (!string.IsNullOrEmpty(aspNetCoreHostingModelValue))
            {
                switch (aspNetCoreHostingModelValue?.ToUpperInvariant())
                {
                    case "INPROCESS":
                        // In process is not supported for AspNetCoreModule.
                        if (string.Equals(aspNetCoreModuleName, "AspNetCoreModule", StringComparison.OrdinalIgnoreCase))
                        {
                            throw new Exception(Resources.WebConfigTransform_InvalidHostingOption);
                        }
                        aspNetCoreElement.SetAttributeValue("hostingModel", aspNetCoreHostingModelValue);
                        break;
                    case "OUTOFPROCESS":
                        aspNetCoreElement.SetAttributeValue("hostingModel", aspNetCoreHostingModelValue);
                        break;
                    default:
                        throw new Exception(Resources.WebConfigTransform_HostingModel_Error);
                }
            }
        }
 
        private static void TransformEnvironmentVariables(XElement envVariablesElement, string? environmentName)
        {
            var envVariableElement =
                envVariablesElement.Elements("environmentVariable")
                .FirstOrDefault(e => string.Equals((string?)e.Attribute("name"), "ASPNETCORE_ENVIRONMENT", StringComparison.OrdinalIgnoreCase));
 
            if (envVariableElement == null)
            {
                envVariableElement = new XElement("environmentVariable");
                envVariablesElement.Add(envVariableElement);
            }
 
            envVariableElement.SetAttributeValue("name", "ASPNETCORE_ENVIRONMENT");
            envVariableElement.SetAttributeValue("value", environmentName);
        }
 
        private static XElement GetOrCreateChild(XElement? parent, string childName)
        {
            var childElement = parent?.Element(childName);
            if (childElement == null)
            {
                childElement = new XElement(childName);
                parent?.Add(childElement);
            }
            return childElement;
        }
 
        private static void SetAttributeValueIfEmpty(XElement element, string attributeName, string? value)
        {
            element.SetAttributeValue(attributeName, (string?)element.Attribute(attributeName) ?? value);
        }
 
        private static void RemoveLauncherArgs(XElement aspNetCoreElement)
        {
            var arguments = (string?)aspNetCoreElement.Attribute("arguments");
 
            if (arguments != null)
            {
                string[] templatizedLauncherArgs = new string[] { "%LAUNCHER_ARGS%", "-argFile IISExeLauncherArgs.txt" };
                foreach (var templateLauncherArg in templatizedLauncherArgs)
                {
                    var position = 0;
                    while ((position = arguments.IndexOf(templateLauncherArg, position, StringComparison.OrdinalIgnoreCase)) >= 0)
                    {
                        arguments = arguments.Remove(position, templateLauncherArg.Length);
                    }
                }
 
                aspNetCoreElement.SetAttributeValue("arguments", arguments.Trim());
            }
        }
 
        public static XDocument? AddProjectGuidToWebConfig(XDocument? document, string? projectGuid, bool ignoreProjectGuid)
        {
            try
            {
                if (document != null && !string.IsNullOrEmpty(projectGuid))
                {
                    IEnumerable<XComment> comments = document.DescendantNodes().OfType<XComment>();
                    projectGuid = projectGuid?.Trim('{', '}', '(', ')').Trim();
                    string projectGuidValue = string.Format("ProjectGuid: {0}", projectGuid);
                    XComment? projectGuidComment = comments.FirstOrDefault(comment => string.Equals(comment.Value, projectGuidValue, StringComparison.OrdinalIgnoreCase));
                    if (projectGuidComment != null)
                    {
                        if (ignoreProjectGuid)
                        {
                            projectGuidComment.Remove();
                        }
 
                        return document;
                    }
 
                    if (!ignoreProjectGuid)
                    {
                        document?.LastNode?.AddAfterSelf(new XComment(projectGuidValue));
                        return document;
                    }
                }
            }
            catch
            {
                // This code path is only used for telemetry.
            }
 
            return document;
        }
    }
}