<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          ASP.NET Core Web API使用小技巧

          共 13000字,需瀏覽 26分鐘

           ·

          2021-03-30 21:32



          轉(zhuǎn)自:墨墨墨墨小宇
          cnblogs.com/danvic712/p/11255423.html

          一、前言


          在目前的軟件開發(fā)的潮流中,不管是前后端分離還是服務(wù)化改造,后端更多的是通過構(gòu)建 API 接口服務(wù)從而為 web、app、desktop 等各種客戶端提供業(yè)務(wù)支持,如何構(gòu)建一個符合規(guī)范、容易理解的 API 接口是我們后端開發(fā)人員需要考慮的。


          在本篇文章中,我將列舉一些我在使用 ASP.NET Core Web API 構(gòu)建接口服務(wù)時使用到的一些小技巧,因才疏學(xué)淺,可能會存在不對的地方,歡迎指出。


          代碼倉儲:https://github.com/Lanesra712/ingos-server


          二、Step by Step


          因為本篇文章中涉及到的一些知識點在之前的文章中也已經(jīng)有具體的解釋了,所以這里只會說明如何在 ASP.NET Core Web API 中如何去使用,不會做過多的詳細(xì)介紹。


          本篇文章中使用的代碼是基于 .NET Core 2.2 + .NET Standard 2.0 進行構(gòu)建的,如果你采用的版本與我使用的不同,可能最終實現(xiàn)起來的代碼會有所不同,請?zhí)崆爸ぁ?/span>


          同時,本篇文章中所有示例代碼都會存在于前言中所列出的 github repo 中,我會嘗試將每個功能點的開發(fā)作為一次 commit,并且也會在后續(xù)進行不定期的更新完善,最終搭建一個基于領(lǐng)域驅(qū)動思想的后端項目模板,如果對你有幫助的話,歡迎持續(xù)關(guān)注。


          1、使用小寫路由

           

          在我之前的一篇文章中《構(gòu)建可讀性更高的 ASP.NET Core 路由》有提到過,因為 .NET 默認(rèn)采用 Pascal 的類命名方式,如果采用默認(rèn)生成的路由,最終構(gòu)建出的路由地址會存在大小寫混在一起的情況,雖然在 .NET Core 中大小寫的路由地址最終都會對于到正確的資源上,但是為了更好的符合前端的規(guī)范,所以這里我們首先按照之前的文章中所列出的方法去修改默認(rèn)生成的路由地址格式。


          因為這里我們最終想要實現(xiàn)的是符合 Restful 風(fēng)格的 API 接口,所以這里我們首先需要將默認(rèn)生成的 URL 地址改為全小寫模式。


          public void ConfigureServices(IServiceCollection services)
          {
          // 采用小寫的 URL 路由模式
          services.AddRouting(options =>
          {
          options.LowercaseUrls = true;
          });
          }



          如果你有看過構(gòu)建可讀性更高的 ASP.NET Core 路由這篇文章,你會發(fā)現(xiàn)其實我們最終實現(xiàn)的是 hyphen(-) 格式的 Url 地址,那么這里我們?yōu)槭裁床贿M行后續(xù)的修改了呢?


          如果你有查看 .NET Core 默認(rèn)模板中生成的 API Controller,仔細(xì)看下,這里其實是使用的特性路由,所以這里我們并不能通過 Startup.UseMvc 定義的傳統(tǒng)路由模板,或是直接在 Startup.Configure 中的 UseMvcWithDefaultRoute 方法去修改我們的生成的路由地址格式。


          [Route("api/[controller]")]
          [ApiController]
          public class ValuesController : ControllerBase
          {
          }


          2、允許跨域請求


          不管是后端接口的服務(wù)化改造,還是只是單純的前后端分離項目開發(fā),我們的前端項目與后端接口通常不會部署在一起,所以我們需要解決前端訪問接口時會涉及到的跨域訪問的問題。


          針對跨域請求,我們可以采用 jsonp、或者是通過給 nginx 服務(wù)器配置響應(yīng)的 header 參數(shù)頭信息、或者是使用 CORS,又或是其它的解決方案。你可以自由選擇,這里我采用在后端接口中直接配置對于 CORS 的支持。


          在 .NET Core 中,已經(jīng)在 Microsoft.AspNetCore.Cors 這個類庫中添加了對于 CORS 的支持,因為這個類庫是存在于我們已經(jīng)安裝的 .NET Core SDK 中,所以這里我們并不需要通過 Nuget 進行安裝,可以直接使用。


          在 .NET Core 中配置 CORS 規(guī)則,我們可以通過在 Startup.ConfigureServices 這個方法中添加不同的授權(quán)策略,之后再針對某個 Controller 或是 Action 通過添加 EnableCors 這個 Attribute 的方式進行配置,這里如果指定了 policy 策略名稱,則會使用指定的策略,如果沒有指定,則適用于系統(tǒng)的默認(rèn)配置。同樣的,我們也可以只設(shè)置一個策略,直接針對整個項目進行配置,這里我采用對整個項目采用通用的跨域請求配置方案。


          在配置 CORS 策略時,我們可以設(shè)置只允許來源于某些 URL 地址的請求可以訪問,或者是指定接口只允許某些 HTTP 方法進行訪問,或者是在請求的 header 中必須包含某些信息才可以訪問我們的接口。


          在下面的代碼中,我定義了針對整個項目的跨域請求策略,這里我只是設(shè)置了對于接口請求方 URL 地址的控制,通過讀取配置文件中的數(shù)據(jù),從而達(dá)到只允許某些 IP 可以訪問的我們接口的目的。


          public class Startup
          {
          // 默認(rèn)的跨域請求策略名稱
          private const string _defaultCorsPolicyName = "Ingos.Api.Cors";

          // This method gets called by the runtime. Use this method to add services to the container.
          public void ConfigureServices(IServiceCollection services)
          {
          services.AddMvc(
          // 添加 CORS 授權(quán)過濾器
          options => options.Filters.Add(new CorsAuthorizationFilterFactory(_defaultCorsPolicyName)) ).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
          // 配置 CORS 授權(quán)策略
          services.AddCors(options => options.AddPolicy(_defaultCorsPolicyName,
          builder => builder.WithOrigins(
          Configuration["Application:CorsOrigins"]
          .Split(",", StringSplitOptions.RemoveEmptyEntries).ToArray()
          )
          .AllowAnyHeader()
          .AllowAnyMethod()
          .AllowCredentials()));
          }
          // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
          public void Configure(IApplicationBuilder app, IHostingEnvironment env)
          {
          // 允許跨域請求訪問
          app.UseCors(_defaultCorsPolicyName);
          }
          }


          例如在下面的設(shè)置中,我只允許這一個地址可以訪問我們的接口,如果需要指定多個的話,則可以通過英文的 , 進行分隔。


          "Application": {
          "CorsOrigins": "http://127.0.0.1:5050"
          }


          某些情況下,如果我們不想進行限制的話,只需要將值改為 * 即可。


          "Application": {
          "CorsOrigins": "*"
          }


          3、添加接口版本控制


          在一些涉及到接口功能升級的場景下,當(dāng)我們需要修改接口邏輯而舊版本的接口無法停用的情況時,為了減少對于原有接口的影響,我們可以采取為接口添加版本信息的形式,從而降低因采用不同版本而造成的影響。如果你想要詳細(xì)了解的話,可以查看這篇文章《ASP.NET Core 實戰(zhàn):構(gòu)建帶有版本控制的 API 接口》。


          在實現(xiàn)具有版本控制的接口前,首先我們需要通過 Nuget 添加下面的兩個 dll,因為我是在 Ingos.Api.Core 這個類庫中進行配置的,所以我安裝到了這個類庫下,你需要根據(jù)你自己的情況選擇最終是安裝到 Api 接口項目中還是在別的類庫下。


          Install-Package Microsoft.AspNetCore.Mvc.Versioning
          Install-Package Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer


          在安裝完成之后,我們就可以在 Startup.ConfigureServices 方法中,為項目中的接口配置版本信息,這里我采用的方案是將版本號添加到接口的 URL 地址中。


          因為對于所有中間件的配置都會在 Startup.ConfigureServices 方法中,為了保持該方法的純凈性,這里我寫了一個擴展方法用于配置我們的 api 的版本,之后直接調(diào)用即可。


          public static class ApiVersionExtension
          {
          /// <summary>
          /// 添加 API 版本控制擴展方法
          /// </summary>
          /// <param name="services">生命周期中注入的服務(wù)集合 <see cref="IServiceCollection"/></param>
          public static void AddApiVersion(this IServiceCollection services)
          {
          // 添加 API 版本支持
          services.AddApiVersioning(o =>
          {
          // 是否在響應(yīng)的 header 信息中返回 API 版本信息
          o.ReportApiVersions = true;

          // 默認(rèn)的 API 版本
          o.DefaultApiVersion = new ApiVersion(1, 0);

          // 未指定 API 版本時,設(shè)置 API 版本為默認(rèn)的版本
          o.AssumeDefaultVersionWhenUnspecified = true;
          });

          // 配置 API 版本信息
          services.AddVersionedApiExplorer(option =>
          {
          // api 版本分組名稱
          option.GroupNameFormat = " v VVVV";
          // 未指定 API 版本時,設(shè)置 API 版本為默認(rèn)的版本
          option.AssumeDefaultVersionWhenUnspecified = true;
          });
          }
          }


          擴展方法最終實現(xiàn)方式如上面的代碼所示,之后我們就可以直接在 ConfigureServices 方法中直接進行調(diào)用這個擴展方法就可以了。


          // This method gets called by the runtime. Use this method to add services to the container.
          public void ConfigureServices(IServiceCollection services)
          {
          // Config api version
          services.AddApiVersion();
          }


          現(xiàn)在我們刪除項目創(chuàng)建時默認(rèn)生成的 ValuesController,在 Controllers 目錄下建立一個 v1 文件夾,代表此文件夾下都是 v1 版本的控制器。添加一個 UsersController 用來獲取系統(tǒng)的用戶資源,現(xiàn)在項目的文件結(jié)構(gòu)如下圖所示。



          現(xiàn)在我們來改造我們的 UsersController,我們只需要在 Controller 或是 Action 上添加 ApiVersion 特性就可以指定當(dāng)前 Controller/Action 的版本信息。同時,因為我需要將 API 的版本信息添加到生成的 URL 地址中,所以這里我們需要修改特性路由的模板,將我們的版本以占位符的形式添加到生成的路由 URL 地址中,修改完成后的代碼及實現(xiàn)的效果如下所示。


          [ApiVersion("1.0")]
          [ApiController]
          [Route("api/v{version:apiVersion}/[controller]")]
          public class UsersController : ControllerBase
          {
          }



          4、添加對于 Swagger 接口文檔的支持


          在前后端分離開發(fā)的情況下,我們需要提供給前端開發(fā)人員一個接口文檔,從而讓前端開發(fā)人員知道以什么樣的 HTTP 方法或是傳遞什么樣的參數(shù)給后端接口,從而獲取到正確的數(shù)據(jù),而 Swagger 則提供了一種自動生成接口文檔的方式,同時也提供類似于 Postman 的功能,可以實現(xiàn)對于接口的實時調(diào)用測試。


          首先,我們需要通過 Nuget 添加 Swashbuckle.AspNetCore 這個 dll 文件,之后我們就可以在此基礎(chǔ)上實現(xiàn)對于 Swagger 的配置。


          Install-Package Swashbuckle.AspNetCore


          與上面配置 API 接口的版本信息相似,這里我依舊采用構(gòu)建擴展方法的方式來實現(xiàn)對于 Swagger 中間件的配置。具體的配置過程可以查看我之前寫的文章(ASP.NET Core 實戰(zhàn):構(gòu)建帶有版本控制的 API 接口),這里只列出最終配置完成的代碼。


          public static void AddSwagger(this IServiceCollection services)
          {
          // 配置 Swagger 文檔信息
          services.AddSwaggerGen(s =>
          {
          // 根據(jù) API 版本信息生成 API 文檔
          //
          var provider = services.BuildServiceProvider().GetRequiredService<IApiVersionDescriptionProvider>();
          foreach (var description in provider.ApiVersionDescriptions)
          {
          s.SwaggerDoc(description.GroupName, new Info
          {
          Contact = new Contact
          {
          Name = "Danvic Wang",
          Email = "[email protected]",
          Url = "https://yuiter.com"
          },
          Description = "Ingos.API 接口文檔",
          Title = "Ingos.API",
          Version = description.ApiVersion.ToString()
          });
          }
          // 在 Swagger 文檔顯示的 API 地址中將版本信息參數(shù)替換為實際的版本號
          s.DocInclusionPredicate((version, apiDescription) =>
          {
          if (!version.Equals(apiDescription.GroupName))
          return false;
          var values = apiDescription.RelativePath
          .Split( / )
          .Select(v => v.Replace("v{version}", apiDescription.GroupName)); apiDescription.RelativePath = string.Join("/", values);
          return true;
          });

          // 參數(shù)使用駝峰命名方式
          s.DescribeAllParametersInCamelCase();

          // 取消 API 文檔需要輸入版本信息
          s.OperationFilter<RemoveVersionFromParameter>();

          // 獲取接口文檔描述信息
          var basePath = Path.GetDirectoryName(AppContext.BaseDirectory);
          var apiPath = Path.Combine(basePath, "Ingos.Api.xml");
          s.IncludeXmlComments(apiPath, true);
          });
          }


          當(dāng)我們配置完成后就可以在 Startup 類中去啟用 Swagger 文檔。


          public void ConfigureServices(IServiceCollection services)
          {
          // 添加對于 swagger 文檔的支持
          services.AddSwagger();
          }
          public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApiVersionDescriptionProvider provider)
          {
          // 啟用 Swagger 文檔
          app.UseSwagger();
          app.UseSwaggerUI(s =>
          {
          // 默認(rèn)加載最新版本的 API 文檔
          foreach (var description in provider.ApiVersionDescriptions.Reverse())
          { s.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json",
          $"Sample API {description.GroupName.ToUpperInvariant()}");
          }
          });
          }



          因為我們在之前設(shè)置構(gòu)建的 API 路由時包含了版本信息,所以在最終生成的 Swagger 文檔中進行測試時,我們都需要在參數(shù)列表中添加 API 版本這個參數(shù)。這無疑是有些不方便,所以這里我們可以通過繼承 IOperationFilter 接口,控制在生成 API 文檔時移除 API 版本參數(shù),接口的實現(xiàn)方法如下所示。


          public class RemoveVersionFromParameter : IOperationFilter
          {
          public void Apply(Operation operation, OperationFilterContext context)
          {
          var versionParameter = operation.Parameters.Single(p => p.Name == "version");
          operation.Parameters.Remove(versionParameter);
          }
          }


          當(dāng)我們實現(xiàn)自定義的接口后就可以在之前針對 Swagger 的擴展方法中調(diào)用這個過濾方法,從而實現(xiàn)移除版本信息的目的,擴展方法中的添加位置如下所示。


          public static void AddSwagger(this IServiceCollection services)
          {
          // 配置 Swagger 文檔信息
          services.AddSwaggerGen(s =>
          {
          // 取消 API 文檔需要輸入版本信息
          s.OperationFilter<RemoveVersionFromParameter>();
          });
          }


          最終的實現(xiàn)效果如下圖所示,可以看到,參數(shù)列表中已經(jīng)沒有版本信息這個參數(shù),但是我們在進行接口測試時會自動幫我們添加上版本參數(shù)信息。



          這里需要注意,因為我們需要在最終生成的 Swagger 文檔中顯示出我們對于 Controller 或是 Action 添加的注釋信息,所以這里我們需要在 Web Api 項目的屬性選項中勾選上輸出 XML 文檔文件。同時如果你不想 VS 一直提示你有方法沒有添加參數(shù)信息,這里我們可以在取消顯示警告這里添加上 1591 這個參數(shù)。



          5、構(gòu)建符合 Restful 風(fēng)格的接口


          在沒有采用 Restful 風(fēng)格來構(gòu)建接口返回值時,我們可能會習(xí)慣于在接口返回的信息中添加一個接口是否請求成功的標(biāo)識,就像下面代碼中示例的這種返回形式。


          {
          sueecss: true
          msg: ,
          data: [{
          id: 20190720214402 ,
          name: zhangsan
          }]
          }


          但是,當(dāng)我們想要構(gòu)建符合 Restful 風(fēng)格的接口時,我們就不能再這樣進行設(shè)計了,我們應(yīng)該通過返回的 HTTP 響應(yīng)狀態(tài)碼來標(biāo)識這次訪問是否成功。一些比較常用的 HTTP 狀態(tài)碼如下表所示。



          我們知道 HTTP 共有四個謂詞方法,分別為 Get、Post、Put 和 Delete,在之前我們可能更多的是使用 Get 和 Post,對于 Put 和 Delete 方法可能并不會使用。


          同樣的,如果我們需要創(chuàng)建符合 Restful 風(fēng)格的接口,我們則需要根據(jù)這四個 HTTP 方法謂詞一些約定俗成的功能定義去定義對應(yīng)接口的 HTTP 方法。



          例如,對于一個獲取所有資源的方法,我們可能會定義接口的默認(rèn)返回 HTTP 狀態(tài)碼為 200 或是 400,當(dāng)狀態(tài)碼為 200 時,代表數(shù)據(jù)獲取成功,接口可以正常返回數(shù)據(jù),當(dāng)狀態(tài)碼為 400 時,則代表接口訪問出現(xiàn)問題,此時則返回錯誤信息對象。


          在 ASP.NET Core Web API 中,我們可以通過在Action上添加 ProducesResponseType 特性來定義接口的返回狀態(tài)碼。通過 F12 按鍵我們可以進入 ProducesResponseType 這個特性,可以看到這個特性存在兩個構(gòu)造方法,我們可以只定義接口返回 HTTP 狀態(tài)碼或者是在定義接口返回的狀態(tài)碼時同時返回的具體對象信息。


          上面給出的接口案例的示例代碼如下所示,從下圖中可以看到,Swagger 會自動根據(jù)我們的 ProducesResponseType 特性來列出我們接口可能返回的 HTTP 狀態(tài)碼和對象信息。這里因為是示例程序,UserListDto 并沒有定義具體的屬性信息,所以這里顯示的是一個不包含任何屬性的對象數(shù)組。


          /// <summary>
          /// 獲取全部的用戶信息
          /// </summary>
          /// <returns></returns>
          [HttpGet]
          [ProducesResponseType(typeof(IEnumerable<UserListDto>), StatusCodes.Status200OK)]
          [ProducesResponseType(StatusCodes.Status400BadRequest)]
          public IActionResult Get()
          {
          // 1、獲取資源數(shù)據(jù)
          // 2、判斷數(shù)據(jù)獲取是否成功
          if (true)
          return Ok(new List<UserListDto>());
          else
          return BadRequest(new
          {
          statusCode = StatusCodes.Status400BadRequest,
          description = "錯誤描述",
          msg = "錯誤信息"
          });
          }



          可能這里你可能會有疑問,當(dāng)接口返回的 HTTP 狀態(tài)碼為 400 時,返回的信息是什么鬼,與我們定義的錯誤信息對象字段不同啊?原來,在 ASP.NET Core 2.1 之后的版本中,對于 API 接口返回 400 的 HTPP 狀態(tài)碼會默認(rèn)返回 ProblemDetails 對象,因為這里我們并沒有將接口中的返回 BadRequest 中的錯誤信息對象作為 ProducesResponseType 特性的構(gòu)造函數(shù)的參數(shù),所以這里就采用了默認(rèn)的錯誤信息對象。


          當(dāng)然,當(dāng)接口的 HTTP 返回狀態(tài)碼為 400 時,最終還是會返回我們自定義的錯誤信息對象,所以這里為了不造成前后端對接上的歧義,我們最好將返回的對象信息也作為參數(shù)添加到 ProducesResponseType 特性中。



          同時,除了上面示例的接口中通過返回 OK 方法和 BadRequest 方法來表明接口的返回 HTTP 狀態(tài)碼,在 ASP.NET Core Web API 中還有下列繼承于 ObjectResult 的方法來表明接口返回的狀態(tài)碼,對應(yīng)信息如下。



          6、使用 Web API 分析器


          在上面的示例中,因為我們需要指定接口需要返回的 HTTP 狀態(tài)碼,所以我們需要提前添加好 ProducesResponseType 特性,在某些時候我們可能在代碼中添加了一種 HTTP 狀態(tài)碼的返回結(jié)果,可是卻忘了添加特性描述,那么有沒有一種便捷的方式提示我們呢?


          在 ASP.NET Core 2.2 及以后更新的 ASP.NET Core 版本中,我們可以通過 Nuget 去添加 Microsoft.AspNetCore.Mvc.Api.Analyze 這個包,從而實現(xiàn)對我們的 API 進行分析,首先我們需要將這個包添加到我們的 API 項目中。


          Install-Package Microsoft.AspNetCore.Mvc.Api.Analyzers


          例如在下面的接口代碼中,我們根據(jù)用戶的唯一標(biāo)識去尋找用戶數(shù)據(jù),當(dāng)獲取不到數(shù)據(jù)的時候,返回的 HTTP 狀態(tài)碼為 400,而我們只添加了 HTTP 狀態(tài)碼為 200 的特性說明。此時,分析器將 HTTP 404 狀態(tài)代碼的缺失特性說明做為一個警告,并提供了修復(fù)此問題的選項,我們進行修復(fù)后就可以自動添加特性。


          /// <summary>
          /// 獲取用戶詳細(xì)信息
          /// </summary>
          /// <param name="id">用戶唯一標(biāo)識</param>
          /// <returns></returns>
          [HttpGet("{id}")]
          [ProducesResponseType(typeof(UserEditDto), StatusCodes.Status200OK)]
          public IActionResult Get(string id)
          {
          // 1、根據(jù) Id 獲取用戶信息
          UserEditDto user = null;
          if (user == null)
          return NotFound();
          else
          return Ok(user);
          }



          但是,在自動完成文檔補全后其實還是需要我們進行一些操作的,例如,如果我們需要指定返回值的 Type 類型,還是需要我們自己手動添加到 ProducesResponseType 特性上的。


          在進行特性補齊的時候,分析器也幫我們填加了一個ProducesDefaultResponseType特性。


          通過在微軟的文檔中指向的 Swagger 文檔(Swagger Default Response)中可以了解到,如果我們接口不管是什么狀態(tài),最終返回的 response 響應(yīng)結(jié)構(gòu)都是相同的,我們就可以直接使用 ProducesDefaultResponseType 特性來指定 response 的響應(yīng)結(jié)構(gòu),而不需要每個 HTTP 狀態(tài)都添加一個特性。


          三、總結(jié)


          在本篇文章中,主要介紹了一些我在使用 ASP.NET Core Web API 的過程中使用到的一些小技巧,以及在以前踩過坑后的一些解決方案,如果對你能有一點的幫助的話,不勝榮幸。同時,如果你有更好的解決方案,或者是針對一些你之前踩過的 Web API 坑的解決方案,也歡迎你在評論區(qū)中提出

          回復(fù) 【關(guān)閉】學(xué)關(guān)
          回復(fù) 【實戰(zhàn)】獲取20套實戰(zhàn)源碼
          回復(fù) 【被刪】學(xué)
          回復(fù) 【訪客】學(xué)
          回復(fù) 【小程序】學(xué)獲取15套【入門+實戰(zhàn)+賺錢】小程序源碼
          回復(fù) 【python】學(xué)微獲取全套0基礎(chǔ)Python知識手冊
          回復(fù) 【2019】獲取2019 .NET 開發(fā)者峰會資料PPT
          回復(fù) 【加群】加入dotnet微信交流群

          臥槽!微信可以改彩色昵稱了?。?!


          臥槽,微信狀態(tài)被玩壞了,附大量微信狀態(tài)視屏素材!




          瀏覽 39
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久久三级久久三级久久三级 | 777777欧美 | 青青草在线观看视频 | 色婷婷狠| 4438全国最大成人网 |