Initial commit.

This commit is contained in:
2026-03-13 10:23:47 +01:00
parent fd81c63020
commit 39c8b861c0
44 changed files with 1144 additions and 4 deletions

View File

@@ -0,0 +1,18 @@
using System.Collections.Concurrent;
namespace ClientApiPoC.Shared.SignalR {
public class ClientTracker {
private ConcurrentDictionary<string, string> _knownClientIds = new();
public ICollection<string> CurrentClientIds => _knownClientIds.Values;
public bool TryAddClientId(string clientId) {
if (string.IsNullOrWhiteSpace(clientId)) throw new ArgumentNullException(nameof(clientId));
return _knownClientIds.TryAdd(clientId, clientId);
}
public bool TryRemoveClientId(string clientId) {
return _knownClientIds.TryRemove(clientId, out _);
}
}
}

View File

@@ -0,0 +1,84 @@
using Microsoft.AspNetCore.SignalR.Client;
namespace ClientApiPoC.Shared.SignalR {
public class TunnelClient : NotifyPropertyChangedBase, IDisposable, IAsyncDisposable {
#region IDisposable-Implementierung
private bool _disposed;
protected virtual void Dispose(bool disposing) {
if (!_disposed) {
if (disposing) {
_ = this.DisposeAsync();
}
_disposed = true;
}
}
public void Dispose() {
Dispose(true);
GC.SuppressFinalize(this);
}
public async ValueTask DisposeAsync() {
await DisconnectAsync();
}
#endregion
private HubConnection? _connection = null;
public bool IsConnected => (this.ConnectionState == HubConnectionState.Connected);
public bool IsConnecting => (this.ConnectionState == HubConnectionState.Connecting || this.ConnectionState == HubConnectionState.Reconnecting);
protected HubConnectionState ConnectionState => (_connection?.State ?? HubConnectionState.Disconnected);
public async Task ConnectAsync(string url, Action<HubConnection> configureConnection) {
try {
if (string.IsNullOrWhiteSpace(url)) throw new ArgumentNullException(nameof(url));
if (this.IsConnected) throw new InvalidOperationException("SignalR connection is already established.");
_connection = new HubConnectionBuilder().WithUrl(url).WithAutomaticReconnect().Build();
_connection.Reconnecting += Connection_Reconnecting;
_connection.Reconnected += Connection_Reconnected;
_connection.Closed += Connection_Closed;
configureConnection(_connection);
await _connection.StartAsync();
} finally {
await UpdateStateAsync();
}
}
public async Task DisconnectAsync() {
try {
if (_connection == null) return;
try {
if (this.IsConnected) await _connection.StopAsync();
} catch { }
_connection.Reconnecting -= Connection_Reconnecting;
_connection.Reconnected -= Connection_Reconnected;
_connection.Closed -= Connection_Closed;
await _connection.DisposeAsync();
_connection = null;
} finally {
await UpdateStateAsync();
}
}
private async Task Connection_Reconnecting(Exception? arg) {
await UpdateStateAsync();
}
private async Task Connection_Reconnected(string? arg) {
await UpdateStateAsync();
}
private async Task Connection_Closed(Exception? arg) {
await UpdateStateAsync();
}
private async Task UpdateStateAsync() {
OnPropertyChanged(nameof(IsConnected));
OnPropertyChanged(nameof(IsConnecting));
await Task.CompletedTask;
}
}
}

View File

@@ -0,0 +1,20 @@
using Microsoft.AspNetCore.SignalR;
namespace ClientApiPoC.Shared.SignalR {
public sealed class TunnelHub : Hub {
private ClientTracker _clientTracker;
public TunnelHub(ClientTracker clientTracker) : base() {
if (clientTracker == null) throw new ArgumentNullException(nameof(clientTracker));
_clientTracker = clientTracker;
}
public override sealed async Task OnConnectedAsync() {
_ = _clientTracker.TryAddClientId(this.Context.ConnectionId);
}
public override sealed async Task OnDisconnectedAsync(Exception exception) {
_ = _clientTracker.TryRemoveClientId(this.Context.ConnectionId);
}
}
}

View File

@@ -0,0 +1,21 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
namespace ClientApiPoC.Shared.SignalR {
public static class TunnelHubExtensions {
public static IServiceCollection AddTunnelHub(this IServiceCollection services) {
services.AddSignalR(options => {
// Nachrichtengröße auf max. 16MB bringen (Standard = 32KB).
options.MaximumReceiveMessageSize = (16 * 1024 * 1024);
});
services.AddSingleton<ClientTracker>();
return services;
}
public static HubEndpointConventionBuilder MapTunnelHub(this IEndpointRouteBuilder app, [StringSyntax("Route")] string pattern) {
return app.MapHub<TunnelHub>(pattern);
}
}
}

View File

@@ -0,0 +1,36 @@
using Microsoft.AspNetCore.SignalR;
namespace ClientApiPoC.Shared.SignalR {
public abstract class TunnelServerBase {
protected IHubContext<TunnelHub> HubContext { get; private set; }
protected ClientTracker Clients { get; private set; }
public TunnelServerBase(IHubContext<TunnelHub> hubContext, ClientTracker clientTracker) {
if (hubContext == null) throw new ArgumentNullException(nameof(hubContext));
if (clientTracker == null) throw new ArgumentNullException(nameof(clientTracker));
this.HubContext = hubContext;
this.Clients = clientTracker;
}
protected ISingleClientProxy? TryGetClient(string clientId) {
ISingleClientProxy? client;
try {
client = this.HubContext.Clients.Client(clientId);
} catch {
client = null;
}
return client;
}
protected IDictionary<string, ISingleClientProxy> GetAllClients() {
var results = new Dictionary<string, ISingleClientProxy>();
var allClientIds = this.Clients.CurrentClientIds;
foreach (var clientId in allClientIds) {
var client = TryGetClient(clientId);
if (client != null) results.Add(clientId, client);
}
return results;
}
}
}