I'm trying to get the parameters of a request on exception and log them. How can I get action parameters from a request if the parameter is an object in asp.net mvc? I can only get the parameters if I send in the parameters like this:
public virtual ActionResult TestAction(string A,string B){
}
But not like this:
public virtual ActionResult TestAction(ObjectQuery query){
}
My code:
protected override void OnException(ExceptionContext filterContext)
{
string action = filterContext.RouteData.Values["action"].ToString();
string controller = filterContext.RouteData.Values["controller"].ToString();
string parameters = filterContext.HttpContext.Request.QueryString
}
EDIT:
QueryString returns empty:
You should use reflection to achieve this:
var parameters = MethodBase.GetCurrentMethod().GetParameters();
foreach (ParameterInfo param in parameters)
{
//do your logging here
}
I maybe over simplifying it, but you can get the parameters passed to the action using the Request.Form or Request.QueryString. The nice thing about them is they are of the type NameValueCollection which makes it easy to merge them together.
Then the exception is being thrown on the respective controller so you can get the method signature via reflection, albeit without the named parameter.
Using the simple code below properly gave the parameters passed to the action and doesn't care if it is a POST or GET operation.
protected override void OnException(ExceptionContext filterContext)
{
//Get all the info we need to define where the error occured and with what data
var param = new NameValueCollection {Request.Form, Request.QueryString};
var controller = filterContext.RouteData.Values["controller"].ToString();
var action = filterContext.RouteData.Values["action"].ToString();
var signature = filterContext.Controller.GetType().GetMethod(action).ToString();
//Let's write it to the stream in this case
Response.Write(string.Format("<h3>Controller: {0}</h3>", controller));
Response.Write(string.Format("<h3>Action: {0}</h3>", action));
Response.Write(string.Format("<h4>Signature: {0}</h4>", signature));
foreach (var key in param.AllKeys)
{
Response.Write(string.Format("<strong>Key:</strong> {0} = {1}<br />", key, param[key]));
}
Response.End();
}
There are some downsides I would imagine though. Collections will probably be represented in a very unfriendly way. Data which wasn't supposed to bind with the specific model will be shown as well.
Related
Is there a way to run request again from MVC exception filter?
I'd like to call the request again when I'm handling the exception in exception filter.
Lets suppose we have the following action and we have defined a custom exception filter as shown below. Now this action will generate error if the id is not provided or a string value is provided to the id.
[CustomExceptionFilter]
public string Welcome(int id)
{
return id.ToString();
}
Now as the action throws error. The following exception filter will handle the rest of the situation. What I have done here in this exception filter is that this will pick the controller name and action name from the RouteData, generate a random value and redirect back to this action.
This is a mere example that came to my mind, of course you can try different things with it.
public class CustomExceptionFilter : FilterAttribute, IExceptionFilter
{
public void OnException(ExceptionContext filterContext)
{
if (!filterContext.ExceptionHandled)
{
var rd = HttpContext.Current.Request.RequestContext.RouteData;
string controllerName = rd.GetRequiredString("controller");
string actionName = rd.GetRequiredString("action");
filterContext.Result = new RedirectResult($"/{controllerName}/{actionName}/{ new Random().Next(100, 200) }");
filterContext.ExceptionHandled = true;
}
}
}
I have a controller that accepts a model UpdateProductCommand like this:
public IHttpActionResult UpdateProduct(UpdateProductCommand command)
{
command.AuditUserName = this.RequestContext.Principal.Identity.Name;
// ....
}
For security issues, the AuditUserName field should never be set outside (from the API call).
How can I remove (or truncate) the value of this field from JSON request?
It can be achieved by a following ModelBinder:
using Newtonsoft.Json.Linq;
public class FieldRemoverModelBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
string content = actionContext.Request.Content.ReadAsStringAsync().Result;
JObject json = JObject.Parse(content);
JToken property = json.GetValue(bindingContext.ModelName, StringComparison.OrdinalIgnoreCase);
property?.Parent.Remove();
bindingContext.Model = json.ToObject(bindingContext.ModelType);
return true;
}
}
Use it like this:
public IHttpActionResult UpdateProduct(([ModelBinder(typeof(FieldRemoverModelBinder), Name = nameof(UpdateProductCommand.AuditUserName))]UpdateProductCommand command)
{
// here command.AuditUserName will always be empty, no matter what's in json
That's what DTOs are for.
You can just create another class (UpdateProductCommandDto for example) that has only the properties you need / want to be used as the input, and then you can just use something like Automapper to map it to a new instance of UpdateProductCommand.
We have an ASP.NET application. We cannot edit source code of controllers. But we can implement ActionFilter.
One of our controller action methods returns JSON. Is it possible to modify it in ActionFilter? We need to add one more property to a returned object.
Maybe, some other way to achieve it?
Found this interesting and as #Chris mentioned, though conceptually I knew this would work, I never tried this and hence thought of giving it a shot. I'm not sure whether this is an elegant/correct way of doing it, but this worked for me. (I'm trying to add Age property dynamically using ActionResult)
[PropertyInjector("Age", 12)]
public ActionResult Index()
{
return Json(new { Name = "Hello World" }, JsonRequestBehavior.AllowGet);
}
And the filter:
public class PropertyInjector : ActionFilterAttribute
{
string key;
object value;
public PropertyInjector(string key, object value)
{
this.key = key;
this.value = value;
}
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
var jsonData = ((JsonResult)filterContext.Result).Data;
JObject data = JObject.FromObject(jsonData);
data.Add(this.key,JToken.FromObject(this.value));
filterContext.Result = new ContentResult { Content = data.ToString(), ContentType = "application/json" };
base.OnActionExecuted(filterContext);
}
}
Update
If it's not dynamic data which is to be injected, then remove filter constructor and hard code key & value directly and then the filter could be registered globally without editing the controller
GlobalFilters.Filters.Add(new PropertyInjector());
I want to make sure that a particular parameter in the QueryString, in my case the request_id is propagated to the redirected action.
Say for example, I have an Action First,
[HttpPost]
public ActionResult First()
{
////////////////////
// Lots of code ...
////////////////////
return RedirectToAction("Second");
}
Now say, the First postback had a parameter in the QueryString, which I would like to pass to the Second action. One way to do it would be to pass the value in the RedirectToAction call itself,
string requestId = Request.QueryString[REQUEST_ID_KEY];
return RedirectToAction("Second", new { REQUEST_ID_KEY = requestId });
But I have to do this in a series of Actions and I am unwilling to incorporate request id propagation logic inside the action. It would be better if I could incorporate this inside an ActionFilter, but I cant figure out how to add parameters to the QueryString from an ActionFilter. Any ideas?
public class PreserveQueryStringAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
var redirectResult = filterContext.Result as RedirectToRouteResult;
if (redirectResult == null)
{
return;
}
var query = filterContext.HttpContext.Request.QueryString;
// Remark: here you could decide if you want to propagate all
// query string values or a particular one. In my example I am
// propagating all query string values that are not already part of
// the route values
foreach (string key in query.Keys)
{
if (!redirectResult.RouteValues.ContainsKey(key))
{
redirectResult.RouteValues.Add(key, query[key]);
}
}
}
}
and then:
[HttpPost]
[PreserveQueryString]
public ActionResult First()
{
////////////////////
// Lots of code ...
////////////////////
return RedirectToAction("Second");
}
If you need it in subsequent action than please add it that param in Session or TempData (But need to re-assign in each action) so you dont need to pass it as a querystring in each action. In case of session, once you done with all actions than remove that key from the Session.
Here is a blogpost I wrote on how to fluently add querystring parameters in the action
Subject says it all: I'm creating a secure and generic wrapper to access a WCF service transparently.
A little background:
what I have done is that I've created similar business classes definition both on server and client. The server-side contains the actual logic while the client side contains only method definitions. Additionally, the client-side classes derive from a SecureFactory which implements a Call method. The body of every method of each derived class contains only a call to Call method. This Call method is responsible for invoking the server service passing such things as to the type of business class and which of its method to invoke to perform the requested operation.
This approach is being designed in order to simplify security checks by restricting passing of security information to only between SecureFactory and Server service. There are tuns of other benefits which you most of already aware of.
Now here's the issue: I'm stuck at as to how to return objects (especially arrays of objects) from Server to Call method?
The server may return a single business object (DataContract applied) as well as list of such objects. Since it's a generic approach, I have only Object to be used as return type. Following is the Call method
public object Call(params object[] parameters)
{
var mb = (new StackFrame(1).GetMethod());
using (Proxy.ServerClient server = new Security.BO.Proxy.ServerClient())
{
try
{
if (((MethodInfo)mb).ReturnType.IsGenericType)
{
var response = server.InvokeForList(SecurityManager.Current.SID, SecurityManager.Current.Token, mb.DeclaringType.ToString(), mb.Name, parameters);
return response.Result.ToList();
}
else
{
var response = server.Invoke(SecurityManager.Current.SID, SecurityManager.Current.Token, mb.DeclaringType.ToString(), mb.Name, parameters);
return response.Result;
}
}
catch (Exception ex)
{
System.Diagnostics.Debugger.Break();
}
}
return null;
}
server methods:
public CollectionResponse InvokeForList(string SID, string token, string type, string method, object[] parameters)
{
// Validation here
var t = assemblyBO.GetType(type, true);
object BO = Activator.CreateInstance(t);
var mi = t.GetMethod(method);
if (mi == null)
throw new MethodNotImplementedException("Method " + method + " could not be found on type " + t.ToString());
object res = mi.Invoke(BO, parameters);
// Convert list to t[]
object list = res.GetType().GetMethod("ToArray").Invoke(res, new object[0]);
// Error here! cannot convert t[] into object[]
return new CollectionResponse((object[])list);
}
The other method Invoke(...) is similar accept it returns Response object instead of CollectionResponse.
Here's the CollectionResponse class: (Response is similar:just it takes only one object)
[DataContract]
public class CollectionResponse
{
[DataMember]
private Object[] _result;
public Object[] Result
{
get
{
return _result;
}
}
public CollectionResponse(Object[] result)
{
this._result = result;
}
}
Initially I was thinking to have only one Invoke for both lists and singleton – but failed with "Connection was closed unexpectedly." still I'm not able to achieve – how can I convert T[] into object[].
Do you have any suggestion to improve it, or any other way of achieving the same?
Thanks
I can see an immediate problem here. You are using reflection which is far less perfromant than the direct call.
For me, that is enough not to follow this route.