Подтвердить что ты не робот

Как поймать исходное (внутреннее) исключение в С#?

Я вызываю функцию, которая генерирует настраиваемое исключение:

GetLockOwnerInfo(...)

Эта функция, в свою очередь, вызывает функцию, которая генерирует исключение:

GetLockOwnerInfo(...)
   ExecuteReader(...)

Эта функция, в свою очередь, вызывает функцию, которая генерирует исключение:

GetLockOwnerInfo(...)
   ExecuteReader(...)
      ExecuteReader(...)

И так далее:

GetLockOwnerInfo(...)
   ExecuteReader(...)
      ExecuteReader(...)
         ExecuteReaderClient(...)
             Fill(...)

Одна из этих функций выдает SqlException, хотя этот код не знает, что такое SqlException.

Более высокие уровни обернут SqlException в другой BusinessRuleException, чтобы включить некоторые специальные свойства и дополнительные сведения, включая "исходное" исключение как InnerException:

catch (DbException ex)
{
    BusinessRuleExcpetion e = new BusinessRuleException(ex)
    ...
    throw e;
}

Более высокие уровни обернут BusinessRuleException в другой LockerException, чтобы включить некоторые специальные свойства и дополнительные сведения, включая "исходное" исключение как InnerException:

catch (BusinessRuleException ex)
{
    LockerException e = new LockerException(ex)
    ...
    throw e;
}

Проблема в том, что я хочу поймать origianl SqlException, чтобы проверить конкретный код ошибки.

Но нет способа "поймать внутреннее исключение":

try
{
   DoSomething();
}
catch (SqlException e)
{
   if (e.Number = 247) 
   {
      return "Someone";
   }
   else
      throw;
}

Я думал о том, чтобы поймать SqlException сразу, когда он был брошен, и скопировать различные значения в повторное исключение - но этот код не зависит от Sql. Он испытывает SqlException, но он не имеет отношения к SqlException.

Я думал об улавливании всех исключений:

try
{
   DoSomething(...);
}
catch (Exception e)
{
   SqlException ex = HuntAroundForAnSqlException(e);
   if (ex != null)
   {
      if (e.Number = 247) 
      {
          return "Someone";
      }
      else
         throw;
   }
   else
      throw;
}

Но этот ужасный код.

Учитывая, что .NET не позволяет вам изменить Message в Exception, чтобы включить дополнительную информацию, каков предполагаемый механизм для обнаружения оригинальных исключений?

4b9b3361

Ответ 1

Мне очень неприятно говорить об этом, но вы не можете поймать внутреннее исключение.

Что вы можете сделать, это проверить его.

Я предлагаю вам поймать ваше исключение высокого уровня (я считаю, что оно было LockerException) и проверить свойство InnerException этого исключения. Проверьте тип, и если это не SqlException, проверьте InnerException этого исключения. Пройдите каждый, пока не найдете тип SqlException, а затем получите нужные данные.

Тем не менее, я согласен с dasblinkenlight, что вы должны рассмотреть - если возможно - тяжелый рефактор вашей инфраструктуры исключений.

Ответ 2

Проверка кода ошибки обернутого исключения не является хорошей практикой, потому что она очень сильно вредит инкапсуляции. Представьте, в какой-то момент переписывая логику, чтобы читать из источника, отличного от SQL, например, веб-службы. Он будет бросать что-то отличное от SQLException при одном и том же условии, и ваш внешний код не сможет обнаружить его.

Вы должны добавить код для блокировки блоков SQLException, чтобы проверить на e.Number = 247 прямо тогда и там, и бросить BusinessRuleException с некоторым свойством, которое отличает его от BusinessRuleException, созданного в ответ на не-t20 > и SQLException с e.Number != 247 каким-либо значимым образом. Например, если магическое число 247 означает, что вы столкнулись с дубликатом (чистая спекуляция с моей стороны в данный момент), вы можете сделать что-то вроде этого:

catch (SQLException e) {
    var toThrow = new BusinessRuleException(e);
    if (e.Number == 247) {
        toThrow.DuplicateDetected = true;
    }
    throw toThrow;
}

Когда вы поймаете BusinessRuleException позже, вы можете проверить его свойство DuplicateDetected и действовать соответственно.

РЕДАКТИРОВАТЬ 1 (в ответ на комментарий, что код DB-чтения не может проверить на SQLException)

Вы также можете изменить BusinessRuleException, чтобы проверить SQLException в своем конструкторе, например:

public BusinessRuleException(Exception inner)
:   base(inner) {
    SetDuplicateDetectedFlag(inner);
}

public BusinessRuleException(string message, Exception inner)
:   base(message, inner) {
    SetDuplicateDetectedFlag(inner);
}

private void SetDuplicateDetectedFlag(Exception inner) {
    var innerSql = inner as SqlException;
    DuplicateDetected = innerSql != null && innerSql.Number == 247;
}

Это менее желательно, потому что он разрушает инкапсуляцию, но по крайней мере он делает это в одном месте. Если вам нужно изучить другие типы исключений (например, поскольку вы добавили источник веб-службы), вы можете добавить его к методу SetDuplicateDetectedFlag, и все будет работать снова.

Ответ 4

Наличие внешнего слоя приложения, касающегося деталей обернутого исключения, является запахом кода; чем глубже обертывание, тем больший запах. Класс, который вы теперь обертываете SqlException, в dbException, предположительно, предназначен для отображения SqlClient в качестве общего интерфейса базы данных. Таким образом, этот класс должен включать средства различения различных исключительных условий. Он может, например, определить dbTimeoutWaitingForLockException и решить бросить его, когда он уловит исключение SqlException и определяет на основе кода ошибки, что был тайм-аут блокировки. В vb.net может быть более чистым иметь тип dbException, который предоставляет перечисление ErrorCause, поэтому можно было бы сказать Catch Ex as dbException When ex.Cause = dbErrorCauses.LockTimeout, но, к сожалению, фильтры исключений не могут использоваться на С#.

Если у вас есть ситуация, когда оболочка внутреннего класса не будет достаточно знать о том, что она делает, чтобы знать, как она должна отображать исключения, может быть полезно, чтобы метод внутреннего класса принимал делегат исключения, который сделайте исключение, которое внутренний класс поймал или "хотел" бросить, и обернуть его таким образом, который соответствует внешнему классу. Такой подход, вероятно, будет излишним в тех случаях, когда внутренний класс вызывается непосредственно из внешнего класса, но может быть полезен, если существуют промежуточные классы.

Ответ 5

Хороший вопрос и хорошие ответы!

Я просто хочу дополнить ответы, уже приведенные с дальнейшими мыслями:

С одной стороны, я согласен с dasblinkenlight и другими пользователями. Если вы поймаете одно исключение, чтобы перебросить исключение другого типа с исходным исключением, установленным как внутреннее исключение, тогда вы должны сделать это только по той причине, что поддерживать контракт метода. (Доступ к серверу SQL - это деталь реализации, которую вызывающий абонент не должен/не должен/не может знать, поэтому он не может ожидать, что будет выбрано SqlException (или DbException).

Применение этого метода, однако, имеет некоторые последствия, о которых следует знать:

  • Вы скрываете основную причину ошибки. В вашем примере вы сообщаете вызывающему, что бизнес-правило недействительно (?), нарушено (?) и т.д., когда на самом деле там была проблема с доступом к БД (что было бы сразу же ясно, если бы DbException было разрешено еще больше раздувать стек вызовов).
  • Вы скрываете место, где первоначально возникла ошибка. Свойство StackTrace исключенного пойма будет указывать на блокирующий блок, расположенный далеко от местоположения, из-за которого произошла ошибка. Это может сделать отладку печально сложной, если вы не возьмете очень осторожно регистрировать трассировки стека всех внутренних исключений. (Это особенно актуально после того, как программное обеспечение было развернуто в производство, и у вас нет средств для отладчик...)

Учитывая, что .NET не позволяет вам изменять сообщение об исключении, чтобы включить дополнительную информацию, каков предполагаемый механизм для обнаружения оригинальных исключений?

Верно, что .NET не позволяет вам изменять сообщение об исключении. Однако он предоставляет другой механизм предоставления дополнительной информации в Exception с помощью словаря Exception.Data. Поэтому, если все, что вы хотите сделать, это добавить дополнительные данные в исключение, тогда нет причин для обертывания исходного исключения и выброса нового. Вместо этого просто выполните:

public void DoStuff(String filename)
{
    try {
       // Some file I/O here...
    }
    catch (IOException ex) {

      // Add filename to the IOException
      ex.Data.Add("Filename", filename);

      // Send the exception along its way
      throw;
    }
}