Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 38 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,42 @@ Find out more:
>
> | Package | Use it for |
> |---------|------------|
> | `Ably.PubSub.Device` | End-user device applications (desktop, mobile, Unity, MAUI, browser-adjacent clients) — *added in the next PR in this stack* |
> | `Ably.PubSub.Server` | Server-side and backend applications — *added in the next PR in this stack* |
> | `Ably.PubSub.Device` | End-user device applications (desktop, mobile, Unity, MAUI, browser-adjacent clients) |
> | `Ably.PubSub.Server` | Server-side and backend applications (ASP.NET, Azure hosts, workers, console apps) |
> | `Ably.PubSub.Core` | Internal implementation shared by the two packages above. Not intended for direct use; you receive it transitively |
>
> Install the package for the side your code runs on, and create clients through that package's factory methods — **they are the supported entry points**. `Ably.PubSub.Core` is internal: a client constructed directly from `AblyRealtime` or `AblyRest` is not classified as device-side or server-side, which Ably's platform behaviour and billing depend on.
>
> ```sh
> dotnet add package Ably.PubSub.Server
> ```
>
> ```csharp
> using IO.Ably.PubSub.Server;
>
> var realtime = PubSubServer.CreateRealtimeClient("<API_KEY>");
> var http = PubSubServer.CreateHttpClient("<API_KEY>");
> ```
>
> ```sh
> dotnet add package Ably.PubSub.Device
> ```
>
> ```csharp
> using IO.Ably.PubSub.Device;
>
> var realtime = PubSubDevice.CreateClient("<API_KEY>");
> ```
>
> Each factory also takes a `ClientOptions` or an `Action<ClientOptions>`. The returned clients are the ordinary `AblyRealtime` and `AblyRest`, so the whole of the `IO.Ably` API remains available — including device-side connectionless operations such as message history, presence reads and token requests, which is why the device package has one door and no separate HTTP factory.
>
> The compiled assembly is now `Ably.PubSub.Core.dll`. The code namespace is unchanged: `using IO.Ably;` and every public type name stay as they are for now.
>
> Today's [`ably.io`](https://www.nuget.org/packages/ably.io) 1.x package is unaffected and continues from a 1.x maintenance branch for a year after 2.0 becomes generally available; it is never published from this branch again. The same applies to `ably.io.push.android` and `ably.io.push.ios`, whose Xamarin-era projects are not part of the 2.0 set (see [PushNotifications.md](./PushNotifications.md)).
>
> Never reference `ably.io` and `Ably.PubSub.*` from the same project: they share the `IO.Ably` namespace, so mixing them is a compile error by design.
>
> This also applies **transitively**. NuGet dedupes only by package ID, so a graph that pulls both `ably.io` 1.x (often via a library dependency) and any `Ably.PubSub.*` package loads *both* assemblies, and every `IO.Ably.*` type then exists twice: you get compile error CS0433 where your own code names those types, and runtime type-identity failures (`InvalidCastException`-class) where a library exposes `IO.Ably` types across its API. There is no type-forwarding between the packages. Detect it with `dotnet nuget why <project> ably.io`; if a dependency genuinely forces both, isolate them with an [`extern alias`](https://learn.microsoft.com/dotnet/csharp/language-reference/keywords/extern-alias) — note `<Aliases>` applies only to a **direct** `PackageReference`, so first promote `ably.io` to a direct reference of the affected project, then add `<Aliases>ablyLegacy</Aliases>` to it and `extern alias ablyLegacy;` in the consuming file — otherwise treat a both-packages graph as unsupported and migrate the transitive dependency off `ably.io`.
>
> The Installation and Usage sections below still describe the 1.x `ably.io` package; they are replaced with `Ably.PubSub.Device`/`Ably.PubSub.Server` instructions before 2.0 general availability.

---

Expand Down Expand Up @@ -62,18 +85,18 @@ Everything you need to get started with Ably:

## Installation

The SDK is available as a [nuget package](https://www.nuget.org/packages/ably.io/). To get started with your project, install the package from the Package Manager Console or the .NET CLI.
Install the package for the side your application runs on. Server-side and backend applications use `Ably.PubSub.Server`; end-user device applications (desktop, mobile, Unity, MAUI) use `Ably.PubSub.Device`. Both bring in `Ably.PubSub.Core` transitively — never install `Ably.PubSub.Core` directly.

Package Manager Console:
Server-side (.NET CLI):

```shell
PM> Install-Package ably.io
dotnet add package Ably.PubSub.Server
```

.NET CLI in your project directory:
Device-side (.NET CLI):

```shell
dotnet add package ably.io
dotnet add package Ably.PubSub.Device
```

### MAUI configuration
Expand All @@ -95,8 +118,11 @@ Add the following to your `.csproj` file to prevent trimming of the Ably assembl
The following code connects to Ably's realtime messaging service, subscribes to a channel to receive messages, and publishes a test message to that same channel:

```csharp
// Initialize Ably Realtime client
var realtime = new AblyRealtime("your-ably-api-key");
// Initialize an Ably Realtime client through the server-side door
using IO.Ably.PubSub.Server;

var realtime = PubSubServer.CreateRealtimeClient("your-ably-api-key");
// Device-side applications use: PubSubDevice.CreateClient("your-ably-api-key");

// Wait for connection to be established
realtime.Connection.On(ConnectionEvent.Connected, args =>
Expand Down Expand Up @@ -175,5 +201,5 @@ var websocketOptions = new MsWebSocketOptions

options.TransportFactory = new MsWebSocketTransport.TransportFactory(websocketOptions);

var realtime = new AblyRealtime(options);
var realtime = PubSubServer.CreateRealtimeClient(options);
```
23 changes: 21 additions & 2 deletions cake-build/helpers/tools.cake
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public class ILRepackHelper
_context = context;
}

public void MergeDLLs(FilePath primaryDll, FilePath[] dllsToMerge, FilePath outputDll)
public void MergeDLLs(FilePath primaryDll, FilePath[] dllsToMerge, FilePath outputDll, FilePath internalizeExcludeFile = null)
{
if (!_context.FileExists(primaryDll))
{
Expand Down Expand Up @@ -56,7 +56,9 @@ public class ILRepackHelper
// Build ILRepack arguments - explicitly merge only the DLLs we specify
var args = new ProcessArgumentBuilder()
.Append("/targetplatform:v4")
.Append("/internalize")
.Append(internalizeExcludeFile != null && _context.FileExists(internalizeExcludeFile)
? $"/internalize:\"{internalizeExcludeFile.FullPath}\""
: "/internalize")
// /lib: Specifies where ILRepack should search for referenced assemblies when loading the primary DLL.
// This is needed because Mono.Cecil (used by ILRepack) must resolve all type references while reading
// the assembly metadata, even before the merge begins. Without this, it fails to resolve types from
Expand All @@ -66,6 +68,23 @@ public class ILRepackHelper
.Append($"/keyfile:\"{rootDir.CombineWithFilePath("IO.Ably.snk").FullPath}\"")
.Append("/parallel")
.Append($"/out:\"{outputDll.FullPath}\"");

// When ILRepack runs under Mono (macOS/Linux), the netstandard2.0 inputs' 'netstandard'
// facade lives in Mono's 4.5/Facades directory, which /targetplatform:v4 does not search;
// add it as an extra /lib so Mono.Cecil can resolve it. No-op on Windows (paths absent).
var monoFacadesCandidates = new[]
{
new DirectoryPath("/Library/Frameworks/Mono.framework/Versions/Current/lib/mono/4.5/Facades"),
new DirectoryPath("/usr/lib/mono/4.5/Facades")
};
foreach (var facades in monoFacadesCandidates)
{
if (_context.DirectoryExists(facades))
{
args.Append($"/lib:\"{facades.FullPath}\"");
break;
}
}

// Add all DLL paths explicitly (primary + merging DLLs)
foreach (var dllPath in dllPaths)
Expand Down
23 changes: 19 additions & 4 deletions cake-build/tasks/build.cake
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ Task("_Build_Ably_Unity_Dll")
.Description("Create merged Unity DLL with all dependencies")
.Does(() =>
{
Information("Merging Unity dependencies into Ably.PubSub.Core.dll...");
Information("Merging Unity dependencies and the Ably.PubSub.Device door into Ably.PubSub.Device.dll...");

var netStandard20BinPath = paths.Src
.Combine("Ably.PubSub.Core")
Expand All @@ -108,8 +108,19 @@ Task("_Build_Ably_Unity_Dll")
throw new Exception($"Newtonsoft.Json.dll not found at: {newtonsoftDll}");
}

var deviceDoorDll = paths.Src
.Combine("Ably.PubSub.Device")
.Combine("bin/Release/netstandard2.0")
.CombineWithFilePath("Ably.PubSub.Device.dll");

if (!FileExists(deviceDoorDll))
{
throw new Exception($"Device door DLL not found: {deviceDoorDll}. Please build the Ably.PubSub.Device project first.");
}

var dllsToMerge = new[]
{
deviceDoorDll,
netStandard20BinPath.CombineWithFilePath("IO.Ably.DeltaCodec.dll"),
netStandard20BinPath.CombineWithFilePath("System.Runtime.CompilerServices.Unsafe.dll"),
netStandard20BinPath.CombineWithFilePath("System.Threading.Channels.dll"),
Expand All @@ -118,7 +129,10 @@ Task("_Build_Ably_Unity_Dll")
};

var unityOutputPath = paths.Root.Combine("unity/Assets/Ably/Plugins");
var outputDll = unityOutputPath.CombineWithFilePath("Ably.PubSub.Core.dll");
// The merged Unity plugin is named after the public device door package; the
// primary input stays Ably.PubSub.Core.dll (its public API survives the merge)
// and the real Ably.PubSub.Device.dll is one of the merged, exclude-protected inputs.
var outputDll = unityOutputPath.CombineWithFilePath("Ably.PubSub.Device.dll");

// Delete existing output DLL if it exists
if (FileExists(outputDll))
Expand All @@ -127,8 +141,9 @@ Task("_Build_Ably_Unity_Dll")
Information($"Deleted existing DLL: {outputDll}");
}

// Merge all dependencies into primary DLL in one go
ilRepackHelper.MergeDLLs(primaryDll, dllsToMerge, outputDll);
// Merge all dependencies into primary DLL in one go, keeping the device door namespace public
var internalizeExclude = paths.Root.Combine("cake-build").CombineWithFilePath("unity-internalize-exclude.txt");
ilRepackHelper.MergeDLLs(primaryDll, dllsToMerge, outputDll, internalizeExclude);

Information($"✓ Unity DLL created at: {outputDll}");
});
Expand Down
10 changes: 6 additions & 4 deletions cake-build/tasks/package.cake
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,13 @@ Task("_Package_Create_NuGet")
Information($"Creating NuGet packages version {version}...");

// The lockstep package set. Every package here is built from this repository
// and released at the same version. Stack PR 2 adds the door packages
// (ably.pubsub.device.nuspec, ably.pubsub.server.nuspec) to this list.
// and released at the same version, and both door packages pin the core exactly.
// Packed core-first so the order matches the publish order stack PR 3 adds.
var nuspecFiles = new[]
{
"nuget/ably.pubsub.core.nuspec"
"nuget/ably.pubsub.core.nuspec",
"nuget/ably.pubsub.device.nuspec",
"nuget/ably.pubsub.server.nuspec"
};

var nugetSettings = new NuGetPackSettings
Expand Down Expand Up @@ -133,7 +135,7 @@ Task("_Package_Unity")
///////////////////////////////////////////////////////////////////////////////

Task("Package")
.Description("Create the NuGet packages (Ably.PubSub.Core)")
.Description("Create the NuGet packages (Ably.PubSub.Core, Ably.PubSub.Device, Ably.PubSub.Server)")
.IsDependentOn("_Package_Create_NuGet");

Task("UnityPackage")
Expand Down
1 change: 1 addition & 0 deletions cake-build/unity-internalize-exclude.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
^IO\.Ably\.PubSub\.Device\..*
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Ably.PubSub.Core\Ably.PubSub.Core.csproj" />
<ProjectReference Include="..\..\src\Ably.PubSub.Server\Ably.PubSub.Server.csproj" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
Expand Down
3 changes: 2 additions & 1 deletion examples/NotificationsPublisher/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.IO;
using System.Text;
using IO.Ably;
using IO.Ably.PubSub.Server;
using IO.Ably.Types;
using Newtonsoft.Json.Linq;
using Terminal.Gui;
Expand Down Expand Up @@ -284,7 +285,7 @@ void InitialiseAbly(string key)
LogLevel = LogLevel.Warning,
AutoConnect = false
};
Ably = new AblyRealtime(options);
Ably = PubSubServer.CreateRealtimeClient(options);
}

var key = GetCurrentKey();
Expand Down
50 changes: 50 additions & 0 deletions nuget/ably.pubsub.device.nuspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
<metadata>
<id>Ably.PubSub.Device</id>
<version>$version$</version>
<title>Ably.PubSub.Device</title>
<authors>Martin Georgiev, Sachin Shinde, Yavor Ivanov, Jack Rutherford, Tom Kirby-Green</authors>
<owners>Ably Real-time Ltd</owners>
<license type="expression">Apache-2.0</license>
<projectUrl>https://github.com/ably/ably-dotnet</projectUrl>
<icon>icon.png</icon>
<readme>README.md</readme>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>The supported entry point for Ably Pub/Sub in an end-user device application - a mobile or desktop app, a Unity game, a set-top box or any other client the end user holds. Create clients through PubSubDevice.CreateClient(...) in the IO.Ably.PubSub.Device namespace; it accepts an API key, an Ably token, a ClientOptions or an Action&lt;ClientOptions&gt;, and returns the ordinary AblyRealtime so the whole of the IO.Ably API remains available, including device-side connectionless operations such as message history, presence reads and token requests. Creating a client any other way - including directly from the Ably.PubSub.Core implementation package this depends on - does not classify it as device-side, which Ably's platform behaviour and billing depend on. Install Ably.PubSub.Server instead in a server-side application. See https://www.ably.com for more info.</description>
<releaseNotes>https://github.com/ably/ably-dotnet/releases</releaseNotes>
<copyright>©2026 Ably Real-time Ltd</copyright>
<tags>ably realtime messaging websocket pubsub presence device mobile dotnet csharp maui unity netstandard</tags>
<repository type="git" url="https://github.com/ably/ably-dotnet.git" branch="main" />
<language />
<!-- The exact pin is the NuGet analogue of ably-js's exact peerDependencies: the door and
the core are released in lockstep, and consumers must never resolve two core
versions. nuget.exe substitutes $version$ inside the dependency version attribute, so
the packed pin equals the package version (asserted by the PackagingSpecs pre-flight
and release-dry-run in the release tooling PR). -->
<dependencies>
<group targetFramework="netstandard2.0">
<dependency id="Ably.PubSub.Core" version="[$version$]" />
</group>
<group targetFramework="net6.0">
<dependency id="Ably.PubSub.Core" version="[$version$]" />
</group>
<group targetFramework="net7.0">
<dependency id="Ably.PubSub.Core" version="[$version$]" />
</group>
</dependencies>
</metadata>
<files>
<file src="..\src\Ably.PubSub.Device\bin\$configuration$\netstandard2.0\Ably.PubSub.Device.dll" target="lib\netstandard2.0" />
<file src="..\src\Ably.PubSub.Device\bin\$configuration$\netstandard2.0\Ably.PubSub.Device.pdb" target="lib\netstandard2.0" />
<file src="..\src\Ably.PubSub.Device\bin\$configuration$\netstandard2.0\Ably.PubSub.Device.xml" target="lib\netstandard2.0" />
<file src="..\src\Ably.PubSub.Device\bin\$configuration$\net6.0\Ably.PubSub.Device.dll" target="lib\net6.0" />
<file src="..\src\Ably.PubSub.Device\bin\$configuration$\net6.0\Ably.PubSub.Device.pdb" target="lib\net6.0" />
<file src="..\src\Ably.PubSub.Device\bin\$configuration$\net6.0\Ably.PubSub.Device.xml" target="lib\net6.0" />
<file src="..\src\Ably.PubSub.Device\bin\$configuration$\net7.0\Ably.PubSub.Device.dll" target="lib\net7.0" />
<file src="..\src\Ably.PubSub.Device\bin\$configuration$\net7.0\Ably.PubSub.Device.pdb" target="lib\net7.0" />
<file src="..\src\Ably.PubSub.Device\bin\$configuration$\net7.0\Ably.PubSub.Device.xml" target="lib\net7.0" />
<file src="..\README.md" target="README.md" />
<file src="..\images\logo.png" target="icon.png" />
</files>
</package>
Loading
Loading