版权护体©作者:whuanle,微信公众号转载文章需要 《NCC开源社区》同意。
- Define a Feature Attribute
- Global Uniform Message Format
- Response Model
- Global Exception Interceptor
- Let’s Clarify
- Cross-Domain Requests
- Configure API Services
- Unified API Model Validation Messages
In the previous article, we set up a basic program structure. Now, we will add some necessary services, such as exception interceptors and cross-domain support.
This tutorial has a lot of code that is closely related. It needs to be properly written as a whole to function correctly, so you might want to follow through the process first and then look back at the analysis.
The content of this chapter is suitable for both ABP and ASP.NET Core.
Source code address: https://github.com/whuanle/AbpBaseStruct
Tutorial result code location: https://github.com/whuanle/AbpBaseStruct/tree/master/src/2/AbpBase
Define a Feature Attribute
This attribute is used to mark an enum representing information.
In the AbpBase.Domain.Shared
project, create an Attributes
directory, then create a SchemeNameAttribute
class with the following content:
/// <summary>
/// Mark an enum representing information
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
public class SchemeNameAttribute : Attribute
{
public string Message { get; set; }
public SchemeNameAttribute(string message)
{
Message = message;
}
}
Global Uniform Message Format
To unify the response format of the web application and to have a standardized template for API development, we need to define an appropriate template.
Create an Apis
directory within AbpBase.Domain.Shared
.
HTTP Status Codes
To accommodate various HTTP request response statuses, we define an enum that identifies the status codes.
In the Apis
directory, create a HttpStateCode.cs
file with the following content:
namespace AbpBase.Domain.Shared.Apis
{
/// <summary>
/// Standard HTTP Status Codes
/// <para>Documentation address<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
}
}
Common Request Results
In the same directory, create a CommonResponseType
enum with the following content:
/// <summary>
/// Common API response messages
/// </summary>
public enum CommonResponseType
{
[SchemeName("")] Default = 0,
[SchemeName("Request Successful")] RequestSuccess = 1,
[SchemeName("Request Failed")] RequestFail = 2,
[SchemeName("Resource Created Successfully")] CreateSuccess = 4,
[SchemeName("Resource Creation Failed")] CreateFail = 8,
[SchemeName("Resource Updated Successfully")] UpdateSuccess = 16,
[SchemeName("Resource Update Failed")] UpdateFail = 32,
[SchemeName("Resource Deleted Successfully")] DeleteSuccess = 64,
[SchemeName("Resource Deletion Failed")] DeleteFail = 128,
[SchemeName("The data of the request didn't pass validation")] BadRequest = 256,
[SchemeName("The server encountered a serious error")] Status500InternalServerError = 512
}
Response Model
In the Apis
directory, create a ApiResponseModel.cs
generic class file with the following content:
namespace AbpBase.Domain.Shared.Apis
{
/// <summary>
/// API response format
/// <para>To avoid misuse, this class cannot be instantiated and can only be generated through predefined static methods</para>
/// </summary>
/// <typeparam name="TData"></typeparam>
public abstract class ApiResponseModel<TData>
{
public HttpStateCode StatusCode { get; set; }
public string Message { get; set; }
public TData Data { get; set; }
/// <summary>
/// Private class
/// </summary>
/// <typeparam name="TResult"></typeparam>
private class PrivateApiResponseModel<TResult> : ApiResponseModel<TResult> { }
}
}
StatusCode: Used to indicate the status of the response;
Message: Response message;
Data: Response data;
You might find it strange; don’t ask or guess, just follow along, and I will explain later why this is written this way.
Next, create a class:
using AbpBase.Domain.Shared.Helpers;
using System;
namespace AbpBase.Domain.Shared.Apis
{
/// <summary>
/// Web response format
/// <para>To avoid misuse, this class cannot be instantiated and can only be generated through predefined static methods</para>
/// </summary>
public abstract class ApiResponseModel : ApiResponseModel<dynamic>
{
/// <summary>
/// Create response format based on the enum
/// </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
{
StatusCode = code,
Message = SchemeHelper.Get(enumType),
};
}
/// <summary>
/// Create a standard response
/// </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
{
StatusCode = code,
Message = SchemeHelper.Get(enumType),
Data = Data
};
}
/// <summary>
/// Request was successful
/// </summary>
/// <param name="code"></param>
/// <param name="Data"></param>
/// <returns></returns>
public static ApiResponseModel CreateSuccess(HttpStateCode code, dynamic Data)
{
return new PrivateApiResponseModel
{
StatusCode = code,
Message = "Success",
Data = Data
};
}
/// <summary>
/// Private class
/// </summary>
private class PrivateApiResponseModel : ApiResponseModel { }
}
}
Also, create a Helpers folder in the project and then create a SchemeHelper
class with the following content:
using AbpBase.Domain.Shared.Attributes;
using System;
using System.Linq;
using System.Reflection;
namespace AbpBase.Domain.Shared.Helpers
{
/// <summary>
/// Get information represented by various enums
/// </summary>
public static class SchemeHelper
{
private static readonly PropertyInfo SchemeNameAttributeMessage = typeof(SchemeNameAttribute).GetProperty(nameof(SchemeNameAttribute.Message));
/// <summary>
/// Get the Message property value of a field that uses the SchemeNameAttribute
/// </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;
}
}
}
Don’t ask what the classes above are for just yet.
Global Exception Interceptor
In the AbpBase.Web
project, create a new folder named Filters
and add a file called WebGlobalExceptionFilter.cs
, with the following content:
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 AbpBase.HttpApi.Filters
{
/// <summary>
/// Web global exception filter that handles unhandled runtime exceptions in 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;
}
}
}
Then, in the AbpBaseWebModule
module's ConfigureServices
function, add:
Configure<MvcOptions>(options =>
{
options.Filters.Add(typeof(WebGlobalExceptionFilter));
});
We have not yet implemented logging, which we will add later.
Explanation
Earlier, we defined ApiResponseModel
along with some other attributes and enums; let us explain the reasoning behind that.
ApiResponseModel
is an Abstract Class
ApiResponseModel<T>
and ApiResponseModel
are abstract classes designed to prevent developers from using them directly like so:
ApiResponseModel mode = new ApiResponseModel
{
Code = 500,
Message = "Failure",
Data = xxx
};
Firstly, this Code
must be filled in according to HTTP status standards. We use the HttpStateCode
enum to indicate that in case of an exception, we should use Status500InternalServerError
to identify the error.
I really dislike having to write a message once for each return in an action.
if(... ...)
return xxxx("Request data cannot be empty");
if(... ...)
return xxxx("xxx must be greater than 10");
... ..
This way, each place has a different message, leading to a lack of uniformity and making it inconvenient to modify.
By using an enum to represent messages instead of writing them directly, we achieve uniformity.
Using an abstract class prevents developers from directly instantiating one and enforces a specific message format for responses. There can be more attempts to appreciate the convenience of this design later.
Cross-Origin Requests
Here, we will configure the Web to globally allow cross-origin requests.
In the AbpBaseWebModule
module:
Add a static variable
private const string AbpBaseWebCosr = "AllowSpecificOrigins";
Create a configuration function:
/// <summary>
/// Configure Cross-Origin Requests
/// </summary>
/// <param name="context"></param>
private void ConfigureCors(ServiceConfigurationContext context)
{
context.Services.AddCors(options =>
{
options.AddPolicy(AbpBaseWebCosr,
builder => builder.AllowAnyHeader()
.AllowAnyMethod()
.AllowAnyOrigin());
});
}
In the ConfigureServices
function, add:
// Cross-Origin Requests
ConfigureCors(context);
In OnApplicationInitialization
, add:
app.UseCors(AbpBaseWebCosr); // Position this after app.UseRouting();
With this, global cross-origin requests are allowed.
Configure API Services
You can configure an API module service using the following module:
Configure<AbpAspNetCoreMvcOptions>(options =>
{
options
.ConventionalControllers
.Create(typeof(AbpBaseHttpApiModule).Assembly, opts =>
{
opts.RootPath = "api/1.0";
});
});
We use this in AbpBase.HttpApi
to create an API service. ABP will identify classes inheriting from AbpController
, ControllerBase
, etc., as API controllers. The code above also sets the default route prefix to api/1.0
.
You can also choose not to set a prefix:
Configure<AbpAspNetCoreMvcOptions>(options =>
{
options.ConventionalControllers.Create(typeof(IoTCenterWebModule).Assembly);
});
Since the API module has already created the API service in its own ConfigureServices
, there is no need to write this part of the code in the Web
module. However, all API modules can also be defined uniformly in the Web
.
Unified API Model Validation Messages
Before Creation
First, if we define an action like this:
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 "Request completed";
}
Using the following parameter request:
{
"Id": "1",
"Iphone": 123456789001234567890,
"Message": null
}
You will receive the following result:
{
"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."
}
Such information is very unfriendly to read, and there will be some trouble for the frontend to adapt.
At this point, we can unify the model validation interceptor to define a user-friendly response format.
Creation Method
In the AbpBase.Web
project, within the Filters
folder, create a file named InvalidModelStateFilter
, with the following content:
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>
/// Unified model validation
/// <para>The controller must be annotated with [ApiController] to be intercepted by this filter</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++;
}
// Response message
var result = ApiResponseModel.Create(HttpStateCode.Status400BadRequest, CommonResponseType.BadRequest, errors);
var objectResult = new BadRequestObjectResult(result);
objectResult.StatusCode = StatusCodes.Status400BadRequest;
return objectResult;
};
});
}
/// <summary>
/// Model to format validation error information
/// </summary>
private class ValidationErrors
{
/// <summary>
/// Field that failed validation
/// </summary>
public string Member { get; set; }
/// <summary>
/// What errors this field has
/// </summary>
public string[] Messages { get; set; }
}
}
}
In the ConfigureServices
function, add the following code:
// Global API request instance validation failure message formatting
context.Services.GlabalInvalidModelStateFilter();
After Creation
Let us see how the unified model validator affects the response for the same request.
Request:
{
"Id": "1",
"Iphone": 123456789001234567890,
"Message": null
}
Response:
{
"statuCode": 400,
"message": "The requested data failed validation",
"data": [
{
"member": "Iphone",
"messages": [
"JSON integer 123456789001234567890 is too large or small for an Int32. Path 'Iphone', line 3, position 35."
]
}
]
}
This indicates that our unified model validation response is effective.
However, some validations may throw exceptions directly without flowing through the interceptors above. Some model validation attributes, when used incorrectly with objects, will cause exceptions. For instance, the MaxLength
attribute in the earlier example is used incorrectly; MaxLength
specifies the maximum length of the array or string data allowed in a property and cannot be applied to int type. Testing with the JSON below will result in an exception.
{
"Id": 1,
"Iphone": 1234567900,
"Message": "nullable"
}
Here are some built-in validation attributes in ASP.NET Core; make sure not to use them incorrectly:
[CreditCard]
: Validates that the property has a credit card format. Requires jQuery validation for other methods.[Compare]
: Validates whether two properties in the model match.[EmailAddress]
: Validates that the property has an email format.[Phone]
: Validates that the property has a phone number format.[Range]
: Validates that the property value is within a specified range.[RegularExpression]
: Validates that the property value matches a specified regular expression.[Required]
: Validates that the field is not null. Refer to the detailed behavior of this attribute.[StringLength]
: Validates that the string property value does not exceed a specified length limit.[Url]
: Validates that the property has a URL format.[Remote]
: Validates client-side inputs by calling an action method on the server.[MaxLength]
: Specifies the maximum length of array or string data allowed in the property.
Refer to: https://docs.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations?view=netcore-3.1
The second article in this series ends here, and the third article will continue with additional foundational services.
Supplement: Why Unified Formats are Necessary
First, take a look at such code:
In each action, there are numerous instances of such writing, with different return messages for the same validation problem, lacking any standards.
A person writes a return
statement, and then adds the corresponding text
they want to express. How many returns
are there in a project? It's all like this code, which is unbearable to look at.
By using a unified model validation and a standard message return format, these situations can be avoided.
文章评论