File: GenerateStaticWebAssetEndpointsPropsFile.cs
Web Access
Project: src\src\sdk\src\StaticWebAssetsSdk\Tasks\Microsoft.NET.Sdk.StaticWebAssets.Tasks.csproj (Microsoft.NET.Sdk.StaticWebAssets.Tasks)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable disable

using System.Security.Cryptography;
using System.Xml;
using Microsoft.Build.Framework;

namespace Microsoft.AspNetCore.StaticWebAssets.Tasks;

public class GenerateStaticWebAssetEndpointsPropsFile : Task
{
    [Required]
    public string TargetPropsFilePath { get; set; }

    public string PackagePathPrefix { get; set; } = "staticwebassets";

    [Required]
    public ITaskItem[] StaticWebAssets { get; set; }

    [Required]
    public ITaskItem[] StaticWebAssetEndpoints { get; set; }

    public override bool Execute()
    {
        var endpoints = StaticWebAssetEndpoint.FromItemGroup(StaticWebAssetEndpoints);
        var assets = StaticWebAsset.ToAssetDictionary(StaticWebAssets);
        if (!ValidateArguments(endpoints, assets))
        {
            return false;
        }

        return ExecuteCore(endpoints, assets);
    }

    private bool ExecuteCore(StaticWebAssetEndpoint[] endpoints, Dictionary<string, StaticWebAsset> assets)
    {
        if (endpoints.Length == 0)
        {
            return !Log.HasLoggedErrors;
        }

        var itemGroup = new XElement("ItemGroup");
        var orderedAssets = endpoints.OrderBy(e => e.Route, StringComparer.OrdinalIgnoreCase)
            .ThenBy(e => e.AssetFile, StringComparer.OrdinalIgnoreCase);

        foreach (var element in orderedAssets)
        {
            var asset = assets[element.AssetFile];
            var path = asset.ReplaceTokens(asset.RelativePath, StaticWebAssetTokenResolver.Instance, TokenResolveMode.Pack);
            var fullPathExpression = $"""$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\{StaticWebAsset.Normalize(PackagePathPrefix)}\{StaticWebAsset.Normalize(path).Replace("/", "\\")}'))""";

            itemGroup.Add(new XElement(nameof(StaticWebAssetEndpoint),
                new XAttribute("Include", element.Route),
                new XElement(nameof(StaticWebAssetEndpoint.AssetFile), fullPathExpression),
                new XElement(nameof(StaticWebAssetEndpoint.Selectors), new XCData(StaticWebAssetEndpointSelector.ToMetadataValue(element.Selectors))),
                new XElement(nameof(StaticWebAssetEndpoint.EndpointProperties), new XCData(StaticWebAssetEndpointProperty.ToMetadataValue(element.EndpointProperties))),
                new XElement(nameof(StaticWebAssetEndpoint.ResponseHeaders), new XCData(StaticWebAssetEndpointResponseHeader.ToMetadataValue(element.ResponseHeaders)))));
        }

        var document = new XDocument(new XDeclaration("1.0", "utf-8", "yes"));
        var root = new XElement("Project", itemGroup);

        document.Add(root);

        var settings = new XmlWriterSettings
        {
            Encoding = Encoding.UTF8,
            CloseOutput = false,
            OmitXmlDeclaration = true,
            Indent = true,
            NewLineOnAttributes = false,
            Async = true
        };

        using var memoryStream = new MemoryStream();
        using (var xmlWriter = XmlWriter.Create(memoryStream, settings))
        {
            document.WriteTo(xmlWriter);
        }

        var data = memoryStream.ToArray();
        WriteFile(data);

        return !Log.HasLoggedErrors;
    }

    private void WriteFile(byte[] data)
    {
        var dataHash = ComputeHash(data);
        var fileExists = File.Exists(TargetPropsFilePath);
        var existingFileHash = fileExists ? ComputeHash(File.ReadAllBytes(TargetPropsFilePath)) : "";

        if (!fileExists)
        {
            Log.LogMessage(MessageImportance.Low, $"Creating file '{TargetPropsFilePath}' does not exist.");
            File.WriteAllBytes(TargetPropsFilePath, data);
        }
        else if (!string.Equals(dataHash, existingFileHash, StringComparison.Ordinal))
        {
            Log.LogMessage(MessageImportance.Low, $"Updating '{TargetPropsFilePath}' file because the hash '{dataHash}' is different from existing file hash '{existingFileHash}'.");
            File.WriteAllBytes(TargetPropsFilePath, data);
        }
        else
        {
            Log.LogMessage(MessageImportance.Low, $"Skipping file update because the hash '{dataHash}' has not changed.");
        }
    }

    private static string ComputeHash(byte[] data)
    {
#if !NET9_0_OR_GREATER
        using var sha256 = SHA256.Create();
        var result = sha256.ComputeHash(data);
#else
        var result = SHA256.HashData(data);
#endif
        return Convert.ToBase64String(result);
    }

    private bool ValidateArguments(StaticWebAssetEndpoint[] endpoints, Dictionary<string, StaticWebAsset> asset)
    {
        var valid = true;
        foreach (var endpoint in endpoints)
        {
            if (!asset.ContainsKey(endpoint.AssetFile))
            {
                Log.LogError($"The asset file '{endpoint.AssetFile}' specified in the endpoint '{endpoint.Route}' does not exist.");
                valid = false;
            }
        }

        return valid;
    }
}