SignalR Integration Layer for Aevatar.AI.
Aevatar SignalR is a bridging layer between the Microsoft Orleans-based distributed actor framework (referred to as GAgent) and SignalR clients. Core features include:
- 🚀 Bidirectional Communication - Connects SignalR clients to GAgent systems
- 🎯 Event Routing - Delivers structured events from clients to target GAgents
- 🔄 Response Feedback - Asynchronously returns GAgent processing results to clients
- 🌐 Elastic Scaling - Manages SignalR connections using Orleans clustering capabilities
| Component | Description |
|---|---|
AevatarSignalRHub |
SignalR Hub endpoint handling client connection lifecycle |
SignalRGAgent |
Proxy GAgent responsible for forwarding client events to target GAgents |
Client → GAgent Method Signature:
Task PublishEventAsync(
GrainId grainId, // Target GAgent identifier (new SignalRGAgent will interact with this GAgent)
string eventTypeName, // Event type name inheriting from EventBase
string eventJson // JSON-serialized event data
)GAgent → Client Method Name:
ReceiveResponse
dotnet add package Aevatar.SignalRusing Aevatar.Core.Abstractions;
using Aevatar.Extensions;
using Aevatar.SignalR;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using SignalRSample.Host;
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseOrleans(silo =>
{
silo.AddMemoryStreams(AevatarCoreConstants.StreamProvider)
.AddMemoryGrainStorage("PubSubStore")
.AddLogStorageBasedLogConsistencyProvider("LogStorage")
.UseLocalhostClustering()
.UseAevatar()
.UseSignalR()
.RegisterHub<AevatarSignalRHub>();
});
builder.WebHost.UseKestrel((_, kestrelOptions) =>
{
kestrelOptions.ListenLocalhost(5001);
});
builder.Services.AddSignalR().AddOrleans();
builder.Services.AddHostedService<HostedService>();
var app = builder.Build();
app.UseRouting();
app.UseAuthorization();
app.MapHub<AevatarSignalRHub>("/aevatarHub");
await app.RunAsync();using Aevatar.Core.Abstractions.Extensions;
using Microsoft.AspNetCore.SignalR.Client;
using Newtonsoft.Json;
using SignalRSample.GAgents;
const string hubUrl = "http://localhost:5001/aevatarHub";
var connection = new HubConnectionBuilder()
.WithUrl(hubUrl)
.WithAutomaticReconnect()
.Build();
connection.On<string>("ReceiveResponse", (message) =>
{
Console.WriteLine($"[Event] {message}");
});
try
{
await connection.StartAsync();
Console.WriteLine($"Init status: {connection.State}");
var eventJson = JsonConvert.SerializeObject(new
{
Greeting = "Test message"
});
await SendEventWithRetry(connection,
"SignalRSample.GAgents.signalR",
"test".ToGuid().ToString("N"),
typeof(NaiveTestEvent).FullName!,
eventJson);
Console.WriteLine("✅ Success");
}
catch (Exception ex)
{
Console.WriteLine($"❌ Abnormal: {ex.Message}");
}
Console.ReadLine();
async Task SendEventWithRetry(HubConnection conn, string grainType, string grainKey, string eventTypeName, string eventJson)
{
var grainId = GrainId.Create(grainType, grainKey);
const int maxRetries = 3;
var retryCount = 0;
while (retryCount < maxRetries)
{
try
{
if (conn.State != HubConnectionState.Connected)
{
Console.WriteLine("Connection broke, retrying...");
await conn.StartAsync();
}
await connection.InvokeAsync("PublishEventAsync", grainId, eventTypeName, eventJson);
return;
}
catch (Exception ex)
{
retryCount++;
Console.WriteLine($"❌ Failed(Retry {retryCount}/{maxRetries}): {ex.Message}");
if (retryCount >= maxRetries)
{
throw;
}
await Task.Delay(1000 * retryCount);
}
}
}[GenerateSerializer]
public class UserLoginEvent : EventBase
{
[Id(0)] public Guid UserId { get; set; }
[Id(1)] public DateTimeOffset LoginTime { get; set; }
}
[GenerateSerializer]
public class LoginResponse : ResponseToPublisherEventBase
{
[Id(0)] public bool Success { get; set; }
[Id(1)] public Guid SessionId { get; set; }
}[GenerateSerializer]
public class MyGAgentState : StateBase
{
// Define properties.
}
[GenerateSerializer]
public class MyStateLogEvent : StateLogEventBase<MyStateLogEvent>
{
// Define properties.
}
[GAgent]
public class MyGAgent : GAgentBase<MyGAgentState, MyStateLogEvent>
{
public override Task<string> GetDescriptionAsync()
{
return Task.FromResult("This is a GAgent for demo.");
}
[EventHandler]
public async Task HandleEventAsync(UserLoginEvent eventData)
{
// Some logic.
await PublishAsync(new LoginResponse
{
Success = true,
SessionId = Guid.NewGuid()
});
}
}This project utilizes code from these outstanding open-source projects:
MIT License © 2024 Aevatar Team