Build Provider is a new feature introduced in ASP.NET 2.0. You can perform very nifty stuff if you know how to use it correctly. In this article I will demonstrate how to create a custom build provider which will generate custom entity classes. I like to thank Fritz Onion for this post about build providers Jaw-dropping Experience with Custom Build Providers which formed the basis for this article.
Introduction:
Build Provider is a new feature introduced in ASP.NET 2.0. You can perform very nifty stuff if you know how to use it correctly. In this article I will demonstrate how to create a custom build provider which will generate custom entity classes. I like to thank Fritz Onion for this post about build providers Jaw-dropping Experience with Custom Build Providers which formed the basis for this article.
What are Build Providers?
To better understand the build providers let’s first take a look at the App_Code folder in ASP.NET 2.0 applications. The App_Code is a special folder in the application which will build the items inside it. This means that if you put an aspx page inside the App_Code then the file is build using the System.Web.Compilation.PageBuildProvider. This is one of the default providers used by the ASP.NET to compile and build .aspx extension pages. There are many other providers used by ASP.NET when building the application. Take a look at the providers in the code below:
<buildProviders>
<add extension=".aspx" type="System.Web.Compilation.PageBuildProvider"/>
<add extension=".ascx" type="System.Web.Compilation.UserControlBuildProvider"/>
<add extension=".master" type="System.Web.Compilation.MasterPageBuildProvider"/>
<add extension=".asix" type="System.Web.Compilation.ImageGeneratorBuildProvider"/>
<add extension=".asmx" type="System.Web.Compilation.WebServiceBuildProvider"/>
<add extension=".ashx" type="System.Web.Compilation.WebHandlerBuildProvider"/>
<add extension=".soap" type="System.Web.Compilation.WebServiceBuildProvider"/>
<add extension=".resx" type="System.Web.Compilation.ResXBuildProvider"/>
<add extension=".resources" type="System.Web.Compilation.ResourcesBuildProvider"/>
<add extension=".wsdl" type="System.Web.Compilation.WsdlBuildProvider"/>
<add extension=".xsd" type="System.Web.Compilation.XsdBuildProvider"/>
<add extension=".js" type="System.Web.Compilation.ForceCopyBuildProvider"/>
</buildProviders>
Each provider is registered using an extension and the type. Now, let’s take a look at how we can create our own custom providers.
Creating the Mapping File:
The first task is to create a mapping file. The mapping file is the file which will be placed in the App_Code folder and build using our custom build provider. Since, we will be creating entity classes and our classes will be based on the database schema we will call it .map files. Let’s take a look at the .map file which contains the information on how to create the entity classes.
<?xml version="1.0" encoding="utf-8" ?>
<mappings>
<mapping connectionString="Server=localhost;Database=Northwind;Trusted_Connection=true"
selectCommand="SELECT * FROM Products" className="Product" namespace="EntityClass.BLL"/>
<mapping connectionString="Server=localhost;Database=Northwind;Trusted_Connection=true"
selectCommand ="SELECT * FROM Categories" className="Category" namespace="EntityClass.BLL" />
</mappings>
The mapping file contains the root called “mappings”. After that each database table is contained inside the “mapping” element. Let’s take a look at the attributes in more details:
connectionString: The connection string used to connect to the database.
selectCommand: The command to execute in order to get the schema.
className: The name of the entity class to be created.
namespace: The name of the namespace.
Creating the Mapping Class:
It is not necessary to create a mapping class but I am doing it over here to organize the code. The only purpose of the mapping class is to hold the information defined in the mapping file. Take a look at the mapping class below:
public class Entity
{
private string _connectionString;
private string _selectCommand;
private string _className;
private string _namespace;
public string ConnectionString
{
get { return this._connectionString; }
set { this._connectionString = value; }
}
public string SelectCommand
{
get { return this._selectCommand; }
set { this._selectCommand = value; }
}
public string ClassName
{
get { return this._className; }
set { this._className = value; }
}
public string Namespace
{
get { return this._namespace; }
set { this._namespace = value; }
}
}
Creating the Custom Entity Builder Class:
Now, we are ready to create our custom entity class. Simply, add a new Class Library project to your solution and then add a class named EntityBuilder. The EntityBuilder class should derive from the BuildProvider class. Next, override the GenerateCode method.
Here is the implementation of the GenerateCode method:
public override void GenerateCode(AssemblyBuilder assemblyBuilder)
{
string virtualPath = base.VirtualPath;
List<Entity> entities = PopulateEntityClass(virtualPath);
CodeCompileUnit code = CodeGenerator.GenerateCode(entities);
assemblyBuilder.AddCodeCompileUnit(this, code);
base.GenerateCode(assemblyBuilder);
}
The AssemblyBuilder class is responsible for creating the assemblies at runtime. The property base.VirtualPath will retrieve the virtual path of the mapping file, Database.map. The PopulateEntityClass method is used to populate our custom entity class with the data from the mapping file. Finally, the CodeGenerate.GenerateCode method is used to generate the code using the CodeDom classes.
PopulateEntityClass Method:
Let’s take a look at this method in more detail.
private List<Entity> PopulateEntityClass(string virtualPath)
{
XmlDocument xDoc = new XmlDocument();
List<Entity> entities = new List<Entity>();
using (Stream s = VirtualPathProvider.OpenFile(virtualPath))
{
xDoc.Load(s);
}
XmlNodeList mappings = xDoc.SelectNodes("mappings/mapping");
foreach (XmlNode mapping in mappings)
{
// create a new entity
Entity entity = new Entity();
entity.ConnectionString = mapping.Attributes["connectionString"].Value;
entity.SelectCommand = mapping.Attributes["selectCommand"].Value;
entity.ClassName = mapping.Attributes["className"].Value;
entity.Namespace = mapping.Attributes["namespace"].Value;
// add to the entity list
entities.Add(entity);
}
return entities;
}
The PopulateEntityClass method takes the virtual path of the mapping file as a parameter. The file is open and loaded into the XmlDocument object named xDoc. Finally, the xDoc object is iterated over and the custom entity class is populated and added to the generic collection.
Generating Code Using CodeDom:
The next step is to generate the code using the CodeDom classes. The GenerateCode method of the CodeGenerator class takes the generic entity collection as a parameter. Later the collection is iterated and the DataTable object is created which confines to the schema defined in the mapping file.
private static void FillTableSchema(Entity entity,DataTable table)
{
using (SqlConnection myConnection = new SqlConnection(entity.ConnectionString))
{
SqlDataAdapter ad = new SqlDataAdapter(entity.SelectCommand, myConnection);
ad.FillSchema(table, SchemaType.Mapped);
}
}
And finally here is the GenerateCode method which returns the CodeCompileUnit after generating the classes.
public static CodeCompileUnit GenerateCode(List<Entity> entities)
{
CodeCompileUnit code = new CodeCompileUnit();
foreach (Entity entity in entities)
{
DataTable table = new DataTable("Data");
FillTableSchema(entity, table);
// iterate through the table to create entities
CodeNamespace ns = new CodeNamespace(entity.Namespace);
CodeNamespaceImport import = new CodeNamespaceImport("System");
ns.Imports.Add(import);
code.Namespaces.Add(ns);
CodeTypeDeclaration type = new CodeTypeDeclaration(entity.ClassName);
ns.Types.Add(type);
for(int i=0; i<table.Columns.Count;i++)
{
CodeMemberField field = new CodeMemberField(table.Columns[i].DataType.Name, "_" + table.Columns[i].ColumnName.ToLower());
CodeMemberProperty property = new CodeMemberProperty();
property.Name = table.Columns[i].ColumnName;
property.Type = new CodeTypeReference(table.Columns[i].DataType.Name);
// the following will make the field public and not virtual
property.Attributes = MemberAttributes.Public | MemberAttributes.Final;
CodeFieldReferenceExpression fieldRef = new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), field.Name);
CodeMethodReturnStatement ret = new CodeMethodReturnStatement(fieldRef);
property.GetStatements.Add(ret);
CodeAssignStatement assign = new CodeAssignStatement(
fieldRef, new CodePropertySetValueReferenceExpression());
property.SetStatements.Add(assign);
type.Members.Add(field);
type.Members.Add(property);
}
}
return code;
}
Configuration Settings in Web.config:
Before you use the custom build provider you will need to register it in the Web.config file.
<compilation debug="true">
<buildProviders>
<add extension=".map" type="EntityClassProvider.EntityBuilder,EntityClassProvider"/>
</buildProviders>
</compilation>
Once, you have registered the custom build provider you can use it. Just make sure that the .map file is placed inside the App_Code folder and also make sure to add a reference to the class library project from the website project.
Here are some of the screens shots of the custom build provider in action. Note that the classes Category and Product are created at runtime.
Conclusion:
In this article we learned how to create a custom build provider which helps us to create our custom entity classes. You can extend the entity build provider by adding the relationship feature. This will enable the classes to have collections of other objects.
I hope you liked the article, happy coding!