Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
127be28
test(common): pin SupportedOSPlatform attribute on Service types (RED)
ottobolyos Jun 11, 2026
8ff10a4
test(xml): pin using-declarations code path under netstandard2.0
ottobolyos Jun 11, 2026
4112a4c
test(integration): pin MqttRelay condition observation flow (RED)
ottobolyos Jun 11, 2026
bedf4b2
fix(common): wrap SupportedOSPlatform in NET5_0_OR_GREATER for multi-TFM
ottobolyos Jun 11, 2026
f439c93
fix(xml): bump LangVersion to 8.0 for using-declarations support
ottobolyos Jun 11, 2026
85cd487
fix(mqtt-relay): rename shadowed observation variable in condition path
ottobolyos Jun 11, 2026
cb5cb00
test(tls): pin SYSLIB0057 X509CertificateLoader migration (RED)
ottobolyos Jun 11, 2026
404b9b7
fix(tls): migrate cert constructors to X509CertificateLoader on net9
ottobolyos Jun 11, 2026
e4c8f2c
fix(mqtt): remove unreachable code after net4x PEM unconditional throw
ottobolyos Jun 11, 2026
5a93e57
fix(http): condition Obsolete on net5+ to silence CS0809 in Ceen bridge
ottobolyos Jun 11, 2026
751a33f
chore(packaging): disable NU5017-tripping empty snupkg generation und…
ottobolyos Jun 11, 2026
91854e0
test(http): pin CA2022 short-read handling for body drain + post body…
ottobolyos Jun 12, 2026
73b7965
fix(http): accumulate ReadAsync results to satisfy CA2022 short-read …
ottobolyos Jun 12, 2026
230ca4d
fix(modules-mqtt): remove unreachable code after net4x PEM throw in m…
ottobolyos Jun 12, 2026
df004dc
fix(mqtt,http): migrate remaining cert constructors to X509Certificat…
ottobolyos Jun 12, 2026
0a74d91
docs(applications): fully qualify NLog.LogLevel cref to silence CS1574
ottobolyos Jun 12, 2026
efebcc6
ci(workflow): add release-pack matrix gate to prevent multi-TFM regre…
ottobolyos Jun 11, 2026
9f86aba
docs(tests): add XML doc summaries on CA2022 + TLS loader fixtures
ottobolyos Jun 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -452,3 +452,66 @@ jobs:
TestResults/**/coverage.cobertura.xml
if-no-files-found: warn
retention-days: 14

# ------------------------------------------------------------------
# Job 4 — Release-pack matrix gate. The build-and-test job above only
# exercises Debug, which compiles each csproj against a single TFM
# (net8.0 — see the per-project `<PropertyGroup Condition="…Debug">`
# blocks). Release / Package configurations multi-target net4x →
# net9.0; some diagnostics only fire on the legacy TFMs (CS0162 from
# `#if NET5_0_OR_GREATER` branches that go unreachable on net4x), and
# some only on net9.0 (SYSLIB0057 X509Certificate2 ctor obsoletion).
# The May-22 regression slipped through CI precisely because Debug
# never built net4x; this job runs `dotnet pack -c Release` across
# the full TFM matrix to catch the next regression class at PR time.
#
# Single-process (no shard), no docs dependency, same draft-skip
# gate as build-and-test. The pack output is written to a workflow-
# local `dist/` directory and discarded — the produced .nupkg files
# are not uploaded as artefacts; the gate's only output is the exit
# code.
# ------------------------------------------------------------------
release-pack:
name: release-pack
if: github.event_name == 'push' || github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Setup .NET 8.0 + 9.0
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
with:
dotnet-version: |
8.0.x
9.0.x

- name: Restore solution
run: dotnet restore MTConnect.NET.sln

- name: Pack -c Release across multi-TFM
run: |
rm -rf dist
mkdir -p dist
dotnet pack MTConnect.NET.sln -c Release \
-p:VersionSuffix=ci-check \
-p:ContinuousIntegrationBuild=true \
-o dist 2>&1 | tee pack.log
shell: bash

# Surface a compact summary of every error / warning that
# tripped the pack. The `dotnet pack` step above pipes into
# `tee pack.log`; the unmasked exit propagates via the
# pipeline's PIPESTATUS through `set -o pipefail`, which is
# the GitHub-default for bash steps. This step only runs on
# failure, by design — when the pack is green, the produced
# nupkgs are not inspected further.
- name: Surface pack errors (if any)
if: failure()
run: |
echo "### Release-pack diagnostics" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
grep -E '(error|warning) (CS|CA|NU|SYSLIB|MSB)' pack.log \
| sort -u | head -100 >> "$GITHUB_STEP_SUMMARY" || true
echo '```' >> "$GITHUB_STEP_SUMMARY"
shell: bash
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public class MTConnectAdapterApplication : IMTConnectAdapterApplication
/// the startup log.</summary>
protected bool _verboseLogging = true;
/// <summary>NLog log-level applied to every internal logger.
/// Defaults to <see cref="LogLevel.Debug"/>; the <c>debug</c>
/// Defaults to <see cref="NLog.LogLevel.Debug"/>; the <c>debug</c>
/// CLI command overrides it.</summary>
protected LogLevel _logLevel = LogLevel.Debug;
/// <summary>File-system watcher that reloads the adapter
Expand Down
4 changes: 4 additions & 0 deletions adapter/MTConnect.NET-Applications-Adapter/Service.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@

using MTConnect.Services;
using NLog;
#if NET5_0_OR_GREATER
using System.Runtime.Versioning;
#endif

namespace MTConnect.Applications
{
/// <summary>
/// Class used to implement a Windows Service for an MTConnect Agent Application
/// </summary>
#if NET5_0_OR_GREATER
[SupportedOSPlatform("windows")]
#endif
public class Service : MTConnectAdapterService
{
private static readonly Logger _serviceLogger = LogManager.GetLogger("service-logger");
Expand Down
13 changes: 11 additions & 2 deletions adapter/Modules/MTConnect.NET-AdapterModule-MQTT/Module.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,16 +109,22 @@ private async Task Worker()
// Add CA (Certificate Authority)
if (!string.IsNullOrEmpty(_configuration.CertificateAuthority))
{
#if NET9_0_OR_GREATER
certificates.Add(X509CertificateLoader.LoadCertificateFromFile(GetFilePath(_configuration.CertificateAuthority)));
#else
certificates.Add(new X509Certificate2(GetFilePath(_configuration.CertificateAuthority)));
#endif
}

// Add Client Certificate & Private Key
if (!string.IsNullOrEmpty(_configuration.PemCertificate) && !string.IsNullOrEmpty(_configuration.PemPrivateKey))
{
#if NET5_0_OR_GREATER
certificates.Add(new X509Certificate2(X509Certificate2.CreateFromPemFile(GetFilePath(_configuration.PemCertificate), GetFilePath(_configuration.PemPrivateKey)).Export(X509ContentType.Pfx)));
var pfxBytes = X509Certificate2.CreateFromPemFile(GetFilePath(_configuration.PemCertificate), GetFilePath(_configuration.PemPrivateKey)).Export(X509ContentType.Pfx);
#if NET9_0_OR_GREATER
certificates.Add(X509CertificateLoader.LoadPkcs12(pfxBytes, null));
#else
throw new Exception("PEM Certificates Not Supported in .NET Framework 4.8 or older");
certificates.Add(new X509Certificate2(pfxBytes));
#endif

clientOptionsBuilder.WithTlsOptions(b => b
Expand All @@ -127,6 +133,9 @@ private async Task Worker()
.WithIgnoreCertificateChainErrors(_configuration.AllowUntrustedCertificates)
.WithAllowUntrustedCertificates(_configuration.AllowUntrustedCertificates)
.WithClientCertificates(certificates));
#else
throw new Exception("PEM Certificates Not Supported in .NET Framework 4.8 or older");
#endif
}

// Add Credentials
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public class MTConnectAgentApplication : IMTConnectAgentApplication
private readonly object _lock = new object();

/// <summary>NLog log-level applied to every internal logger.
/// Defaults to <see cref="LogLevel.Debug"/>; the <c>debug</c>
/// Defaults to <see cref="NLog.LogLevel.Debug"/>; the <c>debug</c>
/// and <c>trace</c> CLI commands override it.</summary>
protected LogLevel _logLevel = LogLevel.Debug;
private MTConnectAgentBroker _mtconnectAgent;
Expand Down
4 changes: 4 additions & 0 deletions agent/MTConnect.NET-Applications-Agents/Service.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@

using MTConnect.Services;
using NLog;
#if NET5_0_OR_GREATER
using System.Runtime.Versioning;
#endif

namespace MTConnect.Applications
{
/// <summary>
/// Class used to implement a Windows Service for an MTConnect Agent Application
/// </summary>
#if NET5_0_OR_GREATER
[SupportedOSPlatform("windows")]
#endif
public class Service : MTConnectAgentService
{
private static readonly Logger _serviceLogger = LogManager.GetLogger("service-logger");
Expand Down
13 changes: 11 additions & 2 deletions agent/Modules/MTConnect.NET-AgentModule-MqttAdapter/Module.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,16 +132,22 @@ private async Task Worker()
// Add CA (Certificate Authority)
if (!string.IsNullOrEmpty(_configuration.CertificateAuthority))
{
#if NET9_0_OR_GREATER
certificates.Add(X509CertificateLoader.LoadCertificateFromFile(GetFilePath(_configuration.CertificateAuthority)));
#else
certificates.Add(new X509Certificate2(GetFilePath(_configuration.CertificateAuthority)));
#endif
}

// Add Client Certificate & Private Key
if (!string.IsNullOrEmpty(_configuration.PemCertificate) && !string.IsNullOrEmpty(_configuration.PemPrivateKey))
{
#if NET5_0_OR_GREATER
certificates.Add(new X509Certificate2(X509Certificate2.CreateFromPemFile(GetFilePath(_configuration.PemCertificate), GetFilePath(_configuration.PemPrivateKey)).Export(X509ContentType.Pfx)));
var pfxBytes = X509Certificate2.CreateFromPemFile(GetFilePath(_configuration.PemCertificate), GetFilePath(_configuration.PemPrivateKey)).Export(X509ContentType.Pfx);
#if NET9_0_OR_GREATER
certificates.Add(X509CertificateLoader.LoadPkcs12(pfxBytes, null));
#else
throw new Exception("PEM Certificates Not Supported in .NET Framework 4.8 or older");
certificates.Add(new X509Certificate2(pfxBytes));
#endif

clientOptionsBuilder.WithTlsOptions(b => b
Expand All @@ -150,6 +156,9 @@ private async Task Worker()
.WithIgnoreCertificateChainErrors(_configuration.AllowUntrustedCertificates)
.WithAllowUntrustedCertificates(_configuration.AllowUntrustedCertificates)
.WithClientCertificates(certificates));
#else
throw new Exception("PEM Certificates Not Supported in .NET Framework 4.8 or older");
#endif
}

// Add Credentials
Expand Down
12 changes: 9 additions & 3 deletions agent/Modules/MTConnect.NET-AgentModule-MqttRelay/Module.cs
Original file line number Diff line number Diff line change
Expand Up @@ -941,11 +941,17 @@ await AsyncVoidGuard.Run(
if (!conditionObservations.IsNullOrEmpty())
{
var multipleObservations = new List<IObservation>(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)
{
Expand Down
18 changes: 18 additions & 0 deletions libraries/MTConnect.NET-HTTP/Ceen/Httpd/HttpServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -172,7 +174,9 @@ public void Setup(bool usessl, ServerConfig config)
/// <param name="socket">The socket handle.</param>
/// <param name="remoteEndPoint">The remote endpoint.</param>
/// <param name="logtaskid">The task ID to use.</param>
#if NET5_0_OR_GREATER
[SupportedOSPlatform("windows")]
#endif
public void HandleRequest(SocketInformation socket, EndPoint remoteEndPoint, string logtaskid)
{
RunClient(socket, remoteEndPoint, logtaskid, Controller);
Expand Down Expand Up @@ -205,7 +209,19 @@ public bool WaitForStop(TimeSpan waitdelay)
/// Initializes the lifetime service.
/// </summary>
/// <returns>The lifetime service.</returns>
/// <remarks>
/// The base member <see cref="MarshalByRefObject.InitializeLifetimeService"/> is
/// only marked obsolete on .NET 5 and newer (CoreCLR removed the .NET Remoting
/// lifetime-service infrastructure there). On .NET Framework targets the base is
/// not obsolete, so applying <see cref="ObsoleteAttribute"/> on the override would
/// trigger CS0809 (obsolete override of non-obsolete member). The attribute is
/// therefore only conditionally compiled in for net5+ — silencing the otherwise
/// CS0672 (non-obsolete override of obsolete member) without forbidding the
/// override on net4x where remoting is still live.
/// </remarks>
#if NET5_0_OR_GREATER
[Obsolete("InitializeLifetimeService is obsolete in .NET 5+; the override exists for legacy AppDomain remoting compatibility.")]
#endif
public override object InitializeLifetimeService()
{
return null;
Expand Down Expand Up @@ -865,7 +881,9 @@ public static Task ListenAsync(
/// <param name="remoteEndPoint">The remote endpoint.</param>
/// <param name="logtaskid">The log task ID.</param>
/// <param name="controller">The controller instance</param>
#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);
Expand Down
14 changes: 13 additions & 1 deletion libraries/MTConnect.NET-HTTP/Ceen/Httpd/LimitedBodyStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,19 @@ public async Task<bool> DiscardAllAsync(System.Threading.CancellationToken cance

var buf = new byte[1024 * 8];
while (m_bytesleft > 0)
await ReadAsync(buf, 0, buf.Length, cancellationToken);
{
// ReadAsync may return fewer bytes than requested (short read)
// and 0 on EOF. CA2022 fires when the return value is ignored
// because the caller can deadlock on a stream that hits EOF
// before m_bytesleft reaches zero — the underlying transport
// may close mid-body. Treat a 0-byte read as the failure path
// and break out; the caller decides whether the missing body
// bytes are recoverable. A non-zero short read is fine — the
// loop's m_bytesleft gate is decremented by ReadAsync itself.
var read = await ReadAsync(buf, 0, buf.Length, cancellationToken);
if (read == 0)
return false;
}

return true;
}
Expand Down
4 changes: 4 additions & 0 deletions libraries/MTConnect.NET-HTTP/Ceen/Httpd/ServerConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,11 @@ public ServerConfig()
/// <param name="password">The certificate password.</param>
public void LoadCertificate(string path, string password)
{
#if NET9_0_OR_GREATER
this.SSLCertificate = X509CertificateLoader.LoadPkcs12FromFile(path, password ?? "");
#else
this.SSLCertificate = new X509Certificate2(path, password ?? "");
#endif
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,37 @@ private static async Task<byte[]> ReadRequestBytes(Stream inputStream)
{
var bufferSize = 1048576 * 2; // 2 MB
var bytes = new byte[bufferSize];
await inputStream.ReadAsync(bytes, 0, bytes.Length);

return TrimEnd(bytes);
// Stream.ReadAsync(buffer, offset, count) is allowed to
// return fewer bytes than requested — anywhere from 1 up
// to count — and 0 indicates EOF. CA2022 fires when the
// return value is ignored because the request body may
// arrive in multiple TCP segments, in which case a single
// ReadAsync call returns only the first segment's bytes.
// Loop until EOF or the buffer is full, accumulating into
// the same `bytes` buffer and tracking the total filled
// length.
var totalRead = 0;
while (totalRead < bytes.Length)
{
var read = await inputStream.ReadAsync(bytes, totalRead, bytes.Length - totalRead);
if (read == 0)
break;
totalRead += read;
}

if (totalRead == 0)
return null;
if (totalRead == bytes.Length)
return bytes;

// Truncate the buffer to the actual filled length. This
// replaces the previous TrimEnd-on-zero-byte heuristic,
// which could over-truncate when the body legitimately
// ended with a 0x00 byte (e.g. a binary payload).
var trimmed = new byte[totalRead];
Array.Copy(bytes, 0, trimmed, 0, totalRead);
return trimmed;
}
catch { }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,17 +290,22 @@ private async Task Worker()
// Add CA (Certificate Authority)
if (!string.IsNullOrEmpty(_caCertPath))
{
#if NET9_0_OR_GREATER
certificates.Add(X509CertificateLoader.LoadCertificateFromFile(GetFilePath(_caCertPath)));
#else
certificates.Add(new X509Certificate2(GetFilePath(_caCertPath)));
#endif
}

// Add Client Certificate & Private Key
if (!string.IsNullOrEmpty(_pemClientCertPath) && !string.IsNullOrEmpty(_pemPrivateKeyPath))
{

#if NET5_0_OR_GREATER
certificates.Add(new X509Certificate2(X509Certificate2.CreateFromPemFile(GetFilePath(_pemClientCertPath), GetFilePath(_pemPrivateKeyPath)).Export(X509ContentType.Pfx)));
var pfxBytes = X509Certificate2.CreateFromPemFile(GetFilePath(_pemClientCertPath), GetFilePath(_pemPrivateKeyPath)).Export(X509ContentType.Pfx);
#if NET9_0_OR_GREATER
certificates.Add(X509CertificateLoader.LoadPkcs12(pfxBytes, null));
#else
throw new Exception("PEM Certificates Not Supported in .NET Framework 4.8 or older");
certificates.Add(new X509Certificate2(pfxBytes));
#endif

clientOptionsBuilder.WithTlsOptions(b => b
Expand All @@ -309,6 +314,9 @@ private async Task Worker()
.WithIgnoreCertificateChainErrors(_allowUntrustedCertificates)
.WithAllowUntrustedCertificates(_allowUntrustedCertificates)
.WithClientCertificates(certificates));
#else
throw new Exception("PEM Certificates Not Supported in .NET Framework 4.8 or older");
#endif
}

// Add Credentials
Expand Down
Loading
Loading