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.
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, butattribute.Value.GetType()changes from the enum type toSystem.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.Secondand boxed(int)1are not equal.Environment
master/ unreleased 6.0.0 branch:AttributeUtil.ReadAttributeValue(src/Castle.Core/DynamicProxy/Internal/AttributeUtil.cs) only special-cases array arguments and otherwise returnsCustomAttributeTypedArgument.Valuedirectly, which for an enum argument is the boxed underlying integer.Minimal repro
Actual output
Expected output
Notes
object(so the enum is boxed at the call site). When the parameter is typed as the enum directly,CustomAttributeTypedArgument.ArgumentTypecarries the enum type and reproduction is correct (seeEnumArgAttributeabove, which works).CustomAttributeTypedArgument.ArgumentTypefor theobjectparameter case reportsSystem.Object, whileCustomAttributeDatafor the original attribute still records the true enum type on the typed argument. A fix would likely involve preserving/restoring the enum type viaEnum.ToObject(...)when reading the argument value inReadAttributeValue, rather than returning the raw boxed integer.