Skip to content

Commit

Permalink
Remove .NET Framework 4.8 support
Browse files Browse the repository at this point in the history
  • Loading branch information
menees committed Nov 12, 2024
1 parent 15a1462 commit ae9d164
Show file tree
Hide file tree
Showing 16 changed files with 31 additions and 157 deletions.
8 changes: 1 addition & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# Remoting
This repo provides a simple [RMI](https://en.wikipedia.org/wiki/Remote_method_invocation) and [IPC](https://en.wikipedia.org/wiki/Inter-process_communication)
library for .NET Framework 4.8 and .NET 6.0+ applications. It's designed to help ease the migration from legacy [WCF](https://en.wikipedia.org/wiki/Windows_Communication_Foundation)
library for .NET 8.0+ applications. It's designed to help ease the migration from legacy [WCF](https://en.wikipedia.org/wiki/Windows_Communication_Foundation)
and [.NET Remoting](https://en.wikipedia.org/wiki/.NET_Remoting) when porting code from .NET Framework to modern .NET. However, this library doesn't try to do
[all the things](https://knowyourmeme.com/memes/all-the-things) like WCF and .NET Remoting.

Expand Down Expand Up @@ -91,9 +91,3 @@ public async Task Base64ExampleAsync()

For more usage examples see:
* [MessageNodeTests](tests/Menees.Remoting.Tests/MessageNodeTests.cs)

## No .NET Standard 2.0 Support
This library doesn't target .NET Standard 2.0 because that standard doesn't support:
* [DispatchProxy.Create](https://docs.microsoft.com/en-us/dotnet/api/system.reflection.dispatchproxy.create)
due to lack of [Reflection.Emit](https://docs.microsoft.com/en-us/dotnet/api/system.reflection.emit) support in .NET Standard 2.0.
* Named pipe security (see [#26869](https://github.com/dotnet/runtime/issues/26869) and [StackOverflow](https://stackoverflow.com/a/54896975/1882616)).
6 changes: 2 additions & 4 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,10 @@
<Copyright>Copyright © 2022-$(CurrentYear) Bill Menees</Copyright>
<Copyright Condition="$(CurrentYear) == '2022'">Copyright © 2022 Bill Menees</Copyright>

<!-- Note: We can't target .NET Standard 2.0 because DispatchProxy.Create isn't supported there (due to lack of Reflection.Emit). -->
<!-- https://docs.microsoft.com/en-us/dotnet/standard/frameworks -->
<MeneesTargetNetFramework></MeneesTargetNetFramework>
<!-- DOTNET_VERSION: Update .NET target version. -->
<!-- DOTNET_VERSION: Update .NET target version. Then update README.md. -->
<MeneesTargetNetCoreBase>net8.0</MeneesTargetNetCoreBase>
<TargetFrameworks>$(MeneesTargetNetCoreBase);$(MeneesTargetNetFramework)</TargetFrameworks>
<TargetFramework>$(MeneesTargetNetCoreBase)</TargetFramework>

<RepoSrcFolder>$(MSBuildThisFileDirectory)</RepoSrcFolder>
<AssemblyOriginatorKeyFile>$(RepoSrcFolder)Menees.Remoting.snk</AssemblyOriginatorKeyFile>
Expand Down
15 changes: 1 addition & 14 deletions src/Menees.Remoting/ClientProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,9 @@
/// </summary>
/// <remarks>
/// DispatchProxy.Create requires this type to be un-sealed.
/// <para/>
/// It's also required to be public for .NET Framework since we can't reference v6.0.0 of DispatchProxy containing fix 30917.
/// https://github.com/dotnet/runtime/issues/30917
/// https://github.com/dotnet/runtime/discussions/64726#discussioncomment-2113733
/// <para/>
/// Note: Since this library is strongly-named, it can't use the InternalsVisibleTo("ProxyBuilder") hack.
/// https://github.com/dotnet/runtime/issues/25595#issuecomment-546330898
/// </remarks>
/// <typeparam name="TServiceInterface"></typeparam>
#if NETFRAMEWORK
[EditorBrowsable(EditorBrowsableState.Never)]
public
#else
internal
#endif
class ClientProxy<TServiceInterface> : DispatchProxy
internal class ClientProxy<TServiceInterface> : DispatchProxy
where TServiceInterface : class
{
#region Private Data Members
Expand Down
9 changes: 0 additions & 9 deletions src/Menees.Remoting/Menees.Remoting.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,4 @@
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />
</ItemGroup>

<Choose>
<When Condition=" '$(TargetFramework)' == '$(MeneesTargetNetFramework)' ">
<ItemGroup>
<!-- See comments at the top of ClientProxy.cs. v6.0.0 isn't available as a NuGet package. -->
<PackageReference Include="System.Reflection.DispatchProxy" Version="4.7.1" />
</ItemGroup>
</When>
</Choose>

</Project>
11 changes: 2 additions & 9 deletions src/Menees.Remoting/Models/Message.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,6 @@ public async Task WriteToAsync(Stream stream, ISerializer serializer, Cancellati
byte[] messageLength = BitConverter.GetBytes(message.Length);
CheckEndianOrder(messageLength);

// In .NET Framework the stream.ReadAsync implementation ignores cancellationToken.
// This is a workaround. https://stackoverflow.com/a/12893018/1882616
using CancellationTokenRegistration registration = cancellationToken.Register(stream.Close);

// Check cancellation before and after each operation so we don't get an ObjectDisposedException
// trying to use the stream after we closed it due to the cancellation token registration.
cancellationToken.ThrowIfCancellationRequested();
Expand Down Expand Up @@ -116,12 +112,9 @@ private static async Task<byte[]> RequireReadAsync(Stream stream, int requiredCo
{
byte[] result = new byte[requiredCount];

// In .NET Framework the stream.ReadAsync implementation ignores cancellationToken.
// This is a workaround. https://stackoverflow.com/a/12893018/1882616
// .NET 7 adds full support for stream and pipe cancellation.
// .NET 7 added full support for stream and pipe cancellation.
// https://devblogs.microsoft.com/dotnet/performance_improvements_in_net_7/#file-i-o
using CancellationTokenRegistration registration = cancellationToken.Register(stream.Close);

//
// We'll check cancellation before and after each operation so we don't get an ObjectDisposedException
// trying to use the stream after we closed it due to the cancellation token registration.
void ThrowIfCancellationRequested()
Expand Down
37 changes: 11 additions & 26 deletions src/Menees.Remoting/NodeSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@ public abstract class NodeSettings
{
#region Private Data Members

// I'm keeping this private for now (even though BaseTests duplicates it) because I may want to
// support other CLR scopes later (e.g., Framework, Core, Mono, Wasm, SQL CLR, Native).
private static readonly bool IsDotNetFramework = RuntimeInformation.FrameworkDescription.Contains("Framework");

private Func<string, Type?> tryGetType = RequireGetType;

#endregion
Expand Down Expand Up @@ -79,12 +75,10 @@ protected NodeSettings(string serverPath)
/// </summary>
/// <remarks>
/// This is useful for type translation and security. It's for translation if you're supporting
/// calls between different runtimes (e.g., Framework and "Core") or versions
/// (e.g., .NET 6.0 and 7.0). When mixing runtimes, many types will be in different
/// assemblies (e.g., int, string, Uri, IPAddress, Stack&lt;T>), so this handler needs
/// to deal with that for all your supported types. Even mixing versions of the same
/// runtime is complicated because strongly-named assemblies embed their version
/// in their AssemblyQualifiedName.
/// calls between different runtime versions (e.g., .NET x.0 and (x+1).0). When mixing versions,
/// some types may be in different assemblies, so this handler needs to deal with that for all
/// your supported types. Mixing versions is also complicated because strongly-named assemblies
/// embed their version in their AssemblyQualifiedName.
/// <para/>
/// A secure system needs to support a known list of legal/safe/valid types that it
/// can load dynamically. It shouldn't just trust and load an arbitrary assembly and
Expand Down Expand Up @@ -135,22 +129,13 @@ public static Type RequireGetType(string typeName)
Assembly? assembly = null;
string simpleName = assemblyName.Name ?? string.Empty;

// Try to translate the simple built-in scalar types correctly across different runtimes.
if ((IsDotNetFramework && simpleName.Equals("System.Private.CoreLib", StringComparison.OrdinalIgnoreCase))
|| (!IsDotNetFramework && simpleName.Equals("MsCorLib", StringComparison.OrdinalIgnoreCase)))
{
assembly = typeof(string).Assembly;
}
else
{
// See if any assembly is already loaded with the same simple name.
// This ignores versions and strong naming, so it's convenient but insecure.
// We'll allow a lower version to match in case a .NET 7.0 client needs to
// call into a .NET 6.0 server.
// https://github.com/dotnet/fsharp/issues/3408#issuecomment-319519926
assemblies ??= AppDomain.CurrentDomain.GetAssemblies();
assembly = assemblies.FirstOrDefault(asm => asm.GetName().Name?.Equals(simpleName, StringComparison.OrdinalIgnoreCase) ?? false);
}
// See if any assembly is already loaded with the same simple name.
// This ignores versions and strong naming, so it's convenient but insecure.
// We'll allow a lower version to match in case a newer (higher) .NET (x+1).0
// client needs to call into an older (lower) .NET x.0 server.
// https://github.com/dotnet/fsharp/issues/3408#issuecomment-319519926
assemblies ??= AppDomain.CurrentDomain.GetAssemblies();
assembly = assemblies.FirstOrDefault(asm => asm.GetName().Name?.Equals(simpleName, StringComparison.OrdinalIgnoreCase) ?? false);

return assembly;
},
Expand Down
8 changes: 3 additions & 5 deletions src/Menees.Remoting/Pipes/PipeClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ internal void SendRequest(TimeSpan connectTimeout, Action<Stream> sendRequest)
}
while (!connected && remainingWaitTime > TimeSpan.Zero);

this.EnsureConnected(connected, pipe);
EnsureConnected(connected, pipe);
this.Logger.LogTrace("Sending request.");
sendRequest(pipe);
this.Logger.LogTrace("Sent request.");
Expand Down Expand Up @@ -116,7 +116,7 @@ internal async Task SendRequestAsync(
}
while (!connected && remainingWaitTime > TimeSpan.Zero);

this.EnsureConnected(connected, pipe);
EnsureConnected(connected, pipe);
this.Logger.LogTrace("Sending request async.");
await sendRequestAsync(pipe, cancellationToken).ConfigureAwait(false);
this.Logger.LogTrace("Sent request async.");
Expand Down Expand Up @@ -146,21 +146,19 @@ private static TimeoutException NewSemaphoreTimeoutException(IOException ex)
return new TimeoutException("Could not connect to the server due to a semaphore timeout.", ex);
}

private void EnsureConnected(bool connected, NamedPipeClientStream pipe)
private static void EnsureConnected(bool connected, NamedPipeClientStream pipe)
{
if (!connected)
{
// The code in NamedPipeClientStream.Connect(int) does "throw new TimeoutException();" with no message.
throw new TimeoutException("Could not connect to the server within the specified timeout period.");
}

this.security?.CheckConnection(pipe);
pipe.ReadMode = Mode;
}

private NamedPipeClientStream CreatePipe(PipeOptions options)
{
// .NET 6 supports PipeOptions.CurrentUserOnly, but we have to simulate that in .NET Framework.
options |= this.security?.Options ?? PipeOptions.None;

this.Logger.LogTrace("Creating pipe client stream with options {Options}.", options);
Expand Down
32 changes: 1 addition & 31 deletions src/Menees.Remoting/Pipes/PipeClientSecurity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,7 @@ public sealed class PipeClientSecurity : ClientSecurity
{
#region Internal Constants

internal const PipeOptions CurrentUserOnlyOption =
#if NETFRAMEWORK
PipeOptions.None;
#else
PipeOptions.CurrentUserOnly;
#endif
internal const PipeOptions CurrentUserOnlyOption = PipeOptions.CurrentUserOnly;

#endregion

Expand Down Expand Up @@ -50,29 +45,4 @@ private PipeClientSecurity()
internal PipeOptions Options { get; }

#endregion

#region Internal Methods

internal void CheckConnection(NamedPipeClientStream pipe)
{
// .NET 6.0 handles PipeOptions.CurrentUserOnly validation. We have to simulate it in .NET Framework.
if (pipe != null && this.Options == PipeOptions.None)
{
#if NETFRAMEWORK
// This code is from .NET 6.0's ValidateRemotePipeUser for Windows.
// https://github.com/dotnet/runtime/blob/main/src/libraries/System.IO.Pipes/src/System/IO/Pipes/NamedPipeClientStream.Windows.cs
PipeSecurity accessControl = pipe.GetAccessControl();
IdentityReference? remoteOwnerSid = accessControl.GetOwner(typeof(SecurityIdentifier));
using WindowsIdentity currentIdentity = WindowsIdentity.GetCurrent();
SecurityIdentifier? currentUserSid = currentIdentity.Owner;
if (remoteOwnerSid != currentUserSid)
{
pipe.Close();
throw new UnauthorizedAccessException("Could not connect to the pipe because it was not owned by the current user.");
}
#endif
}
}

#endregion
}
15 changes: 2 additions & 13 deletions src/Menees.Remoting/Pipes/PipeServerSecurity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,16 @@ public sealed class PipeServerSecurity : ServerSecurity
/// </summary>
/// <param name="security">The pipe's access control and audit security.</param>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="security"/> is null.</exception>
#if NETCOREAPP
[SupportedOSPlatform("windows")]
#endif
public PipeServerSecurity(PipeSecurity security)
{
this.scope = Scope.CustomSecurity;
this.Security = security ?? throw new ArgumentNullException(nameof(security));
#if NETCOREAPP

if (!OperatingSystem.IsWindows())
{
throw new InvalidOperationException("Custom PipeSecurity is not supported on this OS platform.");
}
#endif
}

private PipeServerSecurity(Scope scope)
Expand Down Expand Up @@ -99,14 +96,9 @@ private enum Scope
internal NamedPipeServerStream? CreatePipe(
string pipeName, PipeDirection direction, int maxListeners, PipeTransmissionMode mode, PipeOptions options)
{
NamedPipeServerStream? result = null;

options |= this.scope == Scope.CurrentUserOnly ? PipeClientSecurity.CurrentUserOnlyOption : PipeOptions.None;

#if NETFRAMEWORK
PipeSecurity security = this.Security ?? this.CreateWindowsPipeSecurity();
result = new(pipeName, direction, maxListeners, mode, options, 0, 0, security);
#else
NamedPipeServerStream? result;
if (OperatingSystem.IsWindows())
{
// NamedPipeServerStreamAcl.Create requires pipeSecurity == null with PipeOptions.CurrentUserOnly.
Expand All @@ -132,7 +124,6 @@ private enum Scope
{
throw new InvalidOperationException("Custom PipeSecurity is not supported on this OS platform.");
}
#endif

return result;
}
Expand All @@ -141,9 +132,7 @@ private enum Scope

#region Private Methods

#if NETCOREAPP
[SupportedOSPlatform("windows")]
#endif
private PipeSecurity CreateWindowsPipeSecurity()
{
PipeSecurity result = new();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>$(MeneesTargetNetCoreBase);$(MeneesTargetNetFramework)</TargetFrameworks>
<TargetFramework>$(MeneesTargetNetCoreBase)</TargetFramework>
<IsUnitTestProject>false</IsUnitTestProject>
</PropertyGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>$(MeneesTargetNetCoreBase);$(MeneesTargetNetFramework)</TargetFrameworks>
<TargetFramework>$(MeneesTargetNetCoreBase)</TargetFramework>
<IsUnitTestProject>false</IsUnitTestProject>
</PropertyGroup>

Expand Down
11 changes: 2 additions & 9 deletions tests/Menees.Remoting.TestHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public static int Main(string[] args)
int maxListeners = int.Parse(args[3]);
int minListeners = int.Parse(args[4]);

// .NET Framework supports Load(AssemblyName), but .NET Core requires LoadFrom().
// .NET Core requires LoadFrom() not Load(AssemblyName).
Assembly assembly = Assembly.LoadFrom(assemblyPath);
Type? serviceType = assembly.GetType(typeName);

Expand Down Expand Up @@ -135,16 +135,9 @@ private static ExitCode FatalError(ExitCode exitCode, string message)

private static IDisposable? HandleManualExit(IServerHost host)
{
IDisposable? result;

#if NETCOREAPP
result = PosixSignalRegistration.Create(
IDisposable? result = PosixSignalRegistration.Create(
PosixSignal.SIGINT,
context => host.Exit((int)ExitCode.CtrlC));
#else
result = null;
#endif

return result;
}

Expand Down
2 changes: 0 additions & 2 deletions tests/Menees.Remoting.Tests/BaseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ public class BaseTests

#region Public Properties

public static bool IsDotNetFramework { get; } = RuntimeInformation.FrameworkDescription.Contains("Framework");

public ILoggerFactory LoggerFactory => this.logManager?.LoggerFactory ?? NullLoggerFactory.Instance;

#endregion
Expand Down
12 changes: 0 additions & 12 deletions tests/Menees.Remoting.Tests/MessageNodeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,24 +127,12 @@ public async Task CancellationAsync()
await client.SendAsync("Test", clientCancellationSource.Token).ConfigureAwait(false);
Assert.Fail("Should not reach here.");
}
catch (TaskCanceledException ex)
{
// Sometimes .NET Framework throws this.
// Should.ThrowAsync doesn't work with TaskCanceledException.
// https://github.com/shouldly/shouldly/issues/831
ex.ShouldBeOfType<TaskCanceledException>();
}
catch (OperationCanceledException ex)
{
// Core throws this, and Framework sometimes does.
ex.ShouldBeOfType<OperationCanceledException>();
}

#if NET8_0_OR_GREATER
await serverCancellationSource.CancelAsync().ConfigureAwait(false);
#else
serverCancellationSource.Cancel();
#endif
}

[TestMethod]
Expand Down
11 changes: 3 additions & 8 deletions tests/Menees.Remoting.Tests/NodeSettingsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,9 @@ public void RequireGetType()
const string FrameworkStringTypeName = "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089";
TestVersions(typeof(string), CoreStringTypeName, FrameworkStringTypeName);

// On .NET Framework, we won't be able to load the System.Private.Uri assembly, and NodeSettings
// has no special knowledge of that assembly (like it does for System.Private.CoreLib).
if (!IsDotNetFramework)
{
const string CoreUriTypeName = "System.Uri, System.Private.Uri, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a";
const string FrameworkUriTypeName = "System.Uri, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089";
TestVersions(typeof(Uri), CoreUriTypeName, FrameworkUriTypeName);
}
const string CoreUriTypeName = "System.Uri, System.Private.Uri, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a";
const string FrameworkUriTypeName = "System.Uri, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089";
TestVersions(typeof(Uri), CoreUriTypeName, FrameworkUriTypeName);

const string CoreDictionaryTypeName = "System.Collections.Generic.IReadOnlyDictionary`2[" +
"[System.String, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]," +
Expand Down
Loading

0 comments on commit ae9d164

Please sign in to comment.