Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for .NET Standard 2.0 and 2.1 #513

Merged
merged 49 commits into from
Jun 20, 2024
Merged

Support for .NET Standard 2.0 and 2.1 #513

merged 49 commits into from
Jun 20, 2024

Conversation

mtmk
Copy link
Collaborator

@mtmk mtmk commented Jun 10, 2024

Main focus of this PR is to introduce .NET Standard support as a preview feature and make absolutely sure the .NET 6 & 8 support is not affected at all.

To reduce the performance impact on platforms that support 2.1 (e.g. Unity) we take advantage of 2.1 APIs where possible. We can create follow up PRs to improve .NET Standard performance for both 2.0 and 2.1.

InboxSub WeakRef changes for 2.0

There are minor changes for 2.1 but 2.0 seems to lack quite a few APIs so changes in this class might require a little more scrutiny.

TLS Support

Most of the APIs aren't available in netstandard, hence TLS support will be limited in targets using netstandard. However the important thing is to make sure net8.0 isn't affected at all. Suggestion is to improve netstandard TLS support in follow up PRs and have a limited support for TLS features at least in the initial preview release.

Code Changes in net6.0 and net8.0 Targets

There are minor code changes which will affect the net6.0 and net8.0 builds. These code changes reduce preprocessor usage significantly, however they shouldn't introducing any functional changes or side-effects.

- return ValueTask.CompletedTask;
+ return default;
  • ValueTask.FromResult doesn't exist in netstandard so instead we changed all our usages to using .ctor as it's implemented in source for ValueTask.FromResult
- return ValueTask.FromResult(group);
+ return new ValueTask<Group>(group);
  • Removed unused classes CancellationTimerPool, FixedArrayBufferWriter and ThreadPoolWorkItem and their usages.

  • Replaced TimeSpan operators with Ticks calculation similar to what happens in current TimeSpan source except some bounds checks which should not affect us since we're using constants.

- _backoff *= 2;
+ _backoff = new TimeSpan(_backoff.Ticks * 2);
  • Case-insensitive string comparison for error message checks:
- Error = error;
- IsAuthError = Error.Contains("authorization violation", StringComparison.OrdinalIgnoreCase)
+ Error = error.ToLower();
+ IsAuthError = Error.Contains("authorization violation")
  • In string.Split(), use char instead of string in ReplyToDateTimeAndSeq.cs
- var originalTokens = reply.Split(".").AsSpan();
+ var originalTokens = reply.Split('.').AsSpan();

Also in NatsKVStore.cs

- if (key.StartsWith('.') || key.EndsWith('.'))
+ if (key.StartsWith(".") || key.EndsWith("."))

Tests

There are no changes to the existing tests to make sure no regressions happen. Only couple of tests are deleted related to now unused classes in the library.

The new 'Platform' test targets .NET Framework 4.8.1 and uses netstandard2.0 version of the library. These tests broadly covers all the libraries demonstrating that netstandard target works as expected at least for the basic functionality.

Idea is eventually merge all the tests and run them for all the targets for best platform coverage.

Info

https://learn.microsoft.com/en-us/dotnet/standard/net-standard?tabs=net-standard-2-0#select-net-standard-version

@mtmk mtmk requested a review from caleblloyd June 14, 2024 01:03
@mtmk mtmk marked this pull request as ready for review June 14, 2024 01:03
@mtmk mtmk self-assigned this Jun 14, 2024
@caleblloyd
Copy link
Collaborator

caleblloyd commented Jun 14, 2024

Should we put this in a single file? So we don't have to put it all over the place?

#if NETSTANDARD
global using NATS.Client.Core.Internal.NetStandardExtensions;
#endif

@mtmk mtmk requested a review from scottf June 14, 2024 12:43
using var cts = new CancellationTokenSource(timeout);
await _sslStream.AuthenticateAsClientAsync(options, cts.Token).ConfigureAwait(false);
#else
await _sslStream.AuthenticateAsClientAsync(uri.Host).ConfigureAwait(false);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this effectively ignores _tlsOpts on netstandard

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the problem was I couldn't see na easy way of loading the PEM files

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea we may need to bring in Bouncy Castle or something to do it. Not sure if someone has made a good compat library

Copy link
Collaborator Author

@mtmk mtmk Jun 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

@to11mtm to11mtm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some food for thought but overall this is awesome. <3

Comment on lines 318 to 322
internal static bool IsNotCompletedSuccessfully(this Task? task)
{
return task != null && (!task.IsCompleted || task.IsCanceled || task.IsFaulted);
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure whether it's actually better to bang against Task.Status and/or Task.IsCompleted+Task.Exception!=null or not, here's some sharplab as FTT

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done (task.Status == TaskStatus.RanToCompletion)

Comment on lines 167 to 171
#if NETSTANDARD2_0
if (_flushTask.IsNotCompletedSuccessfully())
#else
if (_flushTask is { IsCompletedSuccessfully: false })
#endif
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stupid question: would it be cleaner to put the logic shifts in IsNotCompletedSuccessfully() extension method and just aggressive inline? Or would it be too ugly for how shims should be composed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes good point, fewer #if the better!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines 378 to 382
#if NETSTANDARD2_0
var span = buffer.IsSingleSegment ? buffer.First.Span : buffer.ToArray();
#else
var span = buffer.IsSingleSegment ? buffer.FirstSpan : buffer.ToArray();
#endif
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another potential candidate for ext-method-consolidation is buffer.FirstSpan vs buffer.First.Span.

I should note that, AFAIR, if the extension method is internal, Resharper/Rider can always let team 're-dupe' the callsites later easily if ppl change their minds.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines 55 to 57
#if NETSTANDARD2_0
return new string(buffer.ToArray());
#else
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC at least if we are pulling in System.Memory for netstandard we can just do buffer.ToString() here and there's logic to handle that for ReadOnlySpan<char> without having to do the unsafe nasty on your own :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very good observation. yes, there is an exception for span in ToString(). nice!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +101 to +109
#if NETSTANDARD2_0
var randomBytes = new byte[(int)PrefixLength];

if (rng == null)
{
using var randomNumberGenerator = RandomNumberGenerator.Create();
randomNumberGenerator.GetBytes(randomBytes);
}
#else
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we able to explain why the randomness this will do is as safe as newer platforms? (It's been a while since I've used the RNG in standard but figured it was good to ask)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think as long as we use 'some kind of' RNG we should be fine here. This is about generating unique IDs and there shouldn't be a huge security concern but please feel free to challenge that, I might be missing something.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remarks from netstandard System.Security.Cryptography.RandomNumberGenerator class docs:

Cryptographic random number generators create cryptographically strong random values.

Using the static members of this class is the preferred way to generate random values.

To create a random number generator, call the Create() method. This is preferred over calling the constructor of the derived class RNGCryptoServiceProvider, which is not available on all platforms.

Comment on lines 161 to 165
#if NETSTANDARD2_0
_bySid.TryRemove(subMetadata.Sid, out _);
#else
_bySid.Remove(subMetadata.Sid, out _);
#endif
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there value in using the different overloads here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


namespace NATS.Client.Core.Internal;

internal sealed class ThreadPoolWorkItem<T> : IThreadPoolWorkItem
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RIP ;_;

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it was a good one. did you write it back in the day?

@@ -128,7 +132,11 @@ internal static string NewInbox(ReadOnlySpan<char> prefix)
var remaining = buffer.Slice((int)totalPrefixLength);
var didWrite = NuidWriter.TryWriteNuid(remaining);
Debug.Assert(didWrite, "didWrite");
#if NETSTANDARD2_0
return new string(buffer.ToArray());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another potential case for buffer.ToString() instead

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines 267 to 277
var bytes = Convert.FromBase64String(response.Message.Data);
var buffer = new ReadOnlySequence<byte>(bytes);
try
{
data = serializer.Deserialize(buffer);
}
catch (Exception e)
{
deserializeException = new NatsDeserializeException(buffer.ToArray(), e);
data = default;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of notes here:

  1. I think if we can, we should try to keep behavior consistent, even if that means the Convert.FromBase64String is done as a try-catch.
  2. Is there an issue on the backlog to convert the StreamMessage.Data type into something a bit less thrashy? System.Buffers.Text has To/From for utf8 byte sequences and it looks like they work with netstandard20, adapting to such an approach longer term may help consolidate this code as well as lower the deserialization cost of KV and other stream reads in NET8 (as we can hold the base64 in UTF8 bytes rather than UTF16 string, maybe pool in future too? Not sure how easy it would be to do that part though...)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(1) 👍
(2) that sounds good. created one: #518

Comment on lines +34 to +37
#if NETSTANDARD2_0
var hash = sha256.ComputeHash(value.ToArray());
hash.AsSpan().CopyTo(destination);
#else
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where this gets tricky is, if the object is above a certain size, grab a pooled array and copy the contents over.

-OR-, it might be better to pay the small price of something like CommunityToolkit.HighPerformance's ReadOnlyMemory<byte> stream adapter to avoid the copies.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

created #519

mtmk added a commit that referenced this pull request Jun 20, 2024
* Support for .NET Standard 2.0 and 2.1 (#513)
@mtmk mtmk mentioned this pull request Jun 20, 2024
Copy link
Collaborator

@caleblloyd caleblloyd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@mtmk mtmk merged commit a7c88e1 into main Jun 20, 2024
10 checks passed
@mtmk mtmk deleted the netstandard-support branch June 20, 2024 13:16
mtmk added a commit that referenced this pull request Jun 24, 2024
* Support for .NET Standard 2.0 and 2.1 (#513)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support .NET Standard 2.0
3 participants