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);
        }
Example #4
0
        /// <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);
            }
        }
Example #6
0
        /// <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);
            }
        }