網(wǎng)站首頁 編程語言 正文
一、前言
隨著近幾年前后端分離、微服務等模式的興起,.Net Core也似有如火如荼之勢 ,自16年發(fā)布第一個版本到19年底的3.1 LTS版本,以及將發(fā)布的.NET 5,.NET Core一路更迭,在部署和開發(fā)工具上也都支持了跨平臺應用。一直對.Net Core有所關注,但未涉及太多實際應用,經(jīng)過一番學習和了解后,于是分享出來。本文主要以.Net Core Web API為例,講述.Net Core的基本應用及注意事項,對于想通過WebAPI搭建接口應用的開發(fā)者,應該能提供一個系統(tǒng)的輪廓和認識,同時和更多的.Net Core開發(fā)者交流互動,探本勘誤,加強對知識的理解,并幫助更多的人。本文以貼近基本的實際操作為主,部分概念或基礎步驟不再贅述,文中如有疏漏,還望不吝斧正。
二、Swagger調(diào)試Web API
開發(fā)環(huán)境:Visual Studio 2019
為解決前后端苦于接口文檔與實際不一致、維護和更新文檔的耗時費力等問題,swagger應運而生,同時也解決了接口測試問題。話不多說,直接說明應用步驟。
- 新建一個ASP.NET Core Web API應用程序,版本選擇.ASP.NET Core 3.1;
- 通過Nuget安裝包:Swashbuckle.AspNetCore,當前示例版本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 = "測試項目",
//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";// 如果設為空,訪問路徑就是根域名/index.html,設置為空,表示直接在根域名訪問;想換一個路徑,直接寫名字即可,比如直接寫c.RoutePrefix = "swagger"; 則訪問路徑為 根域名/swagger/index.html
});
Ctrl+F5進入瀏覽,按上述配置修改路徑為:http://localhost:***/api/index.html,即可看到Swagger頁面:
然而到這里還沒完,相關接口的注釋說明我們看不到,通過配置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 = "測試項目",
//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項目相匹配的XML文件名,AppContext.BaseDirectory屬性用于構造 XML 文件的路徑,關于OpenApiInfo內(nèi)的配置參數(shù)用于文檔的一些描述,在此不作過多說明。
然后右鍵Web API項目、屬性、生成,配置XML文檔的輸出路徑,以及取消不必要的XML注釋警告提醒(增加1591):
這樣,我們以三斜杠(///)方式給類方法屬性等相關代碼添加注釋后,刷新Swagger頁面,即可看到注釋說明。
如果不想將XML文件輸出為debug下的目錄,譬如想要放在項目根目錄(但不要修改成磁盤絕對路徑),可調(diào)整相關代碼如下,xml文件的名字也可以改成自己想要的:
var basePath = Path.GetDirectoryName(typeof(Program).Assembly.Location);//獲取應用程序所在目錄
var xmlPath = Path.Combine(basePath, "CoreAPI_Demo.xml");
c.IncludeXmlComments(xmlPath, true);
同時,調(diào)整項目生成的XML文檔文件路徑為:..\CoreAPI_Demo\CoreAPI_Demo.xml
4.隱藏相關接口
對于不想暴漏給Swagger展示的接口,我們可以給相關Controller或Action頭加上:[ApiExplorerSettings(IgnoreApi = true)]
5.調(diào)整系統(tǒng)默認輸出路徑
項目啟動后,默認會訪問自帶的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文件為例,當然你也定義其他名稱的.json文件進行讀取,讀取方式一致,該文件類似于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)使用,類似地,進行注入并調(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、讀取配置文件到自定義對象
以SystemConfig節(jié)點為例,定義類如下:
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)進行注入調(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)類方式讀取
定義相關靜態(tài)類如下:
public static class MySettings
{
public static SystemConfig Setting { get; set; } = new SystemConfig();
}
調(diào)整Startup類構造函數(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ù)組對象等復雜方式進行文件上傳,.Net Core WebApi有了很大變化,其定義了新的IFormFile對象來接收上傳文件,直接上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對象去也可以通過換成: 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);//相對路徑
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)用上述上傳接口,在項目根目錄新建wwwroot目錄(.net core webapi內(nèi)置目錄 ),添加相關js文件包,然后新建一個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的構建也可以換成: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("上傳文件錯誤");
}
});
}
//方法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>
上述通過構建FormData和ajaxSubmit兩種方式進行上傳,需要注意的是contentType和processData兩個參數(shù)的設置;另外允許一次上傳多個文件,需設置multipart屬性。
在訪問wwwroot下的靜態(tài)文件之前,必須先在Startup類的Configure方法下進行注冊:
public void Configure(IApplicationBuilder app)
{
app.UseStaticFiles();//用于訪問wwwroot下的文件
}
啟動項目,通過訪問路徑:http://localhost:***/index.html,進行上傳測試,成功后,將在wwwroot下的Files目錄下看到上傳的文件。
五、統(tǒng)一WebApi數(shù)據(jù)返回格式
定義統(tǒng)一返回格式
為了方便前后端使用約定好的數(shù)據(jù)格式,通常我們會定義統(tǒng)一的數(shù)據(jù)返回,其包括是否成功、返回狀態(tài)、具體數(shù)據(jù)等;為便于說明,定義一個數(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>();
}
這樣,我們將每一個action接口操作封裝為ApiResult格式進行返回。新建一個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ù)返回的方式,給每個Controller打上[Produces("application/json")]標識,即表示以json方式進行數(shù)據(jù)輸出。
- ApiController:確保每個Controller有ApiController標識,通常,我們會定義一個基類如:BaseController,其繼承自ControllerBase,并將其打上[ApiController]標識,新建的controller都繼承該類;
- Route:路由訪問方式,如不喜歡RESTful方式,可加上Action,即:[Route("api/[controller]/[action]")];
- HTTP 請求:結合前面配置的Swagger,必須確保每個Action都有具體的請求方式,即必須是HttpGet、HttpPost、HttpPut、HttpDelete中的一種,通常情況下,我們使用HttpGet、HttpPost足以。
- 如此,即完成的數(shù)據(jù)返回的統(tǒng)一。
解決T時間格式
.Net Core Web Api默認以首字母小寫的類駝峰式命名返回,但遇到DateTime類型的數(shù)據(jù),會返回T格式時間,如要解決T時間格式,定義一個時間格式轉(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());
});
六、模型驗證
模型驗證在ASP.NET MVC已存在,使用方式基本一致。指對向接口提交過來的數(shù)據(jù)進行參數(shù)校驗,包括必填項、數(shù)據(jù)格式、字符長度、范圍等等。一般的,我們會將POST提交過來的對象定義為一個實體類進行接收,譬如定義一個注冊類如下:
public class RegisterEntity
{
/// <summary>
/// 手機號
/// </summary>
[Display(Name = "手機號")]
[Required(ErrorMessage = "{0}不能為空")]
[StringLength(11, ErrorMessage = "{0}最多{1}個字符")]
public string Mobile { get; set; }
/// <summary>
/// 驗證碼
/// </summary>
[Display(Name = "驗證碼")]
[Required(ErrorMessage = "{0}不能為空")]
[StringLength(6, ErrorMessage = "{0}最多{1}個字符")]
public string Code { get; set; }
/// <summary>
/// 密碼
/// </summary>
[Display(Name = "密碼")]
[Required(ErrorMessage = "{0}不能為空")]
[StringLength(16, ErrorMessage = "{0}最多{1}個字符")]
public string Pwd { get; set; }
}
Display標識提示字段的名稱,Required表示必填,StringLength限制字段的長度,當然還有其他一些內(nèi)置特性,具體可參考官方文檔,列舉一些常見的驗證特性如下:
- [CreditCard]:驗證屬性是否具有信用卡格式。 需要 JQuery 驗證其他方法。
- [Compare]:驗證模型中的兩個屬性是否匹配。
- [EmailAddress]:驗證屬性是否具有電子郵件格式。
- [Phone]:驗證屬性是否具有電話號碼格式。
- [Range]:驗證屬性值是否在指定的范圍內(nèi)。
- [RegularExpression]:驗證屬性值是否與指定的正則表達式匹配。
- [Required]:驗證字段是否不為 null。 有關此屬性的行為的詳細信息,請參閱 [Required] 特性。
- [StringLength]:驗證字符串屬性值是否不超過指定長度限制。
- [Url]:驗證屬性是否具有 URL 格式。
- [Remote]:通過在服務器上調(diào)用操作方法來驗證客戶端上的輸入。
- 上述說明了基本的模型驗證使用方法,以這種方式,同時結合T4模板,
那么上述模型驗證在Web API中是怎么工作的呢?在Startup類的ConfigureServices添加如下代碼:
//模型參數(shù)驗證
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 });
};
});
添加注冊示例Action代碼:
/// <summary>
/// 注冊
/// </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 = "驗證碼過期或不存在";
return result;
}
if (!model.Code.Equals(_code.ToString()))
{
result.Message = "驗證碼錯誤";
return result;
}
/**
相關邏輯代碼
**/
return result;
}
如此,通過配置ApiBehaviorOptions的方式,并讀取驗證錯誤信息的第一條信息并返回,即完成了Web API中Action對請求參數(shù)的驗證工作,關于錯誤信息Message的返回,也可略作封裝,在此略。
七、日志使用
雖然.Net Core WebApi有自帶的日志管理功能,但不一定能較容易地滿足我們的需求,通常會采用第三方日志框架,典型的如:NLog、Log4Net,簡單介紹NLog日志組件的使用;
NLog的使用
① 通過NuGet安裝包:NLog.Web.AspNetCore,當前項目版本4.9.2;
② 項目根目錄新建一個NLog.config文件,關鍵NLog.config的其他詳細配置,可參考官方文檔,這里作簡要配置如下;
<?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關閉--> <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> <!--跳過所有級別的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為必設項。
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("測試一條日志");
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();
}
本地測試后,即可在debug下看到logs目錄下生成的日志文件。
八、依賴注入
使用.Net Core少不了和依賴注入打交道,這也是.Net Core的設計思想之一,關于什么是依賴注入(DI),以及為什么要使用依賴注入,這里不再贅述,先來看一個簡單示例的依賴注入。
public interface IProductRepository
{
IEnumerable<Product> GetAll();
}
public class ProductRepository : IProductRepository
{
public IEnumerable<Product> GetAll()
{
}
}
Startup類進行注冊:
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IProductRepository, ProductRepository>();
}
請求 IProductRepository 服務并用于調(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模式,來實現(xiàn)IProductRepository 接口。其實前述已多次出現(xiàn)通過構造函數(shù)進行注入調(diào)用的示例。
生命周期
services.AddScoped<IMyDependency, MyDependency>();
services.AddTransient<IMyDependency, MyDependency>();
services.AddSingleton<IMyDependency, MyDependency>();
- Transient:每一次請求都會創(chuàng)建一個新實例;
- Scoped:每個作用域生成周期內(nèi)創(chuàng)建一個實例;
- Singleton:單例模式,整個應用程序生命周期內(nèi)只創(chuàng)建一個實例;
這里,需要根據(jù)具體的業(yè)務邏輯場景需求選擇注入相應的生命周期服務。
實際應用中,我們會有很多個服務需要注冊到ConfigureServices內(nèi),一個個寫入顯然繁瑣,而且容易忘記漏寫,一般地,我們可能會想到利用反射進行批量注入,并通過擴展的方式進行注入,譬如:
public static class AppServiceExtensions
{
/// <summary>
/// 注冊應用程序域中的服務
/// </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();//批量注冊服務
}
誠然,這樣配合系統(tǒng)自帶DI注入是能完成我們的批量注入需求的。但其實也有更多選擇,來幫我們簡化DI注冊,譬如選擇其他第三方組件:Scrutor、Autofac…
1、Scrutor的使用
Scrutor是基于微軟注入組件的一個擴展庫,簡單示例如下:
services.Scan(scan => scan
.FromAssemblyOf<Startup>()
.AddClasses(classes => classes.Where(s => s.Name.EndsWith("Repository") || s.Name.EndsWith("Service")))
.AsImplementedInterfaces()
.WithTransientLifetime()
);
以上代碼通過Scan方式批量注冊了以Repository、Service結尾的接口服務,其生命周期為Transient,該方式等同于前述的以反射方式的批量注冊服務。
關于Scrutor的其他用法,大家可以參見官方文檔,這里只做下引子。
2、Autofac
一般情況下,使用MS自帶的DI或采用Scrutor,即可滿足實際需要,如果有更高的應用需求,如要求屬性注入、甚至接管或取代MS自帶的DI,那么你可以選擇Autofac,關于Autofac的具體使用,在此不作詳敘。
九、緩存
MemoryCache使用
按官方說明,開發(fā)人員需合理說用緩存,以及限制緩存大小,Core運行時不會根據(jù)內(nèi)容壓力限制緩存大小。對于使用方式,依舊還是先行注冊,然后控制器調(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;
}
}
上述代碼緩存了一個時間,并設置了滑動過期時間(指最后一次訪問后的過期時間)為3秒;如果需要設置絕對過期時間,將SetSlidingExpiration 改為SetAbsoluteExpiration即可。瀏覽刷新,每3秒后時間將更新。
附一個封裝好的Cache類如下:
public class CacheHelper
{
public static IMemoryCache _memoryCache = new MemoryCache(new MemoryCacheOptions());
/// <summary>
/// 緩存絕對過期時間
/// </summary>
///<param name="key">Cache鍵</param>
///<param name="value">緩存的值</param>
///<param name="minute">minute分鐘后絕對過期</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>
/// 緩存相對過期,最后一次訪問后minute分鐘后過期
/// </summary>
///<param name="key">Cache鍵</param>
///<param name="value">緩存的值</param>
///<param name="minute">滑動過期分鐘</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>
///設置緩存,如果不主動清空,會一直保存在內(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值返回泛型對象
/// </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;
}
}
}
十、異常處理
定義異常處理中間件
這里主要針對全局異常進行捕獲處理并記錄日志,并以統(tǒng)一的json格式返回給接口調(diào)用者;說異常處理前先提下中間件,關于什么是中間件,在此不在贅述,一個中間件其基本的結構如下:
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>
/// 以擴展方式添加中間件
/// </summary>
public static class CustomExceptionMiddlewareExtensions
{
public static IApplicationBuilder UseCustomExceptionMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<CustomExceptionMiddleware>();
}
}
然后在Startup類的Configure方法里添加上述擴展的中間件,見加粗部分:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
//全局異常處理
app.UseCustomExceptionMiddleware();
}
在HandleExceptionAsync方法中,為方便開發(fā)和測試,這里將系統(tǒng)的錯誤返回給了接口調(diào)用者,實際生產(chǎn)環(huán)境中可統(tǒng)一返回固定的錯誤Message消息。
異常狀態(tài)碼的處理
關于http狀態(tài)碼,常見的如正常返回的200,其他401、403、404、502等等等等,因為系統(tǒng)有時候并不總是返回200成功,對于返回非200的異常狀態(tài)碼,WebApi也要做到相應的處理,以便接口調(diào)用者能正確接收,譬如緊接下來的JWT認證,當認證令牌過期或沒有權限時,系統(tǒng)實際會返回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 => "未找到",
_ => "未知錯誤",
};
context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
await context.HttpContext.Response.WriteAsync(Newtonsoft.Json.JsonConvert.SerializeObject(new
{
isSuccess = false,
code,
message
}));
});
代碼很簡單,這里使用系統(tǒng)自帶的異常處理中間件UseStatusCodePages,當然,你還可以自定義過濾器處理異常,不過不推薦,簡單高效直接才是需要的。
關于.NET Core的異常處理中間件,還有其他諸如 UseExceptionHandler、UseStatusCodePagesWithRedirects等等,不同的中間件有其適用的環(huán)境,有的可能更適用于MVC或其他應用場景上,找到合適的即可。
題外話:大家也可以將UseStatusCodePages處理異常狀態(tài)碼的操作封裝到前述的全局異常處理中間件中。
十一、應用安全與JWT認證
關于什么是JWT,在此不作贅述。實際應用中,為了部分接口的安全性,譬如需要身份認證才能訪問的接口資源,對于Web API而言,一般會采用token令牌進行認證,服務端結合緩存來實現(xiàn)。
那為什么要選擇JWT認證呢?原因無外乎以下:服務端不進行保存、無狀態(tài)、適合移動端、適合分布式、標準化等等。關于JWT的使用如下:
通過NuGget安裝包:Microsoft.AspNetCore.Authentication.JwtBearer,當前示例版本3.1.5;
ConfigureServices進行注入,默認以Bearer命名,這里你也可以改成其他名字,保持前后一致即可,注意加粗部分,代碼如下:
appsettings.json添加JWT配置節(jié)點(見前述【配置文件】),添加JWT相關認證類:
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配置,并進行注入:
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認證注入
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認證支持,完成后,Swagger頁面會出現(xiàn)鎖的標識,獲取token后填入Value(Bearer token形式)項進行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 = "測試項目",
//Url = new Uri("http://t.abc.com/")
},
License = new OpenApiLicense
{
Name = "BROOKE許可證",
//Url = new Uri("http://t.abc.com/")
}
});
// 為 Swagger JSON and UI設置xml文檔注釋路徑
//var basePath = Path.GetDirectoryName(typeof(Program).Assembly.Location);//獲取應用程序所在目錄(不受工作目錄影響)
//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認證Swagger授權
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = "JWT授權(數(shù)據(jù)將在請求頭header中進行傳輸) 直接在下框中輸入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注冊,注意,需放到 app.UseAuthorization();前面:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseAuthentication();//jwt認證
app.UseAuthorization();
}
這樣,JWT就基本配置完畢,接下來實施認證登錄和授權,模擬操作如下:
[HttpPost]
public async Task<ApiResult> Login(LoginEntity model)
{
ApiResult result = new ApiResult();
//驗證用戶名和密碼
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);
//更新最后登錄時間
await _memberService.UpdateLastLoginTime(userInfo.MemberID);
result.IsSuccess= 1;
result.ResultData["token"] = jwtToken;
result.Message = "授權成功!";
return result;
}
上述代碼模擬登錄操作(賬號密碼登錄,成功后設置有效期一天),生成token并返回,前端調(diào)用者拿到token后以諸如localstorage方式進行存儲,調(diào)取授權接口時,添加該token到header(Bearer token)進行接口請求。接下來,給需要身份授權的Controller或Action打上Authorize標識:
[Authorize]
[Route("api/[controller]/[action]")]
public class UserController : ControllerBase
{
}
如果要添加基于角色的授權,可限制操作如下:
[Authorize(Roles = "user")]
[Route("api/[controller]/[action]")]
public class UserController : ControllerBase
{
}
//多個角色也可以逗號分隔
[Authorize(Roles = "Administrator,Finance")]
[Route("api/[controller]/[action]")]
public class UserController : ControllerBase
{
}
不同的角色信息,可通過登錄設置ClaimTypes.Role進行配置;當然,這里只是簡單的示例說明角色服務的應用,復雜的可通過注冊策略服務,并結合數(shù)據(jù)庫進行動態(tài)配置。
這樣,一個簡單的基于JWT認證授權的工作就完成了。
十二、跨域
前后端分離,會涉及到跨域問題,簡單的支持跨域操作如下:
添加擴展支持
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類添加相關注冊如下:
public void ConfigureServices(IServiceCollection services)
{
services.ConfigureCors();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseCors("CorsPolicy");//跨域
}
這樣,一個簡單跨域操作就完成了,你也可以通過設置WithOrigins、WithMethods等方法限制請求地址來源和請求方式。
本篇涉及到的源碼地址:https://github.com/Brooke181/CoreAPI_Demo
原文鏈接:https://www.cnblogs.com/lhxsoft/p/16133545.html
- 上一篇:C#集合之有序列表的用法_C#教程
- 下一篇:C#集合之集(set)的用法_C#教程
相關推薦
- 2022-03-17 .NET?Core使用C#掃描并讀取圖片中的文字_C#教程
- 2022-08-15 如何實現(xiàn)響應式(自適應)網(wǎng)頁
- 2024-03-24 spring boot 3 + spring cloud sleuth 無法注入Tracer問題
- 2022-09-18 C++如何實現(xiàn)二叉樹鏈表_C 語言
- 2022-12-27 pytorch?K折交叉驗證過程說明及實現(xiàn)方式_python
- 2022-07-04 C#使用System.Buffer以字節(jié)數(shù)組Byte[]操作基元類型數(shù)據(jù)_C#教程
- 2022-09-16 Pandas篩選DataFrame含有空值的數(shù)據(jù)行的實現(xiàn)_python
- 2022-07-06 C語言深入探究水仙花數(shù)與變種水仙花數(shù)代碼_C 語言
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細win安裝深度學習環(huán)境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結構-簡單動態(tài)字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支