Skip to content
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

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

Open
kmcclellan opened this issue Dec 23, 2024 · 2 comments

Comments

@kmcclellan
Copy link

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).

@dotnet-policy-service dotnet-policy-service bot added the untriaged New issue has not been triaged by the area owner label Dec 23, 2024
Copy link
Contributor

Tagging subscribers to this area: @dotnet/area-system-componentmodel-dataannotations
See info in area-owners.md if you want to be subscribed.

@tarekgh
Copy link
Member

tarekgh commented Dec 24, 2024

The other workaround could be using the options source generator? we have fixed the multi-threading issue there #97045.

@tarekgh tarekgh added this to the 10.0.0 milestone Dec 24, 2024
@tarekgh tarekgh removed the untriaged New issue has not been triaged by the area owner label Dec 24, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants