1

I'm trying to send a List Object from my C# WebService method over to my stored procedure in Oracle.

Before posting here, I've tried all suggested duplicate links. Here's what I've accomplished so far:

  • Success: In C#, I can pass my List values from my HTML page over to my WebService method.
  • Success: In Oracle, I have created a Table, Object Type, Table Type, and Stored Procedure to accept the List values. I was able to test this using an Anonymous block and sample data.
  • Problem: I cannot get to pass my List values from my C# WebMethod over to my Oracle Stored Procedure.

I'm currently using the following setup:

  • Visual Studio 2017
  • .NET Framework 4.6.1
  • Oracle.ManagedDataAccess 18.6.0

Keep in mind that the version of Oracle.ManagedDataAccess 18.6.0 does NOT contain the OracleDbType.Array as suggested in the older examples.


        public class Automobile
        {
            public string Make { get; set; }
            public string Model { get; set; }
            public string Year { get; set; }
            public string Country { get; set; }
        }
        using Oracle.ManagedDataAccess.Client;
        using Oracle.ManagedDataAccess.Types;

        [WebMethod(EnableSession = true)]
        [ScriptMethod(ResponseFormat = ResponseFormat.Json)]
        public string InsertCars(List<Automobile> myCars, int userID)
        {
            DataSet dataSet = new DataSet();

            using (OracleConnection sqlConnection = new OracleConnection(OracleDBConnection))
            {
                using (OracleCommand sqlCommand = new OracleCommand("sp_InsertCars", sqlConnection))
                {
                    sqlConnection.Open();
                    sqlCommand.CommandType = CommandType.StoredProcedure;

                    sqlCommand.Parameters.Add(
                        new OracleParameter
                        {
                            CollectionType = OracleCollectionType.PLSQLAssociativeArray,
                            Direction = ParameterDirection.Input,
                            ParameterName = "p_CarList",
                            UdtTypeName = "tt_Automobile",
                            Size = myCars.Count,
                            Value = myCars.ToArray()
                        }
                    );

                    sqlCommand.Parameters.Add(
                        new OracleParameter
                        {
                            OracleDbType = OracleDbType.Int32,
                            Direction = ParameterDirection.Input,
                            ParameterName = "p_UserID",
                            Value = userID
                        }
                    );

                    sqlCommand.Parameters.Add(
                        new OracleParameter
                        {
                            OracleDbType = OracleDbType.RefCursor,
                            Direction = ParameterDirection.Output,
                            ParameterName = "o_Cursor"
                        }
                    );

                    using (OracleDataAdapter sqlAdapter = new OracleDataAdapter(sqlCommand))
                    {
                        sqlAdapter.SelectCommand = sqlCommand;
                        sqlAdapter.Fill(dataSet);
                    }
                }

                return JsonConvert.SerializeObject(dataSet);
            }
        }

        CREATE TABLE tblCars
        (
            RecordID INT GENERATED BY DEFAULT  AS IDENTITY NOMINVALUE NOMAXVALUE INCREMENT BY 1 START WITH 1 NOCACHE NOCYCLE NOORDER,
            Make     NVARCHAR2(100)   NULL,
            Model    NVARCHAR2(100)   NULL,
            Year     NVARCHAR2(4)     NULL,
            Country  NVARCHAR2(100)   NULL,
            UserID   INT              NULL
        );

        CREATE OR REPLACE TYPE ot_Automobile AS OBJECT
        ( 
            Make varchar2(100),
            Model varchar2(100),
            Year varchar2(4),
            Country varchar2(100)
        );

        CREATE OR REPLACE TYPE tt_Automobile AS TABLE OF ot_Automobile;

        CREATE OR REPLACE PROCEDURE sp_InsertCars 
        (
            p_CarList In tt_Automobile,
            p_UserID In integer,
            o_Cursor Out Sys_RefCursor
        )
        AS
        BEGIN
            DBMS_Output.Enable;

            For RowItem In (Select * From Table(p_CarList))
            Loop
            Insert Into tblCars 
            (
                Make, 
                Model, 
                Year, 
                Country, 
                UserID
            )
            Values(
                RowItem.Make,
                RowItem.Model,
                RowItem.Year,
                RowItem.Country,
                p_UserID
            );        
            End Loop;

            -- Return our results after insert
            Open o_Cursor For
            Select Make, Model, Year, Country From tblCars Where UserID = p_UserID;

        EXCEPTION
            When Others Then
            DBMS_Output.Put_Line('SQL Error: ' || SQLERRM);        

        END sp_InsertCars;

        COMMIT
        /

The result should allow me to pass my array Object from my WebService WebMethod over to my Oracle stored procedure and then loop through each item of the array to perform an Insert.

Here's an example of the data I'm trying to pass in.

Sample data being passed

Ninja Coder
  • 35
  • 1
  • 1
  • 6
  • Please refer below link https://www.codeproject.com/Articles/33829/How-to-use-Oracle-11g-ODP-NET-UDT-in-an-Oracle-Sto – User12345 May 08 '19 at 12:59
  • We typically use associative arrays. There are plenty of answers on SO around this already. One example of that is the second part of the answer [here](https://stackoverflow.com/a/16212803/685760) and another site offers [this example](http://www.vickram.me/passing-arrays-to-oracle-stored-procedure-from-c). – Mr Moose May 08 '19 at 13:06
  • But here he is using nested table. – User12345 May 08 '19 at 13:07
  • Hi @MrMoose, I spent some time going through your provided examples, but they both seem to be passing in a single dimensional array, whereas I'm trying to pass in a 2 dimensional array list of values. I'll be glad to look at any other examples that you may have. – Ninja Coder May 08 '19 at 17:17
  • I was searching through some other examples I've referenced in the past, and came across [this](https://stackoverflow.com/a/9701154/685760) too. It passes multiple dimensions. Hopefully that is a better one for you to follow. – Mr Moose May 08 '19 at 20:24
  • Thanks for the link @MrMoose. It seems that they are suggesting to split the 2 dimensional array in to multiple single dimensional arrays. I'll keep reading the Oracle documentation on the new `Oracle.ManagedDataAccess.Client`. Will report back with my findings. I did however tried asking Tom at Oracle, but he's still out feeding his 23 elephants. :-) – Ninja Coder May 10 '19 at 11:53

2 Answers2

1

this answer depends on commercial package, but if you're as desperate as i am, it's a lifesaver for very reasonable $300 (circa 2020-Q4)... scouts honor, i'm no shill

DevArt's Oracle provider makes elegant work of passing lists of objects to stored procs... it really works... .net core 3.1 compatible, tested on linux, no dependencies on native oracle client ... see my take on a working console app sample below based on the linked forum post

DevArt's "OracleType.GetObjectType()" API makes the UDT marshalling part of this incredibly trivial for us... way less to grok than the existing unmanaged ODP table type support samples i've seen out there for years

strategic consideration - if you already have a sizable code base on an oracle provider, consider just leaving all that code as-is and only take on regression testing this new dependency where the specialized table type support is actually needed

short and sweet sample for quick digestion

using System;
using System.Data;
using Devart.Data.Oracle;
namespace ConsoleApp1
{
    class Program
    {
        private static int oraTable;

        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");

            //good docs:
            //direct connection: https://www.devart.com/dotconnect/oracle/docs/StoredProcedures-OracleCommand.html
            //linux licensing: https://www.devart.com/dotconnect/oracle/docs/?LicensingStandard.html
            using OracleConnection db = new OracleConnection("{insert yours}");

            //devart trial licensing nutshell... on WINDOWS, download & run their installer...
            //the only thing you really need from that install is the license key file dropped here: 
            //   %programdata%\DevArt\License\Devart.Data.Oracle.key
            // then just go nuget the "Devart.Data.Oracle" package reference
            //on Windows, the trial license file gets automatically picked up by their runtime
            //if you're on Linux, basically just read their good instructions out there
            //i've just tested it on WSL so far and plan to try on Azure linux app svc within next day
            //db.ConnectionString = "license key=trial:Devart.Data.Oracle.key;" + db.ConnectionString;

            db.Direct = true; //nugget: crucial!! https://www.devart.com/dotconnect/oracle/docs/DirectMode.html
            db.Open();

            var cmd = db.CreateCommand("UserPermissions_u", CommandType.StoredProcedure);
            cmd.DeriveParameters();
            //tblParm.OracleDbType = OracleDbType.Table;

            //passing "table" type proc parm example: https://forums.devart.com/viewtopic.php?t=22243
            var obj = new OracleObject(OracleType.GetObjectType("UserPerm", db));
            var tbl = new OracleTable(OracleType.GetObjectType("UserPerms", db));

            obj["UserPermissionId"] = "sR1CKjKYSKvgU90GUgqq+w==";
            obj["adv"] = 1;
            tbl.Add(obj);
            cmd.Parameters["IN_Email"].Value = "banderson@kingcounty.gov";
            cmd.Parameters["IN_Permissions"].Value = tbl;
            cmd.ExecuteNonQuery();

            //"i can't believe it's not butter!" -me, just now =)
        }
    }
}

corresponding oracle db definitions:

create or replace type UserPerm as object ( UserPermissionId varchar2(24), std number(1), adv number(1)  );
create or replace type UserPerms as table of UserPerm;

create or replace PROCEDURE UserPermissions_u (
  IN_Email IN varchar2,
  IN_Permissions IN UserPerms
) is 

dummyvar number default 0;

begin

select count(*) into dummyvar from table(IN_Permissions);

end;
/

more elaborate shot at generically reflecting on an inbound object to hydrate the proc parms like the OP's request... take with caution, needs testing/bullet-proofing ... i'd love to get better alternatives if anybody cares to share

using System;
using System.Data;
using Devart.Data.Oracle;
using System.Linq;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections;

namespace ConsoleApp1
{
    public class User
    {
        public string Email { get; set; }
        public List<UserPermissionEffective> Permissions { get; set; }
    }

    public class UserPermissionEffective
    {
        public string UserPermissionId { get; set; }
        public string Email { get; set; }
        public bool Std { get; set; }
        public bool Adv { get; set; }
        public string Mod { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");

            var dto = new User { Email = "testy@mctesterson.com", Permissions = new List<UserPermissionEffective> {
                new UserPermissionEffective { UserPermissionId = "1", Std = false, Adv = true },
                new UserPermissionEffective { UserPermissionId = "2", Std = true, Adv = false }
            } };

            if (dto == null) return;

            //good docs:
            //direct connection: https://www.devart.com/dotconnect/oracle/docs/StoredProcedures-OracleCommand.html
            //linux licensing: https://www.devart.com/dotconnect/oracle/docs/?LicensingStandard.html
            var dbstring = Environment.GetEnvironmentVariable("dbstring");
            using OracleConnection db = new OracleConnection(dbstring);
            db.ConnectionString = "license key=trial:Devart.Data.Oracle.key;" + db.ConnectionString;
            db.Direct = true; //nugget: crucial!! https://www.devart.com/dotconnect/oracle/docs/DirectMode.html
            db.Open();

            var cmd = db.CreateCommand("UserPermissions_u", CommandType.StoredProcedure);
            cmd.DeriveParameters();

            //regex gets everything following the last underscore. e.g. INOUT_PARMNAME yields PARMNAME
            var regex = new Regex(@"([^_\W]+)$", RegexOptions.Compiled);

            //get the inboud model's root properties
            var dtoProps = dto.GetType().GetProperties();

            //loop over all parms assigning model properties values by name 
            //going by parms as the driver versus object properties to favor destination over source
            //since we often ignore some superfluous inbound properties
            foreach (OracleParameter parm in cmd.Parameters)
            {
                var cleanParmName = regex.Match(parm.ParameterName).Value.ToUpper();
                var dtoPropInfo = dtoProps.FirstOrDefault(prop => prop.Name.ToUpper() == cleanParmName);

                //if table type, then drill into the nested list
                if (parm.OracleDbType == OracleDbType.Table)
                {
                    //the type we're assigning from must be a list
                    //https://stackoverflow.com/questions/4115968/how-to-tell-whether-a-type-is-a-list-or-array-or-ienumerable-or/4115970#4115970
                    Assert.IsTrue(typeof(IEnumerable).IsAssignableFrom(dtoPropInfo.PropertyType));

                    var listProperty = (dtoPropInfo.GetValue(dto) as IEnumerable<Object>).ToArray();
                    //don't bother further logic if the list is empty
                    if (listProperty.Length == 0) return;

                    //get the oracle table & item Udt's to be instanced and hydrated from the inbound dto
                    var tableUdt = OracleType.GetObjectType(parm.ObjectTypeName, db);
                    var itemUdt = OracleType.GetObjectType(tableUdt.ItemObjectType.Name, db);
                    var dbList = new OracleTable(tableUdt);
                    //and the internal list item objects

                    var subPropInfos = dtoPropInfo.PropertyType.GenericTypeArguments[0].GetProperties().ToDictionary(i=>i.Name.ToUpper(), i=>i);
                    //for every item passed in...
                    foreach (var dtoSubItem in listProperty) {
                        //create db objects for every row of data we want to send
                        var dbObj = new OracleObject(itemUdt);

                        //and map the properties from the inbound dto sub items to oracle items by name
                        //using reflection to enumerate the properties by name
                        foreach (OracleAttribute field in itemUdt.Attributes)
                        {
                            var val = subPropInfos[field.Name.ToUpper()].GetValue(dtoSubItem);
                            //small tweak to map inbound booleans to 1's & 0's on the db since oracle doesn't support boolean!?!
                            var isDbBool = field.DbType == OracleDbType.Integer && field.Precision == 1;
                            dbObj[field] = isDbBool ? ((bool)val ? 1 : 0) : val;
                        }

                        //lastly add the db obj to the db table
                        dbList.Add(dbObj);
                    }
                    parm.Value = dbList;
                }
                else {
                    parm.Value = dtoPropInfo.GetValue(dto);
                }
            }

            cmd.ExecuteNonQuery();
        }
    }
}

Beej
  • 794
  • 8
  • 15
  • Thank so much for your detailed response. I did reach out to one of the Data Scientists at Oracle and they had mentioned that they did not have the ability to do this straight out of the box. However after much reading, the closest I could have gotten was passing in XML. – Ninja Coder Oct 23 '20 at 19:24
  • for everyone's reference, here's a github post where the Oracle product manager for the .net oracle clients is posting their timeline for implementing this feature: https://github.com/oracle/dotnet-db-samples/issues/56 – Beej Oct 24 '20 at 20:12
0

Please refer following link to setup ODAC Setup Ref and use follwing link to get the ODAC

using Oracle.DataAccess.Client;
using Oracle.DataAccess.Types;
using System;
using System.Data;

namespace Strace_CustomTypes
{
    class Program
    {
        static void Main(string[] args)
        {
            // Setup Ref - https://o7planning.org/en/10509/connecting-to-oracle-database-using-csharp-without-oracle-client
            // ODAC 64bit ODAC122010Xcopy_x64.zip - https://www.oracle.com/technetwork/database/windows/downloads/index-090165.html
            // .Net Framework 4

            // 'Connection string' to connect directly to Oracle.
            string connString = "Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=0.0.0.0)(PORT=1521))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=SIT)));Password=PASSWORD;User ID=USERID";


            OracleConnection straceOracleDBConn = new OracleConnection(connString);
            OracleCommand cmd = new OracleCommand("PKG_TEMP.TEST_ARRAY", straceOracleDBConn);
            cmd.CommandType = CommandType.StoredProcedure;

            try
            {
                straceOracleDBConn.Open();

                CustomVarray pScanResult = new CustomVarray();

                pScanResult.Array = new string[] { "hello", "world" };

                OracleParameter param = new OracleParameter();
                param.OracleDbType = OracleDbType.Array;
                param.Direction = ParameterDirection.Input;

                param.UdtTypeName = "USERID.VARCHAR2_ARRAY";
                param.Value = pScanResult;
                cmd.Parameters.Add(param);

                cmd.ExecuteNonQuery();
            }
            catch (Exception ex)
            {

                Console.WriteLine($"Error: {ex.Message} {Environment.NewLine} {ex.StackTrace}");
            }
            finally
            {
                straceOracleDBConn.Close();
                cmd.Dispose();
                straceOracleDBConn.Dispose();
            }

            Console.WriteLine("Press any key to exit");
            Console.ReadLine();
        }
    }

    //Ref https://www.codeproject.com/Articles/33829/How-to-use-Oracle-11g-ODP-NET-UDT-in-an-Oracle-Sto
    public class CustomVarray : IOracleCustomType, INullable
    {
        [OracleArrayMapping()]
        public string[] Array;

        private OracleUdtStatus[] m_statusArray;
        public OracleUdtStatus[] StatusArray
        {
            get
            {
                return this.m_statusArray;
            }
            set
            {
                this.m_statusArray = value;
            }
        }

        private bool m_bIsNull;

        public bool IsNull
        {
            get
            {
                return m_bIsNull;
            }
        }

        public static CustomVarray Null
        {
            get
            {
                CustomVarray obj = new CustomVarray();
                obj.m_bIsNull = true;
                return obj;
            }
        }


        public void FromCustomObject(OracleConnection con, IntPtr pUdt)
        {
            OracleUdt.SetValue(con, pUdt, 0, Array, m_statusArray);
        }

        public void ToCustomObject(OracleConnection con, IntPtr pUdt)
        {
            object objectStatusArray = null;
            Array = (string[])OracleUdt.GetValue(con, pUdt, 0, out objectStatusArray);
            m_statusArray = (OracleUdtStatus[])objectStatusArray;
        }
    }

    [OracleCustomTypeMapping("USERID.VARCHAR2_ARRAY")]
    public class CustomVarrayFactory : IOracleArrayTypeFactory, IOracleCustomTypeFactory
    {
        public Array CreateArray(int numElems)
        {
            return new string[numElems];
        }

        public IOracleCustomType CreateObject()
        {
            return new CustomVarray();
        }

        public Array CreateStatusArray(int numElems)
        {
            return new OracleUdtStatus[numElems];
        }
    }
}
User12345
  • 486
  • 1
  • 3
  • 18
  • Hi @User12345, as mentioned earlier above, the Oracle.ManagedDataAccess 18.6.0 does not contain the `OracleDbType.Array`. – Ninja Coder May 08 '19 at 13:42
  • I also noticed that my references are `using Oracle.ManagedDataAccess.Client;` and `using Oracle.ManagedDataAccess.Types;` – Ninja Coder May 08 '19 at 13:50