diff --git a/src/StaticWebAssetsSdk/Tasks/ApplyCompressionNegotiation.cs b/src/StaticWebAssetsSdk/Tasks/ApplyCompressionNegotiation.cs index cc9cea511788..d4302c136ffd 100644 --- a/src/StaticWebAssetsSdk/Tasks/ApplyCompressionNegotiation.cs +++ b/src/StaticWebAssetsSdk/Tasks/ApplyCompressionNegotiation.cs @@ -21,7 +21,7 @@ public class ApplyCompressionNegotiation : Task public override bool Execute() { - var assetsById = CandidateAssets.Select(StaticWebAsset.FromTaskItem).ToDictionary(a => a.Identity); + var assetsById = StaticWebAsset.ToAssetDictionary(CandidateAssets); var endpointsByAsset = CandidateEndpoints.Select(StaticWebAssetEndpoint.FromTaskItem) .GroupBy(e => e.AssetFile) diff --git a/src/StaticWebAssetsSdk/Tasks/CollectStaticWebAssetsToCopy.cs b/src/StaticWebAssetsSdk/Tasks/CollectStaticWebAssetsToCopy.cs index 6190bed80c21..9b3f235bb69f 100644 --- a/src/StaticWebAssetsSdk/Tasks/CollectStaticWebAssetsToCopy.cs +++ b/src/StaticWebAssetsSdk/Tasks/CollectStaticWebAssetsToCopy.cs @@ -25,7 +25,7 @@ public override bool Execute() var normalizedOutputPath = StaticWebAsset.NormalizeContentRootPath(Path.GetFullPath(OutputPath)); try { - foreach (var asset in Assets.Select(StaticWebAsset.FromTaskItem)) + foreach (var asset in StaticWebAsset.FromTaskItemGroup(Assets)) { string fileOutputPath = null; if (!(asset.IsDiscovered() || asset.IsComputed())) diff --git a/src/StaticWebAssetsSdk/Tasks/Compression/DiscoverPrecompressedAssets.cs b/src/StaticWebAssetsSdk/Tasks/Compression/DiscoverPrecompressedAssets.cs index d3f4c5ee87b0..8c3ed69a038d 100644 --- a/src/StaticWebAssetsSdk/Tasks/Compression/DiscoverPrecompressedAssets.cs +++ b/src/StaticWebAssetsSdk/Tasks/Compression/DiscoverPrecompressedAssets.cs @@ -29,7 +29,7 @@ public override bool Execute() return true; } - var candidates = CandidateAssets.Select(StaticWebAsset.FromTaskItem).ToArray(); + var candidates = StaticWebAsset.FromTaskItemGroup(CandidateAssets); var assetsToUpdate = new List<ITaskItem>(); var candidatesByIdentity = candidates.ToDictionary(asset => asset.Identity, OSPath.PathComparer); diff --git a/src/StaticWebAssetsSdk/Tasks/Compression/ResolveCompressedAssets.cs b/src/StaticWebAssetsSdk/Tasks/Compression/ResolveCompressedAssets.cs index e1c61a6b9e05..c3f34546bd31 100644 --- a/src/StaticWebAssetsSdk/Tasks/Compression/ResolveCompressedAssets.cs +++ b/src/StaticWebAssetsSdk/Tasks/Compression/ResolveCompressedAssets.cs @@ -54,8 +54,8 @@ public override bool Execute() return true; } - var candidates = CandidateAssets.Select(StaticWebAsset.FromTaskItem).ToArray(); - var explicitAssets = ExplicitAssets?.Select(StaticWebAsset.FromTaskItem).ToArray() ?? []; + var candidates = StaticWebAsset.FromTaskItemGroup(CandidateAssets).ToArray(); + var explicitAssets = ExplicitAssets == null ? [] : StaticWebAsset.FromTaskItemGroup(ExplicitAssets); var existingCompressionFormatsByAssetItemSpec = CollectCompressedAssets(candidates); var includePatterns = SplitPattern(IncludePatterns); diff --git a/src/StaticWebAssetsSdk/Tasks/ComputeEndpointsForReferenceStaticWebAssets.cs b/src/StaticWebAssetsSdk/Tasks/ComputeEndpointsForReferenceStaticWebAssets.cs index cc6d09cc5101..f7ad1954acab 100644 --- a/src/StaticWebAssetsSdk/Tasks/ComputeEndpointsForReferenceStaticWebAssets.cs +++ b/src/StaticWebAssetsSdk/Tasks/ComputeEndpointsForReferenceStaticWebAssets.cs @@ -20,7 +20,7 @@ public class ComputeEndpointsForReferenceStaticWebAssets : Task public override bool Execute() { - var assets = Assets.Select(StaticWebAsset.FromTaskItem).ToDictionary(a => a.Identity, a => a); + var assets = StaticWebAsset.ToAssetDictionary(Assets); var candidateEndpoints = StaticWebAssetEndpoint.FromItemGroup(CandidateEndpoints); var endpoints = new List<StaticWebAssetEndpoint>(); diff --git a/src/StaticWebAssetsSdk/Tasks/ComputeReferenceStaticWebAssetItems.cs b/src/StaticWebAssetsSdk/Tasks/ComputeReferenceStaticWebAssetItems.cs index c25f701b06ed..18cda8b9e90c 100644 --- a/src/StaticWebAssetsSdk/Tasks/ComputeReferenceStaticWebAssetItems.cs +++ b/src/StaticWebAssetsSdk/Tasks/ComputeReferenceStaticWebAssetItems.cs @@ -38,28 +38,19 @@ public override bool Execute() { try { - var existingAssets = Assets - .Where(asset => StaticWebAsset.HasSourceId(asset, Source)) - .Select(StaticWebAsset.FromTaskItem) - .GroupBy( - a => a.ComputeTargetPath("", '/'), - (key, group) => (key, StaticWebAsset.ChooseNearestAssetKind(group, AssetKind))); - - var resultAssets = new List<StaticWebAsset>(); - foreach (var (key, group) in existingAssets) + var existingAssets = StaticWebAsset.AssetsByTargetPath(Assets, Source, AssetKind); + + var resultAssets = new List<StaticWebAsset>(existingAssets.Count); + foreach (var kvp in existingAssets) { - if (!TryGetUniqueAsset(group, out var selected)) + var targetPath = kvp.Key; + var (selected, all) = kvp.Value; + if (all != null) { - if (selected == null) - { - Log.LogMessage(MessageImportance.Low, "No compatible asset found for '{0}'", key); - continue; - } - else - { - Log.LogError("More than one compatible asset found for '{0}'.", selected.Identity); - return false; - } + Log.LogError("More than one compatible asset found for target path '{0}' -> {1}.", + targetPath, + Environment.NewLine + string.Join(Environment.NewLine, all.Select(a => $"({a.Identity},{a.AssetKind})"))); + return false; } if (ShouldIncludeAssetAsReference(selected, out var reason)) @@ -106,22 +97,6 @@ public override bool Execute() return !Log.HasLoggedErrors; } - private static bool TryGetUniqueAsset(IEnumerable<StaticWebAsset> candidates, out StaticWebAsset selected) - { - selected = null; - foreach (var asset in candidates) - { - if (selected != null) - { - return false; - } - - selected = asset; - } - - return selected != null; - } - private bool ShouldIncludeAssetAsReference(StaticWebAsset candidate, out string reason) { if (!StaticWebAssetsManifest.ManifestModes.ShouldIncludeAssetAsReference(candidate, ProjectMode)) diff --git a/src/StaticWebAssetsSdk/Tasks/ComputeStaticWebAssetsForCurrentProject.cs b/src/StaticWebAssetsSdk/Tasks/ComputeStaticWebAssetsForCurrentProject.cs index 0df5b4210b44..3c4d49582d2e 100644 --- a/src/StaticWebAssetsSdk/Tasks/ComputeStaticWebAssetsForCurrentProject.cs +++ b/src/StaticWebAssetsSdk/Tasks/ComputeStaticWebAssetsForCurrentProject.cs @@ -28,28 +28,19 @@ public override bool Execute() { try { - var currentProjectAssets = Assets - .Where(asset => StaticWebAsset.HasSourceId(asset, Source)) - .Select(StaticWebAsset.FromTaskItem) - .GroupBy( - a => a.ComputeTargetPath("", '/'), - (key, group) => (key, StaticWebAsset.ChooseNearestAssetKind(group, AssetKind))); + var currentProjectAssets = StaticWebAsset.AssetsByTargetPath(Assets, Source, AssetKind); - var resultAssets = new List<StaticWebAsset>(); - foreach (var (key, group) in currentProjectAssets) + var resultAssets = new List<StaticWebAsset>(currentProjectAssets.Count); + foreach (var kvp in currentProjectAssets) { - if (!TryGetUniqueAsset(group, out var selected)) + var targetPath = kvp.Key; + var (selected, all) = kvp.Value; + if (all != null) { - if (selected == null) - { - Log.LogMessage(MessageImportance.Low, "No compatible asset found for '{0}'", key); - continue; - } - else - { - Log.LogError("More than one compatible asset found for '{0}'.", selected.Identity); - return false; - } + Log.LogError("More than one compatible asset found for target path '{0}' -> {1}.", + targetPath, + Environment.NewLine + string.Join(Environment.NewLine, all.Select(a => $"({a.Identity},{a.AssetKind})"))); + return false; } if (!selected.IsForReferencedProjectsOnly()) @@ -74,20 +65,4 @@ public override bool Execute() return !Log.HasLoggedErrors; } - - private static bool TryGetUniqueAsset(IEnumerable<StaticWebAsset> candidates, out StaticWebAsset selected) - { - selected = null; - foreach (var asset in candidates) - { - if (selected != null) - { - return false; - } - - selected = asset; - } - - return selected != null; - } } diff --git a/src/StaticWebAssetsSdk/Tasks/ComputeStaticWebAssetsTargetPaths.cs b/src/StaticWebAssetsSdk/Tasks/ComputeStaticWebAssetsTargetPaths.cs index 9f1db9f6de8b..cf3203106fc2 100644 --- a/src/StaticWebAssetsSdk/Tasks/ComputeStaticWebAssetsTargetPaths.cs +++ b/src/StaticWebAssetsSdk/Tasks/ComputeStaticWebAssetsTargetPaths.cs @@ -4,7 +4,6 @@ #nullable disable using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; namespace Microsoft.AspNetCore.StaticWebAssets.Tasks; @@ -27,7 +26,7 @@ public override bool Execute() try { Log.LogMessage(MessageImportance.Low, "Using path prefix '{0}'", PathPrefix); - AssetsWithTargetPath = new TaskItem[Assets.Length]; + AssetsWithTargetPath = new ITaskItem[Assets.Length]; for (var i = 0; i < Assets.Length; i++) { diff --git a/src/StaticWebAssetsSdk/Tasks/Data/StaticWebAsset.cs b/src/StaticWebAssetsSdk/Tasks/Data/StaticWebAsset.cs index f2e518eda5aa..60058355f0b6 100644 --- a/src/StaticWebAssetsSdk/Tasks/Data/StaticWebAsset.cs +++ b/src/StaticWebAssetsSdk/Tasks/Data/StaticWebAsset.cs @@ -3,99 +3,337 @@ #nullable disable +using System.Collections; using System.Diagnostics; using System.Globalization; using System.Security.Cryptography; using Microsoft.AspNetCore.StaticWebAssets.Tasks.Utils; using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; namespace Microsoft.AspNetCore.StaticWebAssets.Tasks; [DebuggerDisplay("{" + nameof(GetDebuggerDisplay) + "(),nq}")] #if WASM_TASKS -internal sealed class StaticWebAsset : IEquatable<StaticWebAsset>, IComparable<StaticWebAsset> +internal sealed class StaticWebAsset : IEquatable<StaticWebAsset>, IComparable<StaticWebAsset>, ITaskItem2 #else -public sealed class StaticWebAsset : IEquatable<StaticWebAsset>, IComparable<StaticWebAsset> +public sealed class StaticWebAsset : IEquatable<StaticWebAsset>, IComparable<StaticWebAsset>, ITaskItem2 #endif { public const string DateTimeAssetFormat = "ddd, dd MMM yyyy HH:mm:ss 'GMT'"; + private bool _modified; + private ITaskItem _originalItem; + private string _identity; + private string _sourceId; + private string _sourceType; + private string _contentRoot; + private string _basePath; + private string _relativePath; + private string _assetKind; + private string _assetMode; + private string _assetRole; + private string _assetMergeBehavior; + private string _assetMergeSource; + private string _relatedAsset; + private string _assetTraitName; + private string _assetTraitValue; + private string _fingerprint; + private string _integrity; + private string _copyToOutputDirectory; + private string _copyToPublishDirectory; + private string _originalItemSpec; + private long _fileLength = -1; + private DateTimeOffset _lastWriteTime = DateTimeOffset.MinValue; + private Dictionary<string, string> _additionalCustomMetadata; + private string _fileLengthString; + private string _lastWriteTimeString; + public StaticWebAsset() { } public StaticWebAsset(StaticWebAsset asset) { - Identity = asset.Identity; - SourceType = asset.SourceType; - SourceId = asset.SourceId; - ContentRoot = asset.ContentRoot; - BasePath = asset.BasePath; - RelativePath = asset.RelativePath; - AssetKind = asset.AssetKind; - AssetMode = asset.AssetMode; - AssetRole = asset.AssetRole; - AssetMergeBehavior = asset.AssetMergeBehavior; - AssetMergeSource = asset.AssetMergeSource; - RelatedAsset = asset.RelatedAsset; - AssetTraitName = asset.AssetTraitName; - AssetTraitValue = asset.AssetTraitValue; - CopyToOutputDirectory = asset.CopyToOutputDirectory; - CopyToPublishDirectory = asset.CopyToPublishDirectory; - OriginalItemSpec = asset.OriginalItemSpec; - FileLength = asset.FileLength; - LastWriteTime = asset.LastWriteTime; + _identity = asset.Identity; + _sourceType = asset.SourceType; + _sourceId = asset.SourceId; + _contentRoot = asset.ContentRoot; + _basePath = asset.BasePath; + _relativePath = asset.RelativePath; + _assetKind = asset.AssetKind; + _assetMode = asset.AssetMode; + _assetRole = asset.AssetRole; + _assetMergeBehavior = asset.AssetMergeBehavior; + _assetMergeSource = asset.AssetMergeSource; + _relatedAsset = asset.RelatedAsset; + _assetTraitName = asset.AssetTraitName; + _assetTraitValue = asset.AssetTraitValue; + _copyToOutputDirectory = asset.CopyToOutputDirectory; + _copyToPublishDirectory = asset.CopyToPublishDirectory; + _originalItemSpec = asset.OriginalItemSpec; + _fileLength = asset.FileLength; + _lastWriteTime = asset.LastWriteTime; + _fingerprint = asset.Fingerprint; + _integrity = asset.Integrity; } - public string Identity { get; set; } + private string GetOriginalItemMetadata(string name) => _originalItem?.GetMetadata(name); - public string SourceId { get; set; } + public string Identity + { + get + { + return _identity ??= + // Register the identity as the full path since assets might have come + // from packages and other sources and the identity (which is typically + // just the relative path from the project) is not enough to locate them. + GetOriginalItemMetadata("FullPath"); + } - public string SourceType { get; set; } + set + { + _modified = true; + _identity = value; + } + } - public string ContentRoot { get; set; } + public string SourceId + { + get => _sourceId ??= GetOriginalItemMetadata(nameof(SourceId)); + set + { + _modified = true; + _sourceId = value; + } + } - public string BasePath { get; set; } + public string SourceType + { + get => _sourceType ??= GetOriginalItemMetadata(nameof(SourceType)); + set + { + _modified = true; + _sourceType = value; + } + } - public string RelativePath { get; set; } + public string ContentRoot + { + get => _contentRoot ??= GetOriginalItemMetadata(nameof(ContentRoot)); + set + { + _modified = true; + _contentRoot = value; + } + } - public string AssetKind { get; set; } + public string BasePath + { + get => _basePath ??= GetOriginalItemMetadata(nameof(BasePath)); + set + { + _modified = true; + _basePath = value; + } + } - public string AssetMode { get; set; } + public string RelativePath + { + get => _relativePath ??= GetOriginalItemMetadata(nameof(RelativePath)); + set + { + _modified = true; + _relativePath = value; + } + } - public string AssetRole { get; set; } + public string AssetKind + { + get => _assetKind ??= GetOriginalItemMetadata(nameof(AssetKind)); + set + { + _modified = true; + _assetKind = value; + } + } - public string AssetMergeBehavior { get; set; } + public string AssetMode + { + get => _assetMode ??= GetOriginalItemMetadata(nameof(AssetMode)); + set + { + _modified = true; + _assetMode = value; + } + } - public string AssetMergeSource { get; set; } + public string AssetRole + { + get => _assetRole ??= GetOriginalItemMetadata(nameof(AssetRole)); + set + { + _modified = true; + _assetRole = value; + } + } - public string RelatedAsset { get; set; } + public string AssetMergeBehavior + { + get => _assetMergeBehavior ??= GetOriginalItemMetadata(nameof(AssetMergeBehavior)); + set + { + _modified = true; + _assetMergeBehavior = value; + } + } - public string AssetTraitName { get; set; } + public string AssetMergeSource + { + get => _assetMergeSource ??= GetOriginalItemMetadata(nameof(AssetMergeSource)); + set + { + _modified = true; + _assetMergeSource = value; + } + } - public string AssetTraitValue { get; set; } + public string RelatedAsset + { + get => _relatedAsset ??= GetOriginalItemMetadata(nameof(RelatedAsset)); + set + { + _modified = true; + _relatedAsset = value; + } + } - public string Fingerprint { get; set; } + public string AssetTraitName + { + get => _assetTraitName ??= GetOriginalItemMetadata(nameof(AssetTraitName)); + set + { + _modified = true; + _assetTraitName = value; + } + } - public string Integrity { get; set; } + public string AssetTraitValue + { + get => _assetTraitValue ??= GetOriginalItemMetadata(nameof(AssetTraitValue)); + set + { + _modified = true; + _assetTraitValue = value; + } + } - public string CopyToOutputDirectory { get; set; } + public string Fingerprint + { + get => _fingerprint ??= GetOriginalItemMetadata(nameof(Fingerprint)); + set + { + _modified = true; + _fingerprint = value; + } + } - public string CopyToPublishDirectory { get; set; } + public string Integrity + { + get => _integrity ??= GetOriginalItemMetadata(nameof(Integrity)); + set + { + _modified = true; + _integrity = value; + } + } - public string OriginalItemSpec { get; set; } + public string CopyToOutputDirectory + { + get => _copyToOutputDirectory ??= GetOriginalItemMetadata(nameof(CopyToOutputDirectory)); + set + { + _modified = true; + _copyToOutputDirectory = value; + } + } - public long FileLength { get; set; } = -1; + public string CopyToPublishDirectory + { + get => _copyToPublishDirectory ??= GetOriginalItemMetadata(nameof(CopyToPublishDirectory)); + set + { + _modified = true; + _copyToPublishDirectory = value; + } + } - public DateTimeOffset LastWriteTime { get; set; } = DateTimeOffset.MinValue; + public string OriginalItemSpec + { + get => _originalItemSpec ??= GetOriginalItemMetadata(nameof(OriginalItemSpec)); + set + { + _modified = true; + _originalItemSpec = value; + } + } + + internal string FileLengthString => _fileLengthString ??= GetOriginalItemMetadata(nameof(FileLength)); - public static StaticWebAsset FromTaskItem(ITaskItem item) + public long FileLength + { + get + { + if (_fileLength < 0) + { + var fileLengthString = FileLengthString; + _fileLength = !string.IsNullOrEmpty(fileLengthString) && + long.TryParse(fileLengthString, NumberStyles.None, CultureInfo.InvariantCulture, out var fileLength) + ? fileLength + : -1; + } + return _fileLength; + } + set + { + _fileLengthString = null; + _modified = true; + _fileLength = value; + } + } + + internal string LastWriteTimeString => _lastWriteTimeString ??= GetOriginalItemMetadata(nameof(LastWriteTime)); + + public DateTimeOffset LastWriteTime + { + get + { + if (_lastWriteTime == DateTimeOffset.MinValue) + { + var lastWriteTimeString = LastWriteTimeString; + _lastWriteTime = !string.IsNullOrEmpty(lastWriteTimeString) && + DateTimeOffset.TryParse(lastWriteTimeString, CultureInfo.InvariantCulture, DateTimeStyles.None, out var lastWriteTime) + ? lastWriteTime + : DateTimeOffset.MinValue; + } + return _lastWriteTime; + } + set + { + _lastWriteTimeString = null; + _modified = true; + _lastWriteTime = value; + } + } + + public static StaticWebAsset FromTaskItem(ITaskItem item, bool validate = false) { var result = FromTaskItemCore(item); result.Normalize(); - result.Validate(); + if (validate) + { + result.Validate(); + } return result; } @@ -119,14 +357,20 @@ internal static IEnumerable<StaticWebAsset> ChooseNearestAssetKind(IEnumerable<S { if (item.HasKind(assetKind)) { + // The moment we find a Build or Publish asset, we start ignoring the + // All assets ignoreAllKind = true; + // We still return multiple Build or Publish items if they are present + // But we won't error out if there are multiple All assets as long as there + // is a single Build or Publish asset present. yield return item; } else if (!ignoreAllKind && item.IsBuildAndPublish()) { if (allKindAssetCandidate != null) { + // At this point we have more than one `All` asset, which is an error yield return allKindAssetCandidate; yield return item; yield break; @@ -141,42 +385,91 @@ internal static IEnumerable<StaticWebAsset> ChooseNearestAssetKind(IEnumerable<S } } - internal static bool ValidateAssetGroup(string path, IReadOnlyList<StaticWebAsset> group, out string reason) + internal static bool ValidateAssetGroup(string path, (StaticWebAsset First, StaticWebAsset Second, IReadOnlyList<StaticWebAsset> Others) group, out string reason) { - StaticWebAsset prototypeItem = null; + var prototypeItem = group.First; StaticWebAsset build = null; StaticWebAsset publish = null; StaticWebAsset all = null; - foreach (var item in group) + + if (group.Second == null) + { + // Most common case, only one asset for the given path + reason = null; + return true; + } + + // Check First against Second for source ID conflict + if (!prototypeItem.HasSourceId(group.Second.SourceId)) + { + reason = $"Conflicting assets with the same target path '{path}'. For assets '{prototypeItem}' and '{group.Second}' from different projects."; + return false; + } + + // Process First + build = group.First.IsBuildOnly() ? group.First : build; + publish = group.First.IsPublishOnly() ? group.First : publish; + all = group.First.IsBuildAndPublish() ? group.First : all; + + // Process Second + if (build != null && group.Second.IsBuildOnly() && !ReferenceEquals(build, group.Second)) + { + reason = $"Conflicting assets with the same target path '{path}'. For 'Build' assets '{build}' and '{group.Second}'."; + return false; + } + build ??= group.Second.IsBuildOnly() ? group.Second : build; + + if (publish != null && group.Second.IsPublishOnly() && !ReferenceEquals(publish, group.Second)) + { + reason = $"Conflicting assets with the same target path '{path}'. For 'Publish' assets '{publish}' and '{group.Second}'."; + return false; + } + publish ??= group.Second.IsPublishOnly() ? group.Second : publish; + + if (all != null && group.Second.IsBuildAndPublish() && !ReferenceEquals(all, group.Second)) + { + reason = $"Conflicting assets with the same target path '{path}'. For 'All' assets '{all}' and '{group.Second}'."; + return false; + } + all ??= group.Second.IsBuildAndPublish() ? group.Second : all; + + if (group.Others == null || group.Others.Count == 0) + { + reason = null; + return true; + } + + // Process rest of the items + foreach (var item in group.Others) { - prototypeItem ??= item; if (!prototypeItem.HasSourceId(item.SourceId)) { reason = $"Conflicting assets with the same target path '{path}'. For assets '{prototypeItem}' and '{item}' from different projects."; return false; } - build ??= item.IsBuildOnly() ? item : build; if (build != null && item.IsBuildOnly() && !ReferenceEquals(build, item)) { reason = $"Conflicting assets with the same target path '{path}'. For 'Build' assets '{build}' and '{item}'."; return false; } + build ??= item.IsBuildOnly() ? item : build; - publish ??= item.IsPublishOnly() ? item : publish; if (publish != null && item.IsPublishOnly() && !ReferenceEquals(publish, item)) { reason = $"Conflicting assets with the same target path '{path}'. For 'Publish' assets '{publish}' and '{item}'."; return false; } + publish ??= item.IsPublishOnly() ? item : publish; - all ??= item.IsBuildAndPublish() ? item : all; if (all != null && item.IsBuildAndPublish() && !ReferenceEquals(all, item)) { reason = $"Conflicting assets with the same target path '{path}'. For 'All' assets '{all}' and '{item}'."; return false; } + all ??= item.IsBuildAndPublish() ? item : all; } + reason = null; return true; } @@ -196,36 +489,13 @@ public static StaticWebAsset FromV1TaskItem(ITaskItem item) return result; } - private static StaticWebAsset FromTaskItemCore(ITaskItem item) => - new() - { - // Register the identity as the full path since assets might have come - // from packages and other sources and the identity (which is typically - // just the relative path from the project) is not enough to locate them. - Identity = item.GetMetadata("FullPath"), - SourceType = item.GetMetadata(nameof(SourceType)), - SourceId = item.GetMetadata(nameof(SourceId)), - ContentRoot = item.GetMetadata(nameof(ContentRoot)), - BasePath = item.GetMetadata(nameof(BasePath)), - RelativePath = item.GetMetadata(nameof(RelativePath)), - AssetKind = item.GetMetadata(nameof(AssetKind)), - AssetMode = item.GetMetadata(nameof(AssetMode)), - AssetRole = item.GetMetadata(nameof(AssetRole)), - AssetMergeSource = item.GetMetadata(nameof(AssetMergeSource)), - AssetMergeBehavior = item.GetMetadata(nameof(AssetMergeBehavior)), - RelatedAsset = item.GetMetadata(nameof(RelatedAsset)), - AssetTraitName = item.GetMetadata(nameof(AssetTraitName)), - AssetTraitValue = item.GetMetadata(nameof(AssetTraitValue)), - Fingerprint = item.GetMetadata(nameof(Fingerprint)), - Integrity = item.GetMetadata(nameof(Integrity)), - CopyToOutputDirectory = item.GetMetadata(nameof(CopyToOutputDirectory)), - CopyToPublishDirectory = item.GetMetadata(nameof(CopyToPublishDirectory)), - OriginalItemSpec = item.GetMetadata(nameof(OriginalItemSpec)), - FileLength = item.GetMetadata("FileLength") is string fileLengthString && - long.TryParse(fileLengthString, out var fileLength) ? fileLength : -1, - LastWriteTime = item.GetMetadata("LastWriteTime") is string lastWriteTimeString && - DateTimeOffset.TryParse(lastWriteTimeString, out var lastWriteTime) ? lastWriteTime : DateTimeOffset.MinValue + private static StaticWebAsset FromTaskItemCore(ITaskItem item) + { + return new() + { + _originalItem = item, }; + } public void ApplyDefaults() { @@ -301,28 +571,15 @@ public static string CombineNormalizedPaths(string prefix, string basePath, stri public ITaskItem ToTaskItem() { - var result = new TaskItem(Identity); - result.SetMetadata(nameof(SourceType), SourceType); - result.SetMetadata(nameof(SourceId), SourceId); - result.SetMetadata(nameof(ContentRoot), ContentRoot); - result.SetMetadata(nameof(BasePath), BasePath); - result.SetMetadata(nameof(RelativePath), RelativePath); - result.SetMetadata(nameof(AssetKind), AssetKind); - result.SetMetadata(nameof(AssetMode), AssetMode); - result.SetMetadata(nameof(AssetRole), AssetRole); - result.SetMetadata(nameof(AssetMergeSource), AssetMergeSource); - result.SetMetadata(nameof(AssetMergeBehavior), AssetMergeBehavior); - result.SetMetadata(nameof(RelatedAsset), RelatedAsset); - result.SetMetadata(nameof(AssetTraitName), AssetTraitName); - result.SetMetadata(nameof(AssetTraitValue), AssetTraitValue); - result.SetMetadata(nameof(Fingerprint), Fingerprint); - result.SetMetadata(nameof(Integrity), Integrity); - result.SetMetadata(nameof(CopyToOutputDirectory), CopyToOutputDirectory); - result.SetMetadata(nameof(CopyToPublishDirectory), CopyToPublishDirectory); - result.SetMetadata(nameof(OriginalItemSpec), OriginalItemSpec); - result.SetMetadata("FileLength", FileLength.ToString(CultureInfo.InvariantCulture)); - result.SetMetadata("LastWriteTime", LastWriteTime.ToString(DateTimeAssetFormat, CultureInfo.InvariantCulture)); - return result; + if (!_modified && _originalItem != null) + { + // We haven't modified the item, we can just return the original item. + // This is still interesting because MSBuild can optimize things and avoid + // additional copies. + return _originalItem; + } + // We can always return ourselves, any property that wasn't modified we will copy from the original item if exists. + return this; } public void Validate() @@ -467,7 +724,7 @@ internal static StaticWebAsset FromProperties( CopyToPublishDirectory = copyToPublishDirectory, OriginalItemSpec = originalItemSpec, FileLength = fileLength, - LastWriteTime = lastWriteTime + LastWriteTime = lastWriteTime, }; result.ApplyDefaults(); @@ -971,6 +1228,339 @@ internal static FileInfo ResolveFile(string identity, string originalItemSpec) throw new InvalidOperationException($"No file exists for the asset at either location '{identity}' or '{originalItemSpec}'."); } + internal static Dictionary<string, StaticWebAsset> ToAssetDictionary(ITaskItem[] candidateAssets, bool validate = false) + { + var dictionary = new Dictionary<string, StaticWebAsset>(candidateAssets.Length); + for (var i = 0; i < candidateAssets.Length; i++) + { + var candidateAsset = FromTaskItem(candidateAssets[i], validate); + dictionary.Add(candidateAsset.Identity, candidateAsset); + } + + return dictionary; + } + + internal static StaticWebAsset[] FromTaskItemGroup(ITaskItem[] candidateAssets, bool validate = false) + { + var result = new StaticWebAsset[candidateAssets.Length]; + for (var i = 0; i != result.Length; i++) + { + var candidateAsset = FromTaskItem(candidateAssets[i], validate); + result[i] = candidateAsset; + } + return result; + } + + internal static Dictionary<string, (StaticWebAsset, List<StaticWebAsset>)> AssetsByTargetPath(ITaskItem[] assets, string source, string assetKind) + { + // We return either the selected asset or a list with all the candidates that were found to be ambiguous + var result = new Dictionary<string, (StaticWebAsset selected, List<StaticWebAsset> all)>(); + for (var i = 0; i < assets.Length; i++) + { + var candidate = assets[i]; + if (!HasSourceId(candidate, source)) + { + continue; + } + if (HasOppositeKind(candidate, assetKind)) + { + continue; + } + var asset = FromTaskItem(candidate); + var key = asset.ComputeTargetPath("", '/'); + if (!result.TryGetValue(key, out var existing)) + { + result[key] = (asset, null); + } + else + { + var (existingAsset, all) = existing; + if (existingAsset == null) + { + Debug.Assert(all != null); + // We are going to error out, just add to the list + all.Add(asset); + } + else if (existingAsset.AssetKind == asset.AssetKind) + { + // We have an ambiguity because there are either two Build, Publish or All assets + result[key] = (null, [existingAsset, asset]); + } + else if (existingAsset.IsBuildAndPublish() && !asset.IsBuildAndPublish()) + { + // There is an All asset overriden by a Build or Publish asset. + result[key] = (asset, null); + } + } + } + return result; + } + + private static bool HasOppositeKind(ITaskItem candidate, string assetKind) + { + var candidateKind = candidate.GetMetadata(nameof(AssetKind)); + return candidateKind switch + { + AssetKinds.Publish => assetKind switch + { + AssetKinds.Build => true, + _ => false, + }, + AssetKinds.Build => assetKind switch + { + AssetKinds.Publish => true, + _ => false, + }, + _ => false + }; + } + + // We provide the minimal ITaskItem2 implementation so that we can return StaticWebAsset instances without having to convert them + // to task items. This is because the underlying implementation uses an immutable dictionary and every call to SetMetadata results + // in a new allocation. + // When the task returns, MSBuild will convert the task into a ProjectItem instance and will copy the custom metadata, at which point + // it will get rid of the instance that we returned and won't use it any longer. + // For that reason, and since we control inside the tasks how this is used, we can safely ignore the pieces that MSBuild won't call. + #region ITaskItem2 implementation + + string ITaskItem2.EvaluatedIncludeEscaped { get => Identity; set => Identity = value; } + string ITaskItem.ItemSpec { get => Identity; set => Identity = value; } + + private static readonly string[] _defaultPropertyNames = [ + nameof(SourceId), + nameof(SourceType), + nameof(ContentRoot), + nameof(BasePath), + nameof(RelativePath), + nameof(AssetKind), + nameof(AssetMode), + nameof(AssetRole), + nameof(AssetMergeBehavior), + nameof(AssetMergeSource), + nameof(RelatedAsset), + nameof(AssetTraitName), + nameof(AssetTraitValue), + nameof(Fingerprint), + nameof(Integrity), + nameof(CopyToOutputDirectory), + nameof(CopyToPublishDirectory), + nameof(OriginalItemSpec), + nameof(FileLength), + nameof(LastWriteTime) + ]; + + ICollection ITaskItem.MetadataNames + { + get + { + if (_additionalCustomMetadata == null) + { + return _defaultPropertyNames; + } + + var result = new List<string>(_defaultPropertyNames.Length + _additionalCustomMetadata.Count); + result.AddRange(_defaultPropertyNames); + + foreach (var kvp in _additionalCustomMetadata) + { + result.Add(kvp.Key); + } + + return result; + } + } + + int ITaskItem.MetadataCount => _defaultPropertyNames.Length + (_additionalCustomMetadata?.Count ?? 0); + + string ITaskItem2.GetMetadataValueEscaped(string metadataName) + { + return metadataName switch + { + // These two are special and aren't "Real metadata" + "FullPath" => Identity ?? "", + nameof(Identity) => Identity ?? "", + // These are common metadata + nameof(SourceId) => SourceId ?? "", + nameof(SourceType) => SourceType ?? "", + nameof(ContentRoot) => ContentRoot ?? "", + nameof(BasePath) => BasePath ?? "", + nameof(RelativePath) => RelativePath ?? "", + nameof(AssetKind) => AssetKind ?? "", + nameof(AssetMode) => AssetMode ?? "", + nameof(AssetRole) => AssetRole ?? "", + nameof(AssetMergeBehavior) => AssetMergeBehavior ?? "", + nameof(AssetMergeSource) => AssetMergeSource ?? "", + nameof(RelatedAsset) => RelatedAsset ?? "", + nameof(AssetTraitName) => AssetTraitName ?? "", + nameof(AssetTraitValue) => AssetTraitValue ?? "", + nameof(Fingerprint) => Fingerprint ?? "", + nameof(Integrity) => Integrity ?? "", + nameof(CopyToOutputDirectory) => CopyToOutputDirectory ?? "", + nameof(CopyToPublishDirectory) => CopyToPublishDirectory ?? "", + nameof(OriginalItemSpec) => OriginalItemSpec ?? "", + nameof(FileLength) => GetFileLengthAsString() ?? "", + nameof(LastWriteTime) => GetLastWriteTimeAsString() ?? "", + _ => _additionalCustomMetadata?.TryGetValue(metadataName, out var value) == true ? (value ?? "") : "", + }; + } + + private string GetFileLengthAsString() => + FileLength == -1 ? (FileLengthString ?? "") : FileLength.ToString(CultureInfo.InvariantCulture); + + private string GetLastWriteTimeAsString() => + LastWriteTime == DateTimeOffset.MinValue ? (LastWriteTimeString ?? "") : LastWriteTime.ToString(DateTimeAssetFormat, CultureInfo.InvariantCulture); + + void ITaskItem2.SetMetadataValueLiteral(string metadataName, string metadataValue) + { + metadataValue ??= ""; + switch (metadataName) + { + case nameof(SourceId): + SourceId = metadataValue; + break; + case nameof(SourceType): + SourceType = metadataValue; + break; + case nameof(ContentRoot): + ContentRoot = metadataValue; + break; + case nameof(BasePath): + BasePath = metadataValue; + break; + case nameof(RelativePath): + RelativePath = metadataValue; + break; + case nameof(AssetKind): + AssetKind = metadataValue; + break; + case nameof(AssetMode): + AssetMode = metadataValue; + break; + case nameof(AssetRole): + AssetRole = metadataValue; + break; + case nameof(AssetMergeBehavior): + AssetMergeBehavior = metadataValue; + break; + case nameof(AssetMergeSource): + AssetMergeSource = metadataValue; + break; + case nameof(RelatedAsset): + RelatedAsset = metadataValue; + break; + case nameof(AssetTraitName): + AssetTraitName = metadataValue; + break; + case nameof(AssetTraitValue): + AssetTraitValue = metadataValue; + break; + case nameof(Fingerprint): + Fingerprint = metadataValue; + break; + case nameof(Integrity): + Integrity = metadataValue; + break; + case nameof(CopyToOutputDirectory): + CopyToOutputDirectory = metadataValue; + break; + case nameof(CopyToPublishDirectory): + CopyToPublishDirectory = metadataValue; + break; + case nameof(OriginalItemSpec): + OriginalItemSpec = metadataValue; + break; + case nameof(FileLength): + _fileLengthString = metadataValue; + _fileLength = -1; + break; + case nameof(LastWriteTime): + _lastWriteTimeString = metadataValue; + _lastWriteTime = DateTimeOffset.MinValue; + break; + default: + _additionalCustomMetadata ??= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + _additionalCustomMetadata[metadataName] = metadataValue; + _modified = true; + break; + } + } + + IDictionary ITaskItem2.CloneCustomMetadataEscaped() + { + var result = new Dictionary<string, string>(((ITaskItem)this).MetadataCount) + { + { nameof(SourceId), SourceId ?? "" }, + { nameof(SourceType), SourceType ?? "" }, + { nameof(ContentRoot), ContentRoot ?? "" }, + { nameof(BasePath), BasePath ?? "" }, + { nameof(RelativePath), RelativePath ?? "" }, + { nameof(AssetKind), AssetKind ?? "" }, + { nameof(AssetMode), AssetMode ?? "" }, + { nameof(AssetRole), AssetRole ?? "" }, + { nameof(AssetMergeBehavior), AssetMergeBehavior ?? "" }, + { nameof(AssetMergeSource), AssetMergeSource ?? "" }, + { nameof(RelatedAsset), RelatedAsset ?? "" }, + { nameof(AssetTraitName), AssetTraitName ?? "" }, + { nameof(AssetTraitValue), AssetTraitValue ?? "" }, + { nameof(Fingerprint), Fingerprint ?? "" }, + { nameof(Integrity), Integrity ?? "" }, + { nameof(CopyToOutputDirectory), CopyToOutputDirectory ?? "" }, + { nameof(CopyToPublishDirectory), CopyToPublishDirectory ?? "" }, + { nameof(OriginalItemSpec), OriginalItemSpec ?? "" }, + { nameof(FileLength), GetFileLengthAsString() ?? "" }, + { nameof(LastWriteTime), GetLastWriteTimeAsString() ?? "" } + }; + if (_additionalCustomMetadata != null) + { + foreach (var kvp in _additionalCustomMetadata) + { + result[kvp.Key] = kvp.Value; + } + } + + return result; + } + + string ITaskItem.GetMetadata(string metadataName) => ((ITaskItem2)this).GetMetadataValueEscaped(metadataName); + void ITaskItem.SetMetadata(string metadataName, string metadataValue) => ((ITaskItem2)this).SetMetadataValueLiteral(metadataName, metadataValue); + + void ITaskItem.RemoveMetadata(string metadataName) => _additionalCustomMetadata?.Remove(metadataName); + + void ITaskItem.CopyMetadataTo(ITaskItem destinationItem) + { + destinationItem.SetMetadata(nameof(SourceId), SourceId ?? ""); + destinationItem.SetMetadata(nameof(SourceType), SourceType ?? ""); + destinationItem.SetMetadata(nameof(ContentRoot), ContentRoot ?? ""); + destinationItem.SetMetadata(nameof(BasePath), BasePath ?? ""); + destinationItem.SetMetadata(nameof(RelativePath), RelativePath ?? ""); + destinationItem.SetMetadata(nameof(AssetKind), AssetKind ?? ""); + destinationItem.SetMetadata(nameof(AssetMode), AssetMode ?? ""); + destinationItem.SetMetadata(nameof(AssetRole), AssetRole ?? ""); + destinationItem.SetMetadata(nameof(AssetMergeBehavior), AssetMergeBehavior ?? ""); + destinationItem.SetMetadata(nameof(AssetMergeSource), AssetMergeSource ?? ""); + destinationItem.SetMetadata(nameof(RelatedAsset), RelatedAsset ?? ""); + destinationItem.SetMetadata(nameof(AssetTraitName), AssetTraitName ?? ""); + destinationItem.SetMetadata(nameof(AssetTraitValue), AssetTraitValue ?? ""); + destinationItem.SetMetadata(nameof(Fingerprint), Fingerprint ?? ""); + destinationItem.SetMetadata(nameof(Integrity), Integrity ?? ""); + destinationItem.SetMetadata(nameof(CopyToOutputDirectory), CopyToOutputDirectory ?? ""); + destinationItem.SetMetadata(nameof(CopyToPublishDirectory), CopyToPublishDirectory ?? ""); + destinationItem.SetMetadata(nameof(OriginalItemSpec), OriginalItemSpec ?? ""); + destinationItem.SetMetadata(nameof(FileLength), GetFileLengthAsString() ?? ""); + destinationItem.SetMetadata(nameof(LastWriteTime), GetLastWriteTimeAsString() ?? ""); + if (_additionalCustomMetadata != null) + { + foreach (var kvp in _additionalCustomMetadata) + { + destinationItem.SetMetadata(kvp.Key, kvp.Value ?? ""); + } + } + } + + IDictionary ITaskItem.CloneCustomMetadata() => ((ITaskItem2)this).CloneCustomMetadataEscaped(); + + #endregion + [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")] internal sealed class StaticWebAssetResolvedRoute(string pathLabel, string path, Dictionary<string, string> tokens) { diff --git a/src/StaticWebAssetsSdk/Tasks/Data/StaticWebAssetEndpoint.cs b/src/StaticWebAssetsSdk/Tasks/Data/StaticWebAssetEndpoint.cs index 93fe5a55ca34..ea8289a3fe95 100644 --- a/src/StaticWebAssetsSdk/Tasks/Data/StaticWebAssetEndpoint.cs +++ b/src/StaticWebAssetsSdk/Tasks/Data/StaticWebAssetEndpoint.cs @@ -3,30 +3,141 @@ #nullable disable +using System.Collections; using System.Collections.Concurrent; using System.Diagnostics; using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; namespace Microsoft.AspNetCore.StaticWebAssets.Tasks; [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")] -public class StaticWebAssetEndpoint : IEquatable<StaticWebAssetEndpoint>, IComparable<StaticWebAssetEndpoint> +public class StaticWebAssetEndpoint : IEquatable<StaticWebAssetEndpoint>, IComparable<StaticWebAssetEndpoint>, ITaskItem2 { + private ITaskItem _originalItem; + private StaticWebAssetEndpointProperty[] _endpointProperties; + private StaticWebAssetEndpointResponseHeader[] _responseHeaders; + private StaticWebAssetEndpointSelector[] _selectors; + private string _assetFile; + private string _route; + private bool _modified; + private string _selectorsString; + private bool _selectorsModified; + private string _responseHeadersString; + private bool _responseHeadersModified; + private string _endpointPropertiesString; + private bool _endpointPropertiesModified; + private Dictionary<string, string> _additionalCustomMetadata; + // Route as it should be registered in the routing table. - public string Route { get; set; } + public string Route + { + get + { + _route ??= _originalItem?.ItemSpec; + return _route; + } + + set + { + _route = value; + _modified = true; + } + } // Path to the file system as provided by static web assets (BasePath + RelativePath). - public string AssetFile { get; set; } + public string AssetFile + { + get + { + _assetFile ??= _originalItem?.GetMetadata(nameof(AssetFile)); + return _assetFile; + } + + set + { + _assetFile = value; + _modified = true; + } + } + + private string SelectorsString + { + get + { + _selectorsString ??= _originalItem?.GetMetadata(nameof(Selectors)); + return _selectorsString; + } + } // Request values that must be compatible for the file to be selected. - public StaticWebAssetEndpointSelector[] Selectors { get; set; } = []; + public StaticWebAssetEndpointSelector[] Selectors + { + get + { + _selectors ??= StaticWebAssetEndpointSelector.FromMetadataValue(SelectorsString); + return _selectors; + } + + set + { + Array.Sort(value); + _selectors = value; + _selectorsModified = true; + _modified = true; + } + } + + private string ResponseHeadersString + { + get + { + _responseHeadersString ??= _originalItem?.GetMetadata(nameof(ResponseHeaders)); + return _responseHeadersString; + } + } // Response headers that must be added to the response. - public StaticWebAssetEndpointResponseHeader[] ResponseHeaders { get; set; } = []; + public StaticWebAssetEndpointResponseHeader[] ResponseHeaders + { + get + { + _responseHeaders ??= StaticWebAssetEndpointResponseHeader.FromMetadataValue(ResponseHeadersString); + return _responseHeaders; + } + set + { + Array.Sort(value); + _responseHeaders = value; + _responseHeadersModified = true; + _modified = true; + } + } + + private string EndpointPropertiesString + { + get + { + _endpointPropertiesString ??= _originalItem?.GetMetadata(nameof(EndpointProperties)); + return _endpointPropertiesString; + } + } // Properties associated with the endpoint. - public StaticWebAssetEndpointProperty[] EndpointProperties { get; set; } = []; + public StaticWebAssetEndpointProperty[] EndpointProperties + { + get + { + _endpointProperties ??= StaticWebAssetEndpointProperty.FromMetadataValue(EndpointPropertiesString); + return _endpointProperties; + } + set + { + Array.Sort(value); + _endpointProperties = value; + _endpointPropertiesModified = true; + _modified = true; + } + } public static IEqualityComparer<StaticWebAssetEndpoint> RouteAndAssetComparer { get; } = new RouteAndAssetEqualityComparer(); @@ -36,9 +147,6 @@ public static StaticWebAssetEndpoint[] FromItemGroup(ITaskItem[] endpoints) for (var i = 0; i < endpoints.Length; i++) { result[i] = FromTaskItem(endpoints[i]); - Array.Sort(result[i].ResponseHeaders); - Array.Sort(result[i].Selectors); - Array.Sort(result[i].EndpointProperties); } Array.Sort(result, (a, b) => (a.Route, b.Route) switch @@ -60,11 +168,7 @@ public static StaticWebAssetEndpoint FromTaskItem(ITaskItem item) { var result = new StaticWebAssetEndpoint() { - Route = item.ItemSpec, - AssetFile = item.GetMetadata(nameof(AssetFile)), - Selectors = StaticWebAssetEndpointSelector.FromMetadataValue(item.GetMetadata(nameof(Selectors))), - ResponseHeaders = StaticWebAssetEndpointResponseHeader.FromMetadataValue(item.GetMetadata(nameof(ResponseHeaders))), - EndpointProperties = StaticWebAssetEndpointProperty.FromMetadataValue(item.GetMetadata(nameof(EndpointProperties))) + _originalItem = item, }; return result; @@ -86,14 +190,15 @@ public static ITaskItem[] ToTaskItems(IList<StaticWebAssetEndpoint> endpoints) return endpointItems; } - public TaskItem ToTaskItem() + public ITaskItem ToTaskItem() { - var item = new TaskItem(Route); - item.SetMetadata(nameof(AssetFile), AssetFile); - item.SetMetadata(nameof(Selectors), StaticWebAssetEndpointSelector.ToMetadataValue(Selectors)); - item.SetMetadata(nameof(ResponseHeaders), StaticWebAssetEndpointResponseHeader.ToMetadataValue(ResponseHeaders)); - item.SetMetadata(nameof(EndpointProperties), StaticWebAssetEndpointProperty.ToMetadataValue(EndpointProperties)); - return item; + if (!_modified && _originalItem != null) + { + return _originalItem; + } + + // If we're implementing ITaskItem2, we can just return this instance + return this; } public override bool Equals(object obj) => Equals(obj as StaticWebAssetEndpoint); @@ -282,4 +387,129 @@ public int GetHashCode(StaticWebAssetEndpoint obj) #endif } } + + #region ITaskItem2 implementation + + string ITaskItem2.EvaluatedIncludeEscaped { get => Route; set => Route = value; } + string ITaskItem.ItemSpec { get => Route; set => Route = value; } + + private static readonly string[] _defaultPropertyNames = [ + nameof(AssetFile), + nameof(Selectors), + nameof(ResponseHeaders), + nameof(EndpointProperties) + ]; + + ICollection ITaskItem.MetadataNames + { + get + { + if (_additionalCustomMetadata == null) + { + return _defaultPropertyNames; + } + + var result = new List<string>(_defaultPropertyNames.Length + _additionalCustomMetadata.Count); + result.AddRange(_defaultPropertyNames); + + foreach (var kvp in _additionalCustomMetadata) + { + result.Add(kvp.Key); + } + + return result; + } + } + + int ITaskItem.MetadataCount => _defaultPropertyNames.Length + (_additionalCustomMetadata?.Count ?? 0); + + string ITaskItem2.GetMetadataValueEscaped(string metadataName) + { + return metadataName switch + { + nameof(AssetFile) => AssetFile ?? "", + nameof(Selectors) => !_selectorsModified ? SelectorsString ?? "" : StaticWebAssetEndpointSelector.ToMetadataValue(Selectors), + nameof(ResponseHeaders) => !_responseHeadersModified ? ResponseHeadersString ?? "" : StaticWebAssetEndpointResponseHeader.ToMetadataValue(ResponseHeaders), + nameof(EndpointProperties) => !_endpointPropertiesModified ? EndpointPropertiesString ?? "" : StaticWebAssetEndpointProperty.ToMetadataValue(EndpointProperties), + _ => _additionalCustomMetadata?.TryGetValue(metadataName, out var value) == true ? (value ?? "") : "", + }; + } + + void ITaskItem2.SetMetadataValueLiteral(string metadataName, string metadataValue) + { + metadataValue ??= ""; + switch (metadataName) + { + case nameof(AssetFile): + AssetFile = metadataValue; + break; + case nameof(Selectors): + _selectorsString = metadataValue; + _selectors = null; + _selectorsModified = false; + break; + case nameof(ResponseHeaders): + _responseHeadersString = metadataValue; + _responseHeaders = null; + _responseHeadersModified = false; + break; + case nameof(EndpointProperties): + _endpointPropertiesString = metadataValue; + _endpointProperties = null; + _endpointPropertiesModified = false; + break; + default: + _additionalCustomMetadata ??= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + _additionalCustomMetadata[metadataName] = metadataValue; + break; + } + _modified = true; + } + + IDictionary ITaskItem2.CloneCustomMetadataEscaped() + { + var result = new Dictionary<string, string>(((ITaskItem)this).MetadataCount) + { + { nameof(AssetFile), AssetFile ?? "" }, + { nameof(Selectors), !_selectorsModified ? SelectorsString ?? "" : StaticWebAssetEndpointSelector.ToMetadataValue(Selectors) }, + { nameof(ResponseHeaders), !_responseHeadersModified ? ResponseHeadersString ?? "" : StaticWebAssetEndpointResponseHeader.ToMetadataValue(ResponseHeaders) }, + { nameof(EndpointProperties), !_endpointPropertiesModified ? EndpointPropertiesString ?? "" : StaticWebAssetEndpointProperty.ToMetadataValue(EndpointProperties) } + }; + + if (_additionalCustomMetadata != null) + { + foreach (var kvp in _additionalCustomMetadata) + { + result[kvp.Key] = kvp.Value; + } + } + + return result; + } + + string ITaskItem.GetMetadata(string metadataName) => ((ITaskItem2)this).GetMetadataValueEscaped(metadataName); + + void ITaskItem.SetMetadata(string metadataName, string metadataValue) => ((ITaskItem2)this).SetMetadataValueLiteral(metadataName, metadataValue); + + void ITaskItem.RemoveMetadata(string metadataName) => _additionalCustomMetadata?.Remove(metadataName); + + void ITaskItem.CopyMetadataTo(ITaskItem destinationItem) + { + destinationItem.SetMetadata(nameof(AssetFile), AssetFile ?? ""); + destinationItem.SetMetadata(nameof(Selectors), !_selectorsModified ? SelectorsString ?? "" : StaticWebAssetEndpointSelector.ToMetadataValue(Selectors)); + destinationItem.SetMetadata(nameof(ResponseHeaders), !_responseHeadersModified ? ResponseHeadersString ?? "" : StaticWebAssetEndpointResponseHeader.ToMetadataValue(ResponseHeaders)); + destinationItem.SetMetadata(nameof(EndpointProperties), !_endpointPropertiesModified ? EndpointPropertiesString ?? "" : StaticWebAssetEndpointProperty.ToMetadataValue(EndpointProperties)); + + if (_additionalCustomMetadata != null) + { + foreach (var kvp in _additionalCustomMetadata) + { + destinationItem.SetMetadata(kvp.Key, kvp.Value ?? ""); + } + } + } + + IDictionary ITaskItem.CloneCustomMetadata() => ((ITaskItem2)this).CloneCustomMetadataEscaped(); + + #endregion } diff --git a/src/StaticWebAssetsSdk/Tasks/DefineStaticWebAssets.Cache.cs b/src/StaticWebAssetsSdk/Tasks/DefineStaticWebAssets.Cache.cs index b9cd0e25b386..f28987f86c22 100644 --- a/src/StaticWebAssetsSdk/Tasks/DefineStaticWebAssets.Cache.cs +++ b/src/StaticWebAssetsSdk/Tasks/DefineStaticWebAssets.Cache.cs @@ -41,7 +41,7 @@ private DefineStaticWebAssetsCache GetOrCreateAssetsCache() var patternMetadata = new[] { nameof(FingerprintPattern.Pattern), nameof(FingerprintPattern.Expression) }; var fingerprintPatternsHash = HashingUtils.ComputeHash(memoryStream, FingerprintPatterns ?? [], patternMetadata); - var propertyOverridesHash = HashingUtils.ComputeHash(memoryStream, PropertyOverrides, nameof(ITaskItem.GetMetadata)); + var propertyOverridesHash = HashingUtils.ComputeHash(memoryStream, PropertyOverrides ?? []); #if NET9_0_OR_GREATER Span<string> candidateAssetMetadata = [ diff --git a/src/StaticWebAssetsSdk/Tasks/DefineStaticWebAssets.cs b/src/StaticWebAssetsSdk/Tasks/DefineStaticWebAssets.cs index c31db63a574e..494e3c3263b9 100644 --- a/src/StaticWebAssetsSdk/Tasks/DefineStaticWebAssets.cs +++ b/src/StaticWebAssetsSdk/Tasks/DefineStaticWebAssets.cs @@ -25,7 +25,7 @@ public partial class DefineStaticWebAssets : Task [Required] public ITaskItem[] CandidateAssets { get; set; } - public ITaskItem[] PropertyOverrides { get; set; } + public string[] PropertyOverrides { get; set; } public string SourceId { get; set; } @@ -73,8 +73,11 @@ public partial class DefineStaticWebAssets : Task public Func<string, string, (FileInfo file, long fileLength, DateTimeOffset lastWriteTimeUtc)> TestResolveFileDetails { get; set; } + private HashSet<string> _overrides; + public override bool Execute() { + _overrides = new HashSet<string>(PropertyOverrides ?? [], StringComparer.OrdinalIgnoreCase); var assetsCache = GetOrCreateAssetsCache(); if (assetsCache.IsUpToDate()) @@ -96,7 +99,7 @@ public override bool Execute() new StaticWebAssetGlobMatcherBuilder().AddIncludePatterns(RelativePathFilter).Build() : null; - var assetsByRelativePath = new Dictionary<string, List<ITaskItem>>(); + var assetsByRelativePath = new Dictionary<string, (ITaskItem First, ITaskItem Second)>(CandidateAssets.Length); var fingerprintPatternMatcher = new FingerprintPatternMatcher(Log, FingerprintCandidates ? (FingerprintPatterns ?? []) : []); var matchContext = StaticWebAssetGlobMatcher.CreateMatchContext(); foreach (var kvp in assetsCache.OutOfDateInputs()) @@ -356,7 +359,7 @@ public override bool Execute() private string ComputePropertyValue(ITaskItem element, string metadataName, string propertyValue, bool isRequired = true) { - if (PropertyOverrides != null && PropertyOverrides.Any(a => string.Equals(a.ItemSpec, metadataName, StringComparison.OrdinalIgnoreCase))) + if (_overrides.Contains(metadataName)) { return propertyValue; } @@ -428,7 +431,9 @@ private string GetCandidateMatchPath(ITaskItem candidate) } } - private void UpdateAssetKindIfNecessary(Dictionary<string, List<ITaskItem>> assetsByRelativePath, string candidateRelativePath, ITaskItem asset) + private void UpdateAssetKindIfNecessary( + Dictionary<string, (ITaskItem First, ITaskItem Second)> assetsByRelativePath, + string candidateRelativePath, ITaskItem asset) { // We want to support content items in the form of // <Content Include="service-worker.development.js CopyToPublishDirectory="Never" TargetPath="wwwroot\service-worker.js" /> @@ -439,14 +444,13 @@ private void UpdateAssetKindIfNecessary(Dictionary<string, List<ITaskItem>> asse // As a result, assets by default have an asset kind 'All' when there is only one asset for the target path and 'Build' or 'Publish' when there are two of them. if (!assetsByRelativePath.TryGetValue(candidateRelativePath, out var existing)) { - assetsByRelativePath.Add(candidateRelativePath, [asset]); + assetsByRelativePath.Add(candidateRelativePath, (asset, null)); } else { - if (existing.Count == 2) + var (first, second) = existing; + if (first != null && second != null) { - var first = existing[0]; - var second = existing[1]; var errorMessage = "More than two assets are targeting the same path: " + Environment.NewLine + "'{0}' with kind '{1}'" + Environment.NewLine + "'{2}' with kind '{3}'" + Environment.NewLine + @@ -462,9 +466,9 @@ private void UpdateAssetKindIfNecessary(Dictionary<string, List<ITaskItem>> asse return; } - else if (existing.Count == 1) + else if (first != null && second == null) { - var existingAsset = existing[0]; + var existingAsset = first; switch ((asset.GetMetadata(nameof(StaticWebAsset.CopyToPublishDirectory)), existingAsset.GetMetadata(nameof(StaticWebAsset.CopyToPublishDirectory)))) { case (StaticWebAsset.AssetCopyOptions.Never, StaticWebAsset.AssetCopyOptions.Never): @@ -484,7 +488,7 @@ private void UpdateAssetKindIfNecessary(Dictionary<string, List<ITaskItem>> asse break; case (StaticWebAsset.AssetCopyOptions.Never, not StaticWebAsset.AssetCopyOptions.Never): - existing.Add(asset); + existing.Second = asset; asset.SetMetadata(nameof(StaticWebAsset.AssetKind), StaticWebAsset.AssetKinds.Build); existingAsset.SetMetadata(nameof(StaticWebAsset.AssetKind), StaticWebAsset.AssetKinds.Publish); Log.LogMessage(MessageImportance.Low, @@ -504,7 +508,7 @@ private void UpdateAssetKindIfNecessary(Dictionary<string, List<ITaskItem>> asse break; case (not StaticWebAsset.AssetCopyOptions.Never, StaticWebAsset.AssetCopyOptions.Never): - existing.Add(asset); + existing.Second = asset; asset.SetMetadata(nameof(StaticWebAsset.AssetKind), StaticWebAsset.AssetKinds.Publish); existingAsset.SetMetadata(nameof(StaticWebAsset.AssetKind), StaticWebAsset.AssetKinds.Build); Log.LogMessage(MessageImportance.Low, diff --git a/src/StaticWebAssetsSdk/Tasks/FilterStaticWebAssetEndpoints.cs b/src/StaticWebAssetsSdk/Tasks/FilterStaticWebAssetEndpoints.cs index 7976724ad6b4..5ddbd12f8530 100644 --- a/src/StaticWebAssetsSdk/Tasks/FilterStaticWebAssetEndpoints.cs +++ b/src/StaticWebAssetsSdk/Tasks/FilterStaticWebAssetEndpoints.cs @@ -32,7 +32,7 @@ public class FilterStaticWebAssetEndpoints : Task public override bool Execute() { var filterCriteria = (Filters ?? []).Select(FilterCriteria.FromTaskItem).ToArray(); - var assetFiles = (Assets ?? []).ToDictionary(a => a.ItemSpec, StaticWebAsset.FromTaskItem); + var assetFiles = Assets != null ? StaticWebAsset.ToAssetDictionary(Assets) : []; var endpoints = StaticWebAssetEndpoint.FromItemGroup(Endpoints ?? []); var endpointFoundMatchingAsset = new Dictionary<string, StaticWebAsset>(); diff --git a/src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetEndpointsManifest.cs b/src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetEndpointsManifest.cs index 17a80c495dff..61706aebbc4f 100644 --- a/src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetEndpointsManifest.cs +++ b/src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetEndpointsManifest.cs @@ -37,7 +37,8 @@ public override bool Execute() try { // Get the list of the asset that need to be part of the manifest (this is similar to GenerateStaticWebAssetsDevelopmentManifest) - var manifestAssets = ComputeManifestAssets(Assets.Select(StaticWebAsset.FromTaskItem), ManifestType) + var assets = StaticWebAsset.FromTaskItemGroup(Assets); + var manifestAssets = ComputeManifestAssets(assets, ManifestType) .ToDictionary(a => a.ResolvedAsset.Identity, a => a, OSPath.PathComparer); // Filter out the endpoints to those that point to the assets that are part of the manifest diff --git a/src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetEndpointsPropsFile.cs b/src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetEndpointsPropsFile.cs index 87682fe487cc..071f42ac285d 100644 --- a/src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetEndpointsPropsFile.cs +++ b/src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetEndpointsPropsFile.cs @@ -25,7 +25,7 @@ public class GenerateStaticWebAssetEndpointsPropsFile : Task public override bool Execute() { var endpoints = StaticWebAssetEndpoint.FromItemGroup(StaticWebAssetEndpoints); - var assets = StaticWebAssets.Select(StaticWebAsset.FromTaskItem).ToDictionary(a => a.Identity, a => a); + var assets = StaticWebAsset.ToAssetDictionary(StaticWebAssets); if (!ValidateArguments(endpoints, assets)) { return false; diff --git a/src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetsDevelopmentManifest.cs b/src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetsDevelopmentManifest.cs index a70eaf4acd68..00d4f39c69e1 100644 --- a/src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetsDevelopmentManifest.cs +++ b/src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetsDevelopmentManifest.cs @@ -49,7 +49,7 @@ public override bool Execute() } var manifest = ComputeDevelopmentManifest( - Assets.Select(StaticWebAsset.FromTaskItem), + StaticWebAsset.FromTaskItemGroup(Assets), DiscoveryPatterns.Select(StaticWebAssetsDiscoveryPattern.FromTaskItem)); PersistManifest(manifest); diff --git a/src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetsManifest.cs b/src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetsManifest.cs index 8acdee2f6f73..1bf04ed48714 100644 --- a/src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetsManifest.cs +++ b/src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetsManifest.cs @@ -44,19 +44,22 @@ public override bool Execute() { try { - var assets = Assets.OrderBy(a => a.GetMetadata("FullPath")).Select(StaticWebAsset.FromTaskItem).ToArray(); + var assets = StaticWebAsset.FromTaskItemGroup(Assets, validate: true); + Array.Sort(assets, (l, r) => string.CompareOrdinal(l.Identity, r.Identity)); - var endpoints = FilterPublishEndpointsIfNeeded(assets) - .OrderBy(a => a.Route) - .ThenBy(a => a.AssetFile) - .ToArray(); + var endpoints = FilterPublishEndpointsIfNeeded(assets); + Array.Sort(endpoints, (l, r) => string.CompareOrdinal(l.Route, r.Route) switch + { + 0 => string.CompareOrdinal(l.AssetFile, r.AssetFile), + int result => result, + }); Log.LogMessage(MessageImportance.Low, "Generating manifest for '{0}' assets and '{1}' endpoints", assets.Length, endpoints.Length); - var assetsByTargetPath = assets.GroupBy(a => a.ComputeTargetPath("", '/'), StringComparer.OrdinalIgnoreCase); + var assetsByTargetPath = GroupAssetsByTargetPath(assets); foreach (var group in assetsByTargetPath) { - if (!StaticWebAsset.ValidateAssetGroup(group.Key, [.. group], out var reason)) + if (!StaticWebAsset.ValidateAssetGroup(group.Key, group.Value, out var reason)) { Log.LogError(reason); return false; @@ -90,7 +93,7 @@ public override bool Execute() return !Log.HasLoggedErrors; } - private IEnumerable<StaticWebAssetEndpoint> FilterPublishEndpointsIfNeeded(IEnumerable<StaticWebAsset> assets) + private StaticWebAssetEndpoint[] FilterPublishEndpointsIfNeeded(StaticWebAsset[] assets) { // Only include endpoints for assets that are going to be available in production. We do the filtering // inside the manifest because its cumbersome to do it in MSBuild directly. @@ -112,10 +115,10 @@ private IEnumerable<StaticWebAssetEndpoint> FilterPublishEndpointsIfNeeded(IEnum } } - return filteredEndpoints; + return [.. filteredEndpoints]; } - return Endpoints.Select(StaticWebAssetEndpoint.FromTaskItem); + return StaticWebAssetEndpoint.FromItemGroup(Endpoints); } private void PersistManifest(StaticWebAssetsManifest manifest) @@ -148,4 +151,36 @@ private void PersistManifest(StaticWebAssetsManifest manifest) Log.LogMessage(MessageImportance.Low, $"Skipping manifest updated because manifest version '{manifest.Hash}' has not changed."); } } + + private static Dictionary<string, (StaticWebAsset First, StaticWebAsset Second, List<StaticWebAsset> Other)> GroupAssetsByTargetPath(StaticWebAsset[] assets) + { + var result = new Dictionary<string, (StaticWebAsset First, StaticWebAsset Second, List<StaticWebAsset> Other)>(StringComparer.OrdinalIgnoreCase); + + foreach (var asset in assets) + { + var targetPath = asset.ComputeTargetPath("", '/'); + + if (result.TryGetValue(targetPath, out var existing)) + { + if (existing.Second == null) + { + // We have first but not second + result[targetPath] = (existing.First, asset, null); + } + else + { + // We already have first and second, add to rest + existing.Other ??= []; + existing.Other.Add(asset); + } + } + else + { + // First asset with this path + result.Add(targetPath, (asset, null, null)); + } + } + + return result; + } } diff --git a/src/StaticWebAssetsSdk/Tasks/MergeConfigurationProperties.cs b/src/StaticWebAssetsSdk/Tasks/MergeConfigurationProperties.cs index cd8a1aea0fbd..97820eae8836 100644 --- a/src/StaticWebAssetsSdk/Tasks/MergeConfigurationProperties.cs +++ b/src/StaticWebAssetsSdk/Tasks/MergeConfigurationProperties.cs @@ -25,7 +25,7 @@ public override bool Execute() { try { - ProjectConfigurations = new TaskItem[CandidateConfigurations.Length]; + ProjectConfigurations = new ITaskItem[CandidateConfigurations.Length]; for (var i = 0; i < CandidateConfigurations.Length; i++) { diff --git a/src/StaticWebAssetsSdk/Tasks/MergeStaticWebAssets.cs b/src/StaticWebAssetsSdk/Tasks/MergeStaticWebAssets.cs index 0d16ba60c071..dbd49ba5a3e2 100644 --- a/src/StaticWebAssetsSdk/Tasks/MergeStaticWebAssets.cs +++ b/src/StaticWebAssetsSdk/Tasks/MergeStaticWebAssets.cs @@ -27,7 +27,8 @@ public class MergeStaticWebAssets : Task public override bool Execute() { - var assets = CandidateAssets.OrderBy(a => a.GetMetadata("FullPath")).Select(StaticWebAsset.FromTaskItem); + var assets = StaticWebAsset.FromTaskItemGroup(CandidateAssets); + Array.Sort(assets, (a, b) => string.CompareOrdinal(a.Identity, b.Identity)); var assetsByTargetPath = assets .GroupBy(a => a.ComputeTargetPath("", '/'), StringComparer.OrdinalIgnoreCase) diff --git a/src/StaticWebAssetsSdk/Tasks/ResolveFingerprintedStaticWebAssetEndpointsForAssets.cs b/src/StaticWebAssetsSdk/Tasks/ResolveFingerprintedStaticWebAssetEndpointsForAssets.cs index c62afb9e0d59..ab4969e9a8f9 100644 --- a/src/StaticWebAssetsSdk/Tasks/ResolveFingerprintedStaticWebAssetEndpointsForAssets.cs +++ b/src/StaticWebAssetsSdk/Tasks/ResolveFingerprintedStaticWebAssetEndpointsForAssets.cs @@ -26,7 +26,7 @@ public class ResolveFingerprintedStaticWebAssetEndpointsForAssets : Task public override bool Execute() { var candidateEndpoints = StaticWebAssetEndpoint.FromItemGroup(CandidateEndpoints); - var candidateAssets = CandidateAssets.Select(StaticWebAsset.FromTaskItem).ToArray(); + var candidateAssets = StaticWebAsset.FromTaskItemGroup(CandidateAssets); var resolvedEndpoints = new List<StaticWebAssetEndpoint>(); var endpointsByAsset = candidateEndpoints.GroupBy(e => e.AssetFile, OSPath.PathComparer) diff --git a/src/StaticWebAssetsSdk/Tasks/ResolveStaticWebAssetEndpointRoutes.cs b/src/StaticWebAssetsSdk/Tasks/ResolveStaticWebAssetEndpointRoutes.cs index 188a586dc5fe..b3138921d14e 100644 --- a/src/StaticWebAssetsSdk/Tasks/ResolveStaticWebAssetEndpointRoutes.cs +++ b/src/StaticWebAssetsSdk/Tasks/ResolveStaticWebAssetEndpointRoutes.cs @@ -18,7 +18,7 @@ public class ResolveStaticWebAssetEndpointRoutes : Task public override bool Execute() { var endpoints = StaticWebAssetEndpoint.FromItemGroup(Endpoints); - var assets = Assets.Select(StaticWebAsset.FromTaskItem).ToDictionary(a => a.Identity, a => a); + var assets = StaticWebAsset.ToAssetDictionary(Assets); foreach (var endpoint in endpoints) { diff --git a/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/ComputeEndpointsForReferenceStaticWebAssetsTest.cs b/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/ComputeEndpointsForReferenceStaticWebAssetsTest.cs index 9eea5aa2c9d8..c4f588ccb014 100644 --- a/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/ComputeEndpointsForReferenceStaticWebAssetsTest.cs +++ b/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/ComputeEndpointsForReferenceStaticWebAssetsTest.cs @@ -103,7 +103,7 @@ private static ITaskItem CreateCandidate( return result.ToTaskItem(); } - private static TaskItem CreateCandidateEndpoint(string route, string assetFile) + private static ITaskItem CreateCandidateEndpoint(string route, string assetFile) { return new StaticWebAssetEndpoint { diff --git a/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/ComputeReferenceStaticWebAssetItemsTest.cs b/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/ComputeReferenceStaticWebAssetItemsTest.cs index ba3466eeedd7..3520d8438c63 100644 --- a/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/ComputeReferenceStaticWebAssetItemsTest.cs +++ b/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/ComputeReferenceStaticWebAssetItemsTest.cs @@ -23,7 +23,7 @@ public void IncludesAssetsFromCurrentProjectAsReferencedAssets() { BuildEngine = buildEngine.Object, Source = "MyPackage", - Assets = new[] { CreateCandidate("wwwroot\\candidate.js", "MyPackage", "Discovered", "candidate.js", "All", "All") }, + Assets = new[] { CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate.js", "All", "All") }, Patterns = new ITaskItem[] { }, AssetKind = "Build", ProjectMode = "Default" @@ -49,7 +49,7 @@ public void IncludesPatternsFromCurrentProject() { BuildEngine = buildEngine.Object, Source = "MyPackage", - Assets = new[] { CreateCandidate("wwwroot\\candidate.js", "MyPackage", "Discovered", "candidate.js", "All", "All") }, + Assets = new[] { CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate.js", "All", "All") }, Patterns = new[] { CreatePatternCandidate("MyPackage\\wwwroot", "base", Directory.GetCurrentDirectory(), "wwwroot\\**", "MyPackage") }, AssetKind = "Build", ProjectMode = "Default" @@ -75,7 +75,7 @@ public void FiltersPatternsFromReferencedProjects() { BuildEngine = buildEngine.Object, Source = "MyPackage", - Assets = new[] { CreateCandidate("wwwroot\\candidate.js", "MyPackage", "Discovered", "candidate.js", "All", "All") }, + Assets = new[] { CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate.js", "All", "All") }, Patterns = new[] { CreatePatternCandidate("Other\\wwwroot", "base", Directory.GetCurrentDirectory(), "wwwroot\\**", "Other") }, AssetKind = "Build", ProjectMode = "Default" @@ -103,8 +103,8 @@ public void PrefersSpecificKindAssetsOverAllKindAssets() Source = "MyPackage", Assets = new[] { - CreateCandidate("wwwroot\\candidate.js", "MyPackage", "Discovered", "candidate.js", "All", "All"), - CreateCandidate("wwwroot\\candidate.other.js", "MyPackage", "Discovered", "candidate.js", "Build", "All") + CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate.js", "All", "All"), + CreateCandidate(Path.Combine("wwwroot", "candidate.other.js"), "MyPackage", "Discovered", "candidate.js", "Build", "All") }, Patterns = new ITaskItem[] { }, AssetKind = "Build", @@ -134,9 +134,9 @@ public void AllAssetGetsIgnoredWhenBuildAndPublishAssetsAreDefined() Source = "MyPackage", Assets = new[] { - CreateCandidate("wwwroot\\candidate.js", "MyPackage", "Discovered", "candidate.js", "All", "All"), - CreateCandidate("wwwroot\\candidate.other.js", "MyPackage", "Discovered", "candidate.js", "Build", "All"), - CreateCandidate("wwwroot\\candidate.publish.js", "MyPackage", "Discovered", "candidate.js", "Publish", "All") + CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate.js", "All", "All"), + CreateCandidate(Path.Combine("wwwroot", "candidate.other.js"), "MyPackage", "Discovered", "candidate.js", "Build", "All"), + CreateCandidate(Path.Combine("wwwroot", "candidate.publish.js"), "MyPackage", "Discovered", "candidate.js", "Publish", "All") }, Patterns = new ITaskItem[] { }, AssetKind = "Build", @@ -166,7 +166,7 @@ public void FiltersAssetsForOppositeKind(string assetKind, string manifestKind) { BuildEngine = buildEngine.Object, Source = "MyPackage", - Assets = new[] { CreateCandidate("wwwroot\\candidate.js", "MyPackage", "Discovered", "candidate.js", assetKind, "All") }, + Assets = new[] { CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate.js", assetKind, "All") }, Patterns = new ITaskItem[] { }, AssetKind = manifestKind, ProjectMode = "Default" @@ -192,7 +192,7 @@ public void FiltersCurrentProjectOnlyAssetsInDefaultMode() { BuildEngine = buildEngine.Object, Source = "MyPackage", - Assets = new[] { CreateCandidate("wwwroot\\candidate.js", "MyPackage", "Discovered", "candidate.js", "All", "CurrentProject") }, + Assets = new[] { CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate.js", "All", "CurrentProject") }, Patterns = new ITaskItem[] { }, AssetKind = "Default", ProjectMode = "Default" @@ -218,7 +218,7 @@ public void IncludesReferenceAssetsInDefaultMode() { BuildEngine = buildEngine.Object, Source = "MyPackage", - Assets = new[] { CreateCandidate("wwwroot\\candidate.js", "MyPackage", "Discovered", "candidate.js", "All", "Reference") }, + Assets = new[] { CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate.js", "All", "Reference") }, Patterns = new ITaskItem[] { }, AssetKind = "Default", ProjectMode = "Default" @@ -244,7 +244,7 @@ public void IncludesCurrentProjectAssetsInRootMode() { BuildEngine = buildEngine.Object, Source = "MyPackage", - Assets = new[] { CreateCandidate("wwwroot\\candidate.js", "MyPackage", "Discovered", "candidate.js", "All", "CurrentProject") }, + Assets = new[] { CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate.js", "All", "CurrentProject") }, Patterns = new ITaskItem[] { }, AssetKind = "Default", ProjectMode = "Root" @@ -270,7 +270,7 @@ public void FiltersReferenceOnlyAssetsInRootMode() { BuildEngine = buildEngine.Object, Source = "MyPackage", - Assets = new[] { CreateCandidate("wwwroot\\candidate.js", "MyPackage", "Discovered", "candidate.js", "All", "Reference") }, + Assets = new[] { CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate.js", "All", "Reference") }, Patterns = new ITaskItem[] { }, AssetKind = "Default", ProjectMode = "Root" @@ -296,7 +296,7 @@ public void FiltersAssetsFromOtherProjects() { BuildEngine = buildEngine.Object, Source = "MyPackage", - Assets = new[] { CreateCandidate("wwwroot\\candidate.js", "Other", "Project", "candidate.js", "All", "All") }, + Assets = new[] { CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "Other", "Project", "candidate.js", "All", "All") }, Patterns = new ITaskItem[] { }, AssetKind = "Build", ProjectMode = "Default" @@ -322,7 +322,7 @@ public void FiltersAssetsFromPackages() { BuildEngine = buildEngine.Object, Source = "MyPackage", - Assets = new[] { CreateCandidate("wwwroot\\candidate.js", "Other", "Package", "candidate.js", "All", "All") }, + Assets = new[] { CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "Other", "Package", "candidate.js", "All", "All") }, Patterns = new ITaskItem[] { }, AssetKind = "Build", ProjectMode = "Default" diff --git a/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/ComputeStaticWebAssetsForCurrentProjectTest.cs b/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/ComputeStaticWebAssetsForCurrentProjectTest.cs index 3006dfd1d314..7e9883ec140f 100644 --- a/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/ComputeStaticWebAssetsForCurrentProjectTest.cs +++ b/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/ComputeStaticWebAssetsForCurrentProjectTest.cs @@ -51,8 +51,8 @@ public void PrefersSpecificKindAssetsOverAllKindAssets() Source = "MyPackage", Assets = new[] { - CreateCandidate("wwwroot\\candidate.js", "MyPackage", "Discovered", "candidate.js", "All", "All"), - CreateCandidate("wwwroot\\candidate.other.js", "MyPackage", "Discovered", "candidate.js", "Build", "All") + CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate.js", "All", "All"), + CreateCandidate(Path.Combine("wwwroot", "candidate.other.js"), "MyPackage", "Discovered", "candidate.js", "Build", "All") }, AssetKind = "Build", ProjectMode = "Default" @@ -81,9 +81,9 @@ public void AllAssetGetsIgnoredWhenBuildAndPublishAssetsAreDefined() Source = "MyPackage", Assets = new[] { - CreateCandidate("wwwroot\\candidate.js", "MyPackage", "Discovered", "candidate.js", "All", "All"), - CreateCandidate("wwwroot\\candidate.other.js", "MyPackage", "Discovered", "candidate.js", "Build", "All"), - CreateCandidate("wwwroot\\candidate.publish.js", "MyPackage", "Discovered", "candidate.js", "Publish", "All") + CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate.js", "All", "All"), + CreateCandidate(Path.Combine("wwwroot", "candidate.other.js"), "MyPackage", "Discovered", "candidate.js", "Build", "All"), + CreateCandidate(Path.Combine("wwwroot", "candidate.publish.js"), "MyPackage", "Discovered", "candidate.js", "Publish", "All") }, AssetKind = "Build", ProjectMode = "Default" diff --git a/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/GenerateStaticWebAssetEndpointsPropsFileTest.cs b/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/GenerateStaticWebAssetEndpointsPropsFileTest.cs index babb4c8c5c67..db86ce0c9efc 100644 --- a/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/GenerateStaticWebAssetEndpointsPropsFileTest.cs +++ b/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/GenerateStaticWebAssetEndpointsPropsFileTest.cs @@ -208,7 +208,7 @@ private static ITaskItem CreateStaticWebAsset( return result.ToTaskItem(); } - private static TaskItem CreateStaticWebAssetEndpoint( + private static ITaskItem CreateStaticWebAssetEndpoint( string route, string assetFile, StaticWebAssetEndpointResponseHeader[] responseHeaders = null, diff --git a/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/ResolveFingerprintedStaticWebAssetEndpointsForAssetsTest.cs b/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/ResolveFingerprintedStaticWebAssetEndpointsForAssetsTest.cs index 8a0da4bc9bb1..95e0f87a0bc5 100644 --- a/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/ResolveFingerprintedStaticWebAssetEndpointsForAssetsTest.cs +++ b/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/ResolveFingerprintedStaticWebAssetEndpointsForAssetsTest.cs @@ -36,7 +36,7 @@ public void Standalone_Selects_EndpointMatching_FilePath(string pattern, string ) ]; - var endpoints = CreateEndpoints(candidateAssets.Select(StaticWebAsset.FromTaskItem).ToArray()); + var endpoints = CreateEndpoints(candidateAssets.Select(a => StaticWebAsset.FromTaskItem(a)).ToArray()); var resolvedEndpoints = new ResolveFingerprintedStaticWebAssetEndpointsForAssets { @@ -79,7 +79,7 @@ public void StandaloneFails_MatchingEndpointNotFound() ) ]; - var endpoints = CreateEndpoints(candidateAssets.Select(StaticWebAsset.FromTaskItem).ToArray()); + var endpoints = CreateEndpoints(candidateAssets.Select(a => StaticWebAsset.FromTaskItem(a)).ToArray()); endpoints = endpoints.Where(e => !e.Route.Contains("asdf1234")).ToArray(); var resolvedEndpoints = new ResolveFingerprintedStaticWebAssetEndpointsForAssets @@ -119,7 +119,7 @@ public void Hosted_AlwaysPrefers_FingerprintedEndpoint(string pattern, string ex ) ]; - var endpoints = CreateEndpoints(candidateAssets.Select(StaticWebAsset.FromTaskItem).ToArray()); + var endpoints = CreateEndpoints(candidateAssets.Select(a => StaticWebAsset.FromTaskItem(a)).ToArray()); var resolvedEndpoints = new ResolveFingerprintedStaticWebAssetEndpointsForAssets { @@ -162,7 +162,7 @@ public void Hosted_FallsBackToNonFingerprintedEndpoint_WhenFingerprintedVersionN ) ]; - var endpoints = CreateEndpoints(candidateAssets.Select(StaticWebAsset.FromTaskItem).ToArray()); + var endpoints = CreateEndpoints(candidateAssets.Select(a => StaticWebAsset.FromTaskItem(a)).ToArray()); var resolvedEndpoints = new ResolveFingerprintedStaticWebAssetEndpointsForAssets { @@ -205,7 +205,7 @@ public void Hosted_FailsWhen_DoesnotFindMatchingEndpoint() ) ]; - var endpoints = CreateEndpoints(candidateAssets.Select(StaticWebAsset.FromTaskItem).ToArray()); + var endpoints = CreateEndpoints(candidateAssets.Select(a => StaticWebAsset.FromTaskItem(a)).ToArray()); endpoints = endpoints.Where(e => !e.Route.Contains("asdf1234")).ToArray(); endpoints[0].AssetFile = Path.GetFullPath("other.js");