Added Addtional Validation Attributes#4084
Conversation
There was a problem hiding this comment.
We have existing cmdlet parameters where these new attributes should be used.
I think it make sense to start using the new attribute right away.
We also need to think through usage on all data types, and avoid introducing unnecessary new public apis (the base classes).
| Context "ValidatePositive" { | ||
| BeforeAll { | ||
| $testCases = @( | ||
| @{ sb = { function Local:foo { param([ValidatePositive()] [int] $bar) }; foo -1 }; FullyQualifiedErrorId = "ParameterArgumentValidationError,foo"; InnerErrorId = "ValidateRangeTooSmall" } |
There was a problem hiding this comment.
Also - this is hard to read with all keys on the same line - I suggest each key on it's own line.
There was a problem hiding this comment.
I'll get this cleaned up. I was trying to follow the existing format of the file. Is it cool to update those other test as part of this PR as well?
| { | ||
| /// <summary> | ||
| /// Minimum allowed value for the attribute | ||
| /// </summary> |
There was a problem hiding this comment.
What about other data types? Smaller should just work, but what about long, float, double or bigint?
There was a problem hiding this comment.
It's possible dynamic helps you here
In validate element, you would write:
dymamic val = element;
if (val > minValue) ...There was a problem hiding this comment.
Any tips on how to include the larger numbers? I was thinking if checking if the element has a CompareTo() but not sure if this is correct. For example, string also have compare to which would be an invalid type.
There was a problem hiding this comment.
Try my suggestion to use dynamic - it might just work. Behind the scenes, the C# compiler will generate code calling the correct operator after testing the type of val.
There was a problem hiding this comment.
sorry, never used this before. Updated to use dynamic
| /// <seealso cref="ValidatePositiveAttribute"/>, | ||
| /// <seealso cref="ValidateNonNegativeAttribute"/> | ||
| /// </remarks> | ||
| [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] |
There was a problem hiding this comment.
This base class is not generally useful, so I'd prefer avoiding introducing a new public type.
|
@dchristian3188 Please put a message in #2634 that you are starting the fix to exclude conflicts. |
We could exclude the new apis at all - if we want usage on all data types we should generalize existing ValidateRange(Range="Positive") |
|
@iSazonov - I was saying we don't need any unnecessary apis like the abstract base class used to share a tiny bit of code - that could easily be done without inheritance. Your suggestion might look like: public enum RangeKind {
Positive, NonNegative, Negative, NonPositive
}
// In ValidateRangeAttribute
RangeKind? _rangeKind;
public ValidateRange(RangeKind kind) { _rangeKind = kind; }
// in ValidateElement
if (_rangeKind.HasValue) { ... }
else { /* existing code */ }That might be fine too, I'm not sure I have a preference either way. |
|
@lzybkr Thanks for you prototype. I like it. We use the similar approach for enhancing ValidateSet with valid values generator. First locally I used inheritance, but your idea (to add new constructor) was better. So I believe we should use the same pattern here. Users will find it easier to know and apply the enhancements than the new apis. |
|
I think you guys are right. Extending Validate range does feel cleaner. |
|
|
||
| /// <Summary> | ||
| /// predefined range kind to use with | ||
| /// ValidateRangeAttribute |
There was a problem hiding this comment.
Please start with capital and end with point.
Also we can fit all on one line.
| /// predefined range kind to use with | ||
| /// ValidateRangeAttribute | ||
| /// </Summary> | ||
| public enum ValidateRangeKind{ |
There was a problem hiding this comment.
Please add space before bracket.
| public enum ValidateRangeKind{ | ||
|
|
||
| /// <Summary> | ||
| /// Range is greater than 0 |
There was a problem hiding this comment.
Please end with a point in all comments.
| /// <Summary> | ||
| /// Range is less than or equal to 0 | ||
| /// </Summary> | ||
| NonPositive |
There was a problem hiding this comment.< C2F2 /p>
I want to discuss the names. We could use names like math operations:
| Name1 | Name2 | Name3 |
|---|---|---|
| Positive | ValueGT0 | GT0 |
| Nonnegative | ValueGEzero | GEzero |
| Negative | ElementLT0 | LT0 |
| NonPositive | ElementLEzero | LEzero |
There was a problem hiding this comment.
My Vote is for
1 - Positive, Negative, NonPostive, NonNegative
2 - ValueGTZero, ValueGEZero, ValueGTZero, ValueGEZero
| if(_rangeKind.HasValue) | ||
| { | ||
| element = o.BaseObject; | ||
| dynamic val = element; |
There was a problem hiding this comment.
Maybe add a comment here what types we expect/process here?
| switch (_rangeKind) | ||
| { | ||
| case ValidateRangeKind.Positive: | ||
| if(val <= 0) |
There was a problem hiding this comment.
We should use CompareTo() as in original code.
| @{ | ||
| sb = { function foo { param([ValidateCount(2, 1)] [string[]] $bar) }; foo } | ||
| FullyQualifiedErrorId = "ValidateRangeMaxLengthSmallerThanMinLength" | ||
| InnerErrorId = "" |
There was a problem hiding this comment.
Indentation.
And below too.
| BeforeAll { | ||
| $testCases = @( | ||
| @{ | ||
| sb = { function foo { param([ValidateRange('Positive')] [int] $bar) }; foo -1 } |
There was a problem hiding this comment.
Please add test with the enum:
ValidateRange([ValidateRangeKind]::Positive)
I guess that's enough replace 'Positive' in one test and don't duplicate all tests.
There was a problem hiding this comment.
I tried this but it failed since ValidateRangeKind is not exposed as a type accelerator. Should i use the full namespace or add [ValidateRangeKind]?
| $validTestCases = @( | ||
| @{ | ||
| sb = { function foo { param([ValidateRange('Positive')] [int] $bar) }; foo 15 } | ||
| TestValue = 15 |
There was a problem hiding this comment.
We use LanguagePrimitives.TryConvertTo so we very "generic" here and we should add tests for non-numeric values.
| } | ||
| } | ||
|
|
||
| Context "ValidateRange - NonNegative" { |
There was a problem hiding this comment.
I think we can combine all ValidateRangeKind-s in one TestCases:
sb = { function foo { param([ValidateRange( '<TestCase-Parameter-Here>' )] [int] $bar) }; foo -1 }
| { | ||
| element = o.BaseObject; | ||
| // Using dynamic to handle different type of numbers such as | ||
| // int, double, decimal and float. |
There was a problem hiding this comment.
CompareTo() allow us be more general. I think we should enhance the comment.
| else | ||
|
|
||
| // minRange and maxRange have the same type, so we just need | ||
| // to compare to one of them |
There was a problem hiding this comment.
Please correct the comment formatting.
| element.ToString(), MaxRange.ToString()); | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Please remove the extra line.
There was a problem hiding this comment.
I see you're reusing existing error messages - one reason for this feature was to improve the error message.
And once we've settled on naming, I'd really like to see this new form of the attribute used in our own code base - there are 80 or so places that could use this.
| /// <Summary> | ||
| /// Predefined range kind to use with ValidateRangeAttribute. | ||
| /// </Summary> | ||
| public enum ValidateRangeKind { |
There was a problem hiding this comment.
Please follow the conventions of the file you are editing, e.g. braces normally go on their own line, and you have no space after if.
| switch (_rangeKind) | ||
| { | ||
| case ValidateRangeKind.Positive: | ||
| if(val.CompareTo(0) <= 0) |
There was a problem hiding this comment.
If the argument does not implement IComparable, the error message will not be good.
| element = o.BaseObject; | ||
| // Using dynamic to handle different type of numbers such as | ||
| // int, double, decimal and float. | ||
| dynamic val = element; |
There was a problem hiding this comment.
This might be too general - e.g. strings implement IComparable, but strings aren't positive or negative.
There was a problem hiding this comment.
I also see you omit the test for PSObject - you'll probably want it for your code as well - and you should also ensure you have tests to cover simple (non-wrapped) arguments and PSObject wrapped arguments.
There was a problem hiding this comment.
This might be too general - e.g. strings implement IComparable , but strings aren't positive or negative.
In general, we cannot know if the type is comparable with zero in advance. Should we test this?
There was a problem hiding this comment.
How do you feel about reusing the GetCommonType method from the validate range class?
We could use the incoming object and 0. I believe this will also throw the correct message if the user attempts to pass an incompatable object such as string
There was a problem hiding this comment.
Can you share an example where the element was a string and you expected an integer?
There was a problem hiding this comment.
Here's what i had for ValidateElement.
protected override void ValidateElement(object element)
{
if (element == null)
{
throw new ValidationMetadataException(
"ArgumentIsEmpty",
null,
Metadata.ValidateNotNullFailure);
}
var o = element as PSObject;
if (o != null)
{
element = o.BaseObject;
}
if (_rangeKind.HasValue)
{
Type commonType = GetCommonType((0).GetType(),element.GetType());
...Then in PowerShell
function foo { param([ValidateRange('Positive')] $bar) }
foo -1At this point, element will be a string when we enter GetCommonType.
What's interesting is that this does work.
function foo { param([ValidateRange('Positive')] $bar) }
foo (-1)I checked the existing code and they solve this by first getting the common type of the min and max values. They then try to cast element to this new type before comparison operations.
Since I don't have a common type, I was thinking of trying the cast to double.
There was a problem hiding this comment.
It is interesting. Maybe it is by design for -1 but confusing:
PS > function foo { param($bar) $bar.Gettype() }
PS > foo
You cannot call a method on a null-valued expression.
At line:1 char:29
+ function foo { param( $bar) $bar.Gettype() }
+ ~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : InvokeMethodOnNull
PS > foo 0
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32 System.ValueType
PS > foo -1
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True String System.Object
PS > foo 1
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32 System.ValueTypeThere was a problem hiding this comment.
In general case, a custom class can implement CompareTo() and allow (CustomClass)0. If the class don't implement this we get an exception - it seems this is acceptable here.
There was a problem hiding this comment.
The real problem here is the parameter is not typed - so I think this is by design. If the parameter was typed as [int], then it would have worked as expected.
Based on this, I think it's still reasonable to use GetCommonType.
|
Ok, i added the check for the common type. Also tried to make the error messages a little more relevant. |
| null, | ||
| Metadata.ValidateRangeElementType, | ||
| element.GetType().Name, | ||
| 0.GetType().Name); |
There was a problem hiding this comment.
0.GetType() and 0.GetType().Name is good condidates for static read only constant.
|
|
||
| if (LanguagePrimitives.TryConvertTo(element, commonType, out resultValue)) | ||
| { | ||
| elementValue = resultValue as IComparable; |
There was a problem hiding this comment.
What if we get elementValue == null ?
There was a problem hiding this comment.
That shouldn't be possible if there are no bugs in GetCommonType. I'd actually use a cast though so that if it did fail, the error would be better - can't convert instead of null reference.
|
|
||
| if (_rangeKind.HasValue) | ||
| { | ||
| ValidatePredefinedRange(element); |
There was a problem hiding this comment.
Comparing is the same for any range kinds (including "user defined"). I believe we should create a common method to compare elements and call it with different arguments for different ranges.
|
I get what you're saying about the methods. I've consolidated them into one method with overload. Let me know how it looks. |
| ValidateRangeKind? _rangeKind; | ||
|
|
||
| private readonly static Type _zeroType = 0.GetType(); | ||
| private readonly static string _zeroTypeName = 0.GetType().Name; |
There was a problem hiding this comment.
Please use s_zeroType for static constant.
And maybe _zeroTypeName = s_zeroType.Name;
|
Hey iSazonov, I'm not sure I follow on adding the additional range type. If we were to add the function foo { param([ValidateRange("ExpliciteRange")] [int] $bar) }; foo 0 } |
|
@dchristian3188 I agree - we should follow HasValue(). That's enough to make a choice in general method. |
|
We could enhance and unify Ranges. Currently Min and Max values are allowed but user can want to exclude them. Ex., "Positive" exclude zero. So the "Positive" range looks: ValidateRange(0, int.MaxValue, RangeOption.ExcludeMin)"Negative": ValidateRange( -int.MaxValue, 0, RangeOption.ExcludeMax)Exclude both (allow 0.5): ValidateRange( 0, 1, RangeOption.ExcludeMin+RangeOption.ExcludeMax)And we can move |
|
Hi iSazonov, I think expanding the range functionality is a cool idea, but might be out of scope for this PR. Do you want me to open a separate issue for tracking? Are there other updates I should make to the validate positive, negative, etc? |
|
@dchristian3188 Please clarify do you plan continue in new PR or want cut off? |
|
Hi iSazonov, I would continue the work you mentioned in the new PR. If we open the issue, would I be able to get it assigned to me? |
|
@lzybkr Could you please continue the code review? |
|
|
||
| ValidateRangeKind? _rangeKind; | ||
|
|
||
| private readonly static Type s_zeroType = 0.GetType(); |
There was a problem hiding this comment.
A static is not necessary, you should just use typeof(int) instead.
| ValidateRangeKind? _rangeKind; | ||
|
|
||
| private readonly static Type s_zeroType = 0.GetType(); | ||
| private readonly static string s_zeroTypeName = s_zeroType.Name; |
There was a problem hiding this comment.
A static is not necessary here either - the string is only used in error cases, so there is little point in always creating it.
| s_zeroTypeName); | ||
| } | ||
|
|
||
| dynamic elementValue = element; |
There was a problem hiding this comment.
With this approach, you don't need dynamic.
|
|
||
| if (LanguagePrimitives.TryConvertTo(element, commonType, out resultValue)) | ||
| { | ||
| elementValue = resultValue as IComparable; |
There was a problem hiding this comment.
That shouldn't be possible if there are no bugs in GetCommonType. I'd actually use a cast though so that if it did fail, the error would be better - can't convert instead of null reference.
|
will this make the value zero validated both as NonNegative and NonPositive ? |
|
Yes 0 will fall into both NonNegative and NonPositive. Positive -gt 0 |
|
Latest PR removes static variables. Also reworked to get rid of the dynamic variable. Let me know if everything looks ok. |
|
@dchristian3188 For
|
|
hi @iSazonov , sorry missed that one. Corrections have been made. Please review |
|
Thers also the issue is 0 defined as positive or negative. I need to test my DSC scripts to make sure if I use these validations I get the desired logic. |
| { | ||
| dynamicZero = (IComparable)resultValue; | ||
| throwTypeError = false; | ||
| } |
There was a problem hiding this comment.
We can add else and exclude throwTypeError.
| <value>The argument cannot be validated because its type "{0}" is not the same type ({1}) as the maximum and minimum limits of the parameter. Make sure the argument is of type {1} and then try the command again.</value> | ||
| </data> | ||
| <data name="ValidateRangePositiveFailure" xml:space="preserve"> | ||
| <value>The argument "{0}" cannot be validated because its value is not greater than zero.</value> |
There was a problem hiding this comment.
We can "validate" but cannot "accept". Maybe better change this. Below too.
There was a problem hiding this comment.
You're right, this does make more sense. It also follows the same verbiage as existing Validate Range
|
Updates made in latest PR |
There was a problem hiding this comment.
Just a few little things now - I think it's just about ready.
| _rangeKind = kind; | ||
| } | ||
|
|
||
| private void ValidateRange(object element, ValidateRangeKind? rangeKind) |
There was a problem hiding this comment.
It's a private method so it doesn't really matter, but rangeKind is not optional, so the parameter should not be nullable.
|
|
||
| if (LanguagePrimitives.TryConvertTo(element, commonType, out resultValue)) | ||
| { | ||
| element = (IComparable)resultValue; |
There was a problem hiding this comment.
I think this cast is pointless.
If the cast were to fail - the error message won't be good.
But I don't think the cast will fail, GetCommonType should not succeed if the type is not IComparable, and the cast isn't necessary when assigning to object.
| null, | ||
| Metadata.ValidateRangeElementType, | ||
| element.GetType().Name, | ||
| typeof(int).Name); |
There was a problem hiding this comment.
Shouldn't this be typeof(commonType).Name?
| <value>The argument cannot be validated because its type "{0}" is not the same type ({1}) as the maximum and minimum limits of the parameter. Make sure the argument is of type {1} and then try the command again.</value> | ||
| </data> | ||
| <data name="ValidateRangePositiveFailure" xml:space="preserve"> | ||
| <value>The argument "{0}" cannot be accepted because its value is not greater than zero.</value> |
There was a problem hiding this comment.
For consistency, validation errors use The argument "{0} cannot be validated - the cannot be accepted phrase is not commonly used in our code base, basically just in this resx file, and maybe we should change those messages.
| @{ sb = { function Local:foo { param([ValidateCount(2, 3)] [string[]] $bar) }; foo 1 }; FullyQualifiedErrorId = "ParameterArgumentValidationError,foo"; InnerErrorId = "ValidateCountMinMaxFailure" } | ||
| @{ sb = { function Local:foo { param([ValidateCount(2, 3)] [string[]] $bar) }; foo 1,2,3,4 }; FullyQualifiedErrorId = "ParameterArgumentValidationError,foo"; InnerErrorId = "ValidateCountMinMaxFailure" } | ||
| @{ | ||
| sb = { function foo { param([ValidateCount(-1,2)] [string[]] $bar) }; foo } |
There was a problem hiding this comment.
I realize you didn't introduce this, but sb is not a great name - it is often used for StringBuilder as well. $ScriptBlock is probably better.
I won't block the PR if you want to leave it though.
Revert attribute parameter (as opposed to PowerShell parameter validation) error message
First pass of additional parameter validations attributes as mentioned here: #2634
New attributes include:
ValidatePositiveAttribute
ValidateNonNegativeAttribute
ValidateNegativeAttribute
ValidateNonPositiveAttribute