Custom Attributes


The first half of this chapter concentrated on some of the attributes contained within the .NET Framework. That's not the whole story though — you can also create your own attributes.

In this section, you will only scratch the surface of what can be done with custom attributes. You will look at the following (invented) attributes:

  • TestCaseAttribute: Links the code used to test a class to the class itself

  • BugFixAttribute: Records who altered what and when within the source code

  • DatabaseTableAttribute and DatabaseColumnAttribute: Shows how to produce database schemas from .NET classes

A custom attribute is simply a special class that must comply with these two specifications:

  • A custom attribute must derive from System.Attribute.

  • The constructor(s) for an attribute may only contain types that can be resolved at compile time — such as strings and integers.

The restriction on the types of parameters allowable on the attribute constructor(s) is due to the way that attributes are persisted into the assembly metadata. When you use an attribute within code, you are using the attribute's constructor inline. For example:

 [assembly: AssemblyKeyFile ("Company.Public") ] 

This attribute is persisted into the assembly metadata as an instruction to call a constructor of AssemblyKeyFileAttribute, which accepts a string. In the preceding example that string is Company.Public. If you define a custom attribute, users of the attribute are basically writing parameters to the constructor of the class.

The first example, TestCaseAttribute, shows how test classes can be coupled with the code that they test.

TestCaseAttribute

When unit testing software it is common to define a set of test classes that exercise your classes to ensure that they perform as expected. This is especially true in regression testing, where you want to ensure that by fixing a bug or adding extra functionality, you have not broken something else.

When working with regulated customers (such as producing software for pharmaceutical companies who work under strict controls from government agencies), it is necessary to provide cross-references between code and tests. the TestCaseAttribute presented here can help to trace between a class and its test class.

The full source code is available in the Chapter27/TestCase directory.

To create a custom attribute class, you must:

  • Create a class derived from System.Attribute

  • Create the constructor(s) and public properties as required

  • Attribute the class to define where it is valid to use your custom attribute

Each of these steps is discussed in turn in the following sections.

Creating the Custom Attribute Class

This is the simplest step. All you need to do here is create a class derived from System.Attribute:

 public class TestCaseAttribute : Attribute { } 

Creating Constructors and Properties

As mentioned earlier, when the user uses an attribute he or she is effectively calling the attribute's constructor. For the test case attribute, you want to define the type of object used to test a given class, so you'll use a String value, as this permits the tested assembly to be deployed separately from the testing assembly:

using System; public class TestCaseAttribute : Attribute { /// <summary> /// Constructor for the class /// </summary> /// <param name="testCase">The object which contains /// the test case code</param> public TestCaseAttribute (string testCase) { _testCase = testCase; } /// <summary> /// Perform the test. /// </summary> public void Test () { // Create an instance of the class under test. // The test case object created is assumed to // test the object in its constructor. object o = Activator.CreateInstance (TestCase); } /// <summary> /// Get the test case type object. /// </summary> /// <value>Returns the type of object that runs the test</value> public Type TestCase { get { if (null == _testType) { _testType = Type.GetType(_testCase); } return _testType; } } /// <summary> /// Store the test case object name. /// </summary> private string  _testCase; /// <summary> /// Cache the type object for the test case. /// </summary> private Type    _testType = null ; }

This defines a single constructor and a read-only member variable TestCase. The Test method is used to instantiate the test case; this simple example will perform the tests within the constructor of the test case class.

Attributing the Class for Usage

The last thing you need to do is attribute your attribute class to indicate where it can be used. For the test case attribute you want to say "this attribute is only valid on classes." You can decide where an attribute that you create is valid. This will be explained in more detail later in the chapter:

 [AttributeUsage(AttributeTargets.Class, AllowMultiple=false, Inherited=true)] public class TestCaseAttribute : Attribute ...

The AttributeUsage attribute has a single constructor, which takes a value from the AttributeTargets enum (described in full later in this section). Here, you have stated that the only valid place to put a TestCase attribute is on a class. You can specify several values in this enum using the | symbol for a logical OR — so other attributes might be valid on classes, or constructors, or properties.

The definition of the attribute here also utilized two properties of that attribute — AllowMultiple and Inherited. I will discuss these properties more fully later in the section.

Now, you need an object to test with a test case. There's nothing particularly magic about this class:

 [TestCase ("TestCase.TestAnObject, TestCase")] public class SomeCodeOrOther { public SomeCodeOrOther () { } public int Do () { return 999; } } 

The class is prefixed with the TestCase attribute and uses a string to define the class used to test the code in question. To complete this example, you need to write the test class. The object used to exercise an instance of the code under test is presented here:

 public class TestAnObject { public TestAnObject () { // Exercise the class under test. SomeCodeOrOther scooby = new SomeCodeOrOther (); if (scooby.Do () != 999) throw new Exception ("Pesky Kids"); } } 

This class simply instantiates the class under test, calls a method, and throws an exception if the returned value is not what was expected. A more complete test case would exercise the object under test completely, by calling all methods on that class, passing in values out of range to check for error conditions, and possibly setting up some other classes used for contextual information — if testing a class that accesses a database, you might pass in a connection object.

Now for the main code. This class will loop through all types in the assembly, looking for those that have the TestCaseAttribute defined. When found, the attribute is retrieved and the Test() method called:

 using System; using System.Reflection;     [AttributeUsage(AttributeTargets.Class,AllowMultiple=false,Inherited=true)] public class TestCaseAttribute : Attribute {     // Code removed for brevity } /// <summary> /// A class that uses the TestCase attribute /// </summary> [TestCaseAttribute("TestCase.TestAnObject, TestCase")] public class SomeCodeOrOther {     // Code removed for brevity }     // Main program class public class UnitTesting { public static void Main () { // Find any classes with test cases in the current assembly. Assembly a = Assembly.GetExecutingAssembly (); // Loop through the types in the assembly and test them if necessary. System.Type[] types = a.GetExportedTypes (); foreach (System.Type t in types) { // Output the name of the type... Console.WriteLine ("Checking type {0}", t.ToString ()); // Does the type include the TestCaseAttribute custom attribute? object[] atts = t.GetCustomAttributes(typeof(TestCaseAttribute), false); if (1 == atts.Length) { Console.WriteLine ("  Found TestCaseAttribute: Running Tests"); // OK, this class has a test case. Run it... TestCaseAttribute tca = atts[0] as TestCaseAttribute; try { // Perform the test... tca.Test (); Console.WriteLine ("  PASSED!"); } catch (Exception ex) { Console.WriteLine ("  FAILED!"); Console.WriteLine (ex.ToString ()); } } } } } 

The new section of code is highlighted. When run, the program gets the executing assembly via the static GetExecutingAssembly() method of the Assembly class. It then calls GetExportedTypes() on that assembly to find a list of all object types publicly accessible in the assembly.

It then checks each exported type in the assembly to see if it includes the TestCase attribute. It retrieves the attribute if it exists (which internally constructs the attribute instance, passing the parameters used within the code to the constructor of the object) and calls the Test method, which tests the code.

When run, the output from the program is:

Checking type TestCaseAttribute Checking type SomeCodeOrOther    Found TestCaseAttribute: Running Tests    PASSED! Checking type TestAnObject Checking type UnitTesting

System.AttributeUsageAttribute

When you're defining a custom attribute class, you must define the type or types on which the attribute can be used. In the preceding example, the TestCase attribute is valid only for use on classes. To define where an attribute can be placed, you add another attribute — AttributeUsage.

In its simplest form, this can be used as shown here:

 [AttributeUsage(AttributeTargets.Class)] 

The single parameter is an enumeration defining where your attribute is valid. If you attempt to attribute a method with the TestCase attribute, you'll receive an error message from the compiler. An invalid usage could be:

 public class TestAnObject { [TestCase ("TestCase.TestAnObject, TestCase")]   // Invalid here public TestAnObject () { etc... } } 

The error reported is:

TestCase.cs(54,4): error CS0592: Attribute 'TestCase' is not valid on this         declaration type. It is valid on 'class' declarations only. 

The AttributeTargets enum defines the following members, which can be combined together using the or operator (|) to define a set of elements that this attribute is valid on.

AttributeTargets Value

Description

All

The attribute is valid on anything within the assembly.

Assembly

The attribute is valid on the assembly — an example is the AssemblyTitle attribute shown earlier in the chapter.

Class

The attribute is valid on a class definition. the TestCase attribute used this value. Another example is the Serializable attribute.

Constructor

The attribute is valid only on class constructors.

Delegate

The attribute is valid only on a delegate.

Enum

The attribute can be added to enumerated values. One example of this attribute is the System.FlagsAttribute, which when applied to an enum defines that the user can use the bitwise or operator to combine values from the enumeration. the AttributeTargets enum uses this attribute.

Event

The attribute is valid on event definitions.

Field

The attribute can be placed on a field, such as an internal member variable. An example of this is the NonSerialized attribute, which was used earlier to define that a particular value should not be stored when the class was serialized.

Interface

The attribute is valid on an interface. One example of this is the GuidAttribute defined within System.Runtime.InteropServices, which permits you to explicitly define the GUID for an interface.

Method

The attribute is valid on a method. the OneWay attribute from System.Runtime.Remoting.Messaging uses this value.

Module

The attribute is valid on a module. An assembly may be created from a number of code modules, so you can use this to place the attribute on an individual module and not the whole assembly.

Parameter

The attribute can be applied to a parameter within a method definition.

Property

The attribute can be applied to a property.

ReturnValue

The attribute is associated with the return value of a function.

Struct

The attribute is valid on a structure.

Attribute Scope

In the first examples in the chapter you saw the Assembly* attributes, which all included syntax similar to the following:

 [assembly: AssemblyTitle("AttributePeek")] 

The assembly: string defines the scope of the attribute, which in this case tells the compiler that the AssemblyTitle attribute should be applied to the assembly itself. You need to use only the scope modifier when the compiler cannot work out the scope itself. For example, if you wish to add an attribute to the return value of a function:

 [MyAttribute ()] public long DoSomething () { ... } 

When the compiler reaches this attribute, it takes an educated guess that you are applying the attribute to the method itself, which is not what you want here, so you can add a modifier to indicate exactly what the attribute is attached to:

 [return:MyAttribute ()] public long DoSomething () { ... } 

If you wish to define the scope of the attribute, choose one of the following values:

  • assembly: The attribute applies to the assembly.

  • field: The attribute applies to a field of an enum or class.

  • event: The attribute applies to an event.

  • method: The attribute applies to the method it precedes.

  • module: The attribute is stored in the module.

  • param: The attribute applies to a parameter.

  • property: The attribute is stored against the property.

  • return: The apply the attribute to the return value of a function.

  • type: The attribute applies to a class, interface, or struct.

Many of these are rarely used, because the scope is not normally ambiguous. However, for assembly, module, and return values you will have to use the scope flag. If there is some ambiguity as to where the attribute is defined, the compiler will choose which object the attribute will be assigned to. This is most common when attributing the return value of a function, as shown here:

 [SomeAttribute] public string DoSomething (); 

Here, the compiler guesses that the attribute applies to the method and not the return value. You need to define the scope in the following way to get the desired effect:

 [return:SomeAttribute] public string DoSomething (); 

AttributeUsage.AllowMultiple

This attribute defines whether the user can add one or more of the same attributes to the element. For example, you could create an attribute that lists all of the bug fixes applied to a section of code. As the assembly evolves, you may want to supply details of several bug fixes on a method.

BugFixAttribute

The code that follows defines a simple BugFixAttribute and uses the AllowMultiple flag so that the attribute can be used more than once on any given chunk of code:

 [AttributeUsage (AttributeTargets.Class | AttributeTargets.Property |  AttributeTargets.Method | AttributeTargets.Constructor , AllowMultiple=true)] public class BugFixAttribute : Attribute { public BugFixAttribute (string bugNumber , string comments) { BugNumber = bugNumber; Comments = comments; } public readonly string BugNumber; public readonly string Comments; } 

The BugFix attribute constructor takes a bug number and a comment string and is marked with AllowMultiple=true to indicate that it can be used as follows:

 [BugFix("101","Created some methods")] public class MyBuggyCode { [BugFix("90125","Removed call to base()")] public MyBuggyCode () { } [BugFix("2112","Returned a non null string")] [BugFix("38382","Returned OK")] public string DoSomething () { return "OK"; } } 

The syntax for setting the AllowMultiple flag is a little strange. The constructor for AttributeUsage only takes a single parameter — the list of flags where the attribute can be used. AllowMultiple is a property on the AttributeUsage attribute, and so the syntax that follows means "construct the attribute, and then set the value of the AllowMultiple property to true":

 [AttributeUsage (AttributeTargets.Class | AttributeTargets.Property |  AttributeTargets.Method | AttributeTargets.Constructor , AllowMultiple=true)] public class BugFixAttribute : Attribute { ... } 

A similar method is used to set the Inherited property. If a custom attribute has properties, you can set these in the same manner. One example might be to add on the name of the person who fixed the bug:

public readonly string BugNumber; public readonly string Comments; public string Author = null; public override string ToString () { if (null == Author) return string.Format ("BugFix {0} : {1}" ,  BugNumber , Comments); else return string.Format ("BugFix {0} by {1} : {2}" ,  BugNumber , Author , Comments); } 

This adds the Author property, and an overridden ToString() implementation that will display the full details if the Author property is set. If the Author property is not defined when you attribute your code, the output from the BugFix attribute just shows the bug number and comments. the ToString() method would be used to display a list of bug fixes for a given section of code — perhaps to print and file away somewhere. Once you have written the BugFix attribute, you need some way to report the fixes made on a class and the members of that class.

The method of reporting bug fixes for a class is to pass the class type (again a System.Type) to the DisplayFixes function shown below. This also uses reflection to find any bug fixes applied to the class, and then iterates through all methods of that class looking for bug fix attributes.

This example can be found in the Chapter22\BugFix directory:

 public static void DisplayFixes (System.Type t) { // Get all bug fixes for the given type, // which is assumed to be a class. object[]  fixes = t.GetCustomAttributes (typeof (BugFixAttribute) , false); Console.WriteLine ("Displaying fixes for {0}" , t); // Display the big fix information. foreach (BugFixAttribute bugFix in fixes) Console.WriteLine ("  {0}" , bugFix); // Now get all members (i.e., functions) of the class. foreach (MemberInfo member in t.GetMembers (BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)) { // And find any big fixes applied to these too. object[] memberFixes = member.GetCustomAttributes(typeof(BugFixAttribute) , false); if (memberFixes.Length > 0) { Console.WriteLine ("  {0}" , member.Name); // Loop through and display these. foreach (BugFixAttribute memberFix in memberFixes) Console.WriteLine ("    {0}" , memberFix); } } } 

The first thing the code does is to retrieve all BugFix attributes from the type itself:

object[] fixes = t.GetCustomAttributes (typeof (BugFixAttribute) ,                                                             false);

These are enumerated and displayed. The code then loops through all members defined on the class, by using the GetMembers() method:

foreach (MemberInfo member in t.GetMembers (           BindingFlags.Instance | BindingFlags.Public |           BindingFlags.NonPublic | BindingFlags.Static))

GetMembers retrieves properties, methods, and fields from a given type. To limit the list of members that are returned, the BindingFlags enum is used (which is defined within System.Reflection).

The binding flags passed to this method indicate which members you are interested in — in this case, you'd like all instance and static members, regardless of visibility, so you specify Instance and Static, together with Public and NonPublic members.

After getting all members, you loop through these, finding any BugFix attributes associated with the particular member, and output these to the console. To output a list of bug fixes for a given class, all you do is call the static DisplayFixes() method, passing in the class type:

BugFixAttribute.DisplayFixes (typeof (MyBuggyCode));

For the MyBuggyCode class presented earlier, this results in the following output:

Displaying fixes for MyBuggyCode    BugFix 101 : Created some methods    DoSomething       BugFix 2112 : Returned a non-null string       BugFix 38382 : Returned OK    .ctor       BugFix 90125 : Removed call to base()

If you wanted to display fixes for all classes in a given assembly, you could use reflection to get all the types from the assembly, and pass each one to the static BugFixAttribute.DisplayFixes method.

AttributeUsage.Inherited

An attribute may be defined as inheritable by setting this flag when defining the custom attribute:

 [AttributeUsage (AttributeTargets.Class, Inherited = true)] public class BugFixAttribute { ... } 

This indicates that the BugFix attribute will be inherited by any subclasses of the class using the attribute, which may or may not be desirable. In the case of the BugFix attribute, this behavior would probably not be desirable, because a bug fix normally applies to a single class and not the derived classes.

Say that you have the following abstract class with a bug fix applied:

 [BugFix("38383","Fixed that abstract bug")] public abstract class GenericRow : DataRow { public GenericRow (System.Data.DataRowBuilder builder) : base (builder) { } } 

If you created a subclass from this class, you wouldn't want the same BugFix attribute to be reflected in the subclass — the subclass has not had that fix done on it. However, if you were defining a set of attributes that linked members of a class to fields in a database table, then you probably would want to inherit these attributes.

It's fairly common when defining database schema to come up with a set of standard columns that most tables include, such as Name and Description. You could code a base class with these fields and include a custom attribute that links a property in the class with a database column. Further subclasses could add more fields in as appropriate.

In the following example, you create DatabaseTable and DatabaseColumn attributes that can be applied to a class so that a database table suitable for persisting that class can be generated automatically.

Generating Database Tables Using Attributes

This final example shows how attributes can be used from .NET classes to generate the database schema — a database design including tables, columns, and data types — so that .NET objects can create their own database tables to be persisted into. You will see how to extract this schema information to generate the SQL to create the tables in a database and to construct in-memory DataTable objects.

As you saw in Chapter 24, you use DataSet, DataTable, DataRow, and DataAdapter objects to access data in ADO.NET. It is important to keep your use of these objects in synch with the underlying database structure. If a database structure changes over time, then you need to ensure that updates to tables, such as adding in new columns, are reflected in the classes that access the database.

In this example, you create subclasses of DataRow that are designed specifically for storing data from a particular database table. In cases where the underlying database schema will not change often, this can provide a very effective way to access databases. If your schema is likely to change frequently, or if you permit users to modify the database structure, it might be better to generate the DataTable objects dynamically by requesting schema information from the database and building the data tables on the fly.

Figure 27-8 shows the relationship between the ADO.NET classes and the underlying database table.

image from book
Figure 27-8

The DataSet consists of one or more DataTable objects, each one having DataRow objects that map to a single row within the database table. The data adapter is used to retrieve data from the database into the DataTable and to write data from the DataTable back to the database.

The DataTable consists largely of boilerplate code, so you will define a base class DataTable object that can serve as a generic container for DataRow objects. the DataRow, however, needs to provide type- safe access to columns within the database, so you will subclass it. The relationship between this object and the underlying table is shown in Figure 27-9:

image from book
Figure 27-9

This example concentrates on Books and Authors. The example consists of just these two tables, which are shown over the course of the next few pages. Although the example is designed to work with SQL Server, you could alter the code to work with Oracle or any other database engine.

The AuthorRow class derives from DataRow and includes properties for each of the columns within the underlying Author table. A DatabaseTable attribute has been added to the row class, and for each property that links to a column in the table there is now a DatabaseColumn attribute. Some of the parameters to these attributes have been removed so that the image will fit on screen. The full details will appear in the following sections.

DatabaseTable Attribute

The first attribute in this example is used to mark a class, in this instance a DataRow, with the name of the database table where the DataRow will be saved. The example code is available in the Chapter27/ DatabaseAttributes directory:

 // Excerpt from DatabaseAttributes.cs /// <summary> /// Attribute to be used on a class to define which database table is used /// </summary> [AttributeUsage (AttributeTargets.Class , Inherited = false ,  AllowMultiple=false)] public class DatabaseTableAttribute : Attribute { /// <summary> /// Construct the attribute. /// </summary> /// <param name="tableName">The name of the database table</param> public DatabaseTableAttribute (string tableName) { TableName = tableName; } /// <summary> /// Return the name of the database table. /// </summary> public readonly string TableName; } 

The attribute consists of a constructor that accepts the name of the table as a string and is marked with the Inherited=false and AllowMultiple=false modifiers. It's unlikely that you would want any subclasses to inherit this attribute, and it is marked as single use as a class will only link to a single table.

Within the attribute class, you store the name of the table as a field rather than a property. This is a matter of personal choice. In this instance, there is no method to alter the value of the table name, so a read- only field will suffice. If you prefer using properties, then feel free to alter the example code.

DatabaseColumn Attribute

This attribute is designed to be placed on public properties of the DataRow class and is used to define the name of the column that the property will link to, together with such things as whether the column can contain a null value:

 // Excerpt from DatabaseAttributes.cs /// <summary> /// Attribute to be used on all properties exposed as database columns /// </summary> [AttributeUsage (AttributeTargets.Property , Inherited=true ,  AllowMultiple=false) ] public class DatabaseColumnAttribute : Attribute { /// <summary> /// Construct a database column attribute. /// </summary> /// <param name="column">The name of the column</param> /// <param name="dataType">The datatype of the column</param> public DatabaseColumnAttribute (string column , ColumnDomain dataType) { ColumnName = column; DataType = dataType; Order = GenerateOrderNumber (); } /// <summary> /// Return the column name. /// </summary> public readonly string ColumnName; /// <summary> /// Return the column domain. /// </summary> public readonly ColumnDomain DataType; /// <summary> /// Get/Set the nullable flag. A property might be better /// </summary> public bool Nullable = false; /// <summary> /// Get/Set the Order number. Again a property might be better. /// </summary> public int Order; /// <summary> /// Get/Set the Size of the column (useful for text columns). /// </summary> public int Size; /// <summary> /// Generate an ascending order number for columns. /// </summary> /// <returns></returns> public static int GenerateOrderNumber () { return nextOrder++; } /// <summary> /// Private value used while generating the order number /// </summary> private static int nextOrder = 100; } /// <summary> /// Enumerated list of column data types /// </summary> public enum ColumnDomain { /// <summary> /// 32 bit /// </summary> Integer, /// <summary> /// 64 bit /// </summary> Long, /// <summary> /// A string column /// </summary> String, /// <summary> /// A date time column /// </summary> DateTime } 

This class is again marked with AllowMultiple=false, because there is always a one-to-one correspondence between a property of a DataRow and the column to which it is linked.

This attribute is marked as inheritable so that you can create a class hierarchy for database rows, because it is likely that you will have some similar columns throughout each table within the schema.

The constructor accepts two arguments. The first is the name of the column that is to be defined within the database. The second argument is an enumerated value from the ColumnDomain enumeration, which consists of four values for this example, but which would be insufficient for production code.

The attribute also has three other properties, which are summarized here:

  • Nullable: Defaulting to false, this property is used when the column is generated to define whether the database value can be set to null.

  • Order: Defines the order number of the column within the table. When the table is generated, the columns will be output in ascending order. The default is to generate an incrementing value, which is done within the constructor. You can naturally override this value as necessary.

  • Size: Defines the maximum number of characters allowed in a string type.

To define a Name column, you can use the following code:

 [DatabaseColumn("NAME",ColumnDomain.String,Order=10,Size=64)] public string Name { get { return (string) this ["NAME"]; } set { this["NAME"] = value; } } 

This defines a field called NAME, and it will be generated as a VARCHAR(64) because the column domain is set to String and the size parameter has been set to 64. It sets the order number to 10 — you will see why later in the chapter. The column will also not allow null values, because the default for the Nullable property is false (thus, the column will be generated as non-null).

The DataRow class has an indexer that takes the name of a field (or ordinal) as the parameter. This returns an object, which is cast to a string before returning it in the get accessor shown above.

Creating Database Rows

The point of this example is to produce a set of strongly typed DataRow objects. In this example you create two classes, Author and Book, which derive from a common base class because they shares some common fields. (See Figure 27-10.)

image from book
Figure 27-10

The GenericRow class defines the Name and Description properties, and the code for this follows. It is derived from DataRow, the base class for all database rows in the Framework.

For the example, two classes derive from GenericRow — one to represent an Author (AuthorRow) and another representing a Book (BookRow). These both contain additional properties, which are linked to fields within the database:

 // Excerpt from DatabaseTables.cs /// <summary> /// Base class row - defines Name and Description columns /// </summary> public abstract class GenericRow : DataRow { /// <summary> /// Construct the object. /// </summary> /// <param name="builder">Passed in from System.Data</param> public GenericRow (System.Data.DataRowBuilder builder) : base (builder) { } /// <summary> /// A column for the record name /// </summary> [DatabaseColumn("NAME",ColumnDomain.String,Order=10,Size=64)] public string Name { get { return (string) this["NAME"]; } set { this["NAME"] = value; } } /// <summary> /// A column for the description, which may be null /// </summary> [DatabaseColumn("DESCRIPTION",ColumnDomain.String,Nullable=true,Order=11, Size=1000)] public string Description { get { return (string) this["DESCRIPTION"]; } set { this["DESCRIPTION"] = value; } } } 

Deriving from DataRow requires that you create a constructor that accepts a single parameter, a DataRowBuilder. This class is internal to the System.Data assembly.

Two properties are then defined, Name and Description, and each of these is attributed accordingly. The name field is attributed as follows:

[DatabaseColumn("NAME",ColumnDomain.String,Order=10,Size=64)]

This defines the column name as NAME, defines its domain as a string of size 64 characters, and sets its order number to 10. I've chosen this because when creating database tables I always prefer the primary key fields to be emitted before any other fields within the table. Setting this value to 10 provides space for numerous identity fields. Any more than 10 fields in a primary key will require a redesign!

The description column is also given a name, domain, and size. the Nullable property is set to true so that you are not forced to define a description column. The other option would be to define a default property and set this to an empty string, which would avoid the use of null in the database. The order number is set to 11, so that the name and description columns are always kept together in the generated schema:

 [DatabaseColumn("DESCRIPTION",ColumnDomain.String,Nullable=true, Order=11,Size=1000)] 

Each property accessor defines a get and set method for the value of the property, and these are strongly typed so that in the case of a string column, a string value is returned to the caller:

 get { return (string) this["NAME"]; } set { this["NAME"] = value; } 

There is some duplication of code here, because the attribute defines the name of the column, so you could use reflection within these methods to retrieve the value of the appropriate column. However, reflection is not the most efficient of API's — because these classes are used to access the underlying columns, you want them to be as fast as possible. To squeeze every last ounce of performance from these accessors, you could use numeric indexes for the columns, since using strings involves a look up for the numeric index value. Be careful when using numeric indexers because they are slightly more difficult to maintain, especially in the instance where a subclass is defined.

The Author row is constructed as follows:

 // Excerpt from DatabaseTables.cs /// <summary> /// Author table, derived from GenericRow /// </summary> [DatabaseTable("AUTHOR")] public class AuthorRow : GenericRow { public AuthorRow (DataRowBuilder builder) : base (builder) { } /// <summary> /// Primary key field /// </summary> [DatabaseColumn("AUTHOR_ID",ColumnDomain.Long,Order=1)] public long AuthorID { get { return (long) this["AUTHOR_ID"]; } set { this["AUTHOR_ID"] = value; } } /// <summary> /// Date the author was hired /// </summary> [DatabaseColumn("HIRE_DATE",ColumnDomain.DateTime,Nullable=true)] public DateTime HireDate { get { return (DateTime) this["HIRE_DATE"]; } set { this["HIRE_DATE"] = value; } } } 

Here, the GenericRow class has been subclassed, and AuthorID and HireDate properties are added in. Note the order number chosen for the AUTHOR_ID column — it is set to one so that it appears as the first column within the emitted table. the HireDate property has no such order number, so its value is generated by the attribute, and these generated values all start from 100; thus, the table will be laid out as AUTHOR_ID, NAME, DESCRIPTION, and finally HIRE_DATE.

The BookRow class again derives from GenericRow, so as to include the name and description properties. It adds BookID, PublishDate and ISBN properties:

 // Excerpt from DatabaseTables.cs /// <summary> /// Table for holding books /// </summary> [DatabaseTable("BOOK")] public class BookRow : GenericRow { public BookRow (DataRowBuilder builder) : base (builder) { } /// <summary> /// Primary key column /// </summary> [DatabaseColumn("BOOK_ID",ColumnDomain.Long,Order=1)] public long BookID { get { return (long) this["BOOK_ID"]; } set { this["BOOK_ID"] = value; } } /// <summary> /// Author who wrote the book /// </summary> [DatabaseColumn("AUTHOR_ID",ColumnDomain.Long,Order=2)] public long AuthorID { get { return (long) this["AUTHOR_ID"]; } set { this["AUTHOR_ID"] = value; } } /// <summary> /// Date the book was published /// </summary> [DatabaseColumn("PUBLISH_DATE",ColumnDomain.DateTime,Nullable=true)] public DateTime PublishDate { get { return (DateTime) this["PUBLISH_DATE"]; } set { this["PUBLISH_DATE"] = value; } } /// <summary> /// ISBN for the book /// </summary> [DatabaseColumn("ISBN",ColumnDomain.String,Nullable=true,Size=10)] public string ISBN { get { return (string) this["ISBN"]; } set { this["ISBN"] = value; } } } 

Generating the SQL

Now that the database rows have been defined, it's time for the code that will generate a database schema from these classes. The example dumps its output to the console, so you could for example pipe the output to a text file by running the .exe from a command prompt.

The following class calls OutputTable for each type that you wish to create a database table for:

 public class DatabaseTest { public static void Main () { OutputTable (typeof (AuthorRow)); OutputTable (typeof (BookRow)); } public static void OutputTable (System.Type t) { // Code in full below } } 

You could utilize reflection to loop through each class in the assembly, check if it is derived from GenericRow, and output the classes automatically. For simplicity's sake the name of the tables that are to be generated are hard-coded: AuthorRow and BookRow.

The OutputTable method is:

 // Excerpt from Database.cs /// <summary> /// Produce SQL Server-style SQL for the passed type. /// </summary> /// <param name="t"></param> public static void OutputTable (System.Type t) { // Get the DatabaseTable attribute from the type. object[]  tableAttributes = t.GetCustomAttributes  (typeof (DatabaseTableAttribute) , true) ; // Check there is one... if (tableAttributes.Length == 1) { // If so output some SQL Console.WriteLine ("CREATE TABLE {0}" ,  ((DatabaseTableAttribute)tableAttributes[0]).TableName); Console.WriteLine ("("); SortedList columns = new SortedList (); // Roll through each property. foreach (PropertyInfo prop in t.GetProperties ())  { // And get any DatabaseColumnAttribute that is defined. object[]  columnAttributes = prop.GetCustomAttributes (typeof (DatabaseColumnAttribute) , true); // If there is a DatabaseColumnAttribute if (columnAttributes.Length == 1) { DatabaseColumnAttribute dca = columnAttributes[0] as DatabaseColumnAttribute; // Convert the ColumnDomain into a SQL Server data type. string  dataType = ConvertDataType (dca); // And add this column SQL into the sorted list - I want the // columns to come out in ascending order of order number. columns.Add (dca.Order, string.Format ("  {0,-31}{1,-20}{2,8}," ,  dca.ColumnName ,  dataType ,  dca.Nullable ? "NULL" : "NOT NULL")); } } // Now loop through the SortedList of columns. foreach (DictionaryEntry e in columns) // And output the string... Console.WriteLine (e.Value); // Then terminate the SQL. Console.WriteLine (")"); Console.WriteLine ("GO"); Console.WriteLine (); } } 

This code reflects over the type passed in and looks for the DatabaseTable attribute. If the DatabaseTable attribute is found, it writes a CREATE TABLE clause to the console, including the name of the table from the attribute.

You then loop through all properties of the type to find any DatabaseColumn attributes. Any property that has this attribute will become a column in the generated table:

foreach (PropertyInfo prop in t.GetProperties ())  {    object[] columnAttributes = prop.GetCustomAttributes (                        typeof (DatabaseColumnAttribute) , true);

The string representation of the column is constructed by calling the ConvertDataType() method, shown in a moment. This is stored within a sorted collection so that the columns are generated based on the value of the Order property of the attribute.

After looping through all attributes and creating entries within the sorted list, you then loop through the sorted list and write each value to the console:

foreach (DictionaryEntry e in columns)    Console.WriteLine(e.Value);

Finally, you add the closing bracket and a GO command, which will instruct SQL Server to execute the batch of statements and thereby create the table.

The last function in this assembly, ConvertDataType(), converts values from the ColumnDomain enumeration into a database specific data type. In addition, for string columns, you create the column representation to include the size of the column, so for instance the Name property from the generic base class is constructed as VARCHAR(64). This column type represents a varying array of characters up to 64 characters in length.

 // Excerpt from Database.cs /// <summary> /// Convert a ColumnDomain to a SQL Server data type. /// </summary> /// <param name="dca">The column attribute</param> /// <returns>A string representing the data type</returns> private static string ConvertDataType (DatabaseColumnAttribute dca) { string dataType = null; switch (dca.DataType) { case ColumnDomain.DateTime: { dataType = "DATETIME"; break; } case ColumnDomain.Integer: { dataType = "INT"; break; } case ColumnDomain.Long: { dataType = "BIGINT"; break; } case ColumnDomain.String: { // Include the size of the string... dataType = string.Format ("VARCHAR({0})" , dca.Size); break; } } return dataType; } 

For each member of the enumeration, you create a column string appropriate for SQL Server. The SQL emitted for the Author and Book classes from this example is:

CREATE TABLE AUTHOR (    AUTHOR_ID                    BIGINT                       NOT NULL,    NAME                         VARCHAR(64)                  NOT NULL,    DESCRIPTION                  VARCHAR(1000)                NULL,    HIRE_DATE                    DATETIME                     NULL, )     GO     CREATE TABLE BOOK (    BOOK_ID                      BIGINT                        NOT NULL,    AUTHOR_ID                    BIGINT                        NOT NULL,    NAME                         VARCHAR(64)                   NOT NULL,    DESCRIPTION                  VARCHAR(1000)                 NULL,    PUBLISH_DATE                 DATETIME                      NULL,    ISBN                         VARCHAR(10)                   NULL, ) GO

This SQL can be run against an empty or preexisting SQL Server database to create the tables. the DataRow classes created can be used to provide type safe access to the data within these tables.

To utilize the derived DataRow classes, you need to provide some code such as the following. This class overrides the minimum set of functions from DataTable and is passed the type of the row in the constructor:

 // Excerpt from DatabaseTables.cs /// <summary> /// Boilerplate data table class /// </summary> public class MyDataTable : DataTable { /// <summary> /// Construct this object based on a DataRow. /// </summary> /// <param name="rowType">A class derived from DataRow</param> public MyDataTable (System.Type rowType) { m_rowType = rowType; ConstructColumns (); } /// <summary> /// Construct the DataColumns for this table. /// </summary> private void ConstructColumns () { SortedList columns = new SortedList (); // Loop through all properties. foreach (PropertyInfo prop in m_rowType.GetProperties ())  { object[]  columnAttributes = prop.GetCustomAttributes  (typeof (DatabaseColumnAttribute) , true); // If it has a DatabaseColumnAttribute if (columnAttributes.Length == 1) { DatabaseColumnAttribute dca = columnAttributes[0] as DatabaseColumnAttribute; // Create a DataColumn. DataColumn  dc = new DataColumn (dca.ColumnName ,  prop.PropertyType); // Set its nullable flag. dc.AllowDBNull = dca.Nullable; // And add it to a temporary column collection columns.Add (dca.Order , dc); } } // Add the columns in ascending order. foreach (DictionaryEntry e in columns) this.Columns.Add (e.Value as DataColumn); } /// <summary> /// Called from within System.Data /// </summary> /// <returns>The type of the rows that this table holds</returns> protected override System.Type GetRowType () { return m_rowType; } /// <summary> /// Construct a new DataRow /// </summary> /// <param name="builder">Passed in from System.Data</param> /// <returns>A type safe DataRow</returns> protected override DataRow NewRowFromBuilder (DataRowBuilder builder) { // Construct a new instance of my row type class. return (DataRow) Activator.CreateInstance (GetRowType() ,  new object[1] { builder }); } /// <summary> /// Store the row type. /// </summary> private System.Type m_rowType; } 

The ConstructColumns() function, called from the constructor, will generate a DataColumn array for the DataTable — these are again retrieved using reflection. The other methods, GetRowType() and NewRowFromBuilder(), override methods in the base DataTable class.

Once you have this derived MyDataTable class, you can easily use it in your own code. The following is an example of adding a couple of author records into the Author table, then outputting these rows to an XML file:

 DataSet       ds = new DataSet (); MyDataTable t = new MyDataTable (typeof (AuthorRow)); ds.Tables.Add (t); AuthorRow    author = (AuthorRow)t.NewRow (); author.AuthorID = 1; author.Name = "Me"; author.HireDate = new System.DateTime (2000,12,9,3,30,0); t.Rows.Add (author); author = (AuthorRow) t.NewRow (); author.AuthorID = 2; author.Name = "Paul"; author.HireDate = new System.DateTime (2001,06,06,23,56,33); t.Rows.Add (author); t.DataSet.WriteXml (@"c:\BegVCSharp\Chapter22\authors.xml"); 

When run, this code produces the following output:

<?xml version="1.0" standalone="yes"?> <NewDataSet>   <Table1>     <AUTHOR_ID>1</AUTHOR_ID>     <NAME>Me</NAME>     <HIRE_DATE>2000-12-09T03:30:00.0000000-00:00</HIRE_DATE>   </Table1>   <Table1>     <AUTHOR_ID>2</AUTHOR_ID>     <NAME>Paul</NAME>     <HIRE_DATE>2001-06-06T23:56:33.0000000+01:00</HIRE_DATE>   </Table1> </NewDataSet>

This example is a practical example of using custom attributes in your code. If you don't mind coupling the database structure into the classes that access the database then this is a good starting point. Tying database tables to classes is acceptable if your schema doesn't change very often, but for more dynamic back ends it may be better to work in a way that keeps data access classes in step with the database tables accessed.

In a full implementation, you might also include attributes to define some or all of the following:

  • Primary key columns

  • Constraints — foreign key and check

  • Versions — a version number on each column attribute and table attribute simplifies the generation of upgrade scripts — you can in fact generate the whole upgrade based on attributes

  • Default values for columns




Beginning Visual C# 2005
Beginning Visual C#supAND#174;/sup 2005
ISBN: B000N7ETVG
EAN: N/A
Year: 2005
Pages: 278

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