Click here to Skip to main content
15,879,096 members
Articles / Web Development / ASP.NET
Article

Experimenting Custom Build Providers

Rate me:
Please Sign up or sign in to vote.
4.69/5 (18 votes)
1 May 2006CPOL4 min read 53K   323   64   9
A simple OR Mapper built with custom build providers, which reads data from XML files.

Introduction

Recently, Microsoft released the source code for the built-in Providers that ship with the .NET 2.0 framework. While I never downloaded them, this release caught my attention, so I started to study this new feature of ASP.NET. I was really impressed by the power of the build providers (I don't know why they didn't get my attention until now), so I decided to share my weekend experience with you.

Declarative type creation

I started this process by creating an XML file in the App_Code, trying to dynamically create a type from it. Here is the file's content:

XML
<types>
    <class name="User" namespace="Mapper.Core">
        <property name="ID" type="System.Int32" />
        <property name="FirstName" type="System.String" />
        <property name="LastName" type="System.String" />
        <property name="Username" type="System.String" />
        <property name="Password" type="System.String" />        
        <property name="Email" type="System.String" />        
    </class>
</types>

After reading some articles about custom build providers (one of them being Javier Lozano's great article which can be found here) and others about CodeDom, I started to build my test provider.

I created the EntityBuildProvider class, extending the System.Web.Compilation.BuildProvider class. The ASP.NET engine executes the GenerateCode method during development and compilation, generating the code for the custom providers in the Temporary ASP.NET Files folder, and includes them in the App_Code assembly.

C#
public override void GenerateCode(AssemblyBuilder assemblyBuilder)
{
    string fileName = base.VirtualPath;
    
    CodeCompileUnit generatedUnit = GenerateUnit(fileName);
    assemblyBuilder.AddCodeCompileUnit(this, generatedUnit);
}

The GenerateUnit method reads the XML file content, and generates the classes with the found properties using CodeDom. I then add my build provider in the web.config file:

XML
<compilation debug="true">
    <buildProviders>
        <add extension=".xml" 
            type="ObjectMapper.EntityBuildProvider, ObjectMapper"/>
    </buildProviders>
</compilation>

And the result is a generated type with full intellisense support in Visual Studio 2005:

Intellisense support

Using some string utility methods, I've added support for PascalCase/camelCase code generation, and the ASP.NET engine creates my User class in the Temporary ASP.NET Files folder and includes it in the App_Code assembly.

C#
namespace Mapper.Core
{
    public class User
    {
        private System.Int32 _id;
        private System.String _firstName;
        private System.String _lastName;
        private System.String _username;
        private System.String _password;
        private System.String _email;

        public User()
        {
        }

        public System.Int32 ID
        {
            get { return _id; }
            set { _id = value; }
        }


        public System.String FirstName
        {
            get { return _firstName; }
            set { _firstName = value; }
        }

        public System.String LastName
        {
            get { return _lastName; }
            set { _lastName = value; }
        }

        public System.String Username
        {
            get { return _username; }
            set { _username = value; }
        }

        public System.String Password
        {
            get { return _password; }
            set { _password = value; }
        }

        public System.String Email
        {
            get { return _email; }
            set { _email = value; }
        }
    }
}

After adding some additional properties for my User class, I click "Save", and Visual Studio executes my custom provider's GenerateCode method again, without me recompiling the application, and all my newly added properties are created and I have full intellisense support for them.

XML
<types>
    <class name="User" namespace="Mapper.Core">
        <property name="ID" type="System.Int32" />
        <property name="FirstName" type="System.String" />
        <property name="LastName" type="System.String" />
        <property name="Username" type="System.String" />
        <property name="Password" type="System.String" />
        <property name="Email" type="System.String" />
        <property name="Enabled" type="System.Boolean" />
        <property name="Phone" type="System.String" />
        <property name="Address1" type="System.String" />
        <property name="Address2" type="System.String" />
    </class>
</types>

Intellisense support

Extending my custom language

Seeing that my custom build provider allows me to use all the power of the .NET framework to implement a custom programming language (even if one of my favorite CodeProject member wrote a great article about the limitations), I started to add more features:

For the class XML node:

  • table attribute - representing the mapped SQL Server table to the generated class.
  • GenerateStoredProcedures attribute (true/false) - specifies if the provider should generate the stored procedures for the mapped table.
  • SqlStoredProceduresPrefix attribute - specifies the prefix of the generated stored procedures.
  • DropExistingStoredProcedures attribute - specifies if the generated SQL script should include DROP statements for the existing database objects.
  • ExportLocation attribute - specifies the physical location where the SQL script files should be generated.

For the property XML node:

  • column attribute - representing the mapped table column to the generated property.
  • IsPrimaryKey attribute - representing if the property is the object's identifier (supports multiple identifiers).
  • IsIdentity attribute - specifies is the mapped column is an identity column.
  • SqlType attribute - specifies the mapped column's SQL type.
  • SqlLength attribute - specifies the mapped column's SQL length.

I've also created some additional string utility methods for getting the plural/singular for a name, also the generated properties have some basic XML comments. Because the purpose of this article is to create custom build providers, we'll not discuss about the CodeDom implementation process. However, if you're interested in this namespace, you can try reading an excellent article which can be found here.

The final XML file would look like this:

XML
<?xml version="1.0" encoding="utf-8" ?>
<types>
    <class name="User" table="Users" namespace="Mapper.Core" 
             SqlStoredProceduresPrefix="DB_"
             GenerateStoredProcedures="true" 
             ExportLocation="E:\GeneratedFiles\">
        <property name="ID" column="ID" IsIdentity="true" 
             IsPrimaryKey="true" 
             type="System.Int32" SqlType="int" />
        <property name="FirstName" column="FirstName" 
             type="System.String" SqlType="nvarchar" 
             SqlLength="50" />
        <property name="LastName" column="LastName" 
             type="System.String" SqlType="nvarchar" 
             SqlLength="50" />
        <property name="Username" column="Username" 
             type="System.String" SqlType="nvarchar" 
             SqlLength="50" />
        <property name="Password" column="Password" 
             type="System.String" SqlType="nvarchar" 
             SqlLength="50" />
        <property name="Enabled" column="Enabled" 
             type="System.Boolean" SqlType="bit" />
        <property name="Email" column="Email" 
             type="System.String" SqlType="nvarchar" 
             SqlLength="50" />
        <property name="Phone" column="Phone" 
             type="System.String" SqlType="nvarchar" 
             SqlLength="50" />
        <property name="Address1" column="Address1" 
             type="System.String" SqlType="nvarchar" 
             SqlLength="50" />
        <property name="Address2" column="Address2" 
             type="System.String" SqlType="nvarchar" 
             SqlLength="50" />
    </class>
</types>

And the generated code by the custom provider:

C#
namespace Mapper.Core {

    [System.Serializable()]

    public class User : ObjectMapper.Utils.Entity {
        
        private int _id;
        private string _firstName;
        private string _lastName;
        private string _username;
        private string _password;
        private bool _enabled;
        private string _email;
        private string _phone;
        private string _address1;
        private string _address2;

        /// <summary>
        /// Gets or sets the user's identifier.
        /// </summary>

        public int ID {
            get {
                return _id;
            }
            set {
                if ((value != this._id)) {
                    this._id = value;
                    base.MarkDirty();
                }
            }
        }

        /// <summary>
        /// Gets or sets the user's first name.
        /// </summary>

        public string FirstName {
            get {
                return _firstName;
            }
            set {
                if ((value != this._firstName)) {
                    this._firstName = value;
                    base.MarkDirty();
                }
            }
        }

        // ----------------------------------------
        // All other properties go here
       // ----------------------------------------
      
        public static Mapper.Core.User GetUser(int id) {
            System.Data.SqlClient.SqlCommand selectCommand;
            selectCommand = 
              ObjectMapperUtils.DataUtility.CreateCommand(
              "DB_Users_Select");
            selectCommand.Parameters.AddWithValue("@ID", id);
            System.Collections.Generic.List<Mapper.Core.User> users;
            users = Mapper.Core.User.UserListFromReader(
                    ObjectMapperUtils.DataUtility.ExecuteReader(
                    selectCommand));
            if ((users.Count > 0)) {
                return users[0];
            }
            return null;
        }
        
        public override void Insert() {
            System.Data.SqlClient.SqlCommand insertCommand;
            insertCommand = 
              ObjectMapperUtils.DataUtility.CreateCommand(
              "DB_Users_Insert");
            insertCommand.Parameters.AddWithValue("@FirstName", this.FirstName);
            insertCommand.Parameters.AddWithValue("@LastName", this.LastName);
            insertCommand.Parameters.AddWithValue("@Username", this.Username);
            insertCommand.Parameters.AddWithValue("@Password", this.Password);
            insertCommand.Parameters.AddWithValue("@Enabled", this.Enabled);
            insertCommand.Parameters.AddWithValue("@Email", this.Email);
            insertCommand.Parameters.AddWithValue("@Phone", this.Phone);
            insertCommand.Parameters.AddWithValue("@Address1", this.Address1);
            insertCommand.Parameters.AddWithValue("@Address2", this.Address2);

            System.Data.IDataReader reader;
            reader = 
              ObjectMapperUtils.DataUtility.ExecuteReader(insertCommand);
            if ((reader.Read() == true)) {
               
                this.ID = System.Convert.ToInt32(reader[0]);
            }
            if ((reader.IsClosed != true)) {
                reader.Close();
            }
           
            base.MarkOld();
        }
        
        public override void Update() {

            System.Data.SqlClient.SqlCommand updateCommand;
            updateCommand = 
              ObjectMapperUtils.DataUtility.CreateCommand("DB_Users_Update");
            updateCommand.Parameters.AddWithValue("@ID", this.ID);
            updateCommand.Parameters.AddWithValue("@FirstName", this.FirstName);
            updateCommand.Parameters.AddWithValue("@LastName", this.LastName);
            updateCommand.Parameters.AddWithValue("@Username", this.Username);
            updateCommand.Parameters.AddWithValue("@Password", this.Password);
            updateCommand.Parameters.AddWithValue("@Enabled", this.Enabled);
            updateCommand.Parameters.AddWithValue("@Email", this.Email);
            updateCommand.Parameters.AddWithValue("@Phone", this.Phone);
            updateCommand.Parameters.AddWithValue("@Address1", this.Address1);
            updateCommand.Parameters.AddWithValue("@Address2", this.Address2);

            ObjectMapperUtils.DataUtility.ExecuteNonQuery(updateCommand);
           
            base.MarkOld();
        }
        
        public override void Delete() {

            System.Data.SqlClient.SqlCommand deleteCommand;
            deleteCommand = 
              ObjectMapperUtils.DataUtility.CreateCommand("DB_Users_Delete");
            deleteCommand.Parameters.AddWithValue("@ID", this.ID);
           
            ObjectMapperUtils.DataUtility.ExecuteNonQuery(deleteCommand);
           
            base.MarkNew();
        }
       
        internal static void Fetch(Mapper.Core.User user, 
                       System.Data.IDataReader reader) {
            Mapper.Core.User.Fetch(user, reader, 0);
        }
        
        internal static void Fetch(Mapper.Core.User user, 
                System.Data.IDataReader reader, int startIndex) {
            user.ID = reader.GetInt32((0 + startIndex));
            user.FirstName = reader.GetString((1 + startIndex));
            user.LastName = reader.GetString((2 + startIndex));
            user.Username = reader.GetString((3 + startIndex));
            user.Password = reader.GetString((4 + startIndex));
            user.Enabled = reader.GetBoolean((5 + startIndex));
            user.Email = reader.GetString((6 + startIndex));
            user.Phone = reader.GetString((7 + startIndex));
            user.Address1 = reader.GetString((8 + startIndex));
            user.Address2 = reader.GetString((9 + startIndex));
        }
        
        internal static System.Collections.Generic.List<Mapper.Core.User> 
                 UserListFromReader(System.Data.IDataReader reader) {

            return Mapper.Core.User.UserListFromReader(reader, 0);
        }
        
        internal static System.Collections.Generic.List<Mapper.Core.User> 
                 UserListFromReader(System.Data.IDataReader reader, 
                 int startIndex) {

       System.Collections.Generic.List<Mapper.Core.User> users;
           users = new 
             System.Collections.Generic.List<Mapper.Core.User>();
           for (
            ; (reader.Read() == true);
           ) {
                Mapper.Core.User user;
                user = new Mapper.Core.User();
               
                Mapper.Core.User.Fetch(user, reader, startIndex);
                user.MarkOld();
                users.Add(user);
             }
           if ((reader.IsClosed != true)) {
                reader.Close();
           }

            return users;
        }
        
        public static 
          System.Collections.Generic.List<Mapper.Core.User> GetUsers() {

            System.Data.SqlClient.SqlCommand selectAllCommand;
            selectAllCommand = 
              ObjectMapperUtils.DataUtility.CreateCommand(
              "DB_Users_SelectAll");

            return Mapper.Core.User.UserListFromReader(
              ObjectMapperUtils.DataUtility.ExecuteReader(
              selectAllCommand));
        }
    }
}

The final screen shows the intellisense support with the generated XML comments as well.

Intellisense support

Using the code

For testing the code in a separate project, you should add references to the ObjectMapper and ObjectMapperUtils assemblies. The web.config file should contain the <buildProviders> node described in this article. Then, you can just add XML files in the described format to your App_Code folder.

Future ideas

  • Relationships support between types (foreign key mappings).
  • Support for creating/altering the tables based on the changes in the XML mapping file.
  • Custom members for objects selection from the data source, in a declarative way.
  • Custom members using code snippets in the XML files.

Points of Interest

Build providers are a powerful feature of ASP.NET 2.0, which can increase our productivity pretty much. I hope I was able to show you the advantages we can have if we're using them.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
Romania Romania
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionNot possible with WebApplication Pin
Muhammed Yaseen14-Nov-11 1:26
Muhammed Yaseen14-Nov-11 1:26 
GeneralMy vote of 5 Pin
daiyu316626-Feb-11 4:34
daiyu316626-Feb-11 4:34 
GeneralDo not understand why people got excited with this Pin
cklein8-Dec-06 12:27
cklein8-Dec-06 12:27 
QuestionWould you mind if I used your code? Pin
Javier Lozano20-Sep-06 9:25
Javier Lozano20-Sep-06 9:25 
AnswerRe: Would you mind if I used your code? Pin
Cristian Odea20-Sep-06 9:31
Cristian Odea20-Sep-06 9:31 
GeneralRe: Would you mind if I used your code? Pin
Javier Lozano20-Sep-06 11:52
Javier Lozano20-Sep-06 11:52 
GeneralAdding query capability Pin
nyxtom22-Aug-06 16:52
nyxtom22-Aug-06 16:52 
How hard would it be to add functionality to generate methods based on queries made in the file. I'm assuming this would just be generation of the sql script or addition to it by adding to the BuildProvider to support queries. I would really like to see more of this implemented like the features you discussed. DLinq has features like relational mapping. It would be great to have a way to implement this like DLinq does.
GeneralBrilliant Pin
William Rawls1-Aug-06 5:51
William Rawls1-Aug-06 5:51 
JokeLook nice Pin
Niels Erik Meier3-May-06 10:36
Niels Erik Meier3-May-06 10:36 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.