Tuesday, October 4, 2011

Data Annotations and Mixing Concerns

I'm sure you can tell that I'm a huge proponent of separation of concerns. Modular components shouldn't mix, they should be properly encapsulated and be concerned only with what they're supposed to do, not what other components are supposed to do. Many times, this puts me at odds with the various tooling support that's out there. This is especially true here in the Microsoft stack where I comfortably live.

I came across an example yesterday that got me thinking about these tools and how they mix one's concerns. Here's the code that I saw:

public class Application
{
  [Required]
  [Display(Name = "First Name")]
  public string ApplicantFirstName { get; set; }

  [Required]
  [Display(Name = "Last Name")]
  public string ApplicantLastName { get; set; }

  [Display(Name = "Birth Date")]
  public DateTime ApplicantBirthDate { get; set; }

  [Required]
  [Display(Name = "Cellphone Number")]
  public string ApplicantCellphoneNumber { get; set; }

  [Display(Name = "Postal Address")]
  public string PostalNumber { get; set; }

  [Display(Name = "Suburb")]
  public string ApplicantSuburb { get; set; }

  [Display(Name = "City")]
  public string ApplicantCity { get; set; }

  [Display(Name = "Post Code")]
  public string ApplicationPostalCode { get; set; }

  [Required]
  [Display(Name = "Email Address")]
  public string ApplicantEmailAddress { get; set; }
}

It's pretty simple, just a POCO with some basic fields on it. But what immediately bothered me was all those annotations on the properties. At first I just thought, "Those really pollute the model and make it look messy." But I couldn't just accept that thought. I needed to quantify my opinion in a more concrete way.

Let's look at the first attribute that we see:

[Required]

What does this do? Well, as far as I can tell, it instructs the UI tooling to make this a "required" field in whatever form is being generated for this model. But is it really required? I guess that depends on where you think the validation lives. But in this case, the validation is happening only in the UI. But that UI code exists in the model. Not ideal, if you ask me. And since the validation is only in the UI, this is perfectly legal:

var app = new Application();
// The "required" field currently has no value
app.ApplicantFirstName = string.Empty;
// The "required" field still has no value

Not very "required" if you ask me. Sure, the UI has some automated magic in place if you use some specific tools, but what if you don't use those tools? What if you have different UIs? One of them may be enforcing this (maybe), but others aren't.

It occurs to me then that this sort of thing is handy for very simple applications. Applications where you don't have different layers interacting across different enterprise levels. Only one application will ever use this model, it will always use the same tools and technologies, no other application will ever share its database, etc. But, honestly, isn't that a very limiting attitude? Have we never seen a situation where someone says "this will always be true" and then a new requirement comes in which breaks that assumption?

So this annotation leaves us with a polluted model. But more to the point it leaves us with a false sense of validation. That field isn't actually required. It just says it is. The model should be the source of truth for enforcing business requirements, and this model isn't doing that. How about something like this?:

public class Application
{
  private string _applicantFirstName;
  public string ApplicantFirstName
  {
    get { return _applicantFirstName; }
    set
    {
      if (string.IsNullOrEmpty(value) ||
          string.IsNullOrWhiteSpace(value))
        throw new ArgumentException("First Name is a required field");
      _applicantFirstName = value;
    }
  }

  private Application() { }

  public Application(string firstName)
  {
    if (string.IsNullOrEmpty(firstName) ||
        string.IsNullOrWhiteSpace(firstName))
      throw new ArgumentException("First Name is a required field");
    ApplicantFirstName = firstName;
  }
}

Now that's a required field. Was that so difficult to write? Not really. Is it difficult to understand? I don't think so. Sure, it's significantly more code than the data annotation version. But it works. It works everywhere. No matter what code uses this model, no matter what tooling generates the UI, no matter what you do this model requires that field. It can't exist without it. The business logic is strictly enforced in the business layer, not sometimes casually suggested to some UI layers.

(Sure, you'll also want to write more validation logic in the UI to make for a better user experience. Just getting an exception within the controller on the first incorrect field isn't ideal. We'll cover validation a lot more in a later post. Suffice it to say, there's more than one kind of validation. And it's OK to write the same checks in multiple places as long as they're for different purposes.)

Now let's take a look at that other annotation:

[Display(Name = "First Name")]

Display? This is the model, not the UI. Why are display concerns happening in the model? Again, this makes it easy for some auto-generating tooling to create forms for you. I get that. But what happens when you want to change that form? You either can't because the tooling doesn't support what you want to do, or you have to change the model for a simple UI change. I don't particularly like either of those options.

And what happens when this model is being used by multiple applications within the business? Marketing required that the field displays as "First Name" in their application because users (customers) understand it. HR, however, wants it to be called "Given Name" because their users are accustomed to that label in another off-the-shelf system they use. So who wins?

Since the UI is being defined in the model, your options are to either create a second model as a completely separate application for the other department or to tell the departments that they need to agree on a name because it can't be different. Again, what kind of options are those? It's far, far too limiting. The field label in the UI is entirely a UI concern. It can be "First Name" or "Given Name" or "Foo Baz" or anything it's required to be within that UI. The application-level logic translates the UI fields to models to interact with the business logic. At least, that's the way it should be.

I get it that these things reduce the amount of code people have to write for simple applications, I really do. But I just see this sort of thing as being far too limiting. Personally, I'd much rather have an understanding of constructors and property setters and exception handling and domain modeling than have an understanding of one particular Microsoft widget tool that works only in specific environments (where that tool is supported and used).

Maybe I'd be happier if I developed a taste for Microsoft's Kool-Aid and enjoyed it in blissful ignorance. But I just can't. We live in an age of frameworks and I truly enjoy how they've made things easier by adding more layers of abstraction to the foundations of software development. I even use a number of frameworks all the time (StructureMap and Agatha are still high on my list of favorites, and that's not even mentioning the various DAL frameworks I use often). But there's a difference between using a framework to help you do something and letting the framework completely take over your development.

Don't be afraid to write a little code. Saving a few lines isn't worth it at the expense of mixing your concerns.

5 comments:

  1. MinimumLength is also an annotation that is valid, making your extra code redundant. My 2 cents

    ReplyDelete
  2. I'm sure there are a good number of useful annotations, and that's certainly one example. By no means am I saying that my version is perfect and should never be changed, I'm merely pointing out that model concerns should be kept in the model and UI concerns should be kept in the UI. And that, additionally, one should be careful with implicit coding (such as with these annotations) to make sure one isn't instilling a false sense of data validation in the code.

    The required field in the original example isn't actually required. That's all.

    ReplyDelete
  3. Curious... I'd actually like to test this, but I can't seem to find the MinimumLength annotation. Is it in a specific .NET version or specific assembly that I don't have?

    I'm curious how it handles object creation, a la my constructor implementation vs. just the annotation. I suspect that, with a default constructor, it doesn't enforce the rule. I'm also curious how it handles whitespace.

    I still think my implementation is a more complete enforcement of the business rule at hand, I'm just curious about the behavior of the various annotations to reduce the amount of code. (Keeping in mind that reducing the code isn't enough if the business logic isn't completely enforced any longer.)

    ReplyDelete
  4. Do you mean the StringLengthAttribute? It seems a little unwieldy with MinimumLength being a named parameter like that. Either way, what does this attribute even do?

    If I have the following (to specify a maximum length):

    public class Application
    {
    [StringLength(5)]
    public string ApplicantFirstName { get; set; }
    }

    Then this doesn't produce an error:

    app.ApplicantFirstName = "this is a test";

    Shouldn't it? Or did I not use the attribute correctly? Then if I try to specify a minimum length:

    public class Application
    {
    [StringLength(5, MinimumLength=1)]
    public string ApplicantFirstName { get; set; }
    }

    Nothing here produces an error:

    var app = new Application();
    // First Name currently has invalid data.
    app.ApplicantFirstName = string.Empty;
    // First Name still has invalid data.
    app.ApplicantFirstName = " ";
    // First Name still has invalid data,
    // at least according to the business rules.
    app.ApplicantFirstName = "this is a test";

    Adding back the "Required" attribute also fails to produce an error in the above usage. I fail to see how this is enforcing the data validation at all. I've removed my "redundant" code, but at the same time I'm no longer enforcing the business rule that a first name is required and must contain a non-whitespace value.

    I'm sure these attributes produce some effect elsewhere in the system, perhaps in the UI or in the DAL. But my point remains. The business logic isn't being enforced in the business layer, the model. And the UI logic is being coupled with the business layer. If these attributes have an effect on a particular UI implementation, that's great. But what if a different UI implementation ignores them? Then the code is in a position where it looks like the business rules should be enforced, but they aren't.

    And since it's all being "enforced" through implicit annotations rather than explicit checks and conditions, there's no actual validation code to throw an error. So the error can go unnoticed for some time (resulting in bad data being entered into the system) and be difficult to diagnose (since it's an implicit error in otherwise normal behavior of the system, not an actual exception being thrown).

    ReplyDelete