Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
7909ef8
Implement DataSync CRUD
jakub-grzesiowski Apr 7, 2026
e1ae240
Add DataSync object to Pubnub instance, implement Relationship CRUD +…
jakub-grzesiowski May 13, 2026
a53d9dd
WIP CRUD implementation for Users, Channels, and Memberships
jakub-grzesiowski May 18, 2026
5751af8
Add If-Match fix to HttpClientService, finish Users, Channels, and Me…
jakub-grzesiowski May 18, 2026
9eb6456
Merge branch 'master' of https://github.com/pubnub/c-sharp into data-…
jakub-grzesiowski May 19, 2026
fb52b50
Implement internal publishes
jakub-grzesiowski Jun 22, 2026
66a68f8
Implement Datasync PAM
jakub-grzesiowski Jun 24, 2026
75a0d53
Read userId and channelId for memberships
kleewho Jun 25, 2026
2fe2119
GetMemberships parsing fix, add temporary PAM Memberships testing sce…
jakub-grzesiowski Jun 26, 2026
c915671
Merge branch 'master' of https://github.com/pubnub/c-sharp into data-…
jakub-grzesiowski Jul 10, 2026
615a510
Remove idempotency key, add grant token logic to all datasync tests
jakub-grzesiowski Jul 15, 2026
765c398
add missing IfMatch parameters to delete operations
jakub-grzesiowski Jul 15, 2026
0538e3c
Add EntityClass support, rename operations to use Set/Update naming c…
jakub-grzesiowski Aug 18, 2026
95eb639
More test fixes
jakub-grzesiowski Sep 3, 2026
29bda19
Update API surface to latest OpenAPI specs
jakub-grzesiowski Sep 3, 2026
45397d6
Update test for CI integration
jakub-grzesiowski Sep 4, 2026
16b50c9
Fix broken file links
jakub-grzesiowski Sep 4, 2026
835e7a4
More broken file link fixes
jakub-grzesiowski Sep 4, 2026
aeaaf25
Fix projection tests
jakub-grzesiowski Sep 7, 2026
3b3bafb
Codacy fixes and missing parameters
jakub-grzesiowski Sep 7, 2026
8f54f64
Update the pubnub.yml file
jakub-grzesiowski Sep 7, 2026
b5b6752
Remove obsolete files
jakub-grzesiowski Sep 8, 2026
dba1a2b
Merge branch 'master' of https://github.com/pubnub/c-sharp into data-…
jakub-grzesiowski Sep 8, 2026
1ab42fb
Revert default version values from parameter objects
jakub-grzesiowski Sep 8, 2026
4b29f7b
Update UWP includes
jakub-grzesiowski Sep 8, 2026
4e7710c
Fix more broken file links
jakub-grzesiowski Sep 8, 2026
2e6085d
Minor test fixes
jakub-grzesiowski Sep 8, 2026
f2d8471
Renamed RelationshipClassVersion to MembershipClassVersion in CreateM…
jakub-grzesiowski Sep 8, 2026
939408c
PubNub SDK v9.0.0 release.
pubnub-release-bot Sep 8, 2026
afda7fc
Fix compilation errors in tests
jakub-grzesiowski Sep 8, 2026
5bb3ae8
Tweak test delay
jakub-grzesiowski Sep 8, 2026
134cd77
More minor test tweaks
jakub-grzesiowski Sep 8, 2026
8776c89
Another tweak
jakub-grzesiowski Sep 8, 2026
cabddcb
Update event parsing for new channel/user/membership event types
jakub-grzesiowski Sep 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ jobs:
PN_PUB_KEY: ${{ secrets.SDK_PUB_KEY }}
PN_SUB_KEY: ${{ secrets.SDK_SUB_KEY }}
PN_SEC_KEY: ${{ secrets.SDK_SEC_KEY }}
PN_DS_PUB_KEY: ${{ secrets.SDK_DS_PUB_KEY }}
PN_DS_SUB_KEY: ${{ secrets.SDK_DS_SUB_KEY }}
PN_DS_SEC_KEY: ${{ secrets.SDK_DS_SEC_KEY }}
PN_PAM_PUB_KEY: ${{ secrets.SDK_PAM_PUB_KEY }}
PN_PAM_SUB_KEY: ${{ secrets.SDK_PAM_SUB_KEY }}
PN_PAM_SEC_KEY: ${{ secrets.SDK_PAM_SEC_KEY }}
Expand Down
54 changes: 47 additions & 7 deletions .pubnub.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
name: c-sharp
version: "8.3.4"
version: "9.0.0"
schema: 1
scm: github.com/pubnub/c-sharp
changelog:
- date: 2026-09-08
version: v9.0.0
changes:
- type: feature
text: "Added Data Sync feature support."
- date: 2026-07-29
version: v8.3.4
changes:
Expand Down Expand Up @@ -1003,6 +1008,41 @@ features:
- OBJECTS-MANAGE-CHANNEL-MEMBERS-V2
- OBJECTS-FILTERING
- OBJECTS-SORTING
data-sync:
- DATASYNC-USER-CREATE
- DATASYNC-USER-GET
- DATASYNC-USER-GET-ALL
- DATASYNC-USER-UPDATE
- DATASYNC-USER-PATCH
- DATASYNC-USER-DELETE
- DATASYNC-CHANNEL-CREATE
- DATASYNC-CHANNEL-GET
- DATASYNC-CHANNEL-GET-ALL
- DATASYNC-CHANNEL-UPDATE
- DATASYNC-CHANNEL-PATCH
- DATASYNC-CHANNEL-DELETE
- DATASYNC-MEMBERSHIP-CREATE
- DATASYNC-MEMBERSHIP-GET
- DATASYNC-MEMBERSHIP-GET-ALL
- DATASYNC-MEMBERSHIP-UPDATE
- DATASYNC-MEMBERSHIP-PATCH
- DATASYNC-MEMBERSHIP-DELETE
- DATASYNC-ENTITY-CREATE
- DATASYNC-ENTITY-GET
- DATASYNC-ENTITY-GET-ALL
- DATASYNC-ENTITY-UPDATE
- DATASYNC-ENTITY-PATCH
- DATASYNC-ENTITY-DELETE
- DATASYNC-RELATIONSHIP-CREATE
- DATASYNC-RELATIONSHIP-GET
- DATASYNC-RELATIONSHIP-GET-ALL
- DATASYNC-RELATIONSHIP-UPDATE
- DATASYNC-RELATIONSHIP-PATCH
- DATASYNC-RELATIONSHIP-DELETE
- DATASYNC-FILTERING
- DATASYNC-SORTING
- DATASYNC-EVENTS-LISTENER
- DATASYNC-ACCESS-MANAGER
files:
- FILES-SEND-FILE
- FILES-LIST-FILES
Expand All @@ -1018,14 +1058,14 @@ features:
- QUERY-PARAM
supported-platforms:
-
version: Pubnub 'C#' 8.3.4
version: Pubnub 'C#' 9.0.0
platforms:
- Windows 10 and up
- Windows Server 2008 and up
frameworks:
- .Net Framework 4.5+
-
version: PubnubPCL 'C#' 8.3.4
version: PubnubPCL 'C#' 9.0.0
platforms:
- Xamarin.Android
- Xamarin.iOS
Expand All @@ -1037,7 +1077,7 @@ supported-platforms:
frameworks:
- .Net 4.5+
-
version: PubnubUWP 'C#' 8.3.4
version: PubnubUWP 'C#' 9.0.0
platforms:
- Windows Phone 10
- Universal Windows Apps
Expand All @@ -1061,7 +1101,7 @@ sdks:
distribution-type: source
distribution-repository: GitHub
package-name: Pubnub
location: https://github.com/pubnub/c-sharp/releases/tag/v8.3.4
location: https://github.com/pubnub/c-sharp/releases/tag/v9.0.0
requires:
-
name: ".Net"
Expand Down Expand Up @@ -1302,7 +1342,7 @@ sdks:
distribution-type: source
distribution-repository: GitHub
package-name: PubNubPCL
location: https://github.com/pubnub/c-sharp/releases/tag/v8.3.4
location: https://github.com/pubnub/c-sharp/releases/tag/v9.0.0
requires:
-
name: ".Net"
Expand Down Expand Up @@ -1653,7 +1693,7 @@ sdks:
distribution-type: source
distribution-repository: GitHub
package-name: PubnubUWP
location: https://github.com/pubnub/c-sharp/releases/tag/v8.3.4
location: https://github.com/pubnub/c-sharp/releases/tag/v9.0.0
requires:
-
name: "Universal Windows Platform Development"
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
v9.0.0 - September 08 2026
-----------------------------
- Added: added Data Sync feature support.

v8.3.4 - July 29 2026
-----------------------------
- Fixed: fixed issue related to Reconnect to retain subscription state.
Expand Down
2 changes: 2 additions & 0 deletions src/Api/PubnubApi/Callbacks/SubscribeCallback.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public abstract class SubscribeCallback

public abstract void ObjectEvent(Pubnub pubnub, PNObjectEventResult objectEvent);

public virtual void DataSyncEvent(Pubnub pubnub, PNDataSyncEventResult dataSyncEvent) { }

public abstract void MessageAction(Pubnub pubnub, PNMessageActionEventResult messageAction);

public abstract void File(Pubnub pubnub, PNFileEventResult fileEvent);
Expand Down
123 changes: 113 additions & 10 deletions src/Api/PubnubApi/EndPoint/Access/GrantTokenOperation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,21 @@ public class GrantTokenOperation : PubnubCoreBase
Users = new Dictionary<string, PNTokenAuthValues>()
};

private const string UuidsUsersConflictMessage = "Either Uuids or Users can be used. Not both.";

// Channels/Spaces and Uuids/Users are alternative names for the same token scopes, so a
// token can only carry one name from each pair. When both are supplied across Resources
// and Patterns the classic name wins and the alternative is left out of the request.
private const string SpacesIgnoredMessage = "GrantToken: Spaces permissions were supplied together with Channels permissions, so the Spaces permissions were ignored and are NOT included in the token. Channels and Spaces are alternative names for the same scope - supply only one of them across Resources and Patterns.";
private const string UsersIgnoredMessage = "GrantToken: Users permissions were supplied together with Uuids permissions, so the Users permissions were ignored and are NOT included in the token. Uuids and Users are alternative names for the same scope - supply only one of them across Resources and Patterns.";

private int grantTTL = -1;
private PNCallback<PNAccessManagerTokenResult> savedCallbackGrantToken;
private Dictionary<string, object> queryParam;
private Dictionary<string, object> grantMeta;
private string pubnubAuthorizedUuid = string.Empty;
private string pubnubAuthorizedUserId = string.Empty;
private PNDataSyncProjections pubnubDataSyncProjections;

public GrantTokenOperation(PNConfiguration pubnubConfig, IJsonPluggableLibrary jsonPluggableLibrary, IPubnubUnitTest pubnubUnit, EndPoint.TokenManager tokenManager, Pubnub instance) : base(pubnubConfig, jsonPluggableLibrary, pubnubUnit, tokenManager, instance)
{
Expand Down Expand Up @@ -71,9 +80,8 @@ public GrantTokenOperation Resources(PNTokenResources resources)
resources.Spaces != null && resources.Spaces.Count > 0) {
throw new ArgumentException("Either Channels or Spaces can be used. Not both.");
}
if (resources.Uuids != null && resources.Uuids.Count > 0 &&
resources.Users != null && resources.Users.Count > 0) {
throw new ArgumentException("Either Uuids or Users can be used. Not both.");
if (HasEntries(resources.Uuids) && HasEntries(resources.Users)) {
throw new ArgumentException(UuidsUsersConflictMessage);
}
pubnubResources = resources;
if (pubnubResources.Channels == null) {
Expand Down Expand Up @@ -102,9 +110,8 @@ public GrantTokenOperation Patterns(PNTokenPatterns patterns)
patterns.Spaces != null && patterns.Spaces.Count > 0) {
throw new ArgumentException("Either Channels or Spaces can be used. Not both.");
}
if (patterns.Uuids != null && patterns.Uuids.Count > 0 &&
patterns.Users != null && patterns.Users.Count > 0) {
throw new ArgumentException("Either Uuids or Users can be used. Not both.");
if (HasEntries(patterns.Uuids) && HasEntries(patterns.Users)) {
throw new ArgumentException(UuidsUsersConflictMessage);
}

pubnubPatterns = patterns;
Expand All @@ -128,6 +135,17 @@ public GrantTokenOperation Patterns(PNTokenPatterns patterns)
return this;
}

/// <summary>
/// Assigns Data Sync projections to resources/patterns. The projection name
/// per resource is encoded into the token's meta "pn-projections" section.
/// Entity-level Data Sync permissions are set through <see cref="Resources"/> / <see cref="Patterns"/>.
/// </summary>
public GrantTokenOperation DataSyncProjections(PNDataSyncProjections projections)
{
this.pubnubDataSyncProjections = projections;
return this;
}

public GrantTokenOperation TTL(int ttl)
{
this.grantTTL = ttl;
Expand Down Expand Up @@ -214,6 +232,11 @@ internal void GrantAccess(PNCallback<PNAccessManagerTokenResult> callback)
});
}

private static bool HasEntries(Dictionary<string, PNTokenAuthValues> perms)
{
return perms != null && perms.Count > 0;
}

private bool FillPermissionMappingWithMaskValues(Dictionary<string, PNTokenAuthValues> dPerms, bool currentAtleastOnePermission, out Dictionary<string, int> dPermsWithMaskValues)
{
dPermsWithMaskValues = new Dictionary<string, int>();
Expand All @@ -230,6 +253,72 @@ private bool FillPermissionMappingWithMaskValues(Dictionary<string, PNTokenAuthV
return internalAtleastOnePermission;
}

private bool AddDataSyncScopes(Dictionary<string, object> collection, PNDataSyncTokenScopes scopes, bool currentAtleastOnePermission)
{
bool atleastOnePermission = currentAtleastOnePermission;
if (scopes == null) {
return atleastOnePermission;
}
atleastOnePermission = AddDataSyncScope(collection, "datasync:entities", scopes.Entities, atleastOnePermission);
atleastOnePermission = AddDataSyncScope(collection, "datasync:relationships", scopes.Relationships, atleastOnePermission);
atleastOnePermission = AddDataSyncScope(collection, "datasync:memberships", scopes.Memberships, atleastOnePermission);
return atleastOnePermission;
}

private bool AddDataSyncScope(Dictionary<string, object> collection, string key, Dictionary<string, PNTokenAuthValues> perms, bool currentAtleastOnePermission)
{
if (perms == null || perms.Count == 0) {
return currentAtleastOnePermission;
}
bool atleastOnePermission = FillPermissionMappingWithMaskValues(perms, currentAtleastOnePermission, out Dictionary<string, int> masks);
collection[key] = masks;
return atleastOnePermission;
}

private Dictionary<string, object> BuildProjectionsMeta()
{
if (this.pubnubDataSyncProjections == null) {
return null;
}
Dictionary<string, object> pnProjections = new Dictionary<string, object>();
Dictionary<string, object> res = BuildProjectionScope(this.pubnubDataSyncProjections.Resources);
Dictionary<string, object> pat = BuildProjectionScope(this.pubnubDataSyncProjections.Patterns);
if (res != null) {
pnProjections["res"] = res;
}
if (pat != null) {
pnProjections["pat"] = pat;
}
return pnProjections.Count > 0 ? pnProjections : null;
}

private static Dictionary<string, object> BuildProjectionScope(PNDataSyncProjectionScope scope)
{
if (scope == null) {
return null;
}
Dictionary<string, object> flat = new Dictionary<string, object>();
FlattenProjections(flat, scope.Entities, "entities");
FlattenProjections(flat, scope.Users, "users");
FlattenProjections(flat, scope.Channels, "channels");
FlattenProjections(flat, scope.Relationships, "relationships");
FlattenProjections(flat, scope.Memberships, "memberships");
return flat.Count > 0 ? flat : null;
}

private static void FlattenProjections(Dictionary<string, object> target, Dictionary<string, string> map, string type)
{
if (map == null) {
return;
}
foreach (KeyValuePair<string, string> kvp in map) {
if (string.IsNullOrEmpty(kvp.Key)) {
continue;
}
target[$"datasync:{type}:{kvp.Key}"] = kvp.Value;
}
}

internal async Task<PNResult<PNAccessManagerTokenResult>> GrantAccess()
{
if (string.IsNullOrEmpty(config.SecretKey) || string.IsNullOrEmpty(config.SecretKey.Trim()) || config.SecretKey.Length <= 0) {
Expand Down Expand Up @@ -341,6 +430,9 @@ private RequestParameter CreateRequestParameter()
atleastOnePermission = FillPermissionMappingWithMaskValues(this.pubnubResources.Spaces, atleastOnePermission, out spBitmaskPermCollection);
atleastOnePermission = FillPermissionMappingWithMaskValues(this.pubnubPatterns.Spaces, atleastOnePermission, out spPatternBitmaskPermCollection);
} else {
if (HasEntries(pubnubResources.Spaces) || HasEntries(pubnubPatterns.Spaces)) {
config?.Logger?.Warn(SpacesIgnoredMessage);
}
spBitmaskPermCollection = new Dictionary<string, int>();
spPatternBitmaskPermCollection = new Dictionary<string, int>();
}
Expand All @@ -363,14 +455,13 @@ private RequestParameter CreateRequestParameter()
atleastOnePermission = FillPermissionMappingWithMaskValues(this.pubnubResources.Users, atleastOnePermission, out userBitmaskPermCollection);
atleastOnePermission = FillPermissionMappingWithMaskValues(this.pubnubPatterns.Users, atleastOnePermission, out userPatternBitmaskPermCollection);
} else {
if (HasEntries(pubnubResources.Users) || HasEntries(pubnubPatterns.Users)) {
config?.Logger?.Warn(UsersIgnoredMessage);
}
userBitmaskPermCollection = new Dictionary<string, int>();
userPatternBitmaskPermCollection = new Dictionary<string, int>();
}

if (!atleastOnePermission) {
config?.Logger?.Warn("GrantToken At least one permission is needed for at least one or more of uuids/users, channels/spaces or groups");
}

Dictionary<string, object> resourcesCollection = new Dictionary<string, object>
{
{ "channels", chBitmaskPermCollection },
Expand All @@ -389,11 +480,23 @@ private RequestParameter CreateRequestParameter()
{ "spaces", spPatternBitmaskPermCollection }
};

atleastOnePermission = AddDataSyncScopes(resourcesCollection, this.pubnubResources.DataSync, atleastOnePermission);
atleastOnePermission = AddDataSyncScopes(patternsCollection, this.pubnubPatterns.DataSync, atleastOnePermission);

if (!atleastOnePermission) {
config?.Logger?.Warn("GrantToken At least one permission is needed for at least one or more of uuids/users, channels/spaces, groups or datasync entities/relationships/memberships");
}

Dictionary<string, object> optimizedMeta = new Dictionary<string, object>();
if (this.grantMeta != null) {
optimizedMeta = this.grantMeta;
}

Dictionary<string, object> pnProjections = BuildProjectionsMeta();
if (pnProjections != null) {
optimizedMeta["pn-projections"] = pnProjections;
}

Dictionary<string, object> permissionCollection = new Dictionary<string, object>
{
{ "resources", resourcesCollection },
Expand Down
Loading
Loading