Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mapping different enums together

Tags:

c#

.net

enums

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.

like image 536
Zohar Peled Avatar asked Sep 28 '17 08:09

Zohar Peled


1 Answers

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.

like image 120
Adriano Repetti Avatar answered Oct 19 '22 11:10

Adriano Repetti