Workaround for string representation of types in attribute constructor parameters (CS0182)

Workaround for string representation of types in attribute constructor parameters (CS0182)

Attributes are a powerful way in C# of adding meta-data to your code which can be queried at runtime through reflection (or statically from the binary). Unfortunately they are constrained by which constructor parameter types they can use. Only constant expressions, most primitives, types, public enums or single dimensional array of the previous types are allowed. For more information see the official documentation.
My use-case was to pass a string containing type information to an attribute and while those strings can be constructed manually, for maintainability reasons, i wanted to use typeof(MyType) this is where the trouble started…

Why are there restrictions on the constructor parameters in the first place? Attribute constructor parameters are embedded in the binary (as strings), at compile time, and the attributes are instantiated with those (deserialized) parameters at runtime. You can verify this by running ildasm.exe on your binary (View -> MetaInfo -> Show! or simply Ctrl+M) which will show the embedded attribute constructor parameters (as strings) in the meta data. Therefor all constructor parameters have to be serializable and evaluated at compile time (const expressions); resulting in the constraints mentioned earlier.

This post is written for .NET framework 4.8,C#7.3, although it applies to the latest version as well (NET core 3.1/.NET 5 preview, C#9).

Problem at hand

For our use-case we want to pass a type ( fully qualified) as string to an attribute constructor. Imagine in a more complex scenario1 the string would also contain additional information and/or multiple types. However just to illustrate the problem at hand passing a single type as string will be sufficient.

1
2
3
4
5
6
7
8
[AttributeUsage (AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public class MyAttribute : Attribute {
    public MyAttribute (string myType) {
        MyType = myType;
    }

    public string MyType { get; }
}

The obvious solutions all fail with the same CS0182 error, “An attribute argument must be a constant expression, typeof expression or array creation expression of an attribute parameter type”, what gives?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
interface IExample { }

// Unfortunately string interpolation is done at runtime, not at compile time
[My($"{typeof(IExample)}")] public class Test1 { }

// Since string interpolation is syntaxtic sugar for string format it is no surprise this fails as well
[My(string.Format("{0}", typeof (IExample)))] public class Test2 { }

// In contrast to string format, string concatenation can be done at compile time (if it consists of const expressions), but still no bueno
[My(typeof(IExample).AssemblyQualifiedName + "")] public class Test3 { }

// Manually converting it to string also does not work, also the reason why the previous attempt failed
[My(typeof(IExample).AssemblyQualifiedName)] public class Test4 { }

// Even though Type has a ToString, which does not include the AssemblyQualifiedName though, this fails as well
[My(typeof(IExample))] public class Test5 { }

Failed attempts to convert a type to string at compile time

typeof(T) itself is not the problem however, if we pass the type directly, instead of converting it to string, everything works as expected. This is no surprise since obviously the type information has to be available at compile time otherwise the compiler and linker would not be able to do their job. Since Type is part of the allowed types it means the compiler is able to evaluate typeof(), serialize the type to string and embed them in the binary.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[AttributeUsage (AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public class MyAttribute : Attribute {
    public MyAttribute (Type myType) {
        MyType = myType;
    }

    public Type MyType { get; }
}

public interface IExample { }

// Works like a charm
[My(typeof(IExample))] public class Test { }

Works when using plain Types

So what is the problem?

Type is not a constant but a run-time type, const Type ConstType = typeof(MyType); will therefor not compile. So if it is a runtime type how come we can use typeof(T) at compile time to pass a type to the constructor of an attribute and the compiler does not complain (and properly serializes it to string). Looking at the language specification/ documentation i could find nothing relevant which could explain this inconsistency, fortunately .NET is open source. Looking at the source code let us crack the case.

In the CLR typeof() is implemented though GetTypeFromHandle. The compiler converts typeof() to loading the type on the stack (ldtoken [type]) and calling GetTypeFromHandle from the CLR. You can verify this yourself by looking at the generated IL of your code; e.g. through ildasm. The function itself, in the CLR, is defined as:

[Pure]
[System.Security.SecuritySafeCritical]  // auto-generated
[ResourceExposure(ResourceScope.None)]
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern Type GetTypeFromHandle(RuntimeTypeHandle handle);

It is marked as [MethodImpl(MethodImplOptions.InternalCall)] (and extern) which means its method body doesn’t contain IL but instead it hooks into the internals of the .NET CLR. Further in the code we find:

// This is the routine that is called by the 'typeof()' operator in C#.  It is one of the most commonly used
// reflection operations. This call should be optimized away in nearly all situations
FCIMPL1_V(ReflectClassBaseObject*, RuntimeTypeHandle::GetTypeFromHandle, FCALLRuntimeTypeHandle th)
{
    FCALL_CONTRACT;

    FCUnique(0x31);
    return FCALL_RTH_TO_REFLECTCLASS(th);
}
FCIMPLEND

If you did not already know, surprise, part of the core of the .NET runtime is implemented in C++. If you further dive into FCALL_RTH_TO_REFLECTCLASS (macro) you will find it returns the ReflectClassBaseObject which is the base class for the RuntimeType class which is the implementation of Type in the current CLR. However this is a sidetrack. A few steps further, and marshalled back to C# land, we just end up with a Type as expected. A dead end. This is indeed how the CLR implements typeof() at runt-time but it does not help us to explain how typeof() is evaluated at compile time.

One important detail though, Type is an abstract class (public abstract class Type : MemberInfo, _Type, IReflect) that can have multiple implementations. In todays CLR (post .NET 4) it is implemented by TypeInfo (public abstract class TypeInfo:Type,IReflectableType) from which RuntimeType inherits. So everything from mscorlib uses RuntimeType as the implementation for Type. There is some history as to why it is implemented this way but that is out of scope for this post. Also if you quickly want to verify this for yourself i recommend using Reference Source from Microsoft.

Back to the compiler, roslyn, in the code analysis step the attribute parameters are validated against the spec. In code there are 3 cases defined: conversion, typeof operator and array creation. In case of the typeof operator the “type” (symbol) is first validated to be unbound and closed constructed. Note however we are not talking about the Type class here but the internal TypedSymbol class which is used in the compile (as base class) to represent all symboles representing a type. If the verifications succeeds the symbol is converted to a TypedConstant which is a dedicated class to represent constant values, passed as type, to an attribute constructor (boolean, byte, string, char, … all the allowed constant types) this, again, is an internal type class in the compiler and has nothing to do with the Type as we know it from CLR. All of this happens down in the LoadAndValidateAttributes function. Eventually the TypedConstant is, together with the internal representation of the attribute, emitted as meta data through AddCustomAttribute on the MetadataBuilder ( System.Reflection.Metadata.Ecma335 in our case) which serializes the TypedConstant(/Imetadataexpression) to string.

So there you have it, the typeof() operator used in attribute constructor parameters looks the same as the typeof() used at run-time but is a completely separate feature implemented in the compiler and has nothing to do with the regular typeof() operator. Therefor all the properties (and methods) you are used to on Type are not accessible when using typeof() in attribute parameters since it is a different type. When you use those properties the compiler will interpret the typeof() as the one from CLR that returns a regular Type which is not a const expresseion (and implemented through RuntimeType), hence you get a compiler error. This also explains why you can find nothing in the documentation and/or specification; it is a compiler feature not a language feature.

To find all this information if have first looked into the roslyn source code but it did not paint the picture for me so i stepped through roslyn to see what actually happens during compilation. You can follow the official installation instructions and get going but if you want/need a bit more hand holding i recommend Erik Schierboom’s post about how nameof() works. Which has a, slightly outdated, section about debugging in rolyn (csc.exe) which should get you going.

Workaround

Unfortunately a real solution2 is not available, but you can circumvent the problem by constructing the string at runtime instead of at compile time. The type has to be passed as Type to the constructor of your attribute after which you can construct a string from it. However note if you want to provide a flexible API, e.g. using params, your attribute class, and thus assembly it resides in, will no longer be CLS (common language specification) compliant (arrays are not allowed in public API). The alternative is to provide overloaded constructors matching the number of Type arguments you need, eliminating need for the CLS violating Type[] array API.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
[AttributeUsage (AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public class MyAttribute : Attribute {
    public MyAttribute (string format, params Type[] types) { // Not CLS compliant
        MyTypeString = string.Format (CultureInfo.InvariantCulture, format, types.Select(t => t.AssemblyQualifiedName).ToArray ()); // string.Format does not take an enumerator so to ToArray is required
    }

    public string MyTypeString { get; }
}

// Examples

[My("{0} && {1}", typeof (double), typeof (int))] public class Test1 { }

[My("{0} my text here", typeof (double))] public class Test2 { }

Workaround, deferring the string construction to run-time

Unfortunately it is not very efficient if you think of it: at compile time you go from type ⇨ string in meta data. At run-time you go from string ⇨ type (attribute constructor argument) ⇨ string (in the attribute constructor). Most likely when the string on your attribute (MyTypeString) is used it is converted back to a type. Quite a bit of superfluous conversions going on3.
Preferably one would create the string at compile time, embed it in the meta data, pass it as string to the attribute constructor and parse/convert it back to a type upon need at run-time; so the string is only converted back to a Type once. Alas this does not seem possible with the current language specification and implementation of roslyn when using typeof().

This is an area where .NET would benefit of explicit indications of runtime and compile time annotations like the C++ constexpr specifier; it would, at least, make the problem more visible.


  1. In a real scenario you might want to express a logic expression with types which is parsed and evaluated at run-time. Allowing to express logic like: typeX && (typeY || typeZ) with an attribute. Or whatever use-case you have at hand to add string representations of types in strings. ↩︎

  2. One can always propose an extension to the language specification and/or open a PR for the roslyn compiler. ↩︎

  3. You could of course skip the serialization to string in the constructor of your attribute and convert the constructor parameters straight to an intermediate representation of your preference ( AST, class, struct, …). Of course this all depends on your use-case but would save you an additional conversion step. ↩︎

Noticed an error in this post? Corrections are appreciated.

© Nelis Oostens