Asp.net webapi enum parameter with default value - c#

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

Related

Create an IEnumerable<AbstractClass> of specific instances from different classes all inheriting from the abstract class

I have controllers which, for the sake of backwards compatibility, only have one action. The JSON request comes with an attribute "type" which determines what the action should do with it.
My idea for a clean solution was to build a set of action handlers. They all inherit from an abstract class called ActionHandler which has two methods
public abstract bool CanHandle(ClientRequest request);
and
public abstract object Handle(dynamic request)
And it has a property
public abstract string ActionForController { get; }
in which the specific actionhandlers just return the name of the controller they want to handle for. This is not very important, but may help clarify something later.
The controller is inserted with an ActionHandlerRegister which has an IEnumerable and a method "GetActionHandler". It returns the first specific ActionHandler that can handle the request.
public ActionHandler GetActionHandler(ClientRequest request)
{
foreach(var actionHandler in ActionHandlers)
{
if (actionHandler.CanHandle(request))
{
return actionHandler;
}
}
throw new BadRequestException(string.Format(CultureInfo.InvariantCulture, BadRequestExceptionTemplate, request.Type));
}
The controllers look like this:
public class LogController : ControllerBase
{
private readonly IActionHandlerRegister<LogController> logHandlers;
public LogController(IActionHandlerRegister<LogController> logHandlers)
{
this.logHandlers = logHandlers ?? throw new ArgumentNullException(nameof(logHandlers));
}
[HttpPost]
public async Task<IActionResult> Post([FromBody] dynamic rawJson)
{
var jsonBody = ((JsonElement)rawJson).ToString();
if (string.IsNullOrEmpty(jsonBody))
{
return BadRequest(ActionHandler.BadRequestRequestNullOrTypeMissingError);
}
var clientRequest = JsonSerializer.Deserialize<ClientRequest>(jsonBody);
if (clientRequest == null || string.IsNullOrEmpty(clientRequest.Type))
{
return BadRequest(ActionHandler.BadRequestRequestNullOrTypeMissingError);
}
try
{
var handler = logHandlers.GetActionHandler(clientRequest);
var result = handler.Handle(rawJson);
return Ok(result);
}
catch (BadRequestException ex)
{
return BadRequest(ex.Message);
}
}
}
For people paying attention: yes, I'm passing the rawjson to handler.Handle. This is because "ClientRequest" is something generic (from which I can read the type) but the handler needs the specific request, so it's deserializing again to something more specific. Maybe there are better solutions for that. Feel free to tell me.
In startup.cs, the insertion of the ActionHandlerRegister into the controller looks like this:
public void RegisterActionHandlersAsSingleton(IServiceCollection services)
{
IEnumerable<ActionHandler> listOfActionHandlers =
from domainAssembly in AppDomain.CurrentDomain.GetAssemblies()
from actionHandlerType in domainAssembly.GetTypes()
where actionHandlerType.IsAssignableFrom(typeof(ActionHandler))
select (ActionHandler)Activator.CreateInstance(actionHandlerType);
services.AddSingleton<IActionHandlerRegister<LogController>>(new ActionHandlerRegister<LogController>(listOfActionHandlers.Where(a => a.ActionForController == nameof(LogController))));
// other controllers ...
}
You might be able to guess, this last piece of code crashes at runtime telling me it's unable to cast to ActionHandler.
System.InvalidCastException: Unable to cast object of type
'System.Object' to type
'TcServerModules.ActionHandlers.ActionHandler'.
I have been playing around with different solutions, but none of them scratch that itch. What would be a nice, true-to OO-design principle

ASP.NET Core Filters - ignore for defined methods

We implement a log information to our database.
I will use a Filters (IActionFilter) functionality for it.
I wrote the following class:
public class ActionFilter: Attribute, IActionFilter
{
DateTime start;
public void OnActionExecuting(ActionExecutingContext context)
{
start = DateTime.Now;
}
public void OnActionExecuted(ActionExecutedContext context)
{
DateTime end = DateTime.Now;
double processTime = end.Subtract(start).TotalMilliseconds;
... some log actions
}
}
Then I have added the following code to the Startup.cs:
services.AddMvc(options => {
options.Filters.Add(typeof(ActionFilter));
});
It works fine. I get the breakpoint in ActionFilter for each my method.
But I want to ignore for logging the most part of methods.
As I understand, I can do it with my own attribute. I didn't work with own attributes before.
Ok, I wrote the following attribute:
public class IgnoreAttribute : Attribute
{
public IgnoreAttribute()
{ }
}
I added the attribute to method:
[Ignore]
[HttpGet]
[Route("api/AppovedTransactionAmountByDays/{daysCount}")]
public JsonResult GetAppovedTransactionAmountByDays(int daysCount)
{
var result = daysCount;
return new JsonResult(result);
}
Of course, there simple actions don't work.
How I must change my attribute or my ActionFilter for ignore of method?
Thanks in advance.
felix-b's note about naming is a good one.
And I want to make another note. You should not store state in the filter when you register it in this way. Since it is an attribute, it is instantiated only once! So you have a massive race condition there. One option would be to use:
services.AddMvc(o =>
{
o.Filters.Add(new ServiceFilterAttribute(typeof(LoggingActionFilter)));
});
And register it as transient:
services.AddTransient<LoggingActionFilter>();
Now the attribute is instantiated every time it is needed, so you can safely store state.
Configuring it so that it ignores the action if the marker attribute is present is also possible:
public class LoggingActionFilter : Attribute, IActionFilter
{
private DateTime start;
private bool skipLogging = false;
public void OnActionExecuting(ActionExecutingContext context)
{
var descriptor = (ControllerActionDescriptor)context.ActionDescriptor;
var attributes = descriptor.MethodInfo.CustomAttributes;
if (attributes.Any(a => a.AttributeType == typeof(SkipLoggingAttribute)))
{
skipLogging = true;
return;
}
start = DateTime.Now;
}
public void OnActionExecuted(ActionExecutedContext context)
{
if (skipLogging)
{
return;
}
DateTime end = DateTime.Now;
double processTime = end.Subtract(start).TotalMilliseconds;
}
}
public class SkipLoggingAttribute : Attribute
{
}
Here we get the action descriptor available from the parameter and find if the method in question has the SkipLogging attribute. If it does, skip the logging code.
First, I would strongly suggest renaming your ActionFilter to something more specific, like LogActionFilterAttribute (pay attention -- the action filter is an attribute). Now instead of applying it globally with options.Filters.Add(...), apply it only to the actions you want to log:
// an action that must be logged -- apply LogActionFilter attribute
[HttpGet]
[Route("api/....")]
[LogActionFilter]
public JsonResult FirstAction(...)
{
//...
}
// an action that should not be logged -- don't apply LogActionFilter
[HttpGet]
[Route("api/....")]
public JsonResult SecondAction(...)
{
//...
}
UPDATED VERSION
If you have an explicit requirement for the "Ignore" attribute, you can implement it as follows.
Leave the global configuration the way you did:
services.AddMvc(options => {
options.Filters.Add(typeof(LoggingActionFilter));
});
The implementation of LoggingActionFilter should change:
// this filter is applied globally during configuration of web application pipeline
public class LoggingActionFilter : IActionFilter
{
// we use private class types as keys for HttpContext.Items dictionary
// this is better than using strings as the keys, because
// it avoids accidental collisions with other code that uses HttpContext.Items
private class StopwatchItemKey { }
private class SuppressItemKey { }
public void OnActionExecuting(ActionExecutingContext context)
{
// here we save timestamp at the beginning of the request
// I use Stopwatch because it's handy in this case
context.HttpContext.Items[typeof(StopwatchItemKey)] = Stopwatch.StartNew();
}
public void OnActionExecuted(ActionExecutedContext context)
{
// check whether SuppressLoggingAttribute was applied to current request
// we check it here in the end of the request because we don't want to depend
// on the order in which filters are configured in the pipeline
if (!context.HttpContext.Items.ContainsKey(typeof(SuppressItemKey)))
{
// since SuppressItemKey was not set for the current request,
// we can do the logging stuff
var clock = (Stopwatch) context.HttpContext.Items[typeof(StopwatchItemKey)];
var elapsedMilliseconds = clock.ElapsedMilliseconds;
DoMyLoggingStuff(context.HttpContext, elapsedMilliseconds);
}
}
// SuppressLoggingAttribute calls this method to set SuppressItemKey indicator
// on the current request. In this way SuppressItemKey remains totally private
// inside LoggingActionFilter, and no one else can use it against our intention
public static void Suppress(HttpContext context)
{
context.Items[typeof(SuppressItemKey)] = null;
}
}
The "Ignore" attribute (I named it SuppressLoggingAttribute) will look like this:
// this filter attribute is selectively applied to controllers or actions
// in order to suppress LoggingActionFilter from logging the request
public class SuppressLoggingAttribute : Attribute, IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
// this will put "suppress" indicator on HttpContext of the current request
LoggingActionFilter.Suppress(context.HttpContext);
}
public void OnActionExecuted(ActionExecutedContext context)
{
}
}
Now you only need to apply the "ignore" attribute wherever necessary:
[HttpGet("{id}")]
[SuppressLogging]
public string Get(int id)
{
return "value";
}
Performance considerations
In contrast to #junnas' answer, my code doesn't use Reflection (MethodInfo.CustomAttributes), and thus it works faster.
If anyone questions the use of Stopwatch: yes, Stopwatch.StartNew() allocates a new Stopwatch object on the heap every request. But assigning DateTime to HttpContext.Items dictionary does the same because it implies boxing. Both DateTime and Stopwatch objects are of 64-bit size, so allocation-wise, both DateTime and Stopwatch options are equal.

Web API Controller not found when class is generic

I'm trying to use a Generic Controller in my Web API. My goal, which I am currently failing at, is to pass in an object from my front end that will have say a typeId. Based on this typeId I was going to use a factory to inject the correct class implementation of a generic interface. I believe my Factory, Interface and Service is correct, but for some reason when I add a Generic to the API I am getting a 404. It works without a generic and just a test method. I am using autofac for my IoC registration.
API Controller:
public class ListItemsController<T> : ApiControllerBase
{
private readonly IListItemsService<T> _service;
public ListItemsController(int listItemTypeId)
{
_service = ListItemsFactory<T>.InitializeService(listItemTypeId);
}
[HttpGet]
[Route("{listItemTypeId: int}")]
public IEnumerable<T> GetAll()
{
return _service.GetAll();
}
[HttpGet]
[Route("test")]
public IHttpActionResult Test()
{
return Ok();
}
}
Factory:
public class ListItemsFactory<T>
{
public ListItemsFactory(IPrimaryContext context) : base()
{
}
public static IListItemsService<T> InitializeService(int listItemType)
{
switch (listItemType)
{
case 1: return (IListItemsService<T>)
new FloorTypeService(new PrimaryContext());
default: return null;
}
}
}
Interface:
public interface IListItemsService<T>
{
IEnumerable<T> GetAll();
void Save(T obj);
T GetById(int id);
void Delete(int id);
}
Error:
No HTTP resource was found that matches the request URI 'http://localhost:9000/api/v1/listitems/test'. No type was found that matches the controller named 'listitems'.
I'm not sure what piece I'm missing here. I'm using routing attributes but here is my API config:
private static void SetupRoutes(HttpConfiguration config)
{
config.MapHttpAttributeRoutes(new CustomDirectRouteProvider());
config.Routes.MapHttpRoute("DefaultApi", "api/v{version}/{controller}/{id}",
new { id = RouteParameter.Optional });
}
Instead of resolving the type and trying to map to the right Controller, you also can create a Controller for each Type, which inherits from your GenericController. Then you don't have to copy the Code, but have a Controller for each Type, where you can route to by RouteAttribute.:
public class ListItemsController<T> : ApiControllerBase
{
//Properties/Fields should be protected to can be accessed from InstanceController.
protected readonly IListItemsService<T> _service;
// I think listItemTypeId is not necessary, if generic-type T is used?
public ListItemsController()
{
_service = ListItemsFactory<T>.InitializeService();
}
[HttpGet] // No need for RouteAttribute, because it will be in InstanceController.
public IEnumerable<T> GetAll()
{
return _service.GetAll();
}
[HttpGet]
[Route("test")] // This can rest here, because you want to use it.
public IHttpActionResult Test()
{
return Ok();
}
}
The implemented InstanceController can look like this:
[RoutePrefix("api/{controller}")]
public class FloorItemsController ListItemsController<Floor>
{
// delegate the Constructor-call to base()
public ListItemsController()
:base()
{
}
// No need to reimplement Methods.
}
The RouteConfiguration should be set back to default, because RouteAttributes are set for this.
Basically, what you'll need to do is to replace the controller activator, with a custom implementation.
First, createa class that implements the IHttpControllerSelector interface. Take a look at this link for some of the thing you should be aware before creating a custom activator. At the bottom there's a link to some code example of a custom implmentation.
Now, this depends on what your rules will actually be, but for perfomance reasons,you should try to build a solution that always map the same controller name to the same closed type of your generic controller type. A simple implementation for your case would look something like this:
public HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
IHttpRouteData routeData = request.GetRouteData();
if (routeData == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
//get the generyc type of your controller
var genericControllerType = typeof(ListItemsController<>);
// Get the route value from which you'll get the type argument from your controller.
string typeParameterArgument = GetRouteVariable<string>(routeData, 'SomeKeyUsedToDecideTheClosedType');
Type typeArgument = //Somehow infer the generic type argument, form your route value based on your needs
Type[] typeArgs = { typeof(typeArgument) };
//obtain the closed generyc type
var t = genericControllerType.MakeGenericType(typeArgs);
//configuration must be an instance of HttpConfiguration, most likeley you would inject this on the activator constructor on the config phase
new HttpControllerDescriptor(_configuration, t.Name, t);
}
Finally, on your ApiConfig class you'll need to add this line:
GlobalConfiguration.Configuration.Services.Replace(typeof(IHttpControllerSelector),
new MyOwnActivatior());
I can't test this code right now, so it might need some tweaking, but hopefully this will guide you on the right direction. Do take notice of the link i pasted above, since there are important considerations you'll need to take into account before implementing a custom activator. Also, check the code example linked on that post to see how to implement the GetControllerMapping method

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.

Prevent MVC Action method from executing if a parameter is null

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

Categories

Resources