在开始之前,我们实现一个之前的遗留问题,这个问题是有人在GitHub Issues(https://github.com/Meowv/Blog/issues/8)上提出来的,就是当我们对Swagger进行分组,实现IDocumentFilter接口添加了文档描述信息后,切换分组时会显示不属于当前分组的Tag。

经过研究和分析发现,是可以解决的,我不知道大家有没有更好的办法,我的实现方法请看:

  1. //SwaggerDocumentFilter.cs
  2. ...
  3. public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
  4. {
  5. var tags = new List<OpenApiTag>{...}
  6. #region 实现添加自定义描述时过滤不属于同一个分组的API
  7. var groupName = context.ApiDescriptions.FirstOrDefault().GroupName;
  8. var apis = context.ApiDescriptions.GetType().GetField("_source", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(context.ApiDescriptions) as IEnumerable<ApiDescription>;
  9. var controllers = apis.Where(x => x.GroupName != groupName).Select(x => ((ControllerActionDescriptor)x.ActionDescriptor).ControllerName).Distinct();
  10. swaggerDoc.Tags = tags.Where(x => !controllers.Contains(x.Name)).OrderBy(x => x.Name).ToList();
  11. #endregion
  12. }
  13. ...

根据调试代码发现,我们可以从context.ApiDescriptions获取到当前显示的是哪一个分组下的API。

然后使用GetType().GetField(string name, BindingFlags bindingAttr)获取到_source,当前项目的所有API,里面同时也包含了ABP默认生成的一些接口。

再将API中不属于当前分组的API筛选掉,用Select查询出所有的Controller名称进行去重。

因为OpenApiTag中的Name名称与Controller的Name是一致的,所以最后将包含controllers名称的tag查询出来取反,即可满足需求。


上一篇文章(https://www.cnblogs.com/meowv/p/12935693.html)集成了GitHub,使用JWT的方式完成了身份认证和授权,保护了我们写的API接口。

本篇主要实现对项目中出现的异常仅需处理,当出现不可避免的错误时,或者未授权用户调用接口时,可以进行有效的监控和日志记录。

目前调用未授权接口,会直接返回一个状态码为401的错误页面,这样显得太不友好,我们还是用之前写的统一返回模型来告诉调用者,你是未授权的,调不了我的接口,上篇也有提到过,我们将用两种方式来解决。

方式一 :使用AddJwtBearer()扩展方法下面的options.Events事件机制。

  1. //MeowvBlogHttpApiHostingModule.cs
  2. ...
  3. //应用程序提供的对象,用于处理承载引发的事件,身份验证处理程序
  4. options.Events = new JwtBearerEvents
  5. {
  6. OnChallenge = async context =>
  7. {
  8. // 跳过默认的处理逻辑,返回下面的模型数据
  9. context.HandleResponse();
  10. context.Response.ContentType = "application/json;charset=utf-8";
  11. context.Response.StatusCode = StatusCodes.Status200OK;
  12. var result = new ServiceResult();
  13. result.IsFailed("UnAuthorized");
  14. await context.Response.WriteAsync(result.ToJson());
  15. }
  16. };
  17. ...

在项目启动时,实例化了OnChallenge,如果用户调用未授权,将请求的状态码赋值为200,并返回模型数据。

如图所示,可以看到已经成功返回了一段比较友好的JSON数据。

  1. {
  2. "Code": 1,
  3. "Message": "UnAuthorized",
  4. "Success": false,
  5. "Timestamp": 1590226085318
  6. }

方式二 :使用中间件的方式。

我们注释掉上面的代码,在.HttpApi.Hosting添加文件夹Middleware,新建一个中间件ExceptionHandlerMiddleware.cs

  1. using Meowv.Blog.ToolKits.Base;
  2. using Meowv.Blog.ToolKits.Extensions;
  3. using Microsoft.AspNetCore.Http;
  4. using System;
  5. using System.Net;
  6. using System.Threading.Tasks;
  7. namespace Meowv.Blog.HttpApi.Hosting.Middleware
  8. {
  9. /// <summary>
  10. /// 异常处理中间件
  11. /// </summary>
  12. public class ExceptionHandlerMiddleware
  13. {
  14. private readonly RequestDelegate next;
  15. public ExceptionHandlerMiddleware(RequestDelegate next)
  16. {
  17. this.next = next;
  18. }
  19. /// <summary>
  20. /// Invoke
  21. /// </summary>
  22. /// <param name="context"></param>
  23. /// <returns></returns>
  24. public async Task Invoke(HttpContext context)
  25. {
  26. try
  27. {
  28. await next(context);
  29. }
  30. catch (Exception ex)
  31. {
  32. await ExceptionHandlerAsync(context, ex.Message);
  33. }
  34. finally
  35. {
  36. var statusCode = context.Response.StatusCode;
  37. if (statusCode != StatusCodes.Status200OK)
  38. {
  39. Enum.TryParse(typeof(HttpStatusCode), statusCode.ToString(), out object message);
  40. await ExceptionHandlerAsync(context, message.ToString());
  41. }
  42. }
  43. }
  44. /// <summary>
  45. /// 异常处理,返回JSON
  46. /// </summary>
  47. /// <param name="context"></param>
  48. /// <param name="message"></param>
  49. /// <returns></returns>
  50. private async Task ExceptionHandlerAsync(HttpContext context, string message)
  51. {
  52. context.Response.ContentType = "application/json;charset=utf-8";
  53. var result = new ServiceResult();
  54. result.IsFailed(message);
  55. await context.Response.WriteAsync(result.ToJson());
  56. }
  57. }
  58. }

RequestDelegate是一种请求委托类型,用来处理HTTP请求的函数,返回的是delegate,实现异步的Invoke方法。

这里我写了一个比较通用的方法,当出现异常时直接执行ExceptionHandlerAsync()方法,当没有异常发生时,在finally中判断当前请求状态,可能是200?404?401?等等,不管它是什么,反正不是200,获取到状态码枚举的Key值用来当作错误信息返回,最后也执行ExceptionHandlerAsync()方法,返回我们自定义的模型。

写好了中间件,然后在OnApplicationInitialization(...)中使用它。

  1. public override void OnApplicationInitialization(ApplicationInitializationContext context)
  2. {
  3. ...
  4. // 异常处理中间件
  5. app.UseMiddleware<ExceptionHandlerMiddleware>();
  6. ...
  7. }

同样可以达到效果,相比之下他还支持状态非401的错误返回,比如我们访问一个不存在的页面:https://localhost:44388/aaa ,也可以友好的进行处理。

当然这两种方式可以共存,互不影响。

还有一种处理异常的方式,就是我们的过滤器Filter,abp已经默认为我们实现了全局的异常模块,详情可以看其文档:https://docs.abp.io/zh-Hans/abp/latest/Exception-Handling ,在这里,我准备移除abp提供的异常处理模块,自己实现一个。

先看一下目前的异常显示情况,我们在HelloWorldController中写一个异常接口。

  1. //HelloWorldController.cs
  2. ...
  3. [HttpGet]
  4. [Route("Exception")]
  5. public string Exception()
  6. {
  7. throw new NotImplementedException("这是一个未实现的异常接口");
  8. }
  9. ...

按理说,他应该会执行到我们写的ExceptionHandlerMiddleware中间件中去,但是被我们的Filter进行拦截了,现在我们移除默认的拦截器AbpExceptionFilter

还是在模块类MeowvBlogHttpApiHostingModuleConfigureServices()方法中。

  1. Configure<MvcOptions>(options =>
  2. {
  3. var filterMetadata = options.Filters.FirstOrDefault(x => x is ServiceFilterAttribute attribute && attribute.ServiceType.Equals(typeof(AbpExceptionFilter)));
  4. // 移除 AbpExceptionFilter
  5. options.Filters.Remove(filterMetadata);
  6. });

options.Filters中找到AbpExceptionFilter,然后Remove掉,此时再看一下有异常的接口。

当我们注释掉我们的中间件时,他就会显示如下图这样。

这个页面有没有很熟悉的感觉?相信做过.net core开发的都遇到过吧。

ok,现在为止已经完美显示了。但到这里还远远不够,说好的自己实现Filter呢?我们现在实现Filter又有什么用呢?我们可以在Filter中可以做一些日志记录。

.HttpApi.Hosting层添加文件夹Filters,新建一个MeowvBlogExceptionFilter.cs的Filter,他需要实现我们的IExceptionFilter接口的OnExceptionAsync()方法即可。

  1. //MeowvBlogExceptionFilter.cs
  2. using Meowv.Blog.ToolKits.Helper;
  3. using Microsoft.AspNetCore.Mvc.Filters;
  4. namespace Meowv.Blog.HttpApi.Hosting.Filters
  5. {
  6. public class MeowvBlogExceptionFilter : IExceptionFilter
  7. {
  8. /// <summary>
  9. /// 异常处理
  10. /// </summary>
  11. /// <param name="context"></param>
  12. /// <returns></returns>
  13. public void OnException(ExceptionContext context)
  14. {
  15. // 日志记录
  16. LoggerHelper.WriteToFile($"{context.HttpContext.Request.Path}|{context.Exception.Message}", context.Exception);
  17. }
  18. }
  19. }

OnException(...)方法很简单,这里只做了记录日志的操作,剩下的交给我们中间件去处理吧。

注意,一定要在移除默认AbpExceptionFilter后,将我们自己实现的MeowvBlogExceptionFilter在模块类ConfigureServices()方法中注入到系统。

  1. ...
  2. Configure<MvcOptions>(options =>
  3. {
  4. ...
  5. // 添加自己实现的 MeowvBlogExceptionFilter
  6. options.Filters.Add(typeof(MeowvBlogExceptionFilter));
  7. });
  8. ...

说到日志,就有很多种处理方式,请选择你熟悉的方式,我这里将使用log4net进行处理,仅供参考。

.ToolKits层添加log4net包,使用命令安装:Install-Package log4net,然后添加文件夹Helper,新建一个LoggerHelper.cs

  1. //LoggerHelper.cs
  2. using log4net;
  3. using log4net.Config;
  4. using log4net.Repository;
  5. using System;
  6. using System.IO;
  7. namespace Meowv.Blog.ToolKits.Helper
  8. {
  9. public static class LoggerHelper
  10. {
  11. private static readonly ILoggerRepository Repository = LogManager.CreateRepository("NETCoreRepository");
  12. private static readonly ILog Log = LogManager.GetLogger(Repository.Name, "NETCorelog4net");
  13. static LoggerHelper()
  14. {
  15. XmlConfigurator.Configure(Repository, new FileInfo("log4net.config"));
  16. }
  17. /// <summary>
  18. /// 写日志
  19. /// </summary>
  20. /// <param name="message"></param>
  21. /// <param name="ex"></param>
  22. public static void WriteToFile(string message)
  23. {
  24. Log.Info(message);
  25. }
  26. /// <summary>
  27. /// 写日志
  28. /// </summary>
  29. /// <param name="message"></param>
  30. /// <param name="ex"></param>
  31. public static void WriteToFile(string message, Exception ex)
  32. {
  33. if (string.IsNullOrEmpty(message))
  34. message = ex.Message;
  35. Log.Error(message, ex);
  36. }
  37. }
  38. }

.HttpApi.Hosting中添加log4net配置文件,log4net.config配置文件如下:

  1. //log4net.config
  2. <?xml version="1.0" encoding="utf-8" ?>
  3. <configuration>
  4. <configSections>
  5. <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net"/>
  6. </configSections>
  7. <log4net debug="false">
  8. <appender name="info" type="log4net.Appender.RollingFileAppender,log4net">
  9. <param name="File" value="log4net/info/" />
  10. <param name="AppendToFile" value="true" />
  11. <param name="MaxSizeRollBackups" value="-1"/>
  12. <param name="MaximumFileSize" value="5MB"/>
  13. <param name="RollingStyle" value="Composite" />
  14. <param name="DatePattern" value="yyyyMMdd\\HH&quot;.log&quot;" />
  15. <param name="StaticLogFileName" value="false" />
  16. <layout type="log4net.Layout.PatternLayout,log4net">
  17. <param name="ConversionPattern" value="%n
  18. {
  19. &quot;system&quot;: &quot;Meowv.Blog&quot;,
  20. &quot;datetime&quot;: &quot;%d&quot;,
  21. &quot;description&quot;: &quot;%m&quot;,
  22. &quot;level&quot;: &quot;%p&quot;,
  23. &quot;info&quot;: &quot;%exception&quot;
  24. }" />
  25. </layout>
  26. <filter type="log4net.Filter.LevelRangeFilter">
  27. <levelMin value="INFO" />
  28. <levelMax value="INFO" />
  29. </filter>
  30. </appender>
  31. <appender name="error" type="log4net.Appender.RollingFileAppender,log4net">
  32. <param name="File" value="log4net/error/" />
  33. <param name="AppendToFile" value="true" />
  34. <param name="MaxSizeRollBackups" value="-1"/>
  35. <param name="MaximumFileSize" value="5MB"/>
  36. <param name="RollingStyle" value="Composite" />
  37. <param name="DatePattern" value="yyyyMMdd\\HH&quot;.log&quot;" />
  38. <param name="StaticLogFileName" value="false" />
  39. <layout type="log4net.Layout.PatternLayout,log4net">
  40. <param name="ConversionPattern" value="%n
  41. {
  42. &quot;system&quot;: &quot;Meowv.Blog&quot;,
  43. &quot;datetime&quot;: &quot;%d&quot;,
  44. &quot;description&quot;: &quot;%m&quot;,
  45. &quot;level&quot;: &quot;%p&quot;,
  46. &quot;info&quot;: &quot;%exception&quot;
  47. }" />
  48. </layout>
  49. <filter type="log4net.Filter.LevelRangeFilter">
  50. <levelMin value="ERROR" />
  51. <levelMax value="ERROR" />
  52. </filter>
  53. </appender>
  54. <root>
  55. <level value="ALL"></level>
  56. <appender-ref ref="info"/>
  57. <appender-ref ref="error"/>
  58. </root>
  59. </log4net>
  60. </configuration>

此时再去调用 .../HelloWorld/Exception,将会得到日志文件,内容是以JSON格式进行存储的。

关于Filter的更多用法可以参考微软官方文档:https://docs.microsoft.com/zh-cn/aspnet/core/mvc/controllers/filters

到这里,系统的异常处理和日志记录便完成了,你学会了吗?

基于 abp vNext 和 .NET Core 开发博客项目 - 异常处理和日志记录的更多相关文章

  1. 基于 abp vNext 和 .NET Core 开发博客项目 - 博客接口实战篇(一)

    系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...

  2. 基于 abp vNext 和 .NET Core 开发博客项目 - 博客接口实战篇(二)

    系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...

  3. 基于 abp vNext 和 .NET Core 开发博客项目 - 博客接口实战篇(三)

    系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...

  4. 基于 abp vNext 和 .NET Core 开发博客项目 - 博客接口实战篇(四)

    系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...

  5. 基于 abp vNext 和 .NET Core 开发博客项目 - 博客接口实战篇(五)

    系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...

  6. 基于 abp vNext 和 .NET Core 开发博客项目 - Blazor 实战系列(一)

    系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...

  7. 基于 abp vNext 和 .NET Core 开发博客项目 - Blazor 实战系列(二)

    系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...

  8. 基于 abp vNext 和 .NET Core 开发博客项目 - Blazor 实战系列(三)

    系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...

  9. 基于 abp vNext 和 .NET Core 开发博客项目 - Blazor 实战系列(四)

    系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...

随机推荐

  1. SSH 超时设置

    在阿里云买了一台乞丐版服务器,搭了一个博客,安装了java,mysql,redis等服务,把以前写的知乎爬虫部署上去,看看爬取效果.程序运行一段时间后,发现cmder上的日志不打了,我原以为爬虫挂了, ...

  2. 详解Linux 安装 JDK、Tomcat 和 MySQL(图文并茂)

    https://www.jb51.net/article/120984.htm

  3. android 动画学习总结

    本文内容是本人阅读诸多前辈的学习心得后整理的,若有雷同,请见谅 Android 动画 分类:帧动画,补间动画,属性动画  . 1.帧动画 将一张张单独的图片连贯的进行播放,从而在视觉上产生一种动画的效 ...

  4. Greenplum列存压缩表索引机制

    列存压缩表,简称AOCS表 数据生成 create table testao(date text, time text, open float, high float, low float, volu ...

  5. CF #632 (Div. 2) 对应题号CF1333

    1333A Little Artem 在一个\(n\)行\(m\)列的格子上染色,每个格子能染黑白两种 构造一种方案,使得四个方向有至少一个白色格子的黑色格子的数量,比四个方向有至少一个黑色格子的白色 ...

  6. 关于【MySQL 子查询——查询最大值】的补充说明

    昨天在使用子查询查找最高分和最低分时遇上了一点问题,情况是这样的:如果找到的最高分或最低分是唯一值则不会有什么问题,但如果有其它班级学生的成绩恰好与查询的最高分或最低分相同时就会把那个学生的信息也显示 ...

  7. vue添加,删除内容

    vue 提交添加内容,点击删除内容 1 html <input v-model="inputValue" /> <button @click="hand ...

  8. RocketMQ-架构篇

    RocketMQ-架构篇 1.Broker Broker是RocketMQ的服务端组件之一,所有消息存储在Broker上,所有的投递.消费请求也都由Broker进行处理.Broker是有状态的应用,本 ...

  9. dp D. Caesar's Legions

    https://codeforces.com/problemset/problem/118/D 这个题目有点思路,转移方程写错了. 这个题目看到数据范围之后发现很好dp, dp[i][j][k1][k ...

  10. crontab自动启动小任务例子(每一分钟将当前日期打入一个文件)

      crontab -l #查看当前定时任务列表 显示没有,那么我们来安装一下(必须在root用户下) – yum install vixie-cron  – yum install crontabs ...