Skip to content

Azure SignalR Service content rewrite #2775

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Mar 26, 2025
Merged
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
298 changes: 273 additions & 25 deletions docs/real-time/azure-signalr-scenario.md

Large diffs are not rendered by default.

Binary file added docs/real-time/media/default-mode-thumb.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/real-time/media/default-mode.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/real-time/media/serverless-mode-thumb.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/real-time/media/serverless-mode.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1,257 changes: 1,257 additions & 0 deletions docs/real-time/media/signalr-modes.excalidraw

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
global using Microsoft.AspNetCore.SignalR;
global using System.Text.Json;
global using System.Text.Json.Serialization;

global using Microsoft.AspNetCore.Http.Connections;
global using Microsoft.AspNetCore.SignalR;
global using Microsoft.Azure.SignalR.Management;
global using Microsoft.Extensions.Caching.Memory;

global using Scalar.AspNetCore;

global using SignalR.ApiService;
global using SignalR.Shared;
67 changes: 64 additions & 3 deletions docs/real-time/snippets/signalr/SignalR.ApiService/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,77 @@

builder.AddServiceDefaults();

builder.Services.AddMemoryCache();
builder.Services.AddSingleton<ServiceHubContextFactory>();
builder.Services.AddProblemDetails();
builder.Services.AddOpenApi();

builder.Services.AddSignalR()
.AddNamedAzureSignalR("signalr");
var isServerlessMode = builder.Configuration.GetValue<bool>("IS_SERVERLESS");

if (!isServerlessMode)
{
builder.Services.AddSignalR()
.AddNamedAzureSignalR("signalr");
}
else
{
builder.Services.AddSingleton(sp =>
{
return new ServiceManagerBuilder()
.WithOptions(options =>
{
options.ConnectionString = builder.Configuration.GetConnectionString("signalr");
})
.WithLoggerFactory(sp.GetRequiredService<ILoggerFactory>())
.BuildServiceManager();
});
}
var app = builder.Build();

if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference(_ => _.Servers = []);
}

app.UseExceptionHandler();

app.MapHub<ChatHub>(HubEndpoints.ChatHub);
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};

if (isServerlessMode)
{
app.MapPost("/chathub/negotiate", async (string? userId, ServiceHubContextFactory factory) =>
{
var hubContext = await factory.GetOrCreateHubContextAsync("chathub", CancellationToken.None);

NegotiationResponse negotiateResponse = await hubContext.NegotiateAsync(new()
{
UserId = userId ?? "user-1",
});

return Results.Json(negotiateResponse, new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
});
});

// try in the command line `CURL -X POST https://localhost:53282/broadcast` to broadcast messages to the clients
app.MapPost("/chathub/broadcast", async (ServiceHubContextFactory factory) =>
{
var hubContext = await factory.GetOrCreateHubContextAsync("chathub", CancellationToken.None);

await hubContext.Clients.All.SendAsync(
HubEventNames.MessageReceived,
new UserMessage("server", "Started..."));
});
}
else
{
app.MapHub<ChatHub>(HubEndpoints.ChatHub);
}

app.MapDefaultEndpoints();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace SignalR.ApiService;

internal sealed class ServiceHubContextFactory(ServiceManager serviceManager, IMemoryCache cache)
{
public async Task<ServiceHubContext> GetOrCreateHubContextAsync(string hubName, CancellationToken cancellationToken)
{
var context = await cache.GetOrCreateAsync(hubName, async _ =>
{
return await serviceManager.CreateHubContextAsync(hubName, cancellationToken);
});

return context ?? throw new InvalidOperationException("Failed to create hub context.");
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<OutputType>Exe</OutputType>
Expand All @@ -9,6 +9,9 @@

<ItemGroup>
<PackageReference Include="Microsoft.Azure.SignalR" Version="1.30.2" />
<PackageReference Include="Microsoft.Azure.SignalR.Management" Version="1.30.2" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.3" />
<PackageReference Include="Scalar.AspNetCore" Version="2.0.30" />
</ItemGroup>

<ItemGroup>
Expand Down
24 changes: 16 additions & 8 deletions docs/real-time/snippets/signalr/SignalR.AppHost/Program.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
var builder = DistributedApplication.CreateBuilder(args);
using Aspire.Hosting.Azure;

var signalr = builder.ExecutionContext.IsPublishMode
? builder.AddAzureSignalR("signalr")
: builder.AddConnectionString("signalr");
var builder = DistributedApplication.CreateBuilder(args);

var isServerless = true;

var signalR = builder.AddAzureSignalR("signalr", isServerless
? AzureSignalRServiceMode.Serverless
: AzureSignalRServiceMode.Default)
.RunAsEmulator();

var apiService = builder.AddProject<Projects.SignalR_ApiService>("apiservice")
.WithReference(signalr);

builder.AddProject<Projects.SignalR_Web>("webfrontend")
.WithReference(apiService);
.WithReference(signalR)
.WaitFor(signalR)
.WithEnvironment("IS_SERVERLESS", isServerless.ToString());

var web = builder.AddProject<Projects.SignalR_Web>("webfrontend")
.WithReference(apiService)
.WaitFor(apiService);

builder.Build().Run();
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />

<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.2.0" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.3.0" />
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="9.1.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.11.2" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.11.2" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

public static class HubEndpoints
{
public const string ChatHub = "/chathub";
public const string ChatHub = $"/{ChatHubWithoutRouteSlash}";

public const string ChatHubWithoutRouteSlash = "chathub";
}

public static class HubClientMethodNames
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@

namespace SignalR.Web.Components.Pages;
namespace SignalR.Web.Components.Pages;

public sealed partial class Home : IAsyncDisposable
{
Expand Down Expand Up @@ -40,16 +39,16 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_username = await Storage.GetItemAsync(UsernameKey)
_username = await Storage.GetItemAsync<string>(UsernameKey)
?? Guid.NewGuid().ToString();
}
}

protected override async Task OnInitializedAsync()
{
var api = Configuration.GetServiceHttpsUri("apiservice");
var apiUri = Configuration.GetServiceHttpsUri("apiservice");

var builder = new UriBuilder(api)
var builder = new UriBuilder(apiUri)
{
Path = HubEndpoints.ChatHub
};
Expand All @@ -71,7 +70,6 @@ protected override async Task OnInitializedAsync()
_hubConnection.On<UserAction>(
HubEventNames.UserTypingChanged, OnUserTypingAsync));


await _hubConnection.StartAsync();

_connectionStatus = ConnectionStatus.Connected;
Expand Down Expand Up @@ -133,7 +131,7 @@ private Task SetIsTypingAsync(bool isTyping)
return Task.CompletedTask;
}

return _hubConnection?.InvokeAsync(
return _hubConnection?.SendAsync(
HubClientMethodNames.ToggleUserTyping,
new UserAction(Name: _username, IsTyping: _isTyping = isTyping)) ?? Task.CompletedTask;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace SignalR.Web.Extensions;
using System.Data.Common;

namespace SignalR.Web.Extensions;

public static class ConfigurationExtensions
{
Expand All @@ -16,4 +18,20 @@ private static Uri GetServiceUri(this IConfiguration config, string name, string

return new(url);
}

public static Uri GetUriFromConnectionString(this IConfiguration config, string name)
{
var connectionString = config.GetConnectionString(name);

ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);

var connectionBuilder = new DbConnectionStringBuilder()
{
ConnectionString = connectionString
};

return connectionBuilder.TryGetValue("Endpoint", out var endpoint) && endpoint is string endpointString
? new Uri(endpointString)
: throw new ArgumentException($"The connection string '{name}' does not contain an 'Endpoint' value.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

<ItemGroup>
<PackageReference Include="Blazor.LocalStorage" Version="9.0.1" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.2" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.3" />
</ItemGroup>

<ItemGroup>
Expand Down
22 changes: 22 additions & 0 deletions docs/snippets/azure/AppHost/Program.ConfigureSignalRInfra.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Azure.Provisioning.SignalR;

internal static partial class Program
{
public static void ConfigureSignalRInfra(IDistributedApplicationBuilder builder)
{
// <configure>
builder.AddAzureSignalR("signalr")
.ConfigureInfrastructure(infra =>
{
var signalRService = infra.GetProvisionableResources()
.OfType<SignalRService>()
.Single();

signalRService.Sku.Name = "Premium_P1";
signalRService.Sku.Capacity = 10;
signalRService.PublicNetworkAccess = "Enabled";
signalRService.Tags.Add("ExampleKey", "Example value");
});
// </configure>
}
}
Loading