How do I go about the [HandleError] filter in asp.net MVC Preview 5?
I set the customErrors in my Web.config file
<customErrors mode="On" defaultRedirect="Error.aspx">
<error statusCode="403" redirect="NoAccess.htm"/>
<error statusCode="404" redirect="FileNotFound.htm"/>
</customErrors>
and put [HandleError] above my Controller Class like this:
[HandleError]
public class DSWebsiteController: Controller
{
[snip]
public ActionResult CrashTest()
{
throw new Exception("Oh Noes!");
}
}
Then I let my controllers inherit from this class and call CrashTest() on them.
Visual studio halts at the error and after pressing f5 to continue, I get rerouted to Error.aspx?aspxerrorpath=/sxi.mvc/CrashTest (where sxi is the name of the used controller.
Off course the path cannot be found and I get "Server Error in '/' Application." 404.
This site was ported from preview 3 to 5.
Everything runs (wasn't that much work to port) except the error handling.
When I create a complete new project the error handling seems to work.
Ideas?
--Note--
Since this question has over 3K views now, I thought it would be beneficial to put in what I'm currently (ASP.NET MVC 1.0) using.
In the mvc contrib project there is a brilliant attribute called "RescueAttribute"
You should probably check it out too ;)
[HandleError]
When you provide only the HandleError attribute to your class (or to your action method for that matter), then when an unhandled exception occurs MVC will look for a corresponding View named "Error" first in the Controller's View folder. If it can't find it there then it will proceed to look in the Shared View folder (which should have an Error.aspx file in it by default)
[HandleError(ExceptionType = typeof(SqlException), View = "DatabaseError")]
[HandleError(ExceptionType = typeof(NullReferenceException), View = "LameErrorHandling")]
You can also stack up additional attributes with specific information about the type of exception you are looking for. At that point, you can direct the Error to a specific view other than the default "Error" view.
For more information, take a look at Scott Guthrie's blog post about it.
It should also be noted that errors that don't set the http error code to 500
(e.g. UnauthorizedAccessException)
will not be handled by the HandleError filter.
Solution for http error code to 500
this is an attribute called [ERROR] put it on an action
public class Error: System.Web.Mvc.HandleErrorAttribute
{
public override void OnException(System.Web.Mvc.ExceptionContext filterContext)
{
if (filterContext.HttpContext.IsCustomErrorEnabled)
{
filterContext.ExceptionHandled = true;
}
base.OnException(filterContext);
//OVERRIDE THE 500 ERROR
filterContext.HttpContext.Response.StatusCode = 200;
}
private static void RaiseErrorSignal(Exception e)
{
var context = HttpContext.Current;
// using.Elmah.ErrorSignal.FromContext(context).Raise(e, context);
}
}
//EXAMPLE:
[Error]
[HandleError]
[PopulateSiteMap(SiteMapName="Mifel1", ViewDataKey="Mifel1")]
public class ApplicationController : Controller
{
}
Attributes in MVC is very useful in error handling at get and post method, it also track for ajax call.
Create a base controller in your application and inherit it in your main controller(EmployeeController).
public class EmployeeController : BaseController
Add below code in base controller.
/// <summary>
/// Base Controller
/// </summary>
public class BaseController : Controller
{
protected override void OnException(ExceptionContext filterContext)
{
Exception ex = filterContext.Exception;
//Save error log in file
if (ConfigurationManager.AppSettings["SaveErrorLog"].ToString().Trim().ToUpper() == "TRUE")
{
SaveErrorLog(ex, filterContext);
}
// if the request is AJAX return JSON else view.
if (IsAjax(filterContext))
{
//Because its a exception raised after ajax invocation
//Lets return Json
filterContext.Result = new JsonResult()
{
Data = Convert.ToString(filterContext.Exception),
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
}
else
{
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
filterContext.Result = new ViewResult()
{
//Error page to load
ViewName = "Error",
ViewData = new ViewDataDictionary()
};
base.OnException(filterContext);
}
}
/// <summary>
/// Determines whether the specified filter context is ajax.
/// </summary>
/// <param name="filterContext">The filter context.</param>
private bool IsAjax(ExceptionContext filterContext)
{
return filterContext.HttpContext.Request.Headers["X-Requested-With"] == "XMLHttpRequest";
}
/// <summary>
/// Saves the error log.
/// </summary>
/// <param name="ex">The ex.</param>
/// <param name="filterContext">The filter context.</param>
void SaveErrorLog(Exception ex, ExceptionContext filterContext)
{
string logMessage = ex.ToString();
string logDirectory = Server.MapPath(Url.Content("~/ErrorLog/"));
DateTime currentDateTime = DateTime.Now;
string currentDateTimeString = currentDateTime.ToString();
CheckCreateLogDirectory(logDirectory);
string logLine = BuildLogLine(currentDateTime, logMessage, filterContext);
logDirectory = (logDirectory + "\\Log_" + LogFileName(DateTime.Now) + ".txt");
StreamWriter streamWriter = null;
try
{
streamWriter = new StreamWriter(logDirectory, true);
streamWriter.WriteLine(logLine);
}
catch
{
}
finally
{
if (streamWriter != null)
{
streamWriter.Close();
}
}
}
/// <summary>
/// Checks the create log directory.
/// </summary>
/// <param name="logPath">The log path.</param>
bool CheckCreateLogDirectory(string logPath)
{
bool loggingDirectoryExists = false;
DirectoryInfo directoryInfo = new DirectoryInfo(logPath);
if (directoryInfo.Exists)
{
loggingDirectoryExists = true;
}
else
{
try
{
Directory.CreateDirectory(logPath);
loggingDirectoryExists = true;
}
catch
{
}
}
return loggingDirectoryExists;
}
/// <summary>
/// Builds the log line.
/// </summary>
/// <param name="currentDateTime">The current date time.</param>
/// <param name="logMessage">The log message.</param>
/// <param name="filterContext">The filter context.</param>
string BuildLogLine(DateTime currentDateTime, string logMessage, ExceptionContext filterContext)
{
string controllerName = filterContext.RouteData.Values["Controller"].ToString();
string actionName = filterContext.RouteData.Values["Action"].ToString();
RouteValueDictionary paramList = ((System.Web.Routing.Route)(filterContext.RouteData.Route)).Defaults;
if (paramList != null)
{
paramList.Remove("Controller");
paramList.Remove("Action");
}
StringBuilder loglineStringBuilder = new StringBuilder();
loglineStringBuilder.Append("Log Time : ");
loglineStringBuilder.Append(LogFileEntryDateTime(currentDateTime));
loglineStringBuilder.Append(System.Environment.NewLine);
loglineStringBuilder.Append("Username : ");
loglineStringBuilder.Append(Session["LogedInUserName"]);
loglineStringBuilder.Append(System.Environment.NewLine);
loglineStringBuilder.Append("ControllerName : ");
loglineStringBuilder.Append(controllerName);
loglineStringBuilder.Append(System.Environment.NewLine);
loglineStringBuilder.Append("ActionName : ");
loglineStringBuilder.Append(actionName);
loglineStringBuilder.Append(System.Environment.NewLine);
loglineStringBuilder.Append("----------------------------------------------------------------------------------------------------------");
loglineStringBuilder.Append(System.Environment.NewLine);
loglineStringBuilder.Append(logMessage);
loglineStringBuilder.Append(System.Environment.NewLine);
loglineStringBuilder.Append("==========================================================================================================");
return loglineStringBuilder.ToString();
}
/// <summary>
/// Logs the file entry date time.
/// </summary>
/// <param name="currentDateTime">The current date time.</param>
string LogFileEntryDateTime(DateTime currentDateTime)
{
return currentDateTime.ToString("dd-MMM-yyyy HH:mm:ss");
}
/// <summary>
/// Logs the name of the file.
/// </summary>
/// <param name="currentDateTime">The current date time.</param>
string LogFileName(DateTime currentDateTime)
{
return currentDateTime.ToString("dd_MMM_yyyy");
}
}
================================================
Finds the Directory : Root/App_Start/FilterConfig.cs
Add below code:
/// <summary>
/// Filter Config
/// </summary>
public class FilterConfig
{
/// <summary>
/// Registers the global filters.
/// </summary>
/// <param name="filters">The filters.</param>
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute());
}
}
Track AJAX Error:
Call CheckAJAXError function in layout page load.
function CheckAJAXError() {
$(document).ajaxError(function (event, jqXHR, ajaxSettings, thrownError) {
var ex;
if (String(thrownError).toUpperCase() == "LOGIN") {
var url = '#Url.Action("Login", "Login")';
window.location = url;
}
else if (String(jqXHR.responseText).toUpperCase().indexOf("THE DELETE STATEMENT CONFLICTED WITH THE REFERENCE CONSTRAINT") >= 0) {
toastr.error('ReferanceExistMessage');
}
else if (String(thrownError).toUpperCase() == "INTERNAL SERVER ERROR") {
ex = ajaxSettings.url;
//var url = '#Url.Action("ErrorLog", "Home")?exurl=' + ex;
var url = '#Url.Action("ErrorLog", "Home")';
window.location = url;
}
});
};
You are missing Error.aspx :) In preview 5, this is located in your Views/Shared folder. Just copy it over from a new Preview 5 project.
[HandleError]
public class ErrorController : Controller
{
[AcceptVerbs(HttpVerbs.Get)]
public ViewResult NotAuthorized()
{
//401
Response.StatusCode = (int)HttpStatusCode.Unauthorized;
return View();
}
[AcceptVerbs(HttpVerbs.Get)]
public ViewResult Forbidden()
{
//403
Response.StatusCode = (int)HttpStatusCode.Forbidden;
return View();
}
[AcceptVerbs(HttpVerbs.Get)]
public ViewResult NotFound()
{
//404
Response.StatusCode = (int)HttpStatusCode.NotFound;
return View();
}
public ViewResult ServerError()
{
//500
Response.StatusCode = (int)HttpStatusCode.NotFound;
return View();
}
}
Related
This is the first time I'm using .net core so maybe I'm doing something wrong. My application does not redirect to the login page after the session is expired, obviously resulting in Null Pointer Exceptions when I try to read session variables.
Startup.cs
services.AddAuthentication()
.AddCookie(options =>
{
options.Cookie.Expiration = TimeSpan.FromMinutes(180);
});
services.AddSession(options => {
options.IdleTimeout = TimeSpan.FromMinutes(180);
});
Am I missing something?
UPDATE:
If I manually delete the .AspNetCore.Identity.Application cookie, than I'll be correctly redirected to the login page.
How can I obtain this behaviour?
You can achieve this by using the OnActionExecuting method of IActionFilter by redefining it in our custom class.
First add it to the services:
services.AddControllersWithViews(options =>
{
options.Filters.Add(typeof(RequestAuthenticationFilter));
});
Then create a custom class RequestAuthenticationFilter and define the OnActionExecuting method.
public class RequestAuthenticationFilter : IActionFilter
{
private readonly IHttpContextAccessor _httpContextAccessor;
public RequestAuthenticationFilter(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
/// <summary>
/// OnActionExecuting
/// </summary>
/// <param name="context"></param>
public void OnActionExecuting(ActionExecutingContext context)
{
var session = _httpContextAccessor.HttpContext.Session.Get("UserId");
if (session == null)
{
context.Result = new RedirectToRouteResult(new RouteValueDictionary(new { action = "Login", controller = "Account", area = "Identity" }));
}
}
/// <summary>
/// OnActionExecuted
/// </summary>
/// <param name="context"></param>
public void OnActionExecuted(ActionExecutedContext context)
{
}
}
Taking a clue from Sonu K's answer, I have created an Attribute, so I can apply it to specific controller or action. Here is my implementation.
public class ValidateSessionAttribute : ActionFilterAttribute
{
public ValidateSessionAttribute() { }
public override void OnActionExecuting(ActionExecutingContext context)
{
var userId = context.HttpContext.Session.Get<long>(Constants.SessionKeys.UserId);
if (userId <= 0)
context.Result =
new RedirectToRouteResult(
new RouteValueDictionary(new { action = "Login", controller = "Account" }));
}
}
Decorate controller with above attribute to start using it.
[ValidateSession]
public class SomeController : Controller
{
}
to change Identity time out from startup.cs
services.ConfigureApplicationCookie(options =>
{
options.ExpireTimeSpan = TimeSpan.FromSeconds(5);
});
Refer to Session Expire problem
I have two controllers both are derivated from a base controller. The code inside them is exactly the same. The only difference is in constructors. Below is my code:
[RoutePrefix("api/v2")]
public class CategoryController : BaseController
{
private IGetTroubleTicketService getTroubleTicketService;
private ICategoryService categoryService;
/// <summary>
/// Constructor for initialization
/// </summary>
public CategoryController()
{
getTroubleTicketService = MethodFactory.Create<IGetTroubleTicketService>();
getTroubleTicketService.SetProvider(new ServiceProvider(Global.Container));
categoryService = MethodFactory.Create<ICategoryService>();
categoryService.SetProvider(new ServiceProvider(Global.Container));
}
/// <summary>
/// Retrieve all Categories
/// </summary>
/// <returns>Categories(Id, Label)</returns>
[HttpGet]
[Route("categoryRef")]
public HttpResponseMessage Categories()
{
try
{
// Validate User Id and Application Id
var user = ValidateUserAndApplication(getTroubleTicketService);
var userLanaguage = Convert.ToInt32(user.Language, CultureInfo.InvariantCulture);
var categories = categoryService.CategoriesData(userLanaguage);
LoggingRequest("categoryRef",null);
response = Request.CreateResponse(HttpStatusCode.OK, categories);
}
catch (Exception exception)
{
//CheckError
CheckError(exception);
}
return response;
}
}
The second one is
[RoutePrefix("api/v2")]
public class ProblemCategoryController : BaseController
{
private IGetTroubleTicketService getTroubleTicketService;
private ICategoryService categoryService;
/// <summary>
/// Constructor for initialization
/// </summary>
public ProblemCategoryController()
{
getTroubleTicketService = MethodFactory.Create<IGetTroubleTicketService>();
getTroubleTicketService.SetProvider(new ServiceProvider(Global.Container));
categoryService = MethodFactory.Create<ICategoryService>();
categoryService.SetProvider(new ServiceProvider(Global.Container));
}
/// <summary>
/// Retrieve all Natures of problem
/// </summary>
/// <returns>Categories(Id, Label)</returns>
[HttpGet]
[Route("problemCategoryRef")]
public HttpResponseMessage ProblemCategories()
{
try
{
// Validate User Id and Application Id
var user = ValidateUserAndApplication(getTroubleTicketService);
var userLanaguage = Convert.ToInt32(user.Language, CultureInfo.InvariantCulture);
var categories = categoryService.CategoriesData(userLanaguage);
LoggingRequest("problemCategoryRef", null);
response = Request.CreateResponse(HttpStatusCode.OK, categories);
}
catch (Exception exception)
{
//CheckError
CheckError(exception);
}
return response;
}
Now as you can see the internal code is exactly the same which I want to avoid creating a Helper class. How can I make this common class for it so as to remove code duplicacy? It's possible without rewrite all the context code to get User and Id app?
I tried this
public class NatureOfProblemHelper : BaseController
{
private IGetTroubleTicketService getTroubleTicketService;
private ICategoryService categoryService;
private string resourceName;
/// <summary>
/// Initializes a new instance of the <see cref="NatureOfProblemHelper"/> class.
/// Constructor for initialization.
/// <param name="resource">Resource requested by user.</param>
/// </summary>
public NatureOfProblemHelper(string resource)
{
getTroubleTicketService = MethodFactory.Create<IGetTroubleTicketService>();
getTroubleTicketService.SetProvider(new ServiceProvider(Global.Container));
categoryService = MethodFactory.Create<ICategoryService>();
categoryService.SetProvider(new ServiceProvider(Global.Container));
resourceName = resource;
}
/// <summary>
/// Retrieve all Natures of problem.
/// </summary>
/// <returns>Categories(Id, Label).</returns>
public HttpResponseMessage GetNaturesOfProblem()
{
// Validate User Id and Application Id
var user = ValidateUserAndApplication(getTroubleTicketService);
var userLanaguage = Convert.ToInt32(user.Language, CultureInfo.InvariantCulture);
var categories = categoryService.CategoriesData(userLanaguage);
LoggingRequest(resourceName, null);
return Request.CreateResponse(HttpStatusCode.OK, categories);
}
And then into each controller
[HttpGet]
[Route("problemCategoryRef")]
public HttpResponseMessage ProblemCategories()
{
try
{
response = natureOfProblem.NaturesOfProblem();
}
catch (Exception exception)
{
//CheckError
CheckError(exception);
}
return response;
}
This build, but I can't get the context that comes from this variable
// Validate User Id and Application Id
var user = ValidateUserAndApplication(getTroubleTicketService);
Why if I put the same lines of code directly in my controller works but if I put in my Helper it doesn't work??
How do you get current culture or browser locale on MVC 4.
I find some samples which gets it from HttpContext and HttpRequest but this doesnt work on MVC 4.
How do you do it on MVC 4?
I find some samples which gets it from HttpContext and HttpRequest but this doesnt work on MVC 4.
I just love the it doesn't work problem description!!! It's like saying to a mechanic whom you don't want to pay for the job: my car doesn't work, tell me what is wrong with it so that I can fix it myself, without showing him your car of course.
Anyway, you still got the HttpRequest in your controller action. Look at the UserLanguages property:
public ActionResult SomeAction()
{
string[] userLanguages = Request.UserLanguages;
...
}
Remark: the value of this property will be null if the user agent didn't send the Accept-Languages request header. So make sure you check whether it is not null before accessing its value to avoid getting NREs.
We use the following code in NuGetGallery
/// <summary>
/// Extensions on <see cref="HttpRequest"/> and <see cref="HttpRequestBase"/>
/// </summary>
public static class HttpRequestExtensions
{
/// <summary>
/// Retrieve culture of client.
/// </summary>
/// <param name="request">Current request.</param>
/// <returns><c>null</c> if not to be determined.</returns>
public static CultureInfo DetermineClientCulture(this HttpRequest request)
{
return DetermineClientCulture(new HttpRequestWrapper(request));
}
/// <summary>
/// Retrieve culture of client.
/// </summary>
/// <param name="request">Current request.</param>
/// <returns><c>null</c> if not to be determined.</returns>
public static CultureInfo DetermineClientCulture(this HttpRequestBase request)
{
if (request == null)
{
return null;
}
string[] languages = request.UserLanguages;
if (languages == null)
{
return null;
}
//first try parse of full langcodes. Stop with first success.
foreach (string language in languages)
{
string lang = language.ToLowerInvariant().Trim();
try
{
return CultureInfo.GetCultureInfo(lang);
}
catch (CultureNotFoundException)
{
}
}
//try parse again with first 2 chars. Stop with first success.
foreach (string language in languages)
{
string lang = language.ToLowerInvariant().Trim();
if (lang.Length > 2)
{
string lang2 = lang.Substring(0, 2);
try
{
return CultureInfo.GetCultureInfo(lang2);
}
catch (CultureNotFoundException)
{
}
}
}
return null;
}
}
usage:
/// <summary>
/// Called before the action method is invoked.
/// </summary>
/// <param name="filterContext">Information about the current request and action.</param>
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (!filterContext.IsChildAction)
{
//no need to do the hassle for a child action
//set the culture from the request headers
var clientCulture = Request.DetermineClientCulture();
if (clientCulture != null)
{
//and/or CurrentUICulture
Thread.CurrentThread.CurrentCulture = clientCulture;
}
}
base.OnActionExecuting(filterContext);
}
I have made an AuthorizeAttributeBase to extend AuthorizeAttribute.
It looks like this:
public abstract class MyAuthorizeAttribute : AuthorizeAttribute
{
//Holds the roles allowed to perform the action.
public IEnumerable<string> roles { get; set; }
/// <summary>
/// Authorizes if the current user may perform the action
/// </summary>
/// <param name="httpContext">Unused - included for override purposes.</param>
/// <returns>true if authorized.</returns>
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
//Return true if user is in the action allowed roles.
if (IsUserInRole)
{
return true;
}
else
{
HttpContext.Current.Response.StatusCode = 401;
return false;
}
}
/// <summary>
/// Checks if the user is member of a role that is allowed by the authorization
/// </summary>
public bool IsUserInRole
{
get
{
if (roles != null)
{
//Check if any of the roles in the session is in the list of roles of the authorization
return (MySessionGetter.GetSession().Roles.Intersect<string>(roles).Any());
}
//If none of the roles match return false.
return false;
}
}
/// <summary>
/// Sets the allowed roles of the authorization
/// </summary>
/// <param name="userRoles">Allowed roles</param>
public void AlowedRoles(IEnumerable<string> userRoles)
{
roles = userRoles;
}
I keep the allowed rolenames like this:
/// <summary>
/// Holds the role names.
/// </summary>
public static class UserRoles
{
public static string Administrators = "Administrators";
public static string Teachers= "Teachers";
}
And use my base like this:
/// <summary>
/// Authorization for the access to the SomeAction
/// </summary>
public class AuthorizeAccessToSomeActionAttribute : MyAuthorizeAttribute
{
public AuthorizeAccessToSomeActionAttribute()
{
AlowedRoles(new List<string> { UserRoles.Adminstrators,
UserRoles.Teachers });
}
}
And last but not least the controller:
/// <summary>
/// The main Index view of application
/// </summary>
/// <returns>Application Index views</returns>
[AuthorizeAccessToSomeAction]
public ActionResult Index()
{
return View("Index");
}
Now what I want to do is make the index switch return value on base of the AuthorizeAttributes.
Lets say Teachers to the TeachersIndex() and Administrators to the AdministratorsIndex().
I tried adding this to the base:
//Checks if the current user is authorized.
public bool IsAuthorized()
{
return AuthorizeCore(new HttpContextWrapper());
}
But I end up having to create new AutorizeAttributes every time.
Making it static seemed to give me even more problems.
Is there a correct way of going about this?
Solved it. :)
The OnAuthorization override got me a new lead.
Found this question.
I put the redirects in a Dictionary<string, RedirectToRouteResult> because I like the idea of keeping all role strings in one place instead of filling up my controllers with magic strings.
public static Dictionary<string, RedirectToRouteResult> HomeRedirect
{
get
{
return new Dictionary<string, RedirectToRouteResult> {
{"Administrators", new RedirectToRouteResult(
new RouteValueDictionary { { "action", "AdministratorIndex" }, { "controller", "MyController" }})},
{"Teacher", new RedirectToRouteResult(
new RouteValueDictionary { { "action", "TeacherIndex" }, { "controller", "MyController" }})}
};
And the override HandleUnauthorizedRequest looks like this now:
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) {
filterContext.Result = UserRoles.HomeRedirect
.SingleOrDefault(m => m.Key == MySessionGetter.GetSession().Roles.First()).Value;
}
Take a look at RedirectToRouteResult and RedirectResult.
Here would be a good start:
// Redirects users of given role to given action
public class AuthorizeAccessToSomeActionAttribute : MyAuthorizeAttribute
{
public string Role { get; set; }
public string RedirectToAction { get; set; }
public AuthorizeAccessToSomeActionAttribute(string role, string action)
{
Role = role;
RedirectToAction = action;
}
public override void OnAuthorization(AuthorizationContext filterContext)
{
// Test if current user is in the role
if (filterContext.HttpContext.User.IsInRole(Role))
{
// Get current routevalues
var rvals = filterContext.RouteData.Values;
// Change action
rvals["action"] = RedirectToAction;
filterContext.Result = new RedirectToRouteResult("Default",rvals);
}
}
}
Usage:
// Redirect role to given action
[AuthorizeAccessToSomeActionAttribute("Teacher", "TeacherIndex" )]
[AuthorizeAccessToSomeActionAttribute("Admin", "AdminIndex" )]
public ActionResult Index()
...
public ActionResult TeacherIndex()
...
public ActionResult AdminIndex()
I have been capturing exceptions and redirecting users to an error page. I pass the exception message and a return URL to tell the user what happened and allow them to return another page.
try
{
return action(parameters);
}
catch (Exception exception)
{
ErrorViewModel errorModel = new ErrorViewModel();
errorModel.ErrorMessage = "An error occured while doing something.";
errorModel.ErrorDetails = exception.Message;
errorModel.ReturnUrl = Url.Action("Controller", "Action");
return RedirectToAction("Index", "Error", errorModel);
}
This seems like way too much code to wrap around every action. I am using a global filter for errors:
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
HandleErrorAttribute attribute = new HandleErrorAttribute();
filters.Add(attribute);
}
and I have my web.config setup like this:
<customErrors mode="On" defaultRedirect="~/Error/Unknown">
But, this only works for unhandled exceptions.
I want exceptions to cause a redirect to an error controller/action taking a parameter holding the exception details. It would be nice if I could indicate the return URL on a action-by-action basis, or to have a default if none is provided.
Instead of putting a try catch in every action, you could override the OnException event of the controller. That event contains all the exception details. All of you other settings look right.
[HandleError]
public class AccountController : Controller
{
[HttpPost]
public ActionResult YourAction(SomeModel model)
{
//do stuff but don't catch exception
return View();
}
protected override void OnException(ExceptionContext filterContext)
{
EventLog.WriteEntry("YourProjectEventName", filterContext.Exception.ToString(), EventLogEntryType.Error);
base.OnException(filterContext);
}
}
Here is a little RedirectOnErrorAttribute class I created:
using System;
using System.Web.Mvc;
using MyApp.Web.Models;
namespace MyApp.Web.Utilities
{
public class RedirectOnErrorAttribute : ActionFilterAttribute
{
/// <summary>
/// Initializes a new instance of a RedirectOnErrorAttribute.
/// </summary>
public RedirectOnErrorAttribute()
{
ErrorMessage = "An error occurred while processing your request.";
}
/// <summary>
/// Gets or sets the controller to redirect to.
/// </summary>
public string ReturnController { get; set; }
/// <summary>
/// Gets or sets the action to redirect to.
/// </summary>
public string ReturnAction { get; set; }
/// <summary>
/// Gets or sets the error message.
/// </summary>
public string ErrorMessage { get; set; }
/// <summary>
/// Redirects the user to an error screen if an exception is thrown.
/// </summary>
/// <param name="filterContext">The filter context.</param>
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
if (filterContext.Exception != null && !filterContext.ExceptionHandled)
{
ErrorViewModel viewModel = new ErrorViewModel();
viewModel.ErrorMessage = ErrorMessage;
viewModel.ErrorDetails = filterContext.Exception.Message;
string controller = ReturnController;
string action = ReturnAction;
if (String.IsNullOrWhiteSpace(controller))
{
controller = (string)filterContext.RequestContext.RouteData.Values["controller"];
}
if (String.IsNullOrWhiteSpace(action))
{
action = "Index";
}
UrlHelper helper = new UrlHelper(filterContext.RequestContext);
viewModel.ReturnUrl = helper.Action(action, controller);
string url = helper.Action("Index", "Error", viewModel);
filterContext.Result = new RedirectResult(url);
filterContext.ExceptionHandled = true;
}
base.OnActionExecuted(filterContext);
}
}
}
Now, I can decorate all of my actions like this:
[RedirectOnError(ErrorMessage="An error occurred while doing something.", ReturnController="Controller", ReturnAction="Action")]