Processing unhandled exceptions in ASP.NET Core 3.1 - asp.net-core

In ASP.NET Core 3.1 a feature was added where unhandled exceptions can be passed onto an instance of ILogger as noted here:
Logging in .NET Core and ASP.NET Core
I have a Server side Blazor website where I want to be able to process these exceptions in a function where I can log them to a database or perhaps send an email. However I am unable to come up with code to do this based on the provided documentation. Could someone provide sample code to trap unhandled exceptions?

I recommand to use Serilog with sinks you need.
In your project add packages :
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Settings.Configuration
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.Debug
dotnet add package Serilog.Sinks.Seq
Serilog.AspNetCore to integrate Serilog with ASP.Net core.
Serilog.Settings.Configuration to read serilog config from .Net core configuration.
Serilog.Sinks.Console to write logs in the console.
Serilog.Sinks.Debug to write logs in Visual Studio output pane.
Serilog.Sinks.Seq to write logs in a Seq server, which is much more powerful than a DB.
Setup login in your Program.cs with :
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseSerilog((hostingContext, loggerConfiguration) => loggerConfiguration
.ReadFrom.Configuration(hostingContext.Configuration))
.Build();
And in Startup.cs:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Admin/Error");
}
app.UseSerilogRequestLogging()
And configure your logs in appsettings.json with:
"Serilog": {
"LevelSwitches": {
"$controlSwitch": "Information"
},
"MinimumLevel": {
"ControlledBy": "$controlSwitch"
},
"WriteTo": [
{
"Name": "Seq",
"Args": {
"serverUrl": "http://localhost:5341/",
"controlLevelSwitch": "$controlSwitch",
"apiKey": "{SeqApiKey}"
}
},
{
"Name": "Console"
},
{
"Name": "Debug"
}
],
"Enrich": [
"FromLogContext",
"WithMachineName",
"WithThreadId"
]
}
This tell Serilog to use the log level configure in Seq for your app.
"LevelSwitches": {
"$controlSwitch": "Information"
}
...
"Args": {
"serverUrl": "http://localhost:5341/",
"controlLevelSwitch": "$controlSwitch",
"apiKey": "{SeqApiKey}"
}
{SeqApiKey} is an Api key configure in your Seq server for your app.
If you want to use a DB, Serilog has long list of sinks you can use.

Doing some playing around, I found a way to do error logging without having to use a third party tool. Here is my code:
public class ExceptionLogger : ILogger
{
public IDisposable BeginScope<TState>(TState state)
{
return null;
}
public bool IsEnabled(LogLevel logLevel)
{
return logLevel == LogLevel.Error;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if (logLevel == LogLevel.Error)
{
while (exception.InnerException != null)
exception = exception.InnerException;
LogException(exception);
}
}
private void LogException(Exception error)
{
...
}
public sealed class ExceptionLoggerProvider : ILoggerProvider
{
public ILogger CreateLogger(string categoryName)
{
return new ExceptionLogger();
}
public void Dispose()
{
}
}
And in Startup.cs add this:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddProvider(new ExceptionLoggerProvider());
...

If you want to do it by yourself, on top of your middleware pipe add:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Admin/Error");
}
app.Use(async (context, next) =>
{
try
{
await next().ConfigureAwait(false);
}
catch(Exception e)
{
LogException(e);
throw;
}
})

Related

WebApplicationFactory doesn't (seem to) send out any data

I am trying to create an integration test for my ASP.NET Core middleware using the WebApplicationFactory type.
My goal is to emulate an ASP.NET Core host, integrate app insights, and actually send data to a real Application Insights instance, to ensure everything is working as expected.
Unfortunately, my test server doesn't send any data to application insights.
This is what my test setup looks like. I have subclassed from WebApplicationFactory as follows:
public class WebTestFixture : WebApplicationFactory<Startup>
{
protected override IHostBuilder CreateHostBuilder()
{
var builder = Host.CreateDefaultBuilder();
builder.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
webBuilder.UseEnvironment("Testing");
});
builder.ConfigureAppConfiguration((context, config) =>
{
config.AddUserSecrets<WebTestFixture>();
});
return builder;
}
}
Then the test Startup class of my test assembly looks as follows:
public class Startup
{
public Startup(IConfiguration configuration) => Configuration = configuration;
public IConfiguration Configuration { get; set; }
public void ConfigureServices(IServiceCollection services)
{
services.AddApplicationInsightsTelemetry();
services.AddAppInsightsHttpBodyLogging();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseAppInsightsHttpBodyLogging();
app.UseEndpoints(endpoints =>
{
endpoints.MapPost("/", async ctx =>
{
ctx.Response.StatusCode = StatusCodes.Status400BadRequest;
await ctx.Response.WriteAsync("Hello from integration test");
});
});
}
}
The instrumentation key is stored in secrets.json.
{
"ApplicationInsights": {
"InstrumentationKey": "***HIDDEN***"
}
}
And finally my test
public class BodyLoggerMiddlewareIntegrationTests : IClassFixture<WebTestFixture>
{
private readonly WebTestFixture _factory;
public BodyLoggerMiddlewareIntegrationTests(WebTestFixture factory) => _factory = factory;
[Fact]
public async void BodyLoggerMiddleware_Should_Send_And_Mask_Data()
{
// Arrange
var client = _factory.CreateClient();
// Act
var requestBody = new
{
Name = "Bommelmaier",
Password = "Abracadabra!",
AccessToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dnZWRJbkFzIjoiYWRtaW4iLCJpYXQiOjE0MjI3Nzk2Mzh9.gzSraSYS8EXBxLN_oWnFSRgCzcmJmMjLiuyu5CSpyHI"
};
var response = await client.PostAsync("/", new JsonContent(requestBody));
// Assert
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
}
}
The test passes and I can see the following line in the debug log (formatted for better readability), which tells me app insights send out my data.
Application Insights Telemetry:
{
"name": "AppRequests",
"time": "2021-12-22T19:13:32.8336126Z",
"iKey": "***HIDDEN***",
"tags": {
"ai.application.ver": "15.0.0.0",
"ai.cloud.roleInstance": "MY_HOST",
"ai.operation.id": "3fe600bbe13fcf41b1e52a0df7f7465e",
"ai.operation.name": "POST /",
"ai.internal.sdkVersion": "aspnet5c:2.18.0+a9fc6af7538cc287d263d9bc216c7910bfc34566",
"ai.internal.nodeName": "MY_HOST"
},
"data": {
"baseType": "RequestData",
"baseData": {
"ver": 2,
"id": "3857ad615d298e4b",
"name": "POST /",
"duration": "00:00:00.2006790",
"success": false,
"responseCode": "400",
"url": "http://localhost/",
"properties": {
"_MS.ProcessedByMetricExtractors": "(Name:'Requests', Ver:'1.1')",
"RequestBody": "{\"Name\":\"Bommelmaier\",\"Password\":\"Abracadabra!\",\"AccessToken\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dnZWRJbkFzIjoiYWRtaW4iLCJpYXQiOjE0MjI3Nzk2Mzh9.gzSraSYS8EXBxLN_oWnFSRgCzcmJmMjLiuyu5CSpyHI\"}",
"AspNetCoreEnvironment": "Production",
"ResponseBody": "Hello from integration test",
"DeveloperMode": "true"
}
}
}
}
However, nothing ever arrives at my App Insights instance, even after having waited more than 30min. Any idea? What am I missing here?
Thank you Andreas Wendl. Posting your suggestion as an answer to help other community members.
You can open Fiddler and have a look if there is outgoing traffic to the hosts mentioned here IP addresses used by Azure Monitor
Also make sure to allow these hosts/IPs in your outgoing network/firewall/proxy/etc. rules.

Injecting DbContext to API controllers makes them return index.html instead of the actual result on published version of NET5 React webapp

I am using a default ReactApp project from visual studio with net5 as framework. This by default comes with a WeatherApiController. Now the problem is, I created a DataContext (DbContext) which I am trying to inject in the ApiController.
[ApiController]
[Route("[controller]")]
public class ActivitiesController : ControllerBase
{
private readonly DataContext _context;
public ActivitiesController(DataContext context)
{
_context = context;
}
/// <summary>
/// Returns an activity by id from the database
/// </summary>
/// <param name="id">Id of activity</param>
/// <returns>Activity</returns>
[HttpGet("{id}")]
public Activity GetActivity(Guid id)
{
return new Activity { Category = "das", City = "city" };
}
[HttpGet]
public IEnumerable<Activity> Get()
{
return _context.Activities.ToArray();
}
}
now when I run this on localhost, everything is working just fine (also the API controller with the DataContext injected), but when I publish it to my domain, any endpoint coming from a controller which has the DataContext injected returns a html file instead of actually working.
public class Startup
{
private readonly IConfiguration _config;
public Startup(IConfiguration config)
{
_config = config;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
// In production, the React files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/build";
});
services.AddDbContext<DataContext>(options =>
{
options.UseMySql(_config.GetConnectionString("DefaultConnection"), ServerVersion.AutoDetect(_config.GetConnectionString("DefaultConnection")));
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
});
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseReactDevelopmentServer(npmScript: "start");
}
});
}
}
I added the DataContext as db context in Startup.cs.
Edit: I managed to get an exception by addin a try catch block as following:
public async Task<List<Activity>> Handle(Query request, CancellationToken cancellationToken)
{
try
{
return await _context.Activities.ToListAsync();
}
catch (Exception ex)
{
List<Activity> activities = new List<Activity>();
activities.Add(new Activity { Description = ex.Message });
return activities;
}
}
And this is the message I get:
Error constructing handler for request of type
MediatR.IRequestHandler2[Application.Reactivities.Activities.List+Query,System.Collections.Generic.List1[Domain.Reactivities.Activity]].
Register your handlers with the container
What could cause the published version only to behave like this and hit this exception when using the DataContext?
Second Edit: I finally managed to log my innerexception and what is going on here is that in production for some reason the database (Google Cloud Sql Database) is timing out on connect.
My Google Cloud SQL database was not accepting my IP address as a valid IP for a connection, so my DataContext was not initializing.
This resulted in my handlers not being initialised correctly so once I added my IP to the allow list, the issue was resolved.

Dot Net Core Web API Return Response From Endpoint

I'm busy trying to create a very simple Dot Net Core Web API (Dot Net 5) and I've run into a strange issue where I cannot get the endpoint to return a response.
I've tried to use
await context.Response.WriteAsync("Hello World!");
as per the documentation but I'm getting the error
'HttpResponse' does not contain a definition for 'WriteAsync'
This is the full Startup.cs code
public class Startup {
public Startup(IConfiguration configuration) {
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services) { }
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
app.UseRouting();
app.UseEndpoints(endpoints => {
endpoints.MapGet("/test", async context => {
//Console.WriteLine("Exec Test");
await context.Response.WriteAsync("Hello World!");
});
});
}
}
I'm sure there's something I'm missing
Add the dependency for Microsoft.AspNetCore.Http.Abstractions.dll which contains the WriteAsync method you are looking for based on the HttpResponse.
Please refer to the following documentation:
https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponsewritingextensions.writeasync?view=aspnetcore-5.0
https://www.carlrippon.com/asp-net-5-hello-world/

.Net Core 2.x Redirect to Error Page

I have a simple ASP.Net Core 2.0 web application and I have enabled Windows Authentication in project property by enabling Windows Authentication and disabled Anonymous Authentication.
For Authorization at Application/Site level filtering by AD security group, I have the following code in Startup.cs:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAuthentication(IISDefaults.AuthenticationScheme);
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.RequireRole("Application - Administrator")
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseBrowserLink();
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseAuthentication();
//app.UseStatusCodePagesWithRedirects("/Home/Error/{0}");
//app.UseStatusCodePagesWithReExecute("/Home/Error", "?statusCode={0}");
app.UseStatusCodePagesWithReExecute("/Home/Error/{0}");
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
To handle Http 403 status code when a un-authorized user tries to access the application, it will be redirect to a custom error page. So I tried the following 3 approaches in Configure method within Startup.cs:
app.UseStatusCodePagesWithRedirects("/Home/Error/{0}");
app.UseStatusCodePagesWithReExecute("/Home/Error", "?statusCode={0}");
app.UseStatusCodePagesWithReExecute("/Home/Error/{0}");
In the HomeController, I tried both the default Error method
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
and customized one to handle specific status code:
public IActionResult Error(string errCode)
{
if (errCode == "500" || errCode == "404" || errCode == "403")
{
return View($"~/Views/Error/{errCode}.cshtml");
}
return View("~/Views/Shared/Error.cshtml");
}
And I have a simple error page 403.cshtml under /Views/Error/ folder.
But none of them works, all display this page:
I am wondering if something I missed or forgot to implement for display a formatted error page?
Thanks in advance.
I am not 100% sure but there should be 2 variations of windows authentications:
The host only allows authenticated users
When you enable Windows Authentication and disable Anonymous Users
[Authorize] and [AllowAnonymous] no effect, because unauthenticated requests never reach your application
Hence you can't / don't need to set up global filter. You might have to setup the friendly error pages on the server?
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/home/error");
}
app.UseStaticFiles();
app.UseMvcWithDefaultRoutes();
}
}
The host allows both anonymous and authenticated users
When you enable both Windows Authentication and Anonymous Users
[Authorize] requires additional setup on Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(IISDefaults.AuthenticationScheme);
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireRole("Application - Administrator")
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
...
app.UseStatusCodePagesWithReExecute("/error", "?code={0}");
app.UseAuthentication();
app.UseMvcWithDefaultRoutes();
}
You need to use [AllowAnonymous] on the error controller to override the [Authorize] global filter to allow anonymous requests.
[AllowAnonymous]
public class ErrorController : Controller
{
public IActionResult Index(int? code)
{
...
}
}
The problem is at the Error method, it missed the AllowAnonymous attribute to allow anonymous access to the error method when the user failed authorization.
Credit to #Calc
Try app.UseStatusCodePagesWithRedirects("/Home/Error/{0}"); in startup.configure(IApplicationBuilder app, IHostingEnvironment env)
Works on ASP.NET CORE2.0 web app
It will handle any kind of HTTP error

Getting SignalR IConnectionManager GetHubContext working in Startup.cs in aspnet core

I can't seem to the following code working. All I'm doing is in ConfigureServies calling _serviceProvider.GetService<IConnectionManager>(); and saving it in a static field and trying to use it later to get access to a IConnectionManager and subsequently call GetHubContext<MyHub> so I can broadcast messages to all connected clients.
_connectionManager.GetHubContext<MyHub>().Clients.All.doSomethingOnClients();
Just as a test, the same line of code inside a webapi controller action method works fine! (with IConnectionManager injected via constructor). That makes me believe my signalr set up is just fine, just how I got things in the startup class is wrong somewhere. GetHashCode on the IConnectionManager in startup and my controller gives different hash codes. I just need to hook things up on the ApplicationLifetime OnStartUp ...
Can you help me understand where things are going wrong please?
public class Startup
{
public static IServiceProvider _serviceProvider;
public static IConnectionManager _connectionManager;
private readonly IHostingEnvironment _hostingEnv;
public IConfigurationRoot Configuration { get; }
public Startup (IHostingEnvironment env)
{
// ...
}
public void ConfigureServices (IServiceCollection services)
{
// ....
services.AddSignalR(options => {
options.Hubs.EnableDetailedErrors = true;
});
services.AddMvc();
// ...
_serviceProvider = services.BuildServiceProvider();
_connectionManager = _serviceProvider.GetService<IConnectionManager>();
}
public void Configure (IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IApplicationLifetime applicationLifetime)
{
// ...
applicationLifetime.ApplicationStarted.Register(OnStartUp);
// ...
app.UseMvc(routes => {
routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});
app.UseSignalR();
// ...
}
public void OnStartUp ()
{
var x = _serviceProvider.GetService<MySingletonObject>();
// MySingletonObject has a VersionUpdated event handler
x.VersionUpdated += OnUpdate;
}
private void OnUpdate (object sender, EventArgs e)
{
// I get here everytime my singleton gets updated fine!
// but the following does not work
_connectionManager.GetHubContext<MyHub>().Clients.All.doSomethingOnClients();
}
}
I am using "Microsoft.AspNetCore.SignalR.Server/0.2.0-alpha1-22362".
1st thing is to realize this version of SignalR isn't shipping, it's just alpha. The problem you're having is because you're building 2 service providers and they're not talking to each other. You call BuildServiceProvider() instead of injecting the IConnectionManager into your Configure method. You can also clean up a lot of the service locator pattern by injecting dependencies directly into configure and then using them in the callbacks.
public class Startup
{
public IConfigurationRoot Configuration { get; }
public Startup (IHostingEnvironment env)
{
// ...
}
public void ConfigureServices (IServiceCollection services)
{
// ....
services.AddSignalR(options => {
options.Hubs.EnableDetailedErrors = true;
});
services.AddMvc();
// ...
}
public void Configure (IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory,
IApplicationLifetime applicationLifetime,
MySingletonObject obj,
IHubContext<MyHub> context)
{
// ...
applicationLifetime.ApplicationStarted.Register(() => OnStartUp(obj, context));
// ...
app.UseMvc(routes => {
routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});
app.UseSignalR();
// ...
}
public void OnStartUp(MySingletonObject obj, IHubContext<MyHub> context)
{
// MySingletonObject has a VersionUpdated event handler
obj.VersionUpdated += (sender, e) =>
{
context.Clients.All.doSomethingOnClients();
};
}
}
Even cleaner would be a another service that would compose everything so you don't end up with so many arguments in your startup method.