SignalR .NET 6 Client Not adding handler - c#

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'.

Related

Adding cookies to SignalR Core requests

I'm using SignalR Core, and for architecture reasons the SignalR server stuff (Hubs and the like) are in a different domain from my main project.
Client has this code:
function connectToHub(url) {
var connection = new signalR.HubConnectionBuilder()
.withUrl(url, { headers: { "cookie": document.cookie } }).build();
connection.on("NotificationUpdated", function (count) {
console.log(count);
});
connection.start().then(function () {
console.log('started');
}).catch(function (err) {
return console.error(err.toString());
});
}
It connects to server fine, but OnConnectedAsync in my hub gets no cookies or any other credentials (Context.User.Identity.Claims is empty)
Here's OnConnectedAsync code
public async override Task OnConnectedAsync()
{
await Groups.AddToGroupAsync(Context.ConnectionId, ((ClaimsIdentity)Context.User.Identity)
.Claims.FirstOrDefault(c => c.Type.Contains("email"))?.Value);
await base.OnConnectedAsync();
}
If I put hub in the same domain as client, it works fine, but if I place it in different domain, client stops attaching cookies.
How to get the client to attach cookies?
(Alternatively, if there are different ways of mapping connectionId with User Claims, I would be fine with that too)

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 js client is not able to start connection, even if logs are showing that connection is being made (only LongPooling works)

I am struggling with configuring and using signalR with .net core mvc 6. The purpose for the signalR hub is to send messages to js clients after invoking method in C# controller (js client is React application configured in MVC as ClientApp).
I enabled debugging for both client signalR instance and asp.net
here are the logs from ASP.NET:
SPA proxy is ready. Redirecting to https://localhost:44440.
dbug: Microsoft.AspNetCore.Http.Connections.Internal.HttpConnectionManager[1]
New connection ctA6QHwS4fvVGcufYvtlAA created.
dbug: Microsoft.AspNetCore.Http.Connections.Internal.HttpConnectionDispatcher[10]
Sending negotiation response.
dbug: Microsoft.AspNetCore.Http.Connections.Internal.HttpConnectionDispatcher[4]
Establishing new connection.
dbug: Microsoft.AspNetCore.SignalR.HubConnectionHandler[5]
OnConnectedAsync started.
dbug: Microsoft.AspNetCore.SignalR.Internal.DefaultHubProtocolResolver[2]
Found protocol implementation for requested protocol: json.
dbug: Microsoft.AspNetCore.SignalR.HubConnectionContext[1]
Completed connection handshake. Using HubProtocol 'json'.
connected!! ctA6QHwS4fvVGcufYvtlAA
and corresponding to them logs with js client:
Utils.ts:194 [2022-02-03T18:40:17.568Z] Debug: Starting HubConnection.
Utils.ts:194 [2022-02-03T18:40:17.568Z] Debug: Starting connection with transfer format 'Text'.
Utils.ts:194 [2022-02-03T18:40:17.576Z] Debug: Sending negotiation request: https://localhost:44440/hubs/order/negotiate?negotiateVersion=1.
Utils.ts:194 [2022-02-03T18:40:21.741Z] Debug: Skipping transport 'WebSockets' because it was disabled by the client.
Utils.ts:194 [2022-02-03T18:40:21.742Z] Debug: Selecting transport 'ServerSentEvents'.
Utils.ts:194 [2022-02-03T18:40:21.742Z] Trace: (SSE transport) Connecting.
Utils.ts:190 [2022-02-03T18:40:25.857Z] Information: SSE connected to https://localhost:44440/hubs/order?id=fxqgKpJnF5Dq5MX-RCfXcg
Utils.ts:194 [2022-02-03T18:40:25.857Z] Debug: The HttpConnection connected successfully.
Utils.ts:194 [2022-02-03T18:40:25.857Z] Debug: Sending handshake request.
Utils.ts:194 [2022-02-03T18:40:25.858Z] Trace: (SSE transport) sending data. String data of length 32.
Utils.ts:194 [2022-02-03T18:40:29.969Z] Trace: (SSE transport) request complete. Response status: 200.
Utils.ts:190 [2022-02-03T18:40:29.978Z] Information: Using HubProtocol 'json'.
Utils.ts:194 [2022-02-03T18:40:59.997Z] Debug: HttpConnection.stopConnection(undefined) called while in state Disconnecting.
index.js:1 [2022-02-03T18:40:59.997Z] Error: Connection disconnected with error 'Error: Server timeout elapsed without receiving a message from the server.'.
console.<computed> # index.js:1
Utils.ts:194 [2022-02-03T18:40:59.997Z] Debug: HubConnection.connectionClosed(Error: Server timeout elapsed without receiving a message from the server.) called while in state Connecting.
Utils.ts:194 [2022-02-03T18:40:59.997Z] Debug: Hub handshake failed with error 'Error: Server timeout elapsed without receiving a message from the server.' during start(). Stopping HubConnection.
Utils.ts:194 [2022-02-03T18:40:59.997Z] Debug: Call to HttpConnection.stop(Error: Server timeout elapsed without receiving a message from the server.) ignored because the connection is already in the disconnected state.
Utils.ts:194 [2022-02-03T18:40:59.997Z] Debug: HubConnection failed to start successfully because of error 'Error: Server timeout elapsed without receiving a message from the server.'.
here is sample code from js client application:
console.log("hub attached");
const hubConnection = new HubConnectionBuilder()
.withUrl(OrderHubUrl, {
transport: HttpTransportType.ServerSentEvents,
accessTokenFactory: () => user.accessToken ?? "",
})
.withAutomaticReconnect()
.configureLogging(LogLevel.Trace)
.build();
this.dispatcher.state.saveState("hubConnection", hubConnection);
const startConnection = async () => {
try {
await hubConnection.start();
console.log("connected");
} catch (e) {
this.dispatcher.dispatch(Event.ShowModal, {
actionName: "OK",
header: "Error",
content: e,
});
}
};
hubConnection.on("ReceiveMessage", (user, message) => {
console.log("message received");
console.log(user);
console.log(message);
});
hubConnection.onreconnecting((e) => console.log("reconnecting", e));
hubConnection.onreconnected((e) => console.log("reconnected", e));
startConnection();
}
as you can see from logs on the top, signalR js client is not able to pass through start method. Instead after some time it throws an error message.
below is my Program.cs file, where i configured signalR (i am suspecting that maybe something wrong is here)
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using OrderMaker.Authentication.Helpers;
using OrderMaker.Authentication.Services;
using OrderMaker.Entities;
using OrderMaker.Modules.Common.Services;
using OrderMaker.Modules.Order.Hubs;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddSignalR(c =>
{
c.EnableDetailedErrors = true;
c.ClientTimeoutInterval = TimeSpan.MaxValue;
c.KeepAliveInterval = TimeSpan.MaxValue;
});
ConfigureConfiguration(builder.Configuration);
ConfigureServices(builder.Services, builder.Configuration);
var app = builder.Build();
app.UseAuthentication();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCors(
x => x
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
app.MapFallbackToFile("index.html");
app.MapHub<OrderHub>("/hubs/order");
app.Run();
void ConfigureConfiguration(ConfigurationManager configuration)
{
using var client = new OrderMakerContext();
client.Database.EnsureCreated();
}
void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
services.AddControllers();
// configure strongly typed settings objects
var appSettingsSection = configuration.GetSection("AppSettings");
services.Configure<AppSettings>(appSettingsSection);
// configure jwt authentication
var appSettings = appSettingsSection.Get<AppSettings>();
var key = Encoding.ASCII.GetBytes(appSettings.Secret);
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
x.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
context.Token = context.Request.Cookies["order_maker_token"];
// If the request is for our hub...
var path = context.HttpContext.Request.Path;
var accessToken = context.Request.Query["access_token"];
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments("/hubs/")))
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
}
);
services.AddScoped<IUserService, UserService>();
services.AddSingleton<ILogService, LogService>();
}
definition of my hub:
public class OrderHub : Hub
{
public async Task SendNotification(List<OrderModel> message)
{
await Clients.All.SendAsync("ReceiveMessage", message);
}
public override async Task OnConnectedAsync()
{
await base.OnConnectedAsync();
Console.WriteLine("connected!! " + Context.ConnectionId);
}
public override async Task OnDisconnectedAsync(Exception exception)
{
await base.OnDisconnectedAsync(exception);
Console.WriteLine("disconnected!!");
}
}
And sample controller from where i want to send message to clients:
private readonly IHubContext<OrderHub> _orderHubContext;
public OrdersController(IHubContext<OrderHub> orderHubContext)
{
_orderHubContext = orderHubContext;
}
[Route("api/[controller]/")]
[HttpPost]
//[Authorize(Roles = $"{Role.Admin},{Role.Manager},{Role.Employee}")]
public async Task<IActionResult> Post([FromBody] List<OrderModel> model)
{
/// some code for creating entities etc
db.SaveChanges();
/// notification to all clients that something new was added
Console.Write(_orderHubContext.Clients.All.ToString());
await _orderHubContext.Clients.All.SendAsync("ReceiveMessage", "hi there");
}
return Ok(new MessageResponse("Order added succesfully."));
}
basically i am lost, i spend two days already figuring out what may cause the issues, but i just can't make this thing working. I would really appreciate any sugestions or help. I tried to disable firewall, use different browser, etc without any success. Connection to hub is being made, c# app see that new connection but js client simply stuck in start() method for a while and throws an error message 'Error: Server timeout elapsed without receiving a message from the server.'.
Update: when i explicitly set type of transport in js clint to LongPolling hub is working as intended, but this is not ideal solution.
--- Update ---
All of those issues are happening only on local machine. I tried to check my luck and deploy to production app with transport fixed to SSE and it works without any issuess, as well as WebSocket transport. The only clue i have is that on localhost app is using kestrel and on server when i am hosting my app is using IIS.
You could try to set the serverTimeout and KeepAliveInterval in your program.cs:
builder.Services.AddSignalR(c =>
{
c.EnableDetailedErrors = true;
c.ClientTimeoutInterval = TimeSpan.FromSeconds(30);
c.KeepAliveInterval = TimeSpan.FromSeconds(15);
});
Reason:
If the server hasn't sent a message within this interval, a ping message is sent automatically to keep the connection open. When changing KeepAliveInterval, change the ServerTimeout or serverTimeoutInMilliseconds setting on the client. The recommended ServerTimeout or serverTimeoutInMilliseconds value is double the KeepAliveInterval value.
So,you'd better not set the value of KeepAliveInterval as max value.
This is related to the SPA Proxy that .NET is setting up.
The message I get is as follows:
Utils.ts:193
[2022-09-29T17:09:19.866Z] Error: Failed to complete negotiation with the server: Error: <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Cannot POST /hub/negotiate</pre>
</body>
</html>
: Status code '404' Either this is not a SignalR endpoint or there is a proxy blocking the connection.
What happens, likely, is that the proxy routes traffic from your spa to the port specified here in your csproj file
eg.
https://localhost:44423
You should not initiate a websocket to this port this is -not- proxied.
In my case, the dotnet served port is in reality https://localhost:7088 (properties/launchsettings.json)
So modify your Cors settings... like below, if the spaproxy e.g. is at 44423 and the .net service port at 7088.
services.AddCors(options =>
{
options.AddPolicy("ClientPermission", policy =>
{
policy.AllowAnyHeader()
.AllowAnyMethod()
.WithOrigins("https://localhost:44423", "https://localhost:7088")
.AllowCredentials();
});
});
Next is your js code looks like this e.g.:
const newConnection = new HubConnectionBuilder()
.withUrl('https://localhost:7088/hub/myhub')
.withAutomaticReconnect()
.build();
For me this solved it.

Bind gRPC services to specific port in aspnetcore

Using aspnetcore 3.1 and the Grpc.AspNetCore nuget package, I have managed to get gRPC services running successfully alongside standard asp.net controllers as described in this tutorial.
However I would like to bind the gRPC services to a specific port (e.g. 5001), preferably through configuration instead of code if possible. This is because I would like to limit how my gRPC services are exposed.
The closest I have come has been using RequireHost when mapping the endpoints:
// Startup.cs
public void Configure(IApplicationBuilder app)
{
// ...
app.useEndpoints(endpoints =>
{
endpoints.MapGrpcService<MyService>()
.RequireHost("0.0.0.0:5001");
});
}
This seems to do what I want but I can't find any documentation about it, and it requires configuration in code per service. Perhaps there is a better way?
This works (server side) with Kestrel:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureKestrel(options =>
{
options.Listen(IPAddress.Loopback, 5000);
options.Listen(IPAddress.Loopback, 5005, configure => configure.UseHttps());
});
webBuilder.UseStartup<Startup>();
});
client side:
var httpHandler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback =
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
};
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
using var channel = GrpcChannel.ForAddress("https://localhost:5005", new GrpcChannelOptions { HttpHandler = httpHandler } );
var client = new Greeter.GreeterClient(channel);
Note:
var httpHandler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback =
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
};
when you have a self-signed certificate without a trust chain (mostly when developing).
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
Is for support of http.
You need to configure the middleware
app.UseRouting();
app.MapWhen(context => {
return context.Connection.LocalPort == 1000
}, newApp => {
newApp.UseRouting();
newApp.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<Service1>();
}
});
app.MapWhen(context => {
return context.Connection.LocalPort == 2000
}, newApp => {
newApp.UseRouting();
newApp.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<Service2>();
}
});
In the ASP.NET Core 6.0 ports can be changed in the Properties > launchSettings.json file. But this file is considered only if you run the server from the Visual Studio or VS Code.
I was trying to run the server directly using the .exe file for testing. The server was running with the default ports: "http://localhost:5000;https://localhost:5001".
Finally, I changed it from the appsettings.json for the .exe file:
"AllowedHosts": "*",
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "https://localhost:7005",
"Protocols": "Http1AndHttp2"
},
"gRPC": {
"Url": "http://localhost:5005",
"Protocols": "Http2"
}
}
As far as I know, there is no other way to set a specific port for the GRPC service.
The grpc service is also running on the asp.net core kestrel server, the server will listen the port not the service.
If your asp.net core application just has GRPC service, you could just set the kestrel server's listen port to 5001.
If you have multiple service like MVC web api or else, RequireHost is the best workaround to allow only specific port access the grpc service.
If you want to prompt the routing system for GRPC service to require the specified port, you could use below port:
routes.MapGrpcService<MyService>().RequireHost("*:5001");
You can try to use the UseWhen method to use the MapGrpcService endpoints only when the request uses the port you defined.
var grpcPort = 5001;
app.UseWhen(context => context.Connection.LocalPort == grpcPort,
builder =>
{
builder.UseRouting();
builder.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<MyService>();
});
});
This has the benefit of not repeating .RequireHost("*:5001"); for every single service, although repeating UseRouting twice may induce weird behaviour: for instance, authentication may not be working unless you put in builder.UseAuthentication() after builder.UseRouting().
However, this behaviour be useful if you want to have a distinct request pipeline for REST and gRPC.

ASP.NET Core WebSockets

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);
}
}

Categories

Resources