.Net?Core?3.1?Web?API基礎(chǔ)知識(shí)詳解(收藏)
一、前言
隨著近幾年前后端分離、微服務(wù)等模式的興起,.Net Core也似有如火如荼之勢(shì) ,自16年發(fā)布第一個(gè)版本到19年底的3.1 LTS版本,以及將發(fā)布的.NET 5,.NET Core一路更迭,在部署和開發(fā)工具上也都支持了跨平臺(tái)應(yīng)用。一直對(duì).Net Core有所關(guān)注,但未涉及太多實(shí)際應(yīng)用,經(jīng)過一番學(xué)習(xí)和了解后,于是分享出來。本文主要以.Net Core Web API為例,講述.Net Core的基本應(yīng)用及注意事項(xiàng),對(duì)于想通過WebAPI搭建接口應(yīng)用的開發(fā)者,應(yīng)該能提供一個(gè)系統(tǒng)的輪廓和認(rèn)識(shí),同時(shí)和更多的.Net Core開發(fā)者交流互動(dòng),探本勘誤,加強(qiáng)對(duì)知識(shí)的理解,并幫助更多的人。本文以貼近基本的實(shí)際操作為主,部分概念或基礎(chǔ)步驟不再贅述,文中如有疏漏,還望不吝斧正。
二、Swagger調(diào)試Web API
開發(fā)環(huán)境:Visual Studio 2019
為解決前后端苦于接口文檔與實(shí)際不一致、維護(hù)和更新文檔的耗時(shí)費(fèi)力等問題,swagger應(yīng)運(yùn)而生,同時(shí)也解決了接口測(cè)試問題。話不多說,直接說明應(yīng)用步驟。
- 新建一個(gè)ASP.NET Core Web API應(yīng)用程序,版本選擇.ASP.NET Core 3.1;
- 通過Nuget安裝包:Swashbuckle.AspNetCore,當(dāng)前示例版本5.5.0;
- 在Startup類的ConfigureServices方法內(nèi)添加以下注入代碼:
services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1", Description = "API文檔描述", Contact = new OpenApiContact { Email = "5007032@qq.com", Name = "測(cè)試項(xiàng)目", //Url = new Uri("http://t.abc.com/") }, License = new OpenApiLicense { Name = "BROOKE許可證", //Url = new Uri("http://t.abc.com/") } }); });
Startup類的Configure方法添加如下代碼:
//配置Swagger app.UseSwagger(); app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); c.RoutePrefix = "api";// 如果設(shè)為空,訪問路徑就是根域名/index.html,設(shè)置為空,表示直接在根域名訪問;想換一個(gè)路徑,直接寫名字即可,比如直接寫c.RoutePrefix = "swagger"; 則訪問路徑為 根域名/swagger/index.html });
Ctrl+F5進(jìn)入瀏覽,按上述配置修改路徑為:http://localhost:***/api/index.html,即可看到Swagger頁面:
然而到這里還沒完,相關(guān)接口的注釋說明我們看不到,通過配置XML文件的方式繼續(xù)調(diào)整代碼如下,新增代碼見加粗部分:
services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1", Description = "API文檔描述", Contact = new OpenApiContact { Email = "5007032@qq.com", Name = "測(cè)試項(xiàng)目", //Url = new Uri("http://t.abc.com/") }, License = new OpenApiLicense { Name = "BROOKE許可證", //Url = new Uri("http://t.abc.com/") } }); var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); c.IncludeXmlComments(xmlPath); });
上述代碼通過反射生成與Web API項(xiàng)目相匹配的XML文件名,AppContext.BaseDirectory屬性用于構(gòu)造 XML 文件的路徑,關(guān)于OpenApiInfo內(nèi)的配置參數(shù)用于文檔的一些描述,在此不作過多說明。
然后右鍵Web API項(xiàng)目、屬性、生成,配置XML文檔的輸出路徑,以及取消不必要的XML注釋警告提醒(增加1591):
這樣,我們以三斜杠(///)方式給類方法屬性等相關(guān)代碼添加注釋后,刷新Swagger頁面,即可看到注釋說明。
如果不想將XML文件輸出為debug下的目錄,譬如想要放在項(xiàng)目根目錄(但不要修改成磁盤絕對(duì)路徑),可調(diào)整相關(guān)代碼如下,xml文件的名字也可以改成自己想要的:
var basePath = Path.GetDirectoryName(typeof(Program).Assembly.Location);//獲取應(yīng)用程序所在目錄 var xmlPath = Path.Combine(basePath, "CoreAPI_Demo.xml"); c.IncludeXmlComments(xmlPath, true);
同時(shí),調(diào)整項(xiàng)目生成的XML文檔文件路徑為:..\CoreAPI_Demo\CoreAPI_Demo.xml
4.隱藏相關(guān)接口
對(duì)于不想暴漏給Swagger展示的接口,我們可以給相關(guān)Controller或Action頭加上:[ApiExplorerSettings(IgnoreApi = true)]
5.調(diào)整系統(tǒng)默認(rèn)輸出路徑
項(xiàng)目啟動(dòng)后,默認(rèn)會(huì)訪問自帶的weatherforecast,如果想調(diào)整為其他路徑,譬如打開后直接訪問Swagger文檔,那么調(diào)整Properties目錄下的launchSettings.json文件,修改launchUrl值為api(前述配置的RoutePrefix值):
{ "$schema": "http://json.schemastore.org/launchsettings.json", "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:7864", "sslPort": 0 } }, "profiles": { "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "launchUrl": "api", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "CoreApi_Demo": { "commandName": "Project", "launchBrowser": true, "launchUrl": "api", "applicationUrl": "http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } }
三、配置文件
以讀取appsettings.json文件為例,當(dāng)然你也定義其他名稱的.json文件進(jìn)行讀取,讀取方式一致,該文件類似于Web.config文件。為方便示例,定義appsettings.json文件內(nèi)容如下:
{ "ConnString": "Data Source=(local);Initial Catalog=Demo;Persist Security Info=True;User ID=DemoUser;Password=123456;MultipleActiveResultSets=True;", "ConnectionStrings": { "MySQLConnection": "server=127.0.0.1;database=mydemo;uid=root;pwd=123456;charset=utf8;SslMode=None;" }, "SystemConfig": { "UploadFile": "/Files", "Domain": "http://localhost:7864" }, "JwtTokenConfig": { "Secret": "fcbfc8df1ee52ba127ab", "Issuer": "abc.com", "Audience": "Brooke.WebApi", "AccessExpiration": 30, "RefreshExpiration": 60 }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*" }
1、配置文件的基本讀取
public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddControllers(); //讀取方式一 var ConnString = Configuration["ConnString"]; var MySQLConnection = Configuration.GetSection("ConnectionStrings")["MySQLConnection"]; var UploadPath = Configuration.GetSection("SystemConfig")["UploadPath"]; var LogDefault = Configuration.GetSection("Logging").GetSection("LogLevel")["Default"]; //讀取方式二 var ConnString2 = Configuration["ConnString"]; var MySQLConnection2 = Configuration["ConnectionStrings:MySQLConnection"]; var UploadPath2 = Configuration["SystemConfig:UploadPath"]; var LogDefault2 = Configuration["Logging:LogLevel:Default"]; } }
以上介紹了2種讀取配置信息的方式,如果要在Controller內(nèi)使用,類似地,進(jìn)行注入并調(diào)用如下:
public class ValuesController : ControllerBase { private IConfiguration _configuration; public ValuesController(IConfiguration configuration) { _configuration = configuration; } // GET: api/<ValuesController> [HttpGet] public IEnumerable<string> Get() { var ConnString = _configuration["ConnString"]; var MySQLConnection = _configuration.GetSection("ConnectionStrings")["MySQLConnection"]; var UploadPath = _configuration.GetSection("SystemConfig")["UploadPath"]; var LogDefault = _configuration.GetSection("Logging").GetSection("LogLevel")["Default"]; return new string[] { "value1", "value2" }; } }
2、讀取配置文件到自定義對(duì)象
以SystemConfig節(jié)點(diǎn)為例,定義類如下:
public class SystemConfig { public string UploadPath { get; set; } public string Domain { get; set; } }
調(diào)整代碼如下:
public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.Configure<SystemConfig>(Configuration.GetSection("SystemConfig")); } }
然后Controller內(nèi)進(jìn)行注入調(diào)用:
[Route("api/[controller]/[action]")] [ApiController] public class ValuesController : ControllerBase { private SystemConfig _sysConfig; public ValuesController(IOptions<SystemConfig> sysConfig) { _sysConfig = sysConfig.Value; } [HttpGet] public IEnumerable<string> GetSetting() { var UploadPath = _sysConfig.UploadPath; var Domain = _sysConfig.Domain; return new string[] { "value1", "value2" }; } }
3、綁定到靜態(tài)類方式讀取
定義相關(guān)靜態(tài)類如下:
public static class MySettings { public static SystemConfig Setting { get; set; } = new SystemConfig(); }
調(diào)整Startup類構(gòu)造函數(shù)如下:
public Startup(IConfiguration configuration, IWebHostEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); Configuration = builder.Build(); //Configuration = configuration; configuration.GetSection("SystemConfig").Bind(MySettings.Setting);//綁定靜態(tài)配置類 }
接下來,諸如直接使用:MySettings.Setting.UploadPath 即可調(diào)用。
四、文件上傳
接口一般少不了文件上傳,相比.netframework框架下webapi通過byte數(shù)組對(duì)象等復(fù)雜方式進(jìn)行文件上傳,.Net Core WebApi有了很大變化,其定義了新的IFormFile對(duì)象來接收上傳文件,直接上Controller代碼:
后端代碼
[Route("api/[controller]/[action]")] [ApiController] public class UploadController : ControllerBase { private readonly IWebHostEnvironment _env; public UploadController(IWebHostEnvironment env) { _env = env; } public ApiResult UploadFile(List<IFormFile> files) { ApiResult result = new ApiResult(); //注:參數(shù)files對(duì)象去也可以通過換成: var files = Request.Form.Files;來獲取 if (files.Count <= 0) { result.Message = "上傳文件不能為空"; return result; } #region 上傳 List<string> filenames = new List<string>(); var webRootPath = _env.WebRootPath; var rootFolder = MySettings.Setting.UploadPath; var physicalPath = $"{webRootPath}/{rootFolder}/"; if (!Directory.Exists(physicalPath)) { Directory.CreateDirectory(physicalPath); } foreach (var file in files) { var fileExtension = Path.GetExtension(file.FileName);//獲取文件格式,拓展名 var saveName = $"{rootFolder}/{Path.GetRandomFileName()}{fileExtension}"; filenames.Add(saveName);//相對(duì)路徑 var fileName = webRootPath + saveName; using FileStream fs = System.IO.File.Create(fileName); file.CopyTo(fs); fs.Flush(); } #endregion result.IsSuccess = true; result.Data["files"] = filenames; return result; } }
前端調(diào)用
接下來通過前端調(diào)用上述上傳接口,在項(xiàng)目根目錄新建wwwroot目錄(.net core webapi內(nèi)置目錄 ),添加相關(guān)js文件包,然后新建一個(gè)index.html文件,內(nèi)容如下:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title></title> <style type="text/css"> </style> <script src="res/scripts/jquery-1.10.2.min.js"></script> <script src="res/scripts/jquery.form.js"></script> <script type="text/javascript"> //方法1 function AjaxUploadfile() { var upload = $("#files").get(0); var files = upload.files; var data = new FormData(); for (var i = 0; i < files.length; i++) { data.append("files", files[i]); } //此處data的構(gòu)建也可以換成:var data = new FormData(document.getElementById("myform")); $.ajax({ type: "POST", url: "/api/upload/uploadfile", contentType: false, processData: false, data: data, success: function (result) { alert("success"); $.each(result.data.files, function (i, filename) { $("#filePanel").append('<p>' + filename + '</p>'); }); }, error: function () { alert("上傳文件錯(cuò)誤"); } }); } //方法2 function AjaxUploadfile2() { $("#myform").ajaxSubmit({ success: function (result) { if (result.isSuccess) { $.each(result.data.files, function (i, filename) { $("#filePanel").append('<p>' + filename + '</p>'); }); } else { alert(result.message); } } }); } </script> </head> <body> <form id="myform" method="post" action="/api/upload/uploadfile" enctype="multipart/form-data"> <input type="file" id="files" name="files" multiple /> <br /><br /> <input type="button" value="FormData Upload" onclick="AjaxUploadfile();" /><br /><br /> <input type="button" value="ajaxSubmit Upload" onclick="AjaxUploadfile2();" /><br /><br /> <div id="filePanel"></div> </form> <script type="text/javascript"> $(function () { }); </script> </body> </html>
上述通過構(gòu)建FormData和ajaxSubmit兩種方式進(jìn)行上傳,需要注意的是contentType和processData兩個(gè)參數(shù)的設(shè)置;另外允許一次上傳多個(gè)文件,需設(shè)置multipart屬性。
在訪問wwwroot下的靜態(tài)文件之前,必須先在Startup類的Configure方法下進(jìn)行注冊(cè):
public void Configure(IApplicationBuilder app) { app.UseStaticFiles();//用于訪問wwwroot下的文件 }
啟動(dòng)項(xiàng)目,通過訪問路徑:http://localhost:***/index.html,進(jìn)行上傳測(cè)試,成功后,將在wwwroot下的Files目錄下看到上傳的文件。
五、統(tǒng)一WebApi數(shù)據(jù)返回格式
定義統(tǒng)一返回格式
為了方便前后端使用約定好的數(shù)據(jù)格式,通常我們會(huì)定義統(tǒng)一的數(shù)據(jù)返回,其包括是否成功、返回狀態(tài)、具體數(shù)據(jù)等;為便于說明,定義一個(gè)數(shù)據(jù)返回類如下:
public class ApiResult { public bool IsSuccess { get; set; } public string Message { get; set; } public string Code { get; set; } public Dictionary<string, object> Data { get; set; } = new Dictionary<string, object>(); }
這樣,我們將每一個(gè)action接口操作封裝為ApiResult格式進(jìn)行返回。新建一個(gè)ProductController示例如下:
[Produces("application/json")] [Route("api/[controller]")] [ApiController] public class ProductController : ControllerBase { [HttpGet] public ApiResult Get() { var result = new ApiResult(); var rd = new Random(); result.Data["dataList"] = Enumerable.Range(1, 5).Select(index => new { Name = $"商品-{index}", Price = rd.Next(100, 9999) }); result.IsSuccess = true; return result; } }
Produces:定義數(shù)據(jù)返回的方式,給每個(gè)Controller打上[Produces("application/json")]標(biāo)識(shí),即表示以json方式進(jìn)行數(shù)據(jù)輸出。
- ApiController:確保每個(gè)Controller有ApiController標(biāo)識(shí),通常,我們會(huì)定義一個(gè)基類如:BaseController,其繼承自ControllerBase,并將其打上[ApiController]標(biāo)識(shí),新建的controller都繼承該類;
- Route:路由訪問方式,如不喜歡RESTful方式,可加上Action,即:[Route("api/[controller]/[action]")];
- HTTP 請(qǐng)求:結(jié)合前面配置的Swagger,必須確保每個(gè)Action都有具體的請(qǐng)求方式,即必須是HttpGet、HttpPost、HttpPut、HttpDelete中的一種,通常情況下,我們使用HttpGet、HttpPost足以。
- 如此,即完成的數(shù)據(jù)返回的統(tǒng)一。
解決T時(shí)間格式
.Net Core Web Api默認(rèn)以首字母小寫的類駝峰式命名返回,但遇到DateTime類型的數(shù)據(jù),會(huì)返回T格式時(shí)間,如要解決T時(shí)間格式,定義一個(gè)時(shí)間格式轉(zhuǎn)換類如下:
public class DatetimeJsonConverter : JsonConverter<DateTime> { public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.String) { if (DateTime.TryParse(reader.GetString(), out DateTime date)) return date; } return reader.GetDateTime(); } public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) { writer.WriteStringValue(value.ToString("yyyy-MM-dd HH:mm:ss")); } }
然后在Startup類的ConfigureServices中調(diào)整services.AddControllers代碼如下:
services.AddControllers() .AddJsonOptions(configure => { configure.JsonSerializerOptions.Converters.Add(new DatetimeJsonConverter()); });
六、模型驗(yàn)證
模型驗(yàn)證在ASP.NET MVC已存在,使用方式基本一致。指對(duì)向接口提交過來的數(shù)據(jù)進(jìn)行參數(shù)校驗(yàn),包括必填項(xiàng)、數(shù)據(jù)格式、字符長(zhǎng)度、范圍等等。一般的,我們會(huì)將POST提交過來的對(duì)象定義為一個(gè)實(shí)體類進(jìn)行接收,譬如定義一個(gè)注冊(cè)類如下:
public class RegisterEntity { /// <summary> /// 手機(jī)號(hào) /// </summary> [Display(Name = "手機(jī)號(hào)")] [Required(ErrorMessage = "{0}不能為空")] [StringLength(11, ErrorMessage = "{0}最多{1}個(gè)字符")] public string Mobile { get; set; } /// <summary> /// 驗(yàn)證碼 /// </summary> [Display(Name = "驗(yàn)證碼")] [Required(ErrorMessage = "{0}不能為空")] [StringLength(6, ErrorMessage = "{0}最多{1}個(gè)字符")] public string Code { get; set; } /// <summary> /// 密碼 /// </summary> [Display(Name = "密碼")] [Required(ErrorMessage = "{0}不能為空")] [StringLength(16, ErrorMessage = "{0}最多{1}個(gè)字符")] public string Pwd { get; set; } }
Display標(biāo)識(shí)提示字段的名稱,Required表示必填,StringLength限制字段的長(zhǎng)度,當(dāng)然還有其他一些內(nèi)置特性,具體可參考官方文檔,列舉一些常見的驗(yàn)證特性如下:
- [CreditCard]:驗(yàn)證屬性是否具有信用卡格式。 需要 JQuery 驗(yàn)證其他方法。
- [Compare]:驗(yàn)證模型中的兩個(gè)屬性是否匹配。
- [EmailAddress]:驗(yàn)證屬性是否具有電子郵件格式。
- [Phone]:驗(yàn)證屬性是否具有電話號(hào)碼格式。
- [Range]:驗(yàn)證屬性值是否在指定的范圍內(nèi)。
- [RegularExpression]:驗(yàn)證屬性值是否與指定的正則表達(dá)式匹配。
- [Required]:驗(yàn)證字段是否不為 null。 有關(guān)此屬性的行為的詳細(xì)信息,請(qǐng)參閱 [Required] 特性。
- [StringLength]:驗(yàn)證字符串屬性值是否不超過指定長(zhǎng)度限制。
- [Url]:驗(yàn)證屬性是否具有 URL 格式。
- [Remote]:通過在服務(wù)器上調(diào)用操作方法來驗(yàn)證客戶端上的輸入。
- 上述說明了基本的模型驗(yàn)證使用方法,以這種方式,同時(shí)結(jié)合T4模板,
那么上述模型驗(yàn)證在Web API中是怎么工作的呢?在Startup類的ConfigureServices添加如下代碼:
//模型參數(shù)驗(yàn)證 services.Configure<ApiBehaviorOptions>(options => { options.InvalidModelStateResponseFactory = (context) => { var error = context.ModelState.FirstOrDefault().Value; var message = error.Errors.FirstOrDefault(p => !string.IsNullOrWhiteSpace(p.ErrorMessage))?.ErrorMessage; return new JsonResult(new ApiResult { Message = message }); }; });
添加注冊(cè)示例Action代碼:
/// <summary> /// 注冊(cè) /// </summary> /// <param name="model"></param> /// <returns></returns> [HttpPost] public async Task<ApiResult> Register(RegisterEntity model) { ApiResult result = new ApiResult(); var _code = CacheHelper.GetCache(model.Mobile); if (_code == null) { result.Message = "驗(yàn)證碼過期或不存在"; return result; } if (!model.Code.Equals(_code.ToString())) { result.Message = "驗(yàn)證碼錯(cuò)誤"; return result; } /** 相關(guān)邏輯代碼 **/ return result; }
如此,通過配置ApiBehaviorOptions的方式,并讀取驗(yàn)證錯(cuò)誤信息的第一條信息并返回,即完成了Web API中Action對(duì)請(qǐng)求參數(shù)的驗(yàn)證工作,關(guān)于錯(cuò)誤信息Message的返回,也可略作封裝,在此略。
七、日志使用
雖然.Net Core WebApi有自帶的日志管理功能,但不一定能較容易地滿足我們的需求,通常會(huì)采用第三方日志框架,典型的如:NLog、Log4Net,簡(jiǎn)單介紹NLog日志組件的使用;
NLog的使用
① 通過NuGet安裝包:NLog.Web.AspNetCore,當(dāng)前項(xiàng)目版本4.9.2;
② 項(xiàng)目根目錄新建一個(gè)NLog.config文件,關(guān)鍵NLog.config的其他詳細(xì)配置,可參考官方文檔,這里作簡(jiǎn)要配置如下;
<?xml version="1.0" encoding="utf-8"?> <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" autoReload="true" throwExceptions="false" internalLogLevel="Off" internalLogFile="NlogRecords.log"> <!--Nlog內(nèi)部日志記錄為Off關(guān)閉--> <extensions> <add assembly="NLog.Web.AspNetCore" /> </extensions> <targets> <target name="log_file" xsi:type="File" fileName="${basedir}/logs/${shortdate}.log" layout="${longdate} | ${level:uppercase=false} | ${message} ${onexception:${exception:format=tostring} ${newline} ${stacktrace} ${newline}" /> </targets> <rules> <!--跳過所有級(jí)別的Microsoft組件的日志記錄--> <logger name="Microsoft.*" final="true" /> <!--<logger name="logdb" writeTo="log_database" />--> <logger name="*" minlevel="Trace" writeTo="log_file" /> </rules> </nlog> <!--https://github.com/NLog/NLog/wiki/Getting-started-with-ASP.NET-Core-3-->
③ 調(diào)整Program.cs文件如下;
public class Program { public static void Main(string[] args) { //CreateHostBuilder(args).Build().Run(); var logger = NLog.Web.NLogBuilder.ConfigureNLog("nlog.config").GetCurrentClassLogger(); try { logger.Debug("init main"); CreateHostBuilder(args).Build().Run(); } catch (Exception exception) { //NLog: catch setup errors logger.Error(exception, "Stopped program because of exception"); throw; } finally { // Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux) NLog.LogManager.Shutdown(); } } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); }).ConfigureLogging(logging => { logging.ClearProviders(); logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace); }).UseNLog();//依賴注入Nlog; }
其中Main函數(shù)里的捕獲異常代碼配置省略也是可以的,CreateHostBuilder下的UseNLog為必設(shè)項(xiàng)。
Controller通過注入調(diào)用如下:
public class WeatherForecastController : ControllerBase { private static readonly string[] Summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; private readonly ILogger<WeatherForecastController> _logger; public WeatherForecastController(ILogger<WeatherForecastController> logger) { _logger = logger; } [HttpGet] public IEnumerable<WeatherForecast> Get() { _logger.LogInformation("測(cè)試一條日志"); var rng = new Random(); return Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = DateTime.Now.AddDays(index), TemperatureC = rng.Next(-20, 55), Summary = Summaries[rng.Next(Summaries.Length)] }) .ToArray(); }
本地測(cè)試后,即可在debug下看到logs目錄下生成的日志文件。
八、依賴注入
使用.Net Core少不了和依賴注入打交道,這也是.Net Core的設(shè)計(jì)思想之一,關(guān)于什么是依賴注入(DI),以及為什么要使用依賴注入,這里不再贅述,先來看一個(gè)簡(jiǎn)單示例的依賴注入。
public interface IProductRepository { IEnumerable<Product> GetAll(); } public class ProductRepository : IProductRepository { public IEnumerable<Product> GetAll() { } }
Startup類進(jìn)行注冊(cè):
public void ConfigureServices(IServiceCollection services) { services.AddScoped<IProductRepository, ProductRepository>(); }
請(qǐng)求 IProductRepository 服務(wù)并用于調(diào)用 GetAll 方法:
public class ProductController : ControllerBase { private readonly IProductRepository _productRepository; public ProductController(IProductRepository productRepository) { _productRepository = productRepository; } public IEnumerable<Product> Get() { return _productRepository.GetAll(); } }
通過使用DI模式,來實(shí)現(xiàn)IProductRepository 接口。其實(shí)前述已多次出現(xiàn)通過構(gòu)造函數(shù)進(jìn)行注入調(diào)用的示例。
生命周期
services.AddScoped<IMyDependency, MyDependency>(); services.AddTransient<IMyDependency, MyDependency>(); services.AddSingleton<IMyDependency, MyDependency>();
- Transient:每一次請(qǐng)求都會(huì)創(chuàng)建一個(gè)新實(shí)例;
- Scoped:每個(gè)作用域生成周期內(nèi)創(chuàng)建一個(gè)實(shí)例;
- Singleton:?jiǎn)卫J剑麄€(gè)應(yīng)用程序生命周期內(nèi)只創(chuàng)建一個(gè)實(shí)例;
這里,需要根據(jù)具體的業(yè)務(wù)邏輯場(chǎng)景需求選擇注入相應(yīng)的生命周期服務(wù)。
實(shí)際應(yīng)用中,我們會(huì)有很多個(gè)服務(wù)需要注冊(cè)到ConfigureServices內(nèi),一個(gè)個(gè)寫入顯然繁瑣,而且容易忘記漏寫,一般地,我們可能會(huì)想到利用反射進(jìn)行批量注入,并通過擴(kuò)展的方式進(jìn)行注入,譬如:
public static class AppServiceExtensions { /// <summary> /// 注冊(cè)應(yīng)用程序域中的服務(wù) /// </summary> /// <param name="services"></param> public static void AddAppServices(this IServiceCollection services) { var ts = System.Reflection.Assembly.Load("CoreAPI.Data").GetTypes().Where(s => s.Name.EndsWith("Repository") || s.Name.EndsWith("Service")).ToArray(); foreach (var item in ts.Where(s => !s.IsInterface)) { var interfaceType = item.GetInterfaces(); foreach (var typeArray in interfaceType) { services.AddTransient(typeArray, item); } } } }
public void ConfigureServices(IServiceCollection services) { services.AddAppServices();//批量注冊(cè)服務(wù) }
誠然,這樣配合系統(tǒng)自帶DI注入是能完成我們的批量注入需求的。但其實(shí)也有更多選擇,來幫我們簡(jiǎn)化DI注冊(cè),譬如選擇其他第三方組件:Scrutor、Autofac…
1、Scrutor的使用
Scrutor是基于微軟注入組件的一個(gè)擴(kuò)展庫,簡(jiǎn)單示例如下:
services.Scan(scan => scan .FromAssemblyOf<Startup>() .AddClasses(classes => classes.Where(s => s.Name.EndsWith("Repository") || s.Name.EndsWith("Service"))) .AsImplementedInterfaces() .WithTransientLifetime() );
以上代碼通過Scan方式批量注冊(cè)了以Repository、Service結(jié)尾的接口服務(wù),其生命周期為Transient,該方式等同于前述的以反射方式的批量注冊(cè)服務(wù)。
關(guān)于Scrutor的其他用法,大家可以參見官方文檔,這里只做下引子。
2、Autofac
一般情況下,使用MS自帶的DI或采用Scrutor,即可滿足實(shí)際需要,如果有更高的應(yīng)用需求,如要求屬性注入、甚至接管或取代MS自帶的DI,那么你可以選擇Autofac,關(guān)于Autofac的具體使用,在此不作詳敘。
九、緩存
MemoryCache使用
按官方說明,開發(fā)人員需合理說用緩存,以及限制緩存大小,Core運(yùn)行時(shí)不會(huì)根據(jù)內(nèi)容壓力限制緩存大小。對(duì)于使用方式,依舊還是先行注冊(cè),然后控制器調(diào)用:
public void ConfigureServices(IServiceCollection services) { services.AddMemoryCache();//緩存中間件 }
public class ProductController : ControllerBase { private IMemoryCache _cache; public ProductController(IMemoryCache memoryCache) { _cache = memoryCache; } [HttpGet] public DateTime GetTime() { string key = "_timeKey"; // Look for cache key. if (!_cache.TryGetValue(key, out DateTime cacheEntry)) { // Key not in cache, so get data. cacheEntry = DateTime.Now; // Set cache options. var cacheEntryOptions = new MemoryCacheEntryOptions() // Keep in cache for this time, reset time if accessed. .SetSlidingExpiration(TimeSpan.FromSeconds(3)); // Save data in cache. _cache.Set(key, cacheEntry, cacheEntryOptions); } return cacheEntry; } }
上述代碼緩存了一個(gè)時(shí)間,并設(shè)置了滑動(dòng)過期時(shí)間(指最后一次訪問后的過期時(shí)間)為3秒;如果需要設(shè)置絕對(duì)過期時(shí)間,將SetSlidingExpiration 改為SetAbsoluteExpiration即可。瀏覽刷新,每3秒后時(shí)間將更新。
附一個(gè)封裝好的Cache類如下:
public class CacheHelper { public static IMemoryCache _memoryCache = new MemoryCache(new MemoryCacheOptions()); /// <summary> /// 緩存絕對(duì)過期時(shí)間 /// </summary> ///<param name="key">Cache鍵</param> ///<param name="value">緩存的值</param> ///<param name="minute">minute分鐘后絕對(duì)過期</param> public static void SetChache(string key, object value, int minute) { if (value == null) return; _memoryCache.Set(key, value, new MemoryCacheEntryOptions() .SetAbsoluteExpiration(TimeSpan.FromMinutes(minute))); } /// <summary> /// 緩存相對(duì)過期,最后一次訪問后minute分鐘后過期 /// </summary> ///<param name="key">Cache鍵</param> ///<param name="value">緩存的值</param> ///<param name="minute">滑動(dòng)過期分鐘</param> public static void SetChacheSliding(string key, object value, int minute) { if (value == null) return; _memoryCache.Set(key, value, new MemoryCacheEntryOptions() .SetSlidingExpiration(TimeSpan.FromMinutes(minute))); } /// <summary> ///設(shè)置緩存,如果不主動(dòng)清空,會(huì)一直保存在內(nèi)存中. /// </summary> ///<param name="key">Cache鍵值</param> ///<param name="value">給Cache[key]賦的值</param> public static void SetChache(string key, object value) { _memoryCache.Set(key, value); } /// <summary> ///清除緩存 /// </summary> ///<param name="key">cache鍵</param> public static void RemoveCache(string key) { _memoryCache.Remove(key); } /// <summary> ///根據(jù)key值,返回Cache[key]的值 /// </summary> ///<param name="key"></param> public static object GetCache(string key) { //return _memoryCache.Get(key); if (key != null && _memoryCache.TryGetValue(key, out object val)) { return val; } else { return default; } } /// <summary> /// 通過Key值返回泛型對(duì)象 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <returns></returns> public static T GetCache<T>(string key) { if (key != null && _memoryCache.TryGetValue<T>(key, out T val)) { return val; } else { return default; } } }
十、異常處理
定義異常處理中間件
這里主要針對(duì)全局異常進(jìn)行捕獲處理并記錄日志,并以統(tǒng)一的json格式返回給接口調(diào)用者;說異常處理前先提下中間件,關(guān)于什么是中間件,在此不在贅述,一個(gè)中間件其基本的結(jié)構(gòu)如下:
public class CustomMiddleware { private readonly RequestDelegate _next; public CustomMiddleware(RequestDelegate next) { _next = next; } public async Task Invoke(HttpContext httpContext) { await _next(httpContext); } }
下面我們定義自己的全局異常處理中間件,代碼如下:
public class CustomExceptionMiddleware { private readonly RequestDelegate _next; private readonly ILogger<CustomExceptionMiddleware> _logger; public CustomExceptionMiddleware(RequestDelegate next, ILogger<CustomExceptionMiddleware> logger) { _next = next; _logger = logger; } public async Task Invoke(HttpContext httpContext) { try { await _next(httpContext); } catch (Exception ex) { _logger.LogError(ex,"Unhandled exception..."); await HandleExceptionAsync(httpContext, ex); } } private Task HandleExceptionAsync(HttpContext httpContext, Exception ex) { var result = JsonConvert.SerializeObject(new { isSuccess = false, message = ex.Message }); httpContext.Response.ContentType = "application/json;charset=utf-8"; return httpContext.Response.WriteAsync(result); } } /// <summary> /// 以擴(kuò)展方式添加中間件 /// </summary> public static class CustomExceptionMiddlewareExtensions { public static IApplicationBuilder UseCustomExceptionMiddleware(this IApplicationBuilder builder) { return builder.UseMiddleware<CustomExceptionMiddleware>(); } }
然后在Startup類的Configure方法里添加上述擴(kuò)展的中間件,見加粗部分:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } //全局異常處理 app.UseCustomExceptionMiddleware(); }
在HandleExceptionAsync方法中,為方便開發(fā)和測(cè)試,這里將系統(tǒng)的錯(cuò)誤返回給了接口調(diào)用者,實(shí)際生產(chǎn)環(huán)境中可統(tǒng)一返回固定的錯(cuò)誤Message消息。
異常狀態(tài)碼的處理
關(guān)于http狀態(tài)碼,常見的如正常返回的200,其他401、403、404、502等等等等,因?yàn)橄到y(tǒng)有時(shí)候并不總是返回200成功,對(duì)于返回非200的異常狀態(tài)碼,WebApi也要做到相應(yīng)的處理,以便接口調(diào)用者能正確接收,譬如緊接下來的JWT認(rèn)證,當(dāng)認(rèn)證令牌過期或沒有權(quán)限時(shí),系統(tǒng)實(shí)際會(huì)返回401、403,但接口并不提供有效的可接收的返回,因此,這里列舉一些常見的異常狀態(tài)碼,并以200方式提供給接口調(diào)用者,在Startup類的Configure方法里添加代碼如下:
app.UseStatusCodePages(async context => { //context.HttpContext.Response.ContentType = "text/plain"; context.HttpContext.Response.ContentType = "application/json;charset=utf-8"; int code = context.HttpContext.Response.StatusCode; string message = code switch { 401 => "未登錄", 403 => "訪問拒絕", 404 => "未找到", _ => "未知錯(cuò)誤", }; context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; await context.HttpContext.Response.WriteAsync(Newtonsoft.Json.JsonConvert.SerializeObject(new { isSuccess = false, code, message })); });
代碼很簡(jiǎn)單,這里使用系統(tǒng)自帶的異常處理中間件UseStatusCodePages,當(dāng)然,你還可以自定義過濾器處理異常,不過不推薦,簡(jiǎn)單高效直接才是需要的。
關(guān)于.NET Core的異常處理中間件,還有其他諸如 UseExceptionHandler、UseStatusCodePagesWithRedirects等等,不同的中間件有其適用的環(huán)境,有的可能更適用于MVC或其他應(yīng)用場(chǎng)景上,找到合適的即可。
題外話:大家也可以將UseStatusCodePages處理異常狀態(tài)碼的操作封裝到前述的全局異常處理中間件中。
十一、應(yīng)用安全與JWT認(rèn)證
關(guān)于什么是JWT,在此不作贅述。實(shí)際應(yīng)用中,為了部分接口的安全性,譬如需要身份認(rèn)證才能訪問的接口資源,對(duì)于Web API而言,一般會(huì)采用token令牌進(jìn)行認(rèn)證,服務(wù)端結(jié)合緩存來實(shí)現(xiàn)。
那為什么要選擇JWT認(rèn)證呢?原因無外乎以下:服務(wù)端不進(jìn)行保存、無狀態(tài)、適合移動(dòng)端、適合分布式、標(biāo)準(zhǔn)化等等。關(guān)于JWT的使用如下:
通過NuGget安裝包:Microsoft.AspNetCore.Authentication.JwtBearer,當(dāng)前示例版本3.1.5;
ConfigureServices進(jìn)行注入,默認(rèn)以Bearer命名,這里你也可以改成其他名字,保持前后一致即可,注意加粗部分,代碼如下:
appsettings.json添加JWT配置節(jié)點(diǎn)(見前述【配置文件】),添加JWT相關(guān)認(rèn)證類:
public static class JwtSetting { public static JwtConfig Setting { get; set; } = new JwtConfig(); } public class JwtConfig { public string Secret { get; set; } public string Issuer { get; set; } public string Audience { get; set; } public int AccessExpiration { get; set; } public int RefreshExpiration { get; set; } }
采用前述綁定靜態(tài)類的方式讀取JWT配置,并進(jìn)行注入:
public Startup(IConfiguration configuration, IWebHostEnvironment env) { //Configuration = configuration; var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); Configuration = builder.Build(); configuration.GetSection("SystemConfig").Bind(MySettings.Setting);//綁定靜態(tài)配置類 configuration.GetSection("JwtTokenConfig").Bind(JwtSetting.Setting);//同上 } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { #region JWT認(rèn)證注入 JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); services.AddAuthentication("Bearer") .AddJwtBearer("Bearer", options => { options.RequireHttpsMetadata = false; options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = JwtSetting.Setting.Issuer, ValidAudience = JwtSetting.Setting.Audience, IssuerSigningKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(JwtSetting.Setting.Secret)) }; }); #endregion }
給Swagger添加JWT認(rèn)證支持,完成后,Swagger頁面會(huì)出現(xiàn)鎖的標(biāo)識(shí),獲取token后填入Value(Bearer token形式)項(xiàng)進(jìn)行Authorize登錄即可,Swagger配置JWT見加粗部分:
services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1", Description = "API文檔描述", Contact = new OpenApiContact { Email = "5007032@qq.com", Name = "測(cè)試項(xiàng)目", //Url = new Uri("http://t.abc.com/") }, License = new OpenApiLicense { Name = "BROOKE許可證", //Url = new Uri("http://t.abc.com/") } }); // 為 Swagger JSON and UI設(shè)置xml文檔注釋路徑 //var basePath = Path.GetDirectoryName(typeof(Program).Assembly.Location);//獲取應(yīng)用程序所在目錄(不受工作目錄影響) //var xmlPath = Path.Combine(basePath, "CoreAPI_Demo.xml"); //c.IncludeXmlComments(xmlPath, true); var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); c.IncludeXmlComments(xmlPath); #region JWT認(rèn)證Swagger授權(quán) c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { Description = "JWT授權(quán)(數(shù)據(jù)將在請(qǐng)求頭header中進(jìn)行傳輸) 直接在下框中輸入Bearer {token}(中間是空格)", Name = "Authorization", In = ParameterLocation.Header, Type = SecuritySchemeType.ApiKey, BearerFormat = "JWT", Scheme = "Bearer" }); c.AddSecurityRequirement(new OpenApiSecurityRequirement() { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } }, new string[] { } } }); #endregion });
Starup類添加Configure注冊(cè),注意,需放到 app.UseAuthorization();前面:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseAuthentication();//jwt認(rèn)證 app.UseAuthorization(); }
這樣,JWT就基本配置完畢,接下來實(shí)施認(rèn)證登錄和授權(quán),模擬操作如下:
[HttpPost] public async Task<ApiResult> Login(LoginEntity model) { ApiResult result = new ApiResult(); //驗(yàn)證用戶名和密碼 var userInfo = await _memberService.CheckUserAndPwd(model.User, model.Pwd); if (userInfo == null) { result.Message = "用戶名或密碼不正確"; return result; } var claims = new Claim[] { new Claim(ClaimTypes.Name,model.User), new Claim(ClaimTypes.Role,"User"), new Claim(JwtRegisteredClaimNames.Sub,userInfo.MemberID.ToString()), }; var key = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(JwtSetting.Setting.Secret)); var expires = DateTime.Now.AddDays(1); var token = new JwtSecurityToken( issuer: JwtSetting.Setting.Issuer, audience: JwtSetting.Setting.Audience, claims: claims, notBefore: DateTime.Now, expires: expires, signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)); //生成Token string jwtToken = new JwtSecurityTokenHandler().WriteToken(token); //更新最后登錄時(shí)間 await _memberService.UpdateLastLoginTime(userInfo.MemberID); result.IsSuccess= 1; result.ResultData["token"] = jwtToken; result.Message = "授權(quán)成功!"; return result; }
上述代碼模擬登錄操作(賬號(hào)密碼登錄,成功后設(shè)置有效期一天),生成token并返回,前端調(diào)用者拿到token后以諸如localstorage方式進(jìn)行存儲(chǔ),調(diào)取授權(quán)接口時(shí),添加該token到header(Bearer token)進(jìn)行接口請(qǐng)求。接下來,給需要身份授權(quán)的Controller或Action打上Authorize標(biāo)識(shí):
[Authorize] [Route("api/[controller]/[action]")] public class UserController : ControllerBase { }
如果要添加基于角色的授權(quán),可限制操作如下:
[Authorize(Roles = "user")] [Route("api/[controller]/[action]")] public class UserController : ControllerBase { } //多個(gè)角色也可以逗號(hào)分隔 [Authorize(Roles = "Administrator,Finance")] [Route("api/[controller]/[action]")] public class UserController : ControllerBase { }
不同的角色信息,可通過登錄設(shè)置ClaimTypes.Role進(jìn)行配置;當(dāng)然,這里只是簡(jiǎn)單的示例說明角色服務(wù)的應(yīng)用,復(fù)雜的可通過注冊(cè)策略服務(wù),并結(jié)合數(shù)據(jù)庫進(jìn)行動(dòng)態(tài)配置。
這樣,一個(gè)簡(jiǎn)單的基于JWT認(rèn)證授權(quán)的工作就完成了。
十二、跨域
前后端分離,會(huì)涉及到跨域問題,簡(jiǎn)單的支持跨域操作如下:
添加擴(kuò)展支持
public static class CrosExtensions { public static void ConfigureCors(this IServiceCollection services) { services.AddCors(options => options.AddPolicy("CorsPolicy", builder => { builder.AllowAnyMethod() .SetIsOriginAllowed(_ => true) .AllowAnyHeader() .AllowCredentials(); })); //services.AddCors(options => options.AddPolicy("CorsPolicy", //builder => //{ // builder.WithOrigins(new string[] { "http://localhost:13210" }) // .AllowAnyMethod() // .AllowAnyHeader() // .AllowCredentials(); //})); } }
Startup類添加相關(guān)注冊(cè)如下:
public void ConfigureServices(IServiceCollection services) { services.ConfigureCors(); }
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseCors("CorsPolicy");//跨域 }
這樣,一個(gè)簡(jiǎn)單跨域操作就完成了,你也可以通過設(shè)置WithOrigins、WithMethods等方法限制請(qǐng)求地址來源和請(qǐng)求方式。
本篇涉及到的源碼地址:https://github.com/Brooke181/CoreAPI_Demo
到此這篇關(guān)于.Net Core 3.1 Web API基礎(chǔ)知識(shí)的文章就介紹到這了,更多相關(guān).Net Core 3.1 Web API內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
ASP.NET連接 Access數(shù)據(jù)庫的幾種方法
這篇文章主要介紹了ASP.NET連接 Access數(shù)據(jù)庫的幾種方法,每種方法都非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友一起學(xué)習(xí)吧2016-08-08asp.net 未能加載文件或程序集“XXX”或它的某一個(gè)依賴項(xiàng)。試圖加載格式不正確的程序。
運(yùn)行asp.net后提示未能加載文件或程序集“XXX”或它的某一個(gè)依賴項(xiàng)。試圖加載格式不正確的程序。2011-07-07.NET Core開發(fā)日志之OData(Open Data Protocol)
這篇文章主要給大家介紹了關(guān)于.NET Core開發(fā)日志之OData(Open Data Protocol)的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-02-02基于Cookie使用過濾器實(shí)現(xiàn)客戶每次訪問只登錄一次
這篇文章主要介紹了基于Cookie使用過濾器實(shí)現(xiàn)客戶每次訪問只登錄一次,需要的朋友可以參考下2017-06-06ASP.NET?Core管理應(yīng)用程序狀態(tài)
這篇文章介紹了ASP.NET?Core管理應(yīng)用程序狀態(tài)的方法,文中通過示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-04-04