为ABP框架添加基础集成服务

2022-04-17 04:38:15
目录
定义一个特性标记全局统一消息格式Http状态码常用的请求结果响应模型全局异常拦截器先说明一下ApiResponseModel是抽象类跨域请求配置API服务统一API模型验证消息创建前创建方式创建后补充:为什么需要统一格式

定义一个特性标记

这个标记用于标记一个枚举代表的信息。

在 AbpBase.Domain.Shared 项目,创建 Attributes目录,然后创建一个 SchemeNameAttribute 类,其内容如下:

    /// <summary>    /// 标记枚举代表的信息    /// </summary>    [AttributeUsage(AttributeTargets.Field)]    public class SchemeNameAttribute : Attribute    {        public string Message { get; set; }        public SchemeNameAttribute(string message)        {            Message = message;        }    }

全局统一消息格式

为了使得 Web 应用统一响应格式以及方便编写 API 时有一个统一的标准,我们需要定义一个合适的模板。

在 AbpBase.Domain.Shared 创建一个Apis 目录。

Http 状态码

为了适配各种 HTTP 请求的响应状态,我们定义一个识别状态码的枚举。

在 Apis 目录,创建一个 HttpStateCode.cs 文件,其内容如下:

namespace AbpBase.Domain.Shared.Apis{    /// <summary>    /// 标准 HTTP 状态码    /// <para>文档地址<inheritdoc cref="https://www.runoob.com/http/http-status-codes.html"/></para>    /// </summary>    public enum HttpStateCode    {        Status412PreconditionFailed = 412,        Status413PayloadTooLarge = 413,        Status413RequestEntityTooLarge = 413,        Status414RequestUriTooLong = 414,        Status414UriTooLong = 414,        Status415UnsupportedMediaType = 415,        Status416RangeNotSatisfiable = 416,        Status416RequestedRangeNotSatisfiable = 416,        Status417ExpectationFailed = 417,        Status418ImATeapot = 418,        Status419AuthenticationTimeout = 419,        Status421MisdirectedRequest = 421,        Status422UnprocessableEntity = 422,        Status423Locked = 423,        Status424FailedDependency = 424,        Status426UpgradeRequired = 426,        Status428PreconditionRequired = 428,        Status429TooManyRequests = 429,        Status431RequestHeaderFieldsTooLarge = 431,        Status451UnavailableForLegalReasons = 451,        Status500InternalServerError = 500,        Status501NotImplemented = 501,        Status502BadGateway = 502,        Status503ServiceUnavailable = 503,        Status504GatewayTimeout = 504,        Status505HttpVersionNotsupported = 505,        Status506VariantAlsoNegotiates = 506,        Status507InsufficientStorage = 507,        Status508LoopDetected = 508,        Status411LengthRequired = 411,        Status510NotExtended = 510,        Status410Gone = 410,        Status408RequestTimeout = 408,        Status101SwitchingProtocols = 101,        Status102Processing = 102,        Status200OK = 200,        Status201Created = 201,        Status202Accepted = 202,        Status203NonAuthoritative = 203,        Status204NoContent = 204,        Status205ResetContent = 205,        Status206PartialContent = 206,        Status207MultiStatus = 207,        Status208AlreadyReported = 208,        Status226IMUsed = 226,        Status300MultipleChoices = 300,        Status301MovedPermanently = 301,        Status302Found = 302,        Status303SeeOther = 303,        Status304NotModified = 304,        Status305UseProxy = 305,        Status306SwitchProxy = 306,        Status307TemporaryRedirect = 307,        Status308PermanentRedirect = 308,        Status400BadRequest = 400,        Status401Unauthorized = 401,        Status402PaymentRequired = 402,        Status403Forbidden = 403,        Status404NotFound = 404,        Status405MethodNotAllowed = 405,        Status406NotAcceptable = 406,        Status407ProxyAuthenticationRequired = 407,        Status409Conflict = 409,        Status511NetworkAuthenticationRequired = 511    }}

常用的请求结果

在相同目录,创建一个 CommonResponseType 枚举,其内容如下:

    /// <summary>    /// 常用的 API 响应信息    /// </summary>    public enum CommonResponseType    {        [SchemeName("")] Default = 0,        [SchemeName("请求成功")] RequstSuccess = 1,        [SchemeName("请求失败")] RequstFail = 2,        [SchemeName("创建资源成功")] CreateSuccess = 4,        [SchemeName("创建资源失败")] CreateFail = 8,        [SchemeName("更新资源成功")] UpdateSuccess = 16,        [SchemeName("更新资源失败")] UpdateFail = 32,        [SchemeName("删除资源成功")] DeleteSuccess = 64,        [SchemeName("删除资源失败")] DeleteFail = 128,        [SchemeName("请求的数据未能通过验证")] BadRequest = 256,        [SchemeName("服务器出现严重错误")] Status500InternalServerError = 512    }

响应模型

在 Apis 目录,创建一个 ApiResponseModel`.cs 泛型类文件,其内容如下:

namespace AbpBase.Domain.Shared.Apis{    /// <summary>    /// API 响应格式    /// <para>避免滥用,此类不能实例化,只能通过预定义的静态方法生成</para>    /// </summary>    /// <typeparam name="TData"></typeparam>    public abstract class ApiResponseModel<TData>    {        public HttpStateCode StatuCode { get; set; }        public string Message { get; set; }        public TData Data { get; set; }        /// <summary>        /// 私有类        /// </summary>        /// <typeparam name="TResult"></typeparam>        private class PrivateApiResponseModel<TResult> : ApiResponseModel<TResult> { }    }}

StatuCode:用于说明此次响应的状态;

Message:响应的信息;

Data:响应的数据;

可能你会觉得这样很奇怪,先不要问,也不要猜,照着做,后面我会告诉你为什么这样写。

然后再创建一个类:

using AbpBase.Domain.Shared.Helpers;using System;namespace AbpBase.Domain.Shared.Apis{    /// <summary>    /// Web 响应格式    /// <para>避免滥用,此类不能实例化,只能通过预定义的静态方法生成</para>    /// </summary>    public abstract class ApiResponseModel : ApiResponseModel<dynamic>    {        /// <summary>        /// 根据枚举创建响应格式        /// </summary>        /// <typeparam name="TEnum"></typeparam>        /// <param name="code"></param>        /// <param name="enumType"></param>        /// <returns></returns>        public static ApiResponseModel Create<TEnum>(HttpStateCode code, TEnum enumType) where TEnum : Enum        {            return new PrivateApiResponseModel            {                StatuCode = code,                Message = SchemeHelper.Get(enumType),            };        }        /// <summary>        /// 创建标准的响应        /// </summary>        /// <typeparam name="TEnum"></typeparam>        /// <typeparam name="TData"></typeparam>        /// <param name="code"></param>        /// <param name="enumType"></param>        /// <param name="Data"></param>        /// <returns></returns>        public static ApiResponseModel Create<TEnum>(HttpStateCode code, TEnum enumType, dynamic Data)        {            return new PrivateApiResponseModel            {                StatuCode = code,                Message = SchemeHelper.Get(enumType),                Data = Data            };        }        /// <summary>        /// 请求成功        /// </summary>        /// <param name="code"></param>        /// <param name="Data"></param>        /// <returns></returns>        public static ApiResponseModel CreateSuccess(HttpStateCode code, dynamic Data)        {            return new PrivateApiResponseModel            {                StatuCode = code,                Message = "Success",                Data = Data            };        }        /// <summary>        /// 私有类        /// </summary>        private class PrivateApiResponseModel : ApiResponseModel { }    }}

同时在项目中创建一个 Helpers 文件夹,再创建一个 SchemeHelper 类,其内容如下:

using AbpBase.Domain.Shared.Attributes;using System;using System.Linq;using System.Reflection;namespace AbpBase.Domain.Shared.Helpers{    /// <summary>    /// 获取各种枚举代表的信息    /// </summary>    public static class SchemeHelper    {        private static readonly PropertyInfo SchemeNameAttributeMessage = typeof(SchemeNameAttribute).GetProperty(nameof(SchemeNameAttribute.Message));        /// <summary>        /// 获取一个使用了 SchemeNameAttribute 特性的 Message 属性值        /// </summary>        /// <typeparam name="T"></typeparam>        /// <param name="type"></param>        /// <returns></returns>        public static string Get<T>(T type)        {            return GetValue(type);        }        private static string GetValue<T>(T type)        {            var attr = typeof(T).GetField(Enum.GetName(type.GetType(), type))                .GetCustomAttributes()                .FirstOrDefault(x => x.GetType() == typeof(SchemeNameAttribute));            if (attr == null)                return string.Empty;            var value = (string)SchemeNameAttributeMessage.GetValue(attr);            return value;        }    }}

上面的类到底是干嘛的,你先不要问。

全局异常拦截器

在 AbpBase.Web 项目中,新建一个 Filters 文件夹,添加一个 WebGlobalExceptionFilter.cs 文件,其文件内容如下:

using AbpBase.Domain.Shared.Apis;using Microsoft.AspNetCore.Http;using Microsoft.AspNetCore.Mvc;using Microsoft.AspNetCore.Mvc.Filters;using Newtonsoft.json;using System.Threading.Tasks;namespace ApbBase.HttpApi.Filters{    /// <summary>    /// Web 全局异常过滤器,处理 Web 中出现的、运行时未处理的异常    /// </summary>    public class WebGlobalExceptionFilter : IAsyncExceptionFilter    {        public async Task OnExceptionAsync(ExceptionContext context)        {            if (!context.ExceptionHandled)            {                ApiResponseModel model = ApiResponseModel.Create(HttpStateCode.Status500InternalServerError,                    CommonResponseType.Status500InternalServerError);                context.Result = new ContentResult                {                    Content = JsonConvert.SerializeObject(model),                    StatusCode = StatusCodes.Status200OK,                    ContentType = "application/json; charset=utf-8"                };            }            context.ExceptionHandled = true;            await Task.CompletedTask;        }    }}

然后 在 AbpBaseWebModule 模块的 ConfigureServices 函数中,加上:

            Configure<MvcOptions>(options =>            {                options.Filters.Add(typeof(WebGlobalExceptionFilter));            });

这里我们还没有将写入日志,后面再增加这方面的功能。

先说明一下

前面我们定义了 ApiResponseModel 和其他一些特性还有枚举,这里解释一下原因。

ApiResponseModel 是抽象类

ApiResponseModel<T> 和 ApiResponseModel 是抽象类,是为了避免开发者使用时,直接这样用:

            ApiResponseModel mode = new ApiResponseModel            {                Code = 500,                Message = "失败",                Data = xxx            };

首先这个 Code 需要按照 HTTP 状态的标准来填写,我们使用 HttpStateCode 枚举来标记,代表异常时,使用 Status500InternalServerError 来标识。

我非常讨厌一个 Action 的一个返回,就写一次消息的。

if(... ...)return xxxx("请求数据不能为空");if(... ...)return xxxx("xxx 要大于 10");... ..

这样每个地方一个消息说明,十分不统一,也不便于修改。

直接使用一个枚举来代表消息,而不能直接写出来,这样就可以达到统一了。

使用抽象类,可以避免开发者直接 new 一个,强制要求一定的消息格式来响应。后面可以进行更多的尝试,来体会我这样设计的便利性。

跨域请求

这里我们将配置 Web 全局允许跨域请求。

在 AbpBaseWebModule 模块中:

添加一个静态变量

private const string AbpBaseWebCosr = "AllowSpecificOrigins";

创建一个配置函数:

        /// <summary>        /// 配置跨域        /// </summary>        /// <param name="context"></param>        private void ConfigureCors(ServiceConfigurationContext context)        {            context.Services.AddCors(options =>            {                options.AddPolicy(AbpBaseWebCosr,                    builder => builder.AllowAnyHeader()                        .AllowAnyMethod()                        .AllowAnyOrigin());            });        }

在 ConfigureServices 函数中添加:

            // 跨域请求            ConfigureCors(context);

在 OnApplicationInitialization 中添加:

            app.UseCors(AbpBaseWebCosr);// 位置在 app.UseRouting(); 后面

就这样,允许全局跨域请求就完成了。

配置 API 服务

你可以使用以下模块来配置一个 API 模块服务:

            Configure<AbpAspNetCoreMvcOptions>(options =>            {                options                    .ConventionalControllers                    .Create(typeof(AbpBaseHttpApiModule).Assembly, opts =>                    {                        opts.RootPath = "api/1.0";                    });            });

我们在 AbpBase.HttpApi 中将其本身用于创建一个 API 服务,ABP 会将继承了 AbpController 、ControllerBase 等的类识别为 API控制器。上面的代码同时将其默认路由的前缀设置为 api/1.0

也可以不设置前缀:

            Configure<AbpAspNetCoreMvcOptions>(options =>            {                options.ConventionalControllers.Create(typeof(IoTCenterWebModule).Assembly);            });

由于 API 模块已经在自己的 ConfigureServices 创建了 API 服务,因此可以不在 Web 模块里面编写这部分代码。当然,也可以统一在 Web 中定义所有的 API 模块。

统一 API 模型验证消息

创建前

首先,如果我们这样定义一个 Action:

        public class TestModel        {            [Required]            public int Id { get; set; }                        [MaxLength(11)]            public int Iphone { get; set; }                        [Required]            [MinLength(5)]            public string Message { get; set; }        }        [HttpPost("/T2")]        public string MyWebApi2([FromBody] TestModel model)        {            return "请求完成";        }

使用以下参数请求:

{    "Id": "1",    "Iphone": 123456789001234567890,    "Message": null}

会得到以下结果:

{    "errors": {        "Iphone": [            "JSON integer 123456789001234567890 is too large or small for an Int32. Path 'Iphone', line 3, position 35."        ]    },    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",    "title": "One or more validation errors occurred.",    "status": 400,    "traceId": "|af964c79-41367b2145701111."}

这样的信息阅读起来十分不友好,前端对接也会有一定的麻烦。

这个时候我们可以统一模型验证拦截器,定义一个友好的响应格式。

创建方式

在 AbpBase.Web 的项目 的 Filters 文件夹中,创建一个 InvalidModelStateFilter 文件,其文件内容如下:

using AbpBase.Domain.Shared.Apis;using Microsoft.AspNetCore.Http;using Microsoft.AspNetCore.Mvc;using Microsoft.Extensions.DependencyInjection;using System.Linq;namespace AbpBase.Web.Filters{    public static class InvalidModelStateFilter    {        /// <summary>        /// 统一模型验证        /// <para>控制器必须添加 [ApiController] 才能被此过滤器拦截</para>        /// </summary>        /// <param name="services"></param>        public static void GlabalInvalidModelStateFilter(this IServiceCollection services)        {            services.Configure<ApiBehaviorOptions>(options =>            {                options.InvalidModelStateResponseFactory = actionContext =>                {                    if (actionContext.ModelState.IsValid)                        return new BadRequestObjectResult(actionContext.ModelState);                    int count = actionContext.ModelState.Count;                    ValidationErrors[] errors = new ValidationErrors[count];                    int i = 0;                    foreach (var item in actionContext.ModelState)                    {                        errors[i] = new ValidationErrors                        {                            Member = item.Key,                            Messages = item.Value.Errors?.Select(x => x.ErrorMessage).ToArray()                        };                        i++;                    }                    // 响应消息                    var result = ApiResponseModel.Create(HttpStateCode.Status400BadRequest, CommonResponseType.BadRequest, errors);                    var objectResult = new BadRequestObjectResult(result);                    objectResult.StatusCode = StatusCodes.Status400BadRequest;                    return objectResult;                };            });        }        /// <summary>        /// 用于格式化实体验证信息的模型        /// </summary>        private class ValidationErrors        {          www.easck.com  /// <summary>            /// 验证失败的字段            /// </summary>            public string Member { get; set; }            /// <summary>            /// 此字段有何种错误            /// </summary>            public string[] Messages { get; set; }        }    }}

在 ConfigureServices 函数中,添加以下代码:

            // 全局 API 请求实体验证失败信息格式化            context.Services.GlabalInvalidModelStateFilter();

创建后

让我们看看增加了统一模型验证器后,同样的请求返回的消息。

请求:

{    "Id": "1",    "Iphone": 123456789001234567890,    "Message": null}

返回:

{    "statuCode": 400,    "message": "请求的数据未能通过验证",    "data": [        {            "member": "Iphone",            "messages": [                "JSON integer 123456789001234567890 is too large or small for an Int32. Path 'Iphone', line 3, position 35."            ]        }    ]}

说明我们的统一模型验证响应起到了作用。

但是有些验证会直接报异常而不会流转到上面的拦截器中,有些模型验证特性用错对象的话,他会报错异常的。例如上面的 MaxLength ,已经用错了,MaxLength 是指定属性中允许的数组或字符串数据的最大长度,不能用在 int 类型上。大家测试一下请求下面的 json,会发现报异常。

{    "Id": 1,    "Iphone": 1234567900,    "Message": "nullable"}

以下是一些 ASP.NET Core 内置验证特性,大家记得别用错:

[CreditCard]:验证属性是否具有信用卡格式。 需要 jquery 验证其他方法。[Compare]:验证模型中的两个属性是否匹配。[EmailAddress]:验证属性是否具有电子邮件格式。[Phone]:验证属性是否具有电话号码格式。[Range]:验证属性值是否在指定的范围内。[RegularExpression]:验证属性值是否与指定的正则表达式匹配。[Required]:验证字段是否不为 null。 有关此属性的行为的详细信息[StringLength]:验证字符串属性值是否不超过指定长度限制。[Url]:验证属性是否具有 URL 格式。[Remote]:通过在服务器上调用操作方法来验证客户端上的输入。[MaxLength ] MaxLength 是指定属性中允许的数组或字符串数据的最大长度

参考:https://docs.microsoft.com/zh-cn/dotnet/api/system.componentmodel.dataannotations?view=netcore-3.1

本系列第二篇到此,接下来第三篇会继续添加一些基础服务。

补充:为什么需要统一格式

首先,你看一下这样的代码:

为ABP框架添加基础集成服务

在每个 Action 中,都充满了这种写法,每个相同的验证问题,在每个 Action 返回的文字都不一样,没有规范可言。一个人写一个 return,就加上一下自己要表达的 文字,一个项目下来,多少 return ?全是这种代码,不堪入目。

通过统一模型验证和统一消息返回格式,就可以避免这些情况。

源码地址:https://github.com/whuanle/AbpBaseStruct

本教程结果代码位置:https://github.com/whuanle/AbpBaseStruct/tree/master/src/2/AbpBase

到此这篇关于为ABP框架添加基础集成服务的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持我们。