6

First, here is why this question is not a duplicate:

I know that multiple questions about converting one enum to another have been asked on SO, I even answered one of them myself, but all the questions I've found on this topic had some way of comparing the different enum values (whether by name or by value). In my particular case, I don't know the values, and the names don't match.

As a part of a GitHub project I'm working on, called ADONETHelper, that's designed to minimize code repetition when working with Ado.Net, I came face to face with the need to translate values between unrelated enums. This is to allow the same code to work with OleDb, Odbc, and SqlClient (and hopefully in the future OracleClient and MySqlClient as well).

What I'm trying to do is create a unification of different enums - specifically - enums that describe sql parameters data types. Currntly, I am supporting 4 enums -
System.Data.DbType,
System.Data.SqlClient.SqlDbType,
System.Data.OleDb.OleDbType,
System.Data.Odbc.OdbcType

but if I want to add support for OracleClient or MySqlClient, I'll have to work pretty hard to add System.Data.OracleClient.OracleType Or MySql.Data.MySqlClient.MySqlDbType. So I'm looking for a more elegant way of doing this then I came up with.

Here is my current code (It works great, but as I wrote, it's hard to add support for new enums):

So first, I have defined my own enum called ADONETType. It has entries such as Boolean, Byte, Binary, Char etc'. Then I've created a static class called DBTypeConverter to provide extension methods to this enum, so that it's value could be converted to the other enums. This is what this class looks like:

internal static class DBTypeConverter
{
    #region private members

    private static List<DbTypeMap> _Map;

    #endregion private members

    #region static constructor

    static DBTypeConverter()
    {
        _Map = new List<DbTypeMap>()
        {
            new DbTypeMap(ADONETType.Boolean, DbType.Boolean, SqlDbType.Bit, OleDbType.Boolean, OdbcType.Bit),
            new DbTypeMap(ADONETType.Byte, DbType.Byte, SqlDbType.TinyInt, OleDbType.UnsignedTinyInt , OdbcType.TinyInt),
            new DbTypeMap(ADONETType.Binary, DbType.Binary, SqlDbType.Binary, OleDbType.Binary, OdbcType.Binary),
            // ... more of the same here ...                
            new DbTypeMap(ADONETType.Xml, DbType.Xml, SqlDbType.Xml, null, null)
        };
    }

    #endregion static constructor

    #region methods

    internal static DbType ToDbType(this ADONETType type)
    {
        return type.ConvertTo<DbType>();
    }

    internal static SqlDbType ToSqlType(this ADONETType type)
    {
        return type.ConvertTo<SqlDbType>();
    }

    internal static OleDbType ToOleDbType(this ADONETType type)
    {
        return type.ConvertTo<OleDbType>();
    }

    internal static OdbcType ToOdbcType(this ADONETType type)
    {
        return type.ConvertTo<OdbcType>();
    }

    private static dynamic ConvertTo<T>(this ADONETType type)
    {
        var returnValue = _Map.First(m => m.ADONETType == type).GetValueByType(typeof(T));
        if(returnValue != null)
        {
            return returnValue;
        }
        throw new NotSupportedException(string.Format("ADONETType {0} is not supported for {1}", type, typeof(T)));
    }

    #endregion methods

    #region private struct

    private struct DbTypeMap
    {
        #region ctor

        public DbTypeMap(ADONETType adonetType, DbType? dbType, SqlDbType? sqlDbType, OleDbType? oleDbType, OdbcType? odbcType)
            : this()
        {
            ADONETType = adonetType;
            DbType = dbType;
            SqlDbType = sqlDbType;
            OleDbType = oleDbType;
            OdbcType = odbcType;
        }

        #endregion ctor

        #region properties

        internal ADONETType ADONETType { get; private set; }
        internal DbType? DbType { get; private set; }
        internal SqlDbType? SqlDbType { get; private set; }
        internal OleDbType? OleDbType { get; private set; }
        internal OdbcType? OdbcType { get; private set; }

        #endregion properties

        #region methods

        internal dynamic GetValueByType(Type type)
        {
            if (type == typeof(ADONETType))
            {
                return this.ADONETType;
            }
            if(type == typeof(DbType))
            {
                return this.DbType;
            }
            if (type == typeof(SqlDbType))
            {
                return this.SqlDbType;
            }
            if (type == typeof(OleDbType))
            {
                return this.OleDbType;
            }
            if (type == typeof(OdbcType))
            {
                return this.OdbcType;
            }
            return null;
        }

        #endregion methods
    }

    #endregion private struct
}

Now, as you can see, In order to provide support for, say, OracleClient, I'll have to do the following:

  1. Add a property for OracleType in the DbTypeMap private struct.
  2. Change the DbTypeMap constructor to accept also the oracle type.
  3. Add another case to the switch in GetValueByType method.
  4. Add the oracle type to the static constructor of DBTypeConverter.
  5. Add a method (internal static OracleType ToOracleType(this ADONETType type)) to DBTypeConverter.

Obvoisuly, this is a lot of work and I would much rather find another way to unify these enums, so adding support to new clients will be easier.
This is when your experties come in to play.

Zohar Peled
  • 79,642
  • 10
  • 69
  • 121
  • 1
    But the whole *point* of `DbType` is to abstract over different systems. You need to do all this mapping instead of having clients code to the generic classes *why*, exactly? – Jeroen Mostert Sep 28 '17 at 08:44
  • The only way to comes to mind would be if the enums are in the same sequence, maybe using reflection or a config file like approach, something like goes through each enum of a given type and line them up by the value they hold, then again if the values are the same a cast from one enum to the other should be enough as well – Vnvizitiu Sep 28 '17 at 08:47
  • --PS: How about making your ADONETType a custom class with virtual members which you can extend for different databases? – Vnvizitiu Sep 28 '17 at 08:53
  • To put it another way: support `DbConnection`, `DbCommand` and `DbParameter` and lo and behold, you now support all the ADO.NET providers. Mapping things to your own layer is reinventing the wheel -- the providers already map from generic to specific and back. – Jeroen Mostert Sep 28 '17 at 08:54
  • @JeroenMostert Perhaps I'm wrong, but I thought that using `DbType` prevents me from using stuff like `SqlDbType.Structured`. also, my code is implemented using SqlClient, OdbcClient and OleDbClient, all inheriting a single abstract class with lots of generics. If this can be simplified it would help a lot but I don't see how. – Zohar Peled Sep 28 '17 at 08:55
  • 1
    Nothing prevents you from casting down the generic classes to provider-specific ones if you *do* need `SqlDbType.Structured`. In fact, that's better than pretending all different possible types are supported in all database providers, when they're not -- at some point you'll have to throw up your hands at runtime and say "sorry, can't do that". It seems probable that in the vast majority of the cases, you can code to the generic classes. This is especially true for a generic helper library that's supposed to work for all providers. It seems like you're just rebuilding `DbProviderFactory`. – Jeroen Mostert Sep 28 '17 at 09:01
  • @JeroenMostert so if I understand you correctly, you are basically saying "don't bother working with `SqlCommend` or `OleDbCommand`, simply work with `DbCommand` and let the framework worry about conversions", right? – Zohar Peled Sep 28 '17 at 09:03
  • Yes! That's what the class is there for. Just because client code is often non-generic and uses the specific classes (because they "know" they will never switch database systems) doesn't mean libraries have to work with them. I maintain a micro-ORM lib myself which has support for TVPs in SQL Server -- by checking at runtime that we are actually dealing with an `SqlConnection`. Most of the rest of the code doesn't need to know what your RDBMS is. I've had a report that it works for MySQL, even though I never tested for that myself. That's abstraction at work. – Jeroen Mostert Sep 28 '17 at 09:06
  • @JeroenMostert, This is something to play with. If I'll go this way I'll have to re-build some parts of my code, but I don't think it's going to be that bad... Thanks! – Zohar Peled Sep 28 '17 at 09:10

1 Answers1

4

Assuming you really need this (consider Jeroen's comment, if not then you may reuse this for something else...) you can simplify things using a list of equivalences. It's simply a list of arrays where array items are equivalent. I'm using an array instead of a class because I do not need to add properties and ctor arguments when adding a new equivalence. To store the equivalence I'm using the special Enum base class but also object works pretty well (no need, AFAIK, for dynamic).

Finding a conversion is then just matter of a search inside these lists (code here is more explicative than performance wise):

public static TTo ConvertTo<TTo>(Enum value)
{
    var set = Mapping.FirstOrDefault(values => Array.IndexOf(values, value) != -1);
    if (set == null)
        throw new InvalidOperationException($"Value {value} is unknown");

    return (TTo)(object)set.First(x => x.GetType() == typeof(TTo));
}

Populate Mapping list as required, it may be defined, for example, as:

private static List<Enum[]> Mapping = new List<Enum[]>
{
    new Enum[] { ADONETType.Byte, DbType.Byte, SqlDbType.TinyInt },
    new Enum[] { ADONETType.Boolean, DbType.Boolean, SqlDbType.Bit },
    new Enum[] { ADONETType.Binary, DbType.Binary, SqlDbType.Binary },
    new Enum[] { ADONETType.Xml, DbType.Xml },
};

Note that ugly double cast (TTo)(object)...ahhhh .NET Generics...a better solution is welcome. To support a new equivalence you still need to map all enum's values into this table, annoying but localized in one single place. If conversion isn't possible (value value isn't defined anywhere) or there is not a known conversion to TTo (for example the last DbType.Xml) then InvalidOperationException is thrown.

Adriano Repetti
  • 65,416
  • 20
  • 137
  • 208