Introduction to ABP (2): Adding Basic Integration Services

2020年9月15日 78点热度 1人点赞 0条评论
内容目录

版权护体©作者:whuanle,微信公众号转载文章需要 《NCC开源社区》同意。

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.

痴者工良

高级程序员劝退师

文章评论