diff --git a/Directory.Packages.props b/Directory.Packages.props
index b48592a3..6bc60ac5 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -19,6 +19,9 @@
+
+
+
diff --git a/samples/FishyFlip.Firehose/FishyFlip.Firehose.csproj b/samples/FishyFlip.Firehose/FishyFlip.Firehose.csproj
index 37128634..b6320f29 100644
--- a/samples/FishyFlip.Firehose/FishyFlip.Firehose.csproj
+++ b/samples/FishyFlip.Firehose/FishyFlip.Firehose.csproj
@@ -2,9 +2,10 @@
Exe
- net7.0
+ net462;net8.0
enable
enable
+ true
@@ -13,7 +14,6 @@
-
all
runtime; build; native; contentfiles; analyzers
diff --git a/samples/FishyFlip.Firehose/Program.cs b/samples/FishyFlip.Firehose/Program.cs
index 866d09c0..e3661054 100644
--- a/samples/FishyFlip.Firehose/Program.cs
+++ b/samples/FishyFlip.Firehose/Program.cs
@@ -2,7 +2,6 @@
// Copyright (c) Drastic Actions. All rights reserved.
//
-using Drastic.Tools;
using FishyFlip;
using FishyFlip.Models;
using FishyFlip.Tools;
@@ -23,7 +22,7 @@
atWebProtocol.OnSubscribedRepoMessage += (sender, args) =>
{
- Task.Run(() => HandleMessageAsync(args.Message)).FireAndForgetSafeAsync();
+ Task.Run(() => HandleMessageAsync(args.Message)).FireAndForget();
};
await atWebProtocol.StartSubscribeReposAsync();
@@ -60,7 +59,7 @@ async Task HandleMessageAsync(SubscribeRepoMessage message)
// Commit.Ops are the actions used when creating the message.
// In this case, it's a create record for the post.
// The path contains the post action and path, we need the path, so we split to get it.
- var url = $"https://bsky.app/profile/{did}/post/{message.Commit.Ops![0]!.Path!.Split("/").Last()}";
+ var url = $"https://bsky.app/profile/{did}/post/{message.Commit.Ops![0]!.Path!.Split('/').Last()}";
Console.WriteLine($"Post URL: {url}, from {repo.Handle}");
if (post.Reply is not null)
@@ -70,4 +69,25 @@ async Task HandleMessageAsync(SubscribeRepoMessage message)
}
}
}
+}
+
+public static class TaskExtensions
+{
+ public static void FireAndForget(this Task task, Action errorHandler = null)
+ {
+ if (task == null)
+ throw new ArgumentNullException(nameof(task));
+
+ task.ContinueWith(t =>
+ {
+ if (errorHandler != null && t.IsFaulted)
+ errorHandler(t.Exception);
+ }, TaskContinuationOptions.OnlyOnFaulted);
+
+ // Avoiding warning about not awaiting the fire-and-forget task.
+ // However, since the method is intended to fire and forget, we don't actually await it.
+#pragma warning disable CS4014
+ task.ConfigureAwait(false);
+#pragma warning restore CS4014
+ }
}
\ No newline at end of file
diff --git a/src/FishyFlip.Tests/AuthorizedTests.cs b/src/FishyFlip.Tests/AuthorizedTests.cs
index 0a953bc7..3c679808 100644
--- a/src/FishyFlip.Tests/AuthorizedTests.cs
+++ b/src/FishyFlip.Tests/AuthorizedTests.cs
@@ -4,6 +4,7 @@
using FishyFlip.Models;
using Microsoft.Extensions.Logging.Debug;
+using System.Net.Http;
namespace FishyFlip.Tests;
diff --git a/src/FishyFlip.Tests/FishyFlip.Tests.csproj b/src/FishyFlip.Tests/FishyFlip.Tests.csproj
index a0eb2fb3..838e6c3e 100644
--- a/src/FishyFlip.Tests/FishyFlip.Tests.csproj
+++ b/src/FishyFlip.Tests/FishyFlip.Tests.csproj
@@ -1,7 +1,7 @@
- net7.0;net8.0
+ net462;net7.0;net8.0
enable
enable
diff --git a/src/FishyFlip/ATWebSocketProtocol.cs b/src/FishyFlip/ATWebSocketProtocol.cs
index 03c870f0..150d870e 100644
--- a/src/FishyFlip/ATWebSocketProtocol.cs
+++ b/src/FishyFlip/ATWebSocketProtocol.cs
@@ -280,6 +280,17 @@ private async Task ReceiveMessages(ClientWebSocket webSocket, CancellationToken
{
try
{
+#if NETSTANDARD
+ var result =
+ await webSocket.ReceiveAsync(new ArraySegment(receiveBuffer), token);
+ if (result is not { MessageType: WebSocketMessageType.Binary, EndOfMessage: true })
+ {
+ continue;
+ }
+
+ byte[] newArray = new byte[result.Count];
+ Array.Copy(receiveBuffer, 0, newArray, 0, result.Count);
+#else
var result =
await webSocket.ReceiveAsync(new Memory(receiveBuffer), token);
if (result is not { MessageType: WebSocketMessageType.Binary, EndOfMessage: true })
@@ -289,6 +300,7 @@ private async Task ReceiveMessages(ClientWebSocket webSocket, CancellationToken
byte[] newArray = new byte[result.Count];
Array.Copy(receiveBuffer, 0, newArray, 0, result.Count);
+#endif
Task.Run(() => this.HandleMessage(newArray)).FireAndForgetSafeAsync(this.logger);
}
diff --git a/src/FishyFlip/FishyFlip.csproj b/src/FishyFlip/FishyFlip.csproj
index 4cb0a362..0441646c 100644
--- a/src/FishyFlip/FishyFlip.csproj
+++ b/src/FishyFlip/FishyFlip.csproj
@@ -1,7 +1,7 @@
-
+
- net7.0;net8.0
+ netstandard2.0;netstandard2.1;net7.0;net8.0
enable
enable
true
@@ -16,4 +16,9 @@
+
+
+
+
+
diff --git a/src/FishyFlip/Globals.cs b/src/FishyFlip/Globals.cs
index 4d6f37c1..8c2d68f9 100644
--- a/src/FishyFlip/Globals.cs
+++ b/src/FishyFlip/Globals.cs
@@ -22,3 +22,16 @@
global using Microsoft.IdentityModel.Tokens;
global using PeterO.Cbor;
global using ATCid = System.String;
+
+#if NETSTANDARD
+namespace System.Runtime.CompilerServices
+{
+#pragma warning disable SA1600 // Elements should be documented
+#pragma warning disable SA1649 // File name should match first type name
+ internal static class IsExternalInit
+ {
+ }
+#pragma warning restore SA1649 // File name should match first type name
+#pragma warning restore SA1600 // Elements should be documented
+}
+#endif
\ No newline at end of file
diff --git a/src/FishyFlip/Tools/ExceptionExtensions.cs b/src/FishyFlip/Tools/ExceptionExtensions.cs
index b2eab5d3..8b5eaf44 100644
--- a/src/FishyFlip/Tools/ExceptionExtensions.cs
+++ b/src/FishyFlip/Tools/ExceptionExtensions.cs
@@ -18,7 +18,11 @@ internal static class ExceptionExtensions
/// Thrown when the provided object is null.
internal static T ThrowIfNull(this T? t)
{
- ArgumentNullException.ThrowIfNull(t);
+ if (t == null)
+ {
+ throw new ArgumentNullException(nameof(t));
+ }
+
return t;
}
}
diff --git a/src/FishyFlip/Tools/HandleValidator.cs b/src/FishyFlip/Tools/HandleValidator.cs
index 91f5284f..1a412fa2 100644
--- a/src/FishyFlip/Tools/HandleValidator.cs
+++ b/src/FishyFlip/Tools/HandleValidator.cs
@@ -52,7 +52,7 @@ internal static bool EnsureValidHandle(string handle, ILogger? logger = default)
return false;
}
- if (l.EndsWith('-') || l.StartsWith('-'))
+ if (l.EndsWith("-") || l.StartsWith("-"))
{
logger?.LogError("Handle parts can not start or end with hyphens");
return false;
diff --git a/src/FishyFlip/Tools/HttpClientExtensions.cs b/src/FishyFlip/Tools/HttpClientExtensions.cs
index 00e7921c..0450f016 100644
--- a/src/FishyFlip/Tools/HttpClientExtensions.cs
+++ b/src/FishyFlip/Tools/HttpClientExtensions.cs
@@ -45,7 +45,11 @@ internal static async Task> Post(
return atError!;
}
+#if NETSTANDARD
+ string response = await message.Content.ReadAsStringAsync();
+#else
string response = await message.Content.ReadAsStringAsync(cancellationToken);
+#endif
if (response.IsNullOrEmpty() && message.IsSuccessStatusCode)
{
response = "{ }";
@@ -85,7 +89,11 @@ internal static async Task> Post(
return atError!;
}
+#if NETSTANDARD
+ string response = await message.Content.ReadAsStringAsync();
+#else
string response = await message.Content.ReadAsStringAsync(cancellationToken);
+#endif
if (response.IsNullOrEmpty() && message.IsSuccessStatusCode)
{
response = "{ }";
@@ -123,7 +131,11 @@ internal static async Task> Post(
return atError!;
}
+#if NETSTANDARD
+ string response = await message.Content.ReadAsStringAsync();
+#else
string response = await message.Content.ReadAsStringAsync(cancellationToken);
+#endif
if (response.IsNullOrEmpty() && message.IsSuccessStatusCode)
{
response = "{ }";
@@ -158,8 +170,13 @@ internal static async Task> Post(
return atError!;
}
+#if NETSTANDARD
+ var blob = await message.Content.ReadAsByteArrayAsync();
+ string response = await message.Content.ReadAsStringAsync();
+#else
var blob = await message.Content.ReadAsByteArrayAsync(cancellationToken);
string response = await message.Content.ReadAsStringAsync(cancellationToken);
+#endif
logger?.LogDebug($"GET BLOB {url}: {response}");
return new Blob(blob);
@@ -191,7 +208,11 @@ internal static async Task> Post(
return atError!;
}
+#if NETSTANDARD
+ using var stream = await message.Content.ReadAsStreamAsync();
+#else
await using var stream = await message.Content.ReadAsStreamAsync(cancellationToken);
+#endif
await CarDecoder.DecodeCarAsync(stream, progress);
return new Success();
}
@@ -226,11 +247,17 @@ internal static async Task> Post(
}
var fileDownload = Path.Combine(filePath, StringExtensions.GenerateValidFilename(fileName));
- await using (var content = File.Create(fileDownload))
+#if NETSTANDARD
+ using var content = File.Create(fileDownload);
+ using var stream = await message.Content.ReadAsStreamAsync();
+ await stream.CopyToAsync(content);
+#else
+ await using (FileStream content = File.Create(fileDownload))
{
await using var stream = await message.Content.ReadAsStreamAsync(cancellationToken);
await stream.CopyToAsync(content, cancellationToken);
}
+#endif
return new Success();
}
@@ -262,7 +289,11 @@ internal static async Task> Post(
return atError!;
}
+#if NETSTANDARD
+ string response = await message.Content.ReadAsStringAsync();
+#else
string response = await message.Content.ReadAsStringAsync(cancellationToken);
+#endif
if (response.IsNullOrEmpty() && message.IsSuccessStatusCode)
{
response = "{ }";
@@ -274,7 +305,11 @@ internal static async Task> Post(
private static async Task CreateError(HttpResponseMessage message, JsonSerializerOptions options, CancellationToken cancellationToken, ILogger? logger = default)
{
+#if NETSTANDARD
+ string response = await message.Content.ReadAsStringAsync();
+#else
string response = await message.Content.ReadAsStringAsync(cancellationToken);
+#endif
ATError atError;
ErrorDetail? detail = default;
if (string.IsNullOrEmpty(response))
diff --git a/src/FishyFlip/Tools/StreamExtensions.cs b/src/FishyFlip/Tools/StreamExtensions.cs
new file mode 100644
index 00000000..c5301b3b
--- /dev/null
+++ b/src/FishyFlip/Tools/StreamExtensions.cs
@@ -0,0 +1,42 @@
+//
+// Copyright (c) Drastic Actions. All rights reserved.
+//
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace FishyFlip.Tools;
+
+///
+/// Stream Extensions.
+///
+public static class StreamExtensions
+{
+#if NETSTANDARD
+ ///
+ /// Asynchronously reads exactly the specified number of bytes from the stream into the buffer.
+ ///
+ /// The stream to read from.
+ /// The buffer to store the read bytes.
+ /// The zero-based byte offset in the buffer at which to begin storing the data read from the stream.
+ /// The maximum number of bytes to read.
+ /// A task representing the asynchronous operation.
+ public static async Task ReadExactlyAsync(this Stream stream, byte[] buffer, int offset, int count)
+ {
+ int totalRead = 0;
+ while (totalRead < count)
+ {
+ int bytesRead = await stream.ReadAsync(buffer, offset + totalRead, count - totalRead);
+ if (bytesRead == 0)
+ {
+ throw new EndOfStreamException("End of stream reached before fulfilling read request.");
+ }
+
+ totalRead += bytesRead;
+ }
+ }
+#endif
+}
diff --git a/src/FishyFlip/Tools/SystemImplementations.cs b/src/FishyFlip/Tools/SystemImplementations.cs
new file mode 100644
index 00000000..03e89ee6
--- /dev/null
+++ b/src/FishyFlip/Tools/SystemImplementations.cs
@@ -0,0 +1,265 @@
+//
+// Copyright (c) Drastic Actions. All rights reserved.
+//
+
+#pragma warning disable SA1403 // File may only contain a single namespace
+#pragma warning disable SA1600 // Elements should be documented
+#pragma warning disable SA1649 // File name should match first type name
+#pragma warning disable SA1309 // Field names should not begin with underscore
+#pragma warning disable SA1101 // Prefix local calls with this
+#pragma warning disable SA1503 // Braces should not be omitted
+#pragma warning disable SA1623 // Property summary documentation should match accessors
+#pragma warning disable SA1615 // Element return value should be documented
+#pragma warning disable SA1201 // Elements should appear in the correct order
+#pragma warning disable SA1611 // Element parameters should be documented
+#pragma warning disable SA1204 // Static elements should appear before instance elements
+
+#if NETSTANDARD2_0
+using System.Runtime.CompilerServices;
+
+namespace System
+{
+ internal readonly struct Index : IEquatable
+ {
+ private readonly int _value;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Index(int value, bool fromEnd = false)
+ {
+ if (value < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative");
+ }
+
+ if (fromEnd)
+ _value = ~value;
+ else
+ _value = value;
+ }
+
+ // The following private constructors mainly created for perf reason to avoid the checks
+ private Index(int value)
+ {
+ _value = value;
+ }
+
+ /// Create an Index pointing at first element.
+ public static Index Start => new Index(0);
+
+ /// Create an Index pointing at beyond last element.
+ public static Index End => new Index(~0);
+
+ /// Create an Index from the start at the position indicated by the value.
+ /// The index value from the start.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Index FromStart(int value)
+ {
+ if (value < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative");
+ }
+
+ return new Index(value);
+ }
+
+ /// Create an Index from the end at the position indicated by the value.
+ /// The index value from the end.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Index FromEnd(int value)
+ {
+ if (value < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative");
+ }
+
+ return new Index(~value);
+ }
+
+ /// Returns the index value.
+ public int Value
+ {
+ get
+ {
+ if (_value < 0)
+ {
+ return ~_value;
+ }
+ else
+ {
+ return _value;
+ }
+ }
+ }
+
+ /// Indicates whether the index is from the start or the end.
+ public bool IsFromEnd => _value < 0;
+
+ /// Calculate the offset from the start using the giving collection length.
+ /// The length of the collection that the Index will be used with. length has to be a positive value.
+ ///
+ /// For performance reason, we don't validate the input length parameter and the returned offset value against negative values.
+ /// we don't validate either the returned offset is greater than the input length.
+ /// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and
+ /// then used to index a collection will get out of range exception which will be same affect as the validation.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public int GetOffset(int length)
+ {
+ var offset = _value;
+ if (IsFromEnd)
+ {
+ offset += length + 1;
+ }
+
+ return offset;
+ }
+
+ public override bool Equals(object? value) => value is Index && _value == ((Index)value)._value;
+
+ public bool Equals(Index other) => _value == other._value;
+
+ /// Returns the hash code for this instance.
+ public override int GetHashCode() => _value;
+
+ /// Converts integer number to an Index.
+ public static implicit operator Index(int value) => FromStart(value);
+
+ /// Converts the value of the current Index object to its equivalent string representation.
+ public override string ToString()
+ {
+ if (IsFromEnd)
+ return "^" + ((uint)Value).ToString();
+
+ return ((uint)Value).ToString();
+ }
+ }
+
+ /// Represent a range has start and end indexes.
+ ///
+ /// Range is used by the C# compiler to support the range syntax.
+ ///
+ /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 };
+ /// int[] subArray1 = someArray[0..2]; // { 1, 2 }
+ /// int[] subArray2 = someArray[1..^0]; // { 2, 3, 4, 5 }
+ ///
+ ///
+ internal readonly struct Range : IEquatable
+ {
+ /// Represent the inclusive start index of the Range.
+ public Index Start { get; }
+
+ /// Represent the exclusive end index of the Range.
+ public Index End { get; }
+
+ public Range(Index start, Index end)
+ {
+ Start = start;
+ End = end;
+ }
+
+ /// Indicates whether the current Range object is equal to another object of the same type.
+ /// An object to compare with this object.
+ public override bool Equals(object? value) =>
+ value is Range r &&
+ r.Start.Equals(Start) &&
+ r.End.Equals(End);
+
+ /// Indicates whether the current Range object is equal to another Range object.
+ /// An object to compare with this object.
+ public bool Equals(Range other) => other.Start.Equals(Start) && other.End.Equals(End);
+
+ /// Returns the hash code for this instance.
+ public override int GetHashCode()
+ {
+ return (Start.GetHashCode() * 31) + End.GetHashCode();
+ }
+
+ /// Converts the value of the current Range object to its equivalent string representation.
+ public override string ToString()
+ {
+ return Start + ".." + End;
+ }
+
+ /// Create a Range object starting from start index to the end of the collection.
+ public static Range StartAt(Index start) => new Range(start, Index.End);
+
+ /// Create a Range object starting from first element in the collection to the end Index.
+ public static Range EndAt(Index end) => new Range(Index.Start, end);
+
+ /// Create a Range object starting from first element to the end.
+ public static Range All => new Range(Index.Start, Index.End);
+
+ /// Calculate the start offset and length of range object using a collection length.
+ /// The length of the collection that the range will be used with. length has to be a positive value.
+ ///
+ /// For performance reason, we don't validate the input length parameter against negative values.
+ /// It is expected Range will be used with collections which always have non negative length/count.
+ /// We validate the range is inside the length scope though.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public (int Offset, int Length) GetOffsetAndLength(int length)
+ {
+ int start;
+ var startIndex = Start;
+ if (startIndex.IsFromEnd)
+ start = length - startIndex.Value;
+ else
+ start = startIndex.Value;
+
+ int end;
+ var endIndex = End;
+ if (endIndex.IsFromEnd)
+ end = length - endIndex.Value;
+ else
+ end = endIndex.Value;
+
+ if ((uint)end > (uint)length || (uint)start > (uint)end)
+ {
+ throw new ArgumentOutOfRangeException(nameof(length));
+ }
+
+ return (start, end - start);
+ }
+ }
+}
+
+namespace System.Runtime.CompilerServices
+{
+ internal static class RuntimeHelpers
+ {
+ ///
+ /// Slices the specified array using the specified range.
+ ///
+#pragma warning disable SA1618 // Generic type parameters should be documented
+ public static T[] GetSubArray(T[] array, Range range)
+#pragma warning restore SA1618 // Generic type parameters should be documented
+ {
+ if (array == null)
+ {
+ throw new ArgumentNullException(nameof(array));
+ }
+
+ (int offset, int length) = range.GetOffsetAndLength(array.Length);
+
+ if (default(T) != null || typeof(T[]) == array.GetType())
+ {
+ if (length == 0)
+ {
+ return Array.Empty();
+ }
+
+ var dest = new T[length];
+ Array.Copy(array, offset, dest, 0, length);
+ return dest;
+ }
+ else
+ {
+ // The array is actually a U[] where U:T.
+ var dest = (T[])Array.CreateInstance(array.GetType().GetElementType(), length);
+ Array.Copy(array, offset, dest, 0, length);
+ return dest;
+ }
+ }
+ }
+}
+#endif
\ No newline at end of file