Skip to content

Commit e954e98

Browse files
authored
Blazor over DI (#12)
1 parent dadfca9 commit e954e98

File tree

11 files changed

+169
-165
lines changed

11 files changed

+169
-165
lines changed

README.md

Lines changed: 53 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,18 @@ C# client SDK for [Centrifugo](https://github.com/centrifugal/centrifugo) and [C
99

1010
## Features
1111

12+
- ✅ Multi-platform support
1213
- ✅ WebSocket and HTTP-streaming transports with automatic fallback
1314
- ✅ Browser-native transports for Blazor WebAssembly (using JS interop)
1415
- ✅ Protobuf binary protocol for efficient communication
1516
- ✅ Automatic command batching for improved network efficiency
1617
- ✅ Automatic reconnection with exponential backoff and full jitter
17-
- ✅ Channel subscriptions with recovery and positioning
18+
- ✅ Channel subscriptions with recovery and Fossil delta compression support
1819
- ✅ JWT authentication with automatic token refresh
19-
- ✅ Publication, join/leave events
20-
- ✅ RPC calls
21-
- ✅ Presence and presence stats
20+
- ✅ Publications and RPC calls
21+
- ✅ Presence and presence stats, join/leave events
2222
- ✅ History API
23-
- ✅ Async/await throughout
2423
- ✅ Thread-safe
25-
- ✅ Comprehensive event system
26-
- ✅ Multi-platform support
2724

2825
## Platform Support
2926

@@ -54,7 +51,7 @@ The SDK supports multiple transport protocols with platform-specific implementat
5451
| **WebSocket** | ✅ Native | ✅ Native | ✅ JS Interop* | ✅ Native | ⚠️ Plugin Required |
5552
| **HTTP Stream** | ✅ Native | ✅ Native | ✅ JS Interop* | ✅ Native | ❌ Not Supported |
5653

57-
**\*Blazor WebAssembly**: Requires `IJSRuntime` parameter in constructor. The SDK automatically uses browser-native implementations via JavaScript interop for both WebSocket and HTTP Stream transports.
54+
**\*Blazor WebAssembly**: Requires `builder.Services.AddCentrifugeClient()` in `Program.cs`. The SDK automatically uses browser-native implementations via JavaScript interop for both WebSocket and HTTP Stream transports.
5855

5956
**Unity WebGL**: Requires a third-party WebSocket plugin. HTTP Stream is not supported in WebGL.
6057

@@ -116,7 +113,8 @@ var result = await client.RpcAsync("method", data);
116113
### Subscriptions
117114

118115
```csharp
119-
// Create subscription, may throw Exception subscription already exists in Client's registry.
116+
// Create subscription, may throw CentrifugeDuplicateSubscriptionException
117+
// if subscription to the channel already exists in Client's registry.
120118
var subscription = client.NewSubscription("chat");
121119

122120
// Setup subscription event handlers
@@ -126,6 +124,11 @@ subscription.Publication += (sender, e) =>
126124
Console.WriteLine($"Message from {e.Channel}: {data}");
127125
};
128126

127+
subscription.Subscribing += (sender, e) =>
128+
{
129+
Console.WriteLine("Subscribing to channel");
130+
};
131+
129132
subscription.Subscribed += (sender, e) =>
130133
{
131134
Console.WriteLine($"Subscribed to channel, recovered: {e.Recovered}");
@@ -251,16 +254,15 @@ var transports = new[]
251254
)
252255
};
253256

254-
// For regular .NET apps
257+
// Create client with transport fallback
255258
var client = new CentrifugeClient(transports);
256259

257-
// For Blazor WebAssembly (pass IJSRuntime)
258-
var client = new CentrifugeClient(transports, jsRuntime);
259-
260260
// Client will try WebSocket first, then fall back to HTTP Stream if needed
261261
client.Connect();
262262
```
263263

264+
**Note**: In Blazor WebAssembly, ensure you've called `builder.Services.AddCentrifugeClient()` first (see Blazor Support section). The SDK will automatically use browser-native transports.
265+
264266
### Advanced Configuration
265267

266268
```csharp
@@ -273,7 +275,7 @@ var options = new CentrifugeClientOptions
273275
// Connection data
274276
Data = Encoding.UTF8.GetBytes("{\"custom\":\"data\"}"),
275277

276-
// Client identification
278+
// Client identification (used for observability)
277279
Name = "my-app",
278280
Version = "1.0.0",
279281

@@ -288,7 +290,7 @@ var options = new CentrifugeClientOptions
288290
// Logging (pass ILogger for debug output)
289291
Logger = loggerFactory.CreateLogger<CentrifugeClient>(),
290292

291-
// Custom headers (requires Centrifugo v6+)
293+
// Custom headers (works over header emulation, requires Centrifugo v6+)
292294
Headers = new Dictionary<string, string>
293295
{
294296
["X-Custom-Header"] = "value"
@@ -497,60 +499,31 @@ The library works in both Blazor Server and Blazor WebAssembly.
497499

498500
### Blazor Server
499501

500-
Works out of the box - uses standard .NET WebSocket client:
501-
502-
```csharp
503-
@inject CentrifugeClient Client
504-
505-
@code {
506-
protected override async Task OnInitializedAsync()
507-
{
508-
Client.Publication += async (sender, e) =>
509-
{
510-
// Update UI on message
511-
await InvokeAsync(StateHasChanged);
512-
};
513-
514-
Client.Connect();
515-
}
516-
}
517-
```
502+
Works out of the box - uses standard .NET WebSocket client. No special configuration needed.
518503

519504
### Blazor WebAssembly
520505

521-
Requires IJSRuntime for browser WebSocket access. Register the client in `Program.cs`:
506+
The SDK provides a simple DI-based configuration approach, similar to SignalR. Register Centrifuge services once in `Program.cs`:
522507

523508
```csharp
524509
using Centrifugal.Centrifuge;
525-
using Microsoft.AspNetCore.Components.Web;
526-
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
527510

528511
var builder = WebAssemblyHostBuilder.CreateDefault(args);
529512

530-
// Register CentrifugeClient as scoped service
531-
builder.Services.AddScoped(sp =>
532-
{
533-
var jsRuntime = sp.GetRequiredService<IJSRuntime>();
534-
var client = new CentrifugeClient(
535-
"ws://localhost:8000/connection/websocket",
536-
jsRuntime,
537-
new CentrifugeClientOptions
538-
{
539-
// Your options here
540-
}
541-
);
542-
return client;
543-
});
513+
// Add Centrifuge client services - automatically initializes browser interop
514+
builder.Services.AddCentrifugeClient();
544515

545516
await builder.Build().RunAsync();
546517
```
547518

548-
Then inject and use in your components:
519+
Then create and use clients anywhere in your app:
549520

550521
```csharp
551522
@page "/"
552-
@inject CentrifugeClient Client
523+
@inject IJSRuntime JS
524+
@inject ILoggerFactory LoggerFactory
553525
@implements IAsyncDisposable
526+
@using System.Text
554527

555528
<h3>Messages</h3>
556529
<ul>
@@ -561,35 +534,57 @@ Then inject and use in your components:
561534
</ul>
562535

563536
@code {
537+
private CentrifugeClient? _client;
564538
private List<string> messages = new();
565539

566540
protected override async Task OnInitializedAsync()
567541
{
568-
Client.Connected += (sender, e) =>
542+
// Create client directly - no need to pass IJSRuntime
543+
_client = new CentrifugeClient(
544+
"ws://localhost:8000/connection/websocket",
545+
options: new CentrifugeClientOptions
546+
{
547+
Logger = LoggerFactory.CreateLogger<CentrifugeClient>()
548+
}
549+
);
550+
551+
_client.Connected += (sender, e) =>
569552
{
570553
messages.Add($"Connected: {e.ClientId}");
571554
InvokeAsync(StateHasChanged);
572555
};
573556

574-
Client.Publication += (sender, e) =>
557+
var subscription = _client.NewSubscription("chat");
558+
559+
subscription.Publication += (sender, e) =>
575560
{
576561
var data = Encoding.UTF8.GetString(e.Data.Span);
577562
messages.Add($"Received: {data}");
578563
InvokeAsync(StateHasChanged);
579564
};
580565

581-
Client.Connect();
566+
_client.Connect();
567+
subscription.Subscribe();
582568
}
583569

584570
public async ValueTask DisposeAsync()
585571
{
586-
// DisposeAsync waits for disconnect to complete before releasing resources
587-
await Client.DisposeAsync();
572+
if (_client != null)
573+
{
574+
await _client.DisposeAsync();
575+
}
588576
}
589577
}
590578
```
591579

592-
**Important**: The JavaScript interop file is automatically included as a static asset. If you encounter module loading issues, ensure your `index.html` has the standard Blazor script tag:
580+
**How it works:**
581+
582+
- The SDK automatically uses browser-native transports when `AddCentrifugeClient()` is called
583+
- WebSocket uses browser's native WebSocket via JS interop
584+
- HTTP Stream uses browser's Fetch API with ReadableStream
585+
- No need to pass `IJSRuntime` to constructors - it's configured globally
586+
587+
**Important**: The JavaScript interop modules are automatically included as static assets. Ensure your `index.html` has the standard Blazor script tag:
593588

594589
```html
595590
<script src="_framework/blazor.webassembly.js"></script>

docker-compose.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ services:
88
environment:
99
- CENTRIFUGO_CLIENT_INSECURE=true
1010
- CENTRIFUGO_HTTP_STREAM_ENABLED=true
11-
- CENTRIFUGO_SSE_ENABLED=true
1211
- CENTRIFUGO_CHANNEL_WITHOUT_NAMESPACE_PRESENCE=true
1312
- CENTRIFUGO_CHANNEL_WITHOUT_NAMESPACE_DELTA_PUBLISH=true
1413
- CENTRIFUGO_CHANNEL_WITHOUT_NAMESPACE_ALLOWED_DELTA_TYPES=fossil

examples/Centrifugal.Centrifuge.BlazorExample/CentrifugeClientFactory.cs

Lines changed: 0 additions & 51 deletions
This file was deleted.

examples/Centrifugal.Centrifuge.BlazorExample/Pages/Home.razor

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
@page "/"
2-
@inject CentrifugeClientFactory ClientFactory
32
@inject IJSRuntime JS
3+
@inject ILoggerFactory LoggerFactory
44
@implements IAsyncDisposable
55
@using System.Text
6+
@using Centrifugal.Centrifuge
67

78
<PageTitle>Centrifuge Blazor Example</PageTitle>
89

@@ -404,7 +405,34 @@
404405
await LogAsync($"Could not save transport preference: {ex.Message}");
405406
}
406407

407-
_client = ClientFactory.CreateClient(useHttpStreaming);
408+
var logger = LoggerFactory.CreateLogger<CentrifugeClient>();
409+
410+
if (useHttpStreaming)
411+
{
412+
_client = new CentrifugeClient(
413+
new[]
414+
{
415+
new CentrifugeTransportEndpoint(
416+
CentrifugeTransportType.HttpStream,
417+
"http://localhost:8000/connection/http_stream"
418+
)
419+
},
420+
options: new CentrifugeClientOptions
421+
{
422+
Logger = logger
423+
}
424+
);
425+
}
426+
else
427+
{
428+
_client = new CentrifugeClient(
429+
"ws://localhost:8000/connection/websocket",
430+
options: new CentrifugeClientOptions
431+
{
432+
Logger = logger
433+
}
434+
);
435+
}
408436

409437
await LogAsync("Centrifuge C# SDK - Blazor WASM Example");
410438
await LogAsync("=========================================");

examples/Centrifugal.Centrifuge.BlazorExample/Program.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Microsoft.AspNetCore.Components.Web;
22
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
33
using Centrifugal.Centrifuge.BlazorExample;
4+
using Centrifugal.Centrifuge;
45
using Microsoft.Extensions.Logging;
56

67
var builder = WebAssemblyHostBuilder.CreateDefault(args);
@@ -13,7 +14,7 @@
1314
builder.Logging.AddFilter("Centrifugal.Centrifuge", LogLevel.Debug);
1415
builder.Logging.AddFilter("Microsoft", LogLevel.Warning); // Suppress Microsoft framework logs
1516

16-
// Register CentrifugeClientFactory for creating clients with different transports
17-
builder.Services.AddScoped<CentrifugeClientFactory>();
17+
// Add Centrifuge client services - automatically initializes browser interop
18+
builder.Services.AddCentrifugeClient();
1819

1920
await builder.Build().RunAsync();

examples/Centrifugal.Centrifuge.BlazorExample/README.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,17 @@ The example uses the Centrifuge SDK's browser-native transports:
6868
- **WebSocket**: Uses browser's native WebSocket via JavaScript interop (`BrowserWebSocketTransport`)
6969
- **HTTP Stream**: Uses browser's Fetch API with ReadableStream (`BrowserHttpStreamTransport`)
7070

71-
The `CentrifugeClient` is registered as a scoped service in `Program.cs` with `IJSRuntime` dependency injection, enabling it to use JavaScript interop for browser APIs.
71+
The SDK uses a simple DI-based configuration approach:
72+
73+
1. Call `builder.Services.AddCentrifugeClient()` in `Program.cs`
74+
2. Create `CentrifugeClient` instances anywhere in your app without passing `IJSRuntime`
75+
76+
This follows standard .NET conventions, just like SignalR's `AddSignalR()` - configure once, use everywhere!
7277

7378
## Code Structure
7479

75-
- **Program.cs**: Registers `CentrifugeClient` as a scoped service with IJSRuntime
76-
- **Pages/Home.razor**: Main example component that connects, subscribes, and logs all events
80+
- **Program.cs**: Registers Centrifuge with `AddCentrifugeClient()`
81+
- **Pages/Home.razor**: Creates client directly and logs all events
7782
- All SDK events are logged both to browser console and displayed in the UI
7883

7984
## Troubleshooting

examples/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ The easiest way is using Docker (note, we are using client insecure mode here an
1717
```bash
1818
docker pull centrifugo/centrifugo:v6
1919
docker run -p 8000:8000 \
20+
-e CENTRIFUGO_ADMIN_ENABLED="true" \
21+
-e CENTRIFUGO_ADMIN_PASSWORD="change-me-to-secure-password" \
22+
-e CENTRIFUGO_ADMIN_SECRET="change-me-to-secure-secret" \
23+
-e CENTRIFUGO_HTTP_STREAM_ENABLED="true" \
2024
-e CENTRIFUGO_CLIENT_INSECURE="true" \
2125
-e CENTRIFUGO_CLIENT_ALLOWED_ORIGINS="*" \
2226
-e CENTRIFUGO_CHANNEL_WITHOUT_NAMESPACE_DELTA_PUBLISH="true" \

0 commit comments

Comments
 (0)