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.
Related
I have a C# / ASP.NET Core Web API project running on https://localhost:7001 and a next js app running on http://localhost:3000.
I can run the C# API from swagger and directly in the browser (https://localhost:7001/api/SourceSystems), but when I try to call it from the next js page using GetStaticProps, I get a 500 error.
Next.js code:
export default function Sourcesystem({systems}) {
return (
<ul>
{systems.map((system) => (
<li key={system.systemName}>{system.systemName} </li>
))}
</ul>
)
};
// This function gets called at build time
export async function getStaticProps() {
// Call an external API endpoint to get posts
const res = await fetch(
'https://localhost:7001/api/Schedules',
{
method:'GET',
}
)
const systems = await res.json()
console.log(systems);
return {
props: {
systems
},
}
}
I have added CORS to the c# code (I think) in
program.cs
var MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy(name: MyAllowSpecificOrigins,
builder =>
{
builder.WithOrigins(
"http://example.com",
"http://www.contoso.com",
"http://localhost:3000"
);
});
});
// services.AddResponseCaching();
builder.Services.AddControllers();
// add dbContext
builder.Services.AddDbContext<GdqcDevContext>(options => options.UseSqlServer("Data Source = RAZERPRO17; Initial Catalog = GDQC_dev; Integrated Security = True; Connect Timeout = 30; Encrypt = False; TrustServerCertificate = False; ApplicationIntent = ReadWrite; MultiSubnetFailover = False"));
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseCors(MyAllowSpecificOrigins);
app.UseAuthorization();
app.MapControllers();
app.Run();
Next is saying the the fetch failed:
I am suspecting something still not set correctly for CORS but I have copied the CORS configuration and the middleware assignment order from documentation and other stackoverflow answers. NOTE: I am running the c# API code using the debug browser rather than directly from IIS. I have read somewhere about the OPTIONS but this is only for the full blown IIS
I have also added a CORS guard annotation to the c# controller with no success
namespace Overwatch_API.Controllers
{
[EnableCors("MyAllowSpecificOrigins")]
[Route("api/[controller]")]
[ApiController]
public class SourceSystemsController : ControllerBase
{
private readonly GdqcDevContext _context;
public SourceSystemsController(GdqcDevContext context)
{
_context = context;
}
// GET: api/SourceSystems
[HttpGet]
public async Task<ActionResult<IEnumerable<SourceSystem>>> GetSourceSystems()
...
UPDATE: It looks like Next is returning the following error message in the logging:
cause: Error: self-signed certificate
at TLSSocket.onConnectSecure (node:_tls_wrap:1538:34)
at TLSSocket.emit (node:events:513:28)
at TLSSocket._finishInit (node:_tls_wrap:952:8)
at ssl.onhandshakedone (node:_tls_wrap:733:12) {
code: 'DEPTH_ZERO_SELF_SIGNED_CERT'
I presume this is related to the SSL cert on the .net core 6 api code, as this is being called with https. How do I get next to accept a self signed cert, or build a propertly signed cert for the dev environment
This issue is common in nextjs.
I found two ways to fixed it.
1. If the issue occurs in dev environment. we can use NODE_TLS_REJECT_UNAUTHORIZED=0 to solve it.
① Overcome the DEPTH_ZERO_SELF_SIGNED_CERT on Node.js
② Self Signed SSL Certificate in NextJS
2. We also can use NODE_EXTRA_CA_CERTS to solve it. It can be used in Dev or Prod Environment.
Node TLS socket : DEPTH_ZERO_SELF_SIGNED_CERT error
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'.
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.
I have a asp.net core web api application that runs great like this:
class Program
{
/// <summary>
/// Main method
/// </summary>
static void Main(string[] args)
{
// pass this as a parameter to specify what database I will like to use
Func<IServiceProvider, IMyDatabase> GetDatabaseFactory = provider =>
{
// used for testing purposes. In production I will use the real DB
return new MyDummyDatabase();
}
// create on a method so that it can be unit tested
WebApplication app = CreateMyAppWebApiApplication(GetDatabaseFactory);
// run application
app.Run();
}
}
And here is the method CreateMyAppWebApiApplication.
/* I removed a lot of stuff I just want to illustrate the idea. Moreover, I have hardocoded a lot of stuff for testing purposes. Once it works I will move it to a configuration file.*/
static WebApplication CreateMyAppWebApiApplication(StartAspDotNetParameters parameters)
{
var builder = WebApplication.CreateBuilder();
builder.WebHost.ConfigureKestrel(k =>
{
var port = 8888;
k.Listen(System.Net.IPAddress.Any, port, listenOptions =>
{
// Enable support for HTTP1 and HTTP2 (required if you want to host gRPC endpoints)
listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
listenOptions.UseHttps();
});
});
#region Add IoC dependencies
// .. code some dependencies I need for the controlers
#endregion
// add controllers
var mvcBuilder = builder.Services.AddControllers();
// serialize enums as string
mvcBuilder.AddJsonOptions(opts =>
opts.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter())
);
// Configure Swagger/OpenAPI. More info: https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
// code to configure...
});
WebApplication? app = builder.Build();
#region Configure the HTTP request pipeline.
// first middleware to intercept swagger.json file
// I have hardocded the path for testing purposes
app.Use(async (HttpContext context, Func<Task> next) =>
{
if (requestUrl.EndsWith("myapp-swagger.json"))
{
var content = File.ReadAllText(#"T:\repos\.....\myapp-swagger.json.json");
context.Response.ContentLength = content.Length;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(content);
return;
}
else
{
// else execute next middleware
await next();
}
});
// enable swagger
app.UseSwagger();
// change swager endpoint
app.UseSwaggerUI(c =>
{
c.RoutePrefix = "documentation";
c.SwaggerEndpoint("/myapp-swagger.json", "MY API");
});
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
// This will run the application
//// execute endpoint
//app.Run();
return app;
#endregion
}
The things important to note about this method are:
// I changed swagger default endpoint
app.UseSwaggerUI(c =>
{
c.RoutePrefix = "documentation";
c.SwaggerEndpoint("/myapp-swagger.json", "MY API");
});
// AND
// first middleware to intercept swagger.json file
// I have hardocded the path for testing purposes
app.Use(async (HttpContext context, Func<Task> next) =>
{
if (requestUrl.EndsWith("myapp-swagger.json"))
{
var content = File.ReadAllText(#"T:\repos\.....\myapp-swagger.json.json");
context.Response.ContentLength = content.Length;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(content);
return;
}
else
{
// else execute next middleware
await next();
}
});
Anyways that code works great.
Now here is the problem:
When I try to run that same code from a Tests project like this:
[Fact]
public async Task TestUserPermissions_IntegrationTest()
{
// pass the same dummyDatabase
WebApplication app = CreateMyAppWebApiApplication(provider =>
{
// used for testing purposes. In production I will use the real DB
return new MyDummyDatabase();
});
loginWorked = false;
var taskLogin = Task.Run(async () =>
{
// make sure app starts by waiting 5 seconds
await Task.Delay(5000);
using var client = new HttpClient();
var json = #"{ 'username':'tono', 'password':'myPassword'}".Replace("'", "\"");
var content = new StringContent(json, Encoding.UTF8, "application/json");
var result = await client.PostAsync("https://localhost:8888/api/LoginController/Login", content);
Console.WriteLine(result.StatusCode);
loginWorked = result.StatusCode == 200;
});
// run application
app.Run();
await taskLogin ;
Assert.True(loginWorked);
}
The app runs but I am not able to consume the API when running in the Test project
Finally found the answer. The controllers where not being found because I was running the project from a different assembly. This solution for stackoverflow made my Test pass:
https://stackoverflow.com/a/59121354/637142
In other words I ended up adding this code
// add controllers
var mvcBuilder = builder.Services.AddControllers();
// add controllers from this assembly. This is needed in case we are calling this method from unit tests project.
mvcBuilder.PartManager.ApplicationParts.Add(new AssemblyPart(typeof(MyCustomController).Assembly));
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);
}
}