repeat common error info logic in ActionFilterAttribute - asp.net-mvc

I'm implementing a web API using the REST for ASP.NET MVC framework (MVC 2). I want to encapsulate this code, ideally in an ActionFilterAttribute (?), so that I can decorate specific actions that always perform this same logic:
if (!ModelState.IsValid) {
return View(
new GenericResultModel(){ HasError=True, ErrorMessage="Model is invalid."});
}
I really don't want to have to copy and paste this boilerplate code into every controller action where I need to do this.
In this web API scenario, I need to be able to do something like this so that the caller can get a result back in JSON or POX form and see that there is an error. In an ASPX view, obviously I wouldn't need something like this as the validation controls would take care of notifying the user of a problem. But I do not have an ASPX view - I am only returning JSON or POX data serialized from my model.
I've started with this code in an ActionFilter but am not sure what to do next (or if it is even the right starting point):
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
bool result = filterContext.Controller.ViewData.ModelState.IsValid;
if (!result)
{
GenericResultModel m = new GenericResultModel() { HasError = true };
// return View(m)
// ?????
}
base.OnActionExecuting(filterContext);
}
How do I accomplish this?

public override void OnActionExecuting(ActionExecutingContext filterContext)
{
// Notice that the controller action hasn't been called yet, so
// don't expect ModelState.IsValid=false here if you have
// ModelState.AddModelError inside your controller action
// (you shouldn't be doing validation in your controller action anyway)
bool result = filterContext.Controller.ViewData.ModelState.IsValid;
if (!result)
{
// the model that resulted from model binding is not valid
// => prepare a ViewResult using the model to return
var result = new ViewResult();
result.ViewData.Model = new GenericResultModel() { HasError = true };
filterContext.Result = result;
}
else
{
// call the action method only if the model is valid after binding
base.OnActionExecuting(filterContext);
}
}

Related

MVC 4 AuthorizeAttribute.HandleUnauthorizedRequest ViewResult - infinite loop

i've stepped through my code a million times and can't find a problem with my implementation..
in custom AuthorizeAttribute i overwrote 2 methods
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (!httpContext.Request.IsAuthenticated)
return false;
var routeData = httpContext.Request.RequestContext.RouteData;
var ctrl = routeData.Values["controller"].ToString();
var action = routeData.Values["action"].ToString();
var user = httpContext.User.Identity.Name;
_logger.Info("[logging all the details]");
return ctrl == "SomeController";
}
protected override void HandleUnauthorizedRequest(AuthorizationContext ctx)
{
ctx.Result = new ViewResult { ViewName = "Unauthorized" };
// base.HandleUnauthorizedRequest(ctx);
}
the authorization logic is mocked to return false only on specific controller, and i've stepped through this to verify it's working correctly.
above code will cause infinite loop. in my log i can see that line hit 666 times (coincidence?) ..
if i do call base.HandleUnauthorizedRequest(ctx), all i get is a blank page. so i reflected what the base does, and it's this
filterContext.Result = new HttpUnauthorizedResult();
so this explains why it renders a blank page instead of redirecting to Unauthorized.cshtml. what i'm not sure about, is why does it go into an infinite loop if i don't call the base.
p.s.
i've verified that if i put the wrong Unauthorized view it will error out (but still hangs indefinitely)
System.InvalidOperationException: The view 'Unauthorized11' or its master was not found or no view engine supports the searched locations
Here is the implementation that i ended up going with and it's working very well.
public override void OnAuthorization(AuthorizationContext filterContext)
{
base.OnAuthorization(filterContext);
// this is overriden for kendo menus to hide
var ctrl = filterContext.RequestContext.RouteData.GetRequiredString("controller");
var action = filterContext.ActionDescriptor.ActionName;
[custom authorization logic on action/ctrl]
// useful to determine if it's authorizing current controller path or menu links
var path = filterContext.HttpContext.Request.PhysicalPath;
_authorizingCurrentPath = path.Contains(ctrl) || path.EndsWith("WebUI") ;
if (userAuth < requiredAuth)
HandleUnauthorizedRequest(filterContext);
}
protected override void HandleUnauthorizedRequest(AuthorizationContext ctx)
{
if (!ctx.HttpContext.User.Identity.IsAuthenticated)
base.HandleUnauthorizedRequest(ctx);
else {
if (_authorizingCurrentPath) {
// handle controller access
ctx.Result = new ViewResult { ViewName = "Unauthorized" };
ctx.HttpContext.Response.StatusCode = 403;
}
else {
// handle menu links
ctx.Result = new HttpUnauthorizedResult();
ctx.HttpContext.Response.StatusCode = 403;
}
}
}
The default implementation of AuthorizeAttribute sets the response on the action context, by not calling into the base the response is never set which causes the filter to repeat the authorization process until a response is set (hence the infinite loop).
You can see this logic in the AuthorizationFilterAttribute class which AuthorizeAttribute derives from.

How to access data after redirect

Again there are multiple articles which says how to access data after redirect. but doesn't serves my purpose.
I am having errorcontroller which is having index action method and error index view.
If there is any error in the application it will caught in Application_Error event.
inside Application_Error event I had logged the error and redirected to Error Index page like this -
protected new void Application_Error(object sender, EventArgs e)
{
Exception error = Server.GetLastError();
log.error(error.Message);
HttpContext.Current.Response.Redirect("~/Error/Index");
}
Now in the error index view, I would like to display the error message. What should I do in Application_Error event which can be access by Error Index view?
Updated : I don't want to use Session as session object may not be available in Application_Error event. this is dependent on when the error occurred.
Approach - 1
As per my knowledge you can use TempData to store the posted data. It is like a DataReader Class, once read, Data will be lost. So that stored data in TempData will become null.
var Value = TempData["keyName"] //Once read, data will be lost
So to persist the data even after the data is read you can Alive it like below
var Value = TempData["keyName"];
TempData.Keep(); //Data will not be lost for all Keys
TempData.Keep("keyName"); //Data will not be lost for this Key
TempData works in new Tabs/Windows also, like Session variable does.
You could use Session Variable also, Only major problem is that Session Variable are very heavy comparing with TempData. Finally you are able to keep the data across Controllers/Area also.
Approach - 2
This works for me. This is very easy and no need to consider any change in Web.Config or Register the Action Filter in Global.asax file.
ok. So, First I am creating a simple Action Filter. This will handle Ajax and Non Ajax requests.
public class MyCustomErrorAttribute : HandleErrorAttribute
{
public override void OnException(ExceptionContext filterContext)
{
filterContext.ExceptionHandled = true;
var debugModeMsg = filterContext.HttpContext.IsDebuggingEnabled
? filterContext.Exception.Message +
"\n" +
filterContext.Exception.StackTrace
: "Your error message";
//This is the case when you need to handle Ajax requests
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.Result = new JsonResult
{
JsonRequestBehavior = JsonRequestBehavior.AllowGet,
Data = new
{
error = true,
message = debugModeMsg
}
};
}
//This is the case when you handle Non Ajax request
else
{
var routeData = new RouteData();
routeData.Values["controller"] = "Error";
routeData.Values["action"] = "Error";
routeData.DataTokens["area"] = "app";
routeData.Values["exception"] = debugModeMsg;
IController errorsController = new ErrorController();
var exception = HttpContext.Current.Server.GetLastError();
var httpException = exception as HttpException;
if (httpException != null)
{
Response.StatusCode = httpException.GetHttpCode();
switch (System.Web.HttpContext.Current.Response.StatusCode)
{
case 504:
routeData.Values["action"] = "Http404";
break;
}
}
var rc = new RequestContext
(
new HttpContextWrapper(HttpContext.Current),
routeData
);
errorsController.Execute(rc);
}
base.OnException(filterContext);
}
}
Now you can implement this Action Filter on Controller as well as on the Action only.Example:
I am going little off topic. I thought this is bit important to explain.
If you pay attention to the above highlighted part. I have specified the order of the Action Filter. This basically describes the order of execution of Action Filter. This is a situation when you have multiple Action Filters implemented over Controller/Action Method
This picture just indicates that let's say you have two Action Filters. OnActionExecution will start to execute on Priority and OnActionExecuted will start from bottom to Top. That means in case of OnActionExecuted Action Filter having highest order will execute first and in case of OnActionExecuting Action Filter having lowest order will execute first. Example below.
public class Filter1 : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
//Execution will start here - 1
base.OnActionExecuting(filterContext);
}
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
//Execution will move here - 5
base.OnActionExecuted(filterContext);
}
}
public class Filter2 : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
//Execution will move here - 2
base.OnActionExecuting(filterContext);
}
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
//Execution will move here - 4
base.OnActionExecuted(filterContext);
}
}
[HandleError]
public class HomeController : Controller
{
[Filter1(Order = 1)]
[Filter2(Order = 2)]
public ActionResult Index()
{
//Execution will move here - 3
ViewData["Message"] = "Welcome to ASP.NET MVC!";
return View();
}
}
You may already aware that there are different types of filters within MVC framework. They are listed below.
Authorization filters
Action filters
Response/Result filters
Exception filters
Within each filter, you can specify the Order property. This basically describes the order of execution of the Action Filters.
Use TempData for getting value.
Some feature about TempData
TempData is a dictionary object that is derived from TempDataDictionary class and stored in short lives session.
TempData is used to pass data from current request to subsequent request means incase of redirection.
It’s life is very short and lies only till the target view is fully loaded.
It’s required typecasting for complex data type and check for null values to avoid error.
It is used to store only one time messages like error messages, validation messages.

Best approach to don't request same info over and over

On my controller I have it inherit a MainController and there I override the Initialize and the OnActionExecuting.
Here I see what is the URL and by that I can check what Client is it, but I learned that for every Method called, this is fired up again and again, even a simple redirectToAction will fire the Initialization of the same controller.
Is there a better technique to avoid this repetition of database call? I'm using Entity Framework, so it will take no time to call the DB as it has the result in cache already, but ... just to know if there is a better technique now in MVC3 rather that host the variables in a Session Variable
sample code
public class MyController : MainController
{
public ActionResult Index()
{
return View();
}
}
public class MainController : Controller
{
public OS_Clients currentClient { get; set; }
protected override void Initialize(System.Web.Routing.RequestContext requestContext)
{
// get URL Info
string url = requestContext.HttpContext.Request.Url.AbsoluteUri;
string action = requestContext.RouteData.GetRequiredString("action");
string controller = requestContext.RouteData.GetRequiredString("controller");
object _clientUrl = requestContext.RouteData.Values["cliurl"];
if (_clientUrl != null && _clientUrl.ToString() != "none")
{
// Fill up variables
this.currrentClient = db.FindClientById(_clientUrl.ToString());
}
base.Initialize(requestContext);
}
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
// based on client and other variables, redirect to Disable or Login Actions
// ... more code here like:
// filterContext.Result = RedirectToAction("Login", "My");
base.OnActionExecuting(filterContext);
}
}
is it still best to do as:
public OS_Clients currentClient {
get {
OS_Clients _currentClient = null;
if (Session["CurrentClient"] != null)
_currentClient = (OS_Clients)Session["CurrentClient"];
return _currentClient;
}
set {
Session["CurrentClient"] = value;
}
}
It seems that you dealing with application security in that case I would suggest to create Authorization filter, which comes much early into the action. You can put your permission checking code over there and the framework will automatically redirect the user to login page if the permission does not meet AuthorizeCore.
Next, if the user has permission you can use the HttpContext.Items as a request level cache. And then you can create another ActionFilter and in action executing or you can use the base controller to get the user from the Httpcontext.items and assign it to controller property.
If you are using asp.net mvc 3 then you can use the GlobalFilters to register the above mentioned filters instead of decorating each controller.
Hope that helps.
In your base controller, you need to cache the result of the first call in a Session variable.
This makes sure the back-end (DB) is not called unnecessarily, and that the data is bound to the user's Session instead of shared across users, as would be the case with the Application Cache.

How to return different view, but presererve ViewModel in OnActionExecuting

I'm trying to return a different view if a condition is met. I want to preserve the Model passed into the view from the action.
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
var subAction = filterContext.RequestContext.RouteData.Values["subaction"].ToString();
var action = filterContext.RequestContext.RouteData.Values["action"].ToString();
if (!string.IsNullOrEmpty(subAction))
{
var view = (ViewResultBase) (filterContext.Result);
filterContext.Result = View(action + subAction, view.ViewData.Model);
}
base.OnActionExecuting(filterContext);
}
When doing it this way I get an "Object reference not set to an instance of an object.". Obviously, the Model haven't been set?
The reason I'm doing it this way, is that I want to keep the naming of the view as simple as possible. And my URL looks like this: /Global/Modules/Admin/Users/Create. That would return the view "UsersCreate". Which works. But the Model is either empty or null!
UPDATE
Actually. It just hit me. The behaviour is correct. Because I'm only returning a View. The action is never executed. The Users-action gets executed, but returns the view UsersCreate. How can I do a RedirectToAction kinda thing, without actually redirect.
OnActionExecuting is run before the action so no model exists yet. You may try with OnActionExecuted which is called once the action returns an ActionResult.
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
...
if (needToRedirect)
{
...
filterContext.Result = new RedirectResult(url);
return;
}
...
}

Is there a way to maintain IsAjaxRequest() across RedirectToAction?

If you don't want any context or an example of why I need this, then skip to The question(s) at the bottom!
In a bid to keep things tidy I initially built my application without JavaScript. I am now attempting to add a layer of unobtrusive JavaScript on the top of it.
In the spirit of MVC I took advantage of the easy routing and re-routing you can do with things like RedirectToAction().
Suppose I have the following URL to kick off the sign up process:
http://www.mysite.com/signup
And suppose the sign up process is two steps long:
http://www.mysite.com/signup/1
http://www.mysite.com/signup/2
And suppose I want, if JavaScript is enabled, the sign up form to appear in a dialog box like ThickBox.
If the user leaves the sign up process at step 2, but later clicks the "sign up" button, I want this URL:
http://www.mysite.com/signup
To perform some business logic, checking the session. If they left a previous sign up effort half way through then I want to prompt them to resume that or start over.
I might end up with the following methods:
public ActionResult SignUp(int? step)
{
if(!step.HasValue)
{
if((bool)Session["SignUpInProgress"] == true)
{
return RedirectToAction("WouldYouLikeToResume");
}
else
{
step = 1;
}
}
...
}
public ActionResult WouldYouLikeToResume()
{
if(Request.IsAjaxRequest())
{
return View("WouldYouLikeToResumeControl");
}
return View();
}
The logic in WouldYouLikeToResume being:
If it's an AJAX request, only return the user control, or "partial", so that the modal popup box does not contain the master page.
Otherwise return the normal view
This fails, however, because once I redirect out of SignUp, IsAjaxRequest() becomes false.
Obviously there are very easy ways to fix this particular redirect, but I'd like to maintain the knowledge of the Ajax request globally to resolve this issue across my site.
The question(s):
ASP.NET MVC is very, very extensible.
Is it possible to intercept calls to RedirectToAction and inject something like "isAjaxRequest" in the parameters?
OR
Is there some other way I can detect, safely, that the originating call was an AJAX one?
OR
Am I going about this the completely wrong way?
As requested by #joshcomley, an automated answer using the TempData approach:
This assumes that you have a BaseController and your controllers are inheriting from it.
public class AjaxianController : /*Base?*/Controller
{
private const string AjaxTempKey = "__isAjax";
public bool IsAjax
{
get { return Request.IsAjaxRequest() || (TempData.ContainsKey(AjaxTempKey)); }
}
protected override RedirectResult Redirect(string url)
{
ensureAjaxFlag();
return base.Redirect(url);
}
protected override RedirectToRouteResult RedirectToAction(string actionName, string controllerName, System.Web.Routing.RouteValueDictionary routeValues)
{
ensureAjaxFlag();
return base.RedirectToAction(actionName, controllerName, routeValues);
}
protected override RedirectToRouteResult RedirectToRoute(string routeName, System.Web.Routing.RouteValueDictionary routeValues)
{
ensureAjaxFlag();
return base.RedirectToRoute(routeName, routeValues);
}
private void ensureAjaxFlag()
{
if (IsAjax)
TempData[AjaxTempKey] = true;
else if (TempData.ContainsKey(AjaxTempKey))
TempData.Remove(AjaxTempKey);
}
}
To use this, make your controller inherit from AjaxianController and use the "IsAjax" property instead of the IsAjaxRequest extension method, then all redirects on the controller will automatically maintain the ajax-or-not flag.
...
Havn't tested it though, so be wary of bugs :-)
...
Another generic approach that doesn't require using state that I can think of may requires you to modify your routes.
Specifically, you need to be able to add a generic word into your route, i.e.
{controller}/{action}/{format}.{ajax}.html
And then instead of checking for TempData, you'd check for RouteData["ajax"] instead.
And on the extension points, instead of setting the TempData key, you add "ajax" to your RouteData instead.
See this question on multiple format route for more info.
This worked for me.
Please note that this doesn't require any session state which is a potential concurrency issue:
protected override void OnActionExecuted(ActionExecutedContext filterContext)
{
base.OnActionExecuted(filterContext);
if (this.Request.IsAjaxRequest)
{
if (filterContext.Result is RedirectToRouteResult)
{
RedirectToRouteResult rrr = (RedirectToRouteResult)filterContext.Result;
rrr.RouteValues.Add("X-Requested-With",Request.Params["X-Requested-With"]);
}
}
}
}
Perhaps you can add a AjaxRedirected key in the TempData property before doing the redirection?
One way to transfer state is to add an extra route parameter i.e.
public ActionResult WouldYouLikeToResume(bool isAjax)
{
if(isAjax || Request.IsAjaxRequest())
{
return PartialView("WouldYouLikeToResumeControl");
}
return View();
}
and then in the Signup method:
return RedirectToAction("WouldYouLikeToResume", new { isAjax = Request.IsAjaxRequest() });
// Don't forget to also set the "ajax" parameter to false in your RouteTable
// So normal views is not considered Ajax
Then in your RouteTable, default the "ajax" parameter to false.
Or another way to go would be override extension points in your BaseController (you do have one, right?) to always pass along the IsAjaxRequest state.
..
The TempData approaches are valid too, but I'm a little allergic of states when doing anything that looks RESTful :-)
Havn't tested/prettify the route though but you should get the idea.
I would just like to offer what I believe is a MUCH better answer than the current accepted one.
Use this:
public class BaseController : Controller
{
private string _headerValue = "X-Requested-With";
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
var ajaxHeader = TempData[_headerValue] as string;
if (!Request.IsAjaxRequest() && ajaxHeader != null)
Request.Headers.Add(_headerValue, ajaxHeader);
}
protected override void OnActionExecuted(ActionExecutedContext filterContext)
{
base.OnActionExecuted(filterContext);
if (Request.IsAjaxRequest() && IsRedirectResult(filterContext.Result))
TempData[_headerValue] = Request.Headers[_headerValue];
}
private bool IsRedirectResult(ActionResult result)
{
return result.GetType().Name.ToLower().Contains("redirect");
}
}
Then make all your controllers inherit from this.
What it does:
Before an action executes this checks to see if there is a value in TempData. If there is then it manually adds its value to the Request object's header collection.
After an action executes it checks if the result was a redirect. If it was a redirect and the request was an Ajax Request before this action was hit then it reads the value of the custom ajax header that was sent and stores it in temp data.
This is better because of two things.
It is shorter and cleaner.
It adds the request header to the Request object after reading the temp data. This allows Request.IsAjaxRequest() to work normally. No calling a custom IsAjax property.
Credit to: queen3 for his question containing this solution. I did modify it to clean it up a bit but it is his solution originally.
The Problem is in the Client-Cache.
To overcome this, just add a cachebreaker
like "?_=XXXXXX" to Location Url in the 302 Response.
Here is my working Filter. Regisiter it in the GlobalFilter Collection.
I added the Location Header to the Redirected Response, so the client script can get the destination url, in the ajax call. (for Google-Analytics)
public class PNetAjaxFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var request = filterContext.HttpContext.Request;
if(request.QueryString["_"] == "ajax")
{
filterContext.HttpContext.Request.Headers["X-Requested-With"] = "XMLHttpRequest";
request.QueryString.Remove("_");
}
}
public override void OnResultExecuted(ResultExecutedContext filterContext)
//public override void OnResultExecuting(ResultExecutingContext filterContext)
{
var context = filterContext.HttpContext;
if (!context.Request.IsAjaxRequest())
return;
var request = context.Request;
String noCacheQuery = String.Empty;
if (request.HttpMethod == "GET")
{
noCacheQuery = request.QueryString["_"];
}
else if (context.Response.IsRequestBeingRedirected)
{
var pragma = request.Headers["Pragma"] ?? String.Empty;
if (pragma.StartsWith("no-cache", StringComparison.OrdinalIgnoreCase))
{
noCacheQuery = DateTime.Now.ToUnixTimestamp().ToString();
}
else
{
//mode switch: one spezial cache For AjaxResponse
noCacheQuery = "ajax";
}
}
if (!String.IsNullOrEmpty(noCacheQuery))
{
if (context.Response.IsRequestBeingRedirected)
{
var location = context.Response.RedirectLocation;
if (location.Contains('?'))
location += "&_=" + noCacheQuery;
else
location += "?_=" + noCacheQuery;
context.Response.RedirectLocation = location;
}
else
{
var url = new UriBuilder(request.Url);
if (url.Port == 80 && url.Scheme == Uri.UriSchemeHttp)
url.Port = -1;
else if(url.Port == 443 && url.Scheme == Uri.UriSchemeHttps)
url.Port = -1;
if(!String.IsNullOrEmpty(url.Query))
url.Query = String.Join("&", url.Query.Substring(1).Split('&').Where(s => !s.StartsWith("_=")));
context.Response.AppendHeader("Location", url.ToString());
}
}
}
}
And here the jQuery:
var $form = $("form");
var action = $form.attr("action");
var $item = $("body");
$.ajax({
type: "POST",
url: action,
data: $form.serialize(),
success: function (data, status, xhr) {
$item.html(data);
var source = xhr.getResponseHeader('Location');
if (source == null) //if no redirect
source = action;
$(document).trigger("partialLoaded", { source: source, item: $item });
}
});

Resources