浅入ABP(2):添加基础集成服务

简介: 浅入ABP(2):添加基础集成服务

定义一个特性标记


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

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 会将继承了 AbpControllerControllerBase 等的类识别为 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

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


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


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

微信图片_20220504113143.png

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

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

相关文章
|
1月前
|
存储 数据可视化 Java
基于MicrometerTracing门面和Zipkin实现集成springcloud2023的服务追踪
Sleuth将会停止维护,Sleuth最新版本也只支持springboot2。作为替代可以使用MicrometerTracing在微服务中作为服务追踪的工具。
103 1
|
2月前
|
Java Maven Windows
使用Java创建集成JACOB的HTTP服务
本文介绍了如何在Java中创建一个集成JACOB的HTTP服务,使Java应用能够调用Windows的COM组件。文章详细讲解了环境配置、动态加载JACOB DLL、创建HTTP服务器、实现IP白名单及处理HTTP请求的具体步骤,帮助读者实现Java应用与Windows系统的交互。作者拥有23年编程经验,文章来源于稀土掘金。著作权归作者所有,商业转载需授权。
使用Java创建集成JACOB的HTTP服务
|
19天前
|
安全 测试技术 数据安全/隐私保护
原生鸿蒙应用市场开发者服务的技术解析:从集成到应用发布的完整体验
原生鸿蒙应用市场开发者服务的技术解析:从集成到应用发布的完整体验
|
1月前
|
存储 Java 开发工具
【三方服务集成】最新版 | 阿里云OSS对象存储服务使用教程(包含OSS工具类优化、自定义阿里云OSS服务starter)
阿里云OSS(Object Storage Service)是一种安全、可靠且成本低廉的云存储服务,支持海量数据存储。用户可通过网络轻松存储和访问各类文件,如文本、图片、音频和视频等。使用OSS后,项目中的文件上传业务无需在服务器本地磁盘存储文件,而是直接上传至OSS,由其管理和保障数据安全。此外,介绍了OSS服务的开通流程、Bucket创建、AccessKey配置及环境变量设置,并提供了Java SDK示例代码,帮助用户快速上手。最后,展示了如何通过自定义starter简化工具类集成,实现便捷的文件上传功能。
【三方服务集成】最新版 | 阿里云OSS对象存储服务使用教程(包含OSS工具类优化、自定义阿里云OSS服务starter)
|
1月前
|
开发框架 .NET API
Windows Forms应用程序中集成一个ASP.NET API服务
Windows Forms应用程序中集成一个ASP.NET API服务
90 9
|
1月前
|
安全 Java 测试技术
ToB项目身份认证AD集成(二):快速搞定window server 2003部署AD域服务并支持ssl
本文详细介绍了如何搭建本地AD域控测试环境,包括安装AD域服务、测试LDAP接口及配置LDAPS的过程。通过运行自签名证书生成脚本和手动部署证书,实现安全的SSL连接,适用于ToB项目的身份认证集成。文中还提供了相关系列文章链接,便于读者深入了解AD和LDAP的基础知识。
|
3月前
|
存储 设计模式 缓存
OpenFeign集成Ribbon负载均衡-过滤和选择服务核心实现
该文章主要介绍了如何在OpenFeign中集成Ribbon以实现负载均衡,并详细分析了Ribbon中服务选择和服务过滤的核心实现过程。文章还涉及了Ribbon中负载均衡器(ILoadBalancer)和负载均衡策略(IRule)的初始化方式。
OpenFeign集成Ribbon负载均衡-过滤和选择服务核心实现
|
2月前
|
编解码 Linux 开发工具
Linux平台x86_64|aarch64架构RTMP推送|轻量级RTSP服务模块集成说明
支持x64_64架构、aarch64架构(需要glibc-2.21及以上版本的Linux系统, 需要libX11.so.6, 需要GLib–2.0, 需安装 libstdc++.so.6.0.21、GLIBCXX_3.4.21、 CXXABI_1.3.9)。
|
3月前
|
域名解析 网络协议 API
【API管理 APIM】APIM集成内部VNet时,常遇见的关于自定义DNS服务问题。
【API管理 APIM】APIM集成内部VNet时,常遇见的关于自定义DNS服务问题。
|
4月前
|
开发框架 前端开发 JavaScript
ABP框架中短信发送处理,包括阿里云短信和普通短信商的短信发送集成
ABP框架中短信发送处理,包括阿里云短信和普通短信商的短信发送集成
ABP框架中短信发送处理,包括阿里云短信和普通短信商的短信发送集成