Prevent MVC Action method from executing if a parameter is null - c#

I've thought of a few ways of doing this but I want to get the community's view. I have a feeling that the answer is cringeworthily simple - I'm not afraid to look stupid (my kids took that fear away from me long ago!)
I'm writing an XML REST web service using MVC2. All XML Types that consumers of the web service will receive and send are governed by simple but extensive XSD, and these parameters will be bound from xml in the request body via a custom default model binder and value provider.
I have a goodly amount of controllers, each with a goodly amount of action methods (not excessive - just 'goodly' ;) ) - and in nearly every case these action methods are going to be accepting model types that are all reference types.
In practically every case it's going to be an error for the caller not to provide these parameter values, and as such a standard error message such as "The parameter {name} type:{ns:type} is required" can be sent back.
What I want to do is to be able to validate parameters are not null before an action method is executed; and then to return an ActionResult that represents the Error to the client (for this I already have an XMLResult type) without the action method itself having to validdate the parameters itself.
So, instead of:
public ActionResult ActionMethod(RefType model)
{
if(model == null)
return new Xml(new Error("'model' must be provided"));
}
Something like:
public ActionResult ActionMethod([NotNull]RefType model)
{
//model now guaranteed not to be null.
}
I know this is exactly the kind of cross-cutting that can be achieved in MVC.
It seems to me that either a base controller override of OnActionExecuting or a custom ActionFilter is the most likely way of doing this.
I'd also like to be able to extend the system so that it automatically picks up XML schema validation errors (added to ModelState during binding by a custom value provider) thus preventing the action method from continuing if any of the parameter values can't be loaded correctly because the XML request is badly formed.

Here's the implementation that I've come up with (while waiting for any better ideas :) )
It's a generic approach and I think is pretty scalable - allowing for hopefully a similar kind of depth to parameter validation as you get with model validation at the same time as providing the error auto-respond functionality (when model state contains one or more errors) that I was looking for.
I hope this isn't too much code for an SO answer(!); I had a load of documentation comments in there that I've taken out to keep it shorter.
So, in my scenario I have two types of model error that, if they occur, should block execution of the action method:
Failed schema validation of the XML from which a parameter value will be constructed
Missing (null) parameter value
Schema validation is currently performed during model binding, and automatically adds model errors to the ModelState - so that's great. So I need a way to perform the auto-null check.
In the end I created two classes to wrap up the validation:
[AttributeUsage(AttributeTargets.Parameter,
AllowMultiple = false, Inherited = false)]
public abstract class ValidateParameterAttribute : Attribute
{
private bool _continueValidation = false;
public bool ContinueValidation
{ get { return _continueValidation; } set { _continueValidation = value; } }
private int _order = -1;
public int Order { get { return _order; } set { _order = value; } }
public abstract bool Validate
(ControllerContext context, ParameterDescriptor parameter, object value);
public abstract ModelError CreateModelError
(ControllerContext context, ParameterDescriptor parameter, object value);
public virtual ModelError GetModelError
(ControllerContext context, ParameterDescriptor parameter, object value)
{
if (!Validate(context, parameter, value))
return CreateModelError(context, parameter, value);
return null;
}
}
[AttributeUsage(AttributeTargets.Parameter,
AllowMultiple = false, Inherited = false)]
public class RequiredParameterAttribute : ValidateParameterAttribute
{
private object _missing = null;
public object MissingValue
{ get { return _missing; } set { _missing = value; } }
public virtual object GetMissingValue
(ControllerContext context, ParameterDescriptor parameter)
{
//using a virtual method so that a missing value could be selected based
//on the current controller's state.
return MissingValue;
}
public override bool Validate
(ControllerContext context, ParameterDescriptor parameter, object value)
{
return !object.Equals(value, GetMissingValue(context, parameter));
}
public override ModelError CreateModelError
(ControllerContext context, ParameterDescriptor parameter, object value)
{
return new ModelError(
string.Format("Parameter {0} is required", parameter.ParameterName));
}
}
With this I can then do this:
public void ActionMethod([RequiredParameter]MyModel p1){ /* code here */ }
But this on its own doesn't do anything of course, so now we need something to actually trigger the validation, to get the model errors and add them to model state.
Enter the ParameterValidationAttribute:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method,
Inherited = false)]
public class ParameterValidationAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var paramDescriptors = filterContext.ActionDescriptor.GetParameters();
if (paramDescriptors == null || paramDescriptors.Length == 0)
return;
var parameters = filterContext.ActionParameters;
object paramvalue = null;
ModelStateDictionary modelState
= filterContext.Controller.ViewData.ModelState;
ModelState paramState = null;
ModelError modelError = null;
foreach (var paramDescriptor in paramDescriptors)
{
paramState = modelState[paramDescriptor.ParameterName];
//fetch the parameter value, if this fails we simply end up with null
parameters.TryGetValue(paramDescriptor.ParameterName, out paramvalue);
foreach (var validator in paramDescriptor.GetCustomAttributes
(typeof(ValidateParameterAttribute), false)
.Cast<ValidateParameterAttribute>().OrderBy(a => a.Order)
)
{
modelError =
validator.GetModelError(filterContext, paramDescriptor, paramvalue);
if(modelError!=null)
{
//create model state for this parameter if not already present
if (paramState == null)
modelState[paramDescriptor.ParameterName] =
paramState = new ModelState();
paramState.Errors.Add(modelError);
//break if no more validation should be performed
if (validator.ContinueValidation == false)
break;
}
}
}
base.OnActionExecuting(filterContext);
}
}
Whew! Nearly there now...
So, now we can do this:
[ParameterValidation]
public ActionResult([RequiredParameter]MyModel p1)
{
//ViewData.ModelState["p1"] will now contain an error if null when called
}
To complete the puzzle we need something that can investigate the model errors and automatically respond if there are any. This is the least tidy of the classes (I hate the name and the parameter type used) and I'll probably change it in my project, but it works so I'll post it anyway:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method,
Inherited = false)]
public abstract class RespondWithModelErrorsAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
ModelStateDictionary modelState =
filterContext.Controller.ViewData.ModelState;
if (modelState.Any(kvp => kvp.Value.Errors.Count > 0))
filterContext.Result = CreateResult(filterContext,
modelState.Where(kvp => kvp.Value.Errors.Count > 0));
base.OnActionExecuting(filterContext);
}
public abstract ActionResult CreateResult(
ActionExecutingContext filterContext,
IEnumerable<KeyValuePair<string, ModelState>> modelStateWithErrors);
}
In my application I have an XmlResult that takes a Model instance and serializes to the response using either DataContractSerializer or XmlSerializer - so I've then created RespondWithXmlModelErrorsAttribute that inherits from this last type to formulate one of those with the model as an Errors class that simply contains each of the model errors as strings. The Response Code is also automatically set to 400 Bad Request.
Thus, now I can do this:
[ParameterValidation]
[RespondWithXmlModelErrors(Order = int.MaxValue)]
public ActionResult([RequiredParameter]MyModel p1)
{
//now if p1 is null, the method won't even be called.
}
In the case of web pages this last stage won't necessarily be required, since model errors are typically included in a re-rendering of page that sent the data in the first place, and the existing MVC approach suits this fine.
But for web services (either XML or JSON) being able to offload error reporting to something else makes writing the actual action method a lot easier - and much more expressive, I feel.

Well you could add constraints using regular expressions to individual route values. Then, if these constraints are not upheld, the action method will not be hit:
routes.MapRoute ("SomeWebService", "service/{userId}",
new { controller = "Service", action = "UserService" },
new { userId = #"\d+" });
Alternatively you could create custom constraints to validate route values together as a pack. This would probably be a better strategy for you. Have a look here: Creating a Custom Route Constraint

Related

Custom validation attribute is not called in Asp.Net Mvc for Url binding, but works for FromBody binding

I have observed a strange behavior in Mvc and not sure if it's a bug or a feature.
I have a customer validation attribute MyAttribute and use it e.g. like this:
public async Task<IActionResult> GetData([MyAttribute] string myparam)
Surprisingly enough the attribute (IsValid function) is never called if this parameter is not in url, but it gets called if the parameter is in Url, but empty (e.g. myParam=). It also gets called if I have a required attribute:
public async Task<IActionResult> GetData([MyAttribute, Required] string myparam)
Now if i have a request class like this:
public class MyRequest
{
[MyAttribute]
public string Test {get;set;}
}
public async Task<IActionResult> GetData([FromBody] MyRequest myparam)
Then the attribute gets called even without Required parameter.
Is there a bug somewhere here or is it intended to be like that?
First it will be very helpful if we debug with Asp.net core source code. See more here: debug-net-core-source-visual-studio-2019.
The key point of this issue in source code is: EnforceBindRequiredAndValidate.
Here is the source code:
private void EnforceBindRequiredAndValidate(
ObjectModelValidator baseObjectValidator,
ActionContext actionContext,
ParameterDescriptor parameter,
ModelMetadata metadata,
ModelBindingContext modelBindingContext,
ModelBindingResult modelBindingResult)
{
RecalculateModelMetadata(parameter, modelBindingResult, ref metadata);
if (!modelBindingResult.IsModelSet && metadata.IsBindingRequired)
{
// Enforce BindingBehavior.Required (e.g., [BindRequired])
var modelName = modelBindingContext.FieldName;
var message = metadata.ModelBindingMessageProvider.MissingBindRequiredValueAccessor(modelName);
actionContext.ModelState.TryAddModelError(modelName, message);
}
else if (modelBindingResult.IsModelSet)
{
// Enforce any other validation rules
baseObjectValidator.Validate(
actionContext,
modelBindingContext.ValidationState,
modelBindingContext.ModelName,
modelBindingResult.Model,
metadata);
}
else if (metadata.IsRequired)
{
// We need to special case the model name for cases where a 'fallback' to empty
// prefix occurred but binding wasn't successful. For these cases there will be no
// entry in validation state to match and determine the correct key.
//
// See https://github.com/aspnet/Mvc/issues/7503
//
// This is to avoid adding validation errors for an 'empty' prefix when a simple
// type fails to bind. The fix for #7503 uncovered this issue, and was likely the
// original problem being worked around that regressed #7503.
var modelName = modelBindingContext.ModelName;
if (string.IsNullOrEmpty(modelBindingContext.ModelName) &&
parameter.BindingInfo?.BinderModelName == null)
{
// If we get here then this is a fallback case. The model name wasn't explicitly set
// and we ended up with an empty prefix.
modelName = modelBindingContext.FieldName;
}
// Run validation, we expect this to validate [Required].
baseObjectValidator.Validate(
actionContext,
modelBindingContext.ValidationState,
modelName,
modelBindingResult.Model,
metadata);
}
}
If we add required attribute The condition 'else if(metadata.IsRequired)' will be hit, and then it will call Validate function.
Here is the differences between issue request and normal request:
the value of modelBindingResult is very important. if the result contains success then it will call validate in function EnforceBindRequiredAndValidate
From the source code we could find this is by-design behavior.
var modelBindingContext = DefaultModelBindingContext.CreateBindingContext(
actionContext,
valueProvider,
metadata,
parameter.BindingInfo,
parameter.Name);
modelBindingResult is coming from above context. You will find that we need to provide related provider and parameter then modelBindingResult.IsModelSet will be set to true, at this time the function validate will be called.

Use FromQuery model binder in output formatter

I have a custom OutputFormatter in my .net core project. In there I want to use some info inside the querystring of the initial request.
Inside the controller this is nicely done with the FromQuery modelbinder, giving me an object to work with. I would like to have this object (model) in my output formatter as well.
Can I somehow call FromQuery as an instance or such, so I can pass in the HttpContext or the querystring even, to get the model?
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
{
// Want a model from my querystring here
}
Use HttpContext.Items. The objects placed in there will be cleaned up at the end of request. And you can pass even more with it, defaulted values or changed values (probably it can be a dangerous point if you are going to change binded object)
// GET api/values
[HttpGet]
public ActionResult Get([FromQuery] QData data)
{
HttpContext.Items["data"] = data;
.......
return Ok(....);
}
Also you can have multiple formatters for different types of requests.
public class Formatter : OutputFormatter
{
public override bool CanWriteResult(OutputFormatterCanWriteContext context)
{
return context.HttpContext.Items["data"] is QData;
}
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
{
var incoming = context.HttpContext.Items["data"] as QData;
.......
}
}
The same way you can put any other object in Items and work with it in formatter. In case of other object it can be more generic and stable solution as it relies on specific structure.

Avoid null model when no data is posted in Web API

This question is similar to what I want to achieve:
Avoiding null model in ASP.Net Web API when no posted properties match the model
But it's gone un-answered.
I have a route which takes a model that is a GET:
[HttpGet, Route("accounts")]
public AccountListResult Post(AccountListRequest loginRequest)
{
return accountService.GetAccounts(loginRequest);
}
The model is populated with additional data from an action filter.
In this case all that needs to be known is the UserId, which the action filter add's to the model based on cookie/header into passed in with the request.
I want to use all the default model binding in WebAPI but I want to avoid null objects.
I don't believe model binding solves my problem.
How do I replace the behaviour of Web API model binding so that instead of Null I receive a new instance when no parameters are passed in
This is closer to what I want to do except its per type which is tedious.
EDIT: Since the question is for Web API, I am posting the Web API solution also below.
You can do this as below in an Action Filter. The below code works only if your model contains default constructor.
Web API Implementation:
public override void OnActionExecuting(HttpActionContext actionContext)
{
var parameters = actionContext.ActionDescriptor.GetParameters();
foreach (var parameter in parameters)
{
object value = null;
if (actionContext.ActionArguments.ContainsKey(parameter.ParameterName))
value = actionContext.ActionArguments[parameter.ParameterName];
if (value != null)
continue;
value = CreateInstance(parameter.ParameterType);
actionContext.ActionArguments[parameter.ParameterName] = value;
}
base.OnActionExecuting(actionContext);
}
protected virtual object CreateInstance(Type type)
{
// Check for existence of default constructor using reflection if needed
// and if performance is not a constraint.
// The below line will fail if the model does not contain a default constructor.
return Activator.CreateInstance(type);
}
MVC Implementation:
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var parameters = filterContext.ActionDescriptor.GetParameters();
foreach (var parameter in parameters)
{
if (filterContext.ActionParameters.ContainsKey(parameter.ParameterName))
{
object value = filterContext.ActionParameters[parameter.ParameterName];
if (value == null)
{
// The below line will fail if the model does not contain a default constructor.
value = Activator.CreateInstance(parameter.ParameterType);
filterContext.ActionParameters[parameter.ParameterName] = value;
}
}
}
base.OnActionExecuting(filterContext);
}
#Sarathy's solution works, but doesn't trigger model validation on objects it creates. This can cause situations where an empty model was passed in to an action but ModelState.IsValid still evaluates true.
For my own purposes, it was necessary to trigger re-validation of the model in the event that an empty model object was created:
public override void OnActionExecuting(HttpActionContext actionContext)
{
var parameters = actionContext.ActionDescriptor.GetParameters();
foreach (var parameter in parameters)
{
object value = null;
if (actionContext.ActionArguments.ContainsKey(parameter.ParameterName))
value = actionContext.ActionArguments[parameter.ParameterName];
if (value != null)
continue;
value = Activator.CreateInstance(parameter.ParameterType);
actionContext.ActionArguments[parameter.ParameterName] = value;
var bodyModelValidator = actionContext.ControllerContext.Configuration.Services.GetBodyModelValidator();
var metadataProvider = actionContext.ControllerContext.Configuration.Services.GetModelMetadataProvider();
bodyModelValidator.Validate(value, value.GetType(), metadataProvider, actionContext, string.Empty);
}
base.OnActionExecuting(actionContext);
}

JSON.Net deserializer does not generate exception when passing an invalid value for an object

I have the following structure:
public class SampleEntity{
public string Name { get; set; }
public OtherEntity Relation { get; set; }
}
public class OtherEntity {
public string Name { get; set; }
}
When i make a call to update an object in my web.api with the following request body:
"{'Name':'Nome', 'Relation':''}"
The deserializer fills the object with null value, but i think the correct action is throw an exception like 'invalid value for field Relation' and i can return a status code 400.
I tried to make a custom converter to do this, but i'm not happy with the solution and i am quite concerned with the performance of this.
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.StartObject)
{
var #object = JObject.Load(reader);
var target = Activator.CreateInstance(objectType);
var objectProperties = target.GetType().GetProperties().Where(x => x.PropertyType.IsPrimitive == false && x.PropertyType != typeof(string));
foreach (var prop in objectProperties)
{
var value = #object[prop.Name];
if (value != null && value.ToString() == string.Empty)
throw new Exception();
}
serializer.Populate(#object.CreateReader(), target);
return target;
}
return reader.Value;
}
The Web API does not automatically return an error to the client when validation fails. It is up to the controller action to check the model state and respond appropriately. While #loop's answer will work, you may want to consider another option whereby you won't even have to enter your controller's action method.
To do this, you can create an action filter to check the model state before the controller action is even invoked.
For example:
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (actionContext.ModelState.IsValid == false)
{
actionContext.Response = actionContext.Request.CreateErrorResponse(
HttpStatusCode.BadRequest, actionContext.ModelState);
}
}
}
Note that you will STILL need to decorate your model with attributes that describe the validation rules. This would be similar to what #loop has suggested.
If model validation fails, this filter returns an HTTP response that contains the validation errors. In that case, the controller action is not invoked.
To apply this filter to all Web API controllers, add an instance of the filter to the HttpConfiguration.Filters collection during configuration:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Filters.Add(new ValidateModelAttribute());
// ...
}
}
Another option is to set the filter as an attribute on individual controllers or controller actions:
public class EntitiesController : ApiController
{
[ValidateModel]
public HttpResponseMessage Post(SampleEntity entity)
{
// ...
}
}
For a more detailed explanation, take a look at this article. To learn about the various model annotations that you can use to define validation rules, e.g. [Required], etc., have a look at this MSDN page.

Asp.net webapi enum parameter with default value

I have a controller
[HttpGet]
[RoutePrefix("api/products/{productId}")]
public HttpResponseMessage Products(int productId,TypeEnum ptype=TypeEnum.Clothes)
{
if(!Enum.IsDefined(typeOf(TypeEnum),ptype))
//throw bad request exception
else
//continue processing
}
Myenum is declared as
public TypeEnum
{
Clothes,
Toys,
Electronics
}
Currently if,some garbage value is passed it is getting converted into default value.
What I want to do is if i call the controller as api/products/1 then the ptype should be assigned default value i.e clothes. If I call the controller as api/products/1?pType=somegarbagevalue then the controller should throw bad request exception. How can I achieve this?
Defining all your enum parameters as strings and then parsing them everywhere means you have to do this on every single action and you will need to come up with a consistent approach such that all parsing errors conform.
This is a parameter binding issue and should not be dealt with in the controller layer, it should be taken care of in the pipeline. One way to do this is to create a custom filter and add it to your config.
public class ModelStateValidationAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (!actionContext.ModelState.IsValid)
{
actionContext.Response = <your standardised error response>
}
}
}
And in your global.asax.cs
...
GlobalConfiguration.Configure(WebApiConfig.Register);
...
public class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
...
config.Filters.Add(new ModelStateValidationAttribute());
...
}
}
If you're having trouble with the model state, it's type is a ModelStateDictionary and you simply iterate over it and then it's Errors property contains all the model binding issues. e.g.
modelState = actionContext.ModelState;
modelState.ForEach(x =>
{
var state = x.Value;
if (state.Errors.Any())
{
foreach (var error in state.Errors)
{
<work your magic>
}
}
});
You have to do with string and use TryParse() to convert string to Enum value.
public HttpResponseMessage Products(int productId,string ptype="Clothes")
{
TypeEnum category = TypeEnum.Clothes;
if(!Enum.TryParse(ptype, true, out category))
//throw bad request exception if you want. but it is fine to pass-through as default Cloathes value.
else
//continue processing
}
It may look naive but the benefit of this approach is to allow ptype parameter to whatever string and to perform process without exception when ptype fails to bind the value.
This type of validation should be handled in pipeline not in controller.
public abstract class ETagMatchAttribute : ParameterBindingAttribute
{
private ETagMatch _match;
public ETagMatchAttribute(ETagMatch match)
{
_match = match;
}
public override HttpParameterBinding GetBinding(HttpParameterDescriptor parameter)
{
if (parameter.ParameterType == typeof(ETag))
{
return new ETagParameterBinding(parameter, _match);
}
return parameter.BindAsError("Wrong parameter type");
}
}
something like this. refer to MSDN link for detailed explanation

Categories

Resources