I’ve been using Paul Wilson‘s excellent O/R mapper for a while now, and I’ve become so enamored by it that every project I’ve worked on in the past six months have used it. (All projects are web sites.) Wilson’s O/R Mapper (WORM for short), combined with Paul Welter‘s excellent CodeSmith templates, saved me many hours of coding and provided excellent functionality — well worth the $50 (for WORM) and $99 (for CodeSmith).
One thing that I often wanted to break away from is the ActiveRecord model used in Welter’s templates — not because it’s wrong (ActiveRecord is a very solid, easy-to-use pattern), but because I wanted more. Specifically, I wanted to separate my business objects from my data access code.
Over the past two weeks, while working on the next version of my online baseball game, CSFBL, I started hacking apart the code generated by Welter’s templates, to the point where I separated the data classes from the data access code. I then upgraded to CodeSmith 4.0 Professional and rewrote the templates to generate code based on my “new” way of doing things.
As of now, the templates generate code that compiles and does what it supposed to — however, before releasing it I want to test it a little more and perhaps write a few unit tests. Maybe even redo Welter’s unit test generator, too, to work with the modified templates.
These modified CodeSmith templates generate two projects: one for your entity objects, and one for your data access objects. The templates (written using CodeSmith 4.0, but will probably work in 3.2 — I’ll test it before publishing) are for .Net 2.0 only, as they make heavy use of generics.
How the Entity layer works
Each table gets a class using the same partial class and generated file technique we know and love. Each of these classes inherit from one or more of the following classes: EntityBase
is the base class for all entity objects. Its main purpose is in exposing three interfaces, IComparable, IComparable , and IEquatable , and defining methods where appropriate. The goal of an EntityBase is to provide a few core features, chief among them being an implementation of CompareTo() and Equals() methods that check for equality based on primary keys. The base provides plumbing for the generated classes to do this reliably.
inherits from EntityBase and implements the IPersistable interface. Essentially, a read-only table (as defined in the Mappings.config) would inherit from EntityBase, and all other objects would inherit from PersistableEntity. The IPersistable interface defines methods to track the state (“dirty” and “deleted”) of the object, and nothing more. ** Note that the implementation of this isn’t fully done in that the states aren’t used effectively yet. This was a last-minute change I made so it’s not complete. **
The actual generated class would inherit from either EntityBase or PersistableEntity. Implementing the IObjectHelper interface is an option in the CodeSmith template (if you don’t implement it, your entity library will not need to reference WORM at all). The generated code creates fields and properties as you would expect. It also does the following:
- Creates a strongly-typed CompareTo() method that will compare every key field.
- Creates a strongly-typed Equals() method that will compare every key field.
- Overrides GetHashCode() to generate a hash code based on primary key fields (currently it’ll only work with numeric fields, but that’ll be fixed soon).
- Overrides ToString() to generate output that presents each primary key separated by a colon.
Finally, the generated code has a second class in it that inherits from a new IIdentity interface. This simple class is used by the Data Access classes for “RetrieveByKey” and “DeleteByKey” methods (see below for details).
How the Data Acess layer works
The DAL is not as much a DAL as it is a gateway between the entities and the O/R mapper, but since I’m no master of the right architecture pattern names to describe a situation, I’ll leave it named as such. After code generation this project will have five files.
- A reference to the mappings file (i.e. Mappings.config). Note that the DAL needs to have this file embedded, not the entity layer.
- A TransactionManager static class, which is basically a stripped-down version of the similar-named class from Welter’s templates. (I just removed what wouldn’t work, not much else changed in this.)
- An updated DataManager static class. This expands on what was in Welter’s templates.
- It allows you to define a providerName in your connectionStrings section that will automatically define the appropriate ObjectSpace, including support for the .Net built-in SqlClient, Odbc, OleDb, and OracleClient, as well as the WilsonXmlDbClient (via a custom provider). Note that I’ve only tested the SqlClient and the WilsonXmlDbClient.
- It allows you to automatically integrate NLog as an interceptor to trace the SQL generated by WORM. The option to do this is in the template. It uses some various techniques I found around the web.
- It implements a technique to cache the IsolatedContext in web environments by using HttpContext (a technique illustrated by David Neal at www.ChristianASP.NET.
Previously I mentioned the IIdentity interface. This is used by the RetrieveByKey and DeleteByKey methods and allows us to maintain generic data access classes while being able to use strongly-typed keys as well.
An example of how the data layer works is shown below. The examples assume entity classes exists called Teacher, whose primary key is TeacherID.
//retrieve a Teacher Teacher teacher = Retrieve<teacher>.RetrieveFirst(); //retrieve all Teachers IList</teacher><teacher> teachers = Retrieve</teacher><teacher>.RetrieveAll(); //retrieve a teacher whose primary key is 74 TeacherIdentity tid = new TeacherIdentity(74); Teacher teacher = Retrieve</teacher><teacher>.RetrieveByKey(tid); //you could also write the above in one line: Teacher teacher = Retrieve</teacher><teacher>.RetrieveByKey(new TeacherIdentity(74)); //saving changes to a teacher teacher.Name = "Dr. Spock"; Persist</teacher><teacher>.Save(teacher); //deleting a teacher Persist</teacher><teacher>.Delete(teacher); //deleting a teacher whose primary key is 74 Persist</teacher><teacher>.DeleteByKey(new TeacherIdentity(74)); //creating a new teacher record will not be tracked by WORM //this is perfectly valid Teacher teacher = new Teacher(); //you can start tracking by calling the Track() method Persist</teacher><teacher>.Track(teacher); //you can also instantiate a new teacher with tracking Teacher teacher = Persist</teacher><teacher>.New();
The unit tests I wrote pretty much did what you see above.
I will hopefully release the actual templates in a few days, once I do some more testing on them and clean up some of the output formatting a bit. In the meantime, feel free to let me know what you think!