1.创建
HttpCookies Cookie=new HttpCookies("CookieName");2.添加内容Cookie.Values.Add("UserName","ABC");Cookie["UserName"]="ABC";
3.修改内容Cookie.Values["UserName"]="CBA";4.读取内容var UserName=Cookie.Values["UserName"];5.设置失效时间(删除cookie就是讲失效时间设置的比现在时间早,在下一次打开网页的时候就会自动删除Cookie)(删除Cookie的时间是下一次打开新的网页,也就是如果没有打开新的网页,Cookie不会被删除,依旧)(可以读取其中的数据)Cookie.Expires=DateTime.Now;6.将修改的信息传入客户端(所有更改类操作都必须这样做,否则不会将更改保存到客户端Cookie)Response.AppendCookies(Cookie):将 Http cookie追加加到Cookies集合,同key的cookie可以重复添加,总是添加而不是更新,它和HttpResponse.Cookies.Add的功能几乎一样,最大的区别是添加前后的事件和抛出的异常不尽相同。HttpResponse.Cookies.Add:将指定cookie添加到Cookies集合,同key的cookie可以重复添加,总是添加而不是更新.HttpResponse.SetCookie:更新Cookies集合中第一个同key的cookie,如果找不到同key的cookie,则添加一个cookie到Cookies集合中。7.获取客户端CookiesRequest.Cookies["CookieName"];8.无法从客户端Cookies中获取Expires的值,所以若要判断该时间,需要一个新的Values[]来存储
第一次写博客,比较浅显,欢迎大牛们指点一二,不胜感激。
** 温馨提示:如需转载本站内容,请注明内容出处。**
本文连接:
最近在做爬虫,之前一直在使用 HttpWebRequest 和 WebClient ,很方便快捷,也很适合新手,但随着抓取任务的增多,多任务,多库等情况的出现,使用一个优秀的爬虫框架是十分必要的。于是开始接触dotnetspider。
借鉴一下框架的设计图,在引入dotnetspider的NuGet包后,我基本也是按照这个进行了分层
Data.Spider - 存放前台页面(Winform、控制台)和实体爬虫(EntitySpider)、Downloader等,相当于发起请求的起点。
Spider.Processor - 处理器,继承 IPageProcessor 实现对抓取内容的处理
Spider.Pipe - 管道,我将它理解为经过了 Processor 处理后的一个回调,将处理好的数据存储(文件、数据库等)
Spider.Entity - 数据实体类,继承 SpiderEntity
Spider.Command - 一些常用的公用命令,我这目前存放着转数据格式类,后台执行JS类,SqlHelper(因架构自带数据库管道,暂时没用)等
这样的分层也是参考了源码的示例
随着这几天的尝试,真的发现这个框架真的非常灵活,以凹凸租车的爬虫为例,上代码
实体类:
[EntityTable("CarWinsSpider", "AtzucheCar", EntityTable.Today)]
[EntitySelector(Expression = "$.data.content[*]", Type = SelectorType.JsonPath)]public class AtzucheModel : SpiderEntity
{
/// <summary>/// 车辆编号/// </summary>[PropertyDefine(Expression = "$.carNo", Type = SelectorType.JsonPath)]public int carNo { get; set; }/// <summary>/// 品牌/// </summary>//[ReplaceFormatter(NewValue = "", OldValue = "\r")]//[ReplaceFormatter(NewValue = "", OldValue = "\t")]//[ReplaceFormatter(NewValue = "", OldValue = " ")]//[ReplaceFormatter(NewValue = "", OldValue = "\n")]//[ReplaceFormatter(NewValue = "", OldValue = "\"")]//[ReplaceFormatter(NewValue = "", OldValue = " ")][PropertyDefine(Expression = "$.brand", Type = SelectorType.JsonPath)]public string brand { get; set; }/// <summary>/// 地址/// </summary>[PropertyDefine(Expression = "$.carAddr", Type = SelectorType.JsonPath)]public string carAddr { get; set; }/// <summary>/// 车系/// </summary>[PropertyDefine(Expression = "$.type", Type = SelectorType.JsonPath)]public string type { get; set; }/// <summary>/// 排量/// </summary>[PropertyDefine(Expression = "$.sweptVolum", Type = SelectorType.JsonPath)]public string sweptVolum { get; set; }/// <summary>/// 图片/// </summary>[PropertyDefine(Expression = "$.coverPic", Type = SelectorType.JsonPath)]public string coverPic { get; set; }/// <summary>/// 日租金/// </summary>[PropertyDefine(Expression = "$.dayPrice", Type = SelectorType.JsonPath)]public int dayPrice { get; set; }/// <summary>/// 公里数/// </summary>[PropertyDefine(Expression = "$.distance", Type = SelectorType.JsonPath)]public string distance { get; set; }/// <summary>/// 评分/// </summary>[PropertyDefine(Expression = "$.evalScore", Type = SelectorType.JsonPath)]public string evalScore { get; set; }[PropertyDefine(Expression = "$.gbType", Type = SelectorType.JsonPath)]public string gbType { get; set; }/// <summary>/// 车牌/// </summary>[PropertyDefine(Expression = "$.plateNum", Type = SelectorType.JsonPath)]public string plateNum { get; set; }[PropertyDefine(Expression = "$.replyTag", Type = SelectorType.JsonPath)]public string replyTag { get; set; }[PropertyDefine(Expression = "$.transCount", Type = SelectorType.JsonPath)]public string transCount { get; set; }/// <summary>/// 年款/// </summary>[PropertyDefine(Expression = "$.year", Type = SelectorType.JsonPath)]public int year { get; set; }[PropertyDefine(Expression = "$.isPrivilege", Type = SelectorType.JsonPath)]public int isPrivilege { get; set; }[PropertyDefine(Expression = "$.isRecommend", Type = SelectorType.JsonPath)]public int isRecommend { get; set; }[PropertyDefine(Expression = "$.isUpgrade", Type = SelectorType.JsonPath)]public int isUpgrade { get; set; }[PropertyDefine(Expression = "$.lat", Type = SelectorType.JsonPath)]public string lat { get; set; }[PropertyDefine(Expression = "$.lon", Type = SelectorType.JsonPath)]public string lon { get; set; }[PropertyDefine(Expression = "$.queryId", Type = SelectorType.JsonPath)]public string queryId { get; set; }[PropertyDefine(Expression = "$.supplyCarService", Type = SelectorType.JsonPath)]public int supplyCarService { get; set; }[PropertyDefine(Expression = "$.freeCarService", Type = SelectorType.JsonPath)]public int freeCarService { get; set; }[PropertyDefine(Expression = "$.isShenMaCar", Type = SelectorType.JsonPath)]public int isShenMaCar { get; set; }[PropertyDefine(Expression = "$.supportGetReturn", Type = SelectorType.JsonPath)]public int supportGetReturn { get; set; }[PropertyDefine(Expression = "$.confirmation", Type = SelectorType.JsonPath)]public int confirmation { get; set; }
}
起始:
/// <summary>
/// 应用程序的主入口点。 /// </summary> [STAThread]static void Main()
{
var site = new Site
{ CycleRetryTimes = 1, SleepTime = 200, Headers = new Dictionary<string, string>() { {"Accept","application/json, text/javascript, */*; q=0.01" }, {"Accept-Encoding","gzip, deflate" }, {"gzip, deflate","zh-CN,zh;q=0.9" }, {"X-Requested-With","XMLHttpRequest" }, { "Referer", "http://www.atzuche.com/hz/car/search"}, { "Connection","keep-alive" }, { "Content-Type","application/json;charset=UTF-8" }, { "Host","www.atzuche.com"}, { "User-Agent","Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36"} }};List<Request> resList = new List<Request>();Request res = new Request();
//res.PostBody = $"id=7&j=%7B%22createMan%22%3A%2218273159100%22%2C%22createTime%22%3A1518433690000%2C%22row%22%3A5%2C%22siteUserActivityListId%22%3A8553%2C%22siteUserPageRowModuleId%22%3A84959%2C%22topids%22%3A%22%22%2C%22wherePhase%22%3A%221%22%2C%22wherePreferential%22%3A%220%22%2C%22whereUsertype%22%3A%220%22%7D&page={i}&shopid=83106681";//据说是post请求需要res.Url = "http://www.atzuche.com/car/searchListMap/2?cityCode=330100&sceneCode=U002&filterCondition%5Blon%5D=120.219294&filterCondition%5Blat%5D=30.259258&filterCondition%5Bseq%5D=4&pageNum=1&pageSize=0";res.Method = System.Net.Http.HttpMethod.Get;resList.Add(res);
var spider = DotnetSpider.Core.Spider.Create(site, new QueueDuplicateRemovedScheduler(), new AtzucheProcessor())
.AddStartRequests(resList.ToArray())//页面抓取整理.AddPipeline(new AtzuchePipe());//数据回调//----------------------------------
spider.Monitor = new DotnetSpider.Core.Monitor.NLogMonitor();spider.Downloader = new DotnetSpider.Core.Downloader.HttpClientDownloader();spider.ClearSchedulerAfterComplete = false;//爬虫结束后不取消调度器//----------------------------------spider.ThreadNum = 1;
spider.Run();Console.WriteLine("Press any key to continue...");
Console.Read();}
这里也可将整个抓取方法当做一个Spider实例单独放置 -> EntitySpider
/// <summary>
/// 应用程序的主入口点。 /// </summary> [STAThread]static void Main()
{
AtzucheEntitySpider dDengEntitySpider = new AtzucheEntitySpider();
dDengEntitySpider.AddPageProcessor(new AtzucheProcessor());//控制器 dDengEntitySpider.AddPipeline(new AtzuchePipe());//回调 dDengEntitySpider.ThreadNum = 1; dDengEntitySpider.Run(); Console.WriteLine("Press any key to continue..."); Console.Read();}
新建爬虫实体类
public class AtzucheEntitySpider : EntitySpider
{ protected override void MyInit(params string[] arguments) { AddPipeline(new SqlServerEntityPipeline("Server=.;Database=AuzucheSpider;uid=sa;pwd=123;MultipleActiveResultSets=true"));//注意连接字符串中数据库不能带 . 亲测报错。。。 AddStartUrl("http://www.atzuche.com/car/searchListMap/2?cityCode=330100&sceneCode=U002&filterCondition%5Blon%5D=120.219294&filterCondition%5Blat%5D=30.259258&filterCondition%5Bseq%5D=4&pageNum=1&pageSize=0"); AddEntityType<AtzucheModel>();//如添加此实体类,框架将会根据此实体类上面的特性选择进行匹配,匹配成功后插入数据库,固可以省略Processor和Pipe,或者不适用此句,通过控制器和回调自定义存储方法 }public AtzucheEntitySpider() : base("AuzucheSpider", new Site
{ CycleRetryTimes = 1, SleepTime = 200, Headers = new Dictionary<string, string>() { {"Accept","application/json, text/javascript, */*; q=0.01" }, {"Accept-Encoding","gzip, deflate" }, {"gzip, deflate","zh-CN,zh;q=0.9" }, {"X-Requested-With","XMLHttpRequest" }, { "Referer", "http://www.atzuche.com/hz/car/search"}, { "Connection","keep-alive" }, { "Content-Type","application/json;charset=UTF-8" }, { "Host","www.atzuche.com"}, { "User-Agent","Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36"} }}){ }}
接下来是处理器:
解析抓取的数据封装到"AtzucheList"内,可Pipe内通过此名称获取处理好的数据。
public class AtzucheProcessor : IPageProcessor
{ public void Process(Page page, ISpider spider) { List<AtzucheModel> list = new List<AtzucheModel>(); var html = page.Selectable.JsonPath("$.data.content").GetValue(); list = JsonConvert.DeserializeObject<List<AtzucheModel>>(html); page.AddResultItem("AtzucheList", list); }}
最后是回调,可在此加入保存数据的代码,至此结束。
public class AtzuchePipe : BasePipeline
{ public override void Process(IEnumerable<ResultItems> resultItems, ISpider spider) { var result = new List<AtzucheModel>(); foreach (var resultItem in resultItems) { Console.WriteLine((resultItem.Results["AtzucheList"] as List<AtzucheModel>).Count); foreach (var item in (resultItem.Results["AtzucheList"] as List<AtzucheModel>)) { result.Add(new AtzucheModel() { carNo = item.carNo }); Console.WriteLine($"{item.carNo}:{item.type} "); } } }}结果图:
总体来说,此框架对新手还是很友好的,灵活写法可以让我们有较多的方式去实现爬虫,因为这个爬虫比较简单,就先写到这里,未来如果可能,会再尝试使用框架内的多线程、代理等功能,如有心得将继续分享,希望能对跟我一样的新手有所帮助,十分感谢。
前言
很长一段时间没有写博客了,今天补上一篇吧,偶尔发现不太愿意写博客了,太耗费时间,不过还是在坚持当中,毕竟或许写出来的东西能帮到一些童鞋吧,接下来我们直奔主题。无论是在EF 6.x还是EF Core中对于原始查询的APi都比较鸡肋,比如我们只想查询单个值,它们是不支持的,比如我们只想有些列,它们也是不支持的,太多太多不支持,唯一支持的是只能返回表中所有列即类中所有字段。所以大部分情况下我都是写原生SQL,原始查询都没怎么用到过,最近有对热爱EF的同行问到怎么利用SqlQuery实现动态查询,我没有答案,压根没想过用这个方法,私下看了看,还是给出一点点思考吧。若对您有帮助就好,没有用就当是我补上了一篇博客吧。
EF 6.x和EF Core实现动态查询
public static IEnumerableSqlQueryDynamic(this DbContext db, string Sql, params SqlParameter[] parameters) { using (var cmd = db.Database.Connection.CreateCommand()) { cmd.CommandText = Sql; if (cmd.Connection.State != ConnectionState.Open) { cmd.Connection.Open(); } foreach (var p in parameters) { var dbParameter = cmd.CreateParameter(); dbParameter.DbType = p.DbType; dbParameter.ParameterName = p.ParameterName; dbParameter.Value = p.Value; cmd.Parameters.Add(dbParameter); } using (var dataReader = cmd.ExecuteReader()) { while (dataReader.Read()) { var row = new ExpandoObject() as IDictionary ; for (var fieldCount = 0; fieldCount < dataReader.FieldCount; fieldCount++) { row.Add(dataReader.GetName(fieldCount), dataReader[fieldCount]); } yield return row; } } } }
那么最终如上查询后返回动态集合,我们该如何转换为集合对象呢?我想都没想如下直接先序列化然后反序列化,若您有更好的解决方案,请自行实现即可。
using (var ctx = new EfDbContext()) { ctx.Database.Log = Console.WriteLine; var dynamicOrders = ctx.SqlQueryDynamic("select * from dbo.Orders"); var ordersJson = JsonConvert.SerializeObject(dynamicOrders); var orders = JsonConvert.DeserializeObject
>(ordersJson); };
当然上述我只是简单查询了一个表,若您有多个表也是好使的,最后反序列化为不同的对象即可,未经测试,您可自行测试。
EF Core使用多个上下文实例池
有很多人无论是在EF 6.x还是在EF Core中一直以来都是使用一个上下文,但是不知我们是否有想过使用多个上下文呢?比如在电商项目中,对于产品相关操作我们可以使用产品上下文,对于加入购物车操作使用购物车上下文,对于订单操作使用订单上下文。这么做的好处是什么呢?我们可以将数据库表也就说将实体拆分成不同的业务。至今我还没看到有人这么做过,如果是我的话,至少我会这么做。
//Add DbContext var dbConnetionString = Configuration.GetConnectionString("DbConnection"); services.AddDbContextPool(options => { options.UseSqlServer(dbConnetionString); }).AddDbContextPool (options => { options.UseSqlServer(dbConnetionString); }).AddDbContextPool (options => { options.UseSqlServer(dbConnetionString); });
在EF Core 2.0中有了上下文实例池,类似于ADO.NET中的连接池一样,但是这玩意你从表面理解那你就大错特错了,有关上下文实例池(从去年开始我着手写了一本关于EF 6.x和EF Core的书籍最近会出版)实现本质,只能说它和ADO.NET中的连接池不是一样的哦。那么如上述使用多个上下文实例池是否就一定好使呢?不好意思,这样配置是错误的。但运行程序你会发现抛出类似如下异常:
Exception message:System.ArgumentException: Expression of type 'Microsoft.EntityFrameworkCore.DbContextOptions`1[MultiContext.Contexts.BContext]' cannot be used for constructor parameter of type 'Microsoft.EntityFrameworkCore.DbContextOptions`1[MultiContext.Contexts.AContext]' Parameter name: arguments[0]Stack trace:...........
在此特性出来时大家都在欢呼能够提高性能,对不起上下文实例池虽然可能在一定程度上提高性能,但是我只能讲只能有可能的性能改进,如果你知道或者看过EF Core实现上下文实例池的原理,就明白了其实现的本质从而恍然大悟我所说的可能的性能上的改进是什么意思。至于为何不能注册多个上下文实例池,我也是私下写项目遇见的,具体请参看github:。
总结
好了今天就到这里,没有过多的解释和叙述,上来就是直奔主题,最近思想放飞中,对写博客慢慢失去了很大的兴趣,偶尔感性中,待我满血复活调节好心情再来和大家继续分享技术,我一直在,一段时间没写博客可能是因为累了,又或者是私下在学习IdentityServer或者其他技术中,干咱这行的,除非转行那就老老实实积累经验和多学点技术吧,年轻不奋斗,那什么时候奋斗呢。今天说了啥,胡思乱想中,莫见怪。
前言
终于踏出第一步探索EF Core原理和本质,过程虽然比较漫长且枯燥乏味还得反复论证,其中滋味自知,EF Core的强大想必不用我再过多废话,有时候我们是否思考过背后到底做了些什么,到底怎么实现的呢?比如本节要讲的在命令行简单敲下dotnet ef migrations add initial初始化表完事,如此简洁。激起了我的好奇,下面我们来看看。本节内容可能比较多,请耐心。
EntityFramework Core命令基础拾遗
我们提前创建好.NET Core Web应用程序和实体模型以及上下文,园中例子太多且我们也只是探讨迁移原理,无关乎其他。
如此简单一个命令就初始化了表,是不是很神奇,我们接下来要做的就是化神奇为简单。我们接下来将上述迁移文件夹删除,再次运行如下命令,看看迁移详细过程。
dotnet ef migrations add init -c EFCoreDbContext -p ..\EfCore.Data\ --verbose
通过如上两张图我们可看出EF迁移将会进行两步:第一步则是编译上下文所在项目,编译启动项目。第二步则是通过编译成功后的上下文所在程序集合启动项目程序集最终实现迁移。总结起来就是简单两小步,背后所需要做的很多,请继续往下看。
EntityFramework Core迁移本质
当我们敲写dotnet ef migrations add initial命令后,紧接着会在启动项目obj文件夹会生成如下文件。
这个东西是做什么的呢,我也不知道,我们打开该文件看看。
一堆的如上东西,什么鬼玩意,刚看到这东西时我懵逼了,于是开始了探索之路。在.NET Core CLI 1.0.0有了称为“项目工具扩展”的功能,我们称之为“CLI工具”。 这些是项目特定的命令行工具,也就是说扩展了dotnet命令。比如我们安装Microsoft.DotNet.Watcher.Tools包则可以使用dotnet watch命令,就是这么个意思。在.NET Core尚未完善时,项目文件采用JSON格式,紧接着改为了以扩展名为.xproj结尾的项目文件,格式也就转换为了XML格式,最后项目文件定型为以.proj结尾,当然数据格式依然是XML,我猜测估计和MSBuild有关,因为微软对XML数据格式的操作已经有非常成熟的库,相比较而言JSON我们使用起来当然更加方便,可能微软需要多做额外的工作,纯属猜测。了解和知道MSBuild的童鞋看到上述数据格式想必格外亲切,再熟悉不过了,我们若仔细看到上述数据参数,就能够明白上述参数是存放的项目参数。在.NET Core中都是利用MSBuild和CLI工具来读取项目信息以用于其他目的。那么问题就来了,如何读取项目信息呢?
利用MSBuild和CLI工具读取项目信息
首先我们需要找到项目中以扩展名为.proj结尾的文件,其次我们需要注入MSBuild Target,最后则启动进程是调用Target,代码如下:
public static void Main(string[] args) { var projectFile = @"D:\Visual Studio 2015\Projects\EFCore2Example\EFCore2Example\EFCore2Example.csproj"; var targetFileName = Path.GetFileName(projectFile) + ".EntityFrameworkCore.targets"; var projectExePath = Path.Combine(@"D:\Visual Studio 2015\Projects\EFCore2Example\EFCore2Example", "obj"); Directory.CreateDirectory(projectExePath); var targetFile = Path.Combine(projectExePath, targetFileName); File.WriteAllText(targetFile, @""); var psi = new ProcessStartInfo { FileName = "dotnet", Arguments = $"msbuild \"{projectFile}\" /t:GetEFProjectMetadata /nologo" }; var process = Process.Start(psi); process.WaitForExit(); if (process.ExitCode != 0) { Console.Error.WriteLine("Invoking MSBuild target failed"); } Console.ReadKey(); } AssemblyName: $(AssemblyName) OutputPath: $(OutputPath) Platform: $(Platform)
默认情况下MSBuildProjectExtensionsPath路径在项目中obj文件夹下如上我们迁移的WebApplication1.csproj.EntityFrameworkCore.targets,我们对targets文件命名一般约定为(MSBuildProjectExtensionsPath)(MSBuildProjectExtensionsPath)(MSBuildProjectFile).<SomethingUnique>.targets,如上代码为我们仿照实际迁移时在obj文件夹下生成的targets文件。当启动dotnet进程运行时会在控制台打印如下参数:
上述只是作为简单的显示信息而使用,利用CLI工具在我们项目内部创建了一个MSBuild目标。这个目标可以完成MSBuild所能做的任何事情,EF Core则是将加载目标读取临时文件的形式来获取项目信息。
var projectFile = @"D:\Visual Studio 2015\Projects\EFCore2Example\EFCore2Example\EFCore2Example.csproj"; var targetFileName = Path.GetFileName(projectFile) + ".EntityFrameworkCore.targets"; var projectExePath = Path.Combine(@"D:\Visual Studio 2015\Projects\EFCore2Example\EFCore2Example", "obj"); Directory.CreateDirectory(projectExePath); var targetFile = Path.Combine(projectExePath, targetFileName); File.WriteAllText(targetFile, @""); var tmpFile = Path.GetTempFileName(); var psi = new ProcessStartInfo { FileName = "dotnet", Arguments = $"msbuild \"{projectFile}\" /t:GetEFProjectMetadata /nologo \"/p:EFProjectMetadataFile={tmpFile}\"" }; var process = Process.Start(psi); process.WaitForExit(); if (process.ExitCode != 0) { Console.Error.WriteLine("Invoking MSBuild target failed"); } var lines = File.ReadAllLines(tmpFile); File.Delete(tmpFile); var properties = new Dictionary (StringComparer.OrdinalIgnoreCase); foreach (var line in lines) { var idx = line.IndexOf(':'); if (idx <= 0) continue; var name = line.Substring(0, idx)?.Trim(); var value = line.Substring(idx + 1)?.Trim(); properties.Add(name, value); } Console.WriteLine("........................................"); Console.WriteLine($"EFCore2Example project has {properties.Count()} properties"); Console.WriteLine($"AssemblyName = { properties["AssemblyName"] }"); Console.WriteLine($"OutputPath = { properties["OutputPath"] }"); Console.WriteLine($"Platform = { properties["Platform"] }"); Console.WriteLine($"PlatformTarget = { properties["PlatformTarget"] }"); Console.WriteLine($"ProjectAssetsFile = { properties["ProjectAssetsFile"] }"); Console.WriteLine($"ProjectDir = { properties["ProjectDir"] }"); Console.WriteLine($"RootNamespace = { properties["RootNamespace"] }"); Console.WriteLine($"RuntimeFrameworkVersion = { properties["RuntimeFrameworkVersion"] }"); Console.WriteLine($"TargetFileName = { properties["TargetFileName"] }"); Console.WriteLine($"TargetFrameworkMoniker = { properties["TargetFrameworkMoniker"] }"); Console.WriteLine("........................................");
上述是控制台中示例,若我们在.NET Core Web应用程序中,此时我们完全可以获取到项目文件而无需如控制台写死项目文件路径,如下:
var projectFiles = Directory.EnumerateFiles(Directory.GetCurrentDirectory(), "*.*proj", SearchOption.TopDirectoryOnly) .Where(f => !string.Equals(Path.GetExtension(f), ".xproj", StringComparison.OrdinalIgnoreCase)) .Take(2).ToList(); var projectFile = projectFiles[0]; var targetFileName = Path.GetFileName(projectFile) + ".EntityFrameworkCore.targets"; .......
此时获取到启动项目信息,如下:
到了这里我们探索完了EF Core如何进行迁移的第一步,同时我们也明白为何要将执行命令路径切换到启动项目项目文件所在目录,因为需要获取到项目信息,然后进行Build也就是生成,如果执行生成错误则返回,否则返回项目详细信息。到这里我们了解了利用MSBuild和CLI工具来获取上下文所在项目详细信息和启动项目详细信息。我们继续往下探讨。
执行.NET Core必需文件和调用ef.exe或者ef.x86.exe应用程序或者ef.dll程序集执行迁移
通过上述MSBuild和CLI工具我们获取到上下文和启动项目详细信息,接下来则是进行迁移,如开头第四张图片所示,所执行命令大致如下:
dotnet exec --depsfile [.deps.json] --addtionalprobingpath [nugetpackage] --runtimeconfig [.runtimeconfig.json] ef.dll migrations add init -c [DbContext] --assembly [DbContextAssmbly] --startup-assembly [StartupProjectAssembly]
一波刚平息 一波又起,首先我们得明白上述命令,比如通过读取扩展名为.deps.json文件来执行--depsfile命令,以及读取扩展名为.runtimeconfig.json文件执行--runtimeconfig命令,那么这两个文件是做什么的呢,我们又得花费一点功夫来讲解。接下来我们利用dotnet命令来创建控制台程序来初识上述两个命令的作用。首先我们运行如下命令创建控制台程序,在此需要特别说明的是在.NET Core 2.0后当通过dotnet build后直接包含了执行dotnet restore命令:
dotnet new Console
此时同时也会在obj文件夹下生成project.assets.json文件,这个文件是做什么的呢?别着急,我们先讲完.deps.json和.runtimeconfig.json继续话题会讲到这个文件的作用,我们继续。
此时我们继续运行生成命令,如下则会生成bin文件夹,同时在如下.netcoreapp2.1文件夹会生成我们需要讲到的两个json文件。
dotnet build
{ "runtimeOptions": { "tfm": "netcoreapp2.1", "framework": { "name": "Microsoft.NETCore.App", "version": "2.1.0-preview1-26216-03" } }}
运行.NET Core应用程序必须要runtimeconfig.json文件,意为“运行时”,我们也可以翻译为共享框架,且运行时和共享框架概念可任意转换。此json文件为运行时配置选项,如果没有runtimeconfig.json文件,将抛出异常,我们删除该文件看看。
通过运行时json文件当运行时指示dotnet运行Microsoft.NETCore.App 2.0.0共享框架 此框架是最常用的框架,但也存在其他框架,例如Microsoft.AspNetCore.App。 与.NET Framework不同,可能会在计算机上安装多个.NET Core共享框架。dotnet读取json文件,并在C:\Program Files\dotnet\shared中查找运行该应用程序所需的文件,如下存在多个运行时框架。当然如果我们安装了更高版本的.net core如2.1.0-preview1-final,此时dotnet将自动选择最高的版本。
好了,我们算是明白.runtimeconfig.json文件主要是用来指示dotnet在运行时使用哪个框架。我们再来看看.deps.json文件,如下:
{ "runtimeTarget": { "name": ".NETCoreApp,Version=v2.1", "signature": "da39a3ee5e6b4b0d3255bfef95601890afd80709" }, "compilationOptions": {}, "targets": { ".NETCoreApp,Version=v2.1": { "认识.NET Core/1.0.0": { "runtime": { "认识.NET Core.dll": {} } } } }, "libraries": { "认识.NET Core/1.0.0": { "type": "project", "serviceable": false, "sha512": "" } }}
deps.json文件是一个依赖关系清单。它可以用来配置来自包的组件的动态链接。NET Core可以配置为从多个位置动态加载程序集,这些位置包括:
应用程序基目录(与入口点应用程序位于同一文件夹中,不需要配置)
- 包缓存文件夹(NuGet恢复缓存或NuGet后备文件夹)
- 优化的包缓存或运行时包存储。
- 共享框架(通过runtimeconfig.json配置)。
好了,对于.deps.json和runtimeconfig.json文件暂时先讲到这里,后续有可能再详细讲解,我们弄明白了这两个文件的大致作用即可。回到我们的话题,那么这两个文件是如何找到的呢?那就得结合我们第一步获取到的项目信息了,在第一部分获取项目信息最后给出的图片里面根据ProjectDir和OutputPath就可以获取到.deps.json和.runtimeconfig.json文件。最后则需要获取ef.dll程序集从而执行相关迁移命令,那么ef.dll程序集是怎么获取到的呢?这个时候就需要获取项目中的信息ProjectAssetsFile即读取project.assets.json文件,获取packageFolders节点下数据,如下:
"packageFolders": { "C:\\Users\\JeffckyWang\\.nuget\\packages\\": {}, "C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackagesFallback\\": {}, "C:\\Program Files\\dotnet\\sdk\\NuGetFallbackFolder": {} },
我们从开头第四张图片可看出对于--addtionalprobingpath有三个路径也就是如上三个路径,我们看看如上三个路径是否存在ef.dll程序集。
如上只有nuget和sdk中有ef.dll程序集,我们依然看看开头第四张图片最终执行的却是sdk中的ef.dll程序集,难道是如果nuget和skd目录在project.assets.json中都存在,那么优先从sdk中查找么,也就是sdk中程序集优先级比nuget程序集高吗,如果sdk中存在对应程序集则直接执行吗。当移除该文件中nuget路径,重新生成会覆盖。所以猜测可能优先查找sdk中是否存在ef.dll程序集。这里还需额外说明一点的是我们在第一节获取到了项目详细信息,其中有一项是TargetFrameworkMoniker,若我们创建的项目是.NET Framework,此时根据TargetFrameworkMoniker来判断,若为.NETCoreApp则执行上述ef.dll程序集否则执行如下路径应用程序来迁移。
手动执行命令迁移
上述我们完整讲述了在命令行中执行dotnet ef命令背后的本质是什么,那么我们接下来利用代码手动来迁移。如下第一个类为解析进程所需的参数类【从dotnet ef源码拷贝而来】
public static class Common { public static string ToArguments(IReadOnlyListargs) { var builder = new StringBuilder(); for (var i = 0; i < args.Count; i++) { if (i != 0) { builder.Append(" "); } if (args[i].IndexOf(' ') == -1) { builder.Append(args[i]); continue; } builder.Append("\""); var pendingBackslashs = 0; for (var j = 0; j < args[i].Length; j++) { switch (args[i][j]) { case '\"': if (pendingBackslashs != 0) { builder.Append('\\', pendingBackslashs * 2); pendingBackslashs = 0; } builder.Append("\\\""); break; case '\\': pendingBackslashs++; break; default: if (pendingBackslashs != 0) { if (pendingBackslashs == 1) { builder.Append("\\"); } else { builder.Append('\\', pendingBackslashs * 2); } pendingBackslashs = 0; } builder.Append(args[i][j]); break; } } if (pendingBackslashs != 0) { builder.Append('\\', pendingBackslashs * 2); } builder.Append("\""); } return builder.ToString(); } }
项目所需的详细信息,我们封装成一个类且其中包含执行build命令的方法,如下:
public class Project { public string AssemblyName { get; set; } public string Language { get; set; } public string OutputPath { get; set; } public string PlatformTarget { get; set; } public string ProjectAssetsFile { get; set; } public string ProjectDir { get; set; } public string RootNamespace { get; set; } public string RuntimeFrameworkVersion { get; set; } public string TargetFileName { get; set; } public string TargetFrameworkMoniker { get; set; } public void Build() { var args = new List{ "build" }; args.Add("/p:GenerateRuntimeConfigurationFiles=True"); args.Add("/verbosity:quiet"); args.Add("/nologo"); var arg = Common.ToArguments(args); var psi = new ProcessStartInfo { FileName = "dotnet", Arguments = arg }; var process = Process.Start(psi); process.WaitForExit(); } }
接下来则是获取项目详细信息、生成、迁移,如下三个方法以及对应方法实现。
//获取项目详细信息 var projectMedata = GetProjectMedata(); //生成 projectMedata.Build(); //执行EF迁移命令 ExecuteEFCommand(projectMedata);
public Project GetProjectMedata() { var projectFiles = Directory.EnumerateFiles(Directory.GetCurrentDirectory(), "*.*proj", SearchOption.TopDirectoryOnly) .Where(f => !string.Equals(Path.GetExtension(f), ".xproj", StringComparison.OrdinalIgnoreCase)) .Take(2).ToList(); var projectFile = projectFiles[0]; var targetFileName = Path.GetFileName(projectFile) + ".EntityFrameworkCore.targets"; var projectExePath = Path.Combine(Path.GetDirectoryName(projectFile), "obj"); Directory.CreateDirectory(projectExePath); var targetFile = Path.Combine(projectExePath, targetFileName); System.IO.File.WriteAllText(targetFile, @""); var tmpFile = Path.GetTempFileName(); var psi = new ProcessStartInfo { FileName = "dotnet", Arguments = $"msbuild \"{projectFile}\" /t:GetEFProjectMetadata /nologo \"/p:EFProjectMetadataFile={tmpFile}\"" }; var process = Process.Start(psi); process.WaitForExit(); if (process.ExitCode != 0) { Console.Error.WriteLine("Invoking MSBuild target failed"); } var lines = System.IO.File.ReadAllLines(tmpFile); System.IO.File.Delete(tmpFile); var properties = new Dictionary (StringComparer.OrdinalIgnoreCase); foreach (var line in lines) { var idx = line.IndexOf(':'); if (idx <= 0) continue; var name = line.Substring(0, idx)?.Trim(); var value = line.Substring(idx + 1)?.Trim(); properties.Add(name, value); } var project = new Project() { AssemblyName = properties["AssemblyName"], OutputPath = properties["OutputPath"], ProjectDir = properties["ProjectDir"], ProjectAssetsFile = properties["ProjectAssetsFile"], TargetFileName = properties["TargetFileName"], TargetFrameworkMoniker = properties["TargetFrameworkMoniker"], RuntimeFrameworkVersion = properties["RuntimeFrameworkVersion"], PlatformTarget = properties["PlatformTarget"], RootNamespace = properties["RootNamespace"] }; return project; }
public void ExecuteEFCommand(Project project) {var depsFile = Path.Combine( project.ProjectDir, project.OutputPath, project.AssemblyName + ".deps.json"); var runtimeConfig = Path.Combine( project.ProjectDir, project.OutputPath, project.AssemblyName + ".runtimeconfig.json"); var projectAssetsFile = project.ProjectAssetsFile; var args = new List{ "exec", "--depsfile" }; args.Add(depsFile); var packageSDKFolder = string.Empty; if (!string.IsNullOrEmpty(projectAssetsFile)) { using (var reader = new JsonTextReader(System.IO.File.OpenText(projectAssetsFile))) { var projectAssets = JToken.ReadFrom(reader); var packageFolders = projectAssets["packageFolders"].Children ().Select(p => p.Name); foreach (var packageFolder in packageFolders) { packageSDKFolder = packageFolder; args.Add("--additionalprobingpath"); args.Add(packageFolder.TrimEnd(Path.DirectorySeparatorChar)); } } } if (System.IO.File.Exists(runtimeConfig)) { args.Add("--runtimeconfig"); args.Add(runtimeConfig); } else if (project.RuntimeFrameworkVersion.Length != 0) { args.Add("--fx-version"); args.Add(project.RuntimeFrameworkVersion); } args.Add(Path.Combine(@"C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.entityframeworkcore.tools.dotnet\2.0.2\tools\netcoreapp2.0", "ef.dll")); args.AddRange(new List () { "migrations", "add", "initial", "-c", "EFCoreDbContext" }); args.Add("--assembly"); args.Add(Path.Combine(project.ProjectDir, project.OutputPath, project.TargetFileName)); args.Add("--startup-assembly"); args.Add(Path.Combine(project.ProjectDir, project.OutputPath, project.TargetFileName));if (!string.IsNullOrEmpty(project.Language)) { args.Add("--language"); args.Add(project.Language); } var arg = Common.ToArguments(args); var psi = new ProcessStartInfo { FileName = "dotnet", Arguments = arg, UseShellExecute = false }; var process = Process.Start(psi); process.WaitForExit(); if (process.ExitCode != 0) { Console.WriteLine("Migration failed"); } }
请注意在上述ExecuteEFCommand方法中已明确标注此时目标迁移目录就是上述当前项目,需要迁移到上下文所在类库中,我们在命令行就可以得到上下文所在项目,此时只需要将上述ExecuteEFCommand方法中标注改为从命令行获取到的项目参数即可,如下我们直接写死:
args.Add("--assembly"); args.Add(Path.Combine(project.ProjectDir, project.OutputPath, "EFCore.Data.dll"));
同时还需添加上下文项目目录参数,如下:
args.Add("--project-dir"); args.Add(@"C:\Users\JeffckyWang\Source\Repos\WebApplication1\EFCore.Data\");if (!string.IsNullOrEmpty(project.Language)){ args.Add("--language"); args.Add(project.Language); }.......
最后将启动项目中生成的迁移目录修改为上下文所在项目,如下:
var sqlStr = @"data source=WANGPENG;User Id=sa;Pwd=sa123;initial catalog=EFCore2xDb; integrated security=True;MultipleActiveResultSets=True;"; services.AddDbContextPool(options => { options.UseSqlServer(sqlStr, d => d.MigrationsAssembly("EFCore.Data")); }, 256);
此时我们再来手动迁移那么将在上下文所在项目中生成迁移文件夹,如下:
了解了执行dotnet ef背后实现的原理,Jeff要说了【那么问题来了】,对于我们而言有何帮助没有呢,当然有而且马上能实现,我们可以写一个批处理文件在发布时直接执行生成数据库表,说完就开干。我们在上述WebApplication1启动项目中创建名为deploy-efcore.bat批处理文件,代码如下:
set EFCoreMigrationsNamespace=%WebApplication1set EFCoreMigrationsDllName=%WebApplication1.dllset EFCoreMigrationsDllDepsJson=%bin\debug\netcoreapp2.0\WebApplication1.deps.json set PathToNuGetPackages=%USERPROFILE%\.nuget\packagesset PathToEfDll=%PathToNuGetPackages%\microsoft.entityframeworkcore.tools.dotnet\2.0.0\tools\netcoreapp2.0\ef.dlldotnet exec --depsfile .\%EFCoreMigrationsDllDepsJson% --additionalprobingpath %PathToNuGetPackages% %PathToEfDll% database update --assembly .\%EFCoreMigrationsDllName% --startup-assembly .\%EFCoreMigrationsDllName% --project-dir . --verbose --root-namespace %EFCoreMigrationsNamespace%pause
总结
本节我们详细讲解了执行dotnet ef命令背后究竟发生了什么,同时也大概讨论了下.NET Core几个配置文件的作用,足够了解这些,当出现问题才不至于手足无措,耗时一天多才写完,不过收获颇多,下面我们给出背后实现大致原理【后面可能会更详细探讨,到时继续更新】图来解释执行dotnet ef命令背后的本质以此来加深印象,希望对阅读的您也能有所帮助,我们下节再会。