diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6dc45d71c..0f715832e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,30 @@
Release Notes
====
+# 02-28-2024
+DotNext 5.1.0
+* Added `Span.Advance` extension method for spans
+* `CollectionType.GetItemType` now correctly recognizes enumerable pattern even if target type doesn't implement `IEnumerable`
+
+DotNext.Metaprogramming 5.1.0
+* Updated dependencies
+
+DotNext.Unsafe 5.1.0
+* Added `UnmanagedMemory.AsMemory` static method that allows to wrap unmanaged pointer into [Memory<T>](https://learn.microsoft.com/en-us/dotnet/api/system.memory-1)
+
+DotNext.Threading 5.1.0
+* Updated dependencies
+
+DotNext.IO 5.1.0
+* Merged [225](https://github.com/dotnet/dotNext/pull/225)
+* Added `AsUnbufferedStream` extension method for [SafeFileHandle](https://learn.microsoft.com/en-us/dotnet/api/microsoft.win32.safehandles.safefilehandle) class
+
+DotNext.Net.Cluster 5.1.0
+* Updated dependencies
+
+DotNext.AspNetCore.Cluster 5.1.0
+* Updated dependencies
+
# 02-25-2024
DotNext 5.0.3
* Fixed behavior to no-op when `GCLatencyModeScope` is initialized to default
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 5d53a499e..d21fb135d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -17,6 +17,8 @@ contact [conduct@dotnetfoundation.org](mailto:conduct@dotnetfoundation.org) with
## Branching Model
This repository uses branching model known as [git flow](https://nvie.com/posts/a-successful-git-branching-model/). Use **develop** as the destination branch in your Pull Request.
+Since 5.x release, squash commit is used to merge all commits related to the release when moving to `main` branch.
+
## Backward Compatibility
Contributions must not contain breaking changes such as backward incompatible modification of API signatures. The only exception is a new major version of the library. However, it should pass through code review and discussion.
diff --git a/README.md b/README.md
index 0a564aeee..e06c40b66 100644
--- a/README.md
+++ b/README.md
@@ -44,28 +44,30 @@ 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: 02-25-2024
+Release Date: 02-28-2024
-DotNext 5.0.3
-* Fixed behavior to no-op when `GCLatencyModeScope` is initialized to default
+DotNext 5.1.0
+* Added `Span.Advance` extension method for spans
+* `CollectionType.GetItemType` now correctly recognizes enumerable pattern even if target type doesn't implement `IEnumerable`
-DotNext.Metaprogramming 5.0.3
+DotNext.Metaprogramming 5.1.0
* Updated dependencies
-DotNext.Unsafe 5.0.3
-* Updated dependencies
+DotNext.Unsafe 5.1.0
+* Added `UnmanagedMemory.AsMemory` static method that allows to wrap unmanaged pointer into [Memory<T>](https://learn.microsoft.com/en-us/dotnet/api/system.memory-1)
-DotNext.Threading 5.0.3
+DotNext.Threading 5.1.0
* Updated dependencies
-DotNext.IO 5.0.3
-* Updated dependencies
+DotNext.IO 5.1.0
+* Merged [225](https://github.com/dotnet/dotNext/pull/225)
+* Added `AsUnbufferedStream` extension method for [SafeFileHandle](https://learn.microsoft.com/en-us/dotnet/api/microsoft.win32.safehandles.safefilehandle) class
-DotNext.Net.Cluster 5.0.3
-* Attempt to fix [221](https://github.com/dotnet/dotNext/issues/221)
+DotNext.Net.Cluster 5.1.0
+* Updated dependencies
-DotNext.AspNetCore.Cluster 5.0.3
-* Attempt to fix [221](https://github.com/dotnet/dotNext/issues/221)
+DotNext.AspNetCore.Cluster 5.1.0
+* Updated dependencies
Changelog for previous versions located [here](./CHANGELOG.md).
diff --git a/src/DotNext.IO/DotNext.IO.csproj b/src/DotNext.IO/DotNext.IO.csproj
index eb282ab74..3608bef3f 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.0.3
+ 5.1.0
DotNext.IO
MIT
diff --git a/src/DotNext.IO/ExceptionMessages.cs b/src/DotNext.IO/ExceptionMessages.cs
index 48197f7c4..6a0aebb7e 100644
--- a/src/DotNext.IO/ExceptionMessages.cs
+++ b/src/DotNext.IO/ExceptionMessages.cs
@@ -23,4 +23,6 @@ internal static string DirectoryNotFound(string path)
internal static string WriterInReadMode => (string)Resources.Get();
internal static string NoConsumerProvided => (string)Resources.Get();
+
+ internal static string FileHandleClosed => (string)Resources.Get();
}
\ No newline at end of file
diff --git a/src/DotNext.IO/ExceptionMessages.restext b/src/DotNext.IO/ExceptionMessages.restext
index 67779fc36..4dbd90f40 100644
--- a/src/DotNext.IO/ExceptionMessages.restext
+++ b/src/DotNext.IO/ExceptionMessages.restext
@@ -3,4 +3,5 @@ StreamNotWritable=Stream is not writable
StreamNotReadable=Stream is not readable
DirectoryNotFound=Directory {0} doesn't exist
WriterInReadMode=The writer is in read-only mode. Dispose active memory manager obtained from writer
-NoConsumerProvided=No actual consumer is provided
\ No newline at end of file
+NoConsumerProvided=No actual consumer is provided
+FileHandleClosed=The file handle is closed
\ No newline at end of file
diff --git a/src/DotNext.IO/IO/StreamExtensions.cs b/src/DotNext.IO/IO/StreamExtensions.cs
index 0a7fc1b20..edb7b6213 100644
--- a/src/DotNext.IO/IO/StreamExtensions.cs
+++ b/src/DotNext.IO/IO/StreamExtensions.cs
@@ -1,4 +1,5 @@
using System.Runtime.CompilerServices;
+using Microsoft.Win32.SafeHandles;
namespace DotNext.IO;
@@ -25,4 +26,24 @@ internal static void ThrowIfEmpty(in Memory buffer, [CallerArgumentExpress
/// An object that represents multiple streams as one logical stream.
public static Stream Combine(this Stream stream, ReadOnlySpan others)
=> others is { Length: > 0 } ? new SparseStream([stream, .. others]) : stream;
+
+ ///
+ /// Creates a stream for the specified file handle.
+ ///
+ ///
+ /// The returned stream doesn't own the handle.
+ ///
+ /// The file handle.
+ /// Desired access to the file via stream.
+ /// The unbuffered file stream.
+ /// is .
+ /// is closed or invalid.
+ public static Stream AsUnbufferedStream(this SafeFileHandle handle, FileAccess access)
+ {
+ ArgumentNullException.ThrowIfNull(handle);
+
+ return handle is { IsInvalid: false, IsClosed: false }
+ ? new UnbufferedFileStream(handle, access)
+ : throw new ArgumentException(ExceptionMessages.FileHandleClosed, nameof(handle));
+ }
}
\ No newline at end of file
diff --git a/src/DotNext.IO/IO/StreamSegment.cs b/src/DotNext.IO/IO/StreamSegment.cs
index 58155d96d..150bedf5f 100644
--- a/src/DotNext.IO/IO/StreamSegment.cs
+++ b/src/DotNext.IO/IO/StreamSegment.cs
@@ -8,29 +8,16 @@
///
/// The segmentation is supported only for seekable streams.
///
-public sealed class StreamSegment : Stream, IFlushable
+/// The underlying stream represented by the segment.
+/// to leave open after the object is disposed; otherwise, .
+public sealed class StreamSegment(Stream stream, bool leaveOpen = true) : Stream, IFlushable
{
- private readonly bool leaveOpen;
- private long length, offset;
-
- ///
- /// Initializes a new segment of the specified stream.
- ///
- /// The underlying stream represented by the segment.
- /// to leave open after the object is disposed; otherwise, .
- /// is .
- public StreamSegment(Stream stream, bool leaveOpen = true)
- {
- BaseStream = stream ?? throw new ArgumentNullException(nameof(stream));
- length = stream.Length;
- offset = 0L;
- this.leaveOpen = leaveOpen;
- }
+ private long length = stream.Length, offset;
///
/// Gets underlying stream.
///
- public Stream BaseStream { get; }
+ public Stream BaseStream => stream;
///
/// Establishes segment bounds.
@@ -40,28 +27,28 @@ public StreamSegment(Stream stream, bool leaveOpen = true)
///
/// The offset in the underlying stream.
/// The length of the segment.
- /// is larger than the reamining length of the underlying stream; or if greater than the length of the underlying stream.
+ /// is larger than the remaining length of the underlying stream; or if greater than the length of the underlying stream.
public void Adjust(long offset, long length)
{
- ArgumentOutOfRangeException.ThrowIfGreaterThan((ulong)offset, (ulong)BaseStream.Length, nameof(offset));
- ArgumentOutOfRangeException.ThrowIfGreaterThan((ulong)offset, (ulong)(BaseStream.Length - offset), nameof(length));
+ ArgumentOutOfRangeException.ThrowIfGreaterThan((ulong)offset, (ulong)stream.Length, nameof(offset));
+ ArgumentOutOfRangeException.ThrowIfGreaterThan((ulong)length, (ulong)(stream.Length - offset), nameof(length));
this.length = length;
this.offset = offset;
- BaseStream.Position = offset;
+ stream.Position = offset;
}
///
/// Gets a value indicating whether the current stream supports reading.
///
/// if the stream supports reading; otherwise, .
- public override bool CanRead => BaseStream.CanRead;
+ public override bool CanRead => stream.CanRead;
///
/// Gets a value indicating whether the current stream supports seeking.
///
/// if the stream supports seeking; otherwise, .
- public override bool CanSeek => BaseStream.CanSeek;
+ public override bool CanSeek => stream.CanSeek;
///
/// Gets a value indicating whether the current stream supports writing.
@@ -75,29 +62,29 @@ public void Adjust(long offset, long length)
///
public override long Position
{
- get => BaseStream.Position - offset;
+ get => stream.Position - offset;
set
{
ArgumentOutOfRangeException.ThrowIfGreaterThan((ulong)value, (ulong)length, nameof(value));
- BaseStream.Position = offset + value;
+ stream.Position = offset + value;
}
}
private long RemainingBytes => length - Position;
///
- public override void Flush() => BaseStream.Flush();
+ public override void Flush() => stream.Flush();
///
- public override Task FlushAsync(CancellationToken token = default) => BaseStream.FlushAsync(token);
+ public override Task FlushAsync(CancellationToken token = default) => stream.FlushAsync(token);
///
- public override bool CanTimeout => BaseStream.CanTimeout;
+ public override bool CanTimeout => stream.CanTimeout;
///
public override int ReadByte()
- => Position < length ? BaseStream.ReadByte() : -1;
+ => Position < length ? stream.ReadByte() : -1;
///
public override void WriteByte(byte value) => throw new NotSupportedException();
@@ -107,30 +94,30 @@ public override int Read(byte[] buffer, int offset, int count)
{
ValidateBufferArguments(buffer, offset, count);
- return BaseStream.Read(buffer, offset, (int)Math.Min(count, RemainingBytes));
+ return stream.Read(buffer, offset, (int)Math.Min(count, RemainingBytes));
}
///
public override int Read(Span buffer)
- => BaseStream.Read(buffer.TrimLength(int.CreateSaturating(RemainingBytes)));
+ => stream.Read(buffer.TrimLength(int.CreateSaturating(RemainingBytes)));
///
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
{
count = (int)Math.Min(count, RemainingBytes);
- return BaseStream.BeginRead(buffer, offset, count, callback, state);
+ return stream.BeginRead(buffer, offset, count, callback, state);
}
///
- public override int EndRead(IAsyncResult asyncResult) => BaseStream.EndRead(asyncResult);
+ public override int EndRead(IAsyncResult asyncResult) => stream.EndRead(asyncResult);
///
public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken token = default)
- => BaseStream.ReadAsync(buffer, offset, (int)Math.Min(count, RemainingBytes), token);
+ => stream.ReadAsync(buffer, offset, (int)Math.Min(count, RemainingBytes), token);
///
public override ValueTask ReadAsync(Memory buffer, CancellationToken token = default)
- => BaseStream.ReadAsync(buffer.TrimLength(int.CreateSaturating(RemainingBytes)), token);
+ => stream.ReadAsync(buffer.TrimLength(int.CreateSaturating(RemainingBytes)), token);
///
public override long Seek(long offset, SeekOrigin origin)
@@ -155,7 +142,7 @@ public override long Seek(long offset, SeekOrigin origin)
///
public override void SetLength(long value)
{
- ArgumentOutOfRangeException.ThrowIfGreaterThan((ulong)value, (ulong)(BaseStream.Length - BaseStream.Position), nameof(value));
+ ArgumentOutOfRangeException.ThrowIfGreaterThan((ulong)value, (ulong)(stream.Length - stream.Position), nameof(value));
length = value;
}
@@ -183,22 +170,22 @@ public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, As
///
public override int ReadTimeout
{
- get => BaseStream.ReadTimeout;
- set => BaseStream.ReadTimeout = value;
+ get => stream.ReadTimeout;
+ set => stream.ReadTimeout = value;
}
///
public override int WriteTimeout
{
- get => BaseStream.WriteTimeout;
- set => BaseStream.WriteTimeout = value;
+ get => stream.WriteTimeout;
+ set => stream.WriteTimeout = value;
}
///
protected override void Dispose(bool disposing)
{
if (disposing && !leaveOpen)
- BaseStream.Dispose();
+ stream.Dispose();
base.Dispose(disposing);
}
@@ -206,7 +193,7 @@ protected override void Dispose(bool disposing)
public override async ValueTask DisposeAsync()
{
if (!leaveOpen)
- await BaseStream.DisposeAsync().ConfigureAwait(false);
+ await stream.DisposeAsync().ConfigureAwait(false);
await base.DisposeAsync().ConfigureAwait(false);
}
}
\ No newline at end of file
diff --git a/src/DotNext.IO/IO/UnbufferedFileStream.Utils.cs b/src/DotNext.IO/IO/UnbufferedFileStream.Utils.cs
new file mode 100644
index 000000000..9a2f24f08
--- /dev/null
+++ b/src/DotNext.IO/IO/UnbufferedFileStream.Utils.cs
@@ -0,0 +1,119 @@
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks.Sources;
+
+namespace DotNext.IO;
+
+internal partial class UnbufferedFileStream : IValueTaskSource, IValueTaskSource
+{
+ private ManualResetValueTaskSourceCore source;
+ private int bytesWritten;
+ private ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter writeTask;
+ private ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter readTask;
+ private Action? readCallback, writeCallback;
+
+ internal ValueTask SubmitWrite(ValueTask writeTask, int bytesWritten)
+ {
+ this.bytesWritten = bytesWritten;
+ this.writeTask = writeTask.ConfigureAwait(false).GetAwaiter();
+ if (this.writeTask.IsCompleted)
+ {
+ OnWriteCompleted();
+ }
+ else
+ {
+ this.writeTask.UnsafeOnCompleted(writeCallback ??= OnWriteCompleted);
+ }
+
+ return new(this, source.Version);
+ }
+
+ internal ValueTask SubmitRead(ValueTask readTask)
+ {
+ this.readTask = readTask.ConfigureAwait(false).GetAwaiter();
+ if (this.readTask.IsCompleted)
+ {
+ OnReadCompleted();
+ }
+ else
+ {
+ this.readTask.UnsafeOnCompleted(readCallback ??= OnReadCompleted);
+ }
+
+ return new(this, source.Version);
+ }
+
+ private void OnWriteCompleted()
+ {
+ var awaiter = writeTask;
+ writeTask = default;
+
+ try
+ {
+ awaiter.GetResult();
+ }
+ catch (Exception e)
+ {
+ source.SetException(e);
+ return;
+ }
+
+ source.SetResult(bytesWritten);
+ }
+
+ private void OnReadCompleted()
+ {
+ var awaiter = readTask;
+ readTask = default;
+
+ int bytesRead;
+ try
+ {
+ bytesRead = awaiter.GetResult();
+ }
+ catch (Exception e)
+ {
+ source.SetException(e);
+ return;
+ }
+
+ source.SetResult(bytesRead);
+ }
+
+ public ValueTaskSourceStatus GetStatus(short token) => source.GetStatus(token);
+
+ public void OnCompleted(Action