diff --git a/src/Build.UnitTests/BackEnd/TaskHostConfiguration_Tests.cs b/src/Build.UnitTests/BackEnd/TaskHostConfiguration_Tests.cs
index da40bef3702..8b77b14045c 100644
--- a/src/Build.UnitTests/BackEnd/TaskHostConfiguration_Tests.cs
+++ b/src/Build.UnitTests/BackEnd/TaskHostConfiguration_Tests.cs
@@ -463,8 +463,14 @@ public void TestTranslationWithAppDomainSetup(byte[] configBytes)
setup.SetConfigurationBytes(configBytes);
- ((ITranslatable)config).Translate(TranslationHelpers.GetWriteTranslator());
- INodePacket packet = TaskHostConfiguration.FactoryForDeserialization(TranslationHelpers.GetReadTranslator());
+ // Set version to 0 for CLR4 (Framework-to-Framework) communication which supports AppDomain.
+ ITranslator writeTranslator = TranslationHelpers.GetWriteTranslator();
+ writeTranslator.NegotiatedPacketVersion = 0;
+ ((ITranslatable)config).Translate(writeTranslator);
+
+ ITranslator readTranslator = TranslationHelpers.GetReadTranslator();
+ readTranslator.NegotiatedPacketVersion = 0;
+ INodePacket packet = TaskHostConfiguration.FactoryForDeserialization(readTranslator);
TaskHostConfiguration deserializedConfig = packet as TaskHostConfiguration;
diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs
index c29d76b1c52..4047772c64e 100644
--- a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs
+++ b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs
@@ -849,6 +849,12 @@ private void DrainPacketQueue(object state)
NodePacketTypeExtensions.WriteVersion(writeStream, context._negotiatedPacketVersion);
writeTranslator.NegotiatedPacketVersion = context._negotiatedPacketVersion;
}
+ else if (!Handshake.IsHandshakeOptionEnabled(_handshakeOptions, HandshakeOptions.CLR2))
+ {
+ // CLR4 task hosts: set version to 0 to enable version-dependent fields.
+ // CLR2 task hosts: leave as null (default) to skip version-dependent fields.
+ writeTranslator.NegotiatedPacketVersion = 0;
+ }
packet.Translate(writeTranslator);
diff --git a/src/Framework/BinaryTranslator.cs b/src/Framework/BinaryTranslator.cs
index f25c79df899..684929529b0 100644
--- a/src/Framework/BinaryTranslator.cs
+++ b/src/Framework/BinaryTranslator.cs
@@ -119,7 +119,7 @@ public TranslationDirection Mode
}
///
- public byte NegotiatedPacketVersion { get; set; }
+ public byte? NegotiatedPacketVersion { get; set; }
///
/// Translates a boolean.
@@ -1006,7 +1006,7 @@ public TranslationDirection Mode
}
///
- public byte NegotiatedPacketVersion { get; set; }
+ public byte? NegotiatedPacketVersion { get; set; }
///
/// Translates a boolean.
diff --git a/src/Framework/ITranslator.cs b/src/Framework/ITranslator.cs
index 71059a49272..3b191f6503c 100644
--- a/src/Framework/ITranslator.cs
+++ b/src/Framework/ITranslator.cs
@@ -84,8 +84,12 @@ internal interface ITranslator : IDisposable
/// from NodePacketTypeExtensions.PacketVersion when nodes are running different MSBuild versions.
/// The negotiated version is used to conditionally serialize/deserialize fields that may
/// not be supported by older packet versions.
+ /// Special values:
+ /// null: CLR2 (NET35) task host communication. Version-dependent fields are skipped because NET35 doesn't have them.
+ /// 0: The constant value for Framework-to-Framework (CLR4) task host communication. Supports HostServices, TargetName, ProjectFile, and AppDomain.
+ /// 2+: .NET task host communication with full support for version-dependent fields.
///
- byte NegotiatedPacketVersion { get; set; }
+ byte? NegotiatedPacketVersion { get; set; }
///
/// Returns the current serialization mode.
diff --git a/src/Shared/INodePacket.cs b/src/Shared/INodePacket.cs
index af4502e57f0..690420ba214 100644
--- a/src/Shared/INodePacket.cs
+++ b/src/Shared/INodePacket.cs
@@ -291,11 +291,9 @@ internal static class NodePacketTypeExtensions
///
/// Defines the communication protocol version for node communication.
///
- /// Version 1: Introduced for the .NET Task Host protocol. This version
- /// excludes the translation of appDomainConfig within TaskHostConfiguration
- /// to maintain backward compatibility and reduce serialization overhead.
- ///
- /// Version 2: Adds support of HostServices and target name translation in TaskHostConfiguration.
+ /// null: CLR2 (NET35) task host. Version-dependent fields skipped (not compiled in NET35).
+ /// 0: The constant value for Framework-to-Framework (CLR4) task host. Supports HostServices, TargetName, ProjectFile.
+ /// 2+: .NET task host with full support for version-dependent fields.
///
/// When incrementing this version, ensure compatibility with existing
/// task hosts and update the corresponding deserialization logic.
diff --git a/src/Shared/NodeEndpointOutOfProcBase.cs b/src/Shared/NodeEndpointOutOfProcBase.cs
index 038c7949dbb..ae422e25e31 100644
--- a/src/Shared/NodeEndpointOutOfProcBase.cs
+++ b/src/Shared/NodeEndpointOutOfProcBase.cs
@@ -721,6 +721,8 @@ private void RunReadLoop(
ITranslator readTranslator = BinaryTranslator.GetReadTranslator(localReadPipe, _sharedReadBuffer);
// parent sends a packet version that is already negotiated during handshake.
+ // For Framework task hosts (CLR2/CLR4) without extended headers, defaults to 0.
+ // For .NET task hosts, read from extended header (>= 1).
readTranslator.NegotiatedPacketVersion = parentVersion;
_packetFactory.DeserializeAndRoutePacket(0, packetType, readTranslator);
}
diff --git a/src/Shared/TaskHostConfiguration.cs b/src/Shared/TaskHostConfiguration.cs
index ddf1d1c1a5d..31732e30bba 100644
--- a/src/Shared/TaskHostConfiguration.cs
+++ b/src/Shared/TaskHostConfiguration.cs
@@ -480,10 +480,10 @@ public void Translate(ITranslator translator)
translator.TranslateCulture(ref _uiCulture);
#if FEATURE_APPDOMAIN
// The packet version is used to determine if the AppDomain configuration should be serialized.
- // If the packet version is bigger then 0, it means the task host will running under .NET.
- // Although MSBuild.exe runs under .NET Framework and has AppDomain support,
- // we don't transmit AppDomain config when communicating with dotnet.exe (it is not supported in .NET 5+).
- if (translator.NegotiatedPacketVersion == 0)
+ // null = CLR2 (NET35) task host - supports AppDomain
+ // 0 = CLR4 (NET472) task host - supports AppDomain
+ // We serialize AppDomain for Framework task hosts (null or 0), but not for .NET (>= 2).
+ if (translator.NegotiatedPacketVersion is null or 0)
{
byte[] appDomainConfigBytes = null;
@@ -507,14 +507,17 @@ public void Translate(ITranslator translator)
translator.Translate(ref _projectFileOfTask);
translator.Translate(ref _taskName);
translator.Translate(ref _taskLocation);
- if (translator.NegotiatedPacketVersion >= 2)
+
+ // null = CLR2 (NET35) task hosts which don't have these fields compiled in.
+ // 0 = CLR4, 2+ = .NET - both support these fields.
+#if NET472 || NETCOREAPP
+ if (translator.NegotiatedPacketVersion.HasValue && translator.NegotiatedPacketVersion is 0 or >= 2)
{
translator.Translate(ref _targetName);
translator.Translate(ref _projectFile);
-#if !NET35
translator.Translate(ref _hostServices);
-#endif
}
+#endif
translator.Translate(ref _isTaskInputLoggingEnabled);
translator.TranslateDictionary(ref _taskParameters, StringComparer.OrdinalIgnoreCase, TaskParameter.FactoryForDeserialization);