Skip to content

Enum attribute argument reproduced as underlying integer when parameter is typed object #748

Description

@tillig

Summary

When DynamicProxy replicates a custom attribute onto a generated proxy, an enum value passed to an attribute constructor parameter that is typed object (rather than the enum type itself) is reproduced as its underlying integer type instead of the original enum type. The attribute is copied, but attribute.Value.GetType() changes from the enum type to System.Int32.

This silently breaks any consumer that relies on the runtime type of the attribute argument — for example, code that uses the value as a dictionary/lookup key, where MyEnum.Second and boxed (int)1 are not equal.

Environment

  • Castle.Core 5.2.1 (latest released)
  • Reproduced on .NET 8
  • The relevant code path appears unchanged on the current master / unreleased 6.0.0 branch: AttributeUtil.ReadAttributeValue (src/Castle.Core/DynamicProxy/Internal/AttributeUtil.cs) only special-cases array arguments and otherwise returns CustomAttributeTypedArgument.Value directly, which for an enum argument is the boxed underlying integer.

Minimal repro

using System;
using System.Linq;
using System.Reflection;
using Castle.DynamicProxy;

public enum MyEnum { First, Second }

[AttributeUsage(AttributeTargets.Parameter)]
public sealed class ObjectArgAttribute : Attribute
{
    public ObjectArgAttribute(object value) => Value = value;
    public object Value { get; }
}

[AttributeUsage(AttributeTargets.Parameter)]
public sealed class EnumArgAttribute : Attribute
{
    public EnumArgAttribute(MyEnum value) => Value = value;
    public MyEnum Value { get; }
}

public class Target
{
    public Target(
        [ObjectArg(MyEnum.Second)] string a,
        [EnumArg(MyEnum.Second)] string b)
    { }

    public virtual void M() { }
}

public static class Program
{
    public static void Main()
    {
        var proxyType = new ProxyGenerator().ProxyBuilder
            .CreateClassProxyType(typeof(Target), Type.EmptyTypes, ProxyGenerationOptions.Default);

        var stringParams = proxyType.GetConstructors().Single()
            .GetParameters()
            .Where(p => p.ParameterType == typeof(string));

        foreach (var p in stringParams)
        {
            var objectArg = p.GetCustomAttribute<ObjectArgAttribute>();
            var enumArg = p.GetCustomAttribute<EnumArgAttribute>();

            if (objectArg != null)
                Console.WriteLine($"ObjectArg.Value type = {objectArg.Value.GetType().FullName}, value = {objectArg.Value}");

            if (enumArg != null)
                Console.WriteLine($"EnumArg.Value   type = {enumArg.Value.GetType().FullName}, value = {enumArg.Value}");
        }
    }
}

Actual output

ObjectArg.Value type = System.Int32, value = 1
EnumArg.Value   type = MyEnum, value = Second

Expected output

ObjectArg.Value type = MyEnum, value = Second
EnumArg.Value   type = MyEnum, value = Second

Notes

  • The problem only occurs when the attribute constructor parameter is typed object (so the enum is boxed at the call site). When the parameter is typed as the enum directly, CustomAttributeTypedArgument.ArgumentType carries the enum type and reproduction is correct (see EnumArgAttribute above, which works).
  • CustomAttributeTypedArgument.ArgumentType for the object parameter case reports System.Object, while CustomAttributeData for the original attribute still records the true enum type on the typed argument. A fix would likely involve preserving/restoring the enum type via Enum.ToObject(...) when reading the argument value in ReadAttributeValue, rather than returning the raw boxed integer.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions