Realtime

Xams provides built-in support for real-time communication between server and client using SignalR.

Creating a Hub

Xams uses a single SignalR hub per browser window instance for optimal performance. You can create service-specific hubs using the ServiceHub attribute and implementing the IServiceHub interface.

Project/Hubs/ChatHub.cs

using Microsoft.AspNetCore.SignalR;
using Xams.Core.Attributes;
using Xams.Core.Contexts;
using Xams.Core.Dtos;
using Xams.Core.Interfaces;
using Xams.Core.Utils;
[ServiceHub(nameof(ChatHub))]
public class ChatHub : IServiceHub
{
private static readonly string GroupName = "MyChatGroup";
public async Task<Response<object?>> OnConnected(HubContext context)
{
await context.Groups.AddToGroupAsync(context.SignalRContext.ConnectionId, GroupName);
return ServiceResult.Success();
}
public async Task<Response<object?>> OnDisconnected(HubContext context)
{
await context.Groups.RemoveFromGroupAsync(context.SignalRContext.ConnectionId, GroupName);
return ServiceResult.Success();
}
public async Task<Response<object?>> OnReceive(HubContext context)
{
var stringMessage = context.Message;
var message = context.GetMessage<ClientMessage>();
if (message.type == "message")
{
await context.Clients.All.SendAsync("ReceiveMessage", message.content);
}
return ServiceResult.Success("Message Received!");
}
public class ClientMessage
{
public string type { get; set; }
public string content { get; set; }
}
public async Task<Response<object?>> Send(HubSendContext context)
{
await context.Clients.All.SendAsync(context.Message as string ?? "");
return ServiceResult.Success();
}
}

Hub Permissions

Hub access is controlled through role-based permissions.

The IServiceHub interface defines four methods with specific permission requirements:

IServiceHub Interface

public interface IServiceHub
{
// Called when a client with hub permissions connects
public Task<Response<object?>> OnConnected(HubContext context);
// Called when a client disconnects or loses permissions
public Task<Response<object?>> OnDisconnected(HubContext context);
// Called when receiving messages from permitted clients
public Task<Response<object?>> OnReceive(HubContext context);
// Called from server-side services to send messages
public Task<Response<object?>> Send(HubSendContext context);
}

Client Implementation

The client connects to the hub using the appContext.signalR() method, which returns a singleton SignalR connection for the browser window.

src/pages/Chat.tsx

const Chat = () => {
const appContext = useAppContext()
const [message, setMessage] = useState('')
const [messages, setMessages] = useState<string[]>([])
const onSubmit = async (e: FormEvent<HTMLFormElement> | undefined) => {
e?.preventDefault()
const connection = await appContext.signalR()
// Use send() for fire-and-forget, invoke() for return values
await connection.send(
'ChatHub',
JSON.stringify({
type: 'message',
content: message,
}),
)
setMessage('')
}
useEffect(() => {
let cleanup: (() => void) | undefined
const connect = async () => {
const signalR = await appContext.signalR()
// Framework automatically reconnects - no manual reconnection logic needed
signalR.on('ReceiveMessage', (message: string) => {
setMessages((msgs) => [...msgs, `${message}`])
})
cleanup = () => signalR.off('ReceiveMessage')
}
// Ensure the connection is established before using it
appContext.signalR()
if (appContext.signalRState === HubConnectionState.Connected) {
connect()
}
return () => cleanup?.()
}, [appContext.signalRState])
return (
<AppLayout>
<ul>
{messages.map((msg, idx) => (
<li key={idx}>{msg}</li>
))}
</ul>
<form onSubmit={onSubmit}>
<div className="flex w-full gap-2">
<TextInput
value={message}
className="w-full"
onChange={(e) => setMessage(e.currentTarget.value)}
></TextInput>
<div>
<Button type="submit">Send</Button>
</div>
</div>
</form>
</AppLayout>
)
}

Sending Messages from Services

Service classes (Actions, Jobs, or Service Logic) can send messages to clients using the HubSend method, which invokes the hub's Send method.

Project/Service/WidgetService.cs

[ServiceLogic("Widget", DataOperation.Create, LogicStage.PreOperation)]
public class WidgetService : IServiceLogic
{
public async Task<Response<object?>> Execute(ServiceContext context)
{
await context.HubSend<ChatHub>(new ChatHub.ServerMessage()
{
type = "message_all_clients",
content = "A new widget is being created!"
});
return ServiceResult.Success();
}
}

The hub's Send method processes server-side messages (see complete example above).

Dependency Injection

ServiceHub classes support constructor-based dependency injection:

Project/Hubs/ChatHub.cs

[ServiceHub(nameof(ChatHub))]
public class ChatHub : IServiceHub
{
private readonly IEmailService _emailService;
public ChatHub(IEmailService emailService)
{
_emailService = emailService;
}
public async Task<Response<object?>> OnConnected(HubContext context)
{
await _emailService.SendWelcome(context.ExecutingUserId);
return ServiceResult.Success();
}
// ... other methods
}

Was this page helpful?