diff --git a/src/Cli/dotnet/commands/dotnet-workload/list/VisualStudioWorkloads.cs b/src/Cli/dotnet/commands/dotnet-workload/list/VisualStudioWorkloads.cs
index 43553bb769e9..66ff2b935416 100644
--- a/src/Cli/dotnet/commands/dotnet-workload/list/VisualStudioWorkloads.cs
+++ b/src/Cli/dotnet/commands/dotnet-workload/list/VisualStudioWorkloads.cs
@@ -25,23 +25,13 @@ internal static class VisualStudioWorkloads
/// Visual Studio product ID filters. We dont' want to query SKUs such as Server, TeamExplorer, TestAgent
/// TestController and BuildTools.
///
- private static readonly string[] s_visualStudioProducts = new string[]
+ private static readonly string[] s_visualStudioProducts =
{
"Microsoft.VisualStudio.Product.Community",
"Microsoft.VisualStudio.Product.Professional",
"Microsoft.VisualStudio.Product.Enterprise",
};
- ///
- /// Default prefix to use for Visual Studio component and component group IDs.
- ///
- private static readonly string s_visualStudioComponentPrefix = "Microsoft.NET.Component";
-
- ///
- /// Well-known prefixes used by some workloads that can be replaced when generating component IDs.
- ///
- private static readonly string[] s_wellKnownWorkloadPrefixes = { "Microsoft.NET.", "Microsoft." };
-
///
/// The SWIX package ID wrapping the SDK installer in Visual Studio. The ID should contain
/// the SDK version as a suffix, e.g., "Microsoft.NetCore.Toolset.5.0.403".
@@ -67,22 +57,9 @@ internal static Dictionary GetAvailableVisualStudioWorkloads(IWo
{
string workloadId = workload.Id.ToString();
// Old style VS components simply replaced '-' with '.' in the workload ID.
- string componentId = workload.Id.ToString().Replace('-', '.');
-
- visualStudioComponentWorkloads.Add(componentId, workloadId);
-
+ visualStudioComponentWorkloads.Add(workload.Id.ToSafeId(), workloadId);
// Starting in .NET 9.0 and VS 17.12, workload components will follow the VS naming convention.
- foreach (string wellKnownPrefix in s_wellKnownWorkloadPrefixes)
- {
- if (componentId.StartsWith(wellKnownPrefix, StringComparison.OrdinalIgnoreCase))
- {
- componentId = componentId.Substring(wellKnownPrefix.Length);
- break;
- }
- }
-
- componentId = s_visualStudioComponentPrefix + "." + componentId;
- visualStudioComponentWorkloads.Add(componentId, workloadId);
+ visualStudioComponentWorkloads.Add(workload.Id.ToSafeId(includeVisualStudioPrefix: true), workloadId);
}
return visualStudioComponentWorkloads;
diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/WorkloadId.cs b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/WorkloadId.cs
index c6adcff13fbc..3517870a3339 100644
--- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/WorkloadId.cs
+++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/WorkloadId.cs
@@ -9,6 +9,10 @@ namespace Microsoft.NET.Sdk.WorkloadManifestReader
///
public readonly struct WorkloadId : IComparable, IEquatable
{
+ private static readonly string s_visualStudioComponentPrefix = "Microsoft.NET.Component";
+
+ private static readonly string[] s_wellKnownWorkloadPrefixes = { "Microsoft.NET.", "Microsoft." };
+
private readonly string _id;
public WorkloadId(string id)
@@ -31,6 +35,27 @@ public WorkloadId(string id)
public override string ToString() => _id;
+ public string ToSafeId(bool includeVisualStudioPrefix = false)
+ {
+ string safeId = _id.Replace('-', '.').Replace(' ', '.').Replace('_', '.');
+
+ if (includeVisualStudioPrefix)
+ {
+ foreach (string wellKnownPrefix in s_wellKnownWorkloadPrefixes)
+ {
+ if (safeId.StartsWith(wellKnownPrefix, StringComparison.OrdinalIgnoreCase))
+ {
+ safeId = safeId.Substring(wellKnownPrefix.Length);
+ break;
+ }
+ }
+
+ safeId = s_visualStudioComponentPrefix + "." + safeId;
+ }
+
+ return safeId;
+ }
+
public static implicit operator string(WorkloadId id) => id._id;
public static bool operator ==(WorkloadId a, WorkloadId b) => a.Equals(b);
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ShowMissingWorkloads.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ShowMissingWorkloads.cs
index f0a32830d6fa..a48a21cd531e 100644
--- a/src/Tasks/Microsoft.NET.Build.Tasks/ShowMissingWorkloads.cs
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/ShowMissingWorkloads.cs
@@ -19,7 +19,7 @@ public class ShowMissingWorkloads : TaskBase
{ "android", "android-aot", "ios", "maccatalyst", "macos", "maui", "maui-android",
"maui-desktop", "maui-ios", "maui-maccatalyst", "maui-mobile", "maui-windows", "tvos" };
private static readonly HashSet WasmWorkloadIds = new(StringComparer.OrdinalIgnoreCase)
- { "wasm-tools", "wasm-tools-net6", "wasm-tools-net7" };
+ { "wasm-tools", "wasm-tools-net6", "wasm-tools-net7", "wasm-tools-net8" };
public ITaskItem[] MissingWorkloadPacks { get; set; }
@@ -70,7 +70,7 @@ out ISet unsatisfiablePacks
{
var suggestedWorkloadsList = GetSuggestedWorkloadsList(suggestedWorkload);
var taskItem = new TaskItem(suggestedWorkload.Id);
- taskItem.SetMetadata("VisualStudioComponentId", ToSafeId(suggestedWorkload.Id));
+ taskItem.SetMetadata("VisualStudioComponentId", suggestedWorkload.Id.ToSafeId(includeVisualStudioPrefix: true));
taskItem.SetMetadata("VisualStudioComponentIds", string.Join(";", suggestedWorkloadsList));
return taskItem;
}).ToArray();
@@ -78,14 +78,10 @@ out ISet unsatisfiablePacks
}
}
- internal static string ToSafeId(string id)
- {
- return id.Replace("-", ".").Replace(" ", ".").Replace("_", ".");
- }
-
private static IEnumerable GetSuggestedWorkloadsList(WorkloadInfo workloadInfo)
{
- yield return ToSafeId(workloadInfo.Id);
+ yield return workloadInfo.Id.ToSafeId();
+ yield return workloadInfo.Id.ToSafeId(includeVisualStudioPrefix: true);
if (MauiWorkloadIds.Contains(workloadInfo.Id.ToString()))
{
yield return MauiCrossPlatTopLevelVSWorkloads;
diff --git a/test/Microsoft.NET.Build.Tests/WorkloadTests.cs b/test/Microsoft.NET.Build.Tests/WorkloadTests.cs
index 7526c08d0353..017ec9d6f61f 100644
--- a/test/Microsoft.NET.Build.Tests/WorkloadTests.cs
+++ b/test/Microsoft.NET.Build.Tests/WorkloadTests.cs
@@ -63,6 +63,7 @@ public void It_should_create_suggested_workload_items()
var getValuesCommand = new GetValuesCommand(testAsset, "SuggestedWorkload", GetValuesCommand.ValueType.Item)
{
+ ShouldEscapeItems = true,
DependsOnTargets = "GetSuggestedWorkloads"
};
getValuesCommand.MetadataNames.Add("VisualStudioComponentId");
@@ -75,11 +76,11 @@ public void It_should_create_suggested_workload_items()
getValuesCommand.GetValuesWithMetadata().Select(valueAndMetadata => (valueAndMetadata.value, valueAndMetadata.metadata["VisualStudioComponentId"]))
.Should()
- .BeEquivalentTo(new[] { ("microsoft-net-sdk-missingtestworkload", "microsoft.net.sdk.missingtestworkload") });
+ .BeEquivalentTo(new[] { ("microsoft-net-sdk-missingtestworkload", "Microsoft.NET.Component.sdk.missingtestworkload") });
getValuesCommand.GetValuesWithMetadata().Select(valueAndMetadata => (valueAndMetadata.value, valueAndMetadata.metadata["VisualStudioComponentIds"]))
.Should()
- .BeEquivalentTo(new[] { ("microsoft-net-sdk-missingtestworkload", "microsoft.net.sdk.missingtestworkload") });
+ .BeEquivalentTo(new[] { ("microsoft-net-sdk-missingtestworkload", "microsoft.net.sdk.missingtestworkload;Microsoft.NET.Component.sdk.missingtestworkload") });
}
[Fact]
diff --git a/test/Microsoft.NET.Sdk.WorkloadManifestReader.Tests/WorkloadIdTests.cs b/test/Microsoft.NET.Sdk.WorkloadManifestReader.Tests/WorkloadIdTests.cs
new file mode 100644
index 000000000000..d91b787d4400
--- /dev/null
+++ b/test/Microsoft.NET.Sdk.WorkloadManifestReader.Tests/WorkloadIdTests.cs
@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.NET.Sdk.WorkloadManifestReader.Tests
+{
+ public class WorkloadIdTests
+ {
+ [Theory]
+ [InlineData("wasm-tools", "wasm.tools")]
+ [InlineData("something_something", "something.something")]
+ public void ItCanCreateSafeIds(string workloadId, string expectedSafeId)
+ {
+ var id = new WorkloadId(workloadId);
+ Assert.Equal(expectedSafeId, id.ToSafeId());
+ }
+
+ [Theory]
+ [InlineData("wasm-tools", "Microsoft.NET.Component.wasm.tools")]
+ [InlineData("microsoft-android-runtime", "Microsoft.NET.Component.android.runtime")]
+ public void ItCanCreateSafeIdsWithVisualStudioStudioPrefix(string workloadId, string expectedSafeId)
+ {
+ var id = new WorkloadId(workloadId);
+ Assert.Equal(expectedSafeId, id.ToSafeId(includeVisualStudioPrefix: true));
+ }
+ }
+}
diff --git a/test/Microsoft.NET.TestFramework/Commands/GetValuesCommand.cs b/test/Microsoft.NET.TestFramework/Commands/GetValuesCommand.cs
index 7288f7a5bc54..32711ced9469 100644
--- a/test/Microsoft.NET.TestFramework/Commands/GetValuesCommand.cs
+++ b/test/Microsoft.NET.TestFramework/Commands/GetValuesCommand.cs
@@ -16,6 +16,8 @@ public enum ValueType
string _valueName;
ValueType _valueType;
+ public bool ShouldEscapeItems { get; set; } = false;
+
public bool ShouldCompile { get; set; } = true;
public string DependsOnTargets { get; set; } = "Compile";
@@ -81,6 +83,13 @@ protected override SdkCommandSpec CreateCommand(IEnumerable args)
{
linesAttribute += $"%09%({_valueName}.{metadataName})";
}
+
+ if (ShouldEscapeItems)
+ {
+ // items with semi-colon delimited values need to be escaped to avoid creating separate items
+ // when the include attribute is evaluated.
+ linesAttribute = $"$([MSBuild]::Escape({linesAttribute}))";
+ }
}
var propertyGroup = project.Root.Elements(ns + "PropertyGroup").FirstOrDefault();