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

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 原文:从头编写 asp.net core 2.0 web api 基础框架 (3)第一部分: http://www.cnblogs.com/cgzl/p/7637250.html 第二部分:http://www.
原文: 从头编写 asp.net core 2.0 web api 基础框架 (3)

第一部分: http://www.cnblogs.com/cgzl/p/7637250.html

第二部分:http://www.cnblogs.com/cgzl/p/7640077.html

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

之前我介绍完了asp.net core 2.0 web api最基本的CRUD操作,接下来继续研究:

IoC和Dependency Injection (控制反转和依赖注入)

先举个例子说明一下:

比如说我们的ProductController,需要使用Mylogger作为记录日志的服务,MyLogger是一个在设计时指定的具体的类,这就是说ProductController对MyLogger有一个依赖。MyLogger通常是在Constructor里面new出来的。假如ProductController还依赖于很多其他的Services,当有问题发生的时候,需要替换或修改MyLogger,那么ProductController的代码就需要更改了,这也违反了设计模式的原则(对修改关闭)。这样做呢,也不利于进行单元测试,单元测试的时候无法提供一个Mock(Mock就是在测试中对于某种不易构建的对象,建立的一个虚拟的版本,以方便测试)版本的MyLogger,因为我们使用的是具体的类。而ProductController同时也控制着MyLogger的生命周期,这是紧耦合。这个时候,Ioc(Inversion of control 控制反转)就有用了!

Ioc把为ProductController选择某个依赖项(具有Log功能的Service)的具体实现类(MyLogger就是可能的具体实现类之一)的这项工作委托给了外部的一个组件。

那么上面讲的Ioc的这项工作是怎么来实现的呢?那就是Depedency Injection这个设计模式。

Dependency Injection可以说是Ioc的一个特定的种类。

DI模式是使用一个特定的对象(Container 容器)来为目标类(ProductController)进行初始化并提供其所需要的依赖项(MyLogger)。Container管理者这些依赖项的生命周期。

下面举一个典型的例子:

    public class ProductController : Controller
    {
        private ILogger<ProductController> _logger; // interface 不是具体的实现类

        public ProductController(ILogger<ProductController> logger)
        {
            _logger = logger;
        }
    。。。。。
    }

ProductController里面需要有一个Field来保留这个依赖项,这里就是指_logger,而_logger不是具体的实现类,它是一个interface,ProductController需要的是一个实现了ILogger<T>接口的类。

看一下Constructor的代码,这种叫做Constructor注入。Constructor需要一个实现了ILogger<T>接口的类的实例,不是一个具体的类,还是一个interface。Container就会为ProductController注入它的依赖项。

这样做的最终结果就是,松耦合!(ProductController不必再为那些工作负责了,也和具体的实现类没有直接联系了)。这时,再需要替换和修改这些依赖项的时候仅需要改非常少的代码或者完全不用改代码了。而且单元测试也可以简单的进行了,因为这些依赖项(ILogger)都可以被实现了ILogger接口的Mock的版本来替代了。

在asp.net core里面呢,Ioc和依赖注入是框架内置的,这点和老版本的asp.net web api 2.2不一样,那时候我们得使用像autofac这样的第三方库来实现Ioc和依赖注入。

在asp.net core里面有一些services是内置的并且已经在Container注册了,比如说记录日志用的Logger。其他的services也可以在container注册,这一般是在StartUp类里面的ConfigureServices方法来实现的,框架级以及应用级的services都可以加进来。

下面我们就把内置的Logger服务注册进去。

使用内置的Logger

因为Logger是asp.net core 的内置service,所以我们就不需要在ConfigureService里面注册了。如果是asp.net core 1.0版本的话,我们需要配置一个或者多个Logger,但是asp.net core 2.0的话就不需要做这个工作了,因为在CreateDefaultBuilder方法里默认给配置了输出到Console和Debug窗口的Logger。这是源码:

 public static IWebHostBuilder CreateDefaultBuilder(string[] args)
        {
            var builder = new WebHostBuilder()
                .UseKestrel()
                .UseContentRoot(Directory.GetCurrentDirectory())
                .ConfigureAppConfiguration((hostingContext, config) =>
                {
                    var env = hostingContext.HostingEnvironment;

                    config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                          .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);

                    if (env.IsDevelopment())
                    {
                        var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
                        if (appAssembly != null)
                        {
                            config.AddUserSecrets(appAssembly, optional: true);
                        }
                    }

                    config.AddEnvironmentVariables();

                    if (args != null)
                    {
                        config.AddCommandLine(args);
                    }
                })
                .ConfigureLogging((hostingContext, logging) =>
                {
                    logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
                    logging.AddConsole();
                    logging.AddDebug();
                })
                .UseIISIntegration()
                .UseDefaultServiceProvider((context, options) =>
                {
                    options.ValidateScopes = context.HostingEnvironment.IsDevelopment();
                });

            if (args != null)
            {
                builder.UseConfiguration(new ConfigurationBuilder().AddCommandLine(args).Build());
            }

            return builder;
        }
View Code

注入Logger

我们可以在ProductController里面注入ILoggerFactory然后再创建具体的Logger。但是还有更好的方式,Container可以直接提供一个ILogger<T>的实例,这时候呢Logger就会使用T的名字作为日志的类别:

namespace CoreBackend.Api.Controllers
{
    [Route("api/[controller]")]
    public class ProductController : Controller
    {
        private ILogger<ProductController> _logger;

        public ProductController(ILogger<ProductController> logger)
        {
            _logger = logger;
        }
......

如果通过Constructor注入的方式不可用,那么我们也可以直接从Container请求来得到它:HttpContext.RequestServices.GetService(typeof(ILogger<ProductController>)); 如果你在Constructor写这句话可能会空指针,因为这个时候HttpContext应该是null吧。

不过还是建议使用Constructor注入的方式!!!

然后我们记录一些日志把:

        [Route("{id}", Name = "GetProduct")]
        public IActionResult GetProduct(int id)
        {
            var product = ProductService.Current.Products.SingleOrDefault(x => x.Id == id);
            if (product == null)
            {
                _logger.LogInformation($"Id为{id}的产品没有被找到..");
                return NotFound();
            }
            return Ok(product);
        }

Log记录时一般都分几个等级,这点我假设大家都知道吧,就不介绍了。

然后试一下:通过Postman访问一个不存在的产品:‘/api/product/22’,然后看看Debug输出窗口:

嗯,出现了,前边是分类,也就是ILogger<T>里面T的名字,然后是级别 Information,然后就是我们记录的Log内容。

再Log一个Exception:

        [Route("{id}", Name = "GetProduct")]
        public IActionResult GetProduct(int id)
        {
            try
            {
                throw new Exception("来个异常!!!");
                var product = ProductService.Current.Products.SingleOrDefault(x => x.Id == id);
                if (product == null)
                {
                    _logger.LogInformation($"Id为{id}的产品没有被找到..");
                    return NotFound();
                }
                return Ok(product);
            }
            catch (Exception ex)
            {
                _logger.LogCritical($"查找Id为{id}的产品时出现了错误!!", ex);
                return StatusCode(500, "处理请求的时候发生了错误!");
            }
        }

  记录Exception就建议使用LogCritical了,这里需要注意的是Exception的发生就表示服务器发生了错误,我们应该处理这个exception并返回500。使用StatusCode这个方法返回特定的StatusCode,然后可以加一个参数来解释这个错误(这里一般不建议返回exception的细节)。

运行试试:

OK。

Log到Debug窗口或者Console窗口还是比较方便的,但是正式生产环境中这肯定不够用。

正式环境应该Log到文件或者数据库。虽然asp.net core 的log内置了记录到Windows Event的方法,但是由于Windows Event是windows系统独有的,所以这个方法无法跨平台,也就不建议使用了。

官方文档上列出了这几个建议使用的第三发Log Provider:

把这几个Log provider注册到asp.net core的方式几乎是一摸一样的,所以介绍一个就行。我们就用比较火的NLog吧。

NLog

首先通过nuget安装Nlog: 

注意要勾上include prerelease,目前还不是正式版。

装完之后,我们就需要为Nlog添加配置文件了。默认情况下Nlog会在根目录寻找一个叫做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">
  <targets>
    <target name="logfile" xsi:type="File" fileName="logs/${shortdate}.log" />

  </targets>
  <rules>
    <logger name="*" minlevel="Info" writeTo="logfile" />
  </rules>
</nlog>

然后设置该文件的属性如下:

对于Nlog的配置就不进行深入介绍了。具体请看官方文档的.net core那部分。

然后需要把Nlog集成到asp.net core,也就是把Nlog注册到ILoggerFactory里面。所以打开Startup.cs,首先注入ILoggerFactory,然后对ILoggerFactory进行配置,为其注册NLog的Provider:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            // loggerFactory.AddProvider(new NLogLoggerProvider());
loggerFactory.AddNLog();
if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler(); } app.UseStatusCodePages(); app.UseMvc(); }

针对LoggerFactory.AddProvider()这种写法,Nlog一个简单的ExtensionMethod做了这个工作,就是AddNlog();

添加完NLog,其余的代码都不需要改,然后我们试下:

在如图所示的位置出现了log文件。内容如下:

自定义Service

一个系统中可能需要很多个自定义的service,下面举一个简单的例子,

建立LocalMailService.cs:

namespace CoreBackend.Api.Services
{
    public class LocalMailService
    {
        private string _mailTo = "developer@qq.com";
        private string _mailFrom = "noreply@alibaba.com";

        public void Send(string subject, string msg)
        {
            Debug.WriteLine($"从{_mailFrom}给{_mailTo}通过{nameof(LocalMailService)}发送了邮件");
        }
    }
}

使用这个Service,我们假装在删除Product的时候发送邮件。

首先,我们要把这个LocalMailService注册给Container。打开Startup.cs进入ConfigureServices方法。这里面有三种方法可以注册service:AddTransient,AddScoped和AddSingleton,这些都表示service的生命周期。

transient的services是每次请求(不是指Http request)都会创建一个新的实例,它比较适合轻量级的无状态的(Stateless)的service。

scope的services是每次http请求会创建一个实例。

singleton的在第一次请求的时候就会创建一个实例,以后也只有这一个实例,或者在ConfigureServices这段代码运行的时候创建唯一一个实例。

我们的LocalMailService比较适合Transient:

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
            services.AddTransient<LocalMailService>();
        }

现在呢,就可以注入LocalMailService的实例了:

 

namespace CoreBackend.Api.Controllers
{
    [Route("api/[controller]")]
    public class ProductController : Controller
    {
        private readonly ILogger<ProductController> _logger;
        private readonly LocalMailService _localMailService;

        public ProductController(
            ILogger<ProductController> logger,
            LocalMailService localMailService)
        {
            _logger = logger;
            _localMailService = localMailService;
        }
        [HttpDelete("{id}")]
        public IActionResult Delete(int id)
        {
            var model = ProductService.Current.Products.SingleOrDefault(x => x.Id == id);
            if (model == null)
            {
                return NotFound();
            }
            ProductService.Current.Products.Remove(model);
            _localMailService.Send("Product Deleted",$"Id为{id}的产品被删除了");
            return NoContent();
        }

然后试一下:

嗯,没问题。

但是现在的写法并不符合DI的意图。所以修改一下代码,首先添加一个interface,然后让LocalMailService去实现它:

namespace CoreBackend.Api.Services
{
    public interface IMailService
    {
        void Send(string subject, string msg);
    }

    public class LocalMailService: IMailService
    {
        private string _mailTo = "developer@qq.com";
        private string _mailFrom = "noreply@alibaba.com";

        public void Send(string subject, string msg)
        {
            Debug.WriteLine($"从{_mailFrom}给{_mailTo}通过{nameof(LocalMailService)}发送了邮件");
        }
    }
}

有了IMailService这个interface,Container就可以为我们提供实现了IMailService接口的不同的类了。

所以再建立一个CloudMailService:

    public class CloudMailService : IMailService
    {
        private readonly string _mailTo = "admin@qq.com";
        private readonly string _mailFrom = "noreply@alibaba.com";

        public void Send(string subject, string msg)
        {
            Debug.WriteLine($"从{_mailFrom}给{_mailTo}通过{nameof(LocalMailService)}发送了邮件");
        }
    }

然后回到ConfigureServices方法里面:

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
            services.AddTransient<IMailService, LocalMailService>();
        }

这句话的意思就是,当需要IMailService的一个实现的时候,Container就会提供一个LocalMailService的实例。

然后改一下ProductController:

namespace CoreBackend.Api.Controllers
{
    [Route("api/[controller]")]
    public class ProductController : Controller
    {
        private readonly ILogger<ProductController> _logger;
        private readonly IMailService _mailService;

        public ProductController(
            ILogger<ProductController> logger,
            IMailService mailService)
        {
            _logger = logger;
            _mailService = mailService;
        }

然后运行一下,效果和上面是一样的。

然而我们注册了LocalMailService,那么CloudMailService是什么时候用呢?

分两种方式:

一、使用compiler directive

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
#if DEBUG
            services.AddTransient<IMailService, LocalMailService>();
#else
            services.AddTransient<IMailService, CloudMailService>();
#endif
        }

这样写就是告诉compiler,如果是Debug build的情况下,那么就使用LocalMailService(把这句话纳入编译的范围),如果是在Release Build的模式下,就是用CloudMailService。

那我们就切换到Release Build模式(或者在DEBUG前边加一个叹号试试):

运行试试,居然没起作用。随后发现原因是这样的:

在Release模式下Debug.WriteLine将不会被调用,因为这是Debug Build模式下专有的方法。。。

那我们就改一下Cloud'MailService,使用logger吧:

 public class CloudMailService : IMailService
    {
        private readonly string _mailTo = "admin@qq.com";
        private readonly string _mailFrom = "noreply@alibaba.com";
        private readonly ILogger<CloudMailService> _logger;

        public CloudMailService(ILogger<CloudMailService> logger)
        {
            _logger = logger;
        }

        public void Send(string subject, string msg)
        {
            _logger.LogInformation($"从{_mailFrom}给{_mailTo}通过{nameof(LocalMailService)}发送了邮件");
        }
    }

然后再试一下看看结果:

这回就没问题了。

二、是通过环境变量控制配置文件

asp.net core 支持各式各样的配置方法,包括使用JSON,xml, ini文件,环境变量,命令行参数等等。建议使用的还是JSON。

创建一个appSettings.json文件,然后把MailService相关的常量存到里面:

{
  "mailSettings": {
    "mailToAddress": "admin__json@qq.com",
    "mailFromAddress": "noreply__json@qq.com"
  }
}

asp.net core 2.0 默认已经做了相关的配置,我们再看一下这部分的源码

public static IWebHostBuilder CreateDefaultBuilder(string[] args)
        {
            var builder = new WebHostBuilder()
                .UseKestrel()
                .UseContentRoot(Directory.GetCurrentDirectory())
                .ConfigureAppConfiguration((hostingContext, config) =>
                {
                    var env = hostingContext.HostingEnvironment;

                    config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                          .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);

               。。。。。。return builder;
        }

红色部分的config的类型是IConfigurationBuilder,它用来配置的。首先是要找到appSettings.json文件,asp.net core 2.0已经做好了相关配置,它默认会从ContentRoot去找appSettings.json文件。

然后使用AddJsonFile这个方法来添加Json配置文件,第一个参数是文件名;第二个参数optional表示这个配置文件是否是可选的,把它设置成false表示我们不必非得用这个配置文件;第三个参数reloadOnChange为true,表示如果运行的时候配置文件变化了,那么就立即重载它。

使用appSettings.json里面的值就需要使用实现了IConfiguration这个接口的对象。建议的做法是:在Startup.cs里面注入IConfiguration(这个时候通过CreateDefaultBuilder方法,它已经建立好了),然后把它赋给一个静态的property:

    public class Startup
    {
        public static IConfiguration Configuration { get; private set; }

        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

然后我们把LocalMailService里面改一下:

    public class LocalMailService: IMailService
    {
        private readonly string _mailTo = Startup.Configuration["mailSettings:mailToAddress"];
        private readonly string _mailFrom = Startup.Configuration["mailSettings:mailFromAddress"];

        public void Send(string subject, string msg)
        {
            Debug.WriteLine($"从{_mailFrom}给{_mailTo}通过{nameof(LocalMailService)}发送了邮件");
        }
    }

通过刚才写的Startup.Configuration来访问json配置文件中的变量,根据json文件中的层次结构,第一层对象我们取的是mailSettings,然后试mailToAddress和mailFromAddress,他们之间用冒号分开,表示它们的层次结构。

通过这种方法取得到的值都是字符串。

然后运行一下试试,别忘了把Build模式改成Debug:

嗯,没问题。

针对不同环境选择不同json配置文件里的值(不是选择文件,而是值)

针对不同的环境选择不同的JSON配置文件,要求这个文件的名字的一部分包含有环境的名称。

添加一个Production环境下的配置文件:appSettings.Production.json, 其中Production是环境的名称,在项目--属性--Debug 里面环境变量的值:

建立好appSettings.Production.json后,可以发现它被作为appSettings.json的一个子文件显示出来,这样很好:

{
  "mailSettings": {
    "mailToAddress": "admin__Production@qq.com"
  }
}

再看一下这部分的源码:

config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
      .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);

AddJsonFile方法调用的顺序非常重要,它决定了多个配置文件的优先级。这里如果某个变量在appSettings和appSettings.Production.json都有,那么appSettings.Production.json的变量会被采用,因为appSettings.Production.json文件是后来才被调用的。

其中env的类型是IHostingEnvirongment,它里面的EnvironmentName就是环境变量的名称,如果环境变量填写的是Production,那就是appSettings.Production.json。

这么写的作用就是如果是在Production环境下,那么appSettings.json里面的部分变量值就会被appSettings.Production.json里面也存在的变量的值覆盖。

试试:首先环境变量是Development:

然后改成Production,试试:

结果如预期。

综上,通过Compiler Directive(设置Debug Build / Release Build),并结合着不同的环境变量和配置文件,asp.net core的配置是非常的灵活的。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
1天前
|
设计模式 前端开发 C#
使用 Prism 框架实现导航.NET 6.0 + WPF
使用 Prism 框架实现导航.NET 6.0 + WPF
28 10
|
16天前
|
开发框架 监控 前端开发
在 ASP.NET Core Web API 中使用操作筛选器统一处理通用操作
【9月更文挑战第27天】操作筛选器是ASP.NET Core MVC和Web API中的一种过滤器,可在操作方法执行前后运行代码,适用于日志记录、性能监控和验证等场景。通过实现`IActionFilter`接口的`OnActionExecuting`和`OnActionExecuted`方法,可以统一处理日志、验证及异常。创建并注册自定义筛选器类,能提升代码的可维护性和复用性。
|
16天前
|
开发框架 .NET 中间件
ASP.NET Core Web 开发浅谈
本文介绍ASP.NET Core,一个轻量级、开源的跨平台框架,专为构建高性能Web应用设计。通过简单步骤,你将学会创建首个Web应用。文章还深入探讨了路由配置、依赖注入及安全性配置等常见问题,并提供了实用示例代码以助于理解与避免错误,帮助开发者更好地掌握ASP.NET Core的核心概念。
37 3
|
22天前
|
安全 API 开发者
Web 开发新风尚!Python RESTful API 设计与实现,让你的接口更懂开发者心!
在当前的Web开发中,Python因能构建高效简洁的RESTful API而备受青睐,大大提升了开发效率和用户体验。本文将介绍RESTful API的基本原则及其在Python中的实现方法。以Flask为例,演示了如何通过不同的HTTP方法(如GET、POST、PUT、DELETE)来创建、读取、更新和删除用户信息。此示例还包括了基本的路由设置及操作,为开发者提供了清晰的API交互指南。
81 6
|
21天前
|
存储 JSON API
实战派教程!Python Web开发中RESTful API的设计哲学与实现技巧,一网打尽!
在数字化时代,Web API成为连接前后端及构建复杂应用的关键。RESTful API因简洁直观而广受欢迎。本文通过实战案例,介绍Python Web开发中的RESTful API设计哲学与技巧,包括使用Flask框架构建一个图书管理系统的API,涵盖资源定义、请求响应设计及实现示例。通过准确使用HTTP状态码、版本控制、错误处理及文档化等技巧,帮助你深入理解RESTful API的设计与实现。希望本文能助力你的API设计之旅。
47 3
|
20天前
|
开发框架 JSON 缓存
震撼发布!Python Web开发框架下的RESTful API设计全攻略,让数据交互更自由!
在数字化浪潮推动下,RESTful API成为Web开发中不可或缺的部分。本文详细介绍了在Python环境下如何设计并实现高效、可扩展的RESTful API,涵盖框架选择、资源定义、HTTP方法应用及响应格式设计等内容,并提供了基于Flask的示例代码。此外,还讨论了版本控制、文档化、安全性和性能优化等最佳实践,帮助开发者实现更流畅的数据交互体验。
43 1
|
22天前
|
JSON API 开发者
惊!Python Web开发新纪元,RESTful API设计竟能如此性感撩人?
在这个Python Web开发的新纪元里,RESTful API的设计已经超越了简单的技术实现,成为了一种追求极致用户体验和开发者友好的艺术表达。通过优雅的URL设计、合理的HTTP状态码使用、清晰的错误处理、灵活的版本控制以及严格的安全性措施,我们能够让RESTful API变得更加“性感撩人”,为Web应用注入新的活力与魅力。
41 3
|
3天前
|
Cloud Native API C#
.NET云原生应用实践(一):从搭建项目框架结构开始
.NET云原生应用实践(一):从搭建项目框架结构开始
|
2月前
|
机器人 API Python
智能对话机器人(通义版)会话接口API使用Quick Start
本文主要演示了如何使用python脚本快速调用智能对话机器人API接口,在参数获取的部分给出了具体的获取位置截图,这部分容易出错,第一次使用务必仔细参考接入参数获取的位置。
138 1
|
2天前
|
人工智能 自然语言处理 PyTorch
Text2Video Huggingface Pipeline 文生视频接口和文生视频论文API
文生视频是AI领域热点,很多文生视频的大模型都是基于 Huggingface的 diffusers的text to video的pipeline来开发。国内外也有非常多的优秀产品如Runway AI、Pika AI 、可灵King AI、通义千问、智谱的文生视频模型等等。为了方便调用,这篇博客也尝试了使用 PyPI的text2video的python库的Wrapper类进行调用,下面会给大家介绍一下Huggingface Text to Video Pipeline的调用方式以及使用通用的text2video的python库调用方式。