65.9K
CodeProject 正在变化。 阅读更多。
Home

RESTful Day #6: 使用 Action Filters、Exception Filters 和 NLog 在 Web API 中进行请求日志记录和异常处理/日志记录

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (78投票s)

2015 年 9 月 8 日

CPOL

14分钟阅读

viewsIcon

264999

downloadIcon

10063

本系列文章将解释如何处理请求并记录它们以进行跟踪和调试,如何处理异常并记录它们。我们将采用一种集中式的方式来处理 WebAPI 中的异常,并编写自定义类以映射到我们遇到的异常类型,然后相应地记录。

目录

引言

在过去五篇文章中,我们学习了很多关于 WebAPI 的知识,包括它的用途、实现和安全方面。本系列文章将解释如何处理请求并记录它们以进行跟踪和调试,如何处理异常并记录它们。我们将采用一种集中式的方式来处理 WebAPI 中的异常,并编写自定义类以映射到我们遇到的异常类型,然后相应地记录。我将使用 NLog 来记录请求和异常。我们将利用 Exception Filters 和 Action Filters 的功能来集中处理 WebAPI 中的请求日志记录和异常处理。

路线图

以下是我为逐步学习 WebAPI 设置的路线图:

 

我将特意使用 Visual Studio 2010 和 .NET Framework 4.0,因为在 .NET Framework 4.0 中,有些实现很难找到,但我会通过展示如何做到这一点来使其变得容易。

请求日志记录

由于我们正在编写 Web 服务,因此我们暴露了我们的端点。我们必须知道请求来自哪里以及哪些请求到达我们的服务器。日志记录非常有益,可以在调试、跟踪、监控和分析等方面帮助我们。

我们已经有了一个现有的设计。如果您打开解决方案,您会看到如下所示的结构,或者也可以在现有解决方案中实现此方法。

在 WebAPI 中设置 NLog

NLog 服务于多种目的,但主要用于日志记录。我们将使用 NLog 将日志记录到文件和 Windows 事件日志中。您可以在 http://NLog-project.org/ 上阅读有关 NLog 的更多信息。

可以使用我们在 Day#5 中使用的示例应用程序,也可以使用其他任何应用程序。我正在使用本系列所有部分都遵循的现有示例应用程序。我们的应用程序结构看起来像这样:

步骤 1:下载 NLog 包

右键单击 WebAPI 项目,然后从列表中选择“管理 Nuget 包”。当出现 Nuget 包管理器时,搜索 NLog。您会看到 Nlog,如下图所示,只需将其安装到我们的项目中。



添加此项后,您将在应用程序中找到以下 NLog DLL 引用。

步骤 2:配置 NLog

要将 NLog 与应用程序集成,请在现有的 WebAPI web.config 文件中添加以下设置:

ConfigSection –

Configuration Section – 我已将 NLog 部分添加到配置中,并定义了目标日志文件的路径和格式,还为 Api Services 添加了事件日志源。

如上目标路径所示,我在应用程序的基目录中创建了一个“APILog”文件夹。

现在我们已经在应用程序中配置了 NLog,并且它已准备好开始进行请求日志记录。请注意,在 rules 部分,我们为日志记录到文件和 Windows 事件日志都定义了规则,您可以选择两者,也可以只选择其中一个。让我们开始使用 Action Filters 记录应用程序中的请求 –

NLogger 类

在 API 中添加一个“Helpers”文件夹,以提高应用程序代码的可读性、更好的理解和可维护性。

首先,将我们的主类“NLogger”添加到同一个 Helper 文件夹中,该类将负责所有类型的错误和信息日志记录。在这里,NLogger 类实现了 ITraceWriter 接口,该接口提供了服务请求的“Trace”方法。

#region Using namespaces.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http.Tracing;
using NLog;
using System.Net.Http;
using System.Text;
using WebApi.ErrorHelper;
#endregion

namespace WebApi.Helpers
{
    /// <summary>
    /// Public class to log Error/info messages to the access log file
    /// </summary>
    public sealed class NLogger : ITraceWriter
    {
        #region Private member variables.
        private static readonly Logger ClassLogger = LogManager.GetCurrentClassLogger();

        private static readonly Lazy<Dictionary<TraceLevel, Action<string>>> LoggingMap = new Lazy<Dictionary<TraceLevel, Action<string>>>(() => new Dictionary<TraceLevel, Action<string>> { { TraceLevel.Info, ClassLogger.Info }, { TraceLevel.Debug, ClassLogger.Debug }, { TraceLevel.Error, ClassLogger.Error }, { TraceLevel.Fatal, ClassLogger.Fatal }, { TraceLevel.Warn, ClassLogger.Warn } });
        #endregion

        #region Private properties.
        /// <summary>
        /// Get property for Logger
        /// </summary>
        private Dictionary<TraceLevel, Action<string>> Logger
        {
            get { return LoggingMap.Value; }
        }
        #endregion

        #region Public member methods.
        /// <summary>
        /// Implementation of TraceWriter to trace the logs.
        /// </summary>
        /// <param name="request"></param>
        /// <param name="category"></param>
        /// <param name="level"></param>
        /// <param name="traceAction"></param>
        public void Trace(HttpRequestMessage request, string category, TraceLevel level, Action<TraceRecord> traceAction)
        {
            if (level != TraceLevel.Off)
            {
                if (traceAction != null && traceAction.Target != null)
                {
                    category = category + Environment.NewLine + "Action Parameters : " + traceAction.Target.ToJSON();
                }
                var record = new TraceRecord(request, category, level);
                if (traceAction != null) traceAction(record);
                Log(record);
            }
        }
        #endregion

        #region Private member methods.
        /// <summary>
        /// Logs info/Error to Log file
        /// </summary>
        /// <param name="record"></param>
        private void Log(TraceRecord record)
        {
            var message = new StringBuilder();

            if (!string.IsNullOrWhiteSpace(record.Message))
                message.Append("").Append(record.Message + Environment.NewLine);

            if (record.Request != null)
            {
                if (record.Request.Method != null)
                    message.Append("Method: " + record.Request.Method + Environment.NewLine);

                if (record.Request.RequestUri != null)
                    message.Append("").Append("URL: " + record.Request.RequestUri + Environment.NewLine);

                if (record.Request.Headers != null && record.Request.Headers.Contains("Token") && record.Request.Headers.GetValues("Token") != null && record.Request.Headers.GetValues("Token").FirstOrDefault() != null)
                    message.Append("").Append("Token: " + record.Request.Headers.GetValues("Token").FirstOrDefault() + Environment.NewLine);
            }

            if (!string.IsNullOrWhiteSpace(record.Category))
                message.Append("").Append(record.Category);

            if (!string.IsNullOrWhiteSpace(record.Operator))
                message.Append(" ").Append(record.Operator).Append(" ").Append(record.Operation);

            
            Logger[record.Level](Convert.ToString(message) + Environment.NewLine);
        }
        #endregion
    }
}

添加 Action Filter

Action filter 将负责处理我们 API 的所有传入请求,并使用 NLogger 类记录它们。我们有一个“OnActionExecuting”方法,如果您将控制器或全局应用程序标记为使用该特定过滤器,则会自动调用该方法。因此,每次命中任何控制器中的任何操作时,“OnActionExecuting”方法都会执行以记录请求。

步骤 1:添加 LoggingFilterAttribute 类

在“ActionFilters”文件夹中创建一个类 LoggingFilterAttribute,并添加以下代码。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http.Filters;
using System.Web.Http.Controllers;
using System.Web.Http.Tracing;
using System.Web.Http;
using WebApi.Helpers;


namespace WebApi.ActionFilters
{
    public class LoggingFilterAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(HttpActionContext filterContext)
        {
            GlobalConfiguration.Configuration.Services.Replace(typeof(ITraceWriter), new NLogger());
            var trace = GlobalConfiguration.Configuration.Services.GetTraceWriter();
            trace.Info(filterContext.Request, "Controller : " + filterContext.ControllerContext.ControllerDescriptor.ControllerType.FullName + Environment.NewLine + "Action : " + filterContext.ActionDescriptor.ActionName, "JSON", filterContext.ActionArguments);
        }
    }
}

LoggingFilterAttribute 类继承自 ActionFilterAttribute,该类位于 System.Web.Http.Filters 下,并重写了 OnActionExecuting 方法。

我已将默认的 "ITraceWriter" 服务替换为我们控制器服务容器中的 NLogger 类实例。现在 GetTraceWriter() 方法将返回我们的实例(NLogger 类实例),而 Info() 将调用我们 NLogger 类的 trace() 方法。

请注意以下代码。

GlobalConfiguration.Configuration.Services.Replace(typeof(ITraceWriter), new NLogger());

用于解决 ITaceWriterNLogger 类之间的依赖关系。然后,我们使用一个名为 trace 的变量来获取实例,并使用 trace.Info() 来记录请求以及我们想要随该请求一起添加的任何文本。

步骤 2:注册 Action Filter (LoggingFilterAttribute)

为了将创建的 Action Filter 注册到应用程序的过滤器中,只需在 WebApiConfig 类中的 config.Filters 中添加您 Action Filter 的新实例。

using System.Web.Http;
using WebApi.ActionFilters;

namespace WebApi.App_Start
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            config.Filters.Add(new LoggingFilterAttribute());
        }
    }
}

现在,此 Action Filter 适用于我们项目中的所有控制器和操作。您可能不相信,但请求日志记录已完成。现在是时候运行应用程序并验证我们的工作了。

运行应用程序

让我们运行应用程序并尝试发出调用,使用基于令牌的授权,我们已经在 day#5 中涵盖了授权。您首先需要使用登录服务对您的请求进行身份验证,然后该服务将返回一个令牌以供其他服务调用。使用该令牌调用其他服务。有关更多详细信息,您可以阅读本系列的第 5 天。

只需运行应用程序,我们就会得到

我们已经添加了测试客户端,但对于新读者来说,只需右键单击 WebAPI 项目,然后在在线包的搜索框中键入 WebAPITestClient,然后转到“管理 Nuget 包”。

您将获得“A simple Test Client for ASP.NET Web API”,只需添加它。您将在 Areas-> HelpPage 中获得一个帮助控制器,如下所示。

我已经在之前的文章中提供了数据库脚本和数据,您可以使用相同的。

在应用程序 URL 中追加 "/help",您将获得测试客户端,

GET

POST

PUT

删除

您可以通过单击每个服务来测试它。单击服务链接后,您将被重定向到该特定服务的测试页面。在该页面上,右下角有一个“Test API”按钮,只需按下该按钮即可测试您的服务。

获取所有产品的服务

在下面的例子中,我已经生成了令牌,现在我正在使用它来调用以从数据库中的产品表中获取所有产品。

在这里,我调用了 allproducts API,为参数 Id 和“Token”标头及其当前值添加值,然后单击以获取结果。

现在让我们看看我们的应用程序中的 APILog 文件夹发生了什么。您会在此处找到已创建的 API 日志,其名称与我们在 web.config 文件中的 NLog 配置中设置的名称相同。日志文件包含所有提供的详细信息,例如时间戳、方法类型、URL、标头信息(Token)、控制器名称、操作和操作参数。您还可以根据您的应用程序需要为该日志添加更多详细信息。

日志记录完成!

异常日志记录

我们的日志设置已完成,现在我们将专注于集中处理异常日志记录,这样就不会有任何异常逃脱日志记录。记录异常非常重要,它可以跟踪所有异常。无论业务、应用程序还是系统异常,所有这些都必须被记录。

实现异常日志记录

步骤 1:Exception Filter Attribute

现在我们将为我们的应用程序添加一个 Action Filter 来记录异常。为此,请在“ActionFilter”文件夹中创建一个类 GlobalExceptionAttribute,并添加下面的代码。该类继承自 ExceptionFilterAttribute,该类位于 System.Web.Http.Filters 下。

我重写了 OnException() 方法,并将默认的 "ITraceWriter" 服务替换为我们控制器服务容器中的 NLogger 类实例,这与我们在上面章节的 Action logging 中所做的相同。现在 GetTraceWriter() 方法将返回我们的实例(NLogger 类实例),而 Info() 将调用 NLogger 类的 trace() 方法。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http.Filters;
using System.Web.Http;
using System.Web.Http.Tracing;
using WebApi.Helpers;
using System.ComponentModel.DataAnnotations;
using System.Net.Http;
using System.Net;

namespace WebApi.ActionFilters
{
    /// <summary>
    /// Action filter to handle for Global application errors.
    /// </summary>
    public class GlobalExceptionAttribute : ExceptionFilterAttribute
    {
        public override void OnException(HttpActionExecutedContext context)
        {
            GlobalConfiguration.Configuration.Services.Replace(typeof(ITraceWriter), new NLogger());
            var trace = GlobalConfiguration.Configuration.Services.GetTraceWriter();
            trace.Error(context.Request, "Controller : " + context.ActionContext.ControllerContext.ControllerDescriptor.ControllerType.FullName + Environment.NewLine + "Action : " + context.ActionContext.ActionDescriptor.ActionName, context.Exception);

            var exceptionType = context.Exception.GetType();

            if (exceptionType == typeof(ValidationException))
            {
                var resp = new HttpResponseMessage(HttpStatusCode.BadRequest) { Content = new StringContent(context.Exception.Message), ReasonPhrase = "ValidationException", };
                throw new HttpResponseException(resp);

            }
            else if (exceptionType == typeof(UnauthorizedAccessException))
            {
                throw new HttpResponseException(context.Request.CreateResponse(HttpStatusCode.Unauthorized));
            }
            else
            {
                throw new HttpResponseException(context.Request.CreateResponse(HttpStatusCode.InternalServerError));
            }
        }
    }
}

步骤 2:修改 NLogger 类

我们的 NLogger 类能够记录所有信息和事件。我对私有方法 Log() 进行了一些更改以处理异常。

#region Private member methods.
/// <summary>
/// Logs info/Error to Log file
/// </summary>
/// <param name="record"></param>
private void Log(TraceRecord record)
{
var message = new StringBuilder();

if (!string.IsNullOrWhiteSpace(record.Message))
                message.Append("").Append(record.Message + Environment.NewLine);

      if (record.Request != null)
{
      	if (record.Request.Method != null)
           	message.Append("Method: " + record.Request.Method + Environment.NewLine);

                if (record.Request.RequestUri != null)
                    message.Append("").Append("URL: " + record.Request.RequestUri + Environment.NewLine);

                if (record.Request.Headers != null && record.Request.Headers.Contains("Token") && record.Request.Headers.GetValues("Token") != null && record.Request.Headers.GetValues("Token").FirstOrDefault() != null)
                    message.Append("").Append("Token: " + record.Request.Headers.GetValues("Token").FirstOrDefault() + Environment.NewLine);
            }

            if (!string.IsNullOrWhiteSpace(record.Category))
                message.Append("").Append(record.Category);

            if (!string.IsNullOrWhiteSpace(record.Operator))
                message.Append(" ").Append(record.Operator).Append(" ").Append(record.Operation);

            if (record.Exception != null && !string.IsNullOrWhiteSpace(record.Exception.GetBaseException().Message))
            {
                var exceptionType = record.Exception.GetType();
                message.Append(Environment.NewLine);
                message.Append("").Append("Error: " + record.Exception.GetBaseException().Message + Environment.NewLine);
            }

            Logger[record.Level](Convert.ToString(message) + Environment.NewLine);
        }

步骤 3:修改 Controller 以处理异常

我们的应用程序现在已准备好运行,但代码中没有异常。因此,我在 ProductController 中添加了一个 throw exception 代码,仅在 Get(int id) 方法中,以便它可以抛出异常来测试我们的异常日志记录机制。如果数据库中不存在具有提供 ID 的产品,它将抛出异常。

  // GET api/product/5
 [GET("productid/{id?}")]
 [GET("particularproduct/{id?}")]
 [GET("myproduct/{id:range(1, 3)}")]
 public HttpResponseMessage Get(int id)
 {
var product = _productServices.GetProductById(id);
      if (product != null)
      	return Request.CreateResponse(HttpStatusCode.OK, product);

 throw new Exception("No product found for this id");
      //return Request.CreateErrorResponse(HttpStatusCode.NotFound,   "No product found for this id");
 }

步骤 4:运行应用程序

运行应用程序并单击 Product/all API。

将参数 id 值设置为 1,并将 Token 标头设置为其当前值,然后单击发送按钮以获取结果。

现在我们可以看到 Status 是 200/OK,并且我们在响应正文中也得到了具有提供 ID 的产品。现在让我们看看 API 日志。

日志已捕获 Product API 的调用,现在提供一个数据库中不存在的新产品 ID 作为参数。我使用 12345 作为产品 ID,结果是。

现在我们可以在响应状态中看到 500/Internal Server Error,让我们检查一下 API 日志。

好吧,现在日志已经捕获了服务器上同一调用的事件和错误,您可以看到调用日志详细信息以及日志中提供的错误消息。

自定义异常日志记录

在上面的部分中,我们实现了异常日志记录,但存在默认的系统响应和状态(即 500/Internal Server Error)。拥有自己的自定义响应和异常以供 API 使用总是好的。这将使客户端更容易使用和理解 API 响应。

步骤 1:添加自定义异常类

向应用程序添加“Error Helper”文件夹,以单独维护我们的自定义异常类,并将“IApiExceptions”接口添加到新创建的“ErrorHelper”文件夹中。

将以下代码添加到 IApiExceptions 接口中,它将作为所有异常类的模板。我添加了四个常用属性供我们的自定义类使用,以维护 Error Code、ErrorDescription、HttpStatus(包含 HTTP 定义的状态码值)和 ReasonPhrase。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;

namespace WebApi.ErrorHelper
{
    /// <summary>
    /// IApiExceptions Interface
    /// </summary>
    public interface IApiExceptions
    {
        /// <summary>
        /// ErrorCode
        /// </summary>
        int ErrorCode { get; set; }
        /// <summary>
        /// ErrorDescription
        /// </summary>
        string ErrorDescription { get; set; }
        /// <summary>
        /// HttpStatus
        /// </summary>
        HttpStatusCode HttpStatus { get; set; }
        /// <summary>
        /// ReasonPhrase
        /// </summary>
        string ReasonPhrase { get; set; }
    }
}

在这里,我将异常分为三类:

  1. API 异常 – 用于 API 级别的异常。
  2. 业务异常 – 用于业务逻辑级别的异常。
  3. 数据异常 – 数据相关的异常。

为了实现这一点,请在同一文件夹中创建三个新类:ApiException.cs、ApiDataException.cs 和 ApiBusinessException 类,这些类实现了 IApiExceptions 接口,并具有以下代码:

#region Using namespaces.
using System;
using System.Net;
using System.Runtime.Serialization;
#endregion


namespace WebApi.ErrorHelper
{
    /// <summary>
    /// Api Exception
    /// </summary>
    [Serializable]
    [DataContract]
    public class ApiException : Exception, IApiExceptions
    {
        #region Public Serializable properties.
        [DataMember]
        public int ErrorCode { get; set; }
        [DataMember]
        public string ErrorDescription { get; set; }
        [DataMember]
        public HttpStatusCode HttpStatus { get; set; }
        
        string reasonPhrase = "ApiException";

        [DataMember]
        public string ReasonPhrase
        {
            get { return this.reasonPhrase; }

            set { this.reasonPhrase = value; }
        }
        #endregion
    }
}

我在这些类中为 ReasonPhrase 属性初始化了不同的默认值,以区分实现,您可以根据应用程序的需求实现您的自定义类。

应用于类的指令是 Serializable 和 DataContract,以确保定义的类或实现的数据合同是可序列化的,并且可以被序列化器序列化。

注意:如果遇到任何程序集问题,请添加“System.Runtime.Serialization.dll”DLL 的引用。

同样,将“ApiBusinessException”和“ApiDataException”类添加到同一个文件夹中,并包含以下代码 –

#region Using namespaces.
using System;
using System.Net;
using System.Runtime.Serialization; 
#endregion

namespace WebApi.ErrorHelper
{
    /// <summary>
    /// Api Business Exception
    /// </summary>
    [Serializable]
    [DataContract]
    public class ApiBusinessException : Exception, IApiExceptions
    {
        #region Public Serializable properties.
        [DataMember]
        public int ErrorCode { get; set; }
        [DataMember]
        public string ErrorDescription { get; set; }
        [DataMember]
        public HttpStatusCode HttpStatus { get; set; }

        string reasonPhrase = "ApiBusinessException";

        [DataMember]
        public string ReasonPhrase
        {
            get { return this.reasonPhrase; }

            set { this.reasonPhrase = value; }
        }
        #endregion

        #region Public Constructor.
        /// <summary>
        /// Public constructor for Api Business Exception
        /// </summary>
        /// <param name="errorCode"></param>
        /// <param name="errorDescription"></param>
        /// <param name="httpStatus"></param>
        public ApiBusinessException(int errorCode, string errorDescription, HttpStatusCode httpStatus)
        {
            ErrorCode = errorCode;
            ErrorDescription = errorDescription;
            HttpStatus = httpStatus;
        } 
        #endregion

    }
}

#region Using namespaces.
using System;
using System.Net;
using System.Runtime.Serialization;
#endregion

namespace WebApi.ErrorHelper
{
    /// <summary>
    /// Api Data Exception
    /// </summary>
    [Serializable]
    [DataContract]
    public class ApiDataException : Exception, IApiExceptions
    {
        #region Public Serializable properties.
        [DataMember]
        public int ErrorCode { get; set; }
        [DataMember]
        public string ErrorDescription { get; set; }
        [DataMember]
        public HttpStatusCode HttpStatus { get; set; }

        string reasonPhrase = "ApiDataException";

        [DataMember]
        public string ReasonPhrase
        {
            get { return this.reasonPhrase; }

            set { this.reasonPhrase = value; }
        }

        #endregion

        #region Public Constructor.
        /// <summary>
        /// Public constructor for Api Data Exception
        /// </summary>
        /// <param name="errorCode"></param>
        /// <param name="errorDescription"></param>
        /// <param name="httpStatus"></param>
        public ApiDataException(int errorCode, string errorDescription, HttpStatusCode httpStatus)
        {
            ErrorCode = errorCode;
            ErrorDescription = errorDescription;
            HttpStatus = httpStatus;
        }
        #endregion
    }
}

JSON 序列化器

有一些对象需要序列化为 JSON,以便进行日志记录和在模块之间传输。为此,我向 Object 类添加了一些扩展方法。

为此,请将“System.Web.Extensions.dll”引用添加到项目中,并将“JSONHelper”类添加到 Helpers 文件夹中,并包含以下代码。

#region Using namespaces.
using System.Web.Script.Serialization;
using System.Data;
using System.Collections.Generic;
using System;

#endregion

namespace WebApi.Helpers
{
    public static class JSONHelper
    {
         #region Public extension methods.
        /// <summary>
        /// Extened method of object class, Converts an object to a json string.
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public static string ToJSON(this object obj)
        {
            var serializer = new JavaScriptSerializer();
            try
            {
                return serializer.Serialize(obj);
            }
            catch(Exception ex)
            {
                return "";
            }
        }
         #endregion
    }
}

在上面的代码中,“ToJSON()”方法是基类 Object 的扩展,它将提供的对象序列化为 JSON 字符串。该方法使用“JavaScriptSerializer”类,该类存在于“System.Web.Script.Serialization”中。

修改 NLogger 类

对于异常处理,我修改了 NLogger 的 Log() 方法,该方法现在将处理不同的 API 异常。

/// <summary>
/// Logs info/Error to Log file
/// </summary>
/// <param name="record"></param>
private void Log(TraceRecord record)
{
var message = new StringBuilder();

      if (!string.IsNullOrWhiteSpace(record.Message))
                message.Append("").Append(record.Message + Environment.NewLine);

            if (record.Request != null)
            {
                if (record.Request.Method != null)
                    message.Append("Method: " + record.Request.Method + Environment.NewLine);

                if (record.Request.RequestUri != null)
                    message.Append("").Append("URL: " + record.Request.RequestUri + Environment.NewLine);

                if (record.Request.Headers != null && record.Request.Headers.Contains("Token") && record.Request.Headers.GetValues("Token") != null && record.Request.Headers.GetValues("Token").FirstOrDefault() != null)
                    message.Append("").Append("Token: " + record.Request.Headers.GetValues("Token").FirstOrDefault() + Environment.NewLine);
            }

            if (!string.IsNullOrWhiteSpace(record.Category))
                message.Append("").Append(record.Category);

            if (!string.IsNullOrWhiteSpace(record.Operator))
                message.Append(" ").Append(record.Operator).Append(" ").Append(record.Operation);

            if (record.Exception != null && !string.IsNullOrWhiteSpace(record.Exception.GetBaseException().Message))
            {
                var exceptionType = record.Exception.GetType();
                message.Append(Environment.NewLine);
                if (exceptionType == typeof(ApiException))
                {
                    var exception = record.Exception as ApiException;
                    if (exception != null)
                    {
                        message.Append("").Append("Error: " + exception.ErrorDescription + Environment.NewLine);
                        message.Append("").Append("Error Code: " + exception.ErrorCode + Environment.NewLine);
                    }
                }
                else if (exceptionType == typeof(ApiBusinessException))
                {
                    var exception = record.Exception as ApiBusinessException;
                    if (exception != null)
                    {
                        message.Append("").Append("Error: " + exception.ErrorDescription + Environment.NewLine);
                        message.Append("").Append("Error Code: " + exception.ErrorCode + Environment.NewLine);
                    }
                }
                else if (exceptionType == typeof(ApiDataException))
                {
                    var exception = record.Exception as ApiDataException;
                    if (exception != null)
                    {
                        message.Append("").Append("Error: " + exception.ErrorDescription + Environment.NewLine);
                        message.Append("").Append("Error Code: " + exception.ErrorCode + Environment.NewLine);
                    }
                }
                else
                    message.Append("").Append("Error: " + record.Exception.GetBaseException().Message + Environment.NewLine);
            }

            Logger[record.Level](Convert.ToString(message) + Environment.NewLine);
        }

上面的代码会检查 TraceRecord 的异常对象,并根据异常类型更新日志记录器。

修改 GlobalExceptionAttribute

正如我们创建了 GlobalExceptionAttribute 来处理所有异常并在发生任何异常时创建响应一样。现在我向其中添加了一些新代码,以使 GlobalExceptionAttribute 类能够处理自定义异常。我仅在此处为您提供修改后的方法的参考。

public override void OnException(HttpActionExecutedContext context)
{
         GlobalConfiguration.Configuration.Services.Replace(typeof(ITraceWriter), new NLogger());
            var trace = GlobalConfiguration.Configuration.Services.GetTraceWriter();
            trace.Error(context.Request, "Controller : " + context.ActionContext.ControllerContext.ControllerDescriptor.ControllerType.FullName + Environment.NewLine + "Action : " + context.ActionContext.ActionDescriptor.ActionName, context.Exception);

            var exceptionType = context.Exception.GetType();

            if (exceptionType == typeof(ValidationException))
            {
                var resp = new HttpResponseMessage(HttpStatusCode.BadRequest) { Content = new StringContent(context.Exception.Message), ReasonPhrase = "ValidationException", };
                throw new HttpResponseException(resp);

            }
            else if (exceptionType == typeof(UnauthorizedAccessException))
            {
                throw new HttpResponseException(context.Request.CreateResponse(HttpStatusCode.Unauthorized, new ServiceStatus() { StatusCode = (int)HttpStatusCode.Unauthorized, StatusMessage = "UnAuthorized", ReasonPhrase = "UnAuthorized Access" }));
            }
            else if (exceptionType == typeof(ApiException))
            {
                var webapiException = context.Exception as ApiException;
                if (webapiException != null)
                    throw new HttpResponseException(context.Request.CreateResponse(webapiException.HttpStatus, new ServiceStatus() { StatusCode = webapiException.ErrorCode, StatusMessage = webapiException.ErrorDescription, ReasonPhrase = webapiException.ReasonPhrase }));
            }
            else if (exceptionType == typeof(ApiBusinessException))
            {
                var businessException = context.Exception as ApiBusinessException;
                if (businessException != null)
                    throw new HttpResponseException(context.Request.CreateResponse(businessException.HttpStatus, new ServiceStatus() { StatusCode = businessException.ErrorCode, StatusMessage = businessException.ErrorDescription, ReasonPhrase = businessException.ReasonPhrase }));
            }
            else if (exceptionType == typeof(ApiDataException))
            {
                var dataException = context.Exception as ApiDataException;
                if (dataException != null)
                    throw new HttpResponseException(context.Request.CreateResponse(dataException.HttpStatus, new ServiceStatus() { StatusCode = dataException.ErrorCode, StatusMessage = dataException.ErrorDescription, ReasonPhrase = dataException.ReasonPhrase }));
            }
            else
            {
                throw new HttpResponseException(context.Request.CreateResponse(HttpStatusCode.InternalServerError));
            }
        }

在上面的代码中,我修改了重写的 OnExeption() 方法,并创建了新的 Http 响应异常,该异常基于不同的异常类型。

修改 Product Controller

现在修改 Product controller 以抛出我们的自定义异常。请查看 Get 方法,我已将其修改为在未找到数据时抛出 APIDataException,在发生任何其他错误时抛出 APIException。

// GET api/product/5
[GET("productid/{id?}")]
[GET("particularproduct/{id?}")]
[GET("myproduct/{id:range(1, 3)}")]
public HttpResponseMessage Get(int id)
{
if (id != null)
      {
      	var product = _productServices.GetProductById(id);
            if (product != null)
            	return Request.CreateResponse(HttpStatusCode.OK, product);

throw new ApiDataException(1001, "No product found for this id.", HttpStatusCode.NotFound);
      }
      throw new ApiException() { ErrorCode = (int)HttpStatusCode.BadRequest, ErrorDescription = "Bad Request..." };
}

运行应用程序

运行应用程序并单击 Product/all API。

将参数 id 值设置为 1,并将 Token 标头设置为其当前值,然后单击发送按钮以获取结果。

现在我们可以看到 Status 是 200/OK,并且我们在响应正文中也得到了具有提供 ID 的产品。现在让我们看看 API 日志。

日志已捕获 Product API 的调用,现在提供一个数据库中不存在的新产品 ID 作为参数。我使用 12345 作为产品 ID,结果是。

现在我们可以看到,有一个自定义错误状态码“1001”和消息“No product found for this id.”。通用的状态码“500/Internal Server Error”现在已被我们提供的代码“404/Not Found”替换,这对客户端或消费者来说更有意义。

让我们看看 APILog。

好吧,现在日志已经捕获了服务器上同一调用的事件和错误,您可以看到调用日志详细信息以及日志中提供的错误消息以及我们的自定义错误代码。我只捕获了错误描述和错误代码,但您可以根据应用程序的需求在日志中添加更多详细信息。

更新 Controller 以实现新的异常处理

以下是实现自定义异常处理和日志记录的 Controller 代码:

Product Controller

using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using AttributeRouting;
using AttributeRouting.Web.Http;
using BusinessEntities;
using BusinessServices;
using WebApi.ActionFilters;
using WebApi.Filters;
using System;
using WebApi.ErrorHelper;

namespace WebApi.Controllers
{
    [AuthorizationRequired]
    [RoutePrefix("v1/Products/Product")]
    public class ProductController : ApiController
    {
        #region Private variable.

        private readonly IProductServices _productServices;

        #endregion

        #region Public Constructor

        /// <summary>
        /// Public constructor to initialize product service instance
        /// </summary>
        public ProductController(IProductServices productServices)
        {
            _productServices = productServices;
        }

        #endregion

        // GET api/product
        [GET("allproducts")]
        [GET("all")]
        public HttpResponseMessage Get()
        {
            var products = _productServices.GetAllProducts();
            var productEntities = products as List<ProductEntity> ?? products.ToList();
            if (productEntities.Any())
                return Request.CreateResponse(HttpStatusCode.OK, productEntities);
            throw new ApiDataException(1000, "Products not found", HttpStatusCode.NotFound);
        }

        // GET api/product/5
        [GET("productid/{id?}")]
        [GET("particularproduct/{id?}")]
        [GET("myproduct/{id:range(1, 3)}")]
        public HttpResponseMessage Get(int id)
        {
            if (id != null)
            {
                var product = _productServices.GetProductById(id);
                if (product != null)
                    return Request.CreateResponse(HttpStatusCode.OK, product);

                throw new ApiDataException(1001, "No product found for this id.", HttpStatusCode.NotFound);
            }
            throw new ApiException() { ErrorCode = (int)HttpStatusCode.BadRequest, ErrorDescription = "Bad Request..." };
        }

        // POST api/product
        [POST("Create")]
        [POST("Register")]
        public int Post([FromBody] ProductEntity productEntity)
        {
            return _productServices.CreateProduct(productEntity);
        }

        // PUT api/product/5
        [PUT("Update/productid/{id}")]
        [PUT("Modify/productid/{id}")]
        public bool Put(int id, [FromBody] ProductEntity productEntity)
        {
            if (id > 0)
            {
                return _productServices.UpdateProduct(id, productEntity);
            }
            return false;
        }

        // DELETE api/product/5
        [DELETE("remove/productid/{id}")]
        [DELETE("clear/productid/{id}")]
        [PUT("delete/productid/{id}")]
        public bool Delete(int id)
        {
            if (id != null && id > 0)
            {
                var isSuccess = _productServices.DeleteProduct(id);
                if (isSuccess)
                {
                    return isSuccess;
                }
                throw new ApiDataException(1002, "Product is already deleted or not exist in system.", HttpStatusCode.NoContent );
            }
            throw new ApiException() {ErrorCode = (int) HttpStatusCode.BadRequest, ErrorDescription = "Bad Request..."};
        }
    }
}

现在您可以看到,我们的应用程序非常强大且可扩展,没有任何异常或事务可以逃脱日志记录。一旦设置完成,您就不必担心每次编写日志记录或请求和异常的代码,而是可以放松并专注于业务逻辑。

结论

在本文中,我们学习了如何在 WebPI 中执行请求日志记录和异常日志记录。执行这些操作可能有多种方法,但我试图以尽可能简单的方式呈现它们。我的方法是让我们进入企业级开发的新阶段,开发人员不必总是担心异常处理和日志记录。我们的解决方案提供了一种将操作集中到一个地方的通用方法;所有请求和异常都会自动得到处理。在我的新文章中,我将通过解释 WebAPI 中的单元测试和 WebAPI 中的 OData 来尝试增强应用程序。您可以从 GitHub 下载本文的完整源代码(包含包)。祝您编码愉快:)

其他系列

我的其他系列文章

MVChttps://codeproject.org.cn/Articles/620195/Learning-MVC-Part-Introduction-to-MVC-Architectu

OOP: https://codeproject.org.cn/Articles/771455/Diving-in-OOP-Day-Polymorphism-and-Inheritance-Ear

© . All rights reserved.