ADO to ADO.NET Common

Since the 2.0 version of the .NET Framework, Microsoft incorporated the System.Data.Common namespace, which contains classes intended to be the base for the implementation of all data providers. This architectural decision allows application designers and programmers to use data provider-agnostic patterns on their data access layers.

VB6 offers several ways to access a database, all of them using COM libraries such as ADO, RDO and DAO. Many of the structures within these packages have their counterpart in the ADO.NET Common architecture, such as Connection and Command; in those cases most of their functionality is covered through our conversion approach.

The most common structure used to retrieve data from the database is the RecordSet object; in the case of ADO and DAO, and Resultset in RDO which are basically a collection of rows retrieved from the data source using a specific SQL command.

As counterpart in the .NET side, we have the DataSet object which also holds data retrieved from the database.

There are differences, though, between the DataSet in .NET and the RecordSet and Resultset; the most important of them, from a functional equivalence standpoint, is it’s capability to “remember” the current position and make all data-related operations affect that record. The importance of this feature increases when these kind of object is passed (as parameter) through the different tiers and components of the application.

To accomplish the same functionality in C# .NET, Mobilize has developed Helper classes to handle all these operations. Internally, this class has the necessary infrastructure to support all database requests such as DataAdapters, using the System.Data.Common namespace.

The RDO and ADODB technologies have as well visual controls to interact with the Recordset and Resultset and to bind visual controls to its data. Those controls ADODC and MSRDC are supported as part of the Data access feature to give more functional equivalence to our clients. Those controls are supported as well via helper implementations and some data binding patterns are supported and automatically upgraded.

Helper Class Design

The Helper classes are based on the specialization inheritance model, where the base class defines all common properties, methods, and the basic functionality. The derived classes overrides, adds new specific behavior to model the details. This design decision gives more functionality without breaking the natural ADO.NET architecture since the Recorset Helper classes are Datasets itself.

This structure is being use on the ADO, RDO, DAO common model as detailed on this diagram:

helper-class-design.jpg

The following code is part of the helper code -which is periodically enhanced; specifically for the open methods that encapsulate the dataset population logic.

 #region Open Operations
        private void OpenRecordset()
        {
            operationFinished = false;
            DbDataAdapter dbAdapter = CreateAdapter(activeConnection);
            dbAdapter.Fill(this);
            operationFinished = true;
            currentView = Tables[0].DefaultView;
            currentView.AllowDelete = true;
            currentView.AllowEdit = true;
            currentView.AllowNew = true;
            if (Tables[0].Rows.Count == 0)
                index = -1;
            else
                MoveFirst();
            newRow = false;
            foundRecordsCounter = 0;
            OnAfterQuery();
        }

        public void Open()
        {
            if (activeConnection == null && activeCommand != null && activeCommand.Connection != null)
               ActiveConnection = activeCommand.Connection;
            else
                throw new InvalidOperationException("The ActiveConnection property must be set before calling this method");
            OpenRecordset();
        }

        public void Open(DbCommand command, String connectionString)
        {
            this.connectionString = connectionString;
            Open(command, CreateConnection());
        }

        public void Open(DbCommand command, DbConnection connection)
        {
            ActiveConnection = connection;
            activeCommand = command;
            Open();
        }

        private void Open(String SQLstr, String connectionString)
        {
            this.connectionString = connectionString;
            CommandType commandType = getCommandType(SQLstr);
            DbCommand command = providerFactory.CreateCommand();
            command.CommandText = SQLstr;
            command.CommandType = commandType;
            Open(command, connectionString);
        }

        #endregion

Configuration file

The Configuration file is an important component that supports this feature. This file contains the list of the providers that the application is capable to use and which are intended to be used as default, as well as the columns which values are calculated or retrieved automatically from the database when an insert occurs, such as identities or values set by triggers.

Configuration Sections

This tag of the configuration file details the custom configuration section handlers used by the application; in the case of an application converted to ADO .Net using Common, it must be as follows:

<configSections>
    <section name="AdoIdentityColumns" type="Artinsoft.VB6.DB.AdoIdentityColumnsConfigurationSection, Artinsoft.VB6.DB" allowExeDefinition="MachineToApplication" allowLocation="true"/>
    <section name="AdoFactories" type="Artinsoft.VB6.DB. AdoFactoriesConfigurationSection, Artinsoft.VB6.DB" allowExeDefinition="MachineToApplication" allowLocation="true"/>
  </configSections>

Factories Configuration Section

This section details the providers that the application may use and which ones will be used as Default Provider.

This section has the following data to be filled:

  • Name: The name of the provider; this name is the identifier of the provider. If the application needs to use a non-default provider, this is the parameter to be passed to the GetFactory method of the AdoFactorManager class.
  • Factorytype: Is the namespace of the factory to be used.
  • Isdefault: this marks the factory as default. Hence this factory is the one created when the parameter-less GetFactory method is called.
  • Databasetype: this is the type of database that you are trying to interact with; this information is taken by the different recordset helpers to handle many subtle differences between the several database managers. Such as the way to handle the value returned by the database on insertions when there is an identity field involved.
<AdoFactories>
    <Factories>
      <add name="SQLServer" factorytype="System.Data.SqlClient" isdefault="true" databasetype=SQLServer/>
      <add name="ODP" factorytype="Oracle.DataAccess.Client" isdefault="false" databasetype=Oracle/>
      <add name="MSOracle" factorytype="System.Data.OracleClient" isdefault="false" databasetype=Oracle />
      <add name="OracleOleDb" factorytype="System.Data.OleDb" isdefault="false" databasetype=Oracle />
      <add name="AccessOleDb" factorytype="System.Data.OleDb" isdefault="false" databasetype=Access />
    </Factories>
  </AdoFactories>

Identity Columns Configuration Section

This section details the columns that are calculated by the database when an Insert is executed, such as identities or sequence values set by a trigger.

This section has the following data to be filled:

  • Tablename: The name of the table where the column belongs.
  • Columnname: The name of the column
  • Sequencename: This is the name of the sequence where the column name is taken.
   <AdoIdentityColumns>
    <IdentityColumns>
      <add tablename="CR_Customreport">
        <columns>
          <add columnname="CR_ID" sequencename="CR_ID"/>
        </columns>
      </add>
    </IdentityColumns>  

Complete Configuration file example

The following example shows the configuration of an application which can connect to SQLServer and Oracle through the Microsoft provider as well as the ODP provider, and an OleDb Connection with the SQL Server provider as default.

<?xml version="1.0"?>
<configuration>
<configSections>
    <section name="AdoIdentityColumns" type="Artinsoft.VB6.DB.AdoIdentityColumnsConfigurationSection, Artinsoft.VB6.DB" allowExeDefinition="MachineToApplication" allowLocation="true"/>
    <section name="AdoFactories" type="Artinsoft.VB6.DB. AdoFactoriesConfigurationSection, Artinsoft.VB6.DB" allowExeDefinition="MachineToApplication" allowLocation="true"/>
  </configSections>

<AdoFactories>
    <Factories>
      <add name="SQLServer" factorytype="System.Data.SqlClient" isdefault="true" databasetype=SQLServer/>
      <add name="ODP" factorytype="Oracle.DataAccess.Client" isdefault="false" databasetype=Oracle/>
      <add name="MSOracle" factorytype="System.Data.OracleClient" isdefault="false" databasetype=Oracle />
      <add name="OracleOleDb" factorytype="System.Data.OleDb" isdefault="false" databasetype=Oracle />
      <add name="AccessOleDb" factorytype="System.Data.OleDb" isdefault="false" databasetype=Access />
    </Factories>
  </AdoFactories>
   <AdoIdentityColumns>
    <IdentityColumns>
      <add tablename="CR_Customreport">
        <columns>
          <add columnname="CR_ID" sequencename="CR_ID"/>
        </columns>
      </add>
    </IdentityColumns>  
</configuration>

Advantages

  • Clearer and more readable C# code; by taking this approach the use of generated temporary variables is avoided and the generated code is a Helper class call.
  • Due to the inheritance of the Helper class from DataSet, integration with ADO.NET-compliant data access frameworks will be easier.
  • Minimal effort on manual changes for scenarios where the RecordSet is manipulated by more than one function or method, or passed between layers with dependency on the current position between the records.
  • The Helper class source code will be included in Mobilize deliverables. This source code will belong to the client, allowing the client to modify it as necessary for new developments.

Limitations

Record accessor classes

These three technologies has different ways to access the data stored on the local in memory data representation structures(Recordset, Resultset); such as Field and Fields classes on ADODB and DAO, and rdoColumn and rdoColumns on RDO.

These classes have particular characteristics since they are used to handle the metadata and the data itself. On the ADO.NET side, the metadata and the data stored on a dataset are handled by two different classes: DataColumn(For metadata), DataRow(For data).

Because of that, these classes are supported as follows:

  • The Field and rdoColumn classes, are type is converted as DataColumn to keep the metadata handling capability. Consequently the Fields and rdoColumns classes are converted as DataColumnCollection
  • The “Value” property of the Field and rdoColumn classes are supported if and only if the invocation to that property is done under the context of a RecordSet instance; in that case, the conversion is an invocation to the indexer properties of the specific RecordSetHelper Class; otherwise the property is not converted.

Errors and Error Classes

These classes are intended to handle and report all the errors occurred during a database interaction using ADO, DAO and RDO.

These classes are not converted automatically and there is no helper provided to achieve this functionality. A manual solution to this would be to use the provider-specific collections contained in .NET, or to use the Try/Try/Catch pattern to handle the exceptions that database operation could generate.

The configuration file

The configuration file is automatically generated by the Visual Basic Upgrade Companion on the root of the migration solution’s output directory.

Code Optimization

No code optimization patterns for the ADO Common feature have been implemented at this stage of the roadmap

Data binding patterns

There are data binding several patterns to implement data bindings using the visual controls MSRDC and ADODC; at this point of the roadmap we support the design time data binding pattern; it means that if you bind a control on design time with one of those controls the VBUC will generate the code that maintains the data binding functionality working.

ADODB Connection Events

This class exposes several events like BeginTransComplete and ConnectComplete; since this class is converted to the DbConnection class; some events are not supported.

The supported events are ConnectComplete and Disconnect even though a manual change is required to fix a compilation error:

C#

  • Find the delegate assignation code:
    this.lConn.Disconnect += new System.Data.StateChangeEventHandler(this.lConn_Disconnect);
    this.lConn.ConnectComplete += new System.Data.StateChangeEventHandler(this.lConn_ConnectComplete);
    
  • Change the name of the event like this:
    this.lConn.StateChange += new System.Data.StateChangeEventHandler(this.lConn_Disconnect);
    this.lConn.StateChange += new System.Data.StateChangeEventHandler(this.lConn_ConnectComplete);
    
  • Do the same with the delegate des assignation:
    this.lConn.Disconnect -= new System.Data.StateChangeEventHandler(this.lConn_Disconnect);
    this.lConn.ConnectComplete -= new System.Data.StateChangeEventHandler(this.lConn_ConnectComplete);
    
  • Change the name of the event like this:
    this.lConn.StateChange -= new System.Data.StateChangeEventHandler(this.lConn_Disconnect);
    this.lConn.StateChange -= new System.Data.StateChangeEventHandler(this.lConn_ConnectComplete);
    

VB .Net

  • Find the delegate method handlers:
    Private Sub cnnAccess_ConnectComplete(ByVal sender As Object, ByVal e As StateChangeEventArgs) Handles cnnAccess.ConnectComplete
    
    Private Sub cnnAccess_Disconnect(ByVal sender As Object, ByVal e As StateChangeEventArgs) Handles cnnAccess.Disconnect
    
  • Change the handles section as follows:
    Private Sub cnnAccess_ConnectComplete(ByVal sender As Object, ByVal e As StateChangeEventArgs) Handles cnnAccess.StateChange
    
    Private Sub cnnAccess_Disconnect(ByVal sender As Object, ByVal e As StateChangeEventArgs) Handles cnnAccess.StateChange
    

Oracle Data Providers

From the .NET Framework 2.0 Common infrastructure perspectives, there are two native data providers to interact with Oracle; one of them is provided by Microsoft as part of the redistributable package of the Framework (System.Data.OracleClient) and the other one is provided by Oracle through its Oracle Data Provider (ODP).

System.Data.OracleClient

This is a data provider implementation provided by Microsoft, included in the .NET Framework and compatible with the Common architecture.

This implementation tries to parse the SQL statements; issues have been detected with some PL/SQL constructs such as:

Select cust.Code ||’’|| cust.Name as Customer, 
addr.First_Line ||‘ ‘||addr.Second_Line
    From Customer cust, Address addr
    WHERE TO_UPPER(cust.Address_Code (+)) = addr.Address_Code

This statement will throw an internal exception in the data adapter when it it is parsed, because of the combination of the old OUTER JOIN syntax and the call to the TO_UPPER function. Even though, this statement can be rewritten using the ANSI LEFT OUTER JOIN syntax:

    Select cust.Code ||’’|| cust.Name as Customer, 
addr.First_Line ||‘ ‘||addr.Second_Line
    From Customer cust
Left Outer Join Address addr 
ON TO_UPPER(cust.Address_Code) = addr.Address_Code

In summary some SQL statements have to be rewritten using this data adapter implementation.

Oracle Data Provider (ODP)

This is a data provider implementation provided by Oracle, included on its client product; this is ADO Common–compatible since version 10.2.0.2, therefore this conversion approach requires this version or a later version of the ODP software installed on the computers that will run the application.

There are differences between some .NET Framework data types and Oracle database such as:

Oracle Type Precision .NET Type Precision
NUMBER 38 Decimal 28
DATE
TimeStamp
Holds BC dates DateTime Holds Just AC dates
INTERVAL DAY TO SECOND 9 TimeSpan 7

To avoid this data loss issue, ODP provides a functionality called Safe Type Mapping, which consists of a property in the OracleDataAdapter class that needs to be set with a Hashtable containing the database column name and the type to be assigned to that column value.

This approach avoids data precision loss, however, from the conversion point of view it presents a problem, since the data types of the columns are replaced with the specified mappings, so data operations done through all the application may fail because of the data type substitution.

Examples

Here are some examples of the transformations needed with and without the Helper approach:

Opening a RecordSet

Source Code

Dim mRS As ADODB.Recordset
SQL = ConvertOracleToSqlServer(dbc, SQL)
mRS.Open SQL, dbc, adOpenForwardOnly, adLockReadOnly

Target Code

RecordSetHelper mRS = new RecordSetHelper();
SQL = ConvertOracleToSqlServer(dbc, SQL);
//A direct call to the helper class which takes care of the 
//adapters and command creations 
mRS.Open(SQL, dbc);

Updating a RecordSet

Source Code

Set rsUpdate.ActiveConnection = dbc
rsUpdate.UpdateBatch

Target Code

rsUpdate.ActiveConnection = dbc;
rsUpdate.UpdateBatch();

RecordSet Iteration

Source Code

With prsChildRecordset
  .Filter = adFilterPendingRecords
  If Not .BOF And Not .EOF Then
   .MoveFirst
        Do Until .EOF
      .Fields(psKey).Value = pvValue
       .Update
       .MoveNext
Loop

Target Code

prsChildRecordset.Filter = adFilterPendingRecords;
if(!prsChildRecordset.BOF && ! prsChildRecordset.EOF)
{
      prsChildRecordset.MoveFirst();
      do{
        prsChildRecordset [psKey] = pvValue;
        prsChildRecordset.Update();
        prsChildRecordset.MoveNext();
      }
  while(!prsChildRecordset.EOF)
}

Download VBUC Free Trial
Download VBUC Now

It's time to eradicate VB6
ROI of eradicating VB6

8 Proven Tips for
Planning a Successful Migration

8 Tips for migration