9

The data access layer in our application is going to be using the UDT functionality of Oracle. We will only be passing UDT objects to and from the database.

At the moment, we generate the custom classes using the function provided with ODP.NET (which creates a truly horrible looking class that we really don't want in our codebase).

We then use a separate mapping class, to map the custom class to one of our business objects (and back when saving).

I am trying to find a better way of doing this.

I thought I would just do-away with the generated classes and just write a mapping class that implemented IOracleCustomType. The From/ToCustomObject methods would then map from my UDT to my business objects. However, this caused me problems when I tried it - I got the error "Object attribute is not mapped to a custom type member". It appears that as well the two methods, I also need attributes in my mapping class - one attribute for each item in the UDT.

For example - a workflow UDT contains three items - a status, created time and created by. My UDT is nice and simple:

TYPE workflow_type AS OBJECT
(status                                  VARCHAR2(8)
,created_by                              VARCHAR2(30)
,created_datetime            DATE
);

As is the business object I want it to end up in:

public class Workflow
{
    /// <summary>
    /// Gets the status of the workflow.
    /// </summary>
    /// <value>The status.</value>
    public string Status { get; private set; }

    /// <summary>
    /// Gets the Windows Logon Id of the user performing the action
    /// </summary>
    public string CreatedBy{ get; private set; }
    /// <summary>
    /// Gets the time of the action 
    /// </summary>
    public DateTime CreatedTime { get; private set; }
}

I want to get from one to the other without having to add Oracle code to the business object.

So my thought was to create a mapping class like this:

public class WorkFlowMapper : IOracleCustomType
{
    public BusinessObjects.WorkFlow BusinessObject {get; private set;}

    public WorkFlowMapper(BusinessObjects.WorkFlow businessObject)
    {
        BusinessObject = businessObject;
    }

    public WorkFlowMapper(){}

    public void FromCustomObject(OracleConnection con, IntPtr pUdt)
    {
        OracleUdt.SetValue(con, pUdt, "STATUS", BusinessObject.Status);
        OracleUdt.SetValue(con, pUdt, "CREATED_BY", BusinessObject.CreatedBy);
        OracleUdt.SetValue(con, pUdt, "CREATED_DATETIME", BusinessObject.CreatedTime);
    }

    public void ToCustomObject(OracleConnection con, IntPtr pUdt)
    {

        BusinessObject = new BusinessObjects.WorkFlow(
            (string)OracleUdt.GetValue(con, pUdt, "STATUS"),
            (string)OracleUdt.GetValue(con, pUdt, "CREATED_BY"),
            (string)OracleUdt.GetValue(con, pUdt, "CREATED_DATETIME")
        );
    }
}

// Factory to create an object for the above class
[OracleCustomTypeMappingAttribute("MYUSER.WORKFLOW_TYPE")]
public class CurrencyExposureFactory : IOracleCustomTypeFactory
{

    public virtual IOracleCustomType CreateObject()
    {
        WorkFlowMapper obj = new WorkFlowMapper();
        return obj;
    }
}

But this doesn't work thanks to the requirement of needing OracleObjectMappingAttribute for each attribute to be mapped (as in the ODP.NET generated classes). This appears really stupid as I won't be using them at all. In fact, I can get my mapping class to work, just by adding in three lines:

    [OracleObjectMappingAttribute("STATUS")] public string a;
    [OracleObjectMappingAttribute("CREATED_BY")] public string b;
    [OracleObjectMappingAttribute("CREATED_DATETIME")] public DateTime c;

Surely there must be a better way than putting in such a horrible hack? Afterall, these variables never get used at all - ODP.NET just appears to need them for getting the type to map to - but I would have thought this could be achieved in a different way. Thoughts?

OrangeDog
  • 36,653
  • 12
  • 122
  • 207
David
  • 682
  • 2
  • 9
  • 15

3 Answers3

2

What is so bad about those extra attributes? There is already a ginormous amount of attributes in the .net classes and frameworks (for instance WCF). Disliking attributes is almost the same as disliking .NET .

Anyway you can explore the possibilities of devart's Oracle provider (http://www.devart.com/dotconnect/oracle/). They have a free version too. Its dealing with udts is based on strings not on attributes.

tuinstoel
  • 7,248
  • 27
  • 27
  • 4
    What's bad is that they should not be needed. The properties themselves are never got or set, the only reason they are needed is because every UDT property appears to need one of those OracleObjectMappingAttribute. It just seems crazy to have a bunch of public properties in a class that are never used (and yes, they also have to be public!). So I guess it's the need for the public properties i'm questioning, rather than the attributes. – David Oct 02 '09 at 08:55
  • Oh, and thank you for the other oracle provider link (unfortunately this will not help me personally as where I work it is only ODP.NET that is used) – David Oct 02 '09 at 08:56
  • There is indeed some 'duplication' in the code because you have to map in the code and to map with attributes but I just don't think it is a big problem. It is a problem but nog a big one. – Theo Oct 02 '09 at 10:25
  • It just seems crazy as the properties with attributes are never actually used to hold data. They are just sitting there to allow the mapping attribute to exist.
    Doesn't that sound a little nasty to you?
    I have posted on the Oracle forums about this - I am convinced that there must be a better way to do this: http://forums.oracle.com/forums/thread.jspa?threadID=965904&tstart=0
    – David Oct 02 '09 at 15:06
  • If you follow the single responsibilty principle to the core you have to have a different class for data access and a different class for business logic anyway. That means that you have to develop mapper methods between the data access class and the business logic class. If you use mapper methods between a data access object and a business logic class you don't have to have oracle attributes on your bl classes. You want a shortcut, you don't want a indepedent separate class for data access. – tuinstoel Oct 02 '09 at 17:33
  • So you're saying that you think that the data access tier should return objects that contain these attributes (this would be my mapping class in my example above) and then at the business tier we should convert from these objects into our business objects? I personally think that this is a little extreme in terms of separation of responsibility. I personally am happy to just keep data-access bits in the data-access tier. I keep business objects in a separate assembly to the business tier - this way they can be passed between tiers happily. Perhaps they could be better named "entities". – David Oct 05 '09 at 10:19
1

Welcome to Oracle. You'll hate it here. You're right, it's absolutely ridiculous that you'd have to provide those attributes when you have to explicitly get/set the value anyway. The .NET ecosystem is so wonderfully generic (specifically in the more modern .NET Core), that you'd fully expect the framework to handle something like this. However Oracle completely missed that memo (as they often do). Most Oracle frameworks (and the entire Java language in general) has this issue. Here's my workaround:

Start with a base UDT class that implements IOracleCustomType

public abstract class BaseUDT : IOracleCustomType
    {
        private IEnumerable<PropertyInfo> GetTypeProperties() => GetType()
                .GetProperties(BindingFlags.Public | BindingFlags.Instance)
                .Where(p => p.GetCustomAttribute<OracleObjectMappingAttribute>() != null);

        public virtual void ToCustomObject(OracleConnection con, IntPtr pUdt)
        {
            foreach (var prop in GetTypeProperties())
            {
                var attr = prop.GetCustomAttribute<OracleObjectMappingAttribute>();
                prop.SetValue(this, OracleUdt.GetValue(con, pUdt, attr.AttributeName));
            }
        }

        public virtual void FromCustomObject(OracleConnection con, IntPtr pUdt)
        {
            foreach (var prop in GetTypeProperties())
            {
                var attr = prop.GetCustomAttribute<OracleObjectMappingAttribute>();
                OracleUdt.SetValue(con, pUdt, attr.AttributeName, prop.GetValue(this));
            }
        }
    }

The above class will look for all public instance properties that have the OracleObjectMappingAttribute attribute applied, and then dynamically get/set there value. Your UDT then becomes just

public class Workflow : BaseUDT
{
    /// <summary>
    /// Gets the status of the workflow.
    /// </summary>
    /// <value>The status.</value>
    [OracleObjectMapping("STATUS")]
    public string Status { get; private set; }

    /// <summary>
    /// Gets the Windows Logon Id of the user performing the action
    /// </summary>
    [OracleObjectMapping("CREATED_BY")]
    public string CreatedBy{ get; private set; }
    /// <summary>
    /// Gets the time of the action 
    /// </summary>
    [OracleObjectMapping("CREATED_DATETIME")]
    public DateTime CreatedTime { get; private set; }
}

I LOVE attributes, as it makes it possible to leverage Reflection in order to cut down on repetitive code. With this approach, the BaseUDT class will iterate all properties and set them accordingly. I understand you hesitation to add business logic to your models, but this is unavoidable in a lot of scenarios.

thomasrea0113
  • 360
  • 1
  • 9
0

You could set your BusinessObject as a private member and map the attributes to the respective properties.

While this doesn't provide any functionality, at least they would function correctly if used.