Attributes


Before delving into details on how to program attributes, you should consider a use case that demonstrates its utility. In the CommandLineHandler example in Listing 14.3, you dynamically set a class's properties based on the command-line option matching the property name. This approach is insufficient, however, when the command-line option is an invalid property name. /?, for example, cannot be supported. Furthermore, this mechanism doesn't provide any way of identifying which options are required versus which are optional.

Instead of relying on an exact match between the option name and the property name, attributes provide a way of identifying additional metadata about the decorated constructin this case, the option that the attribute decorates. With attributes, you can decorate a property as Requiredand provide a /? option alias. In other words, attributes are a means of associating additional data with a property (and other constructs).

Attributes appear within square brackets preceding the construct they decorate. For example, you can modify the CommandLineInfo class to include attributes as shown in Listing 14.7.

Listing 14.7. Decorating a Property with an Attribute

 class CommandLineInfo {   [CommandLineSwitchAlias ("?")]                                                 public bool Help   {       get { return _Help; }       set { _Help = value; }   }   private bool _Help;   [CommandLineSwitchRequired]                                                   public string Out   {       get { return _Out; }       set { _Out = value; }   }   private string _Out;   public System.Diagnostics.ProcessPriorityClass Priority   {       get { return _Priority; }       set { _Priority = value; }   }   private System.Diagnostics.ProcessPriorityClass _Priority =      System.Diagnostics.ProcessPriorityClass.Normal; } 

In Listing 14.7, the Help and Out properties are decorated with attributes. The purpose of these attributes is to allow an alias of /? for /Help, and to indicate that /Out is a required parameter. The idea is that from within the CommandLineHandler.TryParse() method, you enable support for option aliases and, assuming the parsing was successful, you can check that all the required switches were specified.

There are two ways to combine attributes on the same construct. You can either separate the attributes with commas within the same square brackets, or place each attribute within its own square brackets, as shown in Listing 14.8.

Listing 14.8. Decorating a Property with Multiple Attributes

   [CommandLineSwitchRequired]   [CommandLineSwitchAlias("FileName")]   public string Out   {       get { return _Out; }       set { _Out = value; }   }   [CommandLineSwitchRequired,   CommandLineSwitchAlias("FileName")]   public string Out   {       get { return _Out; }       set { _Out = value; }   } 

In addition to decorating properties, developers can use attributes to decorate classes, interfaces, structs, enums, delegates, events, methods, constructors, fields, parameters, return values, assemblies, type parameters, and modules. For the majority of these, applying an attribute involves the same square bracket syntax shown in Listing 14.8. However, this syntax doesn't work for return values, assemblies, and modules.

Assembly attributes are used to add additional metadata about the assembly. Visual Studio's Project Wizard, for example, generates an AssemblyInfo.cs file that includes a host of attributes about the assembly. Listing 14.9 is an example of such a file.

Listing 14.9. Assembly Attributes within AssemblyInfo.cs

 using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General information about an assembly is controlled // through the following set of attributes. Change these // attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("CompressionLibrary")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Michaelis.net")] [assembly: AssemblyProduct("CompressionLibrary")] [assembly: AssemblyCopyright("Copyright© Michaelis.net 2006")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] // Setting ComVisible to false makes the types in this // assembly not visible to COM components. If you need to // access a type in this assembly from COM, set the ComVisible // attribute to true on that type. [assembly ComVisible(false)] // The following GUID is for the ID of the typelib if this // project is exposed to COM [assembly Guid("417a9609-24ae-4323-b1d6-cef0f87a42c3")] // Version information for an assembly consists // of the following four values: // //      Major Version //      Minor Version //      Build Number //      Revision // // You can specify all the values or you can // default the Revision and Build Numbers // by using the '*' as shown below: [assembly AssemblyVersion("1.0.0.0")] [assembly AssemblyFileVersion("1.0.0.0")] 

The assembly attributes define things like company, product, and assembly version number. Similar to assembly, identifying an attribute usage as module requires prefixing it with module. The restriction on assembly and module attributes is that they appear after the using directive but before any namespace or class declarations.

Return attributes, such as the one shown in Listing 14.10, appear before a method declaration but use the same type of syntax structure.

Listing 14.10. Specifying a Return Attribute

   [return Description(       "Returns true if the object is in a valid state.")]   public bool IsValid()   {     // ...     return true;   } 

In addition to assembly and return, C# allows for explicit target identifications of module, class, and method, corresponding to attributes that decorate the module, class, and method. class and method, however, are optional, as demonstrated earlier.

One of the conveniences of using attributes is that the language takes into consideration the attribute naming convention, which is to place Attribute at the end of the name. However, in all the attribute uses in the preceding listings, no such suffix appears, despite the fact that each attribute used follows the naming convention. This is because although the full name (DescriptionAttribute, AssemblyVersionAttribute, and so on) is allowed when applying an attribute, C# makes the suffix optional and generally, no such suffix appears when applying an attribute; it appears only when defining one or using the attribute inline (such as typeof(DescriptionAttribute)).

Custom Attributes

Defining a custom attribute is relatively trivial. Attributes are objects; therefore, to define an attribute, you need to define a class. The characteristic that turns a general class into an attribute is that it derives from System.Attribute. Therefore, you can create a CommandLineSwitchRequiredAttribute class, as shown in Listing 14.11.

Listing 14.11. Defining a Custom Attribute

 public class CommandLineSwitchRequiredAttribute  Attribute { } 

With that simple definition, you now can use the attribute as demonstrated in Listing 14.7. So far, no code responds to the attribute; therefore, the Out property that includes the attribute will have no effect on command-line parsing.

Looking for Attributes

In addition to providing properties for reflecting on a type's members, Type includes methods to retrieve the Attributes decorating that type. Similarly, all the reflection types (PropertyInfo and MethodInfo, for example) include members for retrieving a list of attributes that decorate a type. Listing 14.12 defines a method to return a list of required switches that are missing from the command line.

Listing 14.12. Retrieving a Custom Attribute

 using System; using System.Collections.Specialized; using System.Reflection; public class CommandLineSwitchRequiredAttribute  Attribute {   public static string[] GetMissingRequiredOptions(       object commandLine)   {       StringCollection missingOptions = new StringCollection();       PropertyInfo[] properties =          commandLine.GetType().GetProperties();       foreach (PropertyInfo property in properties)       {           Attribute[] attributes =               (Attribute[])property.GetCustomAttributes(                   typeof(CommandLineSwitchRequiredAttribute),                   false);           if ((attributes.Length > 0) &&               (property.GetValue(commandLine, null)== null))           {               if (property.GetValue(commandLine, null) == null)               {                  missingOptions.Add(property.Name);               }           }       }       string[] results = new string[missingOptions.Count];       missingOptions.CopyTo(results, 0);       return results;   } } 

The code that checks for an attribute is relatively simple. Given a PropertyInfo object (obtained via reflection), you call GetCustomAttributes() and specify the attribute sought, followed by whether to check any overloaded methods. (Alternatively, you can call the GetCustomAttributes() method without the attribute type to return all of the attributes.)

Although it is possible to place code for finding the CommandLineSwitchRequiredAttribute attribute within the CommandLineHandler's code directly, it makes for better object encapsulation to place the code within the CommandLineSwitchRequiredAttribute class itself. This is frequently the pattern for custom attributes. What better location to place code for finding an attribute than in a static method on the attribute class?

Initializing an Attribute through a Constructor

The call to GetCustomAttributes() returns an array of objects that will successfully cast to an Attribute array. However, since the attribute in this example didn't have any instance members, the only metadata information that it provided in the returned attribute was whether it appeared. Attributes can also encapsulate data, however. Listing 14.13 defines a CommandLineAliasAttribute attribute. This is another custom attribute, and it provides alias command-line options. For example, you can provide command-line support for /Help or /? as an abbreviation. Similarly, /S could provide an alias to /Subfolders that indicates that the command should traverse all the subdirectories.

To support this, you need to provide a constructor on the attribute. Specifically, for the alias you need a constructor that takes a string argument. (Similarly, if you want to allow multiple aliases, you need to define an attribute that has a params string array for a parameter.)

Listing 14.13. Providing an Attribute Constructor

 public class CommandLineSwitchAliasAttribute : Attribute {  public CommandLineSwitchAliasAttribute(string alias)                         {                                                                               Alias = alias;                                                          }                                                                           public string Alias   {       get { return _Alias; }       set { _Alias = value; }   }   private string _Alias; } class CommandLineInfo {  [CommandLineSwitchAliasAttribute("?")]                                      public bool Help   {       get { return _Help; }       set { _Help = value; }   }   private bool _Help;   // ... } 

The only restriction on the constructor is that when applying an attribute to a construct, only literal values and types (like typeof(int)) are allowed as arguments. This is to enable their serialization into the resulting CIL. It is not possible, therefore, to call a static method when applying an attribute; in addition, providing a constructor that takes arguments of type System.DateTime would be of little value, since there is no System.DateTime literal.

Given the constructor call, the objects returned from PropertyInfo.GetCustomAttributes() will be initialized with the specified constructor arguments, as demonstrated in Listing 14.14.

Listing 14.14. Retrieving a Specific Attribute and Checking Its Initialization

 PropertyInfo property =     typeof(CommandLineInfo).GetProperty("Help"); CommandLineSwitchAliasAttribute attribute =     (CommandLineSwitchAliasAttribute)         property.GetCustomAttributes(         typeof(CommandLineSwitchAliasAttribute), false)[0]; if(attribute.Alias == "?") {   Console.WriteLine("Help(?)"); }; 

Furthermore, as Listing 14.15 and Listing 14.16 demonstrate, you can use similar code in a GetSwitches() method on CommandLineAliasAttribute that returns a dictionary collection of all the switches, including those from the property names, and associate each name with the corresponding attribute on the command-line object.

Listing 14.15. Retrieving Custom Attribute Instances

 using System; using System.Reflection; using System.Collections.Generic; public class CommandLineSwitchAliasAttribute : Attribute {   public CommandLineSwitchAliasAttribute(string alias)   {       Alias = alias;   }   public string Alias   {       get { return _Alias; }       set { _Alias = value; }   }   private string _Alias;   public static Dictionary<string, PropertyInfo> GetSwitches(       object commandLine)   {       PropertyInfo[] properties = null;       Dictionary<string, PropertyInfo> options =           new Dictionary<string, PropertyInfo>();       properties = commandLine.GetType().GetProperties(           BindingFlags.Public | BindingFlags.NonPublic |           BindingFlags.Instance);       foreach (PropertyInfo property in properties)       {           options.Add(property.Name.ToLower(), property);           foreach (CommandLineSwitchAliasAttribute attribute in                        property.GetCustomAttributes(                                            typeof(CommandLineSwitchAliasAttribute),false))                      {              options.Add(attribute.Alias.ToLower(), property);           }       }       return options;   } } 

Listing 14.16. Updating CommandLineHandler.TryParse() to Handle Aliases

 using System; using System.Reflection; using System.Collections.Generic; public class CommandLineHandler {   // ...   public static bool TryParse(       string[] args, object commandLine,       out string errorMessage)   {       bool success = false;       errorMessage = null;       Dictionary<string, PropertyInfo> options =                                   CommandLineSwitchAliasAttribute.GetSwitches(                                   commandLine);                                                     foreach (string arg in args)       {           PropertyInfo property;           string option;           if (arg[0] == '/' || arg[0] =='-')           {               string[] optionParts = arg.Split(                   new char[] { ':' }, 2);               option = optionParts[0].Remove(0, 1).ToLower();               if (options.TryGetValue(option,out property))                              {                   success = SetOption(                       commandLine, property,                       optionParts, ref errorMessage);               }               else               {                   success = false;                   errorMessage = string.Format(                       "Option '{0}' is not supported.",                       option);               }           }       }       return success;   }     private static bool SetOption(         object commandLine, PropertyInfo property,         string[] optionParts, ref string errorMessage)     {         bool success;         if (property.PropertyType == typeof(bool))         {             // Last parameters for handling indexers             property.SetValue(                 commandLine, true, null);             success = true;         }         else         {             if ((optionParts.Length < 2)                 || optionParts[1] == ""                 || optionParts[1] == ":")             {                 // No setting was provided for the switch.                 success = false;                 errorMessage = string.Format(                     "You must specify the value for the {0} option.",                     property.Name);             }             else if (                 property.PropertyType == typeof(string))             {                 property.SetValue(                     commandLine, optionParts[1], null);                 success = true;             }             else if (property.PropertyType.IsEnum)             {                 success = TryParseEnumSwitch(                     commandLine, optionParts,                     property, ref errorMessage);             }             else             {                 success = false;                 errorMessage = string.Format(                     "Data type '{0}' on {1} is not supported.",                    property.PropertyType.ToString(),                    commandLine.GetType().ToString());             }         }         return success;     } } 

Beginner Topic: Using Hashtable Rather Than Dictionary<TKey,TValue>

Listing 14.15 uses the generic collection Dictionary<string, PropertyInfo>. Unfortunately, this is not available prior to 2.0, and instead, you have to use System.Collections.Hashtable. This is virtually a search-and-replace substitution, except in the call to tryGetValue(), which is not available on Hashtable. In that case, you can retrieve the value using the index operator and then check for null, as follows:

     if((property = (PropertyInfo)options[option])!=null)     { // ... } 



System.AttributeUsageAttribute

Most attributes are intended to decorate only particular constructs. For example, it makes no sense to allow CommandLineOptionAttribute to decorate a class or an assembly. Those contexts would be meaningless. To avoid inappropriate use of an attribute, custom attributes can be decorated with System.AttributeUsageAttribute. Listing 14.17 (for CommandLineOptionAttribute) demonstrates how to do this.

Listing 14.17. Restricting the Constructs an Attribute Can Decorate

 [AttributeUsage(AttributeTargets.Property)] public class CommandLineSwitchAliasAttribute : Attribute {   // ... } 

If the attribute is used inappropriately, as it is in Listing 14.18, it will cause a compile-time error, a characteristic unique to predefined attributes (see page 535), as Output 14.5 demonstrates.

Listing 14.18. AttributeUsageAttribute Restricting Where to Apply an Attribute

 // ERROR: The attribute usage is restricted to properties [CommandLineSwitchAlias("?")] class CommandLineInfo { } 

Output 14.5.

[View full width]

 ...Program+CommandLineInfo.cs(24,17): error CS0592 Attribute 'CommandLineSwitchAlias' is not valid on this declaration type. It is valid on  'property, indexer' declarations only. 

AttributeUsageAttribute's constructor takes an AttributesTargets flag. This enum provides a list of all the possible targets that the runtime allows an attribute to decorate. For example, if you also allowed CommandLineSwitchAliasAttribute on a field, you would update the AttributeUsageAttribute application as shown in Listing 14.19.

Listing 14.19. Limiting an Attribute's Usage with AttributeUsageAttribute

 // Restrict the attribute to properties and methods [AttributeUsage(                                                          AttributeTargets.Field | AttributeTargets.Property)]                  public class CommandLineSwitchAliasAttribute : Attribute {   // ... } 

Named Parameters

In addition to restricting what an attribute can decorate, AttributeUsageAttribute provides a mechanism for allowing duplicates of the same attribute on a single construct. The syntax appears in Listing 14.20.

Listing 14.20. Using a Named Parameter

 [AttributeUsage(AttributeTargets.Property, AllowMultiple=true)] public class CommandLineSwitchAliasAttribute  Attribute {   // ... } 

The syntax is different from the constructor initialization syntax discussed earlier. The AllowMultiple parameter is a named parameter, which is a designation that is unique to attributes. Named parameters provide a mechanism for setting specific public properties and fields within the attribute constructor call, even though the constructor includes no corresponding parameters. The named attributes are optional designations, but they provide a means of setting additional instance data on the attribute without providing a constructor parameter for the purpose. In this case, AttributeUsageAttribute includes a public member called AllowMultiple. Therefore, you can set this member using a named parameter assignment when you use the attribute. Assigning named parameters must occur as the last portion of a constructor, following any explicitly declared constructor parameters.

Named parameters allow for assigning attribute data without providing constructors for every conceivable combination of which attribute properties are specified and which are not. Since many of an attribute's properties may be optional, this is a useful construct in many cases.

Beginner Topic: FlagsAttribute

Chapter 8 introduced enums and included an Expert Topic in regard to FlagsAttribute. This is a framework-defined attribute that targets enums which represent flag type values. Here is similar text as a Beginner Topic, starting with the sample code shown in Listing 14.21.

Listing 14.21. Using FlagsAttribute

[View full width]

 // FileAttributes defined in System.IO. [Flags]  // Decorating an enum with FlagsAttribute.                          public enum FileAttributes {   ReadOnly =          2^0,      // 000000000000001   Hidden =            2^1,      // 000000000000010   // ... } using System; using System.Diagnostics; using System.IO; class Program {   public static void Main()   {       // ...       string fileName = @"enumtest.txt";       FileInfo file = new FileInfo(fileName);       file.Attributes = FileAttributes.Hidden |           FileAttributes.ReadOnly;       Console.WriteLine("\"{0}\" outputs as \"{1}\"",          file.Attributes.ToString().Replace(",", " |"),           file.Attributes);       FileAttributes attributes =           (FileAttributes) Enum.Parse(typeof(FileAttributes),           file.Attributes.ToString());       Console.WriteLine(attributes);        // ...   } } 

Output 14.6 shows the results of Listing 14.21.

Output 14.6.

 "ReadOnly | Hidden" outputs as "ReadOnly, Hidden" 

The flag documents that the enumeration values can be combined. Furthermore, it changes the behavior of the ToString() and Parse() methods. For example, calling ToString() on an enumeration that is decorated with FlagsAttribute writes out the strings for each enumeration flag that is set. In Listing 14.21, file.Attributes.ToString() returns "ReadOnly, Hidden" rather than the 3 it would have returned without the FileAttributes flag. If two enumeration values are the same, the ToString() call would return the first one. As mentioned earlier, however, you should use this with caution because it is not localizable.

Parsing a value from a string to the enumeration also works. Each enumeration value identifier is separated by a comma.

It is important to note that FlagsAttribute does not automatically assign the unique flag values or check that they have unique values. The values of each enumeration item still must be assigned explicitly.


Predefined Attributes

The AttributeUsageAttribute attribute has a special characteristic that you didn't see in the custom attributes you have created thus far in this book. This attribute affects the behavior of the compiler, causing the compiler to sometimes report an error. Unlike the reflection code you wrote earlier for retrieving CommandLineRequiredAttribute and CommandLineSwitchAliasAttribute, AttributeUsageAttribute has no runtime code; instead, it has built-in compiler support.

AttributeUsageAttribute is a predefined attribute. Not only do such attributes provide additional metadata about the constructs they decorate, but also the runtime and compiler behave differently in order to facilitate these attributes' functionality. Attributes like AttributeUsageAttribute, FlagsAttribute, ObsoleteAttribute, and ConditionalAttribute are examples of predefined attributes. They include special behavior that only the CLI provider or compiler can offer because there are no extension points for additional noncustom attributes. In contrast, custom attributes are entirely passive. Listing 14.21 includes a couple of predefined attributes; Chapter 15 includes a few more.

System.ConditionalAttribute

Within a single assembly, the System.Diagnostics.ConditionalAttribute attribute behaves a little like the #if/#endif preprocessor identifier. However, instead of eliminating the CIL code from the assembly, System.Diagnostics.ConditionalAttribute will optionally cause the call to behave like a no-op, an instruction that does nothing. Listing 14.22 demonstrates the concept, and Output 14.7 shows the results.

Listing 14.22. Using Reflection with Generic Types

 #define CONDITION_A using System; using System.Diagnostics; public class Program {   public static void Main()   {       Console.WriteLine("Begin...");       MethodA();       MethodB();       Console.WriteLine("End...");   }   [Conditional("CONDITION_A")]   static void MethodA()   {       Console.WriteLine("MethodA() executing...");   }   [Conditional("CONDITION_B")]   static void MethodB()   {       Console.WriteLine("MethodB() executing...");   } } 

Output 14.7.

 Begin... MethodA() executing... End... 

This example defined CONDITION_A, so MethodA() executed normally. CONDITION_B, however, was not defined through either #define or by using the csc.exe /Define option. As a result, all calls to Program.MethodB() from within this assembly will do nothing and don't even appear in the code.

Functionally, ConditionalAttribute is similar to placing a #if/#endif around the method invocation. The syntax is cleaner, however, because developers create the effect by adding the ConditionalAttribute attribute to the target method without making any changes to the caller itself.

Note that the C# compiler notices the attribute on a called method during compilation, and assuming the preprocessor identifier exists, it eliminates any calls to the method. Note also that ConditionalAttibute does not affect the compiled CIL code on the target method itself (besides the addition of the attribute metadata). Instead, it affects the call site during compilation by removing the calls. This further distinguishes ConditionalAttribute from #if/#endif when calling across assemblies. Because the decorated method is still compiled and included in the target assembly, the determination of whether to call a method is based not on the preprocessor identifier in the callee's assembly, but rather, on the caller's assembly. In other words, if you create a second assembly that defines CONDITION_B, any calls to Program.MethodB() from the second assembly will execute. This is a useful characteristic in many tracing and testing scenarios. In fact, calls to System.Diagnostics.Trace and System.Diagnostics.Debug use this trait with ConditionalAttributes on trACE and DEBUG preprocessor identifiers.

Because methods don't execute whenever the preprocessor identifier is not defined, ConditionalAttribute may not be used on methods that include an out parameter or specify a return other than void. Doing so causes a compile-time error. This makes sense because possibly none of the code within the decorated method will execute, so it is unknown what to return to the caller. Similarly, properties cannot be decorated with ConditionalAttribute. The AttributeUsage (see the section titled System.AttributeUsageAttribute, earlier in this chapter) for ConditionalAttribute is AttributeTargets.Class (starting in .NET 2.0) and AttributeTargets.Method. This allows the attribute to be used on either a method or a class. However, the class usage is special because ConditionalAttribute is allowed only on System.Attribute-derived classes.

When ConditionalAttribute decorates a custom attribute, a feature started in .NET 2.0, the latter can be retrieved via reflection only if the conditional string is defined in the calling assembly. Without such a conditional string, reflection that looks for the custom attribute will fail to find it.

System.ObsoleteAttribute

As mentioned earlier, predefined attributes affect the compiler's and/or the runtime's behavior. ObsoleteAttribute provides another example of attributes affecting the compiler's behavior. The purpose of ObsoleteAttribute is to help with the versioning of code, providing a means of indicating to callers that a particular member is no longer current. Listing 14.23 is an example of ObsoleteAttribute usage. As Output 14.8 shows, any callers that compile code that invokes a member marked with ObsoleteAttribute will cause a compile-time warning, optionally an error.

Listing 14.23. Using ObsoleteAttribute

 class Program {   public static void Main()   {       ObsoleteMethod();   }   [Obsolete]   public static void ObsoleteMethod()   {   } } 

Output 14.8.

 c\SampleCode\ObsoleteAttributeTest.cs(24,17) warning CS0612 Program.ObsoleteMethod()' is obsolete 

In this case, ObsoleteAttribute simply displays a warning. However, there are two additional constructors on the attribute. One of them, ObsoleteAttribute(string message), appends the additional message argument to the compiler's obsolete message. The second, however, is a bool error parameter that forces the warning to be recorded as an error instead.

ObsoleteAttribute allows third parties to notify developers of deprecated APIs. The warning (not an error) allows the original API to work until the developer is able to update the calling code.

Serialization-Related Attributes

Using predefined attributes, the framework supports the capacity to serialize objects onto a stream so that they can be deserialized back into objects at a later time. This provides a means of easily saving a document type object to disk before shutting down an application. Later on, the document may be deserialized so that the user can continue to work on it.

In spite of the fact that an object can be relatively complex and include links to many other types of objects that also need to be serialized the serialization framework is easy to use. In order for an object to be serializable, the only requirement is that it includes a System.SerializableAttribute. Given the attribute, a formatter class reflects over the serializable object and copies it into a stream (see Listing 14.24).

Listing 14.24. Saving a Document Using System.SerializableAttribute

 using System; using System.IO; using System.Runtime.Serialization.Formatters.Binary; class Program {   public static void Main()   {       Stream stream;       Document documentBefore = new Document();       documentBefore.Title =           "A cacophony of ramblings from my potpourri of notes";       Document documentAfter;       using (stream = File.Open(           documentBefore.Title + ".bin", FileMode.Create))       {           BinaryFormatter formatter =               new BinaryFormatter();            formatter.Serialize(stream, documentBefore);                             }       using (stream = File.Open(           documentBefore.Title + ".bin", FileMode.Open))       {           BinaryFormatter formatter =               new BinaryFormatter();           documentAfter = (Document)formatter.Deserialize(                                 stream);                                                             }       Console.WriteLine(documentAfter.Title);   } } // Serializable classes use SerializableAttribute. [Serializable]                                                                class Document {   public string Title = null;   public string Data = null;   [NonSerialized]                                                             public long _WindowHandle = 0;   class Image   {   }   [NonSerialized]                                                             private Image Picture = new Image(); } 

Output 14.9 shows the results of Listing 14.24.

Output 14.9.

 A cacophony of ramblings from my potpourri of notes 

Listing 14.24 serializes and deserializes a Document object. Serialization involves instantiating a formatter (this example uses System.Runtime .Serialization.Formatters.Binary.BinaryFormatter) and calling Serialization() with the appropriate stream object. Deserializing the object simply involves a call to the formatter's Deserialize() method, specifying the stream that contains the serialized object as an argument. However, since the return from Deserialize() is of type object, you also need to cast it specifically to the type that was serialized.

Notice that serialization occurs for the entire object graph (all the items associated with the serialized object [Document] via a field). Therefore, all fields in the object graph also must be serializable.

System.NonSerializable. Fields that are not serializable should be decorated with the System.NonSerializable attribute. This tells the serialization framework to ignore them. The same attribute should appear on fields that should not be persisted for use case reasons. Passwords and Windows handles are good examples of fields that should not be serialized: Windows handles because they change each time a window is re-created and passwords because data serialized into a stream is not encrypted and can easily be accessed. Consider the Notepad view of the serialized document in Figure 14.2.

Figure 14.2. BinaryFormatter Does Not Encrypt Data


Listing 14.24 set the Title field and the resulting *.BIN file includes the text in plain view.

Providing Custom Serialization. One way to add encryption is to provide custom serialization. Ignoring the complexities of encrypting and decrypting, this requires implementing the ISerializable interface in addition to using SerializableAttribute. The interface only requires the GetObjectData() method to be implemented. However, this is sufficient only for serialization. In order to also support deserialization, it is necessary to provide a constructor that takes parameters of type System.Runtime.Serialization.SerializationInfo and System.Runtime.Serialization.StreamingContext (see Listing 14.25).

Listing 14.25. Implementing System.Runtime.Serialization.ISerializable

 using System; using System.Runtime.Serialization; [Serializable] class EncryptableDocument :     ISerializable {   public EncryptableDocument(){ }   enum Field   {       Title,       Data   }   public string Title;   public string Data;   public static string Encrypt(string data)   {       string encryptedData = data;       // Key-based encryption ...       return encryptedData;   }      public static string Decrypt(string encryptedData)   {     string data = encryptedData;     // Key-based decryption...       return data;   } #region ISerializable Members       public void GetObjectData(       SerializationInfo info, StreamingContext context)   {       info.AddValue(           Field.Title.ToString(), Title);       info.AddValue(           Field.Data.ToString(), Encrypt(Data));   }   public EncryptableDocument(       SerializationInfo info, StreamingContext context)   {       Title = info.GetString(           Field.Title.ToString());       Data = Decrypt(info.GetString(           Field.Data.ToString()));   } #endregion } 

Essentially, the System.Runtime.Serialization.SerializationInfo object is a collection of name/value pairs. When serializing, the GetObject() implementation calls AddValue(). To reverse the process, you call one of the Get*() members. In this case, you encrypt and decrypt prior to serialization and deserialization, respectively.

Versioning the Serialization. One more serialization point deserves mentioning: versioning. Objects such as documents may be serialized using one version of an assembly and deserialized using a newer version, sometimes the reverse. Without paying attention, however, version incompatibilities can easily be introduced, sometimes unexpectedly. Consider the scenario shown in Table 14.1.

Table 14.1. Deserialization of a New Version Throws an Exception

Step

Description

Code

0

Define a class decorated with System.SerializableAttribute.

 [Serializable] class Document {         public string Title;         public string Data;  } 


1

Add a field or two (public or private) of any serializable type.

 

2

Serialize the object to a file called *.v1.bin.

[View full width]

Stream stream; Document documentBefore =new Document(); documentBefore.Title = "A cacophony of ramblings from my potpourri of notes"; Document documentAfter; using (stream = File.Open( documentBefore.Title + ".bin", FileMode.Create)) { BinaryFormatter formatter = new BinaryFormatter(); formatter.Serialize( stream, documentBefore); }


3

Add an additional field to the serializable class.

 [Serializable] class Document  {         public string Title;         public string Author;         public string Data;   } 


4

Deserialize the *v1.bin file into the new object (Document) version.

[View full width]

using (stream = File.Open( documentBefore.Title + ".bin", FileMode.Open)) { BinaryFormatter formatter = new BinaryFormatter(); documentAfter = (Document)formatter.Deserialize( stream); }



Surprisingly, even though all you did was to add a new field, deserializing the original file throws a System.Runtime.Serialization.SerializationException. This is because the formatter looks for data corresponding to the new field within the stream. Failure to locate such data throws an exception.

To avoid this, the 2.0 framework includes a System.Runtime.Serialization.OptionalFieldAttribute. When you require backward compatibility, you must decorate serialized fieldseven private oneswith OptionalFieldAttribute (unless, of course, a latter version begins to require it).

Unfortunately, System.Runtime.Serialization.OptionalFieldAttribute is not supported in the earlier framework version. Instead, it is necessary to implement ISerializable, just as you did for encryption, saving and retrieving only the fields that are available. Assuming the addition of the Author field, for example, the implementation shown in Listing 14.26 is required for backward-compatibility support prior to the 2.0 framework:

Listing 14.26. Backward Compatibility Prior to the 2.0 Framework

 [Serializable] public class VersionableDocument : ISerializable {   enum Field   {       Title,       Author,       Data,   }   public VersionableDocument()   {   }   public string Title;   public string Author;   public string Data;   #region ISerializable Members   public void GetObjectData(       SerializationInfo info, StreamingContext context)   {       info.AddValue(Field.Title.ToString(), Title);       info.AddValue(Field.Author.ToString(), Author);       info.AddValue(Field.Data.ToString(), Data);   }   public VersionableDocument(       SerializationInfo info, StreamingContext context)   {       foreach(SerializationEntry entry in info)       {           switch ((Field)Enum.Parse(typeof(Field), entry.Name))           {               case Field.Title:                   Title = info.GetString(                        Field.Title.ToString());                   break;               case Field.Author:                   Author = info.GetString(                       Field.Author.ToString());                   break;               case Field.Data:                   Data = info.GetString(                       Field.Data.ToString());                   break;           }       }   }   #endregion } 

Serializing in GetObjectData() simply involves serializing all fields (assume here that version 1 does not need to open documents from version 2). On deserialization, however, you can't simply call GetString("Author") because if no such entry exists, it will throw an exception. Instead, iterate through all the entries that are in info and retrieve them individually.

Advanced Topic: System.SerializableAttribute and the CIL

In many ways, the serialize attributes behave just like custom attributes. At runtime, the formatter class searches for these attributes, and if the attributes exist, the classes are formatted appropriately. One of the characteristics that make System.SerializableAttribute not just a custom attribute, however, is the fact that the CIL has a special header notation for serializable classes. Listing 14.27 shows the class header for the Person class in the CIL.

Listing 14.27. The CIL for SerializableAttribute

 class auto ansi serializable nested private   beforefieldinit Person   extends [mscorlib]System.Object { } // end of class Person 

In contrast, attributes (including most predefined attributes) generally appear within a class definition (see Listing 14.28).

Listing 14.28. The CIL for Attributes in General

 .class private auto ansi beforefieldinit Person        extends [mscorlib]System.Object {   .custom instance void CustomAttribute::.ctor() =                         ( 01 00 00 00 )                                              } // end of class Person 

In Listing 14.28, CustomAttribute is the full name of the decorating attribute.

SerializableAttribute translates to a set bit within the metadata tables. This makes SerializableAttribute a pseudoattribute, an attribute that sets bits or fields in the metadata tables.





Essential C# 2.0
Essential C# 2.0
ISBN: 0321150775
EAN: 2147483647
Year: 2007
Pages: 185

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net