淺入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
?全是這種代碼,不堪入目。
通過統一模型驗證和統一消息返回格式,就可以避免這些情況。