9

This is the spiritual successor to my previous question Web API attribute routing and validation - possible?, which I think was too general to answer. Most of those issues are solved, but the default value question remains.

Basically I have solved many pieces of the puzzle. I have this:

[HttpGet]
[Route("test/{id}"]
public IHttpActionResult RunTest([FromUri]TestRequest request)
{
    if (!ModelState.IsValid) return BadRequest(ModelState);
    return Ok();
}

My TestRequest class:

public class TestRequest
{
    public string id { get; set; }

    [DefaultValue("SomethingDefault")]
    public string something { get; set; }
}

The problem is that if no parameter is in the query string for something, the model is "valid" and yet something is null.

If I specify a blank value for something (i.e. GET test/123?something=), then the default value comes into play, and the model is valid again.

Why is this? How can I get a default value into my model here? As a bonus, why is it when a parameter is not specified, the default value is not used, but when a blank string is explicitly specific, the default value is used?

(I've been trawling through the ASP.NET stack source code and am knee-deep in model binders and binding contexts. But my best guess can't be right - it looks like the DefaultValueAttribute is used only if the parameter value is null. But that's not the case here)

Community
  • 1
  • 1
Kieren Johnstone
  • 41,277
  • 16
  • 94
  • 144
  • Not meant as a criticism, but are you consuming this service yourself before passing out to external parties? (i.e. eating you own dogfood?). I am always of the impression that using the ModelState validation with a simple return of BadRequest is completely useless to your API user (devs being the user here), as its completely unclear what is wrong. Far better to have a rules engine and in the body of the badRequest pass back helpful error messages, model state is far from helpful. Same applies to attribute rooting with validation in your other question. – BMac May 28 '15 at 07:40
  • Macb - yes, this is an internal prototype. I like the idea of leveraging ModelState here for a few reasons: 1) the models are in the 'web layer' only, so can have layer-specific annotations, 2) annotations on a request are a great way of defining a contract, 3) we are looking at writing client-side components for Angular which can highlight appropriate fields with messages based on this and 4) I'm also keen to explore automatically generating API docs based on annotations. Lots of if statements hides all of this in code. Attrib routing is GREAT for "actually RESTful" APIs too :) – Kieren Johnstone May 28 '15 at 15:13
  • Sorry I may have misunderstood - you say a simple return is useless and unclear. But `return BadRequest(ModelState)` gives the caller a great JSON breakdown of why the request failed, and which fields caused it - unless I misunderstand? – Kieren Johnstone May 28 '15 at 15:14
  • my language was a little strong :-) it really depends on your API user, and from what you have described it sounds like you are building for internal use / use on your own external website - in which case the model validation is great. Personally I like a little control over what is being returned to the user, i.e a "Why" this failed kind of message over a "This" failed message you get with the model validation. For example suppose a "telephone" number field, it's nice to be told that the number does not match UK land-line format - not that it fails to match a given regex. – BMac May 28 '15 at 19:47
  • added to the above, I like my models to be inter-portable. For example, all my models are in a Portable Class library to allow reuse of the model on the consumer (xamarin iOS/Android, windows store apps), further to this we turn the dto objects into type script with http://type.litesolutions.net/ for our knockout web app, making max reuse of the c# models. – BMac May 28 '15 at 20:00
  • I can see why you took that approach. My prototype does too - well at least the relevant response models are in a different assembly, referenced by the client SDK assembly. The requests are more complex though, there's more to it - in a RESTful Web API, the request model might cover some of the request details, but overall there may be routing params, query string param, body params, headers - on the client side, I'd rather wrap these up in a single request object, or model in a better way, then let the SDK build a URL/body/etc as necessary. Model validation lets you give a nice message, btw – Kieren Johnstone May 28 '15 at 20:28
  • (i.e. `[RegularExpression(".*[a-z]*", ErrorMessage="Number doesn't match UK land-line format")]`) – Kieren Johnstone May 28 '15 at 20:29
  • always more than one way to skin the cat :-) can see why your going the model validation route. – BMac May 28 '15 at 21:57

2 Answers2

8

You need to initialize the default value in the constructor for your Model:

public class TestRequest
{
    public TestRequest()
    {
        this.something = "SomethingDefault";
    }

    public string id { get; set; }

    [DefaultValue("SomethingDefault")]
    public string something { get; set; }
}

Update:
With C# 6, you don't need to initialize it in the constructor anymore. You can assign the default value to the property directly:

public class TestRequest
{
    public string id { get; set; }

    [DefaultValue("SomethingDefault")]
    public string something { get; set; } = "SomethingDefault";
}

As documentation of the DefaultValueAttribute states:

Note

A DefaultValueAttribute will not cause a member to be automatically initialized with the attribute's value. You must set the initial value in your code.

In the case where you're providing no value for your something property, the property is initialized and the ModelBinder doesn't have a value to assign to it and thus the property defaults to its default value.

Konstantin Dinev
  • 34,219
  • 14
  • 75
  • 100
  • But it does populate my property automatically - when I specify it should be a blank string. I think the documentation means the attribute itself doesn't do it - but the model binder does, but not in the way I'm expecting – Kieren Johnstone May 27 '15 at 13:28
  • 1
    (Also, this doesn't work - did you try it, or perhaps in some way that I didn't? For me it's overwritten as null when the query parameter is a blank string) – Kieren Johnstone May 27 '15 at 13:29
  • Sorry, to be clear: if I use a constructor to initialise the property, and remove the DefaultValue attribute altogether, then it almost works - a missing parameter gives me the default. But a blank string in the parameter gives null instead. If I keep the DefaultValue attribute, in both cases (null and blank) I get the default value - so I can't specify a blank value – Kieren Johnstone May 27 '15 at 13:34
  • Basically: constructor parameter specifies a value for if no parameter is passed in; DefaultValue specifies a value for if a blank string is passed in. So if I put the default in the constructor, and `[DefaultValue(string.Empty)]`, then it works as I'd expect. Weird! – Kieren Johnstone May 27 '15 at 13:36
  • @KierenJohnstone I thought that was the expected behavior: when the query string specifies an empty value then the value of the something property should be empty. If you want further control over this then I guess the best thing you can do is implement the setter of the something property. – Konstantin Dinev May 27 '15 at 13:50
  • An empty value becoming an empty value IS what I'd expect, but it's not what happens (unless I do a nasty hack like say "DefaultValue(string.Empty)"). I think it boils down to the fact that a missing parameter becomes 'null' (not the default), but a present and empty parameter becomes the default (when IMO it should be a present and empty parameter). – Kieren Johnstone May 27 '15 at 16:41
4

Specifying the default in the constructor works for when no parameter is specified at all, but when a blank string is specified, null is put into the field instead.

As such, adding [DefaultValue("")] actually worked the best - when a blank string was specified, a blank string was passed in. Then the constructor can specify default values for when the parameter is missing.

To get around this, I've created PreserveBlankStringAttribute, derives from DefaultValueAttribute which is equivalent to [DefaultValue("")].

I would very much welcome a better answer than this, please.

Kieren Johnstone
  • 41,277
  • 16
  • 94
  • 144