How return View with model in the ExceptionFilter in Asp.Net Core MVC

Viewed 2487

I have created an Asp.Net Core MVC application. I want to handle two types of errors.

I have create two exceptions: UserFriendlyException and UserFriendlyViewException.

I have tried to create the ExceptionFilter that I need handle these two exceptions according these rules:

If is exception UserFriendlyViewException called then I want to return ViewResult with original ViewName and AddModelError and return original Model.

If is exception UserFriendlyException called then I want to redirect to Error view.

This is my ExceptionFilterAttribute:

public class ControllerExceptionFilterAttribute : ExceptionFilterAttribute
{
    private readonly ITempDataDictionaryFactory _tempDataDictionaryFactory;
    private readonly IModelMetadataProvider _modelMetadataProvider;

    public ControllerExceptionFilterAttribute(ITempDataDictionaryFactory tempDataDictionaryFactory,
                IModelMetadataProvider modelMetadataProvider)
    {
        _tempDataDictionaryFactory = tempDataDictionaryFactory;
        _modelMetadataProvider = modelMetadataProvider;
    }
    public override void OnException(ExceptionContext context)
    {
        if (!(context.Exception is UserFriendlyException) && !(context.Exception is UserFriendlyViewException)) return;

        var tempData = _tempDataDictionaryFactory.GetTempData(context.HttpContext);
        //CreateNotification(NotificationHelpers.AlertType.Error, tempData, context.Exception.Message);
        if (!tempData.ContainsKey(NotificationHelpers.NotificationKey)) return;

        if (context.Exception is UserFriendlyViewException userFriendlyViewException)
        {
            context.ModelState.AddModelError(userFriendlyViewException.ErrorKey, userFriendlyViewException.Message);
        }

        if (context.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor)
        {
            //How pass here Model from context??
            //If exists more views with same name but in another controller how pass correct ViewName?
            var result = new ViewResult
            {
                ViewName = context.Exception is UserFriendlyViewException ?
                controllerActionDescriptor.ActionName
                : "Error",
                TempData = tempData,
                ViewData = new ViewDataDictionary(_modelMetadataProvider, context.ModelState)
                            {
                                {"Notifications", tempData[NotificationHelpers.NotificationKey] },
                            }
            };

            context.ExceptionHandled = true;
            context.Result = result;
        }

        tempData.Remove(NotificationHelpers.NotificationKey);
    }
}

I have two issues:

1.) How can I pass original Model from ExceptionContext to ViewResult?

2.) How return correct ViewName for UserFriendlyViewException if exists more Views with same name but in another Controller?

3 Answers

How can I pass original Model from ExceptionContext to ViewResult?

You may use context.ModelState collection.

 foreach(var item in context.ModelState)
 {
    string parameter = item.Key;
    object rawValue = item.Value.RawValue;
    string attemptedValue = item.Value.AttemptedValue;

    System.Console.WriteLine($"Parameter: {parameter}, value: {attemptedValue}");
 }

Note, collection will contain only bound parameters.


How return correct ViewName for UserFriendlyViewException if exists more Views with same name but in another Controller?

The same View discovery process will be used by framework as in the controller's action, so instead of View name you can specify the path:

A view file path can be provided instead of a view name. If using an absolute path starting at the app root (optionally starting with "/" or "~/"), the .cshtml extension must be specified:

return View("Views/Home/About.cshtml");

You can also use a relative path to specify views in different directories without the .cshtml extension. Inside the HomeController, you can return the Index view of your Manage views with a relative path:

return View("../Manage/Index");

Similarly, you can indicate the current controller-specific directory with the "./" prefix:

return View("./About");

Pass stuff via ViewData

public class UIFriendlyExceptionFilter : IExceptionFilter
{
    public void OnException(ExceptionContext context)
    {
        if (context.Exception is UIFriendlyException uex)
        {
            var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), context.ModelState);

            //here we go
            viewData.Add("Message", MY_CUSTOM_MESSAGE);

            var res = new ViewResult() {
                ViewName = "Error",
                ViewData = viewData
            };
            context.Result = res;
        }
        context.ExceptionHandled = true;
    }
}

Then register in Startup

services.AddControllersWithViews(options =>
{ 
    options.Filters.Add(typeof(UIFriendlyException.UIFriendlyExceptionFilter));
});

Prepare two Filters. One caches the Binding Model and the other handles exceptions.

IActionFilter

public class BindModelCacheFilter : IActionFilter
{
    public static readonly string Key = "BindModelCacheFilterKey. This string can be anything.";

    public void OnActionExecuting(ActionExecutingContext context)
    {
        context.HttpContext.Items[BindModelCacheFilter.Key] = context.ActionArguments;
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
    }
}

IExceptionFilter

public class ExceptionHandlingFilter : IExceptionFilter
{
    private readonly ILogger<ExceptionHandlingFilter> _logger;

    public ExceptionHandlingFilter(ILogger<ExceptionHandlingFilter> logger)
    {
        _logger = logger;
    }

    public void OnException(ExceptionContext context)
    {
        // ...

            var requestPayload = "";
            try
            {
                var actionArguments = context.HttpContext.Items[BindModelCacheFilter.Key] as IDictionary<string, object?>;
                if (0 < actionArguments?.Count)
                {
                    object requestDto = actionArguments.First().Value!;
                    var options = new System.Text.Json.JsonSerializerOptions()
                    {
                        IncludeFields = true,
                        Encoder = System.Text.Encodings.Web.JavaScriptEncoder.Create(System.Text.Unicode.UnicodeRanges.All)
                    };
                    requestPayload = System.Text.Json.JsonSerializer.Serialize(requestDto, options);
                }
            }
            catch
            {
            }
        // ...
    }
}
Related