Kristoffer Lundberg
Kristoffer Lundberg

Reputation: 876

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:

  1. User tries to delete a record
  2. Deletion fails and an error is logged
  3. User redirects to another page
  4. 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.

Upvotes: 7

Views: 1326

Answers (4)

Kristoffer Lundberg
Kristoffer Lundberg

Reputation: 876

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) %>

Upvotes: 4

Erik Funkenbusch
Erik Funkenbusch

Reputation: 93424

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.

Upvotes: 0

RPM1984
RPM1984

Reputation: 73102

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

Upvotes: 2

Tomas Aschan
Tomas Aschan

Reputation: 60564

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.

Upvotes: 1

Related Questions