From 19ea862dfe8973acc43ac8a80b22932bd943ab56 Mon Sep 17 00:00:00 2001 From: sakno Date: Mon, 20 Jan 2025 21:09:34 +0200 Subject: [PATCH] Release 5.18.0 --- CHANGELOG.md | 23 +++ README.md | 32 ++-- src/Directory.Packages.props | 14 +- src/DotNext.AotTests/DelegateHelpersTests.cs | 33 ++++ .../Binary/SevenBitEncodedIntReader.cs | 3 +- src/DotNext.IO/DotNext.IO.csproj | 2 +- src/DotNext.IO/IO/PoolingBufferedStream.cs | 126 +++++++----- .../DotNext.Metaprogramming.csproj | 2 +- .../Linq/Expressions/ExpressionBuilder.cs | 42 ++-- .../Linq/Expressions/LockExpression.cs | 2 +- .../UnsignedRightShiftExpression.cs | 95 +++++++++ .../AsyncStateMachine.Pooling.cs | 8 +- .../CompilerServices/AsyncStateMachine.cs | 12 +- .../AsyncStateMachineBuilder.cs | 2 +- .../EnterGuardedCodeExpression.cs | 7 +- .../CompilerServices/GuardedStatement.cs | 10 +- .../HasNoExceptionExpression.cs | 2 +- .../CompilerServices/IAsyncStateMachine.cs | 10 +- .../CompilerServices/MoveNextExpression.cs | 4 +- .../PreallocatedCharBuffer.cs | 10 +- .../Threading/Tasks/CompletedTask.cs | 24 +-- .../Buffers/MemoryWriterTests.cs | 16 +- .../OptionalStringLengthAttributeTests.cs | 2 +- src/DotNext.Tests/DelegateHelpersTests.cs | 20 +- .../IO/PoolingBufferedStreamTests.cs | 16 ++ .../Expressions/ExpressionBuilderTests.cs | 53 ++++-- .../Metaprogramming/LambdaTests.cs | 9 +- .../Raft/MemoryBasedStateMachineTests.cs | 70 +++---- .../Net/Http/HttpEndPointTests.cs | 10 +- src/DotNext.Tests/OptionalTests.cs | 10 + .../Reflection/TypeExtensionsTests.cs | 18 +- src/DotNext.Tests/ResultTests.cs | 27 ++- .../Runtime/Caching/RandomAccessCacheTests.cs | 4 +- .../Runtime/ValueReferenceTests.cs | 21 +- src/DotNext.Tests/SpanTests.cs | 8 + .../Text/Encodings/Web/FileUriTests.cs | 76 ++++++++ .../Threading/AsyncExclusiveLockTests.cs | 2 +- .../Threading/AtomicContainerTests.cs | 14 ++ .../DotNext.Threading.csproj | 2 +- .../Runtime/Caching/RandomAccessCache.cs | 180 +++++++----------- .../Threading/AsyncExclusiveLock.cs | 2 +- src/DotNext.Unsafe/DotNext.Unsafe.csproj | 2 +- .../Runtime/InteropServices/Pointer.cs | 2 +- src/DotNext/Buffers/IReadOnlySpanConsumer.cs | 2 +- src/DotNext/Buffers/SpanOwner.cs | 74 +++++-- src/DotNext/DotNext.csproj | 2 +- src/DotNext/ExceptionMessages.cs | 2 + src/DotNext/ExceptionMessages.restext | 3 +- src/DotNext/Func.cs | 21 +- src/DotNext/LibraryFeature.cs | 9 + src/DotNext/LibrarySettings.cs | 50 ----- src/DotNext/Net/Http/HttpEndPoint.cs | 8 +- src/DotNext/Optional.cs | 44 ++--- src/DotNext/RandomExtensions.cs | 13 +- src/DotNext/Result.cs | 30 ++- src/DotNext/Runtime/Intrinsics.cs | 10 + src/DotNext/Runtime/ValueReference.cs | 45 +++-- src/DotNext/Sentinel.cs | 8 +- src/DotNext/Text/Encodings/Web/FileUri.cs | 156 +++++++++++++++ .../DotNext.AspNetCore.Cluster.csproj | 2 +- .../Net/Cluster/Messaging/InMemoryMessage.cs | 4 +- .../DotNext.Net.Cluster.csproj | 2 +- .../Text/Json/JsonUtils.cs | 30 --- 63 files changed, 1026 insertions(+), 516 deletions(-) create mode 100644 src/DotNext.AotTests/DelegateHelpersTests.cs create mode 100644 src/DotNext.Metaprogramming/Linq/Expressions/UnsignedRightShiftExpression.cs create mode 100644 src/DotNext.Tests/Text/Encodings/Web/FileUriTests.cs create mode 100644 src/DotNext/LibraryFeature.cs delete mode 100644 src/DotNext/LibrarySettings.cs create mode 100644 src/DotNext/Text/Encodings/Web/FileUri.cs delete mode 100644 src/cluster/DotNext.Net.Cluster/Text/Json/JsonUtils.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 28f3921770..0a2099b5fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,29 @@ Release Notes ==== +# 01-20-2025 +DotNext 5.18.0 +* Introduced `FileUri` class that allows to convert Windows/Unix file names to URI according to `file://` scheme + +DotNext.Metaprogramming 5.18.0 +* Introduced expression tree for unsigned right shift operator `>>>` + +DotNext.Unsafe 5.18.0 +* Updated dependencies + +DotNext.Threading 5.18.0 +* Synchronous `TryAcquire` implemented by `AsyncExclusiveLock` and `AsyncReaderWriterLock` are now implemented in portable way. Previously, WASM target was not supported. Additionally, the method supports lock stealing +* * Improved synchronous support for `RandomAccessCache` class + +DotNext.IO 5.18.0 +* Fixed issue of `PoolingBufferedStream` class when the stream has buffered bytes in the write buffer and `Position` is set to backward + +DotNext.Net.Cluster 5.18.0 +* Updated dependencies + +DotNext.AspNetCore.Cluster 5.18.0 +* Updated dependencies + # 01-03-2025 DotNext 5.17.2 * Improved AOT compatibility diff --git a/README.md b/README.md index b4117803de..07f48532b1 100644 --- a/README.md +++ b/README.md @@ -44,29 +44,29 @@ All these things are implemented in 100% managed code on top of existing .NET AP * [NuGet Packages](https://www.nuget.org/profiles/rvsakno) # What's new -Release Date: 01-03-2025 +Release Date: 01-20-2025 -DotNext 5.17.2 -* Improved AOT compatibility -* Fixed nullability attributes +DotNext 5.18.0 +* Introduced `FileUri` class that allows to convert Windows/Unix file names to URI according to `file://` scheme -DotNext.Metaprogramming 5.17.2 -* Fixed nullability attributes +DotNext.Metaprogramming 5.18.0 +* Introduced expression tree for unsigned right shift operator `>>>` -DotNext.Unsafe 5.17.2 -* Fixed nullability attributes +DotNext.Unsafe 5.18.0 +* Updated dependencies -DotNext.Threading 5.17.2 -* Fixed nullability attributes +DotNext.Threading 5.18.0 +* Synchronous `TryAcquire` implemented by `AsyncExclusiveLock` and `AsyncReaderWriterLock` are now implemented in portable way. Previously, WASM target was not supported. Additionally, the method supports lock stealing +* Improved synchronous support for `RandomAccessCache` class -DotNext.IO 5.17.2 -* Fixed nullability attributes +DotNext.IO 5.18.0 +* Fixed issue of `PoolingBufferedStream` class when the stream has buffered bytes in the write buffer and `Position` is set to backward -DotNext.Net.Cluster 5.17.2 -* Fixed nullability attributes +DotNext.Net.Cluster 5.18.0 +* Updated dependencies -DotNext.AspNetCore.Cluster 5.17.2 -* Fixed nullability attributes +DotNext.AspNetCore.Cluster 5.18.0 +* Updated dependencies Changelog for previous versions located [here](./CHANGELOG.md). diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index a9b5278073..a8e637ea32 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -19,7 +19,7 @@ - + @@ -31,16 +31,16 @@ - - - + + + - - - + + + \ No newline at end of file diff --git a/src/DotNext.AotTests/DelegateHelpersTests.cs b/src/DotNext.AotTests/DelegateHelpersTests.cs new file mode 100644 index 0000000000..632b11fda5 --- /dev/null +++ b/src/DotNext.AotTests/DelegateHelpersTests.cs @@ -0,0 +1,33 @@ +namespace DotNext; + +public class DelegateHelpersTests +{ + [TestMethod] + public void ConstantProvider() + { + Assert.AreSame(Func.Constant(null), Func.Constant(null)); + Assert.IsNull(Func.Constant(null).Invoke()); + + Assert.AreSame(Func.Constant(true), Func.Constant(true)); + Assert.AreSame(Func.Constant(false), Func.Constant(false)); + + Assert.AreEqual(42, Func.Constant(42).Invoke()); + Assert.AreEqual("Hello, world", Func.Constant("Hello, world").Invoke()); + } + + [TestMethod] + public void ValueTypeConst() + { + const long value = 42L; + var provider = Func.Constant(value); + Assert.AreEqual(value, provider.Target); + } + + [TestMethod] + public void StringConst() + { + const string value = "Hello, world"; + var provider = Func.Constant(value); + Assert.AreSame(value, provider.Target); + } +} \ No newline at end of file diff --git a/src/DotNext.IO/Buffers/Binary/SevenBitEncodedIntReader.cs b/src/DotNext.IO/Buffers/Binary/SevenBitEncodedIntReader.cs index e70b393e17..4f5d9b44d3 100644 --- a/src/DotNext.IO/Buffers/Binary/SevenBitEncodedIntReader.cs +++ b/src/DotNext.IO/Buffers/Binary/SevenBitEncodedIntReader.cs @@ -1,5 +1,4 @@ -using System.Numerics; -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace DotNext.Buffers.Binary; diff --git a/src/DotNext.IO/DotNext.IO.csproj b/src/DotNext.IO/DotNext.IO.csproj index 71cb1d48aa..1b3856750b 100644 --- a/src/DotNext.IO/DotNext.IO.csproj +++ b/src/DotNext.IO/DotNext.IO.csproj @@ -11,7 +11,7 @@ .NET Foundation and Contributors .NEXT Family of Libraries - 5.17.2 + 5.18.0 DotNext.IO MIT diff --git a/src/DotNext.IO/IO/PoolingBufferedStream.cs b/src/DotNext.IO/IO/PoolingBufferedStream.cs index 97cec01860..01194fd73e 100644 --- a/src/DotNext.IO/IO/PoolingBufferedStream.cs +++ b/src/DotNext.IO/IO/PoolingBufferedStream.cs @@ -99,8 +99,10 @@ public override long Length get { ThrowIfDisposed(); - - WriteCore(); + + if (WriteCore()) + Reset(); + return stream.Length; } } @@ -111,7 +113,9 @@ public override void SetLength(long value) ArgumentOutOfRangeException.ThrowIfNegative(value); ThrowIfDisposed(); - WriteCore(); + if (WriteCore()) + Reset(); + stream.SetLength(value); } @@ -196,7 +200,8 @@ public void Write() if (!stream.CanWrite) throw new NotSupportedException(); - WriteCore(); + if (WriteCore()) + Reset(); } /// @@ -215,7 +220,7 @@ public ValueTask WriteAsync(CancellationToken token = default) } else if (stream.CanWrite) { - task = WriteCoreAsync(token); + task = WriteCoreAsync(out _, token); } else { @@ -225,21 +230,28 @@ public ValueTask WriteAsync(CancellationToken token = default) return task; } - private void WriteCore() + private bool WriteCore() { Debug.Assert(stream is not null); - - if (WrittenMemory.Span is { IsEmpty: false } writeBuf) + + var writeBuf = WrittenMemory.Span; + bool result; + if (result = !writeBuf.IsEmpty) { stream.Write(writeBuf); writePosition = 0; } + + return result; } - private ValueTask WriteCoreAsync(CancellationToken token) - => WrittenMemory is { IsEmpty: false } writeBuf + private ValueTask WriteCoreAsync(out bool isWritten, CancellationToken token) + { + var writeBuf = WrittenMemory; + return (isWritten = !writeBuf.IsEmpty) ? WriteAndResetAsync(writeBuf, token) : ValueTask.CompletedTask; + } private ValueTask WriteAndResetAsync(ReadOnlyMemory data, CancellationToken token) { @@ -271,12 +283,6 @@ private ref readonly MemoryOwner EnsureBufferAllocated() return ref result; } - private void ResetIfNeeded() - { - if (writePosition is 0 && readLength == readPosition) - Reset(); - } - /// public override void Write(ReadOnlySpan data) { @@ -307,7 +313,7 @@ private void WriteCore(ReadOnlySpan data) if (data.Length > freeBuf.Length) { stream.Write(data); - ResetIfNeeded(); + Reset(); } else { @@ -366,15 +372,15 @@ private async ValueTask WriteCoreAsync(ReadOnlyMemory data, CancellationTo // drain buffered data if needed if (freeBuf.Length < data.Length) { - await WriteCoreAsync(token).ConfigureAwait(false); + await WriteCoreAsync(out _, token).ConfigureAwait(false); freeBuf = buffer.Memory.Slice(writePosition); } - + // if internal buffer has not enough space then just write through if (data.Length > freeBuf.Length) { await stream.WriteAsync(data, token).ConfigureAwait(false); - ResetIfNeeded(); + Reset(); } else { @@ -403,7 +409,6 @@ private int ReadFromBuffer(Span destination) { readBuf.CopyTo(destination, out count); readPosition += count; - ResetIfNeeded(); } else { @@ -433,10 +438,17 @@ public override int Read(Span data) private int ReadCore(Span data) { Debug.Assert(stream is not null); - - var bytesRead = ReadFromBuffer(data); - data = data.Slice(bytesRead); - WriteCore(); + + int bytesRead; + if (WriteCore()) + { + bytesRead = 0; + } + else + { + bytesRead = ReadFromBuffer(data); + data = data.Slice(bytesRead); + } if (data.IsEmpty) { @@ -451,6 +463,9 @@ private int ReadCore(Span data) readLength = stream.Read(EnsureBufferAllocated().Span); bytesRead += ReadFromBuffer(data); } + + if (readPosition == readLength) + Reset(); return bytesRead; } @@ -496,24 +511,37 @@ public override Task ReadAsync(byte[] data, int offset, int count, Cancella private async ValueTask ReadCoreAsync(Memory data, CancellationToken token) { Debug.Assert(stream is not null); - - var bytesRead = ReadFromBuffer(data.Span); - data = data.Slice(bytesRead); - await WriteCoreAsync(token).ConfigureAwait(false); + int bytesRead; + await WriteCoreAsync(out var isWritten, token).ConfigureAwait(false); + if (isWritten) + { + bytesRead = 0; + } + else + { + bytesRead = ReadFromBuffer(data.Span); + data = data.Slice(bytesRead); + } + if (data.IsEmpty) { // nothing to do } else if (data.Length > MaxBufferSize) { + Debug.Assert(readPosition == readLength); bytesRead += await stream.ReadAsync(data, token).ConfigureAwait(false); } else { + Debug.Assert(readPosition == readLength); readLength = await stream.ReadAsync(EnsureBufferAllocated().Memory, token).ConfigureAwait(false); bytesRead += ReadFromBuffer(data.Span); } + + if (readPosition == readLength) + Reset(); return bytesRead; } @@ -536,14 +564,16 @@ public async ValueTask ReadAsync(CancellationToken token = default) if (!stream.CanRead) throw new NotSupportedException(); - - await WriteCoreAsync(token).ConfigureAwait(false); + + await WriteCoreAsync(out _, token).ConfigureAwait(false); var count = PrepareReadBuffer(out var readBuf) ? await stream.ReadAsync(readBuf, token).ConfigureAwait(false) : throw new InternalBufferOverflowException(); readLength += count; - ResetIfNeeded(); + + if (readPosition == readLength) + Reset(); return count > 0; } @@ -568,7 +598,9 @@ public bool Read() ? stream.Read(readBuf.Span) : throw new InternalBufferOverflowException(); readLength += count; - ResetIfNeeded(); + + if (readPosition == readLength) + Reset(); return count > 0; } @@ -660,19 +692,13 @@ public override long Seek(long offset, SeekOrigin origin) AssertState(); ThrowIfDisposed(); - long result; - if (WrittenMemory.Span is { IsEmpty: false } writeBuf) - { - stream.Write(writeBuf); - result = stream.Seek(offset, origin); - ResetIfNeeded(); - } - else + if (WriteCore()) { - result = SeekNoWriteBuffer(offset, origin); + Reset(); + return stream.Seek(offset, origin); } - return result; + return SeekNoWriteBuffer(offset, origin); } private long SeekNoWriteBuffer(long offset, SeekOrigin origin) @@ -765,14 +791,13 @@ public override void CopyTo(Stream destination, int bufferSize) destination.Write(readBuf); readLength = readPosition = 0; } - else if (WrittenMemory.Span is { IsEmpty: false } writeBuf) + else { - stream.Write(writeBuf); - writePosition = 0; + WriteCore(); } stream.CopyTo(destination, bufferSize); - ResetIfNeeded(); + Reset(); } /// @@ -787,14 +812,13 @@ public override async Task CopyToAsync(Stream destination, int bufferSize, Cance await destination.WriteAsync(readBuf, token).ConfigureAwait(false); readLength = readPosition = 0; } - else if (WrittenMemory is { IsEmpty: false } writeBuf) + else { - await stream.WriteAsync(writeBuf, token).ConfigureAwait(false); - writePosition = 0; + await WriteCoreAsync(out _, token).ConfigureAwait(false); } await stream.CopyToAsync(destination, bufferSize, token).ConfigureAwait(false); - ResetIfNeeded(); + Reset(); } /// diff --git a/src/DotNext.Metaprogramming/DotNext.Metaprogramming.csproj b/src/DotNext.Metaprogramming/DotNext.Metaprogramming.csproj index e6c7b4b400..7077587b3e 100644 --- a/src/DotNext.Metaprogramming/DotNext.Metaprogramming.csproj +++ b/src/DotNext.Metaprogramming/DotNext.Metaprogramming.csproj @@ -8,7 +8,7 @@ true false nullablePublicOnly - 5.17.2 + 5.18.0 .NET Foundation .NEXT Family of Libraries diff --git a/src/DotNext.Metaprogramming/Linq/Expressions/ExpressionBuilder.cs b/src/DotNext.Metaprogramming/Linq/Expressions/ExpressionBuilder.cs index 9ad3e4eed3..464a2f5862 100644 --- a/src/DotNext.Metaprogramming/Linq/Expressions/ExpressionBuilder.cs +++ b/src/DotNext.Metaprogramming/Linq/Expressions/ExpressionBuilder.cs @@ -3,7 +3,6 @@ using System.Linq.Expressions; using System.Reflection; using System.Text; -using static System.Linq.Enumerable; namespace DotNext.Linq.Expressions; @@ -32,9 +31,10 @@ public static UnaryExpression UnaryPlus(this Expression expression) /// The equivalent code is -a. /// /// The operand. + /// to perform checked arithmetic operation; otherwise, . /// Unary expression. - public static UnaryExpression Negate(this Expression expression) - => Expression.Negate(expression); + public static UnaryExpression Negate(this Expression expression, bool @checked = false) + => @checked ? Expression.NegateChecked(expression) : Expression.Negate(expression); /// /// Constructs logical NOT expression. @@ -114,9 +114,10 @@ public static BinaryExpression Modulo(this Expression left, Expression right) /// /// The left operand. /// The right operand. + /// to perform checked arithmetic operation; otherwise, . /// Binary expression. - public static BinaryExpression Add(this Expression left, Expression right) - => Expression.Add(left, right); + public static BinaryExpression Add(this Expression left, Expression right, bool @checked = false) + => @checked ? Expression.AddChecked(left, right) : Expression.Add(left, right); private static MethodCallExpression Concat(Expression[] strings) => strings.LongLength switch { @@ -143,9 +144,10 @@ public static MethodCallExpression Concat(this Expression first, params Expressi /// /// The left operand. /// The right operand. + /// to perform checked arithmetic operation; otherwise, . /// Binary expression. - public static BinaryExpression Subtract(this Expression left, Expression right) - => Expression.Subtract(left, right); + public static BinaryExpression Subtract(this Expression left, Expression right, bool @checked = false) + => @checked ? Expression.SubtractChecked(left, right) : Expression.Subtract(left, right); /// /// Constructs binary arithmetic multiplication expression. @@ -155,9 +157,10 @@ public static BinaryExpression Subtract(this Expression left, Expression right) /// /// The left operand. /// The right operand. + /// to perform checked arithmetic operation; otherwise, . /// Binary expression. - public static BinaryExpression Multiply(this Expression left, Expression right) - => Expression.Multiply(left, right); + public static BinaryExpression Multiply(this Expression left, Expression right, bool @checked = false) + => @checked ? Expression.MultiplyChecked(left, right) : Expression.Multiply(left, right); /// /// Constructs binary arithmetic division expression. @@ -265,8 +268,8 @@ public static Expression IsNull(this Expression operand) // handle reference type or value type return operand.Type is { IsValueType: false, IsPointer: false, IsPrimitive: false } - ? Expression.ReferenceEqual(operand, Expression.Constant(null, operand.Type)) : - Const(false); + ? Expression.ReferenceEqual(operand, Expression.Constant(null, operand.Type)) + : Const(false); } /// @@ -280,7 +283,7 @@ public static Expression IsNull(this Expression operand) public static Expression IsNotNull(this Expression operand) { // handle nullable value type - Type? underlyingType = Nullable.GetUnderlyingType(operand.Type); + var underlyingType = Nullable.GetUnderlyingType(operand.Type); if (underlyingType is not null) return operand.Property(nameof(Nullable.HasValue)); @@ -292,7 +295,7 @@ public static Expression IsNotNull(this Expression operand) // handle reference type or value type return operand.Type is { IsValueType: false, IsPointer: false, IsPrimitive: false } ? Expression.ReferenceNotEqual(operand, Expression.Constant(null, operand.Type)) - : Const(true); + : Const(true); } /// @@ -327,9 +330,10 @@ public static BinaryExpression LeftShift(this Expression left, Expression right) /// /// The left operand. /// The right operand. + /// to perform unsigned right sift; otherwise, . /// Binary expression. - public static BinaryExpression RightShift(this Expression left, Expression right) - => Expression.RightShift(left, right); + public static Expression RightShift(this Expression left, Expression right, bool isUnsigned = false) + => isUnsigned ? new UnsignedRightShiftExpression(left, right) : Expression.RightShift(left, right); /// /// Constructs an expression that decrements given expression by 1 and assigns the result back to the expression. @@ -1068,7 +1072,7 @@ public static LoopExpression Loop(this Expression body, LabelTarget @break, Labe /// Test expression. /// Positive branch. /// Negative branch. - /// The type of conditional expression. Default is . + /// The type of conditional expression. Default is . /// Conditional expression. public static ConditionalExpression Condition(this Expression test, Expression? ifTrue = null, Expression? ifFalse = null, Type? type = null) => Expression.Condition(test, ifTrue ?? Expression.Empty(), ifFalse ?? Expression.Empty(), type ?? typeof(void)); @@ -1079,7 +1083,7 @@ public static ConditionalExpression Condition(this Expression test, Expression? /// /// The equivalent code is a ? b : c. /// - /// The type of conditional expression. Default is . + /// The type of conditional expression. Default is . /// Test expression. /// Positive branch. /// Negative branch. @@ -1105,7 +1109,7 @@ public static ConditionalExpression Condition(this Expression test, Exp /// The equivalent code is throw e. /// /// An exception to be thrown. - /// The type of expression. Default is . + /// The type of expression. Default is . /// throw expression. public static UnaryExpression Throw(this Expression exception, Type? type = null) => Expression.Throw(exception, type ?? typeof(void)); @@ -1181,7 +1185,7 @@ internal static Expression AddEpilogue(this Expression expression, bool inferTyp } else { - variables = Empty(); + variables = []; result = instructions.Prepend(expression); } diff --git a/src/DotNext.Metaprogramming/Linq/Expressions/LockExpression.cs b/src/DotNext.Metaprogramming/Linq/Expressions/LockExpression.cs index e360d689e3..e15fb5aa51 100644 --- a/src/DotNext.Metaprogramming/Linq/Expressions/LockExpression.cs +++ b/src/DotNext.Metaprogramming/Linq/Expressions/LockExpression.cs @@ -81,7 +81,7 @@ public Expression Body /// /// The new body of the synchronized block of code. /// Updated expression. - public LockExpression Update(Expression body) => new LockExpression(assignment is null ? SyncRoot : assignment.Right) + public LockExpression Update(Expression body) => new(assignment is null ? SyncRoot : assignment.Right) { Body = body, }; diff --git a/src/DotNext.Metaprogramming/Linq/Expressions/UnsignedRightShiftExpression.cs b/src/DotNext.Metaprogramming/Linq/Expressions/UnsignedRightShiftExpression.cs new file mode 100644 index 0000000000..aae9408342 --- /dev/null +++ b/src/DotNext.Metaprogramming/Linq/Expressions/UnsignedRightShiftExpression.cs @@ -0,0 +1,95 @@ +using System.Linq.Expressions; +using System.Numerics; +using System.Reflection; + +namespace DotNext.Linq.Expressions; + +/// +/// Represents unsigned right shift expression. +/// +public sealed class UnsignedRightShiftExpression : CustomExpression +{ + private const string SpecialName = "op_UnsignedRightShift"; + private const BindingFlags Flags = BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Static; + + /// + /// Initializes a new unsigned right shift expression. + /// + /// The left operand. + /// The shift amount. + /// doesn't support unsigned right shift operator. + public UnsignedRightShiftExpression(Expression expr, Expression shiftAmount) + { + Left = expr; + Right = shiftAmount; + + var shiftOperatorInterface = typeof(IShiftOperators<,,>).MakeGenericType(expr.Type, shiftAmount.Type, expr.Type); + if (shiftOperatorInterface.IsAssignableFrom(expr.Type)) + { + var map = expr.Type.GetInterfaceMap(shiftOperatorInterface); + var index = Array.FindIndex(map.InterfaceMethods, static candidate => candidate.Name is SpecialName); + if (index >= 0) + { + Method = map.TargetMethods[index]; + return; + } + } + + Method = expr.GetType().GetMethod(SpecialName, Flags, null, [], null) is { IsSpecialName: true } method + ? method + : throw new ArgumentException(ExceptionMessages.InterfaceNotImplemented(expr.Type, typeof(IShiftOperators<,,>)), nameof(expr)); + } + + /// + /// Represents a method that implements unsigned right shift. + /// + public MethodInfo Method { get; } + + /// + /// Represents left operand. + /// + public Expression Left { get; } + + /// + /// Represents right operand. + /// + public Expression Right { get; } + + /// + protected override UnsignedRightShiftExpression VisitChildren(ExpressionVisitor visitor) + { + var left = visitor.Visit(Left); + var right = visitor.Visit(Right); + + return ReferenceEquals(left, Left) && ReferenceEquals(right, Right) + ? this + : new(left, right); + } + + /// + public override Expression Reduce() + { + if (Right.Type == typeof(int)) + { + switch (Type.GetTypeCode(Left.Type)) + { + case TypeCode.Byte or TypeCode.UInt16 or TypeCode.UInt32 or TypeCode.UInt64: + return RightShift(Left, Right); + case TypeCode.SByte: + return ConvertAndShift(Left, Right); + case TypeCode.Int16: + return ConvertAndShift(Left, Right); + case TypeCode.Int32: + return ConvertAndShift(Left, Right); + case TypeCode.Int64: + return ConvertAndShift(Left, Right); + } + } + + return Call(Method, Left, Right); + + static UnaryExpression ConvertAndShift(Expression left, Expression right) + where T : struct, IBinaryInteger + => Convert(RightShift(Convert(left, typeof(T)), right), left.Type); + } +} \ No newline at end of file diff --git a/src/DotNext.Metaprogramming/Runtime/CompilerServices/AsyncStateMachine.Pooling.cs b/src/DotNext.Metaprogramming/Runtime/CompilerServices/AsyncStateMachine.Pooling.cs index b9ae730fff..13062e9898 100644 --- a/src/DotNext.Metaprogramming/Runtime/CompilerServices/AsyncStateMachine.Pooling.cs +++ b/src/DotNext.Metaprogramming/Runtime/CompilerServices/AsyncStateMachine.Pooling.cs @@ -14,7 +14,7 @@ namespace DotNext.Runtime.CompilerServices; /// /// The local state of async function used to store computation state. [StructLayout(LayoutKind.Auto)] -internal struct PoolingAsyncStateMachine : IAsyncStateMachine +internal struct PoolingAsyncStateMachine : IAsyncStateMachine> where TState : struct { private readonly Transition> transition; @@ -53,7 +53,7 @@ public uint StateId /// /// Enters guarded code block which represents try block of code - /// inside of async lambda function. + /// inside the async lambda function. /// /// The identifier of the async state machine representing guarded code. [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -78,7 +78,7 @@ public void ExitGuardedCode(uint previousState, bool suspendException) /// /// Attempts to recover from the exception and indicating prologue of try statement - /// inside of async lambda function. + /// inside the async lambda function. /// /// Type of expression to be caught. /// Reference to the captured exception. @@ -203,7 +203,7 @@ public static ValueTask Start(TransitionThe local state of async function used to store computation state. /// Result type of asynchronous function. [StructLayout(LayoutKind.Auto)] -internal struct PoolingAsyncStateMachine : IAsyncStateMachine +internal struct PoolingAsyncStateMachine : IAsyncStateMachine, PoolingAsyncStateMachine> where TState : struct { private readonly Transition> transition; diff --git a/src/DotNext.Metaprogramming/Runtime/CompilerServices/AsyncStateMachine.cs b/src/DotNext.Metaprogramming/Runtime/CompilerServices/AsyncStateMachine.cs index b0c5f75dd2..4c97a974ff 100644 --- a/src/DotNext.Metaprogramming/Runtime/CompilerServices/AsyncStateMachine.cs +++ b/src/DotNext.Metaprogramming/Runtime/CompilerServices/AsyncStateMachine.cs @@ -14,7 +14,7 @@ namespace DotNext.Runtime.CompilerServices; /// /// The local state of async function used to store computation state. [StructLayout(LayoutKind.Auto)] -internal struct AsyncStateMachine : IAsyncStateMachine +internal struct AsyncStateMachine : IAsyncStateMachine> where TState : struct { private readonly Transition> transition; @@ -53,7 +53,7 @@ public uint StateId /// /// Enters guarded code block which represents try block of code - /// inside of async lambda function. + /// inside the async lambda function. /// /// The identifier of the async state machine representing guarded code. [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -78,7 +78,7 @@ public void ExitGuardedCode(uint previousState, bool suspendException) /// /// Attempts to recover from the exception and indicating prologue of try statement - /// inside of async lambda function. + /// inside the async lambda function. /// /// Type of expression to be caught. /// Reference to the captured exception. @@ -208,7 +208,7 @@ public static ValueTask Start(Transition> tran /// The local state of async function used to store computation state. /// Result type of asynchronous function. [StructLayout(LayoutKind.Auto)] -internal struct AsyncStateMachine : IAsyncStateMachine +internal struct AsyncStateMachine : IAsyncStateMachine, AsyncStateMachine> where TState : struct { private readonly Transition> transition; @@ -248,7 +248,7 @@ public uint StateId /// /// Enters guarded code block which represents try block of code - /// inside of async lambda function. + /// inside the async lambda function. /// /// The identifier of the async machine state representing guarded code. [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -273,7 +273,7 @@ public void ExitGuardedCode(uint previousState, bool suspendException) /// /// Attempts to recover from the exception and indicating prologue of try statement - /// inside of async lambda function. + /// inside the async lambda function. /// /// Type of expression to be caught. /// Reference to the captured exception. diff --git a/src/DotNext.Metaprogramming/Runtime/CompilerServices/AsyncStateMachineBuilder.cs b/src/DotNext.Metaprogramming/Runtime/CompilerServices/AsyncStateMachineBuilder.cs index fff5d8b77c..1fc6897338 100644 --- a/src/DotNext.Metaprogramming/Runtime/CompilerServices/AsyncStateMachineBuilder.cs +++ b/src/DotNext.Metaprogramming/Runtime/CompilerServices/AsyncStateMachineBuilder.cs @@ -560,7 +560,7 @@ private static MemberExpression[] CreateStateHolderType(Type returnType, bool us builder.Add(type); } - slots = builder.Build(sm.Build, out _); + slots = builder.Build(sm.Build, out _); } Debug.Assert(sm.StateMachine is not null); diff --git a/src/DotNext.Metaprogramming/Runtime/CompilerServices/EnterGuardedCodeExpression.cs b/src/DotNext.Metaprogramming/Runtime/CompilerServices/EnterGuardedCodeExpression.cs index e7e8d0a3d9..79b3b7fe69 100644 --- a/src/DotNext.Metaprogramming/Runtime/CompilerServices/EnterGuardedCodeExpression.cs +++ b/src/DotNext.Metaprogramming/Runtime/CompilerServices/EnterGuardedCodeExpression.cs @@ -4,13 +4,8 @@ namespace DotNext.Runtime.CompilerServices; using static Linq.Expressions.ExpressionBuilder; -internal sealed class EnterGuardedCodeExpression : TransitionExpression +internal sealed class EnterGuardedCodeExpression(uint stateId) : TransitionExpression(stateId) { - internal EnterGuardedCodeExpression(uint stateId) - : base(stateId) - { - } - public override Type Type => typeof(void); public override Expression Reduce() => Empty(); diff --git a/src/DotNext.Metaprogramming/Runtime/CompilerServices/GuardedStatement.cs b/src/DotNext.Metaprogramming/Runtime/CompilerServices/GuardedStatement.cs index b7b66af465..e7704babab 100644 --- a/src/DotNext.Metaprogramming/Runtime/CompilerServices/GuardedStatement.cs +++ b/src/DotNext.Metaprogramming/Runtime/CompilerServices/GuardedStatement.cs @@ -2,13 +2,7 @@ namespace DotNext.Runtime.CompilerServices; -internal abstract class GuardedStatement : Statement +internal abstract class GuardedStatement(Expression expression, LabelTarget faultLabel) : Statement(expression) { - internal readonly LabelTarget FaultLabel; - - private protected GuardedStatement(Expression expression, LabelTarget faultLabel) - : base(expression) - { - FaultLabel = faultLabel; - } + internal readonly LabelTarget FaultLabel = faultLabel; } \ No newline at end of file diff --git a/src/DotNext.Metaprogramming/Runtime/CompilerServices/HasNoExceptionExpression.cs b/src/DotNext.Metaprogramming/Runtime/CompilerServices/HasNoExceptionExpression.cs index a709e7f751..f7c98b2b2c 100644 --- a/src/DotNext.Metaprogramming/Runtime/CompilerServices/HasNoExceptionExpression.cs +++ b/src/DotNext.Metaprogramming/Runtime/CompilerServices/HasNoExceptionExpression.cs @@ -5,7 +5,7 @@ namespace DotNext.Runtime.CompilerServices; using static Linq.Expressions.ExpressionBuilder; /// -/// Represents exception check inside of state machine. +/// Represents exception check inside the state machine. /// internal sealed class HasNoExceptionExpression : StateMachineExpression { diff --git a/src/DotNext.Metaprogramming/Runtime/CompilerServices/IAsyncStateMachine.cs b/src/DotNext.Metaprogramming/Runtime/CompilerServices/IAsyncStateMachine.cs index 248d97362d..1e17c54300 100644 --- a/src/DotNext.Metaprogramming/Runtime/CompilerServices/IAsyncStateMachine.cs +++ b/src/DotNext.Metaprogramming/Runtime/CompilerServices/IAsyncStateMachine.cs @@ -34,11 +34,19 @@ bool TryRecover([NotNullWhen(true)] out TException? exception) where TException : Exception; } +internal interface IAsyncStateMachine : IAsyncStateMachine + where TState : struct + where TValueTask : struct, IEquatable + where TSelf : struct, IAsyncStateMachine +{ + static abstract TValueTask Start(Transition transition, TState initialState); +} + /// /// Represents body of async method in the form of state machine transitions. /// /// The type of async method state. /// The implementation of async state machine. -/// Asyncronous state machine. +/// Asynchronous state machine. internal delegate void Transition(ref TMachine stateMachine) where TMachine : struct, IAsyncStateMachine; \ No newline at end of file diff --git a/src/DotNext.Metaprogramming/Runtime/CompilerServices/MoveNextExpression.cs b/src/DotNext.Metaprogramming/Runtime/CompilerServices/MoveNextExpression.cs index 2e43ceb982..afaff37d3e 100644 --- a/src/DotNext.Metaprogramming/Runtime/CompilerServices/MoveNextExpression.cs +++ b/src/DotNext.Metaprogramming/Runtime/CompilerServices/MoveNextExpression.cs @@ -29,9 +29,9 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) internal override Expression Reduce(ParameterExpression stateMachine) { - const BindingFlags PublicInstanceFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly; + const BindingFlags publicInstanceFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly; var genericParam = Type.MakeGenericMethodParameter(0).MakeByRefType(); - var moveNext = stateMachine.Type.GetMethod(nameof(AsyncStateMachine.MoveNext), 1, PublicInstanceFlags, null, [genericParam, typeof(uint)], null)!.MakeGenericMethod(awaiter.Type); + var moveNext = stateMachine.Type.GetMethod(nameof(AsyncStateMachine.MoveNext), 1, publicInstanceFlags, null, [genericParam, typeof(uint)], null)!.MakeGenericMethod(awaiter.Type); return stateMachine.Call(moveNext, awaiter, StateId); } } \ No newline at end of file diff --git a/src/DotNext.Metaprogramming/Runtime/CompilerServices/PreallocatedCharBuffer.cs b/src/DotNext.Metaprogramming/Runtime/CompilerServices/PreallocatedCharBuffer.cs index dd91ae7b1e..4add6fb1c8 100644 --- a/src/DotNext.Metaprogramming/Runtime/CompilerServices/PreallocatedCharBuffer.cs +++ b/src/DotNext.Metaprogramming/Runtime/CompilerServices/PreallocatedCharBuffer.cs @@ -1,12 +1,14 @@ +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace DotNext.Runtime.CompilerServices; -internal unsafe struct PreallocatedCharBuffer +[InlineArray(BufferSize)] +internal struct PreallocatedCharBuffer { private const int BufferSize = 64; + + private char element0; - private fixed char buffer[BufferSize]; - - public Span Span => MemoryMarshal.CreateSpan(ref buffer[0], BufferSize); + public Span Span => MemoryMarshal.CreateSpan(ref element0, BufferSize); } \ No newline at end of file diff --git a/src/DotNext.Metaprogramming/Threading/Tasks/CompletedTask.cs b/src/DotNext.Metaprogramming/Threading/Tasks/CompletedTask.cs index e31d5dc15a..0ac2538a44 100644 --- a/src/DotNext.Metaprogramming/Threading/Tasks/CompletedTask.cs +++ b/src/DotNext.Metaprogramming/Threading/Tasks/CompletedTask.cs @@ -1,6 +1,4 @@ -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; namespace DotNext.Threading.Tasks; @@ -37,41 +35,31 @@ public static implicit operator ValueTask(CompletedTask task) [StructLayout(LayoutKind.Auto)] internal readonly struct CompletedTask { - private readonly Exception? failure; - - [AllowNull] - private readonly T result; + private readonly Result result; /// /// Creates task that has completed with a specified exception. /// /// The exception with which to complete the task. - public CompletedTask(Exception failure) => this.failure = failure; + public CompletedTask(Exception failure) => result = new(failure); /// /// Creates task that has completed successfully with a specified result. /// /// The task result. - public CompletedTask(T result) => this.result = result; + public CompletedTask(T result) => this.result = new(result); /// /// Obtains completed synchronously. /// /// Completed task. public static implicit operator Task(CompletedTask task) - => task.failure is null ? Task.FromResult(task.result) : Task.FromException(task.failure); + => task.result.AsTask().AsTask(); /// /// Obtains completed synchronously. /// /// Completed task. public static implicit operator ValueTask(CompletedTask task) - { - var builder = AsyncValueTaskMethodBuilder.Create(); - if (task.failure is null) - builder.SetResult(task.result); - else - builder.SetException(task.failure); - return builder.Task; - } + => task.result.AsTask(); } \ No newline at end of file diff --git a/src/DotNext.Tests/Buffers/MemoryWriterTests.cs b/src/DotNext.Tests/Buffers/MemoryWriterTests.cs index 7058b131a0..720fbca08f 100644 --- a/src/DotNext.Tests/Buffers/MemoryWriterTests.cs +++ b/src/DotNext.Tests/Buffers/MemoryWriterTests.cs @@ -113,7 +113,7 @@ public static void ReadWriteUsingArray() { using var writer = new PoolingArrayBufferWriter { Capacity = 25 }; True(writer.Capacity >= 25); - True(writer.WrittenArray.Count == 0); + Equal(0, writer.WrittenArray.Count); Equal(0, writer.WrittenCount); var memory = writer.GetArray(100); @@ -158,13 +158,13 @@ public static async Task StressTest() using var writer = new PoolingArrayBufferWriter(); // serialize dictionary to memory - using (var output = StreamSource.AsStream(writer)) + await using (var output = writer.AsStream()) { await DictionarySerializer.SerializeAsync(dict, output, buffer); } // deserialize from memory - using (var input = StreamSource.AsStream(writer.WrittenArray)) + await using (var input = writer.WrittenArray.AsStream()) { Equal(dict, await DictionarySerializer.DeserializeAsync(input, buffer)); } @@ -225,7 +225,7 @@ public static void WriterAsReadOnlyCollection() Throws(() => collection[1]); Equal(42, Single(collection)); - writer.AddAll(new[] { 43, 44 }); + writer.AddAll([43, 44]); Equal(3, writer.WrittenCount); Equal(3, collection.Count); Equal(42, collection[0]); @@ -233,7 +233,7 @@ public static void WriterAsReadOnlyCollection() Equal(44, collection[2]); Throws(() => collection[3]); Equal(3, Enumerable.Count(collection)); - Equal(new[] { 42, 43, 44 }, Enumerable.ToArray(collection)); + Equal([42, 43, 44], collection); } [Fact] @@ -296,7 +296,7 @@ public static void WriterAsList() [Fact] public static void Insertion() { - Span block = stackalloc byte[] { 10, 20, 30 }; + Span block = [10, 20, 30]; using var writer = new PoolingArrayBufferWriter(); writer.Insert(0, block); @@ -327,14 +327,14 @@ public static void Insertion() block[2] = 120; writer.Insert(writer.WrittenCount - 1, block); - Equal(random[0..(random.Length - 1)], writer.WrittenMemory.Span.Slice(0, random.Length - 1).ToArray()); + Equal(random[..^1], writer.WrittenMemory.Span.Slice(0, random.Length - 1).ToArray()); Equal(block.ToArray(), writer.WrittenMemory.Span.Slice(random.Length - 1, 3).ToArray()); } [Fact] public static void Overwrite() { - Span block = stackalloc byte[] { 10, 20, 30 }; + Span block = [10, 20, 30]; using var writer = new PoolingArrayBufferWriter(); writer.Overwrite(0, block); diff --git a/src/DotNext.Tests/ComponentModel/DataAnnotations/OptionalStringLengthAttributeTests.cs b/src/DotNext.Tests/ComponentModel/DataAnnotations/OptionalStringLengthAttributeTests.cs index d8787c01c1..d54dee54cc 100644 --- a/src/DotNext.Tests/ComponentModel/DataAnnotations/OptionalStringLengthAttributeTests.cs +++ b/src/DotNext.Tests/ComponentModel/DataAnnotations/OptionalStringLengthAttributeTests.cs @@ -7,7 +7,7 @@ public sealed class OptionalStringLengthAttributeTests : Test public sealed class DataModel { [OptionalStringLength(100, MinimumLength = 5)] - [Required] + [Required(AllowNull = false)] public Optional StringProperty { get; set; } } diff --git a/src/DotNext.Tests/DelegateHelpersTests.cs b/src/DotNext.Tests/DelegateHelpersTests.cs index 14ced299d3..fbcbd0f2d5 100644 --- a/src/DotNext.Tests/DelegateHelpersTests.cs +++ b/src/DotNext.Tests/DelegateHelpersTests.cs @@ -196,8 +196,24 @@ public static void ConstantProvider() Same(Func.Constant(true), Func.Constant(true)); Same(Func.Constant(false), Func.Constant(false)); - Equal(42, Func.Constant(42).Invoke()); - Equal("Hello, world", Func.Constant("Hello, world").Invoke()); + Equal(42, Func.Constant(42).Invoke()); + Equal("Hello, world", Func.Constant("Hello, world").Invoke()); + } + + [Fact] + public static void ValueTypeConst() + { + const long value = 42L; + var provider = Func.Constant(value); + Equal(value, provider.Target); + } + + [Fact] + public static void StringConst() + { + const string value = "Hello, world"; + var provider = Func.Constant(value); + Same(value, provider.Target); } [Fact] diff --git a/src/DotNext.Tests/IO/PoolingBufferedStreamTests.cs b/src/DotNext.Tests/IO/PoolingBufferedStreamTests.cs index 331e3d0c59..7e68bdf996 100644 --- a/src/DotNext.Tests/IO/PoolingBufferedStreamTests.cs +++ b/src/DotNext.Tests/IO/PoolingBufferedStreamTests.cs @@ -373,4 +373,20 @@ public static async Task ReadFromFileAsync() Equal(expected, actual); } + + [Fact] + public static void OverwriteStream() + { + using var buffered = new PoolingBufferedStream(new MemoryStream(), leaveOpen: false); + Equal(0L, buffered.Position); + + buffered.Write("text"u8); + Equal(4L, buffered.Position); + + buffered.Position = 0; + Equal(0L, buffered.Position); + + buffered.Write("text"u8); + Equal(4L, buffered.Position); + } } \ No newline at end of file diff --git a/src/DotNext.Tests/Linq/Expressions/ExpressionBuilderTests.cs b/src/DotNext.Tests/Linq/Expressions/ExpressionBuilderTests.cs index e778cca7af..08f90aa1c0 100644 --- a/src/DotNext.Tests/Linq/Expressions/ExpressionBuilderTests.cs +++ b/src/DotNext.Tests/Linq/Expressions/ExpressionBuilderTests.cs @@ -192,15 +192,15 @@ public static void GotoLabel() var site = label.LandingSite(42.Const()); Equal(ExpressionType.Label, site.NodeType); - Equal(ExpressionType.Constant, site.DefaultValue.NodeType); + Equal(ExpressionType.Constant, site.DefaultValue?.NodeType); } [Fact] public static void ArrayElement() { - var indexer = new int[0].Const().ElementAt(1.Const()); + var indexer = Array.Empty().Const().ElementAt(1.Const()); Equal(ExpressionType.Index, indexer.NodeType); - Equal(ExpressionType.Constant, indexer.Object.NodeType); + Equal(ExpressionType.Constant, indexer.Object?.NodeType); } [Fact] @@ -220,7 +220,7 @@ public static void BinaryOperations() expr = 42.Const().LeftShift(2.Const()); Equal(ExpressionType.LeftShift, expr.NodeType); - expr = 42.Const().RightShift(2.Const()); + expr = (BinaryExpression)42.Const().RightShift(2.Const()); Equal(ExpressionType.RightShift, expr.NodeType); expr = 42.Const().LessThanOrEqual(43.Const()); @@ -321,7 +321,7 @@ public static void WithObject() [Fact] public static void ForEachLoop() { - var expr = new int[3].Const().ForEach((current, continueLabel, breakLabel) => + var expr = new int[3].Const().ForEach((current, _, _) => { Equal(typeof(int), current.Type); Equal(ExpressionType.MemberAccess, current.NodeType); @@ -336,8 +336,8 @@ public static void ForEachLoop() [Fact] public static void ItemIndex() { - const short IndexValue = 10; - var index = new ItemIndexExpression(IndexValue.Const()); + const short indexValue = 10; + var index = new ItemIndexExpression(indexValue.Const()); False(index.IsFromEnd); Equal(ExpressionType.New, index.Reduce().NodeType); Equal(typeof(Index), index.Type); @@ -424,7 +424,7 @@ public static void ListSlice() { var parameter = Expression.Parameter(typeof(List)); var lambda = Expression.Lambda, List>>(parameter.Slice(1.Index(false), 1.Index(true)), parameter).Compile(); - Equal(new[] { 3L, 5L }, lambda(new List { 1L, 3L, 5L, 7L })); + Equal(new[] { 3L, 5L }, lambda([1L, 3L, 5L, 7L])); } [Fact] @@ -490,8 +490,8 @@ public static void ConvertToNullable() var lambda = Expression.Lambda>(2.Const().AsNullable()).Compile(); Equal(2, lambda()); - var lambda2 = Expression.Lambda>(2.Const().AsNullable()).Compile(); - Equal(2, lambda()); + var lambda2 = Expression.Lambda>(new int?(2).Const().AsNullable()).Compile(); + Equal(2, lambda2()); var lambda3 = Expression.Lambda>("Hello, world!".Const().AsNullable()).Compile(); Equal("Hello, world!", lambda3()); @@ -500,8 +500,8 @@ public static void ConvertToNullable() [Fact] public static void ConvertToOptional() { - var lamdba = Expression.Lambda>>(2.Const().AsOptional()).Compile(); - Equal(2, lamdba()); + var lambda = Expression.Lambda>>(2.Const().AsOptional()).Compile(); + Equal(2, lambda()); } [Fact] @@ -604,7 +604,7 @@ public static void InitExpression() Contains(initialization.Bindings, static item => nameof(UriBuilder.Scheme) == item.Member.Name); } - public record class RecordClass(int A); + private record RecordClass(int A); [Fact] public static void MutateRecordClass1() @@ -633,7 +633,7 @@ public static void MutateRecordClass2() Equal(typeof(RecordClass), mut.Reduce().Type); } - public record struct RecordStruct(int A); + private record struct RecordStruct(int A); [Fact] public static void MutateRecordStruct() @@ -658,4 +658,29 @@ public static void MutateRegularStruct() Contains(mut.Bindings, static item => nameof(Net.Cluster.Consensus.Raft.Result.Value) == item.Member.Name); Equal(typeof(Net.Cluster.Consensus.Raft.Result), mut.Reduce().Type); } + + [Theory] + [InlineData(typeof(byte), typeof(BinaryExpression))] + [InlineData(typeof(ushort), typeof(BinaryExpression))] + [InlineData(typeof(uint), typeof(BinaryExpression))] + [InlineData(typeof(ulong), typeof(BinaryExpression))] + [InlineData(typeof(sbyte), typeof(UnaryExpression))] + [InlineData(typeof(short), typeof(UnaryExpression))] + [InlineData(typeof(int), typeof(UnaryExpression))] + [InlineData(typeof(long), typeof(UnaryExpression))] + [InlineData(typeof(nint), typeof(MethodCallExpression))] + [InlineData(typeof(nuint), typeof(MethodCallExpression))] + [InlineData(typeof(Int128), typeof(MethodCallExpression))] + [InlineData(typeof(UInt128), typeof(MethodCallExpression))] + public static void UnsignedRightShift(Type primitiveType, Type expressionType) + { + var expr = new UnsignedRightShiftExpression(Expression.Default(primitiveType), 2.Const()); + IsAssignableFrom(expr.Left); + IsAssignableFrom(expr.Right); + True(expr.Method.IsStatic); + + var reduced = expr.Reduce(); + IsAssignableFrom(expressionType, reduced); + Same(primitiveType, reduced.Type); + } } \ No newline at end of file diff --git a/src/DotNext.Tests/Metaprogramming/LambdaTests.cs b/src/DotNext.Tests/Metaprogramming/LambdaTests.cs index 133b6ac69e..9ebe78bc7f 100644 --- a/src/DotNext.Tests/Metaprogramming/LambdaTests.cs +++ b/src/DotNext.Tests/Metaprogramming/LambdaTests.cs @@ -98,13 +98,12 @@ public static void SimpleAsyncLambda(bool usePooling) } [Theory] - [InlineData(false)] - [InlineData(true)] - public static void SimpleUntypedAsyncLambda(bool usePooling) + [InlineData(AsyncLambdaFlags.None)] + [InlineData(AsyncLambdaFlags.UseTaskPooling)] + public static void SimpleUntypedAsyncLambda(AsyncLambdaFlags flags) { var sumMethod = typeof(LambdaTests).GetMethod(nameof(Sum), BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.DeclaredOnly); - var flags = usePooling ? AsyncLambdaFlags.UseTaskPooling : AsyncLambdaFlags.None; - var lambda = AsyncLambda(new[] { typeof(long), typeof(long) }, typeof(long), flags, fun => + var lambda = AsyncLambda([typeof(long), typeof(long)], typeof(long), flags, fun => { var (arg1, arg2) = fun; var temp = DeclareVariable("tmp"); diff --git a/src/DotNext.Tests/Net/Cluster/Consensus/Raft/MemoryBasedStateMachineTests.cs b/src/DotNext.Tests/Net/Cluster/Consensus/Raft/MemoryBasedStateMachineTests.cs index 79cfbf283b..04daf025d3 100644 --- a/src/DotNext.Tests/Net/Cluster/Consensus/Raft/MemoryBasedStateMachineTests.cs +++ b/src/DotNext.Tests/Net/Cluster/Consensus/Raft/MemoryBasedStateMachineTests.cs @@ -182,7 +182,7 @@ public static async Task QueryAppendEntries(long partitionSize, bool caching, in Equal(2, entries.Count); Equal(0L, entries.First().Term); // element 0 Equal(42L, entries.Skip(1).First().Term); // element 1 - Equal(entry1.Content, await entries[1].ToStringAsync(Encoding.UTF8)); + Equal(entry1.Content, await entries[1].ToStringAsync(Encoding.UTF8, token: token)); Equal(entry1.Context, IsAssignableFrom(entries[1]).Context); return Missing.Value; }; @@ -196,7 +196,7 @@ public static async Task QueryAppendEntries(long partitionSize, bool caching, in Null(snapshotIndex); Single(entries); Equal(43L, entries[0].Term); - Equal(entry2.Content, await entries[0].ToStringAsync(Encoding.UTF8)); + Equal(entry2.Content, await entries[0].ToStringAsync(Encoding.UTF8, token: token)); return Missing.Value; }; @@ -263,10 +263,10 @@ public static async Task AppendWhileReading(long? maxLogEntrySize) Equal(0L, entries[0].Term); Equal(42L, entries[1].Term); - Equal(entry.Content, await entries[1].ToStringAsync(Encoding.UTF8)); + Equal(entry.Content, await entries[1].ToStringAsync(Encoding.UTF8, token: token)); // append a new log entry - return await state.AppendAsync(new TestLogEntry("SET Y = 42") { Term = 43L }); + return await state.AppendAsync(new TestLogEntry("SET Y = 42") { Term = 43L }, token: token); }; var index = await state.ReadAsync(new IO.Log.LogEntryConsumer(checker), 0L); @@ -326,7 +326,7 @@ public static async Task Overwrite(long? maxLogEntrySize) Null(snapshotIndex); Single(entries); False(entries[0].IsSnapshot); - Equal(entry1.Content, await entries[0].ToStringAsync(Encoding.UTF8)); + Equal(entry1.Content, await entries[0].ToStringAsync(Encoding.UTF8, token: token)); return Missing.Value; }; await state.As().ReadAsync(new LogEntryConsumer(checker), 1L, CancellationToken.None); @@ -354,7 +354,7 @@ public static async Task OverwriteUnsealed() Null(snapshotIndex); Single(entries); False(entries[0].IsSnapshot); - Equal(entry2.Content, await entries[0].ToStringAsync(Encoding.UTF8)); + Equal(entry2.Content, await entries[0].ToStringAsync(Encoding.UTF8, token: token)); return Missing.Value; }; await state.As().ReadAsync(new LogEntryConsumer(checker), 1L, CancellationToken.None); @@ -392,7 +392,7 @@ public static async Task LegacyOverwrite() Null(snapshotIndex); Single(entries); False(entries[0].IsSnapshot); - Equal(entry1.Content, await entries[0].ToStringAsync(Encoding.UTF8)); + Equal(entry1.Content, await entries[0].ToStringAsync(Encoding.UTF8, token: token)); return Missing.Value; }; await state.As().ReadAsync(new LogEntryConsumer(checker), 1L, CancellationToken.None); @@ -434,21 +434,21 @@ public static async Task PartitionOverflow(bool useCaching) False(entries[0].IsSnapshot); Equal(0L, entries[0].Term); Equal(42L, entries[1].Term); - Equal(entry1.Content, await entries[1].ToStringAsync(Encoding.UTF8)); + Equal(entry1.Content, await entries[1].ToStringAsync(Encoding.UTF8, token: token)); Equal(entry1.Timestamp, entries[1].Timestamp); Equal(43L, entries[2].Term); - Equal(entry2.Content, await entries[2].ToStringAsync(Encoding.UTF8)); + Equal(entry2.Content, await entries[2].ToStringAsync(Encoding.UTF8, token: token)); Equal(entry2.Timestamp, entries[2].Timestamp); Equal(44L, entries[3].Term); - Equal(entry3.Content, await entries[3].ToStringAsync(Encoding.UTF8)); + Equal(entry3.Content, await entries[3].ToStringAsync(Encoding.UTF8, token: token)); Equal(entry3.Timestamp, entries[3].Timestamp); Equal(45L, entries[4].Term); - Equal(entry4.Content, await entries[4].ToStringAsync(Encoding.UTF8)); + Equal(entry4.Content, await entries[4].ToStringAsync(Encoding.UTF8, token: token)); Equal(entry4.Timestamp, entries[4].Timestamp); Equal(46L, entries[5].Term); - Equal(entry5.Content, await entries[5].ToStringAsync(Encoding.UTF8)); + Equal(entry5.Content, await entries[5].ToStringAsync(Encoding.UTF8, token: token)); Equal(entry5.Timestamp, entries[5].Timestamp); - return default; + return null; }; await state.ReadAsync(new LogEntryConsumer(checker), 0L, CancellationToken.None); } @@ -468,19 +468,19 @@ public static async Task PartitionOverflow(bool useCaching) Equal(6, entries.Count); Equal(0L, entries[0].Term); Equal(42L, entries[1].Term); - Equal(entry1.Content, await entries[1].ToStringAsync(Encoding.UTF8)); + Equal(entry1.Content, await entries[1].ToStringAsync(Encoding.UTF8, token: token)); Equal(entry1.Timestamp, entries[1].Timestamp); Equal(43L, entries[2].Term); - Equal(entry2.Content, await entries[2].ToStringAsync(Encoding.UTF8)); + Equal(entry2.Content, await entries[2].ToStringAsync(Encoding.UTF8, token: token)); Equal(entry2.Timestamp, entries[2].Timestamp); Equal(44L, entries[3].Term); - Equal(entry3.Content, await entries[3].ToStringAsync(Encoding.UTF8)); + Equal(entry3.Content, await entries[3].ToStringAsync(Encoding.UTF8, token: token)); Equal(entry3.Timestamp, entries[3].Timestamp); Equal(45L, entries[4].Term); - Equal(entry4.Content, await entries[4].ToStringAsync(Encoding.UTF8)); + Equal(entry4.Content, await entries[4].ToStringAsync(Encoding.UTF8, token: token)); Equal(entry4.Timestamp, entries[4].Timestamp); Equal(46L, entries[5].Term); - Equal(entry5.Content, await entries[5].ToStringAsync(Encoding.UTF8)); + Equal(entry5.Content, await entries[5].ToStringAsync(Encoding.UTF8, token: token)); Equal(entry5.Timestamp, entries[5].Timestamp); return Missing.Value; }; @@ -541,7 +541,7 @@ public static async Task SnapshotInstallation(bool useCaching) Equal(3, await state.CommitAsync(3, CancellationToken.None)); //install snapshot and erase all existing entries up to 7th (inclusive) await state.AppendAsync(new Int64LogEntry { Content = 100500L, IsSnapshot = true, Term = 0L }, 7); - checker = static (readResult, snapshotIndex, token) => + checker = static (readResult, snapshotIndex, _) => { Equal(3, readResult.Count); Equal(7, snapshotIndex); @@ -567,7 +567,7 @@ public static async Task SnapshotInstallation(bool useCaching) }; await state.As().ReadAsync(new LogEntryConsumer(checker) { LogEntryMetadataOnly = true }, 6, 9, CancellationToken.None); await state.AppendAsync(new Int64LogEntry { Content = 90L, IsSnapshot = true, Term = 0L }, 11); - checker = static (readResult, snapshotIndex, token) => + checker = static (readResult, snapshotIndex, _) => { Single(readResult); Equal(11, snapshotIndex); @@ -586,7 +586,7 @@ public static async Task RewriteLogEntry() Equal(1L, await state.AppendAsync(new TestLogEntry("SET X = 0") { Term = 42L }, true)); await state.AppendAsync(new EmptyLogEntry { Term = 43L }, 1L); - Func, long?, CancellationToken, ValueTask> checker = static (readResult, snapshotIndex, token) => + Func, long?, CancellationToken, ValueTask> checker = static (readResult, snapshotIndex, _) => { Null(snapshotIndex); NotEmpty(readResult); @@ -607,7 +607,7 @@ public static async Task ClearLog() Equal(3, await state.CommitAsync(3, CancellationToken.None)); //install snapshot and erase all existing entries up to 7th (inclusive) await state.AppendAsync(new Int64LogEntry { Content = 100500L, IsSnapshot = true, Term = 0L }, 7); - checker = static (readResult, snapshotIndex, token) => + checker = static (readResult, snapshotIndex, _) => { Equal(3, readResult.Count); Equal(7, snapshotIndex); @@ -736,7 +736,7 @@ public static async Task BackgroundCompaction(bool useCaching) return default; }; await state.As().ReadAsync(new LogEntryConsumer(checker), 1, 6, CancellationToken.None); - checker = static (readResult, snapshotIndex, token) => + checker = static (readResult, snapshotIndex, _) => { Equal(7, readResult.Count); Equal(3, snapshotIndex); @@ -751,7 +751,7 @@ public static async Task BackgroundCompaction(bool useCaching) //read again using (var state = new PersistentStateWithSnapshot(dir, useCaching)) { - checker = static (readResult, snapshotIndex, token) => + checker = static (readResult, snapshotIndex, _) => { Equal(4, readResult.Count); NotNull(snapshotIndex); @@ -759,7 +759,7 @@ public static async Task BackgroundCompaction(bool useCaching) }; await state.As().ReadAsync(new LogEntryConsumer(checker), 1, 6, CancellationToken.None); Equal(0L, state.Value); - checker = static (readResult, snapshotIndex, token) => + checker = static (readResult, snapshotIndex, _) => { Equal(7, readResult.Count); Equal(3, snapshotIndex); @@ -802,7 +802,7 @@ public static async Task ForegroundCompaction(bool useCaching) //read again using (var state = new PersistentStateWithSnapshot(dir, useCaching)) { - checker = static (readResult, snapshotIndex, token) => + checker = static (readResult, snapshotIndex, _) => { Equal(4, readResult.Count); NotNull(snapshotIndex); @@ -810,7 +810,7 @@ public static async Task ForegroundCompaction(bool useCaching) }; await state.As().ReadAsync(new LogEntryConsumer(checker), 1, 6, CancellationToken.None); Equal(0L, state.Value); - checker = static (readResult, snapshotIndex, token) => + checker = static (readResult, snapshotIndex, _) => { Equal(7, readResult.Count); Equal(3, snapshotIndex); @@ -836,7 +836,7 @@ public static async Task IncrementalCompaction(bool useCaching) await state.CommitAsync(5, CancellationToken.None); await state.CommitAsync(CancellationToken.None); Equal(entries.Length + 41L, state.Value); - checker = static (readResult, snapshotIndex, token) => + checker = static (readResult, snapshotIndex, _) => { Equal(2, readResult.Count); Equal(5, snapshotIndex); @@ -849,7 +849,7 @@ public static async Task IncrementalCompaction(bool useCaching) //read again using (var state = new PersistentStateWithSnapshot(dir, useCaching)) { - checker = static (readResult, snapshotIndex, token) => + checker = static (readResult, snapshotIndex, _) => { Equal(2, readResult.Count); NotNull(snapshotIndex); @@ -909,7 +909,7 @@ public static async Task RestoreBackup() { Equal(5, state.LastEntryIndex); Equal(2, state.LastCommittedEntryIndex); - Func, long?, CancellationToken, ValueTask> checker = (entries, snapshotIndex, token) => + Func, long?, CancellationToken, ValueTask> checker = (entries, snapshotIndex, _) => { Equal(entry1.Term, entries[0].Term); Equal(entry2.Term, entries[1].Term); @@ -1064,27 +1064,29 @@ public static async Task EnsureMetadataPersistence() var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); using (var state = new PersistentStateWithoutSnapshot(path, RecordsPerPartition, new() { UseCaching = true })) { - IReadOnlyList entries = + IReadOnlyList entries = [ - new() { Term = 1L, Content = 10L }, - new() { Term = 1L, Content = 11L } + new() { Term = 1L, Content = BitConverter.GetBytes(10L) }, + new() { Term = 1L, Content = BitConverter.GetBytes(43L) } ]; foreach (var entry in entries) { await state.AppendAsync(entry); } + await state.AppendAsync(new BinaryLogEntry> { Content = new() { Value = 42 }, Term = 2L }); await state.CommitAsync(); } using (var state = new PersistentStateWithoutSnapshot(path, RecordsPerPartition, new() { UseCaching = true })) { - Func, long?, CancellationToken, ValueTask> checker = (entries, snapshotIndex, token) => + Func, long?, CancellationToken, ValueTask> checker = (entries, snapshotIndex, _) => { NotEmpty(entries); Null(snapshotIndex); Equal(1L, entries[0].Term); Equal(1L, entries[1].Term); + Equal(2L, entries[2].Term); return ValueTask.FromResult(Missing.Value); }; diff --git a/src/DotNext.Tests/Net/Http/HttpEndPointTests.cs b/src/DotNext.Tests/Net/Http/HttpEndPointTests.cs index ce142f8d01..11786e3af4 100644 --- a/src/DotNext.Tests/Net/Http/HttpEndPointTests.cs +++ b/src/DotNext.Tests/Net/Http/HttpEndPointTests.cs @@ -1,6 +1,5 @@ using System.Net.Sockets; using System.Text; -using Microsoft.AspNetCore.Components.Forms; namespace DotNext.Net.Http; @@ -108,4 +107,13 @@ static bool TryParse(string input, out T result) where T : IParsable => T.TryParse(input, provider: null, out result); } + + [Theory] + [InlineData("192.168.0.1", AddressFamily.InterNetwork)] + [InlineData("2001:0db8:0000:0000:0000:8a2e:0370:7334", AddressFamily.InterNetworkV6)] + [InlineData("host", AddressFamily.Unspecified)] + public static void AutoDetectAddressFamily(string hostName, AddressFamily expected) + { + Equal(expected, new HttpEndPoint(hostName, 8080, false).AddressFamily); + } } \ No newline at end of file diff --git a/src/DotNext.Tests/OptionalTests.cs b/src/DotNext.Tests/OptionalTests.cs index 20ea00979e..554942ed07 100644 --- a/src/DotNext.Tests/OptionalTests.cs +++ b/src/DotNext.Tests/OptionalTests.cs @@ -321,4 +321,14 @@ public static void OptionalToDelegate() functional = Optional.None(); Null(functional.ToDelegate().Invoke()); } + + [Fact] + public static void ConcatTwoValues() + { + Optional<(int, long)> optional = new Optional(42).Concat(43L); + Equal((42, 43L), optional.Value); + + optional = Optional.None.Concat(42L); + False(optional.HasValue); + } } \ No newline at end of file diff --git a/src/DotNext.Tests/Reflection/TypeExtensionsTests.cs b/src/DotNext.Tests/Reflection/TypeExtensionsTests.cs index c960fcd56c..0efbeda82b 100644 --- a/src/DotNext.Tests/Reflection/TypeExtensionsTests.cs +++ b/src/DotNext.Tests/Reflection/TypeExtensionsTests.cs @@ -1,13 +1,8 @@ -using System.Reflection; - namespace DotNext.Reflection; public sealed class TypeExtensionsTests : Test { - public sealed class MyList : List - { - - } + public sealed class MyList : List; [Fact] public static void DelegateSignature() @@ -18,6 +13,13 @@ public static void DelegateSignature() Equal(typeof(string), signature.ReturnParameter.ParameterType); } + [Fact] + public static void InvokeInvalidDelegate() + { + var ex = ThrowsAny(static () => DelegateType.GetInvokeMethod()); + Same(typeof(MulticastDelegate), ex.Argument); + } + [Fact] public static void IsGenericInstanceOf() { @@ -72,7 +74,7 @@ public static void IsUnmanaged() } [Fact] - public static unsafe void IsImmutable() + public static void IsImmutable() { True(typeof(ReadOnlySpan).IsImmutable()); True(typeof(Guid).IsImmutable()); @@ -119,7 +121,7 @@ public static void EqualsMethodResolution() } [Fact] - public static unsafe void DefaultValues() + public static void DefaultValues() { Null(typeof(string).GetDefaultValue()); Null(typeof(void*).GetDefaultValue()); diff --git a/src/DotNext.Tests/ResultTests.cs b/src/DotNext.Tests/ResultTests.cs index 2f0192401b..cf1ca9ad12 100644 --- a/src/DotNext.Tests/ResultTests.cs +++ b/src/DotNext.Tests/ResultTests.cs @@ -125,7 +125,7 @@ public static void OptionalInterop() result = (Result)new Optional("Hello, world!"); True(result.IsSuccessful); Equal("Hello, world!", result.Value); - Equal("Hello, world!", Optional.Create>(result)); + Equal("Hello, world!", Optional.Create(result)); } [Fact] @@ -135,7 +135,7 @@ public static void OptionalInterop2() Optional opt = result; Equal("Hello, world!", opt); - opt = Optional.Create>(result); + opt = Optional.Create(result); Equal("Hello, world!", opt); result = new(EnvironmentVariableTarget.Machine); @@ -225,4 +225,27 @@ public static void ResultToDelegate() private static TResult FromError(TError error) where TResult : struct, IResultMonad => TResult.FromError(error); + + [Fact] + public static async Task ConvertToTask() + { + Result result = 42; + Equal(42, await (ValueTask)result); + + result = Result.FromException(new OperationCanceledException(new CancellationToken(canceled: true))); + True(result.AsTask().IsCanceled); + + result = Result.FromException(new Exception()); + await ThrowsAsync(result.AsTask().AsTask); + } + + [Fact] + public static void ValueRef() + { + Result result = 42; + Equal(42, result.ValueRef); + + result = Result.FromException(new ArithmeticException()); + Throws(() => result.ValueRef); + } } \ No newline at end of file diff --git a/src/DotNext.Tests/Runtime/Caching/RandomAccessCacheTests.cs b/src/DotNext.Tests/Runtime/Caching/RandomAccessCacheTests.cs index ee4e4ec02c..3c69097ffd 100644 --- a/src/DotNext.Tests/Runtime/Caching/RandomAccessCacheTests.cs +++ b/src/DotNext.Tests/Runtime/Caching/RandomAccessCacheTests.cs @@ -134,8 +134,8 @@ public static void AddRemove() writeSession.SetValue("10"); } - False(cache.TryRemove(11L, DefaultTimeout, out _)); - True(cache.TryRemove(10L, DefaultTimeout, out var session)); + False(cache.TryRemove(11L, out _, DefaultTimeout)); + True(cache.TryRemove(10L, out var session, DefaultTimeout)); using (session) { diff --git a/src/DotNext.Tests/Runtime/ValueReferenceTests.cs b/src/DotNext.Tests/Runtime/ValueReferenceTests.cs index ed278d1f89..53c313bc1e 100644 --- a/src/DotNext.Tests/Runtime/ValueReferenceTests.cs +++ b/src/DotNext.Tests/Runtime/ValueReferenceTests.cs @@ -85,6 +85,7 @@ public static void MutableEmptyRef() var reference = default(ValueReference); True(reference.IsEmpty); Null(reference.ToString()); + Throws(() => reference.Value); Span span = reference; True(span.IsEmpty); @@ -99,6 +100,7 @@ public static void ImmutableEmptyRef() var reference = default(ReadOnlyValueReference); True(reference.IsEmpty); Null(reference.ToString()); + Throws(() => reference.Value); ReadOnlySpan span = reference; True(span.IsEmpty); @@ -207,9 +209,7 @@ public static void ReadOnlySpanInterop() True(Unsafe.AreSame(in reference.Value, in span[0])); } - - - private record class MyClass : IResettable + private record MyClass : IResettable { internal static string StaticObject; @@ -224,4 +224,19 @@ public virtual void Reset() } } + + [Fact] + public static unsafe void PinAnonymousValue() + { + ValueReference valueRef = new(42); + fixed (int* ptr = valueRef) + { + Equal(42, *ptr); + } + + fixed (int* ptr = (ReadOnlyValueReference)valueRef) + { + Equal(42, *ptr); + } + } } \ No newline at end of file diff --git a/src/DotNext.Tests/SpanTests.cs b/src/DotNext.Tests/SpanTests.cs index b90a5e5a3d..298476a026 100644 --- a/src/DotNext.Tests/SpanTests.cs +++ b/src/DotNext.Tests/SpanTests.cs @@ -469,6 +469,14 @@ public static void SwapElements() Equal(expected.Slice(0, midpoint), actual.Slice(midpoint)); } + [Fact] + public static void AlignedAllocation() + { + using var owner = new SpanOwner(64); + owner.Span[0] = UInt128.One; + Equal(UInt128.One, owner.Span[0]); + } + [Fact] public static void TransformElements() { diff --git a/src/DotNext.Tests/Text/Encodings/Web/FileUriTests.cs b/src/DotNext.Tests/Text/Encodings/Web/FileUriTests.cs new file mode 100644 index 0000000000..09aaa41cba --- /dev/null +++ b/src/DotNext.Tests/Text/Encodings/Web/FileUriTests.cs @@ -0,0 +1,76 @@ +using System.Text.Encodings.Web; + +namespace DotNext.Text.Encodings.Web; + +public sealed class FileUriTests : Test +{ + public static TheoryData GetPaths() => new() + { + // Windows path + { @"C:\without\whitespace", @"C:\without\whitespace" }, + { @"C:\with whitespace", @"C:\with whitespace" }, + { @"C:\with\trailing\backslash", @"C:\with\trailing\backslash" }, + { @"C:\with\trailing\backslash and space", @"C:\with\trailing\backslash and space" }, + { @"C:\with\..\relative\.\components\", @"C:\relative\components\" }, + { @"C:\with\specials\chars\#\$\", @"C:\with\specials\chars\#\$\" }, + { @"C:\с\кириллицей", @"C:\с\кириллицей" }, + { @"C:\ελληνικά\γράμματα", @"C:\ελληνικά\γράμματα" }, + { @"\\unc\path", @"\\unc\path" }, + + // Unix path + { "/without/whitespace", "/without/whitespace" }, + { "/with whitespace", "/with whitespace" }, + { "/with/trailing/slash/", "/with/trailing/slash/" }, + { "/with/trailing/slash and space/", "/with/trailing/slash and space/" }, + { "/with/../relative/./components/", "/relative/components/" }, + { "/with/special/chars/?/>// buffer = stackalloc char[512]; + True(FileUri.TryCreateFromFileName(fileName, UrlEncoder.Default, buffer, out var charsWritten)); + + var uri = new Uri(buffer.Slice(0, charsWritten).ToString(), UriKind.Absolute); + Equal(expected, uri.LocalPath); + } + + [Fact] + public static void MaxEncodedLength() + { + const string path = "/some/path"; + True(FileUri.GetMaxEncodedLength(path) > path.Length); + } + + [Theory] + [InlineData("~/path/name")] + [InlineData("C:path\\name")] + [InlineData("../path")] + [InlineData("./path")] + [InlineData("")] + public static void CheckFullyQualifiedPath(string path) + { + Throws(() => FileUri.CreateFromFileName(path)); + } +} \ No newline at end of file diff --git a/src/DotNext.Tests/Threading/AsyncExclusiveLockTests.cs b/src/DotNext.Tests/Threading/AsyncExclusiveLockTests.cs index 371deb794c..876fcd931d 100644 --- a/src/DotNext.Tests/Threading/AsyncExclusiveLockTests.cs +++ b/src/DotNext.Tests/Threading/AsyncExclusiveLockTests.cs @@ -186,7 +186,7 @@ public static void SynchronousLock() using var l = new AsyncExclusiveLock(); True(l.TryAcquire(DefaultTimeout)); - False(l.TryAcquire(TimeSpan.Zero)); + False(l.TryAcquire()); } [Fact] diff --git a/src/DotNext.Tests/Threading/AtomicContainerTests.cs b/src/DotNext.Tests/Threading/AtomicContainerTests.cs index 6aad59f587..749549d3cb 100644 --- a/src/DotNext.Tests/Threading/AtomicContainerTests.cs +++ b/src/DotNext.Tests/Threading/AtomicContainerTests.cs @@ -93,4 +93,18 @@ public static void StringConversion() var container = new Atomic { Value = 42M }; Equal(42M.ToString(), container.ToString()); } + + [Fact] + public static void SwapValues() + { + var x = new Atomic { Value = 42 }; + var y = new Atomic { Value = 43 }; + + Equal(42, x.Value); + Equal(43, y.Value); + + x.Swap(ref y); + Equal(43, x.Value); + Equal(42, y.Value); + } } \ No newline at end of file diff --git a/src/DotNext.Threading/DotNext.Threading.csproj b/src/DotNext.Threading/DotNext.Threading.csproj index a4078721e2..b68f8a0bae 100644 --- a/src/DotNext.Threading/DotNext.Threading.csproj +++ b/src/DotNext.Threading/DotNext.Threading.csproj @@ -7,7 +7,7 @@ true true nullablePublicOnly - 5.17.2 + 5.18.0 .NET Foundation and Contributors .NEXT Family of Libraries diff --git a/src/DotNext.Threading/Runtime/Caching/RandomAccessCache.cs b/src/DotNext.Threading/Runtime/Caching/RandomAccessCache.cs index 0e13d071bd..6c4cd44775 100644 --- a/src/DotNext.Threading/Runtime/Caching/RandomAccessCache.cs +++ b/src/DotNext.Threading/Runtime/Caching/RandomAccessCache.cs @@ -1,6 +1,8 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using DotNext.Threading.Tasks; +using static System.Threading.Timeout; namespace DotNext.Runtime.Caching; @@ -66,21 +68,9 @@ public IEqualityComparer? KeyComparer get => keyComparer; init => keyComparer = ReferenceEquals(value, EqualityComparer.Default) ? null : value; } - - /// - /// Opens a session that can be used to modify the value associated with the key. - /// - /// - /// The cache guarantees that the value cannot be evicted concurrently with the returned session. However, - /// the value can be evicted immediately after. The caller must dispose session. - /// - /// The key of the cache record. - /// The token that can be used to cancel the operation. - /// The session that can be used to read or modify the cache record. - /// The operation has been canceled. - /// The cache is disposed. + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] - public async ValueTask ChangeAsync(TKey key, CancellationToken token = default) + private async ValueTask ChangeAsync(TKey key, TimeSpan timeout, CancellationToken token) { var keyComparerCopy = KeyComparer; var hashCode = keyComparerCopy?.GetHashCode(key) ?? EqualityComparer.Default.GetHashCode(key); @@ -90,7 +80,7 @@ public async ValueTask ChangeAsync(TKey key, CancellationTok var lockTaken = false; try { - await bucket.AcquireAsync(token).ConfigureAwait(false); + await bucket.AcquireAsync(timeout, token).ConfigureAwait(false); lockTaken = true; if (bucket.Modify(keyComparerCopy, key, hashCode) is { } valueHolder) @@ -117,6 +107,21 @@ public async ValueTask ChangeAsync(TKey key, CancellationTok } } + /// + /// Opens a session that can be used to modify the value associated with the key. + /// + /// + /// The cache guarantees that the value cannot be evicted concurrently with the returned session. However, + /// the value can be evicted immediately after. The caller must dispose session. + /// + /// The key of the cache record. + /// The token that can be used to cancel the operation. + /// The session that can be used to read or modify the cache record. + /// The operation has been canceled. + /// The cache is disposed. + public ValueTask ChangeAsync(TKey key, CancellationToken token = default) + => ChangeAsync(key, InfiniteTimeSpan, token); + /// /// Opens a session synchronously that can be used to modify the value associated with the key. /// @@ -126,32 +131,13 @@ public async ValueTask ChangeAsync(TKey key, CancellationTok /// /// The key of the cache record. /// The time to wait for the cache lock. + /// The token that can be used to cancel the operation. /// The session that can be used to read or modify the cache record. /// The internal lock cannot be acquired in timely manner. + /// The operation has been canceled. /// The cache is disposed. - public ReadOrWriteSession Change(TKey key, TimeSpan timeout) - { - var keyComparerCopy = KeyComparer; - var hashCode = keyComparerCopy?.GetHashCode(key) ?? EqualityComparer.Default.GetHashCode(key); - var bucket = GetBucket(hashCode); - - bool lockTaken; - if (!(lockTaken = bucket.TryAcquire(timeout))) - throw new TimeoutException(); - try - { - if (bucket.Modify(keyComparerCopy, key, hashCode) is { } valueHolder) - return new(this, valueHolder); - - lockTaken = false; - return new(this, bucket, key, hashCode); - } - finally - { - if (lockTaken) - bucket.Release(); - } - } + public ReadOrWriteSession Change(TKey key, TimeSpan timeout, CancellationToken token = default) + => ChangeAsync(key, timeout, token).Wait(); /// /// Tries to read the cached record. @@ -177,19 +163,9 @@ public bool TryRead(TKey key, out ReadSession session) session = default; return false; } - - /// - /// Tries to invalidate cache record associated with the provided key. - /// - /// The key of the cache record to be removed. - /// The token that can be used to cancel the operation. - /// - /// The session that can be used to read the removed cache record; - /// or if there is no record associated with . - /// The operation has been canceled. - /// The cache is disposed. + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] - public async ValueTask TryRemoveAsync(TKey key, CancellationToken token = default) + private async ValueTask TryRemoveAsync(TKey key, TimeSpan timeout, CancellationToken token) { var keyComparerCopy = KeyComparer; var hashCode = keyComparerCopy?.GetHashCode(key) ?? EqualityComparer.Default.GetHashCode(key); @@ -199,7 +175,7 @@ public bool TryRead(TKey key, out ReadSession session) var lockTaken = false; try { - await bucket.AcquireAsync(token).ConfigureAwait(false); + await bucket.AcquireAsync(timeout, token).ConfigureAwait(false); lockTaken = true; return bucket.TryRemove(keyComparerCopy, key, hashCode) is { } removedPair @@ -222,48 +198,40 @@ public bool TryRead(TKey key, out ReadSession session) bucket.Release(); } } - + /// - /// Tries to invalidate cache record associated with the provided key synchronously. + /// Tries to invalidate cache record associated with the provided key. /// /// The key of the cache record to be removed. - /// The time to wait for the cache lock. - /// The session that can be used to read the removed cache record. - /// if the record associated with exists; otherwise, . - /// The internal lock cannot be acquired in timely manner. + /// The token that can be used to cancel the operation. + /// + /// The session that can be used to read the removed cache record; + /// or if there is no record associated with . + /// The operation has been canceled. /// The cache is disposed. - public bool TryRemove(TKey key, TimeSpan timeout, out ReadSession session) - { - var keyComparerCopy = KeyComparer; - var hashCode = keyComparerCopy?.GetHashCode(key) ?? EqualityComparer.Default.GetHashCode(key); - var bucket = GetBucket(hashCode); - - if (!bucket.TryAcquire(timeout)) - throw new TimeoutException(); - try - { - session = bucket.TryRemove(keyComparerCopy, key, hashCode) is { } removedPair - ? new(Eviction, removedPair) - : default; - } - finally - { - bucket.Release(); - } - - return session.IsValid; - } + public ValueTask TryRemoveAsync(TKey key, CancellationToken token = default) + => TryRemoveAsync(key, InfiniteTimeSpan, token); /// - /// Invalidates the cache record associated with the specified key. + /// Tries to invalidate cache record associated with the provided key synchronously. /// /// The key of the cache record to be removed. + /// The session that can be used to read the removed cache record. + /// The time to wait for the cache lock. /// The token that can be used to cancel the operation. - /// if the cache record associated with is removed successfully; otherwise, . + /// if the record associated with exists; otherwise, . + /// The internal lock cannot be acquired in timely manner. /// The operation has been canceled. /// The cache is disposed. + public bool TryRemove(TKey key, out ReadSession session, TimeSpan timeout, CancellationToken token = default) + { + var result = TryRemoveAsync(key, timeout, token).Wait(); + session = result.GetValueOrDefault(); + return result.HasValue; + } + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] - public async ValueTask InvalidateAsync(TKey key, CancellationToken token = default) + private async ValueTask InvalidateAsync(TKey key, TimeSpan timeout, CancellationToken token) { var keyComparerCopy = KeyComparer; var hashCode = keyComparerCopy?.GetHashCode(key) ?? EqualityComparer.Default.GetHashCode(key); @@ -274,7 +242,7 @@ public async ValueTask InvalidateAsync(TKey key, CancellationToken token = KeyValuePair? removedPair; try { - await bucket.AcquireAsync(token).ConfigureAwait(false); + await bucket.AcquireAsync(timeout, token).ConfigureAwait(false); lockTaken = true; removedPair = bucket.TryRemove(keyComparerCopy, key, hashCode); } @@ -307,46 +275,30 @@ public async ValueTask InvalidateAsync(TKey key, CancellationToken token = return true; } - + + /// + /// Invalidates the cache record associated with the specified key. + /// + /// The key of the cache record to be removed. + /// The token that can be used to cancel the operation. + /// if the cache record associated with is removed successfully; otherwise, . + /// The operation has been canceled. + /// The cache is disposed. + public ValueTask InvalidateAsync(TKey key, CancellationToken token = default) + => InvalidateAsync(key, InfiniteTimeSpan, token); + /// /// Invalidates the cache record associated with the specified key. /// /// The key of the cache record to be removed. /// The time to wait for the cache lock. + /// The token that can be used to cancel the operation. /// if the cache record associated with is removed successfully; otherwise, . /// The internal lock cannot be acquired in timely manner. + /// The operation has been canceled. /// The cache is disposed. - public bool Invalidate(TKey key, TimeSpan timeout) - { - var keyComparerCopy = KeyComparer; - var hashCode = keyComparerCopy?.GetHashCode(key) ?? EqualityComparer.Default.GetHashCode(key); - var bucket = GetBucket(hashCode); - - KeyValuePair? removedPair; - if (!bucket.TryAcquire(timeout)) - throw new TimeoutException(); - try - { - removedPair = bucket.TryRemove(keyComparerCopy, key, hashCode); - } - finally - { - bucket.Release(); - } - - if (removedPair is null) - { - return false; - } - - if (removedPair.ReleaseCounter() is false) - { - Eviction?.Invoke(key, GetValue(removedPair)); - ClearValue(removedPair); - } - - return true; - } + public bool Invalidate(TKey key, TimeSpan timeout, CancellationToken token = default) + => InvalidateAsync(key, timeout, token).Wait(); /// /// Invalidates the entire cache. diff --git a/src/DotNext.Threading/Threading/AsyncExclusiveLock.cs b/src/DotNext.Threading/Threading/AsyncExclusiveLock.cs index 94c5f25286..a50e4064af 100644 --- a/src/DotNext.Threading/Threading/AsyncExclusiveLock.cs +++ b/src/DotNext.Threading/Threading/AsyncExclusiveLock.cs @@ -87,7 +87,7 @@ public bool TryAcquire() private bool IsLockHelpByCurrentThread { - get => lockOwner is not { } owner || ReferenceEquals(owner, Thread.CurrentThread); + get => lockOwner is { } owner && ReferenceEquals(owner, Thread.CurrentThread); set { if (value) diff --git a/src/DotNext.Unsafe/DotNext.Unsafe.csproj b/src/DotNext.Unsafe/DotNext.Unsafe.csproj index 8cd8f29bf0..213947f098 100644 --- a/src/DotNext.Unsafe/DotNext.Unsafe.csproj +++ b/src/DotNext.Unsafe/DotNext.Unsafe.csproj @@ -7,7 +7,7 @@ enable true true - 5.17.2 + 5.18.0 nullablePublicOnly .NET Foundation and Contributors diff --git a/src/DotNext.Unsafe/Runtime/InteropServices/Pointer.cs b/src/DotNext.Unsafe/Runtime/InteropServices/Pointer.cs index 9b7fcb17c2..33b93fac77 100644 --- a/src/DotNext.Unsafe/Runtime/InteropServices/Pointer.cs +++ b/src/DotNext.Unsafe/Runtime/InteropServices/Pointer.cs @@ -149,7 +149,7 @@ public unsafe Pointer(nuint ptr) /// Determines whether this pointer is aligned /// to the size of . /// - public unsafe bool IsAligned => Address % Intrinsics.AlignOf() is 0; + public bool IsAligned => Address % Intrinsics.AlignOf() is 0; /// /// Fills the elements of the array with a specified value. diff --git a/src/DotNext/Buffers/IReadOnlySpanConsumer.cs b/src/DotNext/Buffers/IReadOnlySpanConsumer.cs index 2bae045992..03b3d4a9ae 100644 --- a/src/DotNext/Buffers/IReadOnlySpanConsumer.cs +++ b/src/DotNext/Buffers/IReadOnlySpanConsumer.cs @@ -147,7 +147,7 @@ ValueTask ISupplier, CancellationToken, ValueTask>.Invoke(Read /// /// The type of the consumer argument. [StructLayout(LayoutKind.Auto)] -public readonly record struct BufferConsumer : IReadOnlySpanConsumer, IEquatable> +public readonly record struct BufferConsumer : IReadOnlySpanConsumer { private readonly IBufferWriter output; diff --git a/src/DotNext/Buffers/SpanOwner.cs b/src/DotNext/Buffers/SpanOwner.cs index 28d93f3e41..b243e5131b 100644 --- a/src/DotNext/Buffers/SpanOwner.cs +++ b/src/DotNext/Buffers/SpanOwner.cs @@ -38,7 +38,7 @@ public ref struct SpanOwner /// [EditorBrowsable(EditorBrowsableState.Never)] [CLSCompliant(false)] - public static int StackallocThreshold { get; } = 1 + (LibrarySettings.StackallocThreshold / Unsafe.SizeOf()); + public static int StackallocThreshold { get; } = 1 + Features.StackallocThreshold / Unsafe.SizeOf(); private readonly object? owner; private readonly Span memory; @@ -106,13 +106,7 @@ public SpanOwner(int minBufferSize, bool exactSize = true) if (UseNativeAllocation) { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(minBufferSize); - - unsafe - { - var ptr = NativeMemory.Alloc((uint)minBufferSize, (uint)Unsafe.SizeOf()); - memory = new(ptr, minBufferSize); - } - + memory = Allocate(minBufferSize); owner = Sentinel.Instance; } else @@ -121,10 +115,31 @@ public SpanOwner(int minBufferSize, bool exactSize = true) memory = exactSize ? new(owner, 0, minBufferSize) : new(owner); this.owner = owner; } + + static unsafe Span Allocate(int length) + { + void* ptr; + + if (IsNaturalAlignment) + { + ptr = NativeMemory.Alloc((uint)length, (uint)Unsafe.SizeOf()); + } + else + { + var x = (uint)Unsafe.SizeOf(); + var y = (nuint)(uint)length; + var byteCount = checked(x * y); + ptr = NativeMemory.AlignedAlloc(byteCount, (uint)Intrinsics.AlignOf()); + } + + return new(ptr, length); + } } + private static bool IsNaturalAlignment => Intrinsics.AlignOf() <= nuint.Size; + private static bool UseNativeAllocation - => !LibrarySettings.DisableNativeAllocation && !RuntimeHelpers.IsReferenceOrContainsReferences() && Intrinsics.AlignOf() <= nuint.Size; + => Features.UseNativeAllocation && !RuntimeHelpers.IsReferenceOrContainsReferences(); /// /// Gets the rented memory. @@ -170,7 +185,7 @@ public static implicit operator SpanOwner(Span span) /// Gets textual representation of the rented memory. /// /// The textual representation of the rented memory. - public override readonly string ToString() => memory.ToString(); + public readonly override string ToString() => memory.ToString(); /// /// Returns the memory back to the pool. @@ -181,11 +196,19 @@ public void Dispose() { ArrayPool.Shared.Return(array, RuntimeHelpers.IsReferenceOrContainsReferences()); } - else if (ReferenceEquals(owner, Sentinel.Instance)) + else if (UseNativeAllocation && ReferenceEquals(owner, Sentinel.Instance)) { unsafe { - NativeMemory.Free(Unsafe.AsPointer(ref MemoryMarshal.GetReference(memory))); + var ptr = Unsafe.AsPointer(ref MemoryMarshal.GetReference(memory)); + if (IsNaturalAlignment) + { + NativeMemory.Free(ptr); + } + else + { + NativeMemory.AlignedFree(ptr); + } } } else @@ -195,4 +218,31 @@ public void Dispose() this = default; } +} + +file static class Features +{ + private const string UseNativeAllocationFeature = "DotNext.Buffers.NativeAllocation"; + + // TODO: [FeatureSwitchDefinition(EnableNativeAllocationFeature)] + internal static bool UseNativeAllocation + => LibraryFeature.IsSupported(UseNativeAllocationFeature); + + internal static int StackallocThreshold + { + get + { + const string environmentVariableName = "DOTNEXT_STACK_ALLOC_THRESHOLD"; + const string configurationParameterName = "DotNext.Buffers.StackAllocThreshold"; + const int defaultValue = 511; + const int minimumValue = 14; + + if (AppContext.GetData(configurationParameterName) is not int result) + { + int.TryParse(Environment.GetEnvironmentVariable(environmentVariableName), out result); + } + + return result > minimumValue ? result : defaultValue; + } + } } \ No newline at end of file diff --git a/src/DotNext/DotNext.csproj b/src/DotNext/DotNext.csproj index 7703043416..8c0071567a 100644 --- a/src/DotNext/DotNext.csproj +++ b/src/DotNext/DotNext.csproj @@ -11,7 +11,7 @@ .NET Foundation and Contributors .NEXT Family of Libraries - 5.17.2 + 5.18.0 DotNext MIT diff --git a/src/DotNext/ExceptionMessages.cs b/src/DotNext/ExceptionMessages.cs index 52a1d258d0..91559984b9 100644 --- a/src/DotNext/ExceptionMessages.cs +++ b/src/DotNext/ExceptionMessages.cs @@ -56,4 +56,6 @@ internal static string NoResult(TError errorCode) internal static string EndOfBuffer(long remaining) => string.Format(Resources.GetString("EndOfBuffer")!, remaining); internal static string OverlappedRange => Resources.GetString("OverlappedRange")!; + + internal static string FullyQualifiedPathExpected => Resources.GetString("FullyQualifiedPathExpected")!; } \ No newline at end of file diff --git a/src/DotNext/ExceptionMessages.restext b/src/DotNext/ExceptionMessages.restext index c49693d2f4..10caa647f0 100644 --- a/src/DotNext/ExceptionMessages.restext +++ b/src/DotNext/ExceptionMessages.restext @@ -19,4 +19,5 @@ KeyAlreadyExists=The key already exists ObjectMustNotBeArray=The object must not be represented by the array NoResult=The result of the operation is unavailable. Error code is {0} EndOfBuffer=The buffer has no data to read but the caller expects {0} extra elements -OverlappedRange=The ranges are overlapped \ No newline at end of file +OverlappedRange=The ranges are overlapped +FullyQualifiedPathExpected=The path must be fully-qualified \ No newline at end of file diff --git a/src/DotNext/Func.cs b/src/DotNext/Func.cs index 3dddebb5f1..196eae4d3d 100644 --- a/src/DotNext/Func.cs +++ b/src/DotNext/Func.cs @@ -2,6 +2,8 @@ namespace DotNext; +using Runtime; + /// /// Provides extension methods for delegate and /// predefined functions. @@ -93,17 +95,15 @@ public static Func Constant(T obj) return Unsafe.As>(Constant(Unsafe.As(ref obj))); // slow path - allocates a new delegate - return obj is null - ? Default! - : typeof(T).IsValueType - ? new BoxedConstant(obj).GetValue - : Unsafe.As(ref obj).ReinterpretRefType; + return obj is null ? Default! : obj.UnboxAny; } internal static T? Default() => default; - private static T ReinterpretRefType(this object obj) - => Unsafe.As(ref obj); + private static T UnboxAny(this object obj) + => typeof(T).IsValueType + ? Unsafe.As(ref Intrinsics.GetRawData(obj)) + : Unsafe.As(ref obj); private static Func Constant(bool value) { @@ -484,11 +484,4 @@ public static Result TryInvoke(T value) - { - internal T GetValue() => value; - - public override string? ToString() => value?.ToString(); - } } \ No newline at end of file diff --git a/src/DotNext/LibraryFeature.cs b/src/DotNext/LibraryFeature.cs new file mode 100644 index 0000000000..3e66ed3bd8 --- /dev/null +++ b/src/DotNext/LibraryFeature.cs @@ -0,0 +1,9 @@ +using System.Diagnostics.CodeAnalysis; + +namespace DotNext; + +internal static class LibraryFeature +{ + internal static bool IsSupported([ConstantExpected] string featureName) + => !AppContext.TryGetSwitch(featureName, out var result) || result; +} \ No newline at end of file diff --git a/src/DotNext/LibrarySettings.cs b/src/DotNext/LibrarySettings.cs deleted file mode 100644 index 8467b23d0e..0000000000 --- a/src/DotNext/LibrarySettings.cs +++ /dev/null @@ -1,50 +0,0 @@ -namespace DotNext; - -internal static class LibrarySettings -{ - internal static int StackallocThreshold - { - get - { - const string environmentVariableName = "DOTNEXT_STACK_ALLOC_THRESHOLD"; - const string configurationParameterName = "DotNext.Buffers.StackAllocThreshold"; - const int defaultValue = 511; - const int minimumValue = 14; - - if (AppContext.GetData(configurationParameterName) is not int result) - { - int.TryParse(Environment.GetEnvironmentVariable(environmentVariableName), out result); - } - - return result > minimumValue ? result : defaultValue; - } - } - - internal static bool DisableRandomStringInternalBufferCleanup - { - get - { - const string switchName = "DotNext.Security.DisableRandomStringInternalBufferCleanup"; - const bool defaultValue = false; - - if (!AppContext.TryGetSwitch(switchName, out var result)) - result = defaultValue; - - return result; - } - } - - internal static bool DisableNativeAllocation - { - get - { - const string switchName = "DotNext.Buffers.DisableNativeAllocation"; - const bool defaultValue = false; - - if (!AppContext.TryGetSwitch(switchName, out var result)) - result = defaultValue; - - return result; - } - } -} \ No newline at end of file diff --git a/src/DotNext/Net/Http/HttpEndPoint.cs b/src/DotNext/Net/Http/HttpEndPoint.cs index 96411a65fb..357e03d8cc 100644 --- a/src/DotNext/Net/Http/HttpEndPoint.cs +++ b/src/DotNext/Net/Http/HttpEndPoint.cs @@ -30,7 +30,7 @@ public HttpEndPoint(Uri uri) /// for HTTPS; for HTTP. /// The type of the host name. public HttpEndPoint(string hostName, int port, bool secure, AddressFamily family = AddressFamily.Unspecified) - : base(hostName, port, family) + : base(hostName, port, family is AddressFamily.Unspecified ? ToAddressFamily(hostName) : family) => IsSecure = secure; /// @@ -69,6 +69,8 @@ private static int GetPort(Uri uri, out bool secure) _ => AddressFamily.Unspecified, }; + private static AddressFamily ToAddressFamily(string? hostName) => ToAddressFamily(Uri.CheckHostName(hostName)); + /// /// Gets a value indicating that HTTP over TLS should be used (HTTPS). /// @@ -166,7 +168,7 @@ bool ISpanFormattable.TryFormat(Span destination, out int charsWritten, Re && writer.TryFormat(Port, format, provider) && writer.TryAdd('/')) ? writer.WrittenCount - : default; + : 0; return success; } @@ -183,7 +185,7 @@ bool IUtf8SpanFormattable.TryFormat(Span destination, out int bytesWritten && writer.TryFormat(Port, format, provider) && writer.TryAdd((byte)'/')) ? writer.WrittenCount - : default; + : 0; return success; } diff --git a/src/DotNext/Optional.cs b/src/DotNext/Optional.cs index 97c3fa11ce..463d7dc8ac 100644 --- a/src/DotNext/Optional.cs +++ b/src/DotNext/Optional.cs @@ -168,17 +168,6 @@ public static Optional Null() where T : class? => new(null); - /// - /// Converts the monad to . - /// - /// The type of the underlying value. - /// The type of the monad. - /// The value to convert. - /// The value represented as or if there is no value. - public static Optional Create(TMonad value) - where TMonad : struct, IOptionMonad - => value.TryGet(out var result) ? new(result) : None(); - /// /// Flattens the nested optional value. /// @@ -243,6 +232,16 @@ private static byte GetKindUnsafe([DisallowNull] ref T optionalValue) : NotEmptyValue; } + /// + /// Converts the monad to . + /// + /// The type of the monad. + /// The value to convert. + /// The value represented as or if there is no value. + public static Optional Create(TMonad value) + where TMonad : struct, IOptionMonad + => value.TryGet(out var result) ? new(result) : None; + /// /// Determines whether the object represents meaningful value. /// @@ -340,6 +339,15 @@ public bool TryGet([MaybeNullWhen(false)] out T value, out bool isNull) [return: NotNullIfNotNull(nameof(defaultValue))] public T? Or(T? defaultValue) => HasValue ? value : defaultValue; + /// + /// Concatenates optional values. + /// + /// + /// + /// The optional value that is defined only when both containers have values. + public Optional<(T, TOther)> Concat(in Optional other) + => HasValue && other.HasValue ? new((ValueOrDefault, other.ValueOrDefault)) : default; + /// /// If a value is present, returns the value, otherwise throw exception. /// @@ -610,12 +618,7 @@ private Optional If(TPredicate condition) /// This method uses type /// to get hash code of . /// - public override int GetHashCode() => kind switch - { - UndefinedValue => 0, - NullValue => NullValue, - _ => EqualityComparer.Default.GetHashCode(value!), - }; + public override int GetHashCode() => HasValue ? EqualityComparer.Default.GetHashCode(value) : kind; /// /// Determines whether this container stored the same @@ -666,12 +669,7 @@ public bool Equals(object? other, IEqualityComparer comparer) /// /// The comparer implementing hash code function. /// The hash code of . - public int GetHashCode(IEqualityComparer comparer) => kind switch - { - UndefinedValue => 0, - NullValue => NullValue, - _ => comparer.GetHashCode(value!), - }; + public int GetHashCode(IEqualityComparer comparer) => HasValue ? comparer.GetHashCode(value) : kind; /// /// Wraps value into Optional container. diff --git a/src/DotNext/RandomExtensions.cs b/src/DotNext/RandomExtensions.cs index 9a5a16dfc6..3cfa1fdf1e 100644 --- a/src/DotNext/RandomExtensions.cs +++ b/src/DotNext/RandomExtensions.cs @@ -19,8 +19,6 @@ public static class RandomExtensions /// internal static readonly int BitwiseHashSalt = Random.Shared.Next(); - private static readonly bool CleanupInternalBuffer = !LibrarySettings.DisableRandomStringInternalBufferCleanup; - private interface IRandomBytesSource { void GetBytes(Span bytes); @@ -141,7 +139,7 @@ ref MemoryMarshal.GetReference(allowedInput), new CachedRandomNumberGenerator(random, randomVectorBuffer.Span).Randomize(allowedInput, buffer); } - if (CleanupInternalBuffer) + if (Features.UseRandomStringInternalBufferCleanup) randomVectorBuffer.Span.Clear(); static void FastPath(ref uint randomVectorPtr, ref T inputPtr, uint moduloOperand, Span output) @@ -470,4 +468,13 @@ public static Optional Peek(this Random random, ReadOnlySpan span) _ => span[random.Next(length)], }; } +} + +file static class Features +{ + private const string UseRandomStringInternalBufferCleanupFeature = "DotNext.Security.RandomStringInternalBufferCleanup"; + + // TODO: [FeatureSwitchDefinition(UseRandomStringInternalBufferCleanupFeature)] + internal static bool UseRandomStringInternalBufferCleanup + => LibraryFeature.IsSupported(UseRandomStringInternalBufferCleanupFeature); } \ No newline at end of file diff --git a/src/DotNext/Result.cs b/src/DotNext/Result.cs index c5528982d8..c9b2839eb8 100644 --- a/src/DotNext/Result.cs +++ b/src/DotNext/Result.cs @@ -91,25 +91,20 @@ public static class Result /// Initializes a new successful result. /// /// The value to be stored as result. - public Result(T value) - { - this.value = value; - exception = null; - } + public Result(T value) => this.value = value; /// /// Initializes a new unsuccessful result. /// /// The exception representing error. Cannot be . public Result(Exception error) + : this(ExceptionDispatchInfo.Capture(error)) { - exception = ExceptionDispatchInfo.Capture(error); - Unsafe.SkipInit(out value); } private Result(ExceptionDispatchInfo dispatchInfo) { - value = default!; + Unsafe.SkipInit(out value); exception = dispatchInfo; } @@ -315,6 +310,25 @@ public unsafe T OrInvoke(delegate* defaultFunc) public static Result operator |(in Result x, in Result y) => x.IsSuccessful ? x : y; + /// + /// Converts this result to . + /// + /// The completed task representing the result. + public ValueTask AsTask() + => exception?.SourceException switch + { + null => new(value), + OperationCanceledException canceledEx => ValueTask.FromCanceled(canceledEx.CancellationToken), + { } error => ValueTask.FromException(error), + }; + + /// + /// Converts the result to . + /// + /// The result to be converted. + /// The completed task representing the result. + public static explicit operator ValueTask(in Result result) => result.AsTask(); + /// /// Gets boxed representation of the result. /// diff --git a/src/DotNext/Runtime/Intrinsics.cs b/src/DotNext/Runtime/Intrinsics.cs index 05e738e7d7..eacbfc8b00 100644 --- a/src/DotNext/Runtime/Intrinsics.cs +++ b/src/DotNext/Runtime/Intrinsics.cs @@ -411,4 +411,14 @@ private static void KeepAlive(ref readonly byte location) if (Unsafe.IsNullRef(in location)) throw new ArgumentNullException(nameof(location)); } + + internal static ref byte GetRawData(object obj) + => ref Unsafe.As(obj).data; +} + +file abstract class RawData +{ + internal byte data; + + private RawData() => throw new NotImplementedException(); } \ No newline at end of file diff --git a/src/DotNext/Runtime/ValueReference.cs b/src/DotNext/Runtime/ValueReference.cs index ce111542c9..28f10ff729 100644 --- a/src/DotNext/Runtime/ValueReference.cs +++ b/src/DotNext/Runtime/ValueReference.cs @@ -23,7 +23,7 @@ public readonly struct ValueReference(object owner, ref T fieldRef) : ISupplier, IConsumer { - private readonly nint offset = RawData.GetOffset(owner, in fieldRef); + private readonly nint offset = ValueReference.GetOffset(owner, in fieldRef); /// /// Creates a reference to an array element. @@ -78,7 +78,15 @@ public ValueReference(ref T staticFieldRef) /// /// Gets a reference to the field. /// - public ref T Value => ref RawData.GetObjectData(owner, offset); + public ref T Value => ref ValueReference.GetObjectData(owner, offset); + + /// + /// Obtains managed pointer to the referenced value. + /// + /// The managed pointer to the referenced value. + [EditorBrowsable(EditorBrowsableState.Never)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ref T GetPinnableReference() => ref Value; /// void IConsumer.Invoke(T value) => Value = value; @@ -123,7 +131,7 @@ private Action ToAction() /// public override string? ToString() - => owner is not null ? RawData.GetObjectData(owner, offset)?.ToString() : null; + => owner is not null ? ValueReference.GetObjectData(owner, offset)?.ToString() : null; /// public override bool Equals([NotNullWhen(true)] object? other) @@ -200,7 +208,7 @@ public readonly struct ReadOnlyValueReference(object owner, ref readonly T fi IEqualityOperators, ReadOnlyValueReference, bool>, ISupplier { - private readonly nint offset = RawData.GetOffset(owner, in fieldRef); + private readonly nint offset = ValueReference.GetOffset(owner, in fieldRef); /// /// Creates a reference to an array element. @@ -237,7 +245,15 @@ public ReadOnlyValueReference(ref readonly T staticFieldRef) /// /// Gets a reference to the field. /// - public ref readonly T Value => ref RawData.GetObjectData(owner, offset); + public ref readonly T Value => ref ValueReference.GetObjectData(owner, offset); + + /// + /// Obtains managed pointer to the referenced value. + /// + /// The managed pointer to the referenced value. + [EditorBrowsable(EditorBrowsableState.Never)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ref readonly T GetPinnableReference() => ref Value; /// T ISupplier.Invoke() => Value; @@ -273,7 +289,7 @@ static T ThrowNullReferenceException() /// public override string? ToString() - => owner is not null ? RawData.GetObjectData(owner, offset)?.ToString() : null; + => owner is not null ? ValueReference.GetObjectData(owner, offset)?.ToString() : null; /// public override bool Equals([NotNullWhen(true)] object? other) @@ -321,28 +337,23 @@ public static explicit operator Func(ReadOnlyValueReference reference) => reference.ToFunc(); } -[SuppressMessage("Performance", "CA1812", Justification = "Used for reinterpret cast")] -file abstract class RawData +file static class ValueReference { // TODO: Replace with public counterpart in future private static readonly Func? GetRawObjectDataSize; [DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicMethods, typeof(RuntimeHelpers))] - static RawData() + static ValueReference() { const BindingFlags flags = BindingFlags.DeclaredOnly | BindingFlags.NonPublic | BindingFlags.Static; GetRawObjectDataSize = typeof(RuntimeHelpers) .GetMethod(nameof(GetRawObjectDataSize), flags, [typeof(object)]) ?.CreateDelegate>(); } - - private byte data; - - private RawData() => throw new NotImplementedException(); internal static nint GetOffset(object owner, ref readonly T field, [CallerArgumentExpression(nameof(field))] string? paramName = null) { - ref var rawData = ref Unsafe.As(owner).data; + ref var rawData = ref Intrinsics.GetRawData(owner); var offset = Unsafe.ByteOffset(in rawData, in Intrinsics.InToRef(in field)); // Ensure that the reference is an interior pointer to the field inside the object @@ -354,14 +365,14 @@ internal static nint GetOffset(object owner, ref readonly T field, [CallerArg internal static ref T GetObjectData(object owner, nint offset) { - ref var rawData = ref Unsafe.As(owner).data; + ref var rawData = ref Intrinsics.GetRawData(owner); return ref Unsafe.As(ref Unsafe.Add(ref rawData, offset)); } } file sealed class StaticFieldAccessor(nint offset) { - public T GetValue() => RawData.GetObjectData(Sentinel.Instance, offset); + public T GetValue() => ValueReference.GetObjectData(Sentinel.Instance, offset); - public void SetValue(T value) => RawData.GetObjectData(Sentinel.Instance, offset) = value; + public void SetValue(T value) => ValueReference.GetObjectData(Sentinel.Instance, offset) = value; } \ No newline at end of file diff --git a/src/DotNext/Sentinel.cs b/src/DotNext/Sentinel.cs index e7cd2b335f..90c3607f35 100644 --- a/src/DotNext/Sentinel.cs +++ b/src/DotNext/Sentinel.cs @@ -1,7 +1,13 @@ +using System.Runtime.InteropServices; + namespace DotNext; internal static class Sentinel { // instance has fixed address in memory which is critical to ValueReference implementation - internal static readonly object Instance = GC.AllocateUninitializedArray(length: 0, pinned: true); + internal static readonly object Instance = GC.AllocateUninitializedArray(length: 0, pinned: true); + + // The struct is needed to avoid false positives caused by the type check on the variable that stores singleton + [StructLayout(LayoutKind.Auto)] + private struct Dummy; } \ No newline at end of file diff --git a/src/DotNext/Text/Encodings/Web/FileUri.cs b/src/DotNext/Text/Encodings/Web/FileUri.cs new file mode 100644 index 0000000000..028bcd4132 --- /dev/null +++ b/src/DotNext/Text/Encodings/Web/FileUri.cs @@ -0,0 +1,156 @@ +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Text.Encodings.Web; + +namespace DotNext.Text.Encodings.Web; + +using Buffers; + +/// +/// Represents operations to work with file:// scheme. +/// +public static class FileUri +{ + // On Windows: + // C:\folder => file:///C|/folder + // \\hostname\folder => file://hostname/folder + // \\?\folder => file://?/folder + // \\.\folder => file://./folder + private const string FileScheme = "file://"; + private const char UriPathSeparator = '/'; + private static readonly SearchValues UnixDirectorySeparators = SearchValues.Create([UriPathSeparator]); + private static readonly SearchValues WindowsDirectorySeparators = SearchValues.Create([UriPathSeparator, '\\']); + + /// + /// Encodes file name as URI. + /// + /// The fully-qualified file name. + /// The encoding settings. + /// as URI. The return value can be passed to constructor. + /// is not fully-qualified. + public static string CreateFromFileName(ReadOnlySpan fileName, TextEncoderSettings? settings = null) + { + if (fileName.IsEmpty) + throw new ArgumentException(ExceptionMessages.FullyQualifiedPathExpected, nameof(fileName)); + + return CreateFromFileNameCore(fileName, settings is null ? UrlEncoder.Default : UrlEncoder.Create(settings)); + } + + private static string CreateFromFileNameCore(ReadOnlySpan fileName, UrlEncoder encoder) + { + var maxLength = GetMaxEncodedLengthCore(fileName, encoder); + using var buffer = (uint)maxLength <= (uint)SpanOwner.StackallocThreshold + ? stackalloc char[maxLength] + : new SpanOwner(maxLength); + + return TryCreateFromFileNameCore(fileName, encoder, buffer.Span, out var writtenCount) + ? new(buffer.Span.Slice(0, writtenCount)) + : string.Empty; + } + + /// + /// Gets the maximum number of characters that can be produced by method. + /// + /// The file name to be encoded. + /// The encoder. + /// The maximum number of characters that can be produced by the encoder. + public static int GetMaxEncodedLength(ReadOnlySpan fileName, UrlEncoder? encoder = null) + => fileName.IsEmpty ? 0 : GetMaxEncodedLengthCore(fileName, encoder ?? UrlEncoder.Default); + + private static int GetMaxEncodedLengthCore(ReadOnlySpan fileName, UrlEncoder encoder) + => FileScheme.Length + + Unsafe.BitCast(fileName[0] is not UriPathSeparator) + + encoder.MaxOutputCharactersPerInputCharacter * fileName.Length; + + /// + /// Tries to encode file name as URI. + /// + /// The fully-qualified file name. + /// The encoder that is used to encode the file name. + /// The output buffer. + /// The number of characters written to . + /// if is encoded successfully; otherwise, . + /// is not fully-qualified. + public static bool TryCreateFromFileName(ReadOnlySpan fileName, UrlEncoder? encoder, Span output, out int charsWritten) + { + if (fileName.IsEmpty) + throw new ArgumentException(ExceptionMessages.FullyQualifiedPathExpected, nameof(fileName)); + + return TryCreateFromFileNameCore(fileName, encoder ?? UrlEncoder.Default, output, out charsWritten); + } + + private static bool TryCreateFromFileNameCore(ReadOnlySpan fileName, UrlEncoder encoder, Span output, out int charsWritten) + { + var writer = new SpanWriter(output); + writer.Write(FileScheme); + + bool endsWithTrailingSeparator; + SearchValues directoryPathSeparators; + switch (fileName) + { + case [UriPathSeparator, ..]: // Unix path + directoryPathSeparators = UnixDirectorySeparators; + break; + case ['\\', '\\', .. var rest]: // Windows UNC path + directoryPathSeparators = WindowsDirectorySeparators; + fileName = rest; + break; + default: // Windows path + const char driveSeparator = ':'; + const char escapedDriveSeparatorChar = '|'; + if (GetPathComponent(ref fileName, directoryPathSeparators = WindowsDirectorySeparators, out endsWithTrailingSeparator) + is not [.. var drive, driveSeparator]) + throw new ArgumentException(ExceptionMessages.FullyQualifiedPathExpected, nameof(fileName)); + + writer.Add(UriPathSeparator); + writer.Write(drive); + writer.Write(endsWithTrailingSeparator ? [escapedDriveSeparatorChar, UriPathSeparator] : [escapedDriveSeparatorChar]); + break; + } + + for (;; writer.Add(UriPathSeparator)) + { + var component = GetPathComponent(ref fileName, directoryPathSeparators, out endsWithTrailingSeparator); + if (encoder.Encode(component, writer.RemainingSpan, out _, out charsWritten) is not OperationStatus.Done) + return false; + + writer.Advance(charsWritten); + if (!endsWithTrailingSeparator) + break; + } + + charsWritten = writer.WrittenCount; + return true; + } + + private static ReadOnlySpan GetPathComponent(ref ReadOnlySpan fileName, SearchValues directorySeparatorChars, out bool endsWithTrailingSeparator) + { + ReadOnlySpan component; + var index = fileName.IndexOfAny(directorySeparatorChars); + if (endsWithTrailingSeparator = index >= 0) + { + component = fileName.Slice(0, index); + fileName = fileName.Slice(index + 1); + } + else + { + component = fileName; + fileName = default; + } + + return component; + } + + /// + /// Gets URI that points to the file system object. + /// + /// The information about file system object. + /// The encoding settings. + /// that points to the file system object. + public static Uri GetUri(this FileSystemInfo fileInfo, TextEncoderSettings? settings = null) + { + ArgumentNullException.ThrowIfNull(fileInfo); + + return new(CreateFromFileNameCore(fileInfo.FullName, settings is null ? UrlEncoder.Default : UrlEncoder.Create(settings)), UriKind.Absolute); + } +} \ No newline at end of file diff --git a/src/cluster/DotNext.AspNetCore.Cluster/DotNext.AspNetCore.Cluster.csproj b/src/cluster/DotNext.AspNetCore.Cluster/DotNext.AspNetCore.Cluster.csproj index 88afd7a26c..7fb78a31d7 100644 --- a/src/cluster/DotNext.AspNetCore.Cluster/DotNext.AspNetCore.Cluster.csproj +++ b/src/cluster/DotNext.AspNetCore.Cluster/DotNext.AspNetCore.Cluster.csproj @@ -8,7 +8,7 @@ true true nullablePublicOnly - 5.17.2 + 5.18.0 .NET Foundation and Contributors .NEXT Family of Libraries diff --git a/src/cluster/DotNext.AspNetCore.Cluster/Net/Cluster/Messaging/InMemoryMessage.cs b/src/cluster/DotNext.AspNetCore.Cluster/Net/Cluster/Messaging/InMemoryMessage.cs index d5a2f735d2..fb6988a4c8 100644 --- a/src/cluster/DotNext.AspNetCore.Cluster/Net/Cluster/Messaging/InMemoryMessage.cs +++ b/src/cluster/DotNext.AspNetCore.Cluster/Net/Cluster/Messaging/InMemoryMessage.cs @@ -6,8 +6,8 @@ namespace DotNext.Net.Cluster.Messaging; using IO; // this is not a public class because it's designed for special purpose: bufferize content of another DTO. -// For that purpose we using growable buffer which relies on the pooled memory -internal sealed class InMemoryMessage : Disposable, IDataTransferObject, IBufferedMessage +// For that purpose we are using growable buffer which relies on the pooled memory +internal sealed class InMemoryMessage : Disposable, IBufferedMessage { private readonly int initialSize; private MemoryOwner buffer; diff --git a/src/cluster/DotNext.Net.Cluster/DotNext.Net.Cluster.csproj b/src/cluster/DotNext.Net.Cluster/DotNext.Net.Cluster.csproj index 0c3f1a29b1..0a74e9bff1 100644 --- a/src/cluster/DotNext.Net.Cluster/DotNext.Net.Cluster.csproj +++ b/src/cluster/DotNext.Net.Cluster/DotNext.Net.Cluster.csproj @@ -8,7 +8,7 @@ enable true nullablePublicOnly - 5.17.2 + 5.18.0 .NET Foundation and Contributors .NEXT Family of Libraries diff --git a/src/cluster/DotNext.Net.Cluster/Text/Json/JsonUtils.cs b/src/cluster/DotNext.Net.Cluster/Text/Json/JsonUtils.cs deleted file mode 100644 index f3ce224ec2..0000000000 --- a/src/cluster/DotNext.Net.Cluster/Text/Json/JsonUtils.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Text.Json; - -namespace DotNext.Text.Json; - -internal static class JsonUtils -{ - internal static readonly JsonReaderOptions DefaultReaderOptions; - internal static readonly JsonWriterOptions DefaultWriterOptions; - - static JsonUtils() - { - var options = new JsonSerializerOptions(JsonSerializerDefaults.General); - DefaultReaderOptions = options.GetReaderOptions(); - DefaultWriterOptions = options.GetWriterOptions(); - } - - internal static JsonReaderOptions GetReaderOptions(this JsonSerializerOptions options) => new() - { - AllowTrailingCommas = options.AllowTrailingCommas, - CommentHandling = options.ReadCommentHandling, - MaxDepth = options.MaxDepth, - }; - - internal static JsonWriterOptions GetWriterOptions(this JsonSerializerOptions options) => new() - { - Indented = options.WriteIndented, - Encoder = options.Encoder, - SkipValidation = false, - }; -} \ No newline at end of file