Conversation
Reviewer's GuideThis pull request introduces a new TcpSocket extension for BootstrapBlazor, including a DI-integrated TCP client implementation with timeouts and auto-reconnect, data package adapters and handlers, byte-to-entity converters, utility helpers, and comprehensive unit tests. Class diagram for TcpSocket client and provider interfaces and implementationsclassDiagram
class ITcpSocketClient {
+bool IsConnected
+TcpSocketClientOptions Options
+IPEndPoint LocalEndPoint
+Func<ReadOnlyMemory<byte>, ValueTask>? ReceivedCallBack
+Func<Task>? OnConnecting
+Func<Task>? OnConnected
+ValueTask<bool> ConnectAsync(IPEndPoint, CancellationToken)
+ValueTask<bool> SendAsync(ReadOnlyMemory<byte>, CancellationToken)
+ValueTask<Memory<byte>> ReceiveAsync(CancellationToken)
+ValueTask CloseAsync()
}
class DefaultTcpSocketClient {
-ITcpSocketClientProvider? SocketClientProvider
-ILogger? Logger
+TcpSocketClientOptions Options
+bool IsConnected
+IPEndPoint LocalEndPoint
+Func<ReadOnlyMemory<byte>, ValueTask>? ReceivedCallBack
+Func<Task>? OnConnecting
+Func<Task>? OnConnected
+ValueTask<bool> ConnectAsync(IPEndPoint, CancellationToken)
+ValueTask<bool> SendAsync(ReadOnlyMemory<byte>, CancellationToken)
+ValueTask<Memory<byte>> ReceiveAsync(CancellationToken)
+ValueTask CloseAsync()
+ValueTask DisposeAsync()
}
class ITcpSocketClientProvider {
+bool IsConnected
+IPEndPoint LocalEndPoint
+ValueTask<bool> ConnectAsync(IPEndPoint, CancellationToken)
+ValueTask<bool> SendAsync(ReadOnlyMemory<byte>, CancellationToken)
+ValueTask<int> ReceiveAsync(Memory<byte>, CancellationToken)
+ValueTask CloseAsync()
}
class DefaultTcpSocketClientProvider {
-TcpClient? _client
+bool IsConnected
+IPEndPoint LocalEndPoint
+ValueTask<bool> ConnectAsync(IPEndPoint, CancellationToken)
+ValueTask<bool> SendAsync(ReadOnlyMemory<byte>, CancellationToken)
+ValueTask<int> ReceiveAsync(Memory<byte>, CancellationToken)
+ValueTask CloseAsync()
}
class ITcpSocketFactory {
+ITcpSocketClient GetOrCreate(string, Action<TcpSocketClientOptions>)
+ITcpSocketClient? Remove(string)
}
class DefaultTcpSocketFactory {
-ConcurrentDictionary<string, ITcpSocketClient> _pool
+ITcpSocketClient GetOrCreate(string, Action<TcpSocketClientOptions>)
+ITcpSocketClient? Remove(string)
+ValueTask DisposeAsync()
}
ITcpSocketClient <|.. DefaultTcpSocketClient
ITcpSocketClientProvider <|.. DefaultTcpSocketClientProvider
ITcpSocketFactory <|.. DefaultTcpSocketFactory
DefaultTcpSocketClient --> ITcpSocketClientProvider
DefaultTcpSocketClient --> ITcpSocketFactory
DefaultTcpSocketFactory --> ITcpSocketClient
Class diagram for data package adapters, handlers, and convertersclassDiagram
class IDataPackageAdapter {
+Func<ReadOnlyMemory<byte>, ValueTask>? ReceivedCallBack
+IDataPackageHandler? DataPackageHandler
+ValueTask HandlerAsync(ReadOnlyMemory<byte>, CancellationToken)
+bool TryConvertTo<TEntity>(ReadOnlyMemory<byte>, IDataConverter<TEntity>, out TEntity?)
}
class DataPackageAdapter {
+Func<ReadOnlyMemory<byte>, ValueTask>? ReceivedCallBack
+IDataPackageHandler? DataPackageHandler
+ValueTask HandlerAsync(ReadOnlyMemory<byte>, CancellationToken)
+bool TryConvertTo<TEntity>(ReadOnlyMemory<byte>, IDataConverter<TEntity>, out TEntity?)
}
class IDataPackageHandler {
+Func<ReadOnlyMemory<byte>, ValueTask>? ReceivedCallBack
+ValueTask HandlerAsync(ReadOnlyMemory<byte>, CancellationToken)
}
class DataPackageHandlerBase {
+Func<ReadOnlyMemory<byte>, ValueTask>? ReceivedCallBack
+ValueTask HandlerAsync(ReadOnlyMemory<byte>, CancellationToken)
-Memory<byte> _lastReceiveBuffer
-ReadOnlyMemory<byte> ConcatBuffer(ReadOnlyMemory<byte>)
-void SlicePackage(ReadOnlyMemory<byte>, int)
}
class DelimiterDataPackageHandler {
-ReadOnlyMemory<byte> _delimiter
+ValueTask HandlerAsync(ReadOnlyMemory<byte>, CancellationToken)
}
class FixLengthDataPackageHandler {
-Memory<byte> _data
-int _receivedLength
+ValueTask HandlerAsync(ReadOnlyMemory<byte>, CancellationToken)
}
class IDataConverter {
}
class IDataConverter~TEntity~ {
+bool TryConvertTo(ReadOnlyMemory<byte>, out TEntity?)
}
class DataConverter~TEntity~ {
+bool TryConvertTo(ReadOnlyMemory<byte>, out TEntity?)
-TEntity CreateEntity()
-bool Parse(ReadOnlyMemory<byte>, TEntity)
}
IDataPackageAdapter <|.. DataPackageAdapter
IDataPackageHandler <|.. DataPackageHandlerBase
DataPackageHandlerBase <|-- DelimiterDataPackageHandler
DataPackageHandlerBase <|-- FixLengthDataPackageHandler
IDataConverter <|.. IDataConverter~TEntity~
IDataConverter~TEntity~ <|.. DataConverter~TEntity~
DataPackageAdapter --> IDataPackageHandler
DataPackageAdapter --> IDataConverter~TEntity~
DataPackageHandlerBase --> IDataPackageHandler
Class diagram for data property and type convertersclassDiagram
class DataPropertyConverterAttribute {
+Type? Type
+int Offset
+int Length
+string? EncodingName
+Type? ConverterType
+object?[]? ConverterParameters
}
class DataTypeConverterAttribute {
+Type? Type
}
class IDataPropertyConverter {
+object? Convert(ReadOnlyMemory<byte> data)
}
class DataBoolConverter {
+object? Convert(ReadOnlyMemory<byte> data)
}
class DataStringConverter {
+object? Convert(ReadOnlyMemory<byte> data)
}
class DataByteArrayConverter {
+object? Convert(ReadOnlyMemory<byte> data)
}
class DataInt16BigEndianConverter {
+object? Convert(ReadOnlyMemory<byte> data)
}
class DataInt16LittleEndianConverter {
+object? Convert(ReadOnlyMemory<byte> data)
}
class DataInt32BigEndianConverter {
+object? Convert(ReadOnlyMemory<byte> data)
}
class DataInt32LittleEndianConverter {
+object? Convert(ReadOnlyMemory<byte> data)
}
class DataInt64BigEndianConverter {
+object? Convert(ReadOnlyMemory<byte> data)
}
class DataInt64LittleEndianConverter {
+object? Convert(ReadOnlyMemory<byte> data)
}
class DataUInt16BigEndianConverter {
+object? Convert(ReadOnlyMemory<byte> data)
}
class DataUInt16LittleEndianConverter {
+object? Convert(ReadOnlyMemory<byte> data)
}
class DataUInt32BigEndianConverter {
+object? Convert(ReadOnlyMemory<byte> data)
}
class DataUInt32LittleEndianConverter {
+object? Convert(ReadOnlyMemory<byte> data)
}
class DataUInt64BigEndianConverter {
+object? Convert(ReadOnlyMemory<byte> data)
}
class DataUInt64LittleEndianConverter {
+object? Convert(ReadOnlyMemory<byte> data)
}
class DataSingleBigEndianConverter {
+object? Convert(ReadOnlyMemory<byte> data)
}
class DataSingleLittleEndianConverter {
+object? Convert(ReadOnlyMemory<byte> data)
}
class DataDoubleBigEndianConverter {
+object? Convert(ReadOnlyMemory<byte> data)
}
class DataDoubleLittleEndianConverter {
+object? Convert(ReadOnlyMemory<byte> data)
}
class DataEnumConverter {
+object? Convert(ReadOnlyMemory<byte> data)
}
IDataPropertyConverter <|.. DataBoolConverter
IDataPropertyConverter <|.. DataStringConverter
IDataPropertyConverter <|.. DataByteArrayConverter
IDataPropertyConverter <|.. DataInt16BigEndianConverter
IDataPropertyConverter <|.. DataInt16LittleEndianConverter
IDataPropertyConverter <|.. DataInt32BigEndianConverter
IDataPropertyConverter <|.. DataInt32LittleEndianConverter
IDataPropertyConverter <|.. DataInt64BigEndianConverter
IDataPropertyConverter <|.. DataInt64LittleEndianConverter
IDataPropertyConverter <|.. DataUInt16BigEndianConverter
IDataPropertyConverter <|.. DataUInt16LittleEndianConverter
IDataPropertyConverter <|.. DataUInt32BigEndianConverter
IDataPropertyConverter <|.. DataUInt32LittleEndianConverter
IDataPropertyConverter <|.. DataUInt64BigEndianConverter
IDataPropertyConverter <|.. DataUInt64LittleEndianConverter
IDataPropertyConverter <|.. DataSingleBigEndianConverter
IDataPropertyConverter <|.. DataSingleLittleEndianConverter
IDataPropertyConverter <|.. DataDoubleBigEndianConverter
IDataPropertyConverter <|.. DataDoubleLittleEndianConverter
IDataPropertyConverter <|.. DataEnumConverter
DataPropertyConverterAttribute --> IDataPropertyConverter
DataTypeConverterAttribute --> IDataConverter
Class diagram for TcpSocketClientOptions and utilityclassDiagram
class TcpSocketClientOptions {
+int ReceiveBufferSize
+bool IsAutoReceive
+int ConnectTimeout
+int SendTimeout
+int ReceiveTimeout
+IPEndPoint LocalEndPoint
+bool EnableLog
+bool IsAutoReconnect
+int ReconnectInterval
}
class TcpSocketUtility {
+static IPAddress ConvertToIPAddress(string)
+static IPEndPoint ConvertToIpEndPoint(string, int)
}
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey @ArgoZhang - I've reviewed your changes - here's some feedback:
- Consider refactoring DefaultTcpSocketClient to inject ILogger and ITcpSocketClientProvider directly via constructor parameters instead of resolving them from ServiceProvider at runtime—this will simplify dependency management and improve testability.
- In DefaultTcpSocketFactory.Remove, the removed client is returned but not disposed; consider disposing the client immediately upon removal or provide an overload that does so to avoid potential resource leaks.
- The multiple SetDataPackageAdapter overloads in ITcpSocketClientExtensions share a lot of common logic; consider consolidating them into fewer methods or using a single generic adapter registration approach to reduce duplication and simplify the API surface.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Consider refactoring DefaultTcpSocketClient to inject ILogger<DefaultTcpSocketClient> and ITcpSocketClientProvider directly via constructor parameters instead of resolving them from ServiceProvider at runtime—this will simplify dependency management and improve testability.
- In DefaultTcpSocketFactory.Remove, the removed client is returned but not disposed; consider disposing the client immediately upon removal or provide an overload that does so to avoid potential resource leaks.
- The multiple SetDataPackageAdapter overloads in ITcpSocketClientExtensions share a lot of common logic; consider consolidating them into fewer methods or using a single generic adapter registration approach to reduce duplication and simplify the API surface.
## Individual Comments
### Comment 1
<location> `src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataDoubleBigEndianConverter.cs:21` </location>
<code_context>
+ public object? Convert(ReadOnlyMemory<byte> data)
+ {
+ double ret = 0;
+ if (data.Length <= 8)
+ {
+ Span<byte> paddedSpan = stackalloc byte[8];
</code_context>
<issue_to_address>
Zero-padding for data shorter than 8 bytes may be unsafe.
Validating that data is exactly 8 bytes or clearly documenting the padding behavior would help prevent unintended results.
</issue_to_address>
<suggested_fix>
<<<<<<< SEARCH
/// <param name="data"></param>
public object? Convert(ReadOnlyMemory<byte> data)
{
double ret = 0;
if (data.Length <= 8)
{
Span<byte> paddedSpan = stackalloc byte[8];
data.Span.CopyTo(paddedSpan[(8 - data.Length)..]);
if (BinaryPrimitives.TryReadDoubleBigEndian(paddedSpan, out var v))
{
ret = v;
}
}
return ret;
}
=======
/// <param name="data">A memory buffer containing exactly 8 bytes representing a double in big-endian order.</param>
/// <remarks>
/// The input <paramref name="data"/> must be exactly 8 bytes. Otherwise, the method returns <c>null</c>.
/// </remarks>
public object? Convert(ReadOnlyMemory<byte> data)
{
if (data.Length != 8)
{
// Input must be exactly 8 bytes for a valid double.
return null;
}
if (BinaryPrimitives.TryReadDoubleBigEndian(data.Span, out var v))
{
return v;
}
return null;
}
>>>>>>> REPLACE
</suggested_fix>
### Comment 2
<location> `src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataDoubleLittleEndianConverter.cs:21` </location>
<code_context>
+ public object? Convert(ReadOnlyMemory<byte> data)
+ {
+ double ret = 0;
+ if (data.Length <= 8)
+ {
+ Span<byte> paddedSpan = stackalloc byte[8];
</code_context>
<issue_to_address>
Zero-padding for data shorter than 8 bytes may be unsafe.
Validate that data is exactly 8 bytes or clearly document the behavior when it is not.
</issue_to_address>
<suggested_fix>
<<<<<<< SEARCH
/// <param name="data"></param>
public object? Convert(ReadOnlyMemory<byte> data)
{
double ret = 0;
if (data.Length <= 8)
{
Span<byte> paddedSpan = stackalloc byte[8];
data.Span.CopyTo(paddedSpan[(8 - data.Length)..]);
if (BinaryPrimitives.TryReadDoubleLittleEndian(paddedSpan, out var v))
{
ret = v;
}
}
return ret;
}
=======
/// <param name="data">A memory buffer containing exactly 8 bytes representing a double in little-endian format. If the length is not 8, the method returns null.</param>
public object? Convert(ReadOnlyMemory<byte> data)
{
if (data.Length != 8)
{
// Data must be exactly 8 bytes to convert to double
return null;
}
if (BinaryPrimitives.TryReadDoubleLittleEndian(data.Span, out var v))
{
return v;
}
return null;
}
>>>>>>> REPLACE
</suggested_fix>
### Comment 3
<location> `src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataInt16BigEndianConverter.cs:21` </location>
<code_context>
+ public object? Convert(ReadOnlyMemory<byte> data)
+ {
+ short ret = 0;
+ if (data.Length <= 2)
+ {
+ Span<byte> paddedSpan = stackalloc byte[2];
</code_context>
<issue_to_address>
Zero-padding for data shorter than 2 bytes may be unsafe.
Validating that data is exactly 2 bytes or clearly documenting the behavior is recommended to avoid unexpected results.
</issue_to_address>
<suggested_fix>
<<<<<<< SEARCH
/// <param name="data"></param>
public object? Convert(ReadOnlyMemory<byte> data)
{
short ret = 0;
if (data.Length <= 2)
{
Span<byte> paddedSpan = stackalloc byte[2];
data.Span.CopyTo(paddedSpan[(2 - data.Length)..]);
if (BinaryPrimitives.TryReadInt16BigEndian(paddedSpan, out var v))
{
ret = v;
}
}
return ret;
}
=======
/// <param name="data">A 2-byte big-endian encoded Int16 value. Throws or returns null if not exactly 2 bytes.</param>
public object? Convert(ReadOnlyMemory<byte> data)
{
if (data.Length != 2)
{
// Option 1: throw new ArgumentException("Data must be exactly 2 bytes for Int16 conversion.", nameof(data));
// Option 2: return null;
throw new ArgumentException("Data must be exactly 2 bytes for Int16 conversion.", nameof(data));
}
if (BinaryPrimitives.TryReadInt16BigEndian(data.Span, out var v))
{
return v;
}
return null;
}
>>>>>>> REPLACE
</suggested_fix>
### Comment 4
<location> `src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataInt16LittleEndianConverter.cs:21` </location>
<code_context>
+ public object? Convert(ReadOnlyMemory<byte> data)
+ {
+ short ret = 0;
+ if (data.Length <= 2)
+ {
+ Span<byte> paddedSpan = stackalloc byte[2];
</code_context>
<issue_to_address>
Zero-padding for data shorter than 2 bytes may be unsafe.
Validating that data is exactly 2 bytes or explicitly handling shorter inputs would help prevent unintended results.
</issue_to_address>
<suggested_fix>
<<<<<<< SEARCH
public object? Convert(ReadOnlyMemory<byte> data)
{
short ret = 0;
if (data.Length <= 2)
{
Span<byte> paddedSpan = stackalloc byte[2];
data.Span.CopyTo(paddedSpan[(2 - data.Length)..]);
if (BinaryPrimitives.TryReadInt16LittleEndian(paddedSpan, out var v))
{
ret = v;
}
}
return ret;
}
=======
public object? Convert(ReadOnlyMemory<byte> data)
{
if (data.Length != 2)
{
// Input must be exactly 2 bytes for Int16 conversion
return null;
}
if (BinaryPrimitives.TryReadInt16LittleEndian(data.Span, out var value))
{
return value;
}
return null;
}
>>>>>>> REPLACE
</suggested_fix>
### Comment 5
<location> `src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataSingleBigEndianConverter.cs:21` </location>
<code_context>
+ public object? Convert(ReadOnlyMemory<byte> data)
+ {
+ var ret = 0;
+ if (data.Length <= 4)
+ {
+ Span<byte> paddedSpan = stackalloc byte[4];
</code_context>
<issue_to_address>
Zero-padding for data shorter than 4 bytes may be unsafe.
Validate that data is exactly 4 bytes, or clearly document and handle cases where it is shorter to avoid unintended results.
</issue_to_address>
<suggested_fix>
<<<<<<< SEARCH
/// <param name="data"></param>
public object? Convert(ReadOnlyMemory<byte> data)
{
float ret = 0;
if (data.Length <= 4)
{
Span<byte> paddedSpan = stackalloc byte[4];
data.Span.CopyTo(paddedSpan[(4 - data.Length)..]);
if (BinaryPrimitives.TryReadSingleBigEndian(paddedSpan, out var v))
{
ret = v;
}
}
return ret;
}
=======
/// <param name="data">A 4-byte big-endian IEEE 754 single-precision float value.</param>
/// <returns>The converted float value, or null if input is not exactly 4 bytes.</returns>
public object? Convert(ReadOnlyMemory<byte> data)
{
if (data.Length != 4)
{
// Optionally, throw an exception instead of returning null if that's preferred:
// throw new ArgumentException("Input data must be exactly 4 bytes.", nameof(data));
return null;
}
if (BinaryPrimitives.TryReadSingleBigEndian(data.Span, out var v))
{
return v;
}
return null;
}
>>>>>>> REPLACE
</suggested_fix>
### Comment 6
<location> `src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataUInt64BigEndianConverter.cs:21` </location>
<code_context>
+ public object? Convert(ReadOnlyMemory<byte> data)
+ {
+ double ret = 0;
+ if (data.Length <= 8)
+ {
+ Span<byte> paddedSpan = stackalloc byte[8];
</code_context>
<issue_to_address>
Zero-padding for data shorter than 8 bytes may be unsafe.
Validating that data is exactly 8 bytes or clearly documenting the padding behavior would help prevent unintended results.
</issue_to_address>
<suggested_fix>
<<<<<<< SEARCH
/// <param name="data"></param>
public object? Convert(ReadOnlyMemory<byte> data)
{
ulong ret = 0;
if (data.Length <= 8)
{
Span<byte> paddedSpan = stackalloc byte[8];
data.Span.CopyTo(paddedSpan[(8 - data.Length)..]);
if (BinaryPrimitives.TryReadUInt64BigEndian(paddedSpan, out var v))
{
ret = v;
}
}
return ret;
}
=======
/// <param name="data">A ReadOnlyMemory of exactly 8 bytes representing a big-endian UInt64 value. Returns null if not exactly 8 bytes.</param>
public object? Convert(ReadOnlyMemory<byte> data)
{
if (data.Length != 8)
{
// Only accept exactly 8 bytes to avoid unsafe zero-padding
return null;
}
if (BinaryPrimitives.TryReadUInt64BigEndian(data.Span, out var v))
{
return v;
}
return null;
}
>>>>>>> REPLACE
</suggested_fix>
### Comment 7
<location> `src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataUInt64LittleEndianConverter.cs:21` </location>
<code_context>
+ public object? Convert(ReadOnlyMemory<byte> data)
+ {
+ double ret = 0;
+ if (data.Length <= 8)
+ {
+ Span<byte> paddedSpan = stackalloc byte[8];
</code_context>
<issue_to_address>
Zero-padding for data shorter than 8 bytes may be unsafe.
Validate that data is exactly 8 bytes, or clearly document and handle cases where it is not, to avoid unintended results.
</issue_to_address>
<suggested_fix>
<<<<<<< SEARCH
/// <param name="data"></param>
public object? Convert(ReadOnlyMemory<byte> data)
{
ulong ret = 0;
if (data.Length <= 8)
{
Span<byte> paddedSpan = stackalloc byte[8];
data.Span.CopyTo(paddedSpan[(8 - data.Length)..]);
if (BinaryPrimitives.TryReadUInt64LittleEndian(paddedSpan, out var v))
{
ret = v;
}
}
return ret;
}
=======
/// <param name="data">A ReadOnlyMemory containing exactly 8 bytes representing a UInt64 in little-endian order. If not exactly 8 bytes, returns null.</param>
public object? Convert(ReadOnlyMemory<byte> data)
{
if (data.Length != 8)
{
// Data must be exactly 8 bytes for UInt64 conversion
return null;
}
if (BinaryPrimitives.TryReadUInt64LittleEndian(data.Span, out var v))
{
return v;
}
return null;
}
>>>>>>> REPLACE
</suggested_fix>
### Comment 8
<location> `src/extensions/BootstrapBlazor.Socket/DataHandler/DelimiterDataPackageHandler.cs:60` </location>
<code_context>
+
+ while (data.Length > 0)
+ {
+ var index = data.Span.IndexOfAny(_delimiter.Span);
+ var segment = index == -1 ? data : data[..index];
+ var length = segment.Length + _delimiter.Length;
</code_context>
<issue_to_address>
IndexOfAny may not work as intended for multi-byte delimiters.
IndexOfAny matches any single byte from the delimiter, not the entire sequence. For multi-byte delimiters, use IndexOf to ensure correct splitting.
</issue_to_address>
### Comment 9
<location> `test/UnitTestTcpSocket/TcpSocketFactoryTest.cs:16` </location>
<code_context>
+
+public class DefaultSocketClientProviderTest
+{
+ [Fact]
+ public async Task DefaultSocketClient_Ok()
+ {
</code_context>
<issue_to_address>
Consider adding tests for invalid port numbers in ConnectAsync.
Please add test cases for out-of-range port values (e.g., -1, 70000) to verify that the client throws the correct exceptions.
Suggested implementation:
```csharp
public class TcpSocketFactoryTest
{
[Fact]
public async Task ConnectAsync_InvalidPort_ThrowsException()
{
var factory = new DefaultSocketClientProvider();
var invalidPorts = new[] { -1, 70000 };
foreach (var port in invalidPorts)
{
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () =>
{
await factory.ConnectAsync("127.0.0.1", port);
});
}
}
```
- If `DefaultSocketClientProvider` or its `ConnectAsync` method is not accessible or does not throw `ArgumentOutOfRangeException`, you may need to adjust the exception type or the way the test is written to match your implementation.
- Ensure that `DefaultSocketClientProvider` is available in the test project and that `ConnectAsync` is public and asynchronous.
</issue_to_address>
### Comment 10
<location> `test/UnitTestTcpSocket/TcpSocketPropertyConverterTest.cs:9` </location>
<code_context>
+
+public class DefaultSocketClientProviderTest
+{
+ [Fact]
+ public async Task DefaultSocketClient_Ok()
+ {
</code_context>
<issue_to_address>
Consider adding tests for invalid or insufficient data lengths in converters.
Add tests with byte arrays of incorrect lengths to verify converters handle them correctly, either by returning defaults or throwing expected exceptions.
Suggested implementation:
```csharp
[Fact]
public void UInt16Converter_Ok()
{
var converter = new DataUInt16LittleEndianConverter();
var actual = converter.Convert(new byte[] { 0xFF, 0x00 });
Assert.Equal((ushort)0xFF, actual);
}
[Fact]
public void UInt16Converter_InvalidLength_ThrowsOrDefault()
{
var converter = new DataUInt16LittleEndianConverter();
// Test with empty array
Assert.ThrowsAny<Exception>(() => converter.Convert(new byte[] { }));
// Test with single byte
Assert.ThrowsAny<Exception>(() => converter.Convert(new byte[] { 0x01 }));
}
```
```csharp
[Fact]
public void Int16Converter_Ok()
{
var converter = new DataInt16LittleEndianConverter();
var actual = converter.Convert(new byte[] { 0x01, 0x00 });
Assert.Equal((short)0x01, actual);
}
[Fact]
public void Int16Converter_InvalidLength_ThrowsOrDefault()
{
var converter = new DataInt16LittleEndianConverter();
// Test with empty array
Assert.ThrowsAny<Exception>(() => converter.Convert(new byte[] { }));
// Test with single byte
Assert.ThrowsAny<Exception>(() => converter.Convert(new byte[] { 0x01 }));
}
```
- If there are other converters (e.g., UInt32, Int32, etc.) in this file, similar tests should be added for them.
- If the converters are supposed to return a default value instead of throwing, replace `Assert.ThrowsAny<Exception>` with an assertion for the default value.
- If the converters use a specific exception type, replace `Exception` with the specific type (e.g., `ArgumentException`).
</issue_to_address>
### Comment 11
<location> `src/extensions/BootstrapBlazor.TcpSocket/Extensions/ITcpSocketClientExtensions.cs:68` </location>
<code_context>
+ /// <param name="adapter">The <see cref="IDataPackageAdapter"/> used to process incoming data.</param>
+ /// <param name="callback">A callback function invoked with the processed data. The function receives a <see cref="ReadOnlyMemory{T}"/>
+ /// containing the processed data and returns a <see cref="ValueTask"/>.</param>
+ public static void SetDataPackageAdapter(this ITcpSocketClient client, IDataPackageAdapter adapter, Func<ReadOnlyMemory<byte>, ValueTask> callback)
+ {
+ // 设置 ITcpSocketClient 的回调函数
</code_context>
<issue_to_address>
Consider replacing the multiple SetDataPackageAdapter overloads with a single generic helper method that handles all cases via an optional converter parameter and type checks.
Here’s a way to collapse all five overloads into a single generic helper.
- Use an optional converter parameter (falling back to attribute/DI lookup)
- Special-case `ReadOnlyMemory<byte>` so you still get your raw-bytes callback
- Keep a single place to wire up both the client and adapter callbacks
```csharp
public static class TcpSocketExtensions
{
public static void SetDataPackageAdapter<TEntity>(
this ITcpSocketClient client,
IDataPackageAdapter adapter,
Func<TEntity?, ValueTask> callback,
IDataConverter<TEntity>? converter = null)
{
// 1) assign client → adapter pipeline
client.ReceivedCallBack = buffer => adapter.HandlerAsync(buffer);
// 2) set up converter (if none provided, try attribute or DI)
converter ??= ResolveConverter<TEntity>(client);
// 3) wire adapter → user callback
adapter.ReceivedCallBack = async buffer =>
{
if (typeof(TEntity) == typeof(ReadOnlyMemory<byte>))
{
// raw‐bytes specialization
var raw = (TEntity?)(object)buffer;
await callback(raw);
}
else
{
TEntity? result = default;
if (converter?.TryConvertTo(buffer, out var t) == true)
{
result = t;
}
await callback(result);
}
};
}
private static IDataConverter<TEntity>? ResolveConverter<TEntity>(ITcpSocketClient client)
{
// 1) attribute on TEntity?
var attr = typeof(TEntity).GetCustomAttribute<DataTypeConverterAttribute>();
if (attr?.Type != null
&& Activator.CreateInstance(attr.Type) is IDataConverter<TEntity> fromAttr)
return fromAttr;
// 2) ask DI
if (client is IServiceProvider sp
&& sp.GetService<IOptions<DataConverterCollections>>()?.Value
.TryGetTypeConverter<TEntity>(out var diConv) == true)
{
return diConv;
}
return null;
}
}
```
Steps to apply:
1. Remove the 5 existing `SetDataPackageAdapter` overloads.
2. Copy in the single `SetDataPackageAdapter<TEntity>` above.
3. Delete the private `SetDataAdapterCallback` and `GetSocketDataConverter` helpers (their logic is now in `ResolveConverter`).
4. Any call sites using the old overloads will continue to work:
- raw‐bytes: use `SetDataPackageAdapter<ReadOnlyMemory<byte>>(…)`
- explicit converter: pass it in via the `converter` parameter
- attribute/DI lookup: omit the converter parameter entirely.
This collapses all your boilerplate into one spot, preserves all current behaviors, and keeps the public surface small.
</issue_to_address>
### Comment 12
<location> `src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataInt32BigEndianConverter.cs:12` </location>
<code_context>
+/// <summary>
+/// Sokcet 数据转换为 int 数据大端转换器
+/// </summary>
+public class DataInt32BigEndianConverter : IDataPropertyConverter
+{
+ /// <summary>
</code_context>
<issue_to_address>
Consider replacing multiple similar converter classes with a single generic numeric converter implementation.
```suggestion
You can collapse all of these almost‐identical converters into one generic implementation. For example:
```csharp
public class GenericNumericConverter<T> : IDataPropertyConverter where T : struct
{
private readonly Func<ReadOnlySpan<byte>, T> _reader;
private readonly int _size;
public GenericNumericConverter(Func<ReadOnlySpan<byte>, T> reader, int size)
{
_reader = reader;
_size = size;
}
public object? Convert(ReadOnlyMemory<byte> data)
{
if (data.Length > _size) return default(T);
Span<byte> buffer = stackalloc byte[_size];
data.Span.CopyTo(buffer.Slice(_size - data.Length));
return _reader(buffer);
}
}
```
Then replace your many single‐type classes with registrations, e.g.:
```csharp
// Big‐endian Int32
var int32Be = new GenericNumericConverter<int>(
BinaryPrimitives.ReadInt32BigEndian,
sizeof(int));
// Little‐endian UInt64
var uInt64Le = new GenericNumericConverter<ulong>(
BinaryPrimitives.ReadUInt64LittleEndian,
sizeof(ulong));
```
This centralizes the padding‐and‐read logic while preserving all existing functionality.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| /// <param name="data"></param> | ||
| public object? Convert(ReadOnlyMemory<byte> data) | ||
| { | ||
| double ret = 0; | ||
| if (data.Length <= 8) | ||
| { | ||
| Span<byte> paddedSpan = stackalloc byte[8]; | ||
| data.Span.CopyTo(paddedSpan[(8 - data.Length)..]); | ||
| if (BinaryPrimitives.TryReadDoubleBigEndian(paddedSpan, out var v)) | ||
| { | ||
| ret = v; | ||
| } | ||
| } | ||
| return ret; | ||
| } |
There was a problem hiding this comment.
suggestion (bug_risk): Zero-padding for data shorter than 8 bytes may be unsafe.
Validating that data is exactly 8 bytes or clearly documenting the padding behavior would help prevent unintended results.
| /// <param name="data"></param> | |
| public object? Convert(ReadOnlyMemory<byte> data) | |
| { | |
| double ret = 0; | |
| if (data.Length <= 8) | |
| { | |
| Span<byte> paddedSpan = stackalloc byte[8]; | |
| data.Span.CopyTo(paddedSpan[(8 - data.Length)..]); | |
| if (BinaryPrimitives.TryReadDoubleBigEndian(paddedSpan, out var v)) | |
| { | |
| ret = v; | |
| } | |
| } | |
| return ret; | |
| } | |
| /// <param name="data">A memory buffer containing exactly 8 bytes representing a double in big-endian order.</param> | |
| /// <remarks> | |
| /// The input <paramref name="data"/> must be exactly 8 bytes. Otherwise, the method returns <c>null</c>. | |
| /// </remarks> | |
| public object? Convert(ReadOnlyMemory<byte> data) | |
| { | |
| if (data.Length != 8) | |
| { | |
| // Input must be exactly 8 bytes for a valid double. | |
| return null; | |
| } | |
| if (BinaryPrimitives.TryReadDoubleBigEndian(data.Span, out var v)) | |
| { | |
| return v; | |
| } | |
| return null; | |
| } |
| /// <param name="data"></param> | ||
| public object? Convert(ReadOnlyMemory<byte> data) | ||
| { | ||
| double ret = 0; | ||
| if (data.Length <= 8) | ||
| { | ||
| Span<byte> paddedSpan = stackalloc byte[8]; | ||
| data.Span.CopyTo(paddedSpan[(8 - data.Length)..]); | ||
| if (BinaryPrimitives.TryReadDoubleLittleEndian(paddedSpan, out var v)) | ||
| { | ||
| ret = v; | ||
| } | ||
| } | ||
| return ret; | ||
| } |
There was a problem hiding this comment.
suggestion (bug_risk): Zero-padding for data shorter than 8 bytes may be unsafe.
Validate that data is exactly 8 bytes or clearly document the behavior when it is not.
| /// <param name="data"></param> | |
| public object? Convert(ReadOnlyMemory<byte> data) | |
| { | |
| double ret = 0; | |
| if (data.Length <= 8) | |
| { | |
| Span<byte> paddedSpan = stackalloc byte[8]; | |
| data.Span.CopyTo(paddedSpan[(8 - data.Length)..]); | |
| if (BinaryPrimitives.TryReadDoubleLittleEndian(paddedSpan, out var v)) | |
| { | |
| ret = v; | |
| } | |
| } | |
| return ret; | |
| } | |
| /// <param name="data">A memory buffer containing exactly 8 bytes representing a double in little-endian format. If the length is not 8, the method returns null.</param> | |
| public object? Convert(ReadOnlyMemory<byte> data) | |
| { | |
| if (data.Length != 8) | |
| { | |
| // Data must be exactly 8 bytes to convert to double | |
| return null; | |
| } | |
| if (BinaryPrimitives.TryReadDoubleLittleEndian(data.Span, out var v)) | |
| { | |
| return v; | |
| } | |
| return null; | |
| } |
| /// <param name="data"></param> | ||
| public object? Convert(ReadOnlyMemory<byte> data) | ||
| { | ||
| short ret = 0; | ||
| if (data.Length <= 2) | ||
| { | ||
| Span<byte> paddedSpan = stackalloc byte[2]; | ||
| data.Span.CopyTo(paddedSpan[(2 - data.Length)..]); | ||
| if (BinaryPrimitives.TryReadInt16BigEndian(paddedSpan, out var v)) | ||
| { | ||
| ret = v; | ||
| } | ||
| } | ||
| return ret; | ||
| } |
There was a problem hiding this comment.
suggestion (bug_risk): Zero-padding for data shorter than 2 bytes may be unsafe.
Validating that data is exactly 2 bytes or clearly documenting the behavior is recommended to avoid unexpected results.
| /// <param name="data"></param> | |
| public object? Convert(ReadOnlyMemory<byte> data) | |
| { | |
| short ret = 0; | |
| if (data.Length <= 2) | |
| { | |
| Span<byte> paddedSpan = stackalloc byte[2]; | |
| data.Span.CopyTo(paddedSpan[(2 - data.Length)..]); | |
| if (BinaryPrimitives.TryReadInt16BigEndian(paddedSpan, out var v)) | |
| { | |
| ret = v; | |
| } | |
| } | |
| return ret; | |
| } | |
| /// <param name="data">A 2-byte big-endian encoded Int16 value. Throws or returns null if not exactly 2 bytes.</param> | |
| public object? Convert(ReadOnlyMemory<byte> data) | |
| { | |
| if (data.Length != 2) | |
| { | |
| // Option 1: throw new ArgumentException("Data must be exactly 2 bytes for Int16 conversion.", nameof(data)); | |
| // Option 2: return null; | |
| throw new ArgumentException("Data must be exactly 2 bytes for Int16 conversion.", nameof(data)); | |
| } | |
| if (BinaryPrimitives.TryReadInt16BigEndian(data.Span, out var v)) | |
| { | |
| return v; | |
| } | |
| return null; | |
| } |
| public object? Convert(ReadOnlyMemory<byte> data) | ||
| { | ||
| short ret = 0; | ||
| if (data.Length <= 2) | ||
| { | ||
| Span<byte> paddedSpan = stackalloc byte[2]; | ||
| data.Span.CopyTo(paddedSpan[(2 - data.Length)..]); | ||
| if (BinaryPrimitives.TryReadInt16LittleEndian(paddedSpan, out var v)) | ||
| { | ||
| ret = v; | ||
| } | ||
| } | ||
| return ret; | ||
| } |
There was a problem hiding this comment.
suggestion (bug_risk): Zero-padding for data shorter than 2 bytes may be unsafe.
Validating that data is exactly 2 bytes or explicitly handling shorter inputs would help prevent unintended results.
| public object? Convert(ReadOnlyMemory<byte> data) | |
| { | |
| short ret = 0; | |
| if (data.Length <= 2) | |
| { | |
| Span<byte> paddedSpan = stackalloc byte[2]; | |
| data.Span.CopyTo(paddedSpan[(2 - data.Length)..]); | |
| if (BinaryPrimitives.TryReadInt16LittleEndian(paddedSpan, out var v)) | |
| { | |
| ret = v; | |
| } | |
| } | |
| return ret; | |
| } | |
| public object? Convert(ReadOnlyMemory<byte> data) | |
| { | |
| if (data.Length != 2) | |
| { | |
| // Input must be exactly 2 bytes for Int16 conversion | |
| return null; | |
| } | |
| if (BinaryPrimitives.TryReadInt16LittleEndian(data.Span, out var value)) | |
| { | |
| return value; | |
| } | |
| return null; | |
| } |
| /// <param name="data"></param> | ||
| public object? Convert(ReadOnlyMemory<byte> data) | ||
| { | ||
| float ret = 0; | ||
| if (data.Length <= 4) | ||
| { | ||
| Span<byte> paddedSpan = stackalloc byte[4]; | ||
| data.Span.CopyTo(paddedSpan[(4 - data.Length)..]); | ||
| if (BinaryPrimitives.TryReadSingleBigEndian(paddedSpan, out var v)) | ||
| { | ||
| ret = v; | ||
| } | ||
| } | ||
| return ret; | ||
| } |
There was a problem hiding this comment.
suggestion (bug_risk): Zero-padding for data shorter than 4 bytes may be unsafe.
Validate that data is exactly 4 bytes, or clearly document and handle cases where it is shorter to avoid unintended results.
| /// <param name="data"></param> | |
| public object? Convert(ReadOnlyMemory<byte> data) | |
| { | |
| float ret = 0; | |
| if (data.Length <= 4) | |
| { | |
| Span<byte> paddedSpan = stackalloc byte[4]; | |
| data.Span.CopyTo(paddedSpan[(4 - data.Length)..]); | |
| if (BinaryPrimitives.TryReadSingleBigEndian(paddedSpan, out var v)) | |
| { | |
| ret = v; | |
| } | |
| } | |
| return ret; | |
| } | |
| /// <param name="data">A 4-byte big-endian IEEE 754 single-precision float value.</param> | |
| /// <returns>The converted float value, or null if input is not exactly 4 bytes.</returns> | |
| public object? Convert(ReadOnlyMemory<byte> data) | |
| { | |
| if (data.Length != 4) | |
| { | |
| // Optionally, throw an exception instead of returning null if that's preferred: | |
| // throw new ArgumentException("Input data must be exactly 4 bytes.", nameof(data)); | |
| return null; | |
| } | |
| if (BinaryPrimitives.TryReadSingleBigEndian(data.Span, out var v)) | |
| { | |
| return v; | |
| } | |
| return null; | |
| } |
|
|
||
| while (data.Length > 0) | ||
| { | ||
| var index = data.Span.IndexOfAny(_delimiter.Span); |
There was a problem hiding this comment.
issue (bug_risk): IndexOfAny may not work as intended for multi-byte delimiters.
IndexOfAny matches any single byte from the delimiter, not the entire sequence. For multi-byte delimiters, use IndexOf to ensure correct splitting.
| [Fact] | ||
| public async Task GetOrCreate_Ok() | ||
| { | ||
| // 测试 GetOrCreate 方法创建的 Client 销毁后继续 GetOrCreate 得到的对象是否可用 | ||
| var sc = new ServiceCollection(); | ||
| sc.AddLogging(builder => | ||
| { | ||
| builder.AddProvider(new MockLoggerProvider()); | ||
| }); | ||
| sc.AddBootstrapBlazorTcpSocketFactory(); |
There was a problem hiding this comment.
suggestion (testing): Consider adding tests for invalid port numbers in ConnectAsync.
Please add test cases for out-of-range port values (e.g., -1, 70000) to verify that the client throws the correct exceptions.
Suggested implementation:
public class TcpSocketFactoryTest
{
[Fact]
public async Task ConnectAsync_InvalidPort_ThrowsException()
{
var factory = new DefaultSocketClientProvider();
var invalidPorts = new[] { -1, 70000 };
foreach (var port in invalidPorts)
{
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () =>
{
await factory.ConnectAsync("127.0.0.1", port);
});
}
}- If
DefaultSocketClientProvideror itsConnectAsyncmethod is not accessible or does not throwArgumentOutOfRangeException, you may need to adjust the exception type or the way the test is written to match your implementation. - Ensure that
DefaultSocketClientProvideris available in the test project and thatConnectAsyncis public and asynchronous.
| [Fact] | ||
| public void UInt16Converter_Ok() | ||
| { | ||
| var converter = new DataUInt16LittleEndianConverter(); | ||
| var actual = converter.Convert(new byte[] { 0xFF, 0x00 }); | ||
| Assert.Equal((ushort)0xFF, actual); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Int16Converter_Ok() |
There was a problem hiding this comment.
suggestion (testing): Consider adding tests for invalid or insufficient data lengths in converters.
Add tests with byte arrays of incorrect lengths to verify converters handle them correctly, either by returning defaults or throwing expected exceptions.
Suggested implementation:
[Fact]
public void UInt16Converter_Ok()
{
var converter = new DataUInt16LittleEndianConverter();
var actual = converter.Convert(new byte[] { 0xFF, 0x00 });
Assert.Equal((ushort)0xFF, actual);
}
[Fact]
public void UInt16Converter_InvalidLength_ThrowsOrDefault()
{
var converter = new DataUInt16LittleEndianConverter();
// Test with empty array
Assert.ThrowsAny<Exception>(() => converter.Convert(new byte[] { }));
// Test with single byte
Assert.ThrowsAny<Exception>(() => converter.Convert(new byte[] { 0x01 }));
} [Fact]
public void Int16Converter_Ok()
{
var converter = new DataInt16LittleEndianConverter();
var actual = converter.Convert(new byte[] { 0x01, 0x00 });
Assert.Equal((short)0x01, actual);
}
[Fact]
public void Int16Converter_InvalidLength_ThrowsOrDefault()
{
var converter = new DataInt16LittleEndianConverter();
// Test with empty array
Assert.ThrowsAny<Exception>(() => converter.Convert(new byte[] { }));
// Test with single byte
Assert.ThrowsAny<Exception>(() => converter.Convert(new byte[] { 0x01 }));
}- If there are other converters (e.g., UInt32, Int32, etc.) in this file, similar tests should be added for them.
- If the converters are supposed to return a default value instead of throwing, replace
Assert.ThrowsAny<Exception>with an assertion for the default value. - If the converters use a specific exception type, replace
Exceptionwith the specific type (e.g.,ArgumentException).
| /// <param name="adapter">The <see cref="IDataPackageAdapter"/> used to process incoming data.</param> | ||
| /// <param name="callback">A callback function invoked with the processed data. The function receives a <see cref="ReadOnlyMemory{T}"/> | ||
| /// containing the processed data and returns a <see cref="ValueTask"/>.</param> | ||
| public static void SetDataPackageAdapter(this ITcpSocketClient client, IDataPackageAdapter adapter, Func<ReadOnlyMemory<byte>, ValueTask> callback) |
There was a problem hiding this comment.
issue (complexity): Consider replacing the multiple SetDataPackageAdapter overloads with a single generic helper method that handles all cases via an optional converter parameter and type checks.
Here’s a way to collapse all five overloads into a single generic helper.
- Use an optional converter parameter (falling back to attribute/DI lookup)
- Special-case
ReadOnlyMemory<byte>so you still get your raw-bytes callback - Keep a single place to wire up both the client and adapter callbacks
public static class TcpSocketExtensions
{
public static void SetDataPackageAdapter<TEntity>(
this ITcpSocketClient client,
IDataPackageAdapter adapter,
Func<TEntity?, ValueTask> callback,
IDataConverter<TEntity>? converter = null)
{
// 1) assign client → adapter pipeline
client.ReceivedCallBack = buffer => adapter.HandlerAsync(buffer);
// 2) set up converter (if none provided, try attribute or DI)
converter ??= ResolveConverter<TEntity>(client);
// 3) wire adapter → user callback
adapter.ReceivedCallBack = async buffer =>
{
if (typeof(TEntity) == typeof(ReadOnlyMemory<byte>))
{
// raw‐bytes specialization
var raw = (TEntity?)(object)buffer;
await callback(raw);
}
else
{
TEntity? result = default;
if (converter?.TryConvertTo(buffer, out var t) == true)
{
result = t;
}
await callback(result);
}
};
}
private static IDataConverter<TEntity>? ResolveConverter<TEntity>(ITcpSocketClient client)
{
// 1) attribute on TEntity?
var attr = typeof(TEntity).GetCustomAttribute<DataTypeConverterAttribute>();
if (attr?.Type != null
&& Activator.CreateInstance(attr.Type) is IDataConverter<TEntity> fromAttr)
return fromAttr;
// 2) ask DI
if (client is IServiceProvider sp
&& sp.GetService<IOptions<DataConverterCollections>>()?.Value
.TryGetTypeConverter<TEntity>(out var diConv) == true)
{
return diConv;
}
return null;
}
}Steps to apply:
- Remove the 5 existing
SetDataPackageAdapteroverloads. - Copy in the single
SetDataPackageAdapter<TEntity>above. - Delete the private
SetDataAdapterCallbackandGetSocketDataConverterhelpers (their logic is now inResolveConverter). - Any call sites using the old overloads will continue to work:
- raw‐bytes: use
SetDataPackageAdapter<ReadOnlyMemory<byte>>(…) - explicit converter: pass it in via the
converterparameter - attribute/DI lookup: omit the converter parameter entirely.
- raw‐bytes: use
This collapses all your boilerplate into one spot, preserves all current behaviors, and keeps the public surface small.
| /// <summary> | ||
| /// Sokcet 数据转换为 int 数据大端转换器 | ||
| /// </summary> | ||
| public class DataInt32BigEndianConverter : IDataPropertyConverter |
There was a problem hiding this comment.
issue (complexity): Consider replacing multiple similar converter classes with a single generic numeric converter implementation.
| public class DataInt32BigEndianConverter : IDataPropertyConverter | |
| You can collapse all of these almost‐identical converters into one generic implementation. For example: | |
| ```csharp | |
| public class GenericNumericConverter<T> : IDataPropertyConverter where T : struct | |
| { | |
| private readonly Func<ReadOnlySpan<byte>, T> _reader; | |
| private readonly int _size; | |
| public GenericNumericConverter(Func<ReadOnlySpan<byte>, T> reader, int size) | |
| { | |
| _reader = reader; | |
| _size = size; | |
| } | |
| public object? Convert(ReadOnlyMemory<byte> data) | |
| { | |
| if (data.Length > _size) return default(T); | |
| Span<byte> buffer = stackalloc byte[_size]; | |
| data.Span.CopyTo(buffer.Slice(_size - data.Length)); | |
| return _reader(buffer); | |
| } | |
| } |
Then replace your many single‐type classes with registrations, e.g.:
// Big‐endian Int32
var int32Be = new GenericNumericConverter<int>(
BinaryPrimitives.ReadInt32BigEndian,
sizeof(int));
// Little‐endian UInt64
var uInt64Le = new GenericNumericConverter<ulong>(
BinaryPrimitives.ReadUInt64LittleEndian,
sizeof(ulong));This centralizes the padding‐and‐read logic while preserving all existing functionality.
Link issues
fixes #497
Summary By Copilot
Regression?
Risk
Verification
Packaging changes reviewed?
☑️ Self Check before Merge
Summary by Sourcery
Add a new BootstrapBlazor.TcpSocket extension project providing a DI-based socket factory and client API with async operations, packet adapters, data conversion framework, utilities, and extensive unit tests.
New Features:
Tests: