diff --git a/.pubnub.yml b/.pubnub.yml index c9472b13c..57435ca1f 100644 --- a/.pubnub.yml +++ b/.pubnub.yml @@ -1,8 +1,15 @@ name: c-sharp -version: "8.2.0" +version: "8.2.1" schema: 1 scm: github.com/pubnub/c-sharp changelog: + - date: 2026-05-29 + version: v8.2.1 + changes: + - type: bug + text: "Fixes issue of hereNow returning incorrect Occupancy when Occupancy count value is omitted from the response of single channel hereNow call." + - type: bug + text: "Fix for `HereNow` omitting the channel entry for an empty channel when UUIDs are included." - date: 2026-05-18 version: v8.2.0 changes: @@ -982,14 +989,14 @@ features: - QUERY-PARAM supported-platforms: - - version: Pubnub 'C#' 8.2.0 + version: Pubnub 'C#' 8.2.1 platforms: - Windows 10 and up - Windows Server 2008 and up frameworks: - .Net Framework 4.5+ - - version: PubnubPCL 'C#' 8.2.0 + version: PubnubPCL 'C#' 8.2.1 platforms: - Xamarin.Android - Xamarin.iOS @@ -1001,7 +1008,7 @@ supported-platforms: frameworks: - .Net 4.5+ - - version: PubnubUWP 'C#' 8.2.0 + version: PubnubUWP 'C#' 8.2.1 platforms: - Windows Phone 10 - Universal Windows Apps @@ -1025,7 +1032,7 @@ sdks: distribution-type: source distribution-repository: GitHub package-name: Pubnub - location: https://github.com/pubnub/c-sharp/releases/tag/v8.2.0 + location: https://github.com/pubnub/c-sharp/releases/tag/v8.2.1 requires: - name: ".Net" @@ -1266,7 +1273,7 @@ sdks: distribution-type: source distribution-repository: GitHub package-name: PubNubPCL - location: https://github.com/pubnub/c-sharp/releases/tag/v8.2.0 + location: https://github.com/pubnub/c-sharp/releases/tag/v8.2.1 requires: - name: ".Net" @@ -1617,7 +1624,7 @@ sdks: distribution-type: source distribution-repository: GitHub package-name: PubnubUWP - location: https://github.com/pubnub/c-sharp/releases/tag/v8.2.0 + location: https://github.com/pubnub/c-sharp/releases/tag/v8.2.1 requires: - name: "Universal Windows Platform Development" diff --git a/CHANGELOG b/CHANGELOG index 3169c3d9b..de51427fd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +v8.2.1 - May 29 2026 +----------------------------- +- Fixed: fixes issue of hereNow returning incorrect Occupancy when Occupancy count value is omitted from the response of single channel hereNow call. +- Fixed: fix for `HereNow` omitting the channel entry for an empty channel when UUIDs are included. + v8.2.0 - May 18 2026 ----------------------------- - Added: added support for the publish v2 endpoint, allowing feature-enabled keysets to publish messages of size up to 2MB. diff --git a/src/Api/PubnubApi/JsonDataParse/DeserializeToInternalObjectUtility.cs b/src/Api/PubnubApi/JsonDataParse/DeserializeToInternalObjectUtility.cs index 1ddb4bf83..89325c287 100644 --- a/src/Api/PubnubApi/JsonDataParse/DeserializeToInternalObjectUtility.cs +++ b/src/Api/PubnubApi/JsonDataParse/DeserializeToInternalObjectUtility.cs @@ -417,12 +417,21 @@ public static T DeserializeToInternalObject(IJsonPluggableLibrary jsonPlug, L } hereNowResult.Channels = new Dictionary(); + // This block handles the "compact" here_now response shape that the server returns + // ONLY for a single-channel request (no "payload" wrapper, just a flat "occupancy" plus + // an optional "uuids" list). The channel name itself is not in the body, so it is taken + // from listObject[1] (hereNowChannelName). if (herenowDicObj.ContainsKey("uuids")) { + // Case: single channel requested WITH uuids (disable_uuids=0). + // The channel entry is built and added unconditionally below — even when "uuids" is an + // empty array (i.e. an empty channel with occupancy 0). Previously the channel was only + // added when the array was non-empty, which silently dropped empty channels from the + // result; callers doing Channels.TryGetValue(channel, ...) would then get a false miss. + List uuidDataList = new List(); object[] uuidArray = jsonPlug.ConvertToObjectArray(herenowDicObj["uuids"]); if (uuidArray != null && uuidArray.Length > 0) { - List uuidDataList = new List(); for (int index = 0; index < uuidArray.Length; index++) { Dictionary hereNowChannelItemUuidsDic = @@ -446,25 +455,34 @@ public static T DeserializeToInternalObject(IJsonPluggableLibrary jsonPlug, L uuidDataList.Add(uuidData); } } + } - PNHereNowChannelData channelData = new PNHereNowChannelData(); - channelData.ChannelName = hereNowChannelName; - channelData.Occupants = uuidDataList; - channelData.Occupancy = hereNowResult.TotalOccupancy; + PNHereNowChannelData channelData = new PNHereNowChannelData(); + channelData.ChannelName = hereNowChannelName; + channelData.Occupants = uuidDataList; + channelData.Occupancy = hereNowResult.TotalOccupancy; - hereNowResult.Channels.Add(hereNowChannelName, channelData); - hereNowResult.TotalChannels = hereNowResult.Channels.Count; - } + hereNowResult.Channels.Add(hereNowChannelName, channelData); + hereNowResult.TotalChannels = hereNowResult.Channels.Count; } else { + // Case: single channel requested WITHOUT uuids (disable_uuids=1). + // The response carries only the total "occupancy" and no per-uuid list, so each + // channel's Occupancy is set from TotalOccupancy (the response's "occupancy" value). + // This compact shape is only ever returned for a SINGLE channel, so arrChannel has + // exactly one element and assigning TotalOccupancy to it is correct. (Multi-channel + // requests instead return the "payload" shape handled in the branch above, where each + // channel has its own occupancy.) Previously Occupancy here was hardcoded to 1, which + // reported the wrong count whenever the real occupancy was not 1 (e.g. 3 users -> 1). string channels = listObject[1].ToString(); string[] arrChannel = channels.Split(','); int totalChannels = 0; foreach (string channel in arrChannel) { PNHereNowChannelData channelData = new PNHereNowChannelData(); - channelData.Occupancy = 1; + channelData.ChannelName = channel; + channelData.Occupancy = hereNowResult.TotalOccupancy; hereNowResult.Channels.Add(channel, channelData); totalChannels++; } diff --git a/src/Api/PubnubApi/Properties/AssemblyInfo.cs b/src/Api/PubnubApi/Properties/AssemblyInfo.cs index 1badf7da7..d4dde423d 100644 --- a/src/Api/PubnubApi/Properties/AssemblyInfo.cs +++ b/src/Api/PubnubApi/Properties/AssemblyInfo.cs @@ -11,8 +11,8 @@ [assembly: AssemblyProduct("Pubnub C# SDK")] [assembly: AssemblyCopyright("Copyright © 2021")] [assembly: AssemblyTrademark("")] -[assembly: AssemblyVersion("8.2.0")] -[assembly: AssemblyFileVersion("8.2.0")] +[assembly: AssemblyVersion("8.2.1")] +[assembly: AssemblyFileVersion("8.2.1")] // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. diff --git a/src/Api/PubnubApi/PubnubApi.csproj b/src/Api/PubnubApi/PubnubApi.csproj index 72c248d78..a38e0f745 100644 --- a/src/Api/PubnubApi/PubnubApi.csproj +++ b/src/Api/PubnubApi/PubnubApi.csproj @@ -14,7 +14,7 @@ Pubnub - 8.2.0 + 8.2.1 PubNub C# .NET - Web Data Push API Pandu Masabathula PubNub @@ -22,7 +22,8 @@ http://pubnub.s3.amazonaws.com/2011/powered-by-pubnub/pubnub-icon-600x600.png true https://github.com/pubnub/c-sharp/ - Added support for the publish v2 endpoint, allowing feature-enabled keysets to publish messages of size up to 2MB. + Fixes issue of hereNow returning incorrect Occupancy when Occupancy count value is omitted from the response of single channel hereNow call. +Fix for `HereNow` omitting the channel entry for an empty channel when UUIDs are included. Web Data Push Real-time Notifications ESB Message Broadcasting Distributed Computing PubNub is a Massively Scalable Web Push Service for Web and Mobile Games. This is a cloud-based service for broadcasting messages to thousands of web and mobile clients simultaneously diff --git a/src/Api/PubnubApiPCL/PubnubApiPCL.csproj b/src/Api/PubnubApiPCL/PubnubApiPCL.csproj index c35aee9c3..a94cb101f 100644 --- a/src/Api/PubnubApiPCL/PubnubApiPCL.csproj +++ b/src/Api/PubnubApiPCL/PubnubApiPCL.csproj @@ -14,7 +14,7 @@ PubnubPCL - 8.2.0 + 8.2.1 PubNub C# .NET - Web Data Push API Pandu Masabathula PubNub @@ -22,7 +22,8 @@ http://pubnub.s3.amazonaws.com/2011/powered-by-pubnub/pubnub-icon-600x600.png true https://github.com/pubnub/c-sharp/ - Added support for the publish v2 endpoint, allowing feature-enabled keysets to publish messages of size up to 2MB. + Fixes issue of hereNow returning incorrect Occupancy when Occupancy count value is omitted from the response of single channel hereNow call. +Fix for `HereNow` omitting the channel entry for an empty channel when UUIDs are included. Web Data Push Real-time Notifications ESB Message Broadcasting Distributed Computing PubNub is a Massively Scalable Web Push Service for Web and Mobile Games. This is a cloud-based service for broadcasting messages to thousands of web and mobile clients simultaneously diff --git a/src/Api/PubnubApiUWP/PubnubApiUWP.csproj b/src/Api/PubnubApiUWP/PubnubApiUWP.csproj index 92f2f2db0..35483149c 100644 --- a/src/Api/PubnubApiUWP/PubnubApiUWP.csproj +++ b/src/Api/PubnubApiUWP/PubnubApiUWP.csproj @@ -16,7 +16,7 @@ PubnubUWP - 8.2.0 + 8.2.1 PubNub C# .NET - Web Data Push API Pandu Masabathula PubNub @@ -24,7 +24,8 @@ http://pubnub.s3.amazonaws.com/2011/powered-by-pubnub/pubnub-icon-600x600.png true https://github.com/pubnub/c-sharp/ - Added support for the publish v2 endpoint, allowing feature-enabled keysets to publish messages of size up to 2MB. + Fixes issue of hereNow returning incorrect Occupancy when Occupancy count value is omitted from the response of single channel hereNow call. +Fix for `HereNow` omitting the channel entry for an empty channel when UUIDs are included. Web Data Push Real-time Notifications ESB Message Broadcasting Distributed Computing PubNub is a Massively Scalable Web Push Service for Web and Mobile Games. This is a cloud-based service for broadcasting messages to thousands of web and mobile clients simultaneously diff --git a/src/Api/PubnubApiUnity/PubnubApiUnity.csproj b/src/Api/PubnubApiUnity/PubnubApiUnity.csproj index 00f149e04..dc1c2bec3 100644 --- a/src/Api/PubnubApiUnity/PubnubApiUnity.csproj +++ b/src/Api/PubnubApiUnity/PubnubApiUnity.csproj @@ -15,7 +15,7 @@ PubnubApiUnity - 8.2.0 + 8.2.1 PubNub C# .NET - Web Data Push API Pandu Masabathula PubNub diff --git a/src/UnitTests/PubnubApi.Tests/WhenAClientIsPresented.cs b/src/UnitTests/PubnubApi.Tests/WhenAClientIsPresented.cs index 9a03af3c7..d35cc8d9d 100644 --- a/src/UnitTests/PubnubApi.Tests/WhenAClientIsPresented.cs +++ b/src/UnitTests/PubnubApi.Tests/WhenAClientIsPresented.cs @@ -626,6 +626,119 @@ public static async Task IfWithAsyncHereNowIsCalledThenItShouldReturnInfo() } + // Parses a raw here_now server response through the same deserializer the SDK uses, + // bypassing the network so occupancy parsing can be asserted deterministically. + private static PNHereNowResult ParseHereNowResponse(string serverResponse, string channelArg) + { + PNConfiguration config = new PNConfiguration(new UserId("mytestuuid")) + { + PublishKey = PubnubCommon.PublishKey, + SubscribeKey = PubnubCommon.SubscribeKey, + Secure = false + }; + Pubnub pn = createPubNubInstance(config, authToken); + try + { + List listObject = new List + { + pn.JsonPluggableLibrary.DeserializeToObject(serverResponse), + channelArg + }; + return DeserializeToInternalObjectUtility + .DeserializeToInternalObject(pn.JsonPluggableLibrary, listObject); + } + finally + { + pn.Destroy(); + pn.PubnubUnitTest = null; + } + } + + [Test] + public static void IfHereNowWithoutUUIDsIsCalledThenChannelOccupancyShouldMatchTotalOccupancy() + { + string channel = "mocha"; + // Single channel + disable_uuids returns the compact format: occupancy present, no payload wrapper, no uuids array. + string serverResponse = "{\"status\": 200, \"message\": \"OK\", \"service\": \"Presence\", \"occupancy\": 3}"; + + PNHereNowResult result = ParseHereNowResponse(serverResponse, channel); + + Assert.IsNotNull(result, "here_now result not parsed"); + Assert.AreEqual(3, result.TotalOccupancy, "TotalOccupancy mismatch"); + Assert.IsTrue(result.Channels.TryGetValue(channel, out var channelData), "channel data missing"); + Assert.AreEqual(channel, channelData.ChannelName, "ChannelName not populated"); + Assert.AreEqual(3, channelData.Occupancy, "channel Occupancy should match total occupancy, not be hardcoded to 1"); + } + + [Test] + public static void IfHereNowWithoutUUIDsIsCalledOnEmptyChannelThenOccupancyShouldBeZero() + { + string channel = "mocha"; + // Empty single channel + disable_uuids: occupancy 0, no uuids array. + string serverResponse = "{\"status\": 200, \"message\": \"OK\", \"service\": \"Presence\", \"occupancy\": 0}"; + + PNHereNowResult result = ParseHereNowResponse(serverResponse, channel); + + Assert.IsNotNull(result, "here_now result not parsed"); + Assert.AreEqual(0, result.TotalOccupancy, "TotalOccupancy mismatch"); + Assert.IsTrue(result.Channels.TryGetValue(channel, out var channelData), "channel data missing"); + Assert.AreEqual(channel, channelData.ChannelName, "ChannelName not populated"); + Assert.AreEqual(0, channelData.Occupancy, "empty channel occupancy should be 0, not hardcoded to 1"); + } + + [Test] + public static void IfHereNowWithUUIDsIsCalledThenChannelOccupancyShouldMatchTotalOccupancy() + { + string channel = "mocha"; + // Single channel + include uuids: uuids array present alongside occupancy. + string serverResponse = "{\"status\": 200, \"message\": \"OK\", \"service\": \"Presence\", \"uuids\": [\"u1\", \"u2\", \"u3\"], \"occupancy\": 3}"; + + PNHereNowResult result = ParseHereNowResponse(serverResponse, channel); + + Assert.IsNotNull(result, "here_now result not parsed"); + Assert.AreEqual(3, result.TotalOccupancy, "TotalOccupancy mismatch"); + Assert.IsTrue(result.Channels.TryGetValue(channel, out var channelData), "channel data missing"); + Assert.AreEqual(channel, channelData.ChannelName, "ChannelName not populated"); + Assert.AreEqual(3, channelData.Occupancy, "channel Occupancy should match total occupancy"); + Assert.AreEqual(3, channelData.Occupants.Count, "occupant count mismatch"); + } + + [Test] + public static void IfHereNowWithUUIDsIsCalledOnEmptyChannelThenChannelEntryShouldStillExist() + { + string channel = "mocha"; + // Empty single channel with uuids requested: uuids array present but empty, occupancy 0. + string serverResponse = "{\"status\": 200, \"message\": \"OK\", \"service\": \"Presence\", \"uuids\": [], \"occupancy\": 0}"; + + PNHereNowResult result = ParseHereNowResponse(serverResponse, channel); + + Assert.IsNotNull(result, "here_now result not parsed"); + Assert.AreEqual(0, result.TotalOccupancy, "TotalOccupancy mismatch"); + Assert.IsTrue(result.Channels.TryGetValue(channel, out var channelData), "channel entry should exist even when empty"); + Assert.AreEqual(channel, channelData.ChannelName, "ChannelName not populated"); + Assert.AreEqual(0, channelData.Occupancy, "empty channel occupancy should be 0"); + Assert.IsNotNull(channelData.Occupants, "Occupants should be an empty list, not null"); + Assert.AreEqual(0, channelData.Occupants.Count, "empty channel should have no occupants"); + } + + [Test] + public static void IfHereNowWithoutUUIDsIsCalledOnMultipleChannelsThenEachOccupancyShouldBeCorrect() + { + // Multiple channels always return the payload format with per-channel occupancy, even with disable_uuids. + string serverResponse = "{\"status\": 200, \"message\": \"OK\", \"payload\": {\"channels\": {\"mocha\": {\"occupancy\": 3}, \"chabc\": {\"occupancy\": 5}}, \"total_channels\": 2, \"total_occupancy\": 8}, \"service\": \"Presence\"}"; + + PNHereNowResult result = ParseHereNowResponse(serverResponse, "mocha,chabc"); + + Assert.IsNotNull(result, "here_now result not parsed"); + Assert.AreEqual(8, result.TotalOccupancy, "TotalOccupancy mismatch"); + Assert.AreEqual(2, result.TotalChannels, "TotalChannels mismatch"); + Assert.IsTrue(result.Channels.TryGetValue("mocha", out var mochaData), "mocha channel data missing"); + Assert.AreEqual(3, mochaData.Occupancy, "mocha occupancy mismatch"); + Assert.IsTrue(result.Channels.TryGetValue("chabc", out var chabcData), "chabc channel data missing"); + Assert.AreEqual(5, chabcData.Occupancy, "chabc occupancy mismatch"); + } + + [Test] public static void IfHereNowIsCalledThenItShouldReturnInfoCipher() {