定义一个特性标记
这个标记用于标记一个枚举代表的信息。
在 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框架添加基础集成服务的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持代码网。
发表评论