8

I'm working with a legacy codebase and need to call a stored procedure that I'm not allowed to modify. This stored procedure returns a row or multiple rows of validation data.

Example of result set (two columns, code and text):

0 "success"

OR

3 "short error"
4 "detailed error"

In the procedure itself, the message is selected simply as:

Select 0 as code, 'success' as text

Problem:

I'm using Entity Framework to map the result of this stored procedure to a custom class:

public class ValidationResult
{
    public int code { get; set; }
    public string text { get; set; }
}

The call itself:

var result = context.Database.SqlQuery<ValidationResult>(@"old_sproc").ToList();

I've written some integration tests, and have noticed that when the procedure returns the success message, the 0 comes across as a short. When it returns a non-zero message, it comes across as an int. I assumed that setting code as an int, the short would fit in. Unfortunately, I get the following exception for my success test:

The specified cast from a materialized 'System.Int16' type to the 'System.Int32' type is not valid.

When I switch code to a short to make my success test pass, my failure test fails with the following exception:

The specified cast from a materialized 'System.Int32' type to the 'System.Int16' type is not valid.

ADO.NET is an answer

One solution is to fall back to ADO.NET's SqlDataReader object, so I have that as a fallback solution. I'm wondering if there is something I can do on the EF side to get this working, though.

Garrison Neely
  • 3,238
  • 3
  • 27
  • 39
  • Does changing the `code` property to a `short` solve the problem? – Greg Burghardt Mar 24 '15 at 20:47
  • It breaks the failure case. So it makes success work, but when an error returns, it comes in as an int, resulting in a cast exception just in reverse. – Garrison Neely Mar 24 '15 at 20:56
  • Can you add a property so that you have a short and an int, and then assign based on type that you get in result? – Paul Gibson Mar 25 '15 at 14:08
  • The EF mapping logic requires the property be named exactly the same as the column that is returned from the sproc. So, it would have to be named short code and int code, and that obviously won't compile (not that I didn't try!). – Garrison Neely Mar 25 '15 at 14:11
  • IMO `SqlDataReader` is the better answer. Not only will it give you the flexibility you need it will run quicker too. The code isn't all that difficult once you get started. If there are any stored procedures that return multiple result sets this would be a good experience leading up to those. – Jodrell Mar 26 '15 at 16:43

7 Answers7

6

(This is a follow-up to my previous answer. It is only relevant for and later.)

Short answer:

var sql = "EXECUTE old_sproc WITH RESULT SETS ((code INT, text VARCHAR(MAX)))";
var result = context.Database.SqlQuery<ValidationResult(sql).ToList();

Approach taken in this answer:

This answer will follow in your footsteps and use SqlQuery to execute your stored procedure. (Why not an altogether different approach? Because there might not be any alternative. I'll go into this further below.)

Let's start with an observation about your current code:

var result = context.Database.SqlQuery<ValidationResult>(@"old_sproc").ToList();

The query text "old_sproc" is really abbreviated T-SQL for "EXECUTE old_sproc". I am mentioning this because it's easy to think that SqlQuery somehow treats the name of a stored procedure specially; but no, this is actually a regular T-SQL statement.

In this answer, we will modify your current SQL only a tiny bit.

Implicit type conversions with the WITH RESULT SETS clause:

So let's stay with what you're already doing: EXECUTE the stored procedure via SqlQuery. Starting with SQL Server 2012, the EXECUTE statement supports an optional clause called WITH RESULT SETS that allows you to specify what result sets you expect to get back. SQL Server will attempt to perform implicit type conversions if the actual result sets do not match that specification.

In your case, you might do this:

var sql = "EXECUTE old_sproc WITH RESULT SETS ((code INT, text VARCHAR(MAX)))";
var result = context.Database.SqlQuery<ValidationResult(sql).ToList();

The added clause states that you expect to get back one result set having a code INT and a text VARCHAR(MAX) column. The important bit is code INT: If the stored procedure happens to produce SMALLINT values for code, SQL Server will perform the conversion to INT for you.

Implicit conversions could take you even further: For example, you could specify code as VARCHAR(…) or even NUMERIC(…) (and change your C# properties to string or decimal, respectively).

If you're using Entity Framework's SqlQuery method, it's unlikely to get any neater than that.

For quick reference, here are some quotes from the linked-to MSDN reference page:

"The actual result set being returned during execution can differ from the result defined using the WITH RESULT SETS clause in one of the following ways: number of result sets, number of columns, column name, nullability, and data type."

"If the data types differ, an implicit conversion to the defined data type is performed."

Do I have to write a SQL query? Isn't there another (more ORM) way?

None that I am aware of.

Entity Framework has been evolving in a "Code First" direction in the recent past (it's at version 6 at this time of writing), and that trend is likely to continue.

The book "Programming Entity Framework Code First" by Julie Lerman & Rowan Miller (published in 2012 by O'Reilly) has a short chapter "Working with Stored Procedures", which contains two code examples; both of which use SqlQuery to map a stored procedure's result set.

I guess that if these two EF experts do not show another way of mapping stored procedures, then perhaps EF currently does not offer any alternative to SqlQuery.

(P.S.: Admittedly the OP's main problem is not stored procedures per se; it's making EF perform an automatic type conversion. Even then, I am not aware of another way than the one shown here.)

Community
  • 1
  • 1
stakx - no longer contributing
  • 83,039
  • 20
  • 168
  • 268
  • Ingenious. Just tried this and it works like a charm. Barring something else that fixes it via C#, this is the winner. – Garrison Neely Mar 27 '15 at 13:02
  • even with cast(x as int) in my final SELECT of an sp, it started returning values as decimal (or so .NET thinks so all of a sudden) and WITH RESULT SET was the only cure... – Adam Apr 21 '17 at 21:49
5

If you can't alter the stored procedure itself, you could create a wrapper stored procedure which alters the data in some way, and have EF call that.

Not ideal of course, but may be an option.

ozz
  • 5,098
  • 1
  • 50
  • 73
  • This is definitely an option, but it feels more hacky than just using old-school ADO.NET to get the result. I've got it working using ADO.NET, but I'm still interested in seeing if there is a solution for EF. – Garrison Neely Mar 26 '15 at 19:39
  • You make a great point--the DB in question has over 1500 stored procs, so adding another one is ill-advised. :-) – Garrison Neely Mar 27 '15 at 13:33
2

(Note: If you're working with SQL Server 2012 or later, see my follow-up answer, which shows a much shorter, neater way of doing the same thing described here.)

Here's a solution that stays in EF land and does not require any database schema changes.

Since you can pass any valid SQL to the SqlQuery method, nothing stops you from passing it a multi-statement script that:

  1. DECLAREs a temporary table;
  2. EXECUTEs the stored procedure and INSERTs its result into the temporary table;
  3. SELECTs the final result from that temporary table.

The last step is where you can apply any further post-processing, such as a type conversion.

const string sql = @"DECLARE @temp TABLE ([code] INT, [text] VARCHAR(MAX));
                     INSERT INTO @temp EXECUTE [old_sproc];
                     SELECT CONVERT(INT, [code]) AS [code], [text] FROM @temp;";
                     //     ^^^^^^^^^^^^^      ^^^^^^^^^^^
                     // this conversion might not actually be necessary
                     // since @temp.code is already declared INT, i.e.
                     // SQL Server might already have coerced SMALLINT
                     // values to INT values during the INSERT.

var result = context.Database.SqlQuery<ValidationResult>(sql).ToList();
Community
  • 1
  • 1
stakx - no longer contributing
  • 83,039
  • 20
  • 168
  • 268
0

In the entity framework data modeler page (Model Browser), either change the functional mapping to a specific int which works for the ValidationResult class or create a new functional mapping result class which has the appropriate int and use that as the resulting DTO class.

I leave this process a touch vague because I do not have access to the actual database; instead I provide the process to either create a new functional mapping or modify an existing one. Trial and error will help you overcome the incorrect functional mapping.


Another trick to have EF generate the right information is temporarily drop the stored proc and have a new one return a stub select such as:

select  1 AS Code , 'Text' as text

RETURN @@ROWCOUNT

The reasoning for this is that sometimes EF can't determine what the stored procedure ultimately returns. If that is the case, temporarily creating the stub return and generating EF from it provides a clear picture for the mappings. Then returning the sproc to its original code after an update sometimes does the trick.

ΩmegaMan
  • 29,542
  • 12
  • 100
  • 122
-1

Ignore the int/short. the text is always the same for the same number right? get just the text. have a switch case. Yes its a hack but unless you can fix the root of the problem (and you say you are not allowed) then you should go with the hack that will take the least amount of time to create and will not cause problems down the road for the next person maintaining the code. if this stored proc is legacy it will not have any new kinds of results in the future. and this solution together with a nice comment solves this and lets you go back to creating value somewhere else.

user1852503
  • 4,747
  • 2
  • 20
  • 28
-2

Cast the static message code to an int:

Select cast(0 as int) as code, 'success' as text

This ensures the literal returned is consistent with the int returned by the other query. Leave the ValidationResult.code declared as an int.

Note: I know I missed the part in the question about the SP can't be modified, but given that this makes the answer quite complicated, I'm leaving this here for others who may have the same problem, but are able to solve it much more easily by modifying the SP. This does work if you have a return type inconsistency in the SP and modifying is an option.

AaronLS
  • 37,329
  • 20
  • 143
  • 202
  • 4
    I believe that's not an option, given the "stored procedure that I'm not allowed to modify" bit in the question. – Tieson T. Mar 24 '15 at 20:32
  • Correct. I'm not allowed to modify the stored proc. – Garrison Neely Mar 24 '15 at 20:34
  • 1
    @GarrisonNeely Have you tried setting your code property to type `string`? This would allow you to create another proeprty to proxy the value to/from int/string using TryParse. I'm not sure it will allow this but worth a shot. – AaronLS Mar 24 '15 at 20:40
  • @GarrisonNeely Unfortunately there's not a builtin way to specify type conversion for EF AFAIK: http://stackoverflow.com/a/6909569/84206 – AaronLS Mar 24 '15 at 20:41
  • I actually tried the string route hoping it would be converted but got a similar exception saying that the string to int16 conversion was invalid. – Garrison Neely Mar 24 '15 at 20:42
-3

There is a workaround you could use if you don't find a better solution. Let it be an int. It will work for all error codes. If you get an exception you know the result was a success so you can add a try/catch for that specific exception. It's not pretty and depending on how much this runs it might impact performance.

Another idea, have you tried changing the type of code to object?

Mikael Eliasson
  • 5,157
  • 23
  • 27
  • 1
    When reading again I realized it returns many rows. If you might get many rows in the "success" case this won't work. – Mikael Eliasson Mar 26 '15 at 16:21
  • 1
    And it is obviously an abuse of exception handling. There *must* be a better alternative. – stakx - no longer contributing Mar 26 '15 at 16:23
  • "Exceptional" (http://stackoverflow.com/questions/18064332/asp-net-performance-which-is-faster/18064433#18064433) code is never a good idea. I did try the `code` as an object and it failed, returning null in both cases. – Garrison Neely Mar 26 '15 at 19:37
  • Yes, and I wrote that in the answer too. But considering you are making a db call I bet you won't even notice the exception. Ofcourse it depends on the server. But that DB call would cost ~5-50ms in most cases. The exception would probably cost 0.05-0.1ms. Ofcourse it costs some memory too as it put the data on the stack. If you are calling this code in a loop I would certainly not use an exception. But I wouldn't call the DB in a loop either. – Mikael Eliasson Mar 27 '15 at 06:02