将上下文与 UI 应用程序中引发的异常关联起来的推荐方法是什么?

发布于 2024-11-04 23:10:05 字数 2981 浏览 0 评论 0原文

我有一个 UI 应用程序,它访问数据库并且还必须能够对文件执行各种操作。不用说,在应用程序过程中可能会抛出各种/大量异常,例如:

  • 数据库脱机。
  • 找不到该文件(之前扫描到数据库中)。
  • 该文件被另一个用户/进程锁定,无法访问。
  • 文件的只读属性已设置且无法修改。
  • 安全权限拒绝对文件的访问(读或写)。

在引发异常时,错误的精确细节是已知的。但是,有时您需要让异常在调用堆栈的更高层捕获,以包含异常的上下文,以便您可以创建并呈现用户友好的错误消息;例如,在文件复制、文件移动或文件删除操作期间可能会遇到被另一个进程锁定的文件。

出于讨论目的,假设我们有一个必须对文件执行各种操作的方法;它必须将文件读入内存,修改数据并将数据写回,如下例所示:

private void ProcessFile(string fileName)
{
    try
    {
        string fileData = ReadData(fileName);

        string modifiedData = ModifyData(fileData);

        WriteData(fileName, modifiedData);
    }
    catch (UnauthorizedAccessException ex)
    {
        // The caller does not have the required permission.
    }
    catch (PathTooLongException ex)
    {
        // The specified path, file name, or both exceed the system-defined maximum length.
        // For example, on Windows-based platforms, paths must be less than 248 characters
        // and file names must be less than 260 characters.
    }
    catch (ArgumentException ex)
    {
        // One or more paths are zero-length strings, contain only white space, or contain
        // one or more invalid characters as defined by InvalidPathChars.
        // Or path is prefixed with, or contains only a colon character (:).
    }
    catch (NotSupportedException ex)
    {
        // File name is in an invalid format.
        // E.g. path contains a colon character (:) that is not part of a drive label ("C:\").
    }
    catch (DirectoryNotFoundException ex)
    {
        // The path specified is invalid. For example, it is on an unmapped drive.
    }
    catch (FileNotFoundException ex)
    {
        // File was not found
    }
    catch (IOException ex)
    {
        // Various other IO errors, including network name is not known.
    }
    catch (Exception ex)
    {
        // Catch all for unknown/unexpected exceptions
    }
}

当记录错误消息并向用户显示错误消息时,我们希望尽可能描述出现问题的原因以及任何可能的建议这可能会导致解决。如果文件被锁定,我们应该能够通知用户,以便他可以在文件释放后重试。

在上面的示例中,使用所有异常 catch 子句,我们仍然不知道哪个操作(上下文)导致异常。打开和读取文件、修改数据或将更改写回文件系统时是否发生异常?

一种方法是将 try/catch 块移动到这些“操作”方法中的每一个中。这意味着将相同的异常处理逻辑复制/重复到所有三个方法中。当然,为了避免在多个方法中重复相同的逻辑,我们可以将异常处理封装到另一个通用方法中,该方法将调用捕获通用 System.Exception 并将其传递。

另一种方法是添加“枚举”或其他定义上下文的方法,以便我们知道异常发生的位置,如下所示:

public enum ActionPerformed
{
    Unknown,
    ReadData,
    ModifyData,
    WriteData,
    ...
}

private void ProcessFile(string fileName)
{
    ActionPerformed action;

    try
    {
        action = ActionPerformed.ReadData;
        string fileData = ReadData(fileName);

        action = ActionPerformed.ModifyData;
        string modifiedData = ModifyData(fileData);

        action = ActionPerformed.WriteData;
        WriteData(fileName, modifiedData);
    }
    catch (...)
    {
        ...
    }
}

现在,在每个 catch 子句中,我们将知道异常的上下文引发异常时执行的操作。

是否有推荐的方法来解决识别与异常相关的上下文的问题?这个问题的答案可能是主观的,但如果有一个设计模式或推荐的方法,我愿意遵循它。

I have a UI application, that accesses a database and must also be able to perform various actions on files. Needless to say various/numerous exceptions could be thrown during the course of the application, such as:

  • The database is offline.
  • The file (previously scanned into a database), is not found.
  • The file is locked by another user/process and cannot be accessed.
  • The file's read-only attribute is set and the file cannot be modified.
  • Security permissions deny access to the file (read or write).

The precise details of the error is known at the point where the exception is raised. However, sometimes you need to let the exception be caught higher up the call stack to include context with the exception, so that you can create and present a user friendly error message; e.g. a file being locked by another process could be encountered during a file copy, file move or file delete operation.

Let's say for discussion purposes we have a single method that must perform various actions on a file; it must read a file into memory, modify the data and write the data back out as in the below example:

private void ProcessFile(string fileName)
{
    try
    {
        string fileData = ReadData(fileName);

        string modifiedData = ModifyData(fileData);

        WriteData(fileName, modifiedData);
    }
    catch (UnauthorizedAccessException ex)
    {
        // The caller does not have the required permission.
    }
    catch (PathTooLongException ex)
    {
        // The specified path, file name, or both exceed the system-defined maximum length.
        // For example, on Windows-based platforms, paths must be less than 248 characters
        // and file names must be less than 260 characters.
    }
    catch (ArgumentException ex)
    {
        // One or more paths are zero-length strings, contain only white space, or contain
        // one or more invalid characters as defined by InvalidPathChars.
        // Or path is prefixed with, or contains only a colon character (:).
    }
    catch (NotSupportedException ex)
    {
        // File name is in an invalid format.
        // E.g. path contains a colon character (:) that is not part of a drive label ("C:\").
    }
    catch (DirectoryNotFoundException ex)
    {
        // The path specified is invalid. For example, it is on an unmapped drive.
    }
    catch (FileNotFoundException ex)
    {
        // File was not found
    }
    catch (IOException ex)
    {
        // Various other IO errors, including network name is not known.
    }
    catch (Exception ex)
    {
        // Catch all for unknown/unexpected exceptions
    }
}

When logging and presenting error messages to the user we want to be as descriptive as possible as to what went wrong along with any possible recommendations that could lead to a resolution. If a file is locked, we should be able to inform the user of such, so that he could retry later when the file is released.

In the above example, with all the exception catch clauses, we would still not know which action (context) lead to the exception. Did the exception occur while opening and reading the file, when modifying the data or when writing the changes back out to the file system?

One approach would be to move the try/catch block to within each of the these "action" methods. This would mean copying/repeating the same exception handling logic into all three methods. And of course to avoid repeating the same logic in multiple methods we could encapsulate the exception handling into another common method, which would call for catching the generic System.Exception and passing it on.

Another approach would be to add an "enum" or other means of defining the context, so that we know where the exception occurred as follows:

public enum ActionPerformed
{
    Unknown,
    ReadData,
    ModifyData,
    WriteData,
    ...
}

private void ProcessFile(string fileName)
{
    ActionPerformed action;

    try
    {
        action = ActionPerformed.ReadData;
        string fileData = ReadData(fileName);

        action = ActionPerformed.ModifyData;
        string modifiedData = ModifyData(fileData);

        action = ActionPerformed.WriteData;
        WriteData(fileName, modifiedData);
    }
    catch (...)
    {
        ...
    }
}

Now, within each catch clause, we would know the context of the action being performed when the exception was raised.

Is there a recommended approach in addressing this problem of identifying context related to an exception? The answer to this problem maybe subjective, but if there is a design pattern or recommended approach to this, I would like to follow it.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

暖伴 2024-11-11 23:10:05

创建异常时,请在抛出异常之前将其 Message 属性设置为描述性内容。然后,您可以在更高的位置向用户显示此消息。

When you create the exception, set it's Message property to something descriptive before throwing it. Then higher up you can just display this Message to the user.

还如梦归 2024-11-11 23:10:05

我们通常记录异常(使用 log4net 或 nlog),然后抛出一个自定义异常,并带有用户可以理解的友好消息。

We normally log the exception (with either log4net or nlog) and then throw a custom exception with a friendly message that the user can understand.

幻想少年梦 2024-11-11 23:10:05

我的观点是,MS 本地化异常消息属性的方法是完全错误的。由于 .NET 框架有语言包,您可以从中文安装中得到神秘的(例如普通话)消息。作为一个非所部署产品语言的母语的开发人员,我应该如何调试它?
我会为面向技术开发人员的消息文本保留异常消息属性,并在其数据属性中添加用户消息。

应用程序的每一层都可以从自己的角度向当前抛出的异常添加用户消息。
如果您假设第一个异常确切地知道出了什么问题以及如何修复它,您应该向用户显示第一个添加的用户消息。上面的所有架构层对来自较低层的特定错误的上下文和了解都较少。这将导致对用户来说不太有用的错误消息。因此,最好在仍然有足够上下文的层中创建用户消息,以便能够告诉用户出了什么问题以及是否可以修复以及如何修复。

为了说明这一点,假设有一个软件,其中有登录表单、Web 服务、后端和存储用户凭据的数据库。当检测到数据库问题时,您将在哪里创建用户消息?

  1. 登录表单
  2. Web服务
  3. 后端
  4. 数据库访问层

    1. IResult res = WebService.LoginUser("user", "pwd");
    2. IResult res = RemoteObject.LoginUser("user","pwd");
    3. 字符串 pwd = QueryPasswordForUser("用户");
    4. 用户 user = NHibernate.Session.Get("user"); -> 抛出 SQLException

数据库抛出 SQLException,因为数据库处于维护模式。

在这种情况下,后端 (3) 仍然有足够的上下文来处理数据库问题,但它也知道用户尝试登录。UI

将通过 Web 服务获取不同的异常对象,因为无法保留类型标识跨越应用程序域/进程边界。更深层次的原因是远程客户端没有安装NHibernate和SQL Server,导致无法通过序列化传输异常堆栈。

您必须将异常堆栈转换为更通用的异常,该异常是 Web 服务数据契约的一部分,这会导致 Web 服务边界处的信息丢失。

如果您尝试在最高级别(UI)上尝试将所有可能的系统错误映射到有意义的用户消息,则将 UI 逻辑绑定到后端的内部工作原理。这不仅是一种不好的做法,而且也很难做到,因为您将丢失有用的用户消息所需的上下文。

 catch(SqlException ex)  
 {
    if( ex.ErrorCode == DB.IsInMaintananceMode ) 
       Display("Database ??? on server ??? is beeing maintained. Please wait a little longer or contact your administrator for server ????");
    ....

由于 Web 服务边界,它实际上更像是

 catch(Exception ex)
 {
       Excepton first = GetFirstException(ex);
       RemoteExcepton rex = first as RemoteExcepton;
       if( rex.OriginalType == "SQLException" )
       {
           if( rex.Properties["Data"] == "DB.IsMaintainanceMode" )
           {
              Display("Database ??? on server ??? is beeing maintained. Please wait a little longer or contact your administrator for server ????");

因为异常将被来自其他层的其他异常所包装,因此您在 UI 层中针对后端的内部进行编码。

另一方面,如果您在后端层执行此操作,您就知道您的主机名是什么,您就知道您尝试访问哪个数据库。当你做得正确时,事情会变得容易得多。

   catch(SQLException ex)
   {
       ex.Data["UserMessage"] = MapSqlErrorToString(ex.ErrorCode, CurrentHostName, Session.Database)'
       throw;
   }

作为一般规则,您应该将用户消息添加到最深层的异常中,您仍然知道用户尝试执行的操作。

你的,
阿洛伊斯·克劳斯

My opinion is that the MS approach to localize the message property of exceptions is all wrong. Since there are language packs for the .NET framework you get from a Chinese installation cryptic (e.g. Mandarin) messages back. How am I supposed to debug this as a developer who is not a native speaker of the deployed product language?
I would reserve the exception message property for the technical developer oriented message text and add a user message in its Data property.

Every layer of your application can add a user message from its own perspective to the current thrown exception.
If you assume the that the first exception knows exactly what did go wrong and how it could be fixed you should display the first added user message to the user. All architectural layers above will have less context and knowledge about the specific error from a lower layer. This will result in less helpful error messages for the user. It is therefore best to create the user message in the layer where you have still enough context to be able to tell the user what did go wrong and if and how it can be fixed.

To illustrate the point assume a software where you have a login form, a web service, a backend and a database where the user credentials are stored. Where would you create the user message when a database problem is detected?

  1. Login Form
  2. Web Service
  3. Backend
  4. Database Access Layer

    1. IResult res = WebService.LoginUser("user", "pwd");
    2. IResult res = RemoteObject.LoginUser("user","pwd");
    3. string pwd = QueryPasswordForUser("user");
    4. User user = NHibernate.Session.Get("user"); -> SQLException is thrown

The Database throws a SQLException because the db it is in maintainance mode.

In this case the backend (3) does still have enough context to deal with DB problems but it does also know that a user tried to log in.

The UI will get via the web service a different the exception object because type identity cannot be preserved accross AppDomain/Process boundaries. The deeper reason is that the remote client does not have NHibernate and SQL server installed which makes it impossible to transfer the exception stack via serialization.

You have to convert the exception stack into a more generic exception which is part of the web service data contract which results in information loss at the Web Service boundary.

If you try at the highest level, the UI, try to map all possible system errors to a meaningful user message you bind your UI logic to the inner workings in your backends. This is not only a bad practice it is also hard to do because you will be missing context needed for useful user messages.

 catch(SqlException ex)  
 {
    if( ex.ErrorCode == DB.IsInMaintananceMode ) 
       Display("Database ??? on server ??? is beeing maintained. Please wait a little longer or contact your administrator for server ????");
    ....

Due to the web service boundary it will be in reality more something like

 catch(Exception ex)
 {
       Excepton first = GetFirstException(ex);
       RemoteExcepton rex = first as RemoteExcepton;
       if( rex.OriginalType == "SQLException" )
       {
           if( rex.Properties["Data"] == "DB.IsMaintainanceMode" )
           {
              Display("Database ??? on server ??? is beeing maintained. Please wait a little longer or contact your administrator for server ????");

Since the exception will be wrapped by other exceptions from other layers you are coding in the UI layer against the internals of your backend.

On the other hand if you do it at the backend layer you know what your host name is, you know which database you did try to access. Things become much easier when you do it at the right level.

   catch(SQLException ex)
   {
       ex.Data["UserMessage"] = MapSqlErrorToString(ex.ErrorCode, CurrentHostName, Session.Database)'
       throw;
   }

As a general rule you should be adding your user messages to the exception in the deepest layer where you still know what the user tried to do.

Yours,
Alois Kraus

递刀给你 2024-11-11 23:10:05

如果可能的话,您应该从每个可以抛出异常类型的方法中抛出不同的异常类型。例如,如果您担心 .NET 异常冲突,ModifyData 方法可以在内部捕获共享异常类型并重新抛出它们。

You should throw different exception types if possible from each method that can. For example, your ModifyData method could internally catch shared exception types and rethrow them if you are worried about .NET exception collision.

痴梦一场 2024-11-11 23:10:05

您可以创建自己的异常类,并将其从 catch 块中返回给用户,并将消息放入新的异常类中。

catch (NotSupportedException ex)
    {
        YourCustomExceptionClass exception = new YourCustomExceptionClass(ex.message);
        throw exception;
    }

您可以将尽可能多的信息保存到异常类中,这样用户就可以获得所有信息,并且只有您希望他们拥有的信息。

编辑:

事实上,您可以在自定义异常类中创建一个异常成员并执行此操作。

catch (NotSupportedException ex)
{  
    YourCustomExceptionClass exception = new YourCustomExceptionClass(ex.message);
    exception.yourExceptionMemberofTypeException = ex;
    throw exception;
}

这样,您可以给用户一个好的消息,同时也给他们底层的内部异常。 .NET 一直通过 InnerException 来执行此操作。

You could create your own exception class and throw it back up the user from your catch block and put the message into your new exception class.

catch (NotSupportedException ex)
    {
        YourCustomExceptionClass exception = new YourCustomExceptionClass(ex.message);
        throw exception;
    }

You can save as much info as you want into your exception class and that way the user has all the information and only the information that you want them to have.

EDIT:

In fact, you could make an Exception member in your Custom Exception class and do this.

catch (NotSupportedException ex)
{  
    YourCustomExceptionClass exception = new YourCustomExceptionClass(ex.message);
    exception.yourExceptionMemberofTypeException = ex;
    throw exception;
}

This way, you can give the user a nice message, but also give them the underlying inner exception. .NET does this all the time with InnerException.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文