本文通過.net core 3.1 分享 web api 基礎知識,其他更新版本相差不離
一、前言
隨著近幾年前後端分離、微服務等模式的興起,.net core 也似有如火如荼之勢 ,自 16 年發布第一個版本到 19 年底的 3.1 lts 版本,以及將發布的.net 5,.net core 一路更迭,在部署和開發工具上也都支持了跨平台應用。一直對.net core 有所關注,但未涉及太多實際應用,經過一番學習和了解後,於是分享出來。本文主要以.net core web api 為例,講述.net core 的基本應用及注意事項,對於想通過 webapi 搭建接口應用的開發者,應該能提供一個系統的輪廓和認識,同時和更多的.net core 開發者交流互動,探本勘誤,加強對知識的理解,並幫助更多的人。本文以貼近基本的實際操作為主,部分概念或基礎步驟不再贅述,文中如有疏漏,還望不吝斧正。
二、swagger 調試 web api
開發環境:visual studio 2019
為解決前後端苦於接口文檔與實際不一致、維護和更新文檔的耗時費力等問題,swagger 應運而生,同時也解決了接口測試問題。話不多說,直接說明應用步驟。
- 新建一個 asp.net core web api 應用程式,版本選擇.asp.net core 3.1;
- 通過 nuget 安裝包:swashbuckle.aspnetcore,當前示例版本 5.5.0;
- 在 startup 類的 configureservices 方法內添加以下注入代碼:
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 文件的方式繼續調整代碼如下,新增代碼見加粗部分:
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 內的配置參數用於文檔的一些描述,在此不作過多說明。
然後右鍵 web api 項目、屬性、生成,配置 xml 文檔的輸出路徑,以及取消不必要的 xml 注釋警告提醒(增加 1591):

這樣,我們以三斜槓(///)方式給類方法屬性等相關代碼添加注釋後,刷新 swagger 頁面,即可看到注釋說明。
如果不想將 xml 文件輸出為 debug 下的目錄,譬如想要放在項目根目錄(但不要修改成磁碟絕對路徑),可調整相關代碼如下,xml 文件的名字也可以改成自己想要的:
var basePath = Path.GetDirectoryName(typeof(Program).Assembly.Location);//获取应用程序所在目录
var xmlPath = Path.Combine(basePath, "CoreAPI_Demo.xml");
c.IncludeXmlComments(xmlPath, true);
同時,調整項目生成的 xml 文檔文件路徑為:..\ coreapi_demo\coreapi_demo.xml
隱藏相關接口
對於不想暴漏給 swagger 展示的接口,我們可以給相關 controller 或 action 頭加上:[apiexplorersettings(ignoreapi = true)]
調整系統默認輸出路徑
項目啟動後,默認會訪問自帶的 weatherforecast,如果想調整為其他路徑,譬如打開後直接訪問 swagger 文檔,那麼調整 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 文件內容如下:
{
"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": "*"
}
- 配置文件的基本讀取
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 內使用,類似地,進行注入並調用如下:
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" };
}
}
- 讀取配置文件到自定義對象
以 systemconfig 節點為例,定義類如下:
public class SystemConfig
{
public string UploadPath { get; set; }
public string Domain { get; set; }
}
調整代碼如下:
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 內進行注入調用:
[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" };
}
}
- 綁定到靜態類方式讀取
定義相關靜態類如下:
public static class MySettings
{
public static SystemConfig Setting { get; set; } = new SystemConfig();
}
調整 startup 類構造函數如下:
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);//绑定静态配置类
}
接下來,諸如直接使用:mysettings.setting.uploadpath 即可調用。
四、文件上傳
接口一般少不了文件上傳,相比.net framework 框架下 webapi 通過 byte 數組對象等複雜方式進行文件上傳,.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();
//注:参数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;
}
}
前端調用
接下來通過前端調用上述上傳接口,在項目根目錄新建 wwwroot 目錄(.net core webapi 內置目錄 ),添加相關 js 文件包,然後新建一個 index.html 文件,內容如下:
<!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 兩個參數的設置;另外允許一次上傳多個文件,需設置 multipart 屬性。
在訪問 wwwroot 下的靜態文件之前,必須先在 startup 類的 configure 方法下進行註冊:
public void Configure(IApplicationBuilder app)
{
app.UseStaticFiles();//用于访问wwwroot下的文件
}
啟動項目,通過訪問路徑:http://localhost:***/index.html,進行上傳測試,成功後,將在 wwwroot 下的 files 目錄下看到上傳的文件。
五、統一 webapi 數據返回格式
定義統一返回格式
為了方便前後端使用約定好的數據格式,通常我們會定義統一的數據返回,其包括是否成功、返回狀態、具體數據等;為便於說明,定義一個數據返回類如下:
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:定義數據返回的方式,給每個 controller 打上[produces("application/json")]標識,即表示以 json 方式進行數據輸出。
- apicontroller:確保每個 controller 有 apicontroller 標識,通常,我們會定義一個基類如:basecontroller,其繼承自 controllerbase,並將其打上[apicontroller]標識,新建的 controller 都繼承該類;
- route:路由訪問方式,如不喜歡 restful 方式,可加上 action,即:[route("api/[controller]/[action]")];
- http 請求:結合前面配置的 swagger,必須確保每個 action 都有具體的請求方式,即必須是 httpget、httppost、httpput、httpdelete 中的一種,通常情況下,我們使用 httpget、httppost 足以。
如此,即完成的數據返回的統一。
解決 t 時間格式
net core web api 默認以首字母小寫的類駝峰式命名返回,但遇到 datetime 類型的數據,會返回 t 格式時間,如要解決 t 時間格式,定義一個時間格式轉換類如下:
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 中調整 services.addcontrollers 代碼如下:
services.AddControllers()
.AddJsonOptions(configure =>
{
configure.JsonSerializerOptions.Converters.Add(new DatetimeJsonConverter());
});
六、模型驗證
模型驗證在 asp.net mvc 已存在,使用方式基本一致。指對向接口提交過來的數據進行參數校驗,包括必填項、數據格式、字符長度、範圍等等。一般的,我們會將 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 限制欄位的長度,當然還有其他一些內置特性,具體可參考官方文檔,列舉一些常見的驗證特性如下:
- [creditcard]:驗證屬性是否具有信用卡格式。需要 jquery 驗證其他方法。
- [compare]:驗證模型中的兩個屬性是否匹配。
- [emailaddress]:驗證屬性是否具有電子郵件格式。
- [phone]:驗證屬性是否具有電話號碼格式。
- [range]:驗證屬性值是否在指定的範圍內。
- [regularexpression]:驗證屬性值是否與指定的正則表達式匹配。
- [required]:驗證欄位是否不為 null。有關此屬性的行為的詳細信息,請參閱 [required] 特性。
- [stringlength]:驗證字符串屬性值是否不超過指定長度限制。
- [Url]:驗證屬性是否具有 url 格式。
- [remote]:通過在伺服器上調用操作方法來驗證客戶端上的輸入。
上述說明了基本的模型驗證使用方法,以這種方式,同時結合 t4 模板,通過表對象生成模型驗證實體,省卻了在 action 中編寫大量驗證代碼的工作。當然,一些必要的較為複雜的驗證,或結合資料庫操作的驗證,則單獨寫到 action 或其他應用模塊中。
那麼上述模型驗證在 web api 中是怎麼工作的呢?在 startup 類的 configureservices 添加如下代碼:
//模型参数验证
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 對請求參數的驗證工作,關於錯誤信息 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内部日志记录为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-->
③ 調整 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 函數裡的捕獲異常代碼配置省略也是可以的,createhostbuilder 下的 usenlog 為必設項。
controller 通過注入調用如下:
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 服務並用於調用 getall 方法:
public class ProductController : ControllerBase
{
private readonly IProductRepository _productRepository;
public ProductController(IProductRepository productRepository)
{
_productRepository = productRepository;
}
public IEnumerable<Product> Get()
{
return _productRepository.GetAll();
}
}
通過使用 di 模式,來實現 iproductrepository 接口。其實前述已多次出現通過構造函數進行注入調用的示例。
生命周期
services.AddScoped<IMyDependency, MyDependency>();
services.AddTransient<IMyDependency, MyDependency>();
services.AddSingleton<IMyDependency, MyDependency>();
- transient:每一次請求都會創建一個新實例;
- scoped:每個作用域生成周期內創建一個實例;
- singleton:單例模式,整個應用程式生命周期內只創建一個實例;
這裡,需要根據具體的業務邏輯場景需求選擇注入相應的生命周期服務。
實際應用中,我們會有很多個服務需要註冊到 configureservices 內,一個個寫入顯然繁瑣,而且容易忘記漏寫,一般地,我們可能會想到利用反射進行批量注入,並通過擴展的方式進行注入,譬如:
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();//批量注册服务
}
誠然,這樣配合系統自帶 di 注入是能完成我們的批量注入需求的。但其實也有更多選擇,來幫我們簡化 di 註冊,譬如選擇其他第三方組件:scrutor、autofac…
- 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 的其他用法,大家可以參見官方文檔,這裡只做下引子。
- Autofac
一般情況下,使用 ms 自帶的 di 或採用 scrutor,即可滿足實際需要,如果有更高的應用需求,如要求屬性注入、甚至接管或取代 ms 自帶的 di,那麼你可以選擇 autofac,關於 autofac 的具體使用,在此不作詳敘。
九、緩存
memorycache 使用
按官方說明,開發人員需合理說用緩存,以及限制緩存大小,core 運行時不會根據內容壓力限制緩存大小。對於使用方式,依舊還是先行註冊,然後控制器調用:
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>
///设置缓存,如果不主动清空,会一直保存在内存中.
/// </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>
///根据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;
}
}
}
十、異常處理
定義異常處理中間件
這裡主要針對全局異常進行捕獲處理並記錄日誌,並以統一的 json 格式返回給接口調用者;說異常處理前先提下中間件,關於什麼是中間件,在此不在贅述,一個中間件其基本的結構如下:
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 方法中,為方便開發和測試,這裡將系統的錯誤返回給了接口調用者,實際生產環境中可統一返回固定的錯誤 message 消息。
異常狀態碼的處理
关于 http 状态码,常见的如正常返回的 200,其他 401、403、404、502 等等等等,因为系统有时候并不总是返回 200 成功,对于返回非 200 的异常状态码,WebApi 也要做到相应的处理,以便接口调用者能正确接收,譬如紧接下来的JWT认证,当认证令牌过期或没有权限时,系统实际会返回 401、403,但接口并不提供有效的可接收的返回,因此,这里列举一些常见的异常状态码,并以 200 方式提供给接口调用者,在 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
}));
});
代碼很簡單,這裡使用系統自帶的異常處理中間件 usestatuscodepages,當然,你還可以自定義過濾器處理異常,不過不推薦,簡單高效直接才是需要的。
關於.net core 的異常處理中間件,還有其他諸如 useexceptionhandler、usestatuscodepageswithredirects 等等,不同的中間件有其適用的環境,有的可能更適用於 mvc 或其他應用場景上,找到合適的即可。
題外話:大家也可以將 usestatuscodepages 處理異常狀態碼的操作封裝到前述的全局異常處理中間件中。
十一、應用安全與 jwt 認證
關於什麼是 jwt,在此不作贅述。實際應用中,為了部分接口的安全性,譬如需要身份認證才能訪問的接口資源,對於 web api 而言,一般會採用 token 令牌進行認證,服務端結合緩存來實現。
那為什麼要選擇 jwt 認證呢?原因無外乎以下:服務端不進行保存、無狀態、適合移動端、適合分布式、標準化等等。關於 jwt 的使用如下:
通過 nugget 安裝包:microsoft.aspnetcore.authentication.jwtbearer,當前示例版本 3.1.5;
configureservices 進行注入,默認以 bearer 命名,這裡你也可以改成其他名字,保持前後一致即可,注意加粗部分,代碼如下:
appsettings.json 添加 jwt 配置節點(見前述【配置文件】),添加 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; }
}
採用前述綁定靜態類的方式讀取 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);//绑定静态配置类
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 頁面會出現鎖的標識,獲取 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授权(数据将在请求头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 並返回,前端調用者拿到 token 後以諸如 localstorage 方式進行存儲,調取授權接口時,添加該 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 進行配置;當然,這裡只是簡單的示例說明角色服務的應用,複雜的可通過註冊策略服務,並結合資料庫進行動態配置。
這樣,一個簡單的基於 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
下一篇居間 dapper 在.net core 中的使用,謝謝支持!