微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!
来份ASP.NET Core尝尝
0x01、前言学习ASP.NET Core也有一段时间了,虽说很多内容知识点还是处于一知半解的状态,但是基本的,还是略懂一二。如果有错误,还望见谅。本文还是和之前一样,Demo+在Linux下运行(CentOS7+dotnetcore sdk)开发环境:win10+vs2015+sqlserver2014 0x02、demo新建一个ASP.NET Core Web Application项目--Catcher.EasyDemo.Website干掉Controllers文件夹。由于个人习惯问题,习惯性将Controller分离出来。新建三个Class Library项目:Catcher.EasyDemo.Controllers:剥离出来的ControllerCatcher.EasyDemo.DataAccess:数据访问Catcher.EasyDemo.Models:模型Controller项目需要添加MVC的引用:"Microsoft.AspNetCore.Mvc": "1.0.0"在Controllers中添加HomeController,内容和生成的是一样的。然后在Website中添加引用,这里有两种方式,一种是和平常一样的右键->添加引用,另一种是在project.json中的dependencies节点下面添加 "Catcher.EasyDemo.Controllers": "1.0.0-*",然后就会自动restore,完成之后就能正常跑起来了。(这里就不截图了)下面的话,在Models中添加一个Product类:1 namespace Catcher.EasyDemo.Models2 {3 public class Product4 {5 public int ProductId { get; set; }6 public string ProductName { get; set; }7 public string ProductSource { get; set; }8 public decimal ProductPrice { get; set; }9 }10 }在DataAccess中添加ProductDataAccess类,用于数据交互,里面有用到dapper,所以要添加引用,以及用到了读取json配置的方法,所以还要添加Microsoft.Extensions.Configuration的引用,同时还要添加Models的引用,方法上面已经说过了。这里没有用一些复杂的东西,就一个单例模式和一些简单的数据库操作。1 using Catcher.EasyDemo.Models;2 using Dapper;3 using Microsoft.Extensions.Configuration;4 using System.Collections.Generic;5 using System.Data;6 using System.Data.SqlClient;7 using System.IO;8 using System.Linq;910 namespace Catcher.EasyDemo.DataAccess11 {12 public sealed class ProductDataAccess13 {14 public static ProductDataAccess Instance15 {16 get17 {18 return Nested.instance;19 }20 }2122 class Nested23 {24 static Nested() { }25 internal static readonly ProductDataAccess instance = new ProductDataAccess();26 }2728 /// <summary>29 /// get the connection string form the appsettings.json30 /// </summary>31 /// <returns></returns>32 private string GetConnStr()33 {34 var builder = new ConfigurationBuilder();35 builder.SetBasePath(Directory.GetCurrentDirectory());36 builder.AddJsonFile("appsettings.json");37 var config = builder.Build();38 return config.GetConnectionString("dapperConn");39 }4041 /// <summary>42 /// open the connection43 /// </summary>44 /// <returns></returns>45 private SqlConnection OpenConnection()46 {47 SqlConnection conn = new SqlConnection(GetConnStr());48 conn.Open();49 return conn;50 }5152 /// <summary>53 /// get all products54 /// </summary>55 /// <returns></returns>56 public IList<Product> GetAll()57 {58 using (IDbConnection conn = OpenConnection())59 {60 string sql = @"SELECT [ProductId]61 ,[ProductName]62 ,[ProductSource]63 ,[ProductPrice]64 FROM [dbo].[Product]";65 return conn.Query<Product>(sql).ToList();66 }67 }6869 /// <summary>70 /// delete the product by product's id71 /// </summary>72 /// <param name="pid">id of the product</param>73 /// <returns></returns>74 public bool Delete(int pid)75 {76 using (IDbConnection conn = OpenConnection())77 {78 string sql = string.Format(@"DELETE FROM [dbo].[Product] WHERE [ProductId]={0} ", pid.ToString());79 return conn.Execute(sql) > 0;80 }81 }8283 /// <summary>84 /// add the product85 /// </summary>86 /// <param name="product">entity of the product</param>87 /// <returns></returns>88 public bool Add(Product product)89 {90 using (IDbConnection conn = OpenConnection())91 {92 string sql = string.Format(@"INSERT INTO [dbo].[Product]93 ([ProductName]94 ,[ProductSource]95 ,[ProductPrice])96 VALUES97 ('{0}','{1}',{2})", product.ProductName, product.ProductSource, product.ProductPrice);98 return conn.Execute(sql) > 0;99 }100 }101 }102 }然后在Controllers中添加一个ProductController,具体内容如下: 1 using Microsoft.AspNetCore.Mvc;2 using Catcher.EasyDemo.Models;3 using Catcher.EasyDemo.DataAccess;45 namespace Catcher.EasyDemo.Controllers6 {7 public class ProductController : Controller8 {9 /// <summary>10 /// Index11 /// </summary>12 /// <returns></returns>13 public IActionResult Index()14 {15 return View(ProductDataAcc
做个简单的RSS订阅(ASP.NET Core),节省自己的时间
0x01 前言因为每天上下班路上,午休前,都是看看新闻,但是种类繁多,又要自己找感兴趣的,所以肯定会耗费不少时间。虽说现在有很多软件也可以订阅一些自己喜欢的新闻,要安装到手机,还是挺麻烦的。所以就突发奇想,把一些新闻资源整合一下,省时省力,就根据RSS订阅,用h5结合ASP.NET Core做个小站点,方便一下自己,顺便拿dotNET Core练练手。开发环境:win10+vs2015+sqlite+redis(windows)部署环境:centos7+.net core sdk+jexus+redis(linux) 0x02 开发由于数据量不会大,所以就选用了sqlite,用起来还是挺方便的。RSS的内容都是来自各大新闻网站,不能每次访问都去请求一次,所以要缓存起来。数据库操作选择Dapper、UI框架选择了jquery-weui、还用到了一个js模板引擎art-template。单元测试用的xUnit.net。为了创建项目时,不添加太多东西,所以我是用Xamarin Studio建的项目,然后在VS2015上开发,需要什么东西,自己在添加上去,按需加载。下面是整体结构图:正常情况下的RSS订阅都是xml形式的,基本都很有规律:rss/channel/item下面就是具体的新闻了所以就简单用XDocument去处理这些内容:1 public async Task<IList<Models.Item>> GetItems(string url, int count)2 {3 string xmlStr = await GetXMLStringByUrl(url);4 XDocument doc = XDocument.Parse(xmlStr);5 //the channel image6 string imgUrl = doc.Element("rss").Element("channel").Element("image").Element("url").Value;7 //the rss item8 var results = doc.Element("rss").Element("channel").Elements("item").Select(x => new Models.Item9 {10 title = x.Element("title").Value,11 link = x.Element("link").Value,12 description = x.Element("description").Value,13 pubDate = x.Element("pubDate").Value,14 guid = x.Element("guid").Value,15 ImgUrl = imgUrl16 });1718 return results.Take(count).ToList();19 } 在处理依赖注入时,用的是微软自家的,并没有用Autofac,具体如下。1 public void ConfigureServices(IServiceCollection services)2 {3 services.AddMvc();4 services.AddScoped<IRSSItemRepository, RSSItemRepository>();5 services.AddScoped<IUserRepository, UserRepository>();6 services.AddScoped<IRSSSourceRepository, RSSSourceRepository>();7 //the cache8 services.AddScoped<ICache, RedisCache>();9 } 这两个可以说是这个RSS订阅最重要的两个地方。一个从网络拿数据回来,一个从数据库中拿数据出来。开发的时候,由于要用到redis,我本人是在电脑上装了一个windows版本的,方便调试。操作用的是StackExchange.Redis,自己曾简单封装了一些基本用法,不过这个还不是正式版。 其他部分应该都是比较简单,所以就不说明了。下面来看看在CentOS7下部署。 0x03 部署上一篇来份dotNETCore尝尝,用到的部署方式是纯粹的.NET Core SDK的方式。今天换一种方式来试试用Jexus+.NET Core SDK来部署(当然,也有不安装.NET Core的部署方式)。先把刚才的项目发布一下:这个程序集是不是太多了啊,希望微软能在下一版本改进一下这个。把这些东西上传到 /var/www/easyrss 中在jexus下面的siteconf文件夹添加一个配置easyrss多加了一行AppHost,就可以让jexus支持asp.net core了,更多关于这个的介绍可以参考http://linuxdot.net/bbsfile-4459加上这一句之后,启动这个网站就可以了。AppHost={CmdLine=dotnet /var/www/easyrss/Catcher.EasyRSS.WebSite.dll;AppRoot=/var/www/easyrss;port=5000}启动easyrss这个网站:./jws start easyrss还有重要的一步,启动redis,不然死活都跑不起来。启动redis: ./redis-server打开浏览器就能看到效果了 操作相当的简单吧。赶紧动手试试吧。再放几张效果图(毕竟也花了大半天时间)              0x04 总结1、部署成功后遇到过一个问题,样式和脚本无法正常加载。请求外网的资源比较耗时(网络渣),只好将生产环境的样式换成本地加载了,然后就正常了。 2、ASP.NET Core和ASP.NET MVC在开发的时候,没有太大的区别,应该是很容易过渡的,应该慢慢的也会有更多的用dotNET Core来开发了。 
用Middleware给ASP.NET Core Web API添加自己的授权验证
Web API,是一个能让前后端分离、解放前后端生产力的好东西。不过大部分公司应该都没能做到完全的前后端分离。API的实现方式有很多,可以用ASP.NET Core、也可以用ASP.NET Web API、ASP.NET MVC、NancyFx等。说到Web API,不同的人有不同的做法,可能前台、中台和后台各一个api站点,也有可能一个模块一个api站点,也有可能各个系统共用一个api站点,当然这和业务有必然的联系。安全顺其自然的成为Web API关注的重点之一。现在流行的OAuth 2.0是个很不错的东西,不过本文是暂时没有涉及到的,只是按照最最最原始的思路做的一个授权验证。在之前的MVC中,我们可能是通过过滤器来处理这个身份的验证,在Core中,我自然就是选择Middleware来处理这个验证。下面开始本文的正题:先编写一个能正常运行的api,不进行任何的权限过滤。1 using Dapper;2 using Microsoft.AspNetCore.Mvc;3 using System.Data;4 using System.Linq;5 using System.Threading.Tasks;6 using WebApi.CommandText;7 using WebApi.Common;8 using Common;910 namespace WebApi.Controllers11 {12 [Route("api/[controller]")]13 public class BookController : Controller14 {1516 private DapperHelper _helper;17 public BookController(DapperHelper helper)18 {19 this._helper = helper;20 }2122 // GET: api/book23 [HttpGet]24 public async Task<IActionResult> Get()25 {26 var res = await _helper.QueryAsync(BookCommandText.GetBooks);27 CommonResult<Book> json = new CommonResult<Book>28 {29 Code = "000",30 Message = "ok",31 Data = res32 };33 return Ok(json);34 }3536 // GET api/book/537 [HttpGet("{id}")]38 public IActionResult Get(int id)39 {40 DynamicParameters dp = new DynamicParameters();41 dp.Add("@Id", id, DbType.Int32, ParameterDirection.Input);42 var res = _helper.Query<Book>(BookCommandText.GetBookById, dp, null, true, null, CommandType.StoredProcedure).FirstOrDefault();43 CommonResult<Book> json = new CommonResult<Book>44 {45 Code = "000",46 Message = "ok",47 Data = res48 };49 return Ok(json);50 }5152 // POST api/book53 [HttpPost]54 public IActionResult Post([FromForm]PostForm form)55 {56 DynamicParameters dp = new DynamicParameters();57 dp.Add("@Id", form.Id, DbType.Int32, ParameterDirection.Input);58 var res = _helper.Query<Book>(BookCommandText.GetBookById, dp, null, true, null, CommandType.StoredProcedure).FirstOrDefault();59 CommonResult<Book> json = new CommonResult<Book>60 {61 Code = "000",62 Message = "ok",63 Data = res64 };65 return Ok(json);66 }6768 }6970 public class PostForm71 {72 public string Id { get; set; }73 }7475 }api这边应该没什么好说的,都是一些常规的操作,会MVC的应该都可以懂。主要是根据id获取图书信息的方法(GET和POST)。这是我们后面进行单元测试的两个主要方法。这样部署得到的一个API站点,是任何一个人都可以访问http://yourapidomain.com/api/book 来得到相关的数据。现在我们要对这个api进行一定的处理,让只有权限的站点才能访问它。下面就是编写自定义的授权验证中间件了。Middleware这个东西大家应该都不会陌生了,OWIN出来的时候就有中间件这样的概念了,这里就不展开说明,在ASP.NET Core中是如何实现这个中间件的可以参考官方文档 Middleware。 我们先定义一个我们要用到的option,ApiAuthorizedOptions1 namespace WebApi.Middlewares2 {3 public class ApiAuthorizedOptions4 {5 //public string Name { get; set; }67 public string EncryptKey { get; set; }89 public int ExpiredSecond { get; set; }10 }11 }option内容比较简单,一个是EncryptKey ,用于对我们的请求参数进行签名,另一个是ExpiredSecond ,用于检验我们的请求是否超时。与之对应的是在appsettings.json中设置的ApiKey节点1 "ApiKey": {2 //"username": "123",3 //"password": "123",4 "EncryptKey": "@*api#%^@",5 "ExpiredSecond": "300"6 }有了option,下面就可以编写middleware的内容了我们的api中就实现了get和post的方法,所以这里也就对get和post做了处理,其他http method,有需要的可以自己补充。这里的验证主要是下面的几个方面:1.参数是否被篡改2.请求是否已经过期3.请求的应用是否合法主检查方法:Check1 /// <summary>2 /// the main check method3 /// </summary>4 /// <param name="context"></param>5 /// <param name="requestInfo"></param>6 /// <returns></returns>7 private async Task Check(HttpContext context, RequestInfo requestInfo)8 {9 string computeSinature = HMACMD5Helper.GetEncryptResult($"{requestInfo.ApplicationId}-{requestInfo.Timestamp}-{requestInfo.Nonce}", _options.EncryptKey);10 double tmpTimestamp;11 if (computeSinature.Equals(requestInfo.Sinature) &&12 double.TryParse(requestInfo.Timestamp, out tmpTimestamp))13 {14 if (CheckExpiredTime(tmpTimestamp, _options.ExpiredSecond))15 {16 await ReturnTimeOut(context);17 }18 else19 {20 await CheckApplication(context, requestInfo.ApplicationId, requestInfo.ApplicationPassword);21 }22 }23 else24 {25 await ReturnNoAuthorized(context);26 }27 }Check方法带了2个参数,一个是当前的httpcontext对象和请求的内容信息,当签名一致,并且时间戳能转化成double时才去校验是否超时和Appli
用JWT来保护我们的ASP.NET Core Web API
 在上一篇博客中,自己动手写了一个Middleware来处理API的授权验证,现在就采用另外一种方式来处理这个授权验证的问题,毕竟现在也有不少开源的东西可以用,今天用的是JWT。什么是JWT呢?JWT的全称是JSON WEB TOKENS,是一种自包含令牌格式。官方网址:https://jwt.io/,或多或少应该都有听过这个。先来看看下面的两个图:站点是通过RPC的方式来访问api取得资源的,当站点是直接访问api,没有拿到有访问权限的令牌,那么站点是拿不到相关的数据资源的。就像左图展示的那样,发起了请求但是拿不到想要的结果;当站点先去授权服务器拿到了可以访问api的access_token(令牌)后,再通过这个access_token去访问api,api才会返回受保护的数据资源。这个就是基于令牌验证的大致流程了。可以看出授权服务器占着一个很重要的地位。下面先来看看授权服务器做了些什么并如何来实现一个简单的授权。做了什么?授权服务器在整个过程中的作用是:接收客户端发起申请access_token的请求,并校验其身份的合法性,最终返回一个包含access_token的json字符串。如何实现?我们还是离不开中间件这个东西。这次我们写了一个TokenProviderMiddleware,主要是看看invoke方法和生成access_token的方法。1 /// <summary>2 /// invoke the middleware3 /// </summary>4 /// <param name="context"></param>5 /// <returns></returns>6 public async Task Invoke(HttpContext context)7 {8 if (!context.Request.Path.Equals(_options.Path, StringComparison.Ordinal))9 {10 await _next(context);11 }1213 // Request must be POST with Content-Type: application/x-www-form-urlencoded14 if (!context.Request.Method.Equals("POST")15 || !context.Request.HasFormContentType)16 {17 await ReturnBadRequest(context);18 }19 await GenerateAuthorizedResult(context);20 } Invoke方法其实是不用多说的,不过我们这里是做了一个控制,只接收POST请求,并且是只接收以表单形式提交的数据,GET的请求和其他contenttype类型是属于非法的请求,会返回bad request的状态。下面说说授权中比较重要的东西,access_token的生成。1 /// <summary>2 /// get the jwt3 /// </summary>4 /// <param name="username"></param>5 /// <returns></returns>6 private string GetJwt(string username)7 {8 var now = DateTime.UtcNow;910 var claims = new Claim[]11 {12 new Claim(JwtRegisteredClaimNames.Sub, username),13 new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),14 new Claim(JwtRegisteredClaimNames.Iat, now.ToUniversalTime().ToString(),15 ClaimValueTypes.Integer64)16 };1718 var jwt = new JwtSecurityToken(19 issuer: _options.Issuer,20 audience: _options.Audience,21 claims: claims,22 notBefore: now,23 expires: now.Add(_options.Expiration),24 signingCredentials: _options.SigningCredentials);25 var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);2627 var response = new28 {29 access_token = encodedJwt,30 expires_in = (int)_options.Expiration.TotalSeconds,31 token_type = "Bearer"32 };33 return JsonConvert.SerializeObject(response, new JsonSerializerSettings { Formatting = Formatting.Indented });34 } claims包含了多个claim,你想要那几个,可以根据自己的需要来添加,JwtRegisteredClaimNames是一个结构体,里面包含了所有的可选项。1 public struct JwtRegisteredClaimNames2 {3 public const string Acr = "acr";4 public const string Actort = "actort";5 public const string Amr = "amr";6 public const string AtHash = "at_hash";7 public const string Aud = "aud";8 public const string AuthTime = "auth_time";9 public const string Azp = "azp";10 public const string Birthdate = "birthdate";11 public const string CHash = "c_hash";12 public const string Email = "email";13 public const string Exp = "exp";14 public const string FamilyName = "family_name";15 public const string Gender = "gender";16 public const string GivenName = "given_name";17 public const string Iat = "iat";18 public const string Iss = "iss";19 public const string Jti = "jti";20 public const string NameId = "nameid";21 public const string Nbf = "nbf";22 public const string Nonce = "nonce";23 public const string Prn = "prn";24 public const string Sid = "sid";25 public const string Sub = "sub";26 public const string Typ = "typ";27 public const string UniqueName = "unique_name";28 public const string Website = "website";29 }JwtRegisteredClaimNames还需要一个JwtSecurityToken对象,这个对象是至关重要的。有了时间、Claims和JwtSecurityToken对象,只要调用JwtSecurityTokenHandler的WriteToken就可以得到类似这样的一个加密之后的字符串,这个字符串由3部分组成用‘.’分隔。每部分代表什么可以去官网查找。eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ最后我们要用json的形式返回这个access_token、access_token的有效时间和一些其他的信息。还需要在Startup的Configure方法中去调用我们的中间件。1 var audienceConfig = Configuration.GetSection("Audience");2 var symmetricKeyAsBase64 = audienceConfig["Secret"];3 var ke
初探CSRF在ASP.NET Core中的处理方式
前言前几天,有个朋友问我关于AntiForgeryToken问题,由于对这一块的理解也并不深入,所以就去研究了一番,梳理了一下。在梳理之前,还需要简单了解一下背景知识。AntiForgeryToken 可以说是处理/预防CSRF的一种处理方案。那么什么是CSRF呢?CSRF(Cross-site request forgery)是跨站请求伪造,也被称为One Click Attack或者Session Riding,通常缩写为CSRF或者XSRF,是一种对网站的恶意利用。简单理解的话就是:有人盗用了你的身份,并且用你的名义发送恶意请求。最近几年,CSRF处于不温不火的地位,但是还是要对这个小心防范!更加详细的内容可以参考维基百科:Cross-site request forgery下面从使用的角度来分析一下CSRF在 ASP.NET Core中的处理,个人认为主要有下面两大块视图层面控制器层面视图层面用法@Html.AntiForgeryToken()在视图层面的用法相对比较简单,用的还是HtmlHelper的那一套东西。在Form表单中加上这一句就可以了。原理浅析当在表单中添加了上面的代码后,页面会生成一个隐藏域,隐藏域的值是一个生成的token(防伪标识),类似下面的例子<input name="__RequestVerificationToken" type="hidden" value="CfDJ8FBn4LzSYglJpE6Q0fWvZ8WDMTgwK49lDU1XGuP5-5j4JlSCML_IDOO3XDL5EOyI_mS2Ux7lLSfI7ASQnIIxo2ScEJvnABf9v51TUZl_iM2S63zuiPK4lcXRPa_KUUDbK-LS4HD16pJusFRppj-dEGc" />其中的name="__RequestVerificationToken"是定义的一个const变量,value=XXXXX是根据一堆东西进行base64编码,并对base64编码后的内容进行简单处理的结果,具体的实现可以参见Base64UrlTextEncoder.cs生成上面隐藏域的代码在AntiforgeryExtensions这个文件里面,github上的源码文件:AntiforgeryExtensions.cs其中重点的方法如下:public void WriteTo(TextWriter writer, HtmlEncoder encoder){writer.Write("<input name="");encoder.Encode(writer, _fieldName);writer.Write("" type="hidden" value="");encoder.Encode(writer, _requestToken);writer.Write("" />");}相当的清晰明了!控制器层面用法[ValidateAntiForgeryToken][AutoValidateAntiforgeryToken][IgnoreAntiforgeryToken]这三个都是可以基于类或方法的,所以我们只要在某个控制器或者是在某个Action上面加上这些Attribute就可以了。[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]原理浅析本质是Filter(过滤器),验证上面隐藏域的value过滤器实现:ValidateAntiforgeryTokenAuthorizationFilter和AutoValidateAntiforgeryTokenAuthorizationFilter其中 AutoValidateAntiforgeryTokenAuthorizationFilter是继承了ValidateAntiforgeryTokenAuthorizationFilter,只重写了其中的ShouldValidate方法。下面贴出ValidateAntiforgeryTokenAuthorizationFilter的核心方法:public class ValidateAntiforgeryTokenAuthorizationFilter : IAsyncAuthorizationFilter, IAntiforgeryPolicy{public async Task OnAuthorizationAsync(AuthorizationFilterContext context){if (context == null){throw new ArgumentNullException(nameof(context));}if (IsClosestAntiforgeryPolicy(context.Filters) && ShouldValidate(context)){try{await _antiforgery.ValidateRequestAsync(context.HttpContext);}catch (AntiforgeryValidationException exception){_logger.AntiforgeryTokenInvalid(exception.Message, exception);context.Result = new BadRequestResult();}}}}完整实现可参见github源码:ValidateAntiforgeryTokenAuthorizationFilter.cs当然这里的过滤器只是一个入口,相关的验证并不是在这里实现的。而是在Antiforgery这个项目上,其实说这个模块可能会更贴切一些。由于是面向接口的编程,所以要知道具体的实现,就要找到对应的实现类才可以。在Antiforgery这个项目中,有这样一个扩展方法AntiforgeryServiceCollectionExtensions,里面告诉了我们相对应的实现是DefaultAntiforgery这个类。其实Nancy的源码看多了,看一下类的命名就应该能知道个八九不离十。services.TryAddSingleton<IAntiforgery, DefaultAntiforgery>();其中还涉及到了IServiceCollection,但这不是本文的重点,所以不会展开讲这个,只是提出它在 .net core中是一个重要的点。好了,回归正题!要验证是否是合法的请求,自然要先拿到要验证的内容。var tokens = await _tokenStore.GetRequestTokensAsync(httpContext);它是从Cookie中拿到一个指定的前缀为.AspNetCore.Antiforgery.的Cookie,并根据这个Cookie进行后面相应的判断。下面是验证的具体实现:public bool TryValidateTokenSet(HttpContext httpContext,AntiforgeryToken cookieToken,AntiforgeryToken requestToken,out string message){//去掉了部分非空的判断// Do the tokens have the correct format?if (!cookieToken.IsCookieToken || requestToken.IsCookieToken){message = Resources.AntiforgeryToken_TokensSwapped;return false;}// Are the security tokens embedded in each incoming token identical?if (!object.Equals(cookieToken.SecurityToken, requestToken.SecurityToken)){message = Resources.AntiforgeryToken_SecurityTokenMismatch;return false;}// Is the incoming token meant for the current user?var currentUsername = string.Empty;BinaryBlob currentClaimUid = null;var authenticatedIdentity = GetAuthenticatedIdentity(httpContext.User);if (authenticatedIdentity != null){currentClaimUid = GetClaimUidBlob(_claimUidExtractor.ExtractClaimUid(httpContext.User));if (currentClaimUid == null){currentUsername = authenticatedIdentity.Name ?? string.Empty;}}// OpenID and other similar authentication schemes use URIs for the username.// These should be treated as case-sensitive.var comparer = StringComparer.OrdinalIgnoreCase;if (currentUsername.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||currentUsername.StartsWith("https://", StringComparison.OrdinalIgnoreCase)){comparer = StringComparer.Ordinal;}if (!comparer.Equals(requestToken.Username, currentUsername)){message = Resources.FormatAntiforgeryToken_UsernameMismatch(requestToken.Username, currentUsername);return false;}if (!object.Equals(requestToken.ClaimUid, currentClaimUid)){message = Resources.AntiforgeryToken_ClaimUidMismatch;return false;}// Is the AdditionalData valid?if (_additionalDataProvider != null &&!_additionalDataProvider.ValidateAdditionalData(httpContext, requestToken.AdditionalData)){message = Resources.AntiforgeryToken_AdditionalDataCheckFailed;return false;}message = null;return true;}注:验证前还有一个反序列化的过程,这个反序列化就是从Cookie中拿到要判断的cookietoken和requesttoken如何使用前面粗略介绍了一下其内部的实现,下面再用个简单的例子来看看具体的使用情况:使用一:常规的Form表单先在视图添加一个Form表单<form id="form1" action="/home/ant
在ASP.NET Core中使用AOP来简化缓存操作
前言关于缓存的使用,相信大家都是熟悉的不能再熟悉了,简单来说就是下面一句话。优先从缓存中取数据,缓存中取不到再去数据库中取,取到了在扔进缓存中去。然后我们就会看到项目中有类似这样的代码了。public Product Get(int productId){var product = _cache.Get($"Product_{productId}");if(product == null){product = Query(productId);_cache.Set($"Product_{productId}",product ,10);}return product;}然而在初期,没有缓存的时候,可能这个方法就一行代码。public Product Get(int productId){return Query(productId);}随着业务的不断发展,可能会出现越来越多类似第一段的示例代码。这样就会出现大量“重复的代码”了!显然,我们不想让这样的代码到处都是!基于这样的情景下,我们完全可以使用AOP去简化缓存这一部分的代码。大致的思路如下 :在某个有返回值的方法执行前去判断缓存中有没有数据,有就直接返回了;如果缓存中没有的话,就是去执行这个方法,拿到返回值,执行完成之后,把对应的数据写到缓存中去,下面就根据这个思路来实现。本文分别使用了Castle和AspectCore来进行演示。这里主要是做了做了两件事自动处理缓存的key,避免硬编码带来的坑通过Attribute来简化缓存操作下面就先从Castle开始吧!使用Castle来实现一般情况下,我都会配合Autofac来实现,所以这里也不例外。我们先新建一个ASP.NET Core 2.0的项目,通过Nuget添加下面几个包(当然也可以直接编辑csproj来完成的)。<PackageReference Include="Autofac" Version="4.6.2" /><PackageReference Include="Autofac.Extensions.DependencyInjection" Version="4.2.0" /><PackageReference Include="Autofac.Extras.DynamicProxy" Version="4.2.1" /><PackageReference Include="Castle.Core" Version="4.2.1" />然后做一下前期准备工作1.缓存的使用定义一个ICachingProvider和其对应的实现类MemoryCachingProvider简化了一下定义,就留下读和取的操作。public interface ICachingProvider{object Get(string cacheKey);void Set(string cacheKey, object cacheValue, TimeSpan absoluteExpirationRelativeToNow);}public class MemoryCachingProvider : ICachingProvider{private IMemoryCache _cache;public MemoryCachingProvider(IMemoryCache cache){_cache = cache;}public object Get(string cacheKey){return _cache.Get(cacheKey);}public void Set(string cacheKey, object cacheValue, TimeSpan absoluteExpirationRelativeToNow){_cache.Set(cacheKey, cacheValue, absoluteExpirationRelativeToNow);}}2.定义一个Attribute这个Attribute就是我们使用时候的关键了,把它添加到要缓存数据的方法中,即可完成缓存的操作。这里只用了一个绝对过期时间(单位是秒)来作为演示。如果有其他缓存的配置,也是可以往这里加的。[AttributeUsage(AttributeTargets.Method, Inherited = true)]public class QCachingAttribute : Attribute{public int AbsoluteExpiration { get; set; } = 30;//add other settings ...}3.定义一个空接口这个空接口只是为了做一个标识的作用,为了后面注册类型而专门定义的。public interface IQCaching{}4.定义一个与缓存键相关的接口定义这个接口是针对在方法中使用了自定义类的时候,识别出这个类对应的缓存键。public interface IQCachable{string CacheKey { get; }}准备工作就这4步(AspectCore中也是要用到的),下面我们就是要去做方法的拦截了(拦截器)。拦截器首先要继承并实现IInterceptor这个接口。public class QCachingInterceptor : IInterceptor{private ICachingProvider _cacheProvider;public QCachingInterceptor(ICachingProvider cacheProvider){_cacheProvider = cacheProvider;}public void Intercept(IInvocation invocation){var qCachingAttribute = this.GetQCachingAttributeInfo(invocation.MethodInvocationTarget ?? invocation.Method);if (qCachingAttribute != null){ProceedCaching(invocation, qCachingAttribute);}else{invocation.Proceed();}}}有两点要注意:因为要使用缓存,所以这里需要我们前面定义的缓存操作接口,并且在构造函数中进行注入。Intercept方法是拦截的关键所在,也是IInterceptor接口中的唯一定义。Intercept方法其实很简单,获取一下当前执行方法是不是有我们前面自定义的QCachingAttribute,有的话就去处理缓存,没有的话就是仅执行这个方法而已。下面揭开ProceedCaching方法的面纱。private void ProceedCaching(IInvocation invocation, QCachingAttribute attribute){var cacheKey = GenerateCacheKey(invocation);var cacheValue = _cacheProvider.Get(cacheKey);if (cacheValue != null){invocation.ReturnValue = cacheValue;return;}invocation.Proceed();if (!string.IsNullOrWhiteSpace(cacheKey)){_cacheProvider.Set(cacheKey, invocation.ReturnValue, TimeSpan.FromSeconds(attribute.AbsoluteExpiration));}}这个方法,就是和大部分操作缓存的代码一样的写法了!注意下面几个地方invocation.Proceed()表示执行当前的方法invocation.ReturnValue是要执行后才会有值的。在每次执行前,都会依据当前执行的方法去生成一个缓存的键。下面来看看生成缓存键的操作。这里生成的依据是当前执行方法的名称,参数以及该方法所在的类名。生成的代码如下:private string GenerateCacheKey(IInvocation invocation){var typeName = invocation.TargetType.Name;var methodName = invocation.Method.Name;var methodArguments = this.FormatArgumentsToPartOfCacheKey(invocation.Arguments);return this.GenerateCacheKey(typeName, methodName, methodArguments);}//拼接缓存的键private string GenerateCacheKey(string typeName, string methodName, IList<string> parameters){var builder = new StringBuilder();builder.Append(typeName);builder.Append(_linkChar);builder.Append(methodName);builder.Append(_linkChar);foreach (var param in parameters){builder.Append(param);builder.Append(_linkChar);}return builder.ToString().TrimEnd(_linkChar);}private IList<string> FormatArgumentsToPartOfCacheKey(IList<object> methodArguments, int maxCount = 5){return methodArguments.Select(this.GetArgumentValue).Take(maxCount).ToList();}//处理方法的参数,可根据情况自行调整private string GetArgumentValue(object arg){if (arg is int || arg is long || arg is string)return arg.ToString();if (arg is DateTime)return ((DateTime)arg).ToString("yyyyMMddHHmmss");if (arg is IQCachable)return ((IQCachable)arg).CacheKey;return null;}这里要注意的是GetArgumentValue这个方法,因为一个方法的参数有可能是基本的数据类型,也有可能是自己定义的类。对于自己定义的类,必须要去实现IQCachable这个接口,并且要定义好键要取的值!如果说,在一个方法的参数中,有一个自定义的类,但是这个类却没有实现IQCachable这个接口,那么生成的缓存键将不会包含这个参数的信息。举个生成的例子:MyClass:MyMethod:100:abc:999到这里,我们缓存的拦截器就已经完成了。下面是删除了注释的代码(可去github上查看完整的代码)public class QCachingInterceptor : IInterceptor{private ICachingProvider _cacheProvider;private char _linkChar = ':';public QCachingInterceptor(ICachingProvider cacheProvider){_cacheProvider = cacheProvi
在.NET Core中处理一个接口多个不同实现的依赖注入问题
前言近段时间在准备公司的技术分享,所以这段时间将大部分时间放在准备分享内容上去了。博客也就停了一下下。在.NET Core中处理依赖注入问题时,往往是定义好了一个操作规范的接口,会有N多个基于不同技术的实现,根据实际情况在项目中去使用某一个实现。但是偶尔会出现这样的情况,在某一个地方,需要同时使用到两种或两种以上的实现,这个时候我们要怎么处理呢?借助Autofac等第三方组件时,是可以很容易的实现,但是在写一些基础类库时会避免直接引用太多依赖组件。所以这里是只用微软自带的DI(Microsoft.Extensions.DependencyInjection)去处理。例子引入现在有一个接口和两个实现类。public interface IDemoService{string Get();}public class DemoServiceA : IDemoService{public string Get(){return "Service A";}}public class DemoServiceB : IDemoService{public string Get(){return "Service B";}}常规的方法,我们先在Startup中的ConfigureServices方法中添加我们的service。public void ConfigureServices(IServiceCollection services){services.AddSingleton<IDemoService, DemoServiceA>();services.AddSingleton<IDemoService, DemoServiceB>();services.AddMvc();}然后在控制器中使用private IDemoService _serviceA;private IDemoService _serviceB;public ValuesController(IDemoService serviceA, IDemoService serviceB){_serviceA = serviceA;_serviceB = serviceB;}// GET api/values[HttpGet]public string Get(){return $"{_serviceA.Get()}-{_serviceB.Get()}";}我们的预期结果是:Service A-Service B,可是上面代码的实际结果却并不像我们想的那么简单!!可以看到这里输出的都是Service B,连Service A的影子都没有看到。其实,从代码都可以看出来,它只能拿到其中一个Service的实现类!那么我们要息怎样处理才能达到我们想要的效果呢?其实思路比较简单,上面导致不能拿到对应实现类,本质上来讲应该说是它区分不了那个才是想要的!我们想个办法让它能区分就好了。处理方法给我们的Service起个别名!先是Startup中的ConfigureServices方法。public void ConfigureServices(IServiceCollection services){services.AddSingleton<DemoServiceA>();services.AddSingleton<DemoServiceB>();services.AddSingleton(factory =>{Func<string, IDemoService> accesor = key =>{if (key.Equals("ServiceA")){return factory.GetService<DemoServiceA>();}else if (key.Equals("ServiceB")){return factory.GetService<DemoServiceB>();}else{throw new ArgumentException($"Not Support key : {key}");}};return accesor;});services.AddMvc();}这里并没有直接向上面那样一次性指定接口和对应的实现类,而是用了AddSingleton的另一个重载方法。先将实现类注册一下然后再注册一下Func<string, IDemoService>先来说说这个Func<string, IDemoService>里面的string和IDemoService都分别代表什么。string 毫无疑问就是我们上面说到的别名IDemoService 这个就是我们要用的Service核心在于,factory参数是IServiceProvider类型的!所以我们可以根据这个factory去找到我们前面注册的实现类。这样解释一下,是不是就清晰了呢?然后再来看看在控制器上面怎么用。private IDemoService _serviceA;private IDemoService _serviceB;private readonly Func<string, IDemoService> _serviceAccessor;public ValuesController(Func<string, IDemoService> serviceAccessor){this._serviceAccessor = serviceAccessor;_serviceA = _serviceAccessor("ServiceA");_serviceB = _serviceAccessor("ServiceB");}// GET api/values[HttpGet]public string Get(){return $"{_serviceA.Get()}-{_serviceB.Get()}";}最后看看结果是不是和我们的预期一样。结果与预期一致。总结一对一,或许是最好的方法,也是最为理想的,这样能避开很多不必要的问题。但是现实中总会出现特殊情况,面对这些特殊情况,我们也是需要能够重容的面对。如果您有更好的处理方法,也可以留言讨论。文中的示例代码 DIDemo
谈谈在.NET Core中使用Redis和Memcached的序列化问题
前言在使用分布式缓存的时候,都不可避免的要做这样一步操作,将数据序列化后再存储到缓存中去。序列化这一操作,或许是显式的,或许是隐式的,这个取决于使用的package是否有帮我们做这样一件事。本文会拿在.NET Core环境下使用Redis和Memcached来当例子说明,其中,Redis主要是用StackExchange.Redis,Memcached主要是用EnyimMemcachedCore。先来看看一些我们常用的序列化方法。常见的序列化方法或许,比较常见的做法就是将一个对象序列化成byte数组,然后用这个数组和缓存服务器进行交互。关于序列化,业界有不少算法,这些算法在某种意义上表现的结果就是速度和体积这两个问题。其实当操作分布式缓存的时候,我们对这两个问题其实也是比较看重的!在同等条件下,序列化和反序列化的速度,可以决定执行的速度是否能快一点。序列化的结果,也就是我们要往内存里面塞的东西,如果能让其小一点,也是能节省不少宝贵的内存空间。当然,本文的重点不是去比较那种序列化方法比较牛逼,而是介绍怎么结合缓存去使用,也顺带提一下在使用缓存时,序列化可以考虑的一些点。下面来看看一些常用的序列化的库:System.Runtime.Serialization.Formatters.BinaryNewtonsoft.Jsonprotobuf-netMessagePack-CSharp....在这些库中System.Runtime.Serialization.Formatters.Binary是.NET类库中本身就有的,所以想在不依赖第三方的packages时,这是个不错的选择。Newtonsoft.Json应该不用多说了。protobuf-net是.NET实现的Protocol Buffers。MessagePack-CSharp是极快的MessagePack序列化工具。这几种序列化的库也是笔者平时有所涉及的,还有一些不熟悉的就没列出来了!在开始之前,我们先定义一个产品类,后面相关的操作都是基于这个类来说明。public class Product{public int Id { get; set; }public string Name { get; set; }}下面先来看看Redis的使用。Redis在介绍序列化之前,我们需要知道在StackExchange.Redis中,我们要存储的数据都是以RedisValue的形式存在的。并且RedisValue是支持string,byte[]等多种数据类型的。换句话说就是,在我们使用StackExchange.Redis时,存进Redis的数据需要序列化成RedisValue所支持的类型。这就是前面说的需要显式的进行序列化的操作。先来看看.NET类库提供的BinaryFormatter。序列化的操作using (var ms = new MemoryStream()){formatter.Serialize(ms, product);db.StringSet("binaryformatter", ms.ToArray(), TimeSpan.FromMinutes(1));}反序列化的操作var value = db.StringGet("binaryformatter");using (var ms = new MemoryStream(value)){var desValue = (Product)(new BinaryFormatter().Deserialize(ms));Console.WriteLine($"{desValue.Id}-{desValue.Name}");}写起来还是挺简单的,但是这个时候运行代码会提示下面的错误!说是我们的Product类没有标记Serializable。下面就是在Product类加上[Serializable]。再次运行,已经能成功了。再来看看Newtonsoft.Json序列化的操作using (var ms = new MemoryStream()){using (var sr = new StreamWriter(ms, Encoding.UTF8))using (var jtr = new JsonTextWriter(sr)){jsonSerializer.Serialize(jtr, product);}db.StringSet("json", ms.ToArray(), TimeSpan.FromMinutes(1));}反序列化的操作var bytes = db.StringGet("json");using (var ms = new MemoryStream(bytes))using (var sr = new StreamReader(ms, Encoding.UTF8))using (var jtr = new JsonTextReader(sr)){var desValue = jsonSerializer.Deserialize<Product>(jtr);Console.WriteLine($"{desValue.Id}-{desValue.Name}");}由于Newtonsoft.Json对我们要进行序列化的类有没有加上Serializable并没有什么强制性的要求,所以去掉或保留都可以。运行起来是比较顺利的。当然,也可以用下面的方式来处理的:var objStr = JsonConvert.SerializeObject(product);db.StringSet("json", Encoding.UTF8.GetBytes(objStr), TimeSpan.FromMinutes(1));var resStr = Encoding.UTF8.GetString(db.StringGet("json"));var res = JsonConvert.DeserializeObject<Product>(resStr);再来看看ProtoBuf序列化的操作using (var ms = new MemoryStream()){Serializer.Serialize(ms, product);db.StringSet("protobuf", ms.ToArray(), TimeSpan.FromMinutes(1));}反序列化的操作var value = db.StringGet("protobuf");using (var ms = new MemoryStream(value)){var desValue = Serializer.Deserialize<Product>(ms);Console.WriteLine($"{desValue.Id}-{desValue.Name}");}用法看起来也是中规中矩。但是想这样就跑起来是没那么顺利的。错误提示如下:处理方法有两个,一个是在Product类和属性上面加上对应的Attribute,另一个是用ProtoBuf.Meta在运行时来处理这个问题。可以参考AutoProtobuf的实现。下面用第一种方式来处理,直接加上[ProtoContract]和[ProtoMember]这两个Attribute。再次运行就是我们所期望的结果了。最后来看看MessagePack,据其在Github上的说明和对比,似乎比其他序列化的库都强悍不少。它默认也是要像Protobuf那样加上MessagePackObject和Key这两个Attribute的。不过它也提供了一个IFormatterResolver参数,可以让我们有所选择。下面用的是不需要加Attribute的方法来演示。序列化的操作var serValue = MessagePackSerializer.Serialize(product, ContractlessStandardResolver.Instance);db.StringSet("messagepack", serValue, TimeSpan.FromMinutes(1));反序列化的操作var value = db.StringGet("messagepack");var desValue = MessagePackSerializer.Deserialize<Product>(value, ContractlessStandardResolver.Instance);此时运行起来也是正常的。其实序列化这一步,对Redis来说是十分简单的,因为它显式的让我们去处理,然后把结果进行存储。上面演示的4种方法,从使用上看,似乎都差不多,没有太大的区别。如果拿Redis和Memcached对比,会发现Memcached的操作可能比Redis的略微复杂了一点。下面来看看Memcached的使用。MemcachedEnyimMemcachedCore默认有一个 DefaultTranscoder,对于常规的数据类型(int,string等)本文不细说,只是特别说明object类型。在DefaultTranscoder中,对Object类型的数据进行序列化是基于Bson的。还有一个BinaryFormatterTranscoder是属于默认的另一个实现,这个就是基于我们前面的说.NET类库自带的System.Runtime.Serialization.Formatters.Binary。先来看看这两种自带的Transcoder要怎么用。先定义好初始化Memcached相关的方法,以及读写缓存的方法。初始化Memcached如下:private static void InitMemcached(string transcoder = ""){IServiceCollection services = new ServiceCollection();services.AddEnyimMemcached(options =>{options.AddServer("127.0.0.1", 11211);options.Transcoder = transcoder;});services.AddLogging();IServiceProvider serviceProvider = services.BuildServiceProvider();_client = serviceProvider.GetService<IMemcachedClient>() as MemcachedClient;}这里的transcoder就是我们要选择那种序列化方法(针对object类型),如果是空就用Bson,如果是BinaryFormatterTranscoder用的就是BinaryFormatter。需要注意下面两个说明2.1.0版本之后,Transcoder由ITranscoder类型变更为string类型。2.1.0.5版本之后,可以通过依赖注入的形式来完成,而不用指定string类型的Transcoder。读写缓存的操作如下:private static void MemcachedTrancode(Product product){_client.Store(Enyim.Caching.Memcached.StoreMode.Set, "defalut", product, DateTime.Now.AddMinutes(1));Console.WriteLine("serialize succeed!");var desValue = _client.ExecuteGet<Product>("defalut").Value;Console.WriteLine($"{desValue.Id}-{desValue.
谈谈ASP.NET Core中的ResponseCaching
前言前面的博客谈的大多数都是针对数据的缓存,今天我们来换换口味。来谈谈在ASP.NET Core中的ResponseCaching,与ResponseCaching关联密切的也就是常说的HTTP缓存。在阅读本文内容之前,默认各位有HTTP缓存相关的基础,主要是Cache-Control相关的。这里也贴两篇相关的博客:透过浏览器看HTTP缓存HTTP协议 (四) 缓存回到正题,对于ASP.NET Core中的ResponseCaching,本文主要讲三个相关的小内容客户端(浏览器)缓存服务端缓存静态文件缓存客户端(浏览器)缓存这里主要是通过设置HTTP的响应头来完成这件事的。方法主要有两种:其一,直接用Response对象去设置。这种方式也有两种写法,示例代码如下:public IActionResult Index(){//直接一,简单粗暴,不要拼写错了就好~~Response.Headers[Microsoft.Net.Http.Headers.HeaderNames.CacheControl] = "public, max-age=600";//直接二,略微优雅点//Response.GetTypedHeaders().CacheControl = new Microsoft.Net.Http.Headers.CacheControlHeaderValue()//{// Public = true,// MaxAge = TimeSpan.FromSeconds(600)//};return View();}这两者效果是一样的,大致如下:它们都会给响应头加上 Cache-Control: public, max-age=600,可能有人会问,加上这个有什么用?那我们再来看张动图,应该会清晰不少。这里事先在代码里面设置了一个断点,正常情况下,只要请求这个action都是会进来的。但是从上图可以发现,只是第一次才进了断点,其他直接打开的都没有进,而是直接返回结果给我们了,这也就说明缓存起作用了。同样的,再来看看下面的图,from disk cache也足以说明,它并没有请求到服务器,而是直接从本地返回的结果。注:如果是刷新的话,还是会进断点的。这里需要区分好刷新,地址栏回车等行为。不同浏览器也有些许差异,这里可以用fiddler和postman来模拟。在上面的做法中,我们将设置头部信息的代码和业务代码混在一起了,这显然不那么合适。下面来看看第二种方法,也是比较推荐的方法。其二,用ResponseCacheAttribute去处理缓存相关的事情。对于和上面的同等配置,只需要下面这样简单设置一个属性就可以了。[ResponseCache(Duration = 600)]public IActionResult Index(){return View();}效果和上面是一致的!处理起来是不是简单多了。既然这两种方式都能完成一样的效果,那么ResponseCache这个Attribute本质也是往响应头写了相应的值。但是我们知道,纯粹的Attribute并不能完成这一操作,其中肯定另有玄机!翻了一下源码,可以看到它实现了IFilterFactory这个关键的接口。[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]public class ResponseCacheAttribute : Attribute, IFilterFactory, IOrderedFilter{public IFilterMetadata CreateInstance(IServiceProvider serviceProvider){//..return new ResponseCacheFilter(new CacheProfile{Duration = _duration,Location = _location,NoStore = _noStore,VaryByHeader = VaryByHeader,VaryByQueryKeys = VaryByQueryKeys,});}}也就是说,真正起作用的是ResponseCacheFilter这个Filter,核心代码如下:public void OnActionExecuting(ActionExecutingContext context){var headers = context.HttpContext.Response.Headers;// Clear all headersheaders.Remove(HeaderNames.Vary);headers.Remove(HeaderNames.CacheControl);headers.Remove(HeaderNames.Pragma);if (!string.IsNullOrEmpty(VaryByHeader)){headers[HeaderNames.Vary] = VaryByHeader;}if (NoStore){headers[HeaderNames.CacheControl] = "no-store";// Cache-control: no-store, no-cache is valid.if (Location == ResponseCacheLocation.None){headers.AppendCommaSeparatedValues(HeaderNames.CacheControl, "no-cache");headers[HeaderNames.Pragma] = "no-cache";}}else{headers[HeaderNames.CacheControl] = cacheControlValue;}}它的本质自然就是给响应头部写了一些东西。通过上面的例子已经知道了ResponseCacheAttribute运作的基本原理,下面再来看看如何配置出其他不同的效果。下面的表格列出了部分常用的设置和生成的响应头信息。ResponseCache的设置响应头[ResponseCache(Duration = 600, Location = ResponseCacheLocation.Client)]Cache-Control: private, max-age=600[ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)]Cache-Control:no-cache, no-store[ResponseCache(Duration = 60, VaryByHeader = "User-Agent")]Cache-Control : public, max-age=60Vary : User-Agent注:如果NoStore没有设置成true,则Duration必须要赋值!关于ResponseCacheAttribute,还有一个不得不提的属性:CacheProfileName!它相当于指定了一个“配置文件”,并在这个“配置文件”中设置了ResponseCache的一些值。这个时候,只需要在ResponseCacheAttribute上面指定这个“配置文件”的名字就可以了,而不用在给Duration等属性赋值了。在添加MVC这个中间件的时候就需要把这些“配置文件”准备好!下面的示例代码添加了两份“配置文件”,其中一份名为default,默认是缓存10分钟,还有一份名为Hourly,默认是缓存一个小时,还有一些其他可选配置也用注释的方式列了出来。services.AddMvc(options =>{options.CacheProfiles.Add("default", new Microsoft.AspNetCore.Mvc.CacheProfile{Duration = 600, // 10 min});options.CacheProfiles.Add("Hourly", new Microsoft.AspNetCore.Mvc.CacheProfile{Duration = 60 * 60, // 1 hour//Location = Microsoft.AspNetCore.Mvc.ResponseCacheLocation.Any,//NoStore = true,//VaryByHeader = "User-Agent",//VaryByQueryKeys = new string[] { "aaa" }});});现在“配置文件”已经有了,下面就是使用这些配置了!只需要在Attribute上面指定CacheProfileName的名字就可以了。示例代码如下:[ResponseCache(CacheProfileName = "default")]public IActionResult Index(){return View();}ResponseCacheAttribute中还有一个VaryByQueryKeys的属性,这个属性可以根据不同的查询参数进行缓存!但是这个属性的使用需要结合下一小节的内容,所以这里就不展开了。注:ResponseCacheAttribute即可以加在类上面,也可以加在方法上面,如果类和方法都加了,会优先采用方法上面的配置。服务端缓存先简单解释一下这里的服务端缓存是什么,对比前面的客户端缓存,它是将东西存放在客户端,要用的时候就直接从客户端去取!同理,服务端缓存就是将东西存放在服务端,要用的时候就从服务端去取。需要注意的是,如果服务端的缓存命中了,那么它是直接返回结果的,也是不会去访问Action里面的内容!有点类似代理的感觉。这个相比客户端缓存有一个好处,在一定时间内,“刷新”页面的时候会从这里的缓存返回结果,而不用再次访问Action去拿结果。要想启用服务端缓存,需要在管道中去注册这个服务,核心代码就是下面的两句。public void ConfigureServices(IServiceCollection services){services.AddResponseCaching();}public void Configure(IApplicationBuilder app, IHostingEnvironment env){app.UseResponseCaching();}当然,仅有这两句代码,并不能完成这里提到的服务端缓存。还需要前面客户端缓存的设置,两者结合起来才能起作用。可以看看下面的效果,简单解释一下这张图,第一次刷新的时候,会进入中间件,然后进入Action,返回结果,Fiddler记录到了这一次的请求第二次打开新标签页,直接从浏览器缓存中返回的结果,即没有进入中间件,也没有进入Action,Fiddler也没有记录到相关请求第三次换了一个浏览器,会进入中间件,直接由缓存返回结果,并没有进入Action,此时Fiddler也将该请求记录了下来,响应头包含了Age第三次请求响应头部的部分信息如下:Age: 16Cache-Control: public,max-age=600这个Age是在变化的!它就等价于缓存的寿命。如果启用了日志,也会看到一些比较重要的日记信息。在上一小节中,我们还有提到ResponseCacheAttribute中的VaryByQueryKeys这个属性,它需要结合ResponseCaching中间件一起用的,这点在注释中也是可以看到的!//// Summary:// Gets or sets the query keys to vary by.//// Remarks:// Microsoft.AspNetCore.Mvc.ResponseCacheAttribute.VaryByQueryKeys requ
在.NET Core中使用简单的插件化机制
前言插件化,其实也并不是什么新东西了,像nopCommerce等开源项目都有类似的机制,而且功能比较完善和齐全。相信大家都对接过不少支付方式,支付宝、微信以及各大银行或第三方的支付公司。我们可以把支付相关的操作抽象出来,无非就是支付,异步回调,退款,查询等几个重要的操作。这个时候我们可以将各种支付方式都做为一个插件,这些插件都实现上面的操作,这样我们整合一个入口,去加载相应的插件即可。这个入口常规情况就是MVC或者是WEB API。下面来实现一个简单的例子。简单的例子首先建立一个公共的插件抽象类,简单起见,里面就包含一个无参的抽象方法Handle,这个方法就像上面提到的支付,退款等操作。每一个新的插件都必须要继承这个抽象类并且实现这个抽象方法!public abstract class BasePluginsService{public abstract string Handle();}假设现在有两个插件AA和BB,我们会把AA和BB各建一个类库。其中,AA的就只是返回了AA相关的字符串。public class PluginsService : Common.BasePluginsService{public override string Handle(){return "Plugins.AA";}}BB的也同样只是返回了BB相关的字符串。public class PluginsService : Common.BasePluginsService{public override string Handle(){return "Plugins.BB";}}接下来,主要还是我们的入口,Web项目的处理。在入口处的处理主要是利用反射去确实要调用那个插件里面的Handle方法。下面是获取相应插件实例的方法。private async Task<Common.BasePluginsService> GetPlugin(string type){string cacheKey = $"plugin:{type}";//先尝试从缓存中取if (_cache.TryGetValue(cacheKey, out Common.BasePluginsService service)){return service;}else{var baseDirectory = Directory.GetCurrentDirectory();var dll = $"Plugins.{type}.dll";//Plugins的完整路径var path = Path.Combine(baseDirectory, _options.PluginsPath, dll);try{//预防无法更新dllbyte[] bytes = await System.IO.File.ReadAllBytesAsync(path);var assembly = Assembly.Load(bytes);//创建实例var obj = (Common.BasePluginsService)assembly.CreateInstance($"Plugins.{type}.PluginsService");if (obj != null){_cache.Set(cacheKey, obj, DateTimeOffset.Now.AddSeconds(60));}return obj;}catch (Exception){return null;}}}这里主要有下面两个操作一、 缓存反射结果为了避免每次获取插件都去进行反射操作,这里引用了MemoryCache来缓存了反射的结果。关于缓存,这里需要注意一个问题,缓存的时间!这个对新增加一个插件是没有什么影响,但是对修改一个已经存在的插件就影响比较大了!缓存时候过长会导致没有办法实时出现修改的效果,可以考虑缓存比较短的时间。二、 动态替换dll为了避免出现对一个现存的插件修改之后,无法正常替换的情形,往往提示的是正常使用。需要做一些处理,避免一直占用这个资源!最后就是Action的操作了。[HttpGet]public async Task<string> GetAsync(string type){if (string.IsNullOrWhiteSpace(type)){return "type is empty";}var plugin = await this.GetPlugin(type);if (plugin != null){return plugin.Handle();}return "default";}到这里,基本就完成了。先来看看项目的大致框架:我们是将所有的插件放到Plugins这个项目文件中的。Web项目并不直接引用Plugins下面的项目。运行时是动态加载指定目录里面的dll,然后完成调用的。下面简单来看看效果:通过修改type达到选择不同插件的效果,然后对某个插件进行修改之后,也能正常替换和生效。当然,实际中可能并没有那么直接就能拿type,而是是要根据传进来的参数去搜一下数据库,然后才能拿到type。这也完全取决于不同的设计。总结插件化机制,可以简单的认为是反射的一个实际应用,这个已经能满足不少常规性的要求了。但是完整的插件化还有诸多要考量的东西,这个可以参考nop的实现。它还是有不少好处的,个人认为,最主要的还是隔离了不同的插件,将它们之间相互影响的可能性降低。最后附上本文的示例Demo:PluginsDemo
谈谈Circuit Breaker在.NET Core中的简单应用
前言由于微服务的盛行,不少公司都将原来细粒度比较大的服务拆分成多个小的服务,让每个小服务做好自己的事即可。经过拆分之后,就避免不了服务之间的相互调用问题!如果调用没有处理好,就有可能造成整个系统的瘫痪,好比说其中一些基础服务出现了故障,那么用到这些基础服务的地方都是要做一定的处理的,不能让它们出现大面积的瘫痪!!!正常情况下的解决方案就要对服务进行熔断处理,不能因为提供方出现了问题就让调用方也废了。熔断一般是指软件系统中,由于某些原因使得服务出现了过载现象,为防止造成整个系统故障,从而采用的一种保护措施。对于这个问题,Steeltoe的Circuit Breaker是一个不错的选择。本文的示例代码也是基于它的。Steeltoe的Circuit BreakerSteeltoe是什么呢?Steeltoe可以说是构建微服务的一个解决方案吧。具体的可以访问它的官网:http://steeltoe.io/回归正题,先来看看官方对Circuit Breaker的描述:What do you do when a service you depend on stops responding? Circuit breakers enable you to bypass a failing service, allowing it time to recover, and preventing your users from seeing nasty error messages. Steeltoe includes a .NET implementation of Netflix Hystrix, a proven circuit breaker implementation with rich metrics and monitoring features.不难发现,Circuit Breaker可以让我们很好的处理失败的服务。它也包含了对Netflix Hystrix的.NET(Core)实现。关于熔断机制,有个非常经典的图(这里直接拿了官方文档的图),核心描绘的就是三种状态之间的变化关系。说了那么多,下面还是来看个简单的例子来略微深入理解一下吧。注:服务发现和服务注册不是本文的重点,所以这里不会使用Steeltoe相应的功能。简单例子先定义一个简单的订单服务,这个服务很简单,就一个返回直接返回对应订单号的接口,这里用默认的ASP.NET Core Web API项目做一下调整就好了。[Route("api/[controller]")]public class ValuesController : Controller{// GET api/values/123[HttpGet("{id}")]public string Get(string id){return $"order-{id}";}}再来一个新服务去调用上面的订单服务。先抛开熔断相关的,定义一个用于访问订单服务的Service接口和实现。public interface IOrderService{Task<string> GetOrderDetailsAsync(string orderId);}public class OrderService : IOrderService{public async Task<string> GetOrderDetailsAsync(string orderId){using (HttpClient client = new HttpClient()){return await client.GetStringAsync($"http://localhost:9999/api/values/{orderId}");}}}比较简单,就是发起HTTP请求到订单服务,拿一下返回的结果。忽略熔断的话,现在已经可以通过这个OrderService去拿到结果了。[HttpGet]public async Task<string> Get([FromServices] Services.IOrderService service, string id = "0"){return await service.GetOrderDetailsAsync(id);}结果如下:这是最最最最理想的情况!如果我们把订单服务停了,会发生什么事呢?十分尴尬,这个订单服务的调用方也废了。当然,try-catch也是可以帮我们处理这个尴尬的问题,但这并不是我们想要的结果啊!下面来看看引入Circuit Breaker之后如何略微优雅一点去处理这个问题。定义一个GetOrderDetailsHystrixCommand,让它继承HystrixCommand。public class GetOrderDetailsHystrixCommand : HystrixCommand<string>{private readonly IOrderService _service;private readonly ILogger<GetOrderDetailsHystrixCommand> _logger;private string _orderId;public GetOrderDetailsHystrixCommand(IHystrixCommandOptions options,IOrderService service,ILogger<GetOrderDetailsHystrixCommand> logger) : base(options){this._service = service;this._logger = logger;this.IsFallbackUserDefined = true;}public async Task<string> GetOrderDetailsAsync(string orderId){_orderId = orderId;return await ExecuteAsync();}protected override async Task<string> RunAsync(){var result = await _service.GetOrderDetailsAsync(_orderId);_logger.LogInformation("Get the result : {0}", result);return result;}protected override async Task<string> RunFallbackAsync(){//断路器已经打开if (!this._circuitBreaker.AllowRequest){return await Task.FromResult("Please wait for sometimes");}_logger.LogInformation($"RunFallback");return await Task.FromResult<string>($"RunFallbackAsync---OrderId={_orderId}");}}这里有几个地方要注意:构造函数一定要有IHystrixCommandOptions这个参数RunAsync是真正执行调用的地方RunFallbackAsync是由于某些原因不能拿到返回结果时会执行的地方,所谓的优雅降级。接下来要做的是在Startup中进行注册。public void ConfigureServices(IServiceCollection services){services.AddSingleton<IOrderService, OrderService>();services.AddHystrixCommand<GetOrderDetailsHystrixCommand>("Order", Configuration);services.AddMvc();}可以看到,在添加熔断命令的时候,还用到了Configuration这个参数,这就说明,我们还少了配置!!配置是放到appsettings.json里面的,下面来看一下要怎么配置:{"hystrix": {"command": {"default": {"circuitBreaker": {//是否启用,默认是true"enabled": true,//在指定时间窗口内,熔断触发的最小个数"requestVolumeThreshold": 5,//熔断多少时间后去尝试请求"sleepWindowInMilliseconds": 5000,//失败率达到多少百分比后熔断"errorThresholdPercentage": 50,//是否强制开启熔断"forceOpen": false,//是否强制关闭熔断"forceClosed": false},//是否启用fallback"fallback": {"enabled": true}}}}}需要添加一个名字为hystrix的节点,里面的command节点才是我们要关注的地方!default,是默认的配置,针对所有的Command!如果说有某个特定的Command要单独配置,可以在command下面添加相应的命令节点即可。其他配置项,都已经用注释的方式解释了。下面这张动图模拟了订单服务从可用->不可用->可用的情形。除了服务不可用,可能还有一种情况发生的概率会比较大,超时!举个例子,有一个服务平常都是响应很快,突然有一段时间不知道什么原因,处理请求的速度慢了很多,这段时间内经常出现客户端等待很长的时间,甚至超时了。当遇到这种情况的时候,一般都会设置一个超时时间,只要在这个时间内没有响应就认为是超时了!可以通过下面的配置来完成超时的配置:{"hystrix": {"command": {"default": {"execution": {"timeout": {"enabled": true},"isolation": {"strategy": "THREAD","thread": {//超时时间"timeoutInMilliseconds": 1000}}},}}}}总结这里也只是介绍了几个比较常用和简单的功能,它还可以合并多个请求,缓存请求等诸多实用的功能。总体来说,Steeltoe的熔断功能,用起来还算是比较简单,也比较灵活。更多配置和说明可以参考官方文档。本文的示例代码:CircuitBreakerDemo
看看.NET Core几个Options的简单使用
前言配置,对我们的程序来说是十分重要的一部分。或多或少都会写一部分内容到配置文件中去。由其是在配置中心(Apollo等)做起来之前,配置文件一定会是我们的首选。在.NET Core中,习惯的是用json文件当配置文件。读取的方法是不少,这里主要介绍的是用基于Options的方法来读,可以认为这是一种强类型的形式。本文会介绍一些常见的用法,简单的单元测试示例,如果想探讨内部实现,请移步至雨夜朦胧的博客。先来看看IOptions。IOptions先写好配置文件{"Demo": {"Age": 18,"Name": "catcher"},//others ...}然后定义对应的实体类public class DemoOptions{public int Age { get; set; }public string Name { get; set; }}然后只需要在ConfigureServices方法添加一行代码就可以正常使用了。public void ConfigureServices(IServiceCollection services){services.Configure<DemoOptions>(Configuration.GetSection("Demo"));//others..}最后就是在想要读配置内容的地方使用IOptions去注入就好了。private readonly DemoOptions _normal;public ValuesController(IOptions<DemoOptions> normalAcc){this._normal = normalAcc.Value;}// GET api/values[HttpGet]public string Get(){var age = $"normal-[{_normal.Age}];";var name = $"normal-[{_normal.Name}];";return $"age:{age} nname:{name}";}这个时候的结果,就会大致如下了:这个时候可能会冒出这样的一个想法,如果某天,要修改某个配置项的值,它能及时生效吗?口说无凭,来个动图见证一下。事实证明,使用IOptions的时候,修改配置文件的值,并不会立刻生效!!既然IOptions不行,那么我们就换一个!下面来看看IOptionsSnapshot。IOptionsSnapshot对于Options家族,在Startup注册的时候都是一个样的,区别在于使用它们的时候。private readonly DemoOptions _normal;private readonly DemoOptions _snapshot;public ValuesController(IOptions<DemoOptions> normalAcc,IOptionsSnapshot<DemoOptions> snapshotAcc){this._normal = normalAcc.Value;this._snapshot = snapshotAcc.Value;}// GET api/values[HttpGet]public string Get(){var age = $"normal-[{_normal.Age}];snapshot-[{_snapshot.Age}];";var name = $"normal-[{_normal.Name}];snapshot-[{_snapshot.Name}];";return $"age:{age} nname:{name}";}这个时候修改配置项的值之后,就会立马更新了。本质上,IOptions和IOptionsSnapshot是一样的,只是他们注册的生命周期不一样,从而就有不同的表现结果。当然,还有一个更强大的Options的存在,IOptionsMonitor。它的用法和IOptionsSnapshot没有区别,不同的时,它多了一个配置文件发生改变之后事件处理。下面来看看。IOptionsMonitorprivate readonly DemoOptions _normal;private readonly DemoOptions _snapshot;private readonly DemoOptions _monitor;public ValuesController(IOptions<DemoOptions> normalAcc, IOptionsSnapshot<DemoOptions> snapshotAcc, IOptionsMonitor<DemoOptions> monitorAcc){this._normal = normalAcc.Value;this._snapshot = snapshotAcc.Value;this._monitor = monitorAcc.CurrentValue;monitorAcc.OnChange(ChangeListener);}private void ChangeListener(DemoOptions options, string name){Console.WriteLine(name);}// GET api/values[HttpGet]public string Get(){var age = $"normal-[{_normal.Age}];snapshot-[{_snapshot.Age}];monitor-[{_monitor.Age}];";var name = $"normal-[{_normal.Name}];snapshot-[{_snapshot.Name}];monitor-[{_monitor.Name}];";return $"age:{age} nname:{name}";}效果和上面一样的,不同的是,当保存appsettings.json的时候,会触发一次ChangeListener。虽说Snapshot和Monitor可以让我们及时获取到最新的配置项。但是我们也可以通过PostConfigure或PostConfigureAll来进行调整。PostConfigure/PostConfigureAllpublic void ConfigureServices(IServiceCollection services){services.Configure<DemoOptions>(Configuration.GetSection("Demo"));services.PostConfigureAll<DemoOptions>(x =>{x.Age = 100;});services.AddMvc();}如果我们的代码是这样写的,那么,最终的结果就会是,无论我们怎么修改配置文件,最终展示的Age会一直是100。大家也可以思考一下这个可以用在什么场景。随便给大家看一段Steeltoe服务发现客户端的代码private static void AddDiscoveryServices(IServiceCollection services, IConfiguration config, IDiscoveryLifecycle lifecycle){var clientConfigsection = config.GetSection(EUREKA_PREFIX);int childCount = clientConfigsection.GetChildren().Count();if (childCount > 0){var clientSection = config.GetSection(EurekaClientOptions.EUREKA_CLIENT_CONFIGURATION_PREFIX);services.Configure<EurekaClientOptions>(clientSection);var instSection = config.GetSection(EurekaInstanceOptions.EUREKA_INSTANCE_CONFIGURATION_PREFIX);services.Configure<EurekaInstanceOptions>(instSection);services.PostConfigure<EurekaInstanceOptions>((options) =>{EurekaPostConfigurer.UpdateConfiguration(config, options);});AddEurekaServices(services, lifecycle);}else{throw new ArgumentException("Discovery client type UNKNOWN, check configuration");}}最后就是单元测试遇到Options要怎么处理的问题了。单元测试单元测试,这里用了NSubstitute来作示例。先简单定义一些类public class MyClass{private readonly MyOptions _options;public MyClass(IOptions<MyOptions> optionsAcc){this._options = optionsAcc.Value;}public string Greet(){return $"Hello,{_options.Name}";}}public class MyOptions{public string Name { get; set; }}编写测试类public class MyClassTest{private readonly MyClass myClass;public MyClassTest(){var options = new MyOptions { Name = "catcher"};var fake = Substitute.For<IOptions<MyOptions>>();fake.Value.Returns(options);myClass = new MyClass(fake);}[Fact]public void GreetTest(){var res = myClass.Greet();Assert.Equal("Hello,catcher", res);}}重点在于fake了一下Options(这里只以IOptions为例),然后是告诉测试,如果有用到Value属性的时候,就用返回定义好的Options。也是比较简单的做法,测试的结果自然也是符合预期的。总结这几个Options使用起来还是比较顺手的,至少何时采用那种Options,就得根据场景来定了。
Refit在ASP.NET Core中的实践
前言声名式服务调用,己经不算是一个新鲜的话题了,毕竟都出来好些年了。下面谈谈,最近项目中用到一个这样的组件的简单实践。目前部分项目用到的是Refit这个组件,都是配合HttpClientFactory来使用的。关于HttpClientFactory的一些简单介绍,可以参见官方文档,也可以看看前面的两篇比较粗略的相关介绍。也简单介绍一下背景,目前主要有两类的API接口:第一类是注册到Eureka中的,可以通过服务发现的方式来请求的,这里的都是新的接口。第二类是原始的接口,不能走服务发现,只能通过直连请求的方式来调用,这里的都是些老接口。换句话就是说,要同时兼容这两类接口。由于用HttpClientFactory集成服务发现十分简单,所以优先选了一个本身就带有HttpClientFactory的组件--Refit。什么是RefitRefit是一个自动类型安全的REST库,是RESTful架构的.NET客户端实现,它基于Attribute,提供了把REST API返回的数据转化为(Plain Ordinary C# Object,简单C#对象),POCO to JSON,网络请求(POST,GET,PUT,DELETE等)封装,内部封装使用HttpClient,前者专注于接口的封装,后者专注于网络请求的高效,二者分工协作。我们的应用程序通过 refit请求网络,实际上是使用 refit接口层封装请求参数、Header、Url 等信息,之后由 HttpClient完成后续的请求操作,在服务端返回数据之后,HttpClient将原始的结果交给 refit,后者根据用户的需求对结果进行解析的过程。更多细节可以参考Refit的官网创建一个可调用的API接口直接上控制器的代码了〜〜// GET: api/persons[HttpGet]public IEnumerable<Person> Get(){return new List<Person>{new Person{Id = 1 , Name = "catcher wong", CreateTime = DateTime.Now},new Person{Id = 2 , Name = "james li", CreateTime = DateTime.Now.AddDays(-2)}};}// GET api/persons/5[HttpGet("{id}")]public Person Get(int id){return new Person { Id = id, Name = "name" };}// POST api/persons[HttpPost]public Person Post([FromBody]Person person){if (person == null) return new Person();return new Person { Id = person.Id, Name = person.Name };}// PUT api/persons/5[HttpPut]public string Put([FromBody]int id){return $"put {id}";}// DELETE api/persons/5[HttpDelete("{id}")]public string Delete(int id){return $"del {id}";}Refit的使用先通过Nuget安装Refit的包。然后就是定义我们的interface了public interface IPersonsApi{[Get("/api/persons")]Task<List<Person>> GetPersonsAsync();[Get("/api/persons/{id}")]Task<Person> GetPersonAsync([AliasAs("id")]int personId);[Post("/api/persons")]Task<Person> AddPersonAsync([Body]Person person);[Put("/api/persons")]Task<string> EditPersonAsync([Body]int id);[Delete("/api/persons/{id}")]Task<string> DeletePersonAsync(int id);}来看看这个interface里面涉及到的部分内容。Get,Post等特性就表明了接口的请求方式,后面的值就是请求的相对路径。相对路径中,可以使用占位符,来动态更新参数值。如果方法名和请求参数名不一致,需要用AliasAs指明。通过Body特性声明一个对象作为请求体发送到服务器返回值定义是Task或者IObservable然后是配合HttpClientFactory再通过Nuget安装一下Refit.HttpClientFactory如果PersonApi是注册到Euerka的,可以再添加Steeltoe的引用。public void ConfigureServices(IServiceCollection services){services.AddRefitClient<IPersonsApi>().ConfigureHttpClient(options =>{options.BaseAddress = new Uri(Configuration.GetValue<string>("personapi_url"));//other settings of httpclient})//Steeltoe discovery//.AddHttpMessageHandler<DiscoveryHttpMessageHandler>();services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);}前面在定义IPersonApi的时候,我们只指定了相对路径,而请求IP并没有指定,这里是放到ConfigureHttpClient里面去指定了。同时根据不同环境,配置不同的appsettings.{env}.json,达到切换的效果。同样的,如果想走服务发现,只需要放开注释的AddHttpMessageHandler,同时修改BaseeAddress为服务名的形式就可以了。说了这么多,都还只是配置阶段,下面就来看看具体怎么用。为了演示方便,就不在建一个Service层了,直接在控制器调用一下。用法也很简单,直接在控制器注入一下就可以使用了。[Route("api/[controller]")][ApiController]public class ValuesController : ControllerBase{private readonly IPersonsApi _api;public ValuesController(IPersonsApi api){this._api = api;}// GET api/values[HttpGet]public async Task<List<Person>> GetAsync(){return await _api.GetPersonsAsync();}// GET api/values/5[HttpGet("{id}")]public async Task<Person> Get(int id){return await _api.GetPersonAsync(id);}// POST api/values[HttpPost]public async Task<Person> Post([FromBody] Person value){return await _api.AddPersonAsync(value);}}到这里,代码层面的东西已经处理完了。下面来看看使用Refit效果(这里只看两个Get请求的):都是能正常拿到我们期望的结果。最后再看看输出的日志,确认一下。首先是访问/api/values的确确实实是向我们前面的PersonApi发起了请求。然后是访问/api/values/5555 的可见我们上面的别名(AliasAs)是起了效果的,能拼成正确的请求地址。至于其他类型的请求,这里就不演示了,让大家自己去尝试一下吧。总结Refit用起来还是比较简单的,运行了一段时间也还表现正常!当然本文介绍的也只是一些基本的用法!它还具有不错的扩展性,可以让我们根据自身需求做一些定制化的东西。本文的示例代码RefitClientApi
.NET Core中Object Pool的简单使用
前言复用,是一个重要的话题,也是我们日常开发中经常遇到的,不可避免的问题。举个最为简单,大家最为熟悉的例子,数据库连接池,就是复用数据库连接。那么复用的意义在那里呢?简单来说就是减少不必要的资源损耗。除了数据库连接,可能在不同的情景或需求下,还会有很多其他对象需要进行复用,这个时候就会有所谓的 Object Pool(对象池)。小伙伴们应该也自己实现过类似的功能,或用ConcurrentBag,或用ConcurrentQueue,或用其他方案。这也里分享一个在微软文档中的实现How to: Create an Object Pool by Using a ConcurrentBag当然,在.NET Core中,微软已经帮我们实现了一个简单的Object Pool。我们只需要添加Microsoft.Extensions.ObjectPool的引用即可使用了。Microsoft.Extensions.ObjectPoolMicrosoft.Extensions.ObjectPool可以说是.NET Core的一个基础类库。它位于aspnet的Common项目中,类型其他基础模块都有使用相关的功能,也好比Routing项目。下面就简单看看它的用法。在开始之前,我们先定义一个可以复用的objectpublic class Demo{public int Id { get; set; }public string Name { get; set; }public DateTime CreateTimte { get; set; }}用法1var defalutPolicy = new DefaultPooledObjectPolicy<Demo>();//最大可保留对象数量 = Environment.ProcessorCount * 2var defaultPool = new DefaultObjectPool<Demo>(defalutPolicy);for (int i = 0; i < PoolSize; i++){item1 = defaultPool.Get();Console.WriteLine($"#{i+1}-{item1.Id}-{item1.Name}-{item1.CreateTimte}");}在创建pool之前,我们要先定义一个Policy。这里直接用自带的DefaultPooledObjectPolicy来构造。对象池会有一个维护的最大数量,线程数。通过pool对象的Get方法,从对象池中取出一个对象。上面代码运行结果#1-0--01/01/0001 00:00:00#2-0--01/01/0001 00:00:00#3-0--01/01/0001 00:00:00#4-0--01/01/0001 00:00:00#5-0--01/01/0001 00:00:00#6-0--01/01/0001 00:00:00#7-0--01/01/0001 00:00:00#8-0--01/01/0001 00:00:00这个结果说明,Object Pool 中的对象都是直接new出来的,并没有对一些属性进行贬值操作,这个时候往往没有太多实际意义。因为DefaultPooledObjectPolicy本来就是直接new了一个对象出来,很多时候,这并不是我们所期望的!要想符合我们实际的使用,就要自己定义一个Policy!下面来看看用法2用法2先定义一个Policy,实现 IPooledObjectPolicy 这个接口。T很自然就是我们的Demo类了。public class DemoPooledObjectPolicy : IPooledObjectPolicy<Demo>{public Demo Create(){return new Demo { Id = 1, Name = "catcher", CreateTimte = DateTime.Now };}public bool Return(Demo obj){return true;}}这里要实现Create和Return两个方法。见名知义,Create方法就是用来创建Demo对象的,Return方法就是将Demo对象扔回Object Pool的(有借有还)。然后是用DemoPooledObjectPolicy去替换DefaultPooledObjectPolicy。var demoPolicy = new DemoPooledObjectPolicy();var defaultPoolWithDemoPolicy = new DefaultObjectPool<Demo>(demoPolicy,1);//借item1 = defaultPoolWithDemoPolicy.Get();//还defaultPoolWithDemoPolicy.Return(item1);//借,但是不还item2 = defaultPoolWithDemoPolicy.Get();Console.WriteLine($"{item1.Id}-{item1.Name}-{item1.CreateTimte}");Console.WriteLine($"{item2.Id}-{item2.Name}-{item2.CreateTimte}");Console.WriteLine(item1 == item2);//创建一个新的item3 = defaultPoolWithDemoPolicy.Get();Console.WriteLine($"{item3.Id}-{item3.Name}-{item3.CreateTimte}");Console.WriteLine(item3 == item1);这里定义了对象池只保留一个对象。由于从object pool中取出来之后,有一步还回去的操作,所以item1和item2应当是同一个对象。从object pool中拿出了item2之后,它并没有还回去,所以object pool会基于我们定义的Policy去创建一个新的对象出来。下面是用法2的输出结果:1-catcher-09/17/2018 22:32:381-catcher-09/17/2018 22:32:38True1-catcher-09/17/2018 22:32:38False可以看到item1,item2和item3的各个属性是一样的,并且item1和item2确实是同一个对象。item3和item1并不是同一个。用法3除了DefaultObjectPool外,还有DefaultObjectPoolProvider也可以创建一个Object Pool。创建一个Object Pool,一定是离不开Policy的,所以这里还是用了我们自己定义的DemoPooledObjectPolicy。var defaultProvider = new DefaultObjectPoolProvider();var policy = new DemoPooledObjectPolicy();//default maximumRetained is Environment.ProcessorCount * 2ObjectPool<Demo> pool = defaultProvider.Create(policy);item1 = pool.Get();pool.Return(item1);item2 = pool.Get();Console.WriteLine($"{item1.Id}-{item1.Name}-{item1.CreateTimte}");Console.WriteLine($"{item2.Id}-{item2.Name}-{item2.CreateTimte}");Console.WriteLine(item1 == item2);item3 = pool.Get();Console.WriteLine($"{item3.Id}-{item3.Name}-{item3.CreateTimte}");Console.WriteLine(item3 == item2);用Provider创建Object Pool时,不能指定保留的最大对象数量,只能用的是默认的Environment.ProcessorCount * 2。后面的使用,和用法2是一样的。可以看到item1和item2是同一个对象。从Object Pool中取对象的时候,会取第一个,所以还回去后,再取的话,还是会取到原来的第一个。item3那里是直接从Object Pool中取出来的,没有再次创建,因为这里的Object Pool维护着多个对象,而不是用法2中的只有一个,所以它是直接从Pool中拿的。下面是输出结果1-catcher-09/17/2018 22:38:341-catcher-09/17/2018 22:38:34True1-catcher-09/17/2018 22:38:34False和用法2,本质是一样的。用法4好像上面的用法,都不那么像我们正常使用的。我们还是需要依赖注入的。那么我们最后就来看看怎么结合依赖注入吧。当然这里的本质还是离不开Policy和Provider这两个东西。IServiceCollection services = new ServiceCollection();services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();services.AddSingleton(s =>{var provider = s.GetRequiredService<ObjectPoolProvider>();return provider.Create(new DemoPooledObjectPolicy());});ServiceProvider serviceProvider = services.BuildServiceProvider();var pool = serviceProvider.GetService<ObjectPool<Demo>>();item1 = pool.Get();pool.Return(item1);item2 = pool.Get();Console.WriteLine($"{item1.Id}-{item1.Name}-{item1.CreateTimte}");Console.WriteLine($"{item2.Id}-{item2.Name}-{item2.CreateTimte}");Console.WriteLine(item1 == item2);item3 = pool.Get();Console.WriteLine($"{item3.Id}-{item3.Name}-{item3.CreateTimte}");Console.WriteLine(item3 == item2);我们首先需要完成对Provider的注册,然后直接拿它的实例去创建一个Object Pool即可。如果想在其他地方用,通过构造函数注入即可。这里的结果也是和前面一样的,没什么好多说的。总结在这几种用法中,我们最常用的应该是用法4。但是无论那种用法,我们都需要了解,Object Pool离不开Pool,Policy和Provider这三个家伙。有了这三个,或许我们就可以为所欲为了。当然,它还提供了几个特殊的东西,有兴趣的可以去看看。LeakTrackingObjectPoolStringBuilderPooledObjectPolicy最后用一个脑图结束本文。
ASP.NET Core多环境配置文件问题
前言在我们开发的过程中,往往会有这几个环境,Dev、QA、Pre和Pro。当然不同的环境可能大家的叫法会有点不一样。最常遇到的问题,或许就是不同环境的配置文件问题!一个环境一个配置文件是很常见的做法。在开发的时候,我们可以通过修改launchSettings.json来达到不同环境的切换。本质是通过ASPNETCORE_ENVIRONMENT这个变量值来完成。但是部署到服务器的时候就需要换个方式来处理这个问题了。 因为发布后的文件并没有launchSettings.json。这里简单介绍两种方法来处理这个问题。方法1设置系统的环境变量。修改 /etc/profile 文件,添加下面的配置export ASPNETCORE_ENVIRONMENT=QA再执行source命令,使其生效。source /etc/profile执行 dotnet myweb.dll 的时候就可以看到下面的结果Hosting environment: QAContent root path: /var/www/testwebNow listening on: http://127.0.0.1:47372Application started. Press Ctrl+C to shut down.这种做法,虽然可以完成不同环境的切换问题,但是要为每台机器设置一个环境变量。由于直接是镜像copy出来的系统,好多系统配置是已经做好标准规范的了,所以这样做还是会很麻烦,运维的同学肯定也不愿意每copy一台机器,都帮你改这个东西。所以这个方法自已玩玩的机率比较多。方法2在启动程序的时候,添加一个名为environment的Command-Line参数,同时指定它的值为对应的环境值。下面的例子是托管在Jexus时的写法。AppHost={cmd=dotnet /var/www/testweb/myweb.dll --environment QA;root=/var/www/testweb;port=0;}这个时候看到的日志也是一样的效果。Hosting environment: QAContent root path: /var/www/testwebNow listening on: http://127.0.0.1:47372Application started. Press Ctrl+C to shut down.通过这种方法,可控性看上去比较好,只需要加个参数即可。如果用方法2需要注意一点:在Program.cs中,不要忘记AddCommandLine。不过如果用的是WebHost.CreateDefaultBuilder(args)就可以忽略这一点了。
查看服务器运行多少个ASP.NET Core程序
有时候,我们会想知道某台机器上面跑了什么程序。当程序部署到IIS上面的时候,我们只需要打开IIS一看,就知道有多少个站点在运行了。当我们在CentOS上面部署的时候,就没那么的直观了。当然对于熟悉Linux命令的小伙伴还是很容易的。下面就来看看如何在CentOS上面查看对应的信息。说明,本文的所有环境都是基于Jexus的。查看的命令如下ps -ef | grep AppHost输出结果UID PID PPID C STIME TTY TIME CMDroot 12651 51914 0 Sep30 ? 00:19:38 [AppHost:crm] /data/project/crm/crm.dll --environment Stagingroot 35237 51914 0 Sep29 ? 00:10:26 [AppHost:product] /data/project/product/product.dll --environment Staginghwq 40167 39650 0 10:45 pts/0 00:00:00 grep --color=auto AppHost其中,UID那行标题是手动加上去的。下面是各字段的说明字段说明UID用户名PID进程的IDPPID父进程IDC进程占用CPU的百分比STIME进程启动到现在的时间TTY该进程在那个终端上运行,若与终端无关,则显示?若为pts/0等,则表示由网络连接主机进程。CMD命令的名称和参数上面的示例中列出了两条主要的信息,表明当前服务器运行着两个dotnet core的程序。根据CMD进一步细分,同时也可以看出是那两个程序以第一个为例:[AppHost:crm] /data/project/crm/crm.dll --environment Staging其中,AppHost后面跟着的就是jws的配置文件名称。后面那部分就是运行 dotnet 时指定的dll和相关的参数。了解Jexus的都应该知道Jexus运行dotnet core程序时是父子进程的关系,示例中的两个进程的PPID都是 51914,也很清晰的说明了这个问题。用下面命令查看这个进程的信息时,可以看到它的CMD就是JwsMain,jws的核心进程ps -p 51914输出PID TTY TIME CMD51914 ? 00:01:40 JwsMain
谈谈.NET Core中基于Generic Host来实现后台任务
目录前言什么是Generic Host后台任务示例控制台形式消费MQ消息的后台任务Web形式部署IHostedService和BackgroundService的区别IHostBuilder的扩展写法总结前言很多时候,后台任务对我们来说是一个利器,帮我们在后面处理了成千上万的事情。在.NET Framework时代,我们可能比较多的就是一个项目,会有一到多个对应的Windows服务,这些Windows服务就可以当作是我们所说的后台任务了。我喜欢将后台任务分为两大类,一类是不停的跑,好比MQ的消费者,RPC的服务端。另一类是定时的跑,好比定时任务。那么在.NET Core时代是不是有一些不同的解决方案呢?答案是肯定的。Generic Host就是其中一种方案,也是本文的主角。什么是Generic HostGeneric Host是ASP.NET Core 2.1中的新增功能,它的目的是将HTTP管道从Web Host的API中分离出来,从而启用更多的Host方案。这样可以让基于Generic Host的一些特性延用一些基础的功能。如:如配置、依赖关系注入和日志等。Generic Host更倾向于通用性,换句话就是说,我们即可以在Web项目中使用,也可以在非Web项目中使用!虽然有时候后台任务混杂在Web项目中并不是一个太好的选择,但也并不失是一个解决方案。尤其是在资源并不充足的时候。比较好的做法还是让其独立出来,让它的职责更加单一。下面就先来看看如何创建后台任务吧。后台任务示例我们先来写两个后台任务(一个一直跑,一个定时跑),体验一下这些后台任务要怎么上手,同样也是我们后面要使用到的。这两个任务统一继承BackgroundService这个抽象类,而不是IHostedService这个接口。后面会说到两者的区别。一直跑的后台任务先上代码public class PrinterHostedService2 : BackgroundService{private readonly ILogger _logger;private readonly AppSettings _settings;public PrinterHostedService2(ILoggerFactory loggerFactory, IOptionsSnapshot<AppSettings> options){this._logger = loggerFactory.CreateLogger<PrinterHostedService2>();this._settings = options.Value;}public override Task StopAsync(CancellationToken cancellationToken){_logger.LogInformation("Printer2 is stopped");return Task.CompletedTask;}protected override async Task ExecuteAsync(CancellationToken stoppingToken){while (!stoppingToken.IsCancellationRequested){_logger.LogInformation($"Printer2 is working. {_settings.PrinterDelaySecond}");await Task.Delay(TimeSpan.FromSeconds(_settings.PrinterDelaySecond), stoppingToken);}}}来看看里面的细节。我们的这个服务继承了BackgroundService,就一定要实现里面的ExecuteAsync,至于StartAsync和StopAsync等方法可以选择性的override。我们ExecuteAsync在里面就是输出了一下日志,然后休眠在配置文件中指定的秒数。这个任务可以说是最简单的例子了,其中还用到了依赖注入,如果想在任务中注入数据仓储之类的,应该就不需要再多说了。同样的方式再写一个定时的。定时跑的后台任务这里借助了Timer来完成定时跑的功能,同样的还可以结合Quartz来完成。public class TimerHostedService : BackgroundService{//other ...private Timer _timer;protected override Task ExecuteAsync(CancellationToken stoppingToken){_timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(_settings.TimerPeriod));return Task.CompletedTask;}private void DoWork(object state){_logger.LogInformation("Timer is working");}public override Task StopAsync(CancellationToken cancellationToken){_logger.LogInformation("Timer is stopping");_timer?.Change(Timeout.Infinite, 0);return base.StopAsync(cancellationToken);}public override void Dispose(){_timer?.Dispose();base.Dispose();}}和第一个后台任务相比,没有太大的差异。下面我们先来看看如何用控制台的形式来启动这两个任务。控制台形式这里会同时引入NLog来记录任务跑的日志,方便我们观察。Main函数的代码如下:class Program{static async Task Main(string[] args){var builder = new HostBuilder()//logging.ConfigureLogging(factory =>{//use nlogfactory.AddNLog(new NLogProviderOptions { CaptureMessageTemplates = true, CaptureMessageProperties = true });NLog.LogManager.LoadConfiguration("nlog.config");})//host config.ConfigureHostConfiguration(config =>{//command lineif (args != null){config.AddCommandLine(args);}})//app config.ConfigureAppConfiguration((hostContext, config) =>{var env = hostContext.HostingEnvironment;config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true).AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);config.AddEnvironmentVariables();if (args != null){config.AddCommandLine(args);}})//service.ConfigureServices((hostContext, services) =>{services.AddOptions();services.Configure<AppSettings>(hostContext.Configuration.GetSection("AppSettings"));//basic usageservices.AddHostedService<PrinterHostedService2>();services.AddHostedService<TimerHostedService>();}) ;//consoleawait builder.RunConsoleAsync();////start and wait for shutdown//var host = builder.Build();//using (host)//{// await host.StartAsync();// await host.WaitForShutdownAsync();//}}}对于控制台的方式,需要我们对HostBuilder有一定的了解,虽说它和WebHostBuild有相似的地方。可能大部分时候,我们是直接使用了WebHost.CreateDefaultBuilder(args)来构造的,如果对CreateDefaultBuilder里面的内容没有了解,那么对上面的代码可能就不会太清晰。上述代码的大致流程如下:new一个HostBuilder对象配置日志,主要是接入了NLogHost的配置,这里主要是引入了CommandLine,因为需要传递参数给程序应用的配置,指定了配置文件,和引入CommandLineService的配置,这个就和我们在Startup里面写的差不多了,最主要的是我们的后台服务要在这里注入启动其中,2-5的顺序可以按个人习惯来写,里面的内容也和我们写Startup大同小异。第6步,启动的时候,有多种方式,这里列出了两种行为等价的方式。a. 通过RunConsoleAsync的方式来启动b. 先StartAsync然后再WaitForShutdownAsyncRunConsoleAsync的奥秘,我觉得还是直接看下面的代码比较容易懂。/// <summary>/// Listens for Ctrl+C or SIGTERM and calls <see cref="IApplicationLifetime.StopApplication"/> to start the shutdown process./// This will unblock extensions like RunAsync and WaitForShutdownAsync./// </summary>/// <param name="hostBuilder">The <see cref="IHostBuilder" /> to configure.</param>/// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns>public static IHostBuilder UseConsoleLifetime(this IHostBuilder hostBuilder){return hostBuilder.ConfigureServices((context, collection) => collection.AddSingleton<IHostLifetime, ConsoleLifetime>());}/// <summary>/// Enables console support, builds and starts the host, and waits for Ctrl+C or SIGTE
打造自己的.NET Core项目模板
前言每个人都有自己习惯的项目结构,有人的喜欢在项目里面建解决方案文件夹;有的人喜欢传统的三层命名;有的人喜欢单一,简单的项目一个csproj就搞定。。反正就是萝卜青菜,各有所爱。可能不同的公司对这些会有特定的要求,也可能会随开发自己的想法去实践。那么,问题就来了。如果有一个新项目,你会怎么去创建?可能比较多的方式会是下面三种:简单粗暴型,打开VS就是右键添加,然后引入一堆包,每个项目添加引用。脚本型,基于dotnet cli,创建解决方案,创建项目,添加包,添加项目引用。高大上型,VS项目模板,直接集成到VS上面了。以前我也是基于dotnet cli写好了sh或ps的脚本,然后用这些脚本来生成新项目。但是呢,这三种方式,始终都有不尽人意的地方。因为建好的都是空模板,还要做一堆复杂的操作才可以让项目“正常”的跑起来。比如,这个公共类要抄过来,那个公共类要抄过来。。。这不是明摆着浪费时间嘛。。。下面介绍一个小办法来帮大家省点时间。基于dotnet cli创建自己的项目模板,也就是大家常说的脚手架。dotnet cli项目模板预热开始正题之前,我们先看一下dotnet cli自带的一些模板。可以看到种类还是很多的,由于工作大部分时间都是在写WebAPI,所以这里就用WebAPI来写个简单的模板。下面我们就基于dotnet cli写一个自己的模板。编写自己的模板既然是模板,就肯定会有一个样例项目。下面我们建一个样例项目,大致成这样,大家完全可以按照自己习惯来。这其实就是一个普通的项目,里面添加了NLog,Swagger,Dapper等组件,各个项目的引用关系是建好的。该有的公共类,里面也都包含了,好比宇内分享的那个WebHostBuilderJexusExtensions。下面是这个模板跑起来的效果。就是一个简单的Swagger页面。现在样例已经有了,要怎么把这个样例变成一个模板呢?答案就是template.json!在样例的根目录创建一个文件夹.template.config,同时在这个文件夹下面创建template.json。示例如下:{"author": "Catcher Wong", //必须"classifications": [ "Web/WebAPI" ], //必须,这个对应模板的Tags"name": "TplDemo", //必须,这个对应模板的Templates"identity": "TplDemoTemplate", //可选,模板的唯一名称"shortName": "tpl", //必须,这个对应模板的Short Name"tags": {"language": "C#" ,"type":"project"},"sourceName": "TplDemo", // 可选,要替换的名字"preferNameDirectory": true // 可选,添加目录}在这里,有几个比较重要的东西,一个是shortName,一个是sourceName。shortName,简写,偷懒必备,好比能写 -h 就绝对不写 --helpsourceName,这是个可选的字段,它的值会替换指定的项目名,正常是把项目名赋值在这里。如果不指定,创建的项目就和样例项目保持一致。在写完template.json之后,还需要安装一下这个模板到我们的cli中。使用 dotnet new -i进行模板的安装。下面是安装示例。dotnet new -i ./content/TplDemo这里要注意的是,与.template.config文件夹同级的目录,都会被打包进模板中。在执行安装命令之后,就可以看到我们的模板已经安装好了。这个时候已经迫不及待的想来试试这个模板了。先来看看这个模板的帮助信息。dotnet new tpl -h因为我们目前还没有设置参数,所以这里显示的是还没有参数。下面来创建一个项目试试。从创建一个项目,到运行起来,很简单,效果也是我们预期的。下面来看看,新建的这个HelloTpl这个项目的目录结构和我们的模板是否一样。可以看到,除了名字,其他的内容都是一样的。是不是感觉又可以少复制粘贴好多代码了。虽说,现在建项目,已经能把一个大的模板完整的copy出来了,但是始终不是很灵活!可能有小伙伴会问,明明已经很方便了呀,为什么还会说它不灵活呢?且听我慢慢道来。如果说这个模板是个大而全的模板,包含了中间件A,中间件B,中间件C等N个中间件!而在建新项目的时候,已经明确了只用中间件A,那么其他的中间件对我们来说,可能就没有太大的存在意义!很多时候,不会想让这些多余的文件出现在代码中,有没有办法来控制呢?答案是肯定的!可以把不需要的文件排除掉就可以了。文件过滤模板项目中有一个RequestLogMiddleware,就用它来做例子。我们只需要做下面几件事就可以了。第一步,在template.json中添加过滤加入一个名字为EnableRequestLog的symbol。同时指定源文件{"author": "Catcher Wong",//others..."symbols":{//是否启用RequestLog这个Middleware"EnableRequestLog": {"type": "parameter", //它是参数"dataType":"bool", //bool类型的参数"defaultValue": "false" //默认是不启用}},"sources": [{"modifiers": [{"condition": "(!EnableRequestLog)", //条件,由EnableRequestLog参数决定"exclude": [ //排除下面的文件"src/TplDemo/Middlewares/RequestLogMiddleware.cs","src/TplDemo/Middlewares/RequestLogServiceCollectionExtensions.cs"]}]}]}第二步,在模板的代码中做一下处理主要是Startup.cs,因为Middleware就是在这里启用的。using System;//other using...using TplDemo.Core;#if (EnableRequestLog)using TplDemo.Middlewares;#endif/// <summary>////// </summary>public class Startup{public void Configure(IApplicationBuilder app, IHostingEnvironment env){//other code....#if (EnableRequestLog)//request Logapp.UseRequestLog();#endifapp.UseMvc(routes =>{routes.MapRoute(name: "default",template: "{controller=Home}/{action=Index}/{id?}");});}}这样的话,只要EnableRequestLog是true,那么就可以包含这两段代码了。下面更新一下已经安装的模板。这个时候再去看它的帮助信息,已经可以看到我们加的参数了。下面先建一个默认的(不启用RequestLog)dotnet new tpl -n NoLog这个命令等价于dotnet new tpl -n WithLog -E false下面是建好之后的目录结构和Startup.cs可以看到RequestLog相关的东西都已经不见了。再建一个启用RequestLog的,看看是不是真的起作用了。dotnet new tpl -n WithLog -E true可以看到,效果已经出来了。下面在介绍一个比较有用的特性。动态切换,这个其实和上面介绍的内容相似。动态切换直接举个例子来说明吧。假设我们的模板支持MSSQL, MySQL, PgSQL和SQLite四种数据库操作在新建一个项目的时候,只需要其中一种,好比说要建一个PgSQL的,肯定就不想看到其他三种。这里不想看到,有两个地方,一个是nuget包的引用,一个是代码。上一小节是对某个具体的功能进行了开关的操作,这里有了4个,我们要怎么处理呢?我们可以用类型是choice的参数来完成这个操作。修改template.json,加入下面的内容{"author": "Catcher Wong",//others"symbols":{"sqlType": {"type": "parameter","datatype": "choice","choices": [{"choice": "MsSQL","description": "MS SQL Server"},{"choice": "MySQL","description": "MySQL"},{"choice": "PgSQL","description": "PostgreSQL"},{"choice": "SQLite","description": "SQLite"}],"defaultValue": "MsSQL","description": "The type of SQL to use"},"MsSQL": {"type": "computed","value": "(sqlType == "MsSQL")"},"MySQL": {"type": "computed","value": "(sqlType == "MySQL")"},"PgSQL": {"type": "computed","value": "(sqlType == "PgSQL")"},"SQLite": {"type": "computed","value": "(sqlType == "SQLite")"}}}看了上面的JSON内容之后,相信大家也知道个所以然了。有一个名为sqlType的参数,它有几中数据库选择,默认是MsSQL。还另外定义了几个计算型的参数,它的取值是和sqlType的值息息相关的。MsSQL,MySQL,PgSQL和SQLite这4个参数也是我们在代码里要用到的!!修改csproj文件,让它可以根据sqlType来动态引用nuget包,我们加入下面的内容<ItemGroup Condition="'$(MySQL)' == 'True' "
用Scrutor来简化ASP.NET Core的DI注册
目录背景Scrutor简介Scrutor的简单使用注册接口的实现类注册类自身重复注册处理策略总结相关文章背景在我们编写ASP.NET Core代码的时候,总是离不开依赖注入这东西。而且对于这一块,我们有非常多的选择,比如:M$ 的DI,Autofac,Ninject,Windsor 等。由于M$自带了一个DI框架,所以一般情况下都会优先使用。虽说功能不是特别全,但也基本满足使用了。正常情况下(包括好多示例代码),在要注册的服务数量比较少时,我们会选择一个一个的去注册。好比下面的示例:services.AddTransient<IUserRepository, UserRepository>();services.AddTransient<IUserService, UserService>();在数量小于5个的时候,这样的做法还可以接受,但是,数量一多,还这样子秀操作,可就有点接受不了了。可能会经常出现这样的问题,新加了一个东西,忘记在Startup上面注册,下一秒得到的就是类似下面的错误:System.InvalidOperationException: Unable to resolve service for type 'ScrutorTest.IProductRepository' while attempting to activate 'ScrutorTest.Controllers.ValuesController'.at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.GetService(IServiceProvider sp, Type type, Type requiredBy, Boolean isDefaultParameterRequired)at lambda_method(Closure , IServiceProvider , Object[] )at Microsoft.AspNetCore.Mvc.Controllers.ControllerActivatorProvider.<>c__DisplayClass4_0.<CreateActivator>b__0(ControllerContext controllerContext)at Microsoft.AspNetCore.Mvc.Controllers.ControllerFactoryProvider.<>c__DisplayClass5_0.<CreateControllerFactory>g__CreateController|0(ControllerContext controllerContext)at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeInnerFilterAsync()at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextResourceFilter()at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResourceExecutedContext context)at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeFilterPipelineAsync()at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeAsync()at Microsoft.AspNetCore.Builder.RouterMiddleware.Invoke(HttpContext httpContext)at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)这样一来一回,其实也是挺浪费时间的。为了避免这种情况,我们往往会根据规律在注册的时候,用反射进行批量注册,后面按照对应的规律去写业务代码,就可以避免上面这种问题了。对于这个问题,本文将介绍一个扩展库,来帮我们简化这些操作。Scrutor简介Scrutor是 Kristian Hellang 大神写的一个基于Microsoft.Extensions.DependencyInjection的一个扩展库,主要是为了简化我们对DI的操作。Scrutor主要提供了两个扩展方法给我们使用,一个是Scan,一个是Decorate。本文主要讲的是Scan这个方法。Scrutor的简单使用注册接口的实现类这种情形应该是我们用的最多的一种,所以优先来说这种情况。假设我们有下面几个接口和实现类,public interface IUserService { }public class UserService : IUserService { }public interface IUserRepository { }public class UserRepository : IUserRepository { }public interface IProductRepository { }public class ProductRepository : IProductRepository { }现在我们只需要注册UserRepository和ProductRepository,services.Scan(scan => scan.FromAssemblyOf<Startup>().AddClasses(classes => classes.Where(t=>t.Name.EndsWith("repository",StringComparison.OrdinalIgnoreCase))).AsImplementedInterfaces().WithTransientLifetime());简单解释一下,上面的代码做了什么事:FromAssemblyOf<Startup> 表示加载Startup这个类所在的程序集AddClasses 表示要注册那些类,上面的代码还做了过滤,只留下了以 repository 结尾的类AsImplementedInterfaces 表示将类型注册为提供其所有公共接口作为服务WithTransientLifetime 表示注册的生命周期为 Transient如果了解过Autofac的朋友,看到这样的写法应该很熟悉。对于上面的例子,它等价于下面的代码services.AddTransient<IUserRepository, UserRepository>();services.AddTransient<IProductRepository, ProductRepository>();如果我们在注册完成后,想看一下我们自己注册的信息,可以加上下面的代码:var list = services.Where(x => x.ServiceType.Namespace.Equals("ScrutorTest", StringComparison.OrdinalIgnoreCase)).ToList();foreach (var item in list){Console.WriteLine($"{item.Lifetime},{item.ImplementationType},{item.ServiceType}");}运行dotnet run之后,可以看到下面的输出Singleton,ScrutorTest.UserRepository,ScrutorTest.IUserRepositorySingleton,ScrutorTest.ProductRepository,ScrutorTest.IProductRepository这个时候,如果我们加了一个 IOrderRepository 和 OrderRepostity , 就不需要在Startup上面多写一行注册代码了,Scrutor已经帮我们自动处理了。接下来,我们需要把UserService也注册进去,我们完全可以照葫芦画瓢了。services.Scan(scan => scan.FromAssemblyOf<Startup>().AddClasses(classes => classes.Where(t=>t.Name.EndsWith("repository",StringComparison.OrdinalIgnoreCase))).AsImplementedInterfaces().WithTransientLifetime());services.Scan(scan => scan.FromAssemblyOf<Startup>().AddClasses(classes => classes.Where(t => t.Name.EndsWith("service", StringComparison.OrdinalIgnoreCase))).AsImplementedInterfaces().WithTransientLifetime());也可以略微简单一点点,一个scan里面搞定所有services.Scan(scan => scan.FromAssemblyOf<Startup>().AddClasses(classes => classes.Where(t=>t.Name.EndsWith("repository",StringComparison.OrdinalIgnoreCase))).AsImplementedInterfaces().WithTransientLifetime().AddClasses(classes => classes.Where(t => t.Name.EndsWith("service", StringComparison.OrdinalIgnoreCase))).AsImplementedInterfaces().WithScopedLifetime()//换一下生命周期);这个时候结果如下:Transient,ScrutorTest.UserRepository,ScrutorTest.IUserRepositoryTransient,ScrutorTest.ProductRepository,ScrutorTest.IProductRepositoryScoped,ScrutorTest.UserService,ScrutorTest.IUserService虽然效果一样,但是总想着有没有一些更简单的方法。很多时候,我们写一些接口和实现类的时候,都会根据这样的习惯来命名,定义一个接口IClass,它的实现类就是Class。针对这种情形,Scrutor提供了一个简便的方法来帮助我们处理。使用 AsM
一张图理清ASP.NET Core启动流程
ASP.NET Core知多少系列:总体介绍及目录1. 引言对于ASP.NET Core应用程序来说,我们要记住非常重要的一点是:其本质上是一个独立的控制台应用,它并不是必需在IIS内部托管且并不需要IIS来启动运行(而这正是ASP.NET Core跨平台的基石)。ASP.NET Core应用程序拥有一个内置的Self-Hosted(自托管)的Web Server(Web服务器),用来处理外部请求。不管是托管还是自托管,都离不开Host(宿主)。在ASP.NET Core应用中通过配置并启动一个Host来完成应用程序的启动和其生命周期的管理(如下图所示)。而Host的主要的职责就是Web Server的配置和Pilpeline(请求处理管道)的构建。这张图描述了一个总体的启动流程,从上图中我们知道ASP.NET Core应用程序的启动主要包含三个步骤:CreateDefaultBuilder():创建IWebHostBuilderBuild():IWebHostBuilder负责创建IWebHostRun():启动IWebHost所以,ASP.NET Core应用的启动本质上是启动作为宿主的WebHost对象。其主要涉及到两个关键对象IWebHostBuilder和IWebHost,它们的内部实现是ASP.NET Core应用的核心所在。下面我们就结合源码并梳理调用堆栈来一探究竟!2. 宿主构造器:IWebHostBuilder在启动IWebHost宿主之前,我们需要完成对IWebHost的创建和配置。而这一项工作需要借助IWebHostBuilder对象来完成的,ASP.NET Core中提供了默认实现WebHostBuilder。而WebHostBuilder是由WebHost的同名工具类(Microsoft.AspNetCore命名空间下)中的CreateDefaultBuilder方法创建的。从上图中我们可以看出CreateDefaultBuilder()方法主要干了六件大事:UseKestrel:使用Kestrel作为Web server。UseContentRoot:指定Web host使用的content root(内容根目录),比如Views。默认为当前应用程序根目录。ConfigureAppConfiguration:设置当前应用程序配置。主要是读取 appsettinggs.json 配置文件、开发环境中配置的UserSecrets、添加环境变量和命令行参数 。ConfigureLogging:读取配置文件中的Logging节点,配置日志系统。UseIISIntegration:使用IISIntegration 中间件。UseDefaultServiceProvider:设置默认的依赖注入容器。创建完毕WebHostBuilder后,通过调用UseStartup()来指定启动类,来为后续服务的注册及中间件的注册提供入口。3. 宿主:IWebHost在ASP.Net Core中定义了IWebHost用来表示Web应用的宿主,并提供了一个默认实现WebHost。宿主的创建是通过调用IWebHostBuilder的Build()方法来完成的。那该方法主要做了哪些事情呢,我们来看下面这张【ASP.NET Core启动流程调用堆栈】中的黄色边框部分:其核心主要在于WebHost的创建,又可以划分为三个部分:构建依赖注入容器,初始通用服务的注册:BuildCommonService();实例化WebHost:var host = new WebHost(...);初始化WebHost,也就是构建由中间件组成的请求处理管道:host.Initialize();3.1. 注册初始通用服务BuildBuildCommonService方法主要做了两件事:查找HostingStartupAttribute特性以应用其他程序集中的启动配置注册通用服务若配置了启动程序集,则发现并以IStartup类型注入到IOC容器中3.2. 创建IWebHostpublic IWebHost Build(){//省略部分代码var host = new WebHost(applicationServices,hostingServiceProvider,_options,_config,hostingStartupErrors);}host.Initialize();return host;}3.3. 构建请求处理管道请求管道的构建,主要是中间件之间的衔接处理。而请求处理管道的构建,又包含三个主要部分:注册Startup中绑定的服务;配置IServer;构建管道请求管道的构建主要是借助于IApplicationBuilder,相关类图如下:4. 启动WebHostWebHost的启动主要分为两步:再次确认请求管道正确创建启动Server以监听请求启动 HostedService4.1. 确认请求管道的创建从图中可以看出,第一步调用Initialize()方法主要是取保请求管道的正确创建。其内部主要是对BuildApplication()方法的调用,与我们上面所讲WebHost的构建环节具有相同的调用堆栈。而最终返回的正是由中间件衔接而成的RequestDelegate类型代表的请求管道。4.2. 启动Server我们先来看下类图:从类图中我们可以看出IServer接口主要定义了一个只读的特性集合属性、一个启动和停止的方法声明。在创建宿主构造器IWebHostBuilder时我们通过调用UseKestrel()方法指定了使用KestrelServer作为默认的IServer实现。其方法申明中接收了一个IHttpApplication<TContext> application的参数,从命名来看,它代表一个Http应用程序,我们来看下具体的接口定义:其主要定义了三个方法,第一个方法用来创建请求上下文;第二个方法用来处理请求;第三个方法用来释放上下文。而至于请求上下文,是用来携带请求和返回响应的核心参数,其贯穿与整个请求处理管道之中。ASP.NET Core中提供了默认的实现HostingApplication,其构造函数接收一个RequestDelegate _application(也就是链接中间件形成的处理管道)用来处理请求。var httpContextFactory = _applicationServices.GetRequiredService<IHttpContextFactory>();var hostingApp = new HostingApplication(_application, _logger, diagnosticSource, httpContextFactory);4.3. 启动IHostedServiceIHostedService接口用来定义后台任务,通过实现该接口并注册到Ioc容器中,它会随着ASP.NET Core 程序启动而启动,终止而终止。5. 总结结合源码,通过对ASP.NET Core运行调用堆栈的梳理,其启动流程的总体脉络一目了然,并且了解到主要的几个关键对象:负责创建IWebHost的宿主构造器IWebHostBuilder代表宿主的IWebHost接口用于构建请求管道的IApplicationBuilder中间件衔接而成的RequestDelegate代表Web Server的IServer接口贯穿请求处理管道的请求上下文HttpContext可以用来注册后台服务的IHostedService接口本文经「原本」原创认证,作者圣杰,访问yuanben.io查询【E5OW396N】获取授权信息。
使用Bitbucket Pipeline进行.Net Core项目的自动构建、测试和部署
1. 引言首先,Bitbucket提供支持Mercurial和Git版本控制系统的网络托管服务。简单来说,它类似于GitHub,不同之处在于它支持个人免费创建私有项目仓库。除此之外,Bitbucket提供的Pipeline功能可以帮助我们进行项目的自动构建、测试和部署。2. 使用指南该项目是使用Abp创建的.Net Core版本的模板项目,项目结构如下:点击Pipeline,我们选择.NET Core,即可创建用于配置Pipeline的配置文件bitbucket-pipelines.yml。从图中可以看出,其配置很简单,主要包括以下几个部分:image:了解过docker的同学肯定不陌生,通过指定image来告诉pipeline要拉取何种镜像用于项目编译。export:通过export指定我们要编译的项目名和测试项目名。(注意:需要使用相对路径)。dotnet:.net core的还原、编译和测试命令。由于我们的项目是基于.net core 2.0,且分层架构如下:我们要对bitbucket-pipelines.yml做以下几项修改:修改完成,点击Commit File即刻进入Pipeline运行界面,运行结果如下:至此,我们就完成了.NET Core项目的构建和测试。那如何让它自动进行这两项操作呢?简单,回到Pipeline界面,点击【Schedules】菜单,创建一个【Schedule】即可。那如何完成部署呢?因为我使用的Micosoft Azure进行部署,所以按照官方文档Deploy to Microsoft Azure,我们仅需在bitbucket-pipelines.yml后面添加一条git push命令即可,如下:- git push https://$AZURE_LOGIN:$AZURE_PASSWORD@abpeshop.scm.azurewebsites.net/Abpeshop.git master其中$AZURE_LOGIN和$AZURE_PASSWORD是Azure的部署凭据,我们需要在我们当前项目仓库中定义环境变量,如下图所示:最后无图无真相:3. 最后Bitbucket提供的Pipeline的免费构建时间为50mins/月,但对于我们简单尝鲜来说是足够了!当然如果不够用,其付费策略也很优惠,2$/月,拥有500mins/月的构建时间。当然微软的VSTS,也是一个很好地选择。参考文章:.NET Core 2.0 持续集成,持续发布环境Building NuGet (.NET Core) Using Atlassian Bitbucket PipelinesBuilding .NET Core apps with BitBucket Pipelines and Docker我的博客即将搬运同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan
.NET Core容器化@Docker
温馨提示:本文适合动手演练,效果更佳。 1. 引言我们知道. NET Core最大的特性之一就是跨平台,而对于跨平台,似乎大家印象中就是可以在非Windows系统上部署运行。而至于如何操作,可能就有所欠缺。那这一节我们就结合简单实例一步一步教你如何借助Docker来容器化 .NET Core应用,以完成跨平台的构建和部署。 2. 环境准备自从玩.NET就一直和Windows系统打交道,如果还基于Windows来展开本节内容,不就跑题了吗?!那咱们就切换到Linux系统。如果没有Linux基础和Docker基础,请自觉完成以下两个实验:腾讯云开发者实验室:Linux 基础入门腾讯云开发者实验室:搭建 Docker 环境完成了以上两个实验后,我们就离Linux的世界更近一步。因为后续是基于Linux-CentOS系统进行实操演练,没有Linux上机环境的,可以考虑从腾讯云实验室列表找一个CentOS相关的实验项目作为本文的演练环境。 3. Docker简介在开始之前,有必要对Docker做一下简单了解,可以参考我的上一篇文章Hello Docker。这里就简要的再重复一下。Docker是用Go语言编写基于Linux操作系统的一些特性开发的,其提供了操作系统级别的抽象,是一种容器管理技术,它隔离了应用程序对基础架构(操作系统等)的依赖。相较于虚拟机而言,Docker共享的是宿主机的硬件资源,使用容器来提供独立的运行环境来运行应用。虚拟机则是基于Supervisor(虚拟机管理程序)使用虚拟化技术来提供隔离的虚拟机,在虚拟机的操作系统上提供运行环境!虽然两者都提供了很好的资源隔离,但很明显Docker的虚拟化开销更低!Docker涉及了三个核心概念:Register、Image、Container。1. Registry:仓库。用来存储Docker镜像,比如Docker官方的Docker Hub就是一个公开的仓库,在上面我们可以下载我们需要的镜像。2. Image:镜像。开发人员创建一个应用程序或服务,并将它及其依赖关系打包到一个容器镜像中。镜像是应用程序的配置及其依赖关系的静态形式。3. Container:容器。Container是镜像的运行实例,它是一个隔离的、资源受控的可移植的运行时环境,其中包含操作系统、需要运行的程序、运行程序的相关依赖、环境变量等。它们三者的相互作用关系是:当我们执行Docker pull或Docker run命令时,若本地无所需的镜像,那么将会从仓库(一般为DockerHub)下载(pull)一个镜像。Docker执行run方法得到一个容器,用户在容器里执行各种操作。Docker执行commit方法将一个容器转化为镜像。Docker利用login、push等命令将本地镜像推送(push)到仓库。其他机器或服务器上就可以使用该镜像去生成容器,进而运行相应的应用程序。4. 安装Docker4.1. 使用脚本自动安装Docker在测试或开发环境中 Docker 官方为了简化安装流程,提供了一套便捷的安装脚本,CentOS系统上可以使用这套脚本安装://使用脚本自动化安装Docker$ curl -fsSL get.docker.com -o get-docker.sh$ sudo sh get-docker.sh --mirror Aliyun4.2. 启动Docker执行这个命令后,脚本就会自动的将一切准备工作做好,并且把 Docker CE 的 Edge 版本安装在系统中。//启动 Docker CE$ sudo systemctl enable docker$ sudo systemctl start docker//查看docker版本$ sudo docker -vDocker version 1.12.6, build ec8512b/1.12.64.3 测试Docker是否正确安装命令行执行docker run hello-world:$ docker run hello-worldUnable to find image 'hello-world:latest' locallylatest: Pulling from library/hello-worldca4f61b1923c: Pull completeDigest: sha256:be0cd392e45be79ffeffa6b05338b98ebb16c87b255f48e297ec7f98e123905cStatus: Downloaded newer image for hello-world:latestHello from Docker!This message shows that your installation appears to be working correctly.To generate this message, Docker took the following steps:1. The Docker client contacted the Docker daemon.2. The Docker daemon pulled the "hello-world" image from the Docker Hub.(amd64)3. The Docker daemon created a new container from that image which runs theexecutable that produces the output you are currently reading.4. The Docker daemon streamed that output to the Docker client, which sent itto your terminal.To try something more ambitious, you can run an Ubuntu container with:$ docker run -it ubuntu bashShare images, automate workflows, and more with a free Docker ID:https://cloud.docker.com/For more examples and ideas, visit:https://docs.docker.com/engine/userguide/当执行docker run hello-world时,docker首先会从本地找hello-world的镜像,如果本地没有,它将会从默认的镜像仓库Docker Hub上拉取镜像。镜像拉取到本地后,就实例化镜像得到容器,输出Hello from Docker!。4.4. 配置镜像加速因为默认的镜像仓库远在国外,拉取一个小的镜像时间还可以忍受,若拉取一个上G的镜像就有点太折磨人了,我们使用DaoCloud镜像加速器来进行镜像加速。Linux上配置方法如下:$ curl -sSL https://get.daocloud.io/daotools/set_mirror.sh | sh -s http://37bb3af1.m.daocloud.io`$ sudo systemctl restart docker5. Hello Docker With .NET CoreDocker安装完毕,我们来结合.NET Core玩一玩吧。5.1. 拉取microsoft/dotnet镜像命令行执行docker pull microsoft/dotnet,等几分钟后即可安装完毕,执行docker images可以看到本地已经包含microsoft/dotnet、docker.io/hello-world两个镜像。5.2. 运行microsoft/dotnet镜像使用docker run <image>可以启动镜像,通过指定参数-it以交互模式(进入容器内部)启动。依次执行以下命令://启动一个dotnet镜像$ docker run -it microsoft/dotnet//创建项目名为HelloDocker.Web的.NET Core MVC项目dotnet new mvc -n HelloDocker.Web//进入HelloDocker.Web文件夹cd HelloDocker.Web//启动.NET Core MVC项目dotnet run运行结果如下所示:[root@iZ288a3qazlZ ~]# docker run -it microsoft/dotnetroot@816b4e94de67:/# dotnet new mvc -n HelloDocker.WebThe template "ASP.NET Core Web App (Model-View-Controller)" was created successfully.This template contains technologies from parties other than Microsoft, see https://aka.ms/template-3pn for details.Processing post-creation actions...Running 'dotnet restore' on HelloDocker.Web/HelloDocker.Web.csproj...Restoring packages for /HelloDocker.Web/HelloDocker.Web.csproj...Generating MSBuild file /HelloDocker.Web/obj/HelloDocker.Web.csproj.nuget.g.props.Generating MSBuild file /HelloDocker.Web/obj/HelloDocker.Web.csproj.nuget.g.targets.Restore completed in 1.83 sec for /HelloDocker.Web/HelloDocker.Web.csproj.Restoring packages for /HelloDocker.Web/HelloDocker.Web.csproj...Restore completed in 376.14 ms for /HelloDocker.Web/HelloDocker.Web.csproj.Restore succeeded.root@816b4e94de67:/# cd HelloDocker.Webroot@816b4e94de67:/HelloDocker.Web# dotnet runwarn: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[35]No XML encryptor configured. Key {727df196-978f-4df8-b3d3-e92a77e410ee} may be persisted to storage in unencrypted form.Hosting environment: ProductionContent root path: /HelloDocker.WebNow listening on: http://localhost:5000Application started. Press Ctrl+C to shut down.键盘按住Ctrl+C即可关闭应用,输入exit即可退出当前容器。是不是简单的几步就完成了一个.NET Core MVC项目的创建和运行?!这个时候你可能会好奇,Linux宿主机上并没有安装.NET Core SDK啊,MVC项目是如何创建的呢?这就是Docker神奇的地方,我们从镜像仓库中拉取的dotnet镜像,包含了创建、构建、运行.NET Core项目所需的一切依赖和运行时环境。退出容器之后,执行find -name HelloDocker.Web(查
.NET Core容器化之多容器应用部署@Docker-Compose
1.引言紧接上篇.NET Core容器化@Docker,这一节我们先来介绍如何使用Nginx来完成.NET Core应用的反向代理,然后再介绍多容器应用的部署问题。2. Why Need Nginx.NET Core中默认的Web Server为Kestrel。Kestrel is great for serving dynamic content from ASP.NET, however the web serving parts aren’t as feature rich as full-featured servers like IIS, Apache or Nginx. A reverse proxy-server can allow you to offload work like serving static content, caching requests, compressing requests, and SSL termination from the HTTP server.Kestrel可以很好的用来为ASP.NET提供动态内容,然而在Web服务方面没有IIS、Apache、Nginx这些全功能的服务器完善。我们可以借助一个反向代理服务器接收来自互联网的HTTP请求并在经过一些初步处理(比如请求的缓存和压缩、提供静态内容、SSL Termination)后将其转发给Kestrel。借助反向代理服务器(本文使用Nginx),不仅可以给我们的Web网站提供了一个可选的附加层配置和防御,而且可以简化负载均衡和SSL设置。而更重要的是,反向代理服务器可以很好的与现有的基础设施进行整合。3. Hello Nginx同样我们还是基于Docker来试玩一下Nginx。//拉取Nginx镜像$ docker pull nginx//启动Nginx容器$ docker run -d -p 8080:80 --name hellonginx nginx上面我们以后台运行的方式启动了一个命名为hellonginx的nginx容器,其端口映射到宿主机的8080端口,我们现在可以通过浏览器直接访问http://<ip address>:8080即可看到nginx的欢迎界面。至此,一个Nginx容器就启动完毕了。那如何进行反向代理呢?别急,我们一步一步来。4. 反向代理.NET Core MVC4.1. 启动Web容器还记得我们上一篇本地打包MVC项目创建的hellodocker.web的镜像吗?这里我们再启动该镜像创建一个容器://启动一个helodocker.web的镜像并命名容器为hellodocker.web.nginx# docker run -d -p 5000:5000 --name hellodocker.web.nginx hellodocker.web160166b3556358502a366d1002567972e242f0c8be3a751da0a525f86c124933//尝试访问刚刚运行的容器[root@iZ288a3qazlZ ~]# curl -I http://localhost:5000HTTP/1.1 200 OKDate: Sun, 24 Dec 2017 02:48:16 GMTContent-Type: text/html; charset=utf-8Server: KestrelTransfer-Encoding: chunkedOK,我们开放了宿主机的5000端口用来映射我们启动的MVC容器。4.2. 配置反向代理下面我们就来配置Nginx来反向代理我们刚启动的Web容器。要想Nginx成功代理指定的容器内运行的Web网站,首先我们得知道容器对应的IPAddress。使用docker inspect <container id/name>即可查到。//查看正在运行的容器列表$ docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESd046b7b878a0 hellodocker.web "dotnet run" 5 seconds ago Up 3 seconds 0.0.0.0:5000->5000/tcp hellodocker.web.nginx//使用`|`管道操作符加上`grep`过滤操作符可以直接提取我们要查找的关键字$ docker inspect hellodocker.web.nginx | grep IPAddress"SecondaryIPAddresses": null,"IPAddress": "192.168.0.5","IPAddress": "192.168.0.5",从上面可以看到我的Web容器运行在宿主机的192.168.0.5:5000。下面我们配置Nginx转发请求到192.168.0.5:5000即可完成反向代理。Nginx配置反向代理的配置文件路径为:/etc/nginx/conf.d/default.conf。我们可以通过本地创建一个配置文件挂载到Nginx的容器内部进行反向代理配置。$ cd demo$ mkdir nginx//创建my_nginx.conf文件$ touch my_nginx.conf$ vi my_nginx.confserver {listen 80;location / {proxy_pass http://192.168.0.5:5000;}}上面我们通过指定listen配置nginx去监听80端口,指定proxy_pass为我们Web容器的IP和端口完成反向代理文件的配置。接下来就是启动一个新的Nginx容器并通过挂载的方式将配置文件共享到容器内部。$ docker run -d -p 8080:80> -v $HOME/demo/nginx/my_nginx.conf:/etc/nginx/conf.d/default.conf> nginx95381aa56a336f65b6d01ff9964ae3364f37d25e5080673347c1878b3a5bb514/usr/bin/docker-current: Error response from daemon: driver failed programming external connectivity on endpoint elated_mccarthy (5a576d5991dd164db69b1c568c94c15e47ec7c67e43a3dd6982a2e9b83b60e08): Bind for 0.0.0.0:8080 failed: port is already allocated.我们发现容器启动失败,原因是8080端口被我们刚刚第一次启动的nginx容器占用了。怎么办?两个方法:第一种就是将刚才创建的nginx容器干掉;第二种就是映射到新的端口。这里选择第一种。$ docker ps1bd630b60019 nginx "nginx -g 'daemon off" 59 minutes ago Up 59 minutes 0.0.0.0:8080->80/tcp hellonginx//使用docker rm <container id>删除容器,指定-f进行强制删除$ docker rm 1bd630b60019 -f//重新启动Nginx容器$ docker run -d -p 8080:80> -v $HOME/demo/nginx/my_nginx.conf:/etc/nginx/conf.d/default.conf> nginx793d4c62ec8ac4658d75ea0ab4273a0b1f0a9a68f9708d2f85929872888b121d启动成功后,我们再在浏览器中访问http://<ipaddress>:8080,发现返回的不再是Nginx的默认欢迎页,而是我们启动的Web容器中运行的MVC的首页,说明反向代理配置成功!5. Docker Compose让一切更简单上面的步骤虽然简单,但要分两步进行:第一个就是我们的Web和Nginx要分两次部署,第二个就是我们必须知道Web容器的IP和端口号,以完成反向代理文件的配置。对于需要多个容器(比如需要Nginx、SqlServer、Redis、RabbitMQ等)协调运行的复杂应用中,使用以上方式进行部署时,很显然会很麻烦,而且还要为各个容器之间的网络连接而苦恼。还好,Docker体贴的为我们想到了这一点。借助Compose模块,我们可以编写一个docker-compose.yml文件,使用声明性语法启动一系列相互连接的容器,即可一步完成上面的任务。Docker Compose是一个用来定义和运行复杂应用的Docker工具。使用Compose,你可以在一个文件中定义一个多容器应用,然后使用一条命令来启动你的应用,完成一切准备工作。5.1. 安装Docker Compose依次执行以下命令:$ sudo curl -L https://github.com/docker/compose/releases/download/1.18.0/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose$ sudo chmod +x /usr/local/bin/docker-compose$ docker-compose --versiondocker-compose version 1.18.0, build 1719ceb5.2. 编写第一个docker-compose.ymldockers-compose.yml文件要定义在我们项目的文件夹下,我们的项目文件夹位于$HOME/demo/HelloDocker.Web。$ cd $HOME/demo/HelloDocker.Web$ touch docker-compose.yml$ vi docker-compose.ymlversion: '2'services:hellodocker-web:container_name: hellodocker.web.composebuild: .reverse-proxy:container_name: reverse-proxyimage: nginxports:- "9090:8080"volumes:- ./proxy.conf:/etc/nginx/conf.d/default.conf简单介绍下上面的配置文件,其中定义了两个服务:一个是hellodocker-web,即以我们当前项目目录来构建镜像并启动一个叫hellodocker.web.compose的容器。一个是reverse-proxy,用来使用nginx镜像进行反向代理,其中又通过指定volumes来使用挂载的方式进行配置。$ touch proxy.conf$ vi proxy.confserver {listen 8080;location / {proxy_pass http://hellodocker-web:5000;}}$ ls[root@iZ288a3qazlZ HelloDocker.Web]# lsappsettings.Development.json Controllers
.NET Core+MySql+Nginx 容器化部署
.NET Core容器化@Docker.NET Core容器化之多容器应用部署@Docker-Compose.NET Core+MySql+Nginx 容器化部署GitHub-Demo:Docker.NetCore.MySql1. 引言上两节我们通过简单的demo学习了docker的基本操作。这一节我们来一个进阶学习,完成ASP.NET Core + MySql + Nginx的容器化部署。本文是基于CentOS 7.4环境进行演示,示例项目可以访问Docker.NetCore.MySql进行下载。2. Hello MySQL同样我们还是以循序渐进的方式来展开。首先来基于Docker来试玩一下MySQL。2.1. 创建MySql实例//拉取mysql镜像docker pull mysql$ docker images$REPOSITORY TAG IMAGE ID CREATED SIZEdocker.io/mysql latest 7d83a47ab2d2 13 days ago 408.2 MB//创建一个mysql实例$ docker run --name hello.mysql -e MYSQL_ROOT_PASSWORD=123456 -d mysql$ docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESe21bbd84e0b5 mysql "docker-entrypoint.sh" 3 minutes ago Up 3 minutes 3306/tcp hello.mysql下面我们直接在容器中连接到我们刚刚创建的mysql数据库:$ docker exec -it hello.mysql> mysql -uroot -p123456mysql: [Warning] Using a password on the command line interface can be insecure.Welcome to the MySQL monitor. Commands end with ; or g.Your MySQL connection id is 8Server version: 5.7.20 MySQL Community Server (GPL)Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.Oracle is a registered trademark of Oracle Corporation and/or itsaffiliates. Other names may be trademarks of their respectiveowners.Type 'help;' or 'h' for help. Type 'c' to clear the current input statement.mysql> show databases;+--------------------+| Database |+--------------------+| information_schema || mysql || performance_schema || sys |+--------------------+4 rows in set (0.00 sec)2.2. 挂载数据卷上面创建的mysql实例其数据都在容器内部存储,这样就暴露了一个问题,如果容器销毁,那么对应的数据库数据就会丢失。那如何持久化存储容器内数据呢?我们可以通过挂载数据卷的方式来解决这一问题。//创建数据卷$ docker volume create --name hello.dbhello.db//查看数据卷信息$ docker volume inspect hello.db[{"Name": "hello.db","Driver": "local","Mountpoint": "/var/lib/docker/volumes/hello.db/_data","Labels": {},"Scope": "local"}]// 挂载数据卷启动MySql实例$ docker run --name hello.mysql> -v hello.db:/var/lib/mysql> -e MYSQL_ROOT_PASSWORD=123456 -d mysql上面是使用使用了docker volume create命令创建了一个数据卷,当然我们也可以自行挂载某个目录作为数据卷。3. 准备.NET Core+EFCore+MySql项目为了演示方便,我准备了一个ASP.NET Core+EFCore+MySql的示例项目。其结构如下所示:是基于.NET Core Mvc模板项目,其中定义了一个Product实体,并通过ProductsController暴露WebApi接口。核心代码如下:Product实体类:public class Product{public int ProductId { get; set; }public string Name { get; set; }public decimal Price { get; set; }public int StockQty { get; set; }}DbContext类:public class MySqlDbContext : DbContext{public MySqlDbContext (DbContextOptions<MySqlDbContext> options): base(options){}public DbSet<Product> Products { get; set; }}数据库初始化类:public class DbInitializer{public static void Initialize(MySqlDbContext context){context.Database.EnsureCreated();if (context.Products.Any()){return;}var products = new Product[]{new Product{Name="iphone 6",Price=5000,StockQty=10 },new Product{Name="iphone 7",Price=6000,StockQty=10 },new Product{Name="iphone 7 plus",Price=7000,StockQty=10 },new Product{Name="iphone x",Price=8000,StockQty=10 }};context.Products.AddRange(products);context.SaveChanges();}}该数据库初始化类会在项目启动时运行。详细代码可参考Docker.NetCore.MySql。4. 基于示例项目进行实操演练4.1 安装Git并Clone示例项目$ yum install git$ git --versiongit version 1.8.3.1$ cd ~/demo$ git clone https://github.com/yanshengjie/Docker.NetCore.MySql.gitCloning into 'Docker.NetCore.MySql'...remote: Counting objects: 155, done.remote: Compressing objects: 100% (125/125), done.remote: Total 155 (delta 42), reused 123 (delta 25), pack-reused 0Receiving objects: 100% (155/155), 534.30 KiB | 333.00 KiB/s, done.Resolving deltas: 100% (42/42), done.4.2. 构建镜像细心的你会发现,项目中已经定义了Dockerfile,所以我们可以直接使用docker build构建镜像。# cd Docker.NetCore.MySql[root@iZ288a3qazlZ Docker.NetCore.MySql]# lsappsettings.Development.json docker-compose.yml Program.cs Viewsappsettings.json Dockerfile proxy.conf wwwrootbundleconfig.json Docker.NetCore.MySql.csproj README.mdControllers LICENSE ScaffoldingReadMe.txtData Models Startup.cs//构建镜像# docker build -t docker.netcore.mysql .Sending build context to Docker daemon 3.045 MBStep 1 : FROM microsoft/dotnet:latest---> 7d4dc5c258ebStep 2 : WORKDIR /app---> Using cache---> 98d48a4e278cStep 3 : COPY . /app---> 6b1bf8bb5261Removing intermediate container b86460477977Step 4 : RUN dotnet restore---> Running in 4e0a46f762bbRestoring packages for /app/Docker.NetCore.MySql.csproj...Installing Microsoft.CodeAnalysis.Razor 2.0.0......Restore completed in 216.83 ms for /app/Docker.NetCore.MySql.csproj.---> 4df70c77916eRemoving intermediate container 4e0a46f762bbStep 5 : EXPOSE 5000---> Running in 11b421b3bd3e---> 3506253060feRemoving intermediate container 11b421b3bd3eStep 6 : ENV ASPNETCORE_URLS http://*:5000--
ASP.NET Core知多少6:VS Code联调Angular + .NetCore
ASP.NET Core知多少系列:总体介绍及目录1. 引言最近在看《程序员的成长课》,讲到程序员如何构建技能树,印象深刻。作为一名后台开发的程序员,深感技能单一,就别说技能树了。作为一名合格的后台程序员,至少要掌握一门静态语言,一门动态语言和一门前端语言。静态语言C#算不上精通,动态语言Python也刚刚入门。但前端却是空白,虽说有了解过jquery、bootstrap,但因为项目无所涉及,早已忘得一干二净。近几年,前端框架大行其道,Web开发已经是一个不容忽视的大趋势,在这个趋势下对前端框架一无所知,显然是要淘汰的。所以决定拾起前端,选择学习Angular来弥补自己的前端空白。计划使用.Net Core + Angular开发一个任务清单。2. 环境准备.Net Core已经支持Angular模板,我们只需要使用dotnet new angular -n YourAppName即可创建angualr项目模板。依次安装:Node.js(默认安装,即可安装NPM)执行npm install -g @angular/cli,安装angular cli。开发工具:Visual Studio Code安装最新.Net Core SDK,目前版本V2.1.101。3. 创建并启动项目执行dotnet new angular -n Learning.NetCore.Angular,创建项目后,使用VS Code打开文件夹。项目结构如下图所示。其中ClientApp就是Angular所写的前端部分,实现了前后端分离。打开后我们需要安装以下几个VS Code的扩展,以便我们顺利开发调试。稍后,右下角会弹窗提示我们是否需要调试项目,如下图所示。点击Yes,就会在项目中为我们创建一个.vscode的文件夹。其中包含两个文件,一个是launch.json,一个是tasks.json。其中launch.json用于配置调试相关参数。tasks.json用于配置默认的构建任务。如果没有弹出上图弹窗,我们也可以按下图步骤添加。第一次运行时,我们先执行dotnet build来验证项目能否正确构建, 它会恢复npm依赖,可能会耗时几分钟,npm依赖安装完毕后,以后再次运行就很快了。等构建成功,执行dotnet run运行项目。浏览器访问http://localhost:5000即可看到下图效果。然后键盘按Ctrl+C停止运行。4. 项目调试因为第三步我们已经创建了默认调试配置。直接F5运行,就可以调试.Net Core代码。但是我们该如何联调Angular代码呢?这就是本节的重点了。我们需要修改下我们的launch.json了。打开launch.json点击添加配置,然后选择Chrome:Launch,就会添加在配置文件中添加一个节点,如下所示:{"type": "chrome","request": "launch","name": "Launch Chrome","url": "http://localhost:8080","webRoot": "${workspaceFolder}"},我们需要做相应修改。因为.Net Core项目默认绑定端口为5000,所以我们要将上面url的端口号改为5000。并映射webRoot到wwwroot目录下。同时我们要启用sourceMaps。修改后如下所示:{"type": "chrome","request": "launch","name": "Launch Chrome","url": "http://localhost:5000","webRoot": "${workspaceFolder}/wwwroot","sourceMaps": true},至此我们成功添加一个任务用来启动Chrome,来调试我们的angular。需要简单三步走:终端执行dotnet run,运行项目切换到debug按钮,选择Launch Chrome配置,F5运行。断点ts文件。步骤如下图所示:但是这个时候我们仍然无法做到联调。我们需要要先启动项目,再选具体的某个调试配置进行调试。即同时只能调试Angualr和.NetCore中的一个。那如何二者联调???5. 联调Angualr&&.NetCore同样我们还是要修改launch.json,添加一个compounds配置节点。这个节点允许我们同时启动多个调试任务。配置如下:"compounds": [{"name": ".Net Core + Chrome","configurations": [".NET Core Launch (web)","Launch Chrome"]}],"configurations": [//省略]最终的配置如下:"compounds": [{"name": ".NetCore+Chrome","configurations": [ ".NET Core Launch (web)", "Launch Chrome" ]}],"configurations": [{// chrome debugger},{// .Net Core Launch (web)},]这个配置很简单,就是把我们刚才配置的调试任务组装在一块即可。回到调试界面,选择.NetCore+Chrome,F5运行,就可以同时在angular和.net core代码中断点并调试。如下图所示:细心的你可能会发现,通过这种方式虽然可以完成联调,但还是有点小瑕疵。两个调试任务会分别启动一个网页窗口。那有没有办法解决呢?有的,我们再添加一个.Net Core Launch (console)的调试任务,这个调试任务就不会启动网页窗口。然后再将.Net Core Launch (console)和Launch Chrome组装在一起即可。具体配置看下图:6. 最后本文仅是VS Code开发调试技巧的讲解,希望对你有所帮助。
ASP.NET Core知多少7:对重复编译说NO -- dotnet watch
ASP.NET Core知多少系列:总体介绍及目录1. 引言我们一般的开发过程,就是编码-->编译-->运行-->调试-->定位问题--->修改代码-->编译-->...,循环往复,不辞辛劳,但其实内心是非常抗拒的。今天就介绍下.NET Core平台下的工具--dotnet watch。用于实时监视项目文件变动,若有文件变动,自动重新编译并运行项目,大大节省了我们重复编译运行调试的时间。2. 使用说明安装Microsoft.DotNet.Watcher.ToolsNuGet包控制台执行dotnet watch run即可。然而如果你使用VS Code操作,你会遇到以下错误:error NU1605: Detected package downgrade: Microsoft.NETCore.App from 2.0.6 to 2.0.0. Reference the package directly from the project to select a different version.未找到与命令“dotnet-watch”匹配的可执行文件针对第一个问题,是因为我们默认安装的NuGet包是最新版本的,而目前最新版本为2.0.1,它依赖于:.NETCoreApp 2.0Microsoft.NETCore.App (>= 2.0.6)所以我们需要检查Microsoft.NETCore.App的版本,我的是2.0.0不符合(>=2.0.6)的条件,这里我选择安装 2.0.0版本的Watch即可,命令行执行:dotnet add package Microsoft.DotNet.Watcher.Tools --version 2.0.0。当然也可以升级安装2.0.6版本以上的Microsoft.NETCore.App,来解决这个问题。针对第二个问题,则需要我们手动修改项目csproj文件。添加一个DotNetCliToolReference节点即可。<DotNetCliToolReference Include="Microsoft.DotNet.Watcher.Tools" Version="2.0.0" />
给ASP.NET Core Web发布包做减法
1.引言紧接上篇:ASP.NET Core Web App应用第三方Bootstrap模板。这一节我们来讲讲如何优化ASP.NET Core Web发布包繁重的问题。在ASP.NET Core Web App中我们可以通过Bower或NPM来安装一些JS、CSS插件,来方便我们组织前端组件。但是这也给我带来了一个问题,那就是发布时需要把安装的Bower包或NPM包都要打包上传到服务器。如果现在发布ASP.NET Core Web App,wwwroot下已包含到项目中的文件都会被发布。虽然我们可以使用捆绑和微小的技术对js、css进行压缩来减少网页大小来提升加载速度。但是,我们发布包的大小却不能减少。如果我们项目中引用了较少的前端包文件,也无可厚非。但当我们引用了较多的包文件时。那我们的发布包将会占用很大一部分空间。尤其是当我们进行CI/CD时,将会耗费大量的时间来进行包还原和包文件上传。2. 思路我们就以集成AdminLte的ASP.NET Core Mvc项目为例,看看发布的包大小究竟有多大。从上图我们看到发布后wwwroot/plugins文件夹就占了很大一部分空间。而wwwroot/plugins中就是安装的Bower包。那这些Bower包中的文件我们都有用到吗?显然没有。我们就顶多引用了个js和css文件而已。到这里,减负的思路我们就清晰了。剔除ASP.NET Core Web中未引用的Bower包文件,把没有引用到的文件删除不就得了?!但是你随便打开一个Bower包文件夹,你就不想这么做了,一个一个删要删到什么时候。而且如果直接去删除Bower包中无用的文件,可能会影响bower包的管理,比如bower包的升级降级。不卖关子了,思路如下:新建一个文件夹,将引用的文件复制到另外的目录。(保持原bower包中的目录层级)修改项目中的引用到新的文件夹拷贝路径下。将原来的wwwrootplugins 排除到项目外(Exclude From Project)你可能会说,这么复杂啊,还不如我一个一个删除啊。别怕,我们让这一切自动化。而这个自动化工具就是Gulp.js。3. 行动以我们之前的Demo为例。全局安装 gulp:$ npm install --global gulp作为项目的开发依赖(devDependencies)安装:$ npm install --save-dev gulp$ npm install --save-dev path$ npm install --save-dev del安装成功后会在项目根目录创建package-lock.json文件和node_components文件夹。在项目根目录下创建一个名为 gulpfile.js 的文件。将以下代码粘贴复制进去。const gulp = require('gulp');//1. 引用gulpvar path = require('path');//2. 引用pathvar del = require('del');//3.引用del//定义路径const paths = {src: 'wwwroot/plugins/',dest: 'wwwroot/lib/'};//定义需要完整复制的Bower文件夹const copyFolders = ["bootstrap","font-awesome"];//定义项目中需要引用的bower包中的js、css文件const copyFiles = ["Ionicons/css/ionicons.css","jquery/dist/jquery.min.js","bootstrap/dist/js/bootstrap.min.js"];//在复制之前先清空生成目录gulp.task('clean:all', function (cb) {del([paths.dest], cb);});//复制文件gulp.task('copy:file', () => {//循环遍历文件列表var tasks = copyFiles.map(function (file) {//拼接文件完整路径var scrFullPath = path.join(`${paths.src}`, file);//拼接完整目标路径var index = file.lastIndexOf('/');var destPath = file.substring(0, index);var destFullPath=path.join(`${paths.dest}`, destPath);return gulp.src(scrFullPath).pipe(gulp.dest(destFullPath));});});//复制文件夹gulp.task('copy:folder', () => {var tasks = copyFolders.map(function (folder) {//拼接完整目标路径var destFullPath = path.join(`${paths.dest}`, folder);return gulp.src(path.join(`${paths.src}`, folder + '/**/*')).pipe(gulp.dest(destFullPath));});});//将三个任务组装在一起gulp.task('default', ['clean:all', 'copy:file', 'copy:folder']);代码注释的很详细,就不过多赘述了。有一点需要解释下,为什么需要完整拷贝bootstrap和font-awesome呢?因为引用的font-awesome.min.css会引用包文件的一些字体文件等,为了省事,就把包全部拷贝了一遍。而一般绝大多数包都是简单拷贝css和js文件就ok了的。而至于什么时候拷贝文件,什么时候文件夹。很简单,默认先拷贝文件,运行项目,然后浏览器F12,如果发现有无法加载的error,那就是了。运行gulp右键gulpfile.js-->Task Runner Exploerer-->双击Gulpfile.js-Tasks-default,即可运行。操作动图如下:运行后,需要复制的Bower包文件和文件夹就会复制到wwwrootlib文件夹下。如图:将bower包安装文件夹排除到项目外。更新项目中现有文件的引用到lib目录下。That's all, thank you.4. 效果重新发布,我们可以发现发布的包大小已有40M减小到8M。
ASP.NET Core Web App应用第三方Bootstrap模板
引言作为后端开发来说,前端表示玩不转,我们一般会选择套用一些开源的Bootstrap 模板主题来进行前端设计。那如何套用呢?今天就简单创建一个ASP.NET Core Web MVC 模板项目为例,来应用第三方Bootstrap Template——Admin LTE。1. 创建ASP.NET Core MVC Demo命令行执行dotnet new mvc -n ApplyBootstrapTemplate,即可创建预置的MVC模板项目。项目结构如下图:从项目结构来看,我们可以看到wwwroot目录下包含了css、images、js、lib目录,其中lib目录默认引用了bootstrap、jquery相关包。因为是简单的模板项目,所以UI就很将就。2. 下载AdminLte目前AdminLte在计划发布AdminLTE 3.0版本,不过现在还处于Alpha版本。我们下载AdminLTE-V2.4.3来使用。下载后解压得到的项目结构如下:3. 替换模板基于AdminLTE进行开发,仅需要复制dist目录,及其依赖的bower包就可以了。第一步:我们清空wwwroot下的全部目录(我这边暂时保留了images文件夹,后面会用到)。第二步:然后复制dist目录到wwwroot下。其依赖的bower包是安装在bower_components目录下的。我们无需直接复制整个bower_components文件夹,我们复制bower.json包定义文件即可。第三步:复制AdminLTE下的bower.json到ASP.NET Core Mvc根目录下。第四步:使用VS2017打开项目后,我们可以看到VS2017已经可以识别到未安装的Bower包。右键就可以还原bower包。不过先慢着,我们现在还原就会直接还原bower包到根目录下了,并没有还原bower包到wwwroot文件夹下。第五步:新增.bowerrc文件,配置包安装路径即可。这里我们指定为了wwwrootplugins。(这里没有指定为wwwrootbower_components,与原始AdminLTE的目录结构保持一致,是因为如果指定为wwwrootbower_components,还原包后bower_components默认不会包含在项目中。)第六步:Restore Package,还原成功后,我们会发现plugins文件夹已包含显示在wwwroot目录下了。4. 修改_Layout.cshtml接下来我们将AdminLTE的预置起始页面starter.html移植进我们的布局页面_Layout.cshtml。我们先来观察一下我们默认的布局页。主要有以上几个地方需要注意。根据环境配置css和js的加载@RenderBody()@RenderSection("Scripts", required: false)我们直接暴力复制starter.html的内容复制粘贴到_Layout.cshtml,然后再将以上三个点进行修改即可。然后修改引用的css、js路径即可。修改后的截图如下:最终效果CTRL+F5运行效果图如下,至此我们成功完成AdminLTE主题的应用。DEMO已上传到Github。
ASP.NET Core 中断请求了解一下翻译
ASP.NET Core知多少系列:总体介绍及目录本文所讲方式仅适用于托管在Kestrel Server中的应用。如果托管在IIS和IIS Express上时,ASP.NET Core Module(ANCM)并不会告诉ASP.NET Core在客户端断开连接时中止请求。但可喜的是,ANCM预计在.NET Core 2.2中会完善这一机制。1. 引言假设有一个耗时的Action,在浏览器发出请求返回响应之前,如果刷新了页面,对于浏览器(客户端)来说前一个请求就会被终止。而对于服务端来说,又是怎样呢?前一个请求也会自动终止,还是会继续运行呢?下面我们通过实例寻求答案。2. 实例演示创建一个SlowRequestController,再定义一个Get请求,并通过Task.Delay(10_000)模拟耗时行为。代码如下:public class SlowRequestController : Controller{private readonly ILogger _logger;public SlowRequestController(ILogger<SlowRequestController> logger){_logger = logger;}[HttpGet("/slowtest")]public async Task<string> Get(){_logger.LogInformation("Starting to do slow work");// slow async action, e.g. call external apiawait Task.Delay(10_000);var message = "Finished slow delay of 10 seconds.";_logger.LogInformation(message);return message;}}如果我们发起请求,那么该页面将耗时10s才能完成显示。如果我们检查运行日志,我们发现其输出符合预期:如果在第一次请求返回之前,刷新页面,结果将是怎样呢??从日志中我们可以看出:刷新后,第一个请求虽然在客户端被取消了,但是服务端仍旧会持续运行。从而可以说明MVC的默认行为: 即使用户刷新了浏览器会取消原始请求,但MVC对其一无所知,已经被取消的请求还是会在服务端继续运行,而最终的运行结果将会被丢弃。这样就会造成严重的性能浪费。如果服务端能感知用户中断了请求,并终止运行耗时的任务就好了。幸好,ASP.NET Core开发团队体贴的考虑了这一点,允许我们通过以下两种方式来获取客户端的请求是否被终止。通过HttpContex的RequestAborted属性:通过方法注入CancellationToken参数:if (HttpContext.RequestAborted.IsCancellationRequested){// can stop working now}[HttpGet]public async Task<ActionResult> GetHardWork(CancellationToken cancellationToken){// ...if (cancellationToken.IsCancellationRequested){// stop!}// ...}而这两种方式其实是一样的,因为HttpContext.RequestAborted和cancellationToken对应的是同一个对象:if(cancellationToken == HttpContext.RequestAborted){// this is true!}下面我们就来以cancellationToken为例,看看如何感知客户端请求终止并终止服务端服务。3. 在Action中使用CancellationTokenCancellationToken是由CancellationTokenSource创建的轻量级对象。当某个CancellationTokenSource被取消时,它会通知所有的消费者CancellationToken。取消时,CancellationToken的IsCancellationRequested属性将设置为True,表示CancellationTokenSource已取消。再回到前面的实例,我们有一个长期运行的操作方法(例如,通过调用许多其他API生成只读报告)。由于它是一种昂贵的方法,我们希望在用户取消请求时尽快停止执行操作。下面的代码显示了通过在action方法中注入一个CancellationToken,并将其传递给Task.Delay,来达到同步终止服务端请求的目的:public class SlowRequestController : Controller{private readonly ILogger _logger;public SlowRequestController(ILogger<SlowRequestController> logger){_logger = logger;}[HttpGet("/slowtest")]public async Task<string> Get(CancellationToken cancellationToken){_logger.LogInformation("Starting to do slow work");// slow async action, e.g. call external apiawait Task.Delay(10_000, cancellationToken);var message = "Finished slow delay of 10 seconds.";_logger.LogInformation(message);return message;}}MVC将使用CancellationTokenModelBinder自动将Action中的任何CancellationToken参数绑定到HttpContext.RequestAborted。当我们在Startup.ConfigureServices()中调用services.AddMvc() 或 services.AddMvcCore()时,CancellationTokenModelBinder模型绑定器就会被自动注册。通过这个小改动,我们再尝试在第一个请求返回之前刷新页面,从日志中我们发现,第一个请求将不会继续完成。而是当Task.Delay检测到CancellationToken.IsCancellationRequested属性为true时立即停止执行时并抛出TaskCancelledException。简而言之,用户刷新浏览器,在服务端通过抛出TaskCancelledException异常终止了第一个请求,而该异常通过请求管道再传播回来。在这个场景中,Task.Delay()会监视CancellationToken,因此无需我们手动检查CancellationToken是否被取消。4. 手动检查CancellationToken状态如果你正在调用支持CancellationToken的内置方法,比如Task.Delay()或HttpClient.SendAsync(),那么你可以直接传入CancellationToken,并让内部方法负责实际取消。在其他情况下,您可能正在进行一些同步工作,您希望能够取消这些工作。例如,假设正在构建一份报告来计算公司员工的所有佣金。你循环每个员工,然后遍历他们的每一笔销售。能够在中途取消此报告生成的简单解决方案是检查for循环内的CancellationToken,如果用户取消请求则跳出循环。以下示例通过循环10次并执行某些同步(不可取消)工作来表示此类情况,该工作由对Thread.Sleep()来模拟。在每个循环开始时,我们检查CancellationToken,如果取消则抛出异常。这使得我们可以终止一个长时间运行的同步任务。public class SlowRequestController : Controller{private readonly ILogger _logger;public SlowRequestController(ILogger<SlowRequestController> logger){_logger = logger;}[HttpGet("/slowtest")]public async Task<string> Get(CancellationToken cancellationToken){_logger.LogInformation("Starting to do slow work");for(var i=0; i<10; i++){cancellationToken.ThrowIfCancellationRequested();// slow non-cancellable workThread.Sleep(1000);}var message = "Finished slow delay of 10 seconds.";_logger.LogInformation(message);return message;}}现在,如果你取消请求,则对ThrowIfCancelletionRequested()的调用将抛出一个OperationCanceledException,它将再次传播回过滤器管道和中间件管道。5. 使用ExceptionFilter捕捉取消异常ExceptionFilters是一个MVC概念,可用于处理在您的操作方法或操作过滤器中发生的异常。可以参考官方文档。可以将过滤器应用到控制器级别和操作级别,也可以应用于全局级别。为了简单起见,我们创建一个过滤器并添加到全局过滤器。public class OperationCancelledExceptionFilter : ExceptionFilterAttribute{private readonly ILogger _logger;public OperationCancelledExceptionFilter(ILoggerFactory loggerFactory){_logger = loggerFactory.CreateLogger<OperationCancelledExceptionFilter>();}public override void OnException(ExceptionContext context){if(context.Exception is OperationCanceledException){_logger.LogInformation("Request was cancelled");context.ExceptionHandled = true;context.Result = new StatusCodeResult(499);}}}我们通过重载OnException方法并特殊处理OperationCanceledException异常即可成功捕获取消异常。Task.Delay()抛出的异常是TaskCancelledException类型,其为OperationCanceledException的基类,所以,以上过滤器也可正确捕捉。然后注册过滤器:public class Startup{public void ConfigureServices(IServiceCollection services){services.AddMvc(options =>{options.Filters.Add<OperationCancelledExceptionFilter>();});}}现在再测试,我们发现运行日志将不会包含异常信息,取而代之的是我