Fun with Attribute Based Programming – Extending Enumerations

 

by DonXML

 

The majority of the code can be found inline with this article, or you can download it from here.

 

Attribute based programming is a model by which you add metadata to a class, which allows it to change its behavior based on the metadata.  One way I use it is to extend enumerations, so that they can contain more info than just a name and a value.  A great example is how we create stored procedure parameters in our data access layer.  We created a standard list of parameters that can be passed into and out of stored procs, using standard names, and data types.  This way when you see a parameter name Foo, you can guarantee that it is exactly the same as someone else’s definition of Foo.  The old school approach to this is to create a utility class with a bunch of static methods that you would call to create each stored procedure parameters.  Something like this:

 

static SqlClient.SqlParameter CreateParmApplicationUserIdOutput()

{

SqlClient.SqlParameter NewParameter = new System.Data.SqlClient.SqlParameter();

NewParameter.ParameterName = "pn_Appl_User_ID";

NewParameter.DbType = System.Data.DbType.Int32;

NewParameter.Direction = System.Data.ParameterDirection.Output;

return NewParameter;

}

 

 

But this leads to a lot of duplication of code, and is generally pretty cumbersome.    We thought about creating a generic parameter class based on the standard System.Data parameter interfaces, and passing an enum into the constructor.  This enum would have an entry for each one of our stored procedure parameters.  The problem was, there is much more info needed to fully create a parameter than just name and value.  Our solution, extend the enum with Attributes. 

 

To create custom attributes you will need to create an new class that inherits from the abstract System.Attribute class.  There are 4 things you will need to do to build this class.

  1. Declare the class and inherit from System.Attribute
  2. Apply the AttributeUsageAttribute, which defines the different characteristics of your attributes.  There are 3 basic properties to set
    1. AttributesTargets, which defines which part of a class that can be decorated by this attribute.  The options are All (for all parts), Class (for just the class definition) or Method (for just the methods).
    2. Inherited, which defines if a class that inherits from your attribute decorated class can inherit the attributes.
    3. AllowMultiple which determines if multiple instance of this attribute can exist on your decorated element.
  3. Declare the constructors.  This is the same as any other class.
  4. Declare the properties.  You should have properties for each value that you want to have in your attribute.

 

In our case, we called the class DalParameterSettingsAttribute.  We needed to have 5 different values defined for this attribute:

  1. DataType – the System.Data.DbType for the parameter
  2. Direction – The direction of the parameter defined by System.Data.ParameterDirection
  3. IsNullable – Determines if this parameter can be null.
  4. Name – The name of the parameter
  5. StringOutputLength – If this is an output string, what the max length is of the output length.

 

So the class starts like this:

[AttributeUsage(AttributeTargets.Field,  Inherited = false, AllowMultiple = false)]

public class DalParameterSettingsAttribute : Attribute

{

private System.Data.DbType _DataType;

       private System.Data.ParameterDirection _Direction = System.Data.ParameterDirection.Input;

       private bool _IsNullable = false;

       private string _Name;

       private int _NullValue = -1;

       private int _StringOutputLength = 0;

 

Then the constructors.  We need 5 different constructors, each representing a different type of parameter.

 

Construct a non-nullable input parameter of the specified type.

public DalParameterSettingsAttribute(string name, System.Data.DbType dataType)

{

       _Name = name;

       _DataType = dataType;

}

 

Construct an input parameter that allows for the definition of null properties (default null value = -1).

public DalParameterSettingsAttribute(string name, System.Data.DbType dataType, bool isNullable)

{

       _Name = name;

       _DataType = dataType;

       _IsNullable = isNullable;

}

 

Construct a parameter using the specified values (default null value = -1)

public DalParameterSettingsAttribute(string name, System.Data.DbType dataType, bool isNullable, System.Data.ParameterDirection direction)

{

       _Name = name;

       _DataType = dataType;

       _IsNullable = isNullable;

       _Direction = direction;

}

 

Constrcut a parameter that allows for the definition of null properties.

public DalParameterSettingsAttribute(string name, System.Data.DbType dataType, bool isNullable, System.Data.ParameterDirection direction, int nullValue)

{

       _Name = name;

       _DataType = dataType;

       _IsNullable = isNullable;

       _Direction = direction;

       _NullValue = nullValue;

}

 

Constructor for output string parameter types.

public DalParameterSettingsAttribute(string name, System.Data.DbType dataType, bool isNullable, System.Data.ParameterDirection direction, int nullValue, int stringOutputLength)

{

       _Name = name;

       _DataType = dataType;

       _IsNullable = isNullable;

       _Direction = direction;

       _NullValue = nullValue;

       _StringOutputLength = stringOutputLength;

}

 

The next thing we did was create an enum class called DalParameterType, and added entries for each stored procedure parameter. 

 

public enum DalParameterType

{

 

We then decorated each enum entry with the DalParameterSettings Attribute.

 

[DalParameterSettings("pv_Active_Indicator", System.Data.DbType.Boolean)]

ActiveIndicator,

 

[DalParameterSettings("pn_Appl_User_ID", System.Data.DbType.Int32, true)]

ApplicationUserId,

 

[DalParameterSettings("pn_Appl_User_ID", System.Data.DbType.Int32, true, System.Data.ParameterDirection.Output)]

ApplicationUserIdOutput,

 

[DalParameterSettings("pv_Active_Only", System.Data.DbType.Boolean)]

ActiveOnly,

 

[DalParameterSettings("pn_Animal_Ct", System.Data.DbType.Int32)]

AnimalCount,

 

[DalParameterSettings("pd_Start_Time", System.Data.DbType.DateTime)]

StartTime,

 

[DalParameterSettings("pv_Study_Status_Indc", System.Data.DbType.Boolean)]

StudyStatusIndicator

}

 

Now anytime anyone creates a parameter based on an enum entry, we know it will always have the same properties.  If at some future time we need to go from Int to Decimal, all we need to do is to change it here.

 

Alright, we have an enum, with all the associated meta data for our stored procedure parameters.  How do we go about creating a stored procedure parameter?  By creating an generic DalParameter class, just like we wanted to in the beginning, called DalParameter. 

 

public class DalParameter : System.Data.IDbDataParameter, System.Data.IDataParameter, System.ICloneable

{

       DbType _DbType;

       ParameterDirection _Direction;

       Boolean _IsNullable;

       string _ParameterName;

       string _SourceColumn;

       DataRowVersion _SourceVersion;

       object _Value;

       byte _Precision;

       byte _Scale;

       int _Size;

 

It has all the properties and method of a standard Data Parameter, but we have 2 constructors, one where you can just pass in the DalParameterType, and another where you can pass in the DalParameterType and the parameter value (an object). 

 

public DalParameter(DalParameterType type)

{

       this.InitializeParameterToDefaults();

       this.CreateParameter(type);

}

 

public DalParameter(DalParameterType type, object value)

{

       this.InitializeParameterToDefaults();

       this.CreateParameter(type, value);

}

 

public DbType DbType

{

       get{return _DbType;}

       set{_DbType = value;}

}

 

public ParameterDirection Direction

{

       get{return _Direction;}

       set{_Direction = value;}

}

 

public Boolean IsNullable

{

       get{return _IsNullable;}

       set{_IsNullable = value;}

}

 

public string ParameterName

{

       get{return _ParameterName;}

       set{_ParameterName = value;}

}

 

public string SourceColumn

{

       get{return _SourceColumn;}

       set{_SourceColumn = value;}

}

 

public DataRowVersion SourceVersion

{

       get{return _SourceVersion;}

       set{_SourceVersion = value;}

}

 

public object Value

{

       get{return _Value;}

       set{_Value = value;}

}

 

public byte Precision

{

       get{return _Precision;}

       set{_Precision = value;}

}

 

public byte Scale

{

       get{return _Scale;}

       set{_Scale = value;}

}

      

public int Size

{

       get{return _Size;}

       set{_Size = value;}

}

 

public object Clone()

{

       return this.MemberwiseClone();

}

 

 

When this class is constructed the first thing that happens is the defaults are set for each properity, and then it will go thru the attributes that decorated the enum and build the DalParamter according to the them, and optionally the parameter value.  The beauty here is that you can put all sorts of edit checking or custom processing into the DalParameter creation.

 

private void CreateParameter(DalParameterType type, object value)

{

       FieldInfo field = type.GetType().GetField(type.ToString());

       object[] attribs = field.GetCustomAttributes(typeof(DonXml.Dal.DalParameterSettingsAttribute), false);

       if(attribs.Length > 0)

       {

              //Get parameter values

              DalParameterSettingsAttribute paramVals = ((DonXml.Dal.DalParameterSettingsAttribute)attribs[0]);

 

              //Set name and direction

              this.ParameterName = paramVals.Name;

              this.Direction = paramVals.Direction;

 

              //Set data type and value if necessary

              this.DbType = paramVals.DataType;

              switch(paramVals.DataType)

              {

                     case System.Data.DbType.AnsiString:

                     if(paramVals.Direction.Equals(System.Data.ParameterDirection.Input))

                           {

                                  if(value!=null) this.Value = (string)value;

                           }

                           else

                           {

                                  this.Value = new String(' ', paramVals.StringOutputLength);

                           }

                           break;

                     case System.Data.DbType.Boolean:

                           if(value!=null) this.Value = (bool)value;

                           break;

                     case System.Data.DbType.DateTime:

                           if(value!=null) this.Value = (DateTime)value;

                           break;

                     case System.Data.DbType.Int32:

                           if(value!=null)

                           {

                                          if((paramVals.IsNullable)&&((int)value==paramVals.NullValue))

                                         this.Value = DBNull.Value;

                                  else

                                         this.Value = (int)value;

                           }

                           break;

                     case System.Data.DbType.Double:

                           if(value!=null)

                           {

                                  if((paramVals.IsNullable)&&((double)value==paramVals.NullValue))

                                  this.Value = DBNull.Value;

                                  this.Value = (double)value;

                           }

                           break;

              }

       }

       else

       {

              throw new ApplicationException("Unable to create the specified parameter.");

       }

}

 

The last problem was figuring out how to get this generic DalParameter to be added to the type safe ParameterCollection classes of each .Net Data Provider.  What we did is to add an implicit cast to the match parameter type of each Data Provider.  In this example, we are converting to SqlParameter, but you can do the same for any of the other Data Providers.  You could also add any custom code here to encapsulate any issues converting from your generic parameter type to a specific data provider parameter type.

 

static public implicit operator SqlClient.SqlParameter(DalParameter dalParameter)

{

       SqlClient.SqlParameter sqlParameter = new System.Data.SqlClient.SqlParameter();

       sqlParameter.DbType = dalParameter.DbType;

       sqlParameter.Value = dalParameter.Value;

       sqlParameter.Direction  = dalParameter.Direction;

       //_IsNullable = false;

       sqlParameter.ParameterName = dalParameter.ParameterName;

       sqlParameter.Precision = dalParameter.Precision;

       sqlParameter.Scale = dalParameter.Scale;

       sqlParameter.Size = dalParameter.Size;

       sqlParameter.SourceColumn = dalParameter.SourceColumn;

       sqlParameter.SourceVersion = dalParameter.SourceVersion;

       //sqlParameter.SqlDbType =

       return sqlParameter;

}

 

So with all that work, what does it buy us?  One line of code to add a parameter to command object:

 

Create the Command:

SqlClient.SqlCommand MyCommand = new SqlClient.SqlCommand();

 

Then add the parameter:

MyCommand.Parameters.Add(new DalParameter(DalParameterType.ApplicationUserIdOutput));

 

As easy as that, and you know that the ApplicationUseIdOutput parameter will always be the same, no matter who codes it.  Plus creating new stored procedure parameters is as easy as adding another entry to the DalParameterType enum.  No duplication of code, no magic numbers/strings, and encapsulation of any code specific to a data provider.

 

If you have any questions, you can email me at don at donxml.com.  I also encourage you to read my blog.