---
source_url: https://www.pubnub.com/docs/sdks/c-sharp/api-reference/data-sync
title: DataSync API for C# SDK
updated_at: 2026-09-08T16:52:30.000Z
sdk_name: PubNub C# SDK
sdk_version: 9.0.0
---

# DataSync API for C# SDK

PubNub C# SDK, use the latest version: 9.0.0

Install:

```bash
dotnet add package PubNub@9.0.0
```

## Documentation index

To discover more PubNub resources:

1. Fetch [PubNub's llms.txt](https://www.pubnub.com/llms-full.txt) for a list of available pages in Markdown format.
2. Identify relevant URLs from that index.
3. Fetch the target pages.

Do not assume a path exists, always check the index first.

[DataSync](https://www.pubnub.com/docs/general/data-sync/overview.md) is PubNub's data layer for storing application state (users, channels, memberships, and any custom object type) and keeping every connected client current through real-time events. Use it to model the objects your application works with and to react the moment they change.

DataSync is the successor to [App Context](https://www.pubnub.com/docs/sdks/c-sharp/api-reference/objects.md).

:::note DataSync entities and SDK entities are different things
This page is about **DataSync entities**, the records the service stores and treats as the source of truth for your application state. They are created and read through the `pubnub.DataSync.*` methods documented here.
An **SDK entity** is a local client-side handle such as `pubnub.Channel("room-1")`. It carries no stored state of its own, it just gives you a scoped surface for subscribing and calling APIs. Refer to [SDK entities](https://www.pubnub.com/docs/sdks/c-sharp/api-reference/publish-and-subscribe.md#sdk-entities) in Publish and subscribe.
The C# SDK has no dedicated DataSync SDK entity. To observe a DataSync object you subscribe to its channel with an ordinary `Channel` SDK entity and attach a DataSync listener to the resulting `Subscription`, a `SubscriptionSet`, or the PubNub client. Refer to [Real-time updates](#real-time-updates).
:::

The classes that your objects conform to (their types and schemas) are defined through the Admin API or the [Admin Portal](https://admin.pubnub.com/), not through this SDK:

* Entity classes, which back users, channels, and custom entities: [list](https://www.pubnub.com/docs/admin-api/get-all-entity-class-entries.md), [read](https://www.pubnub.com/docs/admin-api/get-entity-class-by-id.md), [create](https://www.pubnub.com/docs/admin-api/create-a-new-entity-class.md), [replace](https://www.pubnub.com/docs/admin-api/update-entity-class-with-complete-resource-replacement.md), and [delete](https://www.pubnub.com/docs/admin-api/delete-entity-class-by-id.md). Partial updates aren't implemented; replace the complete class version instead.
* Relationship classes, which back memberships and custom relationships: [list](https://www.pubnub.com/docs/admin-api/get-all-relationship-classes.md), [read](https://www.pubnub.com/docs/admin-api/get-relationship-class-by-name-and-version.md), [create](https://www.pubnub.com/docs/admin-api/create-a-new-relationship-class.md), [replace](https://www.pubnub.com/docs/admin-api/update-relationship-class-with-complete-resource-replacement.md), and [delete](https://www.pubnub.com/docs/admin-api/delete-relationship-class-by-name-and-version.md).

`User` and `Channel` entity classes and the `Membership` relationship class are predefined on every keyset, so you only need to define classes for your own custom entities and relationships.

A class definition is what decides, for every object of that class, which `Payload` fields are filterable and sortable, which [projection](https://www.pubnub.com/docs/sdks/c-sharp/api-reference/access-manager.md#grant-token) each field belongs to, and how long the object lives before it expires. Refer to [managing classes](https://www.pubnub.com/docs/general/data-sync/schemas-and-validation.md#managing-classes) for more information.

Every method returns a `PNResult<T>` carrying the HTTP `Status` and, for everything except the `Delete*` methods, a `Result` holding the object or the list of objects. A stored object carries its free-form `Status` and its `ExpiresAt` auto-deletion timestamp (ISO 8601) whenever those are set on it.

###### Authorization

Every DataSync request must be [authorized](https://www.pubnub.com/docs/general/data-sync/access-control.md#authorizing-requests) with a token or signature. A request with no credential fails with a `401`; an invalid credential or a credential that doesn't permit the operation fails with a `403`.

###### Pagination

DataSync pagination is **forward-only**. There is no previous-page cursor, and `PaginationMeta` exposes no `PrevCursor` or `HasPrev`. To revisit an earlier page, page from the start again.

All `Get<Plural>` methods (`GetUsers`, `GetChannels`, `GetMemberships`, `GetEntities`, and `GetRelationships`) accept `Cursor` and `Limit` (default `20`, max `100`) and return a `Meta` object of type `PaginationMeta` with `NextCursor`, `HasNext`, and `Limit`.

To page forward, pass the returned `NextCursor` back as `Cursor` on the next call, and stop when `HasNext` is `false`. `Meta` is only populated when the response carries a `meta` object, so guard the access, for example `response.Result.Meta?.NextCursor`. Refer to [sorting and pagination](https://www.pubnub.com/docs/general/data-sync/data-operations.md#sorting-and-pagination) for details.

###### Filtering and sorting

All `Get<Plural>` methods accept two mutually exclusive filter parameters, `FilterFast` and `Filter`. The SDK sends whatever you set, so a call that carries both reaches the server and fails there with a `400`.

| Parameter | Consistency | Properties it can read | Expression complexity |
| --- | --- | --- | --- |
| `FilterFast` | Strongly consistent, reflects the latest writes | Filtering mode `simple` or `full` | Limited number of conditions |
| `Filter` | Eventually consistent, results can briefly lag writes | Filtering mode `full` only | Full expression language |

Reach for `FilterFast` when the query has to see an object you just wrote, and for `Filter` when you need the full expression language over a `full`-indexed property.

:::warning danger
Filter
is not the strongly consistent one
The two names read the wrong way round if you assume `Filter` is the basic option. `FilterFast` is the strongly consistent, limited one. `Filter` is the richer, eventually consistent one. Earlier builds of the SDK named these `Filter` and `FilterAdvanced` respectively, so a `Filter` expression written against an older build now runs on the other backing store. Rename `Filter` to `FilterFast` and `FilterAdvanced` to `Filter` when you upgrade.
:::

Both parameters share the same expression language, a string built from a property name, an operator, and a value:

| Operators |  |
| --- | --- |
| Comparison | `==`, `!=`, `<`, `>`, `<=`, `>=` |
| Pattern matching | `LIKE` (case-insensitive), `SLIKE` (case-sensitive), `ILIKE` (case-insensitive, same as `LIKE`) |
| Logical | `&&`, `||`, `!`, and parentheses for grouping |

Values are quoted strings, numbers, `true`, `false`, or `null`. Pattern operators (`LIKE`, `SLIKE`, `ILIKE`) apply to string properties only, and `null` is only valid with `==` and `!=`. Reference a property by its `name`, not its declared `path`, and only properties declared on the class are filterable. To reach a nested `Payload` property, declare it as a class property first, then filter by that property's `name`, for example `"price < 100"` for a `product` class that declares `price`.

Some examples:

```csharp
FilterFast = "price < 100"                               // strongly consistent, single condition
FilterFast = "(price < 100 && stock > 0) || price > 500" // strongly consistent, grouped conditions
Filter = "name LIKE \"*sneaker*\""                       // eventually consistent, needs filtering mode "full"
Filter = "!(status == \"discontinued\")"                 // eventually consistent, negated condition
```

Both parameters only work over properties declared on the class, plus the built-in `id`, `createdAt`, `updatedAt`, and `status` fields, which are always filterable and sortable without being declared. `eTag` and `expiresAt` are neither. Refer to [filtering](https://www.pubnub.com/docs/general/data-sync/data-operations.md#filtering) for the two filtering tiers, and [property definitions](https://www.pubnub.com/docs/general/data-sync/schemas-and-validation.md#property-definitions) for how properties are declared.

Use `Sort` to order results. Its value is a comma-separated list of fields, each a property name optionally **suffixed** with `:asc` or `:desc`. A bare name sorts ascending:

```csharp
Sort = "price:desc"                            // single field, descending
Sort = "type,price:desc"                       // type ascending, then price descending
Sort = "createdAt:asc"                         // explicit ascending
```

The `+field` and `-field` prefixes are **not** accepted. A leading `-` is read as part of the property name, so `"-price"` fails with a `400`. You can only sort by properties declared with a filtering mode other than `none`, or by the four built-in fields.

The SDK doesn't validate `FilterFast`, `Filter`, `Sort`, or `Limit` locally, it passes them through as-is. An invalid expression, an unsortable field, or an out-of-range `Limit` only surfaces as an error response from the server, not as a client-side exception.

###### Concurrency (ETag)

Every stored object carries an `ETag`. To guard against concurrent writes, pass the `ETag` you read earlier as `IfMatch` on `Set*` and `Update*` calls.

If the server-side value has changed in the meantime, the operation fails with a `412`, and you should re-read the object and retry. Refer to [optimistic concurrency with ETags](https://www.pubnub.com/docs/general/data-sync/data-operations.md#optimistic-concurrency-with-etags) for details.

`IfMatch` is also optional on every `Delete*` call, across all five resources.

###### Partial updates

The `Update*` methods apply a partial update using raw JSON Patch (RFC 6902). Each operation is a `JsonPatchOperation` with an `Op` (`Add`, `Remove`, `Replace`, `Move`, `Copy`, or `Test`), a `Path` (a full JSON Pointer, for example `/status` or `/payload/price`), and, depending on the operation, a `Value` or a `From`.

Unlike simplified dot-notation patch models, the SDK does not add any prefix for you. To target a field inside the object's `payload`, write the full pointer yourself, for example `/payload/price`, not `/price`. A bare `/price` targets a top-level field named `price`, which doesn't exist. Top-level fields like `/status` can be patched directly. Refer to [partial update](https://www.pubnub.com/docs/general/data-sync/data-operations.md#partial-update) for details.

Correct patch forms:

```csharp
new JsonPatchOperation { Op = JsonPatchOperationType.Replace, Path = "/status", Value = "inactive" }
new JsonPatchOperation { Op = JsonPatchOperationType.Add, Path = "/payload/tags/-", Value = "featured" } // appends to payload.tags
new JsonPatchOperation { Op = JsonPatchOperationType.Remove, Path = "/payload/legacy/field" }
new JsonPatchOperation { Op = JsonPatchOperationType.Move, Path = "/payload/newField", From = "/payload/oldField" }
new JsonPatchOperation { Op = JsonPatchOperationType.Copy, Path = "/payload/backupField", From = "/payload/field" }
new JsonPatchOperation { Op = JsonPatchOperationType.Test, Path = "/payload/price", Value = 79.99 } // fails the whole patch if the current value doesn't match
```

`Move` and `Copy` require `From` (the source pointer) instead of `Value`. `Test` requires `Value` and fails the entire patch if the current value at `Path` doesn't match, use it to guard the rest of the operations against a stale read.

Paths address the object's stored property names, the same keys that come back in responses. The class version is `entityClassVersion` on users, channels, and entities, and `relationshipClassVersion` on memberships and relationships. The fields set at creation cannot be patched: `entityClass` and `entityClassLevel` on users, channels, and entities, `relationshipClass`, `entityAId`, and `entityBId` on relationships, and `userId` and `channelId` on memberships.

The `Operations` list must contain at least one operation.

:::tip Request execution
Use `try`/`catch` when working with the C# SDK.
If a request has invalid parameters (for example, a missing required field), the SDK throws an exception. If the request reaches the server but fails (server error or network issue), the error details are available in the returned `status`.
```csharp
try
{
    PNResult<PNPublishResult> publishResponse = await pubnub.Publish()
        .Message("Why do Java developers wear glasses? Because they can't C#.")
        .Channel("my_channel")
        .ExecuteAsync();
    PNStatus status = publishResponse.Status;
    Console.WriteLine("Server status code : " + status.StatusCode.ToString());
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```
:::

:::note Requires Access Manager
DataSync requires that the *Access Manager* add-on is enabled for your key in the [Admin Portal](https://admin.pubnub.com/). Read the [support page](https://support.pubnub.com/hc/en-us/articles/360051974791-How-do-I-enable-add-on-features-for-my-keys-) on enabling add-on features on your keys.
:::

## Users

Users are built-in objects. There are no top-level `Name` or `Email` properties, all application data lives in the free-form `Payload`. Refer to [users](https://www.pubnub.com/docs/general/data-sync/users-channels-memberships.md#users) for the concept.

### Create user

Creates a user. Supply an `Id` to control the identifier, or omit it to let the server generate one.

#### Method(s)

```csharp
pubnub.DataSync.CreateUser(CreateUserParameters parameters)
```

| Parameter | Description |
| --- | --- |
| `Id`Type: string | User identifier. Omit to let the server generate a UUID. Max 255 characters. |
| `EntityClass`Type: string | Name of the user class this instance belongs to. Defaults to `"User"` on the server if omitted. |
| `EntityClassVersion` *Type: int | Version of the user class schema. |
| `EntityClassLevel`Type: string | Class hierarchy level, `"Global"` or `"SubKey"`, used to disambiguate classes with the same name defined at different levels. |
| `Status`Type: string | Free-form lifecycle status. Max 100 characters. |
| `Payload`Type: Dictionary`<string, object>` | Free-form JSON object holding your application data. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```csharp
try
{
    PNResult<PNDataSyncUserResult> response = await pubnub.DataSync.CreateUser(new CreateUserParameters
    {
        Id = "user-alice",
        EntityClassVersion = 1,
        Payload = new Dictionary<string, object>
        {
            { "name", "Alice" },
            { "type", "shopper" },
        },
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Id);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "user-alice",
        "entityClass": "User",
        "entityClassVersion": 1,
        "entityClassLevel": "Global",
        "payload": {
            "name": "Alice",
            "type": "shopper"
        },
        "createdAt": "2026-07-13T09:00:00.000Z",
        "updatedAt": "2026-07-13T09:00:00.000Z",
        "eTag": "AbQdEfGhIjKlMn"
    }
}
```

### Get user

Returns a single user by `Id`.

#### Method(s)

```csharp
pubnub.DataSync.GetUser(GetUserParameters parameters)
```

| Parameter | Description |
| --- | --- |
| `Id` *Type: string | User identifier. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```csharp
try
{
    PNResult<PNDataSyncUserResult> response = await pubnub.DataSync.GetUser(new GetUserParameters
    {
        Id = "user-alice",
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Id);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "user-alice",
        "entityClass": "User",
        "entityClassVersion": 1,
        "entityClassLevel": "Global",
        "payload": {
            "name": "Alice",
            "type": "shopper"
        },
        "createdAt": "2026-07-13T09:00:00.000Z",
        "updatedAt": "2026-07-13T09:00:00.000Z",
        "eTag": "AbQdEfGhIjKlMn"
    }
}
```

### Get all users

Returns a paginated list of users. All parameters are optional, so you can call `GetUsers` with an empty parameters object. For pagination, filtering, and sorting, refer to [sorting and pagination](https://www.pubnub.com/docs/general/data-sync/data-operations.md#sorting-and-pagination).

#### Method(s)

```csharp
pubnub.DataSync.GetUsers(GetUsersParameters parameters)
```

| Parameter | Description |
| --- | --- |
| `EntityClass`Type: string | User class to list. Omit to list the Global `User` class and all of its subclasses. |
| `Cursor`Type: string | Opaque pagination cursor. Omit for the first page. |
| `Limit`Type: int | Maximum number of users per page. Default `20`. Max `100`. |
| `FilterFast`Type: string | Strongly consistent filter expression, reflects the latest writes. Limited number of conditions. Cannot be combined with `Filter`. |
| `Filter`Type: string | Eventually consistent filter expression, results can briefly lag writes. Full expression language. Cannot be combined with `FilterFast`. |
| `Sort`Type: string | Comma-separated fields, each optionally suffixed with `:asc` or `:desc` (ascending is the default when a field has no suffix), for example `"createdAt:desc,id"`. |
| `EntityClassVersion`Type: int | User class version to list. Omit to list users across every version of the class. |
| `EntityClassLevel`Type: string | Class hierarchy level, `"Global"` or `"SubKey"`, used to disambiguate classes with the same name at different levels. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```csharp
try
{
    PNResult<PNDataSyncUsersListResult> response = await pubnub.DataSync.GetUsers(new GetUsersParameters
    {
        Limit = 20,
        Sort = "createdAt",
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Data.Count);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

#### Response

```json
{
    "status": 200,
    "data": [
        {
            "id": "user-alice",
            "entityClass": "User",
            "entityClassVersion": 1,
            "entityClassLevel": "Global",
            "payload": {
                "name": "Alice",
                "type": "shopper"
            },
            "createdAt": "2026-07-13T09:00:00.000Z",
            "updatedAt": "2026-07-13T09:00:00.000Z",
            "eTag": "AbQdEfGhIjKlMn"
        }
    ],
    "meta": {
        "next_cursor": "b2Zmc2V0PTIw",
        "has_next": true,
        "limit": 20
    }
}
```

#### Other examples

##### Filter with FilterFast

`FilterFast` reflects the latest writes and runs over properties declared with filtering mode `simple` or `full`. Reference a declared property by its `name`, not its `path`.

```csharp
try
{
    PNResult<PNDataSyncUsersListResult> response = await pubnub.DataSync.GetUsers(new GetUsersParameters
    {
        FilterFast = "type == \"shopper\"",
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Data.Count);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

##### Filter with Filter

`Filter` runs over properties declared with filtering mode `full` and can briefly lag recent writes. It shares the same expression language as `FilterFast`, so only one of the two can be sent per call.

```csharp
try
{
    PNResult<PNDataSyncUsersListResult> response = await pubnub.DataSync.GetUsers(new GetUsersParameters
    {
        Filter = "name LIKE \"*Alice*\"",
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Data.Count);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

##### Page through results with Cursor

Pass no `Cursor` on the first call. Take `Meta.NextCursor` from the response and pass it back as `Cursor` on the next call. Stop when `Meta.HasNext` is `false`.

```csharp
try
{
    string cursor = null;
    bool hasNext = true;
    int page = 0;

    while (hasNext)
    {
        PNResult<PNDataSyncUsersListResult> response = await pubnub.DataSync.GetUsers(new GetUsersParameters
        {
            FilterFast = "type == \"shopper\"",
            Limit = 20,
            Cursor = cursor,
        });

        if (!response.Status.Error)
        {
            Console.WriteLine($"Page {++page}: {response.Result.Data.Count}");
            cursor = response.Result.Meta.NextCursor;
            hasNext = response.Result.Meta.HasNext;
        }
        else
        {
            Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
            hasNext = false;
        }
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

### Set user

Replaces a user in full (PUT). Send the complete set of fields, any field you omit is cleared. To guard against concurrent writes, pass `IfMatch` (see [optimistic concurrency with ETags](https://www.pubnub.com/docs/general/data-sync/data-operations.md#optimistic-concurrency-with-etags)).

#### Method(s)

```csharp
pubnub.DataSync.SetUser(SetUserParameters parameters)
```

| Parameter | Description |
| --- | --- |
| `Id` *Type: string | User identifier. |
| `EntityClassVersion` *Type: int | Version of the user class schema. |
| `Status`Type: string | Free-form lifecycle status. Max 100 characters. |
| `Payload`Type: Dictionary`<string, object>` | Free-form JSON object holding your application data. |
| `IfMatch`Type: string | The `ETag` from a prior read. The update succeeds only if it still matches, otherwise the server returns `412`. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```csharp
try
{
    PNResult<PNDataSyncUserResult> response = await pubnub.DataSync.SetUser(new SetUserParameters
    {
        Id = "user-alice",
        EntityClassVersion = 1,
        Payload = new Dictionary<string, object>
        {
            { "name", "Alice B." },
            { "type", "shopper" },
        },
        IfMatch = "AbQdEfGhIjKlMn",
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Id);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "user-alice",
        "entityClass": "User",
        "entityClassVersion": 1,
        "entityClassLevel": "Global",
        "payload": {
            "name": "Alice B.",
            "type": "shopper"
        },
        "createdAt": "2026-07-13T09:00:00.000Z",
        "updatedAt": "2026-07-13T10:15:00.000Z",
        "eTag": "CdEfGhIjKlMnOp"
    }
}
```

### Update user

Applies a partial update to a user with raw JSON Patch operations. Refer to [partial update](https://www.pubnub.com/docs/general/data-sync/data-operations.md#partial-update) for the operation model.

#### Method(s)

```csharp
pubnub.DataSync.UpdateUser(UpdateUserParameters parameters)
```

| Parameter | Description |
| --- | --- |
| `Id` *Type: string | User identifier. |
| `Operations` *Type: List`<JsonPatchOperation>` | One or more JSON Patch operations. Must contain at least one item. |
| `IfMatch`Type: string | The `ETag` from a prior read. The patch succeeds only if it still matches, otherwise the server returns `412`. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```csharp
try
{
    PNResult<PNDataSyncUserResult> response = await pubnub.DataSync.UpdateUser(new UpdateUserParameters
    {
        Id = "user-alice",
        Operations = new List<JsonPatchOperation>
        {
            new JsonPatchOperation { Op = JsonPatchOperationType.Replace, Path = "/payload/name", Value = "Alice B." },
        },
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Id);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "user-alice",
        "entityClass": "User",
        "entityClassVersion": 1,
        "entityClassLevel": "Global",
        "payload": {
            "name": "Alice B.",
            "type": "shopper"
        },
        "createdAt": "2026-07-13T09:00:00.000Z",
        "updatedAt": "2026-07-13T10:20:00.000Z",
        "eTag": "EfGhIjKlMnOpQr"
    }
}
```

### Remove user

Deletes a user by `Id`.

#### Method(s)

```csharp
pubnub.DataSync.DeleteUser(DeleteUserParameters parameters)
```

| Parameter | Description |
| --- | --- |
| `Id` *Type: string | User identifier. |
| `IfMatch`Type: string | The `ETag` from a prior read. The delete succeeds only if it still matches, otherwise the server returns `412`. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```csharp
try
{
    PNResult<PNDataSyncDeleteUserResult> response = await pubnub.DataSync.DeleteUser(new DeleteUserParameters
    {
        Id = "user-alice",
    });

    Console.WriteLine(response.Status.StatusCode);
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

#### Response

```json
{
    "status": 200
}
```

## Channels

Channels work like users. They have no top-level `Name` property, all application data lives in `Payload`, and they support the same operations. Refer to [channels](https://www.pubnub.com/docs/general/data-sync/users-channels-memberships.md#channels) for the concept.

### Create channel

Creates a channel. Supply an `Id` to control the identifier, or omit it to let the server generate one.

#### Method(s)

```csharp
pubnub.DataSync.CreateChannel(CreateChannelParameters parameters)
```

| Parameter | Description |
| --- | --- |
| `Id`Type: string | Channel identifier. Omit to let the server generate a UUID. Max 255 characters. |
| `EntityClass`Type: string | Name of the channel class this instance belongs to. Defaults to `"Channel"` on the server if omitted. |
| `EntityClassVersion` *Type: int | Version of the channel class schema. |
| `EntityClassLevel`Type: string | Class hierarchy level, `"Global"` or `"SubKey"`, used to disambiguate classes with the same name defined at different levels. |
| `Status`Type: string | Free-form lifecycle status. Max 100 characters. |
| `Payload`Type: Dictionary`<string, object>` | Free-form JSON object holding your application data. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```csharp
try
{
    PNResult<PNDataSyncChannelResult> response = await pubnub.DataSync.CreateChannel(new CreateChannelParameters
    {
        Id = "channel-summer-sale",
        EntityClassVersion = 1,
        Payload = new Dictionary<string, object>
        {
            { "name", "Summer Sale" },
            { "type", "promotion" },
        },
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Id);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "channel-summer-sale",
        "entityClass": "Channel",
        "entityClassVersion": 1,
        "entityClassLevel": "Global",
        "payload": {
            "name": "Summer Sale",
            "type": "promotion"
        },
        "createdAt": "2026-07-13T09:05:00.000Z",
        "updatedAt": "2026-07-13T09:05:00.000Z",
        "eTag": "GhIjKlMnOpQrSt"
    }
}
```

### Get channel

Returns a single channel by `Id`.

#### Method(s)

```csharp
pubnub.DataSync.GetChannel(GetChannelParameters parameters)
```

| Parameter | Description |
| --- | --- |
| `Id` *Type: string | Channel identifier. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```csharp
try
{
    PNResult<PNDataSyncChannelResult> response = await pubnub.DataSync.GetChannel(new GetChannelParameters
    {
        Id = "channel-summer-sale",
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Id);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "channel-summer-sale",
        "entityClass": "Channel",
        "entityClassVersion": 1,
        "entityClassLevel": "Global",
        "payload": {
            "name": "Summer Sale",
            "type": "promotion"
        },
        "createdAt": "2026-07-13T09:05:00.000Z",
        "updatedAt": "2026-07-13T09:05:00.000Z",
        "eTag": "GhIjKlMnOpQrSt"
    }
}
```

### Get all channels

Returns a paginated list of channels. All parameters are optional, so you can call `GetChannels` with an empty parameters object. For pagination, filtering, and sorting, refer to [sorting and pagination](https://www.pubnub.com/docs/general/data-sync/data-operations.md#sorting-and-pagination).

#### Method(s)

```csharp
pubnub.DataSync.GetChannels(GetChannelsParameters parameters)
```

| Parameter | Description |
| --- | --- |
| `EntityClass`Type: string | Channel class to list. Omit to list the Global `Channel` class and all of its subclasses. |
| `Cursor`Type: string | Opaque pagination cursor. Omit for the first page. |
| `Limit`Type: int | Maximum number of channels per page. Default `20`. Max `100`. |
| `FilterFast`Type: string | Strongly consistent filter expression, reflects the latest writes. Limited number of conditions. Cannot be combined with `Filter`. |
| `Filter`Type: string | Eventually consistent filter expression, results can briefly lag writes. Full expression language. Cannot be combined with `FilterFast`. |
| `Sort`Type: string | Comma-separated fields, each optionally suffixed with `:asc` or `:desc` (ascending is the default when a field has no suffix). |
| `EntityClassVersion`Type: int | Channel class version to list. Omit to list channels across every version of the class. |
| `EntityClassLevel`Type: string | Class hierarchy level, `"Global"` or `"SubKey"`, used to disambiguate classes with the same name at different levels. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```csharp
try
{
    PNResult<PNDataSyncChannelsListResult> response = await pubnub.DataSync.GetChannels(new GetChannelsParameters
    {
        Limit = 20,
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Data.Count);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

#### Response

```json
{
    "status": 200,
    "data": [
        {
            "id": "channel-summer-sale",
            "entityClass": "Channel",
            "entityClassVersion": 1,
            "entityClassLevel": "Global",
            "payload": {
                "name": "Summer Sale",
                "type": "promotion"
            },
            "createdAt": "2026-07-13T09:05:00.000Z",
            "updatedAt": "2026-07-13T09:05:00.000Z",
            "eTag": "GhIjKlMnOpQrSt"
        }
    ],
    "meta": {
        "next_cursor": null,
        "has_next": false,
        "limit": 20
    }
}
```

#### Other examples

##### Filter with FilterFast

`FilterFast` reflects the latest writes and runs over properties declared with filtering mode `simple` or `full`. Reference a declared property by its `name`, not its `path`.

```csharp
try
{
    PNResult<PNDataSyncChannelsListResult> response = await pubnub.DataSync.GetChannels(new GetChannelsParameters
    {
        FilterFast = "type == \"promotion\"",
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Data.Count);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

##### Filter with Filter

`Filter` runs over properties declared with filtering mode `full` and can briefly lag recent writes. It shares the same expression language as `FilterFast`, so only one of the two can be sent per call.

```csharp
try
{
    PNResult<PNDataSyncChannelsListResult> response = await pubnub.DataSync.GetChannels(new GetChannelsParameters
    {
        Filter = "name LIKE \"*Sale*\"",
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Data.Count);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

##### Page through results with Cursor

Pass no `Cursor` on the first call. Take `Meta.NextCursor` from the response and pass it back as `Cursor` on the next call. Stop when `Meta.HasNext` is `false`.

```csharp
try
{
    string cursor = null;
    bool hasNext = true;
    int page = 0;

    while (hasNext)
    {
        PNResult<PNDataSyncChannelsListResult> response = await pubnub.DataSync.GetChannels(new GetChannelsParameters
        {
            FilterFast = "type == \"promotion\"",
            Limit = 20,
            Cursor = cursor,
        });

        if (!response.Status.Error)
        {
            Console.WriteLine($"Page {++page}: {response.Result.Data.Count}");
            cursor = response.Result.Meta.NextCursor;
            hasNext = response.Result.Meta.HasNext;
        }
        else
        {
            Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
            hasNext = false;
        }
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

### Set channel

Replaces a channel in full (PUT). Refer to [optimistic concurrency with ETags](https://www.pubnub.com/docs/general/data-sync/data-operations.md#optimistic-concurrency-with-etags) for `IfMatch`.

#### Method(s)

```csharp
pubnub.DataSync.SetChannel(SetChannelParameters parameters)
```

| Parameter | Description |
| --- | --- |
| `Id` *Type: string | Channel identifier. |
| `EntityClassVersion` *Type: int | Version of the channel class schema. |
| `Status`Type: string | Free-form lifecycle status. Max 100 characters. |
| `Payload`Type: Dictionary`<string, object>` | Free-form JSON object holding your application data. |
| `IfMatch`Type: string | The `ETag` from a prior read. The update succeeds only if it still matches, otherwise the server returns `412`. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```csharp
try
{
    PNResult<PNDataSyncChannelResult> response = await pubnub.DataSync.SetChannel(new SetChannelParameters
    {
        Id = "channel-summer-sale",
        EntityClassVersion = 1,
        Payload = new Dictionary<string, object>
        {
            { "name", "Summer Sale 2026" },
            { "type", "promotion" },
        },
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Id);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "channel-summer-sale",
        "entityClass": "Channel",
        "entityClassVersion": 1,
        "entityClassLevel": "Global",
        "payload": {
            "name": "Summer Sale 2026",
            "type": "promotion"
        },
        "createdAt": "2026-07-13T09:05:00.000Z",
        "updatedAt": "2026-07-13T11:00:00.000Z",
        "eTag": "IjKlMnOpQrStUv"
    }
}
```

### Update channel

Applies a partial update to a channel with raw JSON Patch operations. Refer to [partial update](https://www.pubnub.com/docs/general/data-sync/data-operations.md#partial-update) for the operation model.

#### Method(s)

```csharp
pubnub.DataSync.UpdateChannel(UpdateChannelParameters parameters)
```

| Parameter | Description |
| --- | --- |
| `Id` *Type: string | Channel identifier. |
| `Operations` *Type: List`<JsonPatchOperation>` | One or more JSON Patch operations. Must contain at least one item. |
| `IfMatch`Type: string | The `ETag` from a prior read. The patch succeeds only if it still matches, otherwise the server returns `412`. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```csharp
try
{
    PNResult<PNDataSyncChannelResult> response = await pubnub.DataSync.UpdateChannel(new UpdateChannelParameters
    {
        Id = "channel-summer-sale",
        Operations = new List<JsonPatchOperation>
        {
            new JsonPatchOperation { Op = JsonPatchOperationType.Replace, Path = "/payload/name", Value = "Summer Sale 2026" },
        },
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Id);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "channel-summer-sale",
        "entityClass": "Channel",
        "entityClassVersion": 1,
        "entityClassLevel": "Global",
        "payload": {
            "name": "Summer Sale 2026",
            "type": "promotion"
        },
        "createdAt": "2026-07-13T09:05:00.000Z",
        "updatedAt": "2026-07-13T11:05:00.000Z",
        "eTag": "KlMnOpQrStUvWx"
    }
}
```

### Remove channel

Deletes a channel by `Id`.

#### Method(s)

```csharp
pubnub.DataSync.DeleteChannel(DeleteChannelParameters parameters)
```

| Parameter | Description |
| --- | --- |
| `Id` *Type: string | Channel identifier. |
| `IfMatch`Type: string | The `ETag` from a prior read. The delete succeeds only if it still matches, otherwise the server returns `412`. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```csharp
try
{
    PNResult<PNDataSyncDeleteChannelResult> response = await pubnub.DataSync.DeleteChannel(new DeleteChannelParameters
    {
        Id = "channel-summer-sale",
    });

    Console.WriteLine(response.Status.StatusCode);
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

#### Response

```json
{
    "status": 200
}
```

## Memberships

A membership links a user to a channel and carries its own `Payload`. In the running example, Alice is a member of `channel-summer-sale` with the payload `{ "role": "viewer" }`. Refer to [memberships](https://www.pubnub.com/docs/general/data-sync/users-channels-memberships.md#memberships) for the concept.

### Create membership

Creates a membership linking a user to a channel.

#### Method(s)

```csharp
pubnub.DataSync.CreateMembership(CreateMembershipParameters parameters)
```

| Parameter | Description |
| --- | --- |
| `Id`Type: string | Membership identifier. Omit to let the server generate a UUID. Max 255 characters. |
| `ChannelId` *Type: string | Identifier of the channel in the membership. |
| `UserId` *Type: string | Identifier of the user in the membership. |
| `RelationshipClassVersion` *Type: int | Version of the membership class schema. |
| `Status`Type: string | Free-form lifecycle status. Max 100 characters. |
| `Payload`Type: Dictionary`<string, object>` | Free-form JSON object holding your application data. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```csharp
try
{
    PNResult<PNDataSyncMembershipResult> response = await pubnub.DataSync.CreateMembership(new CreateMembershipParameters
    {
        ChannelId = "channel-summer-sale",
        UserId = "user-alice",
        RelationshipClassVersion = 1,
        Payload = new Dictionary<string, object>
        {
            { "role", "viewer" },
        },
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Id);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

#### Response

The response already exposes friendly `ChannelId` and `UserId` properties, you don't need to translate from a relationship shape. Those property names are specific to CRUD responses (`PNDataSyncMembershipResult`). A real-time membership event carries the same two ids as `RelationshipData.EntityAId` and `RelationshipData.EntityBId` instead, refer to [Add DataSync listener](https://www.pubnub.com/docs/sdks/c-sharp/api-reference/publish-and-subscribe.md#add-datasync-listener) for details.

```json
{
    "status": 200,
    "data": {
        "id": "membership-alice-summer-sale",
        "channelId": "channel-summer-sale",
        "userId": "user-alice",
        "relationshipClass": "Membership",
        "relationshipClassVersion": 1,
        "payload": {
            "role": "viewer"
        },
        "createdAt": "2026-07-13T09:10:00.000Z",
        "updatedAt": "2026-07-13T09:10:00.000Z",
        "eTag": "MnOpQrStUvWxYz"
    }
}
```

### Get membership

Returns a single membership by `Id`.

#### Method(s)

```csharp
pubnub.DataSync.GetMembership(GetMembershipParameters parameters)
```

| Parameter | Description |
| --- | --- |
| `Id` *Type: string | Membership identifier. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```csharp
try
{
    PNResult<PNDataSyncMembershipResult> response = await pubnub.DataSync.GetMembership(new GetMembershipParameters
    {
        Id = "membership-alice-summer-sale",
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Id);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "membership-alice-summer-sale",
        "channelId": "channel-summer-sale",
        "userId": "user-alice",
        "relationshipClass": "Membership",
        "relationshipClassVersion": 1,
        "payload": {
            "role": "viewer"
        },
        "createdAt": "2026-07-13T09:10:00.000Z",
        "updatedAt": "2026-07-13T09:10:00.000Z",
        "eTag": "MnOpQrStUvWxYz"
    }
}
```

### Get all memberships

Returns a paginated list of memberships. All parameters are optional, `UserId` and `ChannelId` can be set independently, together, or omitted entirely, the SDK doesn't require at least one of them. Filter by `UserId` to list a user's memberships or by `ChannelId` to list a channel's members. For pagination, filtering, and sorting, refer to [sorting and pagination](https://www.pubnub.com/docs/general/data-sync/data-operations.md#sorting-and-pagination).

#### Method(s)

```csharp
pubnub.DataSync.GetMemberships(GetMembershipsParameters parameters)
```

| Parameter | Description |
| --- | --- |
| `Cursor`Type: string | Opaque pagination cursor. Omit for the first page. |
| `Limit`Type: int | Maximum number of memberships per page. Default `20`. Max `100`. |
| `FilterFast`Type: string | Strongly consistent filter expression, reflects the latest writes. Limited number of conditions. Cannot be combined with `Filter`. |
| `Filter`Type: string | Eventually consistent filter expression, results can briefly lag writes. Full expression language. Cannot be combined with `FilterFast`. |
| `Sort`Type: string | Comma-separated fields, each optionally suffixed with `:asc` or `:desc` (ascending is the default when a field has no suffix). |
| `UserId`Type: string | List only memberships for this user. |
| `ChannelId`Type: string | List only memberships for this channel. |
| `RelationshipClassVersion`Type: int | Membership class version to list. Omit to list memberships across every version of the class. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```csharp
try
{
    PNResult<PNDataSyncMembershipsListResult> response = await pubnub.DataSync.GetMemberships(new GetMembershipsParameters
    {
        UserId = "user-alice",
        Limit = 20,
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Data.Count);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

#### Response

```json
{
    "status": 200,
    "data": [
        {
            "id": "membership-alice-summer-sale",
            "channelId": "channel-summer-sale",
            "userId": "user-alice",
            "relationshipClass": "Membership",
            "relationshipClassVersion": 1,
            "payload": {
                "role": "viewer"
            },
            "createdAt": "2026-07-13T09:10:00.000Z",
            "updatedAt": "2026-07-13T09:10:00.000Z",
            "eTag": "MnOpQrStUvWxYz"
        }
    ],
    "meta": {
        "next_cursor": null,
        "has_next": false,
        "limit": 20
    }
}
```

#### Other examples

##### List a channel's members with ChannelId

Pass `ChannelId` instead of `UserId` to list the members of a channel rather than a user's memberships.

```csharp
try
{
    PNResult<PNDataSyncMembershipsListResult> response = await pubnub.DataSync.GetMemberships(new GetMembershipsParameters
    {
        ChannelId = "channel-summer-sale",
        Limit = 20,
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Data.Count);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

##### Filter with FilterFast

`FilterFast` reflects the latest writes and runs over properties declared with filtering mode `simple` or `full`. Reference a declared property by its `name`, not its `path`.

```csharp
try
{
    PNResult<PNDataSyncMembershipsListResult> response = await pubnub.DataSync.GetMemberships(new GetMembershipsParameters
    {
        UserId = "user-alice",
        FilterFast = "role == \"viewer\"",
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Data.Count);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

##### Filter with Filter

`Filter` runs over properties declared with filtering mode `full` and can briefly lag recent writes. It shares the same expression language as `FilterFast`, so only one of the two can be sent per call.

```csharp
try
{
    PNResult<PNDataSyncMembershipsListResult> response = await pubnub.DataSync.GetMemberships(new GetMembershipsParameters
    {
        ChannelId = "channel-summer-sale",
        Filter = "role LIKE \"*mod*\"",
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Data.Count);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

##### Page through results with Cursor

Pass no `Cursor` on the first call. Take `Meta.NextCursor` from the response and pass it back as `Cursor` on the next call. Stop when `Meta.HasNext` is `false`.

```csharp
try
{
    string cursor = null;
    bool hasNext = true;
    int page = 0;

    while (hasNext)
    {
        PNResult<PNDataSyncMembershipsListResult> response = await pubnub.DataSync.GetMemberships(new GetMembershipsParameters
        {
            UserId = "user-alice",
            Limit = 20,
            Cursor = cursor,
        });

        if (!response.Status.Error)
        {
            Console.WriteLine($"Page {++page}: {response.Result.Data.Count}");
            cursor = response.Result.Meta.NextCursor;
            hasNext = response.Result.Meta.HasNext;
        }
        else
        {
            Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
            hasNext = false;
        }
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

### Set membership

Replaces a membership in full (PUT). The linked user and channel are immutable, so `UpdateMembershipParameters` doesn't expose `ChannelId`/`UserId` at all, there's nothing to resend. Refer to [optimistic concurrency with ETags](https://www.pubnub.com/docs/general/data-sync/data-operations.md#optimistic-concurrency-with-etags) for `IfMatch`.

:::note Parameter class naming
Unlike Users, Channels, Entities, and Relationships, the Membership parameter class names don't follow the `Set<X>Parameters` / `Update<X>Parameters` pattern. The full-replace method `SetMembership` takes `UpdateMembershipParameters`, and the partial-patch method `UpdateMembership` (below) takes `PatchMembershipParameters`. This is how the SDK is implemented, call the methods and parameter classes exactly as shown.
:::

#### Method(s)

```csharp
pubnub.DataSync.SetMembership(UpdateMembershipParameters parameters)
```

| Parameter | Description |
| --- | --- |
| `Id` *Type: string | Membership identifier. |
| `RelationshipClassVersion` *Type: int | Version of the membership class schema. |
| `Status`Type: string | Free-form lifecycle status. Max 100 characters. |
| `Payload`Type: Dictionary`<string, object>` | Free-form JSON object holding your application data. |
| `IfMatch`Type: string | The `ETag` from a prior read. The update succeeds only if it still matches, otherwise the server returns `412`. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```csharp
try
{
    PNResult<PNDataSyncMembershipResult> response = await pubnub.DataSync.SetMembership(new UpdateMembershipParameters
    {
        Id = "membership-alice-summer-sale",
        RelationshipClassVersion = 1,
        Payload = new Dictionary<string, object>
        {
            { "role", "moderator" },
        },
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Id);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "membership-alice-summer-sale",
        "channelId": "channel-summer-sale",
        "userId": "user-alice",
        "relationshipClass": "Membership",
        "relationshipClassVersion": 1,
        "payload": {
            "role": "moderator"
        },
        "createdAt": "2026-07-13T09:10:00.000Z",
        "updatedAt": "2026-07-13T12:00:00.000Z",
        "eTag": "OpQrStUvWxYzAb"
    }
}
```

### Update membership

Applies a partial update to a membership with raw JSON Patch operations. Refer to [partial update](https://www.pubnub.com/docs/general/data-sync/data-operations.md#partial-update) for the operation model. See the [note above](#set-membership) about the parameter class naming for Memberships.

#### Method(s)

```csharp
pubnub.DataSync.UpdateMembership(PatchMembershipParameters parameters)
```

| Parameter | Description |
| --- | --- |
| `Id` *Type: string | Membership identifier. |
| `Operations` *Type: List`<JsonPatchOperation>` | One or more JSON Patch operations. Must contain at least one item. |
| `IfMatch`Type: string | The `ETag` from a prior read. The patch succeeds only if it still matches, otherwise the server returns `412`. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```csharp
try
{
    PNResult<PNDataSyncMembershipResult> response = await pubnub.DataSync.UpdateMembership(new PatchMembershipParameters
    {
        Id = "membership-alice-summer-sale",
        Operations = new List<JsonPatchOperation>
        {
            new JsonPatchOperation { Op = JsonPatchOperationType.Replace, Path = "/payload/role", Value = "moderator" },
        },
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Id);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "membership-alice-summer-sale",
        "channelId": "channel-summer-sale",
        "userId": "user-alice",
        "relationshipClass": "Membership",
        "relationshipClassVersion": 1,
        "payload": {
            "role": "moderator"
        },
        "createdAt": "2026-07-13T09:10:00.000Z",
        "updatedAt": "2026-07-13T12:05:00.000Z",
        "eTag": "PqRsTuVwXyZaBc"
    }
}
```

### Remove membership

Deletes a membership by `Id`.

#### Method(s)

```csharp
pubnub.DataSync.DeleteMembership(DeleteMembershipParameters parameters)
```

| Parameter | Description |
| --- | --- |
| `Id` *Type: string | Membership identifier. |
| `IfMatch`Type: string | The `ETag` from a prior read. The delete succeeds only if it still matches, otherwise the server returns `412`. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```csharp
try
{
    PNResult<PNDataSyncDeleteMembershipResult> response = await pubnub.DataSync.DeleteMembership(new DeleteMembershipParameters
    {
        Id = "membership-alice-summer-sale",
    });

    Console.WriteLine(response.Status.StatusCode);
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

#### Response

```json
{
    "status": 200
}
```

## Entities

Entities are instances of the custom classes you define in the Admin Portal. In the running example, `product` is a class and `product-sneaker-42` is an instance. Refer to [entities](https://www.pubnub.com/docs/general/data-sync/data-model.md#entities) for the concept, and [managing classes](https://www.pubnub.com/docs/general/data-sync/schemas-and-validation.md#managing-classes) for how classes are defined.

### Create entity

Creates an entity of a given class and version.

#### Method(s)

```csharp
pubnub.DataSync.CreateEntity(CreateEntityParameters parameters)
```

| Parameter | Description |
| --- | --- |
| `Id`Type: string | Entity identifier. Omit to let the server generate a UUID. Max 255 characters. |
| `EntityClass` *Type: string | Name of the entity class this instance belongs to. |
| `EntityClassVersion` *Type: int | Version of the entity class schema. |
| `EntityClassLevel`Type: string | Class hierarchy level, `"Global"` or `"SubKey"`, used to disambiguate classes with the same name defined at different levels. |
| `Status`Type: string | Free-form lifecycle status. Max 100 characters. |
| `Payload`Type: Dictionary`<string, object>` | Free-form JSON object holding your application data. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```csharp
try
{
    PNResult<PNDataSyncEntityResult> response = await pubnub.DataSync.CreateEntity(new CreateEntityParameters
    {
        Id = "product-sneaker-42",
        EntityClass = "product",
        EntityClassVersion = 1,
        Payload = new Dictionary<string, object>
        {
            { "name", "Retro Sneaker" },
            { "price", 89.99 },
            { "stock", 12 },
        },
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Id);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "product-sneaker-42",
        "entityClass": "product",
        "entityClassVersion": 1,
        "entityClassLevel": "SubKey",
        "payload": {
            "name": "Retro Sneaker",
            "price": 89.99,
            "stock": 12
        },
        "createdAt": "2026-07-13T09:15:00.000Z",
        "updatedAt": "2026-07-13T09:15:00.000Z",
        "eTag": "QrStUvWxYzAbCd"
    }
}
```

### Get entity

Returns a single entity by `Id`.

#### Method(s)

```csharp
pubnub.DataSync.GetEntity(GetEntityParameters parameters)
```

| Parameter | Description |
| --- | --- |
| `Id` *Type: string | Entity identifier. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```csharp
try
{
    PNResult<PNDataSyncEntityResult> response = await pubnub.DataSync.GetEntity(new GetEntityParameters
    {
        Id = "product-sneaker-42",
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Id);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "product-sneaker-42",
        "entityClass": "product",
        "entityClassVersion": 1,
        "entityClassLevel": "SubKey",
        "payload": {
            "name": "Retro Sneaker",
            "price": 89.99,
            "stock": 12
        },
        "createdAt": "2026-07-13T09:15:00.000Z",
        "updatedAt": "2026-07-13T09:15:00.000Z",
        "eTag": "QrStUvWxYzAbCd"
    }
}
```

### Get all entities

Returns a paginated list of entities within a class. The `EntityClass` parameter is required, entities are always listed within the context of their class. For pagination, filtering, and sorting, refer to [sorting and pagination](https://www.pubnub.com/docs/general/data-sync/data-operations.md#sorting-and-pagination).

#### Method(s)

```csharp
pubnub.DataSync.GetEntities(GetEntitiesParameters parameters)
```

| Parameter | Description |
| --- | --- |
| `EntityClass` *Type: string | Name of the entity class to list. |
| `EntityClassVersion`Type: int | Entity class version to list. Omit to list entities across every version of the class. |
| `EntityClassLevel`Type: string | Class hierarchy level, `"Global"` or `"SubKey"`, used to disambiguate classes with the same name at different levels. |
| `Cursor`Type: string | Opaque pagination cursor. Omit for the first page. |
| `Limit`Type: int | Maximum number of entities per page. Default `20`. Max `100`. |
| `FilterFast`Type: string | Strongly consistent filter expression, reflects the latest writes. Limited number of conditions. Cannot be combined with `Filter`. |
| `Filter`Type: string | Eventually consistent filter expression, results can briefly lag writes. Full expression language. Cannot be combined with `FilterFast`. |
| `Sort`Type: string | Comma-separated fields, each optionally suffixed with `:asc` or `:desc` (ascending is the default when a field has no suffix). |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```csharp
try
{
    PNResult<PNDataSyncEntitiesListResult> response = await pubnub.DataSync.GetEntities(new GetEntitiesParameters
    {
        EntityClass = "product",
        Sort = "price:desc",
        Limit = 20,
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Data.Count);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

#### Response

```json
{
    "status": 200,
    "data": [
        {
            "id": "product-sneaker-42",
            "entityClass": "product",
            "entityClassVersion": 1,
            "entityClassLevel": "SubKey",
            "payload": {
                "name": "Retro Sneaker",
                "price": 89.99,
                "stock": 12
            },
            "createdAt": "2026-07-13T09:15:00.000Z",
            "updatedAt": "2026-07-13T09:15:00.000Z",
            "eTag": "QrStUvWxYzAbCd"
        }
    ],
    "meta": {
        "next_cursor": null,
        "has_next": false,
        "limit": 20
    }
}
```

#### Other examples

##### Filter with FilterFast

`FilterFast` reflects the latest writes and runs over properties declared with filtering mode `simple` or `full`. Reference a declared property by its `name` (not its `path`), and combine conditions with `&&` and `||`.

```csharp
try
{
    PNResult<PNDataSyncEntitiesListResult> response = await pubnub.DataSync.GetEntities(new GetEntitiesParameters
    {
        EntityClass = "product",
        FilterFast = "price < 100 && stock > 0",
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Data.Count);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

##### Filter with Filter

`Filter` runs over properties declared with filtering mode `full` and can briefly lag recent writes. It shares the same expression language as `FilterFast`, so only one of the two can be sent per call.

```csharp
try
{
    PNResult<PNDataSyncEntitiesListResult> response = await pubnub.DataSync.GetEntities(new GetEntitiesParameters
    {
        EntityClass = "product",
        Filter = "name LIKE \"*sneaker*\" && !(status == \"discontinued\")",
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Data.Count);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

##### Page through results with Cursor

Pass no `Cursor` on the first call. Take `Meta.NextCursor` from the response and pass it back as `Cursor` on the next call. Stop when `Meta.HasNext` is `false`.

```csharp
try
{
    string cursor = null;
    bool hasNext = true;
    int page = 0;

    while (hasNext)
    {
        PNResult<PNDataSyncEntitiesListResult> response = await pubnub.DataSync.GetEntities(new GetEntitiesParameters
        {
            EntityClass = "product",
            FilterFast = "price < 100",
            Limit = 20,
            Cursor = cursor,
        });

        if (!response.Status.Error)
        {
            Console.WriteLine($"Page {++page}: {response.Result.Data.Count}");
            cursor = response.Result.Meta.NextCursor;
            hasNext = response.Result.Meta.HasNext;
        }
        else
        {
            Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
            hasNext = false;
        }
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

### Set entity

Replaces an entity in full (PUT). `EntityClass` is immutable after creation and cannot be sent. Refer to [optimistic concurrency with ETags](https://www.pubnub.com/docs/general/data-sync/data-operations.md#optimistic-concurrency-with-etags) for `IfMatch`.

#### Method(s)

```csharp
pubnub.DataSync.SetEntity(SetEntityParameters parameters)
```

| Parameter | Description |
| --- | --- |
| `Id` *Type: string | Entity identifier. |
| `EntityClassVersion` *Type: int | Version of the entity class schema. |
| `Status`Type: string | Free-form lifecycle status. Max 100 characters. |
| `Payload`Type: Dictionary`<string, object>` | Free-form JSON object holding your application data. |
| `IfMatch`Type: string | The `ETag` from a prior read. The update succeeds only if it still matches, otherwise the server returns `412`. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```csharp
try
{
    PNResult<PNDataSyncEntityResult> response = await pubnub.DataSync.SetEntity(new SetEntityParameters
    {
        Id = "product-sneaker-42",
        EntityClassVersion = 1,
        Payload = new Dictionary<string, object>
        {
            { "name", "Retro Sneaker" },
            { "price", 79.99 },
            { "stock", 8 },
        },
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Id);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "product-sneaker-42",
        "entityClass": "product",
        "entityClassVersion": 1,
        "entityClassLevel": "SubKey",
        "payload": {
            "name": "Retro Sneaker",
            "price": 79.99,
            "stock": 8
        },
        "createdAt": "2026-07-13T09:15:00.000Z",
        "updatedAt": "2026-07-13T13:00:00.000Z",
        "eTag": "StUvWxYzAbCdEf"
    }
}
```

### Update entity

Applies a partial update to an entity with raw JSON Patch operations. Refer to [partial update](https://www.pubnub.com/docs/general/data-sync/data-operations.md#partial-update) for the operation model.

#### Method(s)

```csharp
pubnub.DataSync.UpdateEntity(UpdateEntityParameters parameters)
```

| Parameter | Description |
| --- | --- |
| `Id` *Type: string | Entity identifier. |
| `Operations` *Type: List`<JsonPatchOperation>` | One or more JSON Patch operations. Must contain at least one item. |
| `IfMatch`Type: string | The `ETag` from a prior read. The patch succeeds only if it still matches, otherwise the server returns `412`. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```csharp
try
{
    PNResult<PNDataSyncEntityResult> response = await pubnub.DataSync.UpdateEntity(new UpdateEntityParameters
    {
        Id = "product-sneaker-42",
        Operations = new List<JsonPatchOperation>
        {
            new JsonPatchOperation { Op = JsonPatchOperationType.Replace, Path = "/payload/price", Value = 79.99 },
        },
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Id);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "product-sneaker-42",
        "entityClass": "product",
        "entityClassVersion": 1,
        "entityClassLevel": "SubKey",
        "payload": {
            "name": "Retro Sneaker",
            "price": 79.99,
            "stock": 12
        },
        "createdAt": "2026-07-13T09:15:00.000Z",
        "updatedAt": "2026-07-13T13:05:00.000Z",
        "eTag": "UvWxYzAbCdEfGh"
    }
}
```

#### Other examples

##### Combine patch operations, and guard the write with IfMatch

A single `UpdateEntity` call can mix `Add`, `Replace`, `Remove`, `Move`, `Copy`, and `Test` in one `Operations` list. All operations in the call apply together or not at all. Add `IfMatch` (the `ETag` from a prior read) to reject the patch with a `412` if the entity changed since you read it, instead of silently overwriting a concurrent change.

```csharp
try
{
    PNResult<PNDataSyncEntityResult> response = await pubnub.DataSync.UpdateEntity(new UpdateEntityParameters
    {
        Id = "product-sneaker-42",
        Operations = new List<JsonPatchOperation>
        {
            new JsonPatchOperation { Op = JsonPatchOperationType.Test, Path = "/payload/stock", Value = 8 },
            new JsonPatchOperation { Op = JsonPatchOperationType.Replace, Path = "/payload/price", Value = 74.99 },
            new JsonPatchOperation { Op = JsonPatchOperationType.Add, Path = "/payload/tags/-", Value = "clearance" },
            new JsonPatchOperation { Op = JsonPatchOperationType.Remove, Path = "/payload/legacy/field" },
            new JsonPatchOperation { Op = JsonPatchOperationType.Move, Path = "/payload/displayName", From = "/payload/legacyName" },
            new JsonPatchOperation { Op = JsonPatchOperationType.Copy, Path = "/payload/previousName", From = "/payload/displayName" },
        },
        IfMatch = "StUvWxYzAbCdEf",
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Id);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

### Remove entity

Deletes an entity by `Id`.

#### Method(s)

```csharp
pubnub.DataSync.DeleteEntity(DeleteEntityParameters parameters)
```

| Parameter | Description |
| --- | --- |
| `Id` *Type: string | Entity identifier. |
| `IfMatch`Type: string | The `ETag` from a prior read. The delete succeeds only if it still matches, otherwise the server returns `412`. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```csharp
try
{
    PNResult<PNDataSyncDeleteEntityResult> response = await pubnub.DataSync.DeleteEntity(new DeleteEntityParameters
    {
        Id = "product-sneaker-42",
    });

    Console.WriteLine(response.Status.StatusCode);
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

#### Response

```json
{
    "status": 200
}
```

## Relationships

A relationship links two entities and carries its own `Payload`. In the running example, the `ProductOwner` relationship links `seller-bob` to `product-sneaker-42`. Refer to [relationships](https://www.pubnub.com/docs/general/data-sync/data-model.md#relationships) for the concept.

### Create relationship

Creates a relationship between two entities.

#### Method(s)

```csharp
pubnub.DataSync.CreateRelationship(CreateRelationshipParameters parameters)
```

| Parameter | Description |
| --- | --- |
| `Id`Type: string | Relationship identifier. Omit to let the server generate a UUID. Max 255 characters. |
| `EntityAId` *Type: string | Identifier of the first linked entity. Immutable after creation. |
| `EntityBId` *Type: string | Identifier of the second linked entity. Immutable after creation. |
| `RelationshipClass` *Type: string | Name of the relationship class this instance belongs to. |
| `RelationshipClassVersion` *Type: int | Version of the relationship class schema. |
| `Status`Type: string | Free-form lifecycle status. Max 100 characters. |
| `Payload`Type: Dictionary`<string, object>` | Free-form JSON object holding your application data. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```csharp
try
{
    PNResult<PNDataSyncRelationshipResult> response = await pubnub.DataSync.CreateRelationship(new CreateRelationshipParameters
    {
        EntityAId = "seller-bob",
        EntityBId = "product-sneaker-42",
        RelationshipClass = "ProductOwner",
        RelationshipClassVersion = 1,
        Payload = new Dictionary<string, object>
        {
            { "since", "2026-07-13" },
        },
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Id);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "rel-bob-owns-sneaker-42",
        "entityAId": "seller-bob",
        "entityBId": "product-sneaker-42",
        "relationshipClass": "ProductOwner",
        "relationshipClassVersion": 1,
        "payload": {
            "since": "2026-07-13"
        },
        "createdAt": "2026-07-13T09:20:00.000Z",
        "updatedAt": "2026-07-13T09:20:00.000Z",
        "eTag": "WxYzAbCdEfGhIj"
    }
}
```

### Get relationship

Returns a single relationship by `Id`.

#### Method(s)

```csharp
pubnub.DataSync.GetRelationship(GetRelationshipParameters parameters)
```

| Parameter | Description |
| --- | --- |
| `Id` *Type: string | Relationship identifier. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```csharp
try
{
    PNResult<PNDataSyncRelationshipResult> response = await pubnub.DataSync.GetRelationship(new GetRelationshipParameters
    {
        Id = "rel-bob-owns-sneaker-42",
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Id);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "rel-bob-owns-sneaker-42",
        "entityAId": "seller-bob",
        "entityBId": "product-sneaker-42",
        "relationshipClass": "ProductOwner",
        "relationshipClassVersion": 1,
        "payload": {
            "since": "2026-07-13"
        },
        "createdAt": "2026-07-13T09:20:00.000Z",
        "updatedAt": "2026-07-13T09:20:00.000Z",
        "eTag": "WxYzAbCdEfGhIj"
    }
}
```

### Get all relationships

Returns a paginated list of relationships within a class. The `RelationshipClass` parameter is required. Filter by `EntityAId` or `EntityBId` to list a specific entity's links. For pagination, filtering, and sorting, refer to [sorting and pagination](https://www.pubnub.com/docs/general/data-sync/data-operations.md#sorting-and-pagination).

#### Method(s)

```csharp
pubnub.DataSync.GetRelationships(GetRelationshipsParameters parameters)
```

| Parameter | Description |
| --- | --- |
| `RelationshipClass` *Type: string | Name of the relationship class to list. |
| `RelationshipClassVersion`Type: int | Relationship class version to list. Omit to list relationships across every version of the class. |
| `EntityAId`Type: string | List only relationships whose first entity is this id. |
| `EntityBId`Type: string | List only relationships whose second entity is this id. |
| `Cursor`Type: string | Opaque pagination cursor. Omit for the first page. |
| `Limit`Type: int | Maximum number of relationships per page. Default `20`. Max `100`. |
| `FilterFast`Type: string | Strongly consistent filter expression, reflects the latest writes. Limited number of conditions. Cannot be combined with `Filter`. |
| `Filter`Type: string | Eventually consistent filter expression, results can briefly lag writes. Full expression language. Cannot be combined with `FilterFast`. |
| `Sort`Type: string | Comma-separated fields, each optionally suffixed with `:asc` or `:desc` (ascending is the default when a field has no suffix). |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```csharp
try
{
    PNResult<PNDataSyncRelationshipsListResult> response = await pubnub.DataSync.GetRelationships(new GetRelationshipsParameters
    {
        RelationshipClass = "ProductOwner",
        EntityAId = "seller-bob",
        Limit = 20,
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Data.Count);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

#### Response

```json
{
    "status": 200,
    "data": [
        {
            "id": "rel-bob-owns-sneaker-42",
            "entityAId": "seller-bob",
            "entityBId": "product-sneaker-42",
            "relationshipClass": "ProductOwner",
            "relationshipClassVersion": 1,
            "payload": {
                "since": "2026-07-13"
            },
            "createdAt": "2026-07-13T09:20:00.000Z",
            "updatedAt": "2026-07-13T09:20:00.000Z",
            "eTag": "WxYzAbCdEfGhIj"
        }
    ],
    "meta": {
        "next_cursor": null,
        "has_next": false,
        "limit": 20
    }
}
```

#### Other examples

##### List an entity's incoming links with EntityBId

Pass `EntityBId` instead of `EntityAId` to list relationships where the entity is on the second side of the link.

```csharp
try
{
    PNResult<PNDataSyncRelationshipsListResult> response = await pubnub.DataSync.GetRelationships(new GetRelationshipsParameters
    {
        RelationshipClass = "ProductOwner",
        EntityBId = "product-sneaker-42",
        Limit = 20,
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Data.Count);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

##### Filter with FilterFast

`FilterFast` reflects the latest writes and runs over properties declared with filtering mode `simple` or `full`. Reference a declared property by its `name`, not its `path`.

```csharp
try
{
    PNResult<PNDataSyncRelationshipsListResult> response = await pubnub.DataSync.GetRelationships(new GetRelationshipsParameters
    {
        RelationshipClass = "ProductOwner",
        FilterFast = "tier == \"gold\"",
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Data.Count);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

##### Filter with Filter

`Filter` runs over properties declared with filtering mode `full` and can briefly lag recent writes. It shares the same expression language as `FilterFast`, so only one of the two can be sent per call.

```csharp
try
{
    PNResult<PNDataSyncRelationshipsListResult> response = await pubnub.DataSync.GetRelationships(new GetRelationshipsParameters
    {
        RelationshipClass = "ProductOwner",
        Filter = "!(tier == \"platinum\")",
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Data.Count);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

##### Page through results with Cursor

Pass no `Cursor` on the first call. Take `Meta.NextCursor` from the response and pass it back as `Cursor` on the next call. Stop when `Meta.HasNext` is `false`.

```csharp
try
{
    string cursor = null;
    bool hasNext = true;
    int page = 0;

    while (hasNext)
    {
        PNResult<PNDataSyncRelationshipsListResult> response = await pubnub.DataSync.GetRelationships(new GetRelationshipsParameters
        {
            RelationshipClass = "ProductOwner",
            EntityAId = "seller-bob",
            Limit = 20,
            Cursor = cursor,
        });

        if (!response.Status.Error)
        {
            Console.WriteLine($"Page {++page}: {response.Result.Data.Count}");
            cursor = response.Result.Meta.NextCursor;
            hasNext = response.Result.Meta.HasNext;
        }
        else
        {
            Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
            hasNext = false;
        }
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

### Set relationship

Replaces a relationship in full (PUT). The linked entities are immutable, so `SetRelationshipParameters` doesn't expose `EntityAId`/`EntityBId` at all, there's nothing to resend. Refer to [optimistic concurrency with ETags](https://www.pubnub.com/docs/general/data-sync/data-operations.md#optimistic-concurrency-with-etags) for `IfMatch`.

#### Method(s)

```csharp
pubnub.DataSync.SetRelationship(SetRelationshipParameters parameters)
```

| Parameter | Description |
| --- | --- |
| `Id` *Type: string | Relationship identifier. |
| `RelationshipClassVersion` *Type: int | Version of the relationship class schema. |
| `Status`Type: string | Free-form lifecycle status. Max 100 characters. |
| `Payload`Type: Dictionary`<string, object>` | Free-form JSON object holding your application data. |
| `IfMatch`Type: string | The `ETag` from a prior read. The update succeeds only if it still matches, otherwise the server returns `412`. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```csharp
try
{
    PNResult<PNDataSyncRelationshipResult> response = await pubnub.DataSync.SetRelationship(new SetRelationshipParameters
    {
        Id = "rel-bob-owns-sneaker-42",
        RelationshipClassVersion = 1,
        Payload = new Dictionary<string, object>
        {
            { "since", "2026-07-13" },
            { "tier", "gold" },
        },
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Id);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "rel-bob-owns-sneaker-42",
        "entityAId": "seller-bob",
        "entityBId": "product-sneaker-42",
        "relationshipClass": "ProductOwner",
        "relationshipClassVersion": 1,
        "payload": {
            "since": "2026-07-13",
            "tier": "gold"
        },
        "createdAt": "2026-07-13T09:20:00.000Z",
        "updatedAt": "2026-07-13T14:00:00.000Z",
        "eTag": "YzAbCdEfGhIjKl"
    }
}
```

### Update relationship

Applies a partial update to a relationship with raw JSON Patch operations. Refer to [partial update](https://www.pubnub.com/docs/general/data-sync/data-operations.md#partial-update) for the operation model.

#### Method(s)

```csharp
pubnub.DataSync.UpdateRelationship(UpdateRelationshipParameters parameters)
```

| Parameter | Description |
| --- | --- |
| `Id` *Type: string | Relationship identifier. |
| `Operations` *Type: List`<JsonPatchOperation>` | One or more JSON Patch operations. Must contain at least one item. |
| `IfMatch`Type: string | The `ETag` from a prior read. The patch succeeds only if it still matches, otherwise the server returns `412`. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```csharp
try
{
    PNResult<PNDataSyncRelationshipResult> response = await pubnub.DataSync.UpdateRelationship(new UpdateRelationshipParameters
    {
        Id = "rel-bob-owns-sneaker-42",
        Operations = new List<JsonPatchOperation>
        {
            new JsonPatchOperation { Op = JsonPatchOperationType.Replace, Path = "/payload/tier", Value = "platinum" },
        },
    });

    if (!response.Status.Error)
    {
        Console.WriteLine(response.Result.Id);
    }
    else
    {
        Console.WriteLine($"Request can't be executed due to error: {response.Status.ErrorData.Information}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

#### Response

```json
{
    "status": 200,
    "data": {
        "id": "rel-bob-owns-sneaker-42",
        "entityAId": "seller-bob",
        "entityBId": "product-sneaker-42",
        "relationshipClass": "ProductOwner",
        "relationshipClassVersion": 1,
        "payload": {
            "since": "2026-07-13",
            "tier": "platinum"
        },
        "createdAt": "2026-07-13T09:20:00.000Z",
        "updatedAt": "2026-07-13T14:05:00.000Z",
        "eTag": "AbCdEfGhIjKlMn"
    }
}
```

### Remove relationship

Deletes a relationship by `Id`.

#### Method(s)

```csharp
pubnub.DataSync.DeleteRelationship(DeleteRelationshipParameters parameters)
```

| Parameter | Description |
| --- | --- |
| `Id` *Type: string | Relationship identifier. |
| `IfMatch`Type: string | The `ETag` from a prior read. The delete succeeds only if it still matches, otherwise the server returns `412`. |

#### Sample code

:::tip Reference code
This example is a self-contained code snippet ready to be run. It includes necessary imports and executes methods with console logging. Use it as a reference when working with other examples in this document.
:::

```csharp
try
{
    PNResult<PNDataSyncDeleteRelationshipResult> response = await pubnub.DataSync.DeleteRelationship(new DeleteRelationshipParameters
    {
        Id = "rel-bob-owns-sneaker-42",
    });

    Console.WriteLine(response.Status.StatusCode);
}
catch (Exception ex)
{
    Console.WriteLine($"Request can't be executed due to error: {ex.Message}");
}
```

#### Response

```json
{
    "status": 200
}
```

## Real-time updates

DataSync objects can publish `create`, `update`, and `delete` events that you receive in real time by subclassing `SubscribeCallback` (or using `SubscribeCallbackExt`) and overriding `DataSyncEvent`. To receive them, subscribe to the object's data channel with a `Channel` SDK entity and register your listener on the resulting `Subscription`, a `SubscriptionSet`, or the PubNub client, as described in [Add DataSync listener](https://www.pubnub.com/docs/sdks/c-sharp/api-reference/publish-and-subscribe.md#add-datasync-listener).

```csharp
Channel channel = pubnub.Channel("product-sneaker-42");
Subscription subscription = channel.Subscription();

subscription.AddListener(new SubscribeCallbackExt(
    (Pubnub pn, PNDataSyncEventResult dataSyncEvent) =>
    {
        string changedId = dataSyncEvent.EntityData?.Id
            ?? dataSyncEvent.RelationshipData?.Id
            ?? dataSyncEvent.Id;
        Console.WriteLine($"{dataSyncEvent.Event} {changedId}");
    },
    (Pubnub pn, PNStatus status) => { }));

subscription.Subscribe<object>();
```

Each event names the change in `Event`, identifies the object kind in `Type`, and carries the object state in `EntityData` or `RelationshipData`. For a `delete` event, neither is populated and the id arrives as `Id` on the event itself, alongside `DeletedAt`.

`Type` names the object kind directly, while the state always arrives in one of the two containers:

| Change to | `Type` | State arrives in |
| --- | --- | --- |
| A user | `user` | `EntityData` |
| A channel | `channel` | `EntityData` |
| An entity | `entity` | `EntityData` |
| A membership | `membership` | `RelationshipData` |
| A relationship | `relationship` | `RelationshipData` |

Dispatch on `Type` rather than on which container is populated, because `EntityData` is shared by three kinds and `RelationshipData` by two. `ClassName` tells you the specific class within a kind, for example which subclass of `User` an event belongs to.

### Where each event is delivered

The channels that receive an event depend on the changed object. A relationship or membership never publishes on a channel named after its own id:

| Change to | On `create` | On `update` or `delete` |
| --- | --- | --- |
| A user | The user's `Id` | The user's `Id`, plus the `Id` of every entity, user, or channel connected to it by a relationship or membership |
| A channel | The channel's `Id` | The channel's `Id`, plus the `Id` of every entity, user, or channel connected to it by a relationship or membership |
| An entity | The entity's `Id` | The entity's `Id`, plus the `Id` of every entity, user, or channel connected to it by a relationship or membership |
| A membership | **Both** the `UserId` and the `ChannelId` of the membership | Same as `create` |
| A relationship | **Both** the `EntityAId` and the `EntityBId` of the relationship | Same as `create` |

:::note Update and delete also fan out to connected objects
An update or delete on a user, channel, or entity is also delivered on the `Id` channel of every other object connected to it by a relationship or membership, in either direction, at the time of the change. A client subscribed to a connected object's channel receives the event too, even though that object itself did not change. A create does not fan out this way, because nothing can be connected to a brand new object yet.
The connected channels are entity, user, and channel ids. Relationships and memberships never get an id channel of their own, so they never appear in this fan-out as a channel name.
:::

So to see memberships appear and disappear for Alice, subscribe to `user-alice` rather than the membership id. A client subscribed to both sides of the same link receives the change twice, once per channel. Deduplicate create and update events on the object's `Id` and `UpdatedAt`, and delete events on `Id` and `DeletedAt`.

For a membership event, `RelationshipData.EntityAId` is the channel id and `RelationshipData.EntityBId` is the user id. The service sends these as `channelId` and `userId`, and the SDK maps them onto the A and B sides so that one result type serves both memberships and your own relationships.

### Projection channels

Each [projection](https://www.pubnub.com/docs/sdks/c-sharp/api-reference/access-manager.md#grant-token) declared on the class gets its own event, published to its own channel, and carrying only that projection's view of the payload:

| Projection | Channel | Payload |
| --- | --- | --- |
| `__default__` (the base projection) | The object's own id, for example `product-sneaker-42` | The fields tagged `__default__` |
| Any named projection, for example `admin` | `__<projection>__<id>`, for example `__admin__product-sneaker-42` | The fields tagged with that projection |

Filtering applies to `Payload` and to `Status`: each is included in a given projection's event only if the class declares it under that projection, falling back to `__default__` when the class never declares `Status`. `Id`, `ETag`, `CreatedAt`, `UpdatedAt`, and `ExpiresAt` are never filtered. Every projection's event carries them unchanged.

So one change to an object whose class declares an `admin` projection publishes two events, and a client subscribed only to `product-sneaker-42` never sees the `admin`-only fields:

```csharp
// Base projection: the __default__ view of the payload.
Subscription baseSubscription = pubnub.Channel("product-sneaker-42").Subscription();

// admin projection: the admin view of the payload.
Subscription adminSubscription = pubnub.Channel("__admin__product-sneaker-42").Subscription();
```

If the class declares no properties at all there is nothing to filter, so a single unfiltered event is published to the object's own id channel.

:::warning Projection channels need channel grants
Publishing to `__<projection>__<id>` is an ordinary channel publish, and it isn't gated by the `DataSyncProjections` entries in a token. Use Access Manager `Channels` grants to control who can subscribe to a projection channel. Anyone who can subscribe there receives the restricted fields. Refer to [Grant token](https://www.pubnub.com/docs/sdks/c-sharp/api-reference/access-manager.md#grant-token).
:::

Delete events are never filtered by a projection.

Events are off by default and are enabled per class in the [Admin Portal](https://admin.pubnub.com). Refer to [enabling events](https://www.pubnub.com/docs/general/data-sync/events.md#enable-datasync-events) for how to turn them on, and [receiving events](https://www.pubnub.com/docs/general/data-sync/events.md#receive-datasync-events) for the listener flow.

Last updated at: 2026-09-08T16:52:30.000Z
