Hey gang, just a quickie today. I have been working recently on setting up server side and client side model validation “by the book” in ASP.NET MVC 2 complete with data annotations and the first question that hit me when I was declaring attributes on my LINQ to SQL partial classes to determine model validation was “hold on, surely there is a way to automagically have MVC infer my validation rules based on my database schema…”, it seemed a little silly to me that I would have to re-declare validation rules on my model classes when I have a perfectly, well-decorated set of rules within SQL Server, not to mention within the LINQ to SQL .dbml designer file, against my database objects. I also felt this was going against the DRY principle which is “Don’t Repeat Yourself”.
So, what if you could avoid the monotonous, not to mention need for duplicated code and have your model automatically validation based on your LINQ to SQL schema? (which in turn was resolved from the database.) and what would you expect it to do? Well we know when we declare VarChar‘s, NVarChar‘s and other text types in the database we specify a length in a fashion of VarChar(100) right? Well, this information serves for what our [StringLength] attributes could be.
The other, and probably more pressing check would be if we could automate our [Required] Attribute so we could determine if the field is required by the user for input. Well, thankfully our LINQ to SQL schema reveals that too; Let’s see how this could come together…
Firstly, the main idea and code skeleton for this approach came from Carl over at “Carl On Development” as he displayed quite an ingenious solution to the problem; His approach was to simply check the actual LINQ to SQL data type of the column of the class in question as a pre-validation check to address the field length problem, once having received the value stored in the brackets of VarChar(100) from the LINQ to SQL model, build a standard StringLengthAttribute class returning this newly sought value, the same as if you were to attribute your property as [StringLength(100)] yourself!
Unfortunately, his text editor appeared to have removed the “<” and “>” signs which made it difficult to determine what generic types he was calling upon in his function. However, after a bit of playing, I worked out what was going on and also then thought this could be taken further, and introduced another test for “Is Null” testing.
Enjoy, and tell me what you think (Remember, as this inherits from the AssociatedValidatorProvider class in the System.Web.Mvc namespace, there is no need to decorate any of your model, or partial model classes with any attributes, meaning no more work is required – the thing will just work once it is registered!):
public class SchemaValidatorProvider : AssociatedValidatorProvider
{
public static readonly string[] TextTypes = new string[] { "char", "varchar", "nchar", "nvarchar" };
public static readonly string LengthRegex = "[" + string.Join("|", TextTypes) + "]\\((.*)\\)";
public bool IsNotNull(ColumnAttribute column)
{
return !column.CanBeNull;
}
public int? ExtractLength(ColumnAttribute column)
{
var match = Regex.Match(column.DbType.ToLower(), LengthRegex, RegexOptions.Compiled);
if (match == null || match.Groups.Count != 2) return null;
return string.Compare(match.Groups[1].Value.ToLower(), "MAX", true) == 0 ? int.MaxValue : int.Parse(match.Groups[1].Value);
}
protected override IEnumerable GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable attributes)
{
var columnAttribute = attributes.OfType().FirstOrDefault();
if (columnAttribute == null || columnAttribute.DbType == null)
yield break;
// Determine max length of column and yield the StringLength attribute if found.
var maxLength = ExtractLength(columnAttribute);
if (maxLength.HasValue)
yield return new StringLengthAttributeAdapter(metadata, context, new StringLengthAttribute(maxLength.Value));
// Determine if state of column is not null, if so yield Required attribute.
if (IsNotNull(columnAttribute))
yield return new RequiredAttributeAdapter(metadata, context, new RequiredAttribute());
}
}
Then over in your Global.asax.cs:
public class MvcApplication : System.Web.HttpApplication
{
....
protected void Application_Start()
{
ModelValidatorProviders.Providers.Add(new SchemaValidatorProvider());
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
...
}
}
Look Ma! Nothing required on the model, it just works!
[MetadataType(typeof(ShareMetadata))]
public partial class User
{
private sealed class UserMetadata
{
// [Required] [StringLength(50)] - No longer required!
[DisplayName("The name")]
public string Name { get; set; }
[Range(1, 100)] // Still might be required depending on your business requirements!
[DisplayName("Cost Per Unit ($)")]
public decimal UnitCost { get; set; }
}
}
What this means is you will not have to declare any “typical” validation rules on your model classes at all, required fields and field length will be completely handled for you automatically without the need for any Data Annotation Attributes!
Hopefully this will cover both validation cases I could think of initially using the in-built, and very extensible ASP.NET MVC validation providers. I would be keen to know if this could be extended further for other checks.
4 Comments
This is a great concept. I’m having trouble compiling the code though. It says that GetValidators has no suitable method to override. It wants the “IEnumerable attributes” parameter to be of type IEnumerable, but then I get other errors with the code. Can you post the full code file including the usings?
This is great, but I’m having troubles using it. I have child.Name defined as not null. I also have a form for parent. ModelState for that form is not valid because of parent.child.Name is null. How can I avoid that?
You have mirrored my exact thoughts!!!
Fantastic post. I will give it a whirl.
I have been itching for the ASP.Net MVC team to hurry up and make this a reality, it seems crazy not to.
MVC 4 really needs to include some huge validation improvements because it is all over the place even in MVC 3.
For those having trouble getting this to compile, view the HTML source and copy the code from there.
< & > angle brackets are not rendering in the displayed HTML. so the Enumerable’s are missing their Attributes.
Great Concept, By the way and I love how you can override the defaults with normal decorations.
One Trackback/Pingback
Automatically Infer MVC Validation Rules Based on LINQ to SQL Schema ā Sweet! | {Programming} & Life…
Thank you for submitting this cool story – Trackback from DotNetShoutout…
Post a Comment