Let me answer myself. From MSDN (http://msdn.microsoft.com/en-us/library/system.attribute.typeid.aspx, http://msdn.microsoft.com/en-us/library/6w3a7b50.aspx):
When you define a custom attribute with
AttributeUsageAttribute.AllowMultiple set to true, you must override
the Attribute.TypeId property to make it unique. If all instances of
your attribute are unique, override Attribute.TypeId to return the
object identity of your attribute. If only some instances of your
attribute are unique, return a value from Attribute.TypeId that would
return equality in those cases. For example, some attributes have a
constructor parameter that acts as a unique key. For these attributes,
return the value of the constructor parameter from the
Attribute.TypeId property.
As implemented, this identifier is merely the Type of the attribute.
However, it is intended that the unique identifier be used to identify
two attributes of the same type.
To summarize:
TypeId is documented as being a "unique identifier used to identify two attributes of the same type". By default, TypeId is just the type of the attribute, so when two attributes of the same type are encountered, they're considered "the same" by MVC validation framework.
The implementation:
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)]
public sealed class SomeAttribute: ValidationAttribute
{
private string Parameter { get; set; }
public override object TypeId
{
get { return string.Format("{0}[{1}]", GetType().FullName, Parameter); }
}
public SomeAttribute(string parameter)
{
Parameter = parameter;
}
That way of TypeId creation is chosen over the alternatives below:
- returning new object - it is too much, instances would be always different,
- returning hash code based on string parameter - can lead to collisions (infinitely many strings can't be mapped injectively into any finite set - best unique identifier for string is the string itself, see here).
After it is done, server side validation cases work. Problem starts when this idea needs to be combined with unobtrusive client side validation. Suppose you have defined your custom validator in the following manner:
public class SomeAttributeValidator : DataAnnotationsModelValidator<SomeAttribute>
{
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
var rule = new ModelClientValidationRule {ValidationType = "someattribute"};
rule.ValidationParameters.Add(...)
yield return rule;
}
Having this:
public class Model
{
[SomeAttribute("first")]
[SomeAttribute("second")]
public string SomeField { get; set; }
results in following error:
Validation type names in unobtrusive client validation rules must be
unique. The following validation type was seen more than once:
someattribute
As said, the solution is to have unique validation types. We have to distinguish each registered attribute instance, which has been used to annotate a field, and provide corresponding validation type for it:
public class SomeAttributeValidator : DataAnnotationsModelValidator<SomeAttribute>
{
private string AnnotatedField { get; set; }
public SomeAttributeValidator(
ModelMetadata metadata, ControllerContext context, SomeAttribute attribute)
: base(metadata, context, attribute)
{
AnnotatedField = string.Format("{0}.{1}",
metadata.ContainerType.FullName, metadata.PropertyName);
}
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
var count = Storage.Get<int>(AnnotatedField) + 1;
Storage.Set(AnnotatedField, count);
var suffix = char.ConvertFromUtf32(96 + count); // gets a lowercase letter
var rule = new ModelClientValidationRule
{
ValidationType = string.Format("someattribute{0}", suffix)
};
(validation type has to consist only with lowercase letters - with the solution above, if you have more than 26 attributes - entire alphabet exhausted, expect an exception)
where Storage
is simple utility which stores data for the current http request:
internal class Storage
{
private static IDictionary Items
{
get
{
if (HttpContext.Current == null)
throw new ApplicationException("HttpContext not available.");
return HttpContext.Current.Items;
}
}
public static T Get<T>(string key)
{
if (Items[key] == null)
return default(T);
return (T)Items[key];
}
public static void Set<T>(string key, T value)
{
Items[key] = value;
}
}
Last, JavaScript part:
$.each('abcdefghijklmnopqrstuvwxyz'.split(''), function(idx, val) {
var adapter = 'someattribute' + val;
$.validator.unobtrusive.adapters.add(adapter, [...], function(options) {
options.rules[adapter] = {
...
};
if (options.message) {
options.messages[adapter] = options.message;
}
});
});
$.each('abcdefghijklmnopqrstuvwxyz'.split(''), function(idx, val) {
var method = 'someattribute' + val;
$.validator.addMethod(method, function(value, element, params) {
...
}, '');
});
For complete solution, go through this sources.