Dot Net

Syrinx .NET Development Blog
Need help on your project? info@syrinx.com, or toll free (888) 579-7469, press 1
Converting Strongly Typed Collections to Generics: Part 2

In Part 1, the idea of converting your strongly typed collections to Generics was introduced.  Backward compatibility and existing client support was maintained.  However once a level of acceptance has been maintained for the new base class and the reduced amount of code in the collection classes, new development can utilize new constructs in c#3.0 which can reduce the code in your strongly typed collections even more, as well as eliminating the strongly typed collections all together.  See the code snippet below which shows the base generic class.

public class BaseGenericCollection<T> : System.Collections.ObjectModel.Collection<T>
{
    internal List<T> m_deletedList = null;

    new public void Add(T t)
    {
        if (this.Contains(t))
            throw new System.ArgumentException("Cannot add duplicate entry to collection.");
        base.Add(t);
    }
    public void Delete(T t)
    {
      if (t == null)
      {
   throw new ArgumentNullException("T", "BaseGenericCollection: Cannot Delete NULL from collection.");
      }
      if (m_deletedList == null)
      {
          m_deletedList = new List<T>();
      }
      this.m_deletedList.Add(t);
      base.Remove(t);
    }
}

The base class now no longer needs the GenericSorter<T> internal class it had in Part 1.  The strongly typed StudentCollection is also no longer needed, but can be kept around for backward compatibility.  However the same functionality can still be achieved, along with type safety, using anonymous methods, or even cleaner syntax with lambda expressions.   Lambda expressions simplify the syntax of defining at run time, logic that can be invoked in response to an event without needing a dedicated method for the event handler.

Consider the following code which illustrates using lambda expressions to sort a collection that has a new base class.

static void Main(string[] args)
    {
      //Load up a Generic collection of Student objects
      BaseGenericCollection<Student> _students = LoadStudents();

      //Notice that the Students can be ordered within the loop using the OrderBy method from the extension
      //methods from System.Linq within CollectionBase<T>
      //and the syntax can be reduced using the Lamda operator instead of an anonymous  method
        foreach (var student in _students.OrderBy(student => student.Major))
        {
          Console.WriteLine("{0,-10} \t{1,-10} \t{2,-10} \t{3,-10:d} \t{4,-10:D} ",
   student.FirstName, student.LastName, student.Major,
   student.EnrollmentDate, student.GraduationDate);
        }


The local student collection instance, _students, is now a BaseGenericCollection<Student> type and still has all the capabilities it had previously but with less code.  The base class’ base class System.Collections.ObjectModel.Collection<T> has an extension method in System.Linq for sorting called OrderBy that requires to be passed a delegate, which can be replaced with a lambda expression.

 

New Constructs

C# 3.0 supports the capability of handling events inline by assigning or delegating a chunk of code directly where an event handler will be referenced, instead of creating a dedicated method to respond to an event.  This approach takes advantage of anonymous methods.  The syntax can be a little “too busy” or confusing.  The syntax can be cleaned up using lambda expressions, which are cleaner, more simply ways of creating anonymous methods.

The MSDN Library defines a lambda expression as “… an anonymous function that can contain expressions and statements, and can be used to create delegates or expression tree types.  The syntax can be broken down as: arguments to be processed => (goes to) statements to be processed.   In the example above, the OrderBy method is passed a student instance (to be evaluated once it is referenced via the foreach loop) which is the object to be processed, then the “goes to” operator (=>)and student.Major, ( the value of the student’s major), which is the statement that is to be processed.  The OrderBy method actually considers it like an identity function according to the MSDN Library.

To illustrate lambda expression or more so, the statements to be processed consider the following example.

//First define the major we are looking for
Program._majorToFind = "Physics";

//Now we can loop thru the collection that is returned from casting to a List<Student> collection and using the FindAll method, using the Lambda operator
foreach (var student in _students.ToList<Student>().FindAll
    (student => student.Major == Program._majorToFind))
    {
        Console.WriteLine("{0,-10} \t{1,-10} \t{2,-10} \t{3,-10:d} \t{4,-10:D} ", student.FirstName,
        student.LastName, student.Major, student.EnrollmentDate, student.GraduationDate);
    }

//Now we utilize the Find method, to find a specific instance with specific data, using the Lambda operator
Program._lastNameToFind = "Valenti";
Student students = _students.ToList<Student>().Find(student => student.LastName == Program._lastNameToFind);

 

 

Notice the foreach, the type that is being retrieved is defined as a var which is an anonymous type that is ultimately returned from the Find and FindAll methods of List<T>.  Those methods expect to receive a System.Predicate<T>, which is ultimately a delegate or method pointer.  As illustrated above, a method pointer can be replaced by an anonymous method which also can be replaced by a lambda expression.  The statements to be processed in the lambda expression are looking for student instances with a student  Major property value of “Physics” and only return instances with those values.

The same is true for the Find method of List<T>.  It requires a method pointer which can be replaced with a lambda expression that returns the student object with the LastName property equal to Program._lastNameToFind.

Your strongly typed collection code can be further reduced than that illustrated in Part 1, by utilizing new constructs added to the c# language.  These new objects and constructs actually make it more readable and understandable once you understand how they are read.  All the compiler time type checking is maintained and the compiler infers the correct type for the anonymous types used in the lambda expressions.

In Part 3, the lambda expression will be replaced with Language Integrated Queries (Linq).

 

Converting Strongly Typed Collections to Generics: Part 1

Many enterprise applications make use of strongly typed collections in their solution.   Although they require more development time, the advantages they provide can outweigh the cost of the extra development time.  Some of the advantages include compile time and run time type safety, processor time savings from not having to downcast, the time to search and perform type checking, and sorting on specific properties.  .Net 1.1 contains base classes that provide all the typical methods and properties needed to build your own strongly typed collection class.  But as previously mentioned extra development time is needed to develop a collection class for each business class that can exist in multiple, simultaneous versions at run time.  This leads to higher development costs and higher maintenance costs.  Business requirements grow over time, and new properties can be added to business classes that require sorting at the application tier, thus needing development time to update the collection class to allow sorting on the new properties.  Also some properties can have their exposure increased and thus needing a new “FindBy…” method in the collection.
Much of this development time can be reduced using Generics, a new construct added to c# 2.0.  However when environments and projects must adhere to consistent standards, before new technologies make their way into new development cycles, sometimes legacy solutions will be converted to the new technology prior to new functionality using it.  Also if it is required to maintain either backward compatibility, or existing client code, removal of the existing strongly typed collections could be prohibited.  However reducing its code base and maintaining its existing signature is possible with Generics.

The topic of converting all your strongly typed collection classes will be discussed and broken up into 3 separate parts.  When large enterprise applications contain many strongly typed collection classes, converting them to Generics can contain large amounts of risk and could be separated into different steps that reduce risk, and keep a higher level of quality.

Here in Converting Strongly Typed Collections to Generics Part 1, the first step will be only to reduce the amount of code in each of the strongly typed collection classes by using Generics.  The basic structure of a non generic strongly typed collection class will be discussed with an example of all the usual parts.  Then the strongly typed collection class will have the majority of its code removed as well as changing its base class to use Generics, all the while maintaining backward compatibility.

 

Strongly Typed Collections

A strongly typed collection class has the typical methods and properties of any collection class but only accepts and processes 1 type.  For example, see the code snippet below that illustrates some typical methods.  Note this is only a snippet of the class; download the project to see the entire collection class as well as the complete example. The snippet below illustrates the typical properties and methods that a collection class and namely a strongly typed collection class will contain.

public class StudentCollection : BaseCollection
{
    public Student this[int index]
    {   //List handles throwing ArgumentOutOfRangeException
        get {return (Student)List[index];}
        set { List[index] = value; }
    }

    public int Add(Student student)
    {
        if (student == null)
        {
            throw new System.ArgumentNullException("student", "Cannot Add NULL to collection.");
        }
        // Is List a Unique list?
        if (IsUnique && List.Contains(student))
        {
            throw new System.ArgumentException("Cannot add duplicate entry to collection.");
        }
        return List.Add(student);
    }

    public StudentCollection FindByMajor(string major)
    {
        StudentCollection studentCollection = new StudentCollection();
        foreach (Student student in this.List)
        {
            if (student.Major == major)
            {
                studentCollection.Add(student);
            }
        }
        return studentCollection;
    }


Note the indexer only indexes on a Student object, and the Add method only adds a Student type.    Also the FindByMajor method is specific to a StudentCollection and searches only for Student objects.
When using the collection, it is pretty straight forward, you can
new a Student object, add it to an instance of a StudentCollection, and find a specific Student instances.   See the code example below.

 

Student student1      = new Student(1, "Ben", "Franklin", "Physics");
StudentCollection students     = new StudentCollection();
students.Add(student1);
StudentCollection studentsToFind = students.FindByMajor("Physics");

 

Normally the collection will be filled at a database tier or other method, and the collection will contain more than 1 Student object.  For simplicity only a snippet is shown here.

Generics
Now we will look at creating a Generic base class and removing the common code used by all collections. 
 

public class BaseGenericCollection<T> : System.Collections.ObjectModel.Collection<T>
{
   internal List<T> m_deletedList = null;

   new public void Add(T t)
   {
        if (this.Contains(t))
           throw new System.ArgumentException("Cannot add duplicate entry to collection.");
        base.Add(t);
   }

 

 

The base class will inherit from System.Collections.ObjectModel.Collection<T> and will contain all the typical properties and methods any collection class has, but it will be instantiated with a type and thus will maintain its type safety.   Notice there is no “FindBy…” method, because we won’t know what type this class will be instantiated with.

Now let’s look at the Student Collection.  Remember, we are only removing the typical parts of strongly typed collection into a reusable base class that maintains type safety.   The StudentCollection class will still exist, but will only contain the custom methods and/or properties specific to a StudentCollection class.

 

public class StudentCollection : BaseGenericCollection<Student>
{
    public StudentCollection FindByMajor(string major)
    {
        StudentCollection studentCollection = new StudentCollection();
        //Create a temporary anonymous collection
        //and use a lambda expression to only select those students with the desired major.
        //The compiler will infer the type and maintain type safety
        var students =
            this.Select((student, index) =>
                    new
                    {
                        index, studentToFind = student.Major == major
                    }
            );
           
        //Loop thru the anonymous type
        foreach (var student in students)
        {
            if (student.studentToFind)
            {
                studentCollection.Add(this[student.index]);
            }
        }
        return studentCollection;
    }
}

 

The StudentCollection class inherits from our BaseGenericCollection which is really a Collection<T> of Student objects which provides the type safety that was required in the legacy application.
We can also take advantage of other new constructs in c#3.0, namely anonymous types and lambda expressions.  An anonymous type can be used to select a collection of student objects that meet the criteria using lambda expressions.  The lambda expression reduces the amount of code compared to an anonymous method.

Upgrading your strongly typed collections to Generics can reduce your code base, and time to maintain the classes.  When there are several strongly typed collections and they are used throughout the application, taking small steps in upgrading them will reduce the risk of introducing defects, and keep the quality at a level that is arguably acceptable by all the teams. 

Next in Part 2 we will discuss removing all the strongly typed collections and having only 1 collection used throughout your application.  The type safety will still be maintained and the “FindBy…” methods will be moved to the tier that instantiates the Generic base class.

Part 6 - Generating Some Classes (and an Interface)

In this installation of my blog series I'll cover creating integrated InterfaceObject, DataAccess and ApplicationObject (BusinessObject) classes based on the definition file that use these newly created stored procedures and provide an API to the users of our classes.

The Interface, Data Access and Application objects will be created using the field values from the CodeGeneratorPropertyList.  The interface will define the basic property signatures for each field.  The Data Access class will perform CRUD and list retrieval based on the stored procedures just created.  The Application Object will interact with the Data Access class using primitive values where possible and the generated interface when not.  So, when a new item is created the DataAccess would be expecting an instance of a class that implements the interface to be passed to it.  The Application Object will pass itself as it is just such a class.

The Application Object will also handle keeping track of whether or not it is dirty (a value has changed since instantiation) and whether or not it is new (the value does not yet exist in the database), s well as check some business rules.  It does this by inheriting from a base class that is standard within the code generator.  This will reduce unnecessary calls to persist unchanged items to the database or to try and delete items that do not yet exist.  The CheckRules method will initially just check that required fields are present and that fields with a given length (varchar, char, etc.) are not longer than allowed.

The Application Object and the Data Access classes will both use partial classes and partial methods to take advantage of the Code Generator's ability to generate new code while at the same time not losing any modified code.  The CheckRules method is a partial method.  The first time the file is generated the generator creates a method signature in the upper code generated class and then implements the actual method in the custom code section.  The generator does not do this if the file already exists so as not to lose any custom code.  Unfortunately, this means that any new rules for required fields or field length need to be implemented manually.

This will be changed in the future to make the CheckRules method a standard code method and add a call to a new partial method named CheckCustomRules.  This is a great example of one of the enormous advantages in creating and using a custom, code generator -the ability to incrementally improve it as time goes on.  This allows the developer to deliver more robust, re-usable code in less time.

The code to generate the interface is very straightforward.  It writes the using statements, the namespace and interface name using the CreateNameSpaceHeader method:

        private static string CreateNameSpaceHeader(string Namespace, string ClassName, IEnumerable<CodeGeneratorProperty> PropertyList)

        {

            StringBuilder retVal = new StringBuilder();

           

            retVal.Append(string.Format("using System;{0}", CodeGenerationHelper.GetEndOfLine()));

            foreach (CodeGeneratorProperty property in PropertyList)

            {

                if (property.DataType.EndsWith("List"))

                {

                    retVal.Append(string.Format("using System.Collections.Generic;{0}", CodeGenerationHelper.GetEndOfLine()));

                    break;

                }

            }

 

            retVal.Append(CodeGenerationHelper.GetEndOfLine());

            retVal.Append(string.Format("namespace {0}{1}", Namespace, CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(string.Format("{{{0}", CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}///<summary>{1}", CodeGenerationHelper.GetTabIndent(1),

                              CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}///The public interface for the {1} object{2}", CodeGenerationHelper.GetTabIndent(1),

                              ClassName, CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}///</summary>{1}", CodeGenerationHelper.GetTabIndent(1),

                              CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(string.Format("\tpublic interface I{0} " + CodeGenerationHelper.GetEndOfLine(), ClassName));

            retVal.Append(string.Format("\t{{{0}", CodeGenerationHelper.GetEndOfLine()));

            return retVal.ToString();

        }

 

 

 and then iterates through the CodeGeneratorPropertyList to create the property signatures using the CreateAccessorSignatures method:

        private static string CreateAccessorSignatures(IEnumerable<CodeGeneratorProperty> PropertyList)

        {

            StringBuilder retVal = new StringBuilder();

           

            retVal.Append(

                string.Format("{0}#region Property Signatures                 {1}", CodeGenerationHelper.GetTabIndent(2),

                              CodeGenerationHelper.GetEndOfLine()));

            foreach (CodeGeneratorProperty property in PropertyList)

            {

                retVal.Append(

                    string.Format("{0}/// <summary>{1}",

                                  CodeGenerationHelper.GetTabIndent(2), CodeGenerationHelper.GetEndOfLine()));

                retVal.Append(

                    string.Format("{0}/// {1}{2}",

                                  CodeGenerationHelper.GetTabIndent(2), property.Description, CodeGenerationHelper.GetEndOfLine()));

                retVal.Append(

                    string.Format("{0}/// </summary>{1}",

                                  CodeGenerationHelper.GetTabIndent(2), CodeGenerationHelper.GetEndOfLine()));

                if (property.DataType.EndsWith("List"))

                {

                    retVal.Append(

                        string.Format("{0}List<{1}> {2}{3}",

                                      CodeGenerationHelper.GetTabIndent(2),

                                      property.DataType.Substring(0, property.DataType.LastIndexOf("List")),

                                      property.PropertyName,

                                      CodeGenerationHelper.GetEndOfLine()));

                }

                else

                {

                    retVal.Append(

                        string.Format("{0} {1} {2}{3}",

                                      CodeGenerationHelper.GetTabIndent(2), property.DataType, property.PropertyName,

                                      CodeGenerationHelper.GetEndOfLine()));

                }

                retVal.Append( CodeGenerationHelper.GetStandAloneOpeningBracket(2));

                if (property.IncludeGet)

                {

                    retVal.Append( string.Format("{0} get;{1}",

                                  CodeGenerationHelper.GetTabIndent(3), CodeGenerationHelper.GetEndOfLine()));

                }

                if (property.IncludeSet)

                {

                    retVal.Append( string.Format("{0} set;{1}",

                                  CodeGenerationHelper.GetTabIndent(3), CodeGenerationHelper.GetEndOfLine()));

                }

                retVal.Append( CodeGenerationHelper.GetStandAloneClosingBracket(2));

                retVal.Append( CodeGenerationHelper.GetEndOfLine());

            }

            retVal.Append(

                string.Format("{0}#endregion Property Signatures{1}", CodeGenerationHelper.GetTabIndent(2),

                              CodeGenerationHelper.GetEndOfLine()));

            return retVal.ToString();

        }

This method also handles the use of List classes that are typically foreign key based collections (e.g UserAddresses is a UserAdressList that has the UserId as the Foreign Key).

The Data Access generation is also very straightforward but with a few more moving parts.  It also creates the header in the same way but then generates the constants that reference the stored procedures created for the CRUD operations, it then uses the CodeGeneratorPropertyList to create Data Access methods (Find, Fetch, Add, Change and Remove) - these methods will be called by the Application Object methods as follows:

Application Object Method

Data Access Method

Exists

Find

Select

Fetch

Save - depending on the IsNew property - Add if IsNew; Change if not IsNew

Add

Change

Delete

Remove

 

A good example of the code generator method for the Data Access class would be CreateAddMethod:

        private static string CreateAddMethod(string ClassName, IEnumerable<CodeGeneratorProperty> PropertyList)

        {

            StringBuilder retVal = new StringBuilder();

            retVal.Append(

                string.Format("{0}#region Add Data               {1}", CodeGenerationHelper.GetTabIndent(2),

                              CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}// called to add new data into the database{1}", CodeGenerationHelper.GetTabIndent(2),

                              CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}internal static bool Add(ref int insertedId, I{1} {1}Object){2}",

                              CodeGenerationHelper.GetTabIndent(2), ClassName, CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}{{{1}", CodeGenerationHelper.GetTabIndent(2), CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}using (SqlConnection cn = ConnectionManager.OpenConnection()){1}",

                              CodeGenerationHelper.GetTabIndent(3), CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}{{{1}", CodeGenerationHelper.GetTabIndent(3), CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}try{1}", CodeGenerationHelper.GetTabIndent(4), CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}{{{1}", CodeGenerationHelper.GetTabIndent(4), CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}using (SqlCommand cm = cn.CreateCommand()){1}", CodeGenerationHelper.GetTabIndent(5),

                              CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}{{{1}", CodeGenerationHelper.GetTabIndent(5), CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}cm.CommandType = CommandType.StoredProcedure;{1}",

                              CodeGenerationHelper.GetTabIndent(6), CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}cm.CommandText = INSERT_SPROC;{1}", CodeGenerationHelper.GetTabIndent(6),

                              CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}cm.Parameters.Add(\"returnValue\", SqlDbType.Int);{1}",

                              CodeGenerationHelper.GetTabIndent(6), CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format(

                    "{0}cm.Parameters[\"returnValue\"].Direction = ParameterDirection.ReturnValue;{1}",

                    CodeGenerationHelper.GetTabIndent(6), CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(CreateParameterList(ClassName, PropertyList, 6));

            retVal.Append(

                string.Format("{0}cm.ExecuteNonQuery();{1}", CodeGenerationHelper.GetTabIndent(6),

                              CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}insertedId = SafeData.ConvertInt(cm.Parameters[\"@{1}{2}\"].Value);{3}",

                              CodeGenerationHelper.GetTabIndent(6),

                              CommonHelper.GetVariableAbbreviation(CommonHelper.GetPrimaryKey(PropertyList)),

                              CommonHelper.GetPrimaryKeyName(PropertyList), CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}if (Convert.ToInt16(cm.Parameters[\"returnValue\"].Value) != 0){1}",

                              CodeGenerationHelper.GetTabIndent(6), CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}{{{1}", CodeGenerationHelper.GetTabIndent(6), CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}return false;{1}", CodeGenerationHelper.GetTabIndent(7),

                              CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}}}{1}", CodeGenerationHelper.GetTabIndent(6), CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}else{1}", CodeGenerationHelper.GetTabIndent(6), CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}{{{1}", CodeGenerationHelper.GetTabIndent(6), CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}return true;{1}", CodeGenerationHelper.GetTabIndent(7),

                              CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}}}{1}", CodeGenerationHelper.GetTabIndent(6), CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}}}{1}", CodeGenerationHelper.GetTabIndent(5), CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}}}{1}", CodeGenerationHelper.GetTabIndent(4), CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}catch (Exception ex){1}", CodeGenerationHelper.GetTabIndent(4), CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}{{{1}", CodeGenerationHelper.GetTabIndent(4), CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}SimpleLogger.LogError(ex.Message);{1}", CodeGenerationHelper.GetTabIndent(5),

                              CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}return false;{1}", CodeGenerationHelper.GetTabIndent(5),

                              CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}}}{1}", CodeGenerationHelper.GetTabIndent(4), CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}}}{1}", CodeGenerationHelper.GetTabIndent(3), CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}}}{1}", CodeGenerationHelper.GetTabIndent(2), CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}#endregion Add Data      {1}", CodeGenerationHelper.GetTabIndent(2),

                              CodeGenerationHelper.GetEndOfLine()));

            return retVal.ToString();

        }

Each of the methods works in the same fashion, creating the method and where necessary calling a helper method named CreateParameterList to generate each Property as it is needed.  The List class has the same process applied to it but only for the SelectList and SelectListByForeignKey methods.  The SelectListByForeignKey are generated for each Foreign Key individually, a future enhancement might be to define multiple key foreign keys in the code generator and have those methods generated as well.  Currently this has to be done manually.

Finally the Application Object generator works in a fashion that combines both of the previous generators.  It creates The Namespace Header as before, it creates the Property accessors, much like the Interface generator, except this time it implements the actual accessor not just the signature and it implements the corresponding public methods listed in the table above.  It implements constructors which as well.  Additionally, the Application Object performs a rules check before allowing the object to be persisted.  The CheckRules method is generated as follows:

        private static string CreateCheckRules(string input, IEnumerable<CodeGeneratorProperty> PropertyList)

        {

            StringBuilder retVal = new StringBuilder();

            retVal.Append(

                string.Format("{0}/// <summary>{1}", CodeGenerationHelper.GetTabIndent(2),

                              CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}/// All business rules for this class are checked in this method{1}",

                              CodeGenerationHelper.GetTabIndent(2), CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}/// </summary>{1}", CodeGenerationHelper.GetTabIndent(2),

                              CodeGenerationHelper.GetEndOfLine()));

            retVal.Append(

                string.Format("{0}public void CheckRules(){1}", CodeGenerationHelper.GetTabIndent(2),

                              CodeGenerationHelper.GetEndOfLine()));

            retVal.Append( CodeGenerationHelper.GetStandAloneOpeningBracket(2));

            // Walk through property list and create rules

            retVal.Append(CommonHelper.GetRulesForRequiredProperties(3, PropertyList));

            retVal.Append(CodeGenerationHelper.GetStandAloneClosingBracket(2));

            return (input + retVal);

        }

 

As one can see, the work for this method is mostly performed in the GetRuleForRequiredProperties helper method:

        public static string GetRulesForRequiredProperties(int TabIndent, IEnumerable<CodeGeneratorProperty> PropertyList)

        {

            string retVal = string.Empty;

            foreach (CodeGeneratorProperty property in PropertyList)

            {

                if (property.IsRequired)

                {

                    switch (property.DataType)

                    {

                        case "int":

                            retVal += GetRequiredInteger(TabIndent, property);

                            break;

                        case "Boolean":

                            retVal += "**** NOT YET IMPLEMENTED IN METHOD GetRulesForRequiredProperties() **** \n";

                            break;

                        case "string":

                            retVal += GetRequiredString(TabIndent, property);

                            break;

                        case "byte[]":

                            retVal += GetRequiredByteArray(TabIndent, property);

                            break;

                        case "Guid":

                            retVal += GetRequiredGuid(TabIndent, property);

                            break;

                        case "DateTime":

                            retVal += GetRequiredDate(TabIndent, property);

                            break;

                        case "Double":

                            retVal += "**** NOT YET IMPLEMENTED IN METHOD GetRulesForRequiredProperties() **** \n";

                            break;

                        case "Decimal":

                            retVal += GetRequiredDecimal(TabIndent, property);

                            break;

                        default:

                            retVal += "**** NOT YET IMPLEMENTED IN METHOD GetRulesForRequiredProperties() **** \n";

                            break;

                    }

                }

            }

            return retVal;

        }

 

This in turn uses other helper methods, such as GetRequiredString:

        private static string GetRequiredString(int TabIndent, CodeGeneratorProperty property)

        {

            StringBuilder retVal = new StringBuilder();

            retVal.Append(

                string.Format("{0}if(String.IsNullOrEmpty(_{1})){2}", CodeGenerationHelper.GetTabIndent(TabIndent),

                              property.PropertyName,

                              CodeGenerationHelper.GetEndOfLine()));

            retVal.Append( CodeGenerationHelper.GetStandAloneOpeningBracket(TabIndent);

            retVal.Append(

                string.Format("{0}BrokenRules.Add(\"{1} is a required field and cannot be null or empty.\");{2}",

                              CodeGenerationHelper.GetTabIndent(TabIndent + 1), property.PropertyName,

                              CodeGenerationHelper.GetEndOfLine()));

            retVal.Append( CodeGenerationHelper.GetStandAloneClosingBracket(TabIndent));

 

            // Greater than max length

            retVal.Append(

                string.Format("{0}if(_{1}.Length > {2}){3}", CodeGenerationHelper.GetTabIndent(TabIndent),

                              property.PropertyName, property.Size,

                              CodeGenerationHelper.GetEndOfLine()));

            retVal.Append( CodeGenerationHelper.GetStandAloneOpeningBracket(TabIndent));

            retVal.Append(

                string.Format("{0}BrokenRules.Add(\"{1} has exceeded its maximum length of {2} characters.\");{3}",

                              CodeGenerationHelper.GetTabIndent(TabIndent + 1), property.PropertyName, property.Size,

                              CodeGenerationHelper.GetEndOfLine()));

            retVal.Append( CodeGenerationHelper.GetStandAloneClosingBracket(TabIndent));

 

            return retVal.ToString();

        }

 

As you can see not every DataType is handled, again, this being a personal code generator it can evolve as needed, instead of having to cover every scenario.

That wraps it up for this installation.  We now have the ability to generate a table, its related stored procedures, a related interface, a related data access class and an application object class that exposes an API for any user. In the next and final installation of this posting we'll look at pulling it all together and writing a little "calorie counter" application to see how it works.

Part 5 - Creating Stored Procedures using SQL Server Management Objects (SMO)

 

In this installation of my blog series I'll cover defining, creating and persisting SQL scripts for stored procedures based on our CodeGeneratorPropertyList using SQL Server Management Objects (SMO).  As I stated at the end of my last post, this takes an entirely different approach than table generation.  Tables are created and then the script is generated from the execution, stored procedures, on the other hand, require us to create the sql script and then execute it against the server.

The code generator will iterate the CodeGenratorPropertyList to generate the four basic CRUD stored procedures (Create, Retrieve, Update and Delete) as well as a generic Exists stored procedure and a generic SelectList stored procedure that returns all of the records and finally Foreign key based stored procedures.  That is, if a field is designated as a foreign key in a table then we will create a stored procedure that retrieves a list based on that Foreign key.  So, for example, the UserAddress table might have the UserId as a foreign key, in this case we would like to generate a SelectList_ByUserId stored procedure to use later on in our classes.

All of the stored procedure code will be generated using a standard layout and indenting to make it readable.  This includes the following:

  • a standard DROP clause with a successful ‘dropped' print message

USE [NAMESPACE]

GO

 

SET QUOTED_IDENTIFIER OFF

GO

SET ANSI_NULLS OFF

GO

 

/*

 * Drop Stored procedure if it exists

 */

IF EXISTS

       (

              SELECT

                     *

              FROM

                     sysobjects

              WHERE

                     id = object_id(N'[NAMESPACE_User_Select_ByUserId]')

              AND

                     OBJECTPROPERTY(id, N'IsProcedure') = 1

       )

       BEGIN

              DROP PROCEDURE [NAMESPACE_User_Select_ByUserId]

              IF @@ERROR = 0

                     BEGIN

                           PRINT '<<NAMESPACE_User_Select_ByUserId stored procedure was dropped successfully>>'

                     END

       END

GO

  • A header

/*

************************************************************************************************************

*

* Name: NAMESPACE_User_Select_ByUserId

*

* Sample Call:

       NAMESPACE_User_Select_ByUserId '10228341-4204-4ddd-9f83-592e4c35cf2b'

*

* ----------------------------------------------------------------------------------------------------------

*

* This Procedure Called by .NET class methods: 

*             NAMESPACE.User.Fetch() 

*

* ----------------------------------------------------------------------------------------------------------

*

* Modification History:

*

* Date        Developer                Description

* ----------------------------------------------------------------------------------------------------------

* 2/12/2008          John P. Frampton               Created

*

************************************************************************************************************

*/

  • the body of the procedure with basic error handling

CREATE PROCEDURE

       NAMESPACE_User_Select_ByUserId

              (

                     @uidUserId           uniqueidentifier

              )

AS

SET NOCOUNT ON

BEGIN

 

       DECLARE @v_intError AS int

 

       SELECT

               [UserId]

              ,[ParentUserId]

              ,[FirstName]

&n