I have a simple C# Web Api project that provides a few restful endpoints.
Controller fatal server error handling/logging is generally well described by either using:
Implementing/overriding Application_Error method in Global.asax.cs
protected override void Application_Error(object sender, EventArgs e)
{
var ex = Server.GetLastError();
_logger.Error("Unexpected error while initializing application", ex);
}
Or by adding an exception handler filter:
config.Filters.Add(new ExceptionHandlingAttribute());
OR
GlobalConfiguration.Configuration.Filters.Add(new ExceptionHandlingAttribute());
public class ExceptionHandlingAttribute : ExceptionFilterAttribute
{
private static readonly ILog _logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
public override void OnException(HttpActionExecutedContext actionExecutedContext)
{
_logger.Error("Unexpected error in API.", actionExecutedContext.Exception);
throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError)
{
Content = new StringContent("An error occurred, please try again or contact the administrator."),
ReasonPhrase = "Critical Exception"
});
}
}
However, when an error occurs during instantiation of a controller due to a failure in dependency injection within the constructor of this code:
public class DataController : ApiController
{
private readonly IDataService _dataService;
public DataController(IDataService dataService)
{
_dataService = dataService;
}
[AllowAnonymous]
[HttpGet]
public IHttpActionResult GetSomeStuff()
{
return Ok(new AjaxResponse("somestuff"));
}
none of the above methods catches the error. How can I catch those errors?
This is described very nicely in this blog post. Excerpt to answer question below:
Create a class:
public class GlobalExceptionHandler : ExceptionHandler
{
public async override Task HandleAsync(ExceptionHandlerContext context, CancellationToken cancellationToken)
{
// Access Exception
// var exception = context.Exception;
const string genericErrorMessage = "An unexpected error occured";
var response = context.Request.CreateResponse(HttpStatusCode.InternalServerError,
new
{
Message = genericErrorMessage
});
response.Headers.Add("X-Error", genericErrorMessage);
context.Result = new ResponseMessageResult(response);
}
}
Then Register you exception handler as below from you application startup or owin setup as below:
public static class SetupFiltersExtensions
{
public static IAppBuilder SetupFilters(this IAppBuilder builder, HttpConfiguration config)
{
config.Services.Replace(typeof (IExceptionHandler), new GlobalExceptionHandler());
return builder;
}
}
As stated in his post, he isn't logging in the above method but prefers to do so through a GlobalErrorLogger as such:
public class GlobalErrorLogger : ExceptionLogger
{
public override void Log(ExceptionLoggerContext context)
{
var exception = context.Exception;
// Write your custom logging code here
}
}
Registered as such:
public static class SetupFiltersExtensions
{
public static IAppBuilder SetupFilters(this IAppBuilder builder, HttpConfiguration config)
{
config.Services.Replace(typeof (IExceptionHandler), new GlobalExceptionHandler());
config.Services.Add(typeof(IExceptionLogger), new GlobalErrorLogger());
return builder;
}
}
I finally found the answer myself which is the following.
One has to implement and override the IHttpControllerActivator interface on a GlobalConfiguration.Configuration.Services level (this is important as config.Services only deals with already instantiated controllers).
Here are some snippets:
Startup.cs
// the following will make sure that any errors that happen within the constructor
// of any controller due to dependency injection error will also get logged
GlobalConfiguration.Configuration.Services.Replace(typeof(IHttpControllerActivator),
new ExceptionHandlingControllerActivator(
GlobalConfiguration.Configuration.Services.GetHttpControllerActivator())
);
ExceptionHandlingControllerActivator.cs
/// <summary>
/// This class handles instantiation of every api controller. It handles and logs
/// any exception that occurs during instatiation of each controller, e.g. errors
/// that can happen due to dependency injection.
/// </summary>
public class ExceptionHandlingControllerActivator : IHttpControllerActivator
{
private IHttpControllerActivator _concreteActivator;
private static readonly ILog _logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
public ExceptionHandlingControllerActivator(IHttpControllerActivator concreteActivator)
{
_concreteActivator = concreteActivator;
}
public IHttpController Create(HttpRequestMessage request, HttpControllerDescriptor controllerDescriptor, Type controllerType)
{
try
{
return _concreteActivator.Create(request, controllerDescriptor, controllerType);
}
catch (Exception ex)
{
_logger.Error("Internal server error occured while creating API controller " + controllerDescriptor.ControllerName, ex);
throw new HttpResponseException(request.CreateErrorResponse(HttpStatusCode.InternalServerError, "Unexpected error while creating controller " + controllerDescriptor.ControllerName));
}
}
}
Related
I've created a service that's used throughout my aspnet project that retrieves and validates a header among other things. Issue is that the Exception Filter is not able to catch the errors that are thrown by the service as it's not in the scope of the Exception Filter thus giving the user an ugly internal server error. Is there any way to gracefully return an argument error with description to the user with the use of the services?
The Startup:
services.AddScoped<UserService>();
services
.AddMvc(x =>
{
x.Filters.Add(typeof(Filters.MyExceptionFilter));
})
The Exception Filter:
public class MyExceptionFilter : IExceptionFilter
{
public void OnException(ExceptionContext context)
{
if (context.Exception is ArgumentException argumentException)
{
var response = context.HttpContext.Response;
context.ExceptionHandled = true;
response.StatusCode = 400;
context.Result = new ObjectResult(argumentException.Message);
return;
}
}
}
The Service:
public class UserService
{
public readonly string UserId;
public UserService(IHttpContextAccessor context)
{
if (!context.HttpContext.Request.Headers.TryGetValue("x-test", out var user))
{
throw new ArgumentException($"x-test header is required.");
}
UserId = user;
//do other stuff
}
}
Controller Action Method:
public async Task<IActionResult> Delete(string id,
[FromServices] UserService userService)
{
//do stuff
}
A rule-of-thumb in C# is do as little work in the constructor as possible. There's a few good reasons for this (e.g. you can't dispose a class that threw an exception in it's constructor). Another good reason is, as you have found out, construction might happen in a different place (e.g. a DI container) than the place you actually use the class.
The fix should be quite straightforward - just move the logic out of the constructor. You could use a Lazy<T> to do this for example:
public class UserService
{
public readonly Lazy<string> _userId ;
public UserService(IHttpContextAccessor context)
{
_userId = new Lazy<string>(() =>
{
if (!context.HttpContext.Request.Headers.TryGetValue("x-test", out var user))
{
throw new ArgumentException($"x-test header is required.");
}
return user;
});
//do other stuff
}
public string UserId => _userId.Value;
}
Or you could just get the value when you needed it:
public class UserService
{
public readonly IHttpContextAccessor _context;
public UserService(IHttpContextAccessor context)
{
_context = context;
//do other stuff
}
public string UserId
{
get
{
if (_context.HttpContext.Request.Headers.TryGetValue("x-test", out var user))
{
return user;
}
else
{
throw new ArgumentException($"x-test header is required.");
}
}
}
}
I use a lot of asp.net core filters.
I'm looking for elegant way to handle exceptions that can be thrown by filters.
For example filter like that:
public async void OnAuthorization(AuthorizationFilterContext context){
Convert.ToInt32("NotConvertable");
}
will throw FormatException which will crush entire application, which is not cool.
I'd like to log error, return 500 but without app crush.
I tried to add middleware before MVC:
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError("FATAL ERROR", ex);
}
but it didnt't help, still crushing.
I'm thinking about make try{}catch with returning 500 with some kind of logging in each filter, but that will lead to large number of repetitions.
Is it any way to handle it globally ?
More context
public class PermissionAttribute:TypeFilterAttribute
{
public PermissionAttribute():base(typeof(PermissionFilter))
{
}
}
public class PermissionFilter : IAuthorizationFilter
{
public async void OnAuthorization(AuthorizationFilterContext context)
{
Convert.ToInt32("NotConvertable");
}
}
And controller:
[Route("api/nav/")]
public class AController : Controller{
...
[HttpGet]
[Route("{id}")]
[PermissionAttribute]
[ProducesResponseType(typeof(SomeClass), 200)]
public async Task<SomeClass> GetAll()
{
...
}
}
Calling this endpoint crush entire process
Startup.cs (shorted)
public void Configure(...some services){
... some app.Use UseLogger, HealthCheck
app.UseMiddleware<PleaseDontCrushMiddleware>(); // Middleware mentioned above
app.UseExceptionHandler(errorHandler.Handle);
a lot of middlewares
app.UseMvc(RouteTable);
}
MVC Service is added this way:
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
config.Filters.Add(new ResponseCacheFilter(new CacheProfile()
{
NoStore = true,
Location = ResponseCacheLocation.None
},
services.BuildServiceProvider().GetService<ILoggerFactory()));
}
Assuming that you will throw some specific exceptions in some methods while other you just want to catch as System.Exception maybe you can use this approach:
You can create Exception handling extension method (if you want to use Logger too just add ILogger parameter in the method and pass it from Startup.Configure):
public static class ExceptionHandler
{
/// <summary>
///
/// </summary>
/// <param name="app"></param>
public static void UseCustomExceptionHandler(this IApplicationBuilder app)
{
app.UseExceptionHandler(eApp =>
{
eApp.Run(async context =>
{
context.Response.StatusCode = 500;
context.Response.ContentType = "application/json";
var errorCtx = context.Features.Get<IExceptionHandlerFeature>();
if (errorCtx != null)
{
var ex = errorCtx.Error;
var message = "Unspecified error ocurred.";
var traceId = traceIdentifierService.TraceId;
if (ex is ValidationException)
{
var validationException = ex as ValidationException;
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
message = string.Join(" | ", validationException.Errors.Select(v => string.Join(",", v.Value)));
}
else if (ex is SomeCustomException)
{
var someCustomException = ex as SomeCustomException;
...
}
var jsonResponse = JsonConvert.SerializeObject(new ErrorResponse
{
TraceId = traceId,
Message = message
});
await context.Response.WriteAsync(jsonResponse, Encoding.UTF8);
}
});
});
}
}
And then you just register it in Startup Configure:
public void Configure(IApplicationBuilder app)
{
...
app.UseCustomExceptionHandler();
...
}
About exceptions in Authorization filters (from microsoft docs):
Do not throw exceptions within authorization filters:
The exception will not be handled.
Exception filters will not handle the exception.
Consider issuing a challenge when an exception occurs in an
authorization filter
You can read more about it here: https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-3.1#action-filters
Edit: This question has been modified completely from its original version.
My objective is to commit the database transaction at the action filer level. I am using Web API .Net Framework (4.8) along with Unity DI.
My Transaction Filter Attribute:
public class TransactionFilterAttribute : ActionFilterAttribute
{
private readonly PortalContext _context;
private DbContextTransaction _transactionScope;
public TransactionFilterAttribute(IUnityContainer container)
{
_context = container.Resolve<PortalContext>();
}
public override void OnActionExecuting(HttpActionContext actionContext)
{
_transactionScope = _context.Database.BeginTransaction();
using (_transactionScope)
{
base.OnActionExecuting(actionContext);
}
}
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
try
{
base.OnActionExecuted(actionExecutedContext);
_transactionScope.Commit(); //Error occurred
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
}
}
}
Unity Configuration:
public static class UnityConfig
{
private static readonly Lazy<IUnityContainer> container = new Lazy<IUnityContainer>(() =>
{
var container = new UnityContainer();
RegisterTypes(container);
return container;
});
public static IUnityContainer Container => container.Value;
private static void RegisterTypes(IUnityContainer container)
{
container.RegisterType<PortalContext>();
}
}
I am getting an error while executing _transactionScope.Commit()
Error: Value cannot be null.
Parameter name: connection --> The underlying provider failed on Commit.
Any help would be appreciated.
You could change your code to look something like this. This would open the transaction when the controller gets called and would wait for the method to return before committing the transaction. This is done by the "await next()" part. You should resolve the _databaseContext from your IOC container so make sure to add it at startup. Using a service filter allows you to use the IOC container within a filter. Have a look at this post to learn more about implementing a service filter in .NET projects: https://www.strathweb.com/2015/06/action-filters-service-filters-type-filters-asp-net-5-mvc-6/. The example below it made for an async method, use the sync version of this method if needed.
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
using (var transaction = _champContext.Database.BeginTransaction())
{
var result = await next();
if ((result.Exception == null || result.ExceptionHandled) &&
IsHttpSuccessStatusCode(context.HttpContext.Response.StatusCode))
{
transaction.Commit();
return;
}
transaction.Rollback();
var controllerActionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;
var controllerName = controllerActionDescriptor?.ControllerName;
var actionName = controllerActionDescriptor?.ActionName;
_logger.Error("Tried to commit transaction for the {ActionName}" +
" method on the {ControllerName} controller with the following parameters: {ActionParameters}" +
" but got exception: {Exception}",
actionName, controllerName, context.ActionArguments, result.Exception);
}
}
This is purely a guess, but I'm wondering if the using clause in your OnActionExecuting override is to blame.
As soon as the base call to OnActionExecuting returns, your _transactionScope is Dispose'd. Thus, assuming that OnActionExecuted isn't called from within the base.OnActionExecuting method, _transactionScope will not be in the state you're expecting in OnActionExecuted.
Suggest:
public override void OnActionExecuting(HttpActionContext actionContext)
{
_transactionScope = _context.Database.BeginTransaction();
base.OnActionExecuting(actionContext);
}
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
try
{
base.OnActionExecuted(actionExecutedContext);
_transactionScope.Commit(); //Error occurred
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
_transactionScope.Rollback(); // because something went wrong during your transaction
}
finally
{
_transactionScope.Dispose(); // now that we're definitely done.
}
}
I am trying to inject CommonService dependency in TableController as like below and it works fine if I call table controller endpoint like "http://localhost:61558/api/TodoItem".
but, It throws an error when I call the same endpoint using "http://localhost:61558/tables/TodoItem"(Correct way as Mobile App SDK call this URL to sync data)
Exception:
"ExceptionType": "System.InvalidOperationException"
ExceptionMessage: "An error occurred when trying to create a controller of type 'TodoItemController'. Make sure that the controller has a parameterless public constructor.",
Startup.cs
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
new MobileAppConfiguration()
.AddMobileAppHomeController()
.MapApiControllers()
.AddTables(new MobileAppTableConfiguration()
.MapTableControllers()
.AddEntityFramework()
)
.ApplyTo(config);
config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;
app.UseWebApi(config);
}
}
I have properly configured DIUnityConfig:
public static void RegisterComponents()
{
var container = new UnityContainer();
container.RegisterType(typeof(ICommonService), typeof(CommonService));
Current = container;
}
Here is the table controller code:
[Authorize]
public class TodoItemController : TableController<TodoItem>
{
private readonly ICommonService _ICommonService;
public TodoItemController(ICommonService commonService)
{
_ICommonService = commonService;
}
protected override void Initialize(HttpControllerContext controllerContext)
{
base.Initialize(controllerContext);
MobileServiceContext context = new MobileServiceContext();
DomainManager = new EntityDomainManager<TodoItem>(context, Request, enableSoftDelete: true);
}
// PATCH tables/TodoItem/48D68C86-6EA6-4C25-AA33-223FC9A27959
public async Task<TodoItem> PatchTodoItem(string id, Delta<TodoItem> patch)
{
var item = await UpdateAsync(id, patch);
await PushToSyncAsync("todoitem", item.Id);
_ICommonService.SendMail();// Want to send mail on business logic.
return item;
}
}
Your table controller is not complete. You need to pass the HttpControllerContext into the constructor and then to the base. Something like this is normal:
public class DatesController : TableController<Dates>
{
protected override void Initialize(HttpControllerContext controllerContext)
{
base.Initialize(controllerContext);
var context = new AppDbContext();
DomainManager = new EntityDomainManager<Dates>(context, Request);
}
// Rest of your controller here
}
Without calling Initialize(), your table controller is never registered.
I'm using ASP.NET Core and FluentValidation.
When a POST action receives invalid input, it's customary to re-render the input form view, with validation errors:
if (!ModelState.IsValid)
return View("nameOfViewRenderedByGetAction", model);
But my validation is actually performed in a service, by FluentValidation, which throws ValidationException. I want to handle it in an exception filter:
public class ValidationFilterAttribute : ActionFilterAttribute, IExceptionFilter
{
public void OnException(ExceptionContext context)
{
// only handle ValidationException
var ex = context.Exception as ValidationException;
if (ex == null) return;
// re-render get action's view, or redirect to get action
// ??
}
}
I'm stuck at the "??" part, because Core has changed the signatures of many types, and ExceptionContext doesn't surface the data I need to make this work.
How do I do this?
It's a little late for an answer but I have a working solution for exactly the same application design. I use ASP.NET Core 3.0 and FluentValidation 8.x.
public class MvcValidationExceptionFilterAttribute : ExceptionFilterAttribute
{
private IModelMetadataProvider ModelMetadataProvider { get; }
public MvcValidationExceptionFilterAttribute(IModelMetadataProvider modelMetadataProvider)
{
ModelMetadataProvider = modelMetadataProvider;
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Framework calls without null")]
public override void OnException(ExceptionContext context)
{
if (context.Exception is ValidationException ex)
{
var validationResult = new ValidationResult(ex.Errors);
validationResult.AddToModelState(context.ModelState, null);
context.Result = new ViewResult { ViewData = new ViewDataDictionary(ModelMetadataProvider, context.ModelState) };
context.ExceptionHandled = true;
}
}
}
As this filter has a dependency we can't use the Attribute directly but register it with dependency injection in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<MvcValidationExceptionFilterAttribute>();
To use the ExceptionFilter either apply it via the ServiceFilterAttribute:
[ServiceFilter(typeof(MvcValidationExceptionFilterAttribute))]
public class MyController : Controller
{
Or apply it globally in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options =>
{
options.Filters.Add<MvcValidationExceptionFilterAttribute>();
})
From an exception filter, You can render a custom view by setting the context result.
public class ValidationFilterAttribute : ActionFilterAttribute, IExceptionFilter
{
public void OnException(ExceptionContext context)
{
// only handle ValidationException
var ex = context.Exception as ValidationException;
if (ex == null) return;
// re-render get action's view, or redirect to get action
var result = new ViewResult { ViewName = "GetView" }
context.HttpContext.Response.Clear();
context.Result = result;
}
}
Where GetView should be the name of your Get action's view.
Sample exception filter that uses a custom developer error view to display details about exceptions.
public class CustomExceptionFilterAttribute : ExceptionFilterAttribute
{
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IModelMetadataProvider _modelMetadataProvider;
public CustomExceptionFilterAttribute(
IHostingEnvironment hostingEnvironment,
IModelMetadataProvider modelMetadataProvider)
{
_hostingEnvironment = hostingEnvironment;
_modelMetadataProvider = modelMetadataProvider;
}
public override void OnException(ExceptionContext context)
{
if (!_hostingEnvironment.IsDevelopment())
{
// do nothing
return;
}
var result = new ViewResult {ViewName = "CustomError"};
result.ViewData = new ViewDataDictionary(_modelMetadataProvider,context.ModelState);
result.ViewData.Add("Exception", context.Exception);
// TODO: Pass additional detailed data via ViewData
context.Result = result;
}
}
Note that the above code is sending the context, model state and exception to the view.
In case all you need is custom error page refer to ASP.NET Core Error Handling
Generally, you should not be using an exception filter to turn an error into success. Consider using an action filter if you have a requirement like that.
Having said that, for some reason if you still need to redirect from an exception filter, this is how it can be done
public class CustomExceptionFilterAttribute : ExceptionFilterAttribute
{
private readonly IHostingEnvironment _hostingEnvironment;
public CustomExceptionFilterAttribute(
IHostingEnvironment hostingEnvironment,
IModelMetadataProvider modelMetadataProvider)
{
_hostingEnvironment = hostingEnvironment;
}
public override void OnException(ExceptionContext context)
{
if (!_hostingEnvironment.IsDevelopment())
{
// do nothing
return;
}
var result = new RedirectToRouteResult(
new RouteValueDictionary(new { controller = "Home", action = "Error" }));
context.Result = result;
}
}