-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Add [ValidateNever]
and IPropertyValidationFilter
#5676
Conversation
var shouldValidate = context.PropertyAttributes.OfType<IShouldValidate>().FirstOrDefault(); | ||
if (shouldValidate == null) | ||
{ | ||
// No [ValidateNever] on the property. Check if container has this attribute. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll update this comment
/// <summary> | ||
/// <see cref="IShouldValidate"/> implementation that unconditionally indicates a property should be excluded from | ||
/// validation. When applied to a property, the validation system excludes that property. When applied to a type, | ||
/// the validation system excludes all properties within that type. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When applied to a type, system behaves much the same as when an SuppressChildValidationMetadataProvider
exists for the same type. Should probably recommend [ValidateNever]
for types users own and SuppressChildValidationMetadataProvider
for types they don't. For example, MVC doesn't use SuppressChildValidationMetadataProvider
any place [ValidateNever]
could be used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That seems like good guidance to put in the docs!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean instead of or in addition to here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ping
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both approaches shown here seem pretty reasonable.
I think we might need a hybrid - IE provide an attribute like [ValidateNever]
but also an abstraction like IShouldValidate
. The requests that we've gotten around "optional" properties fall into the latter, whereas simpler usage would lead to [ValidateNever]
.
{ | ||
/// <summary> | ||
/// Provides and caches <see cref="IModelValidator"/>s for <see cref="ModelMetadata"/>. | ||
/// </summary> | ||
public class ValidatorCache |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably clean this up a bit more before going public with it.
I'd do a few things to it:
- Rename it to
ValidatorFactory
and make it abstract - this lets us hide the constructor - Make it live in DI
- Make it the 'owner' of the the composite
IValidatorProvider
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would then be similar in design to a few other things we already have in the system
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Outdated comment. No longer make ValidatorCache
public.
protected virtual bool ShouldValidateEntry(ValidationEntry entry, ValidationEntry parentEntry) | ||
{ | ||
return true; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Love it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Outdated comment. No longer have (or need) this ValidationVisitor
extensibility point. But, I'll assume you love the IShouldValidate
interface 😺
MetadataProvider, | ||
validationState); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this works out pretty well
/// <returns>List of <see cref="IModelValidator"/>s for <paramref name="metadata"/>.</returns> | ||
public IReadOnlyList<IModelValidator> GetValidators( | ||
ModelMetadata metadata, | ||
IModelValidatorProvider validatorProvider) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is another validator provider besides the composite ever used here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope. But, this isn't that relevant because the PR no longer makes ValidatorCache
public.
/// Gets a value that indicates whether this model should be validated. | ||
/// </summary> | ||
/// <value>Defaults to <c>true</c>.</value> | ||
public abstract bool Validate { get; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI this is a breaking change. To do it in a non-breaking way we'd make it virtual non-abstract
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 Good catch and it applies equally to the new ShouldValidate
property addition. I'll file a bug on the API Checker that it misses this. I'll file the same bug on my brain but won't track that in GitHub.
@@ -34,6 +36,26 @@ public void CreateValidationMetadata(ValidationMetadataProviderContext context) | |||
} | |||
} | |||
} | |||
|
|||
// [ValidateNever] on a type affects properties in that type, not properties that have that type. Thus, | |||
// we ignore context.TypeAttributes for properties and don't check at all for types. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the opposite of what I'd imagine... is that consistent with BindNever
and friends?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this is consistent with [BindNever]
and friends. I copied the binding provider code from there. And, this is exactly the area where the [BindNever]
docs were ambiguous.
{ | ||
// No [ValidateNever] on the property. Check if container has this attribute. | ||
validateNever = context.Key.ContainerType.GetTypeInfo() | ||
.GetCustomAttributes(typeof(ValidateNeverAttribute), inherit: true) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would be better to use GetCustomAttribute
(singular) if the attribute has AllowMultiple = false
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OR the alternative would be to introduce an interface here that ValidateNeverAttribute
implements - that's generally what we've tried to do so this policy can be combined with others
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is where we land with IShouldValidate
.
/// <summary> | ||
/// Indicates that a property should be excluded from validation. When applied to a property, the validation | ||
/// system excludes that property. When applied to a type, the validation system excludes all properties within | ||
/// that type. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The value of usage on a type seems unclear to me because it's equivalent to ValidateChildren = false
right?
Wondering if we should just have a separate attribute for that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This still applies w/ IShouldValidate
and the new [ValidateNever]
. But, I'm not sure I agree this is a bad idea. For one thing, the choices exactly match how [BindNever]
et cetera can be used. For another, ValidateChildren = false
and its SuppressChildValidationMetadataProvider
wrapper configure something that we use metadata for in most analogous situations.
As I said here, my recommendation would be to use [ValidateNever]
on types you own and SuppressChildValidationMetadataProvider
on types you don't.
/// system excludes that property. When applied to a type, the model binding system excludes all properties of that | ||
/// type. | ||
/// system excludes that property. When applied to a type, the model binding system excludes all properties within | ||
/// that type. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this say defined by
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could but I'm not sure it's much more clear than the with
/ within
a type distinction I've made. I'll let you make the call…
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ping
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"defined by" is the technically correct term so I'd rather us do that
throw ex.InnerException; | ||
} | ||
} | ||
_entry = new ValidationEntry(property, key, () => GetModelOnMono(_model, property.PropertyName)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we still need this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lost a comment somewhere: See ValidateNeverProperty_IsSkippedWithoutAccessingModel()
. That and similar tests fail with NREs before the IShouldValidate
implementation is called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They fail on mono or on CLR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Model
accesses fail everywhere.
But, I now suspect you were asking specifically about the Mono workaround. We have repeatedly chosen to leave all similar workarounds alone. I wasn't going to spend the time checking whether the problem has been fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok dope
That's where this PR lands. I reverted the "move stuff" commit and redid the rest of the first approach. |
BTW you are the one that told me to look at the individual commits 😆 If you are unhappy with the quality of feedback then you only have yourself to blame. |
Not unhappy at all! Just trying to make it clear which comments on individual commits still apply and which don't. |
@dougbu, I dont see any modification to SimpleTypeModelBinder.cs. When no value is supplied for a value type a validation error is added there! So a check should be performed there, too! |
/// If <c>null</c>, properties with this <see cref="ModelMetadata"/> are validated. | ||
/// </summary> | ||
/// <value>Defaults to <c>null</c>.</value> | ||
public abstract IShouldValidate ShouldValidate { get; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to come up with a better name for this. Ideas I have IPropertyValidationFilterProvider
- IPropertyValidationFilter
. Basically I like the idea that this would become more similar to something already have in terms of naming and UI
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The analogues to IPropertyFilterProvider
aren't very solid. For one thing, what would the Attribute
s implement and how would users change the Func<T, bool>
? Do you mean the Attribute
s would implement IPropertyValidationFilter
and that would contain a method which returns a method which returns a bool
? If that's the case, where would IPropertyValidationFilterProvider
be used that Func<T, bool>
wouldn't be more appropriate?
For another, the names get very confusing unless we rename IPropertyFilterProvider
to IPropertyBindingFilterProvider
.
And, this feels like it adds a third (counting ValidateChildren
) "primary" mechanism to provide static control of property validation. Not sure we need anything extra just for custom IValidationMetadataProvider
support.
And, this feels like we're creating another layer of indirection to ease configuration in a custom IValidationMetadataProvider
. We don't have a requirement for two mechanisms here.
Let's take this offline…
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Decided offline that only the name needs to change.
Going with IPropertyValidationFilter
. If this was a(nother) MVC do-over, might consider a new name for IPropertyFilterProvider
(something more obviously binding-related) but not touching that now.
throw ex.InnerException; | ||
} | ||
} | ||
_entry = new ValidationEntry(property, key, () => GetModelOnMono(_model, property.PropertyName)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They fail on mono or on CLR?
/// <param name="entry"><see cref="ValidationEntry"/> to check.</param> | ||
/// <param name="parentEntry"><see cref="ValidationEntry"/> containing <paramref name="entry"/>.</param> | ||
/// <returns><c>true</c> if <paramref name="entry"/> should be validated; <c>false</c> otherwise.</returns> | ||
bool ShouldValidateEntry(ValidationEntry entry, ValidationEntry parentEntry); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One thing that I don't love about this is that it doesn't provide you the opportunity to 'close over' the property you are attached to (if you are attached to a property) it's a parameter.
I also don't really love that we're making ValidationEntry
more prominent.
For instance
[MyShouldValidate]
public class Widget
{
public string Name { get; set; }
}
public class MyShouldValidateAttribute : Attribute
{
public bool ShouldValidateEntry(ValidationEntry entry, ValidationEntry parentEntry)
{
// right here I need to look at entry.Metadata to figure out what property we're looking at
}
}
But then again if you're attached to a property you will always be called with the same metadata each time. These feel like different tasks and a lot of the information that's relevant for one is irrelevant for the other.
What if we did something like this? GetPropertyValidationFilter
would be called once for each property and could close over any metadata details that are significant.
public class OptionalOfTValidateAttribute : Attribute, IPropertyValidationFilter
{
public Func<object, bool> GetPropertyValidationFilter(ModelMetadata property)
{
return container => property.ValueGetter(container) != default(Optional<T>);
}
}
public class ValidateNeverAttribute : Attribute, IPropertyValidationFilter
{
public Func<object, bool> GetPropertyValidationFilter(ModelMetadata property)
{
return container => false;
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The requirement is to support dynamic decisions about property validation. ModelMetadata
is insufficient and any ValidationEntry.Model
access before the filter says we should validate is problematic. Note ValidationEntry
is already a public
abstraction.
Again, let's take this offline.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🙈 was the (considered) offline decision
/// <summary> | ||
/// <see cref="IShouldValidate"/> implementation that unconditionally indicates a property should be excluded from | ||
/// validation. When applied to a property, the validation system excludes that property. When applied to a type, | ||
/// the validation system excludes all properties within that type. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That seems like good guidance to put in the docs!
That's not technically a validation error. The model binding system runs before validation and also adds errors to There's a simple workaround: Make the optional If you'd like to discuss this particular scenario further, please open a separate issue. |
- #5642 - lazy-load `ValidationEntry.Model` - avoids `Exception`s when moving to a property that will not be validated nits: - remove duplicate code in `ValidationVisitor` - clarify "all properties of" doc comments - also add missing `<param>` doc in `ViewDataInfo`
- undo breaking change! - `[I]ShouldValidate` -> `[I]PropertyValidationFilter` - self-imposed comment on comment
90edf48
to
722eeef
Compare
IShouldValidate
and [ValidateNever]
[ValidateNever]
and IShouldValidate
🆙📅
|
[ValidateNever]
and IShouldValidate
[ValidateNever]
and IPropertyValidationFilter
ValidationEntry.Model
Exception
s when moving to a property that will not be validatednits:
ValidationVisitor
<param>
doc inViewDataInfo
was:
See individual commits for detailsfirst and third commits are about nits hit while doing this workother early commits require users to subclassDefaultObjectValidator
andValidationVisitor
required a breaking change to theValidationVisitor
constructor because that's effectivelyinternal
nowcan go back to that if we decide current approach has downsides