ASP.NET Core WebSockets - c#

I'm trying to have a WebSocket server up and running on ASP.NET Core. I created an empty web project dotnet new web changed the Program.cs to:
public static void Main(string[] args) {
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder => {
webBuilder.UseStartup<Startup>();
})
.Build()
.Run();
}
And Startup.cs's ConfigureServices method to:
public void ConfigureServices(IServiceCollection services) {
services.AddControllers();
services.AddWebSockets();
}
And Configure method to:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
app.UseWebSockets();
app.UseRouting();
app.UseEndpoints(endpoints => {
endpoints.MapControllers();
endpoints.MapConnectionHandler<WebSocketHandler>("/ws");
});
}
And my WebSocketHandler's OnConnectedAsync method I've the following:
public override async Task OnConnectedAsync(ConnectionContext connection)
{
var context = connection.GetHttpContext();
var endpoint = $"{connection.RemoteEndPoint}";
if (!context.WebSockets.IsWebSocketRequest) {
connection.Abort();
_logger.LogCritical($"Request from {endpoint} endpoint aborted.");
return;
}
var websocket = await context.WebSockets.AcceptWebSocketAsync();
_logger.LogInformation($"WebSocket request from {endpoint} endpoint accepted!");
}
The problem arises when I try to connect to APP_URL/ws and each time the server closes the connection as soon as it receives the request. Here are the logs: https://pastebin.com/raw/34yu7thw
If I place a Task.Delay(-1) at the end of OnConnectedAsync method, it keeps the connection open but drops incoming connections.
I have searched MSDocs and haven't been able to find much documentation on how to use MapConnectionHandler<T>.
Would it be safe for me to have a while loop which receives messages from multiple clients in OnConnectedAsync?
Is this not the right way to handle websocket connections?
Is MapConnectionHandler<T> transient?
I'm really confused and can't figure out it's behavior.

I've implemented a WebSocket server in ASP.NET core using these docs and it worked out quite well for me: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/websockets?view=aspnetcore-3.1
The main idea is that after you accept a request with AcceptWebSocketAsync() you take the returned WebSocket object and use it to send and receive. Typically you would create a loop, calling ReceiveAsync until some condition is met (you determine the session is done when you receive a certain message, or the client disconnects, etc). As the docs state, When using a WebSocket, you must keep the middleware pipeline running for the duration of the connection. So if you're passing that WebSocket connection off to a background worker to perform send/receive on, you need to keep that pipeline open for the duration of your interactions with that client. Then when you're done you signal to the middleware that the connection is finished and it can unwind.
I suspect not keeping the connection open and looping is your issue. I haven't used MapConnectionHandler for WebSockets before, so this might not work, but it's possible the above strategy will be helpful to you, or follow the docs with a background worker like so:
app.Use(async (context, next) => {
var socket = await context.WebSockets.AcceptWebSocketAsync();
var socketFinishedTcs = new TaskCompletionSource<object>();
BackgroundSocketProcessor.AddSocket(socket, socketFinishedTcs);
await socketFinishedTcs.Task;
});

So, I accomplished my goal. Complete use-case is here: https://github.com/Yucked/Rhapsody/blob/beta/src/Controllers/WebSocketHandler.cs
This approach uses System.IO.Pipelines. I don't have the old source code as I scrapped it and can't figure out why connections were being dropped before even after keeping the pipeline open but hey it works now!
app.UseEndpoints(endpoints => {
endpoints.MapControllers();
endpoints.MapConnectionHandler<WebSocketHandler>("/player/{playerId}");
});
public override async Task OnConnectedAsync(ConnectionContext connection) {
var httpContext = connection.GetHttpContext();
if (!httpContext.WebSockets.IsWebSocketRequest) {
await httpContext.Response.CompleteAsync();
return;
}
await httpContext.WebSockets.AcceptWebSocketAsync()
.ContinueWith(async task => {
var webSocket = await task;
await HandleConnectionAsync(webSocket);
});
}
private async Task HandleConnectionAsync(WebSocket websocket) {
try {
do {
var memory = writer.GetMemory(BUFFER_SIZE);
var receiveResult = await webSocket.ReceiveAsync(memory, CancellationToken.None);
if (!receiveResult.EndOfMessage) {
writer.Advance(receiveResult.Count);
continue;
}
await writer.FlushAsync();
} while (webSocket.State == WebSocketState.Open);
}
catch (Exception exception) {
await writer.CompleteAsync(exception);
}
}

Related

SignalR .NET 6 Client Not adding handler

So, I am trying to do a SIMPLE task with SignalR right now. Basically, I have a console app that will have .NET SignalR client, and a .NET 6 Web API with SignalR server.
I am just trying to do a simple flow:
Client sends request to SignalR Hub
Hub processes request, and returns data back to .NET client
I have no user interaction at all, that's why I'm not using JS, as this will not be in the browser.
Here is my current setup for the SignalR Server
OnPremAgentClientHub in Web API:
using Microsoft.AspNetCore.SignalR;
namespace HttpLongPollingServer.Hubs
{
public sealed class OnPremClientHub : Hub
{
public async Task GetOnPremAgentStatus(string clientIp)
{
bool isReachable = OnPremAgentData.IsAgentPingable(clientIp)
await Clients.Caller.SendAsync("OnPremAgentStatusReceived", isReachable);
}
}
}
The relevant code from Program.cs in Web API for SignalR setup:
app.MapHub<OnPremClientHub>("/clienthub", options =>
{
options.Transports = HttpTransportType.LongPolling;
});
Yes, I want to force HTTP Long polling.
Here is my setup in the console app for SignalR client
Program.cs
var hubConnection = new HubConnectionBuilder()
.WithUrl("https://localhost:7184/clienthub")
.WithAutomaticReconnect()
.ConfigureLogging(logging =>
{
logging.AddConsole();
// This will set ALL logging to Debug level
logging.SetMinimumLevel(LogLevel.Debug);
})
.Build();
await hubConnection.StartAsync();
await LongPollingTest.TestHttpSignalRLongPolling(hubConnection);
The TestHttpSignalRLongPolling that calls the SignalR Hub methods:
public static async Task TestHttpSignalRLongPolling(HubConnection hubConnection)
{
await hubConnection.InvokeAsync("GetOnPremAgentStatus", arg1: "192.168.19.128");
hubConnection.On("OnPremAgentStatusReceived", (bool isReachable) => {
if (isReachable)
Console.WriteLine("Agent is reachable");
else
Console.WriteLine("Agent is not reachable");
});
}
Now, I can get the client to invoke the GetOnPremAgentStatus. However, when the client goes to do the hubConnection.On()... I get the following Warning and Error
Microsoft.AspNetCore.SignalR.Client.HubConnection[14]
Failed to find handler for 'OnPremAgentStatusReceived' method.
fail: Microsoft.AspNetCore.SignalR.Client.HubConnection[57]
Failed to bind arguments received in invocation '(null)' of 'OnPremAgentStatusReceived'.
System.IO.InvalidDataException: Invocation provides 1 argument(s) but target expects 0.
at Microsoft.AspNetCore.SignalR.Protocol.JsonHubProtocol.BindTypes(Utf8JsonReader& reader, IReadOnlyList`1 paramTypes)
I'm banging my head against a wall. I feel it's something simple I'm missing, I just don't know what it is.
Any tips or help is appreciated!
Alright, so the answer was staring me straight in the face in the Microsoft docs. The problem was that I was trying to call a callback on an unregistred handler that is located in my Server Hub in my client code. The fix was easy. I simply moved my connection.On to Program.cs after making my Hub Connection, but before starting the connection.
Updated Program.cs looks like this:
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using HttpLongPollingClientConsole;
ServiceProvider service = new ServiceCollection()
.AddLogging((loggingBuilder) => loggingBuilder
.SetMinimumLevel(LogLevel.Debug)
.AddConsole()
.AddDebug())
.BuildServiceProvider();
var hubConnection = new HubConnectionBuilder()
.WithUrl("https://localhost:7184/clienthub", options =>
{
options.Transports = Microsoft.AspNetCore.Http.Connections.HttpTransportType.LongPolling;
})
.WithAutomaticReconnect()
.ConfigureLogging(logging =>
{
logging.AddConsole();
// This will set ALL logging to Debug level
logging.SetMinimumLevel(LogLevel.Debug);
})
.Build();
// Register the handler here!!
hubConnection.On<bool>("OnPremAgentStatusReceived", (isReachable) => {
if (isReachable)
Console.WriteLine("Agent is reachable");
else
Console.WriteLine("Agent is not reachable");
});
hubConnection.StartAsync().Wait();
await LongPollingTest.TestHttpSignalRLongPolling(hubConnection);
Console.ReadKey();
This is explained in the Microsoft Docs very well.
Now, from my Debug output of the SignalR client I can see the Handler being registered properly.
dbug: Microsoft.AspNetCore.SignalR.Client.HubConnection[40]
Registering handler for client method 'OnPremAgentStatusReceived'.

How to integrate Sentry with .NET 6.0 Worker Service?

I integrated Sentry with .NET Core 6.0 Worker Service this way:
NuGet: Sentry 3.17.1
// Program.cs:
using Sentry;
var sentryDsn = Environment.GetEnvironmentVariable("SENTRY_DSN");
using (SentrySdk.Init(o =>
{
o.Dsn = sentryDsn;
o.Debug = true;
o.TracesSampleRate = 1.0;
}))
{
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddHostedService<Worker>();
})
.Build();
await host.RunAsync();
}
// Worker.cs:
namespace demo_heroku_sentry_worker;
using Sentry;
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
public Worker(ILogger<Worker> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
try
{
throw new ApplicationException("Exception inside of worker service");
}
catch (Exception e)
{
SentrySdk.CaptureException(e);
}
await Task.Delay(10000, stoppingToken);
}
}
}
This is working in some way because I see the manually captured error on my Sentry Dashboard. However I'm concerned about these warning messages I receive on the Application Output:
Worker running at: 05/11/2022 15:51:06 +02:00
Debug: Failed to report an error on a session because there is none active.
Info: Capturing event.
Debug: Running processor on exception: Exception inside of worker service
Debug: Creating SentryStackTrace. isCurrentStackTrace: False.
Debug: Running main event processor on: Event abb5b3e2ee3a4dbd***********
Info: Envelope queued up: 'abb5b3e2ee3a4dbda50ef***********'
Debug: Envelope abb5b3e2ee3a4dbda50e*********** handed off to transport. #1 in queue.
Debug: Envelope 'abb5b3e2ee3a4dbda50efe7***********' sent successfully. Payload:
Is there something I am missing?
A few things:
There's nothing wrong with the way you originally implemented it. The debug logs you showed are just Sentry doing it's normal work. If you don't want to see them, don't set o.Debug = true;
You have code that manually reads the SENTRY_DSN environment variable and sets it to o.Dsn during initialization. That's redundant because the SDK will already look for that environment variable if you don't pass a DSN in code. Do one or the other, but not both.
If you were concerned about the message "Failed to report an error on a session because there is none active." - That's just saying that you haven't started a session, so Sentry's "Release Health" feature won't work. You can enable it either by manually calling SentrySdk.StartSession(); and SentrySdk.EndSession(); at appropriate places in your application, or by setting o.AutoSessionTracking = true; which is easier. The latter will start a session when the Sentry SDK is initialized, and end it when it is disposed.
While nothing wrong with the approach you showed, you would get better functionality out of the logging integration. The example code here shows how to use it with the generic host. Use the nuget package Sentry.Extensions.Logging.
Putting it all together :
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddHostedService<Worker>();
})
.ConfigureLogging(logging =>
{
logging.AddSentry(o =>
{
// o.Dsn = "(only if not using the env var)";
o.TracesSampleRate = 1.0;
o.AutoSessionTracking = true;
});
}
.Build();
await host.RunAsync();
It seems that as per the documentation the correct way to integrate Sentry with .NET (Core) 6.0 is the following: (changes indicated <--)
// Program.cs
var builder = WebApplication.CreateBuilder(args);
var sentryDsn = Environment.GetEnvironmentVariable("SENTRY_DSN"); // <--
builder.WebHost.UseSentry(sentryDsn); // <--
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
app.UseSentryTracing(); // <--
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
It does catch unhandled API call errors and it does not print any warnings on the output console.

SignalR behind Yarp.ReverseProxy leads to timeout cause server not answering

I have implemented an yarp.reverse proxy server with the code below:
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpProxy();
services.AddCors(options =>
{
options.AddPolicy("customPolicy", builder =>
{
builder.AllowAnyOrigin();
});
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHttpProxy httpProxy)
{
// Configure our own HttpMessageInvoker for outbound calls for proxy operations
var httpClient = new HttpMessageInvoker(new SocketsHttpHandler()
{
UseProxy = false,
AllowAutoRedirect = false,
AutomaticDecompression = DecompressionMethods.None,
UseCookies = false
});
// Setup our own request transform class
var transformer = new CustomTransformer(); // or HttpTransformer.Default;
var requestOptions = new RequestProxyOptions { Timeout = TimeSpan.FromSeconds(100) };
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.Map("/{**catch-all}", async httpContext =>
{
httpContext.Request.Headers["Connection"] = "upgrade";
await httpProxy.ProxyAsync(httpContext, "http://192.168.178.80:5000", httpClient, requestOptions, transformer);
var errorFeature = httpContext.Features.Get<IProxyErrorFeature>();
save_log(httpContext.Request.Path, "/", "http://192.168.178.80:5000" + httpContext.Request.Path, "3");
// Check if the proxy operation was successful
if (errorFeature != null)
{
var error = errorFeature.Error;
var exception = errorFeature.Exception;
}
});
});
}
And in another app a SignalR server following this example: https://learn.microsoft.com/en-GB/aspnet/core/tutorials/signalr?view=aspnetcore-5.0&tabs=visual-studio
The proxy server works and forwards the request to the signalR server. But the signalR Client is not able to connect to the signalR Server. I always get a Connection disconnected with error
Error: Server timeout elapsed without receiving a message from the server.
in the Java Script console.
But the SSE is connected as you can see in the following browser status report:
signalr.js:2156 [2021-03-25T13:19:29.970Z] Information: SSE connected to https://localhost:44318/chatHub?id=IqKD6P0NsUY9Is6OSrMusQ
The problem seems to be the Proxy Server because if I call the site directly it works. Has somebody any idea what's wrong with my Proxy and how I can solve it?

Failing to connect to SignalR from C# SignalR Client library (404 error)

There's tons of these questions but none of them seem to work or give useful answers. The story is the same as almost all the questions, here's what I've got:
A Hub class:
public class MyHub : Hub
{
public override async Task OnConnectedAsync()
{
var id = Context.ConnectionId;
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
await base.OnDisconnectedAsync(exception);
}
}
My Startup.cs:ConfigureServices method:
public void ConfigureServices(IServiceCollection services)
{
// other stuff that isn't relevant has been hidden
services.AddSignalR(config =>
{
config.EnableDetailedErrors = true;
});
}
My Startup.cs:Configure method:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider)
{
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<MyHub>("myHub");
});
}
And here is my simple test program:
class Program
{
static async Task Main(string[] args)
{
var url = #"http://localhost:5000";
var hub = "MyHub";
var hubConnection = new HubConnection(url);
var proxy = hubConnection.CreateHubProxy(hub);
await hubConnection.Start();
hubConnection.Dispose();
}
}
What have I tried?
Searched this website for a long time checking dozens of similar posts with no luck.
modified the URL many, many times. I have a healthcheck endpoint on my server, which I can hit, so I know my URL is correct (at least the http://localhost:5000 part). I've tried adding /myhub, /signalr/myhub. I've tried using the HTTPS endpoints on port 5001 with all of the above. I've tried changing capitalization (myhub, MyHub, myHub, MYHUB)
For the life of me I cannot get this to work. I always get a 404. There's really not much useful information in the console logs
Request starting HTTP/1.1 GET https://localhost:5001/signalr/myhub/signalr/negotiate?clientProtocol=2.1&connectionData=[%7B%22Name%22:%22MyHub%22%7D]
{ "name": "PROJECT_NAME", "traceId": "0HLRIM50HAMJA:00000001", "url": "https://localhost/signalr/myHub/signalr/negotiate", "time": "2019-11-26T20:49:50.0222176Z", "level": "INFO", "message": "Request finished in 3.6298000000000004ms 404 ", "ElapsedMilliseconds": 3.6298000000000004, "StatusCode": 404, "ContentType": null, "EventId_Id": 2, "EventId_Name": null, "EventId": "2" }
This log message never changes.
If I hit the service with http://localhost:5000/myHub, I get a 400 error, so I can derive from that that my hub is indeed registered. It's just something screwy with the conection.
Any help would be greatly appreciated.
Also, as a I side note, I see lots of examples using HubConnectionBuilder but the Microsoft.AspNet.SignalR.Client package doesn't seem to include it. There is a similarly named package with .Core on the end, but the HubConnectionBuilder in there doesn't provide the necessary methods (I need specifically the WithUrl method, and it doesn't have it.)
I found the answer after some more headbanging (Stack Overflow serves as a really good rubber ducky in case you didn't know!).
The main issue here was that I fell into the pitfall of "which package do I use?". I was using the wrong package. I think I had "Microsoft.AspNet.SignalR.Client" and I needed to use "Microsoft.AspNetCore.SignalR.Client". The ambiguity can be frustrating, to say the least.

ASP.NET Web API HttpContext Response is sent back before IOwinContext Response

We are using Owin middleware in an ASP.NET Web API 2 project hosted in IIS.
I am currently experiencing a strange phenomenon where the IOwinContext.Response.Body is not being written to, and actually, even when I have a break point set in the middleware after awake Next.Invoke(), and it gets hit, the response has already been sent back to the server even if I haven't continued yet.
When I look at the response body on the IOwinContext it is empty. However, I can get the response from the HttpContext.Response.Filter. When I use the HttpContext and hit the break point, then the response isn't sent back until I continue. Below is the current configuration method being used in our Startup.cs class.
public async void Configuration(IAppBuilder app)
{
try
{
// Global Config
var config = GlobalConfiguration.Configuration;
// configure dependency injection
UnityConfig.RegisterComponents();
// configure log for net
log4net.Config.XmlConfigurator.Configure();
// turn around all requests right here
app.Use(async (context, next) =>
{
if (context.Request.Path.ToString() == "/")
{
string text = "UP";
context.Response.StatusCode = 200;
context.Response.ReasonPhrase = text;
await context.Response.WriteAsync(text);
return;
}
await next.Invoke();
});
// Handle exceptions in the OWIN layer here
app.UseUncaughtExceptionHandler();
// add cors headers
app.Use(async (context, next) => { });
// some UI stuff
app.Use(async (context, next) => { });
// Log Request Metrics
app.UseLogRequestMetrics();
// Evaluate Partner Key
app.MapWhen(context => Regex.IsMatch(context.Request.Uri.PathAndQuery.ToLower(), #"/api"), newApp =>
{
#if !DEBUG
newApp.Use<Middleware1>();
#endif
newApp.Use<Middleware2>();
newApp.Use<Middleware3>(); // On the response path back, the IOwinResponse body is already empty
});
WebApiConfig.Register(config);
app.UseWebApi(config); // It seems like I'm losing the response in here, but I don't really know
config.EnsureInitialized();
// Configure object mapping
AutoMapperConfig.Configure();
}
catch (Exception ex)
{
await LogForNetErrorLogger.LogError(ex);
}
}
I'm pretty sure my middleware is messed up, but the response is already gone before it gets back to the first of my middlewares (Middleware3) after the await Next.Invoke()
Any insight or thought provoking would be appreciated. Also, if this isn't enough information please let me know.
So, as in my post above, the problem, I thought, was the HttpResponse was being sent back before the IOwinResponse was. As it turns out, I completely overlooked the mapping section:
app.MapWhen(context => Regex.IsMatch(context.Request.Uri.PathAndQuery.ToLower(), #"/api"), newApp =>
{
#if !DEBUG
newApp.Use<Middleware1>();
#endif
newApp.Use<Middleware2>();
newApp.Use<Middleware3>();
});
When you use app.Map() it branches the middleware. So, if the path matched "/api" it would branch. However, it was also still using the app.UseWebApi() component so that was why I had two different responses and why the response I was expecting wasn't written to the Middleware3 component's IOwinContext.
I fixed it by removing the app.MapWhen() method, changing it from this:
app.MapWhen(context => Regex.IsMatch(context.Request.Uri.PathAndQuery.ToLower(), #"/site"), newApp =>
{
#if !DEBUG
newApp.Use<Middleware1>();
#endif
newApp.Use<Middleware2>();
newApp.Use<Middleware3>(); // On the response path back, the IOwinResponse body is already empty
});
to this:
#if !DEBUG
newApp.Use<Middleware1>();
#endif
newApp.Use<Middleware2>();
newApp.Use<Middleware3>();
and putting this piece of code at the beginning of the middleware components Middleware1, Middleware2, Middleware3:
public override async Task Invoke(IOwinContext context)
{
if (!context.Request.Path.ToString().StartsWith("/api/"))
{
await Next.Invoke(context);
return;
}
// stuff I want to run if the above doesn't match
await Next.Invoke(context);
...
}
Well, at least the fix was simple, even if it took me three weeks to find it. If you want to read up on the IAppBuilder.MapWhen extension method, here is some documentation https://msdn.microsoft.com/en-us/library/owin.mapwhenextensions.mapwhen(v=vs.113).aspx.

Categories

Resources