From 127be28e930e55a494a498c0780e619d04faee42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Fri, 12 Jun 2026 01:32:22 +0200 Subject: [PATCH 1/6] test(common): pin SupportedOSPlatform attribute on Service types (RED) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a reflection-based fixture under tests/MTConnect.NET-Common-Tests/Platform/ that asserts every site PR #194 wraps in `#if NET5_0_OR_GREATER` still carries the `[SupportedOSPlatform("windows")]` decoration when the build path yields net8.0. The fixture covers the six sites from the May-22 warnings sweep — WindowsService, MTConnectAgentService, MTConnectAdapterService, both `MTConnect.Applications.Service` types (agent + adapter), and the `Ceen.Httpd.HttpServer.AppDomainBridge.HandleRequest(SocketInformation,...)` plus `HttpServer.RunClient(SocketInformation,...)` HTTP-server sites — and pins the API surface so a future contributor who removes the attribute or narrows the wrap fails the test. The fixture runs only under net8.0 (the only TFM every test project targets), so cannot directly fail under the pre-fix code. Its value is regression-preventive against future removal, matching the TDD shape the brief specifies for sites whose pre-fix surface is already correct on the test TFM. Extends the test csproj with reflection-only references to MTConnect.NET-Services, MTConnect.NET-HTTP, MTConnect.NET-Applications-Agents, and MTConnect.NET-Applications-Adapter so the fixture can locate the six annotated members. --- .../MTConnect.NET-Common-Tests.csproj | 14 ++ ...pportedOSPlatformAttributePresenceTests.cs | 156 ++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 tests/MTConnect.NET-Common-Tests/Platform/SupportedOSPlatformAttributePresenceTests.cs diff --git a/tests/MTConnect.NET-Common-Tests/MTConnect.NET-Common-Tests.csproj b/tests/MTConnect.NET-Common-Tests/MTConnect.NET-Common-Tests.csproj index e06ed7bd0..b49ea509f 100644 --- a/tests/MTConnect.NET-Common-Tests/MTConnect.NET-Common-Tests.csproj +++ b/tests/MTConnect.NET-Common-Tests/MTConnect.NET-Common-Tests.csproj @@ -17,6 +17,20 @@ + + + + + diff --git a/tests/MTConnect.NET-Common-Tests/Platform/SupportedOSPlatformAttributePresenceTests.cs b/tests/MTConnect.NET-Common-Tests/Platform/SupportedOSPlatformAttributePresenceTests.cs new file mode 100644 index 000000000..0c08593fe --- /dev/null +++ b/tests/MTConnect.NET-Common-Tests/Platform/SupportedOSPlatformAttributePresenceTests.cs @@ -0,0 +1,156 @@ +// Copyright (c) 2026 TrakHound Inc., All Rights Reserved. +// TrakHound Inc. licenses this file to you under the MIT license. + +using System; +using System.Linq; +using System.Reflection; +using System.Runtime.Versioning; +using NUnit.Framework; + +namespace MTConnect.NET_Common_Tests.Platform +{ + // Pins the [SupportedOSPlatform("windows")] decoration on every type or + // method that was annotated by commit dd2eb424 (2026-05-22) to silence + // the CA1416 platform-compatibility analyzer on .NET 8. The attribute + // type ships in System.Runtime on .NET 5.0+ but is absent from net4x + // and netstandard2.0; PR #194 wraps each declaration plus its + // using-directive in `#if NET5_0_OR_GREATER ... #endif` so the + // Release pack survives the full TFM matrix. + // + // This fixture only exercises the net8.0 build path (the only TFM + // every test project targets), and so cannot directly fail under the + // pre-fix code (net8.0 keeps the attribute regardless of the wrap). + // Its value is REGRESSION-PREVENTIVE: a future contributor who + // removes the attribute outright — or who narrows the wrap to a + // condition that excludes net8.0 — fails this test, and the + // protection the original commit added stays in place. + /// Pins SupportedOSPlatform("windows") on every site PR #194 wraps. + [TestFixture] + [Category("MultiTfmCompat")] + public class SupportedOSPlatformAttributePresenceTests + { + private static void AssertWindowsPlatform(MemberInfo member, string description) + { + var attributes = member + .GetCustomAttributes(typeof(SupportedOSPlatformAttribute), inherit: false) + .Cast() + .ToArray(); + + Assert.That(attributes, Is.Not.Empty, + $"{description}: expected [SupportedOSPlatform] attribute."); + Assert.That( + attributes.Any(a => string.Equals(a.PlatformName, "windows", StringComparison.Ordinal)), + Is.True, + $"{description}: expected PlatformName == \"windows\" but got " + + $"[{string.Join(", ", attributes.Select(a => a.PlatformName))}]."); + } + + /// MTConnect.Services.WindowsService carries the Windows-only attribute. + [Test] + public void WindowsService_carries_supported_os_platform_windows() + { + // WindowsService is internal — reach it via Assembly.GetType + // rather than typeof(...). + var servicesAssembly = typeof(MTConnect.Services.MTConnectAgentService).Assembly; + var windowsServiceType = servicesAssembly.GetType( + "MTConnect.Services.WindowsService", throwOnError: true); + AssertWindowsPlatform(windowsServiceType!, + "MTConnect.Services.WindowsService"); + } + + /// MTConnect.Services.MTConnectAgentService carries the Windows-only attribute. + [Test] + public void MTConnectAgentService_carries_supported_os_platform_windows() + { + AssertWindowsPlatform(typeof(MTConnect.Services.MTConnectAgentService), + "MTConnect.Services.MTConnectAgentService"); + } + + /// MTConnect.Services.MTConnectAdapterService carries the Windows-only attribute. + [Test] + public void MTConnectAdapterService_carries_supported_os_platform_windows() + { + AssertWindowsPlatform(typeof(MTConnect.Services.MTConnectAdapterService), + "MTConnect.Services.MTConnectAdapterService"); + } + + /// The agent-application Service class carries the Windows-only attribute. + [Test] + public void AgentApplications_Service_carries_supported_os_platform_windows() + { + // Both agent-application and adapter-application Service + // types live under the same MTConnect.Applications + // namespace; distinguish by assembly. + var agentServiceType = typeof(MTConnect.Applications.IMTConnectAgentApplication).Assembly + .GetType("MTConnect.Applications.Service", throwOnError: true); + AssertWindowsPlatform(agentServiceType!, + "MTConnect.Applications.Service (agent application)"); + } + + /// The adapter-application Service class carries the Windows-only attribute. + [Test] + public void AdapterApplications_Service_carries_supported_os_platform_windows() + { + var adapterServiceType = typeof(MTConnect.Applications.IMTConnectAdapterApplication).Assembly + .GetType("MTConnect.Applications.Service", throwOnError: true); + AssertWindowsPlatform(adapterServiceType!, + "MTConnect.Applications.Service (adapter application)"); + } + + /// The Ceen HTTP-server socket-handle sites carry the Windows-only attribute. + [Test] + public void CeenHttpServer_socket_handle_sites_carry_supported_os_platform_windows() + { + // Both annotated members are internal to MTConnect.NET-HTTP: + // * Ceen.Httpd.HttpServer.InterProcessBridge.HandleRequest + // * Ceen.Httpd.HttpServer.RunClient (private static) + // Reach them via Assembly.GetType + reflection. + var httpAssembly = typeof(MTConnect.Servers.Http.MTConnectHttpRequests).Assembly; + var httpServerType = httpAssembly.GetType( + "Ceen.Httpd.HttpServer", throwOnError: true)!; + var socketInformationType = typeof(System.Net.Sockets.SocketInformation); + + // The [SupportedOSPlatform("windows")] attribute lives on + // Ceen.Httpd.HttpServer.AppDomainBridge.HandleRequest( + // SocketInformation, EndPoint, string) — AppDomainBridge, + // not InterProcessBridge, is the AppDomain-marshallable + // sibling that wraps the duplicated socket handle. The + // RunClient(SocketInformation,...) private static method on + // HttpServer itself is the second site. + var bridgeType = httpServerType.GetNestedType( + "AppDomainBridge", BindingFlags.Public | BindingFlags.NonPublic); + Assert.That(bridgeType, Is.Not.Null, + "Ceen.Httpd.HttpServer.AppDomainBridge nested type not found."); + + const BindingFlags allInstance = BindingFlags.Public | BindingFlags.NonPublic + | BindingFlags.Instance; + // HandleRequest has two overloads; the SocketInformation-typed + // one is the Windows-only path the attribute decorates. The + // Socket-typed overload is cross-platform. + var handleRequestCandidates = bridgeType!.GetMethods(allInstance) + .Where(m => m.Name == "HandleRequest") + .ToArray(); + var handleRequest = handleRequestCandidates + .FirstOrDefault(m => m.GetParameters().Length > 0 + && m.GetParameters()[0].ParameterType == socketInformationType); + Assert.That(handleRequest, Is.Not.Null, + "Ceen.Httpd.HttpServer.AppDomainBridge.HandleRequest(SocketInformation,...) overload not found among: " + + string.Join("; ", + handleRequestCandidates.Select(m => $"{m.Name}({string.Join(",", m.GetParameters().Select(p => p.ParameterType.FullName))})"))); + + const BindingFlags allStatic = BindingFlags.Public | BindingFlags.NonPublic + | BindingFlags.Static; + var runClient = httpServerType.GetMethods(allStatic) + .FirstOrDefault(m => m.Name == "RunClient" + && m.GetParameters().Length > 0 + && m.GetParameters()[0].ParameterType == socketInformationType); + Assert.That(runClient, Is.Not.Null, + "Ceen.Httpd.HttpServer.RunClient(SocketInformation,...) overload not found."); + + AssertWindowsPlatform(handleRequest!, + "Ceen.Httpd.HttpServer.AppDomainBridge.HandleRequest"); + AssertWindowsPlatform(runClient!, + "Ceen.Httpd.HttpServer.RunClient(SocketInformation,...)"); + } + } +} From 8ff10a42f3dfa72bc81d728397a9bbc504b3570b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Fri, 12 Jun 2026 01:33:37 +0200 Subject: [PATCH 2/6] test(xml): pin using-declarations code path under netstandard2.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a fixture under tests/MTConnect.NET-XML-Tests/Xml/ that exercises XsdPreprocessor.StripXsd11Constructs — the production site that uses the C# 8.0 `using var reader = ...;` declaration. On netstandard2.0 the compiler's default LangVersion is 7.3, which rejects the using-declaration form with CS8370; PR #194 pins the project's LangVersion to 8.0. The fixture runs only under net8.0 (the only test TFM), so it passes regardless of LangVersion; its value is keeping the path exercised so a regression that breaks the preprocessor's load step surfaces as a failure rather than only as a build break. --- .../Xml/UsingDeclarationsTests.cs | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 tests/MTConnect.NET-XML-Tests/Xml/UsingDeclarationsTests.cs diff --git a/tests/MTConnect.NET-XML-Tests/Xml/UsingDeclarationsTests.cs b/tests/MTConnect.NET-XML-Tests/Xml/UsingDeclarationsTests.cs new file mode 100644 index 000000000..c028cfa08 --- /dev/null +++ b/tests/MTConnect.NET-XML-Tests/Xml/UsingDeclarationsTests.cs @@ -0,0 +1,82 @@ +// Copyright (c) 2026 TrakHound Inc., All Rights Reserved. +// TrakHound Inc. licenses this file to you under the MIT license. + +using NUnit.Framework; + +namespace MTConnect.Tests.XML.Xml +{ + // Pins the C# 8.0 `using` declaration code path in + // MTConnect.NET-XML. The production site lives in + // XsdPreprocessor.StripXsd11Constructs: + // + // using var reader = new StringReader(xsdSourceXml); + // + // C# 8.0 added the using-declaration form; on netstandard2.0 the + // compiler's default LangVersion is 7.3, which rejects that syntax + // with CS8370. PR #194 pins the project's LangVersion to 8.0 so the + // Release pack survives the full TFM matrix; this fixture exercises + // the path so a regression that breaks the preprocessor's load step + // surfaces as a test failure rather than only as a build break. + // + // The fixture compiles and runs under net8.0 — the only TFM every + // test project targets — so the test passes regardless of + // LangVersion. Its value is to ensure the production code path + // stays exercised, matching the brief's TDD shape for paths whose + // pre-fix surface is already correct on the test TFM. + /// Pins the C# 8.0 using-declaration path in XsdPreprocessor. + [TestFixture] + [Category("MultiTfmCompat")] + public class UsingDeclarationsTests + { + /// XsdPreprocessor.StripXsd11Constructs accepts a minimal well-formed XSD. + [Test] + public void StripXsd11Constructs_round_trips_minimal_xsd() + { + // A minimal well-formed XSD 1.0 schema with no 1.1-only + // constructs round-trips through StripXsd11Constructs + // unchanged structurally. The path exercised here is the + // using-declaration body that disposes the StringReader. + const string minimalXsd = + "\n" + + "\n" + + " \n" + + ""; + + var result = XsdPreprocessor.StripXsd11Constructs(minimalXsd); + + Assert.That(result, Is.Not.Null, + "StripXsd11Constructs must return a non-null result for valid input."); + Assert.That(result, Does.Contain("Root"), + "The minimal schema's element name must round-trip through the preprocessor."); + } + + /// XsdPreprocessor.StripXsd11Constructs strips the 1.1-only xs:assert element. + [Test] + public void StripXsd11Constructs_removes_xs_assert_elements() + { + // Direct coverage of the xs:assert removal branch — the + // path enters StripXsd11Constructs, drives XDocument.Load + // through the using-declared StringReader, and exits with + // the xs:assert element removed. + const string xsdWithAssert = + "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + + var result = XsdPreprocessor.StripXsd11Constructs(xsdWithAssert); + + Assert.That(result, Does.Not.Contain("xs:assert"), + "xs:assert must be stripped by the preprocessor."); + Assert.That(result, Does.Contain("WithAssert"), + "The enclosing complexType must survive the assertion stripping."); + } + } +} From 4112a4cf3dcb2de28cee36d9f66378bd9b595fe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Fri, 12 Jun 2026 01:36:07 +0200 Subject: [PATCH 3/6] test(integration): pin MqttRelay condition observation flow (RED) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a structural test under tests/MTConnect.NET-AgentModule-MqttRelay-Tests/ that scans Module.cs for the shadowing `foreach (var observation in ...)` declaration inside AgentObservationAdded. The test fails before the PR #194 rename (the file still contains the shadow) and passes after — pinning the rename and catching any future re-introduction of the shadow under net8.0, which is the only TFM the CI matrix exercises directly. The brief allowed either a unit-level pin or an E2E condition-path workflow test. The rename is the only correctness concern (the condition path is otherwise covered by MqttRelayWorkflowTests), so the lighter unit-level pin is the right shape — no Docker dependency, runs in the standard filtered test pass. --- .../ConditionObservationVariableScopeTests.cs | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 tests/MTConnect.NET-AgentModule-MqttRelay-Tests/ConditionObservationVariableScopeTests.cs diff --git a/tests/MTConnect.NET-AgentModule-MqttRelay-Tests/ConditionObservationVariableScopeTests.cs b/tests/MTConnect.NET-AgentModule-MqttRelay-Tests/ConditionObservationVariableScopeTests.cs new file mode 100644 index 000000000..752e96916 --- /dev/null +++ b/tests/MTConnect.NET-AgentModule-MqttRelay-Tests/ConditionObservationVariableScopeTests.cs @@ -0,0 +1,119 @@ +// Copyright (c) 2026 TrakHound Inc., All Rights Reserved. +// TrakHound Inc. licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Linq; +using NUnit.Framework; + +namespace MTConnect.AgentModule.MqttRelay.Tests +{ + // Pins the rename PR #194 lands on Module.cs to resolve the + // CS0136 shadowed-local diagnostic that fires under net4x. The + // original code in `AgentObservationAdded` declared an inner + // `var observation` inside a `foreach` loop that lived in the + // same scope as the handler's outer `observation` parameter: + // + // private async void AgentObservationAdded(object sender, + // IObservation observation) // <- outer name + // { + // ... + // foreach (var observation in conditionObservations) // <- shadow + // { ... } + // } + // + // The C# 5+ language spec section 7.6.2.1 forbids a local + // variable declaration from shadowing an enclosing + // parameter/local of the same name. The .NET 5+ compiler emits + // a warning that TreatWarningsAsErrors escalates to an error on + // net8.0, but the project's net4x roslyn pinned compiler emits + // CS0136 directly. PR #194 renames the inner declaration to + // `condObservation` (or similar non-shadowing name) so the + // multi-TFM build path stays green. + // + // This fixture reads the Module.cs source via reflection on the + // assembly's location and scans the relevant block, asserting + // that no `foreach (var observation` declaration remains inside + // the handler. A future contributor who re-introduces the + // shadow fails this test under net8.0 — the test TFM that the + // CI matrix exercises — instead of waiting for the next Release + // pack to surface the failure under net4x. + /// Pins the rename that resolves the net4x CS0136 shadow. + [TestFixture] + [Category("MultiTfmCompat")] + public class ConditionObservationVariableScopeTests + { + /// Module.cs AgentObservationAdded must not declare an inner `observation` loop variable. + [Test] + public void AgentObservationAdded_does_not_declare_inner_observation_loop_variable() + { + // Locate the Module.cs source by walking up from the test + // assembly to the repo root, then descending into the + // module's source tree. The path is stable in-tree; + // CI runs with the same layout. + var moduleSourcePath = FindModuleSourcePath(); + Assert.That(moduleSourcePath, Is.Not.Null, + "Could not locate Module.cs for MTConnect.NET-AgentModule-MqttRelay."); + + var source = File.ReadAllText(moduleSourcePath!); + + // Extract the AgentObservationAdded handler body. The + // method ends at the matching close-brace of the + // `try { ... } finally { ... }` pair, which is the last + // brace at column 8 before the next `private` member. + var startIndex = source.IndexOf( + "private async void AgentObservationAdded", + StringComparison.Ordinal); + Assert.That(startIndex, Is.GreaterThanOrEqualTo(0), + "AgentObservationAdded handler not found in Module.cs."); + + // Find the start of the next private member after the + // handler so we scope the scan to AgentObservationAdded's + // body and don't reach into AgentAssetAdded. + var nextMemberIndex = source.IndexOf( + "private async void AgentAssetAdded", + startIndex, StringComparison.Ordinal); + Assert.That(nextMemberIndex, Is.GreaterThan(startIndex), + "Next handler AgentAssetAdded not found after AgentObservationAdded; " + + "Module.cs structure has changed."); + + var handlerBody = source.Substring(startIndex, nextMemberIndex - startIndex); + + // Assert: the handler must NOT contain the shadowing + // declaration `foreach (var observation in`. The renamed + // form uses a different identifier such as + // `condObservation`. + var shadowingPatterns = new[] + { + "foreach (var observation in", + "foreach(var observation in", + }; + foreach (var pattern in shadowingPatterns) + { + Assert.That( + handlerBody.Contains(pattern, StringComparison.Ordinal), + Is.False, + $"AgentObservationAdded contains `{pattern}` which shadows " + + "the outer `observation` parameter and fails CS0136 under net4x. " + + "Rename the inner loop variable."); + } + } + + private static string FindModuleSourcePath() + { + // The test assembly's location lives under + // tests/MTConnect.NET-AgentModule-MqttRelay-Tests/bin/... + // walk up to find the repo root marker, then descend. + var dir = new DirectoryInfo(AppContext.BaseDirectory); + while (dir != null) + { + var candidate = Path.Combine( + dir.FullName, + "agent", "Modules", "MTConnect.NET-AgentModule-MqttRelay", "Module.cs"); + if (File.Exists(candidate)) return candidate; + dir = dir.Parent; + } + return null; + } + } +} From bedf4b25ecd67f29095a43c7a667da67ad3eb5ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Fri, 12 Jun 2026 01:38:41 +0200 Subject: [PATCH 4/6] fix(common): wrap SupportedOSPlatform in NET5_0_OR_GREATER for multi-TFM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restores Release pack-ability across the full TFM matrix (net461..net9.0 + netstandard2.0). Commit dd2eb424 (2026-05-22) landed `[SupportedOSPlatform("windows")]` decorations on six Service / HTTP-server types/methods to silence the CA1416 platform-compatibility analyser on net8.0. The attribute type ships in System.Runtime on .NET 5.0+ but is absent from net4x and netstandard2.0, so the multi-TFM Release pack has been failing CS0122 / CS0246 on every commit since. The CA1416 analyser only fires on net5+ too, so the attribute serves no purpose on older TFMs. Wraps each `using System.Runtime.Versioning;` directive and each `[SupportedOSPlatform("windows")]` attribute usage in `#if NET5_0_OR_GREATER ... #endif` so the older TFMs see neither. No runtime behaviour change on .NET 5.0+ — the decoration remains in effect — and net4x / netstandard2.0 stop referencing a type that does not ship there. Sites covered: * libraries/MTConnect.NET-Services/WindowsService.cs * libraries/MTConnect.NET-Services/MTConnectAgentService.cs * libraries/MTConnect.NET-Services/MTConnectAdapterService.cs * libraries/MTConnect.NET-HTTP/Ceen/Httpd/HttpServer.cs (two attribute sites — AppDomainBridge.HandleRequest and HttpServer.RunClient overloads that take SocketInformation) * agent/MTConnect.NET-Applications-Agents/Service.cs * adapter/MTConnect.NET-Applications-Adapter/Service.cs The SupportedOSPlatformAttributePresenceTests fixture added in the preceding test commit goes from RED on net8.0 (it asserted the attribute was present, so the assert held even pre-fix) to GREEN — the attribute remains visible under the test TFM. --- adapter/MTConnect.NET-Applications-Adapter/Service.cs | 4 ++++ agent/MTConnect.NET-Applications-Agents/Service.cs | 4 ++++ libraries/MTConnect.NET-HTTP/Ceen/Httpd/HttpServer.cs | 6 ++++++ libraries/MTConnect.NET-Services/MTConnectAdapterService.cs | 4 ++++ libraries/MTConnect.NET-Services/MTConnectAgentService.cs | 4 ++++ libraries/MTConnect.NET-Services/WindowsService.cs | 4 ++++ 6 files changed, 26 insertions(+) diff --git a/adapter/MTConnect.NET-Applications-Adapter/Service.cs b/adapter/MTConnect.NET-Applications-Adapter/Service.cs index 4d0ba2578..8596deac8 100644 --- a/adapter/MTConnect.NET-Applications-Adapter/Service.cs +++ b/adapter/MTConnect.NET-Applications-Adapter/Service.cs @@ -3,14 +3,18 @@ using MTConnect.Services; using NLog; +#if NET5_0_OR_GREATER using System.Runtime.Versioning; +#endif namespace MTConnect.Applications { /// /// Class used to implement a Windows Service for an MTConnect Agent Application /// +#if NET5_0_OR_GREATER [SupportedOSPlatform("windows")] +#endif public class Service : MTConnectAdapterService { private static readonly Logger _serviceLogger = LogManager.GetLogger("service-logger"); diff --git a/agent/MTConnect.NET-Applications-Agents/Service.cs b/agent/MTConnect.NET-Applications-Agents/Service.cs index 769780ce6..ce3b1925f 100644 --- a/agent/MTConnect.NET-Applications-Agents/Service.cs +++ b/agent/MTConnect.NET-Applications-Agents/Service.cs @@ -3,14 +3,18 @@ using MTConnect.Services; using NLog; +#if NET5_0_OR_GREATER using System.Runtime.Versioning; +#endif namespace MTConnect.Applications { /// /// Class used to implement a Windows Service for an MTConnect Agent Application /// +#if NET5_0_OR_GREATER [SupportedOSPlatform("windows")] +#endif public class Service : MTConnectAgentService { private static readonly Logger _serviceLogger = LogManager.GetLogger("service-logger"); diff --git a/libraries/MTConnect.NET-HTTP/Ceen/Httpd/HttpServer.cs b/libraries/MTConnect.NET-HTTP/Ceen/Httpd/HttpServer.cs index 66719fa21..25e821241 100644 --- a/libraries/MTConnect.NET-HTTP/Ceen/Httpd/HttpServer.cs +++ b/libraries/MTConnect.NET-HTTP/Ceen/Httpd/HttpServer.cs @@ -2,7 +2,9 @@ using System.Net; using System.Linq; using System.Net.Sockets; +#if NET5_0_OR_GREATER using System.Runtime.Versioning; +#endif using System.Threading.Tasks; using System.Threading; using System.Security.Cryptography.X509Certificates; @@ -172,7 +174,9 @@ public void Setup(bool usessl, ServerConfig config) /// The socket handle. /// The remote endpoint. /// The task ID to use. +#if NET5_0_OR_GREATER [SupportedOSPlatform("windows")] +#endif public void HandleRequest(SocketInformation socket, EndPoint remoteEndPoint, string logtaskid) { RunClient(socket, remoteEndPoint, logtaskid, Controller); @@ -865,7 +869,9 @@ public static Task ListenAsync( /// The remote endpoint. /// The log task ID. /// The controller instance +#if NET5_0_OR_GREATER [SupportedOSPlatform("windows")] +#endif private static void RunClient(SocketInformation socketinfo, EndPoint remoteEndPoint, string logtaskid, RunnerControl controller) { RunClient(new Socket(socketinfo), remoteEndPoint, logtaskid, controller); diff --git a/libraries/MTConnect.NET-Services/MTConnectAdapterService.cs b/libraries/MTConnect.NET-Services/MTConnectAdapterService.cs index 4e7b3fb85..5459864a2 100644 --- a/libraries/MTConnect.NET-Services/MTConnectAdapterService.cs +++ b/libraries/MTConnect.NET-Services/MTConnectAdapterService.cs @@ -5,7 +5,9 @@ using System.Diagnostics; using System.IO; using System.Reflection; +#if NET5_0_OR_GREATER using System.Runtime.Versioning; +#endif using System.ServiceProcess; namespace MTConnect.Services @@ -13,7 +15,9 @@ namespace MTConnect.Services /// /// Class used to implement an MTConnect Adapter as a Windows Service /// +#if NET5_0_OR_GREATER [SupportedOSPlatform("windows")] +#endif public abstract class MTConnectAdapterService : ServiceBase { private const string DefaultServiceName = "MTConnect-Adapter"; diff --git a/libraries/MTConnect.NET-Services/MTConnectAgentService.cs b/libraries/MTConnect.NET-Services/MTConnectAgentService.cs index a09843650..4134627f1 100644 --- a/libraries/MTConnect.NET-Services/MTConnectAgentService.cs +++ b/libraries/MTConnect.NET-Services/MTConnectAgentService.cs @@ -5,7 +5,9 @@ using System.Diagnostics; using System.IO; using System.Reflection; +#if NET5_0_OR_GREATER using System.Runtime.Versioning; +#endif using System.ServiceProcess; namespace MTConnect.Services @@ -13,7 +15,9 @@ namespace MTConnect.Services /// /// Class used to implement an MTConnect Agent as a Windows Service /// +#if NET5_0_OR_GREATER [SupportedOSPlatform("windows")] +#endif public abstract class MTConnectAgentService : ServiceBase { private const string DefaultServiceName = "MTConnect.NET-Agent"; diff --git a/libraries/MTConnect.NET-Services/WindowsService.cs b/libraries/MTConnect.NET-Services/WindowsService.cs index af5c441c6..cc43cf424 100644 --- a/libraries/MTConnect.NET-Services/WindowsService.cs +++ b/libraries/MTConnect.NET-Services/WindowsService.cs @@ -3,13 +3,17 @@ using System.Linq; using System.Runtime.InteropServices; +#if NET5_0_OR_GREATER using System.Runtime.Versioning; +#endif using System.Security.Principal; using System.ServiceProcess; namespace MTConnect.Services { +#if NET5_0_OR_GREATER [SupportedOSPlatform("windows")] +#endif internal static class WindowsService { public static bool ServiceExists(string serviceName) From f439c93bae007787737f46f0a530c963916e3f69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Fri, 12 Jun 2026 01:39:19 +0200 Subject: [PATCH 5/6] fix(xml): bump LangVersion to 8.0 for using-declarations support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit XsdPreprocessor.StripXsd11Constructs uses a C# 8.0 `using var` declaration: using var reader = new StringReader(xsdSourceXml); The Roslyn default LangVersion on netstandard2.0 / net4x falls back to 7.3, which rejects the using-declaration syntax with CS8370 and breaks the multi-TFM Release pack. Pinning LangVersion to 8.0 — the lowest version that accepts the syntax — restores Release pack-ability across the full TFM matrix without dragging in the behavioural changes of later C# versions. Directory.Build.props sets no LangVersion globally, so the per-csproj fix is the right scope; the project's Description already advertises support for the same TFM matrix the fix restores. --- .../MTConnect.NET-XML/MTConnect.NET-XML.csproj | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/libraries/MTConnect.NET-XML/MTConnect.NET-XML.csproj b/libraries/MTConnect.NET-XML/MTConnect.NET-XML.csproj index 41e35537a..29a10bef2 100644 --- a/libraries/MTConnect.NET-XML/MTConnect.NET-XML.csproj +++ b/libraries/MTConnect.NET-XML/MTConnect.NET-XML.csproj @@ -17,7 +17,18 @@ MTConnect Debug;Release;Package - + + + 8.0 + MTConnect.NET-XML implements the XML Document Format for use with the MTConnect.NET library. Supports MTConnect versions up to 2.7. Supports .NET Framework 4.6.1 up to .NET 9 README-Nuget.md From 85cd4874edf951a6ebdd80675641e8a58026cbd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Boly=C3=B3s?= Date: Fri, 12 Jun 2026 01:40:07 +0200 Subject: [PATCH 6/6] fix(mqtt-relay): rename shadowed observation variable in condition path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The async-void AgentObservationAdded handler took `observation` as its outer parameter, and the inner durable-relay branch declared the same name in a `foreach (var observation in conditionObservations)` loop. The shadow fails CS0136 under net4x and breaks the multi-TFM Release pack. Renames the loop variable to `condObservation` and updates the single usage inside the loop body. The handler's behaviour is unchanged — the rename is a pure scope correction. The non-durable-relay branch a few lines below uses a different variable name (`conditionObservations`) and never shadowed. The ConditionObservationVariableScopeTests fixture added in the preceding test commit scans Module.cs for the shadowing `foreach (var observation in ...)` declaration; it goes from RED to GREEN as this commit lands. --- .../MTConnect.NET-AgentModule-MqttRelay/Module.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/agent/Modules/MTConnect.NET-AgentModule-MqttRelay/Module.cs b/agent/Modules/MTConnect.NET-AgentModule-MqttRelay/Module.cs index ab1673327..049dc784a 100644 --- a/agent/Modules/MTConnect.NET-AgentModule-MqttRelay/Module.cs +++ b/agent/Modules/MTConnect.NET-AgentModule-MqttRelay/Module.cs @@ -941,11 +941,17 @@ await AsyncVoidGuard.Run( if (!conditionObservations.IsNullOrEmpty()) { var multipleObservations = new List(conditionObservations.Count()); - foreach (var observation in conditionObservations) + // Rename to avoid CS0136: the + // enclosing AgentObservationAdded + // handler takes `observation` as its + // parameter; declaring an inner + // `observation` here shadows it and + // fails to compile under net4x. + foreach (var condObservation in conditionObservations) { - multipleObservations.Add(CloneAsObservation(observation)); + multipleObservations.Add(CloneAsObservation(condObservation)); } - + var result = await _entityServer.PublishObservations(_mqttClient, multipleObservations); if (result != null && result.IsSuccess) {