Skip to content

feat(TcpSocket): add TcpSocket project#498

Merged
ArgoZhang merged 5 commits intomasterfrom
feat-socket
Jul 23, 2025
Merged

feat(TcpSocket): add TcpSocket project#498
ArgoZhang merged 5 commits intomasterfrom
feat-socket

Conversation

@ArgoZhang
Copy link
Copy Markdown
Member

@ArgoZhang ArgoZhang commented Jul 23, 2025

Link issues

fixes #497

Summary By Copilot

Regression?

  • Yes
  • No

Risk

  • High
  • Medium
  • Low

Verification

  • Manual (required)
  • Automated

Packaging changes reviewed?

  • Yes
  • No
  • N/A

☑️ Self Check before Merge

⚠️ Please check all items below before review. ⚠️

  • Doc is updated/provided or not needed
  • Demo is updated/provided or not needed
  • Merge the latest code from the main branch

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:

  • Add BootstrapBlazor.TcpSocket extension with ITcpSocketFactory and ITcpSocketClient implementations supporting dependency injection
  • Support asynchronous connect, send, and receive operations with timeouts, cancellation, auto-reconnect, auto-receive, and logging
  • Provide extension methods for ITcpSocketClient to send strings and connect by hostname/port
  • Introduce IDataPackageAdapter and IDataPackageHandler abstractions with FixLengthDataPackageHandler and DelimiterDataPackageHandler for custom packet processing
  • Implement DataConverterCollections and DataConverter framework with IDataPropertyConverter for mapping raw bytes to entities
  • Add TcpSocketUtility for IP address and endpoint conversion and validation

Tests:

  • Add comprehensive unit tests covering factory, client, provider, utility, data adapters, handlers, and data converters

@bb-auto bb-auto Bot added the enhancement New feature or request label Jul 23, 2025
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Jul 23, 2025

Reviewer's Guide

This 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 implementations

classDiagram
    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
Loading

Class diagram for data package adapters, handlers, and converters

classDiagram
    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
Loading

Class diagram for data property and type converters

classDiagram
    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
Loading

Class diagram for TcpSocketClientOptions and utility

classDiagram
    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)
    }
Loading

File-Level Changes

Change Details Files
Add core TCP socket client, provider, factory and configuration
  • Implement DefaultTcpSocketClient with connect/send/receive logic, locks, timeouts and auto-reconnect
  • Provide DefaultTcpSocketClientProvider and DefaultTcpSocketFactory for DI
  • Define ITcpSocketClient, ITcpSocketClientProvider and ITcpSocketFactory interfaces
  • Create TcpSocketClientOptions for buffer sizes, timeouts, endpoints and logging
  • Register services via TcpSocketExtensions and implement TcpSocketUtility helpers for IP conversion
src/extensions/BootstrapBlazor.TcpSocket/DefaultTcpSocketClient.cs
src/extensions/BootstrapBlazor.TcpSocket/DefaultTcpSocketClientProvider.cs
src/extensions/BootstrapBlazor.TcpSocket/DefaultTcpSocketFactory.cs
src/extensions/BootstrapBlazor.TcpSocket/ITcpSocketClient.cs
src/extensions/BootstrapBlazor.TcpSocket/ITcpSocketClientProvider.cs
src/extensions/BootstrapBlazor.TcpSocket/ITcpSocketFactory.cs
src/extensions/BootstrapBlazor.TcpSocket/TcpSocketClientOptions.cs
src/extensions/BootstrapBlazor.TcpSocket/Extensions/TcpSocketExtensions.cs
src/extensions/BootstrapBlazor.TcpSocket/Extensions/TcpSocketUtility.cs
Extend ITcpSocketClient API with overloads and data adapter setup
  • Add SendAsync overload for string content with optional encoding
  • Add ConnectAsync overload for hostname/port
  • Implement SetDataPackageAdapter methods to wire IDataPackageAdapter and callback
  • Integrate generic SetDataPackageAdapter for entity conversion
src/extensions/BootstrapBlazor.TcpSocket/Extensions/ITcpSocketClientExtensions.cs
Introduce data adapters and handlers for processing raw TCP streams
  • Implement IDataPackageAdapter and DataPackageAdapter base
  • Define IDataPackageHandler and DataPackageHandlerBase to manage sticky/split buffers
  • Add FixLengthDataPackageHandler for fixed-length frames
  • Add DelimiterDataPackageHandler for delimiter-based frames
src/extensions/BootstrapBlazor.Socket/DataAdapter/DataPackageAdapter.cs
src/extensions/BootstrapBlazor.Socket/DataHandler/IDataPackageHandler.cs
src/extensions/BootstrapBlazor.Socket/DataHandler/DataPackageHandlerBase.cs
src/extensions/BootstrapBlazor.Socket/DataHandler/FixLengthDataPackageHandler.cs
src/extensions/BootstrapBlazor.Socket/DataHandler/DelimiterDataPackageHandler.cs
Add byte-to-entity conversion infrastructure and converters
  • Define IDataConverter interfaces and DataConverter base class
  • Implement DataConverterCollections to register and retrieve converters
  • Use DataTypeConverterAttribute and DataPropertyConverterAttribute for attribute-driven mapping
  • Provide a suite of IDataPropertyConverter implementations for primitives (big/little endian) and common types
src/extensions/BootstrapBlazor.Socket/DataConverter/DataConverterCollections.cs
src/extensions/BootstrapBlazor.Socket/DataConverter/IDataConverter.cs
src/extensions/BootstrapBlazor.Socket/DataConverter/DataConverter.cs
src/extensions/BootstrapBlazor.Socket/DataConverter/DataPropertyConverterAttribute.cs
src/extensions/BootstrapBlazor.Socket/DataConverter/DataTypeConverterAttribute.cs
src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataInt32BigEndianConverter.cs
src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataInt32LittleEndianConverter.cs
src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataInt16BigEndianConverter.cs
src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataSingleBigEndianConverter.cs
src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataDoubleBigEndianConverter.cs
src/extensions/BootstrapBlazor.Socket/PropertyConverter/DataStringConverter.cs
Add comprehensive unit tests for socket and converter libraries
  • Cover factory, client connect/send/receive scenarios, timeouts, cancel and auto-reconnect
  • Test data adapter handlers (fixed length, delimiter) and entity conversion
  • Validate DataConverterCollections registration and retrieval
  • Verify TcpSocketUtility IP/endpoint parsing
  • Include sample model Foo and EnumEducation for integration tests
test/UnitTestTcpSocket/TcpSocketFactoryTest.cs
test/UnitTestTcpSocket/DefaultSocketClientProviderTest.cs
test/UnitTestTcpSocket/SocketDataConverterCollectionsTest.cs
test/UnitTestTcpSocket/TcpSocketPropertyConverterTest.cs
test/UnitTestTcpSocket/TcpSocketUtiityTest.cs
test/UnitTestTcpSocket/Foo.cs
Update solution and build configuration
  • Add new projects to the solution file
  • Configure test project props in Directory.Build.props
BootstrapBlazor.Extensions.sln
test/Directory.Build.props

Assessment against linked issues

Issue Objective Addressed Explanation
#497 Add a new TcpSocket project to the repository, including implementation code and tests.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@bb-auto bb-auto Bot added this to the v9.2.0 milestone Jul 23, 2025
@ArgoZhang ArgoZhang merged commit fed7837 into master Jul 23, 2025
1 check passed
@ArgoZhang ArgoZhang deleted the feat-socket branch July 23, 2025 10:08
Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

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.

Heres 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:
   - rawbytes: 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 almostidentical 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 singletype 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 paddingandread logic while preserving all existing functionality.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +17 to +31
/// <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;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Suggested change
/// <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;
}

Comment on lines +17 to +31
/// <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;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Suggested change
/// <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;
}

Comment on lines +17 to +31
/// <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;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Suggested change
/// <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;
}

Comment on lines +18 to +31
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;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Suggested change
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;
}

Comment on lines +17 to +31
/// <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;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Suggested change
/// <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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Comment on lines +16 to +25
[Fact]
public async Task GetOrCreate_Ok()
{
// 测试 GetOrCreate 方法创建的 Client 销毁后继续 GetOrCreate 得到的对象是否可用
var sc = new ServiceCollection();
sc.AddLogging(builder =>
{
builder.AddProvider(new MockLoggerProvider());
});
sc.AddBootstrapBlazorTcpSocketFactory();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 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.

Comment on lines +9 to +18
[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()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 Exception with 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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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:

  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.

/// <summary>
/// Sokcet 数据转换为 int 数据大端转换器
/// </summary>
public class DataInt32BigEndianConverter : IDataPropertyConverter
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (complexity): Consider replacing multiple similar converter classes with a single generic numeric converter implementation.

Suggested change
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(TcpSocket): add TcpSocket project

1 participant