Skip to content

InvalidCastException with System.ComponentModel.DataAnnotations.Validator (multi-threaded) #110917

Open
@kmcclellan

Description

@kmcclellan

Description

You will occasionally get an InvalidCastException when using Validator to validate one or more RangeAttribute from multiple threads.

Reproduction Steps

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;

const int repeat = 100_000;
var exceptions = 0;

// Validator loads attributes from TypeDescriptor (which caches instances).
var rangeAttribute = TypeDescriptor.GetProperties(typeof(TestOptions))
    .Find(nameof(TestOptions.WaitTime), false)
    ?.Attributes.OfType<RangeAttribute>().FirstOrDefault();

var rangePropertiesToReset = new List<KeyValuePair<PropertyInfo, object?>>();

foreach (var propertyInfo in typeof(RangeAttribute)
    .GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
{
    if (propertyInfo.CanRead && propertyInfo.CanWrite)
    {
        // We only have a chance to reproduce when attribute is in its initial state.
        rangePropertiesToReset.Add(new(propertyInfo, propertyInfo.GetValue(rangeAttribute)));
    }
}

for (var i = 0; i < repeat; i++)
{
    Parallel.For(
        0,
        2,
        index =>
        {
            if (index == 0)
            {
                // May help to increase on a slower machine (or vice versa).
                Thread.SpinWait(50);
            }

            var options = new TestOptions();
            var context = new ValidationContext(options) { MemberName = nameof(TestOptions.WaitTime) };

            try
            {
                Validator.ValidateProperty(options.WaitTime, context);
            }
            catch (InvalidCastException exception)
            {
                if (Interlocked.Increment(ref exceptions) == 1)
                {
                    Console.WriteLine(exception);
                }
            }
        });

    foreach (var (propertyInfo, propertyValue) in rangePropertiesToReset)
    {
        propertyInfo.SetValue(rangeAttribute, propertyValue);
    }
}

Console.WriteLine();

var frequency = exceptions / (double)repeat;
Console.WriteLine($"{exceptions} exceptions encountered ({frequency:P4})");

class TestOptions
{
    [Range(typeof(TimeSpan), "00:00:00", "01:00:00")]
    public TimeSpan? WaitTime { get; set; }
}

Expected behavior

Validator members should not throw exceptions when invoked from multiple threads.

Actual behavior

System.InvalidCastException: Unable to cast object of type 'System.TimeSpan' to type 'System.String'.
   at System.ComponentModel.DataAnnotations.RangeAttribute.SetupConversion()
   at System.ComponentModel.DataAnnotations.RangeAttribute.IsValid(Object value)
   at System.ComponentModel.DataAnnotations.ValidationAttribute.IsValid(Object value, ValidationContext validationContext)
   at System.ComponentModel.DataAnnotations.ValidationAttribute.GetValidationResult(Object value, ValidationContext validationContext)
   at System.ComponentModel.DataAnnotations.Validator.TryValidate(Object value, ValidationContext validationContext, ValidationAttribute attribute, ValidationError& validationError)
   at System.ComponentModel.DataAnnotations.Validator.GetValidationErrors(Object value, ValidationContext validationContext, IEnumerable`1 attributes, Boolean breakOnFirstError)
   at System.ComponentModel.DataAnnotations.Validator.ValidateProperty(Object value, ValidationContext validationContext)
   at Program.<>c__DisplayClass0_0.<<Main>$>b__0(Int32 index) in C:\Users\kylem\Desktop\HelloDataAnnotations\Program.cs:line 48

318 exceptions encountered (0.3180%)

Regression?

No response

Known Workarounds

  • Use a lock to synchronize access to Validator.
  • Catch the exception and retry.

Configuration

  • .NET SDK 9.0.100
  • .NET Runtime 9.0.0
  • Windows 11 22631.4602 (x64)

Other information

This is likely to occur in applications using Polly - see bug there.

The exception was previously reported as #1143, which simply dismissed thread-safety as a requirement for the attribute instance. Given that attributes are cached at multiple levels by different static components, we need to consider where the best place is to handle this synchronization. If we don't solve it on the attribute level, I would argue that Validator should do its own synchronization (or we should be able to create multiple instances of Validator for use by multiple threads).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions