ASP.NET Core 6 MVC app custom ExceptionFilter does not catch all exceptions - asp.net-core

I have web app with custom exception filter.
public class CustomExceptionFilter : ExceptionFilterAttribute
{
public override void OnException(ExceptionContext context)
{
// do stuff to log exception
}
}
Exception filter is added to filters inside startup class.
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options =>
{
// ...
options.Filters.Add(new CustomExceptionFilter());
// ...
});
}
}
This custom filter catches almost all non-handled exceptions besides this one.
Microsoft.AspNetCore.Mvc.ViewFeatures.CookieTempDataProvider
The temp data cookie .AspNetCore.Mvc.CookieTempDataProvider could not be loaded.
System.IndexOutOfRangeException: Index was outside the bounds of the array.
at System.Text.Json.JsonHelpers.TryParseDateTimeOffset(ReadOnlySpan`1 source, DateTimeParseData& parseData)
at System.Text.Json.JsonHelpers.TryParseAsISO(ReadOnlySpan`1 source, DateTime& value)
at System.Text.Json.JsonReaderHelper.TryGetEscapedDateTime(ReadOnlySpan`1 source, DateTime& value)
at System.Text.Json.JsonDocument.TryGetValue(Int32 index, DateTime& value)
at System.Text.Json.JsonElement.TryGetDateTime(DateTime& value)
at Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure.DefaultTempDataSerializer.DeserializeDictionary(JsonElement rootElement)
at Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure.DefaultTempDataSerializer.Deserialize(Byte[] value)
at Microsoft.AspNetCore.Mvc.ViewFeatures.CookieTempDataProvider.LoadTempData(HttpContext context)
I'm using TempData to preserve some data between posts and redirects. I've looked at all calls where TempData is used but cannot find the place where this error could show up. This particular error is spat out using Serilog.
My question is why the custom exception filter does not catch this IndexOutOfRangeException? Is there a way to catch them or configure Serilog to be more specific? I would like trace where it comes from to get rid of it.
Follow up
Found similar bug that is described in aspnet core git issues. But my problem is not with some format of string. I get out of range exception even if I check TempData count or Keys.
public static bool HasValue(this ITempDataDictionary tempData, string key)
{
try
{
if (tempData == null)
return false;
// if no tempData is set, it enters here, generates no
// exception, but spits out warning through Serilog.
if (tempData.ContainsKey(key) == false)
return false;
if (tempData.Count == 0)
return false;
return tempData.Peek(key) != null;
}
catch (Exception ex)
{
// ...
}
return false;
}
Temp solution so I can sleep at night
Adding logging override to serilog configuration.
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Override("Microsoft.AspNetCore.Mvc.ViewFeatures.CookieTempDataProvider", LogEventLevel.Error);

Related

Core 7 - Api error handling, model state validation + UseExceptionhandler

I am currently working on implementing some Apis using swagger/swashbuckle in net core 7 and implementing some error handling, I've gone down the route of using an exception handler. With separate endpoints from dev/prod.
E.g. Startup.cs
if (env.IsDevelopment())
{
...details ommited
app.UseExceptionHandler("/dev-error");
}
else
{
...details ommited
app.UseExceptionHandler("/error");
}
ErrorController.cs
[AllowAnonymous]
[ApiExplorerSettings(IgnoreApi = true)]
public class ErrorController : Controller
{
private ILogger _logger;
public ErrorController(ILogger logger)
{
_logger = logger;
}
[Route("dev-error")]
public IAttempt DevError()
{
var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
var exception = context.Error;
return Attempt.Fail(exception);
}
[Route("error")]
public IAttempt Error()
{
var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
var exception = context.Error;
_logger.Log(LogLevel.Error, exception, exception.Message);
switch (exception)
{
case UnauthorizedAccessException:
Response.StatusCode = (int) HttpStatusCode.Unauthorized;
return Attempt.Fail("Unauthorised");
default:
Response.StatusCode = (int) HttpStatusCode.InternalServerError;
return Attempt.Fail("Generic Error");
}
}
}
The idea is that all responses are of IAttempt, so that the FE user can check if its succeeded etc. and whether to handle the result or exception in a user friendly way.
This has been working great up until now when I've been implementing Api's that require the model to be validated. I wanted to amend the IAttempt class to provide modelstate feedback, however I have tried many approaches and cant seem to get modelstate validation flow through the exception handler.
I wanted to implement a custom ValidationException that contains the errors which is then handled in these controllers. But when an exception is thrown in either an IActionFilter or when overriding the InvalidModelStateResponseFactory the exception isn't caught by the exception handler.
Is there a work around? Am I missing something?
Alternatively I could define a InvalidModelStateResponseFactory that returns a similar model(IAttempt), but it would be nice for Failed requests to be handled in one place.
Cheers in advance
I think you can make the InvalidModelStateResponseFactory redirect to the ErrorController, sending the required data to create your response
According to your description, I suggest you could consider using the customer action filter to achieve your requirement.
Inside the custom action filter, we could get the model state's results, then you could throw the exception inside it.
More details, you could refer to below codes:
1.Create the custom action filter:
public class CustomValidationActionFilter : IActionFilter
{
public void OnActionExecuted(ActionExecutedContext context)
{
if (!context.ModelState.IsValid)
{
var errorList = context.ModelState.Values
.SelectMany(m => m.Errors)
.Select(m => m.ErrorMessage)
.ToList();
throw new Exception();
}
}
public void OnActionExecuting(ActionExecutingContext context) { }
}
2.Inside the program.cs
builder.Services.AddControllersWithViews(options =>
{
options.Filters.Add(new CustomValidationActionFilter());
});
Then if it thrown the exception, it will go to the controller's error action method, since you set the global exception handler.
I was unnecessarily over complicating things so I have dropped what I attempted to do as in theory responses should be handled accordingly to their response status code rather then the object thats passed in.

Set up logging with Blazor WebAssembly

I'm doing some experiments with Blazor and want to set up logging. I see that Blazor logs to Microsoft.Extensions.Logging out of the box and that the log messages go to the developer console inside the browser. That is a nice start.
Now I want to try and log messages to other destinations as well. It could be a cloud-service. I'm wondering where to set that up. In ASP.NET Core, you would set it up using the ConfigureLogging method in Program.cs. But this isn't available with Blazor:
public static IWebAssemblyHostBuilder CreateHostBuilder(string[] args) =>
BlazorWebAssemblyHost.CreateDefaultBuilder()
.UseBlazorStartup<Startup>()
.ConfigureLogging(...); // <- compile error
As a fallback, I'm trying to set it up through ConfigureServices in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddLogging(builder => builder
.AddMyLogger()
.SetMinimumLevel(LogLevel.Information));
}
with AddMyLogger:
public static ILoggingBuilder AddMyLogger(this ILoggingBuilder builder)
{
builder.Services.AddSingleton<ILoggerProvider, MyLoggerProvider>();
return builder;
}
and MyLoggerProvider:
public class MyLoggerProvider : ILoggerProvider
{
public ILogger CreateLogger(string categoryName)
{
return new MyLogger();
}
public void Dispose()
{
}
}
and MyLogger:
public class MyLogger : ILogger
{
public MyLogger()
{
}
public IDisposable BeginScope<TState>(TState state)
{
return null;
}
public bool IsEnabled(LogLevel logLevel)
{
return true;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
}
}
The AddMyLogger-method is called but my logger is never created or receives any Log-calls.
Am I doing something wrong here or is logging with Blazor WebAssembly simply not ready yet?
I was trying something similar. In my case, the Log method in MyLogger gets called; however it fails at following line of code
using (var streamWriter = new StreamWriter(fullFilePath, true)) //Fails here
{
streamWriter.WriteLine(logRecord);
}
When I put it in try catch block, I got the exception "Children could not be evaluated".
While researching I came across following link. Steve Sanderson's response might make sense of the behavior
Reading local files #16156
BTW It's been a long time, please let me know the solution you came up with.

Asp Net Core SQL Custom Configuration Provider with Dapper and Error Handling

I am setting up my MVC web application to pull configuration data from my SQL Azure database on startup. I have used these two articles (Microsoft, Medium) to guide me but neither include error handling and I want to avoid any Entity Framework references as i'm using Dapper. So far I've got it working with below code but I'm not sure how to handle errors in this scenario. For instance if I remove the try/catch from the Load method in SQLConfigurationProvider then the app crashes on startup but if I include the try/catch then the error is handled and the app starts normally but no config data is available so will eventually break when trying to access a config value. What is the best way to handle these errors gracefully (ie app still loads but displays an error page/message instead)? Also is there any benefit to having the SQLConfigurationSource or would it make more sense just to create the new SqlConnection instance inside SQLConfigurationProvider instead?
Program.cs
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.CaptureStartupErrors(true)
.UseSetting(WebHostDefaults.DetailedErrorsKey, "true")
.UseApplicationInsights()
.ConfigureAppConfiguration((hostingContext, config) =>
{
config.AddSQLConfiguration(); // Custom configuration here
})
.UseStartup<Startup>();
}
ConfigurationExtensions.cs
public static class ConfigurationExtensions
{
public static IConfigurationBuilder AddSQLConfiguration(this IConfigurationBuilder builder)
{
var connectionString = builder.Build().GetConnectionString("DefaultConnection");
return builder.Add(new SQLConfigurationSource(connectionString));
}
}
SQLConfigurationSource.cs
public class SQLConfigurationSource : IConfigurationSource
{
private readonly SqlConnection _connection;
public SQLConfigurationSource(string connectionString)
{
_connection = new SqlConnection(connectionString);
}
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new SQLConfigurationProvider(_connection);
}
}
SQLConfigurationProvider.cs
public class SQLConfigurationProvider : ConfigurationProvider
{
private readonly SqlConnection _connection;
public SQLConfigurationProvider(SqlConnection connection)
{
_connection = connection;
}
public override void Load()
{
try
{
var model = _connection.Query<SQLConfigurationModel>("sp does not exist for example", commandType: CommandType.StoredProcedure);
Data = model.ToDictionary(x => x.Property, x => x.Value);
}
catch (Exception ex)
{
// WHAT TO DO HERE?
}
}
}
public class SQLConfigurationModel
{
public string Property { get; set; }
public string Value { get; set; }
}
---- UPDATE: CLOSE BUT NOT QUITE THERE ----
I added the exception as a configuration value which I then check for in the Configure method of Startup.cs as per below. This helps ensure the app doesn't crash on startup but when I throw the exception it is not getting routed to the Error view even though the exception handler has already been configured with app.UseExceptionHandler("/Home/Error")
// Inside SQLConfigurationProvider
public override void Load()
{
try
{
var model = _connection.Query<SQLConfigurationModel>("sp does not exist for example", commandType: CommandType.StoredProcedure);
Data = model.ToDictionary(x => x.Property, x => x.Value);
}
catch (Exception ex)
{
Data.Add("ConfigurationLoadException", ex.Message);
}
}
// Inside Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseExceptionHandler("/Home/Error");
// Check for custom config exception
string configurationLoadException = Configuration["ConfigurationLoadException"];
if (configurationLoadException.Length > 0)
{
throw new Exception("Configuration Failed: " + configurationLoadException);
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
If your application can't work without the configurations stored in SQL, you should move this code to fetch data to have better error management. That way you will be able to show a proper error message to user and log it better. Other option is use try/catch block in program.cs, and the assumption is that the not having the SQL driven configuration, will not break the startup project but further in the application usage. If that's the case, you will already have error management placed in startup and it can show you a functional error page for this.
This link will give you some views about startup/program.cs error handling
You should configure a custom error handling page Please read following. it's easy to do
Custom Error Page .net Core

Wrapping results of ASP.NET Core WebAPI methods using IResultFilter

I have implemented a result filter like this:
public class ResultWrapperFilter : IResultFilter
{
public void OnResultExecuting(ResultExecutingContext context)
{
if (!(context.ActionDescriptor is ControllerActionDescriptor))
{
return;
}
var objectResult = context.Result as ObjectResult;
if (objectResult == null)
{
return;
}
if (!(objectResult.Value is WrappedResponseBase))
{
objectResult.Value = new WrappedResponse(objectResult.Value);
}
}
public void OnResultExecuted(ResultExecutedContext context)
{
}
}
The filter is used by configuring MvcOptions through ConfigureServices(IServiceCollection services) like this:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<MvcOptions>(
options => { options.Filters.AddService<ResultWrapperFilter>(); });
services.AddMvc();
// ... the rest is omitted for readability
}
The problem I'm experiencing is this filter is causing InvalidCastException: Unable to cast object of type 'WrappedResponse' to type 'System.String' (the method in question has string as the return value type).
Am I even allowed to do this using IResultFilter?
NOTE: I am aware of the possibility of using middleware to accomplish the response wrapping. I don't want to use the middleware to accomplish this because the middleware doesn't have access to context.Result as ObjectResult. Deserializing from the response stream, wrapping and serializing again seems so unnecessary.
An answer just came to me.
When setting objectResult.Value, objectResult.DeclaredType also needs to be set.
So in this case:
if (!(objectResult.Value is WrappedResponseBase))
{
objectResult.Value = new WrappedResponse(objectResult.Value);
objectResult.DeclaredType = typeof(WrappedResponse);
}

How to setup Active Record Session in WCF

I'm trying to setup a session in Active Record for each WCF request. Here is the code:
[WebGet(UriTemplate = "ReadMessages?authUserId={authUserId}&authGuid={authGuid}&userId={userId}&lastMessageId={lastMessageId}&startDate={startDate}&endDate={endDate}")]
public IQueryable<Message> ReadMessages(int authUserId, string authGuid, uint userId, uint lastMessageId,
DateTime startDate, DateTime endDate)
{
UserUtility.Authenticate(authUserId, authGuid);
using (new SessionScope())
{
//get messages.
return from m in MessageData.FindAll(userId, lastMessageId, startDate, endDate)
select ConvertToView(m);
}
}
Even though I have the SessionScope using block, it still gives me a lazy load error because it's returning an IQueryable and so it's converting to a view, which triggers lazy loading, after it's out of the using block I'm guessing. Here is the error:
Initializing[xxx.Business.Schemas.CommonSchemas.Models.Messaging.Message#6575]-failed to lazily initialize a collection of role: xxx.Business.Schemas.CommonSchemas.Models.Messaging.Message.MessageStatusHistories, no session or session was closed
In my configuration, I have IsRunningWebApp as true.
var source = new InPlaceConfigurationSource();
source.IsRunningInWebApp = true;
If you're wondering why I'm returning IQueryable from a WCF web method, it's because I'm using the WCF Web API (http://wcf.codeplex.com/wikipage?title=WCF%20HTTP), which allows you to query your objects in the url querystring using ODATA.
What am I doing wrong? How do I create a session that lives long enough to lazy load the models as I convert them to views on return from the web methods?
I ended up getting it working using this in my Global.asax:
public void Application_BeginRequest(object sender, EventArgs e)
{
HttpContext.Current.Items.Add("ar.sessionscope", new SessionScope());
}
public void Application_EndRequest(object sender, EventArgs e)
{
try
{
var scope = HttpContext.Current.Items["ar.sessionscope"] as SessionScope;
if (scope != null)
scope.Dispose();
}
catch (Exception ex)
{
HttpContext.Current.Trace.Warn("Error", "EndRequest: " + ex.Message, ex);
}
}
Note: I also had to remove the using(new SessionScope()) from the web method, that will interfere with the solution above.