Skip to content

Commit

Permalink
Xml comments.
Browse files Browse the repository at this point in the history
  • Loading branch information
mikasoukhov committed Feb 18, 2025
1 parent 7bb2aa2 commit 4eb7086
Show file tree
Hide file tree
Showing 6 changed files with 399 additions and 6 deletions.
49 changes: 49 additions & 0 deletions Net.SocketIO/ConnectionStateTracker.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
namespace Ecng.Net;

/// <summary>
/// Tracks the connection states of multiple IConnection instances and aggregates their overall state.
/// </summary>
public class ConnectionStateTracker : Disposable, IConnection
{
/// <summary>
/// Wraps an IConnection instance to listen for its state changes.
/// </summary>
private class ConnectionWrapper : Disposable
{
private readonly IConnection _connection;
private readonly Action _stateChanged;

/// <summary>
/// Gets the current connection state of the wrapped connection.
/// </summary>
public ConnectionStates State { get; private set; }

/// <summary>
/// Initializes a new instance of the <see cref="ConnectionWrapper"/> class.
/// </summary>
/// <param name="connection">The connection to wrap.</param>
/// <param name="stateChanged">Callback invoked when the connection state changes.</param>
/// <exception cref="ArgumentNullException">Thrown when connection or stateChanged is null.</exception>
public ConnectionWrapper(IConnection connection, Action stateChanged)
{
_connection = connection ?? throw new ArgumentNullException(nameof(connection));
Expand All @@ -19,12 +34,19 @@ public ConnectionWrapper(IConnection connection, Action stateChanged)
_connection.StateChanged += OnStateChanged;
}

/// <summary>
/// Disposes the managed resources.
/// </summary>
protected override void DisposeManaged()
{
_connection.StateChanged -= OnStateChanged;
base.DisposeManaged();
}

/// <summary>
/// Handles the state change event from the wrapped connection.
/// </summary>
/// <param name="newState">The new state of the connection.</param>
private void OnStateChanged(ConnectionStates newState)
{
State = newState;
Expand All @@ -36,8 +58,17 @@ private void OnStateChanged(ConnectionStates newState)
private readonly SyncObject _currStateLock = new();
private ConnectionStates _currState = ConnectionStates.Disconnected;

/// <summary>
/// Occurs when the overall connection state changes.
/// </summary>
public event Action<ConnectionStates> StateChanged;

/// <summary>
/// Connects all tracked connections asynchronously.
/// </summary>
/// <param name="cancellationToken">A token to observe for cancellation.</param>
/// <returns>A task that represents the asynchronous connect operation.</returns>
/// <exception cref="InvalidOperationException">Thrown when there are no connections to connect.</exception>
public ValueTask ConnectAsync(CancellationToken cancellationToken)
{
var connections = Connections;
Expand All @@ -48,12 +79,24 @@ public ValueTask ConnectAsync(CancellationToken cancellationToken)
return connections.Select(c => c.ConnectAsync(cancellationToken)).WhenAll();
}

/// <summary>
/// Disconnects all tracked connections.
/// </summary>
public void Disconnect()
=> Connections.ForEach(c => c.Disconnect());

/// <summary>
/// Adds a connection to be tracked.
/// </summary>
/// <param name="connection">The connection to add.</param>
public void Add(IConnection connection)
=> _connections.Add(connection, new(connection, UpdateOverallState));

/// <summary>
/// Removes a tracked connection.
/// </summary>
/// <param name="connection">The connection to remove.</param>
/// <returns>True if the connection was successfully removed; otherwise, false.</returns>
public bool Remove(IConnection connection)
{
if (!_connections.TryGetAndRemove(connection, out var wrapper))
Expand All @@ -67,6 +110,9 @@ public bool Remove(IConnection connection)
private IConnection[] Connections => _connections.CachedKeys;
private ConnectionWrapper[] Wrappers => _connections.CachedValues;

/// <summary>
/// Releases the managed resources used by the <see cref="ConnectionStateTracker"/>.
/// </summary>
protected override void DisposeManaged()
{
foreach (var wrapper in Wrappers)
Expand All @@ -75,6 +121,9 @@ protected override void DisposeManaged()
base.DisposeManaged();
}

/// <summary>
/// Updates the overall state based on the states of all tracked connections.
/// </summary>
private void UpdateOverallState()
{
lock (_currStateLock)
Expand Down
15 changes: 15 additions & 0 deletions Net.SocketIO/IConnection.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
namespace Ecng.Net;

/// <summary>
/// Defines a standard contract for a connection that can be established and disconnected.
/// </summary>
public interface IConnection
{
/// <summary>
/// Occurs when the connection state has changed.
/// </summary>
event Action<ConnectionStates> StateChanged;

/// <summary>
/// Asynchronously connects to a target.
/// </summary>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
/// <returns>A task representing the asynchronous operation.</returns>
ValueTask ConnectAsync(CancellationToken cancellationToken);

/// <summary>
/// Disconnects the current connection.
/// </summary>
void Disconnect();
}
10 changes: 10 additions & 0 deletions Net.SocketIO/RestSharpException.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
namespace Ecng.Net;

/// <summary>
/// Represents an exception that is thrown when a RestSharp operation fails.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
/// <param name="response">
/// The <see cref="RestResponse"/> associated with the failed RestSharp operation.
/// </param>
public class RestSharpException(string message, RestResponse response) : InvalidOperationException(message)
{
/// <summary>
/// Gets the response returned from the RestSharp call that caused the exception.
/// </summary>
public RestResponse Response { get; } = response ?? throw new ArgumentNullException(nameof(response));
}
139 changes: 138 additions & 1 deletion Net.SocketIO/RestSharpHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,60 @@

using RestSharp.Authenticators;

/// <summary>
/// Provides helper methods to work with RestSharp requests and responses.
/// </summary>
public static class RestSharpHelper
{
// Private class for wrapping an IAuthenticator.
private class AuthenticatorWrapper : IAuthenticator
{
private readonly SynchronizedDictionary<RestRequest, IAuthenticator> _authenticators = [];
private readonly SynchronizedDictionary<RestRequest, IAuthenticator> _authenticators = new();

private class Holder : Disposable
{
private readonly RestRequest _request;
private readonly AuthenticatorWrapper _parent;

/// <summary>
/// Initializes a new instance of the <see cref="Holder"/> class and registers the authenticator.
/// </summary>
/// <param name="wrapper">The parent <see cref="AuthenticatorWrapper"/> instance.</param>
/// <param name="req">The RestRequest to associate with the authenticator.</param>
/// <param name="auth">The authenticator to register.</param>
public Holder(AuthenticatorWrapper wrapper, RestRequest req, IAuthenticator auth)
{
_parent = wrapper ?? throw new ArgumentNullException(nameof(wrapper));
_request = req ?? throw new ArgumentNullException(nameof(req));
_parent._authenticators.Add(_request, auth);
}

/// <summary>
/// Releases the managed resources and unregisters the authenticator.
/// </summary>
protected override void DisposeManaged()
{
_parent._authenticators.Remove(_request);
base.DisposeManaged();
}
}

/// <summary>
/// Registers an authenticator for a given request.
/// </summary>
/// <param name="req">The request to register the authenticator with.</param>
/// <param name="authenticator">The authenticator to register.</param>
/// <returns>A disposable that, when disposed, unregisters the authenticator.</returns>
public IDisposable RegisterRequest(RestRequest req, IAuthenticator authenticator) => new Holder(this, req, authenticator);

/// <inheritdoc/>
ValueTask IAuthenticator.Authenticate(IRestClient client, RestRequest request)
=> _authenticators.TryGetValue(request, out var auth) && auth != null ? auth.Authenticate(client, request) : default;
}

private static readonly SynchronizedDictionary<object, RestClient> _clients = [];

// Gets a RestClient instance based on a key.
private static RestClient GetClient(object key)
{
return _clients.SafeAdd(key, _ =>
Expand All @@ -55,12 +76,23 @@ private static RestClient GetClient(object key)
});
}

/// <summary>
/// Removes parameters from the request that match the specified filter.
/// </summary>
/// <param name="request">The RestRequest to modify.</param>
/// <param name="filter">The filter function to determine which parameters to remove.</param>
public static void RemoveWhere(this RestRequest request, Func<Parameter, bool> filter)
{
foreach (var par in request.Parameters.Where(filter).ToArray())
request.RemoveParameter(par);
}

/// <summary>
/// Adds a string body to the request with JSON format.
/// </summary>
/// <param name="request">The RestRequest to which the body will be added.</param>
/// <param name="bodyStr">The string representation of the body.</param>
/// <exception cref="ArgumentNullException">Thrown when request is null.</exception>
public static void AddBodyAsStr(this RestRequest request, string bodyStr)
{
if (request is null)
Expand All @@ -69,24 +101,106 @@ public static void AddBodyAsStr(this RestRequest request, string bodyStr)
request.AddStringBody(bodyStr, DataFormat.Json);
}

/// <summary>
/// Invokes the RestRequest synchronously.
/// </summary>
/// <param name="request">The RestRequest to invoke.</param>
/// <param name="url">The URI endpoint for the request.</param>
/// <param name="caller">The caller object for logging context.</param>
/// <param name="logVerbose">An action to log verbose messages.</param>
/// <param name="contentConverter">An optional function to convert the response content.</param>
/// <param name="throwIfEmptyResponse">Indicates whether to throw an exception if the response is empty.</param>
/// <returns>The deserialized object response.</returns>
public static object Invoke(this RestRequest request, Uri url, object caller, Action<string, object[]> logVerbose, Func<string, string> contentConverter = null, bool throwIfEmptyResponse = true)
=> request.Invoke<object>(url, caller, logVerbose, contentConverter, throwIfEmptyResponse);

/// <summary>
/// Invokes the RestRequest asynchronously.
/// </summary>
/// <param name="request">The RestRequest to invoke.</param>
/// <param name="url">The URI endpoint for the request.</param>
/// <param name="caller">The caller object for logging context.</param>
/// <param name="logVerbose">An action to log verbose messages.</param>
/// <param name="token">The cancellation token.</param>
/// <param name="contentConverter">An optional function to convert the response content.</param>
/// <param name="throwIfEmptyResponse">Indicates whether to throw an exception if the response is empty.</param>
/// <returns>A task representing the asynchronous operation, returning the deserialized object response.</returns>
public static Task<object> InvokeAsync(this RestRequest request, Uri url, object caller, Action<string, object[]> logVerbose, CancellationToken token, Func<string, string> contentConverter = null, bool throwIfEmptyResponse = true)
=> request.InvokeAsync<object>(url, caller, logVerbose, token, contentConverter, throwIfEmptyResponse);

/// <summary>
/// Invokes the RestRequest synchronously and returns a deserialized response of type T.
/// </summary>
/// <typeparam name="T">The expected type of the deserialized response.</typeparam>
/// <param name="request">The RestRequest to invoke.</param>
/// <param name="url">The URI endpoint for the request.</param>
/// <param name="caller">The caller object for logging context.</param>
/// <param name="logVerbose">An action to log verbose messages.</param>
/// <param name="contentConverter">An optional function to convert the response content.</param>
/// <param name="throwIfEmptyResponse">Indicates whether to throw an exception if the response is empty.</param>
/// <returns>The deserialized response of type T.</returns>
public static T Invoke<T>(this RestRequest request, Uri url, object caller, Action<string, object[]> logVerbose, Func<string, string> contentConverter = null, bool throwIfEmptyResponse = true)
=> AsyncContext.Run(() => request.InvokeAsync<T>(url, caller, logVerbose, CancellationToken.None, contentConverter, throwIfEmptyResponse));

/// <summary>
/// Invokes the RestRequest synchronously and returns the full RestResponse of type T.
/// </summary>
/// <typeparam name="T">The expected type of the deserialized response.</typeparam>
/// <param name="request">The RestRequest to invoke.</param>
/// <param name="url">The URI endpoint for the request.</param>
/// <param name="caller">The caller object for logging context.</param>
/// <param name="logVerbose">An action to log verbose messages.</param>
/// <param name="contentConverter">An optional function to convert the response content.</param>
/// <param name="throwIfEmptyResponse">Indicates whether to throw an exception if the response is empty.</param>
/// <returns>The full <see cref="RestResponse{T}"/> response.</returns>
public static RestResponse<T> Invoke2<T>(this RestRequest request, Uri url, object caller, Action<string, object[]> logVerbose, Func<string, string> contentConverter = null, bool throwIfEmptyResponse = true)
=> AsyncContext.Run(() => request.InvokeAsync2<T>(url, caller, logVerbose, CancellationToken.None, contentConverter, null, throwIfEmptyResponse));

/// <summary>
/// Asynchronously invokes the RestRequest and returns a deserialized response of type T.
/// </summary>
/// <typeparam name="T">The expected type of the deserialized response.</typeparam>
/// <param name="request">The RestRequest to invoke.</param>
/// <param name="url">The URI endpoint for the request.</param>
/// <param name="caller">The caller object for logging context.</param>
/// <param name="logVerbose">An action to log verbose messages.</param>
/// <param name="token">The cancellation token.</param>
/// <param name="contentConverter">An optional function to convert the response content.</param>
/// <param name="throwIfEmptyResponse">Indicates whether to throw an exception if the response is empty.</param>
/// <returns>A task representing the asynchronous operation, returning the deserialized response of type T.</returns>
public static async Task<T> InvokeAsync<T>(this RestRequest request, Uri url, object caller, Action<string, object[]> logVerbose, CancellationToken token, Func<string, string> contentConverter = null, bool throwIfEmptyResponse = true)
=> (await request.InvokeAsync2<T>(url, caller, logVerbose, token, contentConverter, null, throwIfEmptyResponse)).Data;

/// <summary>
/// Asynchronously invokes the RestRequest and returns the full <see cref="RestResponse{T}"/> response.
/// </summary>
/// <typeparam name="T">The expected type of the deserialized response.</typeparam>
/// <param name="request">The RestRequest to invoke.</param>
/// <param name="url">The URI endpoint for the request.</param>
/// <param name="caller">The caller object for logging context.</param>
/// <param name="logVerbose">An action to log verbose messages.</param>
/// <param name="token">The cancellation token.</param>
/// <param name="contentConverter">An optional function to convert the response content.</param>
/// <param name="auth">An optional authenticator for the request.</param>
/// <param name="throwIfEmptyResponse">Indicates whether to throw an exception if the response is empty.</param>
/// <returns>A task representing the asynchronous operation, returning the full <see cref="RestResponse{T}"/> response.</returns>
public static Task<RestResponse<T>> InvokeAsync2<T>(this RestRequest request, Uri url, object caller, Action<string, object[]> logVerbose, CancellationToken token, Func<string, string> contentConverter = null, IAuthenticator auth = null, bool throwIfEmptyResponse = true)
=> InvokeAsync3<T>(request, url, caller, logVerbose, token, contentConverter, auth, throwIfEmptyResponse);

/// <summary>
/// Asynchronously invokes the RestRequest with extended error handling and returns the full <see cref="RestResponse{T}"/> response.
/// </summary>
/// <typeparam name="T">The expected type of the deserialized response.</typeparam>
/// <param name="request">The RestRequest to invoke.</param>
/// <param name="url">The URI endpoint for the request.</param>
/// <param name="caller">The caller object for logging context.</param>
/// <param name="logVerbose">An action to log verbose messages.</param>
/// <param name="token">The cancellation token.</param>
/// <param name="contentConverter">An optional function to convert the response content.</param>
/// <param name="auth">An optional authenticator for the request.</param>
/// <param name="throwIfEmptyResponse">Indicates whether to throw an exception if the response is empty.</param>
/// <param name="handleErrorStatus">An optional function to handle error HTTP status codes.</param>
/// <returns>A task representing the asynchronous operation, returning the full <see cref="RestResponse{T}"/> response.</returns>
public static async Task<RestResponse<T>> InvokeAsync3<T>(this RestRequest request, Uri url, object caller, Action<string, object[]> logVerbose, CancellationToken token, Func<string, string> contentConverter = null, IAuthenticator auth = null, bool throwIfEmptyResponse = true, Func<HttpStatusCode, bool> handleErrorStatus = null)
{
if (request is null)
Expand Down Expand Up @@ -183,12 +297,29 @@ static string formatHeaders(IEnumerable<Parameter> parameters)
return result;
}

/// <summary>
/// Converts a RestResponse into a RestSharpException with additional error details.
/// </summary>
/// <param name="response">The RestResponse instance.</param>
/// <param name="message">An optional error message.</param>
/// <returns>A new instance of <see cref="RestSharpException"/>.</returns>
public static RestSharpException ToError(this RestResponse response, string message = default)
=> new(message.IsEmpty($"unexpected response code='{response.StatusCode}', msg='{response.ErrorMessage}', desc='{response.StatusDescription}', content='{response.Content}'"), response);

/// <summary>
/// Converts a collection of parameters into a query string representation.
/// </summary>
/// <param name="parameters">The collection of parameters.</param>
/// <param name="encodeValue">Indicates whether the parameter values should be encoded.</param>
/// <returns>A query string representing the parameters.</returns>
public static string ToQueryString(this IEnumerable<Parameter> parameters, bool encodeValue = true)
=> parameters.CheckOnNull(nameof(parameters)).Select(p => $"{p.Name}={p.Value.Format(encodeValue)}").JoinAnd();

/// <summary>
/// Decodes a JSON Web Token (JWT) into its component parts.
/// </summary>
/// <param name="jwt">The JWT string.</param>
/// <returns>An enumerable of strings representing the decoded parts of the JWT.</returns>
public static IEnumerable<string> DecodeJWT(this string jwt)
{
if (jwt.IsEmpty())
Expand All @@ -206,6 +337,12 @@ static string decode(string base64Url)
return parts.Select(decode);
}

/// <summary>
/// Adds a Bearer authorization header to the RestRequest.
/// </summary>
/// <param name="client">The RestRequest to modify.</param>
/// <param name="token">The secure token used for Bearer authentication.</param>
/// <returns>The modified RestRequest.</returns>
public static RestRequest SetBearer(this RestRequest client, SecureString token)
=> client.AddHeader(HttpHeaders.Authorization, AuthSchemas.Bearer.FormatAuth(token));
}
Loading

0 comments on commit 4eb7086

Please sign in to comment.