-
Notifications
You must be signed in to change notification settings - Fork 7.6k
ValidateSetAttribute enhancement: support set values to be dynamically generated from a custom ValidateSetValueGenerator #3784
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
92066ec
78c8bda
84145aa
788f910
021f27c
e45612f
4e1f0bb
5dccbad
a1f1aac
013ef85
f2cfc63
b5466d8
e27daba
18a791c
1319aef
a951f7f
f8ba27e
7ff271f
8b992a9
65349de
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,12 +4,15 @@ | |
|
||
using System.Collections; | ||
using System.Collections.Generic; | ||
using System.Collections.Concurrent; | ||
using System.Text.RegularExpressions; | ||
using System.Globalization; | ||
using System.Management.Automation.Internal; | ||
using System.Diagnostics.CodeAnalysis; | ||
using System.Management.Automation.Language; | ||
using System.Runtime.CompilerServices; | ||
using System.Linq; | ||
using System.Threading.Tasks; | ||
|
||
namespace System.Management.Automation.Internal | ||
{ | ||
|
@@ -1115,7 +1118,7 @@ public sealed class ValidatePatternAttribute : ValidateEnumeratedArgumentsAttrib | |
|
||
/// <summary> | ||
/// Gets or sets the custom error message pattern that is displayed to the user. | ||
/// | ||
/// | ||
/// The text representation of the object being validated and the validating regex is passed as | ||
/// the first and second formatting parameters to the ErrorMessage formatting pattern. | ||
/// <example> | ||
|
@@ -1177,10 +1180,10 @@ public sealed class ValidateScriptAttribute : ValidateEnumeratedArgumentsAttribu | |
{ | ||
/// <summary> | ||
/// Gets or sets the custom error message that is displayed to the user. | ||
/// | ||
/// | ||
/// The item being validated and the validating scriptblock is passed as the first and second | ||
/// formatting argument. | ||
/// | ||
/// | ||
/// <example> | ||
/// [ValidateScript("$_ % 2", ErrorMessage = "The item '{0}' did not pass validation of script '{1}'")] | ||
/// </example> | ||
|
@@ -1353,20 +1356,82 @@ public ValidateCountAttribute(int minLength, int maxLength) | |
} | ||
} | ||
|
||
/// <summary> | ||
/// Optional base class for <see cref="IValidateSetValuesGenerator"/> implementations that want a default implementation to cache valid values. | ||
/// </summary> | ||
public abstract class CachedValidValuesGeneratorBase : IValidateSetValuesGenerator | ||
{ | ||
// Cached valid values. | ||
private string[] _validValues; | ||
private int _validValuesCacheExpiration; | ||
|
||
/// <summary> | ||
/// Initializes a new instance of the CachedValidValuesGeneratorBase class. | ||
/// </summary> | ||
/// <param name="cacheExpirationInSeconds">Sets a time interval in seconds to reset the '_validValues' dynamic valid values cache.</param> | ||
protected CachedValidValuesGeneratorBase(int cacheExpirationInSeconds) | ||
{ | ||
_validValuesCacheExpiration = cacheExpirationInSeconds; | ||
} | ||
|
||
/// <summary> | ||
/// Abstract method to generate a valid values. | ||
/// </summary> | ||
public abstract string[] GenerateValidValues(); | ||
|
||
/// <summary> | ||
/// Get a valid values. | ||
/// </summary> | ||
public string[] GetValidValues() | ||
{ | ||
// Because we have a background task to clear the cache by '_validValues = null' | ||
// we use the local variable to exclude a race condition. | ||
var validValuesLocal = _validValues; | ||
if (validValuesLocal != null) | ||
{ | ||
return validValuesLocal; | ||
} | ||
|
||
var validValuesNoCache = GenerateValidValues(); | ||
|
||
if (validValuesNoCache == null) | ||
{ | ||
throw new ValidationMetadataException( | ||
"ValidateSetGeneratedValidValuesListIsNull", | ||
null, | ||
Metadata.ValidateSetGeneratedValidValuesListIsNull); | ||
} | ||
|
||
if (_validValuesCacheExpiration > 0) | ||
{ | ||
_validValues = validValuesNoCache; | ||
Task.Delay(_validValuesCacheExpiration * 1000).ContinueWith((task) => _validValues = null); | ||
} | ||
|
||
return validValuesNoCache; | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Validates that each parameter argument is present in a specified set | ||
/// </summary> | ||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] | ||
public sealed class ValidateSetAttribute : ValidateEnumeratedArgumentsAttribute | ||
{ | ||
// We can use either static '_validValues' | ||
// or dynamic valid values list generated by instance of 'validValuesGenerator'. | ||
private string[] _validValues; | ||
private IValidateSetValuesGenerator validValuesGenerator = null; | ||
|
||
// The valid values generator cache works across 'ValidateSetAttribute' instances. | ||
private static ConcurrentDictionary<Type, IValidateSetValuesGenerator> s_ValidValuesGeneratorCache = new ConcurrentDictionary<Type, IValidateSetValuesGenerator>(); | ||
|
||
/// <summary> | ||
/// Gets or sets the custom error message that is displayed to the user | ||
/// | ||
/// | ||
/// The item being validated and a text representation of the validation set | ||
/// is passed as the first and second formatting argument to the ErrorMessage formatting pattern. | ||
/// | ||
/// | ||
/// <example> | ||
/// [ValidateSet("A","B","C", ErrorMessage="The item '{0}' is not part of the set '{1}'.") | ||
/// </example> | ||
|
@@ -1380,13 +1445,28 @@ public sealed class ValidateSetAttribute : ValidateEnumeratedArgumentsAttribute | |
public bool IgnoreCase { get; set; } = true; | ||
|
||
/// <summary> | ||
/// Gets the values in the set | ||
/// Gets the valid values in the set. | ||
/// </summary> | ||
public IList<string> ValidValues | ||
{ | ||
get | ||
{ | ||
return _validValues; | ||
if (validValuesGenerator == null) | ||
{ | ||
return _validValues; | ||
} | ||
|
||
var validValuesLocal = validValuesGenerator.GetValidValues(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exception may be thrown from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems related #3769 - completers silently falls back to paths. Get-TestValidateSetPS4 : Cannot validate argument on parameter 'Param1'. Test throw!
At line:1 char:32
+ Get-TestValidateSetPS4 -param1 fdfd
+ ~~~~
+ CategoryInfo : InvalidData: (:) [Get-TestValidateSetPS4], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationError,Get-TestValidateSetPS4 If the generator throws good formatted exception users get it in InnerException. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. #Close. Agreed that wrapping the exception is not necessary. |
||
|
||
if (validValuesLocal == null) | ||
{ | ||
throw new ValidationMetadataException( | ||
"ValidateSetGeneratedValidValuesListIsNull", | ||
null, | ||
Metadata.ValidateSetGeneratedValidValuesListIsNull); | ||
} | ||
|
||
return validValuesLocal; | ||
} | ||
} | ||
|
||
|
@@ -1409,16 +1489,13 @@ protected override void ValidateElement(object element) | |
} | ||
|
||
string objString = element.ToString(); | ||
for (int setIndex = 0; setIndex < _validValues.Length; setIndex++) | ||
foreach (string setString in ValidValues) | ||
{ | ||
string setString = _validValues[setIndex]; | ||
|
||
if (CultureInfo.InvariantCulture. | ||
CompareInfo.Compare(setString, objString, | ||
IgnoreCase | ||
? CompareOptions.IgnoreCase | ||
: CompareOptions.None) == 0) | ||
|
||
{ | ||
return; | ||
} | ||
|
@@ -1455,6 +1532,37 @@ public ValidateSetAttribute(params string[] validValues) | |
|
||
_validValues = validValues; | ||
} | ||
|
||
/// <summary> | ||
/// Initializes a new instance of the ValidateSetAttribute class. | ||
/// Valid values is returned dynamically from a custom class implementing 'IValidateSetValuesGenerator' interface. | ||
/// </summary> | ||
/// <param name="valuesGeneratorType">class that implements the 'IValidateSetValuesGenerator' interface</param> | ||
/// <exception cref="ArgumentException">for null arguments</exception> | ||
public ValidateSetAttribute(Type valuesGeneratorType) | ||
{ | ||
// We check 'IsNotPublic' because we don't want allow 'Activator.CreateInstance' create an instance of non-public type. | ||
if (!typeof(IValidateSetValuesGenerator).IsAssignableFrom(valuesGeneratorType) || valuesGeneratorType.IsNotPublic) | ||
{ | ||
throw PSTraceSource.NewArgumentException("valuesGeneratorType"); | ||
} | ||
|
||
// Add a valid values generator to the cache. | ||
// We don't cache valid values. | ||
// We expect that valid values can be cached in the valid values generator. | ||
validValuesGenerator = s_ValidValuesGeneratorCache.GetOrAdd(valuesGeneratorType, (key) => (IValidateSetValuesGenerator)Activator.CreateInstance(key)); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Allows dynamically generate set of values for ValidateSetAttribute. | ||
/// </summary> | ||
public interface IValidateSetValuesGenerator | ||
{ | ||
/// <summary> | ||
/// Get a valid values. | ||
/// </summary> | ||
string[] GetValidValues(); | ||
} | ||
|
||
#region Allow | ||
|
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.
Not sure if it matters, but I still want to bring it up -- this static dictionary will hold references to the instances of
IValidateSetValuesGenerator
and cause them to not be collected forever. Maybe there won't be too manyIValidateSetValuesGenerator
instances anyways, but this is still a potential memory leaking problem.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.
Above I tried to add cleanup but @lzybkr concluded that it didn't matter - we don't expect a huge number of generators.
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.
#Closed