一文了解.Net Core 3.1 Web API基礎(chǔ)知識(shí)

目錄
一、前言
二、Swagger調(diào)試Web API
三、配置文件
1、配置文件的基本讀取
2、讀取配置文件到自定義對(duì)象
3、綁定到靜態(tài)類方式讀取
四、文件上傳
后端代碼
前端調(diào)用
五、統(tǒng)一WebApi數(shù)據(jù)返回格式
定義統(tǒng)一返回格式
解決T時(shí)間格式
六、模型驗(yàn)證
七、日志使用
NLog的使用
八、依賴注入
生命周期
1、Scrutor的使用
2、Autofac
九、緩存
?MemoryCache使用
十、異常處理
定義異常處理中間件
異常狀態(tài)碼的處理
十一、應(yīng)用安全與JWT認(rèn)證
十二、跨域
?
回到頂部
一、前言
隨著近幾年前后端分離、微服務(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 = "[email protected]",
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頁(yè)面:

然而到這里還沒完,相關(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 = "[email protected]",
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頁(yè)面,即可看到注釋說明。
如果不想將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
隱藏相關(guān)接口
對(duì)于不想暴漏給Swagger展示的接口,我們可以給相關(guān)Controller或Action頭加上:[ApiExplorerSettings(IgnoreApi = true)]調(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/
[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(Configuration.GetSection("SystemConfig"));
}
}

?然后Controller內(nèi)進(jìn)行注入調(diào)用:

[Route("api/[controller]/[action]")]
[ApiController]
public class ValuesController : ControllerBase
{
private SystemConfig _sysConfig;
public ValuesController(IOptionssysConfig)
{
_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)用。
?
回到頂部
四、文件上傳
接口一般少不了文件上傳,相比.net framework框架下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(Listfiles)
{
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)容如下:

"utf-8" />

上述通過構(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
{
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
{
///
/// 手機(jī)號(hào)
///
[Display(Name = "手機(jī)號(hào)")]
[Required(ErrorMessage = "{0}不能為空")]
[StringLength(11, ErrorMessage = "{0}最多{1}個(gè)字符")]
public string Mobile { get; set; }
///
/// 驗(yàn)證碼
///
[Display(Name = "驗(yàn)證碼")]
[Required(ErrorMessage = "{0}不能為空")]
[StringLength(6, ErrorMessage = "{0}最多{1}個(gè)字符")]
public string Code { get; set; }
///
/// 密碼
///
[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模板,通過表對(duì)象生成模型驗(yàn)證實(shí)體,省卻了在action中編寫大量驗(yàn)證代碼的工作。當(dāng)然,一些必要的較為復(fù)雜的驗(yàn)證,或結(jié)合數(shù)據(jù)庫(kù)操作的驗(yàn)證,則單獨(dú)寫到action或其他應(yīng)用模塊中。
那么上述模型驗(yàn)證在Web API中是怎么工作的呢?在Startup類的ConfigureServices添加如下代碼:

//模型參數(shù)驗(yàn)證
services.Configure(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代碼:

///
/// 注冊(cè)
///
///
///
[HttpPost]
public async TaskRegister(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)要配置如下;

"1.0" encoding="utf-8"?>"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.Web.AspNetCore" />
"log_file" xsi:type="File" fileName="${basedir}/logs/${shortdate}.log"
layout="${longdate} | ${level:uppercase=false} | ${message} ${onexception:${exception:format=tostring} ${newline} ${stacktrace} ${newline}" />
"Microsoft.*" final="true" />
"*" minlevel="Trace" writeTo="log_file" />

?
③ 調(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();
}).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_logger;
public WeatherForecastController(ILoggerlogger)
{
_logger = logger;
}
[HttpGet]
public IEnumerableGet()
{
_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
{
IEnumerableGetAll();
}
public class ProductRepository : IProductRepository
{
public IEnumerableGetAll()
{
}
}

Startup類進(jìn)行注冊(cè):
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped();
}
請(qǐng)求 IProductRepository 服務(wù)并用于調(diào)用 GetAll 方法:

public class ProductController : ControllerBase
{
private readonly IProductRepository _productRepository;
public ProductController(IProductRepository productRepository)
{
_productRepository = productRepository;
}
public IEnumerableGet()
{
return _productRepository.GetAll();
}
}

通過使用DI模式,來實(shí)現(xiàn)IProductRepository 接口。其實(shí)前述已多次出現(xiàn)通過構(gòu)造函數(shù)進(jìn)行注入調(diào)用的示例。
生命周期
services.AddScoped();
services.AddTransient();
services.AddSingleton();
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
{
///
/// 注冊(cè)應(yīng)用程序域中的服務(wù)
///
///
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ù)
}
?
誠(chéng)然,這樣配合系統(tǒng)自帶DI注入是能完成我們的批量注入需求的。但其實(shí)也有更多選擇,來幫我們簡(jiǎn)化DI注冊(cè),譬如選擇其他第三方組件:Scrutor、Autofac…
1、Scrutor的使用
Scrutor是基于微軟注入組件的一個(gè)擴(kuò)展庫(kù),簡(jiǎn)單示例如下:

services.Scan(scan => scan
.FromAssemblyOf()
.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類如下:
?View Code
?
回到頂部
十、異常處理
定義異常處理中間件
這里主要針對(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_logger;
public CustomExceptionMiddleware(RequestDelegate next, ILoggerlogger)
{
_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);
}
}
///
/// 以擴(kuò)展方式添加中間件
///
public static class CustomExceptionMiddlewareExtensions
{
public static IApplicationBuilder UseCustomExceptionMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware();
}
}

然后在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頁(yè)面會(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 = "[email protected]",
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 TaskLogin(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ù)庫(kù)進(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)求方式。
?
至此,全篇結(jié)束,本篇涉及到的源碼地址:https://github.com/Brooke181/CoreAPI_Demo
下一篇介紹Dapper在.NET Core中的使用,謝謝支持!
往期精彩回顧
【推薦】.NET Core開發(fā)實(shí)戰(zhàn)視頻課程?★★★
.NET Core實(shí)戰(zhàn)項(xiàng)目之CMS 第一章 入門篇-開篇及總體規(guī)劃
【.NET Core微服務(wù)實(shí)戰(zhàn)-統(tǒng)一身份認(rèn)證】開篇及目錄索引
Redis基本使用及百億數(shù)據(jù)量中的使用技巧分享(附視頻地址及觀看指南)
.NET Core中的一個(gè)接口多種實(shí)現(xiàn)的依賴注入與動(dòng)態(tài)選擇看這篇就夠了
10個(gè)小技巧助您寫出高性能的ASP.NET Core代碼
用abp vNext快速開發(fā)Quartz.NET定時(shí)任務(wù)管理界面
在ASP.NET Core中創(chuàng)建基于Quartz.NET托管服務(wù)輕松實(shí)現(xiàn)作業(yè)調(diào)度
現(xiàn)身說法:實(shí)際業(yè)務(wù)出發(fā)分析百億數(shù)據(jù)量下的多表查詢優(yōu)化
關(guān)于C#異步編程你應(yīng)該了解的幾點(diǎn)建議
