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

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 第一部分: http://www.cnblogs.com/cgzl/p/7637250.html 第二部分:http://www.cnblogs.com/cgzl/p/7640077.html Github源码地址:https://github.

第一部分: 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的配置是非常的灵活的。

下面是我的关于ASP.NET Core Web API相关技术的公众号--草根专栏:

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
30天前
|
Java API 数据库
构建RESTful API已经成为现代Web开发的标准做法之一。Spring Boot框架因其简洁的配置、快速的启动特性及丰富的功能集而备受开发者青睐。
【10月更文挑战第11天】本文介绍如何使用Spring Boot构建在线图书管理系统的RESTful API。通过创建Spring Boot项目,定义`Book`实体类、`BookRepository`接口和`BookService`服务类,最后实现`BookController`控制器来处理HTTP请求,展示了从基础环境搭建到API测试的完整过程。
40 4
|
1月前
|
XML JSON API
ServiceStack:不仅仅是一个高性能Web API和微服务框架,更是一站式解决方案——深入解析其多协议支持及简便开发流程,带您体验前所未有的.NET开发效率革命
【10月更文挑战第9天】ServiceStack 是一个高性能的 Web API 和微服务框架,支持 JSON、XML、CSV 等多种数据格式。它简化了 .NET 应用的开发流程,提供了直观的 RESTful 服务构建方式。ServiceStack 支持高并发请求和复杂业务逻辑,安装简单,通过 NuGet 包管理器即可快速集成。示例代码展示了如何创建一个返回当前日期的简单服务,包括定义请求和响应 DTO、实现服务逻辑、配置路由和宿主。ServiceStack 还支持 WebSocket、SignalR 等实时通信协议,具备自动验证、自动过滤器等丰富功能,适合快速搭建高性能、可扩展的服务端应用。
95 3
|
13天前
|
设计模式 前端开发 数据库
Python Web开发:Django框架下的全栈开发实战
【10月更文挑战第27天】本文介绍了Django框架在Python Web开发中的应用,涵盖了Django与Flask等框架的比较、项目结构、模型、视图、模板和URL配置等内容,并展示了实际代码示例,帮助读者快速掌握Django全栈开发的核心技术。
96 44
|
9天前
|
SQL 安全 PHP
探索PHP的现代演进:从Web开发到框架创新
PHP是一种流行的服务器端脚本语言,自诞生以来在Web开发领域占据重要地位。从简单的网页脚本到支持面向对象编程的现代语言,PHP经历了多次重大更新。本文探讨PHP的现代演进历程,重点介绍其在Web开发中的应用及框架创新,如Laravel、Symfony等。这些框架不仅简化了开发流程,还提高了开发效率和安全性。
17 3
|
8天前
|
前端开发 JavaScript 开发工具
从框架到现代Web开发实践
从框架到现代Web开发实践
18 1
|
12天前
|
SQL 安全 PHP
探索PHP的现代演进:从Web开发到框架创新
PHP 自发布以来一直在 Web 开发领域占据重要地位,历经多次重大更新,从简单的脚本语言进化为支持面向对象编程的现代语言。本文探讨 PHP 的演进历程,重点介绍其在 Web 开发中的应用及框架创新。自 PHP 5.3 引入命名空间后,PHP 迈向了面向对象编程时代;PHP 7 通过优化内核大幅提升性能;PHP 8 更是带来了属性、刚性类型等新特性。
22 3
|
14天前
|
前端开发 JavaScript
Bootstrap Web 前端 UI 框架
Bootstrap 是快速开发 Web 应用程序的前端工具包。
30 3
|
14天前
|
安全 数据库 开发者
Python Web开发:Django框架下的全栈开发实战
【10月更文挑战第26天】本文详细介绍了如何在Django框架下进行全栈开发,包括环境安装与配置、创建项目和应用、定义模型类、运行数据库迁移、创建视图和URL映射、编写模板以及启动开发服务器等步骤,并通过示例代码展示了具体实现过程。
28 2
|
1月前
|
前端开发 JavaScript UED
构建现代Web应用:使用React框架打造单页面应用
【10月更文挑战第9天】构建现代Web应用:使用React框架打造单页面应用
|
1月前
|
前端开发 JavaScript 开发者
探索现代Web前端技术:React框架入门
【10月更文挑战第9天】 探索现代Web前端技术:React框架入门

热门文章

最新文章