First, your first option above is acceptable. The fact that another dev might not trap for a failure is not a huge drawback. This is always the case with validation: You catch it as early and elegantly as you can, but the main thing is to keep the integrity of your data. Simpler to just have a constraint in the database and trap for that though, right? But yes, you want to catch it as early as possible.
If you have the scope and time, better would be to have a smarter object that would handle saving, and perhaps other things you need handled consistently. There's many ways of doing this. It might be a wrapper for your entity. (See Decorator pattern, though I prefer to have my object consistently have a Data
property for access to the entity). It might require a relevant entity in order to be instantiated. Your controller would give the entity to this smart object to save (again, perhaps instantiating this smart object with your entity.) This smart object would know all the validation logic necessary and make sure it happens.
As an example, you could create JobRole business object. ("busJobRole", bus prefix for "business".) It could have a collection DataExceptions
. Your Controller takes the JobRole entity posted back, instantiates a busJobRole, and calls a method SaveIfValid
, which would return true if the item was saved successfully and false if there were validation problems. You then check the busJobRoles DataExceptions
property for the exact problems, and fill your modelstate, etc. Maybe like this:
// Check ModelState first for more basic errors, like email invalid format, etc., and react accordingly.
var jr = new busJobRole(modelJobRole);
if (jr.SaveIfValid == false) {
ModelState.AddModelError(jr.DataExceptions.First.GetKey(0), jr.DataExceptions.First.Get(0))
}
We follow this model so consistently, and I made an extension method for ModelState to accept a NameValue collection (returned by business objects) (vb.net version):
<Extension()> _
Public Sub AddModelErrorsFromNameValueCollection(
ByVal theModelState As ModelStateDictionary,
ByVal collectionOfIssues As NameValueCollection,
Optional ByRef prefix As String = "")
If String.IsNullOrEmpty(prefix) Then
prefix = ""
Else
prefix = prefix & "."
End If
For i = 0 To CollectionOfIssues.Count - 1
theModelState.AddModelError(prefix & CollectionOfIssues.GetKey(i),
CollectionOfIssues.Get(i))
Next
End Sub
this allows for quick, elegant adding of exceptions (determined by business objects) to the ModelState:
ModelState.AddModelErrorsFromNameValueCollection(NewApp.RuleExceptions, "TrainingRequest")
Your worry that other developers might not follow a plan you set up is very valid and good thinking. That's why your scheme needs to be consistent. For example, in my current project, I have two categories of classes that act as I've described. If they're very light, and just deal with caching and validation, they are "data manager" classes (ex: BureauDataManager). Some are true business domain object, very comprehensive, which I prefix with "bus" (ex: busTrainingRequest). The former all inherit from a common base class, to ensure consistency (and reduce code of course). Consistency allows for true encapsulation, for code discoverability, for the right code being in the right (single) place.