├── .gitignore ├── README.md ├── SUMMARY.md ├── book.json ├── deploy.sh ├── img └── pip.svg ├── package-lock.json ├── package.json ├── pages ├── auth-auth2.md ├── auth-authentication.md ├── auth-cookie.md ├── auth-jwt.md ├── authorization.md ├── autofac.md ├── automapper.md ├── config.md ├── dapper.md ├── di-aspnetcore.md ├── di-consume.md ├── di-designmode.md ├── di-di.md ├── di-intro.md ├── di-ioc.md ├── di-lifetime.md ├── di-register.md ├── di-src.md ├── docker-cmd.md ├── docker-dockerfile.md ├── docker-install.md ├── docker-intro.md ├── https.md ├── install-deploy.md ├── install-install.md ├── log.md ├── mailsms.md ├── microservice-consul.md ├── microservice-docker.md ├── microservice-intro.md ├── microservice-ocelot.md ├── microservice-polly.md ├── multithreading-async.md ├── multithreading-basic.md ├── multithreading-intro.md ├── multithreading-synchronization.md ├── multithreading-threadpool.md ├── multithreading-uiresource.md ├── pipeline-diagram.md ├── pipeline-environment.md ├── pipeline-lifetime.md ├── pipeline-middlewire.md ├── rpc.md ├── staticssi.md ├── supplement.md ├── unittest.md ├── webapi-basic.md ├── webapi-multiversion.md ├── webapi-openapi.md ├── webapi-restful.md └── webapi-security.md ├── pipeline.graffle ├── data.plist ├── image10.png ├── image11.png ├── image12.png ├── image19.png ├── image20.png ├── image21.tiff ├── image22.tiff ├── image23.tiff ├── image24.tiff ├── image25.png ├── image26.png ├── image4.jpg ├── image5.jpg ├── image6.jpg ├── image7.jpg ├── image9.png └── preview.jpeg └── website.css /.gitignore: -------------------------------------------------------------------------------- 1 | /_book 2 | /node_modules 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 迁移公告 2 | 此项目已迁移至 https://github.com/colin-chang/dotnet, 请[移步](https://github.com/colin-chang/dotnet)。 3 | 4 | ~~.NET Core 是开放源代码通用开发平台,由 Microsoft 和 .NET 社区在 GitHub 上共同维护。 它跨平台(支持 Windows、macOS 和 Linux),并且可用于生成设备、云和 IoT 应用程序。~~ 5 | 6 | ~~https://docs.microsoft.com/zh-cn/dotnet/core/about~~ 7 | 8 | ~~.NET Core 具有以下特性:~~ 9 | ~~* 跨平台: 可以在 Windows、macOS 和 Linux 操作系统上运行。~~ 10 | ~~* 跨体系结构保持一致: 在多个体系结构(包括 x64、x86 和 ARM)上以相同的行为运行代码。~~ 11 | ~~* 命令行工具: 包括用于本地开发和持续集成方案中的易于使用的命令行工具。~~ 12 | ~~* 部署灵活: 可以包含在应用或已安装的并行用户或计算机范围中。 可搭配 Docker 容器使用。~~ 13 | ~~* 兼容性:.NET Core 通过 .NET Standard与 .NET Framework、Xamarin 和 Mono 兼容。~~ 14 | ~~* 开放源:.NET Core 是一个开放源平台,使用 MIT 和 Apache 2 许可证。 .NET Core 是一个 .NET * oundation 项目。~~ 15 | ~~* 由 Microsoft 支持:.NET Core 由 Microsoft 依据 .NET Core 支持提供支持。~~ 16 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | * [CC Studio](https://ccstudio.org) 2 | * [前言](README.md) 3 | 4 | ## Part I - .NET Core 基础 5 | * [1. 安装部署](pages/index.md) 6 | * [1.1 安装部署](pages/install-install.md) 7 | * [1.2 部署项目](pages/install-deploy.md) 8 | * [2. 配置管理](pages/config.md) 9 | * [3. 依赖注入](pages/index.md) 10 | * [3.1 依赖注入引入](pages/di-intro.md) 11 | * [3.2 控制反转(IoC)](pages/di-ioc.md) 12 | * [3.3 基于IoC的设计模式](pages/di-designmode.md) 13 | * [3.4 依赖注入(DI)](pages/di-di.md) 14 | * [3.5 服务注册](pages/di-register.md) 15 | * [3.6 服务消费](pages/di-consume.md) 16 | * [3.7 服务生命周期](pages/di-lifetime.md) 17 | * [3.8 Asp.Net Core DI 使用](pages/di-aspnetcore.md) 18 | * [3.9 Asp.Net Core DI 源码分析](pages/di-src.md) 19 | * [4. Asp.Net Core管道](pages/index.md) 20 | * [4.1 管道模型](pages/pipeline-diagram.md) 21 | * [4.2 中间件](pages/pipeline-middlewire.md) 22 | * [4.3 生命周期](pages/pipeline-lifetime.md) 23 | * [4.4 环境变量](pages/pipeline-environment.md) 24 | * [5. Session和Cache](pages/supplement.md) 25 | * [6. 认证与授权](pages/index.md) 26 | * [6.1 Authentication](pages/auth-authentication.md) 27 | * [6.2 Cookie Authentication](pages/auth-cookie.md) 28 | * [6.3 JwtBear Authentication](pages/auth-jwt.md) 29 | * [6.4 OAuth2 & OpenID Connect Authentication](pages/auth-auth2.md) 30 | * [6.5 Authorization](pages/authorization.md) 31 | * [7. WebAPI](pages/index.md) 32 | * [7.1 WebAPI和Restful](pages/webapi-restful.md) 33 | * [7.2 WebAPI基础](pages/webapi-basic.md) 34 | * [7.3 多版本管理](pages/webapi-multiversion.md) 35 | * [7.4 安全控制](pages/webapi-security.md) 36 | * [7.5 OpenAPI](pages/webapi-openapi.md) 37 | * [8. 日志管理](pages/log.md) 38 | 39 | 40 | ## Part Ⅱ - 数据访问 41 | * [1. Dapper](pages/dapper.md) 42 | * [2. EF Core](pages/index.md) 43 | 44 | ## Part Ⅱ - 微服务 45 | * [1. Docker](pages/index.md) 46 | * [1.1 Docker简介](pages/docker-intro.md) 47 | * [1.2 安装配置](pages/docker-install.md) 48 | * [1.3 常用命令](pages/docker-cmd.md) 49 | * [1.4 制作镜像](pages/docker-dockerfile.md) 50 | * [2. 微服务](pages/index.md) 51 | * [2.1 微服务架构](pages/microservice-intro.md) 52 | * [2.2 服务治理](pages/microservice-consul.md) 53 | * [2.3 熔断降级](pages/microservice-polly.md) 54 | * [2.4 网关](pages/microservice-ocelot.md) 55 | * [2.5 微服务授权](pages/index.md) 56 | * [2.6 RPC](pages/rpc.md) 57 | * [2.7 Docker+微服务](pages/microservice-docker.md) 58 | 59 | ## PART Ⅲ 其他技术 60 | * [1. 多线程/异步](pages/index.md) 61 | * [1.1 进程、线程和应用程序域](pages/multithreading-intro.md) 62 | * [1.2 进程、线程基础](pages/multithreading-basic.md) 63 | * [1.3 线程同步](pages/multithreading-synchronization.md) 64 | * [1.4 线程池](pages/multithreading-threadpool.md) 65 | * [1.5 UI资源跨线程访问](pages/multithreading-uiresource.md) 66 | * [1.6 异步编程模型](pages/multithreading-async.md) 67 | * [2. 单元测试](pages/unittest.md) 68 | 69 | ## Part IV - 常用解决方案 70 | * [1. AutoFac](pages/autofac.md) 71 | * [2. AutoMapper](pages/automapper.md) 72 | * [3. HTTPS](pages/https.md) 73 | * [4. 邮件短信通知](pages/mailsms.md) 74 | * [5. 在线支付](pages/mailsms.md) 75 | * [6. 页面静态化和SSI](pages/staticssi.md) -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": ".Net Core-软件开发", 3 | "description": ".Net高效跨平台技术方案", 4 | "plugins": [ 5 | "disqus", 6 | "-lunr", "-search", "search-plus", 7 | "advanced-emoji", 8 | "github-buttons", 9 | "multipart", 10 | "splitter", 11 | "tbfed-pagefooter", 12 | "anchors", 13 | "sitemap", 14 | "-sharing", "sharing-plus", 15 | "toggle-chapters", 16 | "book-summary-scroll-position-saver", 17 | "prism", 18 | "-highlight", 19 | "favicon-absolute" 20 | ], 21 | "pluginsConfig": { 22 | "disqus": { 23 | "shortName": "http-colin-chang-site-1" 24 | }, 25 | "github-buttons": { 26 | "buttons": [{ 27 | "user": "colin-chang", 28 | "repo": "netcore", 29 | "type": "star", 30 | "size": "small" 31 | }] 32 | }, 33 | "prism": { 34 | "css": [ 35 | "prismjs/themes/prism-okaidia.css" 36 | ] 37 | }, 38 | "tbfed-pagefooter": { 39 | "copyright": "© Colin Chang", 40 | "modify_label": "文件修订时间:", 41 | "modify_format": "YYYY-MM-DD HH:mm:ss" 42 | }, 43 | "sitemap": { 44 | "hostname": "https://ccstudio.org/netcore" 45 | }, 46 | "sharing": { 47 | "facebook": true, 48 | "twitter": true, 49 | "google": true, 50 | "weibo": false, 51 | "instapaper": false, 52 | "vk": false, 53 | "all": [ 54 | "facebook", "google", "twitter","instapaper","vk","weibo" 55 | ] 56 | }, 57 | "favicon-absolute":{ 58 | "favicon": "https://i.loli.net/2020/02/25/AOjBhkIxtb8dRgl.png", 59 | "bookmark": "https://i.loli.net/2020/02/25/AOjBhkIxtb8dRgl.png", 60 | "appleTouchIcon152": "https://i.loli.net/2020/02/25/wjbT3FDZ2p6BJs1.png", 61 | "appleTouchIconPrecomposed152": "https://i.loli.net/2020/02/25/wjbT3FDZ2p6BJs1.png", 62 | "appleTouchIconMore": { 63 | "120x120": "https://i.loli.net/2020/02/25/h4IDHPOYcrpg2nE.png", 64 | "180x180": "https://i.loli.net/2020/02/25/9q5EVHcRkaNyCiL.png" 65 | }, 66 | "appleTouchIconPrecomposedMore": { 67 | "120x120": "https://i.loli.net/2020/02/25/h4IDHPOYcrpg2nE.png", 68 | "180x180": "https://i.loli.net/2020/02/25/9q5EVHcRkaNyCiL.png" 69 | } 70 | } 71 | }, 72 | "styles": { 73 | "website": "website.css" 74 | } 75 | } -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | 5 | gitbook build 6 | cd _book 7 | rm -f sitemap.xml 8 | rm -f deploy.sh 9 | 10 | git init 11 | git add -A 12 | git commit -m 'deploy' 13 | 14 | git push -f git@github.com:colin-chang/netcore.git master:gh-pages 15 | 16 | cd - 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "netcore", 3 | "version": "1.0.0", 4 | "description": "netcore learning notes", 5 | "main": "index.js", 6 | "dependencies": { 7 | "gitbook-plugin-advanced-emoji": "^0.2.2", 8 | "gitbook-plugin-anchor-navigation": "^0.0.1", 9 | "gitbook-plugin-anchors": "^0.7.1", 10 | "gitbook-plugin-book-summary-scroll-position-saver": "^0.0.7", 11 | "gitbook-plugin-anchor-navigation-ex": "^1.0.14", 12 | "gitbook-plugin-copy-code-button": "^0.0.2", 13 | "gitbook-plugin-disqus": "^0.1.0", 14 | "gitbook-plugin-favicon": "^0.0.2", 15 | "gitbook-plugin-github": "^2.0.0", 16 | "gitbook-plugin-katex": "^1.1.4", 17 | "gitbook-plugin-github-buttons": "^3.0.0", 18 | "gitbook-plugin-multipart": "^0.3.0", 19 | "gitbook-plugin-sitemap": "^1.2.0", 20 | "gitbook-plugin-prism": "^2.4.0", 21 | "gitbook-plugin-splitter": "^0.0.8", 22 | "gitbook-plugin-search-plus": "^1.0.3", 23 | "gitbook-plugin-tbfed-pagefooter": "^0.0.1", 24 | "gitbook-plugin-toggle-chapters": "^0.0.3" 25 | }, 26 | "devDependencies": {}, 27 | "scripts": { 28 | "test": "echo \"Error: no test specified\" && exit 1" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/colin-chang/netcore.git" 33 | }, 34 | "keywords": [ 35 | "netcore" 36 | ], 37 | "author": "Colin Chang", 38 | "license": "ISC", 39 | "bugs": { 40 | "url": "https://github.com/colin-chang/netcore/issues" 41 | }, 42 | "homepage": "https://github.com/colin-chang/netcore#readme" 43 | } 44 | -------------------------------------------------------------------------------- /pages/auth-auth2.md: -------------------------------------------------------------------------------- 1 | # OAuth 2.0 & OpenID Connect 认证 2 | 3 | https://www.cnblogs.com/RainingNight/p/oidc-authentication-in-asp-net-core.html -------------------------------------------------------------------------------- /pages/auth-authentication.md: -------------------------------------------------------------------------------- 1 | # 身份认证 2 | 3 | https://www.cnblogs.com/RainingNight/p/introduce-basic-authentication-in-asp-net-core.html -------------------------------------------------------------------------------- /pages/auth-cookie.md: -------------------------------------------------------------------------------- 1 | # Cookie-based 认证授权 2 | 3 | 这里我们只介绍Asp.Net Core中基于`Cookie`的认证授权使用方式。其认证授权原理参见[Session认证](webapi-security.md/#21-session)。 4 | 5 | Cookie-base认证授权方式多用于Web项目。前后端分离的Web项目含App的多端项目一般多使用JWT认证。 6 | 7 | ## 1. 配置 Authentication 8 | 在Startup中注册认证服务,添加认证中间件。 9 | 10 | ```csharp 11 | public void ConfigureServices(IServiceCollection services) 12 | { 13 | //注册认证服务 14 | services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(); 15 | 16 | services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); 17 | } 18 | 19 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 20 | { 21 | //添加认证中间件 22 | app.UseAuthentication(); 23 | app.UseMvc(); 24 | } 25 | ``` 26 | 27 | ## 2. 登录注销 28 | ```csharp 29 | public class AccountController : Controller 30 | { 31 | public async Task Login(string userName, string password, string returnUrl) 32 | { 33 | if (userName != "admin" || password != "123") 34 | return Content("用户名或密码错误"); 35 | 36 | var claims = new List 37 | { 38 | new Claim(ClaimTypes.Name, userName), 39 | new Claim(ClaimTypes.Role, "admin")//设置角色 40 | }; 41 | 42 | await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, 43 | new ClaimsPrincipal(new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme))); 44 | 45 | if (!string.IsNullOrWhiteSpace(returnUrl)) 46 | return Redirect(returnUrl); 47 | return Ok("登录成功"); 48 | } 49 | 50 | public async Task Logout() 51 | { 52 | await HttpContext.SignOutAsync(); 53 | return Ok("注销成功"); 54 | } 55 | } 56 | ``` 57 | 58 | ## 3. 使用认证授权 59 | ```csharp 60 | [Authorize(Roles = "admin")]//基于基色验证身份 61 | public class HomeController : Controller 62 | { 63 | public IActionResult Index() 64 | { 65 | return View(); 66 | } 67 | } 68 | ``` 69 | 在需要认证授权的`Controller`或`Action`打上`Authorize`标记即可启用认证。认证不通过默认会导航到`/Account/Login`,授权不通过默认会导航到`/Account/AccessDenied`,也可以在注册服务时修改默认配置。 -------------------------------------------------------------------------------- /pages/auth-jwt.md: -------------------------------------------------------------------------------- 1 | # JWT 认证授权 2 | 3 | 这里我们只介绍Asp.Net Core中基于`JWT`的认证授权使用方式。其认证授权原理参见[JWT原理](webapi-security.md/#22-jwt)。 4 | 5 | ## 1. 配置 Authentication 6 | ### 1.1 Jwt配置 7 | 配置文件。 8 | ```json 9 | { 10 | "AllowedHosts": "*", 11 | "JwtSettings": { 12 | "Issuer": "http://localhost:5000", 13 | "Audience": "http://localhost:5000", 14 | "SecretKey": "your_custom_secret_key_more_than_16_bytes" 15 | } 16 | } 17 | ``` 18 | `Issuer`和`Audience`分别代表`JWT`颁发者和接收方,非必须项。`SecretKey`长度至少为128 bits(16 bytes)。 19 | 添加配置项对应实体类。 20 | ```csharp 21 | public class JwtSettings 22 | { 23 | public string Issuer { get; set; } 24 | public string Audience { get; set; } 25 | public string SecretKey { get; set; } 26 | } 27 | ``` 28 | 29 | ### 1.2 Startup配置 30 | 31 | 在Startup中注册认证服务,添加认证中间件。 32 | 33 | ```csharp 34 | public void ConfigureServices(IServiceCollection services) 35 | { 36 | //注册JWT配置 37 | services.Configure(Configuration.GetSection(nameof(JwtSettings))); 38 | 39 | //读取JWT配置 40 | var jwtSettings = new JwtSettings(); 41 | Configuration.Bind(nameof(jwtSettings), jwtSettings); 42 | 43 | //注册认证服务 44 | services.AddAuthentication(options => 45 | { 46 | options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; 47 | options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; 48 | }) 49 | .AddJwtBearer(options => 50 | { 51 | options.TokenValidationParameters = new TokenValidationParameters 52 | { 53 | ValidIssuer = jwtSettings.Issuer, 54 | ValidAudience = jwtSettings.Audience, 55 | IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.SecretKey)) 56 | }; 57 | }); 58 | 59 | //注册授权服务 60 | services.AddAuthorization(options => 61 | options.AddPolicy("sa", policy => policy.RequireClaim("SuperAdmin")));//添加一个名为 "sa"的Policy,要求必须存在"SuperAdmin"的Claim 62 | services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); 63 | } 64 | 65 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 66 | { 67 | //添加认证中间件 68 | app.UseAuthentication(); 69 | app.UseMvc(); 70 | } 71 | ``` 72 | 73 | ## 2. 申请认证 74 | ```csharp 75 | [Route("api/authorize")] 76 | [ApiController] 77 | public class AuthorizeController : ControllerBase 78 | { 79 | private JwtSettings _jwtSettings; 80 | public AuthorizeController(IOptions jwtSettings) 81 | { 82 | _jwtSettings = jwtSettings.Value; 83 | } 84 | 85 | [HttpPost] 86 | public ActionResult Post([FromBody] User user) 87 | { 88 | //登录验证 89 | if (user.UserName != "colin" || user.Password != "123") 90 | return BadRequest("用户名或密码错误"); 91 | 92 | //签发JWT 93 | var claims = new Claim[] 94 | { 95 | new Claim(ClaimTypes.Name, user.UserName), 96 | // new Claim(ClaimTypes.Role, "admin"),//授予admin角色 97 | new Claim("SuperAdmin", "true") //授予SuperAdmin Policy 98 | }; 99 | var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.SecretKey)); 100 | var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); 101 | var token = new JwtSecurityToken(_jwtSettings.Issuer, _jwtSettings.Audience, claims, DateTime.Now, 102 | DateTime.Now.AddMonths(1), credentials); 103 | 104 | var jwt = new JwtSecurityTokenHandler().WriteToken(token); 105 | return Ok(jwt); 106 | } 107 | } 108 | 109 | public class User 110 | { 111 | public string UserName { get; set; } 112 | public string Password { get; set; } 113 | } 114 | ``` 115 | ![JWT认证](https://i.loli.net/2020/02/26/ItW9j2kElwspoZd.jpg) 116 | 117 | ## 3. 使用认证授权 118 | ```csharp 119 | [Authorize("sa")]//基于Policy - "sa" 进行授权检查 120 | [Route("api/values")] 121 | [ApiController] 122 | public class ValuesController : ControllerBase 123 | { 124 | [HttpGet] 125 | public ActionResult> Get() 126 | { 127 | return new string[] {"value1", "value2"}; 128 | } 129 | } 130 | ``` 131 | ![JWT Policy授权](https://i.loli.net/2020/02/26/Nd4FHEakgisMwLj.jpg) 132 | 133 | 标准的`Bearer Token`授权方式,在发送HTTP请求时会在`Request.Header`中添加`Authorization`项,内容是`Bearer Token`。如下图所示。 134 | ![JWT Authorize](https://i.loli.net/2020/02/26/mgjJXvwkTNO1Z6f.jpg) 135 | 136 | 在需要认证授权的`Controller`或`Action`打上`Authorize`标记即可启用认证。现在更多推荐使用基于Policy的授权方式。 -------------------------------------------------------------------------------- /pages/authorization.md: -------------------------------------------------------------------------------- 1 | # 授权 2 | 3 | https://www.cnblogs.com/RainingNight/p/authorization-in-asp-net-core.html -------------------------------------------------------------------------------- /pages/autofac.md: -------------------------------------------------------------------------------- 1 | # AutoFac 2 | -------------------------------------------------------------------------------- /pages/automapper.md: -------------------------------------------------------------------------------- 1 | # AutoMappper 2 | 3 | * [1. 简介](#1-简介) 4 | * [2. 基础应用](#2-基础应用) 5 | * [2.1 简单映射](#21-简单映射) 6 | * [2.2 扁平化映射](#22-扁平化映射) 7 | * [2.3 忽略成员](#23-忽略成员) 8 | * [2.4 自定义映射](#24-自定义映射) 9 | * [2.5 自定义多层映射](#25-自定义多层映射) 10 | * [3. 高级应用](#3-高级应用) 11 | * [3.1 自定义值解析](#31-自定义值解析) 12 | * [3.2 动态类型映射](#32-动态类型映射) 13 | * [3.3 其他](#33-其他) 14 | 15 | ## 1. 简介 16 | 17 | [Automapper](https://automapper.org/) 是一个简单而强大的工具库帮助我们处理对象之间的映射。这些工作通常是枯燥乏味的。目前该项目已被.NET基金会所支持。 18 | 19 | 问AutoMapper有多强大,一句话总结,AutoMapper可以处理几乎所有对象间映射的复杂场景。 20 | 21 | ## 2. 基础应用 22 | AutoMapper 支持: 23 | 24 | * .NET 4.6.1+ 25 | * .NET Standard 2.0+ 26 | 27 | 通过[Nuget](https://www.nuget.org/packages/AutoMapper/)获取AutoMappper即可使用。 28 | 29 | ### 2.1 简单映射 30 | ```csharp 31 | public class User 32 | { 33 | public int Id { get; set; } 34 | public int Age { get; set; } 35 | public string Name { get; set; } 36 | } 37 | 38 | public class UserDTO 39 | { 40 | public int Id { get; set; } 41 | public int Age { get; set; } 42 | public string Name { get; set; } 43 | } 44 | 45 | void BasicMap() 46 | { 47 | //初始化映射关系 48 | Mapper.Initialize(cfg => cfg.CreateMap()); 49 | 50 | var user = new User {Id = 1, Age = 18, Name = "Colin"}; 51 | var userDto = Mapper.Map(user);//对象映射 52 | } 53 | ``` 54 | 55 | AutoMapper中存在以下常用特性: 56 | * AutoMapper将自动**忽略空引用**异常 57 | * 对象成员映射**不区分大小写** 58 | * 继承对象支持映射。 59 | 60 | ### 2.2 扁平化映射 61 | 遵守AutoMapper映射约定命名规范,可以实现对象扁平化映射。目标类属性必须是 源类型中 复杂属性名称+复杂属性类型的内部属性名称。AutoMapper会深度搜索目标类,直到找到匹配的属性为止。 62 | 63 | ```csharp 64 | public class Employee 65 | { 66 | public string Name { get; set; } 67 | public Company Company { get; set; } 68 | } 69 | 70 | public class Company 71 | { 72 | public string Name { get; set; } 73 | public string Address { get; set; } 74 | } 75 | 76 | public class EmployeeDto 77 | { 78 | public string Name { get; set; } 79 | public string CompanyName { get; set; } 80 | public string CompanyAddress { get; set; } 81 | } 82 | 83 | void FlatMap() 84 | { 85 | Mapper.Initialize(cfg => cfg.CreateMap()); 86 | 87 | var employee = new Employee 88 | { 89 | Name = "Colin", 90 | Company = new Company 91 | { 92 | Name = "Chanyi", 93 | Address = "Beijing" 94 | } 95 | }; 96 | var employeeDto = Mapper.Map(employee); 97 | } 98 | ``` 99 | 100 | ### 2.3 忽略成员 101 | 对象映射过程中有些属性可能用不到,我们通过Ignore方法指定忽略映射属性,以减少映射开支和传输流量。 102 | 103 | ```csharp 104 | Mapper.Initialize(cfg => cfg.CreateMap() 105 | .ForMember(d => d.Age, o => o.Ignore()));//忽略Age属性映射 106 | 107 | var user = new User {Id = 1, Age = 18, Name = "Colin"}; 108 | var userDto = Mapper.Map(user); 109 | ``` 110 | 111 | ### 2.4 自定义映射 112 | 当源对象和目标对象的存在不同名或不同级的对应关系时,就需要在初始化映射时手动配置自定义映射关系。 113 | 114 | ```csharp 115 | public class Article 116 | { 117 | public int Id { get; set; } 118 | public string Content { get; set; } 119 | public string TypeName { get; set; } 120 | public IEnumerable Messages { get; set; } 121 | } 122 | 123 | public class ArticleDto 124 | { 125 | public int Id { get; set; } 126 | public string Content { get; set; } 127 | public string Category { get; set; } 128 | public IEnumerable Comments { get; set; } 129 | } 130 | 131 | void CustomMap() 132 | { 133 | Mapper.Initialize(cfg => cfg.CreateMap() 134 | .ForMember(d => d.Category, o => o.MapFrom(s => s.TypeName)) 135 | .ForMember(d => d.Comments, o => o.MapFrom(s => s.Messages)) 136 | ); 137 | 138 | var article = new Article 139 | {Id = 0, Content = "content", TypeName = "fiction", Messages = new[] {"Good"}}; 140 | var articleDto = Mapper.Map(article); 141 | } 142 | ``` 143 | 144 | ### 2.5 自定义多层映射 145 | 自定义复杂对象映射中集合子元素或成员复杂类型又需要自定义映射关系时,姑且称为自定义多层映射,此时我们就需要手动逐个配置映射关系。 146 | 147 | ```csharp 148 | public class Customer 149 | { 150 | public int Id { get; set; } 151 | public string Name { get; set; } 152 | public IEnumerable Orders { get; set; } 153 | } 154 | 155 | public class Order 156 | { 157 | public int Id { get; set; } 158 | public string TradeNo { get; set; } 159 | public int TotalFee { get; set; } 160 | } 161 | 162 | public class CustomerDto 163 | { 164 | public int Id { get; set; } 165 | public string Name { get; set; } 166 | public IEnumerable OrderDtos { get; set; } 167 | } 168 | 169 | public class OrderDto 170 | { 171 | public int Id { get; set; } 172 | public string TradeNo { get; set; } 173 | public int TotalFee { get; set; } 174 | } 175 | 176 | void MultilayerMap() 177 | { 178 | Mapper.Initialize(cfg => 179 | { 180 | //多层映射配置 181 | cfg.CreateMap(); 182 | cfg.CreateMap() 183 | .ForMember(d => d.OrderDtos, o => o.MapFrom(s => s.Orders));//子成员属性映射 184 | }); 185 | 186 | var customer = new Customer() 187 | { 188 | Id = 0, 189 | Name = "Colin", 190 | Orders = new List 191 | { 192 | new Order() 193 | { 194 | Id = 0, 195 | TotalFee = 10, 196 | TradeNo = "123456" 197 | } 198 | } 199 | }; 200 | var customerDto = Mapper.Map(customer); 201 | } 202 | ``` 203 | 204 | ## 3. 高级应用 205 | ### 3.1 自定义值解析 206 | AutoMapper支持自定义解析,需要提供IValueResolver对象。 207 | 208 | ```csharp 209 | public class Student 210 | { 211 | public string Name { get; set; } 212 | public int Score { get; set; } 213 | } 214 | 215 | public class StudentDto 216 | { 217 | public string Name { get; set; } 218 | public Grade Score { get; set; } 219 | } 220 | 221 | public enum Grade { A, B, C } 222 | 223 | //自定义解析器 224 | public class ScoreResolver : IValueResolver 225 | { 226 | public Grade Resolve(Student source, StudentDto destination, Grade destMember, ResolutionContext context) 227 | { 228 | var score = source.Score; 229 | if (score >= 90) 230 | return Grade.A; 231 | else if (score >= 80) 232 | return Grade.B; 233 | else 234 | return Grade.C; 235 | } 236 | } 237 | 238 | void ValueResolverMap() 239 | { 240 | Mapper.Initialize(cfg => 241 | cfg.CreateMap().ForMember(d => d.Score, o => o.MapFrom())); 242 | 243 | var student = new Student {Name = "Colin", Score = 95}; 244 | var studentDto = Mapper.Map(student); 245 | } 246 | ``` 247 | 248 | ### 3.2 动态类型映射 249 | AutoMapper支持.Net动态对象映射。 250 | ```csharp 251 | private static void DynamicMap() 252 | { 253 | Mapper.Initialize(cfg => { });//动态类型映射不需要初始化配置内容 254 | 255 | dynamic user = new ExpandoObject(); 256 | user.Id = 1; 257 | user.Name = "Colin"; 258 | user.Age = 18; 259 | 260 | var u = Mapper.Map(user); 261 | } 262 | ``` 263 | 264 | ### 3.3 其他 265 | AutoMapper支持依赖注入,支持ORM等。如对EF的支持示例如下: 266 | 267 | ```csharp 268 | IQueryable customers = null; 269 | var customersDTO = customers.ProjectTo(); 270 | ``` 271 | 272 | **结束语:** 273 | 274 | 以上只是简单列举了AutoMapper的常用情景,其功能远不止如此,AutoMapper功能异常强大,几乎覆盖了对象映射的所有场景。更多更详尽的AutoMapper使用可以查阅其[官方文档](https://automapper.readthedocs.io/en/latest/)。 275 | 276 | 本文档中所有示例代码已共享到Github。 277 | 278 | 代码下载地址:https://github.com/colin-chang/AutoMapperSample 279 | 280 | > 参考文档 281 | 282 | * https://yq.aliyun.com/articles/318075/ 283 | * https://automapper.readthedocs.io/en/latest/ -------------------------------------------------------------------------------- /pages/config.md: -------------------------------------------------------------------------------- 1 | # 配置管理 2 | 3 | .Net Core项目配置使用方式与.Net Framework程序不同。 4 | 5 | .Net Core配置依赖于[`Microsoft.Extensions.Configuration`](https://www.nuget.org/packages/Microsoft.Extensions.Configuration/)和[`Microsoft.Extensions.Configuration.Json`](https://www.nuget.org/packages/Microsoft.Extensions.Configuration.Json)(使用Json配置文件时需要)。新版`Microsoft.AspNetCore.App`包中默认包含了以上两个Nuget包,所以Asp.Net Core应用管理配置不需要再额外引用相关Nuget包。 6 | 7 | .Net Core 配置内容都是以 key-value 对形式存在的。 8 | 9 | ## 1. 命令行和内存配置 10 | .Net Core程序读取命令行配置依赖于[`Microsoft.Extensions.Configuration.CommandLine`](https://www.nuget.org/packages/Microsoft.Extensions.Configuration.CommandLine)Nuget包(Asp.Net Core默认已安装)。 11 | 12 | 我们可以通过以下语法读取命令行和内存配置数据。 13 | ```csharp 14 | static void Main(string[] args) 15 | { 16 | var settings = new Dictionary 17 | { 18 | {"name", "Colin"}, 19 | {"age", "18"} 20 | }; 21 | 22 | var config = new ConfigurationBuilder() //实例化配置对象工厂 23 | .AddInMemoryCollection(settings) //使用内存集合配置 24 | .AddCommandLine(args) //使用命令行配置 25 | .Build(); //获取配置根对象 26 | 27 | //获取配置 28 | Console.WriteLine($"name:{config["name"]} \t age:{config["age"]}"); 29 | } 30 | ``` 31 | 32 | 运行以上程序。 33 | ```sh 34 | $ dotnet run cmddemo # 输出 name:Colin age:18 35 | $ dotnet run cmddemo name=Robin age=20 # 输出 name:Robin age:20 36 | $ dotnet run cmddemo --name Robin --age 20 # 输出 name:Robin age:20 37 | ``` 38 | 39 | 由于`AddCommandLine()`在`AddInMemoryCollection()`之后,所以当命令行有参数时会覆盖内存配置信息。 40 | 41 | ## 2. Json文件配置 42 | 相比与命令行和内存配置,我们更常用Json文件来存储配置信息。这Json文件内容没有任何要求,只要符合Json格式即可。 43 | 44 | 假定项目目录下有名为`appsettings.json`的配置文件,内容如下: 45 | ```json 46 | { 47 | "AppName": "配置测试", 48 | "Class": { 49 | "ClassName": "三年二班", 50 | "Master": { 51 | "Name": "Colin", 52 | "Age": 25 53 | }, 54 | "Students": [ 55 | { 56 | "Name": "Robin", 57 | "Age": 20 58 | }, 59 | { 60 | "Name": "Sean", 61 | "Age": 23 62 | } 63 | ] 64 | } 65 | } 66 | ``` 67 | 68 | ```csharp 69 | static void Main(string[] args) 70 | { 71 | var config = new ConfigurationBuilder() 72 | .AddJsonFile("appsettings.json") 73 | .Build(); 74 | 75 | Console.WriteLine($"AppName:{config["AppName"]}"); 76 | Console.WriteLine($"ClassName:{config["Class:ClassName"]}"); 77 | Console.WriteLine($"Master:\r\nName:{config["Class:Master:Name"]}\tAge:{config["Class:Master:Age"]}"); 78 | Console.WriteLine("Students:"); 79 | Console.WriteLine($"Name:{config["Class:Students:0:Name"]}\tAge:{config["Class:Students:0:Age"]}"); 80 | Console.WriteLine($"Name:{config["Class:Students:1:Name"]}\tAge:{config["Class:Students:1:Age"]}"); 81 | } 82 | ``` 83 | 84 | 除了可以使用IConfiguration类型的索引器方式读取配置,还可以通过其`GetSection(string key)`方法读取配置。`GetSection()`方法返回类型为`IConfigurationSection`,可以链式编程方式读取多层配置。 85 | 86 | ```csharp 87 | var clsName = config.GetSection("Class").GetSection("ClassName").Value; //clsName="三年二班" 88 | ``` 89 | 90 | ## 3. 配置对象映射 91 | 92 | 前面提到的配置读取方式只能读取到配置项的字符串格式的内容,遇到较为复杂的配置我们更期望配置信息可以映射为C#当中的一个对象。 93 | 94 | 我们为前面使用的配置文件定义实体类内容如下: 95 | ```csharp 96 | public class Class 97 | { 98 | public string ClassName { get; set; } 99 | public Master Master { get; set; } 100 | public IEnumerable Students { get; set; } 101 | } 102 | public abstract class Person 103 | { 104 | public string Name { get; set; } 105 | public int Age { get; set; } 106 | } 107 | public class Master : Person{} 108 | public class Student : Person{} 109 | ``` 110 | 111 | ### 3.1 Bind 112 | [Microsoft.Extensions.Configuration.Binder](https://www.nuget.org/packages/Microsoft.Extensions.Configuration.Binder)为IConfiguration扩展了三个`Bind()`方法,其作用是尝试将给定的配置信息映射为一个对象。 113 | 114 | 1) .Net Core 115 | 116 | ```csharp 117 | var cls = new Class(); 118 | config.Bind("Class",cls); // 执行完成后配置文件内容将映射到cls对象中 119 | ``` 120 | 121 | 2) Asp.Net Core 122 | 123 | Asp.Net Core中默认包含了需要的Nuget包,在`Startup.cs`中直接使用`Configuration.Bind()`即可获得配置映射的Class对象,如需在其他位置使用此配置对象,需要手动将其注册到服务列表中。 124 | ```csharp 125 | public void ConfigureServices(IServiceCollection services) 126 | { 127 | // other services ... 128 | 129 | var cls = new Class(); 130 | Configuration.Bind("Class",cls); 131 | services.AddSingleton(cls); //服务注册 132 | } 133 | ``` 134 | 135 | ### 3.2 Config<T> 136 | 137 | [Microsoft.Extensions.Options.ConfigurationExtensions](https://www.nuget.org/packages/Microsoft.Extensions.Options.ConfigurationExtensions)包为`IServiceCollection`扩展了Configure<T>方法,其作用是注册一个配置对象并绑定为IOptions<T>对象。该种方式配合DI使用,DI的详细介绍参阅[依赖注入](di-intro.md)。 138 | 139 | 1) .Net Core 140 | 141 | 普通.Net Core项目使用DI需要引入[`Microsoft.Extensions.DependencyInjection`](https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection) Nuget包。 142 | 143 | ```csharp 144 | //注册服务 145 | var serviceCollection = new ServiceCollection(); 146 | serviceCollection.Configure(config.GetSection("Class")); 147 | 148 | //消费服务 149 | var cls = serviceCollection.BuildServiceProvider().GetService>().Value; 150 | ``` 151 | 152 | 2) Asp.Net Core 153 | 154 | 在Asp.Net Core中配置使用十分简便,在`Startup.cs`中作如下配置: 155 | ```csharp 156 | public void ConfigureServices(IServiceCollection services) 157 | { 158 | // other services ... 159 | 160 | services.Configure(Configuration.GetSection("Class")); //注册配置服务 161 | } 162 | ``` 163 | 164 | 在控制器等位置消费服务与普通IOptions服务一样。 165 | ```csharp 166 | private readonly Class _cls; 167 | 168 | public HomeController(IOptions classAccesser) 169 | { 170 | _cls = classAccesser.Value; 171 | } 172 | ``` 173 | 174 | ## 4. 配置文件热更新 175 | .Net Core中配置文件是支持热更新的。在`ConfigurationBuilder`的`AddJsonFile()`方法中`reloadOnChange`参数表示配置文件变更后是否自动重新加载(热更新)。 176 | 177 | ```csharp 178 | new ConfigurationBuilder().AddJsonFile("appsettings.json", true, true) 179 | ``` 180 | 181 | [3.1 Bind](#31-bind)方式配置文件读取方式并不支持热更新。[Config<T>](#32-configt)方式支持配置文件热更新但是需要使用 IOptionsSnapshot<T> 替换 IOptions<T>。 182 | 183 | ```csharp 184 | private readonly Class _cls; 185 | 186 | public HomeController(IOptionsSnapshot classAccesser) 187 | { 188 | _cls = classAccesser.Value; 189 | } 190 | ``` 191 | 192 | 在Asp.Net Core中不指定配置文件时默认使用应用根目录下的`appsettings.json`文件作为配置文件并且启用了热更新,这在`WebHost.CreateDefaultBuilder(args)`过程中完成,若要使用自定义配置文件名称可以通过以下方式修改。 193 | 194 | ```csharp 195 | WebHost.CreateDefaultBuilder(args) 196 | .ConfigureAppConfiguration(config => config.AddJsonFile("myconfig.json",true,false)) 197 | ``` 198 | 199 | 开启配置文件热更新后程序会启动一个后台线程监听配置文件是否变动,如果配置文件不需要经常改动可以关闭配置文件热更新以减少系统开支,关闭方式同上。 200 | 201 | ## 5. 配置管理工具类封装 202 | 在Asp.Net Core程序中我们可以方便的通过以上[Config<T>](#32-configt)方式使用配置,但在其它.Net Core应用中DI并未默认被引入,我们可以考虑配置文件读取操作封装为一个工具类。考虑到配置文件热更新问题对象映射我们采用Config<T>方式处理。 203 | 204 | 代码已上传到Github,这里不再展开。 205 | https://github.com/colin-chang/ConfigurationManager.Core 206 | 207 | 具体使用方式可以查看示例项目。 208 | https://github.com/colin-chang/ConfigurationManager.Core/tree/master/ColinChang.ConfigurationManager.Sample 209 | 210 | > 该帮助类已发布到Nuget 211 | 212 | ```sh 213 | # Package Manager 214 | Install-Package ColinChang.ConfigurationManager.Core 215 | 216 | # .NET CLI 217 | dotnet add package ColinChang.ConfigurationManager.Core 218 | ``` 219 | 220 | ## 6. Configuration框架解析 221 | 222 | ![Configuration框架解析](https://i.loli.net/2020/02/26/gjoYkJf2PiR83LO.jpg) -------------------------------------------------------------------------------- /pages/dapper.md: -------------------------------------------------------------------------------- 1 | # Dapper 全攻略 2 | * [1. 简介](#1-简介) 3 | * [2. 项目/模型](#2-项目模型) 4 | * [3. CRUD](#3-crud) 5 | * [4. 其他](#4-其他) 6 | * [5. Dapper Plus](#5-dapper-plus) 7 | 8 | ## 1. 简介 9 | [Dapper](https://github.com/StackExchange/Dapper)是.NET下一个轻量级的ORM框架,它和Entity Framework或Nhibnate不同,属于轻量级的,并且是半自动的。也就是说实体类都要自己写。它没有复杂的配置文件,一个单文件就可以了。Dapper通过提供IDbConnection扩展方法来进行工作。 10 | 11 | Dapper没有定义特定的数据库要求,它支持所有ADO.NET支持的数据库,如SQLite,SQL CE,Firebird,Oracle,MySQL,PostgreSQL,SQL Server等。 12 | 13 | 国外知名网站Stack Overflow生产环境使用Dapper进行数据库访问。 14 | ## 2. 项目/模型 15 | 下面我们通过一个简单的.Net Core控制台项目来快速入门Dappper使用。数据库使用MySQL。 16 | 17 | ### 2.1 创建项目 18 | ```sh 19 | # 创建.net core控制台项目 20 | $ dotnet new console -n DapperDemo 21 | 22 | # 引用Dapper和MySQL nuget包 23 | $ dotnet add package Dapper 24 | $ dotnet add package MySql.Data 25 | ``` 26 | ### 2.2 数据模型 27 | #### 1) 数据库 28 | ![数据库结构](https://i.loli.net/2020/02/26/fW5d4IEkZ2OpvFS.jpg) 29 | ```sql 30 | CREATE TABLE `article` ( 31 | `Id` int(11) NOT NULL AUTO_INCREMENT, 32 | `Title` varchar(255) NOT NULL, 33 | `Content` text NOT NULL, 34 | `Status` int(1) NOT NULL DEFAULT '1', 35 | `UpdateTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, 36 | `AuthorId` int(11) NOT NULL, 37 | PRIMARY KEY (`Id`) 38 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 39 | 40 | CREATE TABLE `author` ( 41 | `Id` int(11) NOT NULL AUTO_INCREMENT, 42 | `NickName` varchar(255) NOT NULL, 43 | `RealName` varchar(255) NOT NULL, 44 | `BirthDate` date DEFAULT NULL, 45 | `Address` varchar(255) DEFAULT NULL, 46 | PRIMARY KEY (`Id`) 47 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 48 | 49 | CREATE TABLE `comment` ( 50 | `Id` int(11) NOT NULL AUTO_INCREMENT, 51 | `ArticleId` int(11) NOT NULL, 52 | `Content` varchar(255) NOT NULL, 53 | `CreateTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, 54 | PRIMARY KEY (`Id`) 55 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 56 | ``` 57 | #### 2) 数据模型 58 | ```csharp 59 | public abstract class BaseModel 60 | { 61 | public int Id { get; set; } 62 | } 63 | 64 | public class Author : BaseModel 65 | { 66 | public string NickName { get; set; } 67 | public string RealName { get; set; } 68 | public DateTime? BirthDate { get; set; } 69 | public string Address { get; set; } 70 | 71 | public Author() { } 72 | public Author(string nickName, string realName) 73 | { 74 | NickName = nickName; 75 | RealName = realName; 76 | } 77 | } 78 | 79 | public class Article : BaseModel 80 | { 81 | public string Title { get; set; } 82 | public string Content { get; set; } 83 | public ArticleStatus Status { get; set; } 84 | public DateTime UpdateTime { get; set; } 85 | public int AuthorId { get; set; } 86 | public Author Author { get; set; } 87 | public IEnumerable Comments { get; set; } 88 | } 89 | 90 | public class Comment : BaseModel 91 | { 92 | public int ArticleId { get; set; } 93 | public Article Article { get; set; } 94 | public string Content { get; set; } 95 | public DateTime CreateTime { get; set; } 96 | } 97 | 98 | public enum ArticleStatus 99 | { 100 | Abnormal, 101 | Normal 102 | } 103 | ``` 104 | ## 3. CRUD 105 | 建立数据库连接。 106 | ```csharp 107 | private static readonly string _connStr; 108 | private static IDbConnection Cnn => new MySqlConnection(_connStr); 109 | static DapperPlus() 110 | { 111 | _connStr = "Server=127.0.0.1;Database=db_dapper;Uid=root;Pwd=xxxxxx;"; 112 | } 113 | ``` 114 | ### 3.1 非查询操作 115 | #### 1) 插入数据 116 | Dapper可以使用同样的方式插入一条或多条数据。 117 | ```csharp 118 | string sql = "INSERT INTO author (NickName,RealName) VALUES(@nickName,@RealName)"; 119 | var colin = new Author("Colin", "Colin Chang"); 120 | var robin = new Author("Robin", "Robin Song"); 121 | 122 | using (var cnn = Cnn) 123 | { 124 | await cnn.ExecuteAsync(sql, new Author[] { colin, robin }); 125 | } 126 | ``` 127 | #### 2) 更新数据 128 | ```csharp 129 | string sql = "UPDATE author SET Address=@address WHERE Id=@id"; 130 | using (var cnn = Cnn) 131 | { 132 | await cnn.ExecuteAsync(sql, new { id = 1, address = "山东" }); 133 | } 134 | ``` 135 | #### 3) 删除数据 136 | ```csharp 137 | string sql = "DELETE FROM author WHERE Id=@id"; 138 | using (var cnn = Cnn) 139 | { 140 | await cnn.ExecuteAsync(sql,new {id=2}); 141 | } 142 | ``` 143 | 144 | ### 3.2 查询操作 145 | #### 1) 简单查询 146 | ```csharp 147 | var sql = "SELECT * FROM author WHERE Id=@id"; 148 | using (var cnn = Cnn) 149 | { 150 | var authors = await cnn.QueryAsync(sql, new { id = 1 }); 151 | } 152 | ``` 153 | 常用的`IN ()`方式查询 154 | ```csharp 155 | var sql = "SELECT * FROM author WHERE Id IN @ids"; 156 | using (var cnn = Cnn) 157 | { 158 | var authors = await cnn.QueryAsync(sql, new { ids = new int[] { 1, 2 } }); 159 | } 160 | ``` 161 | #### 2) 多表连接查询 162 | 此处演示使用三表连接查询,同时包含`1:1`和`1:N`的关系。 163 | ```csharp 164 | var sql = @"SELECT * FROM article AS ar JOIN author AS au ON ar.AuthorId = au.Id LEFT JOIN `comment` AS c ON ar.Id = c.ArticleId"; 165 | var articles = new Dictionary(); 166 | using (var cnn = Cnn) 167 | { 168 | var data = await cnn.QueryAsync(sql, 169 | (article, author, comment) => 170 | { 171 | //1:1 172 | article.Author=author; 173 | 174 | //1:N 175 | if (!articles.TryGetValue(article.Id, out Article articleEntry)) 176 | { 177 | articleEntry = article; 178 | articleEntry.Comments = new List{}; 179 | articles.Add(article.Id, articleEntry); 180 | } 181 | articleEntry.Comments = articleEntry.Comments.Append(comment); 182 | return articleEntry; 183 | }); 184 | // var result= data.Distinct(); 185 | } 186 | var result = articles.Values; 187 | //data.Distinct()和articles.Values都可以拿到数据,且数据内容相同。 188 | ``` 189 | `1:N`关系的连接查,查询出来的数据都是连接展开之后的全部数据记录,以上代码中的Lambda表达式会在遍历没条数据记录时执行一次。 190 | 191 | #### 3) 多结果集查询 192 | Dapper支持多结果集查询,可以执行任意多条查询语句。 193 | ```csharp 194 | // 多结果集查询 195 | string sqls = @" 196 | SELECT * FROM article WHERE Id=@id; 197 | SELECT * FROM `comment` WHERE ArticleId=@articleId;"; 198 | 199 | using (var cnn = Cnn) 200 | { 201 | var reader = await cnn.QueryMultipleAsync(sqls, new { id = 1, articleId = 1 }); 202 | var articles = await reader.ReadAsync
(); 203 | var comments= await reader.ReadAsync(); 204 | 205 | var article = articles.FirstOrDefault(); 206 | if (article != null) 207 | article.Comments = comments; 208 | } 209 | ``` 210 | 多结果集查询中,配合使用多条存在一定关联关系的查询语句,可以在一定程上巧妙的实现连接查询的效果,避免多表连接查询锁表的问题。以上代码即实现了此种效果。 211 | 212 | ## 3. 事务和存储过程 213 | 214 | ### 3.1 事务 215 | ```csharp 216 | var scripts = new SqlScript[] 217 | { 218 | new SqlScript("UPDATE article SET UpdateTime=NOW() WHERE Id=@id",new {id=2}), 219 | new SqlScript("UPDATE author SET BirthDate=NOW() WHERE Id=@id",new {id=1}) 220 | }; 221 | 222 | using (var cnn = Cnn) 223 | { 224 | IDbTransaction tran = null; 225 | try 226 | { 227 | cnn.Open(); 228 | tran = cnn.BeginTransaction(); 229 | foreach (var script in scripts) 230 | await cnn.ExecuteAsync(script.Sql, script.Param, tran, commandType: script.CommandType); 231 | 232 | tran.Commit(); 233 | } 234 | catch 235 | { 236 | tran?.Rollback(); 237 | } 238 | } 239 | ``` 240 | 以上演示用到的脚本模型类如下: 241 | ```csharp 242 | public class SqlScript 243 | { 244 | public string Sql { get; set; } 245 | public object Param { get; set; } 246 | public CommandType CommandType { get; set; } 247 | 248 | public SqlScript(string sql, object param = null, CommandType cmdType = CommandType.Text) 249 | { 250 | Sql = sql; 251 | Param = param; 252 | CommandType = cmdType; 253 | } 254 | } 255 | ``` 256 | ### 3.2 存储过程 257 | Dapper完全支持存储过程。存储过程比较简单,代码就不展示了,读者可以自己按照自己想法随意创建。 258 | ```csharp 259 | using (var cnn = Cnn) 260 | { 261 | var users = cnn.Query("spGetAuthors", new {Id = 1}, commandType: CommandType.StoredProcedure); 262 | } 263 | ``` 264 | 使用传入传出参数的存储过程。 265 | ```csharp 266 | var p = new DynamicParameters(); 267 | p.Add("@a", 11); 268 | p.Add("@b", dbType: DbType.Int32, direction: ParameterDirection.Output); 269 | p.Add("@c", dbType: DbType.Int32, direction: ParameterDirection.ReturnValue); 270 | using (var cnn = Cnn) 271 | { 272 | cnn.Execute("spMagicProc", p, commandType: CommandType.StoredProcedure); 273 | } 274 | 275 | int b = p.Get("@b"); 276 | int c = p.Get("@c"); 277 | ``` 278 | 279 | ## 4. 其他 280 | ### 4.1 参数替换 281 | Dapper支持对SQL语句中bool和数字类型进行替换。 282 | ```csharp 283 | var sql = "SELECT * FROM article WHERE Status= {=Normal}"; 284 | using (var cnn = Cnn) 285 | { 286 | var articles = await cnn.QueryAsync
(sql, new {ArticleStatus.Normal}); 287 | } 288 | ``` 289 | 参数替换在特定类型字段中非常好用,比如"category id", "status code" or "region" 290 | 291 | **参数替换并非采用参数话查询,虽然使用方便但是建议经过测试后谨慎使用。** 292 | 293 | ### 4.2 缓存查询 294 | 默认情况下Dapper会对执行SQL后的整个reader进行缓存,以减少数据库锁定和网络请求时间。然而执行大批量查询操作时缓存会占用大量内存空间,此时执行查询操作可以设置`buffered: false` 以禁用缓存。 295 | 296 | ### 4.3 ANSI编码 297 | Dapper支持varchar类型参数,如果查询语句需要过滤一个varchar类型的字段可以使用以下方式指定编码: 298 | ```csharp 299 | Query("select * from Author where Address = @address", new {address = new DbString { Value = "山东", IsFixedLength = true, Length = 10, IsAnsi = true }); 300 | ``` 301 | SQL Server中查询unicode and ANSI字段时务必使用unicode编码 302 | 303 | ### 4.4 多数据类型行 304 | 某些情况下同一行数据的某个字段可以是不同的数据类型。这种情况使用`IDataReader.GetRowParser`非常方便。 305 | 306 | 307 | 308 | 有shapes表结构如上图,我们可以根据Type字段将每行数据映射为`Circle`,`Square`,`Triangle`等具体类型对象。以下为示例代码: 309 | 310 |
311 | 312 | ```csharp 313 | var shapes = new List(); 314 | using (var reader = connection.ExecuteReader("select * from Shapes")) 315 | { 316 | // Generate a row parser for each type you expect. 317 | // The generic type is what the parser will return. 318 | // The argument (typeof(*)) is the concrete type to parse. 319 | var circleParser = reader.GetRowParser(typeof(Circle)); 320 | var squareParser = reader.GetRowParser(typeof(Square)); 321 | var triangleParser = reader.GetRowParser(typeof(Triangle)); 322 | 323 | var typeColumnIndex = reader.GetOrdinal("Type"); 324 | 325 | while (reader.Read()) 326 | { 327 | IShape shape; 328 | var type = (ShapeType)reader.GetInt32(typeColumnIndex); 329 | switch (type) 330 | { 331 | case ShapeType.Circle: 332 | shape = circleParser(reader); 333 | break; 334 | case ShapeType.Square: 335 | shape = squareParser(reader); 336 | break; 337 | case ShapeType.Triangle: 338 | shape = triangleParser(reader); 339 | break; 340 | default: 341 | throw new NotImplementedException(); 342 | } 343 | 344 | shapes.Add(shape); 345 | } 346 | } 347 | ``` 348 | 349 | ## 5. Dapper Plus 350 | Dapper仅提供了SqlHelper常用功能和对象映射,我们通常会对Dapper进行二次封装扩展以更方便的使用Dapper。 下面Dapper扩展在无损Dapper性能的前提下,基本覆盖了日常数据操作,仅供参考。 351 | 352 | 代码已上传到Github,这里不再展开。 353 | https://github.com/colin-chang/DapperHelper 354 | 355 | 具体使用方式可以查看单元测试 356 | https://github.com/colin-chang/DapperHelper/blob/master/ColinChang.DapperHelper.Test/DapperPlusTest.cs 357 | 358 | > 该帮助类已发布到Nuget 359 | 360 | ```sh 361 | # Package Manager 362 | Install-Package ColinChang.DapperHelper 363 | 364 | # .NET CLI 365 | dotnet add package ColinChang.DapperHelper 366 | ``` -------------------------------------------------------------------------------- /pages/di-aspnetcore.md: -------------------------------------------------------------------------------- 1 | # Asp.Net Core 依赖注入使用 2 | * [1. 依赖注入在管道构建过程中的使用](#1-依赖注入在管道构建过程中的使用) 3 | * [2. 依赖服务注册](#2-依赖服务注册) 4 | * [3. 依赖服务消费](#3-依赖服务消费) 5 | * [3.1 Controller/PageModel](#31-controllerpagemodel) 6 | * [3.2 View](#32-view) 7 | * [3.3 HttpContext获取实例](#33-httpcontext获取实例) 8 | 9 | ## 1. 依赖注入在管道构建过程中的使用 10 | 在ASP.NET Core管道的构架过程中主要涉及三个对象/类型,作为宿主的WebHost和它的创建者WebHostBuilder,以及注册到WebHostBuilder的Startup类型。 如下的代码片段体现了启动ASP.NET Core应用采用的典型编程模式:我们首先创建一个IWebHostBuilder对象,并将Startup类型注册到它之上。在调用Build方法创建WebHost之前,我们还可以调用相应的方式做其他所需的注册工作。当我们调用WebHost的Run方法之后,后者会利用注册的Startup类型来构建完整的管道。那么在管道的构建过程中,DI是如何被应用的呢? 11 | 12 | ```csharp 13 | WebHost.CreateDefaultBuilder(args) 14 | .UseStartup() 15 | .Xxx 16 | .Build() 17 | .Run(); 18 | ``` 19 | 20 | DI在ASP.NET Core管道构建过程中的应用基本体现在下面这个序列图中。当我们调用WebHostBuilder的Build方法创建对应的WebHost的时候,前者会创建一个ServiceCollection对象,并将一系列预定义的服务注册在它之上。接下来WebHostBuilder会利用这个ServiceCollection对象创建出对应的ServiceProvider,这个ServiceProvider和ServiceCollection对象会一并传递给最终创建WebHost对象。当我们调用WebHost的Run方法启动它的时候,如果注册的Startup是一个实例类型,则会以构造器注入的方式创建对应的Startup对象。我们注册的Startup类型的构造函数是允许定义参数的,但是参数类型必须是预先注册到ServiceCollection中的服务类型。 21 | 22 | ![DI在ASP.NET Core管道构建过程中的应用](https://i.loli.net/2020/02/26/Ugw9JOZxdmRMr7h.png) 23 | 24 | 注册的Startup方法可以包含一个可选的ConfigureServices方法,这个方法具有一个类型为IServiceCollection接口的参数。WebHost会将WebHostBuilder传递给它的ServiceCollection作为参数调用这个ConfigureServices方法,而我们则利用这个方法将注册的中间件和应用所需的服务注册到这个ServiceCollection对象上。在这之后,所有需要的服务(包括框架和应用注册的服务)都注册到这个ServiceCollection上面,WebHost会利用它创建一个新的ServiceProvider。WebHost会利用这个ServiceProvider对象以方法注入的方式调用Startup对象/类型的Configure方法,最终完成你对整个管道的建立。换句话会说,定义在Startup类型中旨在用于注册Middleware的Configure方法除了采用IApplicationBuilder作为第一个参数之外,它依然可以采用注册的任何一个服务类型作为后续参数的类型。 25 | 26 | 服务的注册除了现在注册的Startup类型的ConfigureServices方法之外,实际上还具有另一个实现方式,那就是调用IWebHostBuilder定义的ConfigureServices方法。当WebHostBuilder创建出ServiceCollection对象并完成了默认服务的注册后,我们通过调用这个方法所传入的所有Action<IServiceCollection>对象将最终应用到这个ServiceCollection对象上。 27 | 28 | ```csharp 29 | public interface IWebHostBuilder 30 | { 31 | IWebHostBuilder ConfigureServiecs(Action configureServices); 32 | } 33 | ``` 34 | 35 | 值得一提的是,Startup类型的ConfigureServices方法是允许具有一个IServiceProvider类型的返回值,如果这个方法返回一个具体的ServiceProrivder,那么WebHost将不会利用ServiceCollection来创建ServiceProvider,而是直接使用这个返回的ServiceProvider来调用Startup对象/类型的Configure方法。这实际上是一个很有用的扩展点,使用它可以实现针对第三方DI框架(如Unity、Castle、Ninject和AutoFac等)的集成。 36 | 37 | 这里我们只是简单的介绍了Asp.Net Core程序启动的简单过程,具体实现细节属于Asp.Net Core框架的内容,我们将在后续[Asp.Net Core 程序启动源码和DI源码分析](disrc.md)中做详细介绍 38 | 39 | ## 2. 依赖服务注册 40 | 接下来我们通过一个实例来演示如何利用Startup类型的ConfigureServices来注册服务,以及在Startup类型上的两种依赖注入形式。如下面的代码片段所示,我们定义了两个服务接口(IFoo和IBar)和对应的实现类型(Foo和Bar)。其中服务Foo是通过调用WebHostBuilder的ConfigureServices方法进行注册的,而另一个服务Bar的注册则发生在Startup的ConfigureServices方法上。对于Startup来说,它具有一个类型为IFoo的只读属性,该属性在构造函数利用传入的参数进行初始化,不用说这体现了针对Startup的构造器注入。Startup的Configure方法除了ApplicationBuilder作为第一个参数之外,还具有另一个类型为IBar的参数,我们利用它来演示方法注入。 41 | 42 | ```csharp 43 | public interface IFoo { } 44 | public interface IBar { } 45 | public class Foo : IFoo { } 46 | public class Bar : IBar { } 47 | 48 | public class Program 49 | { 50 | public static void Main(string[] args) 51 | { 52 | WebHost.CreateDefaultBuilder(args) 53 | .ConfigureServices(services => services.AddSingleton()) 54 | .UseStartup() 55 | .Build() 56 | .Run(); 57 | } 58 | } 59 | public class Startup 60 | { 61 | public IFoo Foo { get; private set; } 62 | public Startup(IFoo foo) 63 | { 64 | this.Foo = foo; 65 | } 66 | public void ConfigureServices(IServiceCollection services) 67 | { 68 | // 最常用的服务注册方式 69 | services.AddTransient(); 70 | } 71 | 72 | public void Configure(IApplicationBuilder app, IBar bar) 73 | { 74 | app.Run(async context => 75 | { 76 | context.Response.ContentType = "text/html"; 77 | await context.Response.WriteAsync($"IFoo=>{this.Foo}
"); 78 | await context.Response.WriteAsync($"IBar=>{bar}"); 79 | }); 80 | } 81 | } 82 | ``` 83 | 84 | 在Startup的Configure方法中,我们调用IApplicationBulder的Run方法注册了一个Middleware,后者将两个注入的服务的类型作为响应的内容输出。 85 | 86 | ![依赖服务的注册与注入](https://i.loli.net/2020/02/26/ZFNTHnvIwQJgKuA.jpg) 87 | 88 | 另外,WebHostBuilder在创建ServiceCollection之后,会注册一些默认的服务(如IHostingEnvironment,ILoggerFactory等)。这些服务和我们自行注册的服务并没有任何区别,只要我们知道对应的服务类型,就可以通过注入的方式获取并使用它们。 89 | 90 | ASP.NET Core的一些组件已经提供了一些实例的绑定,像AddMvc就是Mvc Middleware在 IServiceCollection上添加的扩展方法。 91 | 92 | ```csharp 93 | public static IMvcBuilder AddMvc(this IServiceCollection services) 94 | { 95 | if (services == null) 96 | { 97 | throw new ArgumentNullException(nameof(services)); 98 | } 99 | 100 | var builder = services.AddMvcCore(); 101 | 102 | builder.AddApiExplorer(); 103 | builder.AddAuthorization(); 104 | AddDefaultFrameworkParts(builder.PartManager); 105 | ... 106 | } 107 | ``` 108 | 109 | ## 3. 依赖服务消费 110 | 依赖服务之后就可以在需要的位置消费服务了。DI的[三种注入方式](di.md),Asp.Net Core默认仅支持构造器注入方式和面向约定的方法注入(框架级别使用,如Starup的Config方法)。上面案例中在Startup的构造函数和Config方法分别体现了两种注入方式。 111 | 112 | 下面我们来演示在Asp.Net Core项目中Startup之外的位置如何消费DI服务。 113 | 114 | ### 3.1 Controller/PageModel 115 | ```csharp 116 | private ILoginService _loginService; 117 | public AccountController( 118 | ILoginService loginService) 119 | { 120 | _loginService = loginService; 121 | } 122 | ``` 123 | 我们只要在控制器的构造函数里面写了这个参数,ServiceProvider就会帮我们注入进来。 124 | 125 | ### 3.2 View 126 | 在View中需要用@inject 再声明一下,起一个别名。 127 | ```html 128 | @using MilkStone.Services; 129 | @model MilkStone.Models.AccountViewModel.LoginViewModel 130 | @inject ILoginService loginService 131 | 132 | 133 | 134 | 135 | @loginService.GetUserName() 136 | 137 | 138 | ``` 139 | 140 | ### 3.3 HttpContext获取实例 141 | HttpContext下有一个RequestedService同样可以用来获取实例对象,不过这种方法一般不推荐。同时要注意GetService<>这是个范型方法,默认如果没有添加Microsoft.Extension.DependencyInjection的using,是不用调用这个方法的。 142 | 143 | ```csharp 144 | HttpContext.RequestServices.GetService>(); 145 | ``` 146 | 147 | > 参考文献 148 | * https://www.cnblogs.com/artech/p/dependency-injection-in-asp-net-core.html 149 | * https://www.cnblogs.com/jesse2013/p/di-in-aspnetcore.html -------------------------------------------------------------------------------- /pages/di-consume.md: -------------------------------------------------------------------------------- 1 | # .Net Core 服务消费 2 | 3 | * [1. ServiceProvider](#1-serviceprovider) 4 | * [2. 消费服务](#2-消费服务) 5 | * [3. 服务集合](#3-服务集合) 6 | * [4. 泛型支持](#4-泛型支持) 7 | * [5. 构造函数选择](#5-构造函数选择) 8 | 9 | ## 1. ServiceProvider 10 | 在采用了依赖注入的应用中,我们总是直接利用DI容器直接获取所需的服务实例,换句话说,DI容器起到了一个服务提供者的角色,它能够根据我们提供的服务描述信息提供一个可用的服务对象。 11 | 12 | 作为一个服务的提供者,ASP.NET Core中的DI容器最终体现为一个IServiceProvider接口。此接口只声明了一个GetService方法以根据指定的服务类型来提供对应的服务实例。 13 | 14 | ```csharp 15 | public interface IServiceProvider 16 | { 17 | object GetService(Type serviceType); 18 | } 19 | 20 | public static class ServiceCollectionContainerBuilderExtensions 21 | { 22 | public static ServiceProvider BuildServiceProvider(this IServiceCollection services); 23 | } 24 | ``` 25 | 26 | ASP.NET Core内部真正使用的是一个实现了IServiceProvider接口的内部类型(该类型的名称为“ServiceProvider”),我们不能直接创建该对象,只能间接地通过调用IServiceCollection接口的扩展方法BuildServiceProvider得到它。 27 | 28 | 由于ASP.NET Core中的ServiceProvider是根据一个代表ServiceDescriptor集合的IServiceCollection对象创建的,当我们调用其GetService方法的时候,它会根据我们提供的服务类型找到对应的ServiceDecriptor对象。如果该ServiceDecriptor对象的ImplementationInstance属性返回一个具体的对象,该对象将直接用服务实例。如果ServiceDecriptor对象的ImplementationFactory返回一个具体的委托,该委托对象将直接用作创建服务实例的工厂。如果这两个属性均为Null,ServiceProvider才会根据ImplementationType属性返回的类型调用相应的构造函数创建被提供的服务实例。**ServiceProvider仅仅支持构造器注入,属性注入和方法注入的支持并未提供。** 29 | 30 | 除了定义在IServiceProvider的这个GetService方法,DI框架为该接口定了如下这些扩展方法。GetService<T>方法会以泛型参数的形式指定服务类型,返回的服务实例也会作对应的类型转换。如果指定服务类型的服务注册不存在,GetService方法会返回Null,如果调用GetRequiredService或者GetRequiredService<T>方法则会抛出一个InvalidOperationException类型的异常。如果所需的服务实例是必需的,我们一般会调用这两个扩展方法。 31 | 32 | ```csharp 33 | public static class ServiceProviderServiceExtensions 34 | { 35 | public static T GetService(this IServiceProvider provider); 36 | 37 | public static T GetRequiredService(this IServiceProvider provider); 38 | public static object GetRequiredService(this IServiceProvider provider, Type serviceType); 39 | 40 | public static IEnumerable GetServices(this IServiceProvider provider); 41 | public static IEnumerable GetServices(this IServiceProvider provider, Type serviceType); 42 | } 43 | ``` 44 | 45 | ## 2. 消费服务 46 | 接下来采用实例演示的方式来介绍如何利用ServiceCollection进行服务注册,以及如何利用ServiceCollection创建对应的ServiceProvider来提供我们需要的服务实例。 47 | 48 | 定义四个服务接口(IFoo、IBar、IBaz和IGux)以及分别实现它们的四个服务类(Foo、Bar、Baz和Gux)如下面的代码片段所示,IGux具有三个只读属性(Foo、Bar和Baz)均为接口类型,并在构造函数中进行初始化。 49 | 50 | ```csharp 51 | public interface IFoo {} 52 | public interface IBar {} 53 | public interface IBaz {} 54 | public interface IGux 55 | { 56 | IFoo Foo { get; } 57 | IBar Bar { get; } 58 | IBaz Baz { get; } 59 | } 60 | 61 | public class Foo : IFoo {} 62 | public class Bar : IBar {} 63 | public class Baz : IBaz {} 64 | public class Gux : IGux 65 | { 66 | public IFoo Foo { get; private set; } 67 | public IBar Bar { get; private set; } 68 | public IBaz Baz { get; private set; } 69 | 70 | public Gux(IFoo foo, IBar bar, IBaz baz) 71 | { 72 | this.Foo = foo; 73 | this.Bar = bar; 74 | this.Baz = baz; 75 | } 76 | } 77 | ``` 78 | 79 | 现在我们在作为程序入口的Main方法中创建了一个ServiceCollection对象,并采用不同的方式完成了针对四个服务接口的注册。具体来说,对于服务接口IFoo和IGux的ServiceDescriptor来说,我们指定了代表服务真实类型的ImplementationType属性,而对于针对服务接口IBar和IBaz的ServiceDescriptor来说,我们初始化的则是分别代表服务实例和服务工厂的ImplementationInstance和ImplementationFactory属性。由于我们调用的是AddSingleton方法,所以四个ServiceDescriptor的Lifetime属性均为Singleton。 80 | 81 | ```csharp 82 | class Program 83 | { 84 | static void Main(string[] args) 85 | { 86 | IServiceCollection services = new ServiceCollection() 87 | .AddSingleton() 88 | .AddSingleton(new Bar()) 89 | .AddSingleton(_ => new Baz()) 90 | .AddSingleton(); 91 | 92 | IServiceProvider serviceProvider = services.BuildServiceProvider(); 93 | Console.WriteLine("serviceProvider.GetService(): {0}",serviceProvider.GetService()); 94 | Console.WriteLine("serviceProvider.GetService(): {0}", serviceProvider.GetService()); 95 | Console.WriteLine("serviceProvider.GetService(): {0}", serviceProvider.GetService()); 96 | Console.WriteLine("serviceProvider.GetService(): {0}", serviceProvider.GetService()); 97 | } 98 | } 99 | ``` 100 | 101 | 接下来我们调用ServiceCollection对象的扩展方法BuildServiceProvider得到对应的ServiceProvider对象,然后调用其扩展方法GetService<T>分别获得针对四个接口的服务实例对象并将类型名称输出到控制台上。运行该程序之后,我们会在控制台上得到如下的输出结果,由此印证ServiceProvider为我们提供了我们期望的服务实例。 102 | 103 | ```csharp 104 | serviceProvider.GetService(): Foo 105 | serviceProvider.GetService(): Bar 106 | serviceProvider.GetService(): Baz 107 | serviceProvider.GetService(): Gux 108 | ``` 109 | 110 | ## 3. 服务集合 111 | 如果我们在调用GetService方法的时候将服务类型指定为IEnumerable<T>,那么返回的结果将会是一个集合对象。除此之外,我们可以直接调用IServiceProvider如下两个扩展方法GetServeces达到相同的目的。在这种情况下,ServiceProvider将会利用所有与指定服务类型相匹配的ServiceDescriptor来提供具体的服务实例,这些均会作为返回的集合对象的元素。如果所有的ServiceDescriptor均与指定的服务类型不匹配,那么最终返回的是一个空的集合对象。 112 | 113 | ```csharp 114 | public static class ServiceProviderExtensions 115 | { 116 | public static IEnumerable GetServices(this IServiceProvider provider); 117 | public static IEnumerable GetServices(this IServiceProvider provider, Type serviceType); 118 | } 119 | ``` 120 | 121 | 值得一提的是,如果ServiceProvider所在的ServiceCollection包含多个具有相同服务类型(对应ServiceType属性)的ServiceDescriptor,当我们**调用GetService方法获取单个服务实例的时候,只有最后一个ServiceDescriptor才是有效的,至于其他的ServiceDescriptor,它们只有在获取服务集合的场景下才有意义。** 122 | 123 | 我们通过一个简单的实例来演示如何利用ServiceProvider得到一个包含多个服务实例的集合。我们在一个控制台应用中定义了如下一个服务接口IFoobar,两个服务类型Foo和Bar均实现了这个接口。在作为程序入口的Main方法中,我们将针针对服务类型Foo和Bar的两个ServiceDescriptor添加到创建的ServiceCollection对象中,这两个ServiceDescriptor对象的ServiceType属性均为IFoobar。 124 | 125 | ```csharp 126 | class Program 127 | { 128 | static void Main(string[] args) 129 | { 130 | IServiceCollection serviceCollection = new ServiceCollection() 131 | .AddSingleton() 132 | .AddSingleton(); 133 | 134 | IServiceProvider serviceProvider = serviceCollection.BuildServiceProvider(); 135 | Console.WriteLine("serviceProvider.GetService(): {0}", serviceProvider.GetService()); 136 | 137 | IEnumerable services = serviceProvider.GetServices(); 138 | int index = 1; 139 | Console.WriteLine("serviceProvider.GetServices():"); 140 | foreach (IFoobar foobar in services) 141 | { 142 | Console.WriteLine("{0}: {1}", index++, foobar); 143 | } 144 | } 145 | } 146 | 147 | public interface IFoobar {} 148 | public class Foo : IFoobar {} 149 | public class Bar : IFoobar {} 150 | ``` 151 | 152 | 在调用ServiceCollection对象的扩展方法BuildServiceProvider得到对应的ServiceProvider对象之后,我们先调用其GetService<T>方法以确定针对服务接口IFoobar得到的服务实例的真实类型就是是Foo还是Bar。接下来我们调用ServiceProvider的扩展方法GetServices<T>获取一组针对服务接口IFoobar的服务实例并将它们的真是类型打印在控制台上。该程序运行后将会在控制台上生成如下的输出结果。 153 | 154 | ``` 155 | serviceProvider.GetService(): Bar 156 | serviceProvider.GetServices(): 157 | Foo 158 | Bar 159 | ``` 160 | 161 | ## 4. 泛型支持 162 | ServiceProvider提供的服务实例不仅限于普通的类型,它对泛型服务类型同样支持。在针对泛型服务进行注册的时候,我们可以将服务类型设定为携带具体泛型参数的“关闭泛型类型”(比如IFoobar<IFoo,IBar>),除此之外服务类型也可以是包含具体泛型参数的“开放泛型类型”(比如IFoo<,>)。前者实际上还是将其视为非泛型服务来对待,后者才真正体现了“泛型”的本质。 163 | 164 | 比如我们注册了某个泛型服务接口IFoobar<,>与它的实现类Foobar<,>之间的映射关系,当我们指定一个携带具体泛型参数的服务接口类型IFoobar<IFoo,IBar>并调用ServiceProvider的GetService方法获取对应的服务实例时,ServiceProvider会针对指定的泛型参数类型(IFoo和IBar)来解析与之匹配的实现类型(可能是Foo和Bar)并得到最终的实现类型(Foobar<Foo,Bar>)。 165 | 166 | 我们同样利用一个简单的控制台应用来演示基于泛型的服务注册与提供方式。如下面的代码片段所示,我们定义了三个服务接口(IFoo、IBar和IFoobar<T1,T2>)和实现它们的三个服务类(Foo、Bar个Foobar<T1,T2>),泛型接口具有两个泛型参数类型的属性(Foo和Bar),它们在实现类中以构造器注入的方式被初始化。 167 | 168 | ```csharp 169 | class Program 170 | { 171 | static void Main(string[] args) 172 | { 173 | IServiceProvider serviceProvider = new ServiceCollection() 174 | .AddTransient() 175 | .AddTransient() 176 | .AddTransient(typeof(IFoobar<,>), typeof(Foobar<,>)) 177 | .BuildServiceProvider(); 178 | 179 | Console.WriteLine("serviceProvider.GetService>().Foo: {0}", serviceProvider.GetService>().Foo); 180 | Console.WriteLine("serviceProvider.GetService>().Bar: {0}", serviceProvider.GetService>().Bar); 181 | } 182 | } 183 | 184 | public interface IFoobar 185 | { 186 | T1 Foo { get; } 187 | T2 Bar { get; } 188 | } 189 | public interface IFoo {} 190 | public interface IBar {} 191 | 192 | public class Foobar : IFoobar 193 | { 194 | public T1 Foo { get; private set; } 195 | public T2 Bar { get; private set; } 196 | public Foobar(T1 foo, T2 bar) 197 | { 198 | this.Foo = foo; 199 | this.Bar = bar; 200 | } 201 | } 202 | public class Foo : IFoo {} 203 | public class Bar : IBar {} 204 | ``` 205 | 206 | 在作为入口程序的Main方法中,我们创建了一个ServiceCollection对象并采用Transient模式注册了上述三个服务接口与对应实现类型之间的映射关系,对于泛型服务IFoobar<T1,T2>/Foobar<T1,T2>来说,我们指定的是不携带具体泛型参数的开放泛型类型IFoobar<,>/Foobar<,>。利用此ServiceCollection创建出对应的ServiceProvider之后,我们调用后者的GetService方法并指定IFoobar<IFoo,IBar>为服务类型。得到的服务对象将会是一个Foobar<Foo,Bar>对象,我们将它的Foo和Bar属性类型输出于控制台上作为验证。该程序执行之后将会在控制台上产生下所示的输出结果。 207 | 208 | ``` 209 | serviceProvider.GetService>().Foo: Foo 210 | serviceProvider.GetService>().Bar: Bar 211 | ``` 212 | 213 | ## 5. 构造函数选择 214 | 当ServiceProvider利用ImplementationType属性返回的真实类型的构造函数来创建最终的服务实例时,如果服务的真实类型定义了多个构造函数,那么ServiceProvider针对构造函数的选择会采用怎样的策略呢? 215 | 216 | 如果ServiceProvider试图通过调用构造函数的方式来创建服务实例,传入构造函数的所有参数必须先被初始化,最终被选择出来的构造函数必须具备一个基本的条件:**ServiceProvider能够提供构造函数的所有参数。** 217 | 218 | 我们在一个控制台应用中定义了四个服务接口(IFoo、IBar、IBaz和IGux)以及实现它们的四个服务类(Foo、Bar、Baz和Gux)。如下面的代码片段所示,我们为Gux定义了三个构造函数,参数均为我们定义了服务接口类型。为了确定ServiceProvider最终选择哪个构造函数来创建目标服务实例,我们在构造函数执行时在控制台上输出相应的指示性文字。 219 | 220 | ```csharp 221 | public interface IFoo {} 222 | public interface IBar {} 223 | public interface IBaz {} 224 | public interface IGux {} 225 | 226 | public class Foo : IFoo {} 227 | public class Bar : IBar {} 228 | public class Baz : IBaz {} 229 | public class Gux : IGux 230 | { 231 | public Gux(IFoo foo) 232 | { 233 | Console.WriteLine("Gux(IFoo)"); 234 | } 235 | 236 | public Gux(IFoo foo, IBar bar) 237 | { 238 | Console.WriteLine("Gux(IFoo, IBar)"); 239 | } 240 | 241 | public Gux(IFoo foo, IBar bar, IBaz baz) 242 | { 243 | Console.WriteLine("Gux(IFoo, IBar, IBaz)"); 244 | } 245 | } 246 | ``` 247 | 248 | 我们在作为程序入口的Main方法中创建一个ServiceCollection对象并在其中添加针对IFoo、IBar以及IGux这三个服务接口的服务注册,针对服务接口IBaz的注册并未被添加。我们利用由它创建的ServiceProvider来提供针对服务接口IGux的实例,究竟能否得到一个Gux对象呢?如果可以,它又是通过执行哪个构造函数创建的呢? 249 | 250 | ```csharp 251 | class Program 252 | { 253 | static void Main(string[] args) 254 | { 255 | new ServiceCollection() 256 | .AddTransient() 257 | .AddTransient() 258 | .AddTransient() 259 | .BuildServiceProvider() 260 | .GetServices(); 261 | } 262 | } 263 | ``` 264 | 265 | 对于定义在Gux中的三个构造函数来说,ServiceProvider所在的ServiceCollection包含针对接口IFoo和IBar的服务注册,所以它能够提供前面两个构造函数的所有参数。由于第三个构造函数具有一个类型为IBaz的参数,这无法通过ServiceProvider来提供。根据我们上面介绍的第一个原则(ServiceProvider能够提供构造函数的所有参数),Gux的前两个构造函数会成为合法的候选构造函数,那么ServiceProvider最终会选择哪一个呢? 266 | 267 | 在所有合法的候选构造函数列表中,最终被选择出来的构造函数具有这么一个特征:**每一个候选构造函数的参数类型集合都是这个构造函数参数类型集合的子集。**如果这样的构造函数并不存在,一个类型为InvalidOperationException的异常会被抛出来。根据这个原则,Gux的第二个构造函数的参数类型包括IFoo和IBar,而第一个构造函数仅仅具有一个类型为IFoo的参数,最终被选择出来的会是Gux的第二个构造函数,所有运行我们的实例程序将会在控制台上产生如下的输出结果。 268 | 269 | ``` 270 | Gux(IFoo, IBar) 271 | ``` 272 | 273 | 接下来我们对实例程序略加改动。如下面的代码片段所示,我们只为Gux定义两个构造函数,它们都具有两个参数,参数类型分别为IFoo&IBar和IBar&IBaz。在Main方法中,我们将针对IBaz/Baz的服务注册添加到创建的ServiceCollection上。 274 | 275 | ```csharp 276 | class Program 277 | { 278 | static void Main(string[] args) 279 | { 280 | new ServiceCollection() 281 | .AddTransient() 282 | .AddTransient() 283 | .AddTransient() 284 | .AddTransient() 285 | .BuildServiceProvider() 286 | .GetServices(); 287 | } 288 | } 289 | 290 | public class Gux : IGux 291 | { 292 | public Gux(IFoo foo, IBar bar) {} 293 | public Gux(IBar bar, IBaz baz) {} 294 | } 295 | ``` 296 | 297 | 对于Gux的两个构造函数,虽然它们的参数均能够由ServiceProvider来提供,但是并没有一个构造函数的参数类型集合能够成为所有有效构造函数参数类型集合的超集,所以ServiceProvider无法选择出一个最佳的构造函数。如果我们运行这个程序,一个InvalidOperationException异常会被抛出来,控制台上将呈现出如下所示的错误消息。 298 | 299 | ``` 300 | Unhandled Exception: System.InvalidOperationException: Unable to activate type 'Gux'. The following constructors are ambigious: 301 | Void .ctor(IFoo, IBar) 302 | Void .ctor(IBar, IBaz) 303 | ... 304 | ``` 305 | 306 | > 参考文献 307 | * http://www.cnblogs.com/artech/p/asp-net-core-di-register.html 308 | * http://www.cnblogs.com/artech/p/asp-net-core-di-life-time.html 309 | * https://www.cnblogs.com/artech/p/net-core-di-08.html -------------------------------------------------------------------------------- /pages/di-designmode.md: -------------------------------------------------------------------------------- 1 | # 基于IoC的设计模式 2 | * [1. 模板方法(Template Method)](#1-模板方法(template-method)) 3 | * [2. 工厂方法(Factory Method)](#2-工厂方法(factory-method)) 4 | * [3. 抽象工厂(Abstract Factory)](#3-抽象工厂(abstract-factory)) 5 | 6 | 正如我们在[前面](ioc.md)提到过的,很多人将IoC理解为一种“面向对象的设计模式”,实际上IoC自身不仅与面向对象没有必然的联系,它也算不上是一种设计模式。一般来讲,设计模式提供了一种解决某种具体问题的方案,但是IoC既没有一个针对性的问题领域,其自身没有提供一种可实施的解决方案,所以我更加倾向于将IoC视为一种设计原则,实际上很多我们熟悉的设计模式背后采用了IoC原则。 7 | 8 | ## 1. 模板方法(Template Method) 9 | 提到IoC,很多人首先想到的是DI,但是在我看来与IoC思想最为接近的倒是另一种被称为“模板方法(Template Method)”的设计模式。模板方法模式与IoC的意图可以说不谋而合,该模式主张将一个可复用的工作流程或者由多个步骤组成的算法定义成模板方法,组成这个流程或者算法的步骤实现在相应的虚方法之中,模板方法根据预先编排的流程去调用这些虚方法。所有这些方法均定义在同一个类中,我们可以通过派生该类并重写相应的虚方法达到对流程定制的目的。 10 | 11 | 对于[控制反转(IoC)](ioc.md)演示的这个MVC的例子,我们可以将整个请求处理流程实现在如下一个MvcEngine类中,请求的监听与接收、目标Controller的激活与执行以及View的呈现分别定义在5个受保护的虚方法中,模板方法StartAsync根据预定义的请求处理流程先后调用这5个方法。 12 | 13 | ```csharp 14 | public class MvcEngine 15 | { 16 | public async Task StartAsync(Uri address) 17 | { 18 | await ListenAsync(address); 19 | while (true) 20 | { 21 | var request = await ReceiveAsync(); 22 | var controller = await CreateControllerAsync(request); 23 | var view = await ExecuteControllerAsync(controller); 24 | await RenderViewAsync(view); 25 | } 26 | } 27 | protected virtual Task ListenAsync(Uri address); 28 | protected virtual Task ReceiveAsync(); 29 | protected virtual Task CreateControllerAsync(Request request); 30 | protected virtual Task ExecuteControllerAsync(Controller controller); 31 | protected virtual Task RenderViewAsync(View view); 32 | } 33 | ``` 34 | 35 | 对于具体的应用来说,如果MvcEngine中针对请求的处理方式完全符合要求,则它只需要创建一个MvcEngine对象,然后指定一个对应的基地址调用模板方法StartAsync开启这个MVC引擎即可。如果该MVC引擎处理请求的某个环节不能满足它的要求,它可以创建MvcEngine的派生类,并重写实现该环节的相应虚方法即可。 36 | 37 | 比如说定义在某个应用程序中的Controller都是无状态的,它希望采用单例(Singleton)的方式重用已经激活的Controller以提高性能,那么它就可以按照如下的方式创建一个自定义的FoobarMvcEngine并按照自己的方式重写 38 | 39 | ```csharp 40 | public class FoobarMvcEngine : MvcEngine 41 | { 42 | protected override Task CreateControllerAsync (Request request) 43 | { 44 | // <<省略实现>> 45 | } 46 | } 47 | ``` 48 | 49 | ## 2. 工厂方法(Factory Method) 50 | 对于一个复杂的流程来说,我们倾向于将组成该流程的各个环节实现在相对独立的组件之中,那么针对流程的定制就可以通过提供定制组件的方式来实现。我们知道23种设计模式之中有一种重要的类型,那就是“创建型模式”,比如常用的“工厂方法”和“抽象工厂”,IoC所体现的针对流程的共享与定制可以通过它们来完成。 51 | 52 | 所谓的工厂方法,说白了就是在某个类中定义用于提供依赖对象的方法,这个方法可以是一个单纯的虚方法,也可以是具有默认实现的虚方法,至于方法声明的返回类型,可以是一个接口或者抽象类,也可以是未被封闭(Sealed)的具体类型。作为它的派生类型,它可以实现或者重写工厂方法以提供所需的具体对象。 53 | 54 | 同样以我们的MVC框架为例,我们让独立的组件来完成组成整个请求处理流程的几个核心环节。具体来说,我们针对这些核心组件定义了如下这几个对应的接口。IWebLister接口用来监听、接收和响应请求(针对请求的响应由ReceiveAsync方法返回的HttpContext对象来完成,后者表示针对当前请求的上下文),IControllerActivator接口用于根据当前请求激活目标Controller对象,已经在后者执行完成后做一些释放回收工作。至于IControllerExecutor和IViewRender接口则分别用来完成针对Controller的执行和针对View的呈现。 55 | 56 | ```csharp 57 | public interface IWebLister 58 | { 59 | Task ListenAsync(Uri address); 60 | Task ReceiveAsync(); 61 | } 62 | 63 | public interface IControllerActivator 64 | { 65 | Task CreateControllerAsync(HttpContext httpContext); 66 | Task ReleaseAsync(Controller controller); 67 | } 68 | 69 | public interface IControllerExecutor 70 | { 71 | Task ExecuteAsync(Controller controller, HttpContext httpContext); 72 | } 73 | 74 | public interface IViewRender 75 | { 76 | Task RendAsync(View view, HttpContext httpContext); 77 | } 78 | ``` 79 | 80 | 我们在作为MVC引擎的MvcEngine类中定义了四个工厂方法(GetWebListener、GetControllerActivator、GetControllerExecutor和GetViewRenderer)来提供上述这4种组件。这四个工厂方法均为具有默认实现的虚方法,我们可以利用它们提供默认的组件。在用于启动引擎的StartAsync方法中,我们利用这些工厂方法提供的对象来具体完成请求处理流程的各个核心环节。 81 | 82 | ```csharp 83 | public class MvcEngine 84 | { 85 | public async Task StartAsync(Uri address) 86 | { 87 | var listener = GetWebLister(); 88 | var activator = GetControllerActivator(); 89 | var executor = GetControllerExecutor(); 90 | var render = GetViewRender(); 91 | await listener.ListenAsync(address); 92 | while (true) 93 | { 94 | var httpContext = await listener.ReceiveAsync(); 95 | var controller = await activator.CreateControllerAsync(httpContext); 96 | try 97 | { 98 | var view = await executor.ExecuteAsync(controller, httpContext); 99 | await render.RendAsync(view, httpContext); 100 | } 101 | finally 102 | 103 | { 104 | await activator.ReleaseAsync(controller); 105 | } 106 | } 107 | } 108 | protected virtual IWebLister GetWebLister(); 109 | protected virtual IControllerActivator GetControllerActivator(); 110 | protected virtual IControllerExecutor GetControllerExecutor(); 111 | protected virtual IViewRender GetViewRender(); 112 | } 113 | ``` 114 | 115 | 对于具体的应用程序来说,如需对请求处理某个环节进行定制,在对应接口实现类中重写对应的工厂方法即可。比如上面提及的以单例模式提供目标Controller对象的实现就定义在SingletonControllerActivator类中,我们在派生于MvcEngine的FoobarMvcEngine类中重写了工厂方法GetControllerActivator使其返回一个SingletonControllerActivator对象。 116 | 117 | ```csharp 118 | public class SingletonControllerActivator : IControllerActivator 119 | { 120 | public Task CreateControllerAsync(HttpContext httpContext) 121 | { 122 | // <<省略实现>> 123 | } 124 | public Task ReleaseAsync(Controller controller) => Task.CompletedTask; 125 | } 126 | 127 | public class FoobarMvcEngine : MvcEngine 128 | { 129 | protected override ControllerActivator GetControllerActivator() => new SingletonControllerActivator(); 130 | } 131 | ``` 132 | 133 | ## 3. 抽象工厂(Abstract Factory) 134 | 虽然工厂方法和抽象工厂均提供了一个“生产”对象实例的工厂,但是两者在设计上却有本质的不同。工厂方法利用定义在某个类型的抽象方法或者虚方法实现了针对单一对象提供方式的抽象,而抽象工厂则利用一个独立的接口或者抽象类来提供一组相关的对象。 135 | 136 | 具体来说,我们需要定义一个独立的工厂接口或者抽象工厂类,并在其中定义多个的工厂方法来提供“同一系列”的多个相关对象。如果希望抽象工厂具有一组默认的“产出”,我们也可以将一个未被封闭的具体类作为抽象工厂,以虚方法形式定义的工厂方法将默认的对象作为返回值。我们根据实际的需要通过实现工厂接口或者继承抽象工厂类(不一定是抽象类)定义具体工厂类来提供一组定制的系列对象。 137 | 138 | 现在我们采用抽象工厂模式来改造我们的MVC框架。如下面的代码片段所示,我们定义了一个名为IMvcEngineFactory的接口作为抽象工厂,其中定义了四个方法来提供请求监听和处理过程使用到的4种核心对象。如果MVC提供了针对这四种核心组件的默认实现,我们可以按照如下的方式为这个抽象工厂提供一个默认实现(MvcEngineFactory)。 139 | 140 | ```csharp 141 | public interface IMvcEngineFactory 142 | { 143 | IWebLister GetWebLister(); 144 | IControllerActivator GetControllerActivator(); 145 | IControllerExecutor GetControllerExecutor(); 146 | IViewRender GetViewRender(); 147 | } 148 | 149 | public class MvcEngineFactory: IMvcEngineFactory 150 | { 151 | IWebLister GetWebLister(); 152 | IControllerActivator GetControllerActivator(); 153 | IControllerExecutor GetControllerExecutor(); 154 | IViewRender GetViewRender(); 155 | } 156 | ``` 157 | 158 | 现在我们采用抽象工厂模式来改造我们的MVC框架。在创建MvcEngine对象时可以提供一个具体的IMvcEngineFactory对象,如果没有显式指定,MvcEngine会使用默认的EngineFactory对象。在用于启动引擎的StartAsync方法中,MvcEngine利用IMvcEngineFactory来获取相应的对象协作完成对请求的处理流程。 159 | 160 | ```csharp 161 | public class MvcEngine 162 | { 163 | public IMvcEngineFactory EngineFactory { get; } 164 | public MvcEngine(IMvcEngineFactory engineFactory = null) 165 | => EngineFactory = engineFactory??new MvcEngineFactory(); 166 | 167 | public async Task StartAsync(Uri address) 168 | { 169 | var listener = EngineFactory.GetWebLister(); 170 | var activator = EngineFactory.GetControllerActivator(); 171 | var executor = EngineFactory.GetControllerExecutor(); 172 | var render = EngineFactory.GetViewRender(); 173 | await listener.ListenAsync(address); 174 | while (true) 175 | { 176 | var httpContext = await listener.ReceiveAsync(); 177 | var controller = await activator.CreateControllerAsync(httpContext); 178 | try 179 | { 180 | var view = await executor.ExecuteAsync(controller, httpContext); 181 | await render.RendAsync(view, httpContext); 182 | } 183 | finally 184 | { 185 | await activator.ReleaseAsync(controller); 186 | } 187 | } 188 | } 189 | } 190 | ``` 191 | 192 | 如果具体的应用程序需要采用上面定义的SingletonControllerActivator以单例的模式来激活目标Controller,我们可以按照如下的方式定义一个具体的工厂类FoobarEngineFactory。最终的应用程序将这么一个FoobarEngineFactory对象作为MvcEngine的EngineFactory。 193 | 194 | ```csharp 195 | public class FoobarEngineFactory : EngineFactory 196 | { 197 | public override ControllerActivator GetControllerActivator() 198 | { 199 | return new SingletonControllerActivator(); 200 | } 201 | } 202 | 203 | public class App 204 | { 205 | static void Main(string[] args) 206 | { 207 | Uri address = new Uri("http://0.0.0.0:8080/mvcapp"); 208 | MvcEngine engine = new MvcEngine(new FoobarEngineFactory()); 209 | engine.Start(address); 210 | } 211 | } 212 | ``` 213 | 214 | 除了上面介绍这三种典型的设计,还有很多其他的设计模式,比如策略模式、观察者模式等等,它们无一不是采用IoC的设计原则。 215 | 216 | > 参考文献 217 | https://www.cnblogs.com/artech/p/net-core-di-02.html -------------------------------------------------------------------------------- /pages/di-di.md: -------------------------------------------------------------------------------- 1 | # 依赖注入(DI) 2 | 3 | * [1. 容器提供服务](#1-容器提供服务) 4 | * [2. 依赖注入方式](#2-依赖注入方式) 5 | * [2.1 构造器注入](#21-构造器注入) 6 | * [2.2 属性注入](#22-属性注入) 7 | * [2.3 方法注入](#23-方法注入) 8 | * [3. Service Locator](#3-service-locator) 9 | * [4. .Net Core中的DI](#4-net-core中的di) 10 | 11 | ## 1. 容器提供服务 12 | 和[基于IoC的设计模式](designmode.md)中介绍的工厂方法和抽象工厂模式一样,DI是一种“对象提供型”的设计模式,在这里我们将提供的对象统称为“服务”、“服务对象”或者“服务实例”。在一个采用DI的应用中,在定义某个服务类型的时候,我们直接将依赖的服务采用相应的方式注入进来。按照“面向接口编程”的原则,被注入的最好是依赖服务的接口而非实现。 13 | 14 | 在应用启动的时候,我们会对所需的服务进行全局注册。服务一般都是针对接口进行注册的,服务注册信息的核心目的是为了在后续消费过程中能够根据接口创建或者提供对应的服务实例。按照“好莱坞法则”,应用只需要定义好所需的服务,服务实例的激活和调用则完全交给框架来完成,而框架则会采用一个独立的“容器(Container)”来提供每一个服务实例。 15 | 16 | 框架用来提供服务的容器称为“DI容器”,也有很多人将其称为“IoC容器”,根据我们在《控制反转》针对IoC的介绍,我不认为后者是一个合理的称谓。DI容器之所以能够按照我们希望的方式来提供所需的服务是因为该容器是根据服务注册信息来创建的,服务注册了包含提供所需服务实例的所有信息。 17 | 18 | 举个简单的例子,我们创建一个名为Cat的DI容器类,那么我们可以通过调用具有如下定义的扩展方法GetService从某个Cat对象获取指定类型的服务对象。我之所以将其命名为Cat,源于我们大家都非常熟悉的一个卡通形象“机器猫(哆啦A梦)”。机器猫的那个四次元口袋就是一个理想的DI容器,大熊只需要告诉哆啦A梦相应的需求,它就能从这个口袋中得到相应的法宝。DI容器亦是如此,服务消费者只需要告诉容器所需服务的类型(一般是一个服务接口或者抽象服务类),就能得到与之匹配的服务实例 19 | 20 | ```csharp 21 | public static class CatExtensions 22 | { 23 | public static T GetService(this Cat cat); 24 | } 25 | ``` 26 | 27 | 对于演示的MVC框架,我们在[基于IoC的设计模式](designmode.md)中分别采用不同的设计模式对框架的核心类型MvcEngine进行了改造,现在我们采用DI的方式并利用上述的这个Cat容器按照如下的方式对其进行重新实现,我们会发现MvcEngine变得异常简洁而清晰。 28 | 29 | ```csharp 30 | public class MvcEngine 31 | { 32 | public Cat Cat { get; } 33 | public MvcEngine(Cat cat) => Cat = cat; 34 | 35 | public async Task StartAsync(Uri address) 36 | { 37 | var listener = Cat.GetService(); 38 | var activator = Cat.GetService(); 39 | var executor = Cat.GetService(); 40 | var render = Cat.GetService(); 41 | await listener.ListenAsync(address); 42 | while (true) 43 | { 44 | var httpContext = await listener.ReceiveAsync(); 45 | var controller = await activator.CreateControllerAsync(httpContext); 46 | try 47 | { 48 | var view = await executor.ExecuteAsync(controller, httpContext); 49 | await render.RendAsync(view, httpContext); 50 | } 51 | finally 52 | { 53 | await activator.ReleaseAsync(controller); 54 | } 55 | } 56 | } 57 | } 58 | ``` 59 | 60 | 从服务消费的角度来讲,我们借助于一个服务接口对消费的服务进行抽象,那么服务消费程序针对具体服务类型的依赖可以转移到对服务接口的依赖上,但是在运行时提供给消费者的总是一个针对某个具体服务类型的对象。不仅如此,要完成定义在服务接口的操作,这个对象可能需要其他相关对象的参与,也就是说提供的这个服务对象可能具有针对其他对象的依赖。作为服务对象提供者的DI容器,在它向消费者提供服务对象之前就会根据服务实现类型和服务注册信息自动创建依赖的服务实例,并将后者注入到当前对象之中。 61 | 62 | ## 2. 依赖注入方式 63 | 从服务使用的角度来讲,我们借助于一个接口对服务进行抽象,那么依赖将转移到服务接口上。在运行时提供给消费者的则是具体服务类型的对象。 64 | 65 | 在具体服务类型中要实现服务接口定义的成员时,可能需要第三方对象辅助,这就产生了对第三方对象的依赖。DI容器在提供服务对象之前会自动注入这些第三方依赖到当前对象中。 66 | 67 | 服务消费程序调用GetService()方法向DI容器索取一个实现了IFoo接口的某个类型的对象,DI容器会根据预先注册的类型匹配关系创建一个类型为Foo的对象。此外,Foo对象依赖Bar和Baz对象的参与才能实现定义在服务接口IFoo之中的操作,所以Foo具有了针对Bar和Baz的直接依赖。至于Baz,它又依赖Qux,那么后者成为了Foo的间接依赖。对于DI容器最终提供的Foo对象,它所直接或者间接依赖的对象Bar、Baz和Qux都会预先被初始化并自动注入到该对象之中。 68 | 69 | ![哆啦A梦案例](https://i.loli.net/2020/02/26/OrkDB5xaN4vMKRt.jpg) 70 | 71 | 从编程的角度来讲,类型中的属性是依赖的一种主要体现形式,如果类型A中具有一个B类型的属性,那么A就对B产生了依赖。所谓依赖注入,我们可以简单地理解为一种针对依赖属性的自动初始化方式。具体来说,我们可以通过三种主要的方式达到这个目的,这就是接下来着重介绍的三种依赖注入方式。 72 | 73 | ### 2.1 构造器注入 74 | 构造器注入就在在构造函数中借助参数将依赖的对象注入到创建的对象之中。如下面的代码片段所示,Foo针对Bar的依赖体现在只读属性Bar上,针对该属性的初始化实现在构造函数中,具体的属性值由构造函数的传入的参数提供。当DI容器通过调用构造函数创建一个Foo对象之前,需要根据当前注册的类型匹配关系以及其他相关的注入信息创建并初始化参数对象。 75 | 76 | ```csharp 77 | public class Foo 78 | { 79 | public IBar Bar{get;} 80 | public Foo(IBar bar) =>Bar = bar; 81 | } 82 | ``` 83 | 84 | 除此之外,构造器注入还体现在对构造函数的选择上面。如下面的代码片段所示,Foo类上面定义了两个构造函数,DI容器在创建Foo对象之前首先需要选择一个适合的构造函数。至于目标构造函数如何选择,不同的DI容器可能有不同的策略,比如可以选择参数最多或者最少的,或者可以按照如下所示的方式在目标构造函数上标注一个InjectionAttribute特性。 85 | 86 | ```csharp 87 | public class Foo 88 | { 89 | public IBar Bar{get;} 90 | public IBaz Baz {get;} 91 | 92 | [Injection] 93 | public Foo(IBar bar) =>Bar = bar; 94 | public Foo(IBar bar, IBaz):this(bar) =>Baz = baz; 95 | } 96 | ``` 97 | 98 | ### 2.2 属性注入 99 | 如果依赖直接体现为类的某个属性,并且该属性不是只读的,我们可以让DI容器在对象创建之后自动对其进行赋值进而达到依赖自动注入的目的。一般来说,我们在定义这种类型的时候,需要显式将这样的属性标识为需要自动注入的依赖属性以区别于该类型的其他普通的属性。如下面的代码片段所示,Foo类中定义了两个可读写的公共属性Bar和Baz,我们通过标注InjectionAttribute特性的方式将属性Baz设置为自动注入的依赖属性。对于由DI容器提供的Foo对象,它的Baz属性将会自动被初始化。 100 | 101 | ```csharp 102 | public class Foo 103 | { 104 | public IBar Bar{get; set;} 105 | 106 | [Injection] 107 | public IBaz Baz {get; set;} 108 | } 109 | ``` 110 | 111 | ### 2.3 方法注入 112 | 体现依赖关系的属性可以通过方法的形式初始化。如下面的代码片段所示,Foo针对Bar的依赖体现在只读属性上,针对该属性的初始化实现在Initialize方法中,具体的属性值由构造函数的传入参数提供。我们同样通过标注特性(InjectionAttribute)的方式将该方法标识为注入方法。DI容器在调用构造函数创建一个Foo对象之后,它会自动调用这个Initialize方法对只读属性Bar进行赋值。在调用该方法之前,DI容器会根据预先注册的类型映射和其他相关的注入信息初始化该方法的参数。 113 | 114 | ```csharp 115 | public class Foo 116 | { 117 | public IBar Bar{get;} 118 | 119 | [Injection] 120 | public void Initialize(IBar bar)=> Bar = bar; 121 | } 122 | ``` 123 | 124 | 除了上述这种通过DI容器在初始化服务过程中自动调用的实现在外,我们还可以利用它实现另一个更加自由的方法注入形式,后者在ASP.NET Core应用具有广泛的应用。ASP.NET Core在启动的时候会调用我们注册的Startup对象来完成中间件的注册,当我们在定义这个Startup类型的时候不需要让它实现某个接口,所以用于注册中间件的Configure方法其实没有一个固定的声明,我们可以按照如下的方式将任意依赖的服务直接注入到这个方法中。 125 | 126 | ```csharp 127 | public class Startup 128 | { 129 | public void Configure(IApplicationBuilder app, IFoo foo, IBar bar, IBaz baz); 130 | } 131 | ``` 132 | 133 | 类似的注入方式同样可以应用到中间件的定义中。与用于注册中间件的Startup类型一样,ASP.NET Core框架下的中间件类型同样不需要实现某个预定义的接口,用于处理请求的InvokeAsync或者Invoke方法上可以按照如下的方式注入任意的依赖服务。 134 | 135 | ```csharp 136 | public class FoobarMiddleware 137 | { 138 | private readonly RequestDelegate _next; 139 | public FoobarMiddleware(RequestDelegate next) =>_next = next; 140 | public Task InvokeAsync(HttpContext httpContext, IFoo foo, IBar bar, IBaz baz); 141 | } 142 | ``` 143 | 144 | 上面这种方式的方法注入促成了一种“面向约定”的编程方式,由于不再需要实现某个预定义的接口或者继承某一个预定义的类型,需要实现的方法的声明也就少了对应的限制,这样就可用采用最直接的方式将依赖的服务注入到所需的方法中。 145 | 146 | 对于上面介绍的这几种注入方式,构造器注入是最为理想的形式,我个人不建议使用属性注入和方法注入(上面介绍这种基于约定的方法注入除外)。我们定义的服务类型应该是独立自治的,我们不应该对它运行的环境做过多的假设和限制,也就是说同一个服务类型可以使用在框架A中,也可以实现在框架B上;在没有使用任何DI容器的应用中可以使用这个服务类型,当任何一种DI容器被使用到应用中之后,该服务类型依旧能够被正常使用。对于上面介绍的这三种注入方式,只有构造器注入能够达到这个目的,而属性注入和方法注入都依赖于某个具体的DI框架来实现针对依赖属性的自动赋值和依赖方法的自动调用。 147 | 148 | ## 3. Service Locator 149 | 假设我们需要定义一个服务类型Foo,它依赖于另外两个服务Bar和Baz,后者对应的服务接口分别为IBar和IBaz。如果当前应用中具有一个DI容器(假设类似于我们在上面定义的Cat),那么我们可以采用如下两种方式来定义这个服务类型Foo。 150 | 151 | ```csharp 152 | public class Foo : IFoo 153 | { 154 | public IBar Bar { get; } 155 | public IBaz Baz { get; } 156 | public Foo(IBar bar, IBaz baz) 157 | { 158 | Bar = bar; 159 | Baz = baz; 160 | } 161 | public async Task InvokeAsync() 162 | { 163 | await Bar.InvokeAsync(); 164 | await Baz.InvokeAsync(); 165 | } 166 | } 167 | 168 | public class Foo : IFoo 169 | { 170 | public Cat Cat { get; } 171 | public Foo(Cat cat) => Cat = cat; 172 | public async Task InvokeAsync() 173 | { 174 | await Cat.GetService().InvokeAsync(); 175 | await Cat.GetService().InvokeAsync(); 176 | } 177 | } 178 | ``` 179 | 180 | 从表面上看,上面提供的这两种服务类型的定义方式貌似都不错,至少它们都解决针对依赖服务的耦合问题,将针对服务实现的依赖转变成针对接口的依赖。那么哪一种更好呢?我想有人会选择第二种定义方式,因为这种定义方式不仅仅代码量更少,针对服务的提供也更加直接。我们直接在构造函数中“注入”了代表“DI容器”的Cat对象,在任何使用到依赖服务的地方,我们只需要利用它来提供对应的服务实例就可以了。 181 | 182 | 但事实上第二种定义方式采用的设计模式根本就不是“依赖注入”,而是一种被称为“Service Locator”的设计模式。Service Locator模式同样具有一个通过服务注册创建的全局的容器来提供所需的服务实例,该容器被称为“Service Locator”。“DI容器”和“Service Locator”实际上是同一事物在不同设计模型中的不同称谓罢了,那么DI和Service Locator之间的差异体现在什么地方呢? 183 | 184 | 我们觉得可以从“DI容器”和“Service Locator”被谁使用的角度来区分这两种设计模式的差别。在一个采用依赖注入的应用中,我们只需要采用标准的注入形式将服务类型定义好,并在应用启动之前完成相应的服务注册就可以了,框架自身的引擎在运行过程中会利用DI容器来提供当前所需的服务实例。换句话说,DI容器的使用者应该是框架而不是应用程序。Service Locator模式显然不是这样,很明显是应用程序在利用它来提供所需的服务实例,所以它的使用者是应用程序。 185 | 186 | 我们也可以从另外一个角度区分两者之间的差别。由于依赖服务是以“注入”的方式来提供的,所以采用依赖注入模式的应用可以看成是将服务“推”给DI容器,Service Locator模式下的应用则是利用Service Locator去“拉”取所需的服务,这一推一拉也准确地体现了两者之间的差异。那么既然两者之间有差别,究竟孰优孰劣呢? 187 | 188 | 早在2010年,Mark Seemann就在它的博客中将Service Locator视为一种“反模式(Anti-Pattern)”,虽然也有人对此提出不同的意见,但我个人是非常不推荐使用这种设计模式的。我反对使用Service Locator与上面提到的反对使用属性注入和方法注入具有类似的缘由。 189 | 190 | 我们既然将一组相关的操作定义在一个能够复用的服务中,不但要求服务自身具有独立和自治的特性,也要求服务之间的应该具有明确的边界,服务之间的依赖关系应该是明确的而不是模糊的。不论是采用属性注入或者构造器注入,还是使用Service Locator来提供当前依赖的服务,这无疑为当前的应用增添了一个新的依赖,即针对DI容器或者Service Locator的依赖。 191 | 192 | 当前服务针对另一个服务的依赖与针对DI容器或者Service Locator的依赖具有本质的不同,前者是一种基于类型的依赖,不论是基于服务的接口还是实现类型,这是一种基于“契约”的依赖。这种依赖不仅是明确的,也是有保障的。但是DI容器也好,Service Locator也罢,它们本质上都是一个黑盒,它能够提供所需服务的前提已经预先添加了对应的服务注册。 193 | 194 | 正因为如此,ASP.NET Core框架使用的DI框架只支持构造器注入,而不支持属性和方法注入(类似于Startup和中间件基于约定的方法注入除外)。但是我们很有可能不知不觉地会按照Service Locator模式来编写我们的代码,从某种意义上讲,当我们在程序中使用IServiceProvider(表示DI容器)来提取某个服务实例的时候,就意味着我们已经在使用Service Locator模式了,所以当我们遇到这种情况下的时候应该多想一想是否一定需要这么做。虽然我们提倡尽可能避免使用Service Locator模式,但是有的时候(有其是在编写框架或者组件的时候),我们是无法避免使用IServiceProvider来提取服务。 195 | 196 | ## 4. .Net Core中的DI 197 | .NET Core针对依赖注入的编程主要体现在两个方面: 198 | * 服务注册:创建一个ServiceCollection对象并将服务注册信息以ServiceDescriptor对象的形式添加其中 199 | * 服务消费: 通过ServiceCollection对象创建对应的ServiceProvider并利用它提供我们需要的服务实例 200 | 201 | 我们将在后续章节中对[服务注册](register.md)和[服务消费](consume.md)进行详细的阐述。 202 | 203 | > 参考文献 204 | https://www.cnblogs.com/artech/p/net-core-di-03.html -------------------------------------------------------------------------------- /pages/di-intro.md: -------------------------------------------------------------------------------- 1 | # Asp.Net Core 无处不在的“依赖注入” 2 | 3 | ASP.NET Core的核心是通过一个Server和若干注册的Middleware构成的管道,不论是管道自身的构建,还是Server和Middleware自身的实现,以及构建在这个管道的应用,都需要相应的服务提供支持,ASP.NET Core自身提供了一个DI容器来实现针对服务的注册和消费。换句话说,不只是ASP.NET Core底层框架使用的服务是由这个DI容器来注册和提供,应用级别的服务注册和提供也需要依赖这个DI容器。学习ASP.NET Core,你必须了解无处不在的“依赖注入”。 4 | 5 | 说到依赖注入(Dependency Injection,以下简称DI),就必须说IoC(Inverse of Control),很多人将这两这混为一谈,其实这是两个完全不同的概念,或者是不同“层次”的两个概念。在本系列后续[控制反转(IoC)](#ioc.md)和[依赖注入(DI)](#di.md)中有详细讲解。 6 | 7 | DI框架具有两个核心的功能,即服务的注册和提供,这两个功能分别由对应的对象来承载, 它们分别是ServiceCollection和ServiceProvider。如下图所示,我们将相应的服务以不同的生命周期模式(Transient、Scoped和Singleton)注册到ServiceCollection对象之上,在利用后者创建的ServiceProvider根据注册的服务类型提取相应的服务对象。 8 | 9 | ![ServiceCollection和ServiceProvider](https://i.loli.net/2020/02/26/1kK8xBc92UHsPq6.jpg) -------------------------------------------------------------------------------- /pages/di-ioc.md: -------------------------------------------------------------------------------- 1 | # 控制反转(IoC) 2 | * [1. 流程控制反转](#1-流程控制反转) 3 | * [2. 好莱坞法则](#2-好莱坞法则) 4 | * [3. 流程定制](#3-流程定制) 5 | 6 | IoC主要体现了这样一种设计思想:通过将一组通用流程的控制从应用转移到框架之中以实现对流程的复用,同时采用“好莱坞原则”是应用程序以被动的方式实现对流程的定制。我们可以采用若干设计模式以不同的方式实现IoC。 7 | 8 | ## 1. 流程控制反转 9 | 很多人将IoC说成是一种“面向对象的设计模式”,但在我个人看来IoC不能算作一种“设计模式”,其自身也与“面向对象”没有直接的关系。很多人之所以不能很准确地理解IoC源于他们忽略了一个最根本的东西,那就是IoC这个短语。换句话说,很多人之所以对IoC产生了诸多误解是因为他们忽略了IoC的定义。 10 | 11 | IoC的全名Inverse of Control,翻译成中文就是“控制反转”或者“控制倒置”。控制反转也好,控制倒置也罢,它体现的意思是控制权的转移,即原来控制权在A手中,现在需要B来接管。那么具体对于软件设计来说,IoC所谓的控制权的转移具有怎样的体现呢?要回答这个问题,就需要先了解IoC的C(Control)究竟指的是怎样一种控制。对于我们所在的任何一件事,不论其大小,其实可以分解成相应的步骤,所以任何一件事都有其固有的流程,IoC涉及的所谓控制可以理解为“针对流程的控制”。 12 | 13 | 我们通过一个具体事例来说明传统的设计在采用了IoC之后针对流程的控制是如何实现反转的。比如说现在设计一个针对Web的MVC类库,我们不妨将其命名为MvcLib。简单起见,这个类库中只包含如下这个同名的静态类。 14 | 15 | ```csharp 16 | public static class MvcLib 17 | { 18 | public static Task ListenAsync(Uri address); 19 | public static Task ReceiveAsync(); 20 | public static Task CreateControllerAsync(Request request); 21 | public static Task ExecuteControllerAsync(Controller controller); 22 | public static Task RenderViewAsync(View view); 23 | } 24 | ``` 25 | 26 | MvcLib提供了如上5个方法帮助我们完成整个HTTP请求流程中的5个核心任务。具体来说,ListenAsync方法启动一个监听器并将其绑定到指定的地址进行HTTP请求的监听,抵达的请求通过ReceiveAsync方法进行接收,我们将接收到的请求通过一个Request对象来表示。CreateControllerAsync方法根据接收到的请求解析并激活请求的目标Controller对象。ExecuteControllerAsync方法执行激活的Controller并返回一个表示视图的View对象。RenderViewAsync最终将View对象转换成HTML并作为当前请求响应的内容返回给请求的客户端。 27 | 28 | 现在我们在这个MvcLib的基础上创建一个真正的MVC应用,那么除了按照MvcLib的规范自定义具体的Controller和View之外,我们还需要自行控制从请求的监听与接收、Controller的激活与执行以及View的最终呈现在内的整个流程,这样一个执行流程反映在如下所示的代码中。 29 | 30 | ```csharp 31 | class Program 32 | { 33 | static async Task Main() 34 | { 35 | Uri address = new Uri("http://0.0.0.0:8080/mvcapp"); 36 | await MvcLib.ListenAsync(address); 37 | while (true) 38 | { 39 | var request = await MvcLib.ReceiveAsync(); 40 | var controller = await MvcLib.CreateControllerAsync(request); 41 | var view = await MvcLib.ExecuteControllerAsync(controller); 42 | await MvcLib.RenderViewAsync(view); 43 | } 44 | } 45 | } 46 | ``` 47 | 48 | 这个例子体现了如图1所示的流程控制方式(应用的代码完全采用异步的方式来处理请求,为了让流程图显得更加简单,我们在流程图中画成了同步的形式,读者不必纠结这个问题)。我们设计的类库(MvcLib)仅仅通过API的形式提供某种单一功能的实现,作为类库消费者的应用程序(App)则需要自行编排整个工作流程。如果从重用的角度来讲,这里被重用的仅限于实现某个环节单一功能的代码,编排整个工作流程的代码并没有得到重用。 49 | 50 | ![MvcLib](https://i.loli.net/2020/02/26/MWA7T98YDoIsbXP.png) 51 | 52 | 但是当我们构建一个应用的时候,我们需要的不仅仅是一个能够提供单一API的类库,我们希望的理想形式是能够直接在一个现有的框架上构建我们的应用。**类库(Library)和框架(Framework)的不同之处在于,前者往往只是提供实现某种单一功能的API,而后者则针对一个目标任务对这些单一功能进行编排形成一个完整的流程,这个流程在一个引擎的驱动下自动执行。** 53 | 54 | 对于我们上面演示MvcLib来说,作为消费者的应用程序需要自行控制整个HTTP请求的处理流程,但这实际上是一个很“泛化”的工作流程,几乎所有的MVC应用均采用这样的流程监听、接收请求并最终对请求予以响应。如果我们将这个流程实现在一个MVC框架之中,由它构建的所有MVC应用就可以直接使用这个请求处理流程,而不需要自行重复实现它。 55 | 56 | 现在我们将MvcLib从类库改造成一个框架,并姑且将其称为MvcFrame。如图2所示,MvcFrame的核心是一个被称为MvcEngine的执行引擎,它驱动一个编排好的工作流对HTTP请求进行一致性处理。如果我们利用MvcFrame构建一个具体的MVC应用,除了根据我们的业务需求定义相应的Controller和View之外,我们只需要初始化这个引擎并直接启动它即可。如果你曾经开发过ASP.NET MVC应用,你会发现ASP.NET MVC就是这么一个框架。 57 | 58 | ![MvcEngine](https://i.loli.net/2020/02/26/aqebD9dHLkOptMY.png) 59 | 60 | 有了上面演示的这个例子作为铺垫,我们应该很容易理解IoC所谓的控制反转。总的来说,IoC是我们设计框架所采用的一种基本思想,所谓的控制反转就是将对应用流程的控制转移到框架中。拿上面这个例子来说,在传统面向类库编程的时代,针对HTTP请求处理的流程牢牢控制在应用程序手中。在引入框架之后,请求处理的控制权转移到了框架手上。 61 | 62 | ## 2. 好莱坞法则 63 | 在好莱坞,把简历递交给演艺公司后就只有回家等待。由演艺公司对整个娱乐项目的完全控制,演员只能被动式的接受电影公司的工作,在需要的环节中,完成自己的演出。“不要给我们打电话,我们会给你打电话(don‘t call us, we‘ll call you)”这是著名的好莱坞法则( Hollywood Principle或者 Hollywood Low),IoC完美地体现了这一法则。 64 | 65 | ![好莱坞法则](https://i.loli.net/2020/02/26/Id7vW8zShk1RQsD.png) 66 | 67 | 在IoC的应用语境中,框架就像是掌握整个电影制片流程的电影公司,由于它是整个工作流程的实际控制者,所以只有它知道哪个环节需要哪些组件。应用程序就像是演员,它只需要按照框架定制的规则注册这些组件就可以了,因为框架会在适当的时机加载并执行注册的组件。 68 | 69 | 以熟悉的ASP.NET Core MVC或者ASP.NET MVC应用开发来说,我们只需要按照约定规则(比如目录结构和命名等)定义相应的Controller类型和View文件就可以了。当ASP.NET (Core )MVC框架在进行处理请求的过程中,它会根据解析生成的路由参数定义为对应的Controller类型,并按照预定义的规则找到我们定义的Controller,然后自动创建并执行它。如果定义在当前Action方法需要呈现一个View,框架自身会根据预定义的目录约定找到我们定义的View文件,并对它实施动态编译和执行。整个流程处处体现了“框架Call应用”的好莱坞法则。 70 | 71 | 总的来说,我们在一个框架的基础上进行应用开发,就相当于在一条调试好的流水线上生成某种商品,我们只需要在相应的环节准备对应的原材料,最终下线的就是我们希望得到的最终产品。IoC几乎是所有框架均具有的一个固有属性,从这个意义上讲,“IoC框架”的说法其实是错误的,世界上并没有什么IoC框架,或者说几乎所有的框架都是IoC框架。 72 | 73 | ## 3. 流程定制 74 | 我们采用IoC实现了流程控制从应用程序向框架自身的反转,但是这个被反转的仅仅是一个泛化的流程,任何一个具体的应用都可能需要对组成该流程的某些环节进行定制。还是以我们的MVC框架来说,可能默认的请求处理流程只考虑到针对HTTP 1.1的支持,但是当我们在设计框架的时候应该提供相应的扩展点来支持HTTP 2。作为一个Web框架,用户认证功能是必备的,但是框架自身不能限制于某一种或者几种固定的认证方式,应该通过扩展的方式让用户可以自由地定制任意的认证模式。 75 | 76 | 我们可以说得更加宽泛点。如下图所示,我们将一个泛化的工作流程(A=>B=>C)定义在框架之中,建立在该框架的两个应用需要对组成这个流程的某些环节进行定制。比如步骤A和C可以被App1重用,但是步骤B却需要被定制(B1),App2则重用步骤A和B,但是需要按照自己的方式处理步骤C。 77 | 78 | ![IoC定制流程](https://i.loli.net/2020/02/26/GbH6jZ8zTInNuC4.png) 79 | 80 | IoC将对流程的控制从应用程序转移到框架之中,框架利用一个引擎驱动整个流程的执行,应用程序无需关心该工作流程的细节,它只需要启动这个引擎即可。但是这个引擎一旦被启动,框架就会完全按照预先编排好的流程进行工作,如果应用程序希望整个流程按照自己希望的方式被执行,针对流程的定制一般发生在启动引擎之前。 81 | 82 | 一般来说,框架会以相应的形式提供一系列的扩展点,应用程序则通过定义扩展的方式实现对流程某个环节的定制。在引擎被启动之前,应用程序将所需的扩展注册到框架之中。一旦引擎被正常启动,这些注册的扩展会自动参与到整个流程的执行过程中。 83 | 84 | 综上所述,IoC一方面通过流程控制从应用程序向框架的反转实现了针对流程自身的重用,另一方面通过内置的扩展机制这个被重用的流程可能自由地被定制,这两个因素决定了框架自身的价值。重用让框架不仅仅是为应用程序提供实现单一功能的API,而是提供一整套可执行的解决方案,可定制则使我们可以为不同的应用程序对框架进行定制,这无疑让框架可以使用到更多的应用之中。 85 | 86 | > 参考文献 87 | https://www.cnblogs.com/artech/p/net-core-di-01.html -------------------------------------------------------------------------------- /pages/di-register.md: -------------------------------------------------------------------------------- 1 | # .Net Core 服务注册 2 | 3 | - [.Net Core 服务注册](#net-core-%e6%9c%8d%e5%8a%a1%e6%b3%a8%e5%86%8c) 4 | - [1. ServiceDescriptor](#1-servicedescriptor) 5 | - [2. IServiceCollection](#2-iservicecollection) 6 | - [1) Add](#1-add) 7 | - [2) Add{Lifetime}](#2-addlifetime) 8 | - [3) TryAdd](#3-tryadd) 9 | - [4) TryAdd{Lifetime}](#4-tryaddlifetime) 10 | - [5) TryAddEnumerable](#5-tryaddenumerable) 11 | - [6) RemoveAll & Replace](#6-removeall--replace) 12 | 13 | **服务注册本质是创建相应的ServiceDescriptor对象并将其添加到指定IServiceCollection集合对象中的过程。** 14 | 15 | ## 1. ServiceDescriptor 16 | ServiceDescriptor提供对服务的描述信息,这些信息将指导ServiceProvider正确地实施服务提供操作。 17 | 18 | ```csharp 19 | public class ServiceDescriptor 20 | { 21 | public Type ServiceType { get; } 22 | public ServiceLifetime Lifetime { get; } 23 | 24 | public Type ImplementationType { get; } 25 | public Func ImplementationFactory { get; } 26 | public object ImplementationInstance { get; } 27 | 28 | public ServiceDescriptor(Type serviceType, object instance); 29 | public ServiceDescriptor(Type serviceType, Func factory, ServiceLifetime lifetime); 30 | public ServiceDescriptor(Type serviceType, Type implementationType, ServiceLifetime lifetime); 31 | } 32 | ``` 33 | 34 | ServiceType属性代表提供服务的类型,由于标准化的服务一般会定义成接口,所以在绝大部分情况下体现为一个接口类型。 35 | 36 | 类型为ServiceLifetime的属性Lifetime体现了ServiceProvider针对服务实例生命周期的控制方式。如下面的代码片段所示,ServiceLifetime是一个枚举类型,定义其中的三个选项(Singleton、Scoped和Transient)体现三种对服务对象生命周期的控制形式,我们在后续对此作专门的介绍。 37 | 38 | ```csharp 39 | public enum ServiceLifetime 40 | { 41 | Singleton, 42 | Scoped, 43 | Transient 44 | } 45 | ``` 46 | 47 | ServiceDescriptor的其他三个属性体现了服务实例的三种提供方式,并对应着三个构造函数。如果我们指定了服务的实现类型(对应于ImplementationType属性),那么最终的服务实例将通过调用定义在实现类型中某一个构造函数来创建。如果指定的是一个Func<IServiceProvider, object>对象(对应于ImplementationFactory属性),那么IServiceProvider对象将会将自身作为输入参数调用该委托对象来提供服务实例。如果我们直接指定一个现有的对象(对应的属性为ImplementationInstance),那么该对象就是最终提供的服务实例。 48 | 49 | 如果我们采用直接提供服务实例的形式来创建ServiceDescriptor对象,意味着服务注册默认采用Singleton生命周期模式。对于通过其他两个构造函数创建的ServiceDescriptor对象来说,则需要显式指定采用的生命周期模式。 50 | 51 | 除了调用上面介绍的三个构造函数来创建对应的ServiceDescriptor对象之外,我们还可以提供定义在ServiceDescriptor类型中一系列静态方法来创建该对象。如下面的代码片段所示,ServiceDescriptor提供了如下两个名为Describe的方法重载来创建对应的ServiceDescriptor对象。 52 | 53 | ```csharp 54 | public class ServiceDescriptor 55 | { 56 | public static ServiceDescriptor Describe(Type serviceType, Func implementationFactory, ServiceLifetime lifetime); 57 | public static ServiceDescriptor Describe(Type serviceType, Type implementationType, ServiceLifetime lifetime); 58 | } 59 | ``` 60 | 61 | 当我们调用上面两个Describe方法来创建ServiceDescriptor对象的时候总是需要指定采用的生命周期模式,为了让对象创建变得更加简单,ServiceDescriptor中还定义了一系列针对三种生命周期模式的静态工厂方法。如下所示的是针对Singleton模式的一组Singleton方法重载的定义,针对其他两种模式的Scoped和Transient方法具有类似的定义。 62 | 63 | ```csharp 64 | public class ServiceDescriptor 65 | { 66 | public static ServiceDescriptor Singleton() where TService: class where TImplementation: class, TService; 67 | public static ServiceDescriptor Singleton(Func implementationFactory) where TService: class where TImplementation: class, TService; 68 | public static ServiceDescriptor Singleton(Func implementationFactory) where TService: class; 69 | public static ServiceDescriptor Singleton(TService implementationInstance) where TService: class; 70 | public static ServiceDescriptor Singleton(Type serviceType, Func implementationFactory); 71 | public static ServiceDescriptor Singleton(Type serviceType, object implementationInstance); 72 | public static ServiceDescriptor Singleton(Type service, Type implementationType); 73 | } 74 | ``` 75 | 76 | ## 2. IServiceCollection 77 | IServiceCollection对象本质上就是一个元素类型为ServiceDescriptor的列表。在默认情况下我们使用的是实现该接口的ServiceCollection类型。 78 | 79 | ```csharp 80 | public interface IServiceCollection : IList 81 | {} 82 | public class ServiceCollection : IServiceCollection 83 | {} 84 | ``` 85 | 86 | ### 1) Add 87 | 考虑到服务注册是一个高频调用的操作,所以DI框架为IServiceCollection接口定义了一系列扩展方法完成服务注册的工作,比如下面的这两个Add方法可以将指定的一个或者多个ServiceDescriptor对象添加到IServiceCollection集合中。 88 | 89 | ```csharp 90 | public static class ServiceCollectionDescriptorExtensions 91 | { 92 | public static IServiceCollection Add(this IServiceCollection collection, ServiceDescriptor descriptor); 93 | public static IServiceCollection Add(this IServiceCollection collection, IEnumerable descriptors); 94 | } 95 | ``` 96 | 97 | ### 2) Add{Lifetime} 98 | DI框架还针对具体生命周期模式为IServiceCollection接口定义了一系列的扩展方法,它们会根据提供的输入创建出对应的ServiceDescriptor对象并将其添加到指定的IServiceCollection对象中。如下所示的是针对Singleton模式的AddSingleton方法重载的定义,针对其他两个生命周期模式的AddScoped和AddTransient方法具有类似的定义。 99 | 100 | ```csharp 101 | public static class ServiceCollectionServiceExtensions 102 | { 103 | public static IServiceCollection AddSingleton(this IServiceCollection services) where TService: class; 104 | public static IServiceCollection AddSingleton(this IServiceCollection services) where TService: class where TImplementation: class, TService; 105 | public static IServiceCollection AddSingleton(this IServiceCollection services, TService implementationInstance) where TService: class; 106 | public static IServiceCollection AddSingleton(this IServiceCollection services, Func implementationFactory) where TService: class where TImplementation: class, TService; 107 | public static IServiceCollection AddSingleton(this IServiceCollection services, Func implementationFactory) where TService: class; 108 | public static IServiceCollection AddSingleton(this IServiceCollection services, Type serviceType); 109 | public static IServiceCollection AddSingleton(this IServiceCollection services, Type serviceType, Func implementationFactory); 110 | public static IServiceCollection AddSingleton(this IServiceCollection services, Type serviceType, object implementationInstance); 111 | public static IServiceCollection AddSingleton(this IServiceCollection services, Type serviceType, Type implementationType); 112 | } 113 | ``` 114 | 115 | ### 3) TryAdd 116 | 虽然针对同一个服务类型可以添加多个ServiceDescriptor,但这情况只有在应用需要使用到同一类型的多个服务实例的情况下才有意义,比如我们可以注册多个ServiceDescriptor来提供同一个主题的多个订阅者。如果我们总是根据指定的服务类型来提取单一的服务实例,这种情况下一个服务类型只需要一个ServiceDescriptor对象就够了。对于这种场景我们可能会使用如下两个名为TryAdd的扩展方法,该方法会根据指定ServiceDescriptor提供的服务类型判断对应的服务注册是否存在,只有**不存在指定类型的服务注册情况下**,我们提供的ServiceDescriptor才会被添加到指定的IServiceCollection对象中。 117 | 118 | ```csharp 119 | public static class ServiceCollectionDescriptorExtensions 120 | { 121 | public static void TryAdd(this IServiceCollection collection, ServiceDescriptor descriptor); 122 | public static void TryAdd(this IServiceCollection collection, IEnumerable descriptors); 123 | } 124 | ``` 125 | 126 | ### 4) TryAdd{Lifetime} 127 | 扩展方法TryAdd同样具有基于三种生命周期模式的版本,如下所示的针对Singleton模式的TryAddSingleton方法的定义。在指定服务类型对应的ServiceDescriptor不存在的情况下,它们会采用提供的实现类型、服务实例创建工厂以及服务实例来创建生命周期模式为Singleton的ServiceDescriptor对象并将其添加到指定的IServiceCollection对象中。针对其他两种生命周期模式的TryAddScoped和TryAddTransient方法具有类似的定义。 128 | 129 | ```csharp 130 | public static class ServiceCollectionDescriptorExtensions 131 | { 132 | public static void TryAddSingleton(this IServiceCollection collection) where TService: class; 133 | public static void TryAddSingleton(this IServiceCollection collection) where TService: class where TImplementation: class, TService; 134 | public static void TryAddSingleton(this IServiceCollection collection, Type service); 135 | public static void TryAddSingleton(this IServiceCollection collection, TService instance) where TService: class; 136 | public static void TryAddSingleton(this IServiceCollection services, Func implementationFactory) where TService: class; 137 | public static void TryAddSingleton(this IServiceCollection collection, Type service, Func implementationFactory); 138 | public static void TryAddSingleton(this IServiceCollection collection, Type service, Type implementationType); 139 | } 140 | ``` 141 | 142 | ## 5) TryAddEnumerable 143 | 除了上面介绍的扩展方法TryAdd和TryAdd{Lifetime}之外,IServiceCollection接口还具有如下两个名为TryAddEnumerable的扩展方法。当TryAddEnumerable方法在决定将指定的ServiceDescriptor添加到IServiceCollection对象之前,它也会做存在性检验。与TryAdd和TryAdd{Lifetime}方法不同的是,该方法在**判断执行的ServiceDescriptor是否存在时会同时考虑服务类型和实现类型。** 144 | 145 | ```csharp 146 | public static class ServiceCollectionDescriptorExtensions 147 | { 148 | public static void TryAddEnumerable(this IServiceCollection services, ServiceDescriptor descriptor); 149 | public static void TryAddEnumerable(this IServiceCollection services, IEnumerable descriptors); 150 | } 151 | ``` 152 | 153 | TryAddEnumerable判断存在性的实现类型不只是ServiceDescriptor的ImplementationType属性。如果ServiceDescriptor是通过一个指定的服务实例创建的,那么该实例的类型会作为用来判断存在与否的实现类型。如果ServiceDescriptor是服务实例工厂来创建的,那么代表服务实例创建工厂的Func<in T, out TResult>对象的第二个参数类型将被用于判断ServiceDescriptor的存在性。扩展方法TryAddEnumerable的实现逻辑可以通过如下这段程序来验证。 154 | 155 | ```csharp 156 | var services = new ServiceCollection(); 157 | 158 | services.TryAddEnumerable(ServiceDescriptor.Singleton()); 159 | Debug.Assert(services.Count == 1); 160 | services.TryAddEnumerable(ServiceDescriptor.Singleton()); 161 | Debug.Assert(services.Count == 1); 162 | services.TryAddEnumerable(ServiceDescriptor.Singleton(new Foo())); 163 | Debug.Assert(services.Count == 1); 164 | Func factory4Foo = _ => new Foo(); 165 | services.TryAddEnumerable(ServiceDescriptor.Singleton(factory4Foo)); 166 | Debug.Assert(services.Count == 1); 167 | 168 | services.TryAddEnumerable(ServiceDescriptor.Singleton()); 169 | Debug.Assert(services.Count == 2); 170 | services.TryAddEnumerable(ServiceDescriptor.Singleton(new Baz())); 171 | Debug.Assert(services.Count == 3); 172 | Func factory4Gux = _ => new Gux(); 173 | services.TryAddEnumerable(ServiceDescriptor.Singleton(factory4Gux)); 174 | Debug.Assert(services.Count == 4); 175 | ``` 176 | 177 | 如果通过上述策略得到的实现类型为Object,那么TryAddEnumerable会因为实现类型不明确而抛出一个ArgumentException类型的异常。这种情况主要发生在提供的ServiceDescriptor对象是由服务实例工厂创建的情况,所以上面实例中用来创建ServiceDescriptor的工厂类型分别为Func<IServiceProvider, Foo>和Func<IServiceProvider, Gux>,而不是Func<IServiceProvider, object>。 178 | 179 | ```csharp 180 | var service = ServiceDescriptor.Singleton(_ => new Foo()); 181 | new ServiceCollection().TryAddEnumerable(service); 182 | ``` 183 | 184 | 假设我们采用如上所示的方式利用一个Lamda表达式来创建一个ServiceDescriptor对象,对于创建的ServiceDescriptor来说,其服务实例工厂是一个Func<IServiceProvider, object>对象,所以当我们将它作为参数调用TryAddEnumerable方法的会抛出如下图所示的ArgumentException异常。 185 | 186 | ![ArgumentException类型的异常](https://i.loli.net/2020/02/26/GSMgkrhYCRnw5fT.png) 187 | 188 | ### 6) RemoveAll & Replace 189 | 上面介绍的这些方法最终的目的都是添加新的ServiceDescriptor到指定的IServiceCollection对象中,有的时候我们还希望删除或者替换现有的某个ServiceDescriptor,这种情况下通常发生在需要对当前使用框架中由某个服务提供的功能进行定制的时候。由于IServiceCollection实现了IList接口,所以我们可以调用其Clear、Remove和RemoveAt方法来清除或者删除现有的ServiceDescriptor。除此之外,我们还可以选择如下这些扩展方法。 190 | 191 | ```csharp 192 | public static class ServiceCollectionDescriptorExtensions 193 | { 194 | public static IServiceCollection RemoveAll( this IServiceCollection collection); 195 | public static IServiceCollection RemoveAll(this IServiceCollection collection, Type serviceType); 196 | public static IServiceCollection Replace(this IServiceCollection collection, ServiceDescriptor descriptor); 197 | } 198 | ``` 199 | 200 | RemoveAll和RemoveAll方法帮助我们针对指定的服务类型来删除添加的ServiceDescriptor。Replace方法会使用指定的ServiceDescriptor去替换第一个具有相同服务类型(对应ServiceType属性)的ServiceDescriptor,实际操作是先删除后添加。如果从目前的IServiceCollection中找不到服务类型匹配的ServiceDescriptor,指定的ServiceDescriptor会直接添加到IServiceCollection对象中,这一逻辑也可以利用如下的程序来验证。 201 | 202 | ```csharp 203 | var services = new ServiceCollection(); 204 | services.Replace(ServiceDescriptor.Singleton()); 205 | Debug.Assert(services.Any(it => it.ImplementationType == typeof(Foo))); 206 | 207 | services.AddSingleton(); 208 | services.Replace(ServiceDescriptor.Singleton()); 209 | Debug.Assert(!services.Any(it=>it.ImplementationType == typeof(Foo))); 210 | Debug.Assert(services.Any(it => it.ImplementationType == typeof(Bar))); 211 | Debug.Assert(services.Any(it => it.ImplementationType == typeof(Baz))); 212 | ``` 213 | 214 | > 参考文献 215 | https://www.cnblogs.com/artech/p/net-core-di-07.html -------------------------------------------------------------------------------- /pages/di-src.md: -------------------------------------------------------------------------------- 1 | # Asp.Net Core 依赖注入源码分析 2 | 3 | * [1. 程序启动DI源码解析](#1-程序启动di源码解析) 4 | * [2. 配置文件DI](#2-配置文件di) 5 | * [2.1 配置文件DI基本使用](#21-配置文件di基本使用) 6 | * [2.2 配置文件DI源码解析](#22-配置文件di源码解析) 7 | 8 | ## 1. 程序启动DI源码解析 9 | 在[Asp.Net Core 依赖注入使用](#aspnetcoredi.md)之“依赖注入在管道构建过程中的使用”中我们简单的介绍了DI在程序启动中的使用过程,接下来让我们从Asp.Net Core源码角度来深入探讨这一过程。 10 | 11 | > 以下分析源码分析基于Asp.Net Core 2.1 https://github.com/aspnet/AspNetCore/tree/release/2.1 12 | 13 | 1) 定位程序入口 14 | 15 | ```csharp 16 | public static void Main(string[] args) 17 | { 18 | CreateWebHostBuilder(args) 19 | .Build() 20 | .Run(); 21 | } 22 | 23 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 24 | WebHost.CreateDefaultBuilder(args) 25 | .UseStartup(); 26 | ``` 27 | 可以看到asp.net core程序实际上是一个控制台程序,运行一个Webhost对象从而启动一个一直运行的监听http请求的任务。 28 | 29 | 2) 定位IWebHostBuilder实现,路径为src/Hosting/Hosting/src/WebHostBuilder.cs 30 | 31 | ![IWebHostBuilder实现](https://i.loli.net/2020/02/26/nlAvz6KjJoRSUpG.png) 32 | 33 | 1) 通过上面的代码我们可以看到首先是通过BuildCommonServices来构建一个ServiceCollection。为什么说这么说呢,先让我们我们跳转到BuidCommonServices方法中看下吧。 34 | 35 | ![BuildCommonServices构建ServiceCollection](https://i.loli.net/2020/02/26/Rn5vt8h7i9Iorab.png) 36 | 37 | 通过`var services = new ServiceCollection();`创建了一个ServiceCollection然后往services里面注入很多内容,如:WebHostOptions ,IHostingEnvironment ,IHttpContextFactory ,IMiddlewareFactory等。最后这个BuildCommonServices就返回了这个services对象。 38 | 39 | 4)UseStartup<Startup>()。 在上面的BuildCommonServices方法中也有对IStartup的注入。首先,判断Startup类是否继承于IStartup接口,如果是继承的,那么就可以直接加入在services 里面去,如果不是继承的话,就需要通过ConventionBasedStartup(methods)把method转换成IStartUp后注入到services里面去。结合上面我们的代码,貌似我们平时用的时候注入的方式都是采用后者。 40 | 41 | 5)回到build方法拿到了BuildCommonServices方法构建的ServiceCollection实例后,通过GetProviderFromFactory(hostingServices) 方法构造出了IServiceProvider 对象。到目前为止,IServiceCollection和IServiceProvider都拿到了。然后根据IServiceCollection和IServiceProvider对象构建WebHost对象。构造了WebHost实例还不能直接返回,还需要通过Initialize对WebHost实例进行初始化操作。那我们看看在初始化函数Initialize中,都做了什么事情吧。 42 | 43 | ![WebHost](https://i.loli.net/2020/02/26/Q9th6Dn5FkeaVZb.png) 44 | 45 | 1) 找到src/Hosting/Hosting/src/Internal/WebHost.cs的Initialize方法。如下图所示:主要就是一个EnsureApplicationServices方法。 46 | 47 | ![WebHost.Initialize](https://i.loli.net/2020/02/26/jqWCQM67w9uysE2.png) 48 | 49 | 1) EnsureApplicationServices内容如下:拿到Startup 对象,然后把_applicationServiceCollection 中的对象注入进去。 50 | 51 | ![EnsureApplicationServices](https://i.loli.net/2020/02/26/Tde1wQa9nouyg7c.png) 52 | 53 | 1) 至此build中注册的对象以及StartUp中注册的对象都已经加入到依赖注入容器中了,接下来就是Run起来了。这个run的代码在src\Hosting\Hosting\src\WebHostExtensions.cs中,代码如下: 54 | 55 | ![WebHost.RunAsync](https://i.loli.net/2020/02/26/pxCqTulYrIfLDi1.png) 56 | 57 | WebHost执行RunAsync运行web应用程序并返回一个只有在触发或关闭令牌时才完成的任务。这就是我们运行ASP.Net Core程序的时候,看到的那个命令行窗口了,如果不关闭窗口或者按Ctrl+C的话是无法结束的。 58 | 59 | ## 2. 配置文件DI 60 | 除了[Asp.Net Core 依赖注入使用](aspnetcoredi.html#2-依赖服务注册)中提到的服务注册方式。我们还可以通过配置文件进行对象注入。需要注意的是通过**读取配置文件注入的对象采用的是Singleton方式。** 61 | 62 | ### 2.1 配置文件DI基本使用 63 | 1)在appsettings.json里面加入如下内容 64 | ```json 65 | { 66 | "Logging": { 67 | "LogLevel": { 68 | "Default": "Warning" 69 | } 70 | }, 71 | "Author": { 72 | "Name":"Colin", 73 | "Nationality":"China" 74 | } 75 | } 76 | ``` 77 | 2) Startup类中ConfigureServices中注册TOptions对象 78 | ```csharp 79 | services.Configure(Configuration.GetSection("Author"));//注册TOption实例对象 80 | ``` 81 | 3)消费配置的服务对象,以Controller为例 82 | ```csharp 83 | private readonly Author author; 84 | public TestController(IOptions option) 85 | { 86 | author = option.Value; 87 | } 88 | ``` 89 | 90 | ### 2.2 配置文件DI源码解析 91 | 92 | 1)在Main方法默认调用了WebHost.CreateDefaultBuilder方法创建了一个IWebHost对象,此方法加载了配置文件并使用一些默认的设置。 93 | 94 | ```csharp 95 | public static void Main(string[] args) 96 | { 97 | CreateWebHostBuilder(args) 98 | .Build() 99 | .Run(); 100 | } 101 | 102 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 103 | WebHost.CreateDefaultBuilder(args) 104 | .UseStartup(); 105 | ``` 106 | 107 | 2)在src\MetaPackages\src\Microsoft.AspNetCore\WebHost.cs 中查看CreateDefaultBuilder方法源码如下。可以看到这个方法会在ConfigureAppConfiguration 的时候默认加载appsetting文件,并做一些初始的设置,所以我们不需要任何操作,就能加载appsettings 的内容了。 108 | 109 | ![CreateDefaultBuilder](https://i.loli.net/2020/02/26/1NERpfLjOwGieDH.png) 110 | 111 | 1) **Asp.Net Core的配置文件是支持热更新的**,即不重启网站也能加载更新。如上图所示只需要在AddJsonFile方法中设置属性reloadOnChange:true即可。 112 | 113 | > 参考文献:https://www.cnblogs.com/yilezhu/p/9998021.html -------------------------------------------------------------------------------- /pages/docker-cmd.md: -------------------------------------------------------------------------------- 1 | # Docker 常用命令 2 | 3 | 命令|说明 4 | :-|:- 5 | [`docker run/creat`](#1docker-runcreat)|运行或创建容器 6 | [`docker start/stop/restart`](#2-docker-startstoprestart)|启动/停止/重启容器 7 | [`docker ps`](#3-docker-ps)|列出容器 8 | [`docker images`](#4-docker-images)|列出本地镜像 9 | [`docker build`](#5-docker-build)|列出本地镜像 10 | [`docker rm/rmi`](#6-docker-rmrmi)|删除容器或镜像 11 | [`docker exec/attach`](#7-docker-execattach)|进入容器 12 | [`docker logs [OPTIONS] CONTAINER`](#8-docker-logs)|查看日志 13 | 14 | Docker操作的相关指令非常多,详细的使用方法可以参考[官方文档](https://docs.docker.com/engine/reference/run/),此处我们只列举部分常用命令及其使用注意事项。 15 | 16 | Docker命令格式一般形如: `docker [command] [OPTIONS]` ,例如 17 | ```sh 18 | # 查看docker帮助文档 19 | $ docker -h 20 | 21 | # 查看docker版本信息 22 | $ docker version 23 | 24 | # 查看docker系统信息,如镜像和容器信息,docker版本,CPU/内存,系统架构等 25 | $ docker info 26 | ``` 27 | 28 | ## 1.docker run/creat 29 | `docker run`命令用于创建并启动指定镜像的一个容器。容器进程是独立和相对封闭的,其拥有独立的文件系统,网络配置,进程树等,类似于一个微型的系统。详细使用方式参见[官方文档](https://docs.docker.com/engine/reference/commandline/run/)。`docker create`用于创建一个一个容器但不启动,语法与`docker run`相同。 30 | 31 | ```sh 32 | # 命令格式 33 | $ docker run [OPTIONS] IMAGE [COMMAND] [ARG...] 34 | ``` 35 | 36 | options|含义 37 | :-|:- 38 | `-i`|以交互模式运行容器,通常与 -t 同时使用 39 | `-t`|为容器重新分配一个伪输入终端,通常与 -i 同时使用 40 | `-d`|后台运行容器,并返回容器ID 41 | `-p`|端口映射。格式为:宿主端口(host port):容器端口(container port) 42 | `-e`|设置环境变量 43 | `-v`|挂载卷。如`docker run -p 80:80 -v /data:/data -d nginx`以后台模式启动一个容器,将容器的 80 端口映射到主机的 80 端口,主机的目录 /data 映射到容器的 /data 44 | `--name`|指定容器名称,不指定则会由系统产生一个随机名字 45 | `--link`|添加链接到另一个容器。如`docker run -p 80:80 --link lottery:web nginx`运行nginx容器并连链接lottery容器同时指定lottery容器链接别名为web。链接可以实现容器间相互访问。 46 | `--restart`|容器退出后重启策略。默认为no。可选项`no,always,unless-stopped,on-failure[:max-retries]` 47 | 48 | ```sh 49 | $ docker run \ 50 | -d \ # 后台方式运行 51 | --name my-mysql \ # 命名当前容器为mac-mysql 52 | -e MYSQL_ROOT_PASSWORD=pwd \ # 指定root用户密码为pwd 53 | mysql # 运行mysql容器 54 | 55 | $ docker run \ 56 | -it \ # 前台终端交互方式运行 57 | --name my-lottery \ # 命名当前容器为my-lottery 58 | --link my-mysql:mysql # 链接到名称为my-mysql的容器并指定别名为mysql 59 | lottery # 运行lottery容器 60 | 61 | $ docker run \ 62 | -d \ # 后台方式运行 63 | --name my-nginx \ # 命名当前容器为my-nginx 64 | -p 8000:80 \ # 宿主机8000端口映射为容器80端口 65 | -v ~/nginx/default.conf:/etc/nginx/conf.d/default.conf \ # 宿主机~/nginx/default.conf挂载到容器为/etc/nginx/conf.d/default.conf 66 | --link my-lottery:web \ # 链接到名称为lottery的容器并指定别名为web 67 | --restart always \ # 退出后总是自动重启 68 | nginx # 运行nginx容器 69 | ``` 70 | 71 | ## 2. docker start/stop/restart 72 | * docker start :启动一个或多个已经被停止的容器 73 | * docker stop :停止一个运行中的容器 74 | * docker restart :重启容器 75 | 76 | ```sh 77 | # 命令格式 78 | $ docker start [OPTIONS] CONTAINER [CONTAINER...] 79 | ``` 80 | 81 | ```sh 82 | # 启动容器 my_container 83 | $ docker start my_container 84 | 85 | # 停止容器 my_container 86 | $ docker stop my_container 87 | 88 | # 重启容器 my_container 89 | $ docker restart my_container 90 | ``` 91 | 92 | ## 3. docker ps 93 | `docker ps`用于列出容器。 94 | 95 | ```sh 96 | # 命令格式 97 | docker ps [OPTIONS] 98 | ``` 99 | 100 | options|含义 101 | :-|:- 102 | `-a `|显示所有的容器。不指定则默认只显示正在运行的容器 103 | `-f `|根据条件过滤显示的内容 104 | `-l `|显示最近创建的一个容器 105 | `-n `|列出最近创建的n个容器 106 | `-q `|仅显示容器简短Id 107 | `-s `|显示总的文件大小 108 | 109 | *每个容器都有唯一的"CONTAINER ID"和NAME。ID有完整长ID和简短ID,两者都可以标识容器。* 110 | 111 | ```sh 112 | # 显示所有容器ID 113 | $ docker ps -aq 114 | 115 | # 显示所有lottery镜像的容器 116 | $ docker ps -a -f=ancestor=lottery 117 | ``` 118 | 119 | ## 4. docker images 120 | `docker images`用于列出本地镜像。 121 | 122 | ```sh 123 | # 命令格式 124 | $ docker images [OPTIONS] [REPOSITORY[:TAG]] 125 | ``` 126 | 127 | options|含义 128 | :-|:- 129 | `-a `|列出本地所有的镜像(含中间映像层,默认情况下,过滤掉中间映像层) 130 | `-f `|显示满足条件的镜像 131 | `-q `|只显示镜像ID 132 | 133 | ```sh 134 | # 列出本地镜像 135 | $ docker images 136 | ``` 137 | 138 | ## 5. docker build 139 | `docker build`命令可以使用Dockerfile构建镜像。Dockerfile相关内容参见[制作镜像](docker-dockerfile.md)。 140 | 141 | ```sh 142 | # 命令格式 143 | $ docker build [OPTIONS] PATH | URL | - 144 | ``` 145 | 146 | options|含义 147 | :-|:- 148 | `-t`|镜像的名字及标签,通常 name:tag 或者 name 格式;可以在一次构建中为一个镜像设置多个标签 149 | `-f` |Dockerfile名称。(默认为 ‘PATH/Dockerfile’) 150 | `--pull`|尝试去更新镜像的新版本 151 | 152 | ```sh 153 | # 在当前目录下使用Dockerfile构建名为"colin/webapp"的镜像,tag为1.0 154 | $ docker build -t colin/webapp:1.0 . 155 | ``` 156 | 157 | ## 6. docker rm/rmi 158 | ### 6.1 docker rm 159 | `docker rm`用于删除容器。删除容器之前需要先停止容器。 160 | 161 | options|含义 162 | :-|:- 163 | `-f `|通过SIGKILL信号强制删除一个运行中的容器 164 | `-l `|移除容器间的网络连接,而非容器本身 165 | `-v `|-v 删除与容器关联的卷 166 | 167 | ```sh 168 | # 删除my-nginx容器 169 | $ docker rm my-nginx 170 | 171 | # 删除所有容器 172 | $ docker rm $(docker ps -aq) 173 | 174 | # 删除所有ubuntu镜像的容器 175 | $ docker rm $(docker ps -aq -f=ancestor=ubuntu) 176 | ``` 177 | 178 | ### 6.2 docker rmi 179 | `docker rmi`用于删除镜像。删除容器之前需要先停止容器。删除镜像之前必须把所有这个镜像的容器删除。使用`docker image rm`指令也可以删除镜像。 180 | 181 | ```sh 182 | # 删除所有nginx镜像的容器 183 | $ docker rm $(docker ps -aq -f=ancestor=nginx) 184 | # 删除nginx镜像 185 | $ docker rmi nginx 186 | ``` 187 | 188 | ## 7. docker exec/attach 189 | 进入Docker容器有多种方式,这里我们介绍最简单的`docker attach`和`docker exec`两种方式 190 | ### 7.1 docker attach 191 | `docker attach`用于附加本地终端输入输出及错误流信息到一个运行中的容器。如果容器创建时未指定交互式(-it)运行,可能无法通过`docker attach`进入到容器中。为了确保可以通过`docker attach`进入容器,执行`docker run`时需要指定`-it`,并在启动后执行`/bin/bash`。如 192 | `docker run -itd --name mysql -e MYSQL_ROOT_PASSWORD=pwd mysql /bin/bash` 193 | 194 | `docker attach`命令进入容器后,可以使用`Ctrl+C`,`Ctrl+D`,`exit`等方式退出,如果container当前在运行bash,CTRL-C自然是当前行的输入,没有退出;如果container当前正在前台运行进程,如输出nginx的access.log日志,CTRL-C不仅会导致退出容器,而且还stop了。退出还可能会导致容器关闭,在attach是可以带上--sig-proxy=false来确保CTRL-D或CTRL-C不会关闭容器。 195 | 196 | docker attach有诸多不便之处,推荐使用`docker exec`方式进入容器替代。 197 | 198 | ```sh 199 | $ docker attach --sig-proxy=false mysql 200 | ``` 201 | 202 | ### 7.2 docker exec 203 | `docker exec`可以进入容器内执行命令。使用方式比较简单,详见[官方文档](https://docs.docker.com/engine/reference/commandline/exec/) 204 | 205 | ```sh 206 | # 进入my-nginx容器并开启一个交互式终端 207 | $ docker exec -it my-nginx /bin/bash 208 | ``` 209 | 210 | ## 8. docker logs 211 | `docker logs`用于查看指定容器的日志。 212 | 213 | ```sh 214 | # 命令格式 215 | $ docker logs [OPTIONS] CONTAINER 216 | ``` 217 | 218 | options|含义 219 | :-|:- 220 | `-f `|跟踪日志输出 221 | `--details`|显示详细日志 222 | `--tail`|从最新日志起算,输出日志行数 223 | `-t `|显示日志记录时间 224 | `--since`|输出指定时间之后的日志。时间格式:`2013-01-02T13:23:37` 225 | `until`|输出指定时间之前的日志。时间格式:`2013-01-02T13:23:37` 226 | 227 | ```sh 228 | # 输出nginx容器最新的10条日志 229 | $ docker logs -ft --tail 10 nginx 230 | ``` -------------------------------------------------------------------------------- /pages/docker-dockerfile.md: -------------------------------------------------------------------------------- 1 | # 制作镜像 2 | 3 | * [1. 镜像简介](#1-镜像简介) 4 | * [2. Dockerfile 指令](#2-dockerfile-指令) 5 | * [3. 制作网站镜像](#3-制作网站镜像) 6 | 7 | ## 1. 镜像简介 8 | Docker容器是一个相对独立运行的Linux服务器环境,在其中部署了我们需要的各种环境和服务,而这些都是在Docker镜像中定义的。 9 | 10 | Docker容器最终多作为一个服务提供者角色(微服务)而存在,如MySQL,Nginx,Redis等镜像的容器。此外也有很多镜像只搭建一些特定的环境,并未直接提供服务,此种镜像多作为被继承者为其它镜像提供基础层,如microsoft/dotnet(.net core),python等。我们常在此类镜像基础上部署自己的网站或服务,打包成自定义镜像。 11 | 12 | Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。我们通过`docker pull IMAGE`来获取使用他人公开的镜像。也可以通过Dockerfile来定制自己的镜像。 13 | 14 | Dockerfile构建的镜像只能在本地使用,上传到DockerHub或者自己的搭建私服后才可以供别人使用。 15 | 16 | 如下是自定义mysql镜像Dockerfile示例(mysql官方有公开镜像,此处仅作示例之用)。 17 | 18 | ```sh 19 | FROM ubuntu 20 | RUN echo 'mysql-server mysql-server/root_password password root'|debconf-set-selections 21 | RUN echo 'mysql-server root'|debconf-set-selections 22 | RUN apt-get update 23 | RUN apt-get install -y mysql-server 24 | RUN /etc/init.d/mysql restart &&\ 25 | mysql -uroot -proot -e "grant all privileges on *.* to 'root'@'%' identified by 'root'" &&\ 26 | mysql -uroot -proot -e "show databases;" 27 | EXPOSE 3306 28 | CMD ["/etc/init.d/mysql","restart"] 29 | ``` 30 | 31 | ## 2. Dockerfile 指令 32 | 33 | options|含义 34 | :-|:- 35 | `FROM`|表示当前镜像继承自哪个镜像 36 | `WORKDIR`|指定工作目录 37 | `CP`|将宿主机文件或目录拷贝到容器中 38 | `RUN`|**镜像构建时**执行命令。多用作预装软件修改配置等 39 | `EXPOSE`|服务允许暴露端口 40 | `CMD/ENTRYPOINT`|**镜像容器启动时**执行命令。多用于启动服务、运行程序等 41 | 42 | 43 | 每个Dockerfile只能有一条`CMD`命令,如果指定了多条,只有最后一条会被执行。 44 | 45 | **如果容器启动时不指定参数,则`CMD`和`ENTRYPOINT`是一样的。否则`CMD`指定的命令会被`docker run` 的容器参数覆盖, 而`ENTRYPOINT`则会把容器参数传递给自身指定的命令。** 46 | 47 | 通过以下案例简单可以证明以上`CMD`和`ENTRYPOINT`的区别。 48 | 49 | 1) 有使用`CMD`的Dockerfile如下: 50 | ```sh 51 | FROM ubuntu 52 | CMD ["uname"] 53 | ``` 54 | 创建并启动容器。 55 | ```sh 56 | # 构建镜像 57 | $ docker build -t cmd . 58 | 59 | # 创建并启动容器 60 | $ docker run -it cmd # 输出 Linux 61 | $ docker run -it cmd -a # 错误输出。"-a": executable file not found in $PATH" 62 | $ docker run -it cmd whoami # 输出 root 63 | ``` 64 | 以上案例中,容器启动时`-a`和`whoami`参数都将覆盖镜像中`CMD`指定的`uname`命令。所以最终运行的分别是`-a`和`whoami`指令。`-a`指令不存在所以运行报错,`whoami`指令则输出当前用户`root`。 65 | 66 | 2) 有使用`ENTRYPOINT`的Dockerfile如下: 67 | ```sh 68 | FROM ubuntu 69 | ENTRYPOINT ["uname"] 70 | ``` 71 | 创建并启动容器。 72 | ```sh 73 | # 构建镜像 74 | $ docker build -t entrypoint . 75 | 76 | # 创建并启动容器 77 | $ docker run -it entrypoint # 输出 Linux 78 | $ docker run -it entrypoint -a # 输出 Linux 06968e7efc5d 4.9.125-linuxkit #1 SMP Fri Sep 7 08:20:28 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux 79 | $ docker run -it entrypoint whoami # 错误输出 uname: extra operand 'whoami' 80 | ``` 81 | 以上案例中,容器启动时`-a`和`whoami`参数都将作为`ENTRYPOINT`指定的`uname`命令的参数。所以最终运行的分别是`uname -a`和`uname whoami`指令。`whoami`参数非法所以最后一条命令报错。 82 | 83 | * `RUN` 是构建镜像时执行的指令,用于安装软件、修改配置等初始化的代码, 可以执行多条; 84 | * `CMD` 相当于镜像的默认开机指令,只能指定一条 CMD,容器运行参数可以覆盖 CMD; 85 | * `ENTRYPOINT` 用于把镜像容器打造成可执行程序,容器运行参数作为可执行程序的参数; 86 | 87 | ## 3. 制作网站镜像 88 | 此处我们简单演示制作一个基于asp.net core的网站镜像,其他镜像制作大同小异。 89 | 90 | 假定我们网站程序已经构建完成并发布,此Dockerfile位于网站发布目录中。 91 | ```sh 92 | FROM microsoft/dotnet:2.2-aspnetcore-runtime # 基于asp.net core 2.2 runtime官方镜像制作本镜像 93 | COPY . /publish # 将宿主机当前目录下所有内容拷贝到镜像的/publish目录中 94 | WORKDIR /publish # 设定当前工作目录为/publish 95 | EXPOSE 5000/tcp # 暴露tcp协议5000端口 96 | CMD ["dotnet","WebApp.dll"] # 容器启动执行 dotnet WebApp.dll命令 97 | ``` 98 | 创建并启动网站容器。 99 | ```sh 100 | $ docker build -t colin/webapp:1.0 . # 在当前目录下使用Dockerfile构建镜像命名为colin/webapp,tag为1.0 101 | $ docker run \ 102 | -d \ 103 | --name webapp \ 104 | -p 8000:5000 \ 105 | --restart always \ 106 | colin/webapp:1.0 # 创建并启动容器 107 | ``` 108 | 完成以上操作后在宿主机通过 http://127.0.0.1:8000 即可访问我们的网站,如果需要暴露到外网,根据微软建议最好使用nginx等服务器作反代。 109 | 110 | 除了以上使用网站已发布内容构建Docker镜像,我们也可以在镜像构建过程中完成源码编译、测试,发布,部署等过程。以[lottery](https://github.com/TechnologyGeeks/lottery)项目为例 111 | ```sh 112 | FROM microsoft/dotnet:2.2-sdk AS build 113 | WORKDIR /app 114 | 115 | # copy csproj and restore as distinct layers 116 | COPY *.sln . 117 | COPY Colin.Lottery.WebApp/*.csproj ./Colin.Lottery.WebApp/ 118 | COPY Colin.Lottery.DataService/*.csproj ./Colin.Lottery.DataService/ 119 | COPY Colin.Lottery.Analyzers/*.csproj ./Colin.Lottery.Analyzers/ 120 | COPY Colin.Lottery.Collectors/*.csproj ./Colin.Lottery.Collectors/ 121 | COPY Colin.Lottery.Models/*.csproj ./Colin.Lottery.Models/ 122 | COPY Colin.Lottery.Common/*.csproj ./Colin.Lottery.Common/ 123 | COPY Colin.Lottery.Utils/*.csproj ./Colin.Lottery.Utils/ 124 | WORKDIR /app/Colin.Lottery.WebApp/ 125 | RUN dotnet restore 126 | 127 | # copy and publish app and libraries 128 | WORKDIR /app/ 129 | COPY Colin.Lottery.WebApp/. ./Colin.Lottery.WebApp/ 130 | COPY Colin.Lottery.DataService/. ./Colin.Lottery.DataService/ 131 | COPY Colin.Lottery.Analyzers/. ./Colin.Lottery.Analyzers/ 132 | COPY Colin.Lottery.Collectors/. ./Colin.Lottery.Collectors/ 133 | COPY Colin.Lottery.Models/. ./Colin.Lottery.Models/ 134 | COPY Colin.Lottery.Common/. ./Colin.Lottery.Common/ 135 | COPY Colin.Lottery.Utils/. ./Colin.Lottery.Utils/ 136 | WORKDIR /app/Colin.Lottery.WebApp/ 137 | RUN dotnet publish -c Release -o out 138 | 139 | FROM build AS testcollector 140 | WORKDIR /app/Colin.Lottery.Collectors.Test 141 | COPY Colin.Lottery.Collectors.Test/. . 142 | ENTRYPOINT ["dotnet", "test", "--logger:trx"] 143 | 144 | FROM build AS testanalyzer 145 | WORKDIR /app/Colin.Lottery.Analyzers.Test 146 | COPY Colin.Lottery.Analyzers.Test/. . 147 | ENTRYPOINT ["dotnet", "test", "--logger:trx"] 148 | 149 | FROM microsoft/dotnet:2.2-aspnetcore-runtime AS runtime 150 | WORKDIR /app 151 | COPY --from=build /app/Colin.Lottery.WebApp/out ./ 152 | ENTRYPOINT ["dotnet", "Colin.Lottery.WebApp.dll"] 153 | ``` 154 | 155 | 以上如果代码编译或测试出错则网站镜像构建通过,一定程度上避免了程序发布错误。 -------------------------------------------------------------------------------- /pages/docker-install.md: -------------------------------------------------------------------------------- 1 | # Docker 安装配置 2 | 3 | * [1. 安装Docker](#1-安装docker) 4 | * [2. 加速器和镜像市场](#2-加速器和镜像市场) 5 | 6 | ## 1. 安装Docker 7 | Docker安装方法可以参考[官网步骤](https://docs.docker.com/install/)。 8 | Windows和MacOS中可以都可以使用[Docker Desktop](https://www.docker.com/products/docker-desktop),界面化操作表简单,此处不作介绍。下面我们以Ubuntu 18.04为例讲解安装步骤。 9 | 10 | ```sh 11 | # 安装docker 12 | $ sudo apt install docker.io 13 | 14 | # 查看docker版本 15 | $ sudo docker version 16 | 17 | # 查看 docker 系统信息 18 | $ sudo docker info 19 | 20 | # 下载镜像,如mysql 21 | $ sudo docker pull mysql 22 | ``` 23 | 24 | ## 2. 加速器和镜像市场 25 | 国内访问Docker Hub可能速度比较慢。我们可以考虑使用加速器和镜像市场。加速器是代理服务器,最终还是访问官方网站,和官网一致和镜像市场的区别,如阿里云或者 DaoCloud 等加速器。镜像市场是私服,不和官网一致,如[DaoCloud 镜像市场](https://hub.daocloud.io/)等。 26 | 27 | ### 1)镜像市场 28 | 如果使用国内镜像市场镜像直接使用`docker pull`命令拉取即可,一般镜像市场都用使用说明。如 29 | 30 | ```sh 31 | # 拉取DaoCloud镜像市场的MySQL 32 | $ docker pull daocloud.io/library/mysql 33 | ``` 34 | 35 | ### 2)加速器 36 | 使用镜像加速器可以按照第三方加速器说明配置即可,如 [DaoCloud加速器](https://www.daocloud.io/mirror) 37 | 38 | ## 3. 配置docker用户组 39 | Linux中每次执行docker指令都需要`sudo`比较麻烦,我们可以把操作用户加入docker用户组来解决。 40 | ```sh 41 | # 添加docker用户组 42 | $ sudo groupadd docker 43 | 44 | # 将当前操作用户添加到docker组 45 | $ sudo gpasswd -a CurrentUserName docker # CurrentUserName为当前操作的用户名 46 | 47 | # 重启docker服务 48 | $ sudo service docker restart 49 | 50 | # 注销用户后重新登录即可 51 | $ logout 52 | ``` -------------------------------------------------------------------------------- /pages/docker-intro.md: -------------------------------------------------------------------------------- 1 | # Docker — 领先的软件容器平台 2 | 3 | * [1. What's the problem](#1-whats-the-problem) 4 | * [2. Docker特点](#2-docker特点) 5 | * [3. Docker基本概念](#3-docker基本概念) 6 | * [3.1 镜像(Image)](#31-镜像image) 7 | * [3.2 容器(Container)](#32-容器container) 8 | * [3.3 仓库(Repository)](#33-镜像repository) 9 | * [4. 容器与虚拟机](#4-容器与虚拟机) 10 | 11 | ## 1. What's the problem 12 | 系统开发常遇到以下问题: 13 | * A电脑运行没问题,B电脑上运行有问题。俗称“我这里没问题呀”🤦‍‍‍🤦‍🤦‍ 14 | * 服务器只运行一个应用造成资源浪费,但是运行多个应用又怕互相干扰 15 | * 在开发人员电脑上开发的系统,跑到服务器上时需要重新安装软件、安装开发包、系统配置,不同系统上可能操作还不一样 16 | 17 | ... 18 | 19 | 以上问题可以用虚拟机技术解决,但是性能低,硬件浪费严重。于是Docker技术应用而生。 20 | 21 | ## 2. Docker特点 22 | 23 | ![docker图解](https://i.loli.net/2020/02/26/quXQjZkO1axJhod.jpg) 24 | 25 | Docker 是世界领先的软件容器平台,具有以下特点。 26 | 27 | * 轻量 28 | 29 | Docker在操作系统上分出多个独立的区域,称作“容器”(Container)。各个容器之间互相“基本隔离”,每个容器可以单独有自己的系统配置、安装的软件、安装 的开发包,各个容器之间的软件“基本”不会互相干扰。在一台机器上运行的多个Docker容器可以共享这台机器的操作系统内核;它们能够迅速启动,只需占用很少的计算和内存资源。镜像是通过文件系统层进行构造的,并共享一些公共文件。这样就能尽量降低磁盘用量,并能更快地下载镜像。Docker性能损耗比虚拟机低很多,都是进程跑在原生操作性系统下,基本没有性能损耗。 30 | 31 | * 标准化 32 | 33 | Docker 容器基于开放式标准。能够在不同平台环境下快速的移植,而不用担心运行环境的变化导致应用无法正常运行的情况。目前支持Linux/Windows/MacOS, 34 | 但**不支持32位系统**。 35 | 36 | * 伸缩性 37 | 38 | Docker还可以更好的满足对于可伸缩性的要求。Docker可以按需自动扩容,自动启动多个服务器、创建多个容器运行更多集群服务器。 39 | 40 | * 即抛性 41 | 42 | Docker部署系统要求可以“即抛”,不保存状态数据。一个容器不再使用时,直接删除即可,不会保存相关数据。 43 | 44 | * 安全性 45 | 46 | Docker赋予应用的隔离性不仅限于彼此隔离,还独立于底层的基础设施。Docker 默认提供最强的隔离,因此应用出现问题,也只是单个容器的问题,而不会波及到整台机器。 47 | 48 | ## 3. Docker基本概念 49 | ### 3.1 镜像(Image) 50 | 操作系统分为内核和用户空间。对于 Linux 而言,内核启动后,会挂载 root 文件系统为其提供用户空间支持。而Docker 镜像(Image),就相当于是一个 root 文件系统。比如官方镜像 ubuntu 16.04 就包含了完整的一套 Ubuntu 16.04 最小系统的 root 文件系统。 51 | 52 | Docker 镜像是**一个特殊的文件系统**,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。 53 | 54 | Docker 设计时,就充分利用 Union FS的技术,将其设计为 **分层存储的架构** 。 镜像实际是由多层文件系统联合组成。 55 | 56 | 镜像构建时,会一层层构建,前一层是后一层的基础。每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。比如,删除前一层文件的操作,实际不是真的删除前一层的文件,而是仅在当前层标记为该文件已删除。在最终容器运行的时候,虽然不会看到这个文件,但是实际上该文件会一直跟随镜像。因此,在构建镜像的时候,需要额外小心,每一层尽量只包含该层需要添加的东西,任何额外的东西应该在该层构建结束前清理掉。 57 | 58 | 分层存储的特征还使得镜像的复用、定制变的更为容易。甚至可以用之前构建好的镜像作为基础层,然后进一步添加新的层,以定制自己所需的内容,构建新的镜像。 59 | 60 | ### 3.2 容器(Container) 61 | 62 | ![容器图解](https://i.loli.net/2020/02/26/ksn3qvM4iSVyNxl.png) 63 | 64 | 镜像( Image )和容器( Container )的关系,就像是面向对象程序设计中的 类 和 实例一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。 65 | 66 | 容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的命名空间。因此容器可以拥有自己的 root 文件系统、自己的网络配置、自己的进程空间,甚至自己的用户 ID 空间。容器内的进程是运行在一个隔离的环境里,使用起来,就好像是在一个独立于宿主的系统下操作一样。这种特性使得容器封装的应用比直接在宿主运行更加安全。也因为这种隔离的特性,很多人初学 Docker 时常常会混淆容器和虚拟机。 67 | 68 | 前面讲过镜像使用的是分层存储,容器也是如此。每一个容器运行时,是以镜像为基础层,在其上创建一个当前容器的存储层,我们可以称这个为容器运行时读写而准备的存储层为容器存储层。容器存储层的生存周期和容器一样,容器消亡时,容器存储层也随之消亡。因此,任何保存于容器存储层的信息都会随容器删除而丢失。 69 | 70 | 按照 Docker 最佳实践的要求,**容器不应该向其存储层内写入任何数据,容器存储层要保持无状态化。**所有的文件写入操作,都应该使用 数据卷(Volume)、或者绑定宿主目录,在这些位置的读写会跳过容器存储层,直接对宿主(或网络存储)发生读写,其性能和稳定性更高。 71 | 72 | 数据卷的生存周期独立于容器,容器消亡,数据卷不会消亡。因此,使用数据卷后,容器删除或者重新运行之后,数据却不会丢失。 73 | 74 | ### 3.3 仓库(Repository)   75 | 镜像构建完成后,可以很容易的在当前宿主机上运行,但如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,Docker Registry 就是这样的服务。 76 | 77 | 一个 Docker Registry 中可以包含多个仓库(Repository);每个仓库可以包含多个标签(Tag);每个标签对应一个镜像。通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本。我们可以通过 <仓库名>:<标签> 的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以 latest 作为默认标签。以 Ubuntu 镜像 为例,ubuntu 是仓库的名字,其内包含有不同的版本标签,如,14.04 ,16.04。我们可以通过 ubuntu:14.04,或者 ubuntu:16.04 来具体指定所需哪个版本的镜像。如果忽略了标签,比如 ubuntu ,那将视为 ubuntu:latest 。 78 | 79 | 仓库名经常以两段式路径形式出现,比如 jwilder/nginx-proxy ,前者往往意味着 Docker Registry 多用户环境下的用户名,后者则往往是对应的软件名。但这并非绝对,取决于所使用的具体 Docker Registry 的软件或服务。 80 | 81 | #### 1) Docker Registry 公开服务 82 | Docker Registry 公开服务是开放给用户使用、允许用户管理镜像的 Registry 服务。一般这类公开服务允许用户免费上传、下载公开的镜像,并可能提供收费服务供用户管理私有镜像。 83 | 84 | 最常使用的 Registry 公开服务是官方的 [Docker Hub](https://hub.docker.com/),这也是默认的 Registry,并拥有大量的高质量的官方镜像。除此以外,还有 CoreOS 的 Quay.io,CoreOS 相关的镜像存储在这里;Google 的 Google Container Registry,Kubernetes 的镜像使用的就是这个服务。国内也有一些云服务商提供类似于 Docker Hub 的公开服务。比如 [网易云镜像服务](https://www.163yun.com/product/repo)、[DaoCloud 镜像市场](https://hub.daocloud.io/)、[阿里云镜像库](https://www.aliyun.com/product/containerservice?utm_content=se_1292836)等。 85 | 86 | 由于某些原因,在国内访问这些服务可能会比较慢。国内的一些云服务商提供了针对 Docker Hub 的镜像服务( Registry Mirror ),这些镜像服务被称为加速器。常见的有 阿里云加速器、DaoCloud 加速器 等。使用加速器会直接从国内的地址下载 Docker Hub 的镜像,比直接从 Docker Hub 下载速度会提高很多。 87 | 88 | #### 2) 私有 Docker Registry 89 | 除了使用公开服务外,用户还可以在本地搭建私有 Docker Registry。Docker 官方提供了Docker Registry 镜像,可以直接使用做为私有 Registry 服务。 90 | 91 | 开源的 Docker Registry 镜像只提供了 Docker Registry API 的服务端实现,足以支持docker命令,不影响使用。但不包含图形界面,以及镜像维护、用户管理、访问控制等高级功能。在官方的商业化版本 Docker Trusted Registry 中,提供了这些高级功能。 92 | 93 | 除了官方的 Docker Registry 外,还有第三方软件实现了 Docker Registry API,甚至提供了用户界面以及一些高级功能。比如,VMWare Harbor 和 Sonatype Nexus。 94 | 95 | ## 4. 容器与虚拟机 96 | 97 | 传统虚拟机技术是虚拟出一套硬件后,在其上运行一个完整操作系统,在该系统上再运行所需应用进程;而容器内的应用进程直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。因此容器要比传统虚拟机更为轻便。 98 | 99 | ![Docker与虚拟机对比图](https://i.loli.net/2020/02/26/rvspaT8EOc4WdMq.png) 100 | 101 | * 容器是一个应用层抽象,用于将代码和依赖资源打包在一起。多个容器可以在同一台机器上运行,共享操作系统内核,但各自作为独立的进程在用户空间中运行 。与虚拟机相比,容器占用的空间较少(容器镜像大小通常只有几十兆),瞬间就能完成启动。 102 | 103 | * 虚拟机 (VM) 是一个物理硬件层抽象,用于将一台服务器变成多台服务器。管理程序允许多个 VM 在一台机器上运行。每个VM都包含一整套操作系统、一个或多个应用、必要的二进制文件和库资源,因此 占用大量空间 。而且 VM 启动也十分缓慢 。 104 | 105 | 两者有不同的使用场景。虚拟机更擅长于彻底隔离整个运行环境。例如,云服务提供商通常采用虚拟机技术隔离不同的用户。而 Docker通常用于隔离不同的应用 ,例如前端,后端以及数据库。 106 | 107 | 108 | > 参考文献 109 | * https://blog.csdn.net/aa1215018028/article/details/80823659 110 | * https://blog.csdn.net/wo18237095579/article/details/80480882 -------------------------------------------------------------------------------- /pages/https.md: -------------------------------------------------------------------------------- 1 | # 全面 HTTPS 时代 2 | 3 | * [1. HTTPS 简介](#1-https-简介) 4 | * [2. HTTPS 工作原理](#2-https-工作原理) 5 | * [3. HTTPS 现状分析](#3-https-现状分析) 6 | * [4. 免费升级到 HTTPS](#4-免费升级到-https) 7 | * [5. GitHub Pages 自定义域名支持 HTTPS](#5-github-pages-自定义域名支持-https) 8 | 9 | ## 1. HTTPS 简介 10 | 11 | #### 1) HTTP 的问题 12 | * 认证网站 13 | 假如你正在访问支付宝,怎样确定你正在访问的是阿里巴巴提供的支付宝而不是假冒伪劣的钓鱼网站呢? 14 | * 数据安全 15 | HTTP协议以明文方式发送内容,不提供任何方式的数据加密,如果攻击者截取了Web浏览器和网站服务器之间的传输报文,就可以直接读懂其中的信息,因此,HTTP协议不适合传输一些敏感信息,比如:信用卡号、密码等支付信息。 16 | 17 | 为了解决上面的问题,HTTPS出场了 👏👏👏 18 | 19 | #### 2) HTTPS 是什么 20 | 传输层安全性(TLS)是HTTPS的官方名称,你可能听说过它称为SSL(安全套接字层),SSL是已弃用的名称,TLS是一种加密协议,可通过计算机网络提供安全通信。 21 | 22 | HTTPS是以安全为目标的HTTP通道,简单讲是HTTP的安全版,即HTTP下加入SSL或TLS,HTTPS的安全基础是SSL/TLS。 23 | 24 | HTTPS协议的主要作用有两个:一种是建立一个信息安全通道,来保证数据传输的安全;另一种就是认证网站的真实性。 25 | 26 | ## 2. HTTPS 工作原理 27 | #### 1) 主体对象 28 | * 客户端。通常是浏览器(Chrome、IE、FireFox等),也可以自己编写的各种语言的客户端程序。 29 | * 服务端。一般指支持Https的网站,比如github、支付宝。 30 | * CA(Certificate Authorities)机构。Https证书签发和管理机构,比如Symantec、Comodo、GoDaddy、GlobalSign。 31 | 32 | ![https主体](https://i.loli.net/2020/02/26/2vJl74IhwHiasWT.png) 33 | 34 | #### 2) 工作流程 35 | 客户端在使用HTTPS方式与Web服务器通信时有以下几个步骤,如图所示。 36 | 37 | ![HTTPS工作原理](https://i.loli.net/2020/02/26/t8dKfR6CbrOMYBS.png) 38 | 39 | 工作流程,基本分为三个阶段: 40 | 41 | 1. 认证服务器。浏览器内置一个受信任的CA机构列表,并保存了这些CA机构的证书。第一阶段服务器会提供经CA机构认证颁发的服务器证书,如果认证该服务器证书的CA机构,存在于浏览器的受信任CA机构列表中,并且服务器证书中的信息与当前正在访问的网站(域名等)一致,那么浏览器就认为服务端是可信的,并从服务器证书中取得服务器公钥,用于后续流程。否则,浏览器将提示用户,根据用户的选择,决定是否继续。当然,我们可以管理这个受信任CA机构列表,添加我们想要信任的CA机构,或者移除我们不信任的CA机构。 42 | 43 | 2. 协商会话密钥。客户端在认证完服务器,获得服务器的公钥之后,利用该公钥与服务器进行加密通信,协商出两个会话密钥,分别是用于加密客户端往服务端发送数据的客户端会话密钥,用于加密服务端往客户端发送数据的服务端会话密钥。在已有服务器公钥,可以加密通讯的前提下,还要协商两个对称密钥的原因,是因为非对称加密相对复杂度更高,在数据传输过程中,使用对称加密,可以节省计算资源。另外,会话密钥是随机生成,每次协商都会有不一样的结果,所以安全性也比较高。 44 | 45 | 3. 加密通讯。此时客户端服务器双方都有了本次通讯的会话密钥,之后传输的所有Http数据,都通过会话密钥加密。这样网路上的其它用户,将很难窃取和篡改客户端和服务端之间传输的数据,从而保证了数据的私密性和完整性。 46 | 47 | *Https就是Http跑在SSL或者TLS上,所以本文讨论的原理和流程其实是SSL和TLS的流程,对于其它使用SSL或者TLS的应用层协议,本文内容一样有效。* 48 | 49 | ## 3. HTTPS 现状分析 50 | 51 | #### 1) 优缺点分析 52 | 53 | 搞明白了Https的工作原理后,其优缺点就很容易理解了。 54 | 55 | **优点** 56 | 57 | 尽管HTTPS并非绝对安全,掌握根证书的机构、掌握加密算法的组织同样可以进行中间人形式的攻击,但HTTPS仍是现行架构下最安全的解决方案,并且解决了网站认证的问题,这两点也正是我们在最开始提出的问题。另外,Google曾表示“比起同等HTTP网站,采用HTTPS加密的网站在搜索结果中的排名将会更高”,因此采用HTTPS的网站更有利于SEO. 58 | 59 | **缺点** 60 | 61 | * HTTPS协议握手阶段比较费时,没有HTTP高效,且会使页面的加载时间延长近功耗增加; 62 | * SSL证书收费 63 | * SSL证书通常需要绑定IP,不能在同一IP上绑定多个域名。 64 | 65 | #### 2) 现状分析 66 | 了解了HTTPS的优缺点,我们再来看下其发展现状。 67 | 68 | 一般来说,很多公司都会直接购买由GlobalSign、GeoTrust、Verisign等全球公认的数字证书颁发机构颁发的SSL证书。购买?没错,大多数SSL证书都需要按年付费使用,而且价格不菲。过去HTTPS被认为比较低效。这也是前面提到的HTTPS的主要缺点。 69 | 70 | 但随着技术的发展,现在机器变得更快,已经解决了性能问题,Let's Encrypt等机构提供免费TLS证书,这两项发展改变了游戏,并使TLS成为主流。 71 | 72 | #### 3) 免费证书 73 | Let's Encrypt 是一个免费、开放,自动化的证书颁发机构,由 ISRG(Internet Security Research Group)运作。ISRG 是一个关注网络安全的公益组织,其赞助商包括 Mozilla、Akamai、Cisco、EFF、Chrome、IdenTrust、Facebook等公司。ISRG 的目的是消除资金和技术领域的障碍,全面推进网站从HTTP到HTTPS过度的进程。 74 | 75 | 目前,包括FireFox、Chrome在内的主流浏览器都支持Let's Encrypt证书,已经有不少用户在真实项目中使用Let's Encrypt证书。Let's Encrypt免费SSL证书的有效期是90天,到期后可以再续期,这样也就可以变相长期使用了。 76 | 77 | ## 4. 免费升级到 HTTPS 78 | 一般的HTTPS使用流程如下。 79 | 80 | ![HTTPS使用流程](https://i.loli.net/2020/02/26/H5OhIoYkGQE3Wvf.png) 81 | 82 | 83 | > Let’s Encrypt TLS 免费证书使用 84 | 85 | Let’s Encrypt TLS证书可以自动化生成和更新,由于他们是免费的,所以没有理由不去做。相信大家也更关注免费证书如何使用,不多说,这就搞起来 😊😊😊 86 | 87 | *以下案例使用 `Ubuntu 18.10/nginx 1.15.5` 环境。* 88 | 89 | * 根据[Let’s Encrypt 官网](https://letsencrypt.org/getting-started)推荐,我们选择使用[ Certbot ACME client](https://certbot.eff.org/) 90 | 91 | * 选择对应的软件和操作系统环境 92 | 93 | ![Certbot官网](https://i.loli.net/2020/02/26/S8XhGaJvfgBOlxz.jpg) 94 | 95 | * 参照网站给出的命令进行安装/配置/更新 证书 96 | 97 | ![证书安装配置](https://i.loli.net/2020/02/26/NAxD6MGTXW9KbUz.jpg) 98 | 99 | * 完成以上配置后,访问你的网站,不出意外已经升级到https了👍 100 | 101 | 自动配置完成之后的Nginx的配置文件形如: 102 | 103 | ```json 104 | server { 105 | server_name localhost 104.199.230.207 bet518.win www.bet518.win; 106 | location / { 107 | proxy_pass localhost:5000; 108 | proxy_http_version 1.1; 109 | proxy_set_header Upgrade $http_upgrade; 110 | proxy_set_header Connection keep-alive; 111 | proxy_set_header Host $host; 112 | proxy_cache_bypass $http_upgrade; 113 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 114 | proxy_set_header X-Forwarded-Proto $scheme; 115 | } 116 | 117 | listen 443 ssl; # managed by Certbot 118 | ssl_certificate /etc/letsencrypt/live/bet518.win/fullchain.pem; # managed by Certbot 119 | ssl_certificate_key /etc/letsencrypt/live/bet518.win/privkey.pem; # managed by Certbot 120 | include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot 121 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot 122 | } 123 | 124 | server { 125 | if ($host = www.bet518.win) { 126 | return 301 https://$host$request_uri; 127 | } # managed by Certbot 128 | 129 | 130 | if ($host = bet518.win) { 131 | return 301 https://$host$request_uri; 132 | } # managed by Certbot 133 | 134 | listen 80; 135 | server_name localhost 104.199.230.207 bet518.win www.bet518.win; 136 | return 404; # managed by Certbot 137 | } 138 | ``` 139 | 140 | ## 5. GitHub Pages 自定义域名支持 HTTPS 141 | 2018年5月1日,GitHub Pages 官方宣布 GitHub Pages 对自定义域名支持 HTTPS。 142 | https://blog.github.com/2018-05-01-github-pages-custom-domains-https/ 143 | 144 | 什么,你问我为什么要GitHub Page使用HTTPS?简单来说,除了 HTTPS 自身带来的加密、防劫持等特性外,Github 官方基于 HTTPS 配合 CND,使得网站的加载速度更快,还能提供额外的防御 DDoS 攻击的保护。 145 | 146 | GitHub官方文档已经说明了如何开启HTTPS。英文不熟悉的小伙伴看我下面的简单粗暴的讲解即可。 147 | 148 | #### 1)绑定域名 149 | 150 | 注册域名不多说。如何使用GitHub Pages中创建博客也不多说。废话完了,下面开始正题,以博主自己的账户为例(GitHub用户名为`colin-chang`) 151 | 152 | * 解析域名 153 | 154 | ![解析域名](https://i.loli.net/2020/02/27/AYqRwvrJu6DoEFm.jpg) 155 | 156 | * Github Pages 项目配置 157 | * 打开 username.github.io 项目设置,并找到 `GitHub Pages` 模块的`Custom domain`填写自己的域名并点击 Save 158 | * 在项目根目录新建一个名为 “CNAME” 的文件,内容为自己的域名 159 | 160 | #### 2)开启HTTPS 161 | 找到如下图所示的`Enforce HTTPS`选项并勾选。然后访问下自己的域名,不出意外的话,已经是https了。 162 | 163 | ![解析域名](https://i.loli.net/2020/02/26/yjJwiaeBrEOxoYd.jpg) 164 | 165 | 166 | 那么如果如果之前已经开启了自定义域名, enforce HTTPS 无法勾选且怎么办?往下看... 167 | 1. 把 Custom domain 中的值清空,并点击 Save 进行保存; 168 | 2. 在 Custom domain 中的填入之前清空的值,填入后点击保存; 169 | 3. 刷新项目设置页,如果 enforce HTTPS 可勾选,勾选即可; 170 | 4. 如果 enforce HTTPS 不可勾选,并且提示 Not yet available for your site because the certificate has not finished being issued” ,说明证书尚未申请完成,等待一天即可。 171 | 5. 完成以上步骤重新访问自己的域名,享受https吧开始 172 | 173 | 注意,如果使用仍然存在问题,请检查自己的网站引用的资源文件有没有使用了 http 协议,请替换成相应的 https 资源。 174 | -------------------------------------------------------------------------------- /pages/install-deploy.md: -------------------------------------------------------------------------------- 1 | # .Net Core 部署 2 | 3 | - [.Net Core 部署](#net-core-%e9%83%a8%e7%bd%b2) 4 | - [1. Linux(Kestrel+Nginx)](#1-linuxkestrelnginx) 5 | - [2. Windows(Kestrel+IIS)](#2-windowskestreliis) 6 | - [3. Docker](#3-docker) 7 | 8 | .Net Core程序可以部署在Windows/Linux/mac平台上。Mac较多的用于开发,鲜少用做服务器环境。下面我们以Asp.Net Core为例,简单梳理一下。 9 | 10 | .net core程序无论是调试还是发布版本,都**建议在程序目录下运行命令,否则可能会出现静态资源文件无法访问的问题**。 11 | 12 | > 发布命令 `dotnet publish -c Release` 13 | 14 | ## 1. Linux(Kestrel+Nginx) 15 | 在Linux中也可以使用 `dotnet ./your_app.dll` 方式在终端中运行.Net Core程序,但是退出终端后,程序就停止了。我们可以将运行命令封装到一个Linux服务中,服务器启动后就可以在后台静默运行了。 16 | 17 | systemd 可用于创建服务文件以启动和监视基础 Web 应用。 systemd 是一个 init 系统,可以提供用于启动、停止和管理进程的许多强大的功能。 18 | 19 | * 创建服务文件 20 | 21 | ```sh 22 | $ sudo vi /etc/systemd/system/lottery.service 23 | ``` 24 | 25 | * 服务文件示例 26 | 27 | ```sh 28 | [Unit] 29 | # 服务描述 30 | Description=Lottery 31 | 32 | [Service] 33 | # 工作目录,此处为.net core程序目录 34 | WorkingDirectory=/home/colin/apps/content/lottery 35 | # dotnet核心命令 36 | ExecStart=/usr/bin/dotnet /home/colin/apps/content/lottery/Lottery.WebApp.dll 37 | # 重启策略 38 | Restart=always 39 | RestartSec=10 40 | # 日志标识 41 | SyslogIdentifier=dotnet-lottery 42 | # 用户 43 | User=colin 44 | # 环境变量 45 | Environment=ASPNETCORE_ENVIRONMENT=Production 46 | Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false 47 | 48 | [Install] 49 | WantedBy=multi-user.target 50 | ``` 51 | 52 | * 服务管理 53 | 54 | ```sh 55 | # 启用服务 56 | $ sudo systemctl enable lottery.service 57 | 58 | # 启动服务 59 | $ sudo systemctl start lottery.service 60 | 61 | # 查看服务状态 62 | $ sudo systemctl status lottery.service 63 | 64 | # 停止服务 65 | $ sudo systemctl stop lottery.service 66 | 67 | # 重启服务 68 | $ sudo systemctl restart lottery.service 69 | ``` 70 | 71 | 完成以上步骤之后,Asp.Net Core程序已经挂载到了Kestrel服务器上并以Linux服务方式后台静默运行。虽然Kestrel服务器对Asp.Net支持非常好,但微软不建议其作为对外服务器,而是建议使用IIS/Nginx/Apache等作为代理服务器对外开放。 72 | 73 | 关于Linux下Nginx部署,参阅: 74 | 75 | https://ccstudio.org/linux/part2/nginx.html 76 | 77 | https://docs.microsoft.com/zh-cn/aspnet/core/host-and-deploy/linux-nginx?view=aspnetcore-2.2 78 | 79 | Apache配置,参阅: 80 | 81 | https://docs.microsoft.com/zh-cn/aspnet/core/host-and-deploy/linux-apache?view=aspnetcore-2.2 82 | 83 | ## 2. Windows(Kestrel+IIS) 84 | Asp.Net Core应用程序部署要求Windows系统环境为: 85 | * Windows 7 或更高版本 86 | * Windows Server 2008 R2 或更高版本 87 | 88 | 整体部署于传统Asp.Net MVC部署方式相似。使用Kestrel+IIS的进程外承载模型时,需要为IIS安装[`AspNetCoreModule`](https://docs.microsoft.com/zh-cn/aspnet/core/host-and-deploy/aspnet-core-module?view=aspnetcore-2.2),然后将应用程序池的.NET CLR版本设置为无托管代码即可。 89 | 90 | Windows 下.Net Core部署流程参阅: 91 | https://docs.microsoft.com/zh-cn/aspnet/core/host-and-deploy/iis/?view=aspnetcore-2.2 92 | 93 | ## 3. Docker 94 | 95 | .Net Core可以使用Docker技术实现跨平台的容器部署。 96 | * .Net Core应用程序Docker部署参阅[制作网站镜像](docker-dockerfile.md) 97 | * Nginx反代服务器Docker部署参阅https://ccstudio.org/linux/part2/nginx.html。 98 | 99 | 可以参阅[lottery](https://github.com/TechnologyGeeks/lottery)项目的部署过程。 -------------------------------------------------------------------------------- /pages/install-install.md: -------------------------------------------------------------------------------- 1 | # .Net Core 安装卸载 2 | 3 | * [1. 安装](#1-安装) 4 | * [2. 卸载](#2-卸载) 5 | 6 | 作为新一代微软高效跨平台技术,.Net Core自诞生以来就是跨平台的,目前支持Windows/mac OS/Linux平台。 7 | 8 | Linux发行版众多,截止到写这篇文档时,.Net Core 2.2支持的Linux发行版如下: 9 | 10 | * RHEL 11 | * Ubuntu 18.04 12 | * Ubuntu 16.04 13 | * Ubuntu 14.04 14 | * Debian 9 15 | * Debian 8 16 | * Fedora 28 17 | * Fedora 27 18 | * CentOS / Oracle 19 | * openSUSE Leap 20 | * SLES 21 | 22 | ## 1. 安装 23 | .Net Core的安装异常简单。到[官网下载](https://dotnet.microsoft.com/download)安装即可。Windows和Mac中都是下载安装包,双击运行安装,不再赘述。Linux选择对应的发行版本,执行官方的安装命令即可。 24 | 25 | >**提醒** Debian 9 安装.Net Core之前要先安装`apt-transport-https`(官方安装步骤遗漏了此步骤) 26 | 27 | ```sh 28 | $ sudo apt-get install apt-transport-https 29 | ``` 30 | 31 | 32 | 如果想体验最新版的.Net Core的特性,则可以到.Net Core的Github项目中下载。这里有.Net Core所有版本,包括历史版本和预览版本。 33 | https://github.com/dotnet/core/tree/master/release-notes 34 | 35 | .Net Core安装包分为`Runtime`和`SDK`。如果只期望在平台上运行.Net Core程序,安装`Runtime`包即可。如果希望在平台上使用.Net Core的高级功能,如开发调试等,则需要安装`SDK`包。`SDK`包含了`Runtime`。 36 | 37 | ## 2. 卸载 38 | .Net Core在Windows卸载非常简单,直接在控制面板中卸载即可。至于Mac和Linux环境下卸载就比较麻烦了。由于安装文件比较分散,所以删除和清理工作也比较繁琐,幸好.NET Foundation提供了卸载脚本。 39 | 40 | ```sh 41 | #!/usr/bin/env bash 42 | # 43 | # Copyright (c) .NET Foundation and contributors. All rights reserved. 44 | # Licensed under the MIT license. See LICENSE file in the project root for full license information. 45 | # 46 | 47 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 48 | 49 | current_userid=$(id -u) 50 | if [ $current_userid -ne 0 ]; then 51 | echo "$(basename "$0") uninstallation script requires superuser privileges to run" >&2 52 | exit 1 53 | fi 54 | 55 | # this is the common suffix for all the dotnet pkgs 56 | dotnet_pkg_name_suffix="com.microsoft.dotnet" 57 | dotnet_install_root="/usr/local/share/dotnet" 58 | dotnet_path_file="/etc/paths.d/dotnet" 59 | 60 | remove_dotnet_pkgs(){ 61 | installed_pkgs=($(pkgutil --pkgs | grep $dotnet_pkg_name_suffix)) 62 | 63 | for i in "${installed_pkgs[@]}" 64 | do 65 | echo "Removing dotnet component - \"$i\"" >&2 66 | pkgutil --force --forget "$i" 67 | done 68 | } 69 | 70 | remove_dotnet_pkgs 71 | [ "$?" -ne 0 ] && echo "Failed to remove dotnet packages." >&2 && exit 1 72 | 73 | echo "Deleting install root - $dotnet_install_root" >&2 74 | rm -rf "$dotnet_install_root" 75 | rm -f "$dotnet_path_file" 76 | 77 | echo "dotnet packages removal succeeded." >&2 78 | exit 0 79 | ``` 80 | 使用以上脚本卸载即可。 81 | 82 | 如果对shell脚本不熟悉的小伙伴也可以使用以下命令快速卸载,以mac为例, 83 | ```sh 84 | $ curl -o uninstall.sh https://gist.githubusercontent.com/colin-chang/1d8da588f399165924dc62dad42598d8/raw/50444ab4db30ab8d6205216dec0c3983333a5d6b/dotnet-uninstall-pkgs.sh && chmod -R 740 uninstall.sh && sudo sh uninstall.sh && rm uninstall.sh 85 | ``` -------------------------------------------------------------------------------- /pages/log.md: -------------------------------------------------------------------------------- 1 | # 日志管理 2 | 3 | ASP.NET Core 支持适用于各种内置和第三方日志记录提供程序的日志记录 API,并统一了日志操作接口`ILogger`,同时默认提供了基础日志的Provider。 4 | 5 | ## 1. 记录日志 6 | ```csharp 7 | public class HomeController : Controller 8 | { 9 | private ILogger _logger; 10 | public HomeController(ILogger logger) 11 | { 12 | _logger = logger; 13 | } 14 | 15 | public IActionResult Index() 16 | { 17 | _logger.LogDebug("日志记录测试内容"); 18 | return View(); 19 | } 20 | } 21 | ``` 22 | 23 | 在Asp.Net Core服务器构建之前的`CreateDefaultBuilder`中配置了默认的日志服务。我们可以在不做任何配置的情况下直接DI使用默认的日志服务,日志可以在控制台,VS调试窗口和事件查看器中查看到输出入的日志。 24 | 25 | ![默认日志配置](https://i.loli.net/2020/02/26/XKQsZ2i6z7CTI5x.jpg) 26 | 27 | 更详细的日志使用请参见[官方文档](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/logging/?view=aspnetcore-2.2) 28 | 29 | ## 2. 第三方日志组件 30 | Asp.Net Core默认的日志提供程序并没有提供写文件、数据库、邮件等功能,我们可以使用第三方日志提供程序完成,如[Nlog](https://nlog-project.org/)。 31 | 32 | 配置步骤非常简单,按[官方文档](https://github.com/NLog/NLog.Web/wiki/Getting-started-with-ASP.NET-Core-2)进行即可。 33 | 34 | 由于实现了统一的日志接口,替换不同的日志提供程序后,使用日志组件记录日志的代码无需修改,这也体现了面向接口多态编程的好处。 35 | 36 | 除了前面提到的日志组件,在大型分布式应用或微服务中就需要将分布式应用中分散各处的日志进行统一整理归类,这就需要分布式日志管理,如经典的日志组件 [ELK](https://ccstudio.org/distribution/pages/log-elk.html)(跨平台),.Net Core 日志组件 [Exceptionless](https://ccstudio.org/distribution/pages/exceptionless.html)(依赖Windows平台)。 37 | 38 | -------------------------------------------------------------------------------- /pages/mailsms.md: -------------------------------------------------------------------------------- 1 | # 邮件和短信通知 2 | 3 | ## 1. 邮件通知 4 | 5 | ## 2. 短信通知 -------------------------------------------------------------------------------- /pages/microservice-consul.md: -------------------------------------------------------------------------------- 1 | # Consul 服务治理 2 | 3 | * [1. 服务治理简介](#1-服务治理简介) 4 | * [2. Consul 服务安装](#2-consul-服务安装) 5 | * [3. 服务注册、注销、健康检查](#3-服务注册、注销、健康检查) 6 | * [4. 服务发现](#4-服务发现) 7 | * [4.1 服务发现和消费](#41-服务发现和消费) 8 | * [4.2 客户端负载均衡](#42-客户端负载均衡) 9 | * [4.3 RestTemplate](#43-resttemplate) 10 | 11 | ## 1. 服务治理简介 12 | 13 | 服务治理包括,服务注册、注销、健康检查、服务发现等过程。 14 | 15 | 微服务架构中,所有服务都会注册到注册中心,客户端需要消费服务时,需要先到注册中心查询对应服务集群,然后按照一定的负载均衡策略消费服务即可。注册中心除了提供服务注册,服务查询工作外,还会按照一定机制对所有注册的服务进行健康检查,以维护服务的可用性。 16 | 17 | 负载均衡策略在客户端,称为客户端负载均衡。当然也可以设置负载均衡服务器专门负责负载均衡任务。注册中心是在服务器机房环境,其消费者也是服务器机房环境内网的其他服务程序,不会对外网公开,所以这里说的客户端负载均衡中客户端消费程序是指服务器中的某个服务而非真正的用户端,所以这里所说客户端负载均衡也是相对可靠的。 18 | 19 | 注册中心有很多实现,如Consul,Eureka,Zookeeper等。这里我们选择 [Consul](https://www.consul.io/)。 20 | 21 | ## 2. Consul 服务安装 22 | 这里我们直接通过Docker方式安装并部署Consul服务。 23 | ``` sh 24 | # 此中配置仅用于开发。详细配置参见 https://hub.docker.com/_/consul 25 | 26 | $ docker pull consul 27 | $ docker run -d --name=consul-dev -e CONSUL_BIND_INTERFACE=eth0 -p 8500:8500 consul 28 | ``` 29 | 30 | 这里暂且只用一台Consul服务器做演示用,生产环境中为了保证注册中心可用性要做注册中心服务集群,每个集群节点至少有一个(通常会有3到5个)Server,和若干Client组成。 31 | 32 | Consul服务部署完成后直接通过 http://127.0.0.1:8500 即可访问其Web控制台。 33 | 34 | ## 3. 服务注册、注销、健康检查 35 | 连接 Consul 服务器需要借助 [Consul驱动](https://www.nuget.org/packages/Consul/)。 36 | 37 | ```sh 38 | $ dotnet add package Consul 39 | ``` 40 | 41 | 在.NET Core中微服务一般体现为WebAPI项目,可以方便地使用HTTP协议进行服务间通信。 42 | 43 | 生产环境中每个服务一般都会存在一个集群,互为备份,保证系统可用性。WebAPI项目默认启动监听 http://5000, 44 | 单机启动多个服务实例时需要区别端口,我们可以在程序启动时动态指定端口,或者使用docker做端口映射。 45 | 46 | #### 1) 配置文件 47 | 使用默认配置文件 `appsettings.json`,`Build Action`为`Content`,`Copy to output directory`为`Copy always` 48 | 49 | 在`appsettings.json`中添加如下配置。配置内容根据实际环境修改即可。 50 | 51 | ```json 52 | { 53 | "BindHosts": [ 54 | "192.168.31.191" 55 | ], 56 | "ConsulClient": { 57 | "Address": "http://127.0.0.1:8500", 58 | "Datacenter": "dc1" 59 | } 60 | } 61 | ``` 62 | 63 | #### 2) 添加健康检查API 64 | ```csharp 65 | [Route("api/[controller]")] 66 | [ApiController] 67 | public class HealthController : ControllerBase 68 | { 69 | [HttpGet] 70 | public ActionResult Get() 71 | { 72 | return Ok(); 73 | } 74 | } 75 | ``` 76 | 77 | #### 3) 修改启动配置 78 | 79 | ```csharp 80 | public class Program 81 | { 82 | public static void Main(string[] args) 83 | { 84 | /* 85 | * 程序启动时必须指定端口号,命令格式为 dotnet run --port 5000 86 | * 87 | * 通过docker方式运行时要显式指定 ENTRYPOINT 参数。 形如 docker run xxx --port 5000 88 | */ 89 | 90 | var config = new ConfigProvider(args); 91 | 92 | // 端口 93 | var portStr = config["port"]; 94 | if (string.IsNullOrWhiteSpace(portStr)) 95 | throw new ArgumentNullException("port", "Please choose a port for current service"); 96 | if (!int.TryParse(args[1], out var port)) 97 | throw new ArgumentException("porn must be a number"); 98 | if (port < 1024 || port > 65535) 99 | throw new ArgumentOutOfRangeException("port", "Invalid port,it must between 1024 and 65535"); 100 | 101 | // IP 102 | var bindHosts = ConfigProvider.GetAppSettings>("BindHosts"); 103 | var urls = bindHosts.Select(host => $"http://{host}:{port}").ToList(); 104 | 105 | CreateWebHostBuilder(args, urls.ToArray()).Build().Run(); 106 | } 107 | 108 | public static IWebHostBuilder CreateWebHostBuilder(string[] args, string[] urls) => 109 | WebHost.CreateDefaultBuilder(args) 110 | .UseUrls(urls) 111 | .UseStartup(); 112 | ``` 113 | 114 | 115 | 116 | #### 4) 服务注册注销 117 | ```csharp 118 | public async void Configure(IApplicationBuilder app, IHostingEnvironment env, 119 | IApplicationLifetime applicationLifetime) 120 | { 121 | app.UseMvc(); 122 | 123 | await Register2Consul(applicationLifetime); 124 | } 125 | 126 | private async Task Register2Consul(IApplicationLifetime applicationLifetime) 127 | { 128 | var serviceName = Assembly.GetEntryAssembly().GetName().Name; 129 | var serviceId = $"{serviceName}_{Guid.NewGuid()}"; 130 | 131 | //Consul 配置 132 | void ConsulConfig(ConsulClientConfiguration ccc) 133 | { 134 | ccc.Address = new Uri(Configuration["ConsulClient:Address"]); 135 | ccc.Datacenter = Configuration["ConsulClient:Datacenter"]; 136 | } 137 | 138 | //注册服务到Consul 139 | using (var client = new ConsulClient(ConsulConfig)) 140 | { 141 | var hosts = new List(); 142 | Configuration.Bind("BindHosts", hosts); 143 | var ip = hosts.LastOrDefault(); 144 | var port = Convert.ToInt32(Configuration["port"]); 145 | 146 | await client.Agent.ServiceRegister(new AgentServiceRegistration 147 | { 148 | ID = serviceId, //服务编号 149 | Name = serviceName, //服务名称 150 | Address = ip, //服务地址,一般绑定本机内网地址 151 | Port = port, // 服务端口 152 | Check = new AgentServiceCheck 153 | { 154 | DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5), // 服务停止多久后从Consul中注销 155 | Interval = TimeSpan.FromSeconds(10), //健康检查间隔(心跳时间) 156 | HTTP = $"http://{ip}:{port}/api/health", //健康检查地址 157 | Timeout = TimeSpan.FromSeconds(5) //检查超时时间 158 | } 159 | }); 160 | } 161 | 162 | //程序退出时候 从Consul注销服务 163 | applicationLifetime.ApplicationStopped.Register(async () => 164 | { 165 | using (var client = new ConsulClient(ConsulConfig)) 166 | { 167 | await client.Agent.ServiceDeregister(serviceId); 168 | } 169 | }); 170 | } 171 | ``` 172 | 173 | 启动两个服务实例后,在Consul中可以看到这个服务信息。 174 | ```sh 175 | $ dotnet SmsService.dll --port 8000 176 | $ dotnet SmsService.dll --port 8001 177 | ``` 178 | 179 | ![Consul服务注册](https://i.loli.net/2020/02/26/O2Gs3VJXeMmLBNC.jpg) 180 | 181 | 服务刚启动时会有短暂的 Failing 状态。服务正常结束(Ctrl+C)会触发 ApplicationStopped,正常注销。即使非正常结束也没关系,Consul 健康检查过一会发现服务器死掉后也会主动注销。如果服务器刚刚崩溃,但是还买来得及注销,消费的使用者可能就会拿到已经崩溃的实 例,这个问题通过后面讲的重试等策略解决。 182 | 183 | 服务只会注册 ip、端口,consul 只会保存服务名、ip、端口这些信息,至于服务提供什么接口、方法、参数,consul 不管,需要消费者知道服务的这些细节。 184 | 185 | ## 4. 服务发现 186 | 这里用控制台测试,真实项目中服务消费者同时也可能是另外一个 Web 应用(比如 Web 服务器调用短信服务器发短信)。 187 | 188 | ### 4.1 服务发现和消费 189 | ```csharp 190 | using (var consulClient = new ConsulClient(c => c.Address = new Uri("http://127.0.0.1:8500"))) 191 | { 192 | //获取所有注册的服务实例 193 | var services = await consulClient.Agent.Services(); 194 | 195 | //遍历并消费服务 196 | foreach (var service in services.Response.Values) 197 | Console.WriteLine($"id={service.ID},name={service.Service},ip={service.Address},port={service.Port}"); 198 | } 199 | ``` 200 | 201 | ### 4.2 客户端负载均衡 202 | 我们可以按照实际需求自定义负载均衡策略,这里我们使用当前`TickCount`与服务实例数取模的方式达到随机获取一台服务器实例的效果,当然在一个毫秒之类会所有请求都压给一台服务器。也可以自己写随机、轮询等客户端负载均衡算法,也可以自己实现按不同权重分配(注册时候 Tags 带上配置、权重等信息)等算法。 203 | 204 | ```csharp 205 | using (var consulClient = new ConsulClient(c => c.Address = new Uri("http://127.0.0.1:8500"))) 206 | { 207 | //获取所有注册的"人脸识别"服务 208 | var faceRecogonitionServices = consulClient.Agent.Services().Result.Response.Values 209 | .Where(s => s.Service.Equals("Xiaoyang.FaceRecognition", StringComparison.OrdinalIgnoreCase)); 210 | 211 | if (faceRecogonitionServices.Any()) 212 | { 213 | //使用 当前时间毫秒数量%人脸服务实例数 随机获得一个服务实例,实现复杂均衡 214 | var frs = faceRecogonitionServices.ElementAt(Environment.TickCount % faceRecogonitionServices.Count()); 215 | Console.WriteLine($"{frs.Address}:{frs.Port}"); 216 | } 217 | } 218 | ``` 219 | 220 | ### 4.3 ConsulRestHelper 221 | 注册中心可以把形如"http://ProductService/api/Product/" 222 | 的虚拟地址请求按照客户端负载均衡算法解析为形如 223 | http://192.168.1.10:8080/api/Product/ 224 | 的真实地址。虚拟地址转化和请求处理过程都是重复性的操作,我们可以仿照 Spring Cloud自己封装一个[ConsulRestHelper](https://github.com/colin-chang/ConsulRestHelper) 帮助类来处理客户端请求服务的过程。其主要功能包括: 225 | * 服务发现。 根据url中服务名获取一个服务实例,把虚拟路径转换为实际连服务器路径; 服务消费者无需指定服务提供者,实现解耦。 226 | * 负载均衡。这里用的是简单的随机负载均衡, 227 | * 处理客户端请求响应内容。 228 | 229 | ConsulRestHelper 已经发布到[Nuget](https://www.nuget.org/packages/ColinChang.ConsulRestHelper/). 230 | 231 | 232 | 服务的注册、消费都是在系统内部服务器之间的进行,终端用户无法访问到Consul。如Web服务器对终端用户来讲的是否服务端,而其在服务治理中的角色则是作为客户端消费者。 233 | 234 | 使用示例: 235 | ```csharp 236 | using (var httpClient = new HttpClient()) 237 | { 238 | var rest = new ConsulRestHelper(httpClient,"http://127.0.0.1:8500"); 239 | var headers = new HttpRequestMessage().Headers; 240 | headers.Add("Authorization", "Bearer token"); 241 | 242 | var ret1 = await rest.GetForEntityAsync("http://Xiaoyang.FaceRecognition/api/values", 243 | headers); 244 | if (ret1.StatusCode == HttpStatusCode.OK) 245 | Console.WriteLine(string.Join(",", ret1.Body)); 246 | } 247 | ``` -------------------------------------------------------------------------------- /pages/microservice-docker.md: -------------------------------------------------------------------------------- 1 | # 微服务容器化实践 2 | 3 | ## 1. 微服务架构实践 4 | Nginx放在什么位置? 5 | 负载均衡 -> 网关集群 -> 注册中心集群 -> 应用服务集群 -> 数据库服务 6 | Docker容器化后的微服务如何通讯? 7 | 使用SingalR后如何反推消息 -------------------------------------------------------------------------------- /pages/microservice-intro.md: -------------------------------------------------------------------------------- 1 | # 微服务架构 2 | 3 | ## 1. 单体结构 4 | 5 | ![单体结构图](https://i.loli.net/2020/02/26/z6kwoObVxfadJIn.png) 6 | 7 | 缺点: 8 | * 只能采用同一种技术,很难用不同的语言或者语言不同版本开发不同模块; 9 | * 系统耦合性强,一旦其中一个模块有问题,整个系统就瘫痪了;一旦升级其中一个模块,整个系统就停机了; 10 | * 要上线必须一起上线,互相等待,无法快速响应需求; 11 | * 集群只能是复制整个系统,即使只是其中一个模块压力大。 12 | 13 | 随着现在 IT 系统规模的扩大、模块的剧增,传统的系统架构已经难以满足要求,因此近几年微服务架构开始流行。 14 | 15 | ## 2. 微服务 16 | 17 | ![微服务架构图](https://i.loli.net/2020/02/26/IgDY2iVhduT3xWk.jpg) 18 | 19 | 优点: 20 | * 可以用不同的语言或者语言不同版本开发不同模块; 21 | * 系统耦合性弱,其中一个模块有问题,可以通过“降级熔断”等手段来保证系统不雪崩; 22 | * 可以独立上线,能够迅速响应需求; 23 | * 可以对不同模块用不同的集群策略,哪里慢集群哪里。 24 | 25 | 缺点: 26 | * 开发难度大,系统结构更复杂; 27 | * 运行效率低; 28 | 29 | 30 | 不仅是阿里、腾讯等大公司在大面积使用微服务架构,很多中小公司的 IT 系统架构也是微服务架构流行了。但是不是所有项目都适合微服务,没有“银弹”。 31 | 32 | 微服务架构要处理哪些问题:服务间通讯;服务治理与服务发现;网关和安全认证;限流与容错;监控等; 33 | 34 | 第一代微服务:Dubbo(Java)、Orleans(.Net)等;和语言绑定紧密。 35 | 第二代微服务:Spring Cloud 等;适合混合开发,正当年。 36 | 第三代微服务:Service Mesh(Service Fabric、Istio、Conduit 等)。目前还在快速发展中,更新迭代比较快。 37 | 38 | ## 3. 微服务选型 39 | Spring Cloud 是使用 Java 开发的,使用 Java 在 Spring Cloud 下开发最爽,使用[steelto](https://steeltoe.io/) 这个开发包也是可以使用.Net Core 开发 Spring Cloud 下的微服务,但 是开发体验没有 Java 那么效率高,而且支持的软件版本更新没有 Java 那么快,使用 Zuul 等 的时候还可能需要写一些 Java 代码。 40 | 41 | 因此,如果整个项目的技术栈是 Spring Cloud 的,那么.Net Core 开发者可以借助于 steelto “寄居”。如果自己有技术栈的选择权,那么可以自己搭建更亲近.Net Core 的微服务框架。 42 | 43 | Service Fabric 是微软开源的微软内部使用的第三代微服务框架,可以使用.Net Core 开发, 也可以使用 Java 开发。但是目前开源的版本还不太适合普通厂商使用(难度;跨平台;通 用性;支持私有云,但是难度大),慎用。愿意花钱在 Azure 云上用 SF,可以用。但是如果 想搭建 SF 私有云,慎用。 44 | 45 | 如果选择.Net Core 技术栈的第二代微服务框架,推荐使用腾讯(微信支付清算网关)在 使用架构:Consul + Ocelot + .Net Core+Polly + ... 46 | 47 | 在微服务中,服务之间的通讯有两种主要形式: 48 | * Restful,也就是传输 Json 格式数据。.Net 中就是对应 WebAPI 技术,不精通 WebAPI 也没关系,和 ASP.Net MVC 差不多,可以使用 PostMan 方便的调试 Restful 接口。 49 | * 二进制 RPC:二进制传输协议,比 Restful 用的 Http 通讯效率更高,但是耦合性更强。技术有 Thrift、gRPC 等。 50 | -------------------------------------------------------------------------------- /pages/microservice-ocelot.md: -------------------------------------------------------------------------------- 1 | # API 网关 2 | 3 | ## 1. API GateWay 4 | 原则上微服务体系中所有在注册中心注册的服务都属于内部服务,不对用户直接开放,生产环境中这些服务多部署于一个局域网中,不能被外网直接访问到。 5 | 6 | 实际应用中我们常需要开放一些服务给用户,比如用户通过手机端或Web端请求文件服务器以加载资源文件,Web用户端直接请求Web应用服务器等。那对于客户端这些请求,我们不可能直接开放所有的服务,客户端记忆所有的服务地址和端口也非常繁琐,一旦服务端配置发生变化,可能会导致客户端无法正常工作,增强了系统之间耦合度。如果服务需要授权访问或者进行限流收费等,那每个服务都需要提供以上功能也导致重复工作。 7 | 8 | API网关就是为了解决以上这些问题。API网关的角色是作为客户端访问服务的统一入口。所有用户请求都首先经过API网关,然后再转发给具体服务。正是“一夫当关”的位置,也在一定程度上体现了AOP的思想。我们可以在网关中进行统一的认证授权、限流收费等。 9 | 10 | .Net微服务体系中目前比较流行的API网管是Ocelot,Nginx进行定制后也可以作为网关使用。 11 | 12 | * 官网:https://github.com/ThreeMammals/Ocelot 13 | * 资料:http://www.csharpkit.com/apigateway.html 14 | * Ocelot 中文文档:http://www.jessetalk.cn/2018/03/19/net-core-apigateway-ocelot-docs/ 15 | 16 | ![多路网关架构](https://i.loli.net/2020/02/26/CE2UqAMKmQzxg7y.jpg) 17 | 18 | ## 2. Ocelot 基本使用 19 | Ocelot 就是一个提供了请求路由、安全验证等功能的 API 网关微服务。在Asp.Net Core中一般表现为一个WebAPI项目,但是我们不需要MVC功能,所以删除MVC服务和中间件以及Controller。 20 | 21 | ### 2.1 基本使用 22 | 23 | 1) 配置文件 24 | Ocelot使用方式比较简单,基本不需要Coding,只要按照其语法规范定义和修改配置文件即可。通过配置文件可以完成对Ocelot的功能配置:路由、服务聚合、服务发现、认证、鉴权、限流、熔断、缓存、Header头传递等。 25 | 以下是最基本的配置信息,在配置文件中包含两个根节点:ReRoutes和GlobalConfiguration。 26 | 27 | ```json 28 | { 29 | "ReRoutes": [], 30 | "GlobalConfiguration": { 31 | "BaseUrl": "https://api.mybusiness.com" 32 | } 33 | } 34 | ``` 35 | 要特别注意一下BaseUrl是我们外部暴露的Url。 36 | 37 | 2) 配置依赖注入与中间件 38 | 39 | ```csharp 40 | public void ConfigureServices(IServiceCollection services) 41 | { 42 | services.AddOcelot(); 43 | } 44 | public async void Configure(IApplicationBuilder app, IHostingEnvironment env) 45 | { 46 | if (env.IsDevelopment()) 47 | { 48 | app.UseDeveloperExceptionPage(); 49 | } 50 | 51 | await app.UseOcelot(); 52 | } 53 | ``` 54 | 55 | ### 2.1 路由 56 | Ocelot的最基本的功能就是路由,也就是请求转发。路由规则定义在ReRoutes配置节点中,ReRoutes是一个数组,其中的每一个元素代表了一个路由。 57 | 58 | ```json 59 | "ReRoutes": [ 60 | { 61 | "DownstreamPathTemplate": "/api/test/{url}", 62 | "DownstreamScheme": "http", 63 | "DownstreamHostAndPorts": [ 64 | { 65 | "Host": "localhost", 66 | "Port": 8000 67 | } 68 | ], 69 | "UpstreamPathTemplate": "/test/{url}", 70 | "UpstreamHttpMethod": [ 71 | "Get", 72 | "Post" 73 | ] 74 | } 75 | ] 76 | ``` 77 | 以上路由规则会将会对该Ocelot服务器的`/test/{url}`请求转发给`http://localhost:8000/api/test/{url}`。允许`Get`和`Post`请求 78 | 79 | 配置项|含义 80 | :-|:- 81 | Downstream|下游服务配置 82 | UpStream|上游服务配置 83 | Aggregates|服务聚合配置 84 | ServiceName, LoadBalancer, UseServiceDiscovery|配置服务发现 85 | AuthenticationOptions|配置服务认证 86 | RouteClaimsRequirement|配置Claims鉴权 87 | RateLimitOptions|限流配置 88 | FileCacheOptions|缓存配置 89 | QosOptions|服务质量与熔断 90 | DownstreamHeaderTransform|Headers信息转发 91 | 92 | #### 2.1.1 万能模板 93 | 上游Host也是路由用来判断的条件之一,由客户端访问时的Host来进行区别。比如当a.jesetalk.cn/users/{userid}和b.jessetalk.cn/users/{userid}两个请求的时候可以进行区别对待。 94 | ```json 95 | { 96 | "UpstreamPathTemplate": "/", 97 | "UpstreamHttpMethod": [ "Get" ], 98 | "UpstreamHost": "ccstudio.org", 99 | "DownstreamPathTemplate": "/", 100 | "DownstreamScheme": "https", 101 | "DownstreamHostAndPorts": [ 102 | { 103 | "Host": "10.0.10.1", 104 | "Port": 80, 105 | } 106 | ] 107 | } 108 | ``` 109 | 110 | #### 2.1.2 优先级 111 | 对多个产生冲突的路由设置Prioirty。 112 | ```json 113 | { 114 | "UpstreamPathTemplate": "/goods/{catchAll}" 115 | "Priority": 0 116 | } 117 | { 118 | "UpstreamPathTemplate": "/goods/delete" 119 | "Priority": 1 120 | } 121 | ``` 122 | 当请求/goods/delete的时候,则下面那个会生效。也就是说Prority是大的会被优先选择。 123 | 124 | ### 2.2 负载均衡 125 | 当下游服务有多个结点的时候,我们可以在DownstreamHostAndPorts中进行配置。通常也结合Consul来实现负载均衡。 126 | ```json 127 | { 128 | "DownstreamPathTemplate": "/api/posts/{postId}", 129 | "DownstreamScheme": "https", 130 | "DownstreamHostAndPorts": [ 131 | { 132 | "Host": "10.0.1.10", 133 | "Port": 5000, 134 | }, 135 | { 136 | "Host": "10.0.1.11", 137 | "Port": 5000, 138 | } 139 | ], 140 | "UpstreamPathTemplate": "/posts/{postId}", 141 | "LoadBalancer": "LeastConnection", 142 | "UpstreamHttpMethod": [ "Put", "Delete" ] 143 | } 144 | ``` 145 | LoadBalancer将决定负载均衡的算法。 146 | 147 | 负载方式|含义 148 | :-|:- 149 | LeastConnection | 将请求发往最空闲的那个服务器 150 | RoundRobin | 轮流发送 151 | NoLoadBalance | 总是发往第一个请求或者是服务发现 152 | 153 | 154 | 155 | ### 2.3 Work with Consul 156 | 上面的案例中转发规则是硬编码的,我们知道实际下游服务之间访问都是通过注册中心来映射的,Ocelet也可以完美的和Consul一起合作。 157 | 158 | 以下用法基于`Consul v1.4.3`和`Ocelot 13.0.0`,如果更新版本方法不可用,请参与官方文档。 159 | 160 | 添加Consul服务提供程序 161 | ```sh 162 | dotnet add package Ocelot.Provider.Consul 163 | ``` 164 | 165 | 注册Consul服务 166 | ```csharp 167 | public void configureservices(iservicecollection services) 168 | { 169 | services.AddOcelot().AddConsul(); 170 | } 171 | ``` 172 | 173 | 修改配置文件如下 174 | ```json 175 | "ReRoutes": [ 176 | { 177 | "UpstreamPathTemplate": "/test/{url}", 178 | "UpstreamHttpMethod": [ 179 | "Get", 180 | "Post" 181 | ], 182 | "DownstreamPathTemplate": "/api/test/{url}", 183 | "DownstreamScheme": "http", 184 | "ServiceName": "Xiaoyang.TemplateService", 185 | "LoadBalancerOptions": { 186 | "Type": "LeastConnection" 187 | }, 188 | "UseServiceDiscovery": true 189 | } 190 | ], 191 | "GlobalConfiguration": { 192 | "ServiceDiscoveryProvider": { 193 | "Host": "localhost", 194 | "Port": 8500, 195 | "Type": "Consul" 196 | }, 197 | "BaseUrl": "http://localhost:5000" 198 | } 199 | ``` 200 | 201 | 以上路由规则会将对该 Ocelot 服务器的`/test/{url}`请求按照最少连接优先的负载均衡策略转发给下游应用服务群,转发格路径格式为`/api/test/{url}`,服务发现与健康检查工作交由地址为`http://localhost:8500`的Consul注册中心处理。`BaseUrl`为当前Ocelot服务地址。 202 | 203 | ## 3. 其他功能 204 | ### 3.1 限流 205 | 206 | ### 3.2 QOS(熔断器) 207 | 208 | ### 3.3 请求缓存 -------------------------------------------------------------------------------- /pages/multithreading-async.md: -------------------------------------------------------------------------------- 1 | # 异步编程模型 2 | * [1. EAP](#1-eap) 3 | * [2. APM](#2-apm) 4 | * [2.1 简单使用](#21-简单使用) 5 | * [2.2 同步调用](#22-同步调用) 6 | * [2.3 委托异步调用](#23-委托异步调用) 7 | * [3. TPL](#3-tpl) 8 | * [3.1 简单使用](#31-简单使用) 9 | * [3.2 同步调用](#32-同步调用) 10 | * [3.3 并行异步](#33-并行异步) 11 | * [3.4 自定义异步方法](#34-自定义异步方法) 12 | * [3.5 异常处理](#35-异常处理) 13 | 14 | .Net 中很多的类接口设计的时候都考虑了多线程问题,简化了多线程程序的开发。不用自己去写`WaitHandler`等这些底层的代码。随着历史的发展,这些类的接口设计演化经历过三种不同的风格:`EAP`、`APM`和`TPL`。 15 | 16 | ## 1. EAP 17 | `EAP`是`Event-based Asynchronous Pattern`(基于事件的异步模型)的简写。 18 | 19 | ```csharp 20 | // 注:WebClient类在.Net Core中不被支持,推荐使用HttpClient替代 21 | var wc = new WebClient(); 22 | wc.DownloadStringCompleted += (s,e)=>{ 23 | MessageBox.Show(e.Result); 24 | }; 25 | 26 | wc.DownloadStringAsync(new Uri("https://www.baidu.com")); 27 | ``` 28 | 29 | `EAP`特点是一个异步方法配一个`***Completed`事件。使用简单,但业务复杂的时比较麻烦,比如下载 A 成功后再下载 B,如果下载 B 成功再下载 C,否则就下载 D,会出现类似JS的多层回调函数嵌套的问题。 30 | 31 | ## 2. APM 32 | `APM`是`Asynchronous Programming Model`(异步编程模型)的缩写。是.Net 旧版本中广泛使用的异步编程模型。 33 | 34 | `APM`方法名字以 `BeginXXX` 开头,调用结束后需要 `EndXXX`回收资源。 35 | 36 | .Net 中有如下的常用类支持`APM`:`Stream`、`SqlCommand`、`Socket` 等。 37 | 38 | ### 2.1 简单使用 39 | 40 | ```csharp 41 | //异步非阻塞方式 42 | var fs = File.OpenRead("/Users/zhangcheng/test.txt"); 43 | var buffer = new byte[10 * 1024]; 44 | fs.BeginRead(buffer, 0, buffer.Length, ar => 45 | { 46 | using (fs) 47 | { 48 | fs.EndRead(ar); 49 | Console.WriteLine(Encoding.UTF8.GetString(buffer)); 50 | } 51 | }, fs); 52 | ``` 53 | 54 | ### 2.2 同步调用 55 | `APM`方法名字以 `BeginXXX` 开头,返回类型为`IAsyncResult`的对象,该对象有一个`AsyncWaitHandle`属性是用来等待异步任务执行结束的一个同步信号。如果等待`AsyncWaitHandle`则,异步会阻塞并转为同步执行。 56 | 57 | ```csharp 58 | // 同步阻塞方式 59 | using(var fs = File.OpenRead("/Users/zhangcheng/test.txt")) 60 | { 61 | var buffer = new byte[10*1024]; 62 | var aResult = 63 | fs.BeginRead(buffer, 0, buffer.Length, null, null); 64 | aResult.AsyncWaitHandle.WaitOne(); //同步等待任务执行结束 65 | fs.EndRead(aResult); 66 | 67 | Console.WriteLine(Encoding.UTF8.GetString(buffer)); 68 | } 69 | ``` 70 | 71 | ### 2.3 委托异步调用 72 | 旧版.NET中,委托类型具有`Invoke`和`BeginInvoke`两个方法分别用于同步和异步调用委托。其中`BeginInvoke`使用的就是APL风格。 73 | 74 | **通过`BeginInvoke`异步调用委托在.NET Core中不被支持。** 75 | 76 | ```csharp 77 | var addDel = new Func((a, b) => 78 | { 79 | Thread.Sleep(500); //模拟耗时操作 80 | return (a + b).ToString(); 81 | }); 82 | 83 | 84 | //委托同步调用 85 | var res = addDel.Invoke(1, 2); 86 | res = addDel(1, 2); //简化写法 87 | 88 | 89 | //委托异步调用 90 | addDel.BeginInvoke(1, 2, ar => 91 | { 92 | var result = addDel.EndInvoke(ar); 93 | Console.WriteLine(result); 94 | }, addDel); 95 | ``` 96 | 97 | ## 3. TPL 98 | ### 3.1 简单使用 99 | 100 | `TPL`是`Task Parallel Library`(并行任务库存)是.Net 4.0 之后带来的新特性,更简洁,更方便。现在.Net 平台下已经广泛使用。 101 | 102 | ```csharp 103 | static async Task Test() 104 | { 105 | using (var fs = File.OpenRead("/Users/zhangcheng/test.txt")) 106 | { 107 | var buffer = new byte[10 * 1024]; 108 | await fs.ReadAsync(buffer, 0, buffer.Length); 109 | Console.WriteLine(Encoding.UTF8.GetString(buffer)); 110 | } 111 | } 112 | ``` 113 | 114 | * **`TPL`风格运行我们用线性方式编写异步程序。** .NET中目前大多数耗时操作都提供了TPL风格的方法。 115 | * **`TPL`风格编程可以大幅提升系统吞吐量**,B/S程序效果更为显著,可以使用异步编程的地方尽量不要使用同步。 116 | * `await`会确保异步结果返回后再执行后续代码,不会阻塞主线程。 117 | * `TPL`风格方法都习惯以 `Async`结尾。 118 | * 使用`await`关键字方法必须使用`async`修饰 119 | * 接口中声明方法时不能使用`async`关键字,在其实现类中可以。 120 | 121 | ###### `TPL`风格方法允许以下三种类型的返回值: 122 | * `Task`。异步Task做返回类型,相当于无返回值。方法被调用时支持`await`等待。 123 | * `Task`<T>。`T`为异步方法内部实际返回类型。 124 | * `void`。使用`void`做返回类型的异步方法,被调用时不支持`await`等待。 125 | 126 | 127 | ### 3.2 同步调用 128 | 129 | 返回`Task`或`Task`<T>的`TPL`方法可以同步调用。调用`Task`对象的`Wait()`方法会同步阻塞线程直到任务执行完成,然后可以通过其`Result`属性拿到最终执行结果。 130 | 131 | 在同步方法中不使用`await`而直接使用`Task`对象的`Result`属性也会导致等待阻塞。 132 | 133 | ```csharp 134 | Task task = TestAsync(); 135 | task.Wait(); //同步等待 136 | Console.Writeline(task.Result); //拿到执行结果 137 | ``` 138 | 139 | **使用APL风格编程,一定要全程使用异步,中间任何环节使用同步,不仅不会提升程序性能,而且容易造成死锁。** 140 | 141 | ### 3.3 并行异步 142 | 143 | 如果存在多个相互无关联的异步任务,使用`await`语法会让多个任务顺序执行,如果想实现并发执行,我们可以使用`Task.WhenAll()`方式。 144 | 145 | ```csharp 146 | static async Task GetWeatherAsync() 147 | { 148 | using (var hc = new HttpClient()) 149 | { 150 | //三个顺序执行 151 | Console.WriteLine(await hc.GetStringAsync("https://baidu.com/getweather")); 152 | Console.WriteLine(await hc.GetStringAsync("https://google.com/getweather")); 153 | Console.WriteLine(await hc.GetStringAsync("https://bing.com/getweather")); 154 | } 155 | } 156 | ``` 157 | 使用`Task.WhenAll()`改造后如下: 158 | ``` csharp 159 | static async Task GetWeatherAsync() 160 | { 161 | using (var hc = new HttpClient()) 162 | { 163 | var task1 = hc.GetStringAsync("https://baidu.com/getweather"); 164 | var task2 = hc.GetStringAsync("https://google.com/getweather"); 165 | var task3 = hc.GetStringAsync("https://bing.com/getweather"); 166 | 167 | // 三个任务并行执行 168 | var results = await Task.WhenAll(task1, task2, task3); 169 | foreach (var result in results) 170 | Console.WriteLine(result); 171 | } 172 | } 173 | ``` 174 | 175 | ### 3.4 自定义异步方法 176 | 177 | ```csharp 178 | Task DoAsync() 179 | { 180 | return Task.Run(() => 181 | { 182 | // do something 183 | }); 184 | } 185 | 186 | Task DoAsync() 187 | { 188 | return Task.Run(() => 189 | { 190 | //do something 191 | return "Hello"; 192 | }); 193 | } 194 | 195 | Task GetDate() 196 | { 197 | // 从简单对象Task 可以使用 Task.FromResult() 198 | return Task.FromResult(DateTime.Today); 199 | } 200 | ``` 201 | 202 | ### 3.5 异常处理 203 | **TPL风格编程中,有些情况下程序出现异常而不会抛出,也不会导致程序异常退出,此时会导致一些莫名的错误**。但是显式的使用`try...catch`可以捕获到这些异常,这就要求开发者在代码编写过程中谨慎权衡,在可能出现的异常的地方进行手动异常处理。 204 | 205 | TPL编程有时会抛出`AggregateException`,这通常发生在并行有多个任务执行的情况下,如上面[并行异步](#whenall)案例的情况。多个并行任务可能有多个异常, 因此`AggregateException`是一个聚合型异常类型,通过其`InnerExceptions` 属性可以获得多个异常对象信息,逐个解析即可。 -------------------------------------------------------------------------------- /pages/multithreading-basic.md: -------------------------------------------------------------------------------- 1 | # 进程、线程基础 2 | 3 | # 1. 进程管理 4 | .NET中使用`Process`类管理维护进程信息。`Process`常用成员如下。 5 | 6 | 成员|含义 7 | :-|:- 8 | `Threads`|获取当前进程的所有线程 9 | `Kill()`|杀掉指定进程 10 | `Process.GetCurrentProcess()`|拿到当前程序进程 11 | `Process.GetProcesses()`|拿到系统当前所有进程 12 | `Process.GetProcessById()`|拿到指定Id的进程 13 | `Process.Start()`|启动一个进程。 14 | 15 | ```csharp 16 | // 启动IE浏览器并访问百度 17 | Process.Start("iexplore","https://www.baidu.com"); 18 | ``` 19 | 20 | # 2. 线程基础 21 | * 多线程可以让一个程序“同时”处理多个事情。后台运行程序,提高程序的运行效率,同时解决耗时操作时GUI出现无响应的情况。 22 | * 一个进程的多个线程之间可以共享程序代码。每个线程会将共享的代码分别拷贝一份去执行,每个线程是单独执行的。 23 | * 线程有前台线程和后台线程,创建一个线程默认为前台线程。 24 | * 只有所有的前台线程都关闭时程序才能退出。只要所有前台线程都关闭后台线程自动关闭。 25 | * 线程被释放时,线程中定义的内容都会自动被释放 26 | 27 | .NET中使用`Thread`类管理维护线程信息。`Thread`常用成员如下。 28 | 29 | 成员|含义 30 | :-|:- 31 | `Name`|线程名 32 | `IsBackground`|获取或设置是否是后台线程 33 | `IsAlive`|表示当前线程的执行状态 34 | `ManagedThreadId`|获取当前托管线程的唯一标示符Id 35 | `Priority`|获取或设置线程的优先级,只是推荐给OS,并不一定执行 36 | `Start()`|启动线程 37 | `Interrupt()`|用于提前唤醒一个在Sleep的线程 38 | `Abort()`|强制终止线程 39 | `Join()`|等待指定线程执行完毕后再接着执行当前线程 40 | `Thread.CurrentThread`|获得当前的线程引用 41 | `Thread.Sleep()`|让当前线程休眠。只能当前线程自身主动休眠,不能被其他线程控制。 42 | 43 | 44 | * `Abort()`方法会引发线程内当前在执行的代码抛出`ThreadAbortException`,可能会造成线程占用资源无法释放,一般情况下不推荐使用。可以通过结束线程执行的方法来结束并释放线程。 45 | * `Interrupt()`唤醒`Sleep`的线程时`Sleep`方法会抛出 `ThreadInterruptedException`,需要我们`catch`异常,否则异常会导致程序崩溃退出。 46 | 47 | ```csharp 48 | var t1 = new Thread(() => 49 | { 50 | try 51 | { 52 | Thread.Sleep(5000); 53 | } 54 | catch (ThreadInterruptedException) 55 | { 56 | Console.WriteLine("t1线程被意外唤醒"); 57 | } 58 | 59 | Console.WriteLine("Fuck"); 60 | }) {IsBackground = true}; 61 | 62 | t1.Start(); 63 | t1.Interrupt(); 64 | ``` -------------------------------------------------------------------------------- /pages/multithreading-intro.md: -------------------------------------------------------------------------------- 1 | # 进程、线程和应用程序域 2 | 3 | ## 1. 进程 4 | 进程(Process)是操作系统中的一个基本概念,它包含着一个运行程序所需要的资源。进程之间是相对独立的,一个进程无法直接访问另 一个进程的数据(除非利用分布式计算方式),一个进程运行的失败也不会影响其他进程的运行,操作系统就是利用进程把工作划分为多个独立的区域的。进程可以理解为一个程序的基本边界。 5 | 6 | 操作系统分配资源的最小单位是进程,进程之间是相互隔离的,即每个进程有属于自己的数据段、程序段、进程控制块。 7 | 8 | ## 2. 线程 9 | 线程(Thread)是任务调度的最小单位。一个线程是一个进程里面的代码执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的。 10 | 11 | ## 3. 应用程序域 12 | 应用程序域(App Domain)提供安全而通用的处理单元,公共语言运行库可使用它来提供应用程序之间的隔离。我们可以单个进程中运行几个应用程序域,而不会造成进程间调用或进程间切换等方面的额外开销。在一个进程内运行多个应用程序的能力显著增强了服务器的可伸缩性。 13 | 14 | 应用程序域允许我们在一个应用程序中出现的错误不会影响其他应用程序。能够在不停止整个进程的情况下停止单个应用程序。应用程序域形成了托管代码的隔离、卸载和安全边界。 15 | 16 | ![进程线程和应用程序域的关系](https://i.loli.net/2020/02/26/OPNaQuS3lg7GKWe.jpg) -------------------------------------------------------------------------------- /pages/multithreading-synchronization.md: -------------------------------------------------------------------------------- 1 | # 线程同步 2 | 3 | * [1. 线程同步](#1-线程同步) 4 | * [1.1 Join](#11-join) 5 | * [1.2 MethodImplAttribute](#12-methodimplattribute) 6 | * [1.3 对象互斥锁](#13-对象互斥锁) 7 | * [1.4 多线程版单例模式](#14-多线程版单例模式) 8 | * [2. 生产者消费者模式](#2-生产者消费者模式) 9 | * [3. WaitHandle](#3-waithandle) 10 | * [3.1 ManualResetEvent](#31-manualresetevent) 11 | * [3.2 AutoResetEvent](#32-autoresetevent) 12 | 13 | ## 1. 线程同步 14 | 当一个方法同时被多个线程调用并修改同一变量时就可能存在脏数据的问题,我们称之为“多线程方法重入”。我们可以通过以下方式来解决此问题。 15 | 16 | ### 1.1 Join 17 | 18 | `Join()`方法可以让当前线程等待指定线程执行结束后再**接着**运行当前线程。 19 | 20 | ```csharp 21 | var t1 = new Thread(() => 22 | { 23 | for (int i = 0; i < 20; i++) 24 | { 25 | Console.WriteLine("t1 " + i); 26 | } 27 | }); 28 | 29 | var t2 = new Thread(() => 30 | { 31 | t1.Join(); //等着 t1 执行结束后接着执行以下代码 32 | 33 | for (int i = 0; i < 20; i++) 34 | { 35 | Console.WriteLine("t2 " + i); 36 | } 37 | }); 38 | 39 | t1.Start(); 40 | t2.Start(); 41 | ``` 42 | 43 | ### 1.2 MethodImplAttribute 44 | 在线程不安全的方法上打上`[MethodImpl(MethodImplOptions.Synchronized)]`标记后,此方法同时只能被一个线程调用,变成了同步方法。 45 | 46 | ```csharp 47 | [MethodImpl(MethodImplOptions.Synchronized)] 48 | public void Count() 49 | { 50 | // do something ... 51 | } 52 | ``` 53 | 54 | ### 1.3 对象互斥锁 55 | ```csharp 56 | var locker = new object(); 57 | public void Count() 58 | { 59 | lock (locker) 60 | { 61 | // do something ... 62 | } 63 | } 64 | ``` 65 | 同一时刻只能有一个线程进入同一个对象的 lock 代码块。必须是同一个对象才能起到 互斥的作用。lock 后必须是引用类型,不一定是 object,只要是对象就行。 66 | 67 | 锁对象选择很重要,选不对起不到同步的作用和可能会造成其他地方被锁,比如用字符串做锁(因为字符串拘留池导致可能用的是其他地方也在用的锁)。 68 | 69 | *lock是对`Monitor`类的简化调用,此处我们就不在讲Monitor的相关使用了。* 70 | 71 | ### 1.4 多线程版单例模式 72 | ```csharp 73 | class God 74 | { 75 | private static God _instance = null; 76 | private static readonly object Locker = new object(); 77 | 78 | private God(){} 79 | 80 | public static God GetInstance() 81 | { 82 | if (_instance == null) 83 | { 84 | lock (Locker) 85 | { 86 | if (_instance == null) 87 | _instance = new God(); 88 | } 89 | } 90 | 91 | return _instance; 92 | } 93 | } 94 | ``` 95 | 以上方式保证线程安全,但是书写较为繁琐,日常开发中推荐使用静态单例方式。 96 | ```csharp 97 | class God 98 | { 99 | private God(){} 100 | 101 | private static readonly God Instance = new God(); 102 | public static God GetInstance() => Instance; 103 | } 104 | ``` 105 | 106 | ## 2. 生产者消费者模式 107 | 多个线程同时修改共享数据可能会发生错误,此时我们常用生产者消费者模式来处理此问题。 108 | 109 | 在生成者和消费者关系中,生产者线程负责产生数据,并把数据存到公共数据区,消费者线程使用数据,从公共数据去中取出数据。我们使用资源加锁的方式来解决线程并发引起的方法重入问题。 110 | 111 | ```csharp 112 | class Program 113 | { 114 | static void Main(string[] args) 115 | { 116 | List list = new List();//创建产品池 117 | //创建5个生产者 118 | for (int i = 0; i < 5; i++) 119 | { 120 | new Thread(() => 121 | { 122 | while (true) 123 | lock (list)//锁定对象解决线程并发引起的方法重入问题 124 | { 125 | //生产一个产品 126 | list.Add(new Product()); 127 | Console.WriteLine("生产产品{0}", list.Count - 1); 128 | Thread.Sleep(500); 129 | } 130 | }) { IsBackground = true }.Start(); 131 | } 132 | 133 | //创建10个消费者 134 | for (int i = 0; i < 10; i++) 135 | { 136 | new Thread(() => 137 | { 138 | while (true) 139 | lock (list) 140 | { 141 | if (list.Count > 0) 142 | { 143 | //消费一个产品 144 | list.RemoveAt(list.Count - 1); 145 | Console.WriteLine("消费产品{0}", list.Count); 146 | Thread.Sleep(200); 147 | } 148 | } 149 | }) { IsBackground = true }.Start(); 150 | } 151 | Console.ReadKey(); 152 | } 153 | } 154 | class Product {} 155 | ``` 156 | 157 | ## 3. WaitHandle 158 | 除了前面提到的“锁”机制外,.NET中WaitHandle还提供了一些线程间协同的方法,使得线程可以通过“信号”进行通讯。 159 | 160 | WaitHandle是一个抽象类,`EventWaitHandle`是其实现类,我们常用`EventWaitHandle`两个子类`ManualResetEvent`和`AutoResetEvent`。 161 | 162 | 信号通讯在`EventWaitHandle`中被通俗的比喻为“门”,主要体现为以下三个方法: 163 | 164 | ```csharp 165 | Set(); // 开门 166 | WaitOne(); // 等待开门 167 | Reset(); // 关门 168 | ``` 169 | 170 | 等待开门除了`WaitOne()`之外还有以下用法。 171 | ```csharp 172 | //等待所有信号都变为“开门状态” 173 | WaitHandle.WaitAll(WaitHandle[] waitHandles); 174 | 175 | //等待任意一个信号变为“开门状态” 176 | WaitHandle.WaitAny(WaitHandle[] waitHandles); 177 | ``` 178 | 179 | ### 3.1 ManualResetEvent 180 | `ManualResetEvent`被比喻为手动门,一旦开门后就保持开门状态,除非手动关门,如同“城门”。 181 | 182 | ```csharp 183 | var mre = new ManualResetEvent(false); //创建"手动门",默认状态为"关门" 184 | new Thread(() => 185 | { 186 | mre.WaitOne(); //等待开门。开门之后后续代码方可执行,否则该线程一直阻塞在此处 187 | Console.WriteLine("开门了..."); 188 | 189 | while (true) 190 | { 191 | Console.WriteLine(DateTime.Now); 192 | Thread.Sleep(1000); 193 | } 194 | }){IsBackground = true}.Start(); 195 | 196 | Console.WriteLine("按任意键开门..."); 197 | Console.ReadKey(); 198 | 199 | mre.Set(); //开门 200 | 201 | Thread.Sleep(5000); 202 | mre.Reset(); //关门 203 | Console.WriteLine("关门了..."); 204 | ``` 205 | 206 | `WaitOne(5000); //最长等待5s`。 207 | 208 | ### 3.2 AutoResetEvent 209 | `AutoResetEvent`被比喻为自动门,一次开门完成后自动关门,如同“地铁的闸机口”。 210 | 211 | ```csharp 212 | var are = new AutoResetEvent(false); //创建"手动门",默认状态为"关门" 213 | new Thread(() => 214 | { 215 | are.WaitOne(); //等待开门。开门之后后续代码方可执行,否则该线程一直阻塞在此处 216 | Console.WriteLine("开门了..."); 217 | 218 | //do something ... 219 | }){IsBackground = true}.Start(); 220 | 221 | Console.WriteLine("按任意键开门..."); 222 | Console.ReadKey(); 223 | 224 | are.Set(); //开门 225 | ``` 226 | 227 | WaitHandle现在.NET中较少使用了,但它们更多作为简单易用的多线程语法的底层实现。 -------------------------------------------------------------------------------- /pages/multithreading-threadpool.md: -------------------------------------------------------------------------------- 1 | # 线程池 2 | ## 1 线程池简介 3 | * 系统中创建一个线程就会开辟一个至少 1M 的内存空间 4 | * 线程还可能会占用部分寄存器 5 | * 线程非常多的时候,OS需要花费大量的时间在不同的线程之间进行切换。 6 | 7 | 我们可以通过线程池对以上问题进行优化。线程池是一组已经创建好的线程,随用随取,用完了不是销毁线程,然后放到线程池中,供其他人用。当需要创建大量线程时,我们推荐使用线程池技术。 8 | 9 | 系统同时处理的线程的个数与系统的硬件资源有关,线程数量与系统运行效率大概呈正态分布。在达到最高值之后,线程数量再增加 OS 将花费大量的时间和资源来切换线程,执行效率反而会下降。 10 | 11 | ## 2. 线程池特点 12 | ### 2.1 线程池特点 13 | 14 | * 线程池线程本身默认都是后台线程,不需要手动启动 15 | * 线程池中的线程可以进行重用,线程使用完成后不会马上释放而是进入线程池等待重用 16 | * 当程序中需要创建大量线程执行小数据量操作时,线程池可以大幅调高线程执行效率。 17 | * 使用线程池操作线程的灵活性较差,我们无法获取线程池中的线程信息,所以无法干预线程池中的线程 18 | * 虽然工作项进入线程池队列的时候保证了先进先出,但是各个工作线程获取工作项放到本地的队列后是使用的先进后出的方式,所以不能保证整体的请求项之间是请求处理的顺序。 19 | * 线程池有最大线程数,最小线程数和默认线程数。ThreadPool.GetMaxThreads()获取线程池的最大线程数和当前线程池大小,线程池大小会根据CPU自动计算获得,不推荐手动修改。ThreadPool.GetMinThreads(),获取线程池最小线程数 20 | * 线程池提高了线程的利用率,非常**适合工作任务非常小,而且又需要使用单独的线程来解决的问题**。 21 | 22 | ### 2.2 手动创建线程与线程池对比 23 | 24 | * 能用线程池的就用线程池,但线程池处理顺序不确定 25 | * 线程池的优势在于线程执行大量小运算 26 | * 要手动干预线程的话必须手动创建线程 27 | * 要设置线程的优先级时,必须手动创建线程 28 | * 线程执行时间较长是,两种方式差异不大 29 | 30 | ### 2.3 使用方式 31 | ```csharp 32 | //有参 33 | ThreadPool.QueueUserWorkItem((s) => Console.WriteLine(s),"Hello"); 34 | 35 | //参数 36 | ThreadPool.QueueUserWorkItem(s => Console.WriteLine("Hello")); 37 | ``` -------------------------------------------------------------------------------- /pages/multithreading-uiresource.md: -------------------------------------------------------------------------------- 1 | # UI资源跨线程调用 2 | 3 | 在`WinForm`或`WPF`程序中,默认只允许在创建控件的线程(一般为UI线程)中访问控件,如果想在其他线程中访问UI资源,需要做特殊处理。 4 | 5 | ## 1. WPF 6 | `Window`类有一个`Dispatcher`对象,该对象是一个队列,用来保存应用程序主线程需要执行的任务。其他线程需要访问UI资源时只需要将操作加入到`Dispatcher`中,然后由主线程负责代为执行。 7 | 8 | ```csharp 9 | private void Button_Click(object sender, RoutedEventArgs e) 10 | { 11 | new Thread(() => ChangeText()).Start(); 12 | } 13 | 14 | private void ChangeText() 15 | { 16 | Random rdm = new Random(); 17 | string num = rdm.Next().ToString(); 18 | 19 | //当前线程不是主线程 20 | if (Dispatcher.Thread != Thread.CurrentThread) 21 | { 22 | Dispatcher.Invoke(new Action(s => txt.Text = s), num); 23 | } 24 | //当前线程是主线程 25 | else 26 | txt.Text = num; 27 | } 28 | ``` 29 | 30 | ## 2. WinForm 31 | `WinForm`当中,我们有两种方式来解决UI资源跨线程访问的问题。 32 | 33 | 在`Form`构造函数中设置`CheckForIllegalCrossThreadCalls = false`,禁止窗体进行非法跨线程调用的校验,这只是屏蔽了非法校验,并没有真正解决问题,不推荐使用。 34 | 35 | 推荐使用以下方式: 36 | 37 | ```csharp 38 | private void button1_Click(object sender, EventArgs e) 39 | { 40 | new Thread(() => ChangeText()).Start(); 41 | } 42 | private void ChangeText() 43 | { 44 | Random rdm = new Random(); 45 | string num = rdm.Next().ToString(); 46 | //当前线程是创建此控件的线程 47 | if (txt.InvokeRequired) 48 | txt.Invoke(new Action(s => txt.Text = s), num); 49 | //当前线程不是创建此控件的线程 50 | else 51 | txt.Text = num; 52 | } 53 | ``` -------------------------------------------------------------------------------- /pages/pipeline-diagram.md: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /pages/pipeline-environment.md: -------------------------------------------------------------------------------- 1 | # ASP.NET Core 环境变量 2 | 3 | * [1. 环境变量简介](#1-环境变量简介) 4 | * [2. 设置环境变量](#2-设置环境变量) 5 | * [2.1 UseEnvironment](#21-useenvironment) 6 | * [2.2 launchSettings.json](#22-launchsettingsjson) 7 | * [3. 使用环境变量](#3-使用环境变量) 8 | * [3.1 前端](#31-前端) 9 | * [3.2 后端](#32-后端) 10 | 11 | ## 1. 环境变量简介 12 | Asp.Net Core的环境变量和启动设置,将开发过程中的调试和测试变的更加简单。我们只需要简单的修改配置文件,就可以实现开发、预演、生产环境的切换。 13 | 14 | ASP.NET Core控制环境切换依赖于“ASPNETCORE_ENVIRONMENT”环境变量。此环境变量框架默认提供了三个值,当然我们也可以定义其它的值: 15 | ``` 16 | Development(开发) 17 | Staging(预演) 18 | Production(生产) 19 | ``` 20 | 21 | ## 2. 设置环境变量 22 | ### 2.1 UseEnvironment 23 | 我们可以使用`IWebHostBuilder`的`UseEnvironment`方法来设定`ASPNETCORE_ENVIRONMENT`变量值。 24 | 25 | ```csharp 26 | WebHost.CreateDefaultBuilder(args) 27 | .UseEnvironment(EnvironmentName.Production) //设置ASPNETCORE_ENVIRONMENT 28 | .UseStartup(); 29 | ``` 30 | 31 | ### 2.2 launchSettings.json 32 | 33 | ![launchSettings.json文件路径](https://i.loli.net/2020/02/26/DSkJVUdcaBZPn8Y.jpg) 34 | 35 | ASP.Net Core包含一个launchSettings.json的文件,此文件是项目启动配置文件。我们可在其中配置`ASPNETCORE_ENVIRONMENT`环境变量。 36 | 37 | ![launchSettings.json文件路径](https://i.loli.net/2020/02/26/z6bPH487DXiqVya.jpg) 38 | 39 | ## 3. 使用环境变量 40 | 41 | 环境变量检测在WebHost构建过程中注册为了`IHostingEnvironment`服务, 该类型的变量表示的是当前应用程序运行的环境,ASP.Net Core提供了四个扩展方法,用于检测 “ASPNETCORE_ENVIRONMENT”当前的值。 42 | 43 | ```csharp 44 | IsDevelopment() //是否为开发环境 45 | IsStaging() //是否为预演环境 46 | IsProduction() //是否为生产环境 47 | IsEnvironment(string environmentname) //是否为某种环境 48 | ``` 49 | 50 | #### 3.1 前端 51 | 在Asp.Net Core视图中我们可以通过`environment`标签来使用环境变量。 52 | 53 | ```html 54 | 55 | 56 | 57 | 58 | 59 | 60 | ``` 61 | 62 | #### 3.2 后端 63 | 64 | 我们可以在任何需要根据环境控制应用程序行为的地方注入并使用`IHostingEnvironment`。比如默认的`Startup`的`Config`方法中根据是否为开发环境显示不同的错误展示方式。 65 | 66 | ```csharp 67 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 68 | { 69 | if (env.IsDevelopment()) 70 | { 71 | app.UseDeveloperExceptionPage(); 72 | } 73 | else 74 | { 75 | app.UseExceptionHandler("/Home/Error"); 76 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 77 | app.UseHsts(); 78 | } 79 | 80 | // do something else ... 81 | } 82 | ``` 83 | 84 | > IHostingEnvironment 常用成员 85 | 86 | 除了读取环境变量,IHostingEnvironment还有以下常用成员。 87 | 88 | 属性|含义 89 | :-|:- 90 | `ApplicationName`|当前程序名称 91 | `ContentRootPath`|网站根目录(绝对路径) 92 | `WebRootPath`|网站`wwwroot`目录(绝对路径) 93 | 94 | 95 | 96 | 97 | 参考文档:https://www.cnblogs.com/tdfblog/p/Environments-LaunchSettings-in-Asp-Net-Core.html -------------------------------------------------------------------------------- /pages/pipeline-lifetime.md: -------------------------------------------------------------------------------- 1 | # 生命周期 2 | 3 | 在[管道模型](pipeline-diagram.md)中我们了解到了Asp.Net Core如何处理一个Http请求的过程及其管道构建过程。这一节我们将对声明周期中的诸多细节做一些简单讲解和补充。 4 | 5 | ## 1.IApplicationLifetime 6 | 在传统Asp.Net MVC中我们可以在Global的Application_Start等管道事件中做某些业务处理,Asp.Net Core的[管道模型](pipeline-diagram.md)已经发生了变化,但`IApplicationLifetime`服务允许我们响应`ApplicationStarted`,`ApplicationStopping`,`ApplicationStopped`三个事件。 7 | 8 | ```csharp 9 | public void Configure(IApplicationBuilder app, IHostingEnvironment env,IApplicationLifetime lifetime) 10 | { 11 | lifetime.ApplicationStarted.Register(() => 12 | { 13 | Console.WriteLine("程序启动完成"); 14 | }); 15 | 16 | lifetime.ApplicationStopped.Register(() => 17 | { 18 | Console.WriteLine("程序已停止"); 19 | }); 20 | 21 | //do something else ... 22 | } 23 | ``` -------------------------------------------------------------------------------- /pages/pipeline-middlewire.md: -------------------------------------------------------------------------------- 1 | # 中间件 2 | 3 | 待整理... 4 | 5 | ## 1. 简介 6 | 7 | ## 2. 工作原理与过程 8 | 9 | ## 3. 常用Middlewire 10 | 11 | ## 4. 自定义Middlewire -------------------------------------------------------------------------------- /pages/rpc.md: -------------------------------------------------------------------------------- 1 | # RPC 2 | 3 | * [1. RPC](#1-rpc) 4 | * [2. Thrift](#2-thrift) 5 | * [2.1 简介](#21-简介) 6 | * [2.2 工作原理](#22-工作原理) 7 | * [2.3 .Net 使用](#23-net-使用) 8 | * [3. 案例源码](#3-案例源码) 9 | 10 | ## 1. RPC 11 | RPC(Remote Procedure Call Protocol) - 远程过程调用协议,是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。 12 | 13 | 单体程序中可以直接调用自身服务方法,但在分布式系统当中,如何实现服务调用呢,RPC技术简单说就是为了解决远程调用服务的一种技术,使得调用者像调用本地服务一样方便透明。 14 | 15 | ![RPC示意图](https://i.loli.net/2020/02/26/naHUR5j2Lgh4A9T.png) 16 | 17 | RPC可以采用HTTP协议,并且使用最为广泛,优点是开放、标准、简单、兼容性升级容易,缺点是性能略低。在 QPS 较高或者对响应时间要求苛刻的情况下,常采用二进制传输、如TCP通讯,更加高效也更加安全。也有部分公司采用私有协议,如腾讯的JCE协议。RPC 虽然效率略高,但是耦合性强,如果兼容性处理不好的话,一旦服务器端接口升级,客户端就要同步更新,没有HTTP灵活。 18 | 19 | RPC在各大互联网公司中被广泛使用,如阿里巴巴的hsf、dubbo(开源)、Facebook的thrift(开源)、Google gRPC(开源)、Twitter的finagle(开源)等。 20 | 21 | .Net Core中常用的RPC框架有 [gRPC](https://grpc.io/)、[Thrift](http://thrift.apache.org/) 等, 22 | gRPC、Thrift 等都支持主流的编程语言。 23 | 24 | 性能方面:Thirft(大约 10 倍) > gRPC > Http。数据涞源于互联网,性能和业务数据的特点有关,仅供参考。
25 | 最佳实践:对内一些性能要求高的场合用 RPC,对内其他场合以及对外采用HTTP。 26 | 27 | ## 2. Thrift 28 | ### 2.1 简介 29 | Thrift是一种接口描述语言和二进制通讯协议,是由Facebook为“大规模跨语言服务开发”而开发的。它通过一个代码生成引擎联合了一个软件栈,来创建不同程度的、无缝的跨平台高效服务,可以使用C#、C++(基于POSIX兼容系统)、Cappuccino、Cocoa、Delphi、Erlang、Go、Haskell、Java、Node.js、OCaml、Perl、PHP、Python、Ruby和Smalltalk。现在是Apache软件基金会的开源项目。 30 | 31 | ### 2.2 工作原理 32 | Thrif通过模版文件定义服务接口规范,模版文件可以生成不同语言平台的代码文件,服务端和客户端分别引用生成的接口文件,服务端实现接口内容,并Host服务公开通讯地址,客户端连接服务端地址,并按照接口规范调用服务即可。 33 | 34 | 由于Thrif需要兼容大多数主流语言平台,所以接口文档不能采用所有语言各自书写一遍,于是Thrif推出 [IDL](http://thrift.apache.org/docs/idl)(interface definition language)来书写接口文档。IDL仅用于定义接口,包含常用的数据类型,语法比价简单,具体语法使用可以参阅 https://www.cnblogs.com/valor-xh/p/6386584.html 35 | 36 | ### 2.3 .Net 使用 37 | 下面我们以.Net Core为例简单演示一下Thrif的使用。Thrif 官方也提供了常用开发语言的案例,参见 https://github.com/colin-chang/thrift-official 38 | 39 | 40 | #### 1) 定义模板 41 | 创建模板文件 UserService.thrift。模板文件扩展名通常为`.thrift` 42 | ``` 43 | namespace csharp ThriftDemo.Contract.Net 44 | namespace netcore ThriftDemo.Contract.Core 45 | 46 | service UserService 47 | { 48 | bool Create(1: Person person) 49 | bool Delete(1: i32 id) 50 | void Update(1: Person person) 51 | Person Query(1: i32 id) 52 | } 53 | 54 | 55 | struct Person 56 | { 57 | 1: i32 id 58 | 2: string name 59 | 3: i32 age 60 | } 61 | ``` 62 | 63 | #### 2) 生成接口文档 64 | Thrift提供了不同平台的模板生成程序Windows下是一个可执行程序使用比较简单,但在mac OS和Linux下使用生成器则需要安装复杂的运行环境,但是Docker让一切变得无比简单,这里给出Docker生成器使用命令。 65 | 66 | ```sh 67 | # .NET sync 68 | docker run --rm -v "$PWD:/data" thrift thrift -o /data --gen csharp /data/UserService.thrift 69 | 70 | # .Net Core async 71 | docker run --rm -v "$PWD:/data" thrift thrift -o /data --gen netcore /data/UserService.thrift 72 | ``` 73 | 74 | 目前 Thrift v0.13.0 为.Net Core提供了TPL风格代码,支持异步,但遗憾的是.Net Framework只有同步版本,两者代码实现也略有不同。 75 | 76 | #### 3) 创建接口项目 77 | * 创建.Net Standard类库项目`ThriftTemplate.Contract`作为接口项目。 78 | * 将上一步生成的接口文件拷贝到当前项目中。 79 | * 引用 [ApacheThrift](https://www.nuget.org/packages/ApacheThrift/) nuget包到项目中 80 | 81 | #### 4) 服务端 82 | ##### 1. 接口实现 83 | * 创建类库项目`ThriftTemplate.Contract.Implement` 84 | * 添加`ThriftTemplate.Contract`接口项目引用 85 | * 实现接口。参考代码如下。 86 | ```csharp 87 | public class UserSvs : UserService.IAsync 88 | { 89 | public Task CreateAsync(Person person, CancellationToken cancellationToken) 90 | { 91 | // your code 92 | return Task.FromResult(true); 93 | } 94 | 95 | public Task DeleteAsync(int id, CancellationToken cancellationToken) 96 | { 97 | // your code 98 | return Task.FromResult(true); 99 | } 100 | 101 | public Task UpdateAsync(Person person, CancellationToken cancellationToken) 102 | { 103 | // your code 104 | return Task.CompletedTask; 105 | } 106 | 107 | public Task QueryAsync(int id, CancellationToken cancellationToken) 108 | { 109 | // your code 110 | return Task.FromResult(new Person {Id = 0, Name = "Colin", Age = 18}); 111 | } 112 | } 113 | ``` 114 | 115 | ##### 2. 运行服务 116 | * 创建服务宿主控制台项目`ThriftTemplate.Server`。 117 | * 管理项目引用 118 | * 引用Nuget 119 | * [ApacheThrift](https://www.nuget.org/packages/ApacheThrift/) 120 | * [Microsoft.Extensions.Logging](https://www.nuget.org/packages/Microsoft.Extensions.Logging) 121 | * 添加项目引用 122 | * 接口项目`ThriftTemplate.Contract` 123 | * 接口实现项目`ThriftTemplate.Contract.Implement` 124 | * 启动服务到指定地址。参考代码如下。 125 | 126 | ```csharp 127 | var serviceProvider = new ServiceCollection() 128 | .AddSingleton(new UserSvs()) 129 | .AddLogging(logging => logging.SetMinimumLevel(LogLevel.Trace)) 130 | .BuildServiceProvider(); 131 | 132 | using (var source = new CancellationTokenSource()) 133 | { 134 | var server = new AsyncBaseServer( 135 | new AsyncProcessor(serviceProvider.GetService()), 136 | new TServerSocketTransport(10010),//监听端口 137 | new TBinaryProtocol.Factory(), 138 | new TBinaryProtocol.Factory(), 139 | serviceProvider.GetService() 140 | ); 141 | 142 | await server.ServeAsync(source.Token); 143 | } 144 | ``` 145 | 146 | #### 5) 客户端 147 | * 创建客户端控制台项目`ThriftTemplate.Client` 148 | * 添加`ThriftTemplate.Contract`接口项目引用。引用 [ApacheThrift](https://www.nuget.org/packages/ApacheThrift/) 149 | * 调用远程服务。参考代码如下。 150 | ```csharp 151 | //创建服务连接 152 | using (var transport = new TSocketClientTransport(IPAddress.Parse("127.0.0.1"), 10010)) 153 | { 154 | using (var protocol = new TBinaryProtocol(transport)) 155 | { 156 | using (var client = new UserService.Client(protocol)) 157 | { 158 | using (var source = new CancellationTokenSource()) 159 | { 160 | await client.OpenTransportAsync(source.Token); 161 | await client.QueryAsync(0, source.Token);//调用服务方法 162 | } 163 | } 164 | } 165 | } 166 | ``` 167 | 168 | #### 6) 项目优化 169 | 以上我们可以发现,客户端调用每个服务端方法都需写类似调用代码较繁琐且重复,于是我们考虑将封装扩展对服务端方法调用,我们发现Thrift生成的接口文件中,接口的所在类都是partial class就是为了方便开发者自行扩展且避免重新生成后造成代码覆盖丢失。 170 | 171 | 每个接口方法都需要提供一个帮助扩展方法,于是我们可以考虑直接实现服务接口,同时为了方便使用我们也可以将服务运行代码进行封装。 172 | 173 | 我们在接口项目`ThriftTemplate.Contract`中新建`UserServiceExtension.cs`并对服务接口调用做如下扩展。 174 | ```csharp 175 | public partial class UserService : UserService.IAsync 176 | { 177 | // 服务端 178 | public static async Task RunAsync(IAsync processor, int port, ILoggerFactory loggerFactory) 179 | { 180 | using (var source = new CancellationTokenSource()) 181 | { 182 | var server = new AsyncBaseServer( 183 | new AsyncProcessor(processor), 184 | new TServerSocketTransport(port), 185 | new TBinaryProtocol.Factory(), 186 | new TBinaryProtocol.Factory(), 187 | loggerFactory 188 | ); 189 | 190 | await server.ServeAsync(source.Token); 191 | } 192 | } 193 | 194 | private readonly IPAddress _host; 195 | private readonly int _port; 196 | 197 | public UserService(IPAddress host, int port) 198 | { 199 | _host = host; 200 | _port = port; 201 | } 202 | 203 | //以下为客户端扩展 204 | 205 | public async Task CreateAsync(Person person) 206 | { 207 | using (var source = new CancellationTokenSource()) 208 | return await CreateAsync(person, source.Token); 209 | } 210 | 211 | public async Task CreateAsync(Person person, CancellationToken cancellationToken) => 212 | await InvokeAsync(async client => await client.CreateAsync(person, cancellationToken), 213 | cancellationToken); 214 | 215 | 216 | public async Task DeleteAsync(int id) 217 | { 218 | using (var source = new CancellationTokenSource()) 219 | return await DeleteAsync(id, source.Token); 220 | } 221 | 222 | public async Task DeleteAsync(int id, CancellationToken cancellationToken) => 223 | await InvokeAsync(async client => await client.DeleteAsync(id, cancellationToken), cancellationToken); 224 | 225 | 226 | public async Task UpdateAsync(Person person) 227 | { 228 | using (var source = new CancellationTokenSource()) 229 | await UpdateAsync(person, source.Token); 230 | } 231 | 232 | public async Task UpdateAsync(Person person, CancellationToken cancellationToken) => 233 | await InvokeAsync(async client => await client.UpdateAsync(person, cancellationToken), cancellationToken); 234 | 235 | 236 | public async Task QueryAsync(int id) 237 | { 238 | using (var source = new CancellationTokenSource()) 239 | return await QueryAsync(id, source.Token); 240 | } 241 | 242 | public async Task QueryAsync(int id, CancellationToken cancellationToken) => 243 | await InvokeAsync(async client => await client.QueryAsync(id, cancellationToken), cancellationToken); 244 | 245 | 246 | private async Task InvokeAsync(Action action, CancellationToken cancellationToken) 247 | { 248 | using (var transport = new TSocketClientTransport(_host, _port)) 249 | { 250 | using (var protocol = new TBinaryProtocol(transport)) 251 | { 252 | using (var client = new Client(protocol)) 253 | { 254 | await client.OpenTransportAsync(cancellationToken); 255 | action(client); 256 | } 257 | } 258 | } 259 | } 260 | 261 | private async Task InvokeAsync(Func> action, CancellationToken cancellationToken) 262 | { 263 | using (var transport = new TSocketClientTransport(_host, _port)) 264 | { 265 | using (var protocol = new TBinaryProtocol(transport)) 266 | { 267 | using (var client = new Client(protocol)) 268 | { 269 | await client.OpenTransportAsync(cancellationToken); 270 | return await action(client); 271 | } 272 | } 273 | } 274 | } 275 | } 276 | ``` 277 | 278 | 使用以上扩展后,服务端和客户端代码可以大大简化。 279 | ```csharp 280 | //服务端 281 | var serviceProvider = new ServiceCollection() 282 | .AddSingleton(new UserSvs()) 283 | .AddLogging(logging => logging.SetMinimumLevel(LogLevel.Trace)) 284 | .BuildServiceProvider(); 285 | await UserService.RunAsync(serviceProvider.GetService(), 10010, serviceProvider.GetService()); 286 | 287 | //客户端 288 | var service = new UserService(IPAddress.Parse("127.0.0.1"), 10010); 289 | await service.QueryAsync(0); 290 | await Service.DeleteAsync(0); 291 | ``` 292 | 293 | #### 7) .Net Framework与.Net Core 294 | [ApacheThrift](https://www.nuget.org/packages/ApacheThrift/)是Apach官方提供的Provider,可以兼容.Net Core和.Net Framework,它为.Net Core和.Net Framework提供了不同的dll,所以也导致使用此package的.Net Core和.Net Framework项目不可互通,如,不能使用一个 .net core客户端调用.net framework服务端。 295 | 296 | 如果需要实现.Net Framework和.Net Core的Thrift互通可以是使用第三方nuget包,如[apache-thrift-netcore](https://www.nuget.org/packages/apache-thrift-netcore/),它可以兼容所有.net实现。 297 | 298 | ## 3. 案例源码 299 | 300 | ![案例解决方案](https://i.loli.net/2020/02/26/xrkURP5wuziALb4.jpg) 301 | 302 | Core目录下是.Net Core版本实现。 303 | Net目录下是.Net Framework版本实现。 304 | Cross目录下是.Net Core与.Net Framework混用版本 305 | 306 | 项目案例下载:https://github.com/colin-chang/ThriftTemplate 307 | 308 | > 参考文档 309 | 310 | * https://www.cnblogs.com/focus-lei/p/8889389.html 311 | * http://doc.oschina.net/grpc?t=58008 -------------------------------------------------------------------------------- /pages/staticssi.md: -------------------------------------------------------------------------------- 1 | # 页面静态化和SSI 2 | 3 | ## 1. 页面静态化 4 | 即使使用缓存,只是降低数据库服务器压力,web 服务器仍然是“每次来访都要跑 一遍代码”,如果所有人访问的结果都一样,就可以直接要响应的内容保存成 html 文件,让用户访问 html 文件。 5 | 6 | 缓存和静态页的区别:静态页的性能比缓存好,能用静态页就用静态页。什么情况下不能用静态页:相同的地址不同的人看的不一样、有的页面有的人不能看。 7 | 8 | ### 1.1 MVC 9 | 之前都是用户请求 Action,获取 html 响应去显示,那么怎么样通过程序去请求 Action 获取响应呢? 10 | 11 | 首先定义如下的方法: 12 | ```csharp 13 | static string RenderViewToString(ControllerContext context, string viewPath, object model = null) 14 | { 15 | var viewEngineResult = ViewEngines.Engines.FindView(context, viewPath, null); 16 | if (viewEngineResult == null) 17 | throw new FileNotFoundException("View" + viewPath + "cannot be found."); 18 | 19 | var view = viewEngineResult.View; 20 | context.Controller.ViewData.Model = model; 21 | using (var sw = new StringWriter()) 22 | { 23 | var ctx = new ViewContext(context, view, 24 | context.Controller.ViewData, 25 | context.Controller.TempData, 26 | sw); 27 | view.Render(ctx, sw); 28 | return sw.ToString(); 29 | } 30 | } 31 | 32 | 然后如下调用: 33 | ```csharp 34 | string html = RenderViewToString(this.ControllerContext, "~/Views/Home/Index.cshtml", person); 35 | 36 | File.WriteAllText("home_index.html",html); 37 | ``` 38 | 39 | 静态化之后可以将用户对`/Home/Index`的请求转为`/home_index.html`。既可以在生成客户端链接的时候直接使用静态地址,也可以在服务端通过路由重定项等处理。 40 | 41 | 我们通常只对**读多写少**的内容进行页面静态化处理,当静态化的页面内容发生“增删改”操作时,重新生成对应的静态页面即可。 42 | 43 | ### 1.2 WebAPI 44 | 目前Web开发中[前后端分离](/distribution/separatefontend.md)技术逐渐成为趋势。前后端分离之后项目架构可以简单抽象为`UI + WebAPI`。那么如何在前后端分离架构中进行页面静态化呢。 45 | 46 | 页面静态化本质是在HTML组装完成之后将其保存为静态文件,用户请求时直接返回保存的静态文件而不用再次动态组装。那在前后端分离之后,HTML的组装工作都是在`UI`完成,所以直接在`UI`层编写静态化逻辑即可。新增需要静态化的资源时,请求`API`数据组装HTML并使用JS将组装完成的HTML页面另存下来,其内容发生`增删改`操作时再次请求`API`重新组装即可。 47 | 48 | ## 2. SSI 49 | 50 | https://www.jianshu.com/p/3898780ac1c9 51 | https://www.cnblogs.com/dehigher/p/10127380.html 52 | -------------------------------------------------------------------------------- /pages/supplement.md: -------------------------------------------------------------------------------- 1 | # 缓存、Session 2 | 3 | ## 1. 缓存 4 | Asp.Net Core不再支持`HttpContext.Cache`,转而使用[`MemoryCache`](https://docs.microsoft.com/zh-cn/aspnet/core/performance/caching/memory?view=aspnetcore-2.2),这是一种服务端内存缓存。使用方式方式非常简单,在`Startup`的`ConfigureServices`方法中注册服务,需要使用的位置注入`IMemoryCache`对象即可。 5 | 6 | 除了内存缓存,我们还可以使用Redis等[分布式缓存](https://docs.microsoft.com/zh-cn/aspnet/core/performance/caching/distributed?view=aspnetcore-2.2) 7 | 8 | ## 2. Session 9 | 在Asp.Net Core中使用Session需要首先添加对Session的支持,否则会报错`Session has not been configured for this application or request`。 10 | 11 | Session使用步骤: 12 | * 1) 注册服务。`ConfigureServices`中`services.AddSession()`; 13 | * 2) 注册中间件。`Configure`中`app.UseSession()`; 14 | * 3)使用Session 15 | 16 | ```csharp 17 | HttpContext.Session.SetString("userName","Colin"); 18 | string userName = HttpContext.Session.GetString("userName") 19 | ``` 20 | 21 | 目前Session默认仅支持存储`int`、`string`和`byte[]`类型,其他复杂类型可以使用json序列化后存储字符串。 22 | 23 | TempData也依赖于 Session,所以也要配置 Session。 24 | 25 | 默认Session为服务器端进程内存储,我们也可以使用Redis做进程外Session。 -------------------------------------------------------------------------------- /pages/unittest.md: -------------------------------------------------------------------------------- 1 | # 单元测试 2 | 3 | - [单元测试](#单元测试) 4 | - [1. 单元测试简介](#1-单元测试简介) 5 | - [1.1 单元测试作用](#11-单元测试作用) 6 | - [1.2 单元测试必要性](#12-单元测试必要性) 7 | - [1.3 TDD](#13-tdd) 8 | - [1.4 单元测试的正确姿势](#14-单元测试的正确姿势) 9 | - [2. .NET单元测试](#2-net单元测试) 10 | - [2.1 Attributes](#21-attributes) 11 | - [2.2 Assertions](#22-assertions) 12 | - [2.3 Xunit示例](#23-xunit示例) 13 | 14 | ## 1. 单元测试简介 15 | 单元测试是针对程序的最小单元来进行正确性检验的测试工作,程序单元就是应用的最小可测试部件,一个单元可能是单个程序,类,对象,方法等。单元测试的想法是写一个方法之前就想好这个方法有什么样的输入输出,在开发完成就 测试一下给定的输出是不是产生期望的输出。 16 | 17 | ### 1.1 单元测试作用 18 | * 减少bug 19 | 20 | 单元测试的目的就是通过足够准确的测试用例保证代码逻辑是正确。所以,在单测过程中,必然可以解决一些bug。因为,一旦某条测试用例没有通过,那么我们就会修改被测试的代码来保证能够通过测试。 21 | 22 | * 减少修复bug的成本 23 | 24 | 一般解决bug的思路都是先通过各种手段定位问题,然后在解决问题。定位问题的时候如果没有单元测试,就只能通过debug的方式一点一点的追踪代码。解决问题的时候更是需要想尽各种方法来重现问题,然后改代码,改了代码之后在集成测试。 25 | 26 | 因为单元规模较小,复杂性较低,因而发现错误后容易隔离和定位,有利于调试工作。 27 | 28 | * 帮助重构,提高重构的成功率 29 | 我相信,对一个程序员来说最痛苦的事就是修改别人的代码。有时候,一个很大的系统会导致很多人不敢改,因为他不知道改了一个地方会不会导致其他地方出错。可以,一旦有了单元测试,开发人员可以很方便的重构代码,只要在重构之后跑一遍单元测试就可以知道是不是把代码“改坏了” 30 | 31 | * 提高开发速度 32 | 不写单测也许能让开发速度更快,但是无法保证自己写出来的代码真的可以正确的执行。写单测可以较少很多后期解决bug的时间。也能让我们放心的使用自己写出来的代码。整体提高开发速度。 33 | 34 | ### 1.2 单元测试必要性 35 | 单元测试可以在软件开发过程的早期就能发现问题。从表面上看,为每个单元程序都编写测试代码似乎是增加了工作量,但是其实这些代码不仅为你织起了一张保护网,而且还可以帮助你快速定位错误从而使你大大减少修复BUG的时间。只要单测的测试用例足够好,那么就可以避免很多低级错误。好的单测不仅不会浪费时间,还会大大节省我们的时间。 36 | 37 | 其实单元测试不仅能保证项目进度还能优化你的设计。设计的程序耦合度也越来越低。每个单元程序的输入输出,业务内容和异常情况都会尽可能变得简单。 38 | 39 | ### 1.3 TDD 40 | Test-Driven Development, 测试驱动开发, 是敏捷开发的一项核心实践和技术,也是一种设计方法论。TDD原理是开发功能代码之前,先编写测试用例代码,然后针对测试用例编写功能代码,使其能够通过。由于TDD对开发人员要求非常高,跟传统开发思维不一样,因此实施起来相当困难。 41 | 42 | ![TTD](https://i.loli.net/2020/02/26/uIyZdrqC8lW3O5V.jpg) 43 | 44 | 测试驱动开发有好处也有坏处。因为每个测试用例都是根据需求来的,或者说把一个大需求分解成若干小需求编写测试用例,所以测试用例写出来后,开发者写的执行代码,必须满足测试用例。如果测试不通过,则修改执行代码,直到测试用例通过。 45 | 46 | ### 1.4 单元测试的正确姿势 47 | * 越重要的代码,越要写单元测试; 48 | * 代码做不到单元测试,多思考如何改进,而不是放弃; 49 | * 边写业务代码,边写单元测试,而不是完成整个新功能后再写; 50 | * 多思考如何改进、简化测试代码。 51 | 52 | ## 2. .NET单元测试 53 | .NET中常见的测试框架有MSTest、Nunit和Xunit,目前比较流行的是Xunit。作为NUnit的改进版,xUnit.Net确实克服了NUnit的不少缺点。xUnit.Net的Assert更精简但是又足以满足单元测试的需要,相比之下NUnit的Assert API略显臃肿。 54 | 55 | ### 2.1 Attributes 56 | NUnit 3.x|MSTest 15.x|xUnit.net 2.x|Comments 57 | :-|:-|:-|:- 58 | [Test]|[TestMethod]|[Fact]|Marks a test method. 59 | [TestFixture]|[TestClass]|n/a|xUnit.net does not require an attribute for a test class; it looks for all test methods in all public (exported) classes in the assembly. 60 | [ExpectedException]|[ExpectedException]|Assert.ThrowsRecord.Exception|xUnit.net has done away with the ExpectedException attribute in favor of Assert.Throws. 61 | [SetUp]|[TestInitialize]|Constructor|We believe that use of [SetUp] is generally bad. However, you can implement a parameterless constructor as a direct replacement. 62 | [TearDown]|[TestCleanup]|IDisposable.Dispose|We believe that use of [TearDown] is generally bad. However, you can implement IDisposable.Dispose as a direct replacement. 63 | [OneTimeSetUp]|[ClassInitialize]|IClassFixture|To get per-class fixture setup, implement IClassFixture on your test class. 64 | [OneTimeTearDown]|[ClassCleanup]|IClassFixture|To get per-class fixture teardown, implement IClassFixture on your test class. 65 | n/a|n/a|ICollectionFixture|To get per-collection fixture setup and teardown, implement ICollectionFixture on your test collection. 66 | [Ignore("reason")]|[Ignore]|[Fact(Skip="reason")]|Set the Skip parameter on the [Fact] attribute to temporarily skip a test. 67 | [Property]|[TestProperty]|[Trait]|Set arbitrary metadata on a test 68 | n/a|[DataSource]|[Theory],[XxxData]|Theory (data-driven test). 69 | 70 | ### 2.2 Assertions 71 | 72 | NUnit 3.x (Constraint)|MSTest 15.x|xUnit.net 2.x|Comments 73 | :-|:-|:-|:- 74 | Is.EqualTo|AreEqual|Equal|MSTest and xUnit.net support generic versions of this method 75 | Is.Not.EqualTo|AreNotEqual|NotEqual|MSTest and xUnit.net support generic versions of this method 76 | Is.Not.SameAs|AreNotSame|NotSame| 77 | Is.SameAs|AreSame|Same| 78 | Does.Contain|Contains|Contains| 79 | Does.Not.Contain|DoesNotContain|DoesNotContain| 80 | Throws.Nothing|n/a|n/a|Ensures that the code does not throw any exceptions. See Note 5 81 | n/a|Fail|n/a|xUnit.net alternative: Assert.True(false, "message") 82 | Is.GreaterThan|n/a|n/a|xUnit.net alternative: Assert.True(x > y) 83 | Is.InRange|n/a|InRange|Ensures that a value is in a given inclusive range 84 | Is.AssignableFrom|n/a|IsAssignableFrom| 85 | Is.Empty|n/a|Empty| 86 | Is.False|IsFalse|FALSE| 87 | Is.InstanceOf|IsInstanceOfType|IsType| 88 | Is.NaN|n/a|n/a|xUnit.net alternative: Assert.True(double.IsNaN(x)) 89 | Is.Not.AssignableFrom|n/a|n/a|xUnit.net alternative: Assert.False(obj is Type) 90 | Is.Not.Empty|n/a|NotEmpty| 91 | Is.Not.InstanceOf|IsNotInstanceOfType|IsNotType| 92 | Is.Not.Null|IsNotNull|NotNull| 93 | Is.Null|IsNull|Null| 94 | Is.True|IsTrue|TRUE| 95 | Is.LessThan|n/a|n/a|xUnit.net alternative: Assert.True(x < y) 96 | Is.Not.InRange|n/a|NotInRange|Ensures that a value is not in a given inclusive range 97 | Throws.TypeOf|n/a|Throws|Ensures that the code throws an exact exception 98 | 99 | ### 2.3 Xunit示例 100 | xUnit基本使用参见https://xunit.github.io/docs/getting-started/netfx/visual-studio 101 | 102 | ```csharp 103 | public class TemplateTest : IClassFixture 104 | { 105 | private readonly TempateFixture _fixture; 106 | 107 | //相当于[TestInitialize] 108 | public TemplateTest(TempateFixture fixture) 109 | { 110 | _fixture = fixture; 111 | } 112 | 113 | [Fact] 114 | public void Test1() 115 | { 116 | Assert.Equal("Colin",_fixture.Name); 117 | } 118 | 119 | [Fact] 120 | public void Test2() 121 | { 122 | Assert.True(true); 123 | } 124 | } 125 | 126 | public class TempateFixture : IDisposable 127 | { 128 | public string Name { get; set; } 129 | 130 | //相当于[ClassInitialize] 131 | public TempateFixture() 132 | { 133 | //数据初始化 134 | Name = "Colin"; 135 | } 136 | 137 | //相当于[ClassCleanup] 138 | public void Dispose() 139 | { 140 | //数据清理 141 | Name = null; 142 | } 143 | } 144 | ``` 145 | 146 | TempateFixture中构造函数和Dispose在单个或多个测试用例都只会执行一次。TemplateTest中构造函数和Dispose(如果直接实现IDisposable)则会在每个测试方法都执行一次。 147 | 148 | 单元测试应该符合可以重复执行的原则,所以我们通常会在测试结束后对测试产生的变化或恢复和清理,如删除产生的过程数据等。测试贡献和清理数据参见https://xunit.github.io/docs/shared-context 149 | 150 | 如果需要在单元测试中输出内容需要使用ITestOutputHelper对象,直接注入即可。 151 | ```csharp 152 | public class TemplateTest 153 | { 154 | private readonly ITestOutputHelper _testOutputHelper; 155 | public TemplateTest(ITestOutputHelper testOutputHelper) 156 | { 157 | _testOutputHelper = testOutputHelper; 158 | } 159 | 160 | [Fact] 161 | public void SaveAsTest() 162 | { 163 | _testOutputHelper.WriteLine("测试输出..."); 164 | } 165 | } 166 | ``` 167 | 168 | 实际案例可以参考 169 | https://github.com/colin-chang/MongoHelper/blob/master/ColinChang.MongoHelper.Test/MongoHelperTest.cs -------------------------------------------------------------------------------- /pages/webapi-basic.md: -------------------------------------------------------------------------------- 1 | # WebAPI 基础 2 | - [WebAPI 基础](#webapi-%e5%9f%ba%e7%a1%80) 3 | - [1. ApiController](#1-apicontroller) 4 | - [2. 路由匹配](#2-%e8%b7%af%e7%94%b1%e5%8c%b9%e9%85%8d) 5 | - [2.1 RouteAttribute 和 HttpMethodAttribute](#21-routeattribute-%e5%92%8c-httpmethodattribute) 6 | - [2.2 Restful 路由](#22-restful-%e8%b7%af%e7%94%b1) 7 | - [2.3 自定义路由](#23-%e8%87%aa%e5%ae%9a%e4%b9%89%e8%b7%af%e7%94%b1) 8 | - [2.3.1 Restful 之殇](#231-restful-%e4%b9%8b%e6%ae%87) 9 | - [2.3.2 自定义Action路由](#232-%e8%87%aa%e5%ae%9a%e4%b9%89action%e8%b7%af%e7%94%b1) 10 | - [2.3.3 回归MVC路由](#233-%e5%9b%9e%e5%bd%92mvc%e8%b7%af%e7%94%b1) 11 | - [3. API参数](#3-api%e5%8f%82%e6%95%b0) 12 | - [3.1 URL参数](#31-url%e5%8f%82%e6%95%b0) 13 | - [3.2 对象参数](#32-%e5%af%b9%e8%b1%a1%e5%8f%82%e6%95%b0) 14 | - [4. 返回值](#4-%e8%bf%94%e5%9b%9e%e5%80%bc) 15 | - [5. 异常处理](#5-%e5%bc%82%e5%b8%b8%e5%a4%84%e7%90%86) 16 | - [5.1 业务性错误](#51-%08%e4%b8%9a%e5%8a%a1%e6%80%a7%e9%94%99%e8%af%af) 17 | - [5.2 常规异常处理](#52-%e5%b8%b8%e8%a7%84%e5%bc%82%e5%b8%b8%e5%a4%84%e7%90%86) 18 | - [5.3 全局异常过滤器](#53-%08%e5%85%a8%e5%b1%80%e5%bc%82%e5%b8%b8%e8%bf%87%e6%bb%a4%e5%99%a8) 19 | 20 | ## 1. ApiController 21 | * WebAPI中`Controller`直接即继承自`ControllerBase`。在ASP.NET Core 2.1之后引入`[ApiController]`用于批注 Web API 控制器类。`[ApiController]`特性通常结合`ControllerBase`来为控制器启用特定 REST 行为。 22 | 23 | ```csharp 24 | [Route("api/[controller]")] 25 | [ApiController] 26 | public class ProductsController : ControllerBase 27 | ``` 28 | 29 | * 在 ASP.NET Core 2.2 或更高版本中,可将`[ApiController]`特性应用于程序集。以这种方式进行注释,会将 web API 行为应用到程序集中的所有控制器。 建议将程序集级别的特性应用于 Startup 类。 30 | ```csharp 31 | [assembly: ApiController] 32 | namespace WebApiSample.Api._22 33 | { 34 | public class Startup 35 | { 36 | } 37 | ``` 38 | 39 | ## 2. 路由匹配 40 | ### 2.1 RouteAttribute 和 HttpMethodAttribute 41 | WebAPI中必须为每个`Controller`使用`[Route]`特性进行路由设定,而不能通过`UseMvc`中定义的传统路由或通过`Startup.Configure`中的`UseMvcWithDefaultRoute`配置路由。 42 | 43 | 与`Controller`设定路由方式一样,我们也可以在`Action`方法上使用`[Route]`单独设定路由,除了`[Route]`,我们也可以使用`HttpMethodAttribute`设定路由,用法相同,`HttpMethodAttribute`包括`[HttpGet]`、`[HttpPost]`、`[HttpPut]`、`[HttpDelete]`等。`Action`路由建立在`Controller`路由之上。 44 | 45 | 使用`HttpMethodAttribute`定义路由时会同时限制`Action`方法的`HTTP`访问方式,如果单纯想为`Action`方法设定路由同时允许多种HTTP访问方式,可以是使用`[Route]`配置路由。 46 | 47 | 路由不区分大小写。 48 | 49 | ```csharp 50 | [Route("api/test")] 51 | public class TestController : ControllerBase 52 | { 53 | // GET api/test 54 | [HttpGet] 55 | public ActionResult Get() 56 | { 57 | return nameof(Get); 58 | } 59 | 60 | //GET api/test/1 61 | [HttpGet("{id}")] 62 | public ActionResult Get(int id) 63 | { 64 | return nameof(Get) + id; 65 | } 66 | 67 | //GET api/test/getbyname/colin 68 | [HttpGet("GetByName/{name?}")] 69 | public ActionResult Get(string name) 70 | { 71 | return "GetByName" + name; 72 | } 73 | 74 | //GET api/test/colin/18 75 | [HttpGet("{name}/{age}")] 76 | public ActionResult Get(string name,int age) 77 | { 78 | return nameof(Get) + name + age; 79 | } 80 | } 81 | ``` 82 | 83 | ### 2.2 Restful 路由 84 | WebAPI默认路由使用`Restful`风格,按照请求方式进行路由,不作标记的情况下,`Action`方法名会按照请求方式进行`StartWith`匹配。所以的`Get()`、`GetById()`、`GetXXX()`没有任何区别。如果使用`[HttpGet]`标记了`Action`方法,则方法名任意取,不必以`GET`开头。同理,`POST`、`PUT`、`DELETE`亦是如此。 85 | 86 | 87 | ### 2.3 自定义路由 88 | #### 2.3.1 Restful 之殇 89 | 完全符合`Restful`风格的API在很多业务常见下并不能满足需求。如之前所说,把所有业务抽象为CRUD操作并不现实,简单通过HTTP状态码也不容易区分处理结果。除此之外,仅通过简单几种谓词语意进行路由在难以满足复杂业务需求。如,根据ID查询用户、根据用户名查询用户、根据手机号查询用户。 90 | ```csharp 91 | // 错误方式,调用报错 92 | [Route("api/test")] 93 | public class TestController : ControllerBase 94 | { 95 | [HttpGet("{id}")] 96 | public ActionResult GetById(int id) 97 | { 98 | return Users.FirstOrDefault(u=>u.Id==id); 99 | } 100 | 101 | [HttpGet("{userName}")] 102 | public ActionResult GetByUserName(string userName) 103 | { 104 | return Users.FirstOrDefault(u=>u.UserName==userName); 105 | } 106 | 107 | [HttpGet("{phoneNumber}")] 108 | public ActionResult GetByPhoneNumber(string phoneNumber) 109 | { 110 | return Users.FirstOrDefault(u=>u.PhoneNumber==phoneNumber); 111 | } 112 | } 113 | ``` 114 | 以上代码可以编译通过,但由于三个`Action`匹配相同路由规则,所以`GET`请求`~/api/test/xxx` 时会出现歧义而抛出`AmbiguousMatchException`。 115 | 116 | #### 2.3.2 自定义Action路由 117 | 此时我们可以通过前面提到的[`RouteAttribute`或`HttpMethodAttribute`](#21-routeattribute-和-httpmethodattribute)来为每个`Action`设置特定路由。 118 | ```csharp 119 | // 自定义Action路由 120 | [Route("api/test")] 121 | public class TestController : ControllerBase 122 | { 123 | //GET api/test/getbyid/1 124 | [HttpGet("GetById/{id}")] 125 | public ActionResult GetById(int id) 126 | { 127 | return Users.FirstOrDefault(u=>u.Id==id); 128 | } 129 | 130 | //GET api/test/getbyusername/colin 131 | [HttpGet("GetByUserName/{userName}")] 132 | public ActionResult GetByUserName(string userName) 133 | { 134 | return Users.FirstOrDefault(u=>u.UserName==userName); 135 | } 136 | 137 | //GET api/test/getbyphonenumber/110 138 | [HttpGet("GetByPhoneNumber/{phoneNumber}")] 139 | public ActionResult GetByPhoneNumber(string phoneNumber) 140 | { 141 | return Users.FirstOrDefault(u=>u.PhoneNumber==phoneNumber); 142 | } 143 | } 144 | ``` 145 | 146 | #### 2.3.3 回归MVC路由 147 | 以上为每个`Action`单独配置路由后解决了`Restful`遇到的问题。不难发现当每个`Action`方法路由名称恰好是自身方法名时,我们便可以通过`Action`名称来访问对应接口,这与`MVC`路由方式效果一致。 148 | 149 | 单独为每个`Action`方法都配置路由较为繁琐,我们可以仿照`MVC`路由方式直接配置Controller路由,路由效果一致,但使用跟简单。 150 | 151 | ```csharp 152 | // 自定义Controller路由 153 | 154 | [Route("api/test/{Action}")] 155 | public class TestController : ControllerBase 156 | { 157 | //GET api/test/getbyid/1 158 | [HttpGet("{id?}")] 159 | public ActionResult GetById(int id) 160 | { 161 | return Users.FirstOrDefault(u=>u.Id==id); 162 | } 163 | 164 | //GET/POST/PUT/DELETE api/test/getbyusername/colin 165 | [Route("{userName}")] 166 | public ActionResult GetByUserName(string userName) 167 | { 168 | return Users.FirstOrDefault(u=>u.UserName==userName); 169 | } 170 | 171 | //GET api/test/getbyphonenumber?phoneNumber=110 172 | [HttpGet] 173 | public ActionResult GetByPhoneNumber(string phoneNumber) 174 | { 175 | return Users.FirstOrDefault(u=>u.PhoneNumber==phoneNumber); 176 | } 177 | } 178 | ``` 179 | `Restful`风格路由与MVC路由只是匹配`Action`方法方式不同,MVC路由通过`Action`方法名定位要比`Restful`通过谓词语意定位更加多变,更容易应付复杂的业务场景。 180 | 181 | ## 3. API参数 182 | `GET`、`POST`、`PUT`、`DELETE`等所有请求方式均可使用 URL参数 和 对象参数 进行参数传递。 183 | 184 | `GET`和`DELETE`请求通常传递数据量较少,多使用URL参数。`POST`和`PUT`请求通常传递数量较大,多使用对象参数。 185 | 186 | ### 3.1 URL参数 187 | 简单参数有两种,QueryString参数和路由参数,这两种都参数以不同形式体现在URL中,所以我们统称为URL参数。 188 | 189 | 在参数少且简单对安全性要求不高的情况下,可以使用URL参数。 190 | 191 | ```csharp 192 | [Route("api/test")] 193 | public class TestController : ControllerBase 194 | { 195 | //GET api/test?name=colin&age=18 196 | [HttpGet] 197 | public ActionResult Get(string name, int age) 198 | { 199 | return name + age; 200 | } 201 | 202 | //DELETE api/test/1 203 | [HttpDelete("{id}")] 204 | public ActionResult Delete(int id) 205 | { 206 | return NoContent(); 207 | } 208 | } 209 | ``` 210 | 211 | ### 3.2 对象参数 212 | 参数内容多且复杂或安全性较高的情况下,在API中接收参数时我们常把参数字段封装到一个参数模型类中。**使用非URL参数而不在服务端封装对象会遇到很多麻烦,不建议使用。** 213 | 214 | 客户端传递对象参数的方式有很多中,一般需要约定`Content-Type`报文头。服务端接收对象参数常使用`[FromXXX]`特性。 215 | 216 | 特性|ContentType|传参方式 217 | :-|:-|:- 218 | `[FromQuery]`| - |`?name=colin&age=18` 219 | `[FromHeader]`| - 或 `application/x-www-form-urlencoded` 或 `multipart/form-data` |`?name=colin&age=18` 或 `key-value`对 220 | `[FromForm]`|`multipart/form-data` 或 `application/x-www-form-urlencoded`| `name-value`对 221 | `[FromBody]` 或 无标记|`application/json`|`{name:'colin',age:18}` 222 | 223 | ```csharp 224 | [Route("api/test")] 225 | public class TestController : ControllerBase 226 | { 227 | [HttpPost] 228 | public ActionResult Post([FromForm] Person p) 229 | { 230 | return CreatedAtAction(nameof(Post), new {id = p.Id}, p); 231 | } 232 | 233 | [HttpPut("{id}")] 234 | public ActionResult Put(int id, [FromBody] Person p) 235 | { 236 | return NoContent(); 237 | } 238 | ``` 239 | 240 | ![POST请求表单参数](https://i.loli.net/2020/02/26/BK8ALvHeqthEYG5.png) 241 | 242 | ![PUT请求JSON参数](https://i.loli.net/2020/02/26/QwxlN83juSkFMWI.png) 243 | 244 | > JSON 245 | 246 | ContentType为applciation/json时,传递参数必须是[JSON格式](https://www.json.org/)。 247 | 248 | 按照JSON官网的规范("A value can be a string in double quotes, or a number, or true or false or null, or an object or an array. These structures can be nested."),JSON可以直接传递字符串、数字和布尔三种简单类型。需要特别注意的是,字符串需要包裹在双引号直接(**双引号作为字符串的一部分**)。 249 | 250 | ```csharp 251 | [HttpPost] 252 | public void Post([FromBody] string value) 253 | { 254 | } 255 | ``` 256 | 257 | ![POST字符串](https://i.loli.net/2020/02/26/ISQYUX3DcBlH7om.jpg) 258 | 259 | ![POST Ajax](https://i.loli.net/2020/02/26/IbmZe26jgABL8yu.jpg) 260 | 261 | ## 4. 返回值 262 | 263 | ASP.NET Core 提供以下 Web API 控制器操作返回类型选项: 264 | * 特定类型 265 | * IActionResult 266 | * ActionResult 267 | 268 | 多数情况下返回数据时统一使用`ActionResult`<T>类型。`T`是实际属数据类型,在`Action`方法中编码时直接返回`T`类型数据即可。ASP.NET Core 自动将对象序列化为 JSON,并将 JSON 写入响应消息的正文中。 269 | 270 | 三种返回类型具体区别和使用参见[官方文档](https://docs.microsoft.com/zh-cn/aspnet/core/web-api/action-return-types?view=aspnetcore-2.2)。 271 | 272 | ## 5. 异常处理 273 | ### 5.1 业务性错误 274 | 简单的错误可以直接使用`HttpStatusCode`返回,如请求资源不能存在直接返回`NotFound`(404)即可。 275 | 276 | 较为复杂的业务错误,如,“用户年龄不合法”、“Id不存在等”,这种情况`HttpStatusCode`不足以满足业务需要, 277 | 一般我们可以自定义一个统一的返回对象来做详细说明。 278 | 279 | ```csharp 280 | public interface IApiResult{} 281 | 282 | public class ApiResult:IApiResult 283 | { 284 | /// 285 | /// 业务码。可自定义一套业务码标准 286 | /// 287 | public int Code { get; set; } = 200; 288 | 289 | /// 290 | /// 消息。一般可用于传输错误消息 291 | /// 292 | public string Message { get; set; } 293 | 294 | /// 295 | /// 数据内容。一般为实际请求数据,如Json 296 | /// 297 | public T Content { get; set; } 298 | 299 | public ApiResult(int code, string message, T content) 300 | { 301 | Code = code; 302 | Message = message; 303 | Content = content; 304 | } 305 | } 306 | ``` 307 | 使用方式如下: 308 | ```csharp 309 | [HttpGet("{age}")] 310 | public ActionResult Get(int age) 311 | { 312 | if (age < 18||age>60) 313 | { 314 | return new ApiResult(0,"年龄超限",null); 315 | } 316 | else 317 | { 318 | return new ApiResult(1,"OK","123"); 319 | } 320 | } 321 | ``` 322 | 323 | ### 5.2 常规异常处理 324 | 在API代码中做好必要的异常捕捉和处理,如用户请求参数合法性校验等。一般API中只做简单的数据采集校验,响应和格式化返回数据等工作,复杂的业务逻辑处理是业务逻辑层的工作,一般在BLL中做异常捕获和处理。 325 | 326 | ### 5.3 全局异常过滤器 327 | 全局未处理异常可以通过异常过滤器来进行捕捉处理。 328 | 329 | 自定义异常过滤器。 330 | ```csharp 331 | public class MyAsyncExceptionFilter : IAsyncExceptionFilter 332 | { 333 | private ILogger _logger; 334 | 335 | public MyAsyncExceptionFilter(ILogger logger) 336 | { 337 | _logger = logger; 338 | } 339 | 340 | public async Task OnExceptionAsync(ExceptionContext context) 341 | { 342 | context.ExceptionHandled = true; 343 | 344 | var msg = context.Exception.Message; 345 | _logger.LogError(msg); 346 | context.Result = new ObjectResult(new ApiResult(500, msg, null)) {StatusCode = 500}; 347 | 348 | await Task.CompletedTask; 349 | } 350 | } 351 | ``` 352 | Startup中注册过滤器。 353 | ```csharp 354 | public void ConfigureServices(IServiceCollection services) 355 | { 356 | services.AddMvc(options => 357 | { 358 | options.Filters.Add(); 359 | }); 360 | } 361 | ``` -------------------------------------------------------------------------------- /pages/webapi-multiversion.md: -------------------------------------------------------------------------------- 1 | # WebAPI 多版本管理 2 | 3 | ## 1. 多版本API 4 | 如果我们为APP设计并提供API,当APP发布新本后可能早期APP没有内置升级机制,也可能有些用户不愿升级,造成多个版本的APP同时在运行。开发新版本App时,要给API增加新的功能或者修改以前接口的规范,这可能会造成旧版 App无法使用,因此在一定情况下会“保留旧接口的运行、新功能用新接口”,这样 就会存在多版本接口共存的问题。 5 | 6 | ## 2. 多版本管理 7 | 8 | 多版本管理常见技术实现方案有以下三种: 9 | * 域名区分。不同版本用不同的域名。如 v1.api.xxx.com、v2.api.xxx.com、v3... 10 | * 反代服务器转发。在url或报文头中携带版本信息,然后`Nginx`等反代服务器按照不同版本将请求转发到不同服务器。 11 | * 路由匹配。多版本共处于同项目中,然后使用`[Route]`将请求路由到不同的`Controller`或`Action`中。 12 | 13 | 通过域名区分和Nginx转发来两种方式进行API多版本管理时,可以借助代码分支隔离多版本代码。旧版API做一个代码分支,除了进行 bug 修复外不再做改动;新接口代码继续演化升级。最后分别部署不同版本API服务,通过域名绑定或反代转发进行区分即可。推荐使用这两种方式。 14 | 15 | 通过路由匹配方式进行多版本管理在系统规模较小时可以在一定程度上节省资源配置消耗,但所有版本代码都共存在一个项目中,不易维护且不利于后期系统拓展。 16 | 17 | 前两种方式都是在运维阶段配置完成,不再赘述。最后路由匹配的方式则需要在开发阶段完成,下面我们来分析一下最后一种方式。 18 | 19 | 1) ControllerRoute 20 | ```csharp 21 | [Route("api/v1/test")] 22 | public class TestV1Controller:ControllerBase 23 | { 24 | //GET api/v1/test/ 25 | [HttpGet] 26 | public ActionResult Get() 27 | { 28 | return "v1-get"; 29 | } 30 | } 31 | 32 | [Route("api/v2/test")] 33 | public class TestV2Controller:ControllerBase 34 | { 35 | //GET api/v2/test/ 36 | [HttpGet] 37 | public ActionResult Get() 38 | { 39 | return "v2-get"; 40 | } 41 | } 42 | ``` 43 | 44 | 2) ActionRoute 45 | ```csharp 46 | [Route("api/test/{Action}")] 47 | public class TestController : ControllerBase 48 | { 49 | //GET api/test/getv1 50 | [HttpGet] 51 | public ActionResult GetV1() 52 | { 53 | return "v1-get"; 54 | } 55 | 56 | //GET api/test/getv2 57 | [HttpGet] 58 | public ActionResult GetV2() 59 | { 60 | return "v2-get"; 61 | } 62 | } 63 | ``` 64 | -------------------------------------------------------------------------------- /pages/webapi-openapi.md: -------------------------------------------------------------------------------- 1 | # OpenAPI 2 | 3 | - [OpenAPI](#openapi) 4 | - [1. AppKey](#1-appkey) 5 | - [2. OpenAPI](#2-openapi) 6 | - [2.1 Swagger](#21-swagger) 7 | - [1) Swagger 规范](#1-swagger-%e8%a7%84%e8%8c%83) 8 | - [2) Swagger UI](#2-swagger-ui) 9 | - [2.2 Swagger 集成](#22-swagger-%e9%9b%86%e6%88%90) 10 | - [3. 开发SDK](#3-%e5%bc%80%e5%8f%91sdk) 11 | 12 | ## 1. AppKey 13 | 当我们将WebAPI对外网公开供所有开发者调用时,除了注意前面讲到[安全控制](webapi-security.md)之外,我们更常用`AppKey`来对开发者进行管理。 14 | 15 | 当一个开发者想基于我们开放的API开发一套第三方程序时,需要先到我们服务器申请一个`AppKey`,这枚`AppKey`即作为这个开发者的唯一身份标志,这个开发者开发的所有程序请求我们的API时都需要携带这枚`AppKey`。当我们发现某个开发者的程序有非法操作时,我们就可以在服务端禁用这枚`AppKey`,那此开发者的所有应用程序的请求API时都会被拒绝,但普通用户使用其他开发者的应用并不受影响。 16 | 17 | 如新浪微博官方开放了API,我们基于官方API开发一套第三方App名为"MyWeibo"。所有新浪微博注册用户都可以使用"MyWeibo"进行任何操作。某天新浪微博官方发现使用"MyWeibo"的用户发送微博都被加了恶意广告,则新浪微博官方直接封禁"MyWeibo"的`AppKey`即可,"MyWeibo"程序不可用,但所有用户仍可正常使用新浪官方微博程序或其他任意客户端使用新浪微博。 18 | 19 | ## 2. OpenAPI 20 | ### 2.1 Swagger 21 | `Swagge`是一个与语言无关的规范,用于描述`REST API`。 `Swagger`项目已捐赠给`OpenAPI`计划,现在`Swagger`也称为`OpenAPI`。它允许计算机和人员了解服务的功能,而无需直接访问实现(源代码、网络访问、文档)。它解决了为 Web API 生成文档和帮助页的问题,具有诸如交互式文档、客户端 SDK 生成和 API 可发现性等优点。 22 | 23 | #### 1) Swagger 规范 24 | `Swagger`核心是`Swagger规范`,默认情况下体现为名为`swagger.json`的文档。它由`Swagger Tool Chain`(或其第三方实现)根据你的服务生成。它描述了 API 的功能以及使用 HTTP 对其进行访问的方式。它驱动`Swagger UI`,并由工具链用来启用发现和客户端代码生成。下面是一个缩减的`Swagger 规范`示例: 25 | 26 | ```json 27 | { 28 | "swagger": "2.0", 29 | "info": { 30 | "version": "v1", 31 | "title": "API V1" 32 | }, 33 | "basePath": "/", 34 | "paths": { 35 | "/api/Todo": { 36 | "get": { 37 | "tags": [ 38 | "Todo" 39 | ], 40 | "operationId": "ApiTodoGet", 41 | "consumes": [], 42 | "produces": [ 43 | "text/plain", 44 | "application/json", 45 | "text/json" 46 | ], 47 | "responses": { 48 | "200": { 49 | "description": "Success", 50 | "schema": { 51 | "type": "array", 52 | "items": { 53 | "$ref": "#/definitions/TodoItem" 54 | } 55 | } 56 | } 57 | } 58 | }, 59 | "post": { 60 | ... 61 | } 62 | }, 63 | "/api/Todo/{id}": { 64 | "get": { 65 | ... 66 | }, 67 | "put": { 68 | ... 69 | }, 70 | "delete": { 71 | ... 72 | }, 73 | "definitions": { 74 | "TodoItem": { 75 | "type": "object", 76 | "properties": { 77 | "id": { 78 | "format": "int64", 79 | "type": "integer" 80 | }, 81 | "name": { 82 | "type": "string" 83 | }, 84 | "isComplete": { 85 | "default": false, 86 | "type": "boolean" 87 | } 88 | } 89 | } 90 | }, 91 | "securityDefinitions": {} 92 | } 93 | ``` 94 | #### 2) Swagger UI 95 | `Swagger UI`提供了基于`Web`的用户界面方便用户直观的查看使用`Swagger`规范内容。Web UI 如下所示: 96 | ![Swagger UI](https://i.loli.net/2020/02/26/HXB2ARYdclVfMnr.png) 97 | 98 | 控制器中的每个公共操作方法都在`Swagger UI`中进行测试。单击方法名称可以展开该部分。添加所有必要的参数,然后单击“试试看!”。 99 | 100 | ![Swagger UI测试](https://i.loli.net/2020/02/26/qurDnSIFZ74mGe5.png) 101 | 102 | ### 2.2 Swagger 集成 103 | 目前在.Net Core平台下比较流行的`Swagger`集成工具有`Swashbuckle`和`NSwag`。两者均包含 Swagger UI 的嵌入式版本,因此可使用中间件注册调用将该嵌入式版本托管在 ASP.NET Core 应用中。 104 | 105 | * `Swashbuckle.AspNetCore` 是一个开源项目,用于生成 ASP.NET Core Web API 的 `Swagger` 文档。 106 | * `NSwag` 是另一个用于生成 Swagger 文档并将 `Swagger UI` 或 ReDoc 集成到 ASP.NET Core Web API 中的开源项目。 此外,`NSwag` 还提供了为 API 生成 C# 和 TypeScript 客户端代码的方法。 107 | 108 | 这里我们选择`NSwag`为例做简述。 109 | * 安装依赖Nuget包 `dotnet add package NSwag.AspNetCore` 110 | * 配置 Swagger。 111 | ```csharp 112 | public void ConfigureServices(IServiceCollection services) 113 | { 114 | services.AddMvc(); 115 | 116 | // 注册Swagger服务 117 | services.AddSwaggerDocument(); 118 | } 119 | 120 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 121 | { 122 | //启用中间件生成 Swagger规范 和 Swagger UI 123 | app.UseOpenApi(); 124 | app.UseSwaggerUi3(); 125 | 126 | app.UseMvc(); 127 | } 128 | ``` 129 | * 查看`Swagger UI`。默认访问 http://localhost:5000/swagger 130 | 131 | 132 | ## 3. 开发SDK 133 | 开放API除了完善的API文档外,为了更加友好的用户体验,服务提供商还会为常用的技术平台提供SDK包。 134 | 135 | SDK包的作用就是按照接口文档,把对所有API的HTTP请求用对应语言平台工具类的方式提供给开发者调用,使用SDK包之后开发者就可以像调用本地工具方法一样使用API,避免了自定义繁琐的HTTP请求等环节。 136 | 137 | 开发SDK方就是写一个工具包。完成SDK开发后可以共享出来共开发者使用,如.Net平台可以发不到Nuget。 138 | -------------------------------------------------------------------------------- /pages/webapi-restful.md: -------------------------------------------------------------------------------- 1 | # WebAPI和Restful 2 | 3 | 本文中如果不做说明,本章所有内容均基于Asp.Net Core WebAPI (2.2)。 4 | 5 | ## 1. WebAPI 6 | `WebAPI`是一种用来开发系统间接口、设备接口`API`的技术,基于`Http`协议,请求和返回格式结果默认是`json`格式。比`WCF`更简单、更通用,比 `WebService`更节省流量、更简洁。 7 | 8 | 普通`ASP.Net MVC`甚至`HttpHandler`也可以开发`API`,但 `WebAPI`是更加专注于此的技术,更专业。 9 | 10 | `Asp.Net WebAPI`和`Asp.Net MVC`有着非常密切的联系,`WebAPI`中可以复用`MVC`的路由、`ModelBinder`、`Filter` 等知识,但是只是相仿, 类名、命名空间等一般都不一样,用法也有一些差别。 11 | 12 | Asp.Net WebAPI 具有以下特点: 13 | * `Action`方法更专注于数据处理 14 | * 更适合于`Restful`风格 15 | * 不依赖于`Web服务器 `,可以`selfhost`,或者寄宿于控制台或服务程序等 16 | * 没有界面。`WebAPI`是接口开发技术,普通用户不会直接和`WebAPI`打交道 17 | 18 | ## 2. Restful 19 | `Http`设计之初是有 **“谓词语义”** 的。这里谓词是指`HttpMethod`,常用的包括`Get`、`Post`、`Put`、`Delete` 等。 20 | 21 | 通常情况下,使用`Get`获取数据,使用`Post`新增数据,使用`Put`修改数据,使用`Delete`删除数据。使用`Http`状态码表示处理结果。如 找不到资源使用`404`,没有权限使用`401`。此设计倾向于把所有业务操作抽象成对资源的CRUD操作。 22 | 23 | 如果`API`设计符合`Http`谓词语义规则,那么就可以称其符合`Restful`风格。Asp.Net WebAPI 设计之初就符合`Restful`风格。 24 | 25 | `Restful`风格设计具有以下优势: 26 | * 方便按类型操作做权限控制,如设置`Delete`权限只需处理`Delete`请求方式即可。 27 | * 不需要复杂的`Action`方法名,转而根据`HttpMethod`匹配请求 28 | * 充分利用`Http`状态码,不需要另做约定 29 | * 浏览器可以自动缓存`Get`请求,有利于系统优化 30 | 31 | `Restful`风格设计同时也有许多弊端。仅通过谓词语义和参数匹配请求理论性太强,许多业务很难完全拆分为CRUD操作,如用户登录同时更新最后登录时间。另外,`Http`状态码有限,在很多业务场景中不足以表述处理结果,如“密码错误”和“AppKey错误”。 32 | 33 | 由于以上问题,导致`Restful`设计在很多业务场景中使用不便,很多大公司`API`也鲜少都能满足`Restful`规范。因此我们的原则是,尽可能遵守`Restful`规范,灵活变通,不追求极端。 -------------------------------------------------------------------------------- /pages/webapi-security.md: -------------------------------------------------------------------------------- 1 | # API 安全控制 2 | 3 | * [1. 安全传输](#1-安全传输) 4 | * [2. 认证授权](#2-认证授权) 5 | * [2.1 Session](#21-session) 6 | * [2.2 JWT](#22-jwt) 7 | * [2.3 JWT 结构](#23-jwt-结构) 8 | * [2.4 JWT 操作](#24-jwt-操作) 9 | 10 | ## 1. 安全传输 11 | 12 | * 认证授权。对API做认证授权,每次请求接口需要携带认证信息,如`JWT Token` 13 | * 请求重放。重复请求一个接口,如充值接口。要避免重复业务处理。每次请求的时候都带着当前时间(时间戳),服务器端比 较一下如果这个时间和当前时间相差超过一定时间,则失效。因此最多被重放一段时间, 这个要求客户端的时间和服务器端的时间要保持相差不大。有些业务场景下要使用一次性验证。 14 | * HTTPS。如果API暴露于外网建议使用HTTPS协议,可以增加被抓包难度。 15 | 16 | ## 2. 认证授权 17 | 认证授权是两个过程,简单说认证是告诉服务器你是谁,授权是服务器告诉你你可以做什么。关于服务端管理用户授权有很多的权限管理方式,这里我们就不做阐述了。这里我们主要看用户认证。 18 | 19 | 目前常用的认证方式有`Session`和`JWT`两种。 下面我们主要介绍两种认证方式的基本原理,两种认证默认都集成到Asp.Net Core中,具体使用方式,参见如下 20 | * [Cookie-based 认证授权](authentication-cookie.md) 21 | * [JWT 认证授权](authentication-jwt.md) 22 | 23 | ### 2.1 Session 24 | 前后端分离通过`Restful API`进行数据交互时,验证用户的登录信息及权限最传统的方式,前端提交用户名密码,后端验证通过后将用户信息记录到称为`Session`的内存区域中,`Session`是一个`key-value`集合,`key`一般名称为`session_id`唯一标识用户的一次会话,服务端会把`session_id`记录到`Cookie`中并返回给客户端,之后客户端每次请求都会带上这个`session_id`,服务端则可以根据`session_id`值来识别用户。 25 | 26 | Session机制使用简单但也存在一些问题。 27 | * 内存开销。每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言`Session`都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。 28 | * 扩展性。用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。但是可以使用Redis等进程外`Session`来解决。 29 | * 安全性。如果我们的页面出现了`XSS`漏洞,由于 `Cookie`可以被`JavaScript`读取导致`session_id`泄露,而作为后端识别用户的标识,`Cookie`的泄露意味着用户信息不再安全。设置 `httpOnly`后`Cookie`将不能被 JS 读取,那么`XSS`注入的问题也基本不用担心了。浏览器会自动的把它加在请求的`header`当中,设置`secure`的话,`Cookie`就只允许通过`HTTPS`传输。`secure`选项可以过滤掉一些使用`HTTP`协议的`XSS`注入,但并不能完全阻止,二期还存在`XSRF`风险。当你浏览器开着这个页面的时候,另一个页面可以很容易的跨站请求这个页面的内容。因为`Cookie`默认被发了出去。 30 | 31 | ### 2.2 JWT 32 | `JWT`(JSON WEB TOKEN)是一个开放标准(RFC 7519)方法实现,用于通信双方之间安全认证。 33 | 34 | ![JWT验证流程图](https://i.loli.net/2020/02/26/sQbX5qamjrDlGSu.png) 35 | 36 | 1. 前端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个`HTTP POST`请求。建议的方式是通过SSL加密的传输(`https`协议),从而避免敏感信息被嗅探。 37 | 2. 服务端验证通过后将一些简单非敏感信息如`UserId`、`UserRole`等写到一个`Json`对象中并使用密钥签名后得到Token返回给客户端。 38 | 3. 前端可以将返回的`JWT Token`保存在`sessionStorage`上,退出登录时前端删除保存的JWT即可。 39 | 4. 前端每次请求将`JWT`放入`HTTP Header`中的Authorization位。(解决XSS和XSRF问题) 40 | 5. 后端验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。 41 | 6. 验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。 42 | 43 | 优势: 44 | * 跨语言支持 45 | * 可以存储简单信息 46 | * 易于扩展。不需要在服务端保存会话信息,易于应用扩展 47 | * SSO。认证信息存在客户端,子系统无需再进行认证 48 | 49 | 劣势: 50 | * 不能强制客户端下线 51 | * 不可存储敏感信息 52 | * `Secret`泄漏后不再安全 53 | 54 | ### 2.3 JWT 结构 55 | ![JWT结构图](https://i.loli.net/2020/02/26/yYPQqZsNBSz2wFC.jpg) 56 | 57 | 如上图所示,`JWT`由`Header`、`Payload`、`Signature`三部分构成。 58 | 59 | #### 1) Header 60 | 61 | 属性|含义 62 | :-|:- 63 | `alg`|声明加密的算法 通常使用`HMAC`或`SHA256` 64 | `typ`|声明类型,这里是`JWT` 65 | 66 | #### 2) Payload 67 | 这部分是我们存放信息的地方。 包含三个部分"标准注册声明"、"公共声明"、"私有声明"。 68 | 69 | 标准注册声明是固定名称,存放固定内容但不强制使用。 70 | 71 | 属性|含义 72 | :-|:- 73 | `iss`|签发者 74 | `sub`|所面向的用户 75 | `aud`|接收方 76 | `exp`|过期时间,这个过期时间必须要大于签发时间 77 | `nbf`|定义在什么时间之前,该jwt都是不可用的. 78 | `iat`|签发时间 79 | `jti`|唯一身份标识,主要用来作为**一次性token,从而回避重放攻击**。 80 | 81 | 公共声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息,但不建议添加敏感信息,因为该部分在客户端可解密。私有声明是提供者和消费者所共同定义的声明。 82 | 83 | #### 3) Signature 84 | 这部分是防篡改签名。`base64`编码`Header`和`Payload`后使用`.`连接组成的字符串,然后通过`Header`中声明的加密方式进行加盐`SecretKey`组合加密,然后就构成了签名。 85 | 86 | 对头部以及负载内容进行签名,可以防止内容被窜改。虽然`Header`和`Payload`可以使用`base64`解码后得到明文,但由于不知道`SecretKey`所以客户端或任何第三方篡改内容后无法获得正确签名,服务端校验签名不正确便会得知认证内容被篡改了进而拒绝请求。 87 | 88 | `SecretKey`保存在服务器端,用来进行`JWT`的签发和验证,务必确保其安全,一旦泄漏,任何人都可以自我签发`JWT`。 89 | 90 | ### 2.4 JWT 操作 91 | ```csharp 92 | public static string CreateJwt(Dictionary payload, string secret) 93 | { 94 | var builder = new JwtBuilder() 95 | .WithAlgorithm(new HMACSHA256Algorithm()) 96 | .WithSecret(secret); 97 | 98 | foreach (var key in payload.Keys) 99 | builder.AddClaim(key, payload[key]); 100 | 101 | return builder.Build(); 102 | } 103 | 104 | public static bool VerifyJwt(string token, string secret, out IDictionary payload) 105 | { 106 | try 107 | { 108 | payload = new JwtBuilder() 109 | .WithSecret(secret) 110 | .MustVerifySignature() 111 | .Decode>(token); 112 | 113 | return true; 114 | } 115 | catch (TokenExpiredException) 116 | { 117 | //JWT过期 118 | payload = null; 119 | return false; 120 | } 121 | catch (SignatureVerificationException) 122 | { 123 | //签名错误 124 | payload = null; 125 | return false; 126 | } 127 | } 128 | ``` 129 | 基于 https://github.com/jwt-dotnet/jwt 130 | 131 | 132 | >参考文档: 133 | * http://www.cnblogs.com/ldybyz/p/6943827.html 134 | * https://www.jianshu.com/p/576dbf44b2ae 135 | * https://lion1ou.win/2017/01/18/ -------------------------------------------------------------------------------- /pipeline.graffle/data.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colin-chang/netcore/b7661ed84d61fce13acb0bcc13d275da6d0083b6/pipeline.graffle/data.plist -------------------------------------------------------------------------------- /pipeline.graffle/image10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colin-chang/netcore/b7661ed84d61fce13acb0bcc13d275da6d0083b6/pipeline.graffle/image10.png -------------------------------------------------------------------------------- /pipeline.graffle/image11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colin-chang/netcore/b7661ed84d61fce13acb0bcc13d275da6d0083b6/pipeline.graffle/image11.png -------------------------------------------------------------------------------- /pipeline.graffle/image12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colin-chang/netcore/b7661ed84d61fce13acb0bcc13d275da6d0083b6/pipeline.graffle/image12.png -------------------------------------------------------------------------------- /pipeline.graffle/image19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colin-chang/netcore/b7661ed84d61fce13acb0bcc13d275da6d0083b6/pipeline.graffle/image19.png -------------------------------------------------------------------------------- /pipeline.graffle/image20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colin-chang/netcore/b7661ed84d61fce13acb0bcc13d275da6d0083b6/pipeline.graffle/image20.png -------------------------------------------------------------------------------- /pipeline.graffle/image21.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colin-chang/netcore/b7661ed84d61fce13acb0bcc13d275da6d0083b6/pipeline.graffle/image21.tiff -------------------------------------------------------------------------------- /pipeline.graffle/image22.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colin-chang/netcore/b7661ed84d61fce13acb0bcc13d275da6d0083b6/pipeline.graffle/image22.tiff -------------------------------------------------------------------------------- /pipeline.graffle/image23.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colin-chang/netcore/b7661ed84d61fce13acb0bcc13d275da6d0083b6/pipeline.graffle/image23.tiff -------------------------------------------------------------------------------- /pipeline.graffle/image24.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colin-chang/netcore/b7661ed84d61fce13acb0bcc13d275da6d0083b6/pipeline.graffle/image24.tiff -------------------------------------------------------------------------------- /pipeline.graffle/image25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colin-chang/netcore/b7661ed84d61fce13acb0bcc13d275da6d0083b6/pipeline.graffle/image25.png -------------------------------------------------------------------------------- /pipeline.graffle/image26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colin-chang/netcore/b7661ed84d61fce13acb0bcc13d275da6d0083b6/pipeline.graffle/image26.png -------------------------------------------------------------------------------- /pipeline.graffle/image4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colin-chang/netcore/b7661ed84d61fce13acb0bcc13d275da6d0083b6/pipeline.graffle/image4.jpg -------------------------------------------------------------------------------- /pipeline.graffle/image5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colin-chang/netcore/b7661ed84d61fce13acb0bcc13d275da6d0083b6/pipeline.graffle/image5.jpg -------------------------------------------------------------------------------- /pipeline.graffle/image6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colin-chang/netcore/b7661ed84d61fce13acb0bcc13d275da6d0083b6/pipeline.graffle/image6.jpg -------------------------------------------------------------------------------- /pipeline.graffle/image7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colin-chang/netcore/b7661ed84d61fce13acb0bcc13d275da6d0083b6/pipeline.graffle/image7.jpg -------------------------------------------------------------------------------- /pipeline.graffle/image9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colin-chang/netcore/b7661ed84d61fce13acb0bcc13d275da6d0083b6/pipeline.graffle/image9.png -------------------------------------------------------------------------------- /pipeline.graffle/preview.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colin-chang/netcore/b7661ed84d61fce13acb0bcc13d275da6d0083b6/pipeline.graffle/preview.jpeg -------------------------------------------------------------------------------- /website.css: -------------------------------------------------------------------------------- 1 | abbr { 2 | cursor: pointer; 3 | padding: 2px 4px; 4 | font-size: 90%; 5 | color: #c7254e; 6 | background-color: #f9f2f4; 7 | border-radius: 4px; 8 | } 9 | 10 | abbr[title] { 11 | border-bottom: none; 12 | text-decoration: none; 13 | } 14 | 15 | .markdown-section pre { 16 | line-height: 1.5; 17 | font-size: 14px; 18 | } 19 | 20 | .markdown-section *{ 21 | font-size: 14px; 22 | /*font-family: '微软雅黑';*/ 23 | } 24 | .markdown-section small{ 25 | font-size: 85%; 26 | } --------------------------------------------------------------------------------