一、前言

  随着近几年前后端分离、微服务等模式的兴起,.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应运而生,同时也解决了接口测试问题。话不多说,直接说明应用步骤。

  1. 新建一个ASP.NET Core Web API应用程序,版本选择.ASP.NET Core 3.1;
  2. 通过Nuget安装包:Swashbuckle.AspNetCore,当前示例版本5.5.0;
  3. 在Startup类的ConfigureServices方法内添加以下注入代码:
    1. services.AddSwaggerGen(c =>
    2. {
    3. c.SwaggerDoc("v1", new OpenApiInfo
    4. {
    5. Title = "My API",
    6. Version = "v1",
    7. Description = "API文档描述",
    8. Contact = new OpenApiContact
    9. {
    10. Email = "5007032@qq.com",
    11. Name = "测试项目",
    12. //Url = new Uri("http://t.abc.com/")
    13. },
    14. License = new OpenApiLicense
    15. {
    16. Name = "BROOKE许可证",
    17. //Url = new Uri("http://t.abc.com/")
    18. }
    19. });
    20.  
    21. });

    Startup类的Configure方法添加如下代码:

    1. //配置Swagger
    2. app.UseSwagger();
    3. app.UseSwaggerUI(c =>
    4. {
    5. c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
    6. c.RoutePrefix = "api";// 如果设为空,访问路径就是根域名/index.html,设置为空,表示直接在根域名访问;想换一个路径,直接写名字即可,比如直接写c.RoutePrefix = "swagger"; 则访问路径为 根域名/swagger/index.html
    7.  
    8. });

    Ctrl+F5进入浏览,按上述配置修改路径为:http://localhost:***/api/index.html,即可看到Swagger页面:

    然而到这里还没完,相关接口的注释说明我们看不到,通过配置XML文件的方式继续调整代码如下,新增代码见加粗部分:

    1. services.AddSwaggerGen(c =>
    2. {
    3. c.SwaggerDoc("v1", new OpenApiInfo
    4. {
    5. Title = "My API",
    6. Version = "v1",
    7. Description = "API文档描述",
    8. Contact = new OpenApiContact
    9. {
    10. Email = "5007032@qq.com",
    11. Name = "测试项目",
    12. //Url = new Uri("http://t.abc.com/")
    13. },
    14. License = new OpenApiLicense
    15. {
    16. Name = "BROOKE许可证",
    17. //Url = new Uri("http://t.abc.com/")
    18. }
    19. });
    20.  
    21. var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    22. var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
    23. c.IncludeXmlComments(xmlPath);
    24. });

    上述代码通过反射生成与Web API项目相匹配的XML文件名,AppContext.BaseDirectory属性用于构造 XML 文件的路径,关于OpenApiInfo内的配置参数用于文档的一些描述,在此不作过多说明。
    然后右键Web API项目、属性、生成,配置XML文档的输出路径,以及取消不必要的XML注释警告提醒(增加1591):

    这样,我们以三斜杠(///)方式给类方法属性等相关代码添加注释后,刷新Swagger页面,即可看到注释说明。
    如果不想将XML文件输出为debug下的目录,譬如想要放在项目根目录(但不要修改成磁盘绝对路径),可调整相关代码如下,xml文件的名字也可以改成自己想要的:

    1. var basePath = Path.GetDirectoryName(typeof(Program).Assembly.Location);//获取应用程序所在目录
    2. var xmlPath = Path.Combine(basePath, "CoreAPI_Demo.xml");
    3. c.IncludeXmlComments(xmlPath, true);

    同时,调整项目生成的XML文档文件路径为:..\CoreAPI_Demo\CoreAPI_Demo.xml

  4. 隐藏相关接口
    对于不想暴漏给Swagger展示的接口,我们可以给相关Controller或Action头加上:[ApiExplorerSettings(IgnoreApi = true)]
  5. 调整系统默认输出路径
    项目启动后,默认会访问自带的weatherforecast,如果想调整为其他路径,譬如打开后直接访问Swagger文档,那么调整Properties目录下的launchSettings.json文件,修改launchUrl值为api(前述配置的RoutePrefix值):
    1. {
    2. "$schema": "http://json.schemastore.org/launchsettings.json",
    3. "iisSettings": {
    4. "windowsAuthentication": false,
    5. "anonymousAuthentication": true,
    6. "iisExpress": {
    7. "applicationUrl": "http://localhost:7864",
    8. "sslPort": 0
    9. }
    10. },
    11. "profiles": {
    12. "IIS Express": {
    13. "commandName": "IISExpress",
    14. "launchBrowser": true,
    15. "launchUrl": "api",
    16. "environmentVariables": {
    17. "ASPNETCORE_ENVIRONMENT": "Development"
    18. }
    19. },
    20. "CoreApi_Demo": {
    21. "commandName": "Project",
    22. "launchBrowser": true,
    23. "launchUrl": "api",
    24. "applicationUrl": "http://localhost:5000",
    25. "environmentVariables": {
    26. "ASPNETCORE_ENVIRONMENT": "Development"
    27. }
    28. }
    29. }
    30. }

三、配置文件

以读取appsettings.json文件为例,当然你也定义其他名称的.json文件进行读取,读取方式一致,该文件类似于Web.config文件。为方便示例,定义appsettings.json文件内容如下:

  1. {
  2. "ConnString": "Data Source=(local);Initial Catalog=Demo;Persist Security Info=True;User ID=DemoUser;Password=123456;MultipleActiveResultSets=True;",
  3. "ConnectionStrings": {
  4. "MySQLConnection": "server=127.0.0.1;database=mydemo;uid=root;pwd=123456;charset=utf8;SslMode=None;"
  5. },
  6. "SystemConfig": {
  7. "UploadFile": "/Files",
  8. "Domain": "http://localhost:7864"
  9. },
  10. "JwtTokenConfig": {
  11. "Secret": "fcbfc8df1ee52ba127ab",
  12. "Issuer": "abc.com",
  13. "Audience": "Brooke.WebApi",
  14. "AccessExpiration": 30,
  15. "RefreshExpiration": 60
  16. },
  17. "Logging": {
  18. "LogLevel": {
  19. "Default": "Information",
  20. "Microsoft": "Warning",
  21. "Microsoft.Hosting.Lifetime": "Information"
  22. }
  23. },
  24. "AllowedHosts": "*"
  25. }

1、配置文件的基本读取

  1. public class Startup
  2. {
  3. public Startup(IConfiguration configuration)
  4. {
  5. Configuration = configuration;
  6. }
  7.  
  8. public IConfiguration Configuration { get; }
  9.  
  10. // This method gets called by the runtime. Use this method to add services to the container.
  11. public void ConfigureServices(IServiceCollection services)
  12. {
  13. services.AddControllers();
  14.  
  15. //读取方式一
  16. var ConnString = Configuration["ConnString"];
  17. var MySQLConnection = Configuration.GetSection("ConnectionStrings")["MySQLConnection"];
  18. var UploadPath = Configuration.GetSection("SystemConfig")["UploadPath"];
  19. var LogDefault = Configuration.GetSection("Logging").GetSection("LogLevel")["Default"];
  20.  
  21. //读取方式二
  22. var ConnString2 = Configuration["ConnString"];
  23. var MySQLConnection2 = Configuration["ConnectionStrings:MySQLConnection"];
  24. var UploadPath2 = Configuration["SystemConfig:UploadPath"];
  25. var LogDefault2 = Configuration["Logging:LogLevel:Default"];
  26.  
  27. }
  28.  
  29. }

以上介绍了2种读取配置信息的方式,如果要在Controller内使用,类似地,进行注入并调用如下:

  1. public class ValuesController : ControllerBase
  2. {
  3. private IConfiguration _configuration;
  4.  
  5. public ValuesController(IConfiguration configuration)
  6. {
  7. _configuration = configuration;
  8. }
  9.  
  10. // GET: api/<ValuesController>
  11. [HttpGet]
  12. public IEnumerable<string> Get()
  13. {
  14. var ConnString = _configuration["ConnString"];
  15. var MySQLConnection = _configuration.GetSection("ConnectionStrings")["MySQLConnection"];
  16. var UploadPath = _configuration.GetSection("SystemConfig")["UploadPath"];
  17. var LogDefault = _configuration.GetSection("Logging").GetSection("LogLevel")["Default"];
  18. return new string[] { "value1", "value2" };
  19. }
  20. }

2、读取配置文件到自定义对象

以SystemConfig节点为例,定义类如下:

  1. public class SystemConfig
  2. {
  3. public string UploadPath { get; set; }
  4. public string Domain { get; set; }
  5.  
  6. }

调整代码如下:

  1. public class Startup
  2. {
  3. public Startup(IConfiguration configuration)
  4. {
  5. Configuration = configuration;
  6. }
  7.  
  8. public IConfiguration Configuration { get; }
  9.  
  10. // This method gets called by the runtime. Use this method to add services to the container.
  11. public void ConfigureServices(IServiceCollection services)
  12. {
  13. services.AddControllers();
  14.  
  15. services.Configure<SystemConfig>(Configuration.GetSection("SystemConfig"));
  16. }
  17.  
  18. }

然后Controller内进行注入调用:

  1. [Route("api/[controller]/[action]")]
  2. [ApiController]
  3. public class ValuesController : ControllerBase
  4. {
  5. private SystemConfig _sysConfig;
  6. public ValuesController(IOptions<SystemConfig> sysConfig)
  7. {
  8. _sysConfig = sysConfig.Value;
  9. }
  10.  
  11. [HttpGet]
  12. public IEnumerable<string> GetSetting()
  13. {
  14. var UploadPath = _sysConfig.UploadPath;
  15. var Domain = _sysConfig.Domain;
  16. return new string[] { "value1", "value2" };
  17. }
  18. }

3、绑定到静态类方式读取

定义相关静态类如下:

  1.   public static class MySettings
  2. {
  3. public static SystemConfig Setting { get; set; } = new SystemConfig();
  4. }

调整Startup类构造函数如下:

  1. public Startup(IConfiguration configuration, IWebHostEnvironment env)
  2. {
  3. var builder = new ConfigurationBuilder()
  4. .SetBasePath(env.ContentRootPath)
  5. .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
  6.  
  7. Configuration = builder.Build();
  8. //Configuration = configuration;

  9. configuration.GetSection("SystemConfig").Bind(MySettings.Setting);//绑定静态配置类
  10. }

接下来,诸如直接使用:MySettings.Setting.UploadPath 即可调用。

四、文件上传

接口一般少不了文件上传,相比.net framework框架下webapi通过byte数组对象等复杂方式进行文件上传,.Net Core WebApi有了很大变化,其定义了新的IFormFile对象来接收上传文件,直接上Controller代码:

后端代码

  1. [Route("api/[controller]/[action]")]
  2. [ApiController]
  3. public class UploadController : ControllerBase
  4. {
  5. private readonly IWebHostEnvironment _env;
  6.  
  7. public UploadController(IWebHostEnvironment env)
  8. {
  9. _env = env;
  10. }
  11.  
  12. public ApiResult UploadFile(List<IFormFile> files)
  13. {
  14. ApiResult result = new ApiResult();

  15.        //注:参数files对象去也可以通过换成: var files = Request.Form.Files;来获取

  16. if (files.Count <= 0)
  17. {
  18. result.Message = "上传文件不能为空";
  19. return result;
  20. }
  21.  
  22. #region 上传
  23.  
  24. List<string> filenames = new List<string>();
  25.  
  26. var webRootPath = _env.WebRootPath;
  27. var rootFolder = MySettings.Setting.UploadPath;
  28.  
  29. var physicalPath = $"{webRootPath}/{rootFolder}/";
  30.  
  31. if (!Directory.Exists(physicalPath))
  32. {
  33. Directory.CreateDirectory(physicalPath);
  34. }
  35.  
  36. foreach (var file in files)
  37. {
  38. var fileExtension = Path.GetExtension(file.FileName);//获取文件格式,拓展名
  39.  
  40. var saveName = $"{rootFolder}/{Path.GetRandomFileName()}{fileExtension}";
  41. filenames.Add(saveName);//相对路径
  42.  
  43. var fileName = webRootPath + saveName;
  44.  
  45. using FileStream fs = System.IO.File.Create(fileName);
  46. file.CopyTo(fs);
  47. fs.Flush();
  48.  
  49. }
  50. #endregion
  51.  
  52. result.IsSuccess = true;
  53. result.Data["files"] = filenames;
  54.  
  55. return result;
  56. }
  57. }

前端调用

接下来通过前端调用上述上传接口,在项目根目录新建wwwroot目录(.net core webapi内置目录 ),添加相关js文件包,然后新建一个index.html文件,内容如下:

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8" />
  5. <title></title>
  6. <style type="text/css">
  7.  
  8. </style>
  9. <script src="res/scripts/jquery-1.10.2.min.js"></script>
  10. <script src="res/scripts/jquery.form.js"></script>
  11. <script type="text/javascript">
  12. //方法1
  13. function AjaxUploadfile() {
  14. var upload = $("#files").get(0);
  15. var files = upload.files;
  16. var data = new FormData();
  17. for (var i = 0; i < files.length; i++) {
  18. data.append("files", files[i]);
  19. }
  20.  
  21. //此处data的构建也可以换成:var data = new FormData(document.getElementById("myform"));
  22.  
  23. $.ajax({
  24. type: "POST",
  25. url: "/api/upload/uploadfile",
  26. contentType: false,
  27. processData: false,
  28. data: data,
  29. success: function (result) {
  30. alert("success");
  31. $.each(result.data.files, function (i, filename) {
  32. $("#filePanel").append('<p>' + filename + '</p>');
  33. });
  34. },
  35. error: function () {
  36. alert("上传文件错误");
  37. }
  38. });
  39. }
  40.  
  41. //方法2
  42. function AjaxUploadfile2() {
  43. $("#myform").ajaxSubmit({
  44. success: function (result) {
  45. if (result.isSuccess) {
  46. $.each(result.data.files, function (i, filename) {
  47. $("#filePanel").append('<p>' + filename + '</p>');
  48. });
  49. }
  50. else {
  51. alert(result.message);
  52. }
  53. }
  54. });
  55. }
  56.  
  57. </script>
  58. </head>
  59. <body>
  60. <form id="myform" method="post" action="/api/upload/uploadfile" enctype="multipart/form-data">
  61. <input type="file" id="files" name="files" multiple /> <br /><br />
  62. <input type="button" value="FormData Upload" onclick="AjaxUploadfile();" /><br /><br />
  63. <input type="button" value="ajaxSubmit Upload" onclick="AjaxUploadfile2();" /><br /><br />
  64. <div id="filePanel"></div>
  65. </form>
  66.  
  67. <script type="text/javascript">
  68.  
  69. $(function () {
  70.  
  71. });
  72.  
  73. </script>
  74. </body>
  75. </html>

上述通过构建FormData和ajaxSubmit两种方式进行上传,需要注意的是contentType和processData两个参数的设置;另外允许一次上传多个文件,需设置multipart属性。

在访问wwwroot下的静态文件之前,必须先在Startup类的Configure方法下进行注册:

  1. public void Configure(IApplicationBuilder app)
  2. {
  3. app.UseStaticFiles();//用于访问wwwroot下的文件
  4. }

启动项目,通过访问路径:http://localhost:***/index.html,进行上传测试,成功后,将在wwwroot下的Files目录下看到上传的文件。

五、统一WebApi数据返回格式

定义统一返回格式

为了方便前后端使用约定好的数据格式,通常我们会定义统一的数据返回,其包括是否成功、返回状态、具体数据等;为便于说明,定义一个数据返回类如下:

  1. public class ApiResult
  2. {
  3. public bool IsSuccess { get; set; }
  4. public string Message { get; set; }
  5. public string Code { get; set; }
  6. public Dictionary<string, object> Data { get; set; } = new Dictionary<string, object>();
  7. }

这样,我们将每一个action接口操作封装为ApiResult格式进行返回。新建一个ProductController示例如下:

  1. [Produces("application/json")]
  2. [Route("api/[controller]")]
  3. [ApiController]
  4. public class ProductController : ControllerBase
  5. {
  6. [HttpGet]
  7. public ApiResult Get()
  8. {
  9. var result = new ApiResult();
  10.  
  11. var rd = new Random();
  12.  
  13. result.Data["dataList"] = Enumerable.Range(1, 5).Select(index => new
  14. {
  15. Name = $"商品-{index}",
  16. Price = rd.Next(100, 9999)
  17. });
  18.  
  19. result.IsSuccess = true;
  20. return result;
  21. }
  22. }
  • 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时间格式,定义一个时间格式转换类如下:

  1. public class DatetimeJsonConverter : JsonConverter<DateTime>
  2. {
  3. public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
  4. {
  5. if (reader.TokenType == JsonTokenType.String)
  6. {
  7. if (DateTime.TryParse(reader.GetString(), out DateTime date))
  8. return date;
  9. }
  10. return reader.GetDateTime();
  11. }
  12.  
  13. public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
  14. {
  15. writer.WriteStringValue(value.ToString("yyyy-MM-dd HH:mm:ss"));
  16. }
  17. }

然后在Startup类的ConfigureServices中调整services.AddControllers代码如下:

  1. services.AddControllers()
  2. .AddJsonOptions(configure =>
  3. {
  4. configure.JsonSerializerOptions.Converters.Add(new DatetimeJsonConverter());
  5. });

六、模型验证

模型验证在ASP.NET MVC已存在,使用方式基本一致。指对向接口提交过来的数据进行参数校验,包括必填项、数据格式、字符长度、范围等等。一般的,我们会将POST提交过来的对象定义为一个实体类进行接收,譬如定义一个注册类如下:

  1. public class RegisterEntity
  2. {
  3. /// <summary>
  4. /// 手机号
  5. /// </summary>
  6. [Display(Name = "手机号")]
  7. [Required(ErrorMessage = "{0}不能为空")]
  8. [StringLength(11, ErrorMessage = "{0}最多{1}个字符")]
  9. public string Mobile { get; set; }
  10.  
  11. /// <summary>
  12. /// 验证码
  13. /// </summary>
  14. [Display(Name = "验证码")]
  15. [Required(ErrorMessage = "{0}不能为空")]
  16. [StringLength(6, ErrorMessage = "{0}最多{1}个字符")]
  17. public string Code { get; set; }
  18.  
  19. /// <summary>
  20. /// 密码
  21. /// </summary>
  22. [Display(Name = "密码")]
  23. [Required(ErrorMessage = "{0}不能为空")]
  24. [StringLength(16, ErrorMessage = "{0}最多{1}个字符")]
  25. public string Pwd { get; set; }
  26. }

  Display标识提示字段的名称,Required表示必填,StringLength限制字段的长度,当然还有其他一些内置特性,具体可参考官方文档,列举一些常见的验证特性如下:

  • [CreditCard]:验证属性是否具有信用卡格式。 需要 JQuery 验证其他方法。
  • [Compare]:验证模型中的两个属性是否匹配。
  • [EmailAddress]:验证属性是否具有电子邮件格式。
  • [Phone]:验证属性是否具有电话号码格式。
  • [Range]:验证属性值是否在指定的范围内。
  • [RegularExpression]:验证属性值是否与指定的正则表达式匹配。
  • [Required]:验证字段是否不为 null。 有关此属性的行为的详细信息,请参阅 [Required] 特性。
  • [StringLength]:验证字符串属性值是否不超过指定长度限制。
  • [Url]:验证属性是否具有 URL 格式。
  • [Remote]:通过在服务器上调用操作方法来验证客户端上的输入。

  上述说明了基本的模型验证使用方法,以这种方式,同时结合T4模板,通过表对象生成模型验证实体,省却了在action中编写大量验证代码的工作。当然,一些必要的较为复杂的验证,或结合数据库操作的验证,则单独写到action或其他应用模块中。

  那么上述模型验证在Web API中是怎么工作的呢?在Startup类的ConfigureServices添加如下代码:

  1. //模型参数验证
  2. services.Configure<ApiBehaviorOptions>(options =>
  3. {
  4. options.InvalidModelStateResponseFactory = (context) =>
  5. {
  6. var error = context.ModelState.FirstOrDefault().Value;
  7. var message = error.Errors.FirstOrDefault(p => !string.IsNullOrWhiteSpace(p.ErrorMessage))?.ErrorMessage;
  8.  
  9. return new JsonResult(new ApiResult { Message = message });
  10. };
  11. });

  添加注册示例Action代码:

  1. /// <summary>
  2. /// 注册
  3. /// </summary>
  4. /// <param name="model"></param>
  5. /// <returns></returns>
  6. [HttpPost]
  7. public async Task<ApiResult> Register(RegisterEntity model)
  8. {
  9. ApiResult result = new ApiResult();
  10.  
  11. var _code = CacheHelper.GetCache(model.Mobile);
  12. if (_code == null)
  13. {
  14. result.Message = "验证码过期或不存在";
  15. return result;
  16. }
  17. if (!model.Code.Equals(_code.ToString()))
  18. {
  19. result.Message = "验证码错误";
  20. return result;
  21. }
  22.  
  23. /**
  24. 相关逻辑代码
  25. **/
  26. return result;
  27. }

  如此,通过配置ApiBehaviorOptions的方式,并读取验证错误信息的第一条信息并返回,即完成了Web API中Action对请求参数的验证工作,关于错误信息Message的返回,也可略作封装,在此略。

七、日志使用

虽然.Net Core WebApi有自带的日志管理功能,但不一定能较容易地满足我们的需求,通常会采用第三方日志框架,典型的如:NLog、Log4Net,简单介绍NLog日志组件的使用;

NLog的使用

① 通过NuGet安装包:NLog.Web.AspNetCore,当前项目版本4.9.2;

② 项目根目录新建一个NLog.config文件,关键NLog.config的其他详细配置,可参考官方文档,这里作简要配置如下;

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. autoReload="true"
  5. throwExceptions="false"
  6. internalLogLevel="Off"
  7. internalLogFile="NlogRecords.log">
  8. <!--Nlog内部日志记录为Off关闭-->
  9. <extensions>
  10. <add assembly="NLog.Web.AspNetCore" />
  11. </extensions>
  12. <targets>
  13. <target name="log_file" xsi:type="File" fileName="${basedir}/logs/${shortdate}.log"
  14. layout="${longdate} | ${level:uppercase=false} | ${message} ${onexception:${exception:format=tostring} ${newline} ${stacktrace} ${newline}" />
  15. </targets>
  16.  
  17. <rules>
  18. <!--跳过所有级别的Microsoft组件的日志记录-->
  19. <logger name="Microsoft.*" final="true" />
  20. <!--<logger name="logdb" writeTo="log_database" />-->
  21. <logger name="*" minlevel="Trace" writeTo="log_file" />
  22.  
  23. </rules>
  24. </nlog>
  25.  
  26. <!--https://github.com/NLog/NLog/wiki/Getting-started-with-ASP.NET-Core-3-->

③ 调整Program.cs文件如下;

  1. public class Program
  2. {
  3. public static void Main(string[] args)
  4. {
  5. //CreateHostBuilder(args).Build().Run();
  6.        
  7. var logger = NLog.Web.NLogBuilder.ConfigureNLog("nlog.config").GetCurrentClassLogger();
  8. try
  9. {
  10. logger.Debug("init main");
  11. CreateHostBuilder(args).Build().Run();
  12. }
  13. catch (Exception exception)
  14. {
  15. //NLog: catch setup errors
  16. logger.Error(exception, "Stopped program because of exception");
  17. throw;
  18. }
  19. finally
  20. {
  21. // Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux)
  22. NLog.LogManager.Shutdown();
  23. }
  24. }
  25.  
  26. public static IHostBuilder CreateHostBuilder(string[] args) =>
  27. Host.CreateDefaultBuilder(args)
  28. .ConfigureWebHostDefaults(webBuilder =>
  29. {
  30. webBuilder.UseStartup<Startup>();
  31. }).ConfigureLogging(logging => {
  32. logging.ClearProviders();
  33. logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
  34. }).UseNLog();//依赖注入Nlog;
  35. }

其中Main函数里的捕获异常代码配置省略也是可以的,CreateHostBuilder下的UseNLog为必设项。

Controller通过注入调用如下:

  1. public class WeatherForecastController : ControllerBase
  2. {
  3. private static readonly string[] Summaries = new[]
  4. {
  5. "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
  6. };
  7.  
  8. private readonly ILogger<WeatherForecastController> _logger;
  9.  
  10. public WeatherForecastController(ILogger<WeatherForecastController> logger)
  11. {
  12. _logger = logger;
  13. }
  14.  
  15. [HttpGet]
  16. public IEnumerable<WeatherForecast> Get()
  17. {
  18. _logger.LogInformation("测试一条日志");
  19. var rng = new Random();
  20. return Enumerable.Range(1, 5).Select(index => new WeatherForecast
  21. {
  22. Date = DateTime.Now.AddDays(index),
  23. TemperatureC = rng.Next(-20, 55),
  24. Summary = Summaries[rng.Next(Summaries.Length)]
  25. })
  26. .ToArray();
  27. }

本地测试后,即可在debug下看到logs目录下生成的日志文件。

八、依赖注入

使用.Net Core少不了和依赖注入打交道,这也是.Net Core的设计思想之一,关于什么是依赖注入(DI),以及为什么要使用依赖注入,这里不再赘述,先来看一个简单示例的依赖注入。

  1. public interface IProductRepository
  2. {
  3. IEnumerable<Product> GetAll();
  4. }
  5.  
  6. public class ProductRepository : IProductRepository
    {
  7. public IEnumerable<Product> GetAll()
    {
  8.  
  9. }
  10. }

Startup类进行注册:

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. services.AddScoped<IProductRepository, ProductRepository>();
  4. }

请求 IProductRepository 服务并用于调用 GetAll 方法:

  1. public class ProductController : ControllerBase
  2. {
  3. private readonly IProductRepository _productRepository;
  1.    public ProductController(IProductRepository productRepository)
    {
  2. _productRepository = productRepository;
    }
  3.  
  4. public IEnumerable<Product> Get()
    {
  5. return _productRepository.GetAll();
    }
  6. }

通过使用DI模式,来实现IProductRepository 接口。其实前述已多次出现通过构造函数进行注入调用的示例。

生命周期  

  1. services.AddScoped<IMyDependency, MyDependency>();
  2. services.AddTransient<IMyDependency, MyDependency>();
  3. services.AddSingleton<IMyDependency, MyDependency>();
  • Transient:每一次请求都会创建一个新实例;
  • Scoped:每个作用域生成周期内创建一个实例;
  • Singleton:单例模式,整个应用程序生命周期内只创建一个实例;

这里,需要根据具体的业务逻辑场景需求选择注入相应的生命周期服务。

实际应用中,我们会有很多个服务需要注册到ConfigureServices内,一个个写入显然繁琐,而且容易忘记漏写,一般地,我们可能会想到利用反射进行批量注入,并通过扩展的方式进行注入,譬如:

  1. public static class AppServiceExtensions
  2. {
  3. /// <summary>
  4. /// 注册应用程序域中的服务
  5. /// </summary>
  6. /// <param name="services"></param>
  7. public static void AddAppServices(this IServiceCollection services)
  8. {
  9. var ts = System.Reflection.Assembly.Load("CoreAPI.Data").GetTypes().Where(s => s.Name.EndsWith("Repository") || s.Name.EndsWith("Service")).ToArray();
  10. foreach (var item in ts.Where(s => !s.IsInterface))
  11. {
  12. var interfaceType = item.GetInterfaces();
  13. foreach (var typeArray in interfaceType)
  14. {
  15. services.AddTransient(typeArray, item);
  16. }
  17. }
  18. }
  19. }
  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. services.AddAppServices();//批量注册服务
  4. }

诚然,这样配合系统自带DI注入是能完成我们的批量注入需求的。但其实也有更多选择,来帮我们简化DI注册,譬如选择其他第三方组件:Scrutor、Autofac…

1、Scrutor的使用

Scrutor是基于微软注入组件的一个扩展库,简单示例如下:

  1. services.Scan(scan => scan
  2. .FromAssemblyOf<Startup>()
  3. .AddClasses(classes => classes.Where(s => s.Name.EndsWith("Repository") || s.Name.EndsWith("Service")))
    .AsImplementedInterfaces()
  4. .WithTransientLifetime()
  5. );

以上代码通过Scan方式批量注册了以Repository、Service结尾的接口服务,其生命周期为Transient,该方式等同于前述的以反射方式的批量注册服务。

关于Scrutor的其他用法,大家可以参见官方文档,这里只做下引子。

2、Autofac

一般情况下,使用MS自带的DI或采用Scrutor,即可满足实际需要,如果有更高的应用需求,如要求属性注入、甚至接管或取代MS自带的DI,那么你可以选择Autofac,关于Autofac的具体使用,在此不作详叙。

九、缓存

MemoryCache使用

按官方说明,开发人员需合理说用缓存,以及限制缓存大小,Core运行时不会根据内容压力限制缓存大小。对于使用方式,依旧还是先行注册,然后控制器调用:

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. services.AddMemoryCache();//缓存中间件
  4. }
  1. public class ProductController : ControllerBase
  2. {
  3. private IMemoryCache _cache;
  4.  
  5. public ProductController(IMemoryCache memoryCache)
  6. {
  7. _cache = memoryCache;
  8. }
  9.  
  10. [HttpGet]
  11. public DateTime GetTime()
  12. {
  13. string key = "_timeKey";
  14.  
  15. // Look for cache key.
  16. if (!_cache.TryGetValue(key, out DateTime cacheEntry))
  17. {
  18. // Key not in cache, so get data.
  19. cacheEntry = DateTime.Now;
  20.  
  21. // Set cache options.
  22. var cacheEntryOptions = new MemoryCacheEntryOptions()
  23. // Keep in cache for this time, reset time if accessed.
  24. .SetSlidingExpiration(TimeSpan.FromSeconds(3));
  25.  
  26. // Save data in cache.
  27. _cache.Set(key, cacheEntry, cacheEntryOptions);
  28. }
  29.  
  30. return cacheEntry;
  31. }
  32. }

上述代码缓存了一个时间,并设置了滑动过期时间(指最后一次访问后的过期时间)为3秒;如果需要设置绝对过期时间,将SetSlidingExpiration 改为SetAbsoluteExpiration即可。浏览刷新,每3秒后时间将更新。

附一个封装好的Cache类如下:

  1. public class CacheHelper
  2. {
  3. public static IMemoryCache _memoryCache = new MemoryCache(new MemoryCacheOptions());
  4.  
  5. /// <summary>
  6. /// 缓存绝对过期时间
  7. /// </summary>
  8. ///<param name="key">Cache键</param>
  9. ///<param name="value">缓存的值</param>
  10. ///<param name="minute">minute分钟后绝对过期</param>
  11. public static void SetChache(string key, object value, int minute)
  12. {
  13. if (value == null) return;
  14. _memoryCache.Set(key, value, new MemoryCacheEntryOptions()
  15. .SetAbsoluteExpiration(TimeSpan.FromMinutes(minute)));
  16. }
  17.  
  18. /// <summary>
  19. /// 缓存相对过期,最后一次访问后minute分钟后过期
  20. /// </summary>
  21. ///<param name="key">Cache键</param>
  22. ///<param name="value">缓存的值</param>
  23. ///<param name="minute">滑动过期分钟</param>
  24. public static void SetChacheSliding(string key, object value, int minute)
  25. {
  26. if (value == null) return;
  27. _memoryCache.Set(key, value, new MemoryCacheEntryOptions()
  28. .SetSlidingExpiration(TimeSpan.FromMinutes(minute)));
  29. }
  30.  
  31. /// <summary>
  32. ///设置缓存,如果不主动清空,会一直保存在内存中.
  33. /// </summary>
  34. ///<param name="key">Cache键值</param>
  35. ///<param name="value">给Cache[key]赋的值</param>
  36. public static void SetChache(string key, object value)
  37. {
  38. _memoryCache.Set(key, value);
  39. }
  40.  
  41. /// <summary>
  42. ///清除缓存
  43. /// </summary>
  44. ///<param name="key">cache键</param>
  45. public static void RemoveCache(string key)
  46. {
  47. _memoryCache.Remove(key);
  48. }
  49.  
  50. /// <summary>
  51. ///根据key值,返回Cache[key]的值
  52. /// </summary>
  53. ///<param name="key"></param>
  54. public static object GetCache(string key)
  55. {
  56. //return _memoryCache.Get(key);
  57. if (key != null && _memoryCache.TryGetValue(key, out object val))
  58. {
  59. return val;
  60. }
  61. else
  62. {
  63. return default;
  64. }
  65. }
  66.  
  67. /// <summary>
  68. /// 通过Key值返回泛型对象
  69. /// </summary>
  70. /// <typeparam name="T"></typeparam>
  71. /// <param name="key"></param>
  72. /// <returns></returns>
  73. public static T GetCache<T>(string key)
  74. {
  75. if (key != null && _memoryCache.TryGetValue<T>(key, out T val))
  76. {
  77. return val;
  78. }
  79. else
  80. {
  81. return default;
  82. }
  83. }
  84.  
  85. }

十、异常处理

定义异常处理中间件

这里主要针对全局异常进行捕获处理并记录日志,并以统一的json格式返回给接口调用者;说异常处理前先提下中间件,关于什么是中间件,在此不在赘述,一个中间件其基本的结构如下:

  1. public class CustomMiddleware
  2. {
  3. private readonly RequestDelegate _next;
  4.  
  5. public CustomMiddleware(RequestDelegate next)
  6. {
  7. _next = next;
  8. }
  9.  
  10. public async Task Invoke(HttpContext httpContext)
  11. {
  12. await _next(httpContext);
  13. }
  14. }

下面我们定义自己的全局异常处理中间件,代码如下:

  1. public class CustomExceptionMiddleware
  2. {
  3. private readonly RequestDelegate _next;
  4. private readonly ILogger<CustomExceptionMiddleware> _logger;
  5.  
  6. public CustomExceptionMiddleware(RequestDelegate next, ILogger<CustomExceptionMiddleware> logger)
  7. {
  8. _next = next;
  9. _logger = logger;
  10. }
  11.  
  12. public async Task Invoke(HttpContext httpContext)
  13. {
  14. try
  15. {
  16. await _next(httpContext);
  17. }
  18. catch (Exception ex)
  19. {
  20. _logger.LogError(ex,"Unhandled exception...");
  21. await HandleExceptionAsync(httpContext, ex);
  22. }
  23. }
  24.  
  25. private Task HandleExceptionAsync(HttpContext httpContext, Exception ex)
  26. {
  27. var result = JsonConvert.SerializeObject(new { isSuccess = false, message = ex.Message });
  28. httpContext.Response.ContentType = "application/json;charset=utf-8";
  29. return httpContext.Response.WriteAsync(result);
  30. }
  31. }
  32.  
  33. /// <summary>
  34. /// 以扩展方式添加中间件
  35. /// </summary>
  36. public static class CustomExceptionMiddlewareExtensions
  37. {
  38. public static IApplicationBuilder UseCustomExceptionMiddleware(this IApplicationBuilder builder)
  39. {
  40. return builder.UseMiddleware<CustomExceptionMiddleware>();
  41. }
  42. }

然后在Startup类的Configure方法里添加上述扩展的中间件,见加粗部分:

  1. public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
  2. {
  3. if (env.IsDevelopment())
  4. {
  5. app.UseDeveloperExceptionPage();
  6. }
  7.  
  8. //全局异常处理
  9. app.UseCustomExceptionMiddleware();
  10. }

在HandleExceptionAsync方法中,为方便开发和测试,这里将系统的错误返回给了接口调用者,实际生产环境中可统一返回固定的错误Message消息。

异常状态码的处理

关于http状态码,常见的如正常返回的200,其他401、403、404、502等等等等,因为系统有时候并不总是返回200成功,对于返回非200的异常状态码,WebApi也要做到相应的处理,以便接口调用者能正确接收,譬如紧接下来的JWT认证,当认证令牌过期或没有权限时,系统实际会返回401、403,但接口并不提供有效的可接收的返回,因此,这里列举一些常见的异常状态码,并以200方式提供给接口调用者,在Startup类的Configure方法里添加代码如下:

  1. app.UseStatusCodePages(async context =>
  2. {
  3. //context.HttpContext.Response.ContentType = "text/plain";
  4. context.HttpContext.Response.ContentType = "application/json;charset=utf-8";
  5.  
  6. int code = context.HttpContext.Response.StatusCode;
  7. string message =
  8. code switch
  9. {
  10. 401 => "未登录",
  11. 403 => "访问拒绝",
  12. 404 => "未找到",
  13. _ => "未知错误",
  14. };
  15.  
  16. context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
  17. await context.HttpContext.Response.WriteAsync(Newtonsoft.Json.JsonConvert.SerializeObject(new
  18. {
  19. isSuccess = false,
  20. code,
  21. message
  22. }));
  23.  
  24. });

代码很简单,这里使用系统自带的异常处理中间件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相关认证类:

  1. public static class JwtSetting
  2. {
  3. public static JwtConfig Setting { get; set; } = new JwtConfig();
  4. }
  5.  
  6. public class JwtConfig
  7. {
  8. public string Secret { get; set; }
  9. public string Issuer { get; set; }
  10. public string Audience { get; set; }
  11. public int AccessExpiration { get; set; }
  12. public int RefreshExpiration { get; set; }
  13. }

采用前述绑定静态类的方式读取JWT配置,并进行注入:

  1. public Startup(IConfiguration configuration, IWebHostEnvironment env)
  2. {
  3. //Configuration = configuration;
  4.  
  5. var builder = new ConfigurationBuilder()
  6. .SetBasePath(env.ContentRootPath)
  7. .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
  8.  
  9. Configuration = builder.Build();
  10.  
  11. configuration.GetSection("SystemConfig").Bind(MySettings.Setting);//绑定静态配置类
  12. configuration.GetSection("JwtTokenConfig").Bind(JwtSetting.Setting);//同上
  13.  
  14. }
  15.  
  16. public IConfiguration Configuration { get; }
  17.  
  18. // This method gets called by the runtime. Use this method to add services to the container.
  19. public void ConfigureServices(IServiceCollection services)
  20. {
  21.  
  22. #region JWT认证注入
  23.  
  24. JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
  25. services.AddAuthentication("Bearer")
  26. .AddJwtBearer("Bearer", options =>
  27. {
  28. options.RequireHttpsMetadata = false;
  29.  
  30. options.TokenValidationParameters = new TokenValidationParameters
  31. {
  32. ValidateIssuer = true,
  33. ValidateAudience = true,
  34. ValidateLifetime = true,
  35. ValidateIssuerSigningKey = true,
  36. ValidIssuer = JwtSetting.Setting.Issuer,
  37. ValidAudience = JwtSetting.Setting.Audience,
  38. IssuerSigningKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(JwtSetting.Setting.Secret))
  39. };
  40. });
  41.  
  42. #endregion
  43.  
  44. }

给Swagger添加JWT认证支持,完成后,Swagger页面会出现锁的标识,获取token后填入Value(Bearer token形式)项进行Authorize登录即可,Swagger配置JWT见加粗部分:

  1. services.AddSwaggerGen(c =>
  2. {
  3. c.SwaggerDoc("v1", new OpenApiInfo
  4. {
  5. Title = "My API",
  6. Version = "v1",
  7. Description = "API文档描述",
  8. Contact = new OpenApiContact
  9. {
  10. Email = "5007032@qq.com",
  11. Name = "测试项目",
  12. //Url = new Uri("http://t.abc.com/")
  13. },
  14. License = new OpenApiLicense
  15. {
  16. Name = "BROOKE许可证",
  17. //Url = new Uri("http://t.abc.com/")
  18. }
  19. });
  20.  
  21. // 为 Swagger JSON and UI设置xml文档注释路径
  22. //var basePath = Path.GetDirectoryName(typeof(Program).Assembly.Location);//获取应用程序所在目录(不受工作目录影响)
  23. //var xmlPath = Path.Combine(basePath, "CoreAPI_Demo.xml");
  24. //c.IncludeXmlComments(xmlPath, true);
  25.  
  26. var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
  27. var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
  28. c.IncludeXmlComments(xmlPath);
  29.  
  30. #region JWT认证Swagger授权
  31. c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
  32. {
  33. Description = "JWT授权(数据将在请求头header中进行传输) 直接在下框中输入Bearer {token}(中间是空格)",
  34. Name = "Authorization",
  35. In = ParameterLocation.Header,
  36. Type = SecuritySchemeType.ApiKey,
  37. BearerFormat = "JWT",
  38. Scheme = "Bearer"
  39. });
  40. c.AddSecurityRequirement(new OpenApiSecurityRequirement()
  41. {
  42. {
  43. new OpenApiSecurityScheme
  44. {
  45. Reference = new OpenApiReference {
  46. Type = ReferenceType.SecurityScheme,
  47. Id = "Bearer"
  48. }
  49. },
  50. new string[] { }
  51. }
  52. });
  53. #endregion
  54.  
  55. });

Starup类添加Configure注册,注意,需放到 app.UseAuthorization();前面:

  1. public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
  2. {
  3. app.UseAuthentication();//jwt认证
  4.  
  5. app.UseAuthorization();
  6.  
  7. }

这样,JWT就基本配置完毕,接下来实施认证登录和授权,模拟操作如下:

  1. [HttpPost]
  2. public async Task<ApiResult> Login(LoginEntity model)
  3. {
  4. ApiResult result = new ApiResult();
  5.  
  6. //验证用户名和密码
  7. var userInfo = await _memberService.CheckUserAndPwd(model.User, model.Pwd);
  8. if (userInfo == null)
  9. {
  10. result.Message = "用户名或密码不正确";
  11. return result;
  12. }
  13. var claims = new Claim[]
  14. {
  15. new Claim(ClaimTypes.Name,model.User),
  16. new Claim(ClaimTypes.Role,"User"),
  17. new Claim(JwtRegisteredClaimNames.Sub,userInfo.MemberID.ToString()),
  18.  
  19. };
  20.  
  21. var key = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(JwtSetting.Setting.Secret));
  22. var expires = DateTime.Now.AddDays(1);
  23. var token = new JwtSecurityToken(
  24. issuer: JwtSetting.Setting.Issuer,
  25. audience: JwtSetting.Setting.Audience,
  26. claims: claims,
  27. notBefore: DateTime.Now,
  28. expires: expires,
  29. signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256));
  30.  
  31. //生成Token
  32. string jwtToken = new JwtSecurityTokenHandler().WriteToken(token);
  33.  
  34. //更新最后登录时间
  35. await _memberService.UpdateLastLoginTime(userInfo.MemberID);
  36.  
  37. result.IsSuccess= 1;
  38. result.ResultData["token"] = jwtToken;
  39. result.Message = "授权成功!";
  40. return result;
  41.  
  42. }

上述代码模拟登录操作(账号密码登录,成功后设置有效期一天),生成token并返回,前端调用者拿到token后以诸如localstorage方式进行存储,调取授权接口时,添加该token到header(Bearer token)进行接口请求。接下来,给需要身份授权的Controller或Action打上Authorize标识:

  1. [Authorize]
  2. [Route("api/[controller]/[action]")]
  3. public class UserController : ControllerBase
  4. {
  5. }

如果要添加基于角色的授权,可限制操作如下:

  1. [Authorize(Roles = "user")]
  2. [Route("api/[controller]/[action]")]
  3. public class UserController : ControllerBase
  4. {
  5. }
  6.  
  7. //多个角色也可以逗号分隔
  8. [Authorize(Roles = "Administrator,Finance")]
  9. [Route("api/[controller]/[action]")]
  10. public class UserController : ControllerBase
  11. {
  12. }

不同的角色信息,可通过登录设置ClaimTypes.Role进行配置;当然,这里只是简单的示例说明角色服务的应用,复杂的可通过注册策略服务,并结合数据库进行动态配置。

这样,一个简单的基于JWT认证授权的工作就完成了。

十二、跨域

前后端分离,会涉及到跨域问题,简单的支持跨域操作如下:

添加扩展支持

  1. public static class CrosExtensions
  2. {
  3. public static void ConfigureCors(this IServiceCollection services)
  4. {
  5.  
  6. services.AddCors(options => options.AddPolicy("CorsPolicy",
  7. builder =>
  8. {
  9. builder.AllowAnyMethod()
  10. .SetIsOriginAllowed(_ => true)
  11. .AllowAnyHeader()
  12. .AllowCredentials();
  13. }));
  14.  
  15. //services.AddCors(options => options.AddPolicy("CorsPolicy",
  16. //builder =>
  17. //{
  18. // builder.WithOrigins(new string[] { "http://localhost:13210" })
  19. // .AllowAnyMethod()
  20. // .AllowAnyHeader()
  21. // .AllowCredentials();
  22. //}));
  23.  
  24. }
  25. }

Startup类添加相关注册如下:

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. services.ConfigureCors();
  4. }
  1. public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
  2. {
  3. app.UseCors("CorsPolicy");//跨域
  4. }

这样,一个简单跨域操作就完成了,你也可以通过设置WithOrigins、WithMethods等方法限制请求地址来源和请求方式。

至此,全篇结束,本篇涉及到的源码地址:https://github.com/Brooke181/CoreAPI_Demo

下一篇介绍Dapper在.NET Core中的使用,谢谢支持!

一文了解.Net Core 3.1 Web API基础知识的更多相关文章

  1. 从头编写asp.net core 2.0 web api 基础框架 (5) + 使用Identity Server 4建立Authorization Server (7) 可运行前后台源码

    前台使用angular 5, 后台是asp.net core 2.0 web api + identity server 4. 从头编写asp.net core 2.0 web api 基础框架: 第 ...

  2. 从头编写 asp.net core 2.0 web api 基础框架 (1)

    工具: 1.Visual Studio 2017 V15.3.5+ 2.Postman (Chrome的App) 3.Chrome (最好是) 关于.net core或者.net core 2.0的相 ...

  3. 从头编写 asp.net core 2.0 web api 基础框架 (3)

    第一部分:http://www.cnblogs.com/cgzl/p/7637250.html 第二部分:http://www.cnblogs.com/cgzl/p/7640077.html 之前我介 ...

  4. 【转载】从头编写 asp.net core 2.0 web api 基础框架 (3)

    Github源码地址:https://github.com/solenovex/Building-asp.net-core-2-web-api-starter-template-from-scratc ...

  5. 【转载】从头编写 asp.net core 2.0 web api 基础框架 (1)

    工具: 1.Visual Studio 2017 V15.3.5+ 2.Postman (Chrome的App) 3.Chrome (最好是) 关于.net core或者.net core 2.0的相 ...

  6. 从头编写 asp.net core 2.0 web api 基础框架 (4) EF配置

    第1部分:http://www.cnblogs.com/cgzl/p/7637250.html 第2部分:http://www.cnblogs.com/cgzl/p/7640077.html 第3部分 ...

  7. 【转载】从头编写 asp.net core 2.0 web api 基础框架 (4) EF配置

    Github源码地址:https://github.com/solenovex/Building-asp.net-core-2-web-api-starter-template-from-scratc ...

  8. 从头编写 asp.net core 2.0 web api 基础框架 (2)

    上一篇是: http://www.cnblogs.com/cgzl/p/7637250.html Github源码地址是: https://github.com/solenovex/Building- ...

  9. 从头编写 asp.net core 2.0 web api 基础框架 (5) EF CRUD

    第1部分:http://www.cnblogs.com/cgzl/p/7637250.html 第2部分:http://www.cnblogs.com/cgzl/p/7640077.html 第3部分 ...

随机推荐

  1. 【Flutter 实战】动画序列、共享动画、路由动画

    老孟导读:此篇文章是 Flutter 动画系列文章第四篇,本文介绍动画序列.共享动画.路由动画. 动画序列 Flutter中组合动画使用Interval,Interval继承自Curve,用法如下: ...

  2. Java高级特性——反射机制(第二篇)

    在Java高级特性——反射机制(第一篇)中,写了很多反射的实例,可能对于Class的了解还是有点迷糊,那么我们试着从内存角度去分析一下. Java内存 从上图可以看出,Java将内存分为堆.栈.方法区 ...

  3. C# 自定义无边框窗体阴影效果

    工作中我们会经常遇到自定义一些窗口的样式,设置无边框然后自定义关闭.最大化等其他菜单,但是这样就失去了Winform自带的边框阴影效果,下面这个方法能让无边框增加阴影效果.代码如下: using Sy ...

  4. 2020重新出发,NOSQL,Redis主从复制

    Redis主从复制 尽管 Redis 的性能很好,但是有时候依旧满足不了应用的需要,比如过多的用户进入主页,导致 Redis 被频繁访问,此时就存在大量的读操作. 对于一些热门网站的某个时刻(比如促销 ...

  5. Solon详解(六)- Solon的校验扩展框架使用与扩展

    Solon详解系列文章: Solon详解(一)- 快速入门 Solon详解(二)- Solon的核心 Solon详解(三)- Solon的web开发 Solon详解(四)- Solon的事务传播机制 ...

  6. [Oracle/sql]查看当前用户名下有多少表 以及查看表字段信息

    SQL> select table_name from user_tables order by table_name; TABLE_NAME ------------------------- ...

  7. CTF资源

    WINDOWS 逆向工程师技能表 https://www.sec-wiki.com/skill/6 Software-Security-Learning https://chybeta.github. ...

  8. idea导入spring源码

    1.环境: Intellij idea 2018.2 gradle 4.10.2 spring framework:5.2.0 注意版本不符合可能会导致编译失败. 参考版本: 1.首先下载安装 Int ...

  9. 真正的解决IDEA中Tomcat控制台乱码的问题

    真正的解决IDEA中Tomcat控制台乱码的问题 解决方案一 网上看到一个真正的解决方案: 首先要分清是tomcat日志编码,与idea的日志显示控制台编码 tomcat日志编码:当在cmd中启动To ...

  10. adb连接手机

    1. 通过wifi, 利用adb来连接手机. 在pc的cmd中输入命令: adb connect 192.168.1.100 其中adb就是手机的ip. 如果连接成功, 就可以进入android的sh ...