乘风破浪,遇见最佳跨平台跨终端框架.Net Core/.Net生态 - 浅析ASP.NET Core路由和终结点,利用Swagger和OpenAPI呈现路由方案

什么是路由

路由(Routing)负责匹配传入的HTTP请求,然后将这些请求发送到应用的可执行终结点(Endpoint)。终结点是应用的可执行请求处理代码单元。终结点在应用中进行定义,并在应用启动时进行配置。终结点匹配过程可以从请求的URL中提取值,并为请求处理提供这些值。通过使用应用中的终结点信息,路由还能生成映射到终结点的URL。

image

应用可以使用以下内容配置路由:

  • Controllers
  • RazorPages
  • SignalR
  • gRPC服务
  • 启用终结点的中间件,例如运行状况检查
  • 通过路由注册的委托和Lambda

路由注册方式

路由系统的核心作用是,将访问URL和应用程序Controller形成一种映射关系,这种映射关系有两种作用:

  • 把Url映射到对应的Controller的Action上
  • 根据Controller和Action的名字来生成URL

ASP.Net Core提供两种方式来注册路由

  • 路由模板的方式,之前传统的方式,作为MVC页面的Web配置
  • RouteAttribute的方式,更适合Web API的场景,更适合前后端分离的架构

路由约束

  • 类型约束
  • 范围约束
  • 正则表达式
  • 是否必选
  • 自定义IRouteConstraint

URL的生成

ASP.Net Core提供了两个类可以根据路由的信息来反向生成URL地址

  • LinkGenerator,全新提供的一个链接生成对象,可从容器获取它,根据需要生成URL地址
  • IUrlHelper,和之前MVC框架中使用的MVCHelper很像

动手实践

https://github.com/TaylorShi/HelloRoutingEndpoint

什么是Swagger

https://swagger.io

image

Swagger是一套功能强大而又易于使用的API开发工具,适用于团队和个人,使开发贯穿整个API生命周期,从设计和文档,到测试和部署。

Swagger由开源、免费和商业化的工具组合而成,允许任何人,从技术工程师到街头聪明的产品经理,都能建立人人喜爱的惊人的API。

Swagger是由SmartBear软件公司建立的,该公司是为团队提供软件质量工具的领导者。SmartBear是软件领域中一些最大的名字的背后,包括Swagger、SoapUI和QAComplete。

Swashbuckle.AspNetCore为用ASP.NET Core构建的API提供Swagger工具。直接从你的路由、控制器和模型中生成漂亮的API文档,包括一个探索和测试操作的用户界面。

除了Swagger 2.0和OpenAPI 3.0生成器外,Swashbuckle还提供了一个嵌入式版本的swagger-ui,它由生成的Swagger JSON驱动。这意味着你可以用始终与最新代码同步的活的文档来补充你的API。最重要的是,它只需要最少的编码和维护,让你能够专注于建立一个很棒的API。

和OpenAPI

Swagger(OpenAPI)是一个与语言无关的规范,用于描述RESTAPI。它使计算机和用户无需直接访问源代码即可了解RESTAPI的功能。

其主要目标是:

  • 尽量减少连接分离的服务所需的工作量
  • 减少准确记录服务所需的时间

.NET的两个主要OpenAPI实现

Swagger项目已于2015年捐赠给OpenAPI计划,自此它被称为OpenAPI。这两个名称可互换使用。不过,"OpenAPI"指的是规范。"Swagger"指的是来自使用OpenAPI规范的SmartBear的开放源代码和商业产品系列。后续开放源代码产品(如OpenAPIGenerator)也属于Swagger系列名称,尽管SmartBear未发布也是如此。

简而言之

  • OpenAPI是一种规范
  • Swagger是一种使用OpenAPI规范的工具。例如,OpenAPIGenerator和SwaggerUI

OpenAPI规范(openapi.json)

OpenAPI规范是描述API功能的文档。该文档基于控制器和模型中的XML和属性注释。它是OpenAPI流的核心部分,用于驱动诸如SwaggerUI之类的工具。

默认情况下,它命名为openapi.json。下面是为简洁起见而缩减的OpenAPI规范的:

{
  "openapi": "3.0.1",
  "info": {
    "title": "API V1",
    "version": "v1"
  },
  "paths": {
    "/api/Todo": {
      "get": {
        "tags": [
          "Todo"
        ],
        "operationId": "ApiTodoGet",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ToDoItem"
                  }
                }
              },
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ToDoItem"
                  }
                }
              },
              "text/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ToDoItem"
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        …
      }
    },
    "/api/Todo/{id}": {
      "get": {
        …
      },
      "put": {
        …
      },
      "delete": {
        …
      }
    }
  },
  "components": {
    "schemas": {
      "ToDoItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int32"
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "isCompleted": {
            "type": "boolean"
          }
        },
        "additionalProperties": false
      }
    }
  }
}

SwaggerUI

SwaggerUI提供了基于Web的UI,它使用生成的OpenAPI规范提供有关服务的信息

Swashbuckle和NSwag均包含SwaggerUI的嵌入式版本,因此可使用中间件注册调用将该嵌入式版本托管在ASP.NET Core应用中。

image

控制器中的每个公共操作方法都可以从UI中进行测试。选择方法名称可以展开该部分。添加所有必要的参数,然后选择Try it Out

Swashbuckle三大组件

Swashbuckle包括Swashbuckle.AspNetCore.SwaggerSwashbuckle.AspNetCore.SwaggerGenSwashbuckle.AspNetCore.SwaggerUI这三大组件

  • Swashbuckle.AspNetCore.Swagger:将SwaggerDocument对象公开为JSON终结点的Swagger对象模型和中间件。
  • Swashbuckle.AspNetCore.SwaggerGen:从路由、控制器和模型直接生成SwaggerDocument对象的Swagger生成器。它通常与Swagger终结点中间件结合,以自动公开SwaggerJSON。
  • Swashbuckle.AspNetCore.SwaggerUI:SwaggerUI工具的嵌入式版本。它解释SwaggerJSON以构建描述WebAPI功能的可自定义的丰富体验。它包括针对公共方法的内置测试工具。

通常来说,我们更倾向于在ASP.Net Core中引入Swashbuckle.AspNetCore这个聚合Nuget包,它将包括上诉三个组件。

通过Swagger来呈现路由方案

引入依赖包

https://www.nuget.org/packages/Swashbuckle.AspNetCore

依赖包

dotnet add package Swashbuckle.AspNetCore

image

设置项目生成XML注释文件

进入项目属性设置,勾选文档文件的选项,让其可以生成包含API文档的文件,最终它会生成一个和项目同名的xml文件。

image

image

添加SwaggerGen服务

Startup.csConfigureServices方法中,添加并配置Swagger相关的服务

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddSwaggerGen(swaggerGenOptions =>
    {
        swaggerGenOptions.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo { Title = "Tesla API", Version = "v1"});

        var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
        var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
        swaggerGenOptions.IncludeXmlComments(xmlPath);
    });
}

这里通过AddSwaggerGen的方法将Swagger注册进来,同时通过SwaggerGenOptions委托入参来传入一些必要信息。

这里我们看到IncludeXmlComments方法把当前程序集的XML包括进来了,这将帮助我们启用XML注释。

/// <summary>
/// 订单是否存在
/// </summary>
/// <param name="id">必须可以转为Long</param>
/// <returns></returns>
[HttpGet("{id:IsLong}")]
public bool OrderExist([FromRoute]object id)
{
    return true;
}

当我们存在这种注释时,它将在SwaggerUI中展示出来。

image

这里甚至我们还可以通过remarks标记来添加示例信息

/// <summary>
/// 订单是否存在
/// </summary>
/// <remarks>
/// 请求示例:
///
///     GET /api/order/OrderExist/123
///
/// </remarks>
/// <param name="id">必须可以转为Long</param>
/// <returns></returns>
[HttpGet("{id:IsLong}")]
public bool OrderExist([FromRoute]object id)
{
    return true;
}

image

我们来看下AddSwaggerGen的定义

public static class SwaggerGenServiceCollectionExtensions
{
    public static IServiceCollection AddSwaggerGen(
        this IServiceCollection services,
        Action<SwaggerGenOptions> setupAction = null)
    {
        // Add Mvc convention to ensure ApiExplorer is enabled for all actions
        services.Configure<MvcOptions>(c =>
            c.Conventions.Add(new SwaggerApplicationConvention()));

        // Register custom configurators that takes values from SwaggerGenOptions (i.e. high level config)
        // and applies them to SwaggerGeneratorOptions and SchemaGeneratorOptoins (i.e. lower-level config)
        services.AddTransient<IConfigureOptions<SwaggerGeneratorOptions>, ConfigureSwaggerGeneratorOptions>();
        services.AddTransient<IConfigureOptions<SchemaGeneratorOptions>, ConfigureSchemaGeneratorOptions>();

        // Register generator and it's dependencies
        services.TryAddTransient<ISwaggerProvider, SwaggerGenerator>();
        services.TryAddTransient<IAsyncSwaggerProvider, SwaggerGenerator>();
        services.TryAddTransient(s => s.GetRequiredService<IOptions<SwaggerGeneratorOptions>>().Value);
        services.TryAddTransient<ISchemaGenerator, SchemaGenerator>();
        services.TryAddTransient(s => s.GetRequiredService<IOptions<SchemaGeneratorOptions>>().Value);
        services.TryAddTransient<ISerializerDataContractResolver>(s =>
        {
#if (!NETSTANDARD2_0)
            var serializerOptions = s.GetService<IOptions<JsonOptions>>()?.Value?.JsonSerializerOptions
                ?? new JsonSerializerOptions();
#else
            var serializerOptions = new JsonSerializerOptions();
#endif

            return new JsonSerializerDataContractResolver(serializerOptions);
        });

        // Used by the <c>dotnet-getdocument</c> tool from the Microsoft.Extensions.ApiDescription.Server package.
        services.TryAddSingleton<IDocumentProvider, DocumentProvider>();

        if (setupAction != null) services.ConfigureSwaggerGen(setupAction);

        return services;
    }

    public static void ConfigureSwaggerGen(
        this IServiceCollection services,
        Action<SwaggerGenOptions> setupAction)
    {
        services.Configure(setupAction);
    }
}

再来看下SwaggerGenOptions的定义

public class SwaggerGenOptions
{
    public SwaggerGeneratorOptions SwaggerGeneratorOptions { get; set; } = new SwaggerGeneratorOptions();

    public SchemaGeneratorOptions SchemaGeneratorOptions { get; set; } = new SchemaGeneratorOptions();

    // NOTE: Filter instances can be added directly to the options exposed above OR they can be specified in
    // the following lists. In the latter case, they will be instantiated and added when options are injected
    // into their target services. This "deferred instantiation" allows the filters to be created from the
    // DI container, thus supporting contructor injection of services within filters.

    public List<FilterDescriptor> ParameterFilterDescriptors { get; set; } = new List<FilterDescriptor>();

    public List<FilterDescriptor> RequestBodyFilterDescriptors { get; set; } = new List<FilterDescriptor>();

    public List<FilterDescriptor> OperationFilterDescriptors { get; set; } = new List<FilterDescriptor>();

    public List<FilterDescriptor> DocumentFilterDescriptors { get; set; } = new List<FilterDescriptor>();

    public List<FilterDescriptor> SchemaFilterDescriptors { get; set; } = new List<FilterDescriptor>();
}

public class FilterDescriptor
{
    public Type Type { get; set; }

    public object[] Arguments { get; set; }
}

看看SwaggerDoc的定义

public static void SwaggerDoc(
    this SwaggerGenOptions swaggerGenOptions,
    string name,
    OpenApiInfo info)
{
    swaggerGenOptions.SwaggerGeneratorOptions.SwaggerDocs.Add(name, info);
}

看下IncludeXmlComments的定义

public static void IncludeXmlComments(
    this SwaggerGenOptions swaggerGenOptions,
    string filePath,
    bool includeControllerXmlComments = false)
{
    swaggerGenOptions.IncludeXmlComments(() => new XPathDocument(filePath), includeControllerXmlComments);
}

启用Swagger、SwaggerUI中间件

接下来还需要在Configure方法中启用SwaggerSwaggerUI这两个中间件。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseSwagger();

    app.UseSwaggerUI(swaggerUIOptions =>
    {
        swaggerUIOptions.SwaggerEndpoint("/swagger/v1/swagger.json", "Tesla API v1");
    });

    app.UseHttpsRedirection();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

也可以写成

app.UseSwaggerUI(swaggerUIOptions =>
{
    swaggerUIOptions.SwaggerEndpoint("v1/swagger.json", "Tesla API v1");
});

其中UseSwagger中间件的作用是将生成的Swagger作为一个JSON端点提供服务,而UseSwaggerUI中间件的作用是服务Swagger-ui(HTML、JS、CSS等)。

注意UseSwaggerUI方法调用启用了静态文件中间件。如果以.NET Framework或.NET Core 1.x为目标,请将Microsoft.AspNetCore.StaticFiles NuGet包添加到项目。

如果要让这个机制生效,模板自带的UseEndpointsendpoints.MapControllers()也是必不可少的。

看下UseSwagger的定义

public static IApplicationBuilder UseSwagger(
    this IApplicationBuilder app,
    Action<SwaggerOptions> setupAction = null)
{
    SwaggerOptions options;
    using (var scope = app.ApplicationServices.CreateScope())
    {
        options = scope.ServiceProvider.GetRequiredService<IOptionsSnapshot<SwaggerOptions>>().Value;
        setupAction?.Invoke(options);
    }

    return app.UseSwagger(options);
}

看下UseSwaggerUI的定义

public static IApplicationBuilder UseSwaggerUI(
    this IApplicationBuilder app,
    Action<SwaggerUIOptions> setupAction = null)
{
    SwaggerUIOptions options;
    using (var scope = app.ApplicationServices.CreateScope())
    {
        options = scope.ServiceProvider.GetRequiredService<IOptionsSnapshot<SwaggerUIOptions>>().Value;
        setupAction?.Invoke(options);
    }

    // To simplify the common case, use a default that will work with the SwaggerMiddleware defaults
    if (options.ConfigObject.Urls == null)
    {
        var hostingEnv = app.ApplicationServices.GetRequiredService<IWebHostEnvironment>();
        options.ConfigObject.Urls = new[] { new UrlDescriptor { Name = $"{hostingEnv.ApplicationName} v1", Url = "v1/swagger.json" } };
    }

    return app.UseSwaggerUI(options);
}

看下SwaggerUIOptions的定义

public class SwaggerUIOptions
{
    /// <summary>
    /// Gets or sets a route prefix for accessing the swagger-ui
    /// </summary>
    public string RoutePrefix { get; set; } = "swagger";

    /// <summary>
    /// Gets or sets a Stream function for retrieving the swagger-ui page
    /// </summary>
    public Func<Stream> IndexStream { get; set; } = () => typeof(SwaggerUIOptions).GetTypeInfo().Assembly
        .GetManifestResourceStream("Swashbuckle.AspNetCore.SwaggerUI.index.html");

    /// <summary>
    /// Gets or sets a title for the swagger-ui page
    /// </summary>
    public string DocumentTitle { get; set; } = "Swagger UI";

    /// <summary>
    /// Gets or sets additional content to place in the head of the swagger-ui page
    /// </summary>
    public string HeadContent { get; set; } = "";

    /// <summary>
    /// Gets the JavaScript config object, represented as JSON, that will be passed to the SwaggerUI
    /// </summary>
    public ConfigObject ConfigObject  { get; set; } = new ConfigObject();

    /// <summary>
    /// Gets the JavaScript config object, represented as JSON, that will be passed to the initOAuth method
    /// </summary>
    public OAuthConfigObject OAuthConfigObject { get; set; } = new OAuthConfigObject();

    /// <summary>
    /// Gets the interceptor functions that define client-side request/response interceptors
    /// </summary>
    public InterceptorFunctions Interceptors { get; set; } = new InterceptorFunctions();
}

在根目录提供Swagger

如果想要直接在根目录提供Swagger UI,我们在UseSwaggerUI那里需要把RoutePrefix的值设置为空,因为它是有默认值swagger的。

app.UseSwaggerUI(swaggerUIOptions =>
{
    swaggerUIOptions.SwaggerEndpoint("/swagger/v1/swagger.json", "Tesla API v1");
    swaggerUIOptions.RoutePrefix = string.Empty;
});

image

看下SwaggerEndpoint的定义

public static void SwaggerEndpoint(this SwaggerUIOptions options, string url, string name)
{
    var urls = new List<UrlDescriptor>(options.ConfigObject.Urls ?? Enumerable.Empty<UrlDescriptor>());
    urls.Add(new UrlDescriptor { Url = url, Name = name });
    options.ConfigObject.Urls = urls;
}

注意事项

Swashbuckle依赖于MVC的Microsoft.AspNetCore.Mvc.ApiExplorer来发现路由和终结点。如果项目调用AddMvc,则自动发现路由和终结点。调用AddMvcCore时,必须显式调用AddApiExplorer方法。

降低到2.0规范

Swashbuckle默认会启动3.0的OpenAPI规范,如果因为对一些应用的兼容需要,比如Microsoft Power Apps和Microsoft Flow,可通过配置降级为2.0的标准。

app.UseSwagger(swaggerOptions =>
{
    swaggerOptions.SerializeAsV2 = true;
});

访问Swagger

这时候,其实我们可以修改launchSettings.json中当前启动配置文件中的launchUrl的值为swagger

{
    "demoForRouting31": {
        "commandName": "Project",
        "launchBrowser": true,
        "launchUrl": "swagger",
        "applicationUrl": "https://localhost:5001;http://localhost:5000",
        "environmentVariables": {
            "ASPNETCORE_ENVIRONMENT": "Development"
        }
    }
}

这时候默认启动就会打开swagger的地址:https://localhost:5001/swagger/index.html

image

同时需要注意的是,还有个前面定义的文档地址:https://localhost:5001/swagger/v1/swagger.json ,它是符合OpenAPI 规范的。

image

添加API信息和说明

Startup.csAddSwaggerGen服务注册时,我们可以在入参委托中基于Microsoft.OpenApi.Models.OpenApiInfo类定义一些API的信息和说明

services.AddSwaggerGen(swaggerGenOptions =>
{
    swaggerGenOptions.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo 
    { 
        Version = "v1",
        Title = "Tesla API",
        Description = "这是一个示例演示",
        TermsOfService = new Uri("https://example.com/terms"),
        Contact = new OpenApiContact
        {
            Name = "Taylor Shi",
            Email = string.Empty,
            Url = new Uri("https://www.cnblogs.com/taylorshi"),
        },
        License = new OpenApiLicense
        {
            Name = "Use under LICX",
            Url = new Uri("https://example.com/license"),
        }
    });

    var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
    swaggerGenOptions.IncludeXmlComments(xmlPath);
});

运行效果

image

基于RouteAttribute方法来标注路由

新建一个OrderController控制器。

/// <summary>
/// 订单控制器
/// </summary>
[Route("api/[controller]/[action]")]
[ApiController]
public class OrderController : ControllerBase
{
    /// <summary>
    /// 订单是否存在
    /// </summary>
    /// <param name="id">必须可以转为Long</param>
    /// <returns></returns>
    [HttpGet("{id:TeslaRouteConsraint}")]
    public bool OrderExist(object id)
    {
        return true;
    }

    /// <summary>
    /// 订单最大值
    /// </summary>
    /// <param name="id">最大值20</param>
    /// <param name="linkGenerator"></param>
    /// <returns></returns>
    [HttpGet("{id:max(20)}")]
    public bool OrderMax(long id, [FromServices] LinkGenerator linkGenerator)
    {
        var a = linkGenerator.GetPathByAction("Reque", "Order");
        return true;
    }

    /// <summary>
    /// 订单请求
    /// </summary>
    /// <returns></returns>
    [HttpGet("{name:required}")]
    public bool OrderRequest(string name)
    {
        return true;
    }

    /// <summary>
    /// 订单编号
    /// </summary>
    /// <param name="number">必须是三个数字</param>
    /// <returns></returns>
    [HttpGet("{number:regex(^\\d{{3}}$)}")]
    public bool OrderNumber(string number)
    {
        return true;
    }
}

OrderController上方标注了ApiControllerRoute,同时为了让它可以生效,我们需要在Startup.csConfigure方法的UseEndpoints中间件这里调用MapControllers

app.UseEndpoints(endpoints =>
{
    // 启用RouteAttribute
    endpoints.MapControllers();
});

运行效果

image

针对OrderExist这个我们使用了自定义的参数

/// <summary>
/// 订单是否存在
/// </summary>
/// <param name="id">必须可以转为Long</param>
/// <returns></returns>
[HttpGet("{id:TeslaRouteConsraint}")]
public bool OrderExist(object id)
{
    return true;
}

针对OrderMax使用了max约束,限制其id的值最大不能超过20

/// <summary>
/// 订单最大值
/// </summary>
/// <param name="id">最大值20</param>
/// <param name="linkGenerator"></param>
/// <returns></returns>
[HttpGet("{id:max(20)}")]
public bool OrderMax(long id, [FromServices]LinkGenerator linkGenerator)
{
    var a = linkGenerator.GetPathByAction("Reque", "Order");
    return true;
}

针对OrderRequest这里我们要求name是必填

/// <summary>
/// 订单请求
/// </summary>
/// <returns></returns>
[HttpGet("{name:required}")]
public bool OrderRequest(string name)
{
    return true;
}

针对OrderNumber这里我们通过正则表达式约束要求number必须是三个数字

/// <summary>
/// 订单编号
/// </summary>
/// <param name="number">必须是三个数字</param>
/// <returns></returns>
[HttpGet("{number:regex(^\\d{{3}}$)}")]
public bool OrderNumber(string number)
{
    return true;
}

image

当约束条件不符合的时候,得到的HTTP响应是404

image

通过数据标记来驱动Swagger

当我们定义一个数据模型的时候,我们可以基于System.ComponentModel.DataAnnotations下的可用属性来标记模型

/// <summary>
/// 订单项
/// </summary>
public class OrderItem
{
    /// <summary>
    /// 订单ID
    /// </summary>
    public long Id { get; set; }

    /// <summary>
    /// 订单名称
    /// </summary>
    [Required]
    public string Name { get; set; }

    /// <summary>
    /// 是否完结
    /// </summary>
    [DefaultValue(false)]
    public bool IsComplete { get; set; }
}

当我们使用的时候

/// <summary>
/// 获取订单项
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet("{id:IsLong}")]
public OrderItem GetOrderItem(long id)
{
    return new OrderItem { Id = id };
}

会看到针对模型的一些Swagger提示

image

回顾下System.ComponentModel.DataAnnotations有哪些可用的清单

数据标记 含义
AssociatedMetadataTypeTypeDescriptionProvider 通过添加在关联类中定义的特性和属性信息,从而扩展某个类的元数据信息
AssociationAttribute 指定实体成员表示数据关系(如外键关系)
CompareAttribute 提供用于比较两个属性的特性
ConcurrencyCheckAttribute 指定属性参与乐观并发检查
CreditCardAttribute 指定数据字段值是信用卡号
CustomValidationAttribute 指定用于验证属性或类实例的自定义验证方法
DataTypeAttribute 指定要与数据字段关联的其他类型的名称
DisplayAttribute 提供允许为实体分部类的类型和成员指定可本地化字符串的通用特性
DisplayColumnAttribute 指定作为外键列显示在被引用表中的列
DisplayFormatAttribute 指定 ASP.NET 动态数据如何显示数据字段以及如何设置数据字段的格式
EditableAttribute 指示数据字段是否可编辑
EmailAddressAttribute 验证电子邮件地址
EnumDataTypeAttribute 启用.NET 枚举,以映射到数据列
FileExtensionsAttribute 验证文件扩展名
FilterUIHintAttribute 表示用于指定列的筛选行为的特性
KeyAttribute 表示唯一标识实体的一个或多个属性
MaxLengthAttribute 指定属性中允许的数组或字符串数据的最大长度
MetadataTypeAttribute 指定要与数据模型类关联的元数据类
MinLengthAttribute 指定属性中允许的数组或字符串数据的最小长度
PhoneAttribute 指定数据字段值是格式标准的电话号码
RangeAttribute 为数据字段的值指定数值范围约束
RegularExpressionAttribute 指定ASP.NET动态数据中的数据字段值必须与指定的正则表达式匹配
RequiredAttribute 指定数据字段值是必需的
ScaffoldColumnAttribute 指定类或数据列是否使用基架
StringLengthAttribute 指定数据字段中允许的字符的最小长度和最大长度
TimestampAttribute 列的数据类型指定为行版本
UIHintAttribute 指定动态数据用来显示数据字段的模板或用户控件
UrlAttribute 提供URL验证
ValidationAttribute 充当所有验证特性的基类
ValidationContext 描述执行验证检查的上下文
ValidationException 表示在使用ValidationAttribute类的情况下验证数据字段时发生的异常
ValidationResult 表示验证请求的结果的容器
Validator 定义一个帮助器类,在与对象、属性和方法关联的ValidationAttribute特性中包含此类时,可使用此类来验证这些项

申明控制器的响应类型

通过Produces标记可以作用于Controller来申明控制器的响应类型

/// <summary>
/// 订单控制器
/// </summary>
[Produces("application/json")]
[Route("api/[controller]/[action]")]
[ApiController]
public class OrderController : ControllerBase
{

实际效果

image

描述响应代码

使用ProducesResponseType可以给Action标记响应代码,同时通过XML描述结构中的<response code="201">可以添加针对不同响应代码的描述。

/// <summary>
/// 订单是否存在
/// </summary>
/// <remarks>
/// 请求示例:
///
///     GET /api/order/OrderExist/123
///     
/// </remarks>
/// <param name="id">必须可以转为Long</param>
/// <returns></returns>
/// <response code="201">Returns the newly created item</response>
/// <response code="400">If the item is null</response>
[HttpGet("{id:IsLong}")]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public bool OrderExist([FromRoute]object id)
{
    return true;
}

效果如下

image

定制Swagger的界面样式

通过启用静态文件中间件和wwwroot文件夹,我们可以把我们的css样式文件追加进来。

public void Configure(IApplicationBuilder app)
{
    app.UseStaticFiles();
app.UseSwaggerUI(c =>
{
     c.InjectStylesheet("/swagger-ui/custom.css");
}

自定义约束(RouteConsraint)

继承自IRouteConstraint来自定义我们的路由约束类TeslaRouteConsraint

/// <summary>
/// 自定义路由约束
/// </summary>
public class TeslaRouteConsraint : IRouteConstraint
{
    /// <summary>
    /// 是否匹配
    /// </summary>
    /// <param name="httpContext"></param>
    /// <param name="route"></param>
    /// <param name="routeKey"></param>
    /// <param name="values"></param>
    /// <param name="routeDirection"></param>
    /// <returns></returns>
    public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
    {
        if(RouteDirection.IncomingRequest == routeDirection)
        {
            var value = values[routeKey];
            if(long.TryParse(value.ToString(), out var longValue))
            {
                return true;
            }
        }
        return false;
    }
}

继承IRouteConstraint接口,它只有一个Match方法,它有几个入参:

  • httpContext,HTTP请求的上下文
  • routeKey,路由参数
  • values,路由参数值的字典,通过它可以获取到对应路由参数的值
  • routeDirection,路由场景,区分验证的使用场景

这里的逻辑是,在路由场景为进入请求的场景时,拿到当前输入参数的值,判断是否可以转成Long。

这里来看下RouteDirection的定义

public enum RouteDirection
{
    //
    // 摘要:
    //     A URL from a client is being processed.
    IncomingRequest = 0,
    //
    // 摘要:
    //     A URL is being created based on the route definition.
    UrlGeneration = 1
}

其中IncomingRequest代表当前验证时用来验证路由是否匹配、UrlGeneration代表验证URL生成。

定义好路由约束类TeslaRouteConsraint之后,我们还需要去Startip.csConfigureServices中添加AddRouting服务,并且将自定义的路由约束添加进来。

services.AddRouting(routeOptions =>
{
    routeOptions.ConstraintMap.Add("TeslaRouteConsraint", typeof(TeslaRouteConsraint));
});

这时候再回到OrderController稍加改造,在路由参数前面增加[FromRoute]标记,然后id后面对应的约束填写前面定义的名称TeslaRouteConsraint

/// <summary>
/// 订单是否存在
/// </summary>
/// <param name="id">必须可以转为Long</param>
/// <returns></returns>
[HttpGet("{id:TeslaRouteConsraint}")]
public bool OrderExist([FromRoute]object id)
{
    return true;
}

当然,根据实际作用,我们可以给自定义约束取更符合其功能的名称。

services.AddRouting(routeOptions =>
{
    routeOptions.ConstraintMap.Add("IsLong", typeof(TeslaRouteConsraint));
});
/// <summary>
/// 订单是否存在
/// </summary>
/// <param name="id">必须可以转为Long</param>
/// <returns></returns>
[HttpGet("{id:IsLong}")]
public bool OrderExist([FromRoute]object id)
{
    return true;
}

效果也是一样的。

image

链接生成(LinkGenerator)

通过容器可以获得LinkGenerator对象,使用GetPathByAction方法可获取指定动作的请求路径,使用GetUriByAction方法可获取指定动作的完整请求地址。

/// <summary>
/// 订单最大值
/// </summary>
/// <param name="id">最大值20</param>
/// <param name="linkGenerator"></param>
/// <returns></returns>
[HttpGet("{id:max(20)}")]
public bool OrderMax(long id, [FromServices]LinkGenerator linkGenerator)
{
    // 获取请求路径
    var actionPath = linkGenerator.GetPathByAction(HttpContext,
        action: "OrderRequest",
        controller: "Order",
        values: new { name = "abc" });

    Console.WriteLine($"ActionPath: {actionPath}");

    // 获取完整URL
    var actionUri = linkGenerator.GetUriByAction(HttpContext,
        action: "OrderRequest",
        controller: "Order",
        values: new { name = "abc" });

    Console.WriteLine($"ActionUrl: {actionUri}");

    return true;
}

/// <summary>
/// 订单请求
/// </summary>
/// <returns></returns>
[HttpGet("{name:required}")]
public bool OrderRequest(string name)
{
    return true;
}

运行下看下结果

ActionPath: /api/Order/OrderRequest/abc
ActionUrl: https://localhost:5001/api/Order/OrderRequest/abc

查看下GetPathByAction的定义,可以看到有其他重载可以使用

public static class ControllerLinkGeneratorExtensions
{
    public static string GetPathByAction(this LinkGenerator generator, HttpContext httpContext, string action = null, string controller = null, object values = null, PathString? pathBase = null, FragmentString fragment = default, LinkOptions options = null);

    public static string GetPathByAction(this LinkGenerator generator, string action, string controller, object values = null, PathString pathBase = default, FragmentString fragment = default, LinkOptions options = null);

    public static string GetUriByAction(this LinkGenerator generator, HttpContext httpContext, string action = null, string controller = null, object values = null, string scheme = null, HostString? host = null, PathString? pathBase = null, FragmentString fragment = default, LinkOptions options = null);

    public static string GetUriByAction(this LinkGenerator generator, string action, string controller, object values, string scheme, HostString host, PathString pathBase = default, FragmentString fragment = default, LinkOptions options = null);
}

查看下GetUriByAction的定义,可以看到有其他重载可以使用

public static class ControllerLinkGeneratorExtensions
{
    public static string GetPathByAction(this LinkGenerator generator, HttpContext httpContext, string action = null, string controller = null, object values = null, PathString? pathBase = null, FragmentString fragment = default, LinkOptions options = null);

    public static string GetPathByAction(this LinkGenerator generator, string action, string controller, object values = null, PathString pathBase = default, FragmentString fragment = default, LinkOptions options = null);

    public static string GetUriByAction(this LinkGenerator generator, HttpContext httpContext, string action = null, string controller = null, object values = null, string scheme = null, HostString? host = null, PathString? pathBase = null, FragmentString fragment = default, LinkOptions options = null);

    public static string GetUriByAction(this LinkGenerator generator, string action, string controller, object values, string scheme, HostString host, PathString pathBase = default, FragmentString fragment = default, LinkOptions options = null);
}

标记接口为废弃状态

通过[Obsolete]标记,我们可以将一个接口标记为已废弃状态,但是它还可以正常工作。

/// <summary>
/// 订单请求
/// </summary>
/// <returns></returns>
[HttpGet("{name:required}")]
[Obsolete]
public bool OrderRequest(string name)
{
    return true;
}

image

Web API定义

  • Restful是可选的
  • 约定好API的表达契约
  • 将API约束在特定目录下,如/api/
  • 使用ObsoleteAttribute可标记即将废弃的API

废弃API的时候应该是采用间隔版本的方式废弃,先将即将废弃的API标记为已废弃,但是它还是可以工作,间隔几个版本以后我们再去将代码删除掉。

基于Swagger进行API版本控制

我们只需要把版本那里进行动态化配置改造即可,然后在Controller的Action通过ApiExplorerSettings完成分组设置。

自定义版本控制枚举ApiVersion

/// <summary>
/// API版本
/// </summary>
public enum ApiVersion
{
    /// <summary>
    /// v1版本
    /// </summary>
    V1 = 1,
    /// <summary>
    /// v2版本
    /// </summary>
    V2 = 2,
    /// <summary>
    /// v3版本
    /// </summary>
    V3 = 3,
}

改造服务注入,遍历ApiVersion来添加Swagger Doc信息

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddSwaggerGen(swaggerGenOptions =>
    {
        typeof(ApiVersion).GetEnumNames().ToList().ForEach(version =>
        {
            swaggerGenOptions.SwaggerDoc(version, new Microsoft.OpenApi.Models.OpenApiInfo
            { 
                Title = "Tesla Open API",
                Version = version,
                Description = $"Tesla Open API {version} Powered by ASP.NET Core"
            });
        });

        var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
        var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
        swaggerGenOptions.IncludeXmlComments(xmlPath);
    });
}

改造中间件注册,遍历ApiVersion来生成Swagger Endpoint的API文档

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseSwagger();

    app.UseSwaggerUI(swaggerUIOptions =>
    {
        typeof(ApiVersion).GetEnumNames().ToList().ForEach(version =>
        {
            swaggerUIOptions.SwaggerEndpoint($"/swagger/{version}/swagger.json", version);
        });
    });

改造Controller,添加分组标记实现API分组

/// <summary>
/// 订单控制器
/// </summary>
[Produces("application/json")]
[Route("api/[controller]/[action]")]
[ApiController]
public class OrderController : ControllerBase
{
    /// <summary>
    /// 订单是否存在
    /// </summary>
    /// <remarks>
    /// 请求示例:
    ///
    ///     GET /api/order/OrderExist/123
    ///     
    /// </remarks>
    /// <param name="id">必须可以转为Long</param>
    /// <returns></returns>
    /// <response code="201">Returns the newly created item</response>
    /// <response code="400">If the item is null</response>
    [HttpGet("{id:IsLong}")]
    [ProducesResponseType(StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ApiExplorerSettings(GroupName = nameof(ApiVersion.V1))]
    public bool OrderExist([FromRoute]object id)
    {
        return true;
    }

    /// <summary>
    /// 订单最大值
    /// </summary>
    /// <param name="id">最大值20</param>
    /// <param name="linkGenerator"></param>
    /// <returns></returns>
    [HttpGet("{id:max(20)}")]
    [ApiExplorerSettings(GroupName = nameof(ApiVersion.V2))]
    public bool OrderMax(long id, [FromServices]LinkGenerator linkGenerator)
    {
        // 获取请求路径
        var actionPath = linkGenerator.GetPathByAction(HttpContext,
            action: "OrderRequest",
            controller: "Order",
            values: new { name = "abc" });

        Console.WriteLine($"ActionPath: {actionPath}");

        // 获取完整URL
        var actionUri = linkGenerator.GetUriByAction(HttpContext,
            action: "OrderRequest",
            controller: "Order",
            values: new { name = "abc" });

        Console.WriteLine($"ActionUrl: {actionUri}");

        return true;
    }

    /// <summary>
    /// 订单请求
    /// </summary>
    /// <returns></returns>
    [HttpGet("{name:required}")]
    [Obsolete]
    [ApiExplorerSettings(GroupName = nameof(ApiVersion.V3))]
    public bool OrderRequest(string name)
    {
        return true;
    }

    /// <summary>
    /// 订单编号
    /// </summary>
    /// <param name="number">必须是三个数字</param>
    /// <returns></returns>
    [HttpGet("{number:regex(^\\d{{3}}$)}")]
    [ApiExplorerSettings(GroupName = nameof(ApiVersion.V1))]
    public bool OrderNumber(string number)
    {
        return true;
    }

    /// <summary>
    /// 获取订单项
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    [HttpGet("{id:IsLong}")]
    [ApiExplorerSettings(GroupName = nameof(ApiVersion.V3))]
    public OrderItem GetOrderItem(long id)
    {
        return new OrderItem { Id = id };
    }
}

查看效果

image

image

使用ApiVersion进行API版本控制

依赖包

https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer

https://www.nuget.org/packages/Swashbuckle.AspNetCore

dotnet add package Swashbuckle.AspNetCore
dotnet add package Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer

自定义一个扩展方法RoutingEndpointExtensions

/// <summary>
/// 路由和终结点扩展
/// </summary>
public static class RoutingEndpointExtensions
{
    /// <summary>
    /// 添加和配置API版本相关服务
    /// </summary>
    /// <param name="services"></param>
    /// <returns></returns>
    public static IServiceCollection AddApiVersions(this IServiceCollection services)
    {
        // 添加API版本控制
        services.AddApiVersioning(apiVersioningOptions =>
        {
            // 返回响应标头中支持的版本信息
            apiVersioningOptions.ReportApiVersions = true;
            // 此选项将用于不提供版本的请求,指向默认版本
            apiVersioningOptions.AssumeDefaultVersionWhenUnspecified = true;
            // 默认版本号,支持时间或数字版本号
            apiVersioningOptions.DefaultApiVersion = new ApiVersion(1, 0);
        });

        // 添加版本管理服务
        services.AddVersionedApiExplorer(apiExplorerOptions =>
        {
            // 设置API组名格式
            apiExplorerOptions.GroupNameFormat = "'v'VVV";
            // 在URL中替换版本
            apiExplorerOptions.SubstituteApiVersionInUrl = true;
            // 当未设置版本时指向默认版本
            apiExplorerOptions.AssumeDefaultVersionWhenUnspecified = true;
        });

        return services;
    }

    /// <summary>
    /// 添加和配置Swagger相关服务
    /// </summary>
    /// <param name="services"></param>
    /// <returns></returns>
    public static IServiceCollection AddSwaggers(this IServiceCollection services)
    {
        // AddApiVersions必须在AddSwaggers之前调用
        var apiVersionDescriptionProvider = services.BuildServiceProvider().GetRequiredService<IApiVersionDescriptionProvider>();

        // 添加SwaggerGen服务
        services.AddSwaggerGen(swaggerGenOptions =>
        {
            // 根据请求方式排序
            swaggerGenOptions.OrderActionsBy(o => o.HttpMethod);

            // 遍历已知的API版本
            apiVersionDescriptionProvider.ApiVersionDescriptions.ToList().ForEach(versionDescription =>
            {
                var group = versionDescription.GroupName.ToString();
                swaggerGenOptions.SwaggerDoc(group, new Microsoft.OpenApi.Models.OpenApiInfo
                {
                    Title = "Tesla Open API",
                    Version = group,
                    Description = $"Tesla Open API {group} Powered by ASP.NET Core"
                });
            });

            // 重载方式
            swaggerGenOptions.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());

            // 遍历已存在的XML
            foreach (var name in Directory.GetFiles(AppContext.BaseDirectory, "*.*",
                SearchOption.AllDirectories).Where(f => Path.GetExtension(f).ToLower() == ".xml"))
            {
                swaggerGenOptions.IncludeXmlComments(name, includeControllerXmlComments: true);
            }
        });

        return services;
    }

    /// <summary>
    /// 使用和配置Swagger、ApiVersion相关中间件
    /// </summary>
    /// <param name="app"></param>
    /// <param name="provider"></param>
    /// <returns></returns>
    public static IApplicationBuilder UseSwaggerAndApiVersions(this IApplicationBuilder app, IApiVersionDescriptionProvider provider)
    {
        app.UseApiVersioning();

        app.UseSwagger();

        app.UseSwaggerUI(swaggerUIOptions =>
        {
            provider.ApiVersionDescriptions.ToList().ForEach(versionDescription =>
            {
                var group = versionDescription.GroupName.ToString();
                swaggerUIOptions.SwaggerEndpoint($"/swagger/{group}/swagger.json", group);
            });
        });

        return app;
    }
}

Startup.csConfigure添加扩展方法

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddApiVersions();
    services.AddSwaggers();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseSwaggerAndApiVersions(provider);

对Controller的分组

image

改造方案

[ApiVersion("1.0", Deprecated = true)]
[Route("api/v{version:ApiVersion}/[controller]/[action]")]
[ApiController]
public class OrderController : ControllerBase
{
[ApiVersion("2.0")]
[Route("api/v{version:ApiVersion}/[controller]/[action]")]
[ApiController]
public class OrderController : ControllerBase
{

效果

image

image

参考

posted @ 2022-10-13 23:56  TaylorShi  阅读(231)  评论(0编辑  收藏  举报