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:
<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.
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:
<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:
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.
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.
<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>
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 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:
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;
public int ID {
get {
return _id;
}
set {
if ((value != this._id)) {
this._id = value;
base.MarkDirty();
}
}
}
public string FirstName {
get {
return _firstName;
}
set {
if ((value != this._firstName)) {
this._firstName = value;
base.MarkDirty();
}
}
}
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.
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.