Click here to Skip to main content
11,642,216 members (63,945 online)
Click here to Skip to main content

OPF.Net : An object persistent framework for .NET

, , 3 Nov 2003 87.2K 310 76
Rate this:
Please Sign up or sign in to vote.
Developing an object-oriented application based on an SQL storage using OPF.Net.

Introduction

The Object Persistence Framework for .Net (OPF.Net) is a complete set of classes that implement an object-relational mapping strategy for object oriented access to traditional relational database management systems and other types of persistent storage types such as XML files. OPF.Net has been designed and implemented for practical use in small to medium size projects and is currently being successfully used in several projects.

Background

The basic idea for OPF.Net and some important concepts behind OPF.Net come from "The Design of a Robust Persistence Layer” written by Scott Ambler (http://www.ambysoft.com/). OPF.Net has been written to bridge the traditional gap between object oriented programming and relational data storage. While Object-Databases have made a big progress, relational database management systems are still unrivalled in terms of performance and represent the de facto standard for most new software development projects. Accessing a relational database from an object oriented application can be implemented in two basic fashions:

  • use a persistence mechanism that maps the applications business objects to the database tables and vice versa
  • directly access and manipulate database tables and records using data access layers and components provided by most modern RAD programming environments

The first approach generally leads to well designed, real object oriented applications with a clear separation between user interface and business logic implemented through reusable business objects.

The second approach generally leads to poorly designed applications breaking the object oriented programming style with no clear separation of user interface, business logic and data storage and no encapsulation of business logic in business objects.

OPF.Net implements a simple and robust mechanism for building applications using persistent business objects by completely encapsulating database access and thus helping to build well designed software.

The Object Persistent Framework

OPF.Net distinguishes between domain classes and data manager classes.

Domain classes are the classes, also called business objects, that actually represent the data to be read, manipulated and written to the persistent storage. Domain classes do not know how to read and write to the storage, they contain no storage specific commands (e.g. SQL) but instead offer methods such as Load, Save and Delete to load, save and delete themselves from the underlying physical storage. The user interface of an application and all its business logic is implemented using the domain classes; application developer do not need to know what kind of underlying storage is used.

Data manager classes perform the actual loading, saving and deleting of domain classes from the persistence layer. The data manager classes are the only classes that know about the underlying storage mechanism and hold storage commands (SQL for DBMS storages) to perform the requested persistence operations. Therefore, to port an application from one DBMS to another, only the data manager classes have to be modified. The domain classes along with the business logic and the user interface do not need to be changed at all.

Associating a domain class with its corresponding data manager class is done through the ObjectBroker. The ObjectBroker is the only part of the framework knowing domain and data manager classes. All persistence methods called for a domain class are passed on to the ObjectBroker along with the domain class itself. The ObjectBroker then searches the associated data manager class and instructs it to perform the requested operation passing the domain class as a parameter. The data manager class execute the requested operation and eventually populates the domain class’ properties on a load or uses the domain class’ properties to fill command parameters on save and delete commands.

Each property of a business object generally maps to a single field in the storage. By loading a business object all its public properties get loaded from the underlying storage. Protected and private properties are by default not persistent. Through custom attributes it is however possible to mark properties as persistent, not persistent and read only.

OPF.Net Architecture in a two tier application

Every application needs to process not only single business objects but also collections of business objects. OPF.Net supports loading, saving and deleting collections of business objects through its Collection domain class and the corresponding Collection data manager class. As with single business objects, the data manager controls how a collection actually gets loaded from the underlying physical storage by defining one or more storage commands (SQL queries for DBMS) to load different collections of business objects of one type.

OPF.Net fully supports transactions if the underlying physical storage supports transactions. Using the StartTransaction, Commit and Rollback methods of the ObjectBroker, an application can start and complete transactions on one ore more used storages at the same time. All access to the physical storage is handled by the Storage class and is derivatives. To support a new type of physical Storage in OPF.Net a only new Storage class has to be written. At present OPF.Net offers Storage classes for MS SQL Server, ADO ,OLE DB and ODBC.

Step-by-step

This section will take you through the process of using OPF.Net, step by step.

Set up OPF.Net

First of all we have to create the ObjectBroker, which is one of the central units in your application. For windows applications (single user) it is recommended to use the SingleInProcessObjectBroker located in the OPF.ObjectBroker namespace.

If you are developing a web or a server application you should use the MultipleInProcessObjectBroker. In that case you have to register a class that implements the ISessionRetriever interface. That interface implements a function that returns a unique ID for each session. In web applications normally this function should return the ID of the current session.

Since we are developing an simple single user windows application we are using the SingleInProcessObjectBroker. To set the ObjectBroker we have to instance it and assign it to OPF.Main.ObjectBroker. OPF.Main is a static class that holds a global information required by the OPF.Net. (eg. MinDatValue containing the minimum date value the storage supports, IDFieldName the name of the field containing the id...)

/// <span class="code-SummaryComment"><summary>
</span>

At this point it would also be possible to set the the IDGenerator. The IDGenerator is a class that generates the IDs for new objects saved to the storage. By default the IDGenerator generated GUIDs which are guarantee to be unique. You have the possiblity to override the class to generate your own IDs.

The next step while setting up the OPF.Net is creating a storage. The storages are located in the OPF.Storages namespace. In our sample we use a OleDbStorage since we are dealing with Access.

We have to set the ConnectionString property or the connection string while connecting to the storage. To immediately check if the storage is available we set the connection string when connecting. It is also possible to not immediately connect because the ObjectBroker detects if a connection to a storage is required.

In the sample application the created storage is set as DefaultStorage of the ObjectBroker. If we are not setting the storage as DefaultStorage, we have to associate (register) each Persistent, Collection or derived classes with the storage.

  ...

  // As next we have to set a default storage. 
  // It is also possible not to do that
  // as we can register each persistent with it's own storage. 
  // If while registering
  // a persistent no storage is set, the default storage is used.
  _Storage = new OleDbStorage();
  OPF.Main.ObjectBroker.DefaultStorage = _Storage;

  // At least in this method we will connect to the 
  // storage. It is also possible only
  // to set the connection string, as the ObjectBroker will 
  // connect automatically if 
  // the storage is needed and the program isn't connected 
  // with the storage.
  // OPF.Main.ObjectBroker.DefaultStorage.ConnectionString 
  // = "connectionstring";
  try
  {
    OPF.Main.ObjectBroker.DefaultStorage.Connect(
      @"Provider=Microsoft.Jet.OLEDB.4.0;
      Data Source=..\db\database.mdb;");
  }
  catch
  {
    MessageBox.Show(
      "Not possible to find the database or to " + 
      "connect with the database! " +
      "(Check Global.cs - line 88)", 
      "Error while connecting with database", 
      MessageBoxButtons.OK, MessageBoxIcon.Error);
    return false;
  }

  ...

Usually in our applications this code is located in a function called InitOPF(). We create this function as we think, that the stuff initiating the OPF.Net should be located in a function and not in Main(). InitOPF() returns a Boolean a successful initialization of the OPF.Net. You can check in the Main() function what InitOPF() returns and proceed in a suitable way.

Registering BusinessObjects

The next step takes is to register the persistents and collections with the corresponding data managers. We also register the storage commands required by the application. A StorageCommand is a class that allows us directly send a query to the storage and return an ArrayList containing the results. Storage commands should generally used for performance reasons only, since the circumvent the business objects. We create normally three functions: RegisterPersistents(), RegisterCollections() and RegisterStorageCommands().

  ...
  
  // Register the persistents.
  RegisterPersistents();

  // Registers the collections.
  RegisterCollections();

  // Registers direct storage commands.
  RegisterStorageCommands();

  return true;
}

RegisterPersistents() registers all persistents and derived classes with the corresponding data manager.

/// <span class="code-SummaryComment"><summary>
</span>

RegisterCollections() registers all collections and derived classes with the corresponding data manager. It is not necessary to set a storage, as the storage registered while registering the persistent or derived class with is data manager is taken.

/// <span class="code-SummaryComment"><summary>
</span>

At the last step we have to register the storage commands. RegisterStorageCommands() registers each StorageCommand with the global ObjectBroker using an alias. To use this command in an application just call ObjectBroker.ExecuteStorageCommand(..) specifying alias and save the result to an arraylist.

The StorageCommand class allows you to set the type of command. There are two types:

  • Retrieve and
  • Execute.

Execute executes a query on the storage and returns nothing. Retrieve normally returns a data record that is mapped to an arraylist. If result should be returned set the StorageCommandType to Retrieve.

/// <span class="code-SummaryComment"><summary>
</span>

So far set up the OPF.Net and registered the collections, persistents and storage commands. Now we can create a business object to show you how that is done and how they are used.

Creating business objects and support classes

All business objects must inherit from the Persistent class. In order to load, save and delete business objects from the storage we also need to create a data manger class by inheriting from DataManager. To support collections of this business object the collection and collection data manager have to created respectively inheriting from Collection and CollectionDataManager.

Let's start with the derived persistent class.

Creating a derived Persistent class

Creating a derived persistent class is very easy. We only have inherit from Persistent and add the properties represents the columns in the table of the storage.

As you will see in the persistent data manager we can set a property that tells the data manager to parse the column names while populating the properties of the derived persistent object. The parsing algorithm that we use is very simple, but powerful enough to take a column named LAST_NAME and convert it to LastName. If parsing is enabled in the persistent data manager the property in the persistent class has to be named LastName. If not, we have to name the property LAST_NAME.

How does the parser work?

First of all the parser converts the column name do lower case. Then the parser eliminates the underscores and converts the first letter following an underscore to upper case. The first letter of the column name is also converted to upper case. The only exception were the underscore isn't eliminated is _ID. In this case the parser converts to _ID (brings ID to upper case).

Examples:

- LAST_NAME = LastName
- FIRSTNAME = Firstname
- PICTURE_ID = Picture_ID
- AUTHOR_HOUSE_ID = AuthorHouse_ID
- LAST_MAN_STANDING = LastManStanding

To get property names that are independent of storage field names they PopuplateProperties() and PopulateParams() method of the persistent datamanager have to be overridden or you can use the custom attribute MapField. It allows you to directly connect a property with a field in the storage. It is strongly recommended to use the MapField custom attribute instead let the OPF.Net parse the properties.

The following code snippet shows the book business object. As you see also we override the constructors to have the possibility to use all constructors of the base class.

/// <span class="code-SummaryComment"><summary>
</span>

Several custom attributes can be used to mark properties that are loaded from storage. If a property is marked with the [Mandatory] attribute the property is checked while saving. If such a property is empty the OPF.Net throws an exception. Mark a mandatory field in the storage with the [Mandatory] attribute. A property marked with [ReadOnly] is only loaded and not saved back to the storage. This is useful if you join tables. [NotPersistent] properties are not loaded nor saved. This attribute can be used for calculated properties that have no corresponding storage field, as the property TileAndAutor in the example above.

If you look at the database coming with this article you will find an additional column named "Dyn_Props". The OPF.Net detects this field as field for dynamic properties.

What are dynamic properties?

Dynamic properties are added dynamically to a persistent or derived class while the program is running. You need a large text field in the table of the persistent in the storage. This field has no corresponding property in the object. We normally use a field named Dyn_Props. Dynamic properties are saved to and read from this field. The OPF.Net will detect this field automatically! Dynamic properties are saved as Xml into the dynamic properties field.

To add a dynamic property to a persistent or derived class you have to call the AddDynamicProperty(..) function of that class. Remove them using RemoveDynamicProperty(..). A DynamicProperty is either of type OPF.SupportedTypes or of DynamicType. A DynamicType is a set of possible values the property value can have. You can see it like an enum. For more information check out the source code of the demo program coming with this article.

Creating a derived DataManager class

As next step we have to create the DataManager for our business object. The data manager contains the queries to load, save and delete the business object from storage. If you set ParseProperties to true the column names are parsed (Look above for more information).

We have to override the SetCommands() function of the data manager and set the queries using SetCommand(..).

/// <span class="code-SummaryComment"><summary>
</span>

Parameters in the commands are marked with a ':'. Parameters are replaced by the storage class while saving, loading or deleting the object in the storage. By setting the ParameterRecognitionString property of the OPF.Main class it is possible to change the ':' to '@' (.Net standard) or something else. We use the ':' because of a former OPF version written in Delphi.

Creating a derived Collection class

The next step takes us to the Collection class and creating a derived collection class. Collections are lists of objects of a certain type loaded from the storage. Each collection can be loaded in several ways by defining one or more loading functions. Example given LoadAll() to load all objects of the given type. For each load function a corresponding load function and command (sql..) has to be defined in the collection's data manger. Each loading function passes an alias name and a parameter collection to the collection data manager for execution (The data manager identifies the command to use using the alias).

The following BookCollection shows how to create a collection where all objects of the table are loaded and where only object with a certain author name are loaded.

/// <span class="code-SummaryComment"><summary>
</span>

As you see in LoadByAuthorName(..) the ParameterCollection takes one parameter that contains the variable with the name of the author and a name for the parameter - in this case "AuthorName". Attention: LoadAll() contains an empty ParameterCollection!! You have always to set a parameter collection!

Creating a derived Collection DataManager class

/// <span class="code-SummaryComment"><summary>
</span>

The collection datamanager is very simple. There are only the queries (defined as strings or compiled dynamically using a delegate) and a function called DefineCollections() that must be overridden. In DefineCollections() you have to the connections between the collection names and the queries using DefineCollection(..).

It is also possible to set a Delegate as query. This delegate is called while loading the collection. In this way a query can be assembled dynamically at runtime.

...

/// <span class="code-SummaryComment"><summary>
</span>

Using BusinessObjects

It's about time to use the business objects generated, don't ya think so?!

Persistent and derived classes

Single business objects are generally not loaded since the ID is generally not known. Normally a collection of objects is loaded.

bo.Book Book = new bo.Book();
Book.Load("1");

To save a business object you have to call the Save() function. To delete it the Delete() function.

try
{
  Book.Save();
}
catch (ConcurrencyException exc)
{
  // Exception is thrown if a concurrency problem occured.
}
catch (ConstraintsException exc)
{
  // Exception is thrown if a constraints problem occured.
  // This type of exception occures, if properties,
  // that are mandatory aren't compiled.
}

Collection and derived classes

A collection or business objects is loaded by using one of the customized load functions. For the book object it's possible to use LoadAll() and LoadByAuthorName(..).

bo.BookCollection BookCol = new bo.BookCollection();
BookCol.LoadByAuthorName("Roddenberry");

To delete an object from the collection you have to call Delete(..).

if (BookCol.Count > 0)
  BookCol.Delete(0); // Delete the first object.

To return an object use the GetObject(..) function or to use the Indexer BookCol[..].

if (BookCol.Count > 0)
{
  bo.Book Book = BookCol[0];
}

The Delete(..) function of the collection does not directly delete an object from storage. It moves the object to a so-called "DeletedList". Only when the Save() function is called all objects on the "DeletedList" are deleted.

if (BookCol.Count > 0)
  BookCol.Delete(0);

try
{
  BookCol.Save();
}
catch (ConcurrencyException exc)
{
  // Exception is thrown if a concurrency problem occured.
}
catch (ConstraintsException exc)
{
  // Exception is thrown if a constraints problem occured.
  // This type of exception occures, if properties, that
  // are mandatory aren't compiled.
}

To loop over a collection you can use the Indexer BookCol[..] or the foreach directive.

foreach(bo.Book Book in BookCol.Objects)
{
  // .. Do something.
}

for(Int32 i = 0; i < BookCol.Count; i++)
{
  bo.Book Book = (bo.Book)BookCol[i];
  // .. Do something.
}

As conclusion a full example that demonstrates how to use the collection:

bo.BookCollection BCol = new bo.BookCollection();
// Load all objects in database in the table "book".
BCol.LoadAll();
// Delete the first object.
if (BCol.Count > 0)
  BCol.Delete(0);

// Loop over all items and do something.
for(Int32 i = 0; i < BCol.Count; i++)
{
  bo.Book Book = (bo.Book)BookCol[i];
  Book.Name = "TestName";
}

// Saves the collection. Deletes the objects in the "deletedlist".
try
{
  BCol.Save();
}
catch (ConcurrencyException exc)
{
  // Exception is thrown if a concurrency problem occured.
}
catch (ConstraintsException exc)
{
  // Exception is thrown if a constraints problem occured.
  // This type of exception occures, if properties,
  // that are mandatory aren't compiled.
}

For more information about the OPF.Net visit the sourceforge page of the framework (http://www.sourceforge.net/projects/opfnet/) or the official homepage (http://www.littleguru.net/ or http://www.sarix.biz/opf).

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

Share

About the Authors

Christian Liensberger
Web Developer
Austria Austria
Studying at the technical university for vienna. Living in Brixen (South Tyrol, Italy) and working at Bozen (also in South Tyrol) during the summer months and holidays...

Martin Geier
Italy Italy
No Biography provided

You may also be interested in...

Comments and Discussions

 
GeneralOpf3 Pin
Christian Liensberger31-Mar-04 0:40
memberChristian Liensberger31-Mar-04 0:40 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.150731.1 | Last Updated 4 Nov 2003
Article Copyright 2003 by Christian Liensberger, Martin Geier
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid