在开发 C# 应用程序时,异常处理是一个至关重要的环节。自定义异常设计和全局异常处理中间件能帮助我们更好地管理和处理程序运行时出现的问题。下面就来详细说说这两方面的内容。

一、自定义异常设计

1. 什么是自定义异常

在 C# 里,系统自带了很多异常类型,像 ArgumentExceptionNullReferenceException 这些。不过在实际开发中,系统自带的异常类型可能没办法满足我们的需求,这时候就需要自定义异常了。自定义异常可以让我们更精准地描述程序里出现的问题,方便后续的调试和维护。

2. 自定义异常的步骤

要自定义一个异常,其实很简单,主要就是创建一个继承自 Exception 类的新类。下面给大家举个例子:

// C# 技术栈
// 自定义一个用户未找到异常类
public class UserNotFoundException : Exception
{
    // 构造函数,接收一个错误消息
    public UserNotFoundException(string message) : base(message)
    {
    }
}

在这个例子中,我们定义了一个 UserNotFoundException 类,它继承自 Exception 类。通过构造函数,我们可以传入一个错误消息,用来描述用户未找到的具体情况。

3. 自定义异常的使用

定义好自定义异常后,就可以在代码里使用它了。下面是一个简单的示例:

// C# 技术栈
public class UserService
{
    private List<string> users = new List<string> { "Alice", "Bob", "Charlie" };

    // 根据用户名查找用户的方法
    public string FindUser(string username)
    {
        // 检查用户列表中是否包含指定的用户名
        if (!users.Contains(username))
        {
            // 如果不包含,抛出用户未找到异常
            throw new UserNotFoundException($"用户 {username} 未找到。");
        }
        return username;
    }
}

class Program
{
    static void Main()
    {
        UserService service = new UserService();
        try
        {
            // 尝试查找一个不存在的用户
            string user = service.FindUser("David");
            Console.WriteLine($"找到用户: {user}");
        }
        catch (UserNotFoundException ex)
        {
            // 捕获用户未找到异常并输出错误消息
            Console.WriteLine($"错误: {ex.Message}");
        }
    }
}

在这个示例中,UserService 类里有一个 FindUser 方法,它会检查用户列表中是否包含指定的用户名。如果不包含,就会抛出 UserNotFoundException 异常。在 Main 方法里,我们调用 FindUser 方法,并且用 try-catch 块来捕获这个异常,然后输出错误消息。

二、全局异常处理中间件的实现

1. 什么是全局异常处理中间件

在 C# 的 Web 应用程序里,当程序出现异常时,我们希望能有一个统一的地方来处理这些异常,避免在每个控制器方法里都写重复的异常处理代码。全局异常处理中间件就可以帮我们实现这个功能,它可以捕获应用程序里所有未处理的异常,然后进行统一的处理。

2. 在 .NET Core 中实现全局异常处理中间件

下面是一个在 .NET Core 里实现全局异常处理中间件的示例:

// C# 技术栈
// 全局异常处理中间件类
public class GlobalExceptionMiddleware
{
    private readonly RequestDelegate _next;

    // 构造函数,接收一个 RequestDelegate 对象
    public GlobalExceptionMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    // 中间件的核心方法
    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            // 调用下一个中间件
            await _next(context);
        }
        catch (Exception ex)
        {
            // 处理异常
            await HandleExceptionAsync(context, ex);
        }
    }

    // 处理异常的方法
    private async Task HandleExceptionAsync(HttpContext context, Exception ex)
    {
        // 设置响应状态码为 500
        context.Response.StatusCode = 500;
        // 设置响应内容类型为 JSON
        context.Response.ContentType = "application/json";
        // 构造错误响应信息
        var errorResponse = new
        {
            ErrorMessage = "发生了内部服务器错误,请稍后再试。",
            ExceptionMessage = ex.Message
        };
        // 将错误响应信息序列化为 JSON 字符串并写入响应流
        await context.Response.WriteAsJsonAsync(errorResponse);
    }
}

// 扩展方法,用于将全局异常处理中间件添加到应用程序的请求管道中
public static class GlobalExceptionMiddlewareExtensions
{
    public static IApplicationBuilder UseGlobalExceptionMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<GlobalExceptionMiddleware>();
    }
}

在这个示例中,GlobalExceptionMiddleware 类是全局异常处理中间件的核心。InvokeAsync 方法会尝试调用下一个中间件,如果出现异常,就会调用 HandleExceptionAsync 方法来处理异常。HandleExceptionAsync 方法会设置响应状态码为 500,并且返回一个包含错误信息的 JSON 响应。

3. 使用全局异常处理中间件

Startup.cs 文件里,我们可以使用扩展方法将全局异常处理中间件添加到应用程序的请求管道中:

// C# 技术栈
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    // 添加全局异常处理中间件
    app.UseGlobalExceptionMiddleware();

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

这样,当应用程序里出现未处理的异常时,就会被全局异常处理中间件捕获并处理。

三、应用场景

1. 自定义异常的应用场景

  • 业务逻辑验证:在进行业务逻辑处理时,如果某些条件不满足,就可以抛出自定义异常。比如在用户注册时,如果用户名已经存在,就可以抛出 UsernameAlreadyExistsException 异常。
  • 数据访问异常:在访问数据库或者其他外部资源时,如果出现问题,也可以抛出自定义异常。比如在查询数据库时,如果查询结果为空,就可以抛出 DataNotFoundException 异常。

2. 全局异常处理中间件的应用场景

  • Web 应用程序:在 Web 应用程序里,全局异常处理中间件可以统一处理所有未处理的异常,避免用户看到系统的错误信息,提高用户体验。
  • 微服务架构:在微服务架构中,每个微服务都可以使用全局异常处理中间件来处理异常,保证服务的稳定性和可靠性。

四、技术优缺点

1. 自定义异常的优缺点

  • 优点
    • 提高代码可读性:自定义异常可以更清晰地表达程序里出现的问题,让代码更容易理解。
    • 方便调试和维护:在调试和维护代码时,自定义异常可以提供更详细的错误信息,帮助我们更快地定位问题。
  • 缺点
    • 增加代码复杂度:定义和使用自定义异常会增加代码的复杂度,尤其是在异常类型较多的情况下。

2. 全局异常处理中间件的优缺点

  • 优点
    • 统一处理异常:全局异常处理中间件可以统一处理所有未处理的异常,避免在每个控制器方法里都写重复的异常处理代码。
    • 提高用户体验:可以给用户返回统一的错误信息,避免用户看到系统的错误信息,提高用户体验。
  • 缺点
    • 可能掩盖问题:如果全局异常处理中间件处理不当,可能会掩盖一些潜在的问题,导致问题难以发现和解决。

五、注意事项

1. 自定义异常的注意事项

  • 异常命名:自定义异常的名称要具有描述性,能够清晰地表达异常的含义。
  • 异常层次结构:要合理设计异常的层次结构,让异常之间有清晰的继承关系。

2. 全局异常处理中间件的注意事项

  • 日志记录:在处理异常时,要记录详细的日志信息,方便后续的排查和分析。
  • 异常过滤:可以根据异常类型进行过滤,对不同类型的异常进行不同的处理。

六、文章总结

自定义异常设计和全局异常处理中间件在 C# 开发中非常重要。自定义异常可以让我们更精准地描述程序里出现的问题,方便调试和维护;全局异常处理中间件可以统一处理所有未处理的异常,提高用户体验和服务的稳定性。在实际开发中,我们要合理使用自定义异常和全局异常处理中间件,同时注意相关的注意事项,这样才能编写出高质量的代码。