Clean up your Web API controllers with model validation and null check filters

Β· 844 words Β· 4 minutes to read

Regardless of what kind of applications you build with Web API, you are bound to write a lot of similar code in many of your actions - to check if the model is valid and to check if the submitted object is null.

This creates unnecessary noise and repetition. In this short blog post, let’s have a quick at how you can delegate this kind of repetitive logic to Web API filters.

More after the jump.

The problem πŸ”—

In many of the MVC/Web API actions you are forced by the framework to do two types of checks - if the Model is valid or if it the model is not null.

I don’t think I have to spend too much time describing the problem. Let me show you a bit of code and you will immediately see what’s wrong here.

public void Post(Team team)  
{  
if (team != null) {  
if (!ModelState.IsValid)  
{  
throw new HttpResponseException(HttpStatusCode.BadRequest);  
}

//do stuff with team  
}  
}  

or



public void Post(string text)  
{  
if(text == null) {  
throw new HttpResponseException(HttpStatusCode.BadRequest);  
}  
//do stuff  
}  

Dealing with the problem using custom filters πŸ”—

We can easily fix the redundancy in our controllers with two simple Web API filters. Remember - Web API, not MVC - so members of the System.Web.Http.Filters not System.Web.Mvc.Filters namespace.

Model validity πŸ”—

public class ValidateModelStateAttribute : ActionFilterAttribute  
{  
public override void OnActionExecuting(HttpActionContext actionContext)  
{  
if (!actionContext.ModelState.IsValid)  
{  
actionContext.Response = actionContext.Request.CreateErrorResponse(  
HttpStatusCode.BadRequest, actionContext.ModelState);  
}  
}  
}  

This is really simple - prior to executing the Web API controller, we peek into the ModelState dictionary, which contains collection of all errors and if it’s not valid we throw the status code 400 (bad request) back at client with the ModelState attached, as it wil contain the errors from our DataAnnotations or, for that matter, any other validation logic built around IValidateableObject.

Now we can see it in action, given a simple model:

public class Team  
{  
[Required]  
public string Name { get; set; } 

[MaxLength(6)]  
public string League { get; set; }  
}  

I will post a completely invalid object, without a name and too long league property:

{"League":"strange long name"}  

You can see we return a nice an informative message alongside error 400. You can obviously project to some sort of a view model to carry that information, instead of using ModelState directly but that’s really up to you.

Dealing with null checks πŸ”—

Another interesting problem is how to eliminate null checks. Obviously if we use Data Annotations and ModelState for complex objects, we can slightly eliminate the need to check for null, but not always.

The problem is if you submit, for example an empty JSON, ModelState would kick in and model will be invalid -all good. But if you submit a completely empty request, the ModelState will be treated as valid (!) and null value passed to the controller which, without a null check, would likely throw an exception!

Moreover, with primitive input (i.e. string, int) we don’t even have data annotations, and then we have to rely on null check.

We can solve the problem by writing another filter:

[AttributeUsage(AttributeTargets.Method, Inherited = true)]  
public class CheckModelForNullAttribute : ActionFilterAttribute  
{  
private readonly Func<Dictionary<string, object>, bool> _validate;

public CheckModelForNullAttribute() : this(arguments =>  
arguments.ContainsValue(null))  
{ }

public CheckModelForNullAttribute(Func<Dictionary<string, object>, bool> checkCondition)  
{  
_validate = checkCondition;  
}

public override void OnActionExecuting(HttpActionContext actionContext)  
{  
if (_validate(actionContext.ActionArguments))  
{  
actionContext.Response = actionContext.Request.CreateErrorResponse(  
HttpStatusCode.BadRequest, "The argument cannot be null");  
}  
}  
}  

This is only slightly more complex. We accept a single input paramter when the argument is declared - a Func to provide a plumbing mechanism for custom logic. By default we simply check - if the argument is null we invalidate the request.

I set the attribute usage to allow inheritance, since this type of attribute you might consider putting i.e. on an abstract base class (you might, for exmple, have the base RESTController class which defines CRUD operations etc).

Now, to use it you simply declare the attribute and try sending a null value:

[CheckModelForNull]  
public void Delete([FromBody]int? id)  

As expected, the request is rejected:

Notice I used nullable int - as otherwise a null would turn into a value 0.

The same will happen with complex types or strings - the filter can also be applied to them. A good example is if we post a null to an action that requires a Team object, like we did before.

Now we are super protected:

[CheckModelForNull]  
[ValidateModelState]  
public void Post(Team team) {  
//do stuff with team directly  
}  

If you wish, you can register the filters globally to avoid having to declare them each time:

GlobalConfiguration.Configuration.Filters.Add(new CheckModelForNullAttribute());  
GlobalConfiguration.Configuration.Filters.Add(new ValidateModelStateAttribute());  

Summary πŸ”—

With these two simple filters you can make your life much, much simpler and your Web API controllers code cleaner. MVC and now Web API framework do a great job at providing us developers with plumbing mechanisms for plugging in common utilities and validation checks that otherwise would have to be repeated inside of controllers.

And you know, the cleaner the controllers, the easier it is to manage your application.

About


Hi! I'm Filip W., a cloud architect from ZΓΌrich πŸ‡¨πŸ‡­. I like Toronto Maple Leafs πŸ‡¨πŸ‡¦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁒󠁳󠁣󠁴󠁿.

You can find me on Github and on Mastodon.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP