Can't bubble up Json.Net exception to my middleware - c#

I'm working on a kind of web-api. The parameters of my controller requests is automatically deserialized to dtos with Json.Net. I'm trying to bubble up the Json.Net deserializing errors to my ApiMiddleware.
var mvcBuilder = services.AddMvc().AddNewtonsoftJson(options =>
{
options.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Error;
options.SerializerSettings.Error = (sender, args) =>
{
var message = args.ErrorContext.Error.Message;
args.ErrorContext.Handled = false;
throw new ValidationException(message);
};
});
I supposed that the exception will buble up and my middleware will able to catch it and log the url of the request and deserializing error. But Json.Net catches all the inner throws by himself, so the only thing that comes to my middleware is "Current error context error is different to requested error”-exception.
I've explored about a dozen of questions somehow connected with my problem, but there was no solution.
So, does anybody know how to throw the ValidationException outside or make the original deserializing-exception delievered to my middleware? Thanks

After reading the ASP.NET Core source code, it seems that NewtonsoftJsonInputFormatter is designed to suppress JsonExceptions and convert it into model state errors. One workaround to this issue in ASP.NET Core 3.1 is to set MvcNewtonsoftJsonOptions.AllowInputFormatterExceptionMessages to false. This causes JsonExceptions to be captured on the model state, which you can then examine in an action filter and throw your own exceptions if you encounter one:
In Startup.cs:
services.AddControllers(c => { c.Filters.Add(new JsonErrorActionFilter()); })
.AddNewtonsoftJson(c => { c.AllowInputFormatterExceptionMessages = false; });
And the action filter could be something like this:
public class JsonErrorActionFilter : IActionFilter, IOrderedFilter
{
public void OnActionExecuted(ActionExecutedContext context) { }
public void OnActionExecuting(ActionExecutingContext context)
{
if (context.Result == null && !context.ModelState.IsValid
&& HasJsonErrors(context.ModelState, out var jsonException))
{
throw new ValidationException(jsonException.Message);
}
}
private static bool HasJsonErrors(ModelStateDictionary modelState, out Exception jsonException)
{
foreach (var entry in modelState.Values)
{
foreach (var error in entry.Errors)
{
if (error.Exception is JsonException)
{
jsonException = error.Exception;
return true;
}
}
}
jsonException = null;
return false;
}
// Set to a large negative value so it runs earlier than ModelStateInvalidFilter
public int Order => -200000;
}
This is likely using MvcNewtonsoftJsonOptions.AllowInputFormatterExceptionMessages in a way that it's not intended to be used, so the solution will probably break if you upgrade to a different version of ASP.NET Core. But for now it may get the job done.

Related

EF Core - API Site: A second operation was started on this context before a previous operation completed

I know there are plenty of other threads covering this topic, but none of the solutions appear to have helped. Hence the creation of this one. Sorry for the lengthy Post!
The problem:
I am running an .Net Core API Website using EF Core. Naturally I am ensuring the context is Transient and created per request. Passing the Context through classes to ensure it's not recreated or executing two queries at the same time. I can debug and follow my code through without error. But once in production environment, I still run into this problem, and I've exhausted all of the top google result solutions...
The actual error:
A second operation was started on this context before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
services.AddDbContext<APIContext>((serviceprovider, options) =>
{
options.UseSqlServer(Configuration.GetConnectionString("DatabaseConnection"));
options.UseInternalServiceProvider(serviceprovider);
});
I've tried:
Several different ways of registering my context, using all the different constructors like below. I've even tried creating as a DbContextPool instead with no luck.
services.AddDbContext<APIContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DatabaseConnection")), ServiceLifetime.Transient, ServiceLifetime.Transient);
Every one of my controllers or middleware, have the DBContext passed via the constructor as DI. Despite a few Async calls (all using await). I can not find any area that may cause this error. I've added a code sample below following a recent error, code removed for brevity.
To provide some context, every request enters my middleware for security, it's the catch in here that notifies me of the error, so I'm not sure if that's the cause? Am I registering the Interface /Middleware correctly?
//Register Request logger
services.AddTransient<IRequestLogger, RequestLoggerConcrete>();
//Constructor for Request Logger
public RequestLoggerConcrete(APIContext context)
{
_context = context;
}
//Method Called in Middleware
public async void InsertLoggingData(LoggerEntity loggerTB)
{
try
{
loggerTB.LoggerID = 0;
_context.Loggers.Add(loggerTB);
await _context.SaveChangesAsync();
}
catch (Exception)
{
throw;
}
}
//The Middleware Constructor
public ApiKeyValidatorsMiddleware(RequestDelegate next, IValidateRequest ivalidaterequest, IRequestLogger irequestlogger)
{
_next = next;
_IValidateRequest = ivalidaterequest;
_IRequestLogger = irequestlogger;
}
//Middleware Execution
public async Task Invoke(HttpContext httpContext)
{
try
{
..... Validation - API Code Code Removed ....
_IRequestLogger.InsertLoggingData(loggertb);
await _next.Invoke(httpContext);
}
catch(Exception ex) {
...Notify Email Removed...
}
}
//Controller Init
public SMSController(APIContext db)
{
_db = db;
}
//Sample Method
public ActionResult GetMessages()
{
return Json(SMSMessages.GetMessages(_db));
}
//Class Code
public static SMSMessageSimple GetMessages(APIContext _db)
{
var lst = (from sms in _db.SMSs
where sms.SentResultCode == null
&& sms.SentToDevice == false
select new SMSMessage()
{
message = sms.Message,
recepient = sms.ToNumber,
id = sms.SMSID.ToString()
}).ToArray();
var DistinctGuids = lst.Select(s => Convert.ToInt32(s.id)).Distinct();
DateTime dtNow = System.DateTime.Now.ToGMT();
_db.SMSs.Where(w => DistinctGuids.Contains(w.SMSID)).Update(u => new SMSEntity()
{
SentToDevice = true,
ModifiedBy = "API",
ModifiedDate = dtNow
});
return new SMSMessageSimple(lst);
}
I get this error on methods that don't even make any calls to the Database, yet on some, the log is inserted but the error persists.. Could it be the app pool? If two separate requests are made at the same time?
Any guidance is appreciated!!

.Net Core API how to handle HTTP exceptions

I have created by some example the ErrorController, which is handling Exception. Currently I have this:
[Route("[controller]")]
[ApiController]
[Authorize]
[ApiExplorerSettings(IgnoreApi = true)]
public class ErrorController : ControllerBase
{
public IActionResult ServerError()
{
var feature = HttpContext.Features.Get<IExceptionHandlerFeature>();
ErrorResponse response;
if (feature != null && feature.Error.GetType() == typeof(HttpResponseException))
{
response = new ErrorResponse
{
Error = "Error during processing request",
Message = feature.Error.Message
};
HttpContext.Response.StatusCode = ((HttpResponseException) feature.Error).HttpCode;
}
else
{
response = new ErrorResponse
{
Error = "Unexpected Server Error",
Message = feature?.Error.Message
};
}
return Content(JsonSerializer.Serialize(response), "application/json");
}
}
So whenever I throw in my method in controllers HttpResponseException, It will read it, and create response with corresponding code. But doing it through this, will log the HttpResponseException, which is not desired behaviour.
I have found solutions with Request.CreateResponse(), but that method does not exists, but when I replicated this method by myself, it is not desired behaviour (because of Swashbuckle/Swagger UI - The returning type is not the model object, but the HttpResponseMessage).
Found also something about ExceptionFilterAttribute, where I produced this code:
public class HttpExceptionFilterAttribute : ExceptionFilterAttribute
{
public override void OnException(ExceptionContext context)
{
if (!(context.Exception is HttpResponseException)) return;
var exception = (HttpResponseException) context.Exception;
context.Result = new ObjectResult(""){StatusCode = exception.HttpCode};
}
}
But can't tell where to globally registered (based on this article).
So how to correctly manage returning desired object or some error with code, in a way that it will not be logged as warn?
Filters are registered globally in the Startup.cs via the options of AddMVC or AddControllersWithViews options.
Another way to handle exceptions globally is using an exception handling middleware which would catch also unexpected exceptions (which is my preferred way).

ASP.NET Core Exception middelware nesting exceptions for each layer

I created a global Exception handler middelware to catch all my custom exceptions.
When throwing an Exception in my DAL I expect that the middelware will catch it as the same type that it was thrown.
// API
[HttpGet]
[Route("api/users")]
public IActionResult Get(int id)
{
var user = _userService.GetById(id);
return Ok(user);
}
// Repository
public async Task<List<User>> GetById(int id)
{
throw new EntityNotFoundException("code", "message");
// .. return user
}
// Exception handler
public async Task Invoke(HttpContext httpContext)
{
try
{
await _next(httpContext);
}
catch (Exception ex) // ex is of type JsonSerializationException
{
if (ex is EntityNotFoundException)
{
// Handle exception
}
}
}
In the above example the Exception is handled but is of type JsonSerializationException with an InnerException of type System.AggregateException that contains another InnerException with type EntityNotFoundException.
It seems that the Exception gets nested for each layer it gets passed along (DAL > Service > API). How can I avoid this so that I can catch the Exception as the original type?
The example you provided looks good but it lacks one important thing, which is single responsibility.
ASP.NET Core has a better approach, which is using exception filters, that can be registered globally too and can be written for each custom-exception and even for unhandled exceptions.
Sample:
public class EntityNotFoundExceptionFilter : IExceptionFilter
{
public EntityNotFoundExceptionFilter(// some dependencies that u want to inject)
{
...
}
public void OnException(ExceptionContext context)
{
if (!(context.Exception is EntityNotFoundException))
{
return;
}
context.ExceptionHandled = true;
context.Result = new NotFoundObjectResult // will produce 404 response, you can also set context.HttpContext.Response.StatusCode based on your exceptions statuscode and return an ObjectResult instead
{
context.Exception.Message
}
}
}
Now in your Startup.cs in the ConfigureServices(...) function add the following
public void ConfigureService(IServiceCollection services)
{
...
services.AddMvc(options =>
{
...
options.Filters.Add(typeof(EntityNotFoundExceptionFilter));
...
}
...
}
You will end up writing many filters but it is a cleaner approach and that is how the asp.net-core filterpipeline should be used + this will be working :)
I am not 100% sure why there are so many exceptions in your current implementation but my guess is that asp.net tries to return the exception and then fails to serialize it and stuff like that.
Edit:
I create a minimal example that can be found here. Just access the url via http://localhost:58741/api/some after cloning the project.

updated validation example for web api

I tried the validation example code for web api on the latest mvc 4 download and I'm getting some errors. Does anyone have an updated example of the ValidationActionFilter class.
Here's the original code
public class ValidationActionFilter : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext context)
{
var modelState = context.ModelState;
if (!modelState.IsValid)
{
dynamic errors = new JsonObject();
foreach (var key in modelState.Keys)
{
var state = modelState[key];
if (state.Errors.Any())
{
errors[key] = state.Errors.First().ErrorMessage;
}
}
context.Response = new HttpResponseMessage<JsonValue>(errors, HttpStatusCode.BadRequest);
}
}
}
I'm getting an error on HttpResponseMessage
The non-generic type 'System.Net.Http.HttpResponseMessage' cannot be used with type arguments
Also it looks like I need to add a Json reference, but should I be using JSON.net instead? An example of this using Json.net?
HttpResponseMessage<T> is from version beta, it no longer exists in the version release, use below instead:
actionContext.Response = actionContext.Request
.CreateResponse(HttpStatusCode.BadRequest, errors);

ASP.NET MVC - Approach for global error handling?

I was wondering what the best implementation for a global error (doesn't have to be errors, can also be success messages) handler would be? Let me break it down for you with an example:
User tries to delete a record
Deletion fails and an error is logged
User redirects to another page
Display error message for user (using a HtmlHelper or something, don't want it to be a specific error page)
I'm just curious what you guys think. I've been considering TempData, ViewData and Session but they all have their pros and cons.
TIA!
UPDATE:
I'll show an example what I exactly mean, maybe I wasn't clear enough.
This is an example of a method that adds a message when user deletes a record.
If user succeeds, user redirects to another page
public ActionResult DeleteRecord(Record recordToDelete)
{
// If user succeeds deleting the record
if (_service.DeleteRecord(recordToDelete)
{
// Add success message
MessageHandler.AddMessage(Status.SUCCESS, "A message to user");
// And redirect to list view
return RedirectToAction("RecordsList");
}
else
{
// Else return records details view
return View("RecordDetails", recordToDelete);
}
}
And in the view "RecordsList", it would be kinda cool to show all messages (both error and success messages) in a HtmlHelper or something.
<%= Html.RenderAllMessages %>
This can be achieved in many ways, I'm just curious what you guys would do.
UPDATE 2:
I have created a custom error (message) handler. You can see the code if you scroll down.
Just for fun, I created my own custom error (message) handler that works pretty much as TempData, but with the small difference that this handler is accessible all over the application.
I'm not going to explain every single step of code, but to sum it all up, I used IHttpModule to fire a method for every request and Session to save data. Below is the code, feel free to edit or give suggestions for improvements.
Web.config (Define module)
<httpModules>
<add name="ErrorManagerModule" type="ErrorManagerNamespace.ErrorManager"/>
</httpModules>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true">
<add name="ErrorManagerModule" type="ErrorManagerNamespace.ErrorManager"/>
</modules>
</system.webServer>
ErrorManager.cs (Error manager handler code)
public class ErrorManager : IRequiresSessionState, IHttpModule
{
private const string SessionKey = "ERROR_MANAGER_SESSION_KEY";
public enum Type
{
None,
Warning,
Success,
Error
}
/*
*
* Public methods
*
*/
public void Dispose()
{
}
public void Init(HttpApplication context)
{
context.AcquireRequestState += new EventHandler(Initiliaze);
}
public static IList<ErrorModel> GetErrors(ErrorManager.Type type = Type.None)
{
// Get all errors from session
var errors = GetErrorData();
// Destroy Keep alive
// Decrease all errors request count
foreach (var error in errors.Where(o => type == ErrorManager.Type.None || o.ErrorType == type).ToList())
{
error.KeepAlive = false;
error.IsRead = true;
}
// Save errors to session
SaveErrorData(errors);
//return errors;
return errors.Where(o => type == ErrorManager.Type.None || o.ErrorType == type).ToList();
}
public static void Add(ErrorModel error)
{
// Get all errors from session
var errors = GetErrorData();
var result = errors.Where(o => o.Key.Equals(error.Key, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
// Add error to collection
error.IsRead = false;
// Error with key is already associated
// Remove old error from collection
if (result != null)
errors.Remove(result);
// Add new to collection
// Save errors to session
errors.Add(error);
SaveErrorData(errors);
}
public static void Add(string key, object value, ErrorManager.Type type = Type.None, bool keepAlive = false)
{
// Create new error
Add(new ErrorModel()
{
IsRead = false,
Key = key,
Value = value,
KeepAlive = keepAlive,
ErrorType = type
});
}
public static void Remove(string key)
{
// Get all errors from session
var errors = GetErrorData();
var result = errors.Where(o => o.Key.Equals(key, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
// Error with key is in collection
// Remove old error
if (result != null)
errors.Remove(result);
// Save errors to session
SaveErrorData(errors);
}
public static void Clear()
{
// Clear all errors
HttpContext.Current.Session.Remove(SessionKey);
}
/*
*
* Private methods
*
*/
private void Initiliaze(object o, EventArgs e)
{
// Get context
var context = ((HttpApplication)o).Context;
// If session is ready
if (context.Handler is IRequiresSessionState ||
context.Handler is IReadOnlySessionState)
{
// Load all errors from session
LoadErrorData();
}
}
private static void LoadErrorData()
{
// Get all errors from session
var errors = GetErrorData().Where(o => !o.IsRead).ToList();
// If KeepAlive is set to false
// Mark error as read
foreach (var error in errors)
{
if (error.KeepAlive == false)
error.IsRead = true;
}
// Save errors to session
SaveErrorData(errors);
}
private static void SaveErrorData(IList<ErrorModel> errors)
{
// Make sure to remove any old errors
HttpContext.Current.Session.Remove(SessionKey);
HttpContext.Current.Session.Add(SessionKey, errors);
}
private static IList<ErrorModel> GetErrorData()
{
// Get all errors from session
return HttpContext.Current.Session[SessionKey]
as IList<ErrorModel> ??
new List<ErrorModel>();
}
/*
*
* Model
*
*/
public class ErrorModel
{
public string Key { get; set; }
public object Value { get; set; }
public bool KeepAlive { get; set; }
internal bool IsRead { get; set; }
public Type ErrorType { get; set; }
}
HtmlHelperExtension.cs (An extension method for rendering the errors)
public static class HtmlHelperExtension
{
public static string RenderMessages(this HtmlHelper obj, ErrorManager.Type type = ErrorManager.Type.None, object htmlAttributes = null)
{
var builder = new TagBuilder("ul");
var errors = ErrorManager.GetErrors(type);
// If there are no errors
// Return empty string
if (errors.Count == 0)
return string.Empty;
// Merge html attributes
builder.MergeAttributes(new RouteValueDictionary(htmlAttributes), true);
// Loop all errors
foreach (var error in errors)
{
builder.InnerHtml += String.Format("<li class=\"{0}\"><span>{1}</span></li>",
error.ErrorType.ToString().ToLower(),
error.Value as string);
}
return builder.ToString();
}
}
Usage for creating errors
// This will only be available for one request
ErrorManager.Add("Key", "An error message", ErrorManager.Type.Error);
// This will be available for multiple requests
// When error is read, it will be removed
ErrorManager.Add("Key", "An error message", ErrorManager.Type.Error, true);
// Remove an error
ErrorManager.Remove("AnotherKey");
// Clear all error
ErrorManager.Clear();
Usage for rendering errors
// This will render all errors
<%= Html.RenderMessages() %>
// This will just render all errors with type "Error"
<%= Html.RenderMessages(ErrorManager.Type.Error) %>
I'm confused by these steps:
Deletion fails and an error is logged
User redirects to another page
Why would you redirect the User when an error occurs? That doesnt make any sense, unless im misunderstanding something.
Generally, i follow these guidelines:
Error with form submission (e.g HTTP POST): check ModelState.IsValid and return the same View and render the error out with #Html.ValidationSummary()
Error with AJAX call: return JsonResult (like #Tomas says), and use basic client-side scripting to inspect the JSON and show the result
Error with domain/business: throw custom exceptions and let the controller catch them and add to ModelState as above
I prefer writing my server layer as an API emitting JSON - in ASP.NET MVC that's real simple - you just create a bunch of nested anonymous objects, and return Json(data);. The JSON object is then consumed by the client layer, which consists of html, css and javascript (I use jQuery a lot, but you might prefer other tools).
Since javascript is dynamic, it is then real easy to just have a property status on the data object, and the client side script can interpret that and display status or error messages as needed.
For example, consider the following action method:
public ActionResult ListStuff()
{
var stuff = Repo.GetStuff();
return Json(new { status = "OK", thestuff = stuff });
}
This will return JSON in the following format:
{ "status": "OK", "thestuf": [{ ... }, { ... }] }
where ... is a placeholder for the properties of stuff. Now, if I want error handling, I can just do
try
{
var stuff = Repo.GetStuff();
return Json(new { status = "OK", thestuff = stuff});
}
catch (Exception ex)
{
Log.Error(ex);
return Json(new { status = "Fail", reason = ex.Message });
}
Since javascript is dynamic, it doesn't matter that the two anonymous objects don't have the same properties. Based on the value of status, I'll only look for properties that are actually there.
This can be implemented even better if you create your own action result classes, which extend JsonResult and add the status property automatically. For example, you can create one for failed requests that takes an exception in the constructor and one for successful ones than take an anonymous object.
If all you're going to do is redirect the user to another page, then you can use any ActionMethod to do so and just redirect to it.
If you want a global error, such as a 500 or 403 or some other error, then the MVC 3 default template creates an _Error.cshtml page for you and registers the error handler in the global.asax.
If you want to catch specific errors, then you can register additional handlers in the same place and tell the system which Error page to use for that error.

Categories

Resources