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.
|
|
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?
|
|
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.
|
|
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.
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 symbols 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 expression (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.
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.
|
|
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.
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. ↩︎One can always propose an extension to the language specification and/or open a PR for the roslyn compiler. ↩︎
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. ↩︎
- Permalink: //oostens.me/posts/workaround-for-string-representation-of-types-in-attribute-constructor-parameters-cs0182/
- License: The text and content is licensed under CC BY-NC-SA 4.0. All source code I wrote on this page is licensed under The Unlicense; do as you please, I'm not liable nor provide warranty.