views:

629

answers:

6

Hello, I'm creating 'generic' wrapper above SQL procedures, and I can resolve all required parameters' names and sqltypes, but is there any way how to get it's 'underlying' .NET type?

My goal is to do something like:

SqlParameter param;
object value;
object correctParam = param.GetNETType().GetMethod("Parse", 
    new Type[] { typeof(string) }).Invoke(value.ToString());
param.Value = correctParam;

Where GetNETType is the thing I need. I know that it can be written as switch inside of param.SqlDbType, but this is shorter way, and shorter commented code means lower mainteance :)

+3  A: 

i think you're missing a step here. The first thing you need to do is query the database for the definition of the stored proc via a select call and inner join to the sys objects table or by using a management wrapper. Then you can "infer" the types of parameters based upon the returned information.

Here is an MSO link to get you started

And an example of how to query the database structure directly

If you run the sql from the second example against your database you'll see exactly what's up:

USE AdventureWorks;
GO
SELECT SCHEMA_NAME(SCHEMA_ID) AS [Schema], 
SO.name AS [ObjectName],
SO.Type_Desc AS [ObjectType (UDF/SP)],
P.parameter_id AS [ParameterID],
P.name AS [ParameterName],
TYPE_NAME(P.user_type_id) AS [ParameterDataType],
P.max_length AS [ParameterMaxBytes],
P.is_output AS [IsOutPutParameter]
FROM sys.objects AS SO
INNER JOIN sys.parameters AS P 
ON SO.OBJECT_ID = P.OBJECT_ID
WHERE SO.OBJECT_ID IN ( SELECT OBJECT_ID 
FROM sys.objects
WHERE TYPE IN ('P','FN'))
ORDER BY [Schema], SO.name, P.parameter_id
GO
Paul Sasik
I'm not sure how it is done (im extending someone else's code), but interating through SqlCommand.Parameters shows me all of required parameters. How can I infer the types?
Yossarian
You can't really infer the type. You have to know the type somehow. Usually the type is hardcoded but you're looking for an automated way and the only way to do that, to "know" the type, is to query the db.
Paul Sasik
I think he might just need a way to map from the SQL type to the corresponding .NET type. e.g. Varchar => String, etc.
Knobloch
Knobloch is right.. I want to know, if there is possible some mapping other than switch(param.SqlDbType)
Yossarian
+10  A: 

Unfortunately, as far as I know this mapping is not exposed in code inside the .NET Framework. I've looked through the .NET Framework reference source before for this, and found that inside the .NET code there's a lot of long per-type switch statements, just like the ones you're trying to avoid, but none of them seem to be exposed externally.

If you really just want to map from SqlTypes to the most likley .NET type, I think your best bet is to simply turn the mapping table in the MSDN docs into code. Note that the table on MSDN has (at least) two errors: #1: there is no .NET type called "DateTime2" (I used DateTime) and there is also no type called "Xml" (I used SqlXml).

Anyway, here's the mapping I've been using-- using a Dictionary instead of a switch for ease of access without a separate method.

public static Dictionary<SqlDbType, Type> TypeMap = new Dictionary<SqlDbType, Type>
{
    { SqlDbType.BigInt, typeof(Int64) },
    { SqlDbType.Binary, typeof(Byte[]) },
    { SqlDbType.Bit, typeof(Boolean) },
    { SqlDbType.Char, typeof(String) },
    { SqlDbType.Date, typeof(DateTime) },
    { SqlDbType.DateTime, typeof(DateTime) },
    { SqlDbType.DateTime2, typeof(DateTime) },
    { SqlDbType.DateTimeOffset, typeof(DateTimeOffset) },
    { SqlDbType.Decimal, typeof(Decimal) },
    { SqlDbType.Float, typeof(Double) },
    { SqlDbType.Int, typeof(Int32) },
    { SqlDbType.Money, typeof(Decimal) },
    { SqlDbType.NChar, typeof(String) },
    { SqlDbType.NText, typeof(String) },
    { SqlDbType.NVarChar, typeof(String) },
    { SqlDbType.Real, typeof(Single) },
    { SqlDbType.SmallInt, typeof(Int16) },
    { SqlDbType.SmallMoney, typeof(Decimal) },
    { SqlDbType.Structured, typeof(Object) }, // might not be best mapping...
    { SqlDbType.Text, typeof(String) },
    { SqlDbType.Time, typeof(TimeSpan) },
    { SqlDbType.Timestamp, typeof(Byte[]) },
    { SqlDbType.TinyInt, typeof(Byte) },
    { SqlDbType.Udt, typeof(Object) },  // might not be best mapping...
    { SqlDbType.UniqueIdentifier, typeof(Guid) },
    { SqlDbType.VarBinary, typeof(Byte[]) },
    { SqlDbType.VarChar, typeof(String) },
    { SqlDbType.Variant, typeof(Object) },
    { SqlDbType.Xml, typeof(SqlXml) }, 
};

Note that one thing you'll need to watch out for is size/precision-- some SQL types (e.g. varchar) have size limits, while .NET types (e.g. string) don't. So being able to know the most-likely .NET type is not really enough... if you're using this to, for example, drive validation rules, you also need to be able to prevent users from entering invalid (e.g. too large) values by knowing more about the parameter, like the precision. Note that, if you look inside the SqlClient source, they use special code to handle cases like setting the precision of a Decimal type from the corresponding SQL precision.

Note that if the only reason you need the .NET type is to be able to stuff data into a stored proc parameter, you might want to try simply using ToString() on all your .NET values, stuffing a string into the Value property of the SqlParameter, and seeing if the framework will do the conversion/parsing for you. For example, for an XML or Date parameter you might be able to get away with sending a string instead.

Also, instead of using reflection to find a Parse() method on each type, since there's a known (and small) list of types, you can get better performance by using strongly-typed parsing code for each, like the code below. (Note that several types (e.g. SqlDbType.Udt) don't necessarily have an obvious parser method-- you'll need to figure out how you want to handle those.)

public static Dictionary<SqlDbType, Func<string, object>>  TypeMapper = new Dictionary<SqlDbType, Func<string, object>>
{
    { SqlDbType.BigInt, s => Int64.Parse(s)},
    { SqlDbType.Binary, s => null },  // TODO: what parser?
    { SqlDbType.Bit, s => Boolean.Parse(s) },
    { SqlDbType.Char, s => s },
    { SqlDbType.Date, s => DateTime.Parse(s) },
    { SqlDbType.DateTime, s => DateTime.Parse(s) },
    { SqlDbType.DateTime2, s => DateTime.Parse(s) },
    { SqlDbType.DateTimeOffset, s => DateTimeOffset.Parse(s) },
    { SqlDbType.Decimal, s => Decimal.Parse(s) },
    { SqlDbType.Float, s => Double.Parse(s) },
    { SqlDbType.Int, s => Int32.Parse(s) },
    { SqlDbType.Money, s => Decimal.Parse(s) },
    { SqlDbType.NChar, s => s },
    { SqlDbType.NText, s => s },
    { SqlDbType.NVarChar, s => s },
    { SqlDbType.Real, s => Single.Parse(s) },
    { SqlDbType.SmallInt, s => Int16.Parse(s) },
    { SqlDbType.SmallMoney, s => Decimal.Parse(s) },
    { SqlDbType.Structured, s => null }, // TODO: what parser?
    { SqlDbType.Text, s => s },
    { SqlDbType.Time, s => TimeSpan.Parse(s) },
    { SqlDbType.Timestamp, s => null },  // TODO: what parser?
    { SqlDbType.TinyInt, s => Byte.Parse(s) },
    { SqlDbType.Udt, s => null },  // consider exception instead
    { SqlDbType.UniqueIdentifier, s => new Guid(s) },
    { SqlDbType.VarBinary, s => null },  // TODO: what parser?
    { SqlDbType.VarChar, s => s },
    { SqlDbType.Variant, s => null }, // TODO: what parser?
    { SqlDbType.Xml, s => s }, 
};

The code to use above is pretty easy, e.g. :

        string valueToSet = "1234";
        SqlParameter p = new SqlParameter();
        p.SqlDbType = System.Data.SqlDbType.Int;
        p.Value = TypeMapper[p.SqlDbType](valueToSet);
Justin Grant
+3  A: 

You cannot necessarily implicitly and accurately extract the correct .NET CTS ("underlying") type because it might change depending on the value in the parameter - the SqlParameter's .DbType and .SqlDbType are mutable and explicitly settable by the programmer (or the code-generation engine) In the case of an output parameter, the .DbType/.SqlDbType can be wrong even after having been right for a time, for example if the value underneath that comes back suddenly is different than expected in .NET terms. The values are driven by the data store and .NET SqlParameter copes as best it can with its explicit types. The data value of the SqlParameter should be considered weakly typed in .NET terms (evidenced by the parm.Value property's System.Object return value).

Your best bet is

  1. Use one of the mapping methods outlined by other posters - of course that has its own implicit assumption that the SQL Parameter type will always be correct for the data in it.
  2. possibly test the value coming back from the output parameter and assume successive values are of the same kind. Of course that's really up to the database.
  3. Find another strategy instead of relying on the Microsoft Sql namespace - you might be much happier in the future.

Testing the value for a .NET CTS type would look something like System.Type t = paramInstance.Value.GetType(); Null will cause an exception. You'd still need to cast it appropriately using a switch or if/else, unless you pull out some fancy reflection techniques.

John K
+1  A: 

If you can resolve to the correct SqlType, Reflection will get you the explicit cast to a .NET type. The return value would be the underlying System.Type. Caching the result should make up for the perf on 1st lookup.

Mark Brackett
+1  A: 

Take a look at what they do in linq to sql t4, it seem to work nicely.

You might be able to find out what you need by looking at the code.

Fredou
+3  A: 

No one else seems to want to tell you, but what you're doing is probably not the best way to do it.

object correctParam = param.GetNETType().GetMethod("Parse", 
    new Type[] { typeof(string) }).Invoke(value.ToString());
param.Value = correctParam;

You're saying that you're given a string value, which you know has to be assigned to a parameter, and you want to stuff that value in there any way that it can fit?

Please consider why you are doing this. You are making the assumption that the following code is right:

param.Value = NetType.Parse(value.toString())

There's no clear reason why this is better than:

param.Value = value;

But since you want to do it, it seems safe to assume that you have tried this and found that your real problem is that value isn't the right type for the parameter. Thus you want a magical fix that you can run which will always make sure that value is the right type. What you really want is likely:

SetParam(param, value);

Where this function stuffs the value into the parameter. This actually makes things a bit easier if value is not simply of type object as you say, but has a real type (like int or string). This is because you can use method overloading like SetParam(SqlParam param, int value) or generics to infer the value type SetParam<T>(SqlParam param, T value).

So we know the function you want, what we don't know is why. In most reasonable scenarios you have an idea of the types of the values, and you also have an idea of the type of the parameter. You are asking for a way to cram a value that doesn't match a parameter into a parameter that you don't understand.

There are two main reasons I can think of for this request:

  1. You in reality know that the types are compatible, and are looking for a general way to do this to avoid writing a lot of code. So you know that you are trying to assign a long to a parameter that is a SqlInt, and are relying on string conversions to get you past the type safety issues.

  2. You don't really understand the code that you are using and are trying to patch in a fix to get something working.

It's really important to be honest with yourself about which case you are in. If you are in the first case, then you can write a method like SetParam that I described above fairly easily. You will have to write a switch statement (or like the best answer above, a Dictionary lookup). You are going to have to lose precision (casting a long to an int doesn't work for large numbers, but neither will your Parse) but it will work.

If you're in the second case, stop for a minute. Recognize that you are setting yourself up for more bugs in the future (because converting to and from string will not solve the problems you have of not understanding the types). You know that you need help, which is why you are on Stack Overflow and offering a bounty for help, and you are dealing with a codebase that you don't understand. I can tell right now from your question that you are going to dig yourself a deeper hole than you realize if this is your situation, because you have already refused the best answer (to do a switch statement based on parameter type) for no strong reason.

So, if you are in the second case, the thing that is going to help you most is not an answer from Stack Overflow, unless you are willing to describe your real problem more completely. What will help you is understanding where the values are coming from (is it UI? Is it a different subsystem, which rules do they follow? Is there a reason the types don't match?) and where they are going (what is the definition of the stored procedure you are calling? What are the parameter types defined as?). I imagine you probably don't even need to go into SQL to find this, as whoever gave you the SqlParam probably already has defined it properly for you. If you defined it, you do indeed need to go to the SQL to figure it out, immediately.

EWizard