www.codemag.com Open in urlscan Pro
13.84.36.2  Public Scan

URL: https://www.codemag.com/Article/2207021/Simplifying-ADO.NET-Code-in-.NET-6-Part-1
Submission: On August 30 via manual from PL — Scanned from PL

Form analysis 0 forms found in the DOM

Text Content

 * Home
   
    * CODE Home
    * About Us
    * Videos
    * Press Releases
    * People
    * Careers
    * Privacy Policy
    * Contact Us

 * Magazine
   
    * Magazine Home
    * CODE Focus Magazine
    * All Issues
    * Subscribe
    * Mobile
    * My (Digital) Magazines
    * Where is my Magazine?
    * My Subscriber Account
    * Advertise
    * Write

 * Consulting
   
    * Consulting Home
    * Services & Technologies
    * Artificial Intelligence (AI)
    * Cloud Computing
    * Custom Application Development
    * Executive Briefing (AI)
    * Low-Code/No-Code
    * Cyber Security
    * Copilots in Your Apps!
    * Project Rescue
    * CODE Labs
    * Business Document Copilot
    * Legacy Conversion and Maintenance
    * Free Hour of Consulting
    * VFP Conversion
    * Energy Software
    * Contact Us

 * Staffing
   
    * Staffing Home
    * Our Services
    * About Us
    * Careers

 * Training
   
    * Training Home
    * State of AI
    * CODE Presents
    * State of .NET
    * Classes
    * Mentoring
    * Lunch with CODE

 * Framework
   
    * Framework Home
    * Get Started & Documentation
    * Download
    * Support & Services

 * VFP Conversion
   
    * VFP Conversion Home
    * Services
    * Tools
    * Articles
    * Fox End of Life
    * Contact Us

 * Login



 * Home
    * CODE Home
    * About Us
    * Videos
    * Press Releases
    * People
    * Careers
    * Privacy Policy
    * Contact Us

 * Magazine
    * Magazine Home
    * CODE Focus Magazine
    * All Issues
    * Subscribe
    * Mobile
    * My (Digital) Magazines
    * Where is my Magazine?
    * My Subscriber Account
    * Advertise
    * Write

 * Consulting
    * Consulting Home
    * Services & Technologies
    * Artificial Intelligence (AI)
    * Cloud Computing
    * Custom Application Development
    * Executive Briefing (AI)
    * Low-Code/No-Code
    * Cyber Security
    * Copilots in Your Apps!
    * Project Rescue
    * CODE Labs
    * Business Document Copilot
    * Legacy Conversion and Maintenance
    * Free Hour of Consulting
    * VFP Conversion
    * Energy Software
    * Contact Us

 * Staffing
    * Staffing Home
    * Our Services
    * About Us
    * Careers

 * Training
    * Training Home
    * State of AI
    * CODE Presents
    * State of .NET
    * Classes
    * Mentoring
    * Lunch with CODE

 * Framework
    * Framework Home
    * Get Started & Documentation
    * Download
    * Support & Services

 * VFP Conversion
    * VFP Conversion Home
    * Services
    * Tools
    * Articles
    * Fox End of Life
    * Contact Us

 * Login


Advertisement:



SIMPLIFYING ADO.NET CODE IN .NET 6: PART 1

By Paul Sheriff
Published in: CODE Magazine: 2022 - July/August
Last updated: August 31, 2022

Download File

Share

Download File
Syntax Highlight Theme:
Brown Paper Codepen Embed Color Brewer Dracula Dark Darkula Default Dracula Far
Foundation Github Github Gist GoogleCode Grayscale Idea IR Black Kava Docs Kava
Docs Dark Kimbie.Dark Kimbie.Light Magula Mono Blue Monokai Monokai Sublime
Obsidian Paraiso Dark Paraiso Light Rails Casts Rainbow Solarized Dark Solarized
Light Sunburst Twilight VS VS2015 XCode ZenBurn


PUBLISHED IN:


FILED UNDER:

 * .NET 6
 * ADO.NET


Advertisement:


When developers think of how to access data, many use the Entity Framework (EF),
Dapper, NHibernate, or some other object-relational mapper (ORM). Each of these
ORMs use ADO.NET to submit their SQL queries to the back-end database. So, why
do many developers use ORMs instead of just using ADO.NET directly? Simply put,
ORMs allow you to write. If each of these ORMs are simply wrappers around
ADO.NET, can you write your own wrapper to cut down the amount of code you need
to write? Absolutely! This series of articles shows you how to create a set of
reusable wrapper classes to make it simpler to work with ADO.NET in .NET 6.

In these articles, you write code to map columns to properties in a class just
like the ORMs do. You check for attributes such as [Column] and [Key] on class
properties and use those attributes in your data application. You build SQL
statements from your classes and submit that SQL to the database efficiently and
safely. By the end of these articles, you're going to have a design pattern for
typical CRUD applications that's fast and requires less ADO.NET code, just like
the most popular ORMs out there. Besides learning about ADO.NET, these articles
teach you how to create a set of generic classes and methods. By the end of
these articles, you'll have some insight into how many ORMs work.


WHAT'S IN THESE ADO.NET WRAPPER CLASSES

The classes you're going to create in these articles are not intended to be an
ORM. Instead, they are going to help you perform standard CRUD operations in an
efficient manner and with less code. The classes you're going to learn about
will perform the same, and sometimes better, than the corresponding ORMs because
there is less overhead. Another advantage of using the classes described herein
is that you're writing straight-forward C# code and you don't have to learn any
new configuration or tooling. You're going to make these classes generic so that
you can use them with any .NET data provider such as SQL Server or Oracle.


READ OPERATIONS

The classes you're going to learn about in these articles will perform standard
CRUD operations. For reading data, you can submit dynamic SQL, call views, or
invoke stored procedures to return data and have that data automatically mapped
to C# classes.

Standard data annotations such as the [Column] attribute can be used to map a
column to a property name that's different from the column name. You can use the
[NotMapped] attribute so you can add properties that have nothing to do with the
table. You're going to create a custom attribute called [Search] to help you
handle many searching scenarios in a generic manner.

Advertisement



MODIFY OPERATIONS

These wrapper classes also allow you to submit action queries such as INSERT,
UPDATE, and DELETE using either dynamic SQL or stored procedures. You're going
to be able to retrieve the identity value generated from SQL Server, handle data
validation using the .NET data annotations, and add your own custom validation
code. Finally, you'll create a way to get excellent exception information to
help you track down any potential problems quickly and easily.


USING THE ADO.NET WRAPPER CLASSES

Once you build a few reusable classes that you'll learn about in this article,
all you'll need are an entity class, a search class, and a repository class for
each table in your database. Let's look at some typical code you're going to
have to write to retrieve rows from a table.


GET ALL ROWS

Once you have a “Database Context”, a “Repository”, and an “Entity” class built,
you can retrieve all rows in the table by using code shown in the following code
snippet. Now, you must admit, this is very simple code and right one par with
what you'd write when using an ORM such as the Entity Framework.

 
using AdvWorksDbContext db = new(connectString);
List<Product> list = db.Products.Search();
// Display the Results
foreach (var item in list) 
{
    Console.WriteLine(item.ToString());
}


In the code above, create an instance of a database context class, named
AdvWorksDbContext, within a using so the connection is closed and disposed of
properly after the data is read. Within the AdvWorksDbContext class, you expose
an instance of a ProductRepository class as a property named Products. Pass in a
connection string to the constructor of the AdvWorksDbContext class. This
connection string is used by the base class to create a connection to the
database. Here's what your database context class is going to look like.

 
public class AdvWorksDbContext: SqlServerDatabaseContext 
{
    public AdvWorksDbContext(string connectString): base(connectString) 
    {
    }
    
    public override void Init() 
    {
        base.Init();
        Products = new(this);
    }
    
    public ProductRepository Products { get; set; }
}



SEARCH FOR SPECIFIC ROWS

If you wish to search for specific rows within a table, you need only create a
class with properties set to the values to search for. The code shown in Listing
1 allows you to search for data in the SalesLT.Product table from the
AdventureWorksLT database. This code returns a list of product objects that
represent each row found that matches the criteria.


LISTING 1: A SAMPLE OF HOW TO SEARCH FOR DATA USING THE ADO.NET WRAPPER YOU'RE
GOING TO CREATE IN THIS ARTICLE

using AdvWorksDbContext db = new(connectString);

// Search for products that
// Start with 'C' AND ListPrice >= 50
List<Product> list = db.Products.Search(new ProductSearch() 
{
    Name = "C",
    ListPrice = 50
});

// Display the Results
foreach (var item in list) 
{
    Console.WriteLine(item.ToString());
}


The Search() method has two overloads, one of which is passed an instance of a
ProductSearch class. The Name property in the ProductSearch is set to the value
“C” and the ListPrice is set to “50”. The ProductSearch class looks like the
following:

 
public class ProductSearch 
{
    [Search("LIKE")]
    public string Name { get; set; }
    
    [Search(">=")]
    public decimal? ListPrice { get; set; }
}


A custom [Search] attribute (which you'll build in the next article) specifies
what operator to use when building the WHERE clause to submit to the database.
The Search() method generates a SELECT statement with a WHERE clause when a
search object is passed in as shown in the following code snippet.

 
SELECT * FROM SalesLT.Product
WHERE Name LIKE 'C%' AND ListPrice >= 50;


Obviously, you're going to need a way to add, edit, and delete rows of data
within a table. More code will be added to this class to perform the other CRUD
functionality as you work your way through these articles. All you need is an
entity class to map to your table, a search class, and a repository class for
each table. You then expose each repository class from the database context
class, and you're ready to write the code shown in Listing 1.


GETTING STARTED

To really learn how to use these ADO.NET wrapper classes, I recommend that you
follow along as I go through the steps in this article. To start, create a .NET
6 console application and download the AdventureWorksLT SQL Server database.


BUILD A .NET 6 CONSOLE APPLICATION

Open Visual Studio 2022 and click on the Create a new project option. Locate the
template for a Console App that runs on .NET on Windows, Linux, and macOS.
Highlight that template and click the Next button. Type in AdoNetWrapperSamples
for the Project name property. Click the Next button and make sure you choose
.NET 6.0 (Long-term support) for the Framework property.

Right mouse-click on the AdoNetWrapperSamples project and select Manage NuGet
Packages… Click on the Browse tab, type in System.Data.SqlClient, and install
this package into your project. This package provides all the ADO.NET classes
you need to connect to a database and to retrieve and modify data to/from a
table.


GET THE ADVENTUREWORKSLT DATABASE

For these articles, I'm going to use the AdventureWorksLT database, so you need
to install this database into your SQL Server. You can get the database and
instructions at this link. You can also get a recent database backup and the
appropriate .SQL files to create the tables at my GitHub located at
https://github.com/PaulDSheriff/AdventureWorksLT. Once you have the
AdventureWorksLT database installed into your SQL Server, you're ready to start
building the classes to access the Product table within that database.


THE BASICS FOR BUILDING AN ENTITY LIST

To read data from a table in ADO.NET, you need to use three different classes.
First you need to create a SqlConnection object with the appropriate connection
string to your database. Place that SqlConnection object into the Connection
property of a SqlCommand object and set the CommandText property to the SQL to
submit. Finally, call the ExecuteReader() method on the SqlCommand object to
create an instance of a SqlDataReader object. Loop through the rows in the data
reader and each time through, build an instance of a Product object and populate
the properties with the columns from the Product table you're reading.


THE PRODUCT CLASS

In the AdventureWorksLT database, there's a Product table in the SalesLT schema.
To retrieve the data from that table and create an instance of a Product object
for each row, create a Product entity class as shown in Listing 2. I'm not using
all the columns in the table, just enough to illustrate the concept of creating
a collection of Product objects.


LISTING 2: AN ENTITY CLASS HAS ONE PROPERTY FOR EACH COLUMN IN YOUR TABLE

#nullable disable

using System.ComponentModel.DataAnnotations.Schema;

namespace AdoNetWrapperSamples.EntityClasses;

[Table("Product", Schema = "SalesLT")]
public partial class Product
{
    public int ProductID { get; set; }
    public string Name { get; set; }
    public string ProductNumber { get; set; }
    public string Color { get; set; }
    public decimal StandardCost { get; set; }
    public decimal ListPrice { get; set; }
    public DateTime SellStartDate { get; set; }
    public DateTime? SellEndDate { get; set; }
    public DateTime? DiscontinuedDate { get; set; }

    public override string ToString() 
    {
        return $"Product Name: {Name} - Product ID: {ProductID} - List Price: {ListPrice:c}";
    }
}


Right mouse-click on the project and add a new folder named EntityClasses. Right
mouse-click on this EntityClasses folder and add a new class named Product. Add
the code shown in Listing 2. Please note that due to space limitations in the
magazine, the string in the ToString() method is broken across multiple lines.
You'll need to put that string all on one line.


THE DATAREADEREXTENSIONS CLASS

One of the challenges you may encounter when reading data from a table, is a
null value in columns. When a null is read from a data reader, .NET interprets
it as a DbNull object. You can't put this value into a .NET data type, even if
it's a nullable type. So, the best thing to do is to create an extension method,
named GetData<T>(), to handle these DbNull values. For example, if your reader
variable is named rdr, call the GetData() method passing in the data type that
you wish the data to be converted to as shown in the following code snippet.

 
ProductID = rdr.GetData<int>("ProductID");
Name = rdr.GetData<string>("Name");


Right mouse-click on the project and add a new folder named Common. Right
mouse-click on this Common folder and add a new class named
DataReaderExtensions. Add the code shown in Listing 3 to this file.


LISTING 3: EXTENSION METHOD TO HANDLE NULL VALUES IN THE DATA READER

#nullable disable

using System.Data;

namespace AdoNetWrapper.Common;

public static class DataReaderExtensions 
{
    public static T GetData<T>(this IDataReader dr, string name, T returnValue = default) 
    {
        var value = dr[name];

        if (!value.Equals(DBNull.Value)) 
        {
            returnValue = (T)value;
        }

        return returnValue;
    }
}



CREATE A PRODUCTREPOSITORY CLASS

Now that you have a Product class to represent the Product table, create a class
called ProductRepository to perform the actual calls to the database and create
the collection of Product objects. Right mouse-click on the project and add a
new folder named RepositoryClasses. Right mouse-click on this RepositoryClasses
folder and add a new class named ProductRepository. Add the code shown below to
this file.

 
#nullable disable

using AdoNetWrapper.Common;
using AdoNetWrapperSamples.EntityClasses;
using System.Data;
using System.Data.SqlClient;

namespace AdoNetWrapperSamples.RepositoryClasses;

public class ProductRepository 
{
}


Within the ProductRepository class, create a method called Search() as shown in
Listing 4. This method accepts a connection string and a SQL string. It then
creates the new SqlConnection and SqlCommand objects. It opens the connection
and then invokes the ExecuteReader() method to create a SqlDataReader object.


LISTING 4: THE SEARCH() METHOD IS THE PUBLIC API TO PERFORM SEARCHING OF RECORDS

public virtual List<Product> Search(string connectString, string sql) 
{
    List<Product> ret;

    // Create a connection
    using SqlConnection cnn=new(connectString);

    // Create a command object
    using SqlCommand cmd = new(sql, cnn);

    // Open the connection
    cnn.Open();

    // Create a data reader within a using
    // so it is closed and disposed of properly
    using SqlDataReader rdr = cmd.ExecuteReader(CommandBehavior.CloseConnection);

    // Build the collection of entity objects
    ret = BuildEntityList(rdr);

    return ret;
}


The Search() method calls the BuildEntityList() method to which the
SqlDataReader object is passed. This method is responsible for building a
Product object from each row in the table. Calls to the extension method,
GetData<T>(), make sure null values are handled correctly. Add the
BuildEntityList() method (Listing 5) to the ProductRespository class.


LISTING 5: THE BUILDENTITYLIST() IS RESPONSIBLE FOR LOADING DATA INTO AN ENTITY
OBJECT

protected virtual List<Product> BuildEntityList(IDataReader rdr) 
{
    List<Product> ret = new();

    // Loop through all rows in the data reader
    while (rdr.Read()) 
    {
        // Create new object and add to collection
        ret.Add(new Product 
        {
            ProductID = rdr.GetData<int>("ProductID"),
            Name = rdr.GetData<string>("Name"),
            ProductNumber = rdr.GetData<string>("ProductNumber"),
            Color = rdr.GetData<string>("Color"), 
            StandardCost = rdr.GetData<decimal>("StandardCost"),
            ListPrice = rdr.GetData<decimal>("ListPrice"),
            SellStartDate = rdr.GetData<DateTime>("SellStartDate"),
            SellEndDate = rdr.GetData<DateTime?>("SellEndDate"),
            DiscontinuedDate = rdr.GetData<DateTime?>("DiscontinuedDate")
        });
    }

    return ret;
}


It's now time to try out the code you wrote to ensure that you can read data
from the Product table and build a collection of Product objects. Open the
Program.cs file and delete all the code. Add the code shown in Listing 6 to this
file.


LISTING 6: CREATE A PRODUCTREPOSITORY OBJECT AND SEARCH FOR ALL PRODUCT DATA

#nullable disable

using AdoNetWrapperSamples.EntityClasses;
using AdoNetWrapperSamples.RepositoryClasses;

string ConnectString = "Server=Localhost; Database=AdventureWorksLT; Integrated Security=Yes";
string Sql = "SELECT * FROM SalesLT.Product";

ProductRepository repo = new();
List<Product> list = repo.Search(ConnectString, Sql);

Console.WriteLine("*** Display the Data ***");
// Display Data
foreach (var item in list) 
{
    Console.WriteLine(item.ToString());
}

Console.WriteLine();
Console.WriteLine($"Total Items: {list.Count}");
Console.WriteLine();


Be sure to adjust the connection string to the correct values for connecting to
your server. If you're unsure about what connection string to use, visit
www.connectionstrings.com as they provide a wealth of information on how to
build connection strings for all sorts of databases.


TRY IT OUT

Run the console application, and if you typed everything in correctly, you
should see a set of products displayed, along with the total items returned, as
shown in Figure 1.


Figure 1: You should see a couple hundred Product objects appear when you run
the application.


READ CONNECTION STRING FROM APPSETTINGS.JSON

Instead of hard-coding your connection string, it's a better idea to put that
connection string into a .json file. Right mouse-click on the project and select
Add > New Item… from the context-sensitive menu. Click on the General tab and
locate the JSON File item. Set the Name property to appsettings.json and click
the Add button. Add the code shown below into the .json file. Be sure to adjust
the connection string to the one you need for your server. Also, your connection
string shouldn't be broken across multiple lines, I had to do that for
formatting purposes of this magazine.

 
{
    "ConnectionStrings": {
        "DefaultConnection": "Server=Localhost; Database=AdventureWorksLT; Integrated Security=Yes"
    }
}


Make sure that the appsettings.json file is always copied to the directory where
the program is run from. Click on the appsettings.json file to give it focus,
then right mouse-click on it, and select Properties from the context-sensitive
menu. From within the Properties window, change the Copy to Output Directory
property to Copy always as shown in Figure 2.


Figure 2: Ensure that the appsettings.json file is always copied to the
distribution directory.


ADD THE HOSTING PACKAGE

To use the Configuration service used to read configuration settings in .NET 6,
add a package called Microsoft Extensions Hosting. Right mouse-click on the
project and select Manage NuGet Packages… Click on the Browse tab, enter
Microsoft.Extensions.Hosting and press Enter. In the list below, highlight the
package and click on the Install button to add this package to your project.
Open the Program.cs file and add a few using statements.

 
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;


Delete the line of code you wrote earlier that defines and initializes the
ConnectString variable and replace it with the following lines of code. These
lines of code create a hosting service from which you can request a
Configuration service. You then call the GetValue<string>() method to read the
connection string from the appsettings.json file.

 
// Setup Host
using IHost host = Host.CreateDefaultBuilder().Build();

// Ask service provider for configuration
IConfiguration config = host.Services.GetRequiredService<IConfiguration>();

// Get connection string
string ConnectString = config.GetValue<string>("ConnectionStrings:DefaultConnection");



TRY IT OUT

Run the application and, if you typed everything in correctly, everything should
work just as it did before.


CREATE A GENERIC DATABASE CONTEXT CLASS

If you look at the code in Listing 4, you can figure out that you don't really
want to write this code repeatedly for each table you wish to read data from.
There are a few problems with this code. First off, there's a lot of it, and
secondly, you have hard-coded references to the Sql* classes. What if you want
to use Oracle, SqlLite, or another database server at some point? It would be
nice if you had a more generic approach.

To achieve this goal, you're going to build a few classes, as shown in Figure 3.
The first class is called DatabaseContext, which is an abstract class that only
contains methods used to create connections, commands, parameters, and data
readers. This class implements the IDisposable interface so it can be wrapped
within a using. The methods for creating the various objects are meant to be
overridden by the SqlServerDatabaseContext, or the OracleDatabaseContext classes
with concrete implementations of the specific objects for their database
provider. For example, the CreateConnection() method in the DatabaseContext
looks like the following:

 
public abstract IDbConnection CreateConnection(string connectString);


The overridden method in the SqlServerDatabaseContext class returns an actual
SqlConnection object as shown in the following code snippet:

 
public override SqlConnection CreateConnection(string connectString) 
{
    return new SqlConnection(connectString);
}


The SqlServerDatabaseContext and OracleDatabaseContext classes are meant to be
generic to work with any SQL Server or Oracle database respectively. The
AdvWorksDbContext class shown in Figure 3 inherits from the
SqlServerDatabaseContext and is meant to work with just the AdventureWorksLT
database. The Repository classes use the services of the
SqlServerDatabaseContext to read, insert, update, and delete data from their
appropriate tables.


Figure 3: Build classes from the most generic to the most specific for a good
architecture.


BUILD THE ABSTRACT DATABASECONTEXT CLASS

Right mouse-click on the Common folder and create a new class named
DatabaseContext as shown in Listing 7. This class has a constructor to which a
connection string must be passed. After all, without a connection string,
there's no way you're going to be able to interact with a database. It has a few
public properties, Init() and Dispose() methods. Most of the properties are
self-explanatory, but the ParameterPrefix property is going to be
implementation-dependent. For example, when using SQL Server, parameters are
prefixed with the at sign (@). However, when using Oracle, parameters are
prefixed with the colon (:). In the Init() method of the
SqlServerDatabaseConnect class is where you initialize the ParameterPrefix to
the @ sign.


LISTING 7: THE ABSTRACT DATABASECONTEXT CLASS PROVIDES A GENERIC INTERFACE FOR
CREATING ADO.NET OBJECTS

#nullable disable

using System.Data;

namespace AdoNetWrapper.Common;

/// <summary>
/// Abstract base class for all
/// ADO.NET database operations
/// </summary>
public abstract partial class DatabaseContext: IDisposable 
{
    public DatabaseContext(string connectString) 
    {
        ConnectionString = connectString;
        Init();
    }

    public string ConnectionString { get; set; }
    public string ParameterPrefix { get; set; }
    public IDbCommand CommandObject { get; set; }
    public IDataReader DataReaderObject { get; set; }

    protected virtual void Init() 
    {
        ParameterPrefix = string.Empty;
    }
}



CREATE A DISPOSE METHOD

Add a Dispose() method (Listing 8) to the DatabaseContext class to check the
various properties to ensure that they're closed and disposed of properly. Check
the DataReaderObject property and if it's not null, call the Close() and
Dispose() methods on this object. Next, check the CommandObject property and
ensure it's not null. If it's not, check the Connection property, and if it's
not null, check whether the Transaction property is not null and dispose of the
Transaction property. Call the Close() and Dispose() on the Connection property
and then finally dispose of the CommandObject. The Dispose() method in this
class runs when you either explicitly call the Dispose() method or the instance
of the DatabaseContext goes out of scope when wrapped within a using.


LISTING 8: YOU MUST HAVE A DISPOSE() METHOD IN ORDER TO ENSURE THERE ARE NO
MEMORY LEAKS

public virtual void Dispose() 
{
    // Close/Dispose of data reader object
    if (DataReaderObject != null) 
    {
        DataReaderObject.Close();
        DataReaderObject.Dispose();
    }

    // Close/Dispose of command object
    if (CommandObject != null) 
    {
        if (CommandObject.Connection != null) 
        {
            if (CommandObject.Transaction != null) 
            {
                CommandObject.Transaction.Dispose();
            }
            CommandObject.Connection.Close();
            CommandObject.Connection.Dispose();
        }
        CommandObject.Dispose();
    }
}


Now that you have the basics of the DatabaseContext class created, add a couple
of method overloads to create connection objects. In this class, it's important
to only use the interface objects such as IDbConnection so this class is
completely database independent.

 
public virtual IDbConnection CreateConnection() 
{
    return CreateConnection(ConnectionString);
}

public abstract IDbConnection CreateConnection(string connectString);


The next two overloaded methods are used to build command objects. Use the
IDbCommand interface as the return values from these methods.

 
public virtual IDbCommand CreateCommand(string sql 
{
    return CreateCommand(CreateConnection(), sql);
}

public abstract IDbCommand CreateCommand(IDbConnection cnn, string sql);


When you need to build parameters for either dynamic SQL or for submitting
stored procedures, you need a couple of methods to create parameters. These
methods use the IDataParameter interface, but when you create the
SqlServerDatabaseContext class, you return an actual SqlParameter object.

 
public abstract IDataParameter CreateParameter(string paramName, object value);

public abstract IDataParameter CreateParameter();


The next method to create helps you retrieve any output parameters returned from
a stored procedure. This method GetParameter() returns an IDataParameter object,
but is an abstract method so you must override it in the SqlServerDataContext,
or OracleDataContext class you create.

 
public abstract IDataParameter GetParameter(string paramName);


The next two methods are used to create an instance of a data reader object.
Ensure you're returning the IDataReader interface from these methods in this
class.

 
public virtual IDataReader CreateDataReader() 
{
    return CreateDataReader(CommandObject, CommandBehavior.CloseConnection);
}

public virtual IDataReader CreateDataReader(CommandBehavior cmdBehavior) 
{
    return CreateDataReader(CommandObject, cmdBehavior);
}


You need one additional method for creating a data reader. This method accepts a
command object and the CommandBehavior enumeration. The default for the
CommandBehavior is to close the connection when the reader is closed, but the
optional parameter allows you to change that behavior if needed.

This method is the one that is called by the previous two methods you created.
It opens the connection, and calls the ExecuteReader() method on the command
object to build the actual data reader object. The data reader object created is
placed into the DataReaderObject property.

 
public virtual IDataReader CreateDataReader(IDbCommand cmd, CommandBehavior cmdBehavior = CommandBehavior.CloseConnection)
{
    // Open Connection
    cmd.Connection.Open();

    // Create DataReader
    DataReaderObject = cmd.ExecuteReader(cmdBehavior);
    return DataReaderObject;
}



CREATE A SQL SERVER DATABASE CONTEXT CLASS

Now that you have the generic DatabaseContext class created, build the
SqlServerDatabaseContext class. This class supplies the concrete Sql* objects to
work with any SQL Server database. Right mouse-click on the Common folder and
create a new class named SqlServerDatabaseContext. Add the code shown in Listing
9. This class has a constructor that must accept a connection string, and it
passes it directly to the base class. In the Init() method, which is called from
the base class, is where you set the ParameterPrefix property to the at (@)
sign, which will be used by the methods dealing with parameters.


LISTING 9: THE SQLSERVERDATABASECONTEXT CLASS INHERITS FROM THE DATABASECONTEXT
CLASS AND INITIALIZES THE PARAMETERPREFIX PROPERTY IN THE INIT() METHOD.

#nullable disable

using System.Data;
using System.Data.SqlClient;

namespace AdoNetWrapper.Common;

/// <summary>
/// Database context using ADO.NET
/// for SQL Server Databases
/// </summary>
public partial class SqlServerDatabaseContext: DatabaseContext 
{
    public SqlServerDatabaseContext(string connectString) : base(connectString) { }

    protected override void Init() 
    {
        base.Init();

        ParameterPrefix = "@";
    }
}


Let's now add the various overrides to the methods that create the concrete
implementations of the SQL objects. Start by overriding the CreateConnection()
method to return a SqlConnection object. You only need to override the one
method as the other method in the base class simply calls this one.

 
public override SqlConnection CreateConnection (string connectString) 
{
    return new SqlConnection(connectString);
}


Override the one method for creating a command object because the other two
methods in the base class call this one. In this method, set the CommandObject
property with the new SqlCommand() object you create. You need to have this
command object property public so you can add parameters to it and dispose of it
when the database context class goes out of scope.

 
public override SqlCommand CreateCommand(IDbConnection cnn, string sql) 
{
    CommandObject = new SqlCommand(sql, (SqlConnection)cnn);
    CommandObject.CommandType = CommandType.Text;
    return (SqlCommand)CommandObject;
}


The next methods to override are the ones that create new parameter objects. The
first method accepts a parameter name and the value to assign to the parameter.
It checks to see if the paramName parameter starts with the value in the
ParameterPrefix property or not. If it doesn't, then the ParameterPrefix is
added to the paramName parameter before the SqlParameter object is created. The
second method returns an empty SqlParameter object.

 
public override SqlParameter CreateParameter(string paramName, object value) 
{
    if (!paramName.StartsWith(ParameterPrefix)) 
    {
        paramName = ParameterPrefix + paramName;
    }
    return new SqlParameter(paramName, value);
}

public override SqlParameter CreateParameter() 
{
    return new SqlParameter();
}


Some stored procedures return an OUTPUT parameter. After submitting a command,
you can retrieve one of the OUTPUT parameters by querying the appropriate
parameter object and requesting the value. The GetParameter() method shown below
accepts a parameter name to locate, ensures it starts with the appropriate
prefix, and then accesses the Parameters property on the CommandObject to
retrieve the value.

 
public override SqlParameter GetParameter(string paramName) 
{
    if (!paramName.StartsWith(ParameterPrefix)) 
    {
        paramName = ParameterPrefix + paramName;
    }
    return ((SqlCommand)CommandObject).Parameters[paramName];
}


The last method to override is the one that creates the SqlDataReader object.
Pass in an instance of a command object and optionally, the CommandBehavior to
use after closing the reader.

 
public override SqlDataReader CreateDataReader(IDbCommand cmd, CommandBehavior cmdBehavior = CommandBehavior.CloseConnection) 
{
    // Open Connection
    cmd.Connection.Open();

    // Create DataReader
    DataReaderObject = cmd.ExecuteReader(cmdBehavior);

    return (SqlDataReader)DataReaderObject;
}


You're not going to create the OracleDatabaseContext class in these articles.
However, you follow the same procedures I did in this section for building the
SqlServerDatabaseContext class, just substituting the appropriate Oracle*
classes for the Sql* classes used here.


MODIFY THE SEARCH() METHOD TO USE THE SQL SERVER DATABASE CONTEXT CLASS

Now that you have built the generic SqlServerDatabaseContext class, you should
use this to perform the searching. Open the ProductRepository.cs file and remove
the using System.Data.SqlClient;* statement. Modify the Search() method to look
like the following code.

 
public virtual List<Product> Search(string connectString, string sql) 
{
    List<Product> ret;

    using SqlServerDatabaseContext dbContext = new(connectString);

    // Create Command Object with SQL
    dbContext.CreateCommand(sql);

    ret = BuildEntityList(dbContext.CreateDataReader());

    return ret;
}


As you can see, this code is much simpler, but still takes advantage of the
using to make sure all resources are appropriately disposed of. You first create
an instance of the SqlServerDatabaseContext class as part of the using. You then
create a command object with the SQL to submit. Finally, you create the data
reader and pass it to the BuildEntityList() method.


TRY IT OUT

Run the console application again, and you should see the same number of product
objects displayed as you saw earlier.


GENERICALLY CREATING AN ENTITY LIST

The code for working with a database has been greatly simplified, but let's now
work on the BuildEntityList() method. This method is hard-coded to build just
Product objects. Let's make this a generic method to which you can pass any type
of object to build a collection out of.

You're going to use .NET Reflection to build the objects. Yes, I know what
you're thinking: Isn't reflection very slow? It used to be, but starting with
.NET 3.5, Microsoft did a serious overhaul of the reflection classes to ensure
that it would be fast enough to support the new Entity Framework they were
releasing at the same time. In fact, the code shown in Listing 10 is like the
code the Entity Framework uses for populating entity collections.


LISTING 10: USE REFLECTION TO READ THE DATA AND CREATE A LIST OF ANY TYPE OF
ENTITY CLASSES

protected virtual List<TEntity> BuildEntityList<TEntity>(IDataReader rdr) 
{
    List<TEntity> ret = new();
    string columnName;

    // Get all the properties in <TEntity>
    PropertyInfo[] props = typeof(TEntity).GetProperties();

    // Loop through all rows in the data reader
    while (rdr.Read()) 
    {
        // Create new instance of Entity
        TEntity entity = Activator.CreateInstance<TEntity>();

        // Loop through columns in data reader
        for (int index = 0; index < rdr.FieldCount; index++) 
        {
            // Get field name from data reader
            columnName = rdr.GetName(index);

            // Get property that matches the field name
            PropertyInfo col = props.FirstOrDefault(col => col.Name == columnName);

            if (col != null) 
            {
                // Get the value from the table
                var value = rdr[columnName];
                // Assign value to property if not null
                if (!value.Equals(DBNull.Value)) 
                {
                    col.SetValue(entity, value, null);
                }
            }
        }
        // Add new entity to the list
        ret.Add(entity);
    }

    return ret;
}


Open the ProductRepository.cs file and add a new using statement.

 
using System.Reflection;


Turn the Search() method into a generic method by passing in a type parameter,
<TEntity>, as seen in the code below.

 
public virtual List<TEntity> Search<TEntity>(string connectString, string sql) 
{
    List<TEntity> ret;

    using SqlServerDatabaseContext dbContext = new(connectString);
    dbContext.CreateCommand(sql);

    ret = BuildEntityList<TEntity>(dbContext.CreateDataReader());

    return ret;
}


When you make the call to the Search() method from the Program.cs file, specify
the name of the class to create as the list of objects. In the code shown below,
you pass in Product to the <TEntity> type parameter.

 
List<Product> list = repo.Search<Product>(ConnectString, Sql);



MAKE BUILDENTITYLIST() A GENERIC METHOD

Modify the BuildEntityList() to be a generic method as shown in Listing 10. Add
the <TEntity> type parameter like you did in the Search() method. The
SqlDataReader object has a GetName() method, which retrieves the current column
name via an index. You need to be able to map this column name to the same
property name in the type of class passed in. Use the typeof() method on the
type parameter passed in and then call the GetProperties() method to return all
the properties in the class. You now have all the property names, and you can
get the column name from the data reader. All you need to do now is to map the
value from the data reader into the property of the class.

Loop through each row in the Product table and each time through, create a new
instance of a Product object using the Activator.CreateInstance() method. Loop
through each column in the data reader object and set the field name into the
variable named columnName. This code assumes that the column name in the table
exactly matches the property name in the class. Later you're going to learn how
to map column names to property names using an attribute. Look up the property
by calling the props.FirstOrDefault() method to locate where the property name
is the same as the column name.

Advertisement


If a property is found with the same name, check to see if the value in the
column is not a DbNull value. If it isn't, use the reflection SetValue() method
on the property to set the value on the newly created instance of the Product
class to the value from the column in the table. Add the new instance of the
Product class to the collection and repeat this process until all rows in the
table have been processed.


TRY IT OUT

Open the Program.cs file and modify the call to the Search() method to look like
the following code.

 
List<Product> list = repo.Search<Product>(ConnectString, Sql);


Run the console application and, once again, you should see the same product
objects displayed.


HANDLE THE [COLUMN] ATTRIBUTE ON THE ENTITY CLASS

One of the problems with the way the BuildEntityList() method is written, is
that it assumes the column name is the same as the property name in your class.
There are many times when you might have column names with characters that
aren't valid in property names. Or maybe your column names are all upper-case
and you want your properties to be camel case. Or maybe you just want your
property name to be more descriptive than the column name is. Whatever, the
reason, simply add a [Column] attribute above the property you want to be
different. Open the Product.cs file and rename the ProductID and the Name
properties to be Id and ProductName respectively.

 
public int Id { get; set; }
public string ProductName { get; set; }


Above the Id property, add the [Column] attribute to specify the name of the
actual column in the table.

 
[Column("ProductID")]
public int Id { get; set; }


Do the same for the renamed ProductName property, and add the [Column] attribute
to be the actual name of the column in the table.

 
[Column("Name")]
public string ProductName { get; set; }



CHECK FOR THE [COLUMN] ATTRIBUTE WHEN READING DATA

Add some code to the BuildEntityList() to take advantage of the [Column]
attribute. Open the ProductRepository.cs file and add a using statement at the
top of the file.

 
using System.ComponentModel.DataAnnotations.Schema;


In the BuildEntityList() method, modify the code within the loop, as shown in
Listing 11. The first part of this code is the same. The difference is that
you're checking to see if the variable col is a null, and if it is, that means
the column name didn't match any property names. Call the FirstOrDefault()
method again on the props collection, but you're now looking for any column that
has a [Column] attribute, and whether the Name property in the [Column]
attribute matches the column name from the data reader. If you find it, the col
property is now set to a valid property, and now you can set the value into that
property.


LISTING 11: ADD CODE TO CHECK FOR THE [COLUMN] ATTRIBUTE

for (int index = 0; index < rdr.FieldCount; index++) 
{
    // Get field name from data reader
    columnName = rdr.GetName(index);
    
    // Get property in entity that matches the field name
    PropertyInfo col = props.FirstOrDefault(col => col.Name == columnName);

    if (col == null) 
    {
        // Is column name in a [Column] attribute?
        col = props.FirstOrDefault(c => c.GetCustomAttribute<ColumnAttribute>()?.Name == columnName);
    }

    if (col != null) 
    {
        // Get the value from the table
        var value = rdr[columnName];
        
        // Assign value to the property if not null
        if (!value.Equals(DBNull.Value)) 
        {
            col.SetValue(entity, value, null);
        }
    }
}



TRY IT OUT

Run the console application again and you should see the same product objects
displayed.


BUILD SELECT STATEMENT FROM PROPERTIES

If you take a look at the SalesLT.Product table in the AdventureWorksLT
database, you'll see that there are many more columns than properties in the
Product class. This means that sending the SELECT * FROM SalesLT.Product SQL
statement to the server returns much more data than is needed. Obviously, a less
wasteful approach is needed.

Actually, there's no need to pass in a SQL statement to the Search() method at
all, as the statement can be inferred from the properties within the Product
class. So, let's eliminate the SQL statement and read the property names (and
any [Column] attributes) to build the SELECT statement to send to the database.

Let's also add some additional functionality to check whether a [NotMapped]
attribute has been added to any properties. Sometimes you need additional
properties in your entity class, but those properties aren't mapped to any
column in the table, this is what the [NotMapped] attribute is for. To
illustrate the [NotMapped] attribute, open the Product.cs file and add the
following property somewhere within this class.

 
[NotMapped]
public bool IsSelected { get; set; }


Instead of gathering the properties of the class within the BuildEntityList()
method, let's move this functionality into a new method. The new method uses
reflection to collect the column name and the property information for each
property in the entity class and return that as a collection of objects. Create
a class to hold that information by right mouse-clicking on the Common folder
and adding a new class called ColumnMapper as shown in the code snippet below.

 
#nullable disable

using System.Reflection;

namespace AdoNetWrapper.Common;

public class ColumnMapper
{
    public string ColumnName { get; set; }
    public PropertyInfo PropertyInfo { get; set; }
}


Open the ProductRepository.cs file and add a few new properties to this class.
You can see the collection of ColumnMapper classes that are going to hold the
information about each property in the Product entity class. There are also
properties for the schema and table name of the table this repository class is
working with. The SQL property will hold the last statement used to retrieve
data from the table.

 
public string SchemaName { get; set; }
public string TableName { get; set; }
public List<ColumnMapper> Columns { get; set; }
public string SQL { get; set; }


Add a method named Init() to the ProductRepository class to initialize some of
these properties to valid start values.

 
protected virtual void Init() 
{
    SchemaName = "dbo";
    TableName = string.Empty;
    SQL = string.Empty;
    Columns = new();
}


Add a constructor to the ProductRepository() class to call the Init() method.

 
public ProductRepository() 
{
    Init();
}



ADD A METHOD TO BUILD A COLLECTION OF COLUMNS

Let's add a new method named BuildColumnCollection() to build the collection of
ColumnMapper objects, as shown in Listing 12, and remove this functionality from
the BuildEntityList() method. Once this collection is built, you're going to be
able to do two things: build the SELECT statement to submit to retrieve the data
and map the data from the data reader to the properties in the class.


LISTING 12: ADD A METHOD TO BUILD A COLLECTION OF COLUMN INFORMATION FROM THE
PROPERTIES OF YOUR ENTITY CLASS

protected virtual List<ColumnMapper> BuildColumnCollection<TEntity>() 
{
    List<ColumnMapper> ret = new();
    ColumnMapper colMap;

    // Get all the properties in <TEntity>
    PropertyInfo[] props = typeof(TEntity).GetProperties();

    // Loop through all properties
    foreach (PropertyInfo prop in props) 
    {
        // Is there a [NotMapped] attribute?
        NotMappedAttribute nm = prop.GetCustomAttribute<NotMappedAttribute>();
        
        // Only add properties that map to a column
        if (nm == null) 
        {
            // Create a column mapping object
            colMap = new() 
            {
                // Set column properties
                PropertyInfo = prop,
                ColumnName = prop.Name
            };

            // Is column name in [Column] attribute?
            ColumnAttribute ca = prop.GetCustomAttribute<ColumnAttribute>();
            if (ca != null && !string.IsNullOrEmpty(ca.Name)) 
            {
                // Set column name from [Column] attr
                colMap.ColumnName = ca.Name;
            }

            // Create collection of columns
            ret.Add(colMap);
        }
    }

    return ret;
}


In this method, call the GetProperties() method on the TEntity type passed in.
Once you have all the properties from the class, loop through each one and check
to see if there's a [NotMapped] attribute on each column, if there is, bypass
that column and don't add it to the collection of ColumnMapper objects.
Otherwise, create a new ColumnMapper object and set the PropertyInfo property to
the current PropertyInfo object, and the property name to the ColumnName
property.

Next, check for a [Column] attribute on the property. If the [Column] attribute
exists, make sure the Name property has been filled in on that attribute. It's
possible to just specify a type for the column and not set the name for the
column when using the [Column] attribute. If the Name property exists, replace
the ColumnName property in the ColumnMapper object. Finally, add the
ColumnMapper object to the collection to be returned, and repeat this process
until all properties have been processed.


ADD A METHOD TO SET TABLE AND SCHEMA PROPERTIES

To build the SELECT statement, you need the schema name and table name in the
database you're building the SQL for. By default, the SchemaName property is
initialized to “dbo,” as that's the most common schema in SQL Server. The
TableName property is set to the entity class name. However, most entity classes
use the [Table] attribute to specify the table name and optionally the schema
name. Create a method in your RepositoryBase class named SetTableAndSchemaName
and add the following code to this method.

 
protected virtual void SetTableAndSchemaName(Type typ) 
{
    // Is there is a [Table] attribute?
    TableAttribute table = typ.GetCustomAttribute<TableAttribute>();
    
    // Assume table name is the class name
    TableName = typ.Name;
    if (table != null) 
    {
        // Set properties from [Table] attribute
        TableName = table.Name;
        SchemaName = table.Schema ?? SchemaName;
    }
}



ADD METHOD TO BUILD A SELECT STATEMENT

You're now going to create a method called BuildSelectSql() to call the
BuildColumnCollection() method you just wrote. After building the column
collection, loop through it and use the ColumnName property to create a SELECT
statement to submit to the database. Use a StringBuilder object to create the
SELECT statement, so bring in the System.Text namespace by adding a using
statement at the top of the ProductRepository class.

 
using System.Text;


Add the BuildSelectSql() method to the repository class, as shown in Listing 13.
Call the BuildColumnCollection() method to fill in the Columns property. Call
the SetTableAndSchemaName() method to set the TableName and SchemaName
properties. Build the SELECT statement by iterating over the Columns collection
and adding each ColumnName property to the SELECT statement.


LISTING 13: ADD A METHOD TO BUILD THE SELECT SQL STATEMENT TO SUBMIT TO THE
DATABASE

protected virtual string BuildSelectSql<TEntity>() 
{
    Type typ = typeof(TEntity);
    StringBuilder sb = new(2048);
    string comma = string.Empty;

    // Build Column Mapping Collection
    Columns = BuildColumnCollection<TEntity>();

    // Set Table and Schema properties
    SetTableAndSchemaName(typ);

    // Build the SELECT statement
    sb.Append("SELECT");
    foreach (ColumnMapper item in Columns) 
    {
        // Add column
        sb.Append($"{comma} [{item.ColumnName}]");
        comma = ",";
    }
    
    // Add 'FROM schema.table'
    sb.Append($" FROM {SchemaName}.{TableName}");

    return sb.ToString();
}



MODIFY THE SEARCH() METHOD

Modify the Search() method as shown in Listing 14.


LISTING 14: THE BUILDENTITYLIST() IS NOW MUCH SIMPLER BECAUSE THE COLUMN
INFORMATION IS PRE-BUILT

protected virtual List<TEntity>BuildEntityList<TEntity>(IDataReader rdr) 
{
    List<TEntity> ret = new();

    // Loop through all rows in the data reader
    while (rdr.Read()) 
    {
        // Create new instance of Entity
        TEntity entity = Activator.CreateInstance<TEntity>();

        // Loop through columns collection
        for (int index = 0; index < Columns.Count; index++) 
        {
            // Get the value from the reader
            var value = rdr[Columns[index].ColumnName];

            // Assign value to the property if not null
            if (!value.Equals(DBNull.Value)) 
            {
                Columns[index].PropertyInfo.SetValue(entity, value, null);
            }
        }

        // Add new entity to the list
        ret.Add(entity);
    }

    return ret;
}


Remove the string sql parameter passed in. Add the call to the BuildSelectSql()
method and place the resulting SELECT statement into the SQL property of this
class. Pass the SQL property to the CreateCommand() method. Then call the
BuildEntityList() method as you did before.

 
public virtual List<TEntity> Search<TEntity>(string connectString) 
{
    List<TEntity> ret;

    // Build SELECT statement
    SQL = BuildSelectSql<TEntity>();

    using SqlServerDatabaseContext dbContext = new(connectString);
    dbContext.CreateCommand(SQL);

    // REST OF THE CODE HERE

    return ret;
}



MODIFY THE BUILDENTITYLIST METHOD

Because you added the code to check for the [Column] attribute and to ignore any
columns with the [NotMapped] attribute, the BuildEntityList() method is now much
simpler. Replace the BuildEntityList() method with the code in Listing 14.


TRY IT OUT

Open the Program.cs file, remove the Sql declaration and assignment, and remove
it from the second parameter passed to the Search() method.

 
List<Product> list = repo.Search<Product>(ConnectString);


Run the console application and you should see the same product objects
displayed.


SUMMARY

In this article, you learned how to use .NET reflection to create a generic way
to build a collection of entity objects from any table in a database. You
learned how to use the [Column] and the [NotMapped] attributes to set the column
name or ignore properties in your entity class. An abstract base class was
illustrated from which you then provided a concrete implementation for accessing
any SQL Server database. This same base class could then be used as the basis
for any other database context classes you need to access Oracle, SQL Lite, or
other databases that have a .NET provider.

In the next article, you're going to refactor the repository class so it's much
more generic. You'll also add search capabilities to your repository class,
retrieve a scalar value, work with multiple result sets, and call stored
procedures.


GETTING THE SAMPLE CODE

You can download the sample code for this article by visiting www.CODEMag.com
under the issue and article, or by visiting www.pdsa.com/downloads. Select
“Articles” from the Category drop-down. Then select " Simplifying ADO.NET Code
in .NET 6 - Part 1" from the Item drop-down.


GET .NET 6 HELP FOR FREE

How does a FREE hour-long CODE Consulting virtual meeting with our expert .NET
consultants sound? Yes, FREE. No strings. No commitment. No credit cards.
Nothing to buy. For more information, visit www.codemag.com/consulting or email
us at info@codemag.com.


THIS ARTICLE WAS FILED UNDER:

 * .NET 6
 * ADO.NET




THIS ARTICLE WAS PUBLISHED IN:



HAVE ADDITIONAL TECHNICAL QUESTIONS?

Get help from the experts at CODE Magazine - sign up for our free hour of
consulting!

Contact CODE Consulting at techhelp@codemag.com.




CONTACT

6605 Cypresswood Dr.

Suite 425

Houston TX 77379

USA
Voice
+1 (832) 717-4445

Fax
+1 (832) 717-4460

Email: info@codemag.com
1993 - 2024 EPS Software Corp.
Privacy Policy


SITE MAP


CONSULTING

 * Consulting Home
 * Services & Technologies
 * Artificial Intelligence (AI)
 * Cloud Computing
 * Custom Application Development
 * Executive Briefing (AI)
 * Low-Code/No-Code
 * Cyber Security
 * Copilots in Your Apps!
 * Project Rescue
 * CODE Labs
 * Business Document Copilot
 * Legacy Conversion and Maintenance
 * Free Hour of Consulting
 * VFP Conversion
 * Energy Software
 * Contact Us


MAGAZINE

 * Magazine Home
 * CODE Focus Magazine
 * All Issues
 * Subscribe
 * Mobile
 * My (Digital) Magazines
 * Where is my Magazine?
 * My Subscriber Account
 * Advertise
 * Write


FRAMEWORK

 * Framework Home
 * Get Started & Documentation
 * Download
 * Support & Services


TRAINING

 * Training Home
 * State of AI
 * CODE Presents
 * State of .NET
 * Classes
 * Mentoring
 * Lunch with CODE


STAFFING

 * Staffing Home
 * Our Services
 * About Us
 * Careers


VFP CONVERSION

 * VFP Conversion Home
 * Services
 * Tools
 * Articles
 * Fox End of Life
 * Contact Us




We use cookies to make this site work properly. For more information, see our
Privacy Policy. Do you agree to us using cookies? Sure, I know how this works! -
No way. Get me out of here!