BassUtils.Oracle - Helpers for Calling Oracle from C#

Oracle, eh?

My new job requires me to deal with Oracle for the first time in anger. I’ve found Oracle to be both impressive (PL/SQL is far more powerful than T-SQL) and frustrating. When things go wrong the error messages are often confusing, at best, and at worst positively misleading.

Most of my time has been spent trying to get data in and out of Oracle using C#, in particular I have been looking at doing bulk-loads of classes, in other words, how to get multiple records of data into and out of Oracle ergonomically. I didn’t want to use bulk-insert techniques because I wanted to avoid creating lots of staging tables. Instead I just wanted to pass a set of records into a stored procedure. My investigations were not helped by the fact that there are multiple, very similar, ways of doing things in Oracle, and it is quite possible to create stored procedures and data types in the Oracle server that you can’t call via the Oracle C# client library.

After much teeth gnashing, I have finally arrived at a set of techniques which allows me to pass a table of structured data to an Oracle stored procedure as a single parameter or get such a table back out again - in MS SQL server terms, it’s a Table Valued Parameter. This involves using Oracle User-Defined Data Types.

There are two alternative techniques which can also be used. Array Binding is a purely client-side technique which allows you to execute a SQL statement multiple times using a single network call. Associative Arrays are a a PL/SQL feature which you can call from C#, but you are limited to passing arrays of scalar types (ints, strings etc.)

NuGet Package and Examples Project

I have packaged up my lessons learned in a new NuGet package, BassUtils.Oracle which contains many helper methods for creating OracleParameters in the correct way - this is about 80% of the battle. The remainder is knowing what you can and can’t do in PL/SQL and which is compatible with calling it from C#, and I have documented what I have learnt so far in the comments.

The package has a README, but I recommend cloning the GitHub Repository and walking through the BassUtils.OracleExamples project which shows how to use all 3 of the above techniques against a demo Oracle database - a SQL script to create the database is included.

The OracleExamples App

Hopefully these examples will help others avoid some of the torture I have experienced over the last few weeks :-).

If you don’t have access to a ‘play’ Oracle instance you can install Oracle using Docker. Again, the CreateObjects.sql script has instructions for what you need to do after you’ve pulled the Oracle image.

A Brief Taster

Let’s walk through passing a table of objects and an associative array to a function and getting a table of objects back. This is copied directly from the BassUtils.OracleExamples project, so if you cloned the project you can see it in action.

First we define some schema-level types. Unfortunately they have to be schema-level to use UDTs from C#, that is one of the restrictions that are so frustrating.

CREATE TYPE objPerson AS OBJECT
  (
  Age INTEGER,
  FirstName VARCHAR2(255),
  LastName VARCHAR2(255),
  Note VARCHAR2(255),
  UpdatedDate DATE
  );

-- Create a schema-level collection type (nested table type) of another schema-level type.
CREATE TYPE tblPerson AS TABLE OF objPerson;

Let’s also define an associative array type. These must be defined at package level:

CREATE PACKAGE COLLECTION_TYPES IS
	...
	TYPE arrNumber IS TABLE OF NUMBER INDEX BY BINARY_INTEGER;
	...
END COLLECTION_TYPES;

Given the above we can define the following function. And this time it can be defined at schema-level or package level!

FUNCTION FunctionTakingTablesOfObjects
	(
	pPeople tblPerson,
	pAgeMultipliers COLLECTION_TYPES.arrNumber
	) RETURN tblPerson
AS
	vPeople tblPerson := pPeople;
BEGIN
  	-- Extend the table by 1 element so that we can add the extra person. 
	vPeople.EXTEND();
	vPeople(vPeople.LAST) := NEW objPerson
		(
		Age => 19,
		FirstName => 'Zaphod',
		LastName => 'Beeblebrox',
		Note => 'Galactic President',
		UpdatedDate => SYSDATE
		);
		
	FOR idx IN vPeople.FIRST..vPeople.LAST LOOP
    	vPeople(idx).Age := vPeople(idx).Age * pAgeMultipliers(Idx);
    	vPeople(idx).LastName := UPPER(vPeople(idx).LastName);
  	END LOOP;
		
	RETURN vPeople;
END;	

Here the parameter pPeople is a table of objects (a TVP) and pAgeMultipliers is an associative array - just an array of numbers. I arranged things in the C# such that when this function is called there are enough age multipliers to multiply each of the people’s ages.

Using BassUtils.Oracle helper methods, we can call this function like so:

using var conn = Db.MakeConnection();
using var cmd = conn.CreateCommand();
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandText = "DemoUser.MyPackage.FunctionTakingTablesOfObjects";

// The return parameter is a table of UDTs.
var returnParam = cmd.Parameters.AddReturnUdtArray("DemoUser.tblPerson");

// The first parameter is a table of PersonRecord. This is expressed in the special
// C# wrapper class that we had to write to tell Oracle how to serialize these things.
var people = new PersonRecordArray() { Rows = People.MakeFuturama() };
cmd.Parameters.AddUdtArray("pPeople", "DemoUser.tblPerson", people);

// The second parameter is an associative array of decimals, the type being declared in a package.
// This is the only way to pass an array of primitive scalars via ODP.Net.
cmd.Parameters.AddAssociativeArray("pAgeMultipliers", new[] { 1.0m, 1.1m, 1.2m, 4.4m });

The magic here is in the helper function AddReturnUdtArray, AddUdtArray and AddAssociativeArray which do the heavy lifting of creating properly setup OracleParameter objects.

The comments in the code allude to a the C# classes PersonRecord and PersonRecordArray. The code for these is too long to show here, but can be seen in the repo. This class is what Oracle ODP.Net uses to map to and from the objPerson and tblPerson types we created in the SQL above. It’s very long-winded and mechanical (I guess the Oracle engineers never heard of POCO) but I hope to write another NuGet package soon which will generate them from a DSL.

Final Thoughts

Like most things, it’s easy once you know how - and have captured that knowledge in a package that simplifies the task. It’s probable that there are more techniques and helpers which could be added to BassUtils.Oracle - I would welcome pull requests.

I Know Oracle

Well, a bit at least!

comments powered by Disqus