diff --git a/src/Api/PubnubApi/Model/RequestResponse/AddChannelsToChannelGroupRequest.cs b/src/Api/PubnubApi/Model/RequestResponse/AddChannelsToChannelGroupRequest.cs
new file mode 100644
index 000000000..c3a845e4f
--- /dev/null
+++ b/src/Api/PubnubApi/Model/RequestResponse/AddChannelsToChannelGroupRequest.cs
@@ -0,0 +1,36 @@
+using System;
+
+namespace PubnubApi
+{
+ ///
+ /// Request object for adding channels to a channel group
+ ///
+ public class AddChannelsToChannelGroupRequest
+ {
+ ///
+ /// The channels to add to the channel group
+ ///
+ public string[] Channels { get; set; }
+
+ ///
+ /// The channel group to add channels to
+ ///
+ public string ChannelGroup { get; set; }
+
+ ///
+ /// Validates the request parameters
+ ///
+ internal void Validate()
+ {
+ if (Channels == null || Channels.Length == 0)
+ {
+ throw new ArgumentException("Channels cannot be null or empty");
+ }
+
+ if (string.IsNullOrEmpty(ChannelGroup) || ChannelGroup.Trim().Length == 0)
+ {
+ throw new ArgumentException("ChannelGroup cannot be null or empty");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Api/PubnubApi/Model/RequestResponse/AddChannelsToChannelGroupResponse.cs b/src/Api/PubnubApi/Model/RequestResponse/AddChannelsToChannelGroupResponse.cs
new file mode 100644
index 000000000..7e3ab2165
--- /dev/null
+++ b/src/Api/PubnubApi/Model/RequestResponse/AddChannelsToChannelGroupResponse.cs
@@ -0,0 +1,48 @@
+using System;
+
+namespace PubnubApi
+{
+ ///
+ /// Response object for adding channels to a channel group
+ ///
+ public class AddChannelsToChannelGroupResponse
+ {
+ ///
+ /// Indicates whether the operation was successful
+ ///
+ public bool Success { get; }
+
+ ///
+ /// The response message
+ ///
+ public string Message { get; }
+
+ ///
+ /// The exception if the operation failed
+ ///
+ public Exception Exception { get; }
+
+ private AddChannelsToChannelGroupResponse(bool success, string message, Exception exception = null)
+ {
+ Success = success;
+ Message = message;
+ Exception = exception;
+ }
+
+ ///
+ /// Creates a successful response
+ ///
+ internal static AddChannelsToChannelGroupResponse CreateSuccess(PNChannelGroupsAddChannelResult result)
+ {
+ return new AddChannelsToChannelGroupResponse(true, "Channel(s) added successfully", null);
+ }
+
+ ///
+ /// Creates a failure response
+ ///
+ internal static AddChannelsToChannelGroupResponse CreateFailure(Exception exception)
+ {
+ return new AddChannelsToChannelGroupResponse(false, exception?.Message ?? "Operation failed", exception);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Api/PubnubApi/Model/RequestResponse/DeleteChannelGroupRequest.cs b/src/Api/PubnubApi/Model/RequestResponse/DeleteChannelGroupRequest.cs
new file mode 100644
index 000000000..82d2106ce
--- /dev/null
+++ b/src/Api/PubnubApi/Model/RequestResponse/DeleteChannelGroupRequest.cs
@@ -0,0 +1,26 @@
+using System;
+
+namespace PubnubApi
+{
+ ///
+ /// Request object for deleting a channel group
+ ///
+ public class DeleteChannelGroupRequest
+ {
+ ///
+ /// The channel group to delete
+ ///
+ public string ChannelGroup { get; set; }
+
+ ///
+ /// Validates the request parameters
+ ///
+ internal void Validate()
+ {
+ if (string.IsNullOrEmpty(ChannelGroup) || ChannelGroup.Trim().Length == 0)
+ {
+ throw new ArgumentException("ChannelGroup cannot be null or empty");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Api/PubnubApi/Model/RequestResponse/DeleteChannelGroupResponse.cs b/src/Api/PubnubApi/Model/RequestResponse/DeleteChannelGroupResponse.cs
new file mode 100644
index 000000000..e0f8427a3
--- /dev/null
+++ b/src/Api/PubnubApi/Model/RequestResponse/DeleteChannelGroupResponse.cs
@@ -0,0 +1,48 @@
+using System;
+
+namespace PubnubApi
+{
+ ///
+ /// Response object for deleting a channel group
+ ///
+ public class DeleteChannelGroupResponse
+ {
+ ///
+ /// Indicates whether the operation was successful
+ ///
+ public bool Success { get; }
+
+ ///
+ /// The response message
+ ///
+ public string Message { get; }
+
+ ///
+ /// The exception if the operation failed
+ ///
+ public Exception Exception { get; }
+
+ private DeleteChannelGroupResponse(bool success, string message, Exception exception = null)
+ {
+ Success = success;
+ Message = message;
+ Exception = exception;
+ }
+
+ ///
+ /// Creates a successful response
+ ///
+ internal static DeleteChannelGroupResponse CreateSuccess(PNChannelGroupsDeleteGroupResult result)
+ {
+ return new DeleteChannelGroupResponse(true, result?.Message ?? "Channel group deleted successfully", null);
+ }
+
+ ///
+ /// Creates a failure response
+ ///
+ internal static DeleteChannelGroupResponse CreateFailure(Exception exception)
+ {
+ return new DeleteChannelGroupResponse(false, exception?.Message ?? "Operation failed", exception);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Api/PubnubApi/Model/RequestResponse/DeleteMessageRequest.cs b/src/Api/PubnubApi/Model/RequestResponse/DeleteMessageRequest.cs
new file mode 100644
index 000000000..20392d6fb
--- /dev/null
+++ b/src/Api/PubnubApi/Model/RequestResponse/DeleteMessageRequest.cs
@@ -0,0 +1,171 @@
+using System;
+using System.Collections.Generic;
+
+namespace PubnubApi
+{
+ ///
+ /// Request model for delete message operations using the request/response API pattern.
+ /// This request deletes messages from a specific channel within an optional timetoken range.
+ ///
+ public class DeleteMessageRequest
+ {
+ ///
+ /// The channel from which to delete messages. Required field.
+ /// Only a single channel is supported for delete operations.
+ ///
+ public string Channel { get; set; }
+
+ ///
+ /// The starting timetoken for the deletion range (inclusive).
+ /// If not specified, deletes from the beginning of the channel history.
+ ///
+ public long? Start { get; set; }
+
+ ///
+ /// The ending timetoken for the deletion range (exclusive).
+ /// If not specified, deletes to the end of the channel history.
+ ///
+ public long? End { get; set; }
+
+ ///
+ /// Additional query parameters for the request.
+ /// Allows for future extensibility and custom parameters.
+ ///
+ public Dictionary QueryParameters { get; set; }
+
+ ///
+ /// Validates the request parameters.
+ ///
+ /// Thrown when validation fails
+ public void Validate()
+ {
+ // Validate required channel
+ if (string.IsNullOrWhiteSpace(Channel))
+ {
+ throw new ArgumentException("Channel is required for delete message operation");
+ }
+
+ // Validate timetoken values if provided
+ if (Start.HasValue && Start.Value < 0)
+ {
+ throw new ArgumentException("Start timetoken cannot be negative");
+ }
+
+ if (End.HasValue && End.Value < 0)
+ {
+ throw new ArgumentException("End timetoken cannot be negative");
+ }
+
+ // Validate logical timetoken range
+ if (Start.HasValue && End.HasValue && Start.Value > End.Value)
+ {
+ throw new ArgumentException("Start timetoken must be less than or equal to end timetoken");
+ }
+ }
+
+ ///
+ /// Creates a new DeleteMessageRequest for deleting all messages in a channel.
+ ///
+ /// The channel to delete messages from
+ /// A configured DeleteMessageRequest
+ public static DeleteMessageRequest ForChannel(string channel)
+ {
+ if (string.IsNullOrWhiteSpace(channel))
+ {
+ throw new ArgumentException("Channel cannot be null or empty", nameof(channel));
+ }
+
+ return new DeleteMessageRequest
+ {
+ Channel = channel
+ };
+ }
+
+ ///
+ /// Creates a new DeleteMessageRequest for deleting messages within a specific timetoken range.
+ ///
+ /// The channel to delete messages from
+ /// The starting timetoken (inclusive)
+ /// The ending timetoken (exclusive)
+ /// A configured DeleteMessageRequest
+ public static DeleteMessageRequest ForChannelWithRange(string channel, long start, long end)
+ {
+ if (string.IsNullOrWhiteSpace(channel))
+ {
+ throw new ArgumentException("Channel cannot be null or empty", nameof(channel));
+ }
+
+ if (start < 0)
+ {
+ throw new ArgumentException("Start timetoken cannot be negative", nameof(start));
+ }
+
+ if (end < 0)
+ {
+ throw new ArgumentException("End timetoken cannot be negative", nameof(end));
+ }
+
+ if (start > end)
+ {
+ throw new ArgumentException("Start timetoken must be less than or equal to end timetoken");
+ }
+
+ return new DeleteMessageRequest
+ {
+ Channel = channel,
+ Start = start,
+ End = end
+ };
+ }
+
+ ///
+ /// Creates a new DeleteMessageRequest for deleting messages from a specific start time.
+ ///
+ /// The channel to delete messages from
+ /// The starting timetoken (inclusive)
+ /// A configured DeleteMessageRequest
+ public static DeleteMessageRequest ForChannelFromTime(string channel, long start)
+ {
+ if (string.IsNullOrWhiteSpace(channel))
+ {
+ throw new ArgumentException("Channel cannot be null or empty", nameof(channel));
+ }
+
+ if (start < 0)
+ {
+ throw new ArgumentException("Start timetoken cannot be negative", nameof(start));
+ }
+
+ return new DeleteMessageRequest
+ {
+ Channel = channel,
+ Start = start
+ };
+ }
+
+ ///
+ /// Creates a new DeleteMessageRequest for deleting messages up to a specific end time.
+ ///
+ /// The channel to delete messages from
+ /// The ending timetoken (exclusive)
+ /// A configured DeleteMessageRequest
+ public static DeleteMessageRequest ForChannelUntilTime(string channel, long end)
+ {
+ if (string.IsNullOrWhiteSpace(channel))
+ {
+ throw new ArgumentException("Channel cannot be null or empty", nameof(channel));
+ }
+
+ if (end < 0)
+ {
+ throw new ArgumentException("End timetoken cannot be negative", nameof(end));
+ }
+
+ return new DeleteMessageRequest
+ {
+ Channel = channel,
+ End = end
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Api/PubnubApi/Model/RequestResponse/DeleteMessageResponse.cs b/src/Api/PubnubApi/Model/RequestResponse/DeleteMessageResponse.cs
new file mode 100644
index 000000000..286d2f279
--- /dev/null
+++ b/src/Api/PubnubApi/Model/RequestResponse/DeleteMessageResponse.cs
@@ -0,0 +1,111 @@
+using System;
+
+namespace PubnubApi
+{
+ ///
+ /// Response model for delete message operations using the request/response API pattern.
+ /// Indicates successful deletion of messages from a channel.
+ ///
+ public class DeleteMessageResponse
+ {
+ ///
+ /// Gets a value indicating whether the delete operation was successful.
+ /// Always true for a successfully returned response (errors throw exceptions).
+ ///
+ public bool Success { get; private set; }
+
+ ///
+ /// Gets the HTTP status code from the delete operation.
+ /// Typically 200 for successful operations.
+ ///
+ public int StatusCode { get; private set; }
+
+ ///
+ /// Gets the channel from which messages were deleted.
+ ///
+ public string Channel { get; private set; }
+
+ ///
+ /// Gets the start timetoken used in the delete operation, if specified.
+ ///
+ public long? StartTimetoken { get; private set; }
+
+ ///
+ /// Gets the end timetoken used in the delete operation, if specified.
+ ///
+ public long? EndTimetoken { get; private set; }
+
+ ///
+ /// Private constructor to enforce factory method usage.
+ ///
+ private DeleteMessageResponse()
+ {
+ Success = true;
+ }
+
+ ///
+ /// Creates a successful delete message response.
+ ///
+ /// The channel from which messages were deleted
+ /// The HTTP status code from the operation
+ /// The start timetoken used, if any
+ /// The end timetoken used, if any
+ /// A DeleteMessageResponse indicating success
+ internal static DeleteMessageResponse CreateSuccess(string channel, int statusCode, long? startTimetoken = null, long? endTimetoken = null)
+ {
+ if (string.IsNullOrWhiteSpace(channel))
+ {
+ throw new ArgumentException("Channel cannot be null or empty", nameof(channel));
+ }
+
+ return new DeleteMessageResponse
+ {
+ Success = true,
+ StatusCode = statusCode,
+ Channel = channel,
+ StartTimetoken = startTimetoken,
+ EndTimetoken = endTimetoken
+ };
+ }
+
+ ///
+ /// Creates a successful delete message response from PubNub result.
+ /// Since PNDeleteMessageResult is empty, we only use status information.
+ ///
+ /// The PubNub delete message result (currently unused as it's empty)
+ /// The channel from which messages were deleted
+ /// The HTTP status code from the operation
+ /// The start timetoken used, if any
+ /// The end timetoken used, if any
+ /// A DeleteMessageResponse indicating success
+ internal static DeleteMessageResponse CreateSuccess(PNDeleteMessageResult result, string channel, int statusCode, long? startTimetoken = null, long? endTimetoken = null)
+ {
+ // Note: PNDeleteMessageResult is currently an empty class, so we don't use it
+ // This signature is provided for consistency with other response creators
+ return CreateSuccess(channel, statusCode, startTimetoken, endTimetoken);
+ }
+
+ ///
+ /// Returns a string representation of the delete message response.
+ ///
+ /// A formatted string describing the deletion
+ public override string ToString()
+ {
+ var rangeInfo = "";
+ if (StartTimetoken.HasValue && EndTimetoken.HasValue)
+ {
+ rangeInfo = $" (Range: {StartTimetoken.Value} to {EndTimetoken.Value})";
+ }
+ else if (StartTimetoken.HasValue)
+ {
+ rangeInfo = $" (From: {StartTimetoken.Value})";
+ }
+ else if (EndTimetoken.HasValue)
+ {
+ rangeInfo = $" (Until: {EndTimetoken.Value})";
+ }
+
+ return $"DeleteMessageResponse: Success={Success}, Channel={Channel}, StatusCode={StatusCode}{rangeInfo}";
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Api/PubnubApi/Model/RequestResponse/FetchHistoryRequest.cs b/src/Api/PubnubApi/Model/RequestResponse/FetchHistoryRequest.cs
new file mode 100644
index 000000000..69d57b3b5
--- /dev/null
+++ b/src/Api/PubnubApi/Model/RequestResponse/FetchHistoryRequest.cs
@@ -0,0 +1,129 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace PubnubApi
+{
+ ///
+ /// Request model for fetch history operations using the request/response API pattern.
+ ///
+ public class FetchHistoryRequest
+ {
+ ///
+ /// The channels to fetch history from. Required field.
+ ///
+ public string[] Channels { get; set; }
+
+ ///
+ /// The start timetoken to fetch messages from.
+ /// If not specified, fetches from the beginning of history.
+ ///
+ public long? Start { get; set; }
+
+ ///
+ /// The end timetoken to fetch messages until.
+ /// If not specified, fetches until the most recent message.
+ ///
+ public long? End { get; set; }
+
+ ///
+ /// Maximum number of messages to return per channel.
+ /// Default is 100 for single channel, 25 for multiple channels or when including message actions.
+ ///
+ public int? MaximumPerChannel { get; set; }
+
+ ///
+ /// Whether to return messages in reverse order (oldest first).
+ /// Default is false (newest first).
+ ///
+ public bool Reverse { get; set; } = false;
+
+ ///
+ /// Whether to include metadata with each message.
+ /// Default is false.
+ ///
+ public bool IncludeMeta { get; set; } = false;
+
+ ///
+ /// Whether to include message actions with each message.
+ /// Only supported when fetching history for a single channel.
+ /// Default is false.
+ ///
+ public bool IncludeMessageActions { get; set; } = false;
+
+ ///
+ /// Whether to include the publisher UUID with each message.
+ /// Default is true.
+ ///
+ public bool IncludeUuid { get; set; } = true;
+
+ ///
+ /// Whether to include the message type indicator.
+ /// Default is true.
+ ///
+ public bool IncludeMessageType { get; set; } = true;
+
+ ///
+ /// Whether to include custom message type information.
+ /// Default is false.
+ ///
+ public bool IncludeCustomMessageType { get; set; } = false;
+
+ ///
+ /// Additional query parameters to include in the request.
+ ///
+ public Dictionary QueryParameters { get; set; }
+
+ ///
+ /// Validates that the request has all required fields and valid combinations.
+ ///
+ /// Thrown when required fields are missing or invalid.
+ public void Validate()
+ {
+ if (Channels == null || Channels.Length == 0 || Channels.Any(c => string.IsNullOrEmpty(c?.Trim())))
+ {
+ throw new ArgumentException("Channels is required and cannot be null, empty, or contain empty channel names.", nameof(Channels));
+ }
+
+ if (IncludeMessageActions && Channels.Length > 1)
+ {
+ throw new ArgumentException("IncludeMessageActions is only supported when fetching history for a single channel.", nameof(IncludeMessageActions));
+ }
+
+ if (Start.HasValue && Start.Value < 0)
+ {
+ throw new ArgumentException("Start timetoken must be non-negative.", nameof(Start));
+ }
+
+ if (End.HasValue && End.Value < 0)
+ {
+ throw new ArgumentException("End timetoken must be non-negative.", nameof(End));
+ }
+
+ if (MaximumPerChannel.HasValue && MaximumPerChannel.Value <= 0)
+ {
+ throw new ArgumentException("MaximumPerChannel must be greater than 0.", nameof(MaximumPerChannel));
+ }
+ }
+
+ ///
+ /// Gets the effective maximum per channel value based on the request configuration.
+ ///
+ /// The maximum number of messages per channel to fetch.
+ internal int GetEffectiveMaximumPerChannel()
+ {
+ if (MaximumPerChannel.HasValue && MaximumPerChannel.Value > 0)
+ {
+ return MaximumPerChannel.Value;
+ }
+
+ // Default based on configuration
+ if (IncludeMessageActions || (Channels != null && Channels.Length > 1))
+ {
+ return 25;
+ }
+
+ return 100;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Api/PubnubApi/Model/RequestResponse/FetchHistoryResponse.cs b/src/Api/PubnubApi/Model/RequestResponse/FetchHistoryResponse.cs
new file mode 100644
index 000000000..df3ce170a
--- /dev/null
+++ b/src/Api/PubnubApi/Model/RequestResponse/FetchHistoryResponse.cs
@@ -0,0 +1,189 @@
+using System;
+using System.Collections.Generic;
+
+namespace PubnubApi
+{
+ ///
+ /// Response model for fetch history operations using the request/response API pattern.
+ ///
+ public class FetchHistoryResponse
+ {
+ ///
+ /// Information about a single history message.
+ ///
+ public class HistoryMessage
+ {
+ ///
+ /// The timetoken when this message was published.
+ ///
+ public long Timetoken { get; internal set; }
+
+ ///
+ /// The message content.
+ ///
+ public object Entry { get; internal set; }
+
+ ///
+ /// Metadata associated with this message.
+ ///
+ public Dictionary Meta { get; internal set; }
+
+ ///
+ /// Message actions associated with this message.
+ ///
+ public Dictionary> Actions { get; internal set; }
+
+ ///
+ /// The UUID of the publisher.
+ ///
+ public string Uuid { get; internal set; }
+
+ ///
+ /// The message type indicator.
+ ///
+ public int MessageType { get; internal set; }
+
+ ///
+ /// Custom message type if specified.
+ ///
+ public string CustomMessageType { get; internal set; }
+
+ ///
+ /// Creates a HistoryMessage from PNHistoryItemResult.
+ ///
+ internal static HistoryMessage FromPNHistoryItem(PNHistoryItemResult item)
+ {
+ if (item == null) return null;
+
+ return new HistoryMessage
+ {
+ Timetoken = item.Timetoken,
+ Entry = item.Entry,
+ Meta = item.Meta,
+ Actions = item.ActionItems,
+ Uuid = item.Uuid,
+ MessageType = item.MessageType,
+ CustomMessageType = item.CustomMessageType
+ };
+ }
+ }
+
+ ///
+ /// Information about pagination for fetching more messages.
+ ///
+ public class MoreInfo
+ {
+ ///
+ /// The start timetoken for fetching more messages.
+ ///
+ public long Start { get; internal set; }
+
+ ///
+ /// The end timetoken for fetching more messages.
+ ///
+ public long End { get; internal set; }
+
+ ///
+ /// The maximum number of messages that were requested.
+ ///
+ public int Max { get; internal set; }
+
+ ///
+ /// Creates a MoreInfo from PNFetchHistoryResult.MoreInfo.
+ ///
+ internal static MoreInfo FromPNMoreInfo(PNFetchHistoryResult.MoreInfo info)
+ {
+ if (info == null) return null;
+
+ return new MoreInfo
+ {
+ Start = info.Start,
+ End = info.End,
+ Max = info.Max
+ };
+ }
+ }
+
+ ///
+ /// Messages organized by channel name.
+ ///
+ public Dictionary> Messages { get; internal set; }
+
+ ///
+ /// Pagination information for fetching additional messages.
+ ///
+ public MoreInfo More { get; internal set; }
+
+ ///
+ /// Indicates whether the fetch history operation was successful.
+ ///
+ public bool IsSuccess { get; internal set; }
+
+ ///
+ /// HTTP status code from the fetch history request.
+ ///
+ public int StatusCode { get; internal set; }
+
+ ///
+ /// Any error information if the fetch failed.
+ ///
+ public string ErrorMessage { get; internal set; }
+
+ ///
+ /// Creates a successful FetchHistoryResponse from PNFetchHistoryResult.
+ ///
+ /// The PNFetchHistoryResult from the internal operation
+ /// HTTP status code
+ /// A successful FetchHistoryResponse
+ internal static FetchHistoryResponse CreateSuccess(PNFetchHistoryResult result, int statusCode = 200)
+ {
+ var response = new FetchHistoryResponse
+ {
+ IsSuccess = true,
+ StatusCode = statusCode,
+ Messages = new Dictionary>(),
+ More = MoreInfo.FromPNMoreInfo(result?.More)
+ };
+
+ // Convert messages
+ if (result?.Messages != null)
+ {
+ foreach (var channelMessages in result.Messages)
+ {
+ var convertedMessages = new List();
+ if (channelMessages.Value != null)
+ {
+ foreach (var message in channelMessages.Value)
+ {
+ var historyMessage = HistoryMessage.FromPNHistoryItem(message);
+ if (historyMessage != null)
+ {
+ convertedMessages.Add(historyMessage);
+ }
+ }
+ }
+ response.Messages[channelMessages.Key] = convertedMessages;
+ }
+ }
+
+ return response;
+ }
+
+ ///
+ /// Creates an error FetchHistoryResponse.
+ ///
+ /// The error message
+ /// HTTP status code
+ /// An error FetchHistoryResponse
+ internal static FetchHistoryResponse CreateError(string errorMessage, int statusCode = 400)
+ {
+ return new FetchHistoryResponse
+ {
+ IsSuccess = false,
+ ErrorMessage = errorMessage,
+ StatusCode = statusCode,
+ Messages = new Dictionary>()
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Api/PubnubApi/Model/RequestResponse/ISubscription.cs b/src/Api/PubnubApi/Model/RequestResponse/ISubscription.cs
new file mode 100644
index 000000000..1f2875306
--- /dev/null
+++ b/src/Api/PubnubApi/Model/RequestResponse/ISubscription.cs
@@ -0,0 +1,145 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace PubnubApi
+{
+ ///
+ /// Represents an active subscription to PubNub channels or channel groups.
+ /// Provides lifecycle management and event handling for real-time messages.
+ ///
+ public interface ISubscription : IDisposable
+ {
+ ///
+ /// Gets whether this subscription is currently active and receiving messages.
+ ///
+ bool IsActive { get; }
+
+ ///
+ /// Gets the channels this subscription is listening to.
+ ///
+ string[] Channels { get; }
+
+ ///
+ /// Gets the channel groups this subscription is listening to.
+ ///
+ string[] ChannelGroups { get; }
+
+ ///
+ /// Gets whether presence events are enabled for this subscription.
+ ///
+ bool PresenceEnabled { get; }
+
+ ///
+ /// Event raised when a message is received on any subscribed channel.
+ ///
+ event EventHandler> MessageReceived;
+
+ ///
+ /// Event raised when a presence event occurs on any subscribed channel.
+ ///
+ event EventHandler PresenceEvent;
+
+ ///
+ /// Event raised when the subscription status changes (connected, disconnected, etc.).
+ ///
+ event EventHandler StatusChanged;
+
+ ///
+ /// Event raised when a signal is received on any subscribed channel.
+ ///
+ event EventHandler> SignalReceived;
+
+ ///
+ /// Event raised when an object event occurs (for App Context).
+ ///
+ event EventHandler ObjectEvent;
+
+ ///
+ /// Event raised when a message action event occurs.
+ ///
+ event EventHandler MessageActionEvent;
+
+ ///
+ /// Event raised when a file event occurs.
+ ///
+ event EventHandler FileEvent;
+
+ ///
+ /// Asynchronously stops the subscription and releases resources.
+ ///
+ /// Token to cancel the stop operation.
+ /// Task representing the stop operation.
+ Task StopAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// Synchronously stops the subscription and releases resources.
+ ///
+ void Stop();
+ }
+
+ ///
+ /// Event arguments for message events.
+ ///
+ /// The type of the message payload.
+ public class PNMessageEventArgs : EventArgs
+ {
+ public PNMessageResult Message { get; set; }
+ public Pubnub Pubnub { get; set; }
+ }
+
+ ///
+ /// Event arguments for presence events.
+ ///
+ public class PNPresenceEventArgs : EventArgs
+ {
+ public PNPresenceEventResult Presence { get; set; }
+ public Pubnub Pubnub { get; set; }
+ }
+
+ ///
+ /// Event arguments for status events.
+ ///
+ public class PNStatusEventArgs : EventArgs
+ {
+ public PNStatus Status { get; set; }
+ public Pubnub Pubnub { get; set; }
+ }
+
+ ///
+ /// Event arguments for signal events.
+ ///
+ /// The type of the signal payload.
+ public class PNSignalEventArgs : EventArgs
+ {
+ public PNSignalResult Signal { get; set; }
+ public Pubnub Pubnub { get; set; }
+ }
+
+ ///
+ /// Event arguments for object events.
+ ///
+ public class PNObjectEventArgs : EventArgs
+ {
+ public PNObjectEventResult ObjectEvent { get; set; }
+ public Pubnub Pubnub { get; set; }
+ }
+
+ ///
+ /// Event arguments for message action events.
+ ///
+ public class PNMessageActionEventArgs : EventArgs
+ {
+ public PNMessageActionEventResult MessageAction { get; set; }
+ public Pubnub Pubnub { get; set; }
+ }
+
+ ///
+ /// Event arguments for file events.
+ ///
+ public class PNFileEventArgs : EventArgs
+ {
+ public PNFileEventResult FileEvent { get; set; }
+ public Pubnub Pubnub { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Api/PubnubApi/Model/RequestResponse/ListChannelGroupsRequest.cs b/src/Api/PubnubApi/Model/RequestResponse/ListChannelGroupsRequest.cs
new file mode 100644
index 000000000..5867c0ff9
--- /dev/null
+++ b/src/Api/PubnubApi/Model/RequestResponse/ListChannelGroupsRequest.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace PubnubApi
+{
+ ///
+ /// Request object for listing all channel groups
+ ///
+ public class ListChannelGroupsRequest
+ {
+ // No parameters needed for this request
+
+ ///
+ /// Validates the request parameters
+ ///
+ internal void Validate()
+ {
+ // No validation needed as there are no parameters
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Api/PubnubApi/Model/RequestResponse/ListChannelGroupsResponse.cs b/src/Api/PubnubApi/Model/RequestResponse/ListChannelGroupsResponse.cs
new file mode 100644
index 000000000..ce9d51103
--- /dev/null
+++ b/src/Api/PubnubApi/Model/RequestResponse/ListChannelGroupsResponse.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+
+namespace PubnubApi
+{
+ ///
+ /// Response object for listing all channel groups
+ ///
+ public class ListChannelGroupsResponse
+ {
+ ///
+ /// The list of channel groups
+ ///
+ public List Groups { get; }
+
+ ///
+ /// The exception if the operation failed
+ ///
+ public Exception Exception { get; }
+
+ private ListChannelGroupsResponse(List groups, Exception exception = null)
+ {
+ Groups = groups;
+ Exception = exception;
+ }
+
+ ///
+ /// Creates a successful response
+ ///
+ internal static ListChannelGroupsResponse CreateSuccess(PNChannelGroupsListAllResult result)
+ {
+ return new ListChannelGroupsResponse(result?.Groups ?? new List(), null);
+ }
+
+ ///
+ /// Creates a failure response
+ ///
+ internal static ListChannelGroupsResponse CreateFailure(Exception exception)
+ {
+ return new ListChannelGroupsResponse(new List(), exception);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Api/PubnubApi/Model/RequestResponse/ListChannelsForChannelGroupRequest.cs b/src/Api/PubnubApi/Model/RequestResponse/ListChannelsForChannelGroupRequest.cs
new file mode 100644
index 000000000..a620e46ab
--- /dev/null
+++ b/src/Api/PubnubApi/Model/RequestResponse/ListChannelsForChannelGroupRequest.cs
@@ -0,0 +1,26 @@
+using System;
+
+namespace PubnubApi
+{
+ ///
+ /// Request object for listing channels in a channel group
+ ///
+ public class ListChannelsForChannelGroupRequest
+ {
+ ///
+ /// The channel group to list channels for
+ ///
+ public string ChannelGroup { get; set; }
+
+ ///
+ /// Validates the request parameters
+ ///
+ internal void Validate()
+ {
+ if (string.IsNullOrEmpty(ChannelGroup) || ChannelGroup.Trim().Length == 0)
+ {
+ throw new ArgumentException("ChannelGroup cannot be null or empty");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Api/PubnubApi/Model/RequestResponse/ListChannelsForChannelGroupResponse.cs b/src/Api/PubnubApi/Model/RequestResponse/ListChannelsForChannelGroupResponse.cs
new file mode 100644
index 000000000..788cdd762
--- /dev/null
+++ b/src/Api/PubnubApi/Model/RequestResponse/ListChannelsForChannelGroupResponse.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Collections.Generic;
+
+namespace PubnubApi
+{
+ ///
+ /// Response object for listing channels in a channel group
+ ///
+ public class ListChannelsForChannelGroupResponse
+ {
+ ///
+ /// The list of channels in the channel group
+ ///
+ public List Channels { get; }
+
+ ///
+ /// The channel group name
+ ///
+ public string ChannelGroup { get; }
+
+ ///
+ /// The exception if the operation failed
+ ///
+ public Exception Exception { get; }
+
+ private ListChannelsForChannelGroupResponse(List channels, string channelGroup, Exception exception = null)
+ {
+ Channels = channels;
+ ChannelGroup = channelGroup;
+ Exception = exception;
+ }
+
+ ///
+ /// Creates a successful response
+ ///
+ internal static ListChannelsForChannelGroupResponse CreateSuccess(PNChannelGroupsAllChannelsResult result)
+ {
+ return new ListChannelsForChannelGroupResponse(
+ result?.Channels ?? new List(),
+ result?.ChannelGroup,
+ null);
+ }
+
+ ///
+ /// Creates a failure response
+ ///
+ internal static ListChannelsForChannelGroupResponse CreateFailure(Exception exception)
+ {
+ return new ListChannelsForChannelGroupResponse(new List(), null, exception);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Api/PubnubApi/Model/RequestResponse/MessageCountsRequest.cs b/src/Api/PubnubApi/Model/RequestResponse/MessageCountsRequest.cs
new file mode 100644
index 000000000..c1234aa3a
--- /dev/null
+++ b/src/Api/PubnubApi/Model/RequestResponse/MessageCountsRequest.cs
@@ -0,0 +1,149 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace PubnubApi
+{
+ ///
+ /// Request model for message counts operations using the request/response API pattern.
+ /// This request retrieves the count of messages for specified channels within given timetoken ranges.
+ ///
+ public class MessageCountsRequest
+ {
+ ///
+ /// The channels to get message counts for. Required field.
+ /// At least one channel must be specified.
+ ///
+ public string[] Channels { get; set; }
+
+ ///
+ /// The timetokens for each channel to count messages from.
+ /// Can be a single timetoken (applied to all channels) or an array matching the number of channels.
+ /// If not specified, counts all messages in the channels.
+ /// When single timetoken: counts messages from that time to now for all channels.
+ /// When multiple timetokens: each timetoken corresponds to its channel at the same index.
+ ///
+ public long[] ChannelTimetokens { get; set; }
+
+ ///
+ /// Additional query parameters for the request.
+ /// Allows for future extensibility and custom parameters.
+ ///
+ public Dictionary QueryParameters { get; set; }
+
+ ///
+ /// Validates the request parameters.
+ ///
+ /// Thrown when validation fails
+ public void Validate()
+ {
+ // Validate required channels
+ if (Channels == null || Channels.Length == 0)
+ {
+ throw new ArgumentException("Channels are required for message counts operation");
+ }
+
+ // Validate each channel is not null or empty
+ if (Channels.Any(channel => string.IsNullOrWhiteSpace(channel)))
+ {
+ throw new ArgumentException("Channel names cannot be null or empty");
+ }
+
+ // Validate timetoken array if provided
+ if (ChannelTimetokens != null && ChannelTimetokens.Length > 0)
+ {
+ // Timetokens must be either 1 (for all channels) or match channel count
+ if (ChannelTimetokens.Length != 1 && ChannelTimetokens.Length != Channels.Length)
+ {
+ throw new ArgumentException(
+ $"ChannelTimetokens must have either 1 element (for all channels) or {Channels.Length} elements (one per channel)");
+ }
+
+ // Validate timetokens are non-negative
+ if (ChannelTimetokens.Any(tt => tt < 0))
+ {
+ throw new ArgumentException("Timetokens cannot be negative");
+ }
+ }
+ }
+
+ ///
+ /// Creates a new MessageCountsRequest for a single channel with optional timetoken.
+ ///
+ /// The channel to get message count for
+ /// Optional timetoken to count messages from
+ /// A configured MessageCountsRequest
+ public static MessageCountsRequest ForChannel(string channel, long? fromTimetoken = null)
+ {
+ if (string.IsNullOrWhiteSpace(channel))
+ {
+ throw new ArgumentException("Channel cannot be null or empty", nameof(channel));
+ }
+
+ var request = new MessageCountsRequest
+ {
+ Channels = new[] { channel }
+ };
+
+ if (fromTimetoken.HasValue)
+ {
+ request.ChannelTimetokens = new[] { fromTimetoken.Value };
+ }
+
+ return request;
+ }
+
+ ///
+ /// Creates a new MessageCountsRequest for multiple channels with optional shared timetoken.
+ ///
+ /// The channels to get message counts for
+ /// Optional shared timetoken to count messages from for all channels
+ /// A configured MessageCountsRequest
+ public static MessageCountsRequest ForChannels(string[] channels, long? fromTimetoken = null)
+ {
+ if (channels == null || channels.Length == 0)
+ {
+ throw new ArgumentException("Channels cannot be null or empty", nameof(channels));
+ }
+
+ var request = new MessageCountsRequest
+ {
+ Channels = channels
+ };
+
+ if (fromTimetoken.HasValue)
+ {
+ request.ChannelTimetokens = new[] { fromTimetoken.Value };
+ }
+
+ return request;
+ }
+
+ ///
+ /// Creates a new MessageCountsRequest for multiple channels with individual timetokens.
+ ///
+ /// The channels to get message counts for
+ /// Individual timetokens for each channel (must match channel count)
+ /// A configured MessageCountsRequest
+ public static MessageCountsRequest ForChannelsWithIndividualTimetokens(string[] channels, long[] channelTimetokens)
+ {
+ if (channels == null || channels.Length == 0)
+ {
+ throw new ArgumentException("Channels cannot be null or empty", nameof(channels));
+ }
+
+ if (channelTimetokens == null || channelTimetokens.Length != channels.Length)
+ {
+ throw new ArgumentException(
+ $"ChannelTimetokens must have {channels.Length} elements to match channel count",
+ nameof(channelTimetokens));
+ }
+
+ return new MessageCountsRequest
+ {
+ Channels = channels,
+ ChannelTimetokens = channelTimetokens
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Api/PubnubApi/Model/RequestResponse/MessageCountsResponse.cs b/src/Api/PubnubApi/Model/RequestResponse/MessageCountsResponse.cs
new file mode 100644
index 000000000..2b5a77bd9
--- /dev/null
+++ b/src/Api/PubnubApi/Model/RequestResponse/MessageCountsResponse.cs
@@ -0,0 +1,123 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace PubnubApi
+{
+ ///
+ /// Response model for message counts operations using the request/response API pattern.
+ /// Contains the message counts for requested channels.
+ ///
+ public class MessageCountsResponse
+ {
+ ///
+ /// Dictionary mapping channel names to their respective message counts.
+ /// Key: Channel name
+ /// Value: Count of messages in that channel
+ ///
+ public Dictionary Channels { get; private set; }
+
+ ///
+ /// Gets the total message count across all channels.
+ ///
+ public long TotalMessageCount => Channels?.Sum(kvp => kvp.Value) ?? 0;
+
+ ///
+ /// Gets the number of channels in the response.
+ ///
+ public int ChannelCount => Channels?.Count ?? 0;
+
+ ///
+ /// Private constructor to enforce factory method usage.
+ ///
+ private MessageCountsResponse()
+ {
+ Channels = new Dictionary();
+ }
+
+ ///
+ /// Creates a successful message counts response from PubNub result.
+ ///
+ /// The PubNub message count result
+ /// A MessageCountsResponse containing the channel counts
+ internal static MessageCountsResponse CreateSuccess(PNMessageCountResult result)
+ {
+ if (result == null)
+ {
+ throw new ArgumentNullException(nameof(result));
+ }
+
+ return new MessageCountsResponse
+ {
+ Channels = result.Channels ?? new Dictionary()
+ };
+ }
+
+ ///
+ /// Creates an empty message counts response.
+ /// Used when no messages are found or for error scenarios.
+ ///
+ /// An empty MessageCountsResponse
+ internal static MessageCountsResponse CreateEmpty()
+ {
+ return new MessageCountsResponse
+ {
+ Channels = new Dictionary()
+ };
+ }
+
+ ///
+ /// Gets the message count for a specific channel.
+ ///
+ /// The channel name
+ /// The message count for the channel, or 0 if not found
+ public long GetCountForChannel(string channel)
+ {
+ if (string.IsNullOrWhiteSpace(channel))
+ {
+ throw new ArgumentException("Channel cannot be null or empty", nameof(channel));
+ }
+
+ return Channels != null && Channels.TryGetValue(channel, out var count) ? count : 0;
+ }
+
+ ///
+ /// Checks if the response contains data for a specific channel.
+ ///
+ /// The channel name to check
+ /// True if the channel is present in the response, false otherwise
+ public bool HasChannel(string channel)
+ {
+ if (string.IsNullOrWhiteSpace(channel))
+ {
+ return false;
+ }
+
+ return Channels != null && Channels.ContainsKey(channel);
+ }
+
+ ///
+ /// Gets all channel names from the response.
+ ///
+ /// An array of channel names
+ public string[] GetChannelNames()
+ {
+ return Channels?.Keys.ToArray() ?? Array.Empty();
+ }
+
+ ///
+ /// Returns a string representation of the message counts response.
+ ///
+ /// A formatted string showing channel counts
+ public override string ToString()
+ {
+ if (Channels == null || Channels.Count == 0)
+ {
+ return "MessageCountsResponse: No channels";
+ }
+
+ var channelInfo = string.Join(", ", Channels.Select(kvp => $"{kvp.Key}:{kvp.Value}"));
+ return $"MessageCountsResponse: {ChannelCount} channel(s), Total: {TotalMessageCount} [{channelInfo}]";
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Api/PubnubApi/Model/RequestResponse/PublishRequest.cs b/src/Api/PubnubApi/Model/RequestResponse/PublishRequest.cs
new file mode 100644
index 000000000..d239ad06a
--- /dev/null
+++ b/src/Api/PubnubApi/Model/RequestResponse/PublishRequest.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Collections.Generic;
+
+namespace PubnubApi
+{
+ ///
+ /// Request model for publish operations using the request/response API pattern.
+ ///
+ public class PublishRequest
+ {
+ ///
+ /// The message to publish. Can be any serializable object.
+ ///
+ public object Message { get; set; }
+
+ ///
+ /// The channel to publish to. Required field.
+ ///
+ public string Channel { get; set; }
+
+ ///
+ /// Whether to store the message in history. Default is true.
+ ///
+ public bool StoreInHistory { get; set; } = true;
+
+ ///
+ /// Time to live for the message in hours. Default is -1 (no TTL).
+ ///
+ public int Ttl { get; set; } = -1;
+
+ ///
+ /// Custom metadata to include with the message.
+ ///
+ public Dictionary Metadata { get; set; }
+
+ ///
+ /// Whether to use HTTP POST instead of GET. Default is false.
+ ///
+ public bool UsePost { get; set; } = false;
+
+ ///
+ /// Custom message type identifier.
+ ///
+ public string CustomMessageType { get; set; }
+
+ ///
+ /// Additional query parameters to include in the request.
+ ///
+ public Dictionary QueryParameters { get; set; }
+
+ ///
+ /// Validates that the request has all required fields.
+ ///
+ /// Thrown when required fields are missing or invalid.
+ public void Validate()
+ {
+ if (string.IsNullOrEmpty(Channel?.Trim()))
+ {
+ throw new ArgumentException("Channel is required and cannot be null or empty.", nameof(Channel));
+ }
+
+ if (Message == null)
+ {
+ throw new ArgumentException("Message is required and cannot be null.", nameof(Message));
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Api/PubnubApi/Model/RequestResponse/PublishResponse.cs b/src/Api/PubnubApi/Model/RequestResponse/PublishResponse.cs
new file mode 100644
index 000000000..1abe62ae7
--- /dev/null
+++ b/src/Api/PubnubApi/Model/RequestResponse/PublishResponse.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Generic;
+
+namespace PubnubApi
+{
+ ///
+ /// Response model for publish operations using the request/response API pattern.
+ ///
+ public class PublishResponse
+ {
+ ///
+ /// The timetoken when the message was published.
+ ///
+ public long Timetoken { get; internal set; }
+
+ ///
+ /// The channel the message was published to.
+ ///
+ public string Channel { get; internal set; }
+
+ ///
+ /// Indicates whether the publish operation was successful.
+ ///
+ public bool IsSuccess { get; internal set; }
+
+ ///
+ /// HTTP status code from the publish request.
+ ///
+ public int StatusCode { get; internal set; }
+
+ ///
+ /// Additional response headers if available.
+ ///
+ public Dictionary Headers { get; internal set; }
+
+ ///
+ /// Any error information if the publish failed.
+ ///
+ public string ErrorMessage { get; internal set; }
+
+ ///
+ /// Creates a successful PublishResponse.
+ ///
+ /// The publish timetoken
+ /// The channel published to
+ /// HTTP status code
+ /// A successful PublishResponse
+ public static PublishResponse CreateSuccess(long timetoken, string channel, int statusCode = 200)
+ {
+ return new PublishResponse
+ {
+ Timetoken = timetoken,
+ Channel = channel,
+ IsSuccess = true,
+ StatusCode = statusCode,
+ Headers = new Dictionary()
+ };
+ }
+
+ ///
+ /// Creates an error PublishResponse.
+ ///
+ /// The channel that was attempted to publish to
+ /// The error message
+ /// HTTP status code
+ /// An error PublishResponse
+ public static PublishResponse CreateError(string channel, string errorMessage, int statusCode = 400)
+ {
+ return new PublishResponse
+ {
+ Channel = channel,
+ IsSuccess = false,
+ ErrorMessage = errorMessage,
+ StatusCode = statusCode,
+ Headers = new Dictionary()
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Api/PubnubApi/Model/RequestResponse/RemoveChannelsFromChannelGroupRequest.cs b/src/Api/PubnubApi/Model/RequestResponse/RemoveChannelsFromChannelGroupRequest.cs
new file mode 100644
index 000000000..b4cda2b4e
--- /dev/null
+++ b/src/Api/PubnubApi/Model/RequestResponse/RemoveChannelsFromChannelGroupRequest.cs
@@ -0,0 +1,36 @@
+using System;
+
+namespace PubnubApi
+{
+ ///
+ /// Request object for removing channels from a channel group
+ ///
+ public class RemoveChannelsFromChannelGroupRequest
+ {
+ ///
+ /// The channels to remove from the channel group
+ ///
+ public string[] Channels { get; set; }
+
+ ///
+ /// The channel group to remove channels from
+ ///
+ public string ChannelGroup { get; set; }
+
+ ///
+ /// Validates the request parameters
+ ///
+ internal void Validate()
+ {
+ if (Channels == null || Channels.Length == 0)
+ {
+ throw new ArgumentException("Channels cannot be null or empty");
+ }
+
+ if (string.IsNullOrEmpty(ChannelGroup) || ChannelGroup.Trim().Length == 0)
+ {
+ throw new ArgumentException("ChannelGroup cannot be null or empty");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Api/PubnubApi/Model/RequestResponse/RemoveChannelsFromChannelGroupResponse.cs b/src/Api/PubnubApi/Model/RequestResponse/RemoveChannelsFromChannelGroupResponse.cs
new file mode 100644
index 000000000..0c8c42205
--- /dev/null
+++ b/src/Api/PubnubApi/Model/RequestResponse/RemoveChannelsFromChannelGroupResponse.cs
@@ -0,0 +1,48 @@
+using System;
+
+namespace PubnubApi
+{
+ ///
+ /// Response object for removing channels from a channel group
+ ///
+ public class RemoveChannelsFromChannelGroupResponse
+ {
+ ///
+ /// Indicates whether the operation was successful
+ ///
+ public bool Success { get; }
+
+ ///
+ /// The response message
+ ///
+ public string Message { get; }
+
+ ///
+ /// The exception if the operation failed
+ ///
+ public Exception Exception { get; }
+
+ private RemoveChannelsFromChannelGroupResponse(bool success, string message, Exception exception = null)
+ {
+ Success = success;
+ Message = message;
+ Exception = exception;
+ }
+
+ ///
+ /// Creates a successful response
+ ///
+ internal static RemoveChannelsFromChannelGroupResponse CreateSuccess(PNChannelGroupsRemoveChannelResult result)
+ {
+ return new RemoveChannelsFromChannelGroupResponse(true, result?.Message ?? "Channel(s) removed successfully", null);
+ }
+
+ ///
+ /// Creates a failure response
+ ///
+ internal static RemoveChannelsFromChannelGroupResponse CreateFailure(Exception exception)
+ {
+ return new RemoveChannelsFromChannelGroupResponse(false, exception?.Message ?? "Operation failed", exception);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Api/PubnubApi/Model/RequestResponse/SubscribeRequest.cs b/src/Api/PubnubApi/Model/RequestResponse/SubscribeRequest.cs
new file mode 100644
index 000000000..628e5feac
--- /dev/null
+++ b/src/Api/PubnubApi/Model/RequestResponse/SubscribeRequest.cs
@@ -0,0 +1,113 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace PubnubApi
+{
+ ///
+ /// Request model for subscribe operations using the request/response API pattern.
+ ///
+ public class SubscribeRequest
+ {
+ ///
+ /// Array of channels to subscribe to.
+ ///
+ public string[] Channels { get; set; }
+
+ ///
+ /// Array of channel groups to subscribe to.
+ ///
+ public string[] ChannelGroups { get; set; }
+
+ ///
+ /// The timetoken to use for the subscription. Default is -1 (use server time).
+ ///
+ public long Timetoken { get; set; } = -1;
+
+ ///
+ /// Whether to include presence events for the subscribed channels.
+ ///
+ public bool WithPresence { get; set; } = false;
+
+ ///
+ /// Additional query parameters to include in the subscription request.
+ ///
+ public Dictionary QueryParameters { get; set; }
+
+ ///
+ /// Optional callback for handling received messages.
+ /// Alternative to using events on ISubscription.
+ ///
+ public Action> OnMessage { get; set; }
+
+ ///
+ /// Optional callback for handling presence events.
+ /// Alternative to using events on ISubscription.
+ ///
+ public Action OnPresence { get; set; }
+
+ ///
+ /// Optional callback for handling status changes.
+ /// Alternative to using events on ISubscription.
+ ///
+ public Action OnStatus { get; set; }
+
+ ///
+ /// Optional callback for handling signal events.
+ /// Alternative to using events on ISubscription.
+ ///
+ public Action> OnSignal { get; set; }
+
+ ///
+ /// Optional callback for handling object events.
+ /// Alternative to using events on ISubscription.
+ ///
+ public Action OnObjectEvent { get; set; }
+
+ ///
+ /// Optional callback for handling message action events.
+ /// Alternative to using events on ISubscription.
+ ///
+ public Action OnMessageAction { get; set; }
+
+ ///
+ /// Optional callback for handling file events.
+ /// Alternative to using events on ISubscription.
+ ///
+ public Action OnFile { get; set; }
+
+ ///
+ /// Validates that the request has at least one channel or channel group.
+ ///
+ /// Thrown when both channels and channel groups are empty.
+ public void Validate()
+ {
+ bool hasChannels = Channels != null && Channels.Length > 0 && Channels.Any(c => !string.IsNullOrWhiteSpace(c));
+ bool hasChannelGroups = ChannelGroups != null && ChannelGroups.Length > 0 && ChannelGroups.Any(cg => !string.IsNullOrWhiteSpace(cg));
+
+ if (!hasChannels && !hasChannelGroups)
+ {
+ throw new ArgumentException("Either Channels or ChannelGroups (or both) must be provided with at least one valid entry.");
+ }
+
+ // Clean up arrays to remove null/empty entries
+ if (Channels != null)
+ {
+ Channels = Channels.Where(c => !string.IsNullOrWhiteSpace(c)).ToArray();
+ }
+ else
+ {
+ Channels = new string[0];
+ }
+
+ if (ChannelGroups != null)
+ {
+ ChannelGroups = ChannelGroups.Where(cg => !string.IsNullOrWhiteSpace(cg)).ToArray();
+ }
+ else
+ {
+ ChannelGroups = new string[0];
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Api/PubnubApi/Model/RequestResponse/SubscriptionImpl.cs b/src/Api/PubnubApi/Model/RequestResponse/SubscriptionImpl.cs
new file mode 100644
index 000000000..1db132f45
--- /dev/null
+++ b/src/Api/PubnubApi/Model/RequestResponse/SubscriptionImpl.cs
@@ -0,0 +1,250 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using PubnubApi.EndPoint;
+
+namespace PubnubApi
+{
+ ///
+ /// Internal implementation of ISubscription that manages an active PubNub subscription.
+ ///
+ internal class SubscriptionImpl : ISubscription
+ {
+ private readonly Pubnub pubnub;
+ private readonly SubscribeRequest request;
+ private readonly SubscribeCallbackAdapter callbackAdapter;
+ private readonly object subscribeOperation;
+ private bool isActive;
+ private bool disposed = false;
+
+ public bool IsActive => isActive && !disposed;
+
+ public string[] Channels { get; }
+
+ public string[] ChannelGroups { get; }
+
+ public bool PresenceEnabled { get; }
+
+ // Events
+ public event EventHandler> MessageReceived;
+ public event EventHandler PresenceEvent;
+ public event EventHandler StatusChanged;
+ public event EventHandler> SignalReceived;
+ public event EventHandler ObjectEvent;
+ public event EventHandler MessageActionEvent;
+ public event EventHandler FileEvent;
+
+ public SubscriptionImpl(Pubnub pubnubInstance, SubscribeRequest subscribeRequest, object operation)
+ {
+ pubnub = pubnubInstance ?? throw new ArgumentNullException(nameof(pubnubInstance));
+ request = subscribeRequest ?? throw new ArgumentNullException(nameof(subscribeRequest));
+ subscribeOperation = operation ?? throw new ArgumentNullException(nameof(operation));
+
+ Channels = request.Channels ?? new string[0];
+ ChannelGroups = request.ChannelGroups ?? new string[0];
+ PresenceEnabled = request.WithPresence;
+
+ // Create the callback adapter that bridges to our events and request callbacks
+ callbackAdapter = new SubscribeCallbackAdapter(this, request, pubnubInstance);
+
+ // Add the adapter to the appropriate listener list based on operation type
+ if (operation is ISubscribeOperation