C # Attributes: About All Aspects

Hello reader. This article describes attributes from all sides - from the specification, meaning and definition of attributes, creating your own and working with them, ending with adding attributes on runtime and the most useful and interesting existing attributes. If you are interested in the topic of attributes in C #, then welcome to cat.









Content



  1. Introduction Defining and Assigning Attributes
  2. Interesting attributes with support from runtime. Here, brief information will be given on various attributes, the existence of which few people know and even less who use. Since this is absolutely impractical information, there will be no much ranting (contrary to my passion for inapplicable knowledge)
  3. Some of the little-known attributes that are good to know.
  4. Defining your attribute and processing it. Adding Attributes at Run Time


Introduction



As always, start with definitions and specifications. This will help to understand and understand attributes at all levels, which, in turn, is very useful in order to find the right applications for them.



Start by defining metadata. Metadata is data that describes and refers to types defined by CTS . Metadata is stored in a way that is independent of any particular programming language. Thus, metadata provides a general mechanism for exchanging information about a program for use between tools that require it (compilers and debuggers, as well as the program itself), as well as between VES . Metadata is included in the assembly manifest. They can be stored in a PE file along with IL code or in a separate PE file, where there will be only an assembly manifest.

An attribute is a characteristic of a type or its members (or other language constructs) that contains descriptive information. Although the most common attributes are predefined and have a specific format in the metadata, custom attributes can also be added to the metadata. Attributes are commutative, i.e. the order of their declaration over the element is unimportant



From a syntactic point of view (in metadata) there are the following attributes



  1. Using special syntax in IL. For example, keywords are attributes. And for them there is a special syntax in IL. There are quite a lot of them; listing everything does not make sense
  2. Using generalized syntax. These include user and library attributes.
  3. Security attributes. These include attributes that inherit from SecurityAttribute (directly or indirectly). They are processed in a special way. There is a special syntax for them in IL, which allows you to create xml that describes these attributes directly


Example



C # code containing all of the above types of attributes
[StructLayout(LayoutKind.Explicit)] [Serializable] [Obsolete] [SecurityPermission(SecurityAction.Assert)] public class Sample { }
      
      







Resulting IL
 .class public EXPLICIT ansi SERIALIZABLE beforefieldinit AttributeSamples.Sample extends [System.Runtime]System.Object { .custom instance void [System.Runtime]System.ObsoleteAttribute::.ctor() = (01 00 00 00 ) .permissionset assert = { class 'System.Security.Permissions.SecurityPermissionAttribute, System.Runtime.Extensions, Version=4.2.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' = {}} .method public hidebysig specialname rtspecialname instance void .ctor() cil managed {/*constructor body*/} }
      
      







As you can see, StructLayoutAttribute has a special syntax, since in IL it is represented as "explicit". ObsoleteAttribute uses a common syntax - in IL begins with ".custom". SecurityPermissionAttribute as a security attribute has turned into a ".permissionset assert".



User attributes add user information to metadata. This mechanism can be used to store application-specific information at compile time and to access it at run time or for reading and analysis by another tool. Although any user-defined type can be used as an attribute, CLS conformance requires that the attributes inherit from System.Attribute. The CLI predefines some attributes and uses them to control runtime behavior. Some languages ​​define attributes to represent language features not represented directly in CTS.



As already mentioned, attributes are stored in metadata, which, in turn, are generated at the compilation stage, i.e. entered in the PE file (usually * .dll). Thus, you can add an attribute at runtime only by modifying the executable file at runtime (but the times of self-changing programs are long gone). It follows that they cannot be added at the execution stage, but this is not entirely accurate. If we form our assembly, define types in it, then at the stage of execution we can create a new type and hang attributes on it. So formally, we can still add attributes at runtime (the example will be at the very bottom).



And now a little about the limitations



If for some reason there are 2 attributes in the same assembly with the names Name and NameAtribute, then it becomes impossible to put the first of them. When using [Name] (that is, without a suffix), the compiler says it sees uncertainty. When using [NameAttribute] we will put NameAttribute, which is logical. There is a special syntax for such a mystical situation with a lack of imagination when naming. To put the first version without a suffix, you can specify the sign of the dog (that is, [Name] - a joke, it’s not necessary) before the attribute name [@Name].



Custom attributes can be added to anything but custom attributes. This refers to metadata, i.e. if we put an attribute in C # above the attribute class, then in metadata it will refer to the class. But you cannot add an attribute to "public". But you can do with assemblies, modules, classes, value types, enums, constructors, methods, properties, fields, events, interfaces, parameters, delegates, return values, or generalized parameters. The example below provides obvious and not very examples of how you can put an attribute on a particular construction.



Attribute Declaration Syntax
 using System; using System.Runtime.InteropServices; using System.Security.Permissions; using AttributeSamples; [assembly:All] [module:All] namespace AttributeSamples { [AttributeUsage(AttributeTargets.All)] public class AllAttribute : Attribute { } [All] //   public class Usage { [All] //   [return:All] //     public int GiveMeInt<[All]T>([All]int param) { return 5 + param; } [All] //   [field:All] //        public event Action Event; [All] //   [field: All] //       public int Action { get; set; } } }
      
      







Attributes have 2 types of parameters - named and positional. Positional parameters include constructor parameters. To named ones - public properties with an accessible setter. Moreover, these are not just formal names; all parameters can be indicated when declaring an attribute in brackets after its name. Named ones are optional.



Types of Parameters
 [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class AttrParamsAttribute : Attribute { public AttrParamsAttribute(int positional) //  { } public int Named { get; set; } //  } [AttrParams(1)] [AttrParams(1, Named = 2)] public class AttrParams { }
      
      







Valid parameters (of both types) for the attribute must be one of the following types:



  1. bool, byte, char, double, float, int, long, short, string and further on primitive, except decimal
  2. object
  3. System.Type
  4. enum
  5. A one-dimensional array of any of the above types


This is largely due to the fact that it should be a compile-time constant, and the types above can accept this constant (by accepting an object we can pass int). But for some reason, the argument cannot be of type ValueType, although this is possible from a logical point of view.



There are two types of user attributes: genuine custom attributes and pseudo-custom .

In the code they look the same (they are indicated above the language structure in square brackets), but they are processed differently:



  1. The original user attribute is stored directly in the metadata; attribute parameters are stored as is. They are available at runtime and are saved as a set of bytes (I hasten to remind you that they are known at compile time)
  2. A pseudo-user attribute is recognized because its name is one of a special list. Instead of storing its data directly in the metadata, it is analyzed and used to set bits or fields in the metadata tables, and the data is then discarded and cannot be received further. Metadata tables are checked at runtime faster than genuine user attributes, while less information is required to store information.


Pseudo-user attributes are not visible reflection
 [Serializable] [StructLayout(LayoutKind.Explicit)] public class CustomPseudoCustom { } class Program { static void Main() { var onlyCustom = typeof(CustomPseudoCustom).GetCustomAttributes(); // SerializableAttribute } }
      
      







Most user attributes are introduced at the language level. They are stored and returned by the runtime, while the runtime does not know anything about the meaning of these attributes. But all pseudo-user attributes plus some custom attributes are of particular interest to compilers and to the CLI. So we move on to the next section.



Attributes with runtime support



This section is purely informative, if there is no interest in using the runtime, then you can scroll to the next section.



The table below lists pseudo-user attributes and special user attributes (CLIs or compilers handle them in a special way).



Pseudo-user attributes (they cannot be obtained through reflection).

CLI Attributes:

Attribute Description
AssemblyAlgorithmIDAttribute Writes the identifier of the hash algorithm used. Sets the Assembly.HashAlgId field
AssemblyFlagsAttribute Writes flags for the corresponding assembly. Sets the Assembly.Flags field
DllImportAttribute Provides information about code implemented in an unmanaged library. Sets the Method.Flags.PinvokeImpl bit of the corresponding method; adds a new entry to ImplMap (by setting the values ​​of MappingFlags, MemberForwarded, ImportName and ImportScope)
StructLayoutAttribute Allows you to explicitly set the method for placing fields of reference or significant type. Sets the TypeDef.Flags.LayoutMask field for the type. It can also set the TypeDef.Flags.StringFormatMask, ClassLayout.PackingSize and ClassLayout.ClassSize fields
FieldOffsetAttribute Defines the byte offset of fields in a reference or significant type. Sets the value of FieldLayout.OffSet for the corresponding method.
Inattribute Indicates that the parameter is passed as an [in] argument. Sets the Param.Flags.In bit for the corresponding parameter.
Outattribute Indicates that the parameter is passed as an [out] argument. Sets the Param.Flags.Out bit for the corresponding parameter.
Marshalasattribute Defines how data is marshaled between managed and unmanaged code. Sets the Field.Flags.HasFieldMarshal bit for the field (or the Param.Flags.HasFieldMarshal bit for the parameter); Adds an entry to the FieldMarshal table (setting the values ​​of Parent and NativeType)
MethodImplAttribute Defines implementation details for a method. Sets the value of Method.ImplFlags for the corresponding method




CLS Attributes - Languages ​​must support them:

Attribute Description
AttributeUsageAttribute Used to indicate how an attribute can be used.
ObsoleteAttribute Indicates that the item should not be used.
CLSCompliantAttribute Indicates whether the item is declared as CLS compliant.


Miscellaneous interesting

Attribute Description
ThreadStaticAttribute Provides stream type fields
ConditionalAttribute Marks the method as invoked based on a compilation condition (specified in / define). If the condition is not met, then the method will not be called (and will not be compiled into IL). Only the void method can be tagged. Otherwise, a compilation error will occur.
DecimalConstantAttribute Stores decimal constant value in metadata
DefaultMemberAttribute Defines a member of the class to use by default with the InvokeMember method.
CompilationRelaxationsAttribute Indicates whether exceptions to instruction checks are strict or relaxed. Currently, you can only pass the NoStringInterning parameter, which marks the assembly as not requiring string literal interning. But this mechanism can still be used.
FlagsAttribute Attribute indicating whether enum should be treated as bit flags
IndexerNameAttribute Specifies the name by which the indexer will be known in programming languages ​​that do not directly support this feature.
ParamArrayAttribute Indicates that the method accepts a variable number of parameters.


Useful Attributes



An integral part of software product development is debugging. And often in a large and complex system, it takes dozens and hundreds of times to run the same method and monitor the state of objects. At the same time, at a time of 20 it already begins to enrage specifically the need to expand one object deep 400 times to see the value of one variable and restart the method again.

For quieter and faster debugging, you can use attributes that modify the behavior of the debugger.



DebuggerDisplayAttribute indicates how the type or its member is displayed in the debugger variables window (and not only).



The only argument to the constructor is a string with a display format. What will be between the braces will be calculated. The format is like an interpolated string, only without a dollar. You cannot use pointers in a computed value. By the way, if you have an overridden ToString, then its value will be shown as if it were in this attribute. If there is both a ToString and an attribute, then the value is taken from the attribute.





DebuggerBrowsableAttribute defines how a field or property is displayed in the debugger variables window. Accepts a DebuggerBrowsableState, which has 3 options:









DebuggerTypeProxy - if the object is viewed in the debugger hundreds of times a day, you can get confused and spend 3 minutes creating a proxy object that displays the source object as it should. Typically, the proxy object to display is the inner class. Actually, it will be displayed instead of the target object.







Other useful attributes



ThreadStatic - an attribute that allows you to make a static variable its own for each thread. To do this, put the attribute above the static field. It is worth remembering an important nuance - initialization by a static constructor will be performed only once, and the variable will change in the thread that the static constructor will execute. In the rest, it will remain defaulted. (PS. If you need this behavior, I advise you to look towards the ThreadLocal class).



A little about the engine compartment nuances. In both Linux and Windows, there is a memory area local to the stream ( TLS and TSD, respectively). However, these areas themselves are very small. Therefore, a ThreadLocalInfo structure is created, a pointer to which is placed in TLS. Accordingly, only one slot is used. The structure itself contains 3 fields - Thread, AppDomain, ClrTlsInfo. We are interested in the first. It is it that organizes the storage of flow statics in memory, using ThreadLocalBlock and ThreadLocalModule for this.



In this way:





Well, since we are talking about this, it is worth mentioning about asynchronous methods. As an attentive reader may notice, if we use asynchrony, then the continuation will not necessarily be executed in the same thread (we can influence the execution context, but not the thread). Accordingly, we get a crap if we use ThreadLocal. In this case, it is recommended to use AsyncLocal. But the article is not about this, so we went further.



InternalsVisibleTo - allows you to specify the assembly, which will be visible to elements marked internal . It may seem that if some assembly needs certain types and their members, you can simply mark them public and not steam. But good architecture implies hiding implementation details. Nevertheless, they may be needed for some infrastructure things, for example, test projects. Using this attribute, you can support both encapsulation and the required percentage of test coverage.



HandleProcessCorruptedStateExceptions - allows you to scare timid programmers and catch exceptions of a damaged state. By default, for such exceptions, the CLR does not trap. In general, the best solution would be to let the application crash. These are dangerous exceptions that indicate that the process memory is corrupted, so using this attribute is a very bad idea. But it is possible in some cases, for local development it will be useful to set this attribute for a while. To catch the exception of a damaged state, simply put this attribute above the method. And if it has already reached the use of this attribute, it is recommended (however, as always) to catch some specific exception.



DisablePrivateReflection - makes all private members of the assembly unattainable for reflection. The attribute is put on the assembly.



Defining Your Attribute



Not just because this section is the last. After all, the best way to understand in which cases it will be beneficial to use the attribute is to look at already used ones. It is difficult to say a formalized rule when you should think about your own attribute. Often they are used as additional information about the type / its member or another language construct that is common to completely different entities. As an example, all the attributes used for serialization / ORM / formatting, etc. Due to the extensive application of these mechanisms to completely different types, often not known to the developers of the corresponding mechanism, the use of attributes is a great way to enable the user to provide declarative information for this mechanism.



Using your attributes can be divided into 2 parts:



  1. Creating an attribute and using it
  2. Getting an attribute and processing it


Creating an attribute and using it



To create your attribute, it is enough to inherit from System.Attribute . In this case, it is advisable to adhere to the mentioned naming style - end the class name on Attribute. However, there will be no mistake if you omit this suffix. As mentioned earlier, attributes can have 2 types of parameters - positional and named. The logic of their application is the same as with the properties and parameters of the constructor of the class - the values ​​necessary to create the object for which there is no reasonable "default" are placed in positional (ie, constructor). What can be reasonably defaulted, which will often be used, is better distinguished into a named one (i.e. a property).



Important in creating an attribute is limiting its places of use. AttributeUsageAttribute is used for this. The required parameter (positional) is the AttributeTarget, which determines where the attribute is used (method, assembly, etc.). Optional (named) parameters are:



  1. AllowMultiple - indicates whether it is possible to place more than one attribute over the place of its application or not. False by default
  2. Inherited - determines whether this attribute will belong to classes inheritors (in case of placement over the base class) and overridden methods (in case of placement over the method). The default is true.


After that, you can load the attributes with a payload. An attribute is declarative information, which means everything that is defined in it should describe the construction to which it relates. The attribute should not contain any deep logic. For the processing of the attributes you define, special services should be responsible that will just process them. But the fact that the attribute should not have logic does not mean that it should not have methods.



A method (function) is also information and can also describe a construction. And using polymorphism in attributes, you can provide a very powerful and convenient tool where the user can influence both the information used by your tool and certain stages of execution and processing.At the same time, he will not need to produce classes, inject dependencies, cost factories and their interfaces that will create these classes. It will be enough to create a single heir class that encapsulates the details of working with the element to which it relates. But, as a rule, the usual ROSO attribute with a couple of properties is enough.



Retrieving and processing an attribute



The processing of the received attributes depends on the specific case and can be done in completely different ways. It is difficult to give useful functions and tricks for this.



Attributes are obtained at run time using reflection. There are various ways to get an attribute from a specific element.



But everything originates from the ICustomAttributeProvider interface . It is implemented by such types as Assembly, MemberInfo, Module, ParameterInfo. In turn, the successors of MemberInfo are Type, EventInfo, FieldInfo, MethodBase, PropertyInfo.



The interface has only 3 functions, and they are not very convenient. They work with arrays (even if we know that there can only be one attribute) and are not parameterized by type (they use object). Therefore, you will rarely have to directly access the functions of this interface (I never said it because I do not want to be categorical). For ease of use, there is a CustomAttributeExtensions class , which contains many extension methods for all kinds of types that perform simple operations for casting, selecting a single value, and so on, thereby freeing the developer from this need. Also, these methods are available as static in the Attribute class with the most useful function of ignoring the inherit parameter (for non-conformists).



The main functions used are listed below. The first parameter indicating which type extends the method, I omitted. Also, wherever the bool inherit parameter is specified, there is overload without it (with the default value of true ). This parameter indicates whether the attributes of the parent class or the base method should be taken into account when executing the method (if used on an overridden method). If in the attribute inherit = flase , then even setting it to true will not help to take into account the attributes of the base class

Method name Description
GetCustomAttributes <LogAttribute> (bool inherit) Gets an enumeration of attributes of the specified type. If the attribute is one, an enumeration of 1 element will be returned
GetCustomAttribute <LogAttribute> (bool inherit) returns a single attribute of the specified type. If there are several, throw a System.Reflection.AmbiguousMatchException: Multiple custom attributes of the same type found exception
GetCustomAttributes () returns an enumeration of attributes of all types
GetCustomAttributesData () returns an enumeration CustomAttributeData, in which there are properties that allow you to get a constructor, parameters (named and positional), constructor arguments
IsDefined (Type attrType, bool inherit) returns true if the attribute is declared over the element, false if not


For clarity, I propose to look at a small demo of the work of all the functions mentioned.



Return value of the above methods
 [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class LogAttribute : Attribute { public string LogName { get; set; } } [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)] public class SerializeAttribute : Attribute { public string SerializeName { get; set; } } [Log(LogName = "LogBase1")] [Log(LogName = "LogBase2")] [Serialize(SerializeName = "SerializeBase1")] [Serialize(SerializeName = "SerializeBase2")] public class RandomDomainEntityBase { [Log(LogName = "LogMethod1")] [Log(LogName = "LogMethod2")] [Serialize(SerializeName = "SerializeMethod1")] [Serialize(SerializeName = "SerializeMethod2")] public virtual void VirtualMethod() { throw new NotImplementedException(); } } [Log(LogName = "LogDerived1")] [Log(LogName = "LogDerived2")] [Serialize(SerializeName = "SerializeDerived1")] [Serialize(SerializeName = "SerializeDerived2")] public class RandomDomainEntityDerived : RandomDomainEntityBase { [Log(LogName = "LogOverride1")] [Log(LogName = "LogOverride2")] [Serialize(SerializeName = "SerializeOverride1")] [Serialize(SerializeName = "SerializeOverride2")] public override void VirtualMethod() { throw new NotImplementedException(); } } class Program { static void Main() { Type derivedType = typeof(RandomDomainEntityDerived); MemberInfo overrideMethod = derivedType.GetMethod("VirtualMethod"); // overrideMethod.GetCustomAttribute(typeof(LogAttribute)); //System.Reflection.AmbiguousMatchException: Multiple custom attributes of the same type found. // overrideMethod.GetCustomAttribute<LogAttribute>(false); // System.Reflection.AmbiguousMatchException: Multiple custom attributes of the same type found. // Attribute.GetCustomAttribute(derivedType, typeof(SerializeAttribute)); // System.Reflection.AmbiguousMatchException: Multiple custom attributes of the same type found. IEnumerable<Attribute> allCustom = overrideMethod.GetCustomAttributes(); //LogOverride1 LogOverride2 SerializeOverride1 SerializeOverride2 LogMethod1 LogMethod2 IList<CustomAttributeData> allCustomInfo = overrideMethod.GetCustomAttributesData(); //  ,     IEnumerable<LogAttribute> typedCustom1 = overrideMethod.GetCustomAttributes<LogAttribute>(inherit:false); //LogOverride1 LogOverride2 IEnumerable<LogAttribute> typedInheritedCustom1 = overrideMethod.GetCustomAttributes<LogAttribute>(inherit:true); //LogOverride1 LogOverride2 LogMethod1 LogMethod2 IEnumerable<SerializeAttribute> typedCustom2 = overrideMethod.GetCustomAttributes<SerializeAttribute>(inherit:false); //SerializeOverride1 SerializeOverride2 IEnumerable<SerializeAttribute> typedInheritedCustom2 = overrideMethod.GetCustomAttributes<SerializeAttribute>(inherit:true); //SerializeOverride1 SerializeOverride2 Attribute[] customFromStaticClass = Attribute.GetCustomAttributes(overrideMethod, typeof(SerializeAttribute), inherit:true); //SerializeOverride1 SerializeOverride2 IEnumerable<Attribute> classCustom = derivedType.GetCustomAttributes(); //LogDerived1 LogDerived2 SerializeDerived1 SerializeDerived2 LogBase1 LogBase2 IList<CustomAttributeData> classCustomInfo = derivedType.GetCustomAttributesData(); //  ,     IEnumerable<LogAttribute> typedClassCustom1 = derivedType.GetCustomAttributes<LogAttribute>(false); //LogDerived1 LogDerived2 IEnumerable<LogAttribute> typedInheritedClassCustom1 = derivedType.GetCustomAttributes<LogAttribute>(true); //LogDerived1 LogDerived2 LogBase1 LogBase2 IEnumerable<SerializeAttribute> typedClassCustom2 = derivedType.GetCustomAttributes<SerializeAttribute>(false); //SerializeDerived1 SerializeDerived2 IEnumerable<SerializeAttribute> typedInheritedClassCustom2 = derivedType.GetCustomAttributes<SerializeAttribute>(true); //SerializeDerived1 SerializeDerived2 } }
      
      







Well, for academic interest, I give an example of defining attributes at runtime. This code does not claim to be the most beautiful and supported.



The code
 public class TypeCreator { private const string TypeSignature = "DynamicType"; private const string ModuleName = "DynamicModule"; private const string AssemblyName = "DynamicModule"; private readonly TypeBuilder _typeBuilder = GetTypeBuilder(); public object CreateTypeInstance() { _typeBuilder.DefineNestedType("ClassName"); CreatePropertyWithAttribute<SerializeAttribute>("PropWithAttr", typeof(int), new Type[0], new object[0]); Type newType = _typeBuilder.CreateType(); return Activator.CreateInstance(newType); } private void CreatePropertyWithAttribute<T>(string propertyName, Type propertyType, Type[] ctorTypes, object[] ctorArgs) where T : Attribute { var attributeCtor = typeof(T).GetConstructor(ctorTypes); CustomAttributeBuilder caBuilder = new CustomAttributeBuilder(attributeCtor, ctorArgs); PropertyBuilder newProperty = CreateProperty(propertyName, propertyType); newProperty.SetCustomAttribute(caBuilder); } private PropertyBuilder CreateProperty(string propertyName, Type propertyType) { FieldBuilder fieldBuilder = _typeBuilder.DefineField(propertyName, propertyType, FieldAttributes.Private); PropertyBuilder propertyBuilder = _typeBuilder.DefineProperty(propertyName, PropertyAttributes.HasDefault, propertyType, null); MethodAttributes getSetAttr = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig; MethodBuilder getter = GenerateGetter(); MethodBuilder setter = GenerateSetter(); propertyBuilder.SetGetMethod(getter); propertyBuilder.SetSetMethod(setter); return propertyBuilder; MethodBuilder GenerateGetter() { MethodBuilder getMethodBuilder = _typeBuilder.DefineMethod($"get_{propertyName}", getSetAttr, propertyType, Type.EmptyTypes); ILGenerator getterIl = getMethodBuilder.GetILGenerator(); getterIl.Emit(OpCodes.Ldarg_0); getterIl.Emit(OpCodes.Ldfld, fieldBuilder); getterIl.Emit(OpCodes.Ret); return getMethodBuilder; } MethodBuilder GenerateSetter() { MethodBuilder setMethodBuilder = _typeBuilder.DefineMethod($"set_{propertyName}", getSetAttr, null,new [] { propertyType }); ILGenerator setterIl = setMethodBuilder.GetILGenerator(); setterIl.Emit(OpCodes.Ldarg_0); setterIl.Emit(OpCodes.Ldarg_1); setterIl.Emit(OpCodes.Stfld, fieldBuilder); setterIl.Emit(OpCodes.Ret); return setMethodBuilder; } } private static TypeBuilder GetTypeBuilder() { var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(AssemblyName), AssemblyBuilderAccess.Run); ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(ModuleName); TypeBuilder typeBuilder = moduleBuilder.DefineType(TypeSignature, TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit | TypeAttributes.AutoLayout,null); return typeBuilder; } } public class Program { public static void Main() { object instance = new TypeCreator().CreateTypeInstance(); IEnumerable<Attribute> attrs = instance.GetType().GetProperty("PropWithAttr").GetCustomAttributes(); // Serializable } }
      
      








All Articles