protected void SetupValidator(ModelMetadata metadata) { var fieldId = $"{metadata.ContainerType.FullName}.{metadata.PropertyName}".ToLowerInvariant(); AttributeFullId = $"{Attribute.TypeId}.{fieldId}".ToLowerInvariant(); AttributeWeakId = $"{typeof(T).FullName}.{fieldId}".ToLowerInvariant(); var item = ProcessStorage <string, CacheItem> .GetOrAdd(AttributeFullId, _ => // map cache is based on static dictionary, set-up once for entire application instance { // (by design, no reason to recompile once compiled expressions) IDictionary <string, Expression> fields = null; Attribute.Compile(metadata.ContainerType, parser => { fields = parser.GetFields(); FieldsMap = fields.ToDictionary(x => x.Key, x => Helper.GetCoarseType(x.Value.Type)); ConstsMap = parser.GetConsts(); EnumsMap = parser.GetEnums(); MethodsList = parser.GetMethods(); }); // compile the expression associated with attribute (to be cached for subsequent invocations) AssertClientSideCompatibility(); ParsersMap = fields .Select(kvp => new { FullName = kvp.Key, ParserAttribute = (kvp.Value as MemberExpression)?.Member.GetAttributes <ValueParserAttribute>().SingleOrDefault() }).Where(x => x.ParserAttribute != null) .ToDictionary(x => x.FullName, x => x.ParserAttribute.ParserName); if (!ParsersMap.ContainsKey(metadata.PropertyName)) { var currentField = metadata.ContainerType .GetProperties().Single(p => metadata.PropertyName == p.Name); var valueParser = currentField.GetAttributes <ValueParserAttribute>().SingleOrDefault(); if (valueParser != null) { ParsersMap.Add(new KeyValuePair <string, string>(metadata.PropertyName, valueParser.ParserName)); } } return(new CacheItem { FieldsMap = FieldsMap, ConstsMap = ConstsMap, EnumsMap = EnumsMap, MethodsList = MethodsList, ParsersMap = ParsersMap }); }); FieldsMap = item.FieldsMap; ConstsMap = item.ConstsMap; EnumsMap = item.EnumsMap; MethodsList = item.MethodsList; ParsersMap = item.ParsersMap; }
/// <summary> /// Generates client validation rule with the basic set of parameters. /// </summary> /// <param name="type">The validation type.</param> /// <returns> /// Client validation rule with the basic set of parameters. /// </returns> /// <exception cref="System.ComponentModel.DataAnnotations.ValidationException"></exception> protected ModelClientValidationRule GetBasicRule(string type) { try { var rule = new ModelClientValidationRule { ErrorMessage = FormattedErrorMessage, ValidationType = ProvideUniqueValidationType(type) }; rule.ValidationParameters.Add("expression", Expression.ToJson()); Debug.Assert(FieldsMap != null); if (FieldsMap.Any()) { rule.ValidationParameters.Add("fieldsmap", FieldsMap.ToJson()); } Debug.Assert(ConstsMap != null); if (ConstsMap.Any()) { rule.ValidationParameters.Add("constsmap", ConstsMap.ToJson()); } Debug.Assert(EnumsMap != null); if (EnumsMap.Any()) { rule.ValidationParameters.Add("enumsmap", EnumsMap.ToJson()); } Debug.Assert(MethodsList != null); if (MethodsList.Any()) { rule.ValidationParameters.Add("methodslist", MethodsList.ToJson()); } Debug.Assert(ParsersMap != null); if (ParsersMap.Any()) { rule.ValidationParameters.Add("parsersmap", ParsersMap.ToJson()); } Debug.Assert(ErrFieldsMap != null); if (ErrFieldsMap.Any()) { rule.ValidationParameters.Add("errfieldsmap", ErrFieldsMap.ToJson()); } return(rule); } catch (Exception e) { throw new ValidationException( $"{GetType().Name}: collecting of client validation rules for {FieldName} field failed.", e); } }
/// <summary> /// Retrieves a collection of client validation rules (which are next sent to browsers). /// </summary> /// <returns> /// A collection of client validation rules. /// </returns> public override IEnumerable <ModelClientValidationRule> GetClientValidationRules() { var rule = new ModelClientValidationRule { ErrorMessage = FormattedErrorMessage, ValidationType = ProvideUniqueValidationType("assertthat") }; rule.ValidationParameters.Add("expression", Expression); rule.ValidationParameters.Add("fieldsmap", FieldsMap.ToJson()); rule.ValidationParameters.Add("constsmap", ConstsMap.ToJson()); rule.ValidationParameters.Add("parsersmap", ParsersMap.ToJson()); yield return(rule); }
/// <summary> /// Attaches client validation rule to the context. /// </summary> /// <param name="context">The Client Model Validation Context.</param> /// <param name="type">The validation type.</param> /// <param name="defaultErrorMessage">The default Error Message.</param> /// <returns> /// void /// </returns> /// <exception cref="System.ComponentModel.DataAnnotations.ValidationException"></exception> protected void AttachValidationRules(ClientModelValidationContext context, string type, string defaultErrorMessage) { var errorMessage = !string.IsNullOrEmpty(FormattedErrorMessage) ? FormattedErrorMessage : defaultErrorMessage; var uniqueValidationType = ProvideUniqueValidationType(type); try { MergeAttribute(context.Attributes, "data-val", "true"); MergeAttribute(context.Attributes, $"data-val-{uniqueValidationType}", errorMessage); MergeAttribute(context.Attributes, $"data-val-{uniqueValidationType}-expression", Expression.ToJson()); Debug.Assert(FieldsMap != null); if (FieldsMap.Any()) { MergeAttribute(context.Attributes, $"data-val-{uniqueValidationType}-fieldsmap", FieldsMap.ToJson()); } Debug.Assert(ConstsMap != null); if (ConstsMap.Any()) { MergeAttribute(context.Attributes, $"data-val-{uniqueValidationType}-constsmap", ConstsMap.ToJson()); } Debug.Assert(EnumsMap != null); if (EnumsMap.Any()) { MergeAttribute(context.Attributes, $"data-val-{uniqueValidationType}-enumsmap", EnumsMap.ToJson()); } Debug.Assert(MethodsList != null); if (MethodsList.Any()) { MergeAttribute(context.Attributes, $"data-val-{uniqueValidationType}-methodslist", MethodsList.ToJson()); } Debug.Assert(ParsersMap != null); if (ParsersMap.Any()) { MergeAttribute(context.Attributes, $"data-val-{uniqueValidationType}-parsersmap", ParsersMap.ToJson()); } Debug.Assert(ErrFieldsMap != null); if (ErrFieldsMap.Any()) { MergeAttribute(context.Attributes, $"data-val-{uniqueValidationType}-errfieldsmap", ErrFieldsMap.ToJson()); } } catch (Exception e) { throw new ValidationException( $"{GetType().Name}: collecting of client validation rules for {FieldName} field failed.", e); } }
public override void AddValidation(ClientModelValidationContext context) { SetupValidator(context.ModelMetadata); var validationId = GetUniqueValidationId(context); var formattedErrorMessage = GetErrorMessage(context); MergeAttribute(context.Attributes, "data-val", "true"); MergeAttribute(context.Attributes, $"data-val-{validationId}", formattedErrorMessage); MergeExpressiveAttribute(context, validationId, "expression", Attribute.Expression); if (AllowEmpty.HasValue) { MergeExpressiveAttribute(context, validationId, "allowempty", AllowEmpty); } if (FieldsMap != null && FieldsMap.Any()) { MergeExpressiveAttribute(context, validationId, "fieldsmap", FieldsMap); } if (ConstsMap != null && ConstsMap.Any()) { MergeExpressiveAttribute(context, validationId, "constsmap", ConstsMap); } if (EnumsMap != null && EnumsMap.Any()) { MergeExpressiveAttribute(context, validationId, "enumsmap", EnumsMap); } if (MethodsList != null && MethodsList.Any()) { MergeExpressiveAttribute(context, validationId, "methodslist", MethodsList); } if (ParsersMap != null && ParsersMap.Any()) { MergeExpressiveAttribute(context, validationId, "parsersmap", ParsersMap); } if (ErrFieldsMap != null && ErrFieldsMap.Any()) { MergeExpressiveAttribute(context, validationId, "errfieldsmap", ErrFieldsMap); } }
/// <summary> /// Constructor for expressive model validator. /// </summary> /// <param name="metadata">The model metadata.</param> /// <param name="attribute">The expressive attribute instance.</param> /// <param name="processCache">An IMemoryCache instance, scoped to the process.</param> /// <param name="requestCache">A RequestCache instance, scoped to the request.</param> /// <exception cref="System.ComponentModel.DataAnnotations.ValidationException"></exception> protected ExpressiveValidator(ModelMetadata metadata, T attribute, IMemoryCache processCache, IMemoryCache requestCache) { _requestCache = requestCache; try { Debug.WriteLine($"[ctor entry] process: {Process.GetCurrentProcess().Id}, thread: {Thread.CurrentThread.ManagedThreadId}"); var fieldId = $"{metadata.ContainerType.FullName}.{metadata.PropertyName}".ToLowerInvariant(); AttributeFullId = $"{attribute.TypeId}.{fieldId}".ToLowerInvariant(); AttributeWeakId = $"{typeof(T).FullName}.{fieldId}".ToLowerInvariant(); FieldName = metadata.PropertyName; // Parsing the attributes and constructing the maps can be quite expensive. They only need to be parsed once for the // application, so the results of this parsing are stored in a cache scoped to the process. // // Note that, unlike the implementation in the original Expressive Annotations (which used Lazy<T> and a ConcurrentDictionary), // this use of IMemoryCache for caching the data about parsed attributes doesn't guarantee that the cache entries will only // be added once. If a second concurrent request triggers parsing of a set of attributes before the first request has completed, // one of them will end up overwriting the cache entries of the other, so parsing will happen twice. (Or more times, if there // are multiple requests). But once one cache entry has been added, subsequent requests for the same attribute will get the // parsing results from the cache. // // Unless the attributes are extremely numerous, complex and slow to parse, causing performance problems the first time the // application is used after starting, this shouldn't really be a problem. // // Good discussion about similar issues here: // https://www.hanselman.com/blog/EyesWideOpenCorrectCachingIsAlwaysHard.aspx var item = processCache.GetOrCreate(AttributeFullId, entry => { Debug.WriteLine($"[cache add] process: {Process.GetCurrentProcess().Id}, thread: {Thread.CurrentThread.ManagedThreadId}"); entry.SetPriority(CacheItemPriority.NeverRemove); IDictionary <string, Expression> fields = null; attribute.Compile(metadata.ContainerType, parser => { fields = parser.GetFields(); FieldsMap = fields.ToDictionary(x => x.Key, x => Helper.GetCoarseType(x.Value.Type)); ConstsMap = parser.GetConsts(); EnumsMap = parser.GetEnums(); MethodsList = parser.GetMethods(); }); // compile the expression associated with attribute (to be cached for subsequent invocations) AssertClientSideCompatibility(); ParsersMap = fields .Select(kvp => new { FullName = kvp.Key, ParserAttribute = (kvp.Value as MemberExpression)?.Member.GetAttributes <ValueParserAttribute>().SingleOrDefault() }).Where(x => x.ParserAttribute != null) .ToDictionary(x => x.FullName, x => x.ParserAttribute.ParserName); if (!ParsersMap.ContainsKey(metadata.PropertyName)) { var currentField = metadata.ContainerType .GetProperties().Single(p => metadata.PropertyName == p.Name); var valueParser = currentField.GetAttributes <ValueParserAttribute>().SingleOrDefault(); if (valueParser != null) { ParsersMap.Add(new KeyValuePair <string, string>(metadata.PropertyName, valueParser.ParserName)); } } return(new ProcessCacheItem { FieldsMap = FieldsMap, ConstsMap = ConstsMap, EnumsMap = EnumsMap, MethodsList = MethodsList, ParsersMap = ParsersMap }); }); FieldsMap = item.FieldsMap; ConstsMap = item.ConstsMap; EnumsMap = item.EnumsMap; MethodsList = item.MethodsList; ParsersMap = item.ParsersMap; Expression = attribute.Expression; IDictionary <string, Guid> errFieldsMap; FormattedErrorMessage = attribute.FormatErrorMessage(metadata.GetDisplayName(), attribute.Expression, metadata.ContainerType, out errFieldsMap); // fields names, in contrast to values, do not change in runtime, so will be provided in message (less code in js) ErrFieldsMap = errFieldsMap; } catch (Exception e) { throw new ValidationException( $"{GetType().Name}: validation applied to {metadata.PropertyName} field failed.", e); } }
/// <summary> /// Constructor for expressive model validator. /// </summary> /// <param name="metadata">The model metadata instance.</param> /// <param name="context">The controller context instance.</param> /// <param name="attribute">The expressive attribute instance.</param> /// <exception cref="System.ComponentModel.DataAnnotations.ValidationException"></exception> protected ExpressiveValidator(ModelMetadata metadata, ControllerContext context, T attribute) : base(metadata, context, attribute) { try { Debug.WriteLine($"[ctor entry] process: {Process.GetCurrentProcess().Id}, thread: {Thread.CurrentThread.ManagedThreadId}"); var fieldId = $"{metadata.ContainerType.FullName}.{metadata.PropertyName}".ToLowerInvariant(); AttributeFullId = $"{attribute.TypeId}.{fieldId}".ToLowerInvariant(); AttributeWeakId = $"{typeof (T).FullName}.{fieldId}".ToLowerInvariant(); FieldName = metadata.PropertyName; ResetSuffixAllocation(); var item = MapCache <string, CacheItem> .GetOrAdd(AttributeFullId, _ => // map cache is based on static dictionary, set-up once for entire application instance { // (by design, no reason to recompile once compiled expressions) Debug.WriteLine($"[cache add] process: {Process.GetCurrentProcess().Id}, thread: {Thread.CurrentThread.ManagedThreadId}"); var parser = new Parser(); parser.RegisterToolchain(); parser.Parse <bool>(metadata.ContainerType, attribute.Expression); var fields = parser.GetFields(); FieldsMap = fields.ToDictionary(x => x.Key, x => Helper.GetCoarseType(x.Value.Type)); ConstsMap = parser.GetConsts(); ParsersMap = fields .Select(kvp => new { FullName = kvp.Key, ParserAttribute = ((MemberExpression)kvp.Value).Member.GetAttributes <ValueParserAttribute>().SingleOrDefault() }).Where(x => x.ParserAttribute != null) .ToDictionary(x => x.FullName, x => x.ParserAttribute.ParserName); if (!ParsersMap.ContainsKey(metadata.PropertyName)) { var currentField = metadata.ContainerType .GetProperties().Single(p => metadata.PropertyName == p.Name); var valueParser = currentField.GetAttributes <ValueParserAttribute>().SingleOrDefault(); if (valueParser != null) { ParsersMap.Add(new KeyValuePair <string, string>(metadata.PropertyName, valueParser.ParserName)); } } AssertNoNamingCollisionsAtCorrespondingSegments(); attribute.Compile(metadata.ContainerType); // compile expressions in attributes (to be cached for subsequent invocations) return(new CacheItem { FieldsMap = FieldsMap, ConstsMap = ConstsMap, ParsersMap = ParsersMap }); }); FieldsMap = item.FieldsMap; ConstsMap = item.ConstsMap; ParsersMap = item.ParsersMap; Expression = attribute.Expression; IDictionary <string, Guid> errFieldsMap; FormattedErrorMessage = attribute.FormatErrorMessage(metadata.GetDisplayName(), attribute.Expression, metadata.ContainerType, out errFieldsMap); // fields names, in contrast to values, do not change in runtime, so will be provided in message (less code in js) ErrFieldsMap = errFieldsMap; } catch (Exception e) { throw new ValidationException( $"{GetType().Name}: validation applied to {metadata.PropertyName} field failed.", e); } }