tags:

views:

91

answers:

1

I have a repository data access pattern like so:

IRepository<T>
{
  .. query members ..
  void Add(T item);
  void Remove(T item);
  void SaveChanges();
}

Imagine the scenario where I have a repository of users, users have a user name which is unique, if I create a new user with a username that exists (imagine I have a dumb ui layer that doesn't check), when I add it to the repository, all is fine.. when I hit SaveChanges, my repository attempts to save the item to the database, my database is enforcing these rules luckily and throws me back an aborted exception due to a unique key violation.

It seems to me that, generally this validation is done at the layer ABOVE the repository, the layers that call it know they should ensure this rule, and will pre-check and execute (hopefully in some kind of transaction scope to avoid races, but doesn't always seem possible with the medium ignorance that exists).

Shouldn't my repository be enforcing these rules? what happens if my medium is dumb, such as a flat database without any integrity checks?

And if the repository is validating these kind of things, how would they inform callers about the violation in a way that the caller can accurately identify what went wrong, exceptions seem like a poor way to handle this because their relatively expensive and are hard to specialize down to a specific violation..

I've been playing around with a 'Can' pattern, for example.. CanAdd with Add, add will call CanAdd and throw an invalid operation exception if can returns a violation.. CanAdd also returns a list of violations about what went wrong.. this way I can start to stack these routines through the stack.. for example, the service layer above would also have a 'Can' method that would return the repositories report + any additional violations it wanted to check (such as more complicated business rules, such as which users can invoke specific actions).

Validation of data is such a fundamental yet I feel there is no real guidance for how to handle more advanced validation requirements reliably.


Edit, additionally in this case, how do you handle validation of entities that are in the repository and are updated via change tracking.. for example:

using (var repo = ...)
{
  var user = repo.GetUser('user_b');
  user.Username = 'user_a';

  repo.SaveChanges(); // boom!
}

As you could imagine, this will cause an exception.. going deeper down the rabbit hole, imagine I've got a validation system in place when I add the user, and I do something like this:

using (var repo = ...)
{
  var newUser = new User('user_c');
  repo.Add(newUser); // so far so good.

  var otherUser = repo.GetUser('user_b');
  otherUser.Username = 'user_c';

  repo.SaveChanges(); // boom!
}

In this case, validating when adding the user was pointless, as 'downstream' actions could screw us up anyway, the add validation rule would need to check the actual persistence storage AND any items queued up to persist.

This still doesn't stop the previous change tracking problem.. so now do I start to validate the save changes call? it seems like there would be a huge amount of violations that could happen from aparently unrelated actions.

Perhaps I'm asking for an unrealistic, perfect safety net?

Thanks in advance, Stephen.

+1  A: 

The ideal rule is that each of your layers should be a black box and none of them should depend on validation of another layer. The reason behind this is that the DB has no idea of the UI and vice versa. So when the DB throws an exception, the UI must have DB knowledge (bad thing) to convert that into something the UI layer can understand, so it can eventually convert it into something the user can understand. Ugh.

Unfortunately, making validation on every layer is also hard. My solution: Either put the validation in a single place (maybe the business layer) and make the other layers really dumb. They don't check anything elsewhere.

Or write your validation in an abstract way into the model and then generate all validation from that. For example:

String name;
Description nameDesc = new Description("name",
        new MaxLength(20), new NotNull());

This way, you can write code which examines the Description stuff (generate code or even at runtime) and do the validation in each layer with little cost because one change fixes all layers.

[EDIT] For validation, you only have these cases:

  • Duplicate key
  • Above some limit
  • Below some limit
  • Null (not specified)
  • Empty
  • Formatting error (date fields, etc)

So you should be able to get away with these exception classes which have object, field, old&new value plus special info like the limit that was hit. So I'm wondering where your many exception classes come from.

For your other question, this is ... uh ... "solved" by the two phase commit protocol. I say "solved", because there are situations when the protocol breaks down and in my experience, it's much better to give the user a "Retry?" dialog or some other means to fix the problem rather than investing a lot of time into TPC.

Aaron Digulla
Hi Aaron, what I have currently is somewhat similar, I yield a different object for each type of violation, all these violations would be returned to the original caller (ui layer), where the ui layer can identity violations and provide localized error messages for the violations.. *cont*
meandmycode
..the problem is that each violation is its own class, the reasoning behind this is that I might yield a 'invalid user name' exception that has a min and max length property that can be formatted into the localized error message.. I just seem to have an army of these violation classes.. *cont*
meandmycode
Additionally, the 'can' pattern doesnt make guarentees about the success of the method, but I guess this is understandable.. technically theres a race condition between asking 'can i do this' and saying.. ok do it, where a violation could occur (but I can somewhat accept this small window).
meandmycode
Aaron, you've been most helpful, thank you very much for sharing your knowledge and experience to help me rationalize a clean solution.
meandmycode