This article shares the basics of Web APIs through. NET Core 3.1, and other updated versions are similar
1. Preface
With the rise of front-end separation, microservices and other models in recent years,. NET Core seems to be in full swing. From the first version released in 16 to the 3.1 LTS version at the end of 19, as well as the upcoming release of. NET 5,. NET Core has been changing all the way, and cross-platform applications are also supported in deployment and development tools. I have always paid attention to. NET Core, but I didn't involve too many practical applications. After some learning and understanding, I shared it. This article mainly takes the. NET Core Web API as an example to describe the basic applications and matters needing attention of. NET Core. For developers who want to build interface applications through the Web API, they should be able to provide a system outline and understanding, and at the same time communicate with more. NET Core developers. Interact, explore errors, strengthen understanding of knowledge, and help more people. This article focuses on practical operations that are close to the basics. Some concepts or basic steps will not be described in detail. If there are any omissions in the article, we hope to correct them freely.
2. Swagger debugging the Web API
Development environment: Visual Studio 2019
In order to solve the problems that the front and back sides suffer from inconsistent interface documents with the actual situation, and the time-consuming and laborious maintenance and updating of documents, swagger came into being, and also solved the problem of interface testing. Without saying much, I will directly explain the application steps.
- Create a new ASP.NET Core Web API application with version selection.ASP.NET Core 3.1;
- Install the package via NuGet: Swashbuckle.AspNetCore, current example version 5.5.0;
- Add the following injection code to the ConfigureServices method of the Startup class:
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/")
}
});
});
Add the following code to the Configure method of the Startup class:
//配置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 enters browsing, and according to the above configuration, modify the path to: http://localhost:***/api/index.html, you will see the Swagger page:

However, it's not over yet. The comments on the relevant interfaces indicate that we can't see it. Continue to adjust the code by configuring the XML file as follows. See the bold part for the new code:
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);
});
The above code generates an XML file name that matches the Web API project through reflection. The AppContext.BaseDirectory attribute is used to construct the path of the XML file. Some descriptions of the configuration parameters in OpenApiInfo used in the document will not be explained too much here.
Then right click on the Web API project, properties, and generation, configure the output path of the XML document, and cancel unnecessary XML comment warnings (add 1591):

In this way, after we add comments to related code such as class method attributes in a triple slash (//) manner, we refresh the Swagger page, and you can see the comments.
If you don't want to output the XML file as a directory under debug, for example, if you want to put it in the project root directory (but don't change it to the absolute path to the disk), you can adjust the relevant code as follows, and you can also change the name of the XML file to what you want:
var basePath = Path.GetDirectoryName(typeof(Program).Assembly.Location);//获取应用程序所在目录
var xmlPath = Path.Combine(basePath, "CoreAPI_Demo.xml");
c.IncludeXmlComments(xmlPath, true);
At the same time, the path of the XML document file generated by the adjustment project is: ..\ CoreAPI_Demo\CoreAPI_Demo.xml
** Hide related interfaces **
For interfaces that we don't want to expose to Swagger, we can add to the relevant Controller or Action header: [ApiExploreSettings (IgnoreApi = true)]
** Adjust the system default output path **
After the project is started, it will access the built-in weatherforecast by default. If you want to adjust it to another path, such as directly accessing the Swagger document after opening it, adjust the launchSettings.json file in the Properties directory and modify the launchUrl value to api (RoutePrefix value configured above):
{
"$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"
}
}
}
}
3. Configuration File
Take reading the appsettings.json file as an example. Of course, you can also define a.json file with other names to read, and the reading method is consistent. This file is similar to the Web.config file. For convenience of example, the content of the appsettings.json file is defined as follows:
{
"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": "*"
}
- Basic reading of configuration files
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"];
}
}
The above describes two ways to read configuration information. If you want to use it in the Controller, similarly inject it and call it as follows:
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" };
}
}
- Read configuration files to custom objects
Taking the SystemConfig node as an example, the class is defined as follows:
public class SystemConfig
{
public string UploadPath { get; set; }
public string Domain { get; set; }
}
The adjustment codes are as follows:
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"));
}
}
Then an injection call is made in the 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" };
}
}
- Read by binding to a static class
Define relevant static classes as follows:
public static class MySettings
{
public static SystemConfig Setting { get; set; } = new SystemConfig();
}
Adjust the Startup class constructor as follows:
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);//绑定静态配置类
}
Next, you can call it directly by using: MySettings.Seting.UploadPath.
4. File upload
Interface generally requires file uploading. Compared to webapi under the. net framework, which uploads files through complex methods such as byte array objects, the. NET Core WebApi has undergone great changes. It defines a new IFormFile object to receive uploaded files and directly upload them to the Controller code:
** Back-end code **
[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;
}
}
** Front-end call **
Next, by calling the above upload interface by the front end, creating a new wwwroot directory (. net core webapi built-in directory) in the project root directory, adding relevant js file packages, and then creating a new index.html file, the content is as follows:
<!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>
The above upload is carried out by building FormData and ajaxSubmit. What needs to be paid attention to is the setting of the two parameters contentType and processData; in addition, multiple files are allowed to be uploaded at one time, and the multipart attribute needs to be set.
Before accessing static files under wwwroot, you must first register under the Configure method of the Startup class:
public void Configure(IApplicationBuilder app)
{
app.UseStaticFiles();//用于访问wwwroot下的文件
}
Start the project and use the access path: http://localhost:***/index.html. After success, you will see the uploaded file in the Files directory under wwwroot.
5. Unified WebApi data return format
** Define unified return format **
In order to facilitate the use of the agreed data format between the front and back ends, we usually define a unified data return, which includes success, return status, specific data, etc.; for ease of explanation, a data return class is defined as follows:
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>();
}
In this way, we encapsulate each action interface operation into ApiResult format and return it. Create a new ProductController example as follows:
[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: Define the data return method and mark each Controller with the [Produces("application/json")] flag, which means that data is output in json method.
- ApiController: Ensure that each Controller has the ApiController identity. Generally, we will define a base class such as BaseController, which inherits from ControllerBase and marks it with the [ApiController] identity. All newly created controllers inherit this class;
- Route: Route access method. If you don't like the RESTful method, you can add Action, that is: [Route("api/[controller]/[action]")];
- HTTP request: Combined with the Swagger configured previously, we must ensure that each Action has a specific request method, that is, it must be one of HttpGet, HttpPost, HttpPut, and HttpDelete. Under normal circumstances, it is enough for us to use HttpGet and HttpPost.
In this way, the unification of data return is completed.
** Solve the T time format **
By default,. NET Core Web Api returns with a hump-like initial and lower-case name. However, when encountering data of type DateTime, it will return T-format time. To solve the T-time format, define a time format conversion class as follows:
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"));
}
}
Then adjust the services.AddControllers code in ConfigureServices of the Startup class as follows:
services.AddControllers()
.AddJsonOptions(configure =>
{
configure.JsonSerializerOptions.Converters.Add(new DatetimeJsonConverter());
});
6. Model verification
Model verification already exists in ASP.NET MVC and is used in basically the same way. refers to parameter verification of data submitted to the interface, including required items, data format, character length, range, etc. Generally, we will define the object submitted by POST as an entity class to receive. For example, we will define a registration class as follows:
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 identifies the name of the prompt field, Required means required, StringLength limits the length of the field, and of course there are other built-in features. For details, please refer to the official document. Some common verification features are listed as follows:
- [CreditCard]: Verify that the attribute has a credit card format. JQuery is required to verify other methods.
- [Compare]: Verify that two attributes in the model match.
- [EmailAddress]: Verify that the attribute has an email format.
- [Phone]: Verify that the attribute has a phone number format.
- [Range]: Verify that the attribute value is within the specified range.
- [RegularExpression]: Verify that the property value matches the specified regular expression.
- [Required]: Verify that the field is not null. For more information about the behavior of this attribute, see the [Required] attribute.
- [StringLength]: Verify that the string attribute value does not exceed the specified length limit.
- [Url]: Verify that the attribute has URL format.
- [Remote]: Verify input on the client by calling action methods on the server.
The above explains the basic method of using model verification. In this way, combined with the T4 template, model verification entities are generated through table objects, eliminating the need to write a lot of verification code in action. Of course, some necessary complex verifications, or verifications combined with database operations, are written separately into actions or other application modules.
So how does the above model verification work in the Web API? Add the following code to ConfigureServices in the Startup class:
//模型参数验证
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 });
};
});
Add registration example Action code:
/// <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;
}
In this way, by configuring ApiBehaviorOptions, reading the first message of the verification error message and returning it, the verification of the request parameters by Action in the Web API is completed. The return of the error message Message can also be slightly encapsulated, so I will omit it here.
7. Log use
Although. NET Core WebApi has its own log management functions, it may not be easy to meet our needs. It usually adopts third-party logging frameworks, such as NLog and Log4Net, which briefly introduces the use of the NLog logging component;
** Use of NLog **
① Install the package through NuGet: NLog.Web.AspNetCore, current project version 4.9.2;
② Create a new NLog.config file in the root directory of the project. For other detailed configurations of key NLog.config, please refer to the official documentation. The brief configuration here is as follows;
<?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-->
③ Adjust the Program.cs file as follows;
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;
}
Among them, it is also possible to omit the code configuration for capturing exceptions in the Main function, and the UserNLog under CreateHostBuilder is a mandatory item.
Controller calls via injection as follows:
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();
}
After testing locally, you can see the log files generated in the logs directory under debug.
8. Dependency injection
Using. NET Core is indispensable to deal with dependency injection, which is also one of the design ideas of. NET Core. I won't go into detail here about what dependency injection (DI) is and why dependency injection should be used. Let's first look at a simple example of dependency injection.
public interface IProductRepository
{
IEnumerable<Product> GetAll();
}
public class ProductRepository : IProductRepository
{
public IEnumerable<Product> GetAll()
{
}
}
Register for the Startup class:
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IProductRepository, ProductRepository>();
}
Request the IProductRepository service and use it to call the GetAll method:
public class ProductController : ControllerBase
{
private readonly IProductRepository _productRepository;
public ProductController(IProductRepository productRepository)
{
_productRepository = productRepository;
}
public IEnumerable<Product> Get()
{
return _productRepository.GetAll();
}
}
The IProductRepository interface is implemented by using the DI pattern. In fact, there have been many examples of injection calls made through constructors.
** Life cycle **
services.AddScoped<IMyDependency, MyDependency>();
services.AddTransient<IMyDependency, MyDependency>();
services.AddSingleton<IMyDependency, MyDependency>();
- Transient: Each request creates a new instance;
- Scoped: Create one instance during each scope generation cycle;
- Singleton: Singleton model, where only one instance is created throughout the application life cycle;
Here, it is necessary to select and inject corresponding life cycle services according to the requirements of specific business logic scenarios.
In actual applications, we will have many services that need to be registered in ConfigureServices. Writing one by one is obviously cumbersome, and it is easy to forget that writing is omitted. Generally, we may think of using reflection to inject in batches and inject in an extended way. For example:
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();//批量注册服务
}
It is true that this cooperation with the system's own DI injection can fulfill our batch injection requirements. But there are actually more options to help us simplify DI registration, such as choosing other third-party components: Scrutor, Autofac...
- Use of Scrutor
Scrutor is an extension library based on Microsoft injection components. A simple example is as follows:
services.Scan(scan => scan
.FromAssemblyOf<Startup>()
.AddClasses(classes => classes.Where(s => s.Name.EndsWith("Repository") || s.Name.EndsWith("Service")))
.AsImplementedInterfaces()
.WithTransientLifetime()
);
The above code batches registration of interface services ending in Repository and Service through Scan method, and its life cycle is Transient, which is equivalent to the above-mentioned batch registration of services in reflection method.
For other uses of Scrutor, you can refer to the official documentation. Here is just a primer.
- Autofac
Under normal circumstances, using the DI that comes with MS or using Scrutor can meet actual needs. If there are higher application requirements, such as requiring attribute injection, or even taking over or replacing the DI that comes with MS, then you can choose Autofac. The specific use of Autofac will not be described in detail here.
9. Cache
**MemoryCache usage **
According to official instructions, developers need to reasonably use cache and limit the cache size. Core runtime will not limit the cache size based on content pressure. For the usage method, it is still still necessary to register first, and then the controller calls:
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;
}
}
The above code caches it for a time and sets the sliding expiration time (the expiration time after the last access) to 3 seconds; if you need to set the absolute expiration time, change SetSlidingExpiration to SetAbsoluteExpiration. Browsing is refreshed, and the time will be updated every 3 seconds.
Attached is an encapsulated Cache class as follows:
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;
}
}
}
10. Exception handling
** Define exception handling middleware ***
Here, we mainly capture and process global exceptions and record logs, and return them to the interface caller in a unified json format; before talking about exception handling, we first mention middleware. What is middleware, I won't go into detail here. The basic structure of a middleware is as follows:
public class CustomMiddleware
{
private readonly RequestDelegate _next;
public CustomMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext httpContext)
{
await _next(httpContext);
}
}
Let's define our own global exception handling middleware, with the following code:
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>();
}
}
Then add the above extended middleware to the Configure method of the Startup class, see the bold part:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
//全局异常处理
app.UseCustomExceptionMiddleware();
}
In the HandleExceptionAsync method, for convenience of development and testing, system errors are returned to the interface caller. In the actual production environment, a fixed error Message message can be returned uniformly.
** Handling of exception status codes **
关于 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
}));
});
The code is very simple. We use the system's own exception handling middleware UseStatusCodePages. Of course, you can also customize the filter to handle exceptions, but it is not recommended. Simple, efficient and straightforward are needed.
Regarding. NET Core's exception handling middleware, there are other middleware such as UseExceptionHandler, UseStatusCodePagesWithRedirects, etc. Different middleware have their own suitable environments, and some may be more suitable for MVC or other application scenarios. Just find the right one.
Off-topic: You can also encapsulate UseStatusCodePages 'operation of processing exception status codes into the aforementioned global exception handling middleware.
11. Application security and JWT certification
I will not go into details about what JWT is here. In practical applications, for the security of some interfaces, such as interface resources that require identity authentication to access, for Web APIs, token tokens are generally used for authentication, and the server combines caching to achieve it.
So why choose JWT certification? The reasons are as follows: the server does not save, is stateless, is suitable for mobile, is suitable for distribution, is standardized, etc. About the use of JWT is as follows:
Install the package via NuGget: Microsoft.AspNetCore.Authentication.JwtBearer, current example version 3.1.5;
ConfigureServices injects, and it is named after Bearer by default. You can also change it to another name here and keep it consistent. Pay attention to the bold part, and the code is as follows:
appsettings.json Add JWT configuration nodes (see the above [Configuration File]) and add JWT-related certification classes:
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; }
}
Use the above-mentioned method of binding static classes to read the JWT configuration and inject it:
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
}
Add JWT authentication support to Swagger. After completion, the lock identification will appear on the Swagger page. After obtaining the token, fill in the Value (in the form of a Bearer token) item to Authorize login. See the bold part for Swagger to configure 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
});
Add Configure registration to the Starup class. Note that it needs to be placed in front of app.UseAuthorization();:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseAuthentication();//jwt认证
app.UseAuthorization();
}
In this way, JWT is basically configured, and then authentication login and authorization are implemented. The simulation operations are as follows:
[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;
}
The above code simulates the login operation (login with an account password, and the validity period is set for one day after success), generates a token and returns it. After the front-end caller gets the token, it stores it in a way such as localstorage. When calling the authorization interface, it adds the token to the header (Bearer token) to make an interface request. Next, mark the Controller or Action that requires identity authorization with Authorize:
[Authorize]
[Route("api/[controller]/[action]")]
public class UserController : ControllerBase
{
}
If you want to add role-based authorization, you can limit the following actions:
[Authorize(Roles = "user")]
[Route("api/[controller]/[action]")]
public class UserController : ControllerBase
{
}
//多个角色也可以逗号分隔
[Authorize(Roles = "Administrator,Finance")]
[Route("api/[controller]/[action]")]
public class UserController : ControllerBase
{
}
Different role information can be configured by logging in and setting ClaimTypes.Role; of course, this is just a simple example to illustrate the application of role services, and complex ones can be dynamically configured by registering policy services and combining them with the database.
In this way, a simple job based on JWT certification and authorization is completed.
12. Cross-domain
Separation of front and back ends will involve cross-domain issues. Simple cross-domain operations are as follows:
Add extended support
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();
//}));
}
}
Add relevant registrations to the Startup class as follows:
public void ConfigureServices(IServiceCollection services)
{
services.ConfigureCors();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseCors("CorsPolicy");//跨域
}
In this way, a simple cross-domain operation is completed, and you can also limit the source of the request address and the request method by setting WithOrigins, WithMethods and other methods.
至此,全篇结束,本篇涉及到的源码地址:https://github.com/Brooke181/CoreAPI_Demo
The next article introduces the use of Dapper in. NET Core, thank you for your support!