Click here to Skip to main content
15,880,543 members
Articles / Programming Languages / C#
Article

Object Mapping Part II - Schema Code Generator

,
Rate me:
Please Sign up or sign in to vote.
4.75/5 (12 votes)
17 Dec 20067 min read 85.1K   426   68   19
Generate an object mapping class from a schema definition.

The schema generator and ViewRecord class were written by Justin Dunlap, and the article and unit tests by Marc Clifton.

Introduction

In Part I, I described the ViewRecord class, a base class for the code generated classes that implements a row cursor to be used by the application in conjunction with the object-mapping (OM) class. This article describes what the code generator does and the required object graph to represent the schema.

The motivation for this code, which Justin Dunlap wrote, is so that I can write client and server-side business rules that look like:

employee.LastName="Clifton";

rather than:

dataViewEmp[0]["LastName"]="Clifton";

However, since my client/server architecture handles the table relationships for me, I wanted something that wasn't fully object relational mapping (ORM), but rather just object-mapping, where a class is generated that maps to a virtualized view of the database schema. The virtualized view is an abstracted collection of tables and fields, including view-only, or calculated, fields. The tradeoff, of course, is that the business rule still has to handle relationships in a more raw, manual way than would be necessary in a true ORM environment. For example, if I have two tables, employee and company, I still have to write:

C#
employee.CompanyID=company.ID;

to establish the relationship, rather than something more "natural", such as:

C#
company.Employees.Add(employee);

The former case is simple enough to work with, and dealing with foreign keys in a more natural way never made it into the requirements.

Requirements

What did make it into the requirements were the following:

  • Utilize nullable types for value types for fields that are nullable.
  • Automatically convert between DBNull.Value and "null" when moving data between the OM instance and the underlying row.
  • The nullable type should not be exposed to the application--meaning, the internal fields can be nullable, but the property exposing those fields is never a nullable type. Instead, the generated code implements explicit methods for setting a field value to null and querying whether a field value is null. This was somewhat of an arbitrary requirement.
  • Perform simple validation--whether the field allows null and min/max value range checking. This may be expanded in the future.
  • Support common SQL data types and map them appropriately to C# types (not all SQL types are supported):
    • varchar <=> string
    • integer <=> int
    • real <=> double
    • money <=> decimal
    • bit <=> bool
    • datetime <=> datetime
    • uniqueidentifier <=> guid
  • Rather than using one of the free or commercial template-based generators, I wanted to utilize CodeDom to simplify the code generation process by not relying on a tool that possibly did both more and at the same time less than what I wanted. I also wanted the code generator embedded in the schema designer that I use. By using CodeDom, I can emit the generated code in any supported .NET language. Woohoo. I admit that I'm biased against template based code generators and didn't even bother to look to see if there was something that could handle the different requirements.
  • Implement property change events.

The View Schema Object Graph

The object graph of the view schema that gets passed to the code generator is a collection of classes that must implement some minimal interface properties and methods. While I typically instantiate the object graph from XML, you may choose to do something different. The only requirement is that the object graph must consist of classes implementing these interfaces. Some of the setters aren't necessary--they are included here so that the object graph can be easily deserialized.

C#
using System;
using System.Collections.Generic;

namespace Interacx.Dev
{
  /// <summary>
  /// The view schema is the root instance, consisting of a collection of 
  /// fields.
  /// A view schema is a collection of view fields that map to one or more 
  /// table fields
  /// across one or more tables.
  /// </summary>
  public interface IViewSchema
  {
    string Name { get; set;}
    Dictionary<string, IViewField> Fields { get;}

    IViewTable GetViewTable(string tableName);
  }

  /// <summary>
  /// A view field points to a IViewTable instance, which manages properties 
  /// specific
  /// to the view of a table.
  /// </summary>
  public interface IViewTable
  {
    bool ReadOnly {get; set;}
  }

  /// <summary>
  /// The view field maps to a table field.
  /// </summary>
  public interface IViewField
  {
    string Name { get; set;}
    ITableField TableField { get; set;}
  }

  /// <summary>
  /// A calculated view field is a field specific to the view that does not
  /// map to an underlying table.
  /// </summary>
  public interface ICalcViewField : IViewField
  {
    FieldType DataType { get; set;}
    Type DataTypeAsType { get;}
  }

  /// <summary>
  /// The table field contains information relevant to the way the field is
  /// configured in the database. It also points to the parent table.
  /// </summary>
  public interface ITableField
  {
    string Name { get; set;}
    Type DataTypeAsType { get;}
    FieldType DataType { get; set;}
    bool AllowNull { get; set;}
    ITable Table {get; set;}
    string MinValue { get; set;}
    string MaxValue { get; set;}
  }

  /// <summary>
  /// The table that maps directly to the database table.
  /// </summary>
  public interface ITable
  {
    string Name { get; set;}
  }
}

I have included mock classes for these interfaces in the unit tests for the code generator, and will describe them next.

Mock Schema Objects

Walking through the mock schema objects is a good way of understanding what each of them does.

MockViewSchema

C#
public class MockViewSchema : IViewSchema
{
  protected string name;
  protected Dictionary<string, IViewField> fields;
  protected Dictionary<string, IViewTable> viewTables;

  /// <summary>
  /// Gets/sets Name
  /// </summary>
  public string Name
  {
    get { return name; }
    set { name = value; }
  }

  public Dictionary<string, IViewField> Fields
  {
    get { return fields; }
  }

  public MockViewSchema(string name)
  {
    this.name = name;
    Initialize();
  }

  public void AddViewTable(ITable table, bool readOnly)
  {
    viewTables.Add(table.Name, new MockViewTable(readOnly));
  }

  public void AddViewField(IViewField viewField)
  {
    fields.Add(viewField.Name, viewField);
  }

  public IViewTable GetViewTable(string tableName)
  {
    return viewTables[tableName];
  }

  protected void Initialize()
  {
    fields = new Dictionary<string, IViewField>();
    viewTables = new Dictionary<string, IViewTable>();
  }
}

As the code illustrates, the minimal view schema must maintain a collection of view fields and a collection of view tables. Ideally, your view fields would reference their containing view table, but this isn't actually necessary in the minimal implementation. The only reason we maintain this collection is to determine if the view table is read-only, which is a completely independent information from the schema.

The ReadOnly Property

A read-only view table will only generate getter properties in the generated code. There must be a 1::1 relationship between the MockTable and the MockViewTable, and the names must be identical, given how the schema code generator looks up the MockViewTable instance. (This is my fault, not Justin's, as he was coding to an already implemented object model).

MockViewTable

C#
public class MockViewTable : IViewTable
{
  protected bool readOnly;

  /// <summary>
  /// Gets/sets readOnly
  /// </summary>
  public bool ReadOnly
  {
    get { return readOnly; }
    set { readOnly = value; }
  }

  public MockViewTable(bool readOnly)
  {
    this.readOnly = readOnly;
  }
}

This class simply encapsulates the ReadOnly property, as described above.

MockViewField

C#
public class MockViewField : IViewField
{
  protected string name;
  protected ITableField tableField;

  /// <summary>
  /// Gets/sets name
  /// </summary>
  public string Name
  {
    get { return name; }
    set { name = value; }
  }

  public ITableField TableField
  {
    get { return tableField; }
    set { tableField = value; }
  }

  public MockViewField(string name, ITableField tableField)
  {
    this.name = name;
    this.tableField = tableField;
  }
}

A MockViewField instance encapsulates the reference to the actual table field, and also provides a mechanism to alias the table field's name via the Name property. The ability to alias the table field name is important when dealing with multiple tables that have common field names.

MockCalcViewField

C#
public class MockCalcViewField : MockViewField
{
  protected FieldType dataType;
  protected Type type;

  /// <summary>
  /// Gets/sets dataType
  /// </summary>
  public FieldType DataType
  {
    get { return dataType; }
    set
    {
      dataType = value;
      type = Helpers.DetermineType(dataType);
    }
  }

  public Type DataTypeAsType
  {
    get { return type; }
  }

  public MockCalcViewField(string name, FieldType dataType)
    : base(name, null)
  {
    DataType = dataType;
  }
}

The virtual view includes the ability to create calculated view fields. These are view fields that the application might want for internal use but are not represented in the table schema. These fields are expected, however, to have corresponding columns in the DataView instance that is associated with the record. However, in my client/server application, they are not persisted to the database.

MockTableField

C#
public class MockTableField : ITableField
{
  protected string name;
  protected FieldType dataType;
  protected bool allowNull;
  protected ITable table;
  protected string minValue;
  protected string maxValue;
  protected Type dataTypeAsType;

  /// <summary>
  /// Returns dataTypeAsType
  /// </summary>
  public Type DataTypeAsType
  {
    get { return dataTypeAsType; }
  }

  /// <summary>
  /// Gets/sets maxValue
  /// </summary>
  public string MaxValue
  {
    get { return maxValue; }
    set { maxValue = value; }
  }

  /// <summary>
  /// Gets/sets minValue
  /// </summary>
  public string MinValue
  {
    get { return minValue; }
    set { minValue = value; }
  }

  /// <summary>
  /// Gets/sets table
  /// </summary>
  public ITable Table
  {
    get { return table; }
    set { table = value; }
  }

  /// <summary>
  /// Gets/sets allowNull
  /// </summary>
  public bool AllowNull
  {
    get { return allowNull; }
    set { allowNull = value; }
  }

  /// <summary>
  /// Gets/sets dataType
  /// </summary>
  public FieldType DataType
  {
    get { return dataType; }
    set 
    {
      dataType = value;
      dataTypeAsType = Helpers.DetermineType(dataType);
    }
  }

  /// <summary>
  /// Gets/sets name
  /// </summary>
  public string Name
  {
    get { return name; }
    set { name = value; }
  }

  public MockTableField(string name, FieldType fieldType, bool allowNull, 
        ITable parentTable, string minVal, string maxVal)
  {
    this.name = name;
    DataType = fieldType;
    this.allowNull = allowNull;
    table = parentTable;
    minValue = minVal;
    maxValue = maxVal;
  }
}

The MockTableField is definitely the workhorse of the object graph, as it maintains the data type, nullability, and validation information.

MockTable

C#
public class MockTable : ITable
{
  protected string name;

  /// <summary>
  /// Gets/sets name
  /// </summary>
  public string Name
  {
    get { return name; }
    set { name = value; }
  }

  public MockTable(string name)
  {
    this.name = name;
  }
}

The MockTable isn't actually even necessary, were it not for the two lines in the code generator that acquire the MockViewTable instance using the MockTable name, in order to determine whether the MockViewTable is read only or not. Something to be refactored at some point, I suppose.

An Example

Let's look at a simple example in which an Employee table and view is created. The Employee table consists of the following fields:

  • ID (Guid)
  • LastName (string)
  • FirstName (string)
  • TerminationDate (datetime, nullable)

Initialization of the Object Graph

Using our mock objects, the code to generate the view schema is:

C#
[TestFixture, ProcessTest]
public class CodeGenTests
{
  protected MockViewSchema viewSchema;
  protected SchemaCodeGenerator scg;

  [TestFixtureSetUp]
  public void FixtureSetup()
  {
    viewSchema = new MockViewSchema("Employee");

    // Create the mock table.
    MockTable tableEmployee=new MockTable("Employee");

    // Create the mock table fields.
    MockTableField empID=new MockTableField("ID", FieldType.Guid, false, 
      tableEmployee, null, null);
    MockTableField empFirstName=new MockTableField("FirstName", 
      FieldType.String, false, tableEmployee, null, null);
    MockTableField empLastName = new MockTableField("LastName", 
      FieldType.String, false, tableEmployee, null, null);
    MockTableField empTermDate = new MockTableField("TerminationDate", 
      FieldType.Date, true, tableEmployee, null, null);

    // Add a mock view for this table.
    viewSchema.AddViewTable(tableEmployee, false);

    // Add the mock view fields for this view.
    viewSchema.AddViewField(new MockViewField("ID", empID));
    viewSchema.AddViewField(new MockViewField("FirstName", empFirstName));
    viewSchema.AddViewField(new MockViewField("LastName", empLastName));
    viewSchema.AddViewField(new MockViewField("TerminationDate", empTermDate));

    // Instantiate the schema code generator.
    scg = new SchemaCodeGenerator();
  }

Calling the Code Generator

Followed by a test case that, umm, just emits the code to the debug window.

C#
[Test, Sequence(0)]
public void BasicTest()
{
  StringBuilder sb = new StringBuilder();
  StringWriter sw = new StringWriter(sb);
  scg.GenerateRecordClass(viewSchema, "Interacx.GeneratedCode", 
     CodeDomProvider.CreateProvider("CSharp"), sw);
  System.Diagnostics.Debug.WriteLine(sb.ToString());
  // It worked if it didn't throw an exception.
}

And the Resulting Generated Code

There are several sections to the generated code:

  • The constructor
  • The field list
  • The field accessors (properties)
  • The event execution methods
  • The overrides to the three abstract methods in the ViewRecord base class

The result is a class specific to the view schema whereas the base class handles the common navigation and row cursor management functions.

Here's the complete code that got generated:

C#
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:2.0.50727.42
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

using Interacx;
using Interacx.Dev;
using System;
using System.Data;


namespace Interacx.GeneratedCode
{
  public class EmployeeRecord : ViewRecord
  {
    public EmployeeRecord(System.Data.DataView dataView) : 
      base(dataView)
    {
    }

    private Guid iD;
    private String firstName;
    private String lastName;
    private System.Nullable<Date> terminationDate;

    public virtual System.Guid ID
    {
      get
      {
        return this.iD;
      }
      set
      {
          if ((value == Guid.Empty))
        {
          throw new ArgumentException("value");
        }
        if ((this.iD == value))
        {
          return;
        }
        this.iD = value;
        this.OnIDChanged(EventArgs.Empty);
      }
    }

    public virtual string FirstName
    {
      get
      {
        if ((this.firstName != null))
        {
          return this.firstName;
        }
        else
        {
          throw new InvalidFieldStateException("FirstName");
        }
      }
      set
      {
        if ((value == null))
        {
          throw new ArgumentNullException("value");
        }
        if ((this.firstName == value))
        {
          return;
        }
        this.firstName = value;
        this.OnFirstNameChanged(EventArgs.Empty);
      }
    }

    public virtual string LastName
    {
      get
      {
        if ((this.lastName != null))
        {
          return this.lastName;
        }
        else
        {
          throw new InvalidFieldStateException("LastName");
        }
      }
      set
      {
        if ((value == null))
        {
          throw new ArgumentNullException("value");
        }
        if ((this.lastName == value))
        {
          return;
        }
        this.lastName = value;
        this.OnLastNameChanged(EventArgs.Empty);
      }
    }

    public virtual System.DateTime TerminationDate
    {
      get
      {
        if (this.terminationDate.HasValue)
        {
          return this.terminationDate.Value;
        }
        else
        {
          throw new InvalidFieldStateException("TerminationDate");
        }
      }
      set
      {
        if ((this.terminationDate.HasValue 
          && (this.terminationDate == value)))
        {
          return;
        }
        this.terminationDate = value;
        this.OnTerminationDateChanged(EventArgs.Empty);
      }
    }

    public virtual bool IsTerminationDateNull()
    {
      return (this.terminationDate.HasValue == false);
    }

    public virtual void SetTerminationDateNull()
    {
      this.terminationDate = null;
    }

    public event System.EventHandler IDChanged;

    /// <summary>
    /// Raises the <see cref="IDChanged" /> event.
    /// </summary>
    /// <param name="e">The value passed for the event's e parameter.</param>
    protected virtual void OnIDChanged(System.EventArgs e)
    {
      if ((this.IDChanged != null))
      {
        this.IDChanged(this, e);
      }
    }

    public event System.EventHandler FirstNameChanged;

    /// <summary>
    /// Raises the <see cref="FirstNameChanged" /> event.
    /// </summary>
    /// <param name="e">The value passed for the event's e parameter.</param>
    protected virtual void OnFirstNameChanged(System.EventArgs e)
    {
      if ((this.FirstNameChanged != null))
      {
        this.FirstNameChanged(this, e);
      }
    }

    public event System.EventHandler LastNameChanged;

    /// <summary>
    /// Raises the <see cref="LastNameChanged" /> event.
    /// </summary>
    /// <param name="e">The value passed for the event's e parameter.</param>
    protected virtual void OnLastNameChanged(System.EventArgs e)
    {
      if ((this.LastNameChanged != null))
      {
        this.LastNameChanged(this, e);
      }
    }

    public event System.EventHandler TerminationDateChanged;

    /// <summary>
    /// Raises the <see cref="TerminationDateChanged" /> event.
    /// </summary>
    /// <param name="e">The value passed for the event's e parameter.</param>
    protected virtual void OnTerminationDateChanged(System.EventArgs e)
    {
      if ((this.TerminationDateChanged != null))
      {
        this.TerminationDateChanged(this, e);
      }
    }

    public virtual EmployeeRecord NewRecord()
    {
      return new EmployeeRecord(this.dataView);
    }

    protected override void ValidateAndCommitFields()
    {
      this.currentRow["ID"] = this.iD;
      if ((this.firstName == null))
      {
        throw new NoNullAllowedException("FirstName");
      }
      this.currentRow["FirstName"] = this.firstName;
      if ((this.lastName == null))
      {
        throw new NoNullAllowedException("LastName");
      }
      this.currentRow["LastName"] = this.lastName;
      if (this.terminationDate.HasValue)
      {
        this.currentRow["TerminationDate"] = this.terminationDate.Value;
      }
      else
      {
        this.currentRow["TerminationDate"] = DBNull.Value;
      }
    }

    protected override void LoadAllFields()
    {
      this.iD = ((System.Guid)(this.ConvertIfDBNull(this.currentRow["ID"])));
      this.firstName = 
          ((string)(this.ConvertIfDBNull(this.currentRow["FirstName"])));
      this.lastName = 
          ((string)(this.ConvertIfDBNull(this.currentRow["LastName"])));
      this.terminationDate = 
          ((Nullable<DateTime>)(this.ConvertIfDBNull(
              this.currentRow["TerminationDate"])));
      }

    protected override void LoadField(string fieldName)
    {
      if ((fieldName == "ID"))
      {
        this.iD = ((System.Guid)(this.ConvertIfDBNull(this.currentRow["ID"])));
      }
      if ((fieldName == "FirstName"))
      {
        this.firstName = ((string)(this.ConvertIfDBNull(
             this.currentRow["FirstName"])));
      }
      if ((fieldName == "LastName"))
      {
        this.lastName = ((string)(this.ConvertIfDBNull(
             this.currentRow["LastName"])));
      }
      if ((fieldName == "TerminationDate"))
      {
        this.terminationDate = ((Nullable<DateTime>)(
             this.ConvertIfDBNull(this.currentRow["TerminationDate"])));
      }
    }
  }
}

Possible Enhancements

Here are some ideas I have:

  • As I was writing this, it occurred to me that it would be possible to deal with the foreign key ID problem in a more natural way if the code generator assigns the ID, given an assignment like this:
  • C#
    employee.Company=company;
  • As mentioned earlier, validation might be extended to include things like regex.
  • Default values might be interesting to add.
  • Support for additional SQL types (should be easy enough for value types, by extending the enumeration and adding the correct conversion to the C# type in the Helpers class). Dealing with types like image, xml, and blob's might be more complicated.

For now though, I'm quite happy with how the code generator works, and prefer to actually accrue some use-case time before deciding whether and how it needs to be improved.

Conclusion

As I've mentioned before (probably to the point of annoyance), I don't totally buy into the idea of ORM because I feel that the server should generate the SQL on the fly based on the current schema. If I cut out the ORM, I can modify the schema and reload it without shutting down the server to recompile the code. Nor do I see the place for ORM in a general purpose server architecture. I do, however, see where object mapping is very useful for client and server-side business rules and custom client applications that interface with the server through some client API. The result of the work done by Justin in creating the schema code generator has been invaluable in making it easier to write these business rules and custom client applications.

References

The code generator utilizes the Coding Patterns Library by Omer van Kloeten, described here.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Architect Interacx
United States United States
Blog: https://marcclifton.wordpress.com/
Home Page: http://www.marcclifton.com
Research: http://www.higherorderprogramming.com/
GitHub: https://github.com/cliftonm

All my life I have been passionate about architecture / software design, as this is the cornerstone to a maintainable and extensible application. As such, I have enjoyed exploring some crazy ideas and discovering that they are not so crazy after all. I also love writing about my ideas and seeing the community response. As a consultant, I've enjoyed working in a wide range of industries such as aerospace, boatyard management, remote sensing, emergency services / data management, and casino operations. I've done a variety of pro-bono work non-profit organizations related to nature conservancy, drug recovery and women's health.

Written By
Web Developer
United States United States
My main goal as a developer is to improve the way software is designed, and how it interacts with the user. I like designing software best, but I also like coding and documentation. I especially like to work with user interfaces and graphics.

I have extensive knowledge of the .NET Framework, and like to delve into its internals. I specialize in working with VG.net and MyXaml. I also like to work with ASP.NET, AJAX, and DHTML.

Comments and Discussions

 
QuestionLink to part I broken - find it here; https://www.codeproject.com/Articles/16795/Object-Mapping-Part-I-The-Row-Cursor Pin
klinkenbecker30-Nov-23 1:45
klinkenbecker30-Nov-23 1:45 
QuestionJust another DataSet? Pin
Igor Tkachev10-Jan-07 8:03
Igor Tkachev10-Jan-07 8:03 
AnswerRe: Just another DataSet? Pin
Igor Tkachev10-Jan-07 8:06
Igor Tkachev10-Jan-07 8:06 
GeneralRe: Just another DataSet? Pin
Marc Clifton11-Jan-07 11:33
mvaMarc Clifton11-Jan-07 11:33 
GeneralImportance of O Pin
Fernando Armburu26-Dec-06 2:55
Fernando Armburu26-Dec-06 2:55 
GeneralRe: Importance of O Pin
Marc Clifton26-Dec-06 4:44
mvaMarc Clifton26-Dec-06 4:44 
GeneralRe: Importance of O Pin
Thanks for all the fish31-Aug-07 7:43
Thanks for all the fish31-Aug-07 7:43 
GeneralGood Work Pin
icestatue21-Dec-06 2:09
icestatue21-Dec-06 2:09 
GeneralDesign points Pin
P.J. Tezza18-Dec-06 15:00
P.J. Tezza18-Dec-06 15:00 
GeneralRe: Design points Pin
Marc Clifton19-Dec-06 2:28
mvaMarc Clifton19-Dec-06 2:28 
GeneralRe: Design points Pin
P.J. Tezza19-Dec-06 9:55
P.J. Tezza19-Dec-06 9:55 
GeneralRe: Design points Pin
Marc Clifton26-Dec-06 4:51
mvaMarc Clifton26-Dec-06 4:51 
GeneralRe: Design points Pin
P.J. Tezza26-Dec-06 12:00
P.J. Tezza26-Dec-06 12:00 
GeneralRe: Design points Pin
Marc Clifton26-Dec-06 12:14
mvaMarc Clifton26-Dec-06 12:14 
GeneralRe: Design points Pin
P.J. Tezza26-Dec-06 14:28
P.J. Tezza26-Dec-06 14:28 
QuestionAren't you re-creating typed datasets? Pin
AlexY18-Dec-06 7:04
AlexY18-Dec-06 7:04 
AnswerRe: Aren't you re-creating typed datasets? Pin
Marc Clifton18-Dec-06 8:08
mvaMarc Clifton18-Dec-06 8:08 
GeneralPutting the "R" back in O-M Pin
Philip Laureano17-Dec-06 18:34
Philip Laureano17-Dec-06 18:34 
GeneralRe: Putting the "R" back in O-M Pin
Marc Clifton26-Dec-06 5:05
mvaMarc Clifton26-Dec-06 5:05 

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.