当前位置 : 主页 > 网络编程 > ASP >

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

来源:互联网 收集:自由互联 发布时间:2023-01-17
目录 定义一个特性标记 全局统一消息格式 Http 状态码 常用的请求结果 响应模型 全局异常拦截器 先说明一下 ApiResponseModel 是抽象类 跨域请求 配置 API 服务 统一 API 模型验证消息 创建
目录
  • 定义一个特性标记
  • 全局统一消息格式
    • 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
            {
                /// <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

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

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

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

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

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

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

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

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

    上一篇:为ABP框架增加日志组件与依赖注入服务
    下一篇:没有了
    网友评论