什么是路由

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

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

  • 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

    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实现

  • Swashbuckle
  • NSwag
  • 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应用中。

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

    Swashbuckle三大组件

    Swashbuckle包括 Swashbuckle.AspNetCore.Swagger Swashbuckle.AspNetCore.SwaggerGen Swashbuckle.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
    

    设置项目生成XML注释文件

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

    添加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中展示出来。

    这里甚至我们还可以通过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;
    

    我们来看下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;
    

    看下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

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

    添加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);
    

    基于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();
    

    针对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;
    

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

    通过数据标记来驱动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提示

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

    [Route("api/[controller]/[action]")] [ApiController] public class OrderController : ControllerBase

    描述响应代码

    使用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;
    

    定制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;
    

    效果也是一样的。

    链接生成(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;
    

    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 };
    

    使用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的分组

    [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
    
  • ASP.NET Core中的路由
  • Asp.Net Core EndPoint 终结点路由工作原理解读
  • ASP.NET Core路由中间件[1]: 终结点与URL的映射
  • Asp.Net Core EndPoint 终结点路由工作原理解读
  • Endpoint Routing in ASP.NET Core 2.2 Explained
  • domaindrivendev/Swashbuckle.AspNetCore
  • 带有Swagger/OpenAPI的ASP.NET Core Web API文档
  • OpenAPITools/openapi-generator
  • Swashbuckle和ASP.NET Core入门
  • NSwag和ASP.NET Core入门
  • .NET OpenAPI工具命令参考和安装
  • Swashbuckle, ApiExplorer, and Routing
  • System.ComponentModel.DataAnnotations命名空间
  • 在ASP.Net Core Web API中使用Swagger进行版本控制
  • .Net Core WebApi —— Swagger版本控制
  • 利用swagger和API Version实现api版本控制
  •