浅入ABP(2):添加基础集成服务
浅入ABP(2):添加基础集成服务
版权护体作者:痴者工良,微信公众号转载文章需要 《NCC开源社区》同意。
上一篇,我们已经搭建起了一个基本的程序结构,下面我们来添加一些必要的服务,例如异常拦截器、跨域等。
本教程的代码比较多,关联性比较强,需要整体写好后,才能正常使用,所以可以先按照过程做一次,再回头看解析。
本章的内容不仅适合 ABP, ASP.NET Core 也可以直接使用。
源码地址:https://github.com/whuanle/AbpBaseStruct
本教程结果代码位置:https://github.com/whuanle/AbpBaseStruct/tree/master/src/2/AbpBase
定义一个特性标记
这个标记用于标记一个枚举代表的信息。
在 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 ?全是这种代码,不堪入目。
通过统一模型验证和统一消息返回格式,就可以避免这些情况。
浅入ABP(2):添加基础集成服务的更多相关文章
- 浅入ABP(1):搭建基础结构的 ABP 解决方案
浅入ABP(1):搭建基础结构的 ABP 解决方案 目录 浅入ABP(1):搭建基础结构的 ABP 解决方案 搭建项目基础结构 ApbBase.Domain.Shared 创建过程 ApbBase.D ...
- 浅入 ABP 系列(4):事件总线
浅入 ABP 系列(4):事件总线 版权护体作者:痴者工良,微信公众号转载文章需要 <NCC开源社区>同意. 目录 浅入 ABP 系列(4):事件总线 事件总线 关于事件总线 为什么需要这 ...
- 浅入深出之Java集合框架(上)
Java中的集合框架(上) 由于Java中的集合框架的内容比较多,在这里分为三个部分介绍Java的集合框架,内容是从浅到深,如果已经有java基础的小伙伴可以直接跳到<浅入深出之Java集合框架 ...
- 浅入深出之Java集合框架(中)
Java中的集合框架(中) 由于Java中的集合框架的内容比较多,在这里分为三个部分介绍Java的集合框架,内容是从浅到深,如果已经有java基础的小伙伴可以直接跳到<浅入深出之Java集合框架 ...
- 浅入深出之Java集合框架(下)
Java中的集合框架(下) 由于Java中的集合框架的内容比较多,在这里分为三个部分介绍Java的集合框架,内容是从浅到深,哈哈这篇其实也还是基础,惊不惊喜意不意外 ̄▽ ̄ 写文真的好累,懒得写了.. ...
- 重新学习MySQL数据库2:『浅入浅出』MySQL 和 InnoDB
重新学习Mysql数据库2:『浅入浅出』MySQL 和 InnoDB 作为一名开发人员,在日常的工作中会难以避免地接触到数据库,无论是基于文件的 sqlite 还是工程上使用非常广泛的 MySQL.P ...
- 『浅入浅出』MySQL 和 InnoDB
作为一名开发人员,在日常的工作中会难以避免地接触到数据库,无论是基于文件的 sqlite 还是工程上使用非常广泛的 MySQL.PostgreSQL,但是一直以来也没有对数据库有一个非常清晰并且成体系 ...
- Spring的数据库编程浅入浅出——不吹牛逼不装逼
Spring的数据库编程浅入浅出——不吹牛逼不装逼 前言 上文书我写了Spring的核心部分控制反转和依赖注入,后来又衔接了注解,在这后面本来是应该写Spring AOP的,但我觉得对于初学者来说,这 ...
- 浅入深出Vue:文章列表
终于到我们小项目的最后一个功能了,那就是列表页展示! 新建组件 先来新建组件 List.vue: <template> <div></div> </templ ...
- 浅谈Abp vNext的模块化设计
abp的模块化给我留下深刻的印象,模块化不是什么新概念,大家都习以为常,但是为什么要模块化,模块化的意义或者说目的是什么?也许我们思考得并不深入.难得的是abp不仅完美的阐述了模块化概念,而且把模块化 ...
随机推荐
- SMFL 教程&个人笔记(2)
本文大部分来自官方教程的Google翻译 但是加了一点点个人的理解和其他相关知识 转载请注明 原文链接 :https://www.cnblogs.com/Multya/p/16317401.html ...
- 【DP】DMOPC '21 Contest 8 P5 - Tree Building
Problem Link 给定 \(n,m\) 和一个长为 \(m\) 的代价序列,对于一棵 \(n\) 个节点,每个节点度数不超过 \(m\) 的树,定义它的代价为 \(\sum\limits_{i ...
- Vite 按需引入 Ant Design Vue 3.0
Vite 按需引入 Ant Design Vue 3.0 第一步下载: npm i unplugin-vue-components -D 需要注意的是:Vite你可以用 unplugin-vue-co ...
- 解决pycharm编辑超大超大项目时CPU占用100%
在编辑py文件时,cpu占用100%其实和内存的关系不大,因为这个现象是间歇性的,不是持续的. 我试过给pycharm分配16GB的内存,也是一样没有缓解CPU占用高. 项目和pycharam也都是存 ...
- NLP文本匹配任务Text Matching [无监督训练]:SimCSE、ESimCSE、DiffCSE 项目实践
NLP文本匹配任务Text Matching [无监督训练]:SimCSE.ESimCSE.DiffCSE 项目实践 文本匹配多用于计算两个文本之间的相似度,该示例会基于 ESimCSE 实现一个无监 ...
- 【4】jupyter notebook快速入门、以及常用快捷键使用
相关文章: [1]Anaconda安装超简洁教程,瞬间学会! [2]Anaconda下:ipython文件的打开方式,Jupyter Notebook中运行.py文件,快速打开ipython文件的方法 ...
- C/C++ Qt 监控文件状态变化
实现对特定文件的监控,Qt中提供了QFileSystemWatcher调用这个接口可以快速实现监控功能,当有文件发生变化是自动触发并输出文件具体信息. filesystem.h #ifndef FIL ...
- 顺颂秋冬<一>
起名字真难. 原来想给这个合集起个积极的名字,记录鄙人浅薄的认知和内心的荒芜. 以及所遇见的温暖. 想来想去,不过是 浮生旧茶 西楼残月之类的 难堪大用. 后来想起来一句, 即,顺颂时祺,秋绥冬禧, ...
- 使用Docker部署Tomcat
目录 使用Docker部署Tomcat 1. 获取镜像 2. 第一次启动tomcat 3.带参数启动 4.查看tomcat日志 5.时区问题 使用Docker部署Tomcat 1. 获取镜像 dock ...
- Java - CodeForces - 469C
题目: 现在有一个容器,里面有n个物品,编号为1-n,现在小q可以进行一些操作,每次取出任意两个数,可以把这两个数的编号相加,相减,相乘,再把结果放回容器.问最后小q能否在n-1次操作后使得容器里的唯 ...