Sunday, November 21, 2010

Part 3 – Creating the Domain class with CodeDom and IronRuby

This is the final post in a three part series in which I discuss how I used IronRuby to generate data access code.  In this post I’m going to discuss how IronRuby was used to generate a C# domain class for each model.

All source files can be downloaded from here

Generating the Domain/Service Layer Code

Now that we have models to truck the data from the DAL to the presentation layer we need the code to move the data between the two layers.  In this version of the project I used the CodeDom to generate the C# code.  This will change as I move this project more into our production environment.  I plan on moving towards a templating engine such as Ruby’s ERB or ASP.NET MVC 3 Razor view engine.  In the beginning of the project I had intended to write this layer using the Emit approach but it found it to be overkill and at this layer the chances are better that we will need to modify the generated code which is not possible if we go down the Emit path.  With that said lets dive into the CodeDom approach. 

In this post I will walk you through how I created the class, added a private field, the constructor

System.CodeDom – The Setup

In order generate the code we must first create a CodeCompileUnit object.  Think of it as a container for the code tree we are about to create.  Next we CodeNamespace object passing in the namespace that will contain the class we are creating. The last bit of setup we’ll do is to import any namespaces our class will need using the CodeNamespaceImport object.Here’s what the setup code looks like:

@target_unit = CodeCompileUnit.new
@nspace_holder = CodeNamespace.new(@namespace)
@nspace_holder.Imports.Add CodeNamespaceImport.new(@model_namespace)
view raw gistfile1.rb hosted with ❤ by GitHub
Creating the Domain Class

Now we can actually create our class type using the CodeTypeDeclaration class passing in the name of the type we are creating.  This call creates a type but we still need to indicate we are creating a class by setting the CodeTypeDeclaration.IsClass property to true.  We’ll also make the class public by setting the TypeAttributes property to TypeAttributes.Public.  After creating the class type we’ll add it to the namespace by passing the type to the @nspace_holder.Imports.Add method.  The code for creating the class type is below.

@target_class = CodeTypeDeclaration.new(@class_name)
@target_class.IsClass = true
@target_class.TypeAttributes = class_attrs
@nspace_holder.Types.Add(@target_class)
view raw gistfile1.rb hosted with ❤ by GitHub
Adding a Field

Our class will need a field to store the reference to the repository it will use. The first step is to create a CodeMemberField object and set the attributes to private, give it the name _repo and set the type to be IRepository.  Next we’ll add it to our class by adding it to the Members list.

def add_field(name, type, attributes = MemberAttributes.Public)
field = CodeMemberField.new
field.Attributes = attributes
field.Name = name
field.Type = CodeTypeReference.new type
field.Comments.Add( CodeCommentStatement.new("Repository"))
@target_class.Members.Add(field)
end
view raw gistfile1.rb hosted with ❤ by GitHub
Adding the Constructor

Once we have the field to hold the repository we need to set it to something.  The constructor will have one parameter IRepository repo.  The body of the constructor will have a check to ensure that the repo parameter is not null.  If it is null it will throw an ArgumentNullException.

def add_constructor
arg_name = "repo"
# Create the constructor's object
ctor = CodeConstructor.new
ctor.Attributes = MemberAttributes.Public
ctor.Parameters.Add(CodeParameterDeclarationExpression.new("IRepository", arg_name))
# Grab the references for both the parameter we just added and the field we created
# earlier
arg_ref = CodeArgumentReferenceExpression.new(arg_name)
repo_ref = CodeFieldReferenceExpression.new
repo_ref.FieldName = @repo_field_name
# Creating the condition statement for the if/else that will wrap our assignment statement
condition_statement = CodeSnippetExpression.new("#{arg_name} != null")
# Create the statement that will be used when the condition statement is true
assign_statement = CodeAssignStatement.new(repo_ref, arg_ref)
ctor.Statements.Add(create_if_else_statement(condition_statement, assign_statement,@arg_null_exception_stmt))
@target_class.Members.Add(ctor)
end
view raw gistfile1.rb hosted with ❤ by GitHub

Creating the constructor object is as simple as create a new CodeConstructor object.  We make the constructor public by setting its attributes to MemberAttributes.Public.  Next, the IRepository parameter is added to the Parameters list by creating a CodeParameterDeclarationExpression object passing in the type and name of the parameter. 

Once we have created the parameter we need to grab a reference to it so we can use it to set the class’s _repo field to the value of the parameter. We pass the parameter and field references to the CodeAssignmentStatement constructor.  This object will be used as the true statement in the if/else code which is created when we call the create_if_else_statement method.

def create_if_else_statement(condition, true_statements, false_statements)
is_null_check = CodeConditionStatement.new
is_null_check.Condition = condition
is_null_check.TrueStatements.Add(true_statements)
is_null_check.FalseStatements.Add(false_statements)
is_null_check
end
view raw gistfile1.rb hosted with ❤ by GitHub

As you can see this method is pretty straight forward.  You pass in a condition to test, the statements to execute if the condition is true or false.  It returns the statement that we will add to the constructor object’s Statements list. The last step is to add the constructor to the class's Members list.

Creating the Get Methods

The domain class has two Get methods. The first one returns an IQueryable that when executed would return all records. The second will return the record that matches the Id parameter that is passed in.

All methods we create are started by calling the basic_method.  This method instantiates a CodeMemberMethod object, setting the Attributes to be public method, the name of the method and the return type.

def basic_method(name, return_type, attrs = MemberAttributes.Public)
method = CodeMemberMethod.new
method.Attributes = attrs
method.Name = name
method.ReturnType = return_type
method
end
view raw gistfile1.rb hosted with ❤ by GitHub

The first get method

This method will return an IQueryable.  In order to set that up we create a CodeTypeReference object passing in IQueryable<T> where T is the EF model type to the constructor. Our next step is to add the return statement to the method’s body.  This is done by creating a CodeMethodReturnStatement passing the results of the create_repo_method_call method to it’s constructor.

def create_repo_method_call(method_name)
repo_call = CodeMethodInvokeExpression.new()
repo_call.Method = CodeMethodReferenceExpression.new(
CodeTypeReferenceExpression.new( CodeTypeReference.new("_repo")),
method_name,
@code_type_ref_of_model.ToArray)
repo_call
end
view raw gistfile1.rb hosted with ❤ by GitHub

The create_repo_method_call is a way to create calls to the repository class.  It creates a CodeMethodInvokeExpress object that represents the method we will call in our method.  It takes 3 parameters.  The first is a CodeTypeReferenceExpression which in our case is the _repo field. The second parameter is the name of the method we are going to call, in this case its Get. The final parameter takes an array of parameters that the called method receives.  If there are no parameters it is an empty array.

After the create_repo_method_call returns and the results of the call are stored in the method’s Statements property the method is added to the class’s Members list.

The Second get method

The second Get method takes an Id parameter which is used to find the single record that matches it. After the method’s CodeMemberMethod object is created the first thing we do is add the Id parameter following the same process we did with the constructor and create a reference to it.  Next, a MethodInvokeExpression object is created to make the call to the repository’s Get method.  In addition to this call there will be two other method calls chained to it.  The first is a LINQ Where method that takes a CodeSnippetExpression object as its parameter.  Finally an FirstOrDefault method is added to the chain.  Here is the code for the add_get_methods.

def add_get_methods
method = basic_method("Get", CodeTypeReference.new("IQueryable<#{@ef_model_name}>"))
method.Statements.Add( CodeMethodReturnStatement.new( create_repo_method_call("Get") ) )
@target_class.Members.Add(method)
method_id = basic_method("Get", CodeTypeReference.new(@ef_model_name))
method_id.Parameters.Add(CodeParameterDeclarationExpression.new(
CodeTypeReference.new(1.GetType), "id"))
where_clause = CodeMethodInvokeExpression.new()
where_clause.Method = CodeMethodReferenceExpression.new( create_repo_method_call("Get") , "Where")
keys = @element_svc.get_keys( @table_name )
where_clause.Parameters.Add(CodeSnippetExpression.new("p => p.#{keys[0]} == id"))
first_or_default_clause = CodeMethodInvokeExpression.new()
first_or_default_clause.Method = CodeMethodReferenceExpression.new( where_clause, "FirstOrDefault")
method_id.Statements.Add( CodeMethodReturnStatement.new(first_or_default_clause))
@target_class.Members.Add(method_id)
end
view raw gistfile1.rb hosted with ❤ by GitHub

The Complete Domain/Service Generated Through this Process

//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.1
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace BaseballStatsSite.Services
{
using BaseballStats.Models;
using System;
using System.Linq;
using BaseballStatsSite.Storage;
public class BattingStatService
{
// Repository
private IRepository _repo;
public BattingStatService(IRepository repo)
{
if (repo != null)
{
_repo = repo;
}
else
{
throw new System.ArgumentNullException();
}
}
public virtual IQueryable<BattingStat> Get()
{
return _repo.Get<BattingStat>();
}
public virtual BattingStat Get(int id)
{
return _repo.Get<BattingStat>().Where(p => p.lahmanID == id).FirstOrDefault();
}
public virtual void Add(BattingStatsModel model)
{
if (model != null)
{
_repo.Add<BattingStat>(this.ConvertToEfModel(model));
}
else
{
throw new System.ArgumentNullException();
}
}
public virtual void Update(BattingStatsModel model)
{
if (model != null)
{
var rec = this.GetRecord(model.Id);
this.UpdateEfModel(rec, model);
_repo.Update<BattingStat>(rec);
}
else
{
throw new System.ArgumentNullException();
}
}
public virtual void Delete(int id)
{
if (id > 1)
{
var rec = this.GetRecord(id);
_repo.Delete<BattingStat>(rec);
}
else
{
throw new System.ArgumentOutOfRangeException();
}
}
public virtual BattingStat ConvertToEfModel(BattingStatsModel model)
{
var rec = new BattingStat();
if (model != null)
{
// Assign values
model.Id = rec.lahmanID;
model.Year = rec.yearID;
model.Stint = rec.stint;
model.TeamId = rec.teamID;
model.LeagueId = rec.lgID;
model.Games = rec.g;
model.BattingGames = rec.g_batting;
model.AtBats = rec.ab;
model.Runs = rec.r;
model.Hits = rec.h;
model.Doubles = rec.doubles;
model.Triples = rec.triples;
model.Hr = rec.hr;
model.Rbi = rec.rbi;
model.Sb = rec.sb;
model.Cs = rec.cs;
model.Bb = rec.bb;
model.K = rec.so;
model.Ibb = rec.ibb;
model.Hbp = rec.hbp;
model.Sh = rec.sh;
model.Sf = rec.sf;
model.Gidp = rec.gidp;
}
else
{
throw new System.ArgumentNullException();
}
return rec;
}
public virtual BattingStatsModel ConvertToViewModel(BattingStat record)
{
var model = new BattingStatsModel();
if (record != null)
{
// Setting properties
model.Id = record.lahmanID;
model.Year = record.yearID;
model.Stint = record.stint;
model.TeamId = record.teamID;
model.LeagueId = record.lgID;
model.Games = record.g;
model.BattingGames = record.g_batting;
model.AtBats = record.ab;
model.Runs = record.r;
model.Hits = record.h;
model.Doubles = record.doubles;
model.Triples = record.triples;
model.Hr = record.hr;
model.Rbi = record.rbi;
model.Sb = record.sb;
model.Cs = record.cs;
model.Bb = record.bb;
model.K = record.so;
model.Ibb = record.ibb;
model.Hbp = record.hbp;
model.Sh = record.sh;
model.Sf = record.sf;
model.Gidp = record.gidp;
}
else
{
throw new System.ArgumentNullException();
}
return model;
}
private void UpdateEfModel(BattingStat rec, BattingStatsModel model)
{
if (rec != null && model != null)
{
// Setting properties
model.Id = rec.lahmanID;
model.Year = rec.yearID;
model.Stint = rec.stint;
model.TeamId = rec.teamID;
model.LeagueId = rec.lgID;
model.Games = rec.g;
model.BattingGames = rec.g_batting;
model.AtBats = rec.ab;
model.Runs = rec.r;
model.Hits = rec.h;
model.Doubles = rec.doubles;
model.Triples = rec.triples;
model.Hr = rec.hr;
model.Rbi = rec.rbi;
model.Sb = rec.sb;
model.Cs = rec.cs;
model.Bb = rec.bb;
model.K = rec.so;
model.Ibb = rec.ibb;
model.Hbp = rec.hbp;
model.Sh = rec.sh;
model.Sf = rec.sf;
model.Gidp = rec.gidp;
}
else
{
throw new System.ArgumentNullException();
}
}
private BattingStat GetRecord(int id)
{
if (id > 1)
{
var rec = this.Get(id);
if (rec != null)
{
return rec;
}
else
{
throw new System.ArgumentNullException();
}
}
else
{
throw new System.ArgumentOutOfRangeException();
}
}
}
}
view raw gistfile1.cs hosted with ❤ by GitHub
Summary

Now we have the domain/service layer classes to go with the models we created in Part 2. The C# classes have methods to Get, Add, Update, Delete records that map to the models we’ve created.  These classes also have methods to map between the EF models and our models.  Generating these two layers of code and adding them to our MVC projects gives us the potential to have basic application up and running quickly.

What’s next with this project?  On the System.Emit portion of the project I will be adding the ability to store data from one model class into multiple tables, add attributes to the properties, and a way to generate models from non-database data sources such as flat files, URLs and HL7 messages.  I will continue to generate the models using the System.Emit process.  I will be changing my approach on how the source code is generated to use either Ruby on Rails’ ERB view engine or the new Razor view engine in ASP.NET MVC.  I believe this approach will make it easier for us to make changes to the process and makes it easier to maintain in the long run. I will be adding the ability to generate unit tests, controller class boiler plates and perhaps HTML views as well.

I enjoyed working on this series and I hope you enjoyed it! 

No comments:

Post a Comment