Entity Framework, RAISEERROR, Microsoft и все-все-все

Столкнулся на днях с очаровательным багом в Entity Framework: при работе с некоторыми хранимыми процедурами EF не выбрасывает исключение при срабатывании внутри хранимки T-SQL инструкции RAISERROR. Microsoft не может вопроизвести проблему и поэтому отмахивается от неё, поэтому пришлось вооружиться отладчиком, напильником и такой-то матерью, резко выдохнуть и самостоятельно докопаться до истины.
Минимальный код вопроизведения бага совместными усилиями удалось изготовить такой:
SQL:

CREATE PROCEDURE [dbo].[Add_Test_Record]
AS
BEGIN
    -- ↓ содержимое выборки, равно как и наличие оного, значения не имеют ↓
    select 1 as id -- where 0=1 
    RAISERROR ('Неуловимый Джо', 16, 1) WITH NOWAIT, SETERROR
END

EDMX:

    
      
        
          
        
      
    

C#:

using (var model = new SPEED_DEVEntities())
{
    try
    {
        var res = model.Add_Test_Record().ToArray();
        Console.WriteLine("Passed without exception :-(");
        Console.WriteLine(res.Any() ? res.First().Value.ToString() : "");
    }
    catch (Exception e)
    {
        Console.Write(e);
    }
}

Console.ReadKey();

Причём, вот с таким вариантом хранимки всё работает подобающим образом:

CREATE PROCEDURE [dbo].[Add_Test_Record]
AS
BEGIN
    RAISERROR ('Неуловимый Джо', 16, 1) WITH NOWAIT, SETERROR
    select 1 as id -- where 0=1
END

Озадаченно пишем обвязку на ADO.NET, руководствуясь наиофициальнейшей документацией:

SqlConnection connection = new SqlConnection("…");
SqlCommand command = new SqlCommand("Add_Test_Record", connection);
command.CommandType = CommandType.StoredProcedure;
SqlParameter RetVal = command.Parameters.Add("RetVal", SqlDbType.Int);
RetVal.Direction = ParameterDirection.ReturnValue;

connection.Open();
try
{
    SqlDataReader reader = command.ExecuteReader();
    while (reader.Read())
    {
        Console.WriteLine("{0}", reader.GetInt32(0));
    }

    reader.Close();
    Console.WriteLine("Passed without exceptions :-(");
}
catch (Exception e)
{
    Console.WriteLine(e);
}

Console.ReadKey();

Иииии… всё равно не получаем столь желанного исключения!

На всякий случай проверяем наличие ошибки при помощи SQL Server Management Studio:
2013-03-22_1
Всё в порядке, ошибка на месте.
Так в чём же дело?

А дело, оказывается, в том, что MS SQL Server умеет (и любит!) возвращать к одной операции несколько результатов, перебирать которые можно методом SqlDataReader.NextResult. Только авторы EF об этом, видимо, не догадались (что и неудивительно, так как руководствовались они, вероятно, вышеупомянутой официальной документацией). В результате EF разбирает первый из результатов, возвращаемых хранимкой, и бессовестно игнорирует остальные. Наша тестовая хранимка возвращает два результата: выборку интов (уж пустую или с интами — неважно) и информацию об ошибке, именно в таком порядке. Это прекрасно объясняет, почему всё работает, стоит лишь поменять строки в хранимке местами: информация об ошибке оказывается в первом результате, и EF честно генерирует для него исключение.
Вот этот вариант обвязки, написанной врукопашную, работает корректно:

SqlConnection connection = new SqlConnection("…");
SqlCommand command = new SqlCommand("Add_Test_Record", connection);
command.CommandType = CommandType.StoredProcedure;
SqlParameter RetVal = command.Parameters.Add("RetVal", SqlDbType.Int);
RetVal.Direction = ParameterDirection.ReturnValue;

connection.Open();
try
{
    SqlDataReader reader = command.ExecuteReader();
    do
    {
        while (reader.Read())
        {
            Console.WriteLine("{0}", reader.GetInt32(0));
        }
    }
    while (reader.NextResult());

    reader.Close();
    Console.WriteLine("Passed without exceptions :-(");
}
catch (Exception e)
{
    Console.WriteLine(e);
}

Console.ReadKey();

Что же нам теперь делать? Ну… Не знаю :-).
Ждать стабильной версии EF6, где этого бага нет (по необъяснимому стечению обстоятельств его поправили именно в OpenSource версии фреймворка).
Или взывать к Microsoft.
Или как-нибудь уговорить своих DBA писать хранимки так, чтобы они не генерировали никаких выборок перед потенциальными райзами (например, выборки можно прятать в переменные табличного типа и селектить из них где-нибудь в конце… костыль, конечно, но вполне может оказаться меньшим злом. Учтите, кстати, что инструкция output также порождает выборку).
Или отказаться от EF-привязок к потенциально опасным хранимкам и писать их на ADO.NET врукопашную.
Или ещё что-нибудь придумать. Я, кстати, тоже ещё подумаю, может получится какой-нибудь workaround сообразить минимальными усилиями.

1 thought on “Entity Framework, RAISEERROR, Microsoft и все-все-все

  1. Тоже нашел этот баг, полез искать решения, а таких постов в инете 1000. О чем думает microsoft понятия не имею.

    Нравится

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход /  Изменить )

Google photo

Для комментария используется ваша учётная запись Google. Выход /  Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход /  Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход /  Изменить )

Connecting to %s