M
Mike Hofer
I have a large .NET 1.1 application in which I have a discovered a need
to implement the functionality of partial classes. (To make a long
story short, a custom tool generates DAC classes, but I often want to
add functionality to those classes without losing those changes when
the classes are regenerated.)
So I find myself trying to figure out how to do this properly, and with
minimal impact on my existing code base. I think I have a plan, but I
would like your comments and suggestions for improvement or
alternatives.
Note that the tool in question was developed inhouse, so we can change
it as needed.
THE CURRENT SITUATION. The tool in question is responsible for
generating classes that exactly match the schema of the database. (The
tool is limited to SQL Server.)
For every table and view, a type safe class is created that contains
one property for every column. (For simplicity, I call this the model
class.) The model class's properties contain code to check for
ranges, null values, and so forth; also, a new instance of the class
initializes all properties to their default values as specified in the
database (that way, when you instantiate a new instance, it has all the
appropriate default values). Note that the model class is /not/ a
dataset. There should be /no/ direct connections between the model
class and any object in the System.Data.* namespaces; we want a full
layer of abstraction between the model and the database (that layer is
provided by the controller class).
Below, you can see an example of a simple table in our application, and
the class that the tool generates.
----------------------------------------------------------------------
Table:
+-----------------------------+
| <<Table>> |
| RaceList |
+-----------------------------+
| PK | ID | int (identity) |
| | Text | varchar(255) |
+-----------------------------+
Class:
Option Strict On
Public Class Race
Public Sub New()
End Sub
Private m_isDirty As Boolean = False
Public Property IsDirty() As Boolean
Get
Return m_isDirty
End Get
Set(ByVal value As Boolean)
m_isDirty = Value
End Set
End Property
Private m_ID As Integer = 0
Public Property ID() As Integer
Get
Return m_ID
End Get
Set(ByVal value As Integer)
If m_ID <> Value Then
m_ID = value
m_isDirty = True
End If
End Set
End Property
Private m_Text As String = String.Empty
Public Property Text() As String
Get
Return m_Text
End Get
Set(ByVal value As String)
If m_Text <> Value Then
If Not value Is Nothing AndAlso value.Length > 255 Then
Throw New ArgumentException("String too long")
End If
m_Text = value
m_isDirty = True
End If
End Set
End Property
End Class ' Race
(Note that the tool has built-in logic for identifying lookup tables,
so it mangled the name on this class, but you get the point.)
The tool also generates a type safe collection of model classes, and a
controller class that contains Exists, Delete, Load, and Save methods
for the model class. The controller class encapsulates all interaction
with the database, and works with instances of the model class. (The
controller class is permitted to reference the System.Data.*
namespaces.)
Finally, the tool generates a separate class for each stored procedure
in the database. For every parameter, there is a corresponding property
of the appropriate type. (Believe it or not, this was the original
reason the tool was created. For some reason, ADO.NET didn't include
type safe wrappers for stored procedures, and inlined all the SQL.)
This allows us to write code like this:
Dim proc As New InsertEmployeeProcedure
proc.FirstName = "John"
proc.LastName = "Doe"
proc.EmployeeNumber = 12795374
proc.ExecuteNonQuery()
THE PROBLEM. This scenario works, and usually works /very/ well.
However, there have been a few times when I wanted to add functionality
to the model or controller class, but couldn't because the tool would
have regenerated the classes and discarded my additions. So it occurs
to me that I need to be able to separate custom code from generated
code.
I considered using a region in the source code, but dropped the idea
because it would have added the inherent complexities of parsing the
source file and nested regions, and so forth and so on.
The perfect solution is partial classes. But alas, they aren't
available until 2.0, and the client isn't going to be an early
adopter. So I have to devise a solution that uses 1.1 technologies, and
do it in such a way that I don't break the code or make coding any
more complex than it already is.
THE PROPOSED SOLUTION. I want to emulate partial classes by putting all
the generated code in a base class, and creating "empty" derived
classes.
Model Classes: We will create two separate classes: the model base
class, and the actual model class. The model base class will look
exactly like the model class does now, except for the constructors. The
public constructor will be replaced by a protected friend constructor.
This will prevent the class from being inherited by anyone outside of
the current assembly. The new model class will inherit from the model
base class, and will initially include no additional functionality.
However, when you need to add functionality to the model, you add it to
THIS class.
Collection Classes: The collection classes will remain type safe, but
they will take members of the new model class, and not of the base
class.
Controller Classes: The tool will create two separate classes: the
controller base class, and the actual controller class. The controller
base class will look exactly as it does now, but will contain only one
constructor (with protected friend access).
The resulting classes would look like this:
+------------------------------+ +-------+
+ RaceModel | + Race |
+------------------------------+ <-- +-------+
+------------------------------+ +-------+
| ~ New | + + New |
| + <<property>> ID : Integer | +-------+
| + <<property>> Text : String |
+------------------------------+
+----------------------------+
+ RaceCollection |
+----------------------------+
+ + Count : Integer +
+ + Item(Index) : Race |
+----------------------------+
+ + New() |
+ + Add(Race) : Integer |
+ + Contains(Race) : Boolean |
+ + IndexOf(Race) : Integer |
+ + Insert(Integer, Race) |
+ + Remove(Race) |
+----------------------------+
+--------------------------------+ +----------------+
| RaceControllerBase | | RaceController |
+--------------------------------+ +----------------+
+--------------------------------+ +----------------+
| ~ New | <-- | + New() |
| + Exists(Integer) : Boolean | | + Delete(Race) |
| ~ Delete(Race, SqlTransaction) | | + Save(Race) |
| ~ Insert(Race, SqlTransaction) | +----------------+
| + Load(Integer) : Race |
| ~ Update(Race) |
| ~ Update(Race, SqlTransaction) |
+--------------------------------+
(I used the tilde [~] to indicate friend methods.) Each class would
reside in a separate file. Any custom functionality would be placed on
the Race or RaceController class; the RaceModel and RaceControllerBase
classes will be overwritten by the tool as requested by the user.
When the tool generates classes, it will always generate the base
classes, but will generate the derived classes only if they do not
already exist. In that way, you can have the tool generate your base
classes, where the classes /must/ always match the database schema, and
retain any additional code you've added to the derived model and
controller. This allows the tool to keep the classes in synch with the
schema, while retaining your custom code.
SUMMARY. I need a solution that will help me to separate my generated
database code from my custom database code. The solution has to be
relatively simple to understand, and must not add an inordinate amount
of complexity to the system. Finally, the solution must have minimal
impact on the body of the code that currently uses these components. I
/think/ this solution solves that problem, but I'm sure it could use
improvement.
I look forward to your comments and suggestions.
to implement the functionality of partial classes. (To make a long
story short, a custom tool generates DAC classes, but I often want to
add functionality to those classes without losing those changes when
the classes are regenerated.)
So I find myself trying to figure out how to do this properly, and with
minimal impact on my existing code base. I think I have a plan, but I
would like your comments and suggestions for improvement or
alternatives.
Note that the tool in question was developed inhouse, so we can change
it as needed.
THE CURRENT SITUATION. The tool in question is responsible for
generating classes that exactly match the schema of the database. (The
tool is limited to SQL Server.)
For every table and view, a type safe class is created that contains
one property for every column. (For simplicity, I call this the model
class.) The model class's properties contain code to check for
ranges, null values, and so forth; also, a new instance of the class
initializes all properties to their default values as specified in the
database (that way, when you instantiate a new instance, it has all the
appropriate default values). Note that the model class is /not/ a
dataset. There should be /no/ direct connections between the model
class and any object in the System.Data.* namespaces; we want a full
layer of abstraction between the model and the database (that layer is
provided by the controller class).
Below, you can see an example of a simple table in our application, and
the class that the tool generates.
----------------------------------------------------------------------
Table:
+-----------------------------+
| <<Table>> |
| RaceList |
+-----------------------------+
| PK | ID | int (identity) |
| | Text | varchar(255) |
+-----------------------------+
Class:
Option Strict On
Public Class Race
Public Sub New()
End Sub
Private m_isDirty As Boolean = False
Public Property IsDirty() As Boolean
Get
Return m_isDirty
End Get
Set(ByVal value As Boolean)
m_isDirty = Value
End Set
End Property
Private m_ID As Integer = 0
Public Property ID() As Integer
Get
Return m_ID
End Get
Set(ByVal value As Integer)
If m_ID <> Value Then
m_ID = value
m_isDirty = True
End If
End Set
End Property
Private m_Text As String = String.Empty
Public Property Text() As String
Get
Return m_Text
End Get
Set(ByVal value As String)
If m_Text <> Value Then
If Not value Is Nothing AndAlso value.Length > 255 Then
Throw New ArgumentException("String too long")
End If
m_Text = value
m_isDirty = True
End If
End Set
End Property
End Class ' Race
(Note that the tool has built-in logic for identifying lookup tables,
so it mangled the name on this class, but you get the point.)
The tool also generates a type safe collection of model classes, and a
controller class that contains Exists, Delete, Load, and Save methods
for the model class. The controller class encapsulates all interaction
with the database, and works with instances of the model class. (The
controller class is permitted to reference the System.Data.*
namespaces.)
Finally, the tool generates a separate class for each stored procedure
in the database. For every parameter, there is a corresponding property
of the appropriate type. (Believe it or not, this was the original
reason the tool was created. For some reason, ADO.NET didn't include
type safe wrappers for stored procedures, and inlined all the SQL.)
This allows us to write code like this:
Dim proc As New InsertEmployeeProcedure
proc.FirstName = "John"
proc.LastName = "Doe"
proc.EmployeeNumber = 12795374
proc.ExecuteNonQuery()
THE PROBLEM. This scenario works, and usually works /very/ well.
However, there have been a few times when I wanted to add functionality
to the model or controller class, but couldn't because the tool would
have regenerated the classes and discarded my additions. So it occurs
to me that I need to be able to separate custom code from generated
code.
I considered using a region in the source code, but dropped the idea
because it would have added the inherent complexities of parsing the
source file and nested regions, and so forth and so on.
The perfect solution is partial classes. But alas, they aren't
available until 2.0, and the client isn't going to be an early
adopter. So I have to devise a solution that uses 1.1 technologies, and
do it in such a way that I don't break the code or make coding any
more complex than it already is.
THE PROPOSED SOLUTION. I want to emulate partial classes by putting all
the generated code in a base class, and creating "empty" derived
classes.
Model Classes: We will create two separate classes: the model base
class, and the actual model class. The model base class will look
exactly like the model class does now, except for the constructors. The
public constructor will be replaced by a protected friend constructor.
This will prevent the class from being inherited by anyone outside of
the current assembly. The new model class will inherit from the model
base class, and will initially include no additional functionality.
However, when you need to add functionality to the model, you add it to
THIS class.
Collection Classes: The collection classes will remain type safe, but
they will take members of the new model class, and not of the base
class.
Controller Classes: The tool will create two separate classes: the
controller base class, and the actual controller class. The controller
base class will look exactly as it does now, but will contain only one
constructor (with protected friend access).
The resulting classes would look like this:
+------------------------------+ +-------+
+ RaceModel | + Race |
+------------------------------+ <-- +-------+
+------------------------------+ +-------+
| ~ New | + + New |
| + <<property>> ID : Integer | +-------+
| + <<property>> Text : String |
+------------------------------+
+----------------------------+
+ RaceCollection |
+----------------------------+
+ + Count : Integer +
+ + Item(Index) : Race |
+----------------------------+
+ + New() |
+ + Add(Race) : Integer |
+ + Contains(Race) : Boolean |
+ + IndexOf(Race) : Integer |
+ + Insert(Integer, Race) |
+ + Remove(Race) |
+----------------------------+
+--------------------------------+ +----------------+
| RaceControllerBase | | RaceController |
+--------------------------------+ +----------------+
+--------------------------------+ +----------------+
| ~ New | <-- | + New() |
| + Exists(Integer) : Boolean | | + Delete(Race) |
| ~ Delete(Race, SqlTransaction) | | + Save(Race) |
| ~ Insert(Race, SqlTransaction) | +----------------+
| + Load(Integer) : Race |
| ~ Update(Race) |
| ~ Update(Race, SqlTransaction) |
+--------------------------------+
(I used the tilde [~] to indicate friend methods.) Each class would
reside in a separate file. Any custom functionality would be placed on
the Race or RaceController class; the RaceModel and RaceControllerBase
classes will be overwritten by the tool as requested by the user.
When the tool generates classes, it will always generate the base
classes, but will generate the derived classes only if they do not
already exist. In that way, you can have the tool generate your base
classes, where the classes /must/ always match the database schema, and
retain any additional code you've added to the derived model and
controller. This allows the tool to keep the classes in synch with the
schema, while retaining your custom code.
SUMMARY. I need a solution that will help me to separate my generated
database code from my custom database code. The solution has to be
relatively simple to understand, and must not add an inordinate amount
of complexity to the system. Finally, the solution must have minimal
impact on the body of the code that currently uses these components. I
/think/ this solution solves that problem, but I'm sure it could use
improvement.
I look forward to your comments and suggestions.